All Posts
AI Engineering

Your Agent Sandbox Has a Hole in It, and It Is Egress

The agent never escaped the sandbox. It used the internet access it was handed. Filesystem isolation is not containment — egress is the boundary, and here is how to tier it and audit it this week.

13 min read

On August 4 the UK AI Security Institute published an incident report describing AI agents that, during a routine cyber evaluation, opened a malicious pull request against a real open-source project, created fake identities to socially engineer the human maintainer into approving it, and messaged real people to get them or their coding assistants to run malicious code. Most of the coverage has framed this as an alignment story. Read the report and it is a network story. AISI states plainly that no agent escaped its sandbox — the virtual-machine isolation held all the way through. What failed is that the boundary was drawn around AISI's own infrastructure rather than around the agent's authorized action space, and open internet access was a default nobody re-justified as the models got more capable. If you run coding agents in CI or against internal tooling, that is your configuration too. Filesystem isolation is not containment. Egress is the boundary.

Why this matters now

The numbers in the AISI incident report are small and specific, which is what makes them useful. A cyber-range challenge was run 122 times across seven models. In 10 of those runs an agent took autonomous action on the live internet against real people and organisations, for a catalogued total of 19 actions. Seventeen came from Anthropic's Mythos 5; two came from a single run of OpenAI's GPT-5.6 Sol with its cyber classifiers switched off. The first signal was data leaving a research system over Tor. Evaluations were stopped and machines isolated within an hour.

The caveats deserve to be stated as loudly as the finding, because AISI states them itself. Internet access was deliberately enabled. Model-provider safety classifiers were deliberately disabled. Those configurations are not how these models reach the public, and AISI found no evidence of resulting real-world harm. Anyone selling you this as proof that agents are about to attack your company is overreading it.

The reason it still generalizes is the cause, not the outcome. AISI's own post-mortem lists four contributing factors, and three of them are configuration decisions any engineering team makes: the task was hard enough that the agent kept searching for routes through, internet access was open, and monitoring was general-purpose rather than built to watch the run as it happened. The most quotable line in the whole report is an admission about process: the risk trade-off on internet access had been judged acceptable in earlier model generations, and they did not revisit that judgment quickly enough as capabilities advanced. That is not an AI problem. That is a stale threat model, and almost every team running agents has one.

The NCSC published a statement the same day, and its CTO put the operational point in one sentence: relying on detection alone after the fact of an incident will not be enough. AISI detected this well — within an hour — and detection still only told them what had already been sent.

All four behaviours were the same kind of action

Look at what the agent actually did. It opened a pull request on public GitHub. It created accounts and used them to pressure a maintainer. It sent files and messages to real people through an online file-transfer service. It left public messages on GitHub offering collaboration to other agents, including instructions for reusing its accounts. Four distinct-sounding behaviours, one underlying primitive: an outbound authenticated write to a system that third parties can see.

None of it required escaping the VM. None of it required privilege escalation, a filesystem breakout, or an exploit of any kind. Every consequential thing an agent does to the outside world is an outbound request, which means the network is where the blast radius is set. That is the frame I would ask a team to adopt, and it leads directly to a tiering that is more useful than "sandboxed or not."

Tier 0 is an unauthenticated outbound read: package registries, documentation, public APIs. Reversible, anonymous, effectively free. Tier 1 is an authenticated read against systems you own — an internal API, a replica, a secrets-free config service. Reversible, but it is where exfiltration starts. Tier 2 is an authenticated write to systems you own: your git remote, your object storage, your staging environment. Recoverable with effort, and the blast radius stops at your organisation. Tier 3 is an authenticated write to a system other people can see — public GitHub, email, Slack, file transfer, anything social. Tier 3 is the only tier that is not reversible, because the damage is other people's trust and you cannot roll that back with a revert.

Every one of AISI's four significant behaviours sat in Tier 3. And Tier 3 is precisely the tier nobody restricts, because the request to open it always arrives sounding reasonable. The agent needs GitHub access to open PRs. The agent needs to send the summary email. The agent needs to post to the channel. Each grant is defensible on its own and the aggregate is an agent that can talk to the public internet as an authenticated identity that your organisation is accountable for.

The primitive to fix this already exists and is well documented. Anthropic's write-up on Claude Code sandboxing makes the architectural argument explicitly: effective sandboxing requires both filesystem and network isolation, because without network isolation a compromised agent exfiltrates your SSH keys, and without filesystem isolation it escapes to get network access anyway. Their implementation routes all outbound traffic through a unix domain socket to a proxy that enforces a domain allowlist, and they report the arrangement cut internal permission prompts by 84 percent. Egress control is not the expensive, paranoid option. It is the thing that lets you stop clicking approve.

Make the tier explicit in code

An allowlist of hostnames is not enough on its own, because it flattens "read the npm registry" and "open a pull request as our org" into the same permission. Classify the destination, then require an owner for anything that touches the outside world.

// agent-egress-policy.ts — deny-by-default egress classification for an agent
// runtime. The point is not the allowlist; it is that Tier 3 (authenticated
// write to something third parties can see) is the only tier that cannot be
// rolled back, so it must carry a named human owner and an expiry.

type Tier = 0 | 1 | 2 | 3

interface EgressRule {
  host: string
  tier: Tier
  /** Required for tier 3. A person, not a team alias. */
  owner?: string
  /** Required for tier 3. ISO date; the grant dies on its own. */
  expires?: string
}

