Put an AI agent in one process and day one gives you two walls
First post in the agent sandbox series. Ask an AI to work for two hours straight and the gap between "it runs" and "it ships" turns out to be one thing: isolation. A small service, deliberately killed by the system, makes the problem visible. Jargon is explained as it appears — no Kubernetes background needed.
Picture an ordinary request: have an AI pull data from thirty websites, clean it up, and turn it into a report. This is not a chat — one question, one answer, done. It runs for two hours straight, downloads files, eats memory, and executes code the AI writes for itself along the way.
Getting that working on your own laptop takes an afternoon. Turning it into a service several people use at once is where the difficulty starts.
This opens a six-part series on building an agent sandbox from scratch. Each post fixes one problem the previous one exposed, and the code lives in agent-sandbox-oss so you can run it yourself. Terms get explained where they first appear; no Kubernetes background required.
One thing about the code: it is a teaching skeleton. Real crawling is replaced by “sleep for a bit”, and the cluster runs on a laptop. It is not meant to ship. Its value is that every crash in it is a crash real projects have.
The obvious way to write it, and the assumption hiding inside
The natural implementation: a web endpoint takes a task, starts working in the background, and keeps the progress in memory.
IN_MEMORY_DB = {} # task progress lives here
async def run_data_research_task(task_id, target_pages, simulate_leak=False):
for i in range(target_pages):
await asyncio.sleep(0.5) # pretending to fetch a page
IN_MEMORY_DB[task_id]["pages_crawled"] += 1 # note it down: one more page
Deployment is equally natural: one program, one container, a 128MB memory ceiling.
A container is best thought of as a disposable little server: the program and everything it needs, packed together, ready to start or destroy at any moment. The memory ceiling is the line drawn around it — cross it and the system shuts it down.
Nothing here is wrong. It runs, returns task IDs, reports progress. What it does is quietly assume that every task behaves.
In the real world it does not. Among thirty websites one page is always enormous, and AI-generated code eventually writes an infinite loop. The next two sections are what happens when that assumption breaks.
Wall one: one task goes down, everybody goes with it
The code has a switch that simulates hitting a giant page — every fetched page appends 20MB to memory. On the seventh page memory crosses the 128MB line and the system shuts the whole program down.
In Kubernetes (a system for running containers, K8s from here on) you can watch it happen:
kubectl get pod -l app=agent -w
# NAME READY STATUS RESTARTS
# agent-monolith-xxx 1/1 Running 0
# agent-monolith-xxx 0/1 OOMKilled 0 ← over the memory line, killed
# agent-monolith-xxx 1/1 Running 1 ← restarted seconds later, looks fine
OOMKilled means “used too much memory, terminated”. Watching it restart on its own, the natural reaction is relief: the system healed itself.
Then you ask about a different task that was running at the same time — the well-behaved one, quietly fetching small pages:
curl localhost:8000/status/<some-other-task-id>
# {"detail":"Task not found. Data lost due to Pod crash!"}
It is gone too. An hour and forty minutes of progress, back to zero.
What got killed was not “the task that misbehaved” — it was the program holding all of the tasks. An analogy: ten people share a flat with one kitchen, one of them burns a pan and sets off the sprinklers. The sprinklers soak the whole flat. Everybody’s dinner is ruined.
There is a worse detail. Because progress only existed in memory, after the restart you cannot even find out which page it had reached. K8s restarts protect whether the service can be reached, not how far the work got. It reopens the door; it does not give back your two hours.
I call it the monolith illusion: RESTARTS: 1 looks like a harmless number, and it costs every task that was running at the time.
Wall two: an agent executes code the AI just wrote
This is where agents differ most from ordinary backend services. An ordinary service runs code you wrote, reviewed and tested. An agent runs code a model produced seconds ago.
And whatever that program can reach is fully visible from inside the container:
cat /var/run/secrets/kubernetes.io/serviceaccount/token
That command reads the access credential the platform mounted for the program — effectively a key. One program means one shared set of files, one shared set of environment variables, one shared key. So code generated for task A can read the data task B collected, and the key sitting next to it.
No amount of care while coding fixes this. Inside one process there is no boundary — the operating system simply does not offer “parts of the same process, protected from each other”.
What this step settles
The naive version is worth running by hand, because it disproves two very natural ideas at once:
- “just give it more memory” — a 1GB ceiling only postpones the crash by a few dozen pages; the collateral damage is identical;
- “just catch the error” — the kill comes from the operating system kernel, and the program never gets a chance to say a word.
Which leaves one road: give every task a container of its own. Sounds obvious enough.
The bill arrives in the next post: creating a container on demand leaves the user waiting 45 to 90 seconds.
In one line: inside one process there is no boundary. Isolation is not a performance optimisation, it is the precondition for offering this kind of service at all.
Code: agent-sandbox-oss/lab1 — a local cluster and one command is all it takes.
- Kubernetes
- AI Agent
- Cloud Native
- Architecture
- Chaos Engineering