The workspace container runs node as pid 1: the entrypoint ends with exec node /app/apps/workspace-agent/dist/main.mjs, and neither deploy/compose.yaml nor deploy/workspace.Dockerfile adds an init (init: true, tini, dumb-init). Node only waits on children it spawned itself, so any orphan reparented to pid 1 is never reaped.
OpenCode's tool calls run arbitrary shells, background jobs, and double-forks. Every process they leave behind when their parent exits becomes a zombie for the life of the container. A long-running workspace can accumulate them until the pid table is exhausted, at which point fork fails for OpenCode's tools and for the service's own git.
The isolation harness shows it directly (#1661, phase 4d): after root signals a detached uid-10001 process tree, all three processes exit but remain in /proc in state Z, reparented to node. The harness now records them rather than failing on them.
This predates the uid split; it matters more now that the container is meant to run untrusted tool processes.
Fix
Add init: true to the workspace service in deploy/compose.yaml (Docker's bundled tini), and --init wherever the image is run directly in CI (WORKSPACE_DOCKER_SECURITY_FLAGS, deploy/tests/isolation-harness.sh, deploy/egress-smoke.sh). tini forwards SIGTERM, so the service's shutdown drain keeps working.
The service then stops being pid 1. The harness reads the service as /proc/1 in several places (identity, /proc/1/environ and /proc/1/mem denials, the "cannot signal the service" control); those need to target the service's real pid. deploy/validate-stack.sh should require init: true.
Acceptance: the harness's phase 4d tree is fully reaped (no Z entries), and a check asserts it.
The workspace container runs
nodeas pid 1: the entrypoint ends withexec node /app/apps/workspace-agent/dist/main.mjs, and neitherdeploy/compose.yamlnordeploy/workspace.Dockerfileadds an init (init: true,tini,dumb-init). Node only waits on children it spawned itself, so any orphan reparented to pid 1 is never reaped.OpenCode's tool calls run arbitrary shells, background jobs, and double-forks. Every process they leave behind when their parent exits becomes a zombie for the life of the container. A long-running workspace can accumulate them until the pid table is exhausted, at which point
forkfails for OpenCode's tools and for the service's owngit.The isolation harness shows it directly (#1661, phase 4d): after root signals a detached uid-10001 process tree, all three processes exit but remain in
/procin stateZ, reparented to node. The harness now records them rather than failing on them.This predates the uid split; it matters more now that the container is meant to run untrusted tool processes.
Fix
Add
init: trueto theworkspaceservice indeploy/compose.yaml(Docker's bundled tini), and--initwherever the image is run directly in CI (WORKSPACE_DOCKER_SECURITY_FLAGS,deploy/tests/isolation-harness.sh,deploy/egress-smoke.sh). tini forwards SIGTERM, so the service's shutdown drain keeps working.The service then stops being pid 1. The harness reads the service as
/proc/1in several places (identity,/proc/1/environand/proc/1/memdenials, the "cannot signal the service" control); those need to target the service's real pid.deploy/validate-stack.shshould requireinit: true.Acceptance: the harness's phase 4d tree is fully reaped (no
Zentries), and a check asserts it.