Snowcat Cockpit is a node-local control surface for Snowcat execution capacity. It reports machine and provider readiness, launches retained managed workers, and can run one bounded campaign across every repository enrolled on the local node. One Go binary presents lifecycle inventory through an embedded, loopback-only Pilothouse dashboard. The original Bash, tmux, and ttyd launcher remains available as the measured trial harness.
Snowcat remains unchanged. Each managed worker continues to claim and report work through Snowcat's existing MCP contract, with a worker-local Cockpit relay binding short renewal to provider liveness. Generic launcher workers retain their existing provider MCP configuration.
Start with the architecture, the node design, and the production roadmap.
make build writes a version-stamped Linux binary to
dist/snowcat-cockpit; make install installs the same command through Go.
make ci is the complete credential-free repository gate and is also exposed
as make check for the frostyard Go-repository convention.
Version tags run GoReleaser Pro for linux/amd64 and linux/arm64, publish
archives and frostyard-snowcat-cockpit deb/rpm/apk packages, and separately
publish the three multi-architecture worker images. The nightly workflow
replaces the single dev prerelease after the Tests workflow succeeds.
The node requires Go 1.26.6 or newer to build. Inspect this machine without creating state or claiming work:
go run ./cmd/snowcat-cockpit doctor
go run ./cmd/snowcat-cockpit doctor --json
go run ./cmd/snowcat-cockpit profilesinstall-kit is not an inspection command: it materializes the locked
Snowcat worker kit on disk in the selected --skills-dir (default
$HOME/.agents/skills), writing any file the kit is missing there. It
never replaces a file whose content differs from the lock:
go run ./cmd/snowcat-cockpit install-kitProve one provider can see the locked skills and reach Snowcat without giving it claim authority:
go run ./cmd/snowcat-cockpit preflight \
--provider codex \
--mcp-server snowcat \
--repository frostyard/firnThe result is valid for 15 minutes and is bound to the locked kit revision. Provider output and configuration are never written to Cockpit state.
Start the embedded dashboard on loopback:
go run ./cmd/snowcat-cockpit serve \
--skills-dir /path/to/snowcat/.agents/skillsTo enable operator-triggered queue snapshots and bounded fleet launch, supply the Snowcat HTTP MCP endpoint and a dedicated token through the starting shell's environment, never command-line arguments:
export SNOWCAT_COCKPIT_MCP_URL=https://snowcat.example/mcp
read -rsp 'Snowcat Cockpit observer token: ' SNOWCAT_COCKPIT_MCP_TOKEN
export SNOWCAT_COCKPIT_MCP_TOKEN
go run ./cmd/snowcat-cockpit serveMint this dedicated token with Snowcat's server-enforced observer profile:
npm run queue -- token mint member:you@example.com cockpit-observer --profile observerThe profile grants only list_work and get_work. Outside a running campaign,
the dashboard observes only when you press Observe once, launch a fleet, or
press Observe work for one retained worker. A one-shot fleet is capped to
eligible work and 12 workers and never refills. An explicitly started board
campaign is the bounded persistent exception described below.
For the standard local credential files at
${XDG_CONFIG_HOME:-$HOME/.config}/snowcat/profile-observer.env (one
export SNOWCAT_OBSERVER_TOKEN=<token>) and
${XDG_CONFIG_HOME:-$HOME/.config}/snowcat/mcp-token.env (one each of
export SNOWCAT_MCP_TOKEN=<token>,
export CF_ACCESS_CLIENT_ID=<client-id>, and
export CF_ACCESS_CLIENT_SECRET=<client-secret>), build once and use the
checked-in wrapper. Override either path with SNOWCAT_COCKPIT_OBSERVER_ENV and
SNOWCAT_COCKPIT_WORKER_ENV respectively. Both files must be regular,
non-symlink, current-user-owned files with mode 0600. The wrapper verifies
both files, reads only those expected declarations, fixes the MCP URL to
https://snowcat.frostyard.org/mcp, and removes the source variables
before starting Cockpit:
make build
bin/snowcat-cockpit-serve --listen 127.0.0.1:7682On Linux, install that same checked deployment as a systemd user service rather than keeping the node in tmux:
make build
dist/snowcat-cockpit node install --listen 127.0.0.1:7682
dist/snowcat-cockpit node status
journalctl --user -u snowcat-cockpit.servicenode install captures only allowlisted non-secret configuration such as the
current PATH, provider config paths, pinned worker-image references, and the
paths to the protected observer and worker credential files. The service reads
both Snowcat tokens only at process start and gets a GitHub token from the
current gh login. It installs a
content-addressed release, enables startup with the user manager, and requires
both active systemd state and a version-matched loopback health response.
After rebuilding, rerun node install with the same node arguments to publish
and select the new release; node restart restarts the currently selected
release. An existing tmux-hosted node must be stopped explicitly before the
first install so it releases the selected port; the installer will not kill an
unknown process. node uninstall removes only the unit and service-selection
files. It retains prior releases, node state, managed sources, worker terminals,
and workspaces.
Open http://127.0.0.1:7682. The node creates only a stable, non-secret ID
under ${XDG_STATE_HOME:-$HOME/.local/state}/snowcat-cockpit. Retained worker
workspaces live under that state directory too, so an explicit --state-dir
override (or $XDG_STATE_HOME) MUST NOT resolve onto a tmpfs mount such as
/tmp: a tmpfs both loses retained workspaces on reboot and can fill and fail
tool output writes under sustained campaign load. It refuses a
non-loopback listen address. A provider's launch control is enabled only while
its live MCP receipt is current. Structural profile readiness verifies the
canonical Snowcat skills byte-for-byte against the revision locked into
Cockpit. install-kit is the only command that materializes those embedded
skills outside a Cockpit-owned isolated workspace; it never replaces a file
whose content differs from the lock.
In the Board campaign card, enroll each repository slug once. Cockpit
derives a retained source path beneath <state-dir>/sources; the operator does
not supply a path. Start all enrolled repositories then:
- clones missing managed sources or fetches their credential-free GitHub origins and pins immutable default-branch commits;
- refreshes the selected providers' Snowcat MCP preflight, with one immediate retry;
- observes all enrolled repositories and fills the three role capacities up to a combined maximum of 12 workers;
- remains running while proposals await human admission or Snowcat's verifier creates delayed review work.
After the start proof expires, Cockpit refreshes a provider only when its lane has eligible work. An idle campaign does not spend inference keeping receipts warm.
The local enrollment is execution configuration, not a copy or assertion of Snowcat's control-plane enrollment. Snowcat still decides claim eligibility. Campaign stop prevents new launches but never stops a worker or deletes a source, terminal, or workspace. On restart an interrupted campaign remains stopped until the operator explicitly starts it again.
While a campaign runs, it cleans a worker's workspace automatically once
nothing about it can still matter: its provider process exited (not failed)
and its Snowcat attempt reached a terminal outcome or its lane found nothing
to claim. SNOWCAT_COCKPIT_RETAIN_WORKSPACES bounds how many such workspaces
accumulate before cleanup catches up — a non-negative integer count (default
20) or a duration such as 6h; a worker that failed, lost its lease, or left
an unclean tree stays retained for explicit worker cleanup exactly as today.
Use serve --source-root <directory> to place retained managed sources outside
the state directory. No campaign configuration or state contains provider,
GitHub, observer, or worker lease credentials. See the
board-campaign contract for
the exact lifecycle and retry bounds.
Launch one retained host worker from the dashboard, or from the CLI:
go run ./cmd/snowcat-cockpit worker launch \
--provider codex \
--role discoverer \
--repository frostyard/firn \
--source /path/to/local/firn \
--base-ref HEAD
go run ./cmd/snowcat-cockpit worker launch \
--provider codex \
--role implementer \
--repository frostyard/firn \
--source /path/to/local/firn \
--base-ref HEAD
go run ./cmd/snowcat-cockpit workers
go run ./cmd/snowcat-cockpit worker observe worker-0123456789abcdef
go run ./cmd/snowcat-cockpit worker attach worker-0123456789abcdef
go run ./cmd/snowcat-cockpit worker stop worker-0123456789abcdef
go run ./cmd/snowcat-cockpit worker cleanup worker-0123456789abcdefLaunch creates a unique cockpit/<worker-id> branch and Git worktree. Stop
retains the workspace; cleanup refuses a running or dirty workspace and leaves
the branch intact. Observe makes one exact, read-only Snowcat correlation call;
the result is displayed but never written into the worker record.
For unattended Codex, Claude, and Copilot workers, build the three provider
targets of the one worker Containerfile in the selected runtime's local store
and pin launches to the resulting image IDs (the three share every layer;
oci/baseline.json lists what they guarantee). Podman remains the default:
make oci-image
# Run the three export commands printed by make oci-image.
read -rsp 'Snowcat worker token: ' SNOWCAT_MCP_TOKEN; echo
export SNOWCAT_MCP_TOKEN
read -rsp 'Cloudflare Access client ID: ' SNOWCAT_CF_ACCESS_CLIENT_ID; echo
export SNOWCAT_CF_ACCESS_CLIENT_ID
read -rsp 'Cloudflare Access client secret: ' SNOWCAT_CF_ACCESS_CLIENT_SECRET; echo
export SNOWCAT_CF_ACCESS_CLIENT_SECRET
export SNOWCAT_MCP_URL=https://snowcat.frostyard.org/mcp
go run ./cmd/snowcat-cockpit worker launch \
--adapter oci \
--runtime podman \
--provider copilot \
--role reviewer \
--repository frostyard/firn \
--source /path/to/local/firnFor explicit Docker compatibility, run make docker-image and use
--runtime docker with the printed SNOWCAT_COCKPIT_DOCKER_*_IMAGE exports.
Cockpit records whether the Docker daemon is rootless or rootful; rootful Docker
is not described as host isolation. Docker bridge workers resolve the fixed
Cloudflare Tunnel endpoint through ordinary DNS. Direct CLI launches may still
set SNOWCAT_COCKPIT_DOCKER_ADD_HOST to an explicit hostname:IPv4 value for
an independently managed endpoint; the checked-in wrapper does not synthesize
one.
Version tags publish all three worker images for linux/amd64 and
linux/arm64 to ghcr.io/frostyard/snowcat-cockpit-worker. The
Publish worker images workflow writes the immutable manifest reference for
each provider to its run summary; configure Cockpit with that full
name:tag@sha256:digest reference rather than the mutable tag alone.
OCI mode requires Linux containers, the selected runtime and its pinned image,
rootless posture when Podman is selected,
private regular non-symlink provider files at either
${CODEX_HOME:-$HOME/.codex}/{auth.json,config.toml} or
${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json or
${COPILOT_HOME:-$HOME/.copilot}/mcp-config.json, and
${GH_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/gh}/{hosts.yml,config.yml},
and a current provider preflight. The worker receives only those exact
files, its worktree, SNOWCAT_MCP_TOKEN, SNOWCAT_MCP_URL,
SNOWCAT_CF_ACCESS_CLIENT_ID, SNOWCAT_CF_ACCESS_CLIENT_SECRET, and GH_TOKEN
by environment-variable name. When the checked-in serve wrapper starts with an
OCI image configured, it
projects GH_TOKEN from the current gh keyring login unless the operator
already supplied it. It also projects the wrapper's fixed Snowcat URL by
environment-variable name and the Cloudflare Access service-token pair from the
protected worker credential file for every provider's worker-local lease relay.
Host mode remains the default interactive path. See
the rootless OCI worker contract for the complete
boundary.
Build a standalone binary with make build; the result is
dist/snowcat-cockpit.
- Bash 5 or newer
- tmux
- ttyd only for browser access
- An already configured coding-agent client and an existing isolated checkout for every writing worker
tmux retains the environment of the first slot for the life of the cockpit session. Start that first slot from the complete worker environment. If a credential or environment value changes, stop all slots and start a fresh one.
Launch harmless commands first:
bin/snowcat-cockpit start clock /tmp -- watch -n 1 date
bin/snowcat-cockpit start shell /tmp -- bash
bin/snowcat-cockpit list
bin/snowcat-cockpit attachLaunch one bounded Snowcat worker in an existing checkout:
bin/snowcat-cockpit work updex-1 codex \
/path/to/updex frostyard/updex issue-resolutionThe supported provider conveniences are codex, claude, and copilot.
Use start when a client needs extra flags:
bin/snowcat-cockpit start updex-2 /path/to/updex -- \
codex --model gpt-5.6-terra \
"Work the Snowcat queue for frostyard/updex. Claim at most one item, then stop."If ttyd is installed, expose the same tmux session on loopback:
bin/snowcat-cockpit web 7681Then open http://127.0.0.1:7681. The browser is a writable,
credential-bearing terminal. Keep it on loopback and place any remote access
behind an operator-controlled private boundary.
Clean up explicitly:
bin/snowcat-cockpit stop updex-1
bin/snowcat-cockpit stop updex-2make ci