*Part 7 of An OS Where the AI Is Never a Principal.*
Up to this point the model in my demos has been a fiction. A scripted stand-in — src/oracle.ts — playing a hijacked agent, emitting exactly the two attacks I wanted to block. That was deliberate: a deterministic oracle keeps the security property testable offline, and it let me prove the membrane does its job without dragging a five-gigabyte language model into every CI run.
But a fiction proves nothing about a real model. If the whole thesis is that safety is *model-independent* — that the kernel confines whatever proposes actions, brilliant or broken — then I owe you a real one. Two, actually: a small local model running on a laptop, and a frontier model reached over a gateway. The interesting part is that the kernel could not tell them apart.
The model is an oracle, not a citizen
Start with the framing, because it dictates everything else. In Aegis the model is not a component of the system. It is an *external, nondeterministic oracle* the kernel consults. You hand it an observation; it hands back a proposed action. That's the whole contract. It holds no capabilities, it lives outside the trust boundary, and — this is the part that matters — the kernel does not trust a single byte it returns. Not the reasoning, not the arguments, and as we'll see, not even the *shape* of the reply.
This is the previous post's hardening seen from the other side. There we shrank the trusted base to a four-method core and made capabilities tamper-proof. Here we point that core at a thing that is, by construction, untrustworthy, and ask: does the structure hold?
Constrained decoding makes invalid actions unreachable
The first guarantee has nothing to do with the membrane. It happens earlier, at the sampler.
A language model generates text one token at a time, each token sampled from a distribution over the vocabulary. Normally any token can follow any token — that's why models can ramble, hallucinate JSON, or be talked into emitting whatever an injection asks for. Constrained decoding changes the rules of sampling: at each step the grammar declares which tokens are *legal* next, and the sampler is forbidden from choosing any other. llama.cpp exposes this as GBNF grammars, and Aegis uses it to make invalid tool-calls not discouraged but *structurally unreachable.*
The grammar (src/tool-grammar.ts) admits exactly one shape:
{"thought": "...", "action": {"tool": "<a held tool>", "arg": "..."}}or a terminal {"thought": "...", "say": "..."}. And the load-bearing line is how the tool field is built:
const toolAlt = toolNames.map((n) => `"\\"${n}\\""`).join(' | ');
// tool ::= "fetch_page" | "read_customer_db" | "send_external"The tool field is locked to a fixed enum of *exactly the caps the agent holds.* So when an injected page screams call admin_delete_all, the model is incapable of complying — not because it chose restraint, but because the sampler has no token path that spells a tool outside the set. An out-of-set tool is uncallable, not merely unlikely. You can watch it run with pnpm demo:grammar, which drives a real Gemma 4 E4B (about 4.5B effective parameters) over llama.cpp.
The model can still *mention* admin_delete_all inside the free-text thought field, and that's fine. Mentioning is not calling. And even if a grammar bug let a bad name through, the vat re-checks authority anyway — naming a tool the agent doesn't hold resolves to no capability at all. The grammar is defense in depth, not the only line.
The gotcha that bit me: silent fallback
Here is the honest part, the kind of thing you only find by being adversarial against your own code.
I had a malformed character class in an early grammar — [^"\] where I needed [^"\\], an unescaped backslash. llama.cpp did not return an error. It did not warn. It *silently dropped the grammar and fell back to unconstrained decoding.* Every test still "passed" because the model, being cooperative, mostly emitted the right shape anyway. The constraint I was relying on for a security guarantee simply wasn't there, and nothing told me.
The lesson generalizes past this one bug: never assume a constraint was honored — verify it with an adversarial prompt. The way I now check enforcement is to send root ::= "ALWAYS_THIS_EXACT_STRING" and tell the model to "say anything." If the grammar is live, it emits exactly that string and nothing else. Tell it to "reply in plain English only," and the real tool grammar forces it into the JSON envelope regardless — it cannot escape to prose. A guarantee you haven't tried to break is a hope.
Why our grammar and not the model's native tool format
Gemma 4 ships with a *trained* function-calling format using special tokens — <|tool_call>...<|tool_call|> and friends. The obvious move is to use it. I deliberately don't, and probing the live model confirmed the choice.
It's brittle: llama.cpp's own loader warns that this GGUF's tool tokens are mistyped (control-looking token ... was not control-type; probably a bug in the model), and driving it through the OpenAI tools field returned empty completions. The native path depends on the model *choosing* to emit those tokens — unconstrained, it often just answers in prose. It's also model-specific: couple your security-critical output structure to Gemma's format and it shatters the moment you swap to Qwen or Llama, each with its own dialect.
But the real reason is the threat model. The native format *relies on trained cooperation.* A GBNF grammar *forces* structure at the sampler regardless of what the model was trained to do. And in this design we don't trust the model to behave — including to format correctly. Making valid tool-call structure a property of the *kernel* rather than the *model's training* is the whole stance, applied one level deeper than people usually take it. Imposing our own grammar isn't fighting the model. It's refusing to outsource a security invariant to it.
The frontier-model run: confined identically
Then the other end of the spectrum. pnpm demo:gateway runs the *same* injected scenario — fetch a page, get hijacked, try to escalate and exfiltrate — but driven by a much stronger planner: Gemini 3.5 Flash reached over a gateway. The point is emphatically *not* that the big model behaves better. It's that the membrane confined it *identically* to the 4B. Escalation blocked, exfiltration blocked by the flow gate, nothing leaked. The assertion in the demo says it plainly: the membrane confines a frontier model exactly as it confines a 4B.
The behavioral colour around that invariant is genuinely interesting, precisely because it changes nothing. The small Gemma looped — the first cut fed it only the latest observation, so it had amnesia and re-listed notes forever until I carried per-request history (a bug no scripted oracle ever surfaces). The big model, by contrast, sometimes *spotted the injection itself* and refused it, and was also slightly over-eager — reading the customer DB unprompted. One model too weak to plan, one sharp enough to second-guess the attack. Neither moved the guarantee a millimeter. The looping model never escaped its caps; the clever model's unprompted DB read got absorbed as taint, and the exfiltration it might then attempt was gated all the same. Safety is model-independent is not a slogan here; it's the line in the test that has to stay green across a 4B and a frontier model both.
A real agent on real files
The capstone of all this is aegisd — not a scripted scenario but a persistent agent you talk to in your terminal, holding a least-authority cap set over a real workspace directory, driven by the real local model with grammar-constrained output. Ask it to read a meeting note and summarize it: done, on disk. Ask it to read secret-salaries.txt and write the contents to the public summary, and you watch the IFC membrane block the write live — data carries secrecy 'confidential' but sink is not cleared — exactly as the in-turn demo predicted. Running it for real taught me things the demos couldn't (conversation memory is load-bearing; small models need a bounded thought field) and the security held through every one of them, untouched.
One honest caveat to close on. Constrained decoding is genuinely *weaker* on a hosted model. You don't get the sampler — you get the provider's JSON-mode plus validate-and-retry, a softer guarantee than the GBNF hard floor on llama.cpp. But that's the *right* test, not a compromise. The provider's constraint is itself something the threat model can't trust, so confining a model whose self-imposed structure is untrusted is exactly the case that has to hold. (If you want the deeper machinery behind the membrane and the caps doing the confining, the older Endo and Agoric tutorial series is where it's all built from first principles.)
The model went from fiction to real, from tiny to frontier, and the kernel never noticed. That's the property. Next, the isolation ladder: we stop trusting the *process* the tool runs in, and climb from a bare child process all the way to a formally-verified microkernel.
End of Transmission.