cellos-ctl
cellctl is the kubectl-style CLI for CellOS. It is a thin client over
cellos-server: every subcommand corresponds to exactly one HTTP call,
there is no client-side cache, and exit codes are a stable contract
(0 success / 1 usage / 2 API / 3 validation).
Configuration lives in ~/.cellos/config (TOML); the typical fields are
server_url and api_token. CLI flags and the CELLCTL_SERVER /
CELLCTL_TOKEN environment variables override the on-disk values.
Commands
| Command | What it does |
|---|---|
apply -f <file> |
Submit a formation YAML spec (POST /v1/formations). |
get formations / get cells |
List resources. |
describe formation <name> / describe cell <name> |
Show state + recent events. |
delete formation <name> |
Tear down a formation (and its cells). |
logs <cell> [-f] |
Stream CloudEvents for one cell. |
events [--formation N] [-f] |
Stream global / formation-scoped events. |
rollout status <name> |
Poll a formation to a terminal state. |
diff -f <file> |
Compare local YAML against the server-side formation. |
config show / config set-server <url> / config set-token <token> |
Read/write ~/.cellos/config. |
version |
Print client + server versions. |
webui |
Spin up a localhost browser proxy for the web view. |
Formation manifest shapes
cellctl apply -f <file> forwards the file's contents verbatim to
POST /v1/formations. The server accepts either of two shapes; pick
one per file (mixing them in a single document is rejected).
Kubectl-style — matches contracts/schemas/formation-v1.schema.json
and is the preferred form for new manifests. New operators who already
think in kubectl terms will recognise it immediately:
apiVersion: cellos.dev/v1
kind: Formation
metadata:
name: demo
spec:
coordinator: coord
members:
- name: coord
- name: worker-a
authorizedBy: coord
Flat — the server's canonical internal form. Shorter; used by older manifests and by the server's test fixtures:
name: demo
coordinator: coord
members:
- id: coord
- id: worker-a
authorizedBy: coord
The mapping (kubectl → flat) is:
| kubectl path | flat path |
|---|---|
metadata.name |
name |
spec.coordinator |
coordinator |
spec.members[].name |
members[].id |
spec.members[].authorizedBy |
members[].authorizedBy |
apiVersion MUST be cellos.dev/v1 and kind MUST be Formation;
anything else is rejected with 400 /problems/bad-request. After the
adapter normalizes the document, the usual ADR-0010 admission checks
(no-coordinator, multiple-coordinators, authority-not-narrowing,
cycle, duplicate-member-id) run on the flat form and surface their
stable /problems/formation/* discriminants. The
duplicate-member-id type was split out from multiple-coordinators
so operators can tell a structural admission rejection ("two members
both named coord — pick one to be the coordinator") apart from a
typo ("two members share the same id — rename one").
webui
cellctl webui is the only supported way to reach the CellOS browser
view (ADR-0017). It is a foreground process that runs a localhost
reverse proxy in front of cellos-server:
cellctl webui # default: bind BOTH loopback TCP and Unix socket
cellctl webui --open # also launch the system browser
cellctl webui --bind loopback # TCP loopback only (no Unix socket)
cellctl webui --bind unix # Unix socket only — no browser URL (Linux/macOS)
On startup the command:
- Reads
~/.cellos/config(or$CELLCTL_SERVER/$CELLCTL_TOKEN) for the upstream URL and bearer token. - Binds one or both listeners according to
--bind. The default (auto) binds both a loopback TCP port (so the browser can reach the proxy) and a Unix domain socket at${XDG_RUNTIME_DIR:-/tmp}/cellctl-webui-<pid>.sockwith mode0600(so inter-process tooling can bypass loopback without exposing a TCP port to anything else on the machine). Both URLs are printed on stdout. The Unix socket is removed on graceful shutdown. On Windows,autodegrades to loopback-only and--bind unixerrors. - Mints a 32-byte random session token and prints the launch URL with
the token in the URL fragment:
http://127.0.0.1:<port>/#sess=<base64>. Fragments are never transmitted in HTTP requests, so the token does not appear in proxy logs, server logs, orRefererheaders. - Serves the Vite-built bundle from
crates/cellos-ctl/static/. The bundle readslocation.hash, posts to/auth/exchange, receives anHttpOnly; SameSite=Strictcookie, and clears the fragment. - Reverse-proxies
GET /v1/*andGET /ws/eventsupstream, injectingAuthorization: Bearer <token>on every outbound request. The bundle never sees the bearer token. - Refuses any non-
GETmethod with HTTP 405 (Allow: GET). This is the structural enforcement of ADR-0016's read-only browser boundary — even a compromised bundle cannot write through the proxy. - Exits on
Ctrl-C, printingshutting downon stderr.
Security properties
- The bearer token in
~/.cellos/confignever leaves the machine; it lives on the proxy's outbound socket and nowhere else. - The browser sees a session cookie that is meaningless outside this
cellctl webuiprocess. When the process exits, the cookie is dead. - Browser writes are impossible: the proxy returns 405 on any non-GET
to anything other than
/auth/exchange(the cookie-mint endpoint). - The launch URL's token lives only in the fragment, so it cannot
appear in HTTP access logs, the
Refererheader, or anything else that sees the path/query.
Flags
| Flag | Default | Meaning |
|---|---|---|
--open |
off | After binding, launch the system browser at the URL. Ignored if there is no loopback URL (i.e. --bind unix). |
--bind <auto|loopback|unix> |
auto |
auto: bind both loopback TCP and a Unix socket (Linux/macOS) or loopback-only (Windows). loopback: TCP only. unix: Unix socket only — no browser-reachable URL. |
Bind modes in detail
| Mode | TCP loopback | Unix socket | Browser-reachable? | Use case |
|---|---|---|---|---|
auto (default) |
yes (random high port) | yes (/tmp/cellctl-webui-<pid>.sock, mode 0600) |
yes | Normal desktop use. The browser uses the TCP URL; other local tools (forwarders, curl --unix-socket, socat) can use the UDS without exposing the proxy on a TCP port. |
loopback |
yes | no | yes | Same as today's behavior. Useful if you don't want a socket file in $XDG_RUNTIME_DIR / /tmp. |
unix |
no | yes | no | Inter-process forwarding only (e.g. ssh -L 8080:/tmp/cellctl-webui-<pid>.sock to expose the proxy to a remote browser through an SSH tunnel). --open is a no-op. |
Trade-offs:
autois the operator-friendly default. The cost is one extra file in$XDG_RUNTIME_DIR(or/tmpif the env var is unset) for the lifetime of thecellctl webuiprocess. The file is mode0600(owner read+write only) and is unlinked onCtrl-C.loopbackis for environments where you specifically don't want a UDS — e.g. running inside a sandbox that disallows AF_UNIX, or paranoia about leftover socket files surviving a crash. If you crash withauto, the stale.sockfile is harmless (a fresh run unlinks and re-binds) but it's still clutter.unixis the highest-isolation mode: nothing on the loopback interface, only filesystem permissions guard the proxy. The cost is that the browser can't reach it directly — you need a forwarder. Best used over an SSH tunnel into a remote operator host.
Building the bundle
cellctl webui looks up crates/cellos-ctl/static/ (set via
CARGO_MANIFEST_DIR at build time; override with
$CELLCTL_WEBUI_BUNDLE_DIR for development). If the directory is
missing, run the Vite build first:
npm --prefix web run build
See ADR-0017 for the full design rationale and ADR-0016 for the read-only-browser invariant the 405-on-non-GET rule enforces.