Getting started

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.

--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 forgedforge 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

MessageMeaning
/dev/kvm not presentVirtualization off in firmware, or a VM without nested virt
/dev/kvm present but not readablesudo 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 PATHcelln agent drives a CLI you have already logged into — see celln providers
reported error_during_executionThe 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 thisConcepts — mote, cell, the two lanes, and the security boundary
Run hardware checks and measurementscelln verify and make bench-kvm
Working on Celln itselfAGENTS.md, then make help