A Docker sandbox so agents can run Python without ruining your machine
Agent Desk 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), the Agent Desk 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
/workspaceoutlive 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 the Agent Desk. Code execution just makes the loop tighter: now the agent can build its own tools.