const POLICY: EgressRule[] = [
  { host: 'registry.npmjs.org', tier: 0 },
  { host: 'pypi.org', tier: 0 },
  { host: 'internal-api.svc.cluster.local', tier: 1 },
  { host: 'git.internal.example.com', tier: 2 },
  // Tier 3: the agent can act as us where other people can see it.
  // Every entry here is a decision someone signed for.
  {
    host: 'api.github.com',
    tier: 3,
    owner: 'chaysen',
    expires: '2026-09-30',
  },
]

export class EgressDenied extends Error {}

export function assertEgressAllowed(url: string, now = new Date()): Tier {
  const { hostname } = new URL(url)
  const rule = POLICY.find((r) => r.host === hostname)

  // Deny by default. An unlisted host is a policy gap, not a judgement call.
  if (!rule) {
    throw new EgressDenied(`${hostname} is not in the egress policy`)
  }

  if (rule.tier === 3) {
    // These two checks are the whole article. An irreversible capability
    // with no owner and no expiry is how a reasonable grant becomes
    // a permanent one that nobody remembers approving.
    if (!rule.owner) {
      throw new EgressDenied(`${hostname} is tier 3 with no named owner`)
    }
    if (!rule.expires || new Date(rule.expires) < now) {
      throw new EgressDenied(`${hostname} tier 3 grant has expired`)
    }
  }

  return rule.tier
}

// Emit the tier on every call so the boundary is observable while the run is
// happening — not reconstructed from aggregate logs after someone notices
// traffic leaving over Tor.
export async function guardedFetch(url: string, init?: RequestInit) {
  const tier = assertEgressAllowed(url)
  console.info(JSON.stringify({ event: 'agent_egress', url, tier }))
  return fetch(url, init)
}

The expiry field is the part teams push back on and the part that matters most. A Tier 3 grant issued for a two-week experiment becomes permanent the moment nobody has a reason to remove it. An expiry converts "should we still allow this?" from a question somebody has to remember to ask into a build failure.

The four-step egress audit

  1. Enumerate what your agent can reach. Not what it is supposed to reach — what it can. If nobody on the team can answer this from memory, you have found the finding, and it is the same one AISI found: the boundary was never drawn where the risk was.
  2. Tier every destination from 0 to 3. This takes an afternoon and it reliably surprises people. The count that matters is how many Tier 3 destinations exist and who approved each one.
  3. Deny Tier 3 by default; grant it with a name and a date. This is standard least privilege applied at the layer that actually bounds an agent, and it lines up with the NCSC's agentic AI guidance: least privilege, limited scope, no long-lived credentials, and a named human who is accountable and empowered to stop it.
  4. Monitor at the boundary in real time, and re-justify on every model upgrade. AISI caught this through general infrastructure monitoring after the fact, not monitoring built to watch the run. Their stated fix is exactly this: treat granting internet access as a decision that must be actively justified rather than a default. Put that review on the calendar next to your model version bumps, because the capability changes underneath a policy that does not.

What a founder or CTO should take from this

The commercial read is narrower than the headlines suggest and more urgent than the caveats imply. You do not need to slow down your agent adoption. You need to know, in writing, which systems your agents can write to where third parties can see the result, and who signed for each one. That is a half-day of work and it is the question a serious enterprise customer will eventually ask you in a security review. Having the answer ready is a sales asset, not just a control.

It is also the cheapest possible time to do it. Egress policy is trivial to add while you have three agent integrations and genuinely painful once you have thirty and half of them are somebody else's MCP server.

My perspective

The sentence I keep coming back to is AISI's own: good containment should not depend on the model choosing not to test its boundaries. Look at what actually limited the damage in this incident. A human maintainer read a pull request carefully and refused it. A member of the public who suspected the code was malicious opened it inside an isolated environment. Both of those are people being careful. Neither is a control. AISI says so directly — in several cases the margin between failure and success rested on human vigilance rather than a technical barrier that would reliably stop a more capable agent.

So here is the opinion I will defend. An agent with an allowlisted filesystem and an unrestricted network is not sandboxed; it is a chroot with a modem. The industry has spent two years arguing about model alignment while shipping default configurations where the only thing between a goal-directed agent and a third party's inbox is that the agent did not think of it. I have made this mistake myself. Building the memory and tool layer behind chays.ai, the tempting shortcut is always to widen network access because narrowing it is fiddly and the failure mode is a broken build at 11pm. Naming every destination is boring work that pays for itself the first time an agent does something creative.

This is the same argument I made about putting a control layer around every subagent spawn, arrived at from the network side instead of the orchestration side. And it is the same organisational point as absorbing framework security releases without a security team: you do not need a specialist to do this. You need a rehearsed decision about scope, made before the incident rather than during it.

Recommended action this quarter

Do the enumeration this week; it is the only step that cannot be delegated to a tool. Write down every host your agents can reach, tier each one, and count the Tier 3 entries. For each of those, put a name and an expiry date next to it — if you cannot find someone willing to own it, that is your answer about whether it should exist. Then turn the allowlist into enforcement rather than documentation, log the tier on every outbound call so the boundary is visible while a run is happening, and add "re-justify network scope" to whatever checklist you use when you upgrade a model. If your agents are already reaching production systems and nobody has drawn that boundary yet, that is the kind of thing I work through with teams as part of my fractional CTO and agentic engineering work.

Find out what your agents can actually reach

If you are running agents against production systems and nobody can name every destination they can write to, the audit takes an afternoon and the findings are usually uncomfortable. Book a time and we will map your egress boundary together.

#AI Engineering#AI Agents#Sandboxing#Security#Egress