cellos-ctl 0.5.1

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

## 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.