Tend · sandboxing · implementation plan

Move Tend’s agent runs into one sandbox

I’ll replace the workflow’s in-place PR checkout and separate setup step with a disposable working copy and one sandboxed execution path for Claude and Codex.

The setup and shutdown changes below incorporate our last two discussions. The self-hosted rollout is the remaining decision.

What needs to change

The workflows currently replace the Actions working tree with PR code after trusted setup. Cleanup then needs local-action paths repaired. Repository setup runs separately from the agent, and Claude and Codex have different sandbox and shutdown paths.

I’ll keep the runner checkout fixed, move repository code into a disposable clone, and give a trusted supervisor control of the whole run: setup, agent execution, process shutdown, then output collection.

Implementation order

  1. Replace the in-place checkout

    • Change the workflows. In review.yaml.j2 and mention.yaml.j2, remove the second checkout and gh pr checkout. Keep the reviewed runner tree for orchestration and POST cleanup; remove _restore_local_actions_run from workflows.py.
    • Add workspace preparation. Create prepare_agent_workspace.py to resolve the event’s exact commit and clone it into a disposable directory. Pass checkout mode and base branch through the workflow macro and both actions.
    • Separate trusted inputs. Restore sensitive startup configuration from the exact base commit. Point the agent’s GITHUB_WORKSPACE at the clone; keep runner Git configuration and GitHub command files outside its access.
  2. Put setup and both harnesses through one entry point

    • Add the shared lifecycle. Introduce agent_lifecycle.py, launched by sandbox_runtime.mjs inside Anthropic Sandbox Runtime. It will check the boundary, run repository setup, then dispatch to Claude or Codex.
    • Repository setup. Move sandbox_setup out of the separate action step and into that lifecycle. Dependency installation can execute PR code, so it must run inside the same boundary as the agent.
    • Adapt the launchers. Update setup_sandbox.py to assign the clone to the sandbox user. Change run_claude.py to launch inside the existing sandbox. Require the outer sandbox in codex/runner.py and disable Codex’s nested sandbox there. Preserve sandbox-local test-server access while denying direct host loopback.
    Scope of the network change

    The credential broker still accepts arbitrary destinations. This work isolates runner files and processes; it does not prevent exfiltration of files the agent can read. Destination restrictions require a separate design.

  3. Move final shutdown and export into the supervisor

    • Stop and verify. The trusted supervisor owns shutdown. Add launch_sandbox_runtime.py to reap every process under the sandbox user and verify none remain before exporting, on both normal exit and cancellation. Move this responsibility outside the individual harnesses.
    • Bound the export. Add read_regular_nofollow in _safe_files.py. Read only named outputs, reject symlink path components and non-regular files, and enforce size limits before copying anything back to the runner.
    • Make disposal explicit. Stop the Codex model proxy, then call dispose_sandbox_resources.py to remove the dedicated clone and runtime directories. Refuse deletion if sandbox processes remain; restore temporary host policy afterward.
    Why shutdown has to move outside the harness

    Claude already cleans up child processes, but the export guarantee must hold for both harnesses even when a CLI exits or is cancelled. A surviving child could replace an output while the runner reads it.

How I’ll verify the change

I’ll add regression coverage alongside each step, regenerate the workflows, and run the lifecycle probes against both harnesses before handing over the implementation.

Exercise Required result
Review and mention workflows The runner stays at its original commit; the agent clone has the expected event commit and base configuration.
Setup attempts runner-file access; tests start a local server Runner access fails. Sandbox-local connections work; direct host loopback fails.
CLI exits or is cancelled with a background child Every sandbox process stops before the first export read.
Output is a symlink or exceeds its size limit The export refuses it without reading the target or copying the file.

Include self-hosted runners in this rollout?

I’ll add the Linux namespace prerequisite check before launching the sandbox. On disposable GitHub-hosted VMs, the launcher can temporarily adjust and later restore host policy. Self-hosted Linux runners need administrator-configured namespace and AppArmor support.

Include them with an explicit prerequisite Check support before setup; stop with an actionable error when host policy blocks the sandbox. I recommend this: it preserves support without changing administrator-owned policy. Start with GitHub-hosted runners Refuse self-hosted execution for now and add it after a separate validation pass.

After this decision, I’ll implement the steps in order, update the runner requirements in the docs, and regenerate the action workflows. The handoff will include the diff and results from both harnesses.