A streaming popen on the Executor, and ACP in the sandbox¶
The Executor gains one streaming primitive — popen(argv, cwd, env) — alongside the
request/response run. It returns a long-lived process with line-buffered stdio pipes, which is
what the ACP harness needs (it speaks JSON-RPC 2.0 over the agent's stdio). Routing ACP's
process launch through popen lifts ADR 0014's last deferral: ACP agents now run inside a
docker sandbox, not just on the host.
Status¶
accepted (extends ADR 0014; completes the harness-in-sandbox set)
Context¶
ADR 0014 routed every harness's effects through the Executor so they land wherever the Sandbox
is. It worked for the CLI family and the in-process openai loop, but explicitly deferred ACP:
ACP is a bidirectional, long-lived JSON-RPC conversation over the agent subprocess's stdin/stdout,
and the Executor's only process primitive was run — fire a command, wait, collect stdout. You
cannot drive a session over that. So AcpConnection called subprocess.Popen directly, pinning
the ACP agent to the host even when the rest of the Cell ran in a container.
Decision¶
Add Executor.popen(argv, *, cwd, env) -> Popen: spawn a long-lived process in the sandbox
with text, line-buffered stdin/stdout/stderr pipes.
- Host default (base class /
LocalExecutor):subprocess.Popen— today's behavior. ContainerExecutor:docker exec -iinto the running container, env forwarded by the same delta+passthrough filter asrun. The agent runs in the sandbox; its JSON-RPC stdio is piped to the host driver, and itsfs/*and permission callbacks come back over that channel exactly as before (the bind mount keeps the host-sidefshandlers correct).HarborExecutor: raisesNotImplementedError— Harbor'sexecis request/response with no persistent-stdio channel, so an ACP session can't be driven through it. Failing clearly beats hanging; the message points atbackend: dockeror a non-ACP harness.
AcpConnection takes an optional executor (default LocalExecutor) and spawns through
popen; the ACP harness passes ctx.executor, so the same container.harness: true switch that
sandboxes every other harness now sandboxes ACP too. Whether ACP runs in the sandbox follows the
same rule as the rest (ADR 0014's _harness_runtime): host by default, container when opted in.
Considered options¶
- Stream over
run.runis buffered request/response; there is no way to interleave reads and writes mid-command. Rejected — wrong shape for a session protocol. - A separate ACP-specific transport abstraction. A second seam beside the Executor, duplicating
cwd/env/teardown. Rejected —
popenis one method on the seam touchstone already has. - Leave ACP on the host (ADR 0014 status quo). Leaves untrusted ACP-agent commands on the orchestrator and makes ACP the one family that can't be isolated. Rejected.
Consequences¶
- Every harness family can run in a docker sandbox: output-only CLI,
claude-code-stream,openai, and now ACP.container.harness: trueis the single switch. - ACP on Harbor is explicitly unsupported, with an actionable error rather than a hang — an honest boundary, since Harbor offers no stdio stream. (A future Harbor streaming primitive, or a websocket ACP transport, would lift it; out of scope here.)
- The container path is validated by a
docker exec -istdio round-trip test; the host path is covered by the existing ACP suite (which now spawns throughLocalExecutor.popen). popenis a small, general addition — any future long-lived in-sandbox process (a language server, a debugger bridge) composes it without a new seam.