Skip to content

feat(sandbox): stateful executor toolset + research_demo data_analyst#99

Merged
shoom1 merged 9 commits into
developfrom
feature/stateful-executor-toolset
Jul 8, 2026
Merged

feat(sandbox): stateful executor toolset + research_demo data_analyst#99
shoom1 merged 9 commits into
developfrom
feature/stateful-executor-toolset

Conversation

@shoom1

@shoom1 shoom1 commented Jul 8, 2026

Copy link
Copy Markdown
Owner

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_analyst sub-agent in research_demo. Built spec → plan → subagent-driven TDD, per-task reviewed, whole-branch reviewed (opus).

What changed

  • Unified config (efa4138): replaced sandbox_execute_enabled + sandbox_backend with a single stateful_executor_backend (none/local/docker, default none). none fail-closes; safer (no accidental unsandboxed-local default). Pluggable-backend seam kept for future modal/runpod.
  • inputs staging (55b5ed0): sandbox_execute(code, session_id, inputs=[]) copies host files to a guaranteed inputs/<name> before the code runs (documented contract; no separate staging tool for the model to remember).
  • Per-path gating (c364b18): each inputs path is checked against filesystem.read via a list-aware PermissionEngine._resolve (scalar/None resolution provably unchanged — no regression to other gated tools).
  • Shared 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).
  • research_demo 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).
  • Final fix wave (b3eb719): local-backend outputs/ pre-create (+ fixed a jupyter_client 8.x km.cwd no-op so relative I/O lands in the workspace), data-mount point pre-create, stage_inputs None guard, cosmetics.

Verification

  • Full offline suite: 1827 passed, 0 failures.
  • Docker-live suite on a real daemon: 15/15 — inputs staging, outputs copy, cross-session DataFrame persistence, end-to-end analyst flow, and the isolation boundary.
  • Whole-branch review (opus): Ready to merge, no Critical.

Known limitation / follow-ups (non-blocking)

  • macOS data-mount disk-litter: sandbox_data_mounts leave 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 (like inputs), deferred to avoid a large-dataset efficiency tradeoff.
  • Deferred Minors: shared-dir 0o777; outputs re-copy/dedupe per execute; ui_order collisions; a couple of research_demo prompt/docstring cosmetics.

Design spec + plan live under gitignored docs/superpowers/.

https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv

shoom1 added 9 commits July 8, 2026 15:03
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
@shoom1 shoom1 merged commit e89098a into develop Jul 8, 2026
2 checks passed
@shoom1 shoom1 deleted the feature/stateful-executor-toolset branch July 8, 2026 20:57
Sign up for free to 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.

1 participant