Celln runs agents in isolated cells that borrow verified tools instead of rebuilding Linux environments.
$ celln agent --tool python "decode this base64 and name the file type: R0lGODlhAQABAAAAACw="
✔ /usr/bin/python permitted in the agent lane
GIF imageA model wrote that code. An attested python ran it, on input you have no reason
to trust, in a cell with no network and nothing writable but /tmp — then the
cell dissolved.
Read the second line again. It says agent lane, and you never had to ask
for it. python is fully attested and keeps its hash, but code a model wrote is
agent-authored input, and handing that to a tool marked interpreter = true
demotes the invocation. Authority is decided per call, not per binary.
Most agent runtimes give an agent a machine. Celln gives it a temporary, read-only lease on the tools it needs.
Read the documentation → · Guided tutorial →
Software is a service the host provides to the process, not property the machine owns.
brew install sympozium-ai/celln/cellnHomebrew names the sympozium-ai/homebrew-celln repository as the
sympozium-ai/celln tap. The tap and source repository are public. On Linux,
the formula downloads the static release archive; it does not build Celln with
Rust locally. Building from source needs the one static target that the local
build plane uses for generated programs:
rustup target add x86_64-unknown-linux-muslRelease archives target Linux x86_64; Celln does not publish an ARM64 archive while its KVM backend is x86-specific.
or from source
git clone https://github.com/sympozium-ai/celln.git
cd celln
cargo build --release -p celln-cli
./target/release/celln doctorSealing cells needs Linux with /dev/kvm; generated-program cells additionally
need gcc, cpio, and e2fsprogs. Everywhere else celln still validates
specs, and celln doctor says which prerequisites you have.
The one-liner at the top is the short path. For anything you'd repeat, a spec is the durable artifact — reviewable, checked in, and the thing that says what a cell may ever be lent. Two tools, from two separate images, in one cell:
# cell.toml
name = "two-tools"
[cell]
memory = "512MiB"
[[tool]]
alias = "/usr/bin/python"
image = "python" # digest-pinned; a tag is refused
exec = "/usr/local/bin/python3.12"
interpreter = true
[[tool]]
alias = "/usr/bin/curl"
image = "curl" # a second, independent image
exec = "/usr/bin/curl"
[[run]]
exec = "/usr/bin/python"
args = ["-c", "import ssl; print('python ', ssl.OPENSSL_VERSION)"]
[[run]]
exec = "/usr/bin/curl"
args = ["--version"]$ celln setup # once: provider CLI + default tool images
$ celln run cell.toml
● sealing cell two-tools
· cell sealed, 2 tool(s) lent read-only
· image mounted at /tools1
✔ pilot: /usr/bin/python permitted:tool exit=0
✔ pilot: /usr/bin/curl permitted:tool exit=0
python OpenSSL 3.5.6 7 Apr 2026
curl 8.21.0 (x86_64-pc-linux-musl) libcurl/8.21.0 OpenSSL/3.5.7 …
● cell dissolvedLook at the two OpenSSL versions. python came from a glibc image, curl from a musl one — two libcs and two TLS stacks in the same cell, each its own sealed namespace, neither aware of the other. Nothing was installed, and nothing persists: the tools are read-only memory the host lent and can revoke.
Need a tool that isn't shipped? One command — give it a tag, celln pins the digest:
$ celln image add node:22-slim
● node:22-slim → node@sha256:0f1cd7…
+ added node to ~/.celln/tools.toml
/usr/bin/node → /usr/local/bin/node1. Get a tool. Most real tools are a closure — a binary, its loader and
the shared objects it resolves by absolute path — so celln lends them as a
sealed filesystem built from a digest-pinned OCI image. celln setup
materialises the defaults; pulling is its own step so starting a cell never
waits on a registry.
$ celln image catalogue
✔ python /usr/bin/python /bin/sh materialised
✔ curl /usr/bin/curl materialised2. Write a spec — what your agent may be lent, and what it intends to run.
celln image spec python > agent.toml # or: celln spec initname = "code-reviewer"
[cell]
memory = "512MiB"
require_tier = "verified"
[[tool]]
alias = "/usr/bin/python" # the name your agent uses
image = "python" # a catalogue name; a tag is refused
exec = "/usr/local/bin/python3.12"
interpreter = true # see below
[run]
exec = "/usr/bin/python"
args = ["-c", "print(1 + 1)"]
input = "data" # the agent wrote itA tool comes from exactly one of three places: image + exec for a closure,
path for a single static binary already on this host, or builtin = "fetch"
for the brokered HTTPS capability. A cell can declare several [[run]]
invocations and mount several images at once.
3. Check it — validation, plus what the trust model will decide.
$ celln spec check agent.toml
✔ code-reviewer 1 tool(s), 512MiB memory, require_tier=verified
tools
/usr/bin/python interpreter python → /usr/local/bin/python3.12
run
/usr/bin/python -c print(1 + 1)
runs in the agent lane — demoted: an interpreter fed agent-authored inputpython is fully attested, but an invocation fed agent-written input moves to
the agent lane — including the python -c "…" form that file-level taint
tracking misses.
4. Run it.
$ celln run agent.toml
● sealing cell code-reviewer
+ /usr/bin/python tier=verified cold — verified now, forged queued
✔ /usr/bin/python permitted in the agent lane
· cell sealed, 1 tool(s) lent read-only
✔ pilot: /usr/bin/python permitted:agent
· /usr/bin/python exit=0
2
● cell dissolvedThe verdict appears twice on purpose: once on the host before the cell exists, and once from pilot inside it, after re-hashing the bytes it actually found.
A guided tutorial works through three worked examples — an agent using python, a cell reaching a named host with no network stack of its own, and two independent toolchains in one cell.
5. Verify the isolation.
$ 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 cellA cell exists to contain code you would rather not run unsealed.
--show-source prints what the model wrote.
With a lent interpreter. The shortest path: name a catalogue tool, and the model writes that tool's language.
$ celln agent --tool python "decode this base64 and name the file type: R0lGODlhAQABAAAAACw="
● asking openai for Python to run as /usr/bin/python
· replied in 6s, 8 lines
≡ /usr/bin/python tier=verified warm — page map, no build
✔ /usr/bin/python permitted in the agent lane
· cell sealed, 1 tool(s) lent read-only
✔ pilot: /usr/bin/python permitted:agent
· /usr/bin/python exit=0
GIF imageYou never had to ask for the agent lane there. The model's 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.
Or forged from source. Without --tool, the model writes Rust and Celln
compiles it, which is where the more interesting claim lives:
$ 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 /tmp/celln-agent-1844068/program.rs
+ rebuilt, reproduced blake3:c0d7ceb8247d62bee808d6dc84b1ea57abeb7c16c95e46b5dc126f9abacd40b7 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. The
program was graded forged — we compiled it ourselves from source we hold. It
is still author=agent, and agent-authored code never carries tool-lane
authority at any tier. Pilot gives it only its own executable plus a writable
workspace; Landlock rejects other filesystem access and seccomp rejects network
and privileged syscalls.
Compiling is not a way around that. rustc fed model-written source is
python fed model-written source with the interpretation moved earlier; if the
laundering ban stops one it has to stop both.
- Tool lane: host-provided, attested tools use only the authority the cell loans them.
- Agent lane: agent-authored code gets only explicitly loaned capabilities: its executable and workspace by default, with no network.
- Data: bytes the agent produced or fetched. Data never gains authority by being handed to an attested tool.
An attested interpreter fed an agent-written script runs in the agent lane; it does not inherit tool-lane authority.
The model writes the program on the host; forge compiles it twice, in
different directories, and compares the bytes; assay grades on what that
rebuild reported and records who wrote it; the binary is sealed into the cell as
read-only memory; and pilot re-hashes it in the guest and decides for itself.
Under DAX there is no page-cache copy, so the instructions the guest executes
are the host's pages.
A forged tier requires a matching rebuild and records the reproduced recipe.
Otherwise the artifact is verified. assay checks that a proof names the
bytes being admitted. This establishes reproducibility on this machine and
toolchain, not across every environment.
Pick who writes it. A provider is an inference backend — who writes the program, not what runs in the cell:
$ celln setup # finds a provider CLI and materialises the default tool images
✔ default provider: openai (~/.config/celln/config.toml)
$ celln providers
✔ anthropic claude-opus-5 claude
✔ openai (cli default) codex default
✔ local qwen2.5-coder ollama
$ celln providers --set-default anthropic # change the saved default
$ celln agent --provider openai "…" # override it for one invocation
$ CELLN_PROVIDER=local celln agent "…" # override it for one shell commandThe saved setting is credential-free:
# ~/.config/celln/config.toml (or $XDG_CONFIG_HOME/celln/config.toml)
[provider]
default = "openai"celln agent "…" is for work that generates code to run; that path is where
Celln seals and governs the resulting program. Without --tool the model
writes Rust, which is forged into a static binary and attested. With
--tool python it writes that tool's language instead, and the program is
interpreted by a lent, attested interpreter — which makes it agent-authored
input, and so agent-lane, automatically.
Network-shaped work must declare exactly where it may reach before a model is called:
celln agent --allow-host example.com "crawl https://example.com/ …"Without --allow-host, Celln returns Unsupported; it does not generate a
crawler that can never connect.
Backends are subprocess adapters over CLIs you have already authenticated, not
linked SDKs. celln never reads, stores, or forwards a key; credentials remain
on the host.
Cells have no ambient network, so API credentials never enter the guest; only brokered bytes cross the boundary. Grading records provenance, not program correctness or safety.
Getting the model itself into a cell is the same problem as any other egress, and gets the same answer — an attested network stack behind a broker, never an ambient NIC.
Human-readable on a terminal, NDJSON the moment it is not. No flag needed,
though --json and --no-json force it either way.
celln run agent.toml | jq -r 'select(.event=="tool_resolved") | "\(.alias) \(.tier)"'
celln ps -a --json | jq -r "select(.status==\"failed\") | .id"
celln doctor --json | jq -e '.can_seal_cells // empty' >/dev/null && echo "can seal"Diagnostics go to stderr, so they never land in the data. Exit codes mean
something: 0 ok · 1 error · 2 spec invalid · 3 host cannot seal cells ·
4 refused by the trust model · 5 unsupported.
Based on our hardware tests, celln run seals a microVM and lends tools as
read-only memory that the guest cannot modify. celln verify runs a guest that
enters protected mode and maps a sealed page writable in page tables it wrote.
The test also checks revocation in an already-running cell.
celln agent runs agent-authored programs behind a Landlock filesystem boundary
and a seccomp syscall filter.
The guest may execute /tools/program and write only to /celln/work; network,
mounting, tracing, and privilege gain are refused. celln run remains the
spec-driven sealing path.
Run celln verify and make bench-kvm on a KVM host to reproduce the hardware
checks and measurements.
The full set is published at sympozium-ai.github.io/celln.
| Topic | Where | Covers |
|---|---|---|
| Start here | Start here | Five commands, about five minutes: install, connect an agent, then doctor / spec / run / verify / agent in order. |
| Command reference | CLI | The commands most people need, grouped: daily use, asking an agent, running declared tools, inspecting a run. |
| Tutorial | Tutorial | Three worked cells, each teaching one idea; plus adding your own tool. |
| Concepts hub | Concepts | The four-page series below, as one entry point. |
| The model | Model | Mote, cell, assay, warden, pilot — the vocabulary and one cell's lifecycle. |
| Tool lane | Tool lane | celln spec / celln run: declaring a tool, closures and images, tiers, the interpreter flag. |
| Agent lane | Agent lane | celln agent walkthrough: forging, attestation, choosing a backend, brokered egress. |
| Security boundary | Security | What's hardware-enforced, what's brokered, and what Celln does not claim. |
| Vocabulary reference | docs/NAMES_AND_CONVENTIONS.md | Every term (mote, cell, lane, tier, …) in one place. |
| Run the hardware checks | celln verify and make bench-kvm |
Reproduce the isolation proofs and spawn-latency measurements on your own KVM host. |
| Working on Celln itself | AGENTS.md, then make help |
Contributor setup, the Makefile targets, what CI runs. |
- mote — the substrate at rest. The seed.
- cell — a live, sealed, tool-loaned mote. Every cell is a sealed mote.
- tool lane / agent lane / data — attested host tools use loaned authority; agent-authored execution is bounded in the agent lane; data never gains authority by crossing into a tool.
Pre-alpha, single-host. Name pending formal trademark/domain clearance.
