Constrain, observe, review: agent safety as infrastructure
A self-hosted agent sandbox with exactly three egress paths, the breakout attempt it contained, and the reference architecture enterprise agent platforms should be heading for: every path an agent can take runs through a control point you own.
Get an email when the next article lands
Before starting the SaaS project, which is the subject of the rest of this series, in March 2026 I developed a self-hosted AI assistant platform. The assistant in question was OpenClaw, the open-source personal assistant that has had so much press coverage this year. The part I actually designed was everything around it: the containment.
The principle can be summed up in a single sentence: don’t trust the agent; constrain it and ensure all its actions are observable and reviewable. You can apply this principle at either the infrastructure or process level, and an enterprise-wide rollout requires both. This article focuses on the infrastructure approach since I developed that one first. Much of the content later in Track B of this series is the process version of the same idea.
Is this overkill for a startup? Probably, but at a certain scale it becomes essential, not optional.
Three ways out. Nothing else.
That heading is a verbatim quote from the architecture section of the project’s README, and it was the entire design in four words.
The assistant ran inside a declarative Podman Compose stack (CI ran the full stack under both Podman and Docker on every pull request), and every byte leaving it had exactly one of three routes:
- The API gateway (APISIX) was responsible for all privileged API access. As the README states: “OpenClaw never sees the real API keys; APISIX injects credentials at the gateway and enforces finer-grained permissions than most APIs natively support. This keeps secrets out of the assistant’s context entirely.” In the event that the agent is compromised or is poorly prompted or tricked into carrying out a foolish action, there will be no key in its context to leak.
- An LLM proxy (LiteLLM) carried all model traffic: it held the provider keys, enforced per-key budgets, and logged every prompt and response in full, with traces landing in Langfuse.
- A TLS-intercepting egress proxy (mitmproxy) carried everything else, with two separate filter layers: a domain blocklist deciding what the agent could reach at all, and an upload allowlist deciding where it could send data. Multipart form uploads and raw binary posts were blocked unless the destination was explicitly allowlisted; the defaults were npm, PyPI, and GitHub, the places a coding assistant legitimately sends things.
Note: the third path is really two controls wearing one proxy: reachability and exfiltration are different problems, enforced by different mechanisms. That will become more important later.
The three isolated container networks, were arranged in such a way that “network membership alone grants no access” (as stated in the README, exactly); the containers ran without using root privileges; the configuration was mounted read-only; and key authentication was applied to every gateway route, “enforced in code, not convention”. Any changes to the gateway routes had to go through an approval process in Gitea, so a route change became a version-controlled and approved event rather than a simple edit. At the time I didn’t see it that way, but that had been the first human gate, many months before I designed the merge gates for a real product.
The README stated five principles by name: pragmatic simplicity, defense in depth, declarative and reproducible, modular by design, and validated assumptions. The last one was important: a validation script ran both positive (can reach) and negative (cannot reach) checks, so I wasn’t just unit testing behaviour, I was testing that the architecture itself matched intent…which has become a key part of everything I have built since.
The day the cage earned its keep
In early April, while I was still testing the initial build, I gave the caged agent a poorly written prompt asking it to work out a fix for an issue with one of the services I self-host. This prompt should have said to just evaluate and propose a fix, but the agent decided it meant to figure out and apply one.
My self-hosting setup has reasonable complexity; separate application and data hosts, Proxmox as my virtualisation platform and a combination of Linux VMs, LXCs and Containers (on Podman and Docker) running on top. The agent had read-only access to all logs and statuses, except where I stored secrets. So the agent shouldn’t have been able to apply any fixes…but that didn’t stop it trying. I sat and watched it probe: try a path, hit a control, reframe, try another way out, hit the next control. It was systematically looking for a way around the sandbox, and it never found one.
The agent wasn’t malicious, it was merely attempting to carry out my poorly defined task. It had an objective, the controls were seen as obstacles, and it dealt with them in the way it deals with any other obstacle…by finding a route around them. This behaviour lies behind the reports of frontier models trying to escape during safety tests, and behind the August account of the Australian man whose agent managed to hack into his gym’s class-booking system in order to move him up the waitlist. That agent was OpenClaw: the same assistant I had in the cage. I watched a smaller, earlier version of it in action, pointed at my own walls. And the lesson is precisely the one that this article is about: it was the constraint layer that kept the agent in check, not the model’s own judgment. The model never chose to stay within the sandbox; the cage I set up decided that.
If your argument for agent safety is based on the model deciding not to do the wrong thing, then you don’t actually have a safety argument…at best you only have a well-behaved model for today.
What “a way out” actually means
For a single assistant working with one operator, three paths was the appropriate simplification; however, that simplification ceased to be appropriate the moment I began drawing the same diagram for platforms which have many agents, since the “way out” option brings together channels that have different risk profiles, different owners, and different controls:
- Model traffic. All your LLM traffic and token spend to models from the major providers, or any of the various platforms hosting frontier or open-weight models.
- Tool and API traffic. The agent acting on systems, over MCP as well as plain APIs and CLIs. This was the March build’s API gateway path, credentials injected at the boundary.
- Web egress. Browsing, fetching, downloading. The March build already knew this path needed two controls, not one: reachability (the blocklist) and exfiltration (the upload allowlist) are separate problems.
- The code host. This one is easy to miss because it does not look like egress; it looks like work. But agents push code to it, its hooks trigger runners that execute inside your network, and vendors will now run their own coding agents inside the host itself. It is a channel, and a busy one.
- Inbound. The direction the March build never had to face, because a personal assistant is a client. A platform agent is also a server: other systems, and other agents, will call into it. The moment that happens, you have ingress to govern too.
Name the paths and make the controls specific. Each one gets its own gateway, its own policy, and its own logs.
The reference architecture
The diagram that I use nowadays when somebody asks me what an enterprise agent platform ought to look like is intentionally generalised: it focuses on capabilities rather than on specific vendors. It is the ‘here are the kinds of things you need to protect’ kind of outline. Although you might end up combining certain capabilities into a single product depending on the products or solutions you decide to implement, you will still need those capabilities.
Walking it briefly:
- Agent runtimes come in at least three forms that you manage: local agents, which are directed by the user and run on developers’ machines; headless agents that run without supervision; and self-hosted runners, which carry out workflows triggered by the hooks of the code host. There is a fourth type that you do not control: the coding agent provided by the vendor and housed within your SaaS code host. It is shown with a dashed line because it is a runtime within your environment running someone else’s platform, and the only controls are contractual (unless you choose to replace it with a self-hosted platform).
- Every path terminates at a control point you own. Model traffic passes an LLM gateway; tool calls pass an MCP gateway; agent-issued API calls pass an API gateway that injects credentials; web traffic passes a proxy. The gateways, not the agents, hold the keys, the budgets, and the logs.
- Externally hosted models sit outside the boundary, and the LLM gateway is the only line that crosses to them. Internally hosted models live beside the gateway, but they still sit behind it: the gateway is where policy and observability live, regardless of where the model runs.
- The web proxy, the package proxy, and the inbound MCP gateway are mounted on the boundary itself. They are the wall’s doors: two govern what goes out (web traffic and dependency pulls, the latter for your own runtimes and the vendor’s hosted coding agent alike), one governs what the outside world may ask of your agents and services.
- Observability hangs off the choke points, not off the agents. You log where the traffic must pass, because agents can be spawned, modified, and misconfigured; the gateway cannot be routed around.
This map isn’t of the enterprise rollout that I’m involved with; it merely represents the general form that I think agent platforms should eventually take, and my straightforward assessment is that the majority have not achieved this: usually the model path is given a gateway at an early stage since that is where the invoices originate, and the other paths are then treated like ordinary networking, ordinary networking assuming that the entity within the network is not being actively creative.
Watch-points, path by path
What you should be considering at each control point:
- LLM gateway: per-key budgets, model allowlists, full prompt/response logging and your own guardrails, such as DLP policies and anything else you need to protect. This layer has matured fastest; LiteLLM, the exact component the March build used, now sits in a recognised category alongside the gateway offerings from Kong, Cloudflare, Portkey and others.
- Tool and API gateways: inject credentials at the boundary so the agent never holds a real key, and enforce permissions finer than the upstream API natively offers. Intent-based permissions, with ephemeral keys; build a bespoke set of permissions for the agent’s use case, limit them to exactly what you intend for them to do.
- Web egress: separate reachability from exfiltration, and filter uploads by destination. In April 2026, Lasso Security demonstrated exfiltrating credentials from a vendor’s agent sandbox using only allowlisted binaries, with npm packages and GitHub repositories as the channel; their core finding was that “sandboxes cannot evaluate intent”. Read that against the March build’s upload allowlist, whose defaults were npm, PyPI, and GitHub: the same channels. The cage held against a naive breakout; the category’s residual gap is exfiltration through the channels you authorised.
- Package proxy/firewall: often forgotten, as a capability. Up to now, we often relied on developers’ judgement about the safety of an open source package, but rule-based controls are increasingly important. Recent supply chain attacks have already strongly indicated this isn’t optional, but agents increase the likelihood of this risk significantly. Control the licenses, origins, commit history of contributors and known CVEs of your products.
- The code host: treat hooks, runners, and hosted coding agents as what they are, execution inside your boundary. Branch protection, hook approval, and runner isolation are security controls now, not developer conveniences.
- Inbound MCP traffic: it’s not necessary for everyone, but if you do have requirements regarding inbound agents then an inbound MCP gateway will be essential.
The market arrived, mostly
Solutions for parts of this already exist. Anthropic released its work on Claude Code sandboxing in October 2025, which was five months before my first commit; this involved the use of bubblewrap and Seatbelt to achieve filesystem and network isolation, and it open-sourced the runtime while also inviting others to consider adopting the technology for their own agents. OpenAI’s Codex, on the other hand, enabled OS-level sandboxing by default, the documentation clearly stating: “By default, the agent runs with network access turned off.” And in the same week in March 2026 when I committed the blocklist for the egress proxy, NVIDIA announced OpenShell, an open-source secure runtime for autonomous agents, whose key positioning line is the thesis of this article coming from a vendor: “Instead of relying on behavioral prompts, OpenShell enforces constraints on the environment the agent runs in.” Around them, sandboxed code execution became in its own right a distinct product category, including solutions based on Firecracker microVMs and container-based platforms. In fact, while I was researching this article I discovered that Matt Pocock had already started Sandcastle, a project concerned with orchestrating sandboxed coding agents, two days after my first commit.
So on harness-level sandboxing I was mid-wave, not ahead of it. Where the March build genuinely was ahead, and where I think the gap still is: the full egress-governance shape, an LLM proxy, a credential-injecting API gateway, and a TLS-intercepting egress proxy with exfiltration filtering, composed as one self-hosted stack. The vendors solved the box the agent runs in. The reference architecture above is my argument that the interesting problem is the doors, and that a composed, off-the-shelf answer for the doors still does not really exist.
What happened to the cage
I used the platform as my daily driver for about six weeks. Then I noticed I was doing less and less with it, and leaning further into Claude Code, and eventually I was honest with myself about why: my work is SDLC-shaped, and the product was shaped as a general personal assistant. The stack itself is dormant, not dead; it still has a use as a self-hosted option, and I may yet come back to it. Major AI vendors have their own solutions, but as model/vendor mobility becomes more of a consideration, those solutions become less viable, and we need security solutions we fully control.
The philosophy did not disappear along with it; when my work was transferred to a SaaS product geared towards production, the cage was reinterpreted as a combination of process and targeted infrastructure, and you can match up each element with its corresponding part:
- Credential injection at the gateway became the credential-isolation rule: agents never hold the actual secrets, and CI holds the deploy token.
- The Gitea route-approval workflow became the human merge gates on anything security-relevant or sensitive.
- Full prompt and response logging was made attributable to agent identity and became an audit trail that could be reviewed.
- In later articles the negative checks employed by the validation script continue to be referred to again and again: you have to demonstrate the failure path as well as the successful one, since ‘it works’ and ‘it cannot go wrong in the way I claimed’ are different statements.
Infrastructure bounds what an agent can do. Process bounds what it may do. You can start with either layer; the March project started with the one you can build alone in a month of evenings. But the stance is the same on both layers, and it is not cynicism about the models. Constraining the agent is what made it safe to hand the agent more autonomy. The README’s hero line was “Secure, transparent, yours”, and I still think that is the right order: lock it down first, light it up second, and then it can be trusted with real work.


