Skip to content

A sandboxed child that dies without sending names the tool it was running - #112

Merged
Shashankss1205 merged 2 commits into
mainfrom
fix/sandbox-child-death-eof
Aug 13, 2026
Merged

A sandboxed child that dies without sending names the tool it was running#112
Shashankss1205 merged 2 commits into
mainfrom
fix/sandbox-child-death-eof

Conversation

@Shashankss1205

Copy link
Copy Markdown
Collaborator

Closes #111.

The gap

poll() returns true when the pipe is readable, and a closed pipe is readable. So a child that died without sending — os._exit, a segfault, an OOM-kill — fell through the timeout guard into recv():

RAISED builtins.EOFError: ''
does the message name the tool? False

run has two failure shapes and this was neither: SandboxViolation for a confinement breach, RuntimeError(f"tool {name!r} failed: …") for the tool's own exception.

Why it mattered

AgentNode's loop catches it under the blanket clause and renders f"TOOL_ERROR: {exc}". str(EOFError('')) is '', so the model was handed:

TOOL_ERROR:

Nothing after the colon. Told its call failed, given no way to tell why, which tool, or whether a retry could help — and the same text went into the trace, so the audit trail couldn't explain it either. The _CALL_SHAPE_ERROR hint just below can't fire on it, since it matches on message text and there is none.

This is a defect shape already closed here once: "a phase the budget curtailed reported nothing at all … wrote [budget_exhausted] with nothing after it." Same empty message, one layer down.

After

clean exit(3)  -> RuntimeError: tool 't' failed: the sandboxed child exited without sending a result, exit code 3
SIGKILL        -> RuntimeError: tool 't' failed: the sandboxed child exited without sending a result, exit code -9 (killed by signal 9)
normal tool    -> returned 'fine'
tool raises    -> RuntimeError: tool 't' failed: ValueError("tool's own bug")

A negative exit code renders as the signal that killed it — which is what tells an OOM-kill apart from a deliberate _exit.

Deliberately still a RuntimeError, not a SandboxViolation. A child dying is not evidence it tried to escape confinement, and a violation is a specific accusation that lands in the trace as one. One of the tests asserts that directly.

Verification

  • Three new tests in tests/test_harness_gate.py (parametrised over clean exit and SIGKILL, plus one pinning the signal detail). All go red without the fix, with the EOFError raising out of multiprocessing/connection.py exactly as reported.
  • The normal return, tool-raises, and timeout paths are untouched and still covered.
  • uv run pytest green · uv run ruff check . clean.

The forked child runs module-level functions rather than closures, since a fork child needs picklable top-level bodies for this to behave the same way under other start methods.

🤖 Generated with Claude Code

Shashankss1205 and others added 2 commits August 14, 2026 00:44
…ning

`poll()` returns true when the pipe is readable, and a closed pipe is
readable. So a child that died without sending — `os._exit`, a segfault, an
OOM-kill, anything that kills the process rather than raising inside
`spec.fn` — fell through the timeout guard into `recv()` and raised a bare
`EOFError('')`.

`run` has two failure shapes and that was neither: `SandboxViolation` for a
confinement breach, `RuntimeError(f"tool {name!r} failed: ...")` for the tool's
own exception. An EOFError with an empty message is attributable to nothing.

Downstream is where it bit. `AgentNode`'s loop catches it under its blanket
`except Exception` and renders `f"TOOL_ERROR: {exc}"` — and `str(EOFError(''))`
is `''`, so the model was handed `TOOL_ERROR:` with nothing after the colon. It
was told its call failed and given no way to tell why, which tool, or whether a
retry could help; the same text went into the trace, so the audit trail could
not explain the failure either. The `_CALL_SHAPE_ERROR` hint below that clause
cannot fire on it, since it matches on message text and there is none.

This is the shape of a defect already closed here once: a curtailed phase
writing `[budget_exhausted] ` with nothing after it. Same empty message, one
layer down.

Now a `RuntimeError` naming the tool and the exit code, with a negative one
rendered as the signal that killed it — which is what tells an OOM-kill apart
from a deliberate `_exit`. Deliberately not a `SandboxViolation`: a child dying
is not evidence it tried to escape confinement, and a violation is a specific
accusation that lands in the trace as one.

The three new tests go red without the fix, with the EOFError raising out of
`multiprocessing/connection.py` exactly as reported. The normal return, the
tool-raises and the timeout paths are untouched.

Closes #111

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Shashankss1205
Shashankss1205 force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596f Compare August 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into main Aug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205 Shashankss1205 mentioned this pull request Aug 13, 2026
Shashankss1205 added a commit that referenced this pull request Aug 13, 2026
Two defects closed since 0.1.6, both found by re-verifying a stale bug
backlog against main rather than by a report:

- admission accepted `END` as an edge source and `START` as a target, so a
  graph that cannot be built was admitted and failed in materialisation —
  charged to the execution-failure allowance rather than the rejection one,
  and reaching the planner as prose instead of a code and a remedy (#108/#109).
- a sandboxed child that died without sending escaped as a bare `EOFError('')`,
  which the agent loop rendered to the model as `TOOL_ERROR:` and nothing
  else (#111/#112).

The version moves in the two places CI compares and the six where prose
states it. `tests/test_deep_dive.py` and `tests/test_cookbook_*.py` assert
all but the README line, which is how they stay right.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

harness: a sandboxed child that dies without sending escapes as a bare EOFError, so the agent is told 'TOOL_ERROR:' and nothing else

1 participant