Skip to content

feat(tools): RunPython — isolated Python execution with code mode + agent-in-agent - #62

Open
bpb02 wants to merge 3 commits into
mainfrom
python-tool
Open

feat(tools): RunPython — isolated Python execution with code mode + agent-in-agent#62
bpb02 wants to merge 3 commits into
mainfrom
python-tool

Conversation

@bpb02

@bpb02 bpb02 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds RunPython, a Timbal-native tool that executes LLM-authored Python in an isolated subprocess (uv PEP 723, with venv+pip fallback). No MCP — everything is a Timbal tool.

  • Code mode: the executed script can call exposed Timbal tools through a Unix-socket RPC bridge. Top-level await is supported, so async agent code (await inner(...).collect()) just works.
  • Dependencies: explicit deps + auto-detection from imports, with base-name dedupe so a pinned spec (e.g. timbal @ file://...) is not shadowed by a bare auto-detected import.
  • Hardening (E2B / Pydantic-Monty inspired):
    • Secrets stripped from the child env by default; opt-in env_passthrough allowlist.
    • Head+tail output truncation (max_output_chars) to protect the context window.
    • Process-group kill on timeout (no orphaned uv/python children).
    • Code-size and return_value-size limits.
  • Headline use case (validated end-to-end): install timbal inside the sandbox and run an Agent within an Agent with a real Anthropic call.

Tests

  • Ported competitor cases (pydantic monty, mcp-run-python).
  • Hardening tests (secret isolation, env passthrough, truncation, size limits, timeout kill).
  • Top-level await tests.
  • Real integration tests (-m integration): inner agent runs; outer agent delegates to inner agent.
  • Manual runner: python/tests/tools/validate_run_python_agent.py.

Test plan

  • uv run python -m pytest python/tests/tools/test_run_python.py → 62 passed (incl. 2 real integration)
  • uv run ruff check on all touched files → clean
  • Full suite uv run python -m pytest python/tests → 2206 passed (server-test failures are a local .env TIMBAL_LOG_LEVEL issue via load_dotenv(), unrelated to this PR)
  • Reviewer: run integration tests with ANTHROPIC_API_KEY set: pytest python/tests/tools/test_run_python.py -m integration -o addopts=""

Next steps (planned)

Three follow-up workstreams, designed to compose (the tool-result journal IS the cache IS the artifact store):

1. Token-context optimization

  • Structured preview of return_value for large objects (shape/columns/head) instead of dumping the full value into the LLM context.
  • Compact output envelope: {return_value, stdout(truncated), artifacts:[{id, kind, bytes, preview}]}.
  • Pass-by-reference for large inputs (read_artifact(id)), keeping intermediates inside the sandbox.

2. Code & artifact persistence ("save to view later")

  • Ephemeral (run-scoped, TTL) and persistent (configurable dir / platform blob) tiers.
  • Persist script.py, stdout, return_value, and files the script produced (diff the run dir).
  • Content-addressing (hash of code+deps) → dedup, a reusable snippet library, and result memoization (same code+deps+inputs → cached result → 0 tokens / 0 latency).
  • References surfaced in OutputEvent.metadata; companion get_run_artifact(id) tool for on-demand fetch.
  • Implications: TTL/retention + redaction for secrets/PII, storage GC, cache-staleness policy.

3. Human-in-the-loop inside the script — replay design (chosen)

Align with Timbal's existing HITL model (suspend() raises Suspend, the run unwinds + persists, and on resume the handler re-executes from the top with memoized resume values keyed by a stable suspension_id).

  • The RPC bridge keeps a deterministic call journal keyed by (tool_name, args, ordinal).
  • A HITL/approval tool: if a decision exists in run_context._resume_values → return it (replay); otherwise the parent raises Suspend(...), RunPython unwinds, kills the subprocess, and the normal PauseRequired machinery bubbles up so the outer agent emits an InteractionEvent/ApprovalEvent and persists for resume.
  • On resume, RunPython re-runs the script from the top: non-HITL tool calls return journaled results (side effects don't re-fire), the HITL call returns the human's decision, and the script continues to the next suspension or to completion.
  • Implication / constraint: the script must be deterministic & idempotent up to each suspension (same control flow, same tool-call order/args on replay) — same caveat as LangGraph's interrupt(). suspend() must come before non-idempotent side effects.

Made with Cursor

bpb02 and others added 3 commits June 25, 2026 16:05
Adds RunPython, a Timbal-native tool that runs LLM-authored Python in an
isolated subprocess (uv PEP 723 with venv+pip fallback), with:

- Code mode: the script can call exposed Timbal tools via a Unix-socket RPC
  bridge, plus top-level `await` support so async agent code just works.
- Dependency handling: explicit deps + auto-detection from imports, with
  base-name dedupe so a pinned spec (e.g. `timbal @ file://...`) is not
  shadowed by a bare auto-detected import.
- Hardening (E2B/Monty-inspired): secrets stripped from the child env by
  default with an `env_passthrough` allowlist, head+tail output truncation,
  process-group kill on timeout, and code/return-value size limits.
- Headline use case validated end-to-end: install timbal inside the sandbox
  and run an Agent within an Agent (real Anthropic call).

Includes ported competitor test cases (pydantic monty / mcp-run-python),
hardening and async tests, real integration tests, and a manual validation
runner. Registers RunPython in the tools package and ignores RunPython
execution artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
…r module

Move child-process logic from an embedded _CHILD_PREAMBLE string into
_run_python_runner.py (stdlib-only, lintable, unit-testable). The parent
now writes user_code.py and passes config via env (TIMBAL_USER_CODE_PATH,
TIMBAL_EXPOSED_TOOLS) instead of interpolating code into a generated script.

- uv executor uses --with per dependency (no PEP 723 header codegen)
- venv executor runs the static runner after pip install
- Add 8 unit tests for the runner module in isolation

Co-authored-by: Cursor <cursoragent@cursor.com>
Use a localhost TCP server when asyncio.start_unix_server is unavailable
(Windows CI). The child runner connects via tcp://host:port or unix path.
Add a regression test that simulates the Windows transport.

Co-authored-by: Cursor <cursoragent@cursor.com>
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