Codumentor logo Codumentor

Phase 5 — Tier-1 Sandbox

Goal

Add an opt-in Tier-1 sandbox that runs external agents inside a bubblewrap-isolated overlay of the repo, reusing the machinery the workspace_isolation plugin already implements (IsolatedExecutionStrategy, SnapshotManager, SandboxPolicy). Add per-backend egress allow-lists and basic resource limits.

Preconditions

Phases 1–4 shipped: four adapters all running on the Tier-0 subprocess sandbox.

Deliverables

  1. sandbox/tier1.py providing the same async def spawn(...) context manager interface as tier0.py, but routing the subprocess through IsolatedExecutionStrategy.
  2. Per-backend egress allow-list configuration (codumentor.yaml.external_agent.backends.<name>.allowed_egress: [host1, host2, ...]).
  3. Resource limits: configurable memory cap and wall-clock cap per invocation (defaults: 2 GiB, 5 minutes).
  4. Plugin-instance setting default_sandbox_tier: 0 | 1 (already named in §4.6 of the index).
  5. A decision and reasoning in the handoff on whether to: - (a) import IsolatedExecutionStrategy directly from src/codumentor/plugins/workspace_isolation/strategies.py, accepting the cross-plugin import, or - (b) lift it into a shared module like src/codumentor/sandbox/ that both plugins import.
  6. Tests covering: process actually runs in the overlay (write to /workspace/<repo> lands in the upper dir, not the host repo), egress allow-list rejects disallowed hosts, memory limit terminates a runaway process.

New files

src/codumentor/plugins/external_agent/sandbox/tier1.py
tests/test_external_agent_sandbox_tier1.py

Possibly:

src/codumentor/sandbox/__init__.py
src/codumentor/sandbox/isolated_execution.py   (if option (b) chosen)

Touched: plugin.py (read default_sandbox_tier config, pick spawner), tools/base.py (pass sandbox tier into runner), workspace_isolation/strategies.py (only if option (b) chosen — move, leave a re-export shim if external imports exist).

Implementation steps

  1. Read the workspace_isolation survey result. The relevant entry points are IsolatedExecutionStrategy.__init__ at src/codumentor/plugins/workspace_isolation/strategies.py:263, SnapshotManager in snapshots.py, and SandboxPolicy. The strategy is a clean callable; the plugin-level orchestration around it (session-evict hooks, 30-day TTL retention) is NOT what we want — external-agent invocations are point-in-time.
  1. Decide on extraction (a vs b). Default to (a) — direct import. Choose (b) only if (a) requires you to import private symbols or you find substantial coupling to WorkspaceIsolationPlugin's state. Whatever you choose, do not break workspace_isolation's tests.
  1. sandbox/tier1.py: implements async def spawn(adapter, creds, prompt, session_id, *, repo_path, allowed_egress, limits) -> ProcessHandle matching the Tier-0 interface. Inside: - Build an IsolatedExecutionStrategy with repos_dir = repo_path.parent, upper_dir = <plugin storage>/sandboxes/<invocation_uuid>/upper, work_dir = .../work, and a SandboxPolicy that enables --unshare-net unless allowed_egress is non-empty (in which case wire up an egress filter — see step 4). - Translate the host repo path to the sandbox path (/workspace/<repo>) so the adapter's cwd argument lands correctly. - On exit, clean up the upper/work dirs (we do not retain like workspace_isolation does — different lifecycle).
  1. Egress allow-list: bwrap does not have a built-in DNS/IP allow-list. Implement via one of: - Run the subprocess with --unshare-net and provide a tiny in-sandbox HTTP proxy (running on the host, accessible via a Unix socket bind-mounted in) that enforces the allow-list. This is the cleanest option. - Or: skip the in-bwrap network and route all egress through a host-side proxy, set HTTPS_PROXY in the env. - Decide and document. The minimum required allow-list per backend is its API host (e.g., api.anthropic.com for Claude Code). Per-backend defaults live in each adapter as a class attribute default_allowed_egress: list[str].
  1. Resource limits: prefer prlimit / cgroups-v2 via systemd-run --user --scope -p MemoryMax=.... Wall-clock cap is enforced by the existing runner timeout — no new work needed there beyond making the default configurable per backend.
  1. Tests: - Spawn a fake binary that writes to /workspace/<repo>/test.txt; assert the file appears in the upper dir and NOT in the host repo. - Attempt to curl a disallowed host from inside the sandbox; assert failure. - Spawn a fake binary that allocates 4 GiB; assert it is killed under a 2 GiB cap. - All tests must skip with a clear reason if bwrap is not installed (CI may not have it).

Acceptance criteria

Out of scope

Handoff

Fill out the template. In "Contracts established/changed":

In "Open questions": flag any places where workspace_isolation's reuse felt awkward — phase 7's MCP bridge may need the same sandbox path translation logic.