The model is in the kernel, so a tool call is a function call
Every assistant that can do things sends its tool calls somewhere. Across a socket, over a process boundary, through a JSON schema and a parser on the far side that has to decide whether the arguments are real. That boundary is where most of the engineering goes, and most of the failures.
GLaDOS does not have one. The language model and the NVMe driver are in the same address space at the same privilege level, so when the model decides to read a file, the read is a function call. This piece is about what that arrangement actually removes, and what it costs, because it is not free.
Where the model lives
UEFI already hands over long mode, CPL 0 and an identity map, so this UEFI
application is the kernel. There is no ELF loading, no relocation, no
handoff ABI, and after ExitBootServices it runs on page tables it
built itself. There is one address space and no process isolation; nothing is
ever mapped out of reach of anything else.
So the model is not a service the kernel talks to. It is a module in the kernel, sitting beside the TCP stack and the FAT writer, compiled into the same binary and reachable by a direct call.
| Weights | ~570 MB |
|---|---|
| Copied to heap | ~140 KB |
| Read as | &'static [u8] |
| Freed | never |
Only the norm weights are copied. Everything else is referenced where the firmware put it.
Loading it at all
There is exactly one moment in the life of this machine when a filesystem
exists, and it is before ExitBootServices. After that call the
firmware's drivers are gone and nothing can open a file until the kernel's own
NVMe and FAT code is running. So the model, the tokenizer and the root
certificate bundle are all read in that window, near the top of
main, before anything else happens.
The allocation underneath that is worth knowing about, because it was a
bug first. UEFI's allocate_pool is the obvious call and it
refuses a request the size of a 1.8 GB checkpoint. The failure did not
present as an allocation error; it presented as no model, which sent
somebody looking at the loader. There is a fallback to
allocate_pages now, and a comment naming the checkpoint that
forced it.
What arrives is a pointer and a length, handed out as
&'static [u8] and never freed. The model is then
referenced in place rather than copied: the forward pass
indexes into the firmware's pool by arithmetic, and only about 140 KB of
norm weights are lifted into the heap. On a machine whose heap tops out at
320 MiB in one contiguous span, copying 570 MB was never on the
table.
One consequence that catches people: the checkpoint is identified by its contents, not its filename. The loader tries the GLaDOS layout, then falls back to the llama2.c one. A file named wrongly still loads; a file that is secretly a different format still fails.
What a tool call is
The model does not emit JSON. It emits an applet name and its arguments, under a grammar built from the live applet table — that is its own subject. What happens next is the part this piece is about, and it is short. The name resolves to a match arm, the arm calls the function, the function returns a value.
model decides ls /ai/tools
|
grammar admits only names in the applet table
|
dispatch crate::sysbox::ls(path)
|
returns a list, in this address space
There is no serialisation anywhere on that path. No schema to keep in sync with the implementation, no parser that can disagree with the writer, no transport that can truncate, no version skew between two halves that ship separately. Those are not latency costs, they are correctness costs, and removing the boundary removes them rather than making them faster.
The naming is a rule rather than a taste. A builtin is named after the
Rust path it calls, flattened: crate::net::tcp::connect becomes
tcp_connect, crate::dev::rtc::now becomes
rtc_now. The audience is a small model and whoever is reading
the kernel source beside it, and both can apply a rule they were told once to
a subsystem they have never seen. Hand-picked names read better one at a time
and have to be memorised one at a time, which is the cost that matters.
The gate in front of it
An address space with no isolation is only safe if something decides what
may be called, and that decision has to be in one place. It is a table:
every builtin is a row of (name, Touch, min args, max args), and
dispatch refuses anything absent from the table before it looks at
the match.
An arm added without a row is unreachable — dead code, not an ungated capability. A row added without an arm answers "no implementation" — broken, not dangerous.
Neither failure is the dangerous one. That is the whole argument.
It replaced two denylists. Those were correct for eleven raw builtins and stopped being correct the moment the language was wired to the network, because a denylist grants by default: the builtin somebody forgets to add is the one that matters, and it fails open.
Touch has seven classes — Pure, Read, Write, Net,
Model, Draw, Raw — but the sandbox question stays binary. Pure, Read
and Write are allowed to a stored program; everything else needs an operator
to trust it by name and hash. That follows from what an operator can actually
hold in their head: "may write outside itself but not open sockets" is not a
sentence anybody can check a program against.
The line for Net is whether a packet leaves the machine, which is why listing interfaces is Read and opening a socket is not.
What this costs
Being honest about the trade is the point of writing it down.
There is no isolation, and that is not a bug to be fixed later. A language model with function-call access to raw memory and I/O ports is exactly what this design produces. The mitigation is the capability table and operator trust, not a boundary, because there is no boundary.
It is also why one particular thing is refused outright: no private key ever lives on this machine. One address space, no process isolation, and a model that can read memory is the worst available place to keep one. Signing happens elsewhere, and the kernel only ever verifies.
Only one thing may hold the model at a time. Two mutable borrows of the engine at once is undefined behaviour; two callers decoding into one KV cache is not undefined but corrupts the cache, the position and the last token, and produces confident nonsense. So the engine records its holder and a claim is taken for the length of a call, or for a whole episode where one spans many. That replaced a flag-and-id pair per task, which is how a third holder went unnoticed for a while: a nightly job that set neither flag and handed out a second mutable reference to anyone who asked during its twenty seconds.
A fault is fatal. Every interrupt vector except the breakpoint diverges. There is no recovery, so a bad pointer anywhere is a halted machine with a register dump rather than an error return. That constrains how anything here is written far more than the absence of syscalls does.
