Three cells, three ideas.
Each example here runs for real and teaches one thing. By the end you will have run an interpreter on agent-written code, reached the internet from a cell that has no network, and lent two independent toolchains to one cell — and you will have seen the trust model refuse things along the way.
Every output below is copied from a real run. You need Linux with /dev/kvm, and skopeo and e2fsprogs installed.
Setup: what a tool is
A tool in Celln is not a name. It is bytes, hashed, that the host vouches for. Most real tools — python, curl, node — are not one file but a closure: a binary, its loader, and the shared objects it resolves by absolute path. Celln lends that closure as a sealed, read-only filesystem built from an OCI image.
$ celln setup $ celln image catalogue ✔ python /usr/bin/python /bin/sh materialised ✔ curl /usr/bin/curl materialised
The catalogue is compiled into the binary and pinned by digest, never by tag. A tag can be moved; if a spec named one, what your cell is lent could change without your spec changing. Updating a pin is a reviewed commit, which is why a CI job re-resolves the tags weekly and opens a pull request rather than changing anything silently.
Pulling is deliberately its own step. A registry round trip takes seconds to minutes; doing it while building a cell would put the network on the spawn path. A cell only ever maps bytes that already exist.
$ celln image spec python > my.toml # scaffolds a runnable spec
A · An agent allowed to use python
The idea: the same binary can carry different authority depending on what it is fed.
examples/cells/a-agent-uses-python.toml:
name = "agent-uses-python"
[cell]
memory = "512MiB"
[[tool]]
alias = "/usr/bin/python"
image = "python"
exec = "/usr/local/bin/python3.12"
interpreter = true
[run]
exec = "/usr/bin/python"
input = "data" # the agent wrote what follows
args = ["-c", "…"]
Two lines are doing the work. interpreter = true says this binary executes whatever it is handed. input = "data" says the thing it is being handed came from the agent, not through the attestation gate.
$ celln run examples/cells/a-agent-uses-python.toml ✔ /usr/bin/python permitted in the agent lane ✔ pilot: /usr/bin/python permitted:agent agent code running in a cell computed: 97e5c19818d31f1152e0193e socket: PermissionError - confined to the agent lane
python is fully attested, and this invocation still ran in the agent lane. That is the laundering ban: an attested interpreter fed agent-authored input is demoted for that call, so passing code through a trusted binary is not a way to launder it into trusted authority.
The demotion is not cosmetic. Notice socket() failing — the agent lane adds a seccomp filter and a Landlock boundary immediately before execve. Change input to "none" and the same binary, with the same hash, runs in the tool lane and can open a socket. Same bytes, different authority, decided per invocation.
input = "data" to "none" and watch the lane change in the output. Nothing else about the spec moves.B · Reaching a host from a cell with no network
The idea: capabilities are brokered, not granted.
A cell has no network stack at all. Put curl in one and it runs perfectly and reaches nothing:
$ celln run examples/cells/curl.toml # with args = ["https://example.com"]
curl: (28) Resolving timed out after 5002 milliseconds
Egress is a capability the host performs on the cell's behalf, and you have to name who it may talk to:
[cell] memory = "512MiB" allow_hosts = ["google.com", "www.google.com"] [[tool]] alias = "/fetch" builtin = "fetch" [run] exec = "/fetch" args = ["https://google.com"]
$ celln run examples/cells/b-fetch-google.toml
· egress brokered by the host: google.com, www.google.com
✔ pilot: /fetch permitted:tool
✔ /fetch exit=0
fetched 82445 bytes; starts: '<!doctype html><html itemscope=""…'
Real bytes, from the real internet, into a cell with no interface. The guest hands a URL over four I/O ports; the host validates it, pins the DNS result before connecting, follows redirects one hop at a time re-authorising each, and bounds the size and the time. Ask for somewhere you did not declare:
pilot-fetch: CELLN_FETCH_ERROR:host "example.com" is not declared in this cell's allowlist
A spec that declares builtin = "fetch" with no allow_hosts is a spec error, not a cell with open egress. Hermetic is the default you have to opt out of, one host at a time.
curl do this? It would need a socket, so the host would have to broker raw TCP instead of a validated URL — and that discards the DNS pinning, the redirect re-authorisation and the protocol allowlist that make the capability safe. The rule this follows is broker capabilities, never binaries: the cell asks for fetch(url), never for curl(argv).C · Several tools, from several images
The idea: tools map onto images; they do not own them.
It is tempting to solve "I need python and curl" by finding one big image with both. Don't: that lends a whole distribution — compilers, headers, package managers — to run two commands. Lend a tool and its closure instead, and let a cell mount more than one:
[[tool]] alias = "/usr/bin/python" image = "python" exec = "/usr/local/bin/python3.12" interpreter = true [[tool]] alias = "/bin/sh" image = "python" # same image: shares the mount exec = "/bin/dash" interpreter = true [[tool]] alias = "/usr/bin/curl" image = "curl" # a second, independent image exec = "/usr/bin/curl"
$ celln run examples/cells/c-curl-and-python.toml · cell sealed, 3 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 ✔ pilot: /bin/sh 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 …
Each image becomes its own sealed namespace with its own mount. The clearest evidence they are genuinely separate is in that output: python reports OpenSSL 3.5.6 from a glibc image, curl reports OpenSSL 3.5.7 from a musl one. Two libcs, two TLS stacks, one cell, neither aware of the other.
Because tools reference images rather than owning them, all three shapes are the same mechanism:
| Shape | Meaning |
|---|---|
| one tool, one image | the ordinary case |
| many tools, one image | python and sh above — one mount, one physical copy |
| many tools, many images | independent closures side by side |
Sharing is also what makes this cheap. An image is content-addressed, so every cell leasing it maps the same physical copy — sixty-four cells, one allocation. And because the pages are sealed read-only by hardware, one cell cannot alter what another sees.
Adding a tool
The catalogue ships with a small default set, but adding your own is one command. You do not need the digest, and you do not need to know the image's internal layout:
$ celln image add busybox:1.36
● busybox:1.36 → busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
● pulling …
· 1 layer(s), 4 MiB
+ 10 MiB tier=verified blake3:8a71ce8bd4eae1c1…
+ added busybox to ~/.celln/tools.toml
/usr/bin/busybox → /bin/busybox
· use it with: celln image spec busybox > cell.toml
You named a tag, and celln resolved it to the digest it points at right now and pinned that. The tag is remembered only so it can be re-checked later; what gets lent is the digest. It then materialised the image, looked inside it without mounting it, and found the executables worth exposing.
$ celln image catalogue ✔ python /usr/bin/python /bin/sh materialised ✔ curl /usr/bin/curl materialised ✔ busybox /usr/bin/busybox materialised, local
It is usable immediately, exactly like a shipped entry:
$ celln image spec busybox > cell.toml $ celln run cell.toml ✔ pilot: /usr/bin/busybox permitted:tool · /usr/bin/busybox exit=0
When the guess needs help
By default celln exposes binaries named after the image. When that is not the binary you need, --tool takes alias=path-in-image: the left side is the stable name used in a spec; the right side is the executable inside the image. For Go:
$ celln image add golang:1.27rc2-alpine3.24 \
--tool /usr/bin/go=/usr/local/go/bin/go
$ celln image spec golang > go.toml
Celln never inherits the host environment. If a tool requires runtime
variables, declare them beside the invocation (or in [agent] for
an agent-selected invocation). They are part of the reviewed cell policy and
are passed exactly as written after the image is entered:
[agent] exec = "/usr/bin/go" prompt = "report the Go version" [agent.env] GOROOT = "/usr/local/go" GOCACHE = "/tmp/go-cache"
Use [run.env] for a pinned [run] instead. Celln
does not copy the assay host's environment, and model-authored environment is
agent input: it cannot be used to retain tool-lane authority.
If nothing obvious matches, celln refuses and lists what it found rather than guessing. Anything whose name looks like an interpreter is marked as one, so the laundering ban applies to it — check that, because an unmarked interpreter keeps full tool-lane authority even when fed agent-written code.
--default makes celln setup materialise it too. celln image remove <name> drops it.
Local, shipped, or neither
| Where | Use it for |
|---|---|
celln image add → ~/.celln/tools.toml | Your own tools. Local entries shadow shipped ones of the same name, so you can pin your own build of something. |
A digest straight in a spec — image = "node@sha256:…" | One-off or project-specific tools. No catalogue entry needed at all. |
crates/celln-cli/tools.toml, via a pull request | Tools worth shipping to everyone. CI re-resolves those tags weekly and opens a PR when upstream moves. |
image pull refuses anything over 512 MiB. If a tool is static it needs no image at all: a spec can lend it directly with path.What you learned
- A tool is bytes the host vouches for, pinned by digest, and usually a closure rather than a file.
- Authority is decided per invocation. An attested interpreter fed agent-written input is demoted to the agent lane and confined — the laundering ban.
- The cell is hermetic. Reaching the network is a brokered capability you declare host by host, exposed as a narrow verb rather than a binary with argv.
- Lend closures, not distributions. Several images can be mounted at once, shared between tools, and shared between cells at one physical copy.
Next: the concepts in order, or the security boundary for what is enforced and what is not claimed.