Agentz #docker #sandbox #tool-use

A Docker sandbox so agents can run Python without ruining your machine

Agentz agents can now write and run Python. Not "generate a code block and hope you paste it somewhere" — actually execute it, get stdout back, and keep iterating. The obvious problem with giving an LLM an exec() is everything about it, so the execution happens inside a Docker container.

The shape of it

The design is one long-lived container, not a container per execution. On startup (or via a setup flow the app can drive itself), Agentz builds an image from python:3.11-slim, adds a non-root executor user and a workspace, and runs it like this:

docker run -d --rm --name strands-python-sandbox -p 8765:8765 \
  -e AGENT_CALLBACK_URL=http://host.docker.internal:8766 \
  --memory=512m --cpus=1 strands-python-sandbox

Inside the container a small Flask server listens on port 8765. When an agent calls the run_code tool, the Electron main process POSTs {code, input, dependencies, timeout} to /execute. The server wraps the user code — injecting an INPUT variable parsed from JSON and an agent API object — writes it to a temp file in /workspace, runs it with subprocess.run(capture_output=True, timeout=...), deletes the temp file in a finally, and returns {success, stdout, stderr, exit_code, duration_ms}.

Per-execution containers would be cleaner isolation, but a warm container answers in milliseconds and — more importantly — keeps its pip cache. Dependencies are installed lazily on first use and remembered for the container's lifetime, with a 120-second per-package timeout so one misbehaving install can't wedge an execution forever. The base image already ships the usual suspects: pandas, numpy, matplotlib, scipy, requests, BeautifulSoup, Pillow.

Timeouts are layered like a paranoid onion: an AbortController on the JavaScript side (default 300s), a hard 10-minute cap on the Flask side, and the subprocess timeout underneath. Memory is capped at 512MB and CPU at one core.

The interesting part: the callback API

A sandbox that can only compute is much less useful than one that can see what the agent knows. So the container gets a phone line back to the host: Electron runs a callback server on port 8766, and the injected agent object inside the sandbox turns method calls into HTTP POSTs to host.docker.internal.

From inside sandboxed Python, agent code can call agent.search_memory(), agent.get_facts(), agent.query_graph(), agent.get_documents(), agent.store_result(), and agent.log(). That means an agent can write a script that pulls its own knowledge graph, crunches it with pandas, and stores the result back — which is the actual point. Code execution isn't a party trick; it's how an agent does the things LLMs are bad at (arithmetic, aggregation, anything with more than twenty rows) against its own data.

Alongside run_code there's a small code lifecycle: save_code puts a script in the agent's code library with a name, dependency list, and input/output schemas; list_code and update_code manage it; and create_code_job schedules a saved script to run on a cron-ish schedule, with every execution logged — stdout, stderr, exit code, duration — to the agent's database.

Getting Docker onto the machine

This is a desktop app, so "requires Docker" can't mean "go read a wiki". On macOS the app can bootstrap the whole chain itself: install Homebrew if missing, then the Docker CLI, then Colima as the VM runtime (allocated 2 CPUs and 4GB), then build the image, then start the container — streaming progress events to the UI the whole way. It's the least glamorous code in the feature and it took a disproportionate share of the time.

Honest notes on the word "sandbox"

The container protects the host filesystem: nothing is volume-mounted, the code runs as a non-root user, and resources are capped. But it's worth being precise about what this sandbox is not:

  • There's no network isolation. Sandboxed code has full internet access, deliberately — agents fetch data. If your threat model includes hostile code exfiltrating things, this isn't your sandbox.
  • State persists between runs. One container serves all executions, so installed packages and stray files in /workspace outlive the run that created them. Only the temp script itself is cleaned up.
  • The code wrapper is built with string interpolation. The callback URL and agent ID are f-string'd straight into generated Python source. Fine when the app controls both values; a footgun to remember if that ever changes.

The threat model here is accidents, not adversaries: an LLM that enthusiastically writes shutil.rmtree against the wrong path, an infinite loop, a package that eats all your RAM. For a single-user desktop app whose code author is your own agent, containing accidents is the job, and a warm container with hard caps does it while keeping executions fast.

The pattern that's emerging — give the agent a tool, give the tool's output somewhere durable to live, let the agent schedule it — keeps repeating across Agentz. Code execution just makes the loop tighter: now the agent can build its own tools.