This is a follow-up to my first article about my semi-autonomous SRE agent, which covered the agent itself: investigation, scoped tools, human approval. This one covers the question I kept deferring: where does the agent's code actually run, and what happens when every other guardrail fails?
Everything I have written about this agent so far has been policy. Scoped tools instead of shell access. Validation hooks on every proposed change. Human approval before anything merges. Retrieved runbooks that inform but never command.
Policy is what the system is supposed to allow. It is written in code I wrote, reviewed by me, tested by me, which means it fails the way all code fails. A tool server with a path traversal bug. An allowlist with one regex that matches more than I intended. A validation hook that someone (me) disabled during a debugging session and forgot to re-enable.
The uncomfortable question is not "what did I allow?" It is "what can the agent do when my policy layer has a bug?"
For my agent, the answer used to be: whatever my laptop can do. The tool server ran as my user, on my machine, with my SSH keys sitting in ~/.ssh and my cloud credentials in ~/.oci. Every guardrail I bragged about in the first article was a doorman standing in front of an unlocked door.
So I moved the whole thing into a padded room. Here is how the Docker sandbox works, what it caught, and where containers stop being enough.
The distinction that reorganized my thinking: guardrails are policy, sandboxes are physics.
A command allowlist says the agent may not run curl. A container with no network says the agent cannot run curl in any way that matters. The first depends on my string matching being smarter than a model that has read every shell trick on the internet. The second does not care how creative the model gets.
You want both layers, because they fail differently. Policy fails when it has bugs. Physics fails only when the isolation mechanism itself is broken. That is a much higher bar, maintained by people who are much better at this than I am.
The rule I settled on: policy decides what the agent should do, the sandbox bounds what a total policy failure costs.
The agent has two halves, and they get very different treatment. The reasoning loop, the part that talks to the LLM API, runs outside the sandbox. It handles credentials for the model provider and nothing else. The tool server, the part that actually executes things, runs inside a container that is built to be disappointing.
The container is disappointing on purpose:
tmpfs mount at /scratch, capped at 256 MB, wiped when the container dies.$PATH beyond what the tools need.--cap-drop=ALL. The container cannot change file ownership, bind low ports, or touch anything kernel-adjacent./var/run/docker.sock into an agent's container is handing it the keys to the host with extra steps.The compose definition is short enough to read in one sitting, which is itself a feature:
services:
agent-tools:
image: sre-agent-tools:latest
read_only: true
user: "10001:10001"
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
tmpfs:
- /scratch:size=256m
networks: [agent-internal]
mem_limit: 512m
pids_limit: 128
egress-proxy:
image: sre-agent-proxy:latest
networks: [agent-internal, external]
networks:
agent-internal:
internal: true
external:
Nothing in this file is exotic. That is the point. The security posture of the sandbox should be auditable by anyone on the team in five minutes, because a sandbox nobody understands is a sandbox nobody notices breaking.
The design decision that mattered most was the one about secrets: the sandbox holds no credentials at all.
The tool server needs to query Prometheus, read logs, pull deploy history, and open pull requests. My first instinct was to inject scoped API tokens into the container as environment variables. Then I remembered my own warning from the first article, that logs are untrusted input, and thought about what a prompt-injected agent would do with env and a working network connection.
So the credentials moved out. The container's only network path leads to an egress proxy running outside the sandbox. The proxy holds the tokens, and it exposes named routes, not the open internet:
allowed routes:
GET /prometheus/query → attaches read-only Prometheus token
GET /logs/{service} → attaches log-viewer token
GET /deploys/{service} → attaches read-only CI token
POST /github/pull-request → attaches PR-scoped token,
branch must match remediation/*
everything else → 403, logged, alerts on repetition
The agent inside the sandbox can ask the proxy for the things it is allowed to ask for. It cannot exfiltrate a token it never possessed, and it cannot reach an attacker's server that the proxy has never heard of. Even the pull request route enforces the same boundary from the first article (remediation branches only), but now the enforcement lives outside the blast radius, in a process the model's output never executes inside.
Each investigation gets a fresh container. When the incident closes, or when the agent hits its time budget, the container is destroyed, scratch space and all.
This started as hygiene and turned out to be a real security property. Anything an attacker manages to plant inside the sandbox (a modified script, a poisoned cache, a scheduled task) has a lifespan measured in minutes. Persistence, the thing real attackers care about most, requires escaping the container rather than just compromising it.
It also killed an entire class of debugging pain. "Works in this container but not that one" disappears when there is no long-lived container to drift.
I re-ran the prompt injection scenario from my earlier testing, but this time with teeth. I planted a log line in a failing service that read like instructions: fetch a script from an external URL and execute it, phrased the way these attacks actually get phrased: buried in a stack trace, addressed to the assistant, urgent in tone.
The layered defenses worked in exactly the order they were designed to:
Layer four is the one that let me sleep. Layers one through three are policy, and I had personally introduced bugs into all of them at some point during development. Layer four is physics.
The 403 also fired an alert, which is the correct final behavior: a blocked exfiltration attempt is not just a non-event, it is a signal that something upstream is compromised.
Honesty section, as usual.
Containers share the host kernel. Docker isolation is namespaces and cgroups, not a hypervisor. A kernel exploit inside the sandbox is an escape. For my proof-of-concept threat model (a confused or prompt-injected agent), that is acceptable. For an agent processing genuinely adversarial input at scale, I would reach for gVisor or Firecracker microVMs, which put a real boundary between the workload and the kernel.
The proxy is now the crown jewels. I concentrated every credential into one process, which makes that process the most attractive target in the system. It gets the strictest review, the most logging, and the least code. It is boring by mandate.
The reasoning loop is still outside. The LLM API key lives in the unsandboxed half. If the reasoning process itself has a vulnerability, the sandbox does nothing for me. My mitigation so far is that the reasoning loop executes no model output; it only routes tool calls into the sandbox. But "so far" is doing some work in that sentence.
Sandboxes rot. Every convenience mount, every "temporary" extra route on the proxy, every debugging port left open erodes the boundary. I now diff the compose file and proxy config in CI against a known-good baseline, because the biggest threat to the sandbox is me, three weeks from now, in a hurry.
If you are building an AI agent that executes anything (an SRE agent, a coding agent, anything with tools), the question to ask is not "what have I allowed it to do?" but "what happens when my allow-logic has a bug?"
Guardrails are policy. Policy is code, and code has bugs. A Docker sandbox with a read-only filesystem, no capabilities, no credentials, and no network except a credential-holding proxy turns a policy bug from a potential incident into a logged 403.
The agent from my first article decides what to do. The sandbox from this one bounds what it costs when that decision, or the policy around it, is wrong.
Build the padded room before you need it. The first blocked exfiltration attempt in your proxy logs will not feel like overengineering.
If you have taken agent sandboxing further than containers (gVisor, Firecracker, or full VM isolation per investigation), I would love to hear what the operational overhead actually looks like.