ADR 0003 · The caliband launch contract (daemon args + workspace init)
- Status: accepted
- Date: 2026-07-05
- Source: caliban #366 (follow-up to #283) · epic #274 · builds on ADR 0002
Context
ADR 0002 reconciled a CalibanTask into a
correctly-shaped Sandbox/SA/NetworkPolicy but explicitly deferred the exact caliband
container contract ("the exact caliband env contract isn't built yet"). A home-cluster
dry run exposed that the deferred contract is load-bearing: the pod as built by #283
would not run a live caliband. Verified against caliban's caliband.rs arg parser and
the caliban image's Dockerfile:
- The image is
ENTRYPOINT ["caliband"],CMD ["--help"]. #283 set no containercommand/args, so the pod runscaliband --help→ prints usage → exits → CrashLoop. - caliband takes
--workspace-root <path>as a required flag (there is no env for it); #283 setCALIBAN_WORKSPACE_ROOTenv, which caliband ignores. - The network switch is
--listen <host:port>(orCALIBAN_DAEMON_LISTEN, formathost:port); #283 setCALIBAND_LISTEN=tcp://…— wrong name and wrong format. CALIBAN_WORKSPACE_SOURCES(an invented env) is read by nothing, and no one clones the workspace sources into the/workPVC, so the workspace is empty.- caliband's
--tls-cert/--tls-key/--tokenare optional, so--listenwithout them is valid plaintext TCP.
Decision
-
Run caliband as a daemon via container
args.build_sandboxsets the caliband container'sargsto["--workspace-root", <workspace_root>, "--listen", "0.0.0.0:<caliband_port>"], replacing the image's--helpCMD (thecalibandENTRYPOINT is kept). This uses only flags caliband actually parses. -
Drop the mismatched env. Remove
CALIBAND_LISTEN,CALIBAN_WORKSPACE_ROOT, andCALIBAN_WORKSPACE_SOURCES(none are read; listen + root are now flags, sources are handled by the init container). KeepGONZALO_ENDPOINT/CALIBAN_ROUTER_CONFIG_REF(consumed by the caliban agent runtime, harmless if unset). -
Populate the workspace with a git-clone init container. Add an init container (image from a new
git_imagesetting, default a publicalpine/git) that mounts the workspace volume and, for eachspec.workspace.sources[], clonesrepoatrefintopath— idempotently (skip if<path>/.gitalready exists, so pause/resume and pod restarts over the persistent PVC don't refetch or fail). Public sources only for now; private-source auth is a follow-up. -
Plaintext TCP for the first e2e. The operator runs caliband with
--listenand no TLS/token. This is sufficient to prove the pipeline (a reachable caliband on the Sandbox DNS) and matches caliban's optional-TLS design. Provisioning per-Sandbox certs- a bearer token (and wiring prospero's matching client) is a tracked security follow-up — the transport already supports it (caliban #280).
Consequences
- Applying a
CalibanTasknow yields a caliband that actually starts, has its workspace cloned, and listens on the Sandbox endpoint — closing the gap between "objects render" and "an agent can run". This unblocks the home-cluster e2e. - Plaintext TCP is a deliberate, temporary posture. The default-deny NetworkPolicy (ADR 0002) still constrains who can reach the port, but the control/stream bytes are unencrypted and unauthenticated until the TLS/token follow-up lands. Not for multi-tenant/production use.
- The init container needs a git image and egress to clone sources — the ADR 0002 NetworkPolicy already allows general egress. Private sources will need credential projection (deferred).
advertise-host/ per-agent ports — resolved (#24/#25). Control-plane reachability (serviceFQDN:<port>) worked, but per-agent attach dialing from outside the pod (prospero's session plane) needed--advertise-host <serviceFQDN>+ per-agent port routing.build_sandboxnow passes--advertise-host <sandbox>.<ns>.svc.cluster.localand pins--agent-port-base, and the NetworkPolicy opens the matching per-agent stream window alongside the control port.- Session-plane TLS needs the CA, not just the keypair — resolved
(#32). The TLS follow-up to
decision 4 landed
--tls-cert/--tls-keybut no--tls-ca, so caliband had no trust anchor to hand down to the workers it spawns. Their Idle/Running reports failed the handshake and were silently dropped, leaving interactive agents stuck inRunning.build_sandboxnow also passes--tls-ca <mount>/ca.crt(the key was already in the mountedkubernetes.io/tlsSecret) and publishesCALIBAN_CONTROL_TLS_CA+CALIBAN_CONTROL_TLS_SERVER_NAMEon the caliband container, which the workers inherit. The verified name comes from the newsession_server_namesetting and must equal the serving cert's SAN —build_status_client'slocalhostdefault never matches in-cluster. caliban #510 replaces the env inheritance with explicit forwarding inExecWorkerLauncher. - The verified name must be on argv, not only in the env — resolved
(#35). Once caliban #510
landed, that explicit forwarding became the problem:
ExecWorkerLaunchersets each worker'sCALIBAN_CONTROL_TLS_SERVER_NAMEfrom caliband's own resolved name, overriding the value the worker used to inherit from the pod env. Absent--tls-server-name, caliband resolves that name from--advertise-host(the per-pod DNS name), which the serving cert cannot prove — its only SAN is the session-plane name. So workers verified against the wrong name, the handshake failed, and interactive agents were still stuck inRunningeven with the CA correctly in place.build_sandboxnow passes--tls-server-name <session_server_name>alongside--tls-ca. The general lesson: when a downstream component forwards a value explicitly, the env var stops being the contract — argv is. git clone --depth 1 --branch <ref>accepts branch/tag names only, not commit SHAs — arefset to a raw commit SHA will fail the clone (the CRD'srefdefaults tomain; SHA-pinning is a follow-up).GONZALO_ENDPOINTwas never consumed — resolved (#41, #53). Decision 2 kept it as "consumed by the caliban agent runtime", but nothing in caliban reads it: caliban reaches a remote gonzalod only through itsstorage.substrate/storage.remote.url/storage.remote.token_envsettings. (CALIBAN_ROUTER_CONFIG_REFwas the same kind of mistake, resolved in #44.) caliban #659 added environment overrides for those settings, andspec.statenow projectsCALIBAN_STORAGE_SUBSTRATE(local→fs,remote→remote),CALIBAN_STORAGE_REMOTE_URL, and, withstate.tokenRef,CALIBAN_STORAGE_REMOTE_TOKEN_ENV=GONZALO_TOKENwithGONZALO_TOKENfrom the referenced Secret. The operator sets caliban's settings by environment rather than authoring its settings file (ADR 0005). Requires a caliban build that includes #659.- The control-plane TLS env vars are no longer published — resolved (#59).
The note above still says the operator "publishes
CALIBAN_CONTROL_TLS_CA+CALIBAN_CONTROL_TLS_SERVER_NAMEon the caliband container, which the workers inherit". It no longer does. caliband ≥ 0.8.0 (caliban#510, #512) sets both on each worker it spawns, explicitly, from its own--tls-ca/--tls-server-name— and the operator passes both flags. Publishing the same values in the pod env was therefore a second mechanism that the argv overrides and that could only drift from it, so it was removed. Inheritance is not how the workers get these values; caliband sets them.caliband_env_carries_no_control_plane_tls_varskeeps them out. - The cluster DNS domain is no longer hardcoded — resolved (#54). The
advertise-hostnote above writes<sandbox>.<ns>.svc.cluster.localas a fixed suffix. The domain is nowCALIBAN_CLUSTER_DOMAIN, defaulting tocluster.local, so the behaviour described is the default rather than the only possibility. A cluster served under another domain no longer gets an unreachable advertise host.