Aperture Institute
Operating systems and machine reasoning.
Built from scratch. Measured before it is claimed.
HomeResearch › The model is in the kernel

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.

Where a checkpoint sits
Weights~570 MB
Copied to heap~140 KB
Read as&'static [u8]
Freednever

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.

Why an allowlist

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.

Next: making a wrong answer unreachable →

In the source
The shape of it
Rings
0 only
Address spaces
1
Syscalls
0
Serialisation
none
Capability classes
7
Sandbox answer
binary