feat(sandbox): stateful executor toolset + research_demo data_analyst#99
Merged
Conversation
Adds stage_inputs() helper that copies host files into session_dir/inputs/<basename> before code runs. Threads inputs=None through ABC, SandboxManager, both backends (docker + local), and the module-level and factory-bound sandbox_execute tools. Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
…m.read
- PermissionEngine._resolve: list/tuple target_arg values now emit one
ResolvedCapability per item, each canonicalized individually; scalar
and None target_arg paths are unchanged (no regression).
- sandbox_execute: declares Capability("filesystem.read", target_arg="inputs")
alongside python.exec.stateful so every staged file path is evaluated
by the permission engine exactly like read_file.
- Tests: TestResolveListTargetArg (engine) + test_inputs_declares_filesystem_read
(sandbox); full permissions suite (112) and sandbox suite (56) green.
Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
Mount <workspace_dir>/artifacts (or sandbox_outputs_dir) at /workspace/outputs in every container so code can write final deliverables there; append host paths of all files in that dir to ExecutionResult.artifacts after each successful run. Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
Wire a stateful data-analysis specialist into the research_demo example: - data_analyst AgentConfig with sandbox_execute, read_file, write_file, ask_clarification; coordinator delegates to it for multi-step analysis - examples/research_demo/data/benchmarks.csv (5-row ImageNet benchmark set) - ResearchDemoSettings.model_post_init sets sandbox_data_mounts (:samples) and sandbox_outputs_dir defaults so docker backend works with one flip Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
Docker creates nested bind-mount targets as root when they don't exist on the host, leaving a root-owned outputs/ dir that breaks pytest teardown and pollutes the session workspace. Pre-create <working_dir>/outputs before runtime.start() so the mount point is always host-user-owned. Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
…ount
Nested bind-mounts under /workspace caused macOS Docker Desktop to stamp
ACL/xattr entries on <session_dir>/outputs, making the directory
un-removable at teardown (PermissionError). On Linux the same pattern
creates a root-owned directory.
Fix: remove the Mount("/workspace/outputs") from ContainerSpec entirely.
The agent continues writing to /workspace/outputs/ inside the container
(it is a plain subdirectory of the /workspace bind-mount, pre-created
host-owned). After a successful execute(), each regular file is copied
with shutil.copy2 from <working_dir>/outputs/ to the shared
_outputs_dir(), and the shared destination paths are appended to
ExecutionResult.artifacts. Only this session's files are returned.
Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
… stage_inputs guard
- JupyterLocalBackend.execute: pre-create working_dir/outputs/ before running code
so open('outputs/x','w') never raises FileNotFoundError (local backend).
Also fix _get_or_create_session to pass cwd via km.start_kernel(cwd=...) —
km.cwd is a no-op in jupyter_client 8.x; the correct API is a kwarg.
- JupyterDockerBackend._start_session: extract _parse_data_mounts() helper and
use it in both _build_spec and _start_session; pre-create working_dir/data/<name>
dirs/files before runtime.start() so Docker does not create them as root.
- stage_inputs: raise ValueError early when session_dir is None instead of
letting Path(None) propagate a TypeError that callers' except ValueError miss.
- examples/research_demo/agents.py: update docstring to reflect three agents.
- tests/permissions/test_engine.py: convert unused ResolvedCapability import to
a functional isinstance assertion.
Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
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
Makes the Docker-backed stateful Jupyter executor usable by agents via a clean, pluggable config and a small toolset, and demonstrates it with a new
data_analystsub-agent in research_demo. Built spec → plan → subagent-driven TDD, per-task reviewed, whole-branch reviewed (opus).What changed
efa4138): replacedsandbox_execute_enabled+sandbox_backendwith a singlestateful_executor_backend(none/local/docker, defaultnone).nonefail-closes; safer (no accidental unsandboxed-local default). Pluggable-backend seam kept for futuremodal/runpod.inputsstaging (55b5ed0):sandbox_execute(code, session_id, inputs=[])copies host files to a guaranteedinputs/<name>before the code runs (documented contract; no separate staging tool for the model to remember).c364b18): eachinputspath is checked againstfilesystem.readvia a list-awarePermissionEngine._resolve(scalar/None resolution provably unchanged — no regression to other gated tools).outputs/(60719f0,8e18015,157a182): final deliverables land in a shared, session-independent dir visible to downstream agents. Originally a nested bind mount; redesigned to copy-after-execute after a docker-live test surfaced a nested-mount ownership/ACL footgun (root-owned on Linux, file-sharing ACL on macOS).data_analyst(72693b3): new leaf agent + sample dataset + demo config; the coordinator delegates multi-step data analysis to it. Demo runs without Docker by default (none).b3eb719): local-backendoutputs/pre-create (+ fixed a jupyter_client 8.xkm.cwdno-op so relative I/O lands in the workspace), data-mount point pre-create,stage_inputsNoneguard, cosmetics.Verification
Known limitation / follow-ups (non-blocking)
sandbox_data_mountsleave empty, un-removable mount-point dirs on macOS Docker Desktop (file-sharing ACL); clean on Linux (CI/prod). Not a crash — the manager never rmtrees prod session dirs. True cross-platform fix = copy-in (likeinputs), deferred to avoid a large-dataset efficiency tradeoff.0o777; outputs re-copy/dedupe per execute;ui_ordercollisions; a couple of research_demo prompt/docstring cosmetics.Design spec + plan live under gitignored
docs/superpowers/.https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv