Skip to content

feat(isolation): container backend for folder projects (Phase B)#2153

Open
Wirasm wants to merge 4 commits into
devfrom
feat/container-isolation-phase-b
Open

feat(isolation): container backend for folder projects (Phase B)#2153
Wirasm wants to merge 4 commits into
devfrom
feat/container-isolation-phase-b

Conversation

@Wirasm

@Wirasm Wirasm commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Problem: Folder projects run in place at the live root — any write lands immediately, a bad run can corrupt live business data, and there is no isolation between "agent working" and "files changed."
  • Why it matters: Governed, unattended business-ops packs need to operate on real client folders without risking the live data.
  • What changed: Implements Phase B of the container-isolation plan — the Docker container backend behind the Phase A kind-routed seam. Folder projects can opt into --container (or container.enabled config); the run executes inside a container over a read-only bind of the project root plus a writable overlayfs upper layer, mounted at the same absolute path as the host cwd. Claude, bash:, and script: nodes all execute inside the container.
  • What did NOT change (scope boundary): Repo-kind projects (worktree path) are untouched. Phase C — pause/resume (docker stop/restart) and the approval-gated write-back of the overlay diff to the live root — is not in this PR. A Phase-B container run's overlay changes are discarded on teardown (documented); the milestone is "isolation works, live root is never touched mid-run."

UX Journey

Before

$ archon workflow run implement "reorganize invoices"   (folder project)
Folder project — running in place (no worktree isolation).
  agent cwd = ~/ops-client (THE LIVE ROOT) -> every write lands immediately

After

$ archon workflow run implement --container "reorganize invoices"
Folder project — running in container (image archon-runner:latest).
[container] created 202b938610e4
  claude + bash + script nodes exec INSIDE via docker exec
  overlay mounted at the host cwd path; writes land in the overlay only
  env = Archon-managed bag + minimal base (never host process.env)
[ok] nodes complete -> Container and overlay volume removed.
  (Phase B: overlay changes NOT applied to the live root — write-back is Phase C.)

Non-Claude provider + --container -> hard fail BEFORE any node runs:
  "Provider 'codex' cannot run inside a container yet (containerExec capability)."

Architecture Diagram

After

CLI workflow run (folder + --container)
  -> resolveFolderBackend({container, store, containerConfig})   [~ isolation/backend-router]
       -> [+] ContainerBackend.prepare()                        [+ isolation/backends/container]
             |- [+] dockerPreflight + dockerCli                 [+ isolation/container/docker-exec]
             |- docker volume create + docker run (overlay)     [+ isolation/docker/runner.Dockerfile + entrypoint.sh]
             \- isolation_environments row (provider=container)
  -> executeWorkflow(..., execContext={kind:container})         [~ workflows/executor]
       -> executeDagWorkflow -> [+] containerExec fail-fast     [~ workflows/dag-executor]
             |- AI node -> Claude provider -> [+] buildContainerSpawn -> docker exec -i   [+ providers/claude/container-spawn]
             \- bash/script/until_bash -> runSubprocess -> docker exec                     [~ workflows/dag-executor]
  -> backend.destroy()  (terminal run) -> docker rm -f + volume rm
  events: container_created/destroyed -> emitter + DB rows + CLI progress + console normalizer

Connection inventory:

From To Status Notes
cli/commands/workflow isolation/backend-router modified folder branch now selects container backend + threads envId/cwd
isolation/backends/container isolation/container/docker-exec new prepare/destroy shell docker
isolation/backends/container isolation/store (IIsolationStore) new tracks env row
workflows/dag-executor providers/claude/container-spawn new (indirect, via SendQueryOptions.execContext) Claude in-container spawn
workflows/dag-executor docker (execFileAsync) new bash/script/until_bash container branch
core/config isolation (ContainerBackendConfig) new container.* config merged repo>global

Label Snapshot

  • Risk: risk: medium
  • Size: size: L
  • Scope: isolation|workflows|providers|cli|core|config|adapters|server|web
  • Module: isolation:container-backend

Change Metadata

  • Change type: feature
  • Primary scope: multi (isolation + workflows + providers + cli)

Linked Issue

Validation Evidence (required)

bun run validate     # exit 0 — all 8 checks (type-check, lint, format, 4 generated-file checks, tests)
  • Evidence: bun run validate green. New unit tests: isolation/src/container/docker-exec.test.ts, isolation/src/backends/container.test.ts (incl. SQLite string-metadata regression), providers/src/claude/container-spawn.test.ts, isolation/src/backend-router.test.ts (container routing), workflows/src/dag-executor.test.ts (containerExec fail-fast + container-command normalization).
  • Live containerized smoke (Docker Desktop macOS, native overlay): folder project + --container + a bash-node workflow -> container created, bash ran INSIDE (hostname == container id, overlay mounted at the host cwd path), proof.txt landed in the overlay only, host root unchanged, container + volume torn down cleanly (no leak), container_created/container_destroyed DB rows persisted. Non-Claude (codex) provider + --container -> capability fail-fast, no container leak. Image-missing -> actionable preflight error.
  • Not verified: Linux/VPS live run (macOS Docker Desktop only); a real Claude API turn in-container (bash-node smoke exercises the docker-exec + overlay path; the Claude spawn hook is unit-tested against a fake child).

Security Impact (required)

  • New permissions/capabilities? Yes — container runs with --cap-add SYS_ADMIN --security-opt apparmor=unconfined (native overlay mount) and in-container work runs as root under IS_SANDBOX=1. Scoped to the isolation container, which is the boundary (read-only lower bind + overlay upper on a VM-local volume + Archon-managed env only). Opt-in via --container/config; never the default.
  • New external network calls? No (container --network bridge|none; no new Archon-side calls). The runner image build pulls the Claude/bun/uv installers.
  • Secrets/tokens handling changed? No new storage. Per-user AI creds + GitHub token ride the existing requestOptions.env (managed bag) into the container via docker exec -e; host process.env is deliberately NOT forwarded (the isolation boundary).
  • File system access scope changed? Yes (narrowed for container runs): the project root is bind-mounted read-only; writes go to an overlay upper volume, not the live root.

Compatibility / Migration

  • Backward compatible? Yes — host/repo/worktree/in-place paths byte-identical; container is strictly opt-in.
  • Config/env changes? Yes (additive) — new container: { image?, network?, memoryMb?, pidsLimit?, enabled? } in .archon/config.yaml (repo>global); new --container CLI flag; new workflow container.enabled policy. All optional.
  • Database migration needed? No — reuses isolation_environments (provider 'container', branch_name sentinel ''); new workflow_events types are a TS const (the column has no CHECK constraint).
  • Upgrade steps: build the runner image once — bun run build:runner-image (or docker build -t archon-runner:latest -f packages/isolation/docker/runner.Dockerfile packages/isolation/docker).

