GitHub Codespaces Nodes
Run a temporary, browser-accessible Cacophony node from GitHub Codespaces.
When to use this path
- Spin up a clean-room node for a one-off experiment.
- Share a session with a collaborator who has repository access.
- Burst agent work away from your local CPU.
- Attach from the web, TUI, or Android surfaces through the rendezvous path.
Minimal flow
REPO="owner/repo"
# 1. Resolve bootstrap material, create the Codespace, install caco there,
# and enroll it as a dynamic Cacophony node.
caco codespace new --repo "$REPO" --display-name my-experiment
# If your org does not offer the default machine (`basicLinux32gb`), pass
# --machine explicitly or export CACO_CODESPACE_MACHINE.
# To target an enterprise GitHub host (e.g. ghe.example.com), pass --host or set
# CACO_CODESPACE_HOST; it also derives from the --repo/--source-repo URL host.
# caco codespace new --repo "owner/repo" --host ghe.example.com
# 2. Verify the enrolled `cs-<hash>` node is visible.
caco node list
caco codespace ls
# 3. Spawn an agent on the new node.
CS_HASH="8hexhash"
AGENT_ID="agent-id"
caco agent new --node "cs-${CS_HASH}" --type claude --project myproj
# 4. Attach from the usual surfaces.
caco agent attach --id "$AGENT_ID"
What the command does
-
Resolve bootstrap material first
The bootstrap URL comes from
--rendezvous,CACO_BOOTSTRAP_URL, legacyCACO_RENDEZVOUS_URLorpki.bootstrap.public_url; the token fromCACO_BOOTSTRAP_TOKEN, legacyCACO_ENROLL_TOKEN,CACO_BOOTSTRAP_TOKEN_FILE,~/.cacophony/tokens/node.tokenor the configuredpki.bootstrapsources. If either is missing, nothing is provisioned. The URL must be reachable from GitHub: private, loopback, RFC1918, unique-local, link-local and Tailscale/CGNAT literal IPs are refused unlessCACO_CODESPACE_ALLOW_PRIVATE_BOOTSTRAP=1marks an operator-managed tunnel. -
Create the Codespace
gh codespace create --machine "$MACHINE" --default-permissions, with no interactive prompts;MACHINEisCACO_CODESPACE_MACHINEorbasicLinux32gb. For an enterprise host,--hostorCACO_CODESPACE_HOST(or the host in--repo) setsGH_HOSTfor creation, secrets and the SSH bootstrap alike. -
Project secrets for first start
gh codespace user-secret setstores the bootstrap token and URL,CACO_NODEand readable provider material such asCACO_LITELLM_MASTER_KEY,CACO_GOOGLE_API_KEYandCACO_PI_AUTH_JSONas per-Codespace secrets, passed throughGH_USER_SECRET_VALUErather than process arguments. During bootstrap they move into owner-only files under~/.cacophony/secrets/codespace/, and theCACOPHONY_*_FILEvariables point there. -
Enroll over SSH
Unless
--no-waitis given, it opensgh codespace ssh, clones Cacophony alongside when the target repository lacks its devcontainer hook, installscacoif missing (falling back tocargo install --path crates/caco), runscaco codespace enroll, which joins through/v1/bootstrap/join, writes~/.cacophony/bin/run-caco-daemon.shand runscaco up --skip-update. The script is sent as a remote shell command, so repositories that are not Cacophony enroll reliably too. -
Report
It prints the Codespace name, the derived
cs-<hash>node ID, the secret projection status and the enrollment result. Confirm the node withcaco node listorcaco codespace ls, then remove or rotate the temporary Codespace secrets.
public_host and public_cluster_port that are both reachable from GitHub and present in the node certificate SAN. For example, the root cluster's ms-mac node advertises the operator-managed ms-mac.example.ts.net Funnel DNS name on port 443 rather than either the private Tailnet IP or a Tailnet-only alias, so peer probes validate before beads-primary traffic routes. Static private-mesh peers continue to use the node's private host:cluster_port; the public endpoint is only the alternate route for dynamic/public callers that cannot reach the private mesh address.
Security notes
- Bootstrap tokens are sensitive operator bootstrap material and should be rotated or removed from Codespaces secrets after enrollment.
- Do not paste full
env/printenvoutput from a Codespace into logs or beads; first-party bootstrap failure output redacts obvious token/key assignments, but manual shell diagnostics can still expose secrets. - Operator-host mesh master keys stay on the operator host.
caco codespace newprojects only the repository runtime provider material it can read from the operator host. Add per-task or project-specific extra secrets through GitHub Codespaces user secrets or the repo-owned container secret-file path.- Routine mesh traffic should continue to use the relay/rendezvous path rather than exposing arbitrary ports.
Implemented commands
caco codespace new, ls, stop, resume, revoke, remove, rekey, and enroll. The design-stage caco codespace secret ... commands are not implemented yet.
Lifecycle commands
| Command | Purpose |
|---|---|
caco codespace ls | List live GitHub Codespaces and derived cs-${CS_HASH} node IDs. |
caco codespace status | Read-only desired-vs-real reconcile view over declared codespace_nodes pools (consumes Config::reconcile_codespace_nodes); add --json for machine output. Resolves real per-member mesh liveness from the daemon node list (reachable-joined / unreachable-declared / missing), degrading network-resiliently to unknown when the daemon view is unavailable. Pure inspection: no Codespace creation, secrets, or mesh mutation. |
caco codespace reconcile --dry-run | Read-only desired-vs-real reconcile plan over declared codespace_nodes pools: renders which pool members to create / no-op / leave-suspended / skip by reusing the same Config::reconcile_codespace_nodes core as status, with no cloud mutation, cert issuance, or secret projection. Without --dry-run it errors rather than implying a mutation it cannot perform; the mutating reconcile verbs remain deferred. |
caco codespace stop "cs-${CS_HASH}" | Stop a Codespace while preserving its volume. |
caco codespace resume "cs-${CS_HASH}" | Wake a suspended Codespace through GitHub SSH. |
caco codespace revoke "cs-${CS_HASH}" | Remove the dynamic mesh peer pin while leaving the Codespace available for inspection. |
caco codespace rekey "cs-${CS_HASH}" | Prepare explicit identity rotation after a stale pubkey pin or rebuild. |
# caco codespace remove "cs-${CS_HASH}" | Destructive: uncomment only after caco codespace ls confirms the stopped Codespace is disposable; prefer stop first. |
caco codespace enroll [--reinit] | Enroll or repair enrollment from inside the Codespace using CACO_NODE, CACO_BOOTSTRAP_URL, and CACO_BOOTSTRAP_TOKEN. |
caco ssh "cs-${CS_HASH}" -- <command> | SSH-family operator access for Codespace nodes; resolves the node ID through caco codespace ls and delegates to gh codespace ssh. |
caco "@cs-${CS_HASH}" version / caco "@cs-${CS_HASH}" tui | Run a caco command or TUI in the live Codespace through the same GitHub Codespaces SSH path. |
codespace_nodes pool (planned, design-stage)
caco codespace new provisions one Codespace at a time, and each becomes a dynamic cs-<hash> mesh peer. For recurring pools, the codespace_nodes config key declares the desired set of Codespace-hosted microVM nodes instead.
- Shipped: the
codespace_nodeskey and schema, expanded into ordinary node entries at load time;caco codespace status, which compares each declared member with its live mesh state; andcaco codespace reconcile --dry-run, which shows which members would be created, left alone, left suspended or skipped. Both are read-only, andreconcilewithout--dry-runrefuses rather than pretending to act. - Not yet built: the commands that create, suspend or delete Codespaces (
reconcilefor real,init,down,destroy). They need the operator's GitHub identity plus Tailscale and certificate projection; treat the commands described below as design intent, not something you can run. The design reference isdocs/investigations/codespace-nodes-design.md.
Design summary
codespace_nodes:
cs: # prefix; becomes cs-0, cs-1, ...
github_identity: harryaskham_microsoft # which gh account/token
host_project: infinity-microsoft/picasso # repo the codespace is created from
machine: largePremiumLinux # 16c/64GB/128GB (verified available)
replicas: 3
# optional: variant (isolated default), ts_tag, region/idle_timeout, ...
At config load the codespace_nodes.<prefix> map with replicas: N desugars into N real NodeEntrys merged into config.nodes named prefix-0 .. prefix-(N-1). Each entry gets the standard static-node shape (Tailscale MagicDNS host, microVM SSH, daemon service, speech: disabled, role: default, and health_expectations.expected_unreachable: true so the fleet/health surfaces treat Codespace auto-suspend as expected), plus a provenance marker so the reconcile path and operator surfaces know the entry is Codespace-backed. This is the key design choice: desugar into nodes, not into dynamic_nodes (dynamic_nodes are regex join-templates; codespace_nodes are concrete known-name static nodes). Every existing node resolver — caco node list, caco ssh, @prefix-1, fleet/health, beads routing, TUI Nodes — then sees them as ordinary static nodes with no Codespace-awareness required.
Reconcile commands (read-only views implemented; mutating verbs planned)
The only Codespace-aware operator surface is the reconciler. Its read-only desired-vs-real views have shipped; the mutating convergence verbs remain planned. Each invocation compares the desired pool against live mesh state:
| Command | Status / behavior |
|---|---|
caco codespace status | Implemented. Read-only desired-vs-real reconcile view over declared codespace_nodes pools, resolving real per-member mesh liveness (--json for machine output). Pure inspection — no creation, secrets, or mesh mutation. |
caco codespace reconcile --dry-run | Implemented. Read-only reconcile plan (which members to create / no-op / leave-suspended / skip), reusing the same Config::reconcile_codespace_nodes core as status; without --dry-run it errors rather than implying a mutation it cannot perform. |
caco codespaces init / caco codespaces reconcile / caco codespaces up | For each desugared prefix-K, check whether a live cert-joined caco node prefix-K is in the mesh. If yes, no-op. If no, idempotently gh codespace create, caco cert issue --node prefix-K (authority side, same path as the micro-0 proof), push the cert plus Tailscale auth key (tagged tag:caco-cs-pool) plus joined config as Codespace secrets / bootstrap inputs, run the in-Codespace microVM bootstrap, and wait until prefix-K reports healthy/cert-joined. |
caco codespaces down / caco codespaces destroy | Suspend or delete the backing Codespace(s) for a prefix; the node-id-to-Codespace-name mapping reuses the existing stop/resume resolution. |
The in-Codespace bootstrap is the same proven path captured in the micro-0 rollout runbook (/dev/kvm access, cloud-hypervisor plus seed cache, caco microvm preflight, guest Tailscale join, caco microvm node up, then caco up --skip-update). See docs/microvm-node-rollout.md for the verified per-node sequence the reconciler will drive.
Why static-node desugaring, not dynamic
The verified feasibility pass (this design's source) confirmed that a GitHub Codespace on Azure exposes /dev/kvm, kvm_intel nested=Y, vmx/svm, cgroup v2, /dev/net/tun, and /dev/vhost-vsock, with passwordless sudo; caco microvm preflight passes every required hardware/kernel check. The only bootstrap gaps are the hypervisor binary and the cache directories, which the per-pool init handles. Because each prefix-K is a concrete known-name node with a stable Tailscale MagicDNS hostname, treating them as static NodeEntrys lets every existing node resolver work unchanged. The separate dynamic_nodes template surface stays reserved for regex-named join patterns (for example the existing cs- rendezvous path used by ad-hoc caco codespace new Codespaces); the two surfaces are complementary, not overlapping.
Decomposition (planned beads)
- cs-config-schema: add the
codespace_nodes.<prefix>model + schema entry + unknown-key validation + theexpand_codespace_nodes()load-time pass that appends syntheticNodeEntrys toconfig.nodeswith provenance. - cs-cert-join-projection: authority-side cert issuance plus Codespace secret projection for the joined config, Tailscale auth key, and bootstrap inputs the in-Codespace path expects.
- cs-codespaces-reconcile: the
caco codespaces reconcile/init/status/downcommands and their convergence loop. - cs-first-real-init: end-to-end provisioning of the first
cs-Nnode through this path on a 64 GB Codespace. - cs-micro0-decommission: retire the static
micro-0proof node in favour of thecs-Npool (config slice landed in 7eb8d47164; operator-action destructive cleanup tracked in).
Container-hosted operation
The repo-owned deploy/codespaces/ path runs an already-published Cacophony image inside the Codespace. It is runtime-only: CACO_CODESPACE_IMAGE must point at an image produced by Azure remote build, and the compose file must not grow a local build: stanza.
mkdir -p deploy/codespaces/runtime/{state,secrets}
# Write secret values directly to files instead of exporting plaintext tokens.
# Example: your-secret-command bootstrap-token > deploy/codespaces/runtime/secrets/bootstrap-token
export GIT_SHA="$(git rev-parse --short HEAD)"
export ACR_LOGIN_SERVER="${CACO_ACR_LOGIN_SERVER:?set ACR login server}"
export CACO_CODESPACE_IMAGE="${ACR_LOGIN_SERVER}/cacophony:${GIT_SHA}"
export CACO_CODESPACE_CONFIG="$HOME/.cacophony/config.yaml"
just codespaces-container-validate
docker compose -f deploy/codespaces/docker-compose.yml pull
docker compose -f deploy/codespaces/docker-compose.yml up -d
Secrets today
Codespaces start with no application secrets. caco codespace new only attempts to project bootstrap values used for enrollment: CACO_BOOTSTRAP_TOKEN, CACO_BOOTSTRAP_URL, CACO_NODE, plus legacy aliases CACO_ENROLL_TOKEN and CACO_RENDEZVOUS_URL. The bootstrap path uses per-Codespace user secrets through gh codespace user-secret set NAME --codespace "$CODESPACE_NAME" and passes values through GH_USER_SECRET_VALUE instead of command-line arguments. The repo bootstrap immediately materializes known token variables into files under ~/.cacophony/secrets/codespace/ and unsets plaintext variables before running caco up.
Per-task provider-secret commands are not available yet. For now, use these supported paths and prefer *_FILE variables wherever possible:
- Native devcontainer: set provider secrets with
gh secret set --user --app codespacesor the GitHub UI, supplying values from stdin or a file. Example:PROVIDER_API_KEY="${PROVIDER_API_KEY:?set provider API key}"; printf '%s' "$PROVIDER_API_KEY" | gh secret set PROVIDER_API_KEY --user --app codespaces --repos "$REPO". Before deleting a Codespaces user secret, rungh secret list --user --app codespacesand confirm no other Codespace or repository still depends on that user-level value. - Repo-owned container flow: mount config from
CACO_CODESPACE_CONFIGand project secret files underdeploy/codespaces/runtime/secrets/.
Troubleshooting
gh codespace createreportsno terminal: use a currentcaco codespace newbuild, which passes--machineand--default-permissions; if your org has a different machine allow-list, passMACHINE="allowed-machine"; caco codespace new --repo "$REPO" --machine "$MACHINE".- Bootstrap token missing or expired: push fresh
CACO_BOOTSTRAP_TOKENandCACO_BOOTSTRAP_URLvalues from the operator host, then runcaco codespace enrollandcaco upinside the Codespace. - Stale peer pin / rebuild: run
caco codespace rekey "cs-${CS_HASH}"on the operator host, thencaco codespace enroll --reinitinside the Codespace. - Bootstrap authority unreachable: current
caco codespace newbuilds reject private literal bootstrap IPs before provisioning. Verify the configured bootstrap authority URL and public reachability from GitHub-hosted Codespaces; if you bypassed the guard withCACO_CODESPACE_ALLOW_PRIVATE_BOOTSTRAP=1, confirm the operator-managed tunnel is active before retrying enrollment. caco ssh cs-$CS_HASH/caco @cs-$CS_HASH tui: Codespace nodes are dynamic nodes, but they have a first-party SSH-family exception because GitHub owns the transport. The CLI resolves the derived node ID throughcaco codespace lsand delegates togh codespace ssh -c "$CODESPACE_NAME" -- ..., so nodynamic_nodes.codespace.sshhost metadata is required.- Suspended node looks offline: use
caco codespace ls --refresh; suspended Codespaces should render as expected offline rather than generic peer failure.
For design-stage background, use the internal architecture and key-distribution notes in the repository tree. Treat examples for not-yet-shipped subcommands as design notes, not operator commands.