fix(server): process-group kills never reach every process the user owns - #14461
Conversation
A fake spawner in tests reports pid 1, and the OpenCode runtime cleans up with process.kill(-pid). That became kill(-1, SIGKILL), which signals every process the user owns: running the provider tests killed the T3 server, cloudflared and every agent on the machine. signalProcessGroup refuses a pid of 0, 1 or a non-integer with ESRCH, like a group that already exited, and every server process-group signal (OpenCode runtime and server ledger, ACP, Pi) goes through it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — The runtime change is a focused process-group safety fix with bounded impact and tests covering both rejected and valid PIDs. The new test file also adds a file-level static-analysis suppression directive, so human review is warranted. You can add or adjust custom eligibility rules. Learn more. |
6562235
into
t3code/codex-turn-mapping
Running the server's provider tests killed every process the developer's user owned on the machine: the T3 server, cloudflared, and every running agent. It happened three times on cups while agents worked on the OpenCode 2 stack.
Several tests fake the process spawner and report every spawned process as pid 1 (
ChildProcessSpawner.ProcessId(1), for example inProviderRegistry.test.ts). The OpenCode runtime cleans up a command by killing its process group withprocess.kill(-pid, "SIGKILL"). With pid 1 that iskill(-1, SIGKILL), which on POSIX signals every process the caller is allowed to signal. A pid of 0 would signal the server's own group.Fix
process/processGroup.tsaddssignalProcessGroup(pid, signal). It refuses a pid of 0 or 1, or one that is not an integer, by throwing ESRCH, the same error a group that has already exited gives, so every caller's existing handling stays the same. Every process-group signal in the server now goes through it:opencodeRuntime.ts);A real child's pid is never 0 or 1, so production behaviour doesn't change.
Verification
processGroup.test.ts: pids 1, 0, −1, NaN and 1.5 are refused with ESRCH, using signal 0 only, so the test is harmless even without the guard; a real spawned group is still killed. Without the guard the first test fails (pid 1: expected undefined to be 'ESRCH'). Both runs were inside a user and PID namespace (unshare -U --map-current-user -p -f --mount-proc), so nothing outside could be signalled.ProviderRegistry,CodexDriver,providerMaintenanceRunner,providerSnapshot.OpenCodeServerLedger.test.tsandAcpSessionRuntime.processTree.test.tspass outside the sandbox (they observe real process groups, so they fail inside it on the base branch too). The exception ispreserves exact argv and strips wrapper-only environment before exec, which fails on the base branch too on this machine because it expectsnodein/usr/binor/bin.tscexits 0 forapps/server, and lint is clean on the touched files.mainhas the same twoopencodeRuntime.tskills; this PR only targets V2.🤖 Generated with Claude Code