Skip to content

Commit 4c06e87

Browse files
chernistryclaude
andauthored
feat: wire spawn error categories + worktree isolation check (AGENT-001, AGENT-002) (#594)
- spawner.py: import classify_spawn_error/RetryStrategy; break inner retry loop immediately for NO_RETRY and RETRY_AFTER_FIX errors (adapter not installed, permission denied) instead of trying alternate providers - worktree.py: call validate_worktree_isolation() after create(); cleanup and raise WorktreeError if .sdd symlink, mutable-state symlink, or hardlink violations are detected Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9561d61 commit 4c06e87

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

src/bernstein/core/spawner.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from bernstein.core.prometheus import agent_spawn_duration, merge_duration
4040
from bernstein.core.router import ProviderHealthStatus, RouterError, TierAwareRouter
4141
from bernstein.core.sandbox import DockerSandbox, spawn_in_sandbox
42+
from bernstein.core.spawn_errors import RetryStrategy, classify_spawn_error
4243
from bernstein.core.team_state import TeamStateStore
4344
from bernstein.core.traces import AgentTrace, TraceStore, finalize_trace, new_trace
4445
from bernstein.core.worktree import WorktreeError, WorktreeManager, WorktreeSetupConfig
@@ -1459,8 +1460,26 @@ def _spawn_for_tasks_internal(self, tasks: list[Task], model_override: str | Non
14591460
except RouterError:
14601461
provider_name = None
14611462
except (SpawnError, Exception) as exc:
1463+
categorized = classify_spawn_error(exc, provider=provider_name)
14621464
attempt_errors.append(f"{adapter_name}: {exc}")
14631465

1466+
# Fail-fast for permanent and operator-fix errors — no
1467+
# point trying alternate providers when the binary is
1468+
# missing or credentials are invalid.
1469+
if categorized.retry_strategy in (
1470+
RetryStrategy.NO_RETRY,
1471+
RetryStrategy.RETRY_AFTER_FIX,
1472+
):
1473+
logger.warning(
1474+
"Spawn failure is non-retryable (strategy=%s session=%s adapter=%s): %s",
1475+
categorized.retry_strategy.value,
1476+
session_id,
1477+
adapter_name,
1478+
exc,
1479+
)
1480+
self._adapter_health.record_failure(adapter_name)
1481+
break
1482+
14641483
# Check for auth error (T499)
14651484
is_auth_error = False
14661485
log_path = spawn_cwd / ".sdd" / "logs" / f"{session_id}.log"
@@ -1488,10 +1507,11 @@ def _spawn_for_tasks_internal(self, tasks: list[Task], model_override: str | Non
14881507

14891508
self._adapter_health.record_failure(adapter_name)
14901509
logger.warning(
1491-
"Agent spawn failed (session=%s provider=%s adapter=%s): %s",
1510+
"Agent spawn failed (session=%s provider=%s adapter=%s strategy=%s): %s",
14921511
session_id,
14931512
provider_name,
14941513
adapter_name,
1514+
categorized.retry_strategy.value,
14951515
exc,
14961516
)
14971517
if self._router is None or provider_name is None:

src/bernstein/core/worktree.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from bernstein.core.git_ops import branch_delete, worktree_add, worktree_list, worktree_remove
2828
from bernstein.core.platform_compat import process_alive
29+
from bernstein.core.worktree_isolation import validate_worktree_isolation
2930

3031
if TYPE_CHECKING:
3132
import threading
@@ -298,8 +299,22 @@ def create(self, session_id: str) -> Path:
298299
worker_pid = os.getpid()
299300
write_worktree_lock(self.repo_root, session_id, pid=worker_pid)
300301

302+
allowed_symlinks: tuple[str, ...] = ()
301303
if self._setup_config is not None:
302304
setup_worktree_env(self.repo_root, worktree_path, self._setup_config)
305+
allowed_symlinks = self._setup_config.symlink_dirs
306+
307+
isolation_result = validate_worktree_isolation(
308+
worktree_path,
309+
self.repo_root,
310+
allowed_symlink_dirs=allowed_symlinks,
311+
)
312+
if not isolation_result.passed:
313+
self.cleanup(session_id)
314+
raise WorktreeError(
315+
f"Worktree isolation violated for session '{session_id}': "
316+
+ "; ".join(isolation_result.violations)
317+
)
303318

304319
return worktree_path
305320

0 commit comments

Comments
 (0)