Skip to content

Commit d12d804

Browse files
sjarmakclaude
andcommitted
fix: add random suffix to Daytona sandbox names to prevent collisions
Long task names truncated to 40 chars caused identical sandbox names for different tasks (109 DaytonaErrors in last benchmark run). Adding a 4-char hex random suffix ensures uniqueness while staying under the 63-char Daytona name limit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2e085d2 commit d12d804

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

ccb_harbor/daytona.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import secrets
5+
from pathlib import Path
6+
7+
from harbor.environments.daytona import DaytonaEnvironment
8+
9+
10+
def _sanitize_label_value(value: str, max_length: int = 63) -> str:
11+
normalized = re.sub(r"[^a-zA-Z0-9_.-]+", "-", value.strip()).strip("-.")
12+
if not normalized:
13+
return ""
14+
return normalized[:max_length]
15+
16+
17+
class GuardedDaytonaEnvironment(DaytonaEnvironment):
18+
"""Repo-local Daytona environment with stable labels and sandbox names."""
19+
20+
def __init__(
21+
self,
22+
*args,
23+
label_launcher: str | None = None,
24+
label_run_id: str | None = None,
25+
label_benchmark: str | None = None,
26+
label_task_id: str | None = None,
27+
label_config: str | None = None,
28+
label_job_name: str | None = None,
29+
label_category: str | None = None,
30+
label_model: str | None = None,
31+
label_mcp_type: str | None = None,
32+
task_source_dir: str | None = None,
33+
**kwargs,
34+
):
35+
task_source_path = Path(task_source_dir) if task_source_dir else None
36+
derived_task_id = label_task_id or (
37+
task_source_path.name if task_source_path and task_source_path.name else None
38+
)
39+
derived_benchmark = label_benchmark or (
40+
task_source_path.parent.name if task_source_path and task_source_path.parent.name else None
41+
)
42+
43+
self._ccb_labels = {
44+
"managed_by": "codescalebench",
45+
"launcher": label_launcher or "unknown",
46+
"run_id": label_run_id or "",
47+
"benchmark": derived_benchmark or "",
48+
"task_id": derived_task_id or "",
49+
"config": label_config or "",
50+
"job_name": label_job_name or "",
51+
"category": label_category or "",
52+
"model": label_model or "",
53+
"mcp_type": label_mcp_type or "",
54+
}
55+
self._ccb_labels = {
56+
key: sanitized
57+
for key, value in self._ccb_labels.items()
58+
if (sanitized := _sanitize_label_value(str(value)))
59+
}
60+
61+
sandbox_name_parts = [
62+
self._ccb_labels.get("benchmark", ""),
63+
self._ccb_labels.get("task_id", ""),
64+
self._ccb_labels.get("config", ""),
65+
]
66+
sandbox_name_core = "-".join(part for part in sandbox_name_parts if part)
67+
# Reserve 5 chars for random suffix (-xxxx) to prevent collisions
68+
# when long task names truncate to the same prefix.
69+
sandbox_name_core = _sanitize_label_value(sandbox_name_core, max_length=40)
70+
rand_suffix = secrets.token_hex(2) # 4 hex chars
71+
session_hint = kwargs.get("session_id", "")
72+
session_hint = _sanitize_label_value(str(session_hint), max_length=12)
73+
if sandbox_name_core:
74+
self._ccb_sandbox_name = f"ccb-{sandbox_name_core}-{rand_suffix}"
75+
if session_hint:
76+
self._ccb_sandbox_name = f"{self._ccb_sandbox_name}-{session_hint}"
77+
else:
78+
self._ccb_sandbox_name = f"ccb-{session_hint or 'sandbox'}-{rand_suffix}"
79+
self._ccb_sandbox_name = _sanitize_label_value(self._ccb_sandbox_name, max_length=63)
80+
81+
super().__init__(*args, **kwargs)
82+
83+
async def _create_sandbox(self, params):
84+
labels = dict(getattr(params, "labels", None) or {})
85+
labels.update(self._ccb_labels)
86+
params.labels = labels
87+
if not getattr(params, "name", None):
88+
params.name = self._ccb_sandbox_name
89+
await super()._create_sandbox(params)

0 commit comments

Comments
 (0)