Human Verification (required)

  • Verified scenarios: folder + --container bash-node run (isolation + teardown + events); non-Claude provider fail-fast + no leak; image-missing preflight error; native overlay mount on Docker Desktop macOS.
  • Edge cases checked: SQLite metadata-as-string teardown (found + fixed a real container leak); fuse-device-absent retry path (unit-tested); host process.env never forwarded to the container.
  • What was not verified: Linux/VPS live run; a real Claude API turn in-container; multi-repo-root git-inside-container (a Phase C write-back scenario).

Side Effects / Blast Radius (required)

  • Affected subsystems: isolation (new backend), workflows (dag-executor exec sites + fail-fast + events), providers (Claude spawn + capability flag), cli (flag/config/teardown), core (config), adapters/server/web (event passthrough + console rendering).
  • Potential unintended effects: the runSubprocess refactor (host process.env merge moved into the helper) touches all deterministic exec sites — mitigated by the host path being byte-identical + the full workflows suite green.
  • Guardrails: capability fail-fast prevents a silent host downgrade; teardown removes containers on terminal runs; labels (diy.archon.managed) scope all future pruning.

Rollback Plan (required)

  • Fast rollback: revert the single commit 3ac6f93a; container is opt-in so no host behavior depends on it. Any orphaned containers: docker ps -a --filter label=diy.archon.managed=true + docker rm -f.
  • Feature toggles: don't pass --container / don't set container.enabled -> in-place (default) is unchanged.
  • Observable failure symptoms: isolation.container_* warn/error logs; preflight errors on missing daemon/image.

Risks and Mitigations

  • Risk: in-container root + CAP_SYS_ADMIN unacceptable to some operators.
    • Mitigation: opt-in only; the container is the isolation boundary; non-root hardening noted as follow-up (deviation DB1 in the plan).
  • Risk: $ARTIFACTS_DIR not mounted into the container (a node writing directly to it fails).
    • Mitigation: documented limitation (deviation DB6); host-side typed-output sidecars still work; most folder-ops workflows write to the workspace (the overlay).
  • Risk: dual-dialect metadata (SQLite string vs Postgres object) — already bit us as a container leak.
    • Mitigation: fixed (readContainerMetadata parses both) + regression-tested + throws loudly on unusable metadata; store-layer fix noted for follow-up.

Summary by CodeRabbit

  • New Features
    • Added optional containerized execution for folder-based workflows via global --container and workflow configuration, including container backend settings and runner image defaults.
    • Introduced Docker-backed container isolation for deterministic in-container subprocess execution with env hardening.
    • Added container lifecycle events across CLI/UI (created/destroyed).
  • Bug Fixes
    • Improved container/Docker preflight validation and more resilient overlay mounting (fuse → native) plus better cleanup on failures/termination.
  • Documentation
    • Added container isolation security posture guidance.
  • Tests
    • Expanded coverage for container backend, Docker exec utilities, and in-container spawning.
  • Chores
    • Added runner image build tooling and container runner Docker setup.

Implement the Docker container isolation backend behind the Phase A
kind-routed seam. Folder projects can opt into --container (or
container.enabled config); the run executes inside a container over a
read-only bind of the project root plus a writable overlayfs upper layer,
mounted at the same absolute path as the host cwd.

- docker-exec.ts: thin `docker` CLI wrapper + daemon/image preflight; new
  docker error patterns in the isolation classifier.
- runner.Dockerfile + entrypoint.sh: debian-slim + claude native binary +
  git/bash/bun/uv; entrypoint mounts native overlay (fuse-overlayfs
  fallback) and signals a ready sentinel. Built via bun run build:runner-image.
- ContainerBackend.prepare/destroy: per-run upper volume + labeled container,
  ready-poll, tracked isolation_environments row (branch_name sentinel).
- Claude in-container spawn via the SDK spawnClaudeCodeProcess hook over
  docker exec -i; kill() crosses the boundary via in-container pkill.
- Container env policy: the container receives only the Archon-managed env
  bag + a minimal base, never host process.env (the isolation boundary).
- bash/script/loop until_bash nodes exec in-container via docker exec; host
  path stays byte-identical.
- containerExec provider capability + pre-dispatch fail-fast: a container run
  whose AI nodes resolve to a non-Claude provider is rejected before any node
  runs (no silent host downgrade).
- --container CLI flag, container.* config (repo>global), workflow
  container.enabled policy, repo-kind + --container hard error.
- container_created/container_destroyed events across emitter, DB rows, CLI
  progress, and the console normalizer.
- Fix: container metadata is a JSON string on SQLite (object on Postgres) —
  destroy() now parses both, else it skipped docker rm and leaked the
  container (caught by the live smoke).

Phase C (pause/resume + approval-gated write-back) not included.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 435c2235-37f3-460c-9b25-b60dcc6f8c75

📥 Commits

Reviewing files that changed from the base of the PR and between 195246d and 77909d8.

📒 Files selected for processing (6)
  • packages/cli/src/commands/workflow.test.ts
  • packages/cli/src/commands/workflow.ts
  • packages/isolation/docker/SECURITY.md
  • packages/isolation/src/backends/container.test.ts
  • packages/isolation/src/backends/container.ts
  • packages/providers/src/claude/provider.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/isolation/docker/SECURITY.md
  • packages/isolation/src/backends/container.ts
  • packages/isolation/src/backends/container.test.ts

📝 Walkthrough

Walkthrough

Adds Docker-based container isolation for folder workflows, including runner image tooling, overlay-backed environments, CLI/configuration controls, container-aware workflow and provider execution, lifecycle events, cleanup, security documentation, and tests.

Changes

Container isolation

