Repository navigation
fix: preserve oversized sandbox JSON-RPC responses - #4371
Conversation
|
Thanks for digging into this — #4299 is a real problem (an unbounded-output command shouldn't be able to take down an entire eval), and bounding the response on the sandbox side before JSON-RPC serialization is a sensible shape for the fix. The test coverage is appreciated too. I do have a concern about how the limit is determined, though. Inspect already has an affordance for callers to adjust the max exec output size: the 10 MiB default can be raised via the
Would the existing affordances help your use case? If the failures you're seeing come from output that's legitimately large but bounded, raising Happy to discuss direction — thanks again for the contribution! |
42fc17b to
9ddafe8
Compare
|
Thanks, that makes sense. I pushed an update in The sandbox-side cap is no longer a fixed 1 MiB value and no longer reads I also updated the tests to cover both sides of that contract:
Validation run locally:
|
|
Thanks for the quick turnaround. Your update does what I suggested, and the test coverage is right on. That said, after looking at underlying failure more deeply, I don't think we should merge a payload-capping fix at all. The bug is really in the transport layer: if the sandbox provider truncates the exec stream at the output cap, the host-side size check passes and a clipped JSON line flows straight into A more correct fix I have in mind is chunked egress in the shared JSON-RPC transport: when a response exceeds a threshold, the sandbox-side dispatcher returns the first chunk plus a continuation handle, and the host loops fetching chunks until it has the complete frame before parsing. Every wire exchange is small, and it fixes the same latent bug in Since the transport fix is a bigger, cross-cutting change, a practical note in the meantime: if the output you're hitting is large but bounded, the shipping code plus raising I want to be upfront that if I do the fuller fix, it may take a little while to get prioritized. I have no expectation that you take it on — but if it's something you'd be interested in tackling, I'd be glad to support that; either way, let me know if the workaround covers your immediate need. |
|
Thanks, that direction makes sense. I pushed an updated transport-level approach in db0258f. What changed:
I also added host-side transport tests plus sandbox-side chunking coverage, including the in-process text_editor path under a low chunk threshold. Validated locally:
|
|
Thanks @Kevin-Li-2025. I'll take a look today. |
…tput-limit # Conflicts: # CHANGELOG.md
epatey
left a comment
There was a problem hiding this comment.
Thanks again for the continued work on this, @yinli-systems and apologies for the two-week gap on my end. The transport-level chunking is exactly the shape we discussed, and the implementation and test coverage are in good shape overall.
One area the shared chunk directory doesn't handle yet: mixed-user tool calls. Tools can be exec'd with a user= argument, so within a single eval the CLI may run as different uids. Two situations need to work:
- A
user=-scoped call creates the directory first. The directory ends up owned by that uid, and a later root-invoked call fails the owner check inensure_json_rpc_response_chunk_dir()(uid 1000 isn't in(0, 0)). Because that check runs at the top of_execfor all requests, every subsequent root tool call fails - not just chunked ones. - The path is pre-occupied. The directory lives at a fixed path in a world-writable location, so sandbox code can create something else there first (e.g. a plain file). Today that also fails all tool calls, including small responses that never touch the chunk machinery.
Related design constraint: we deliberately keep the injected tooling hidden from the model - it lives in a dot-prefixed random-hash directory precisely so an agent doesn't stumble on the harness (see the _cli.py docstring). A fixed, self-describing path like /tmp/inspect-sandbox-tools-json-rpc-chunks cuts against that. So the chunk storage should (a) live under the hidden random tools directory or a similarly random hidden path, and (b) preserve per-identity isolation - e.g. per-uid 0700 subdirectories with files owned by the requesting identity, which also sidesteps the cross-user ownership problem in scenario 1. (For remote-dispatch tools, another option is holding chunks in memory in the long-lived server; only the in-process tools genuinely need disk.)
I've pushed two failing tests to this branch that pin down both scenarios: test_chunk_dir_accepts_agent_owned_dir_when_running_as_root and test_small_response_unaffected_by_unusable_chunk_dir. They're written against the behavior rather than a specific fix, so any approach that makes them pass works.
Also a heads-up: I bumped sandbox_tools_version.txt on your behalf, since this change requires publishing new sandbox-tools binaries — I'll handle that build/publish once the PR is green.
Thanks again for sticking with this.
Signed-off-by: Kevin-Li-2025 <[email protected]>
Signed-off-by: Kevin-Li-2025 <[email protected]>
Tolerating an agent-owned chunk dir means the owning sandbox uid can swap the entry between the ownership check and the chmod that follows; chmod on a path follows a symlink planted there, so root could be made to widen an arbitrary path to 1733. Check and chmod through an O_DIRECTORY|O_NOFOLLOW descriptor instead. Existing chunk-dir tests simulated ownership by patching lstat/chmod, which the fd path no longer calls; repointed to os.fstat/os.fchmod so they still pin their scenarios.
Summary
Fixes #4299.
Sandbox tool responses can exceed the host exec-output limit, which truncates a JSON-RPC frame before parsing. This change preserves the complete response through a bounded continuation protocol: the sandbox spills an oversized serialized response to a per-user, permission-checked file, returns small base64 chunks, and the host validates and reassembles them before JSON-RPC parsing.
bash_sessionalso receives the host's effectiveSandboxEnvironmentLimits.MAX_EXEC_OUTPUT_SIZEvalue and bounds each PTY read, preventing a single interactive response from growing without limit while retaining subsequent output for later interactions. The continuation protocol is enabled only for the current sandbox CLI transport, so legacy transports keep their existing behavior.The implementation validates handles, ownership, offsets, totals, completion, base64 and UTF-8; separates concurrent transfers; and releases temporary files on success and error. A root-owned sticky shared directory remains usable by non-root sandbox users without permitting unsafe ownership or modes.
Validation
git diff --checkpassed.bash_sessionoutput with eventual tail delivery.AI assistance
AI tooling assisted with implementation and test iteration. I reviewed the final design and diff, traced the transport and sandbox trust boundaries, and ran the validations listed above.