Skip to content

fix(shell): kill + reap the child process group on timeout (AB-006) - #305

Merged
hartsock merged 1 commit into
mainfrom
fix/ab-006-shell-timeout-reap
Jul 28, 2026
Merged

fix(shell): kill + reap the child process group on timeout (AB-006)#305
hartsock merged 1 commit into
mainfrom
fix/ab-006-shell-timeout-reap

Conversation

@hartsock

Copy link
Copy Markdown
Member

Summary

Fixes #269 / AB-006. ShellTool's safe-subset engine, on timeout, detached the blocking worker and killed nothing — the child and its descendants ran to completion; the caller saw timed_out: true while the system had merely stopped waiting (the post-sleep side effects still happened).

Now the timeout is enforced where the children live:

  • each pipeline stage is spawned as its own process-group leader (process_group(0));
  • run_pipeline polls the stages with try_wait to a deadline and, on the deadline, SIGKILLs each stage's process group (so grandchildren die too) and reaps them;
  • Captured carries a confirmed-timeout flag; the envelope's timed_out is derived from a real termination.

The outer tokio timeout now only bounds when invoke returns — even if it fires and detaches the worker, that worker keeps running run_pipeline, which reaches the same deadline and does the kill+reap. This mirrors the Brush engine's existing kill_worker_tree supervisor. rustix (already a Brush dep) is made non-optional to provide the safe kill_process_group under the crate's #![forbid(unsafe_code)].

Test plan

Non-privileged real-spawn E2E regressions (validated locally):

  • real_timed_out_child_is_killed_and_never_writes_markersh -c 'sleep 3; touch M' under a 1s timeout → timed_out: true and M never appears after the sleep window. Before the fix the detached child wrote it.
  • real_timed_out_grandchild_is_killed — nested sh -c 'sh -c "…"' proves the process-group kill reaches a grandchild.
  • All 56 lib tests pass (the two existing mock-based timeout tests still hold once the outer timeout is the return-bound, not the killer); clippy -D warnings + fmt clean; full pre-push gate incl. bare-Brush [pre-push] OK.

Scope

agent-bridle-tool-shell. The Brush engine already handled this; this brings the safe-subset engine to parity. Windows Job-Object teardown (vs the Unix process group) remains a follow-up — the #[cfg(unix)] group-kill is a no-op there and falls back to child.kill().

Fixes #269

🤖 Generated with Claude Code

WHAT: ShellTool's safe-subset engine detached the blocking worker on timeout and
killed NOTHING — the child (and its descendants) ran to completion; the caller
saw "timed_out" while the system had only stopped waiting. Now each pipeline
stage is spawned as its own process-group leader (`process_group(0)`), and
`run_pipeline` enforces the deadline itself: it polls the stages with `try_wait`
and, on the deadline, SIGKILLs each stage's process GROUP (catching grandchildren)
and reaps them. `Captured`/`SpawnCfg` carry the confirmed-timeout flag + the
deadline; `timed_out` is derived from a real termination. The outer tokio timeout
now only bounds when `invoke` returns — even if it fires and detaches the worker,
the worker keeps running `run_pipeline`, which reaches the same deadline and kills.
Mirrors the Brush engine's existing `kill_worker_tree` supervisor. `rustix`
(already a dep for Brush) is made non-optional to provide the safe
`kill_process_group` under the crate's `#![forbid(unsafe_code)]`.

WHY: #269 / AB-006 — a fail-open resource/persistence defect. Regression E2E
(non-privileged, real spawns): a `sleep 3; touch MARKER` child under a 1s timeout
reports timed_out AND the marker is never written; a nested-`sh` variant proves
the process-group kill reaches a grandchild. Before the fix the detached child
wrote the marker.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@hartsock hartsock added the risk:high High-risk change label Jul 28, 2026
@hartsock
hartsock merged commit 488e159 into main Jul 28, 2026
9 of 10 checks passed
@hartsock
hartsock deleted the fix/ab-006-shell-timeout-reap branch July 28, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high High-risk change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AB-006: shell timeout does not terminate/reap the child process

1 participant