log: capture sandbox stderr and keep a durable on-disk copy#144
Merged
Conversation
fd 2 becomes a capture pipe like fd 1: child-process stderr, C-extension writes, and direct sys.stderr prints replay on the host /log namespace under agentix.sandbox.stderr (#138). The structured record bridge stays — capture is additive, and the root console handler is repointed at the REAL stderr so stdlib records are not delivered twice and a worker diagnostic on a broken outbound pipe cannot self-amplify. Every bridged record and captured line is also appended to a size-bounded $AGENTIX_LOG_DIR/sandbox.log inside the sandbox (#139), so output that outlives the connection — or the host — has a post-mortem artifact. Best-effort: a write failure detaches the file handler for good rather than reporting into its own capture pipe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review confirmed five defects, all fixed with empirical repros: captured-line file writes now go through handler.handle() (an unlocked emit racing a threaded rollover closed the stream and permanently detached the file); the durable log is per-worker (sandbox-<id>.log — two processes rotating one file race doRollover's rename, same permanent detach, and the default dir is machine-shared); main() restores fd 2 from the saved real stderr on the way out (a crash traceback printed after the loop closed went to a reader-less pipe — zero bytes anywhere, exit misreported as 120); outbound-write failures log a traceback once, not per frame (each logged traceback became a /log frame on the same broken queue, flooding the durable file during the death window); and detaching the file handler now announces itself on /log so a truncated post-mortem file is distinguishable from a quiet sandbox. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Delivers the two observability gaps inherited from #122's
/logintent, additively — the structured LogRecord bridge with ack/replay stays; this only adds what that bridge cannot see.sys.stderrprints replay on the host/lognamespace underagentix.sandbox.stderr(same chunked, deadlock-free drain as stdout). The root console handler is repointed at the saved REAL stderr, so stdlib records are not delivered twice and a worker diagnostic emitted against a broken outbound pipe cannot self-amplify.$AGENTIX_LOG_DIR/sandbox-<worker-id>.log(default/tmp/agentix; empty disables), so output that outlives the connection — or the host — has a post-mortem artifact. Best-effort: a write failure detaches the handler rather than reporting into its own capture pipe, and announces the detach on/logso a truncated file is distinguishable from a quiet sandbox.Review
Three-lens adversarial review (fd lifecycle / log-routing topology / durability promises) with refuters per finding — 5 distinct defects confirmed with empirical repros, all fixed in the second commit:
handler.emit()from the drain path racing a threaded rollover permanently detached the file →handler.handle()(locked, still bypasses the logger tree).main()restores fd 2 from the saved real stderr on the way out; verified by spawning a worker with an injectedread_framecrash — full traceback now reaches the container stderr, exit 1./log(the wire is not the broken resource).Known standing constraint (pre-existing in kind for the fd-1 capture, now shared by fd 2, documented in-code): a remote
async defthat blocks the event loop while its child spews ≥64 KiB into a captured fd wedges both sides — async callables must not block the loop; sync callables run in threads and are unaffected.Testing
sys.stderrprint → host record; child-process stderr → host record; stdlib records NOT re-captured as raw stderr; durable file contains records + both stdio streams.pytest -q tests plugins: 607 passed, 6 deselected;ruffclean; pyright 0 errors at 3.11 and 3.13.Closes #138. Closes #139.
🤖 Generated with Claude Code