Layer / File(s) Summary
Container contracts and backend wiring
packages/core/src/config/*, packages/isolation/src/*, packages/workflows/src/schemas/*, packages/workflows/src/event-emitter.ts, packages/providers/src/*
Defines container configuration, workflow policy, lifecycle event types, provider capability flags, and backend-router wiring.
Docker runner and isolation backend
packages/isolation/docker/*, packages/isolation/src/backends/*, packages/isolation/src/container/*, scripts/build-runner-image.sh, package.json
Builds the runner image, mounts overlay workspaces, performs Docker preflight, tracks container environments, retries overlay setup, waits for readiness, and cleans up resources.
CLI container selection and teardown
packages/cli/src/cli.ts, packages/cli/src/commands/workflow.ts
Adds --container, resolves flag/workflow/config precedence, prepares folder container contexts, renders lifecycle output, and destroys containers after runs.
Container-aware workflow and provider execution
packages/workflows/src/dag-executor.ts, packages/providers/src/claude/*
Routes deterministic commands and Claude through docker exec, limits forwarded environment variables, and rejects unsupported providers before execution.
Lifecycle event consumers
packages/server/src/adapters/web/workflow-bridge.ts, packages/web/src/experiments/console/primitives/event.ts, packages/adapters/src/chat/slack/workflow-bridge.ts
Adds SSE and console mappings for container lifecycle events and ignores those events in Slack actionable-thread handling.
Security documentation and validation
packages/isolation/docker/SECURITY.md, packages/isolation/src/**/*.test.ts, packages/providers/src/**/*.test.ts, packages/workflows/src/**/*.test.ts
Documents overlay, environment, privilege, and concurrency boundaries and adds coverage for Docker, backend, provider, workflow, and cleanup behavior.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ContainerBackend
  participant Docker
  participant RunnerEntrypoint
  participant DAGExecutor
  participant ClaudeProvider
  CLI->>ContainerBackend: prepare folder environment
  ContainerBackend->>Docker: run preflight and runner container
  Docker->>RunnerEntrypoint: start container
  RunnerEntrypoint->>RunnerEntrypoint: mount overlay and create readiness sentinel
  ContainerBackend->>Docker: poll readiness
  ContainerBackend-->>CLI: return container execution context
  CLI->>DAGExecutor: execute workflow with container context
  DAGExecutor->>ClaudeProvider: dispatch container-compatible provider
  ClaudeProvider->>Docker: execute Claude with docker exec
  DAGExecutor-->>CLI: emit workflow lifecycle events
  CLI->>ContainerBackend: destroy environment
Loading

Possibly related PRs

  • coleam00/Archon#2145: Extends the folder isolation backend seam into Docker-backed container execution.
  • coleam00/Archon#1137: Provides related provider capability plumbing consumed by container compatibility checks.
  • coleam00/Archon#1178: Relates to managed environment propagation in workflow subprocess execution.

Suggested reviewers: buun-dev

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Concise and specific, it captures the main addition: a container backend for folder projects in Phase B.
Description check ✅ Passed Mostly matches the template, but the Architecture Diagram is missing the required Before view and connection inventory.
Docstring Coverage ✅ Passed Docstring coverage is 89.74% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/container-isolation-phase-b

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/providers/src/claude/provider.ts (1)

599-662: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Empty stderrLines during container execution breaks error classification.

Bypassing the SDK by inheriting stderr natively in buildContainerSpawn prevents the SDK from capturing Claude Code's stderr stream. Consequently, the inline stderr callback in provider.ts is never executed, leaving stderrLines empty. This breaks classifyAndEnrichError during container runs, leading to missing diagnostics for CLI errors like invalid API keys or rate-limits.

Share the stderr callback with the spawn hook and switch back to pipe to restore error classification context.

  • packages/providers/src/claude/provider.ts#L599-L662: Extract the inline stderr callback into a variable and pass it to both buildContainerSpawn and the returned SDK options.
  • packages/providers/src/claude/container-spawn.ts#L110-L128: Update buildContainerSpawn to accept an onStderr callback, change stdio to ['pipe', 'pipe', 'pipe'], and pipe child.stderr's data directly to onStderr.
🛠️ Proposed fix to bridge `stderr` between the container and error classifier

In packages/providers/src/claude/provider.ts:

+  const stderrCallback = (data: string): void => {
+    const output = data.trim();
+    if (!output) return;
+    stderrLines.push(output);
+
+    const isError =
+      output.toLowerCase().includes('error') ||
+      output.toLowerCase().includes('fatal') ||
+      output.toLowerCase().includes('failed') ||
+      output.toLowerCase().includes('exception') ||
+      output.includes('at ') ||
+      output.includes('Error:');
+
+    const isInfoMessage =
+      output.includes('Spawning Claude Code') ||
+      output.includes('--output-format') ||
+      output.includes('--permission-mode');
+
+    if (isError && !isInfoMessage) {
+      getLog().error({ stderr: output }, 'subprocess_error');
+    }
+  };
+
   const containerExecContext =
     requestOptions?.execContext?.kind === 'container' ? requestOptions.execContext : undefined;
   const spawnOverride = containerExecContext
-    ? { spawnClaudeCodeProcess: buildContainerSpawn(containerExecContext) }
+    ? { spawnClaudeCodeProcess: buildContainerSpawn(containerExecContext, stderrCallback) }
     : {};
 
   return {
@@ -637,28 +660,7 @@
     systemPrompt: requestOptions?.systemPrompt ?? { type: 'preset', preset: 'claude_code' },
     settingSources: assistantDefaults.settingSources ?? ['project', 'user'],
     hooks: buildToolCaptureHooks(toolResultQueue),
-    stderr: (data: string): void => {
-      const output = data.trim();
-      if (!output) return;
-      stderrLines.push(output);
-
-      const isError =
-        output.toLowerCase().includes('error') ||
-        output.toLowerCase().includes('fatal') ||
-        output.toLowerCase().includes('failed') ||
-        output.toLowerCase().includes('exception') ||
-        output.includes('at ') ||
-        output.includes('Error:');
-
-      const isInfoMessage =
-        output.includes('Spawning Claude Code') ||
-        output.includes('--output-format') ||
-        output.includes('--permission-mode');
-
-      if (isError && !isInfoMessage) {
-        getLog().error({ stderr: output }, 'subprocess_error');
-      }
-    },
+    stderr: stderrCallback,
   };
 }

