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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

Shashankss1205and 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
Shashankss1205force-pushed the fix/sandbox-child-death-eof branch from 110b813 to d68596fCompareAugust 13, 2026 19:14
@Shashankss1205
Shashankss1205 merged commit ad6d305 into mainAug 13, 2026
6 checks passed
@Shashankss1205
Shashankss1205 deleted the fix/sandbox-child-death-eof branch August 13, 2026 19:19
@Shashankss1205Shashankss1205 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 freeto 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

@Shashankss1205