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
-
Replace the in-place checkout
-
Change the workflows. In
review.yaml.j2andmention.yaml.j2, remove the second checkout andgh pr checkout. Keep the reviewed runner tree for orchestration and POST cleanup; remove_restore_local_actions_runfromworkflows.py. -
Add workspace preparation. Create
prepare_agent_workspace.pyto 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_WORKSPACEat the clone; keep runner Git configuration and GitHub command files outside its access.
-
Change the workflows. In
-
Put setup and both harnesses through one entry point
-
Add the shared lifecycle. Introduce
agent_lifecycle.py, launched bysandbox_runtime.mjsinside Anthropic Sandbox Runtime. It will check the boundary, run repository setup, then dispatch to Claude or Codex. -
Repository setup. Move
sandbox_setupout 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.pyto assign the clone to the sandbox user. Changerun_claude.pyto launch inside the existing sandbox. Require the outer sandbox incodex/runner.pyand 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.
-
Add the shared lifecycle. Introduce
-
Move final shutdown and export into the supervisor
-
Stop and verify. The trusted supervisor owns shutdown.
Add
launch_sandbox_runtime.pyto 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_nofollowin_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.pyto 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.
-
Stop and verify. The trusted supervisor owns shutdown.
Add
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.
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.