cellos-ctl 0.5.0

cellctl — kubectl-style CLI for CellOS execution cells and formations. Thin HTTP client over cellos-server with apply/get/describe/logs/events/webui.
Documentation

cellos

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.

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:

  1. Reads ~/.cellos/config (or $CELLCTL_SERVER / $CELLCTL_TOKEN) for the upstream URL and bearer token.
  2. 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>.sock with mode 0600 (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, auto degrades to loopback-only and --bind unix errors.
  3. 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, or Referer headers.
  4. Serves the Vite-built bundle from crates/cellos-ctl/static/. The bundle reads location.hash, posts to /auth/exchange, receives an HttpOnly; SameSite=Strict cookie, and clears the fragment.
  5. Reverse-proxies GET /v1/* and GET /ws/events upstream, injecting Authorization: Bearer <token> on every outbound request. The bundle never sees the bearer token.
  6. Refuses any non-GET method 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.
  7. Exits on Ctrl-C, printing shutting down on stderr.

Security properties

  • The bearer token in ~/.cellos/config never 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 webui process. 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 Referer header, 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:

  • auto is the operator-friendly default. The cost is one extra file in $XDG_RUNTIME_DIR (or /tmp if the env var is unset) for the lifetime of the cellctl webui process. The file is mode 0600 (owner read+write only) and is unlinked on Ctrl-C.
  • loopback is 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 with auto, the stale .sock file is harmless (a fresh run unlinks and re-binds) but it's still clutter.
  • unix is 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.