security: restricted unpickler for sandbox-to-host return values#137
Merged
Conversation
…116, PR #122 stage E) Return values travel host-ward as pickle.dumps(result) and the host decoded them with plain pickle.loads. pickle reconstructs objects by invoking whatever callables a stream names, so decoding a value shaped by a less-trusted sandbox workload (a cloned repo, a generated patch, a benchmark task) is a trust boundary: the returned object can direct reconstruction on the host (#116). Keep pickle as the return codec (the msgpack-returns alternative was evaluated and closed as #118) and decode host-side through a strict allowlist (agentix/runtime/shared/safepickle.py). find_class permits only: - a reviewed set of value types whose construction has no external effect (stdlib data: datetime/decimal/fractions/uuid/collections/ pathlib; builtin containers + exceptions; numpy arrays), and - a small set of inert reconstruction helpers (copyreg, numpy _reconstruct), and refuses everything else WITHOUT importing it. A denylist was rejected as unsound: a stream can name a C-accelerator module (_operator vs operator), a callable produced by one reconstruction step is invoked by the next without passing find_class (so attribute-access helpers must never be admitted), and many ordinary constructors have side effects. A closed allowlist of value types closes all three. First-party types (agentix.*) are trusted by default: the framework and its plugins are the trusted computing base that builds the bundle and runs the sandbox, and their return types (TunnelHandle, BashResult, agent results) are inert. This keeps the framework's own paths (Proxy.start, bash.run, agent adapters) working without setup. A workload's own return types are refused by default; opt in with safepickle.allow_module(prefix) / allow_callable(module, name), or set AGENTIX_PICKLE_TRUST=1 to trust the sandbox fully. A refusal raises agentix.RestrictedUnpickleError. Only the sandbox->host direction is restricted (client._unpickle_value, both the SIO and HTTP result paths). The sandbox-side decode of host-supplied arguments/context stays plain pickle -- the trusted host->sandbox direction. Tests: non-allowlisted callables (subprocess.check_output/Popen, os.system, eval) refused; attribute-access helpers (operator/_operator attrgetter/itemgetter/methodcaller, getattr) refused; refusal does not import the named module; object-dtype numpy arrays gate nested globals; first-party return types (TunnelHandle, BashResult) and permitted value types + builtin exceptions + workload-via-allow_module round-trip; trust bypass; end-to-end refusal over a real remote() call. PROTOCOL.md documents the boundary; RestrictedUnpickleError exported. Closes #116. Co-Authored-By: Claude <noreply@anthropic.com>
The unpickling paragraph still described the pre-exact policy: blanket agentix.* prefix trust and the deleted allow_module()/allow_callable() surfaces. Name the six individually registered first-party return types and point extension at allow_type(Class), matching safepickle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Ray-cluster build notes are unrelated to the restricted unpickler; untrack them so the stage E diff stays on-topic. The file stays on disk as an untracked local note. 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
Return values from
client.remote(fn, ...)travel as pickle bytes and are decoded on the host. Plainpickle.loadsreconstructs objects by invoking whatever callables the stream names, so decoding a value influenced by sandbox-side code (a cloned repo, a generated patch, a benchmark task) is a trust-boundary concern. This PR keeps pickle as the wire format but decodes sandbox return values through a strict allowlist.agentix/runtime/shared/safepickle.pyaddsrestricted_loads, wired into the host-side decode on both the Socket.IO and HTTP result paths (RuntimeClient._unpickle_value).find_classadmits only exact, individually reviewed globals: stdlib value types (datetime,decimal,fractions,uuid,collections,pathlib), builtin containers and exceptions, numpyndarray/dtype, inert reconstruction helpers (copyreg, numpy_reconstruct), and six first-party return types registered by exact module and name —TunnelHandle,BashResult,UploadResult,ClaudeCodeResult, the qwen_codeResult, andPrepareEnvResult. Everything else is refused without importing the named module.EXT1/EXT2/EXT4) are rejected before decoding: the copyreg extension registry resolves names outsidefind_class, so it cannot be validated against an exact allowlist.agentix.RestrictedUnpickleError(exported). Extension point:safepickle.allow_type(Class)trusts one exact class;AGENTIX_PICKLE_TRUST=1restores plain decode for deployments where the whole sandbox is trusted.arguments/contextis unchanged — that is the trusted host→sandbox direction.agentix/runtime/PROTOCOL.mddocuments the policy.Compatibility
Returning builtins, stdlib value types, numpy arrays, and all framework result types works unchanged. A workload returning its own classes now needs a host-side
allow_type(Class)opt-in (or the full-trust env var); the refusal message says exactly this.Testing
pytest -q tests plugins: 576 passed, 6 deselectedtests/runtime/test_safepickle.py,tests/runtime/test_protocol.py(end-to-end refusal),tests/test_public_exports.py— 98 passedruff check .clean; pyright 0 errors at--pythonversion 3.11and3.13Closes #116. Stage E of the #122 split (stages A–D: #133, #134, #135, #136; stage F deferred).
🤖 Generated with Claude Code