`Execute` SIGKILLs every command when `ps` is unavailable: `factory-execute-supervisor` treats a failed liveness probe as owner death · Issue #33 · Factory-AI/factory · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Execute SIGKILLs every command when ps is unavailable: factory-execute-supervisor treats a failed liveness probe as owner death #33
In any environment where ps cannot be executed, 100% of Execute tool calls fail instantly with:
Error: Command terminated by signal: SIGKILL
The command itself is never at fault: trivial commands (pwd, /bin/echo hi) die in 57–88 ms. Every other tool (Read, Edit, Grep, MCP tools, fire-and-forget) works normally in the same session.
The cause is factory-execute-supervisor: its owner-liveness probe shells out to ps, and it cannot distinguish "the probe failed" from "my owner died." When the probe fails it concludes the owner is gone and SIGKILLs the whole process group, which contains the command it was supervising.
Environment
Droid CLI: 0.219.0 (also reported on 0.218.0, see Related)
Host: macOS 27.0 / arm64, droid exec --auto high, non-interactive, running under a seatbelt (sandbox-exec) profile
Independently reproduced on Ubuntu 24.04 + Docker (linked issue)
Root cause
droid spawns every Execute command through an inline bash supervisor (createUnixProcess):
A failed probe is read as owner death.ps ... || return 1 fires when ps is missing, non-executable, or denied. None of those mean the owner exited.
No grace period. The sleep 2 is inside the loop body, so the first failed probe falls straight through to kill -KILL -- -$$. The kill lands within milliseconds, before the command can produce output.
Because the supervisor leads the process group, kill -KILL -- -$$ takes down the command shell with it, surfacing as SIGKILL to the user.
Why fire-and-forget is unaffected
The fire-and-forget branch spawns exec nohup bash -c '…' directly, with no supervisor wrapper. It is the only shell path that survives, which matches the symptom pattern reported in both environments.
Reproduction
The supervisor logic can be exercised on its own, with no sandbox and no container. Save the script above as epf.sh (with the trailing owner_pid=$1 … exit "$status" lines from the binary), then:
# The supervisor must be its own process-group leader, matching droid's detached: true.# ps reachable -> works
setsid bash epf.sh $$ /bin/bash 'echo SUPERVISOR_OK'# -> SUPERVISOR_OK, exit 0 (measured: 7 ms)# ps unreachable -> command is SIGKILLed
env PATH=/nonexistent setsid bash epf.sh $$ /bin/bash '/bin/echo SUPERVISOR_OK'# -> no output, exit 137 (SIGKILL) (measured: 5 ms)
End-to-end, in a container without procps:
docker run --rm -it debian:12-slim # ships no `ps`# install droid, then:
droid exec --auto high "use a shell command to print hello"# -> Error: Command terminated by signal: SIGKILL
Affected environments
Any environment where ps is absent or not executable:
Slim/distroless containers without procps (the linked Docker report).
Sandboxed execution on macOS. /bin/ps is setuid-root (-rwsr-xr-x), and a seatbelt (sandbox-exec) profile refuses to exec setuid binaries even under (allow default): ps exits 126 (Operation not permitted) while every non-setuid binary runs fine. The denial is the kernel's setuid-exec rule, not a filesystem allowlist, so the sandbox cannot grant it without allowing setuid-root exec. Droid's Execute tool therefore cannot function under a restrictive macOS sandbox at all.
Observed on this machine: 19 of 19 Execute calls SIGKILLed in sandboxed sessions, versus 0 SIGKILLs across 1,131 Execute calls in unsandboxed sessions.
Suggested fix
Given the blast radius (SIGKILL of the entire process group), the probe should fail open, not closed.
Preferred: distinguish probe failure from owner death. When ps itself fails, treat the result as unknown and continue the loop instead of killing. A probe that cannot run says nothing about the owner:
Fall back to kill -0 "$owner_pid" alone when ps is unavailable. This still detects owner death; it only loses the PID-reuse guard that the parent-identity check provides.
Require N consecutive negative results, and move sleep so the first iteration is never immediately fatal.
One note on a tempting shortcut: replacing the ps probe with bash's $PPID does not preserve the current semantics. Bash fixes $PPID at shell startup, so after the owner dies and the shell is reparented, $PPID still equals owner_pid while ps -o ppid= reports the new parent. Detecting that reparenting is the only thing the ps call buys; a $PPID comparison reduces to kill -0 "$owner_pid" and should be presented as such.
Independent confirmation on a third environment, plus new evidence. Still reproduces on CLI 0.226.1/0.226.2 (auto-updated mid-session from 0.223.0), so this is still live ~6 weeks after filing.
Environment (this report)
Droid CLI 0.223.0 → auto-updated to 0.226.1 → 0.226.2 on 2026-09-25; symptom present on ALL versions
Bare-metal Linux (not Docker): Ubuntu 24.04.4 LTS, kernel 7.0.0-31-generic (OEM), AMD Ryzen AI Max+ 395 (gfx1151), 128 GB RAM
Surfaces: droid exec --input-format stream-jsonrpc (headless worker) and tui, under tmux 3.4
New data points
Persists across OS reboot — foreground Execute SIGKILLs before and after a full reboot (07:22 reboot, symptom unchanged after 07:24 boot). Rules out transient host state.
Random race confirmed with twin probes: two identical foreground echo invocations — one exits 0, one is SIGKILLed. Externals (/bin/true, uname -r, python3 --version) die ~always within ~1s; late in the day even builtins get killed after producing output. Suggests the probe failure itself is racy in our environment, not a clean deterministic first-probe failure.
Same-root corroboration from the daemon itself: the daemon's BackgroundProcessTracker hits the identical wall on this host — uncaught exception EACCES: permission denied, posix_spawn 'ps' (tree-kill force-kill path) — plus repeated WARN [ResourceMonitor] Failed to sample process tree RSS | reason: no_trusted_ps_executable. So ps is unavailable to multiple CLI subsystems here, and the supervisor is the one that reacts lethally.
Kernel-level causes excluded on this host (bare-metal, so complementing the macOS/Docker reports): /proc/self/status → Seccomp: 0, NoNewPrivs: 0; kern.log/syslog zero entries at kill timestamps; ulimits clean; OOM impossible (128 GB, load ~1). User-side PreToolUse hooks ruled out (pass-through stubs; a deny produces a denial message, not SIGKILL).
Operational workaround, verified across versions and reboot: route every command through fire-and-forget background execution → output lands in /tmp/droid-bg-*.out → read the file via the harness Read tool. With this pattern the session is fully functional (including journalctl, python, etc.); foreground is the only casualty.
Ask (echoing the OP, with emphasis)
Beyond fixing the supervisor (failed probe ≠ owner death; add grace period), please consider letting the CLI's own subsystems (supervisor liveness probe, ResourceMonitor, BackgroundProcessTracker/tree-kill) use a ps that works inside your own sandbox — three independent subsystems degrade on the same masked binary, and only the supervisor's failure mode is destructive.
Activity
[gobby-#22402] fix: keep sandboxed Droid Execute alive with a re-sign…
[gobby-#22402] fix: keep the Droid ps shim ahead of /bin in Droid's h…
slamdunk111 commentedon Sep 25, 2026
Independent confirmation on a third environment, plus new evidence. Still reproduces on CLI 0.226.1/0.226.2 (auto-updated mid-session from 0.223.0), so this is still live ~6 weeks after filing.
Environment (this report)
droid exec --input-format stream-jsonrpc(headless worker) andtui, under tmux 3.4New data points
echoinvocations — one exits 0, one is SIGKILLed. Externals (/bin/true,uname -r,python3 --version) die ~always within ~1s; late in the day even builtins get killed after producing output. Suggests the probe failure itself is racy in our environment, not a clean deterministic first-probe failure.EACCES: permission denied, posix_spawn 'ps'(tree-kill force-kill path) — plus repeated WARN[ResourceMonitor] Failed to sample process tree RSS | reason: no_trusted_ps_executable. Sopsis unavailable to multiple CLI subsystems here, and the supervisor is the one that reacts lethally./proc/self/status→Seccomp: 0,NoNewPrivs: 0;kern.log/syslogzero entries at kill timestamps; ulimits clean; OOM impossible (128 GB, load ~1). User-side PreToolUse hooks ruled out (pass-through stubs; a deny produces a denial message, not SIGKILL)./tmp/droid-bg-*.out→ read the file via the harness Read tool. With this pattern the session is fully functional (includingjournalctl, python, etc.); foreground is the only casualty.Ask (echoing the OP, with emphasis)
Beyond fixing the supervisor (failed probe ≠ owner death; add grace period), please consider letting the CLI's own subsystems (supervisor liveness probe, ResourceMonitor, BackgroundProcessTracker/tree-kill) use a
psthat works inside your own sandbox — three independent subsystems degrade on the same masked binary, and only the supervisor's failure mode is destructive.