Five commands, about five minutes.
Celln drives a provider CLI you already trust — Codex, Claude, or Ollama. A KVM Linux host adds hardware-isolated cells for declared tools; generated programs use Celln's separate agent boundary. Each command below checks a specific property and explains the expected result.
Install
brew install sympozium-ai/celln/celln # Linux only: required to build static generated programs rustup target add x86_64-unknown-linux-musl
Homebrew names the sympozium-ai/homebrew-celln repository as the sympozium-ai/celln tap. On Linux the formula downloads the static release archive; it does not build Celln with Rust locally. Release archives target Linux x86_64 — there is no ARM64 archive while the KVM backend is x86-specific.
What can this machine do?
celln doctor
host ✔ kvm /dev/kvm available ✔ cpu-virt vmx/svm present ✔ guest-kernel /boot/vmlinuz-7.1.4-200.fc44.x86_64 ✔ cargo /home/you/.cargo/bin/cargo ✔ this machine can seal real, hardware-isolated cells.
Every failed check prints what to do about it. Sealing cells needs Linux with /dev/kvm; generated-program cells additionally need gcc, cpio, and e2fsprogs. Everywhere else celln still validates specs. Exit code 3 means "cannot seal cells," so a script can branch without parsing text:
celln doctor -q || echo "no hardware isolation here"
Connect an agent
celln setup # discovers logged-in codex, claude, or ollama celln providers # shows available inference providers
celln setup saves your preferred provider to a credential-free config file. Change it later with celln providers --set-default anthropic, or override a single request with --provider or CELLN_PROVIDER.
Write and check a spec
A spec is what your agent may be lent, and what it intends to run. Nothing not listed here can execute inside the cell.
A tool is bytes the host vouches for, and most real tools are a closure rather than one file — a binary, its loader, and the shared objects it resolves by absolute path. Celln lends a closure as a sealed filesystem built from a digest-pinned image, materialised ahead of time by celln setup.
celln image spec python > agent.toml
[[tool]] alias = "/usr/bin/python" image = "python" # a catalogue name; celln resolves the pin exec = "/usr/local/bin/python3.12" interpreter = true
path = "/usr/bin/…" still works, but only for a genuinely static binary: a cell carries no loader and no libc, and on a typical host about three of the two thousand binaries in /usr/bin qualify. Adding your own tool is one command.
interpreter = true is the most consequential line in the file. An interpreter fed something the agent wrote is moved to the agent lane for that invocation, so an agent cannot launder its own code into full authority by handing it to python. celln spec check warns if you forget on a name it recognises.
$ celln spec check agent.toml ✔ code-reviewer 1 tool(s), 256MiB memory, require_tier=verified tools /usr/bin/python interpreter /usr/bin/python3 run /usr/bin/python review.py runs in the agent lane — demoted: an interpreter fed agent-authored input
Expected result. It tells you the lane your run will land in before anything is sealed. Mistakes read like a compiler, and every one carries a fix:
invalid spec in agent.toml
✘ tool[0].path
/nope/missing does not exist
fix: point at a real file on this host; a tool is bytes, and they have to come from somewhere
✘ run.exec
"/usr/bin/ghost" is not one of the tools
fix: add a [[tool]] with alias = "/usr/bin/ghost", or point run.exec at one of: /usr/bin/python
A typo is an error, not a shrug — teir = "forged" is rejected rather than ignored, because a silently-dropped tier means a cell quietly running at the wrong trust level.
Seal a cell
$ celln agent agent.toml ● sealing cell code-reviewer + /usr/bin/python tier=verified cold — verified now, forged queued · microVM sealed, phase=Materialise · /usr/bin/python sealed read-only into the cell · authority ratcheted to Work — no further tools can be lent ✔ /usr/bin/python permitted in the agent lane ● cell dissolved
Expected result.
cold — verified now, forged queued: an unseen tool is admitted at Verified in seconds and a hermetic rebuild is queued behind the traffic. Launch is never slow; trust upgrades asynchronously. Run it again and it iswarm — page map, no build.authority ratcheted to Work: once any agent-authored code has run, the cell can never be lent another tool. Authority only shrinks, and the host enforces it, so a compromised cell cannot roll it back.- Your Python binary is sealed into the microVM's physical address space as read-only memory.
--dry-run stops after resolving tools, if you only want to see what a spec would pull in.
Prove the isolation, on your machine
$ celln verify proving isolation on this machine ✔ a ring-0 guest with its own page tables cannot write lent tool code ✔ revoking a tool stops it in an already-running cell ✔ isolation holds on this machine.
Expected result. The first proof builds a guest that enters 32-bit protected mode, installs page tables it wrote itself, maps the sealed tool page writable in them, and writes. Guest-side that write is entirely legal — ring 0, PTE says writable, no fault. It still does not land, because stage-2 sits below the guest's own translation. Nothing in the guest is enforcing this. The guest is root.
Have a model write the code, and run it sealed
A cell exists to contain code you would rather not run unsealed. There are two ways in, and both end up in the agent lane.
In the spec
Replace [run] with [agent], and the file keeps the policy — which tools, how much memory, which hosts — while only the program is written on demand:
name = "analyst" [cell] memory = "512MiB" [[tool]] alias = "/usr/bin/python" image = "python" exec = "/usr/local/bin/python3.12" interpreter = true [agent] exec = "/usr/bin/python" # which declared tool interprets the answer task = "write a python program that prints a 5x5 multiplication table"
$ celln run analyst.toml ● asking openai for Python to run as /usr/bin/python · replied in 7s, 10 lines ✔ /usr/bin/python permitted in the agent lane ✔ pilot: /usr/bin/python permitted:agent 1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25
--task "…" overrides the task in the file, so one reviewed spec can serve many jobs. You never had to ask for the agent lane there: model-written code is agent-authored input handed to a tool marked interpreter = true, so it is demoted for that invocation. python keeps its hash and loses its authority.
agent.exec has to name a declared tool marked interpreter = true, and a cell either declares runs or asks for one — never both.
Without a file
$ celln agent --tool python "print the first 12 fibonacci numbers" ✔ /usr/bin/python permitted in the agent lane 0 1 1 2 3 5 8 13 21 34 55 89
This builds the spec above in memory and runs it through the same path, so an ad-hoc run reaches exactly the same trust decisions as a declared one.
Or forged from source
Without --tool, the model writes Rust and Celln compiles it:
$ celln agent "print the first 100 primes, space separated" ● asking anthropic (claude-opus-5) to build: print the first 100 primes, space separated · waiting for claude (up to 90s; --timeout changes it) · replied in 5s · selected sealed runtime: Rust 2021 (static musl); 23 source lines + rebuilt, reproduced blake3:c0d7ceb8… 436 KiB tier=forged author=agent · cell sealed, tools lent read-only ✔ pilot: /agent/program permitted:agent 2 3 5 7 11 13 17 19 23 29 31 …
The program ran in the agent lane. It was graded forged — forge rebuilt it twice and the bytes matched, so the tier was earned rather than asserted. It is still author=agent, and agent-authored code never carries tool-lane authority at any tier. Compiling is not a way around that: rustc fed model-written source is python fed model-written source with the interpretation moved earlier.
A cell has no network at all, so a task that genuinely needs one will build and then do nothing — celln agent warns up front when a task looks like that. Declare exactly where it may reach before the model is called:
celln agent --allow-host example.com "crawl https://example.com/…"
Pick who writes it, and see what this host can use:
celln providers celln agent --provider openai "…" # or --provider local celln agent --show-source "…" # print the program it wrote
Backends are subprocess adapters over CLIs you have already authenticated — celln never reads, stores, or forwards a key.
Piping
Human on a terminal, NDJSON when not. No flag required:
# which tools went cold? celln agent agent.toml | jq -r 'select(.event=="tool_resolved" and .warm==false) | .alias' # fail a build if a spec would run something in the tool lane celln spec check agent.toml | jq -e 'select(.event=="run_plan") | .lane=="data"' # just the proofs celln verify | jq -c 'select(.event=="proof")'
Diagnostics and warnings go to stderr, so | jq never chokes on prose. --json and --no-json force the mode when you want it explicit.
Two more worth knowing: celln image catalogue lists available tools and whether their sealed images are materialised. celln providers lists which model backends this host can use.
When something says it cannot
| Message | Meaning |
|---|---|
/dev/kvm not present | Virtualization off in firmware, or a VM without nested virt |
/dev/kvm present but not readable | sudo usermod -aG kvm $USER, then log out and in |
no readable /boot/vmlinuz-* | Only affects the boot-path demos, not celln run |
anthropic needs `claude` on PATH | celln agent drives a CLI you have already logged into — see celln providers |
reported error_during_execution | The model CLI failed, not Celln. Some prompts trigger it every time; rewording helps more than retrying |
Where to go next
| Understand the model behind all of this | Concepts — mote, cell, the two lanes, and the security boundary |
| Run hardware checks and measurements | celln verify and make bench-kvm |
| Working on Celln itself | AGENTS.md, then make help |