In packages/providers/src/claude/container-spawn.ts:

 export function buildContainerSpawn(
   execContext: Extract<ExecutionContext, { kind: 'container' }>,
+  onStderr: (data: string) => void,
   spawnFn: Spawner = spawn as unknown as Spawner
 ): (options: SpawnOptions) => SpawnedProcess {
   return (options: SpawnOptions): SpawnedProcess => {
     const dockerArgs = buildDockerExecArgs(execContext, options);
     getLog().debug(
       { containerId: execContext.containerId, argc: options.args.length },
       'claude.container_spawn_started'
     );
 
-    // stderr inherited: the SpawnedProcess contract exposes only stdin/stdout, so
-    // the SDK can't read the child's stderr — inheriting surfaces container-side
-    // Claude errors in Archon's own stderr instead of swallowing them. The
-    // 'pipe','pipe','inherit' stdio makes stdin/stdout non-null.
     const child = spawnFn('docker', dockerArgs, {
-      stdio: ['pipe', 'pipe', 'inherit'],
+      stdio: ['pipe', 'pipe', 'pipe'],
     });
 
     // The 'pipe','pipe' stdio guarantees stdin/stdout are present; guard rather
     // than assert so a misbehaving spawner fails loudly instead of NPE-ing later.
     if (!child.stdin || !child.stdout) {
       throw new Error('docker exec child is missing piped stdin/stdout');
     }
+
+    if (child.stderr) {
+      child.stderr.on('data', (chunk: Buffer | string) => {
+        onStderr(chunk.toString());
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/provider.ts` around lines 599 - 662, Update
packages/providers/src/claude/provider.ts lines 599-662 to extract the existing
stderr handler into a reusable callback, pass it to buildContainerSpawn, and
retain it in the returned SDK options. Update
packages/providers/src/claude/container-spawn.ts lines 110-128 so
buildContainerSpawn accepts onStderr, uses piped stdin/stdout/stderr, and
forwards child.stderr data directly to onStderr, restoring stderrLines and error
classification during container runs.
🧹 Nitpick comments (1)
packages/cli/src/commands/workflow.ts (1)

1147-1151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the required structured log-event naming.

workflow.running_in_container does not follow {domain}.{action}_{state}. Rename it to a concrete state event such as workflow.container_backend_selected.

As per coding guidelines, use structured Pino event names in {domain}.{action}_{state} format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/workflow.ts` around lines 1147 - 1151, Update the
structured log event in the container-running branch near the container image
message to use the required {domain}.{action}_{state} naming format, renaming
workflow.running_in_container to workflow.container_backend_selected while
preserving its existing fields and behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/workflow.ts`:
- Around line 1486-1496: The Phase B workflow must not retain paused containers
or expose unsupported Phase C lifecycle states. In
packages/cli/src/commands/workflow.ts:1486-1496, update the container teardown
flow to destroy paused containers rather than preserving them for resume. In
packages/workflows/src/store.ts:53-59, remove the container_stopped and
container_resumed states until Phase C implements suspend/resume behavior.
- Around line 1152-1161: Wrap the post-prepare workflow lifecycle in a
try/finally block so every failure after backend.prepare()—including
persistence, resume hydration, or executeWorkflow()—still invokes the container
backend’s destroy teardown. Apply the same acquisition-scoped cleanup to the
corresponding lifecycle at the additional location, preserving the existing
destroy behavior and execution flow.
- Around line 1134-1138: Update the folderConfig loading in the wantsContainer
selection flow to stop swallowing loadConfig errors: remove the fallback to
undefined and let loadConfig(codebase.default_cwd) propagate its failure or fail
explicitly. Preserve the existing container precedence and default behavior only
when configuration loads successfully.

In `@packages/isolation/docker/runner.Dockerfile`:
- Around line 33-46: Update the Claude, Bun, and uv installation RUN blocks to
use explicitly pinned installer or tool versions instead of mutable URLs, and
verify each downloaded installer or artifact against a trusted checksum or
signature before executing it as root. Preserve the existing executable presence
checks and fail the build when verification or installation fails.

In `@packages/isolation/src/backend-router.test.ts`:
- Around line 59-64: Update the test covering container backend rejection in
resolveFolderBackend to include a separate assertion where containerConfig is
provided but store is omitted, and verify it still throws the expected “not
wired up” error. Keep the existing assertion for both options being absent.

In `@packages/isolation/src/backends/container.ts`:
- Around line 274-278: Reduce workload container privileges in
packages/isolation/src/backends/container.ts by removing SYS_ADMIN and the
unconfined AppArmor security option while preserving conditional /dev/fuse
handling. In packages/isolation/docker/runner.Dockerfile, switch to a non-root
workload user after privileged overlay initialization, ensuring initialization
still runs with the required privileges.
- Around line 221-240: Update the teardown flow around the container and volume
removal catches so genuine removal failures do not proceed to persist destroyed
status; retain a recoverable state or propagate the failure, while preserving
successful cleanup behavior. In
packages/isolation/src/backends/container.test.ts lines 217-227, update the
assertion to expect the recoverable/error outcome instead of destroyed.
- Around line 133-180: The environment persistence step around store.create must
clean up resources on failure. Wrap the store.create call in a try/catch, call
forceRemove(containerName, volume), log the raw error with
isolation.container_prepare_failed, and rethrow the original error; preserve the
existing metadata and persistence behavior on success.

In `@packages/isolation/src/errors.ts`:
- Around line 45-50: Update the “no such image” error message in the preflight
error classification to avoid recommending the fixed archon-runner image tag.
Preserve the requested image reference when reporting the error, or use
tag-neutral guidance that also applies to versioned and custom container images.

In `@packages/providers/src/claude/container-spawn.ts`:
- Around line 153-157: Update the kill method in the child-process wrapper to
default its signal parameter to SIGTERM, matching ChildProcess.kill() behavior
and ensuring killInContainer receives a valid signal for parameterless calls.

In `@packages/workflows/src/store.ts`:
- Around line 53-59: Remove container_stopped and container_resumed from the
public lifecycle event list in packages/workflows/src/store.ts, leaving only the
Phase B-supported container_created and container_destroyed events so
unsupported suspend/resume inputs are not exposed.

---

Outside diff comments:
In `@packages/providers/src/claude/provider.ts`:
- Around line 599-662: Update packages/providers/src/claude/provider.ts lines
599-662 to extract the existing stderr handler into a reusable callback, pass it
to buildContainerSpawn, and retain it in the returned SDK options. Update
packages/providers/src/claude/container-spawn.ts lines 110-128 so
buildContainerSpawn accepts onStderr, uses piped stdin/stdout/stderr, and
forwards child.stderr data directly to onStderr, restoring stderrLines and error
classification during container runs.

---

Nitpick comments:
In `@packages/cli/src/commands/workflow.ts`:
- Around line 1147-1151: Update the structured log event in the
container-running branch near the container image message to use the required
{domain}.{action}_{state} naming format, renaming workflow.running_in_container
to workflow.container_backend_selected while preserving its existing fields and
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b06000b7-617c-4ea9-b22e-7e3c065a51b5

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc48cb and 3ac6f93.

📒 Files selected for processing (38)
  • package.json
  • packages/adapters/src/chat/slack/workflow-bridge.ts
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/workflow.ts
  • packages/core/src/config/config-loader.ts
  • packages/core/src/config/config-types.ts
  • packages/isolation/docker/entrypoint.sh
  • packages/isolation/docker/runner.Dockerfile
  • packages/isolation/package.json
  • packages/isolation/src/backend-router.test.ts
  • packages/isolation/src/backend-router.ts
  • packages/isolation/src/backends/container.test.ts
  • packages/isolation/src/backends/container.ts
  • packages/isolation/src/container/docker-exec.test.ts
  • packages/isolation/src/container/docker-exec.ts
  • packages/isolation/src/errors.ts
  • packages/isolation/src/index.ts
  • packages/isolation/src/types.ts
  • packages/providers/package.json
  • packages/providers/src/claude/capabilities.ts
  • packages/providers/src/claude/container-spawn.test.ts
  • packages/providers/src/claude/container-spawn.ts
  • packages/providers/src/claude/provider.ts
  • packages/providers/src/codex/capabilities.ts
  • packages/providers/src/codex/provider.test.ts
  • packages/providers/src/community/copilot/capabilities.ts
  • packages/providers/src/community/opencode/capabilities.ts
  • packages/providers/src/community/pi/capabilities.ts
  • packages/providers/src/registry.test.ts
  • packages/providers/src/types.ts
  • packages/server/src/adapters/web/workflow-bridge.ts
  • packages/web/src/experiments/console/primitives/event.ts
  • packages/workflows/src/dag-executor.test.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/event-emitter.ts
  • packages/workflows/src/schemas/workflow.ts
  • packages/workflows/src/store.ts
  • scripts/build-runner-image.sh

Comment thread packages/cli/src/commands/workflow.ts
Comment thread packages/cli/src/commands/workflow.ts Outdated
Comment thread packages/cli/src/commands/workflow.ts Outdated
Comment on lines +33 to +46
# Claude Code native binary (self-contained). Installs to /root/.local/bin/claude.
RUN curl -fsSL https://claude.ai/install.sh | bash \
&& test -x /root/.local/bin/claude \
|| (echo "FATAL: claude binary not found after install" >&2 && exit 1)

# bun (script: nodes with runtime: bun) → /root/.bun/bin/bun
RUN curl -fsSL https://bun.sh/install | bash \
&& test -x /root/.bun/bin/bun \
|| (echo "FATAL: bun not found after install" >&2 && exit 1)

# uv (script: nodes with runtime: uv) → /root/.local/bin/uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& test -x /root/.local/bin/uv \
|| (echo "FATAL: uv not found after install" >&2 && exit 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Pin and verify all downloaded installers.

These mutable URLs execute arbitrary network content as root during builds. Pin tool versions and verify downloaded artifacts/checksums so the runner image is reproducible and a compromised installer endpoint cannot compromise the image.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/isolation/docker/runner.Dockerfile` around lines 33 - 46, Update the
Claude, Bun, and uv installation RUN blocks to use explicitly pinned installer
or tool versions instead of mutable URLs, and verify each downloaded installer
or artifact against a trusted checksum or signature before executing it as root.
Preserve the existing executable presence checks and fail the build when
verification or installation fails.

Comment thread packages/isolation/src/backend-router.test.ts
Comment thread packages/isolation/src/backends/container.ts
Comment thread packages/isolation/src/backends/container.ts Outdated
Comment thread packages/isolation/src/errors.ts Outdated
Comment thread packages/providers/src/claude/container-spawn.ts Outdated
Comment thread packages/workflows/src/store.ts
Address the prp-review findings on the Phase B container backend.

- Mount strategy: try fuse-overlayfs (no CAP_SYS_ADMIN, closes the
  mount -o remount,rw /mnt/lower escape) FIRST, fall back to native
  overlay + CAP_SYS_ADMIN only when fuse can't mount. Mode dictated via
  ARCHON_OVERLAY_MODE; fuse fails fast (entrypoint exits, waitForReady
  detects the exit). Add SECURITY.md with the honest threat model:
  native mode grants the remount escape and is NOT a sandbox against a
  hostile agent (only rootless/userns daemons get the fuse path); also
  documents the docker-exec-secrets-in-host-ps and $ARTIFACTS_DIR caveats.
- CLI teardown moved into finally so a throw from executeWorkflow before
  its own try/catch can't orphan a privileged container+volume.
- destroy() distinguishes idempotent not-found (mark destroyed) from a
  genuine docker failure (leave row active, THROW); CLI surfaces a loud
  clean-up-manually message. Replace the swallow-everything test.
- Runtime containerExec backstop in resolveNodeProviderAndModel so the
  hand-mirrored pre-scan isn't a single point of failure.
- PID-scope the in-container kill (per-invocation pidfile via
  sh -c 'echo $$ > f; exec claude "$@"') so an abort doesn't kill
  sibling Claude nodes sharing the container.
- Validate container.network (reject anything but bridge/none).
- Env-isolation enforcement-point tests (process.env canary) for both the
  Claude provider env and the bash/script docker exec argv.
- Wire classifyIsolationError on the CLI container-prepare path; add
  cleanup breadcrumbs.
@Wirasm

Wirasm commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

PR Review Summary

Multi-agent review of PR #2153feat(isolation): container backend for folder projects (Phase B) (feat/container-isolation-phase-bdev, 38 files, +1971/−57, reviewed at head 3ac6f93a). Seven specialized agents: code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer, docs-impact, code-simplifier. Full validation suite run on a clean checkout of the PR head.

Critical Issues (5 found)

Agent Issue Location
code-reviewer Resume of a container run silently drops isolation or leaks a duplicate container. workflow resume never forwards container intent: with ad-hoc --container the resumed run falls through to in-place and executes on the live root with no isolation and no warning; with container.enabled persisted it creates a second container with a fresh empty overlay (prior overlay state gone, original paused container never reattached despite the code comment claiming resume reattaches). Fix: block container+resume with an explicit "Phase C" error, or implement reattach. packages/cli/src/commands/workflow.ts:1121, 2091-2095
code-reviewer --container is dead on arrival for compiled-binary installs. resolveClaudeBinaryPath() runs unconditionally before the container branch is consulted; in a compiled binary it throws "Claude Code not found" unless Claude is installed on the host — defeating the runner image that bakes Claude in. Masked in dev-mode smoke tests (BUNDLED_IS_BINARY=false short-circuits). Fix: skip/soften host binary resolution when execContext.kind === 'container'. packages/providers/src/claude/provider.ts:1173
silent-failure-hunter Container teardown is happy-path-only — a thrown exception leaks the container. The destroy block is plain sequential code after the executeWorkflow try/finally (the finally only unsubscribes events). Any throw (e.g. hydrateResumableRun rethrow) propagates to main()'s generic catch: container + overlay volume left running, no log, no user notice, and no cleanup command currently recognizes container envs. Fix: move teardown into a finally spanning creation→end. packages/cli/src/commands/workflow.ts:1454-1535, cli.ts:1012-1018
silent-failure-hunter SIGINT/SIGTERM handler never tears down the container. Ctrl+C marks the run failed and exits — the container leaks. Unlike worktrees, there is no resume path (Phase C) and no cleanup command that finds it, so leave-alive isn't a defensible default here. Fix: best-effort destroy in the signal handler, or at minimum print the manual docker rm -f command. packages/cli/src/commands/workflow.ts:1366-1394
silent-failure-hunter Swallowed config-load failure silently downgrades security posture. loadConfig(...).catch(() => undefined) with zero logging: if config fails to parse while --container was passed, the run proceeds with hardcoded defaults — a configured network: 'none' silently becomes bridge, pinned image and memory/pids caps are dropped. Violates "never silently broaden permissions." Fix: log (see the tier_notice.config_load_failed precedent at line 478) and fail fast or warn when --container is explicit. packages/cli/src/commands/workflow.ts:1136

Important Issues (7 found)

Agent Issue Location
silent-failure-hunter destroy() swallows all docker rm/volume rm failures (not just not-found), marks the row destroyed, and the CLI prints "Container and overlay volume removed" even when removal genuinely failed — false success with no rediscovery path. Distinguish not-found from real failures. packages/isolation/src/backends/container.ts:221-242, workflow.ts:1524-1527
code-reviewer bash:/script: container exec forwards env verbatim with no PATH/HOME denylist (the Claude spawn path has CONTAINER_ENV_DENYLIST for exactly this reason) — a project env var setting PATH clobbers in-container resolution with confusing "command not found." Share the denylist. packages/workflows/src/dag-executor.ts:2169-2172 vs container-spawn.ts:56
type-design-analyzer IsolationEnvironmentRow.metadata: Record<string, unknown> misrepresents runtime reality — SQLite returns a raw JSON string. This exact mismatch already caused the container leak this PR patches locally via readContainerMetadata(); nothing stops the next consumer from hitting it again. Normalize at the store/adapter boundary so the type is true. packages/isolation/src/types.ts:264, backends/container.ts:87-99
pr-test-analyzer No test locks the container env-isolation boundary: isContainerRun ? buildContainerBaseEnv() : buildSubprocessEnv() is "the isolation boundary itself" per its own comment, and the host-path test exists (provider.test.ts:931) but the container-negative counterpart doesn't. A refactor could silently leak host secrets into containers. (Rated 8/10.) packages/providers/src/claude/provider.ts:1179
pr-test-analyzer No test on the destroy-vs-keep-alive teardown gate (isPaused → keep container vs destroy). Both failure modes (destroying a paused run's overlay; leaking on terminal runs) are untested. (Rated 8/10.) packages/cli/src/commands/workflow.ts:1491-1535
comment-analyzer Default image tag documented as archon-runner:<version> in two places; the code's DEFAULT_RUNNER_IMAGE is archon-runner:latest (chosen deliberately, with rationale). Misleads operators into re-tagging after every build. scripts/build-runner-image.sh:3-6, packages/core/src/config/config-types.ts:74
comment-analyzer Schema doc claims enabled: false "defers to the flag / config default" — the ?? chain means false hard-disables (config cannot re-enable); only omission defers. Comment contradicts the correct precedence description two lines above it. packages/workflows/src/schemas/workflow.ts:74

Suggestions (11 found)

Agent Suggestion Location
pr-test-analyzer Test the --container precedence chain (flag > workflow > config) and the repo-kind hard-fail workflow.ts:1097-1170
pr-test-analyzer Extract + test runSubprocess's hand-rolled docker-exec argv builder (mirror buildDockerExecArgs, which is tested) dag-executor.ts:2161-2179
pr-test-analyzer One executeDagWorkflow test with container execContext (fail-fast fires before any node; container_created emitted + persisted); cover on_reject and tier-override branches of collectContainerIncompatibleProviders dag-executor.ts:5449-5567
pr-test-analyzer Console normalizer container_* cases and workflow-bridge container_lifecycle SSE case untested — the exact DB-vs-emitter naming bug class that caused PR #1878 event.ts:284-307, workflow-bridge.ts:185-192
pr-test-analyzer No automated coverage of the real Docker path (image build, overlay mount fallback, exec round-trip) — file as a follow-up e2e smoke docker/, scripts/build-runner-image.sh
silent-failure-hunter Prepare-failure cleanup paths (forceRemove, volume rollback) swallow docker errors with zero logging — add the log.warn treatment destroy() uses; log readContainerMetadata JSON.parse failures with context container.ts:150, 344-347, 87-99
type-design-analyzer ContainerLifecycleEvent.phase includes 'creating' with no DB event counterpart and no emitter — drop it or comment why it's reserved event-emitter.ts:194-199, store.ts:56-59
code-simplifier Replace the 3-level nested ternary for container lifecycle labels with a lookup table (in-file precedent: NODE_TRANSITION_BY_EVENT) packages/web/src/experiments/console/primitives/event.ts:294-301
code-simplifier Drop redundant containerEnvId (always equals isolationEnvId); hoist the duplicated isPaused expression workflow.ts:1029-1030, 1492, 1538
code-simplifier Partial<ContainerBackendConfig> instead of the hand-copied inline param type; use the package's lazy getLog() convention in container.ts (only eager logger in the package); name isHostRun once in provider.ts workflow.ts:152, container.ts:40, provider.ts:599-616
comment-analyzer The env-id docker label holds containerName, not the env row id — rename or clarify; reword "container-side cwd" (contradicts the same-absolute-path invariant); "before any container work" is imprecise (container already exists at that check); re-verify the SDK abort "grace window" claim; ContainerLifecycleEvent.image is declared but never populated types.ts:446, workflow.ts:1153, dag-executor.ts:5529, container-spawn.ts:107

Strengths

  • ExecutionContext discriminated union is genuinely enforced, not decorative — buildContainerSpawn takes Extract<ExecutionContext, {kind:'container'}>, making the container-only path uncallable without narrowing (type-design 9/10)
  • Required (non-optional) containerExec capability flag forces a compile-time decision from every provider — all 5 capability files updated, and the pre-dispatch fail-fast rejects incompatible runs before any node executes
  • Fail-fast discipline where it was designed in: resolveFolderBackend throws on missing wiring rather than silently downgrading; dockerPreflight validates daemon + image before creating any resource; prepare() rolls back volume/container on partial failure
  • Every new test uses DI fakes (DockerRunner/Spawner) — zero new mock.module() calls, and the package test-script batching was updated correctly
  • The SQLite-string-metadata dialect bug was caught by live smoke testing and locked in as a regression test; the load-bearing "host process.env never crosses the boundary" comment was verified accurate against SDK source
  • Validation is fully green on the PR head: type-check (10 packages), lint (--max-warnings 0), tests (isolation 290, providers, workflows — all pass)

Documentation Issues

  • docs-web getting-started/concepts.md (~122), guides/multi-repo-projects.md (12, 39-49), getting-started/overview.md (276), reference/database.md (75, 89), reference/architecture.md (1056) — all state folder projects "run in place — no isolation" as an absolute; now false. Reword as the default with a --container pointer.
  • reference/cli.md (~208) — no --container flag entry for workflow run.
  • reference/configuration.md — the entire container: config block (image, network, memoryMb, pidsLimit, enabled), precedence order, defaults, and the bun run build:runner-image step are undocumented.
  • reference/variables.md (22) — $ARTIFACTS_DIR is not mounted into the container; a bash:/script: node writing to it fails under --container. Needs a caveat.
  • reference/security.md — needs a container-backend subsection: --cap-add SYS_ADMIN + apparmor=unconfined, root-in-container as the accepted boundary, host env never forwarded.
  • reference/architecture.md (567-778) — the "Adding a New Isolation Provider" tutorial's hypothetical ContainerProvider implements IIsolationProvider now actively misleads: the real container backend uses the separate IIsolationBackend seam. Add a note / rename the example.
  • CLAUDE.md — isolation package tree missing backend-router.ts, backends/, container/, docker/; DB schema item 4 still says isolation_environments is worktree-only.
  • deployment/docker.md — the canonical compose stack doesn't mount /var/run/docker.sock, so --container cannot work out of the box in dockerized Archon deployments; needs a caveat.

Verdict

CRITICAL ISSUES

Recommended Actions

  1. Fix the teardown-lifecycle cluster first — criticals 3–5 plus the destroy() false-success share one root cause: teardown is a happy-path side effect, not a guaranteed cleanup step (try/finally + signal handler + honest destroy result).
  2. Decide the resume story: block --container + resume with an explicit Phase-C error (cheapest safe option) or implement reattach — the current silent in-place downgrade is the exact failure mode this PR exists to prevent.
  3. Fix compiled-binary support by skipping host Claude binary resolution for container runs.
  4. Add the two critical tests (container env-isolation boundary; destroy-vs-keep-alive gate) and fix the two wrong comments (image tag default, enabled: false semantics).
  5. Docs pass per the list above (can trail the code fixes).
  6. Re-run review after fixes.

Multi-agent review (7 agents) at PR head 3ac6f93a · validation: type-check ✅ lint ✅ tests ✅

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/providers/src/claude/provider.ts (1)

1201-1210: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not resolve the Claude executable on the host for container runs.

resolveClaudeBinaryPath() is unconditional, so a container run fails when Claude is installed only in the runner image—even though the container spawn path does not use the resolved host executable.

Proposed fix
-const resolvedCliPath = await resolveClaudeBinaryPath(assistantDefaults.claudeBinaryPath);
+const resolvedCliPath =
+  requestOptions?.execContext?.kind === 'container'
+    ? undefined
+    : await resolveClaudeBinaryPath(assistantDefaults.claudeBinaryPath);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/provider.ts` around lines 1201 - 1210, Update
the setup before the retry loop so resolveClaudeBinaryPath is called only for
host execution, not container runs. Preserve the container spawn path without
requiring a host-installed Claude executable, while retaining the existing
resolved path behavior for non-container requests and continuing to build the
subprocess environment as before.
packages/providers/src/claude/container-spawn.ts (1)

164-170: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the abort race before the PID file exists.

For an already-aborted or immediately aborted signal, the killer can read the PID file before the shell writes it. The script then returns success without killing anything, allowing Claude to continue. Retry PID-file lookup for a short bounded interval and terminate the local docker exec child as well; add a pre-aborted-signal regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/container-spawn.ts` around lines 164 - 170,
Update the abort handling around onAbort in container-spawn.ts so cancellation
waits briefly and retries PID-file lookup until the file exists or a short
bounded timeout expires, then kills the in-container process. Also terminate the
local docker exec child when abort occurs, including for already-aborted and
immediately aborted signals, and add a regression test covering a pre-aborted
signal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/workflow.ts`:
- Around line 1523-1531: Update the SIGINT/SIGTERM handling around the workflow
execution and its container teardown block so termination requests cancel the
active workflow, await the resulting cleanup in the existing finally path, and
only then exit with status 1. Ensure both signals use this
cancellation-and-teardown flow instead of calling process.exit immediately,
while preserving normal paused-run behavior.
- Around line 178-182: Update the pidsLimit validation in the workflow
configuration flow to reject finite non-integer values as well as undefined,
non-finite, and non-positive values. Preserve the existing error message and
handling for valid positive integers.

In `@packages/isolation/docker/SECURITY.md`:
- Around line 8-12: Update the native overlay wording in SECURITY.md to describe
native as the fallback mode rather than the default. In the one-line summary,
replace the “default native overlay mode” phrasing with “native fallback mode”
while preserving the existing limitation that it is not a sandbox against
malicious or prompt-injected agents.

In `@packages/isolation/src/backends/container.ts`:
- Around line 434-442: Update isRunning and its waitForReady caller so Docker
inspect failures remain an unknown/error state rather than returning false; only
an explicit stdout value of “false” should trigger the native-mode privilege
fallback. Preserve polling or propagate a clear infrastructure error for
timeouts and other inspect failures, without silently broadening permissions.

In `@packages/providers/src/claude/provider.ts`:
- Around line 147-150: Update the debug log in the environment-key mirroring
block to use a structured Pino event name matching the required
{domain}.{action}_{state} format, or remove the log if no appropriate event name
exists. Preserve the existing key assignment behavior.

---

Outside diff comments:
In `@packages/providers/src/claude/container-spawn.ts`:
- Around line 164-170: Update the abort handling around onAbort in
container-spawn.ts so cancellation waits briefly and retries PID-file lookup
until the file exists or a short bounded timeout expires, then kills the
in-container process. Also terminate the local docker exec child when abort
occurs, including for already-aborted and immediately aborted signals, and add a
regression test covering a pre-aborted signal.

In `@packages/providers/src/claude/provider.ts`:
- Around line 1201-1210: Update the setup before the retry loop so
resolveClaudeBinaryPath is called only for host execution, not container runs.
Preserve the container spawn path without requiring a host-installed Claude
executable, while retaining the existing resolved path behavior for
non-container requests and continuing to build the subprocess environment as
before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12af00e6-c8ad-4b89-b9de-7448adb54d25

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac6f93 and a94b04c.

📒 Files selected for processing (13)
  • packages/cli/src/commands/workflow.ts
  • packages/isolation/docker/SECURITY.md
  • packages/isolation/docker/entrypoint.sh
  • packages/isolation/docker/runner.Dockerfile
  • packages/isolation/src/backends/container.test.ts
  • packages/isolation/src/backends/container.ts
  • packages/providers/package.json
  • packages/providers/src/claude/container-env.test.ts
  • packages/providers/src/claude/container-spawn.test.ts
  • packages/providers/src/claude/container-spawn.ts
  • packages/providers/src/claude/provider.ts
  • packages/workflows/src/dag-executor.test.ts
  • packages/workflows/src/dag-executor.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/providers/package.json
  • packages/isolation/docker/runner.Dockerfile
  • packages/workflows/src/dag-executor.ts

Comment thread packages/cli/src/commands/workflow.ts
Comment thread packages/cli/src/commands/workflow.ts
Comment thread packages/isolation/docker/SECURITY.md Outdated
Comment thread packages/isolation/src/backends/container.ts Outdated
Comment thread packages/providers/src/claude/provider.ts
- (A) Stop swallowing loadConfig errors in the container-selection path — a
  malformed config that would carry container.* policy now fails the run
  instead of silently downgrading to an in-place host run.
- (B) Phase C lifecycle no longer leaks into Phase B: reject pause-capable
  workflows (approval/interactive nodes) BEFORE creating a container, and
  always destroy a container run in teardown (no more leaving a privileged
  container for a resume path that doesn't exist). Trim container_stopped/
  container_resumed from the store event types + emitter phase union +
  console normalizer.
- (C) Wrap store.create() so a row-insert rejection removes the container +
  volume before rethrowing (no orphan).
- (D) Default SpawnedProcess.kill() signal to 'SIGTERM' so a no-arg kill()
  doesn't crash in toPosixSignal.
- (E) Pin the runner image: base by digest, Claude/bun/uv by version (build
  args); document the TLS-fetched-but-not-checksummed installer residual in
  SECURITY.md.
- (F) Image-missing classifier message is image-agnostic (points at
  bun run build:runner-image) so it can't recommend a tag that contradicts a
  custom container.image.
- (G) backend-router test asserts the independently-missing-store case.
- (H) verified destroy() applies the not-found-vs-genuine distinction to both
  the container and volume rm and skips updateStatus on a genuine failure.

validate green; live smoke re-run (pause-block rejects approval workflow with
no container created; non-pausing run creates + always-destroys, host untouched).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/workflow.ts`:
- Around line 1193-1204: Update the wantsContainer handling in the workflow
execution path to reject --resume before creating a new container or overlay.
Check the resume state alongside the existing pause validation and throw a clear
error explaining that container-isolated workflows cannot resume because prior
filesystem changes are unavailable; preserve the existing non-resume container
behavior.
- Around line 1600-1615: Update the teardown error handling around the destroy()
catch to preserve the caught destroyErr rather than swallowing it; after the
finally block, when workflow execution otherwise succeeded, rethrow the
preserved error so the CLI does not report success or exit successfully. Keep
the existing console.error and getLog().error reporting unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c06c515-d2b4-419a-b294-0e5355159435

📥 Commits

Reviewing files that changed from the base of the PR and between a94b04c and 195246d.

📒 Files selected for processing (12)
  • packages/cli/src/commands/workflow.ts
  • packages/isolation/docker/SECURITY.md
  • packages/isolation/docker/runner.Dockerfile
  • packages/isolation/src/backend-router.test.ts
  • packages/isolation/src/backends/container.test.ts
  • packages/isolation/src/backends/container.ts
  • packages/isolation/src/container/docker-exec.test.ts
  • packages/isolation/src/errors.ts
  • packages/providers/src/claude/container-spawn.ts
  • packages/web/src/experiments/console/primitives/event.ts
  • packages/workflows/src/event-emitter.ts
  • packages/workflows/src/store.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/isolation/src/backend-router.test.ts
  • packages/isolation/src/container/docker-exec.test.ts
  • packages/workflows/src/event-emitter.ts
  • packages/isolation/src/errors.ts
  • packages/isolation/docker/SECURITY.md
  • packages/isolation/docker/runner.Dockerfile
  • packages/providers/src/claude/container-spawn.ts
  • packages/isolation/src/backends/container.test.ts

Comment thread packages/cli/src/commands/workflow.ts
Comment thread packages/cli/src/commands/workflow.ts
- (1) container.ts: a Docker inspect ERROR is now 'unknown', not 'stopped'
  — waitForReady only fast-fails into the native+SYS_ADMIN fallback on an
  EXPLICIT Running=false, so a transient inspect blip can't silently
  escalate privilege. New tri-state containerState() replaces isRunning().
- (2) CLI signal handler: SIGINT/SIGTERM now awaits container teardown
  before process.exit, so Ctrl-C doesn't orphan a privileged container
  (the teardown finally is bypassed by process.exit).
- (3) reject --resume for container runs before creating a container — the
  prior overlay was destroyed, so a resume would run downstream nodes
  against a fresh empty overlay.
- (4) a container teardown failure after an otherwise-successful run now
  FAILS the CLI (rethrow after finally) instead of printing 'completed
  successfully' + exit 0 with a leaked privileged container.
- (5) container.memoryMb/pidsLimit require a positive INTEGER (Number.isInteger,
  not isFinite) — docker --memory/--pids-limit reject fractions.
- (6) SECURITY.md: native is the FALLBACK mode (fuse is tried first), not
  the default.
- (7) rename the api-key-mirror debug log to claude.api_key_mirrored
  ({domain}.{action}_{state}).

Tests: inspect-blip-no-fallback (backend); resolveContainerBackendConfig +
describeWorkflowPause (CLI, both now exported). validate green; live sanity
(resume-block creates no container; plain --container run + fuse→native +
clean destroy).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant