# nucleic-runner container image

The Covalence runner's host-of-record container (design:
[docs/COVALENCE_RUNNER.md](../../docs/COVALENCE_RUNNER.md) §2, §9; narOS milestone N3:
[docs/NAROS.md](../../docs/NAROS.md) §7.2). Extends the **narOS agent tier**
(`ghcr.io/abkslm/naros-agent`, built from [`os/`](../../os) — it replaced
[`nucleic-sandbox`](../nucleic-sandbox)) with the `nucleicd` daemon, so a single container
is both the sync host of record and the agent sandbox — the CLOUD_RUNTIME `RunSpec.container =
nil` model, where the container *is* the sandbox. The base brings nash forced as `/bin/sh`
(docs/NASH.md §7.3), the narOS identity/manifest, and the full agent toolchain; this image
stamps the runner-tier VARIANT on top.

## Building

The `nucleicd` **Linux** build is real — the CLOUD_RUNTIME Phase-2 port landed: the full
NucleicProtocol test suite (Noise handshake vectors, relay-enrollment PoP, sealed-credential
crypto) passes on Linux under swift-crypto, GRDB runs on Linux, the approval server listens on
BSD sockets, secrets live in the 0600-file store, and the relay WebSocket is SwiftNIO. A booted
Linux `nucleicd` enrolls with the deployed relay and mints pairing codes — verified on **both**
aarch64 (glibc) and **x86_64** (the fully-static musl build, run under qemu).

- **Self-host on a Mac:** you don't need this image at all — `swift run nucleicd` runs the
  daemon natively, joining the mesh as a `PeerKind.cloud` host.
- **Container build — Cloudflare Containers run `linux/amd64` ONLY.** The image binary is a
  fully-static x86_64-musl cross-build (works at native speed from any host arch; the script
  installs the Static Linux SDK and gives its sysroot the SQLite that GRDB links):
  ```sh
  docker run --rm -v "$PWD":/src -w /src swift:6.3-bookworm scripts/build-nucleicd-linux.sh
  cp .build/x86_64-swift-linux-musl/release/nucleicd containers/nucleic-runner/
  docker build --platform linux/amd64 \
    -t registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner:v1 containers/nucleic-runner
  wrangler containers push registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner:v1
  ```

## Self-hosted variant (tailscale)

The image ships in two flavours, chosen by the `WITH_TAILSCALE` build arg:

- **Nucleic-hosted (default, `WITH_TAILSCALE=0`)** — what CI builds and pushes to Cloudflare.
  These runners are reachable **only over Nucleic's relay infra** (`nucleicd` listens relay-only),
  so no tailscale is installed; it would be dead weight the cloud runner never uses.
- **Self-hosted (`WITH_TAILSCALE=1`)** — for an operator who wants the container reachable over
  **their own tailnet**. This installs the OS-level `tailscale` package. Build + tag it separately:
  ```sh
  docker build --platform linux/amd64 --build-arg WITH_TAILSCALE=1 \
    -t nucleic-runner:self-hosted containers/nucleic-runner
  ```
  `nucleicd`'s own Tailnet transport (`TailscaleKit`) is Darwin-only with no Linux build, so on
  Linux "tailscale" means the **system daemon**: the operator supplies a `TS_AUTHKEY` and runs
  `tailscaled` + `tailscale up` alongside `nucleicd` (nothing starts it automatically — the default
  entrypoint is just the daemon). A Mac self-host needs no container at all — see **Building** above.

## Registry — Cloudflare's managed registry (not GHCR)

The image the `containers` block deploys lives in **Cloudflare's own managed registry**
(`registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner`), not GHCR: Cloudflare Containers /
Wrangler don't support GHCR as a pull source, and any external registry needs an extra
`wrangler containers registries configure` + pull credentials. The managed registry needs
none of that — `wrangler containers push` authenticates with the account's
`CLOUDFLARE_API_TOKEN` and Cloudflare pulls it at deploy time with no further config.
`.github/workflows/runner-image.yml` does the build + push in CI (below); the base
`naros-agent` image stays on GHCR because it's only pulled at *build* time (by Docker in
CI), never by Cloudflare. The FROM tag is pinned by the Dockerfile's `NAROS_VERSION` arg,
whose default must match `os/VERSION` (CI asserts the lockstep) — and the tag exists only
after `.github/workflows/naros.yml` has published that release, so sequence a version bump
as: naros.yml green first, then this image.

One gate before this image deploys to Cloudflare: `runner-image.yml` must run once (on a
`main`/`dev` push or via `workflow_dispatch`) to publish
`registry.cloudflare.com/<ACCOUNT_ID>/nucleic-runner:v1` — the `containers` block in
[`cloud/nucleic-runner/wrangler.jsonc`](../../cloud/nucleic-runner/wrangler.jsonc) references it.
The rest of the runner stack (Worker + pool DO) deploys and is tested without it.

## Runtime contract

The pool (`RunnerPool` DO → `RunnerContainer` DO) injects at start:
`NUCLEIC_RUNNER_{POOL_ID,INSTANCE_ID,EPOCH,INSTANCE_TOKEN,CONTROL_URL}` and optional
`NUCLEIC_RELAY_URL`. `nucleicd` enrolls with the relay, listens relay-only, heartbeats the pool
with its fencing epoch, and serves a one-shot pairing payload on loopback `:9200` for the
provisioning proxy.
