Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 container command/args, so the pod runs caliband --help → prints usage → exits → CrashLoop.
  • caliband takes --workspace-root <path> as a required flag (there is no env for it); #283 set CALIBAN_WORKSPACE_ROOT env, which caliband ignores.
  • The network switch is --listen <host:port> (or CALIBAN_DAEMON_LISTEN, format host:port); #283 set CALIBAND_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 /work PVC, so the workspace is empty.
  • caliband's --tls-cert/--tls-key/--token are optional, so --listen without them is valid plaintext TCP.

Decision

  1. Run caliband as a daemon via container args. build_sandbox sets the caliband container's args to ["--workspace-root", <workspace_root>, "--listen", "0.0.0.0:<caliband_port>"], replacing the image's --help CMD (the caliband ENTRYPOINT is kept). This uses only flags caliband actually parses.

  2. Drop the mismatched env. Remove CALIBAND_LISTEN, CALIBAN_WORKSPACE_ROOT, and CALIBAN_WORKSPACE_SOURCES (none are read; listen + root are now flags, sources are handled by the init container). Keep GONZALO_ENDPOINT / CALIBAN_ROUTER_CONFIG_REF (consumed by the caliban agent runtime, harmless if unset).

  3. Populate the workspace with a git-clone init container. Add an init container (image from a new git_image setting, default a public alpine/git) that mounts the workspace volume and, for each spec.workspace.sources[], clones repo at ref into path — idempotently (skip if <path>/.git already 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.

  4. Plaintext TCP for the first e2e. The operator runs caliband with --listen and 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 CalibanTask now 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_sandbox now passes --advertise-host <sandbox>.<ns>.svc.cluster.local and 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-key but 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 in Running. build_sandbox now also passes --tls-ca <mount>/ca.crt (the key was already in the mounted kubernetes.io/tls Secret) and publishes CALIBAN_CONTROL_TLS_CA + CALIBAN_CONTROL_TLS_SERVER_NAME on the caliband container, which the workers inherit. The verified name comes from the new session_server_name setting and must equal the serving cert's SAN — build_status_client's localhost default never matches in-cluster. caliban #510 replaces the env inheritance with explicit forwarding in ExecWorkerLauncher.
  • The verified name must be on argv, not only in the env — resolved (#35). Once caliban #510 landed, that explicit forwarding became the problem: ExecWorkerLauncher sets each worker's CALIBAN_CONTROL_TLS_SERVER_NAME from 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 in Running even with the CA correctly in place. build_sandbox now 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 — a ref set to a raw commit SHA will fail the clone (the CRD's ref defaults to main; SHA-pinning is a follow-up).
  • GONZALO_ENDPOINT was 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 its storage.substrate / storage.remote.url / storage.remote.token_env settings. (CALIBAN_ROUTER_CONFIG_REF was the same kind of mistake, resolved in #44.) caliban #659 added environment overrides for those settings, and spec.state now projects CALIBAN_STORAGE_SUBSTRATE (local → fs, remote → remote), CALIBAN_STORAGE_REMOTE_URL, and, with state.tokenRef, CALIBAN_STORAGE_REMOTE_TOKEN_ENV=GONZALO_TOKEN with GONZALO_TOKEN from 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_NAME on 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_vars keeps them out.
  • The cluster DNS domain is no longer hardcoded — resolved (#54). The advertise-host note above writes <sandbox>.<ns>.svc.cluster.local as a fixed suffix. The domain is now CALIBAN_CLUSTER_DOMAIN, defaulting to cluster.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.