# Nucleic — Antimatter Runner

> **Status (2026-07-10): designed; foundation implemented and verified end to end** — a Linux
> `nucleicd` (both arches, incl. Cloudflare's required linux/amd64) boots, enrolls with the
> **deployed production relay**, listens on it, and mints working pairing codes; the Cloudflare
> control plane is written and unit-tested; the wire-protocol extensions are landed and
> capability-gated. **The dispatch gap is closed and the first real agent turns have run on a
> Linux runner**: a fresh runner seeds projects at boot (`NUCLEIC_RUNNER_PROJECTS`) or over the
> wire (`ClientMsg.createProject` → `HostMsg.projectCreated`, gated on
> `WireCapabilities.canCreateProjects`, with the iOS Add Project sheet driving it), and real
> `claude` sessions — dispatched via `startChat` through the **deployed production relay**,
> streaming events back, with a destructive Bash call surfacing an approval that a remote
> client answered over the wire — completed end to end on Linux (`nucleic-smoke`, §11.1).
> **Cloudflare container enablement (item 3) is code-complete**: the loopback `:9200` pairing
> endpoint is live (curl-verified), the presence→wake hook is built + tested on both Workers,
> and both image-publish CI workflows are authored. What remains is purely operational: run
> the two CI workflows, uncomment the `containers` block, and deploy with the secrets
> (`RUNNER_TOKEN_SECRET`, `RUNNER_ADMIN_SECRET`, `RUNNER_WAKE_SECRET` ×2 + `RUNNER_WAKE_URL`).
> **Settings→pool (item 4) is wired**: `RunnerPoolClient` PoP-enrolls, provisions, pushes the
> knobs, lists instances, and pulls the one-shot pairing code from the Settings pane; only
> mesh-riding the pool credential remains of it. Next up: the credential mesh device side
> (item 5). §11 is the handoff brief for whoever picks this up.
>
> The Antimatter Runner is a headless Nucleic host a user can dispatch agents onto *as if it
> were a macOS host*. It joins the user's Antimatter mesh as `PeerKind.cloud`, speaks the
> existing Noise/CBOR sync protocol through the deployed relay, and provides the same backend
> functionality the macOS app's backend provides. First deployment target is the Cloudflare
> developer platform (Workers + Durable Objects + Containers); self-hosting (plain container,
> or a spare Mac running `nucleicd` directly) is a stated goal and shapes every seam below.
>
> Builds on [CLOUD_RUNTIME.md](CLOUD_RUNTIME.md) (the `nucleicd` port thesis) and
> [MESH_TRANSFER.md](MESH_TRANSFER.md) (the mesh the runner joins). Where CLOUD_RUNTIME is the
> business/platform plan, this doc is the runner's *product + integration* design: mesh
> membership, virtualization tiers, intelligence delegation, the credential mesh, and the
> load balancer + Settings surface.

## 0. Status

### 0.1 Done (implemented + verified)

| Piece | Where | State |
|---|---|---|
| Runner control plane (Worker + `RunnerPool`/`RunnerContainer` DOs) | `cloud/nucleic-runner/` | Implemented + unit-tested (32 node:test tests; typecheck + wrangler dry-run clean; `make runner-{test,build,deploy}`). KV namespace provisioned; NOT yet `wrangler deploy`ed (needs the two secrets, §8) |
| Wire protocol: intelligence delegation + sealed credential mesh | `Sources/NucleicProtocol/Sync/RunnerMessages.swift` + envelope/capability plumbing in `MessageEnvelope`/`WireMessages`/`PeerTypes`/`SyncClient` | Implemented + tested; additive, capability-gated; every exhaustive switch handled (ConnectionHandler, iOS RemoteStore ×2, HostConnection) |
| Sealed-credential crypto (`SealedCredentialBox`) | `Sources/NucleicProtocol/Sync/SealedCredential.swift` | Implemented + tested (X25519 + HKDF + ChaChaPoly; AAD binds the credential kind); passes on Darwin AND Linux |
| `Welcome.hostKind` + `AppStore.meshSelfKind` + `PeerClient.Config.selfKind` | protocol + core | A runner truthfully pins as `.cloud` on peers; transfer destinations filter on `canOwnSessions` (not `== .mac`) |
| `nucleicd` headless host daemon | `Sources/nucleicd/` | Runs on **macOS** (self-host rung) and **Linux** — the CLOUD_RUNTIME Phase-2 port landed (§9). Boot-verified on aarch64-glibc and on **x86_64** (fully-static musl cross-build via `scripts/build-nucleicd-linux.sh`, run under qemu): enrolls with the production relay via PoP, `listening on: relay`, mints working pairing codes, clean SIGTERM shutdown, pool heartbeats with epoch fencing (409 → exit 75) |
| Linux protocol conformance | `swift test --filter NucleicProtocolTests` on Linux | All 162 tests green under swift-crypto — Noise handshake vectors, relay-enrollment PoP (byte-pinned against the Worker), sealed boxes. The CLOUD_RUNTIME go/no-go, answered **go** |
| Runner container image | `containers/nucleic-runner/` | Dockerfile ships the static amd64 `nucleicd`; proven build recipe scripted (`scripts/build-nucleicd-linux.sh`) |
| Settings ▸ Antimatter ▸ Runner UI, wired to the pool (item 4) | `RunnerSection` in `Sources/NucleicApp/RemoteAccessView.swift` → `AppStore.runnerPoolClient()` → `RunnerPoolClient` (`Sources/NucleicCore/Sync/RunnerPoolClient.swift`); PoP enroll via `RunnerEnrollment` (NucleicProtocol, context `nucleic-runner-enroll-v1:`) | Implemented: knob changes debounce-push `PATCH /settings`, Provision/Refresh drive `/provision` + `/status` (per-instance list), the one-shot pairing code surfaces after provision. Proof byte-pinned against the Worker beside the relay vectors (RelayEnrollmentTests — 169 protocol tests green on Linux); credential in Keychain (Darwin) / 0600 file (Linux) with the 401-re-enroll retry. Mesh-riding the pool credential remains (item 4 note) |
| Project seeding — both layers of the former dispatch gap | `NUCLEIC_RUNNER_PROJECTS` boot seeding in `Sources/nucleicd/Nucleicd.swift`; `ClientMsg.createProject` (`WireCreateProjectRequest` in `RunnerMessages.swift`, gated on `WireCapabilities.canCreateProjects`) → `SyncHostBridge.createProject` → `AppStore.addClonedProject` (controlled clone; `branch` supported), answered by `HostMsg.projectCreated` correlated on `requestID` | Implemented + tested (wire round-trip/legacy tests; iOS switches handled). Idempotent by normalized origin remote (`AppStore.projectMatching(remote:)`) — a reboot or client retry never stacks `repo-2` clones. Boot-smoke-verified on Linux |
| Phone-initiated project creation (CLOUD_RUNTIME §6) | iOS `ProjectsView` Add Project sheet → `RemoteStore.createProject` (host picker over live hosts advertising `canCreateProjects`; `addProject` state settled by the correlated `projectCreated` reply) | Implemented; iOS builds. The project row itself arrives via the dashboard push |
| Loopback control endpoint (item 3d) | `Sources/nucleicd/ControlEndpoint.swift`: `GET /pairing` (one-shot mint → serve → burn, then 410) + `GET /health` on `127.0.0.1:9200`; auto-on in pool mode, `NUCLEIC_RUNNER_CONTROL_PORT` opts a self-host in | Implemented; curl-verified on Linux (ok / payload / 410 / 404 / clean shutdown). This is what the Worker's `GET /v1/pool/pairing` proxy reaches |
| Presence→wake hook, both sides (item 3e) | Runner: `cloud/nucleic-runner/src/wake.ts` (+ index.ts route `POST /v1/hook/wake`, env `RUNNER_WAKE_SECRET`); nucleicd heartbeats carry `roomId`, the Worker refreshes `room:<roomId>` → poolId in KV. Edge: `handleRelay` stamps `x-nucleic-room`; `Room.maybeWakeRunner` nudges the hook on client-connect-without-host (30s throttle, fire-and-forget) | Implemented + unit-tested both sides (runner 40 green; edge 66 green; both typecheck + dry-run clean). Deploy gate: `RUNNER_WAKE_SECRET` on both Workers, `RUNNER_WAKE_URL` var on the edge |
| Image-publish CI (items 3a/3b) | `.github/workflows/sandbox-image.yml` (now multi-arch arm64+amd64) and new `.github/workflows/runner-image.yml` (static amd64 nucleicd → boot smoke vs the deployed relay → push `ghcr.io/abkslm/nucleic-runner`) | Authored; must run once on a CI branch before the wrangler `containers` block (now registry-image, uncomment-ready) goes live |
| **First real agent turns on a Linux runner** (was §0.2 item 2) | `nucleic-smoke` (`Sources/nucleic-smoke/`, NucleicProtocol-only executable) plays the iPhone programmatically: pair from the `--print-pairing` code, dial the **deployed production relay**, `startChat`, stream events, answer approvals | Verified end to end on Linux (glibc container): real `claude` turns ran Bash/Write/Read in a fresh worktree of the seeded controlled project and completed (`awaitingInput`); a destructive `rm` surfaced an approval even under Control-project auto mode (RiskClassifier) and the client's allow over the relay resumed the turn. Required fix: `ContainerServiceSettings.serviceEnabled` is hard-false on Linux (tier-0 in-place; the stub `ContainerManager` otherwise killed every dispatch because controlled projects pin the service on) |
| Darwin regression | host + xcodebuild | NucleicApp/nucleicd/NucleicCore build; 162 protocol + 100+ core sync/transfer tests pass; iOS app builds — all re-verified after every port wave |

### 0.2 Remaining (ordered — this is the work queue)

1. ~~**`createProject` — the dispatch gap.**~~ **Done** (kept here so items 2–8 keep their
   numbers; details in §0.1). Both layers landed: (a) `NUCLEIC_RUNNER_PROJECTS` boot seeding
   and (b) the capability-gated `createProject` wire verb, built via the §11.4 recipe. What
   remains of it is *client UI*: nothing sends `createProject` yet — the phone/Mac affordance
   ships with CLOUD_RUNTIME §6's phone-initiated project creation (the iOS `RemoteStore`
   switches already handle the case inertly).
2. ~~**First real agent turn on a Linux runner.**~~ **Done** (details in §0.1; numbering kept).
   Real `claude` turns ran on a Linux `nucleicd` — dispatched, streamed, and approved over the
   deployed relay via `nucleic-smoke` (§11.1 has the recipe). Credentials were copied files
   (`~/.claude/.credentials.json`), exactly the interim posture item 5 replaces. Two behaviors
   learned the hard way: Control projects force `auto` on (`createSession`'s `resolvedAuto`),
   so only **destructive** calls surface approvals on a runner's default project shape; and
   `ContainerServiceSettings.serviceEnabled` is now hard-false on Linux (tier-0 in-place) —
   see §11.3.
3. **Cloudflare container enablement — mostly landed; two ops gates + one edge edit remain.**
   - (a) ~~arm64-only base~~ **CI authored**: `sandbox-image.yml` now publishes the
     `nucleic-sandbox` multi-arch index (arm64 + amd64, same `v6` tag). *Gate: the workflow
     must actually run on a CI branch and the per-CLI `--version` checks pass on amd64.*
   - (b) ~~build + push runner image~~ **CI authored**: new `runner-image.yml` cross-builds
     the static amd64 `nucleicd`, boot-smokes it against the deployed relay, and pushes
     `ghcr.io/abkslm/nucleic-runner:v1`. *Gate: same — must run once.*
   - (c) `wrangler.jsonc` carries the uncomment-ready registry-image `containers` block (plus
     `wrangler containers registries configure ghcr.io` if the package stays private).
     Uncomment + `make runner-deploy` after (a)+(b) publish.
   - (d) ~~loopback pairing endpoint~~ **Done**: `nucleicd` serves `GET /pairing` (one-shot:
     mint → serve → burn, then 410) + `GET /health` on `127.0.0.1:9200`
     (`Sources/nucleicd/ControlEndpoint.swift`), on automatically in pool mode and via
     `NUCLEIC_RUNNER_CONTROL_PORT` on a self-host. Curl-verified on Linux end to end.
   - (e) ~~wake hook~~ **Done, both sides.** Host heartbeats carry the runner's relay
     `roomId`; the runner Worker registers `room:<roomId>` → poolId in KV (wake.ts, refreshed
     per heartbeat) and `POST /v1/hook/wake` (bearer: `RUNNER_WAKE_SECRET`) resolves it and
     forwards a pool-auth `/wake` — unit-tested (8 tests). Edge side: `handleRelay` stamps
     `x-nucleic-room` on the Room fetch, and `Room.fetch` nudges the hook (fire-and-forget,
     30s-throttled, first nudge always fires) when a **client** connects to a room with no
     host socket — an ordinary Mac's room just 404s at the runner Worker. Unit-tested (3
     tests: request shape + throttle, unconfigured no-op, unreachable-hook swallow). *Deploy
     gate: set `RUNNER_WAKE_SECRET` on both Workers + `RUNNER_WAKE_URL` on the edge.*
4. **Wire Settings → pool — done except mesh-riding the credential.**
   `RunnerPoolClient` (NucleicCore) mirrors `RelayAccess`: PoP enroll under the
   `nucleic-runner-enroll-v1:` context (`RunnerEnrollment` in NucleicProtocol, byte-pinned
   against the Worker next to the relay vectors), Keychain/0600-file credential with the
   single 401-re-enroll retry, `provision`/`wake`/`stopAll`, debounce-pushed
   `PATCH /settings` on knob changes, `GET /status` decoded into `RunnerPoolStatus`, and the
   `GET /v1/pool/pairing` proxy pull. `RunnerSection` (Settings ▸ Antimatter ▸ Runner) now
   drives it: Provision/Refresh buttons, the per-instance status list, the one-shot pairing
   code row, and knob changes ride to the pool automatically. **Remaining:** the pool
   credential should ride the mesh to the owner's other devices like relay memberships do
   (today only the enrolling Mac holds it; any device can still PoP-enroll itself, which
   rotates the secret).
5. **Credential mesh, device side.** The wire types + crypto are done; build the vault, the
   `credentialNeeded`/`credentialProvision`/`credentialUpdate` handlers (Mac first), the
   refresh-lease loop (`CredentialRefreshLease.merged` is implemented + tested), and the
   runner-side landing of records into files/env (§6).
6. **Intelligence executor, device side** (AFM behind `canProvideIntelligence`) + the runner's
   `AgentIntelligenceProvider` and `nucleic.runner.intelligenceMode` plumbing (§5).
7. **Tier-1 sandboxes in nucleicd** — a `RunnerSandboxProvider` seam calling
   `/v1/pool/sandbox/acquire`/`release` (pool side is done + tested; `at_capacity` must fall
   back to tier-0 in-place, never fail a dispatch).
8. **Hardening tail:** NucleicCoreTests on Linux (only the protocol suite runs there today —
   the fakes need `FileHandle.bytes` alternatives); checkpoint/restore to R2 (CLOUD_RUNTIME
   §3.5); golden-transcript conformance; weekly image-rebuild CI.

## 1. Thesis

A runner is **just another mesh host**. `PeerKind.cloud` already exists, `PeerCapabilities
{canHost, canRunAgents}` already describe it, the relay (live at
`relay.nucleic.blakeslee.xyz`) already routes to any host that enrolls with proof-of-possession
of its static key, and every dispatch verb a phone or sibling Mac uses against a Mac host
(`startChat`, approvals, `integrate`, session transfer, transcript fetch) works unchanged
against a host that happens to run in a container. So the runner is not a new protocol — it is:

1. **`nucleicd`** — the headless NucleicCore host (single `SessionController` writer,
   `TranscriptWriter` seq authority, `ApprovalCoordinator`, `WorktreeManager`, the three+
   adapters, `SyncHost` over a relay-only listener).
2. **A control plane** (`cloud/nucleic-runner`) that provisions, wakes, scales, and caps the
   containers `nucleicd` runs in — the part that is Cloudflare-specific and deliberately thin,
   so a self-hosted docker/podman driver can replace it behind the same HTTP surface.
3. **Three integration seams** that make a cloud host *feel* like a macOS host: sandbox tiering
   (§4), intelligence delegation (§5), and the credential mesh (§6).

## 2. Topology

```
 iPhone ─┐ Noise/CBOR (E2EE)          ┌────────────── Cloudflare ─────────────────┐
 Mac ────┼── WebSocket ───────────────►│ nucleic-edge Worker + Room DO (deployed)  │
 Mac #2 ─┘  (ciphertext only)         │        │ ciphertext frames                 │
                                      │        ▼                                   │
    Settings ▸ Runner ── REST ───────►│ nucleic-runner Worker + RunnerPool DO      │
    (pool credential)                 │   placement · maxContainers · fencing      │
                                      │        │ start/stop/exec                   │
                                      │        ▼                                   │
                                      │ ┌── runner container (host-of-record) ───┐ │
                                      │ │ nucleicd — SessionController,          │ │
                                      │ │ Transcript, Approvals, Worktrees,      │ │
                                      │ │ SyncHost ◄─ Noise terminates HERE      │ │
                                      │ │ claude/codex/grok + shims              │ │
                                      │ └───────────┬────────────────────────────┘ │
                                      │             │ acquire/release (pool API)    │
                                      │        ┌────▼─────┐  ┌──────────┐           │
                                      │        │ session  │  │ session  │  … ≤ N    │
                                      │        │ sandbox  │  │ sandbox  │ (Settings)│
                                      │        └──────────┘  └──────────┘           │
                                      └────────────────────────────────────────────┘
```

- **One mesh peer per runner.** The runner's `nucleicd` owns one `DeviceIdentity`; its
  `HostID`/relay `roomId` is `sha256(staticKey)` like any Mac. Devices see one "Cloud" peer in
  the mesh roster, not N containers. This mirrors the macOS model exactly: a Mac is one peer
  that *internally* fans sessions out across sandbox containers via `ContainerManager`.
- **Session sandboxes are the scaling unit**, not mesh peers. `nucleicd` acquires them from the
  `RunnerPool` DO (Cloudflare Containers instances) the way `ContainerManager` drives
  `ContainerEngine` locally. The pool enforces the **max containers** setting and runs the
  **load balancer** (§7).
- The **host-of-record container** is also a sandbox (`RunSpec.container = nil` — the
  CLOUD_RUNTIME insight), so a runner with `maxContainers = 1` still works: sessions simply
  share the host container, exactly like the shared `nucleic-control` container locally.

## 3. Mesh integration (how devices see the runner)

Pairing and dispatch reuse mesh mechanics verbatim:

- **Enrollment/pairing.** Provisioning happens from the Mac app (Settings ▸ Antimatter ▸
  Runner) or, later, iOS. The provisioning device calls the runner control plane, which boots
  the container; `nucleicd` generates its identity *inside* the container (the private key
  never leaves it), enrolls with the relay via the existing X25519 PoP flow, and returns a
  `PairingPayload` (host static key + fresh PSK + relay room fields — the QR payload, minus the
  QR) over the provisioning channel. The device completes Noise XXpsk0 through the relay and
  pins the runner like any host. Because the runner is a `MeshMember` with
  `kind: .cloud, capabilities: {canHost: true, canRunAgents: true}`, **roster gossip introduces
  it to every other mesh device automatically** — one device provisions, the whole mesh can
  dial it.
- **Dispatch.** `startChat` into a runner project, approvals (first-responder-wins), model/
  effort/auto/ship changes, `fetchDiff`, transcript fetch, and session transfer
  (`TransferOffer` → chunks → 2-phase commit) are the existing verbs; the runner advertises the
  matching `WireCapabilities` and the Mac sidebar's unified local/remote representation renders
  its projects and sessions like a sibling Mac's. "Move to Cloud" is the same "Move to Mac"
  flow gated on `canReceiveSessionTransfer` + `PeerKind.canOwnSessions` (already true for
  `.cloud`). A fresh runner's empty store is populated by `ClientMsg.createProject` (clone a
  git URL as a controlled repo, gated on `WireCapabilities.canCreateProjects` — any
  AppStore-backed host advertises it, cockpit or runner) or by boot-time seeding (§9); the new
  project reaches devices through the dashboard broadcast.
- **Presence/wake.** A device connecting to the runner's room signals the RunnerPool DO to wake
  the container if asleep (relay presence → pool wake hook), mirroring CLOUD_RUNTIME §3.3.
  *(Runner side built — §0.2 item 3e: host heartbeats register `room:<roomId>` → poolId and the
  authenticated `POST /v1/hook/wake` forwards a pool `/wake`. The edge Room's nudge on
  client-connect-without-host is the one remaining edit, spelled out in item 3e.)* Provisioning
  now has both channels: `--print-pairing` on stdout (self-host) and the loopback `:9200`
  one-shot `GET /pairing` behind the Worker's `GET /v1/pool/pairing` proxy (container path).

## 4. Virtualization tiers (mirroring the macOS desktop's concept)

The macOS host tiers work as: **lightweight containers** (Apple containerization) for ordinary
build/test/scripting, **desktop VMs** (Linux/macOS, Virtualization.framework) for GUI work,
with per-tier caps (`nucleic.macvm.maxConcurrent`, enforced in `MacVMManager.ensureRunning`).
The runner mirrors the concept on Linux infrastructure:

| Tier | Local (Mac) analogue | Runner implementation | Status |
|---|---|---|---|
| 0 — in-place | shared `nucleic-control` container | agent runs in the host-of-record container (`RunSpec.container = nil`) | now |
| 1 — lightweight session containers | per-session `ContainerEngine` containers | sibling Cloudflare Containers acquired from `RunnerPool` (`/v1/pool/sandbox/acquire`); on self-host, a docker/podman driver behind the same seam | now (pool side); nucleicd driver next |
| 2 — Linux desktop VMs (GUI orchestration) | `linux_vm_exec` / `linux_vm_computer` | **future** — Cloudflare Containers cannot nest KVM, so tier 2 lands with self-host (QEMU/KVM on the operator's box) or an alternate provider slotted behind the same `RunnerSandboxProvider` seam. The pool schema already carries `tier` so the wire/API doesn't churn when it lands | future, planned-for |

The seam that keeps tier 2 cheap later: `nucleicd` asks an abstract *sandbox provider* for
"a sandbox of tier T with resources R"; the Cloudflare provider maps T0/T1 today and rejects
T2 with a typed "unsupported on this provider" the UI can explain; a self-host provider maps
T2 to a desktop VM. Agent-facing tools stay identical (`linux_container` now; `linux_vm_*`
when tier 2 exists), so sessions are portable between a Mac host and a runner.

## 5. Intelligence: replacing Apple Foundation Models in the cloud

`IntelligenceProviding` (12 methods — summaries, naming, turn classification, triage, bash
map/reduce, trunk-summary folds, host-exec reason audits) is the seam; AFMs are app-side only.
The runner offers **the user's choice** of replacement (`nucleic.runner.intelligenceMode`):

1. **`agent`** — an `AgentIntelligenceProvider` backed by one of the user's authenticated
   agents (the credential mesh already puts Claude/Codex/Grok credentials on the runner): a
   small/cheap SKU, strict prompt templates, heuristics as validation guardrails (reusing
   `HeuristicSummary.echoesCommands` / `.isGrounded` the way `AppleIntelligenceProvider` does).
2. **`mesh`** — delegate to an Apple-Intelligence-capable device in the mesh. A Mac/iPhone
   advertising `WireClientCapabilities.canProvideIntelligence` receives
   `HostMsg.intelligenceRequest` (kind + inputs, content-minimal), runs it on its local
   `AFMRequestQueue` (background priority), replies `ClientMsg.intelligenceResult`. The runner
   applies per-kind timeouts and falls through when no capable device is connected. (Wire
   types land in this slice; the device-side executor rides the existing AFM queue.)
3. **`heuristic`** — `HeuristicIntelligence`, the same model-free fallback the Mac uses with
   AI off. Always the terminal fallback of modes 1–2; `auditHostExecReason` stays fail-closed
   (`.notChecked`) whenever no model path is available.

Delegation is capability-gated both directions: the host advertises
`WireCapabilities.canDelegateIntelligence` (so a client never sends an unsolicited result at a
host that would choke on the unknown `ClientMsg` tag), and only clients advertising
`canProvideIntelligence` are asked.

## 6. The credential mesh

Today, credentials live on the Mac (Keychain: Claude OAuth JSON, control API keys, GitHub
token; dot-dirs: `~/.codex/auth.json`, `~/.grok/config.toml`, …), get seeded into sandbox
containers per turn, and refreshed tokens are synced *back* (`syncClaudeLoginBack`,
`syncCodexLoginBack`). The runner generalizes exactly this, over the mesh's E2EE channel:

- **The vault.** Each client device keeps a `CredentialManifest` — the mirrored set of
  credential records `{kind, payload, updatedAt, provenance}` for agents + platforms (Claude
  OAuth, Anthropic/OpenAI/xAI API keys, Codex auth.json, agent home files, GitHub token). The
  manifest is **mirrored across all mesh devices** (macOS, iOS) via the existing roster-gossip
  discipline (idempotent, `updatedAt`-monotonic per kind), so any single mesh member can serve
  a runner and any single member can run refreshes for the whole group.
- **At rest / Secure Enclave.** The vault is encrypted at rest with a device-local key. Where
  Secure Enclave exists, the vault key is wrapped by an SE-resident **P-256** key
  (`kSecAttrTokenIDSecureEnclave`); honesty note per CLOUD_RUNTIME §5: Curve25519 keys cannot
  live in the SE, so the *sealing* keypair (below) is Keychain-held, and what the SE protects
  is the at-rest wrap. Never claim more.
- **Sealed injection (device → runner).** The runner's `nucleicd` generates an ephemeral
  X25519 *credential-sealing* keypair at provisioning (inside the container; distinct from the
  Noise identity so credentials can be re-sealed across identity rotation). When it lacks or
  loses a credential (fresh boot, expiry), it pushes `HostMsg.credentialNeeded {kinds,
  sealingPublicKey}` to connected clients; a holding device replies
  `ClientMsg.credentialProvision(SealedCredentialEnvelope)` — each record sealed directly *to
  the runner's key* with `SealedCredentialBox` (ephemeral X25519 → HKDF-SHA256 →
  ChaChaPoly-AEAD, context `nucleic-sealed-credential-v1`, recipient key + kind bound as AAD).
  The relay and control plane only ever carry ciphertext — same posture as sandboxed sessions
  today, where the host seeds credential files straight into the container.
- **On the runner.** Records land as 0600 files/env exactly where the local sandbox seeding
  puts them (`.credentials.json`, `~/.codex/auth.json`, agent homes, `GITHUB_TOKEN`), then flow
  into session sandboxes through the existing per-turn seeding.
- **Refresh, one member for the group.** OAuth refresh rotates refresh tokens, so concurrent
  refreshers revoke each other (the exact race `codexControlAuthMode == .apiKey` exists to
  dodge). The mesh elects **one refresher per credential kind** via a
  `CredentialRefreshLease {kind, holderDeviceID, expiresAt}` — gossiped like roster state,
  preferring a Mac (Keychain, usually awake), falling back to the runner itself. The lease
  holder refreshes; the rotated record propagates as a manifest update
  (`HostMsg.credentialUpdate` to devices, sealed per-device; ordinary mirror gossip between
  devices); everyone else only ever *reads*. A stale lease (holder offline past `expiresAt`)
  is simply re-acquired — monotonic `updatedAt` makes late writes harmless.

## 7. Load balancer + Settings

- **Placement.** `RunnerPool` DO owns the instance registry `{instanceId, state, sessions,
  epoch, lastSeenAt}` and places each `sandbox/acquire` by policy:
  `least-sessions` (default) or `round-robin`. Placement is a pure function
  (`placeSandbox()`), unit-tested, and returns the chosen instance + a **monotonic fencing
  epoch** (stale instances' writes are rejected — CLOUD_RUNTIME §3.3's zombie-writer answer).
- **Max containers.** `Settings ▸ Antimatter ▸ Runner` gains a stepper
  (`nucleic.runner.maxContainers`, default 4, clamp 1…16 — mirroring
  `nucleic.macvm.maxConcurrent`'s pattern) plus a balancer-policy picker
  (`nucleic.runner.balancerPolicy`). The Mac pushes these to
  `PATCH /v1/pool/settings` with the pool credential *(the push client is §0.2 item 4 — today
  the knobs persist locally only)*; the DO clamps server-side too (`clampSettings`, the same
  1…16 bounds — keep `RunnerSettings.maxContainersRange` and pool.ts `MAX_CONTAINERS_RANGE`
  in lockstep) and enforces at acquire time (`at_capacity` → nucleicd falls back to tier-0
  in-place execution, never a failed dispatch).
- **Scale-to-zero.** DO alarms reap idle sandboxes (idle-timeout parity with
  `ContainerManager`'s idle timers) and sleep the host container per CLOUD_RUNTIME §3.6
  lifecycle economics; relay presence wakes it.

## 8. Control plane (`cloud/nucleic-runner`)

Follows `nucleic-edge` conventions exactly (strict TS, node:test, hash-only KV storage,
constant-time compares, HMAC `payload.signature` tokens).

- **Auth.** Two-tier, mirroring `relayToken`: a **pool credential** (`poolId.poolSecret`,
  minted at pool creation, hash-only in KV) and short-lived **instance tokens** the DO mints
  for container↔pool calls. Pool creation requires X25519 PoP of the *provisioning device's*
  static key (context `nucleic-runner-enroll-v1:` — same algorithm as `relayEnroll`, different
  context string so proofs can't be replayed across services); the pool credential is then
  shared to other mesh devices over the E2EE mesh channel, like relay memberships are today.
- **Routes.**
  - `POST /v1/runner/enroll/challenge` / `POST /v1/runner/enroll` — PoP → `{poolId, poolSecret}`
  - `GET /v1/pool/status` · `PATCH /v1/pool/settings` (pool credential) — Settings surface
  - `POST /v1/pool/provision` / `POST /v1/pool/wake` / `POST /v1/pool/stop` — host container
    lifecycle
  - `POST /v1/pool/sandbox/acquire` / `POST /v1/pool/sandbox/release` (instance token) —
    nucleicd's tier-1 sandbox seam
  - `POST /v1/pool/heartbeat` (instance token) — liveness + session counts for placement
- **Bindings.** `NUCLEIC_RUNNER_POOLS` DO namespace (`RunnerPool`), `NUCLEIC_RUNNER_CONTAINERS`
  DO namespace (`RunnerContainer`, wrapping the Containers runtime; image
  `containers/nucleic-runner`), KV `NUCLEIC_RUNNER_TOKENS` (provisioned), secrets
  `RUNNER_TOKEN_SECRET` / `RUNNER_ADMIN_SECRET` (set via `wrangler secret put` before the first
  deploy). Domain: `runner.nucleic.blakeslee.xyz`.
- **Self-hosting.** The Worker surface is deliberately plain HTTP + one DO; a self-host bundle
  is the same `nucleicd` image plus a small compose file where the "pool" is a local supervisor
  speaking the same routes (docker/podman). Nothing in `nucleicd` knows it's on Cloudflare.

## 9. `nucleicd`

The headless host daemon. It **runs on macOS and on Linux**: macOS for the spare-Mac self-host
rung, Linux for the runner container. The CLOUD_RUNTIME Phase-2 port is done — the seams landed
exactly where §3.1 predicted: CryptoKit→swift-crypto behind `canImport(CryptoKit)` (interop
pinned by the protocol suite passing on Linux), Keychain→0600-file `LinuxSecretStore`, the
approval server's TCP leg on BSD sockets (mirroring its AF_UNIX path), the relay WebSocket on
SwiftNIO (corelibs' URLSession WS rides libcurl's experimental WS, absent from distro builds),
`os.Logger`→a stderr shim, Apple containerization/Virtualization excluded with stub actors
(`LinuxSupport.swift`) so the shared object graph compiles unchanged. Design rules:

- **Relay-first, LAN-optional.** The runner listens on the relay (`RelayListener`) always; LAN
  and tailnet only when configured (a self-hosted Mac mini on your LAN wants both).
- **Config via env** (`NUCLEIC_RUNNER_*`): control-plane URL + instance token, relay URL,
  data dir, transports, display name. No interactive setup; `nucleicd --print-pairing` emits
  the pairing payload for the provisioning channel. `NUCLEIC_RUNNER_PROJECTS` (comma/
  whitespace-separated git URLs) clones + registers projects at boot — idempotent by
  normalized origin remote, and a failed seed logs and moves on rather than blocking boot.
- **Same single-writer runtime.** It drives the same `AppStore`-rooted object graph headlessly
  (the CLOUD_RUNTIME "no TS twin / no fork" commitment) — identical semantics local and cloud.

## 10. Risks / honesty notes

- **Trust tier.** A runner container holds plaintext code, transcripts, and injected
  credentials while running — it is a *trusted endpoint like your Mac* on operated
  infrastructure, **never** "E2EE execution." Publish the CLOUD_RUNTIME §5 three-tier table
  wherever runner marketing appears; the credential mesh's sealed injection means Nucleic's
  edge code never sees plaintext credentials, which is the honest, strong claim.
- **Linux port has landed its go/no-go** (CLOUD_RUNTIME §11.1's tail risk is now bounded): the
  protocol vectors pass on Linux and a booted Linux `nucleicd` enrolls + listens on the deployed
  relay. Remaining Linux follow-ups are operational, not existential: run the *NucleicCore* test
  suite on Linux (only the protocol suite runs there today), verify a full agent turn in-container
  (`claude` on the BSD-socket approval server), image push + weekly rebuild CI, and the
  checkpoint/restore choreography (CLOUD_RUNTIME §3.5).
- **Anthropic ToS flux** on BYO subscription tokens: unchanged mitigation — API-key/Console
  fallback first-class (`controlAuthMode == .apiKey` already exists end-to-end).
- **SE overclaim hazard:** the SE wraps the vault at rest; sealing keys are Curve25519 in the
  Keychain. Copy must not blur this (see §6).

## 11. Handoff — field notes for the next agent

Everything below was learned the hard way in the implementation passes; read it before
touching this subsystem.

### 11.1 How to build & verify each piece

| What | Where it runs | Command |
|---|---|---|
| Control-plane worker | any Linux/macOS with node | `make runner-test` · `make runner-build` (typecheck + wrangler dry-run) · `make runner-deploy` |
| Darwin Swift | macOS host | `swift build --target NucleicApp` / `--target nucleicd`; `swift test --filter NucleicProtocolTests` (+ `SyncHostTests`, `WireMessageTests`, `PeerClientTests`, `SessionTransferTests`) |
| iOS | macOS host, **xcodebuild only** | `xcodebuild -project ios/NucleicRemote/NucleicRemote.xcodeproj -scheme NucleicRemote -destination 'generic/platform=iOS' build` — `swift build` never compiles this target, and protocol changes have silently broken it before (MESH_TRANSFER's own warning; it bit again here) |
| Linux (glibc, native arch) | `swift:6.3-bookworm` container, repo mounted | `apt-get install -y unzip libsqlite3-dev` (unzip: SwiftPM validates the Sparkle **binary artifact** at resolution even on Linux!), then `swift build --scratch-path /build --product nucleicd` and `swift test --scratch-path /build --filter NucleicProtocolTests` — **always use a container-local `--scratch-path`**, never the shared worktree's `.build` (it would poison the macOS build) |
| Linux amd64 static (the Cloudflare artifact) | same container, any arch | `scripts/build-nucleicd-linux.sh` (installs the Static Linux SDK 0.1.0 for Swift 6.3.3 with pinned checksum, compiles the SQLite amalgamation into its sysroot for GRDB, `swift build --swift-sdk x86_64-swift-linux-musl -c release`) |
| Boot smoke test | any of the above | `NUCLEIC_RUNNER_DATA_DIR=/tmp/rd NUCLEIC_RUNNER_NAME=Test timeout --signal=TERM 25 <nucleicd> --print-pairing` → expect `listening on: relay`, a `nucleic://pair?d=…` line on stdout, `shutting down`. On a non-native arch: prefix `qemu-x86_64-static` (static binaries need no binfmt setup). Note: this enrolls a throwaway room on the **production** relay — harmless (hash-derived roomIds can't squat; credentials age out) |
| **Agent-turn smoke test** (`nucleic-smoke`) | Linux box/container with `claude` + credentials (`~/.claude/.credentials.json`) | Boot nucleicd with a seeded project and `--print-pairing` (pipe through `stdbuf -oL` when redirecting — the pairing line is otherwise stuck in stdio buffering), then `nucleic-smoke '<pairing-code>' --prompt '…' --model haiku --timeout 200` → pairs over the relay, starts a chat in the first project, streams events, answers approvals, exits 0 on turn completion. Gotchas: the pairing secret is **one-time** (restart nucleicd for a fresh code per run); make sure exactly ONE nucleicd is up — N daemons supersede each other's relay room socket and the room silently swallows client frames (and slim images lack `pkill`: kill by `/proc/*/comm` scan); a prompt needs a **destructive** call (e.g. `rm`) to surface an approval, because Control projects run auto |

### 11.2 Port gotchas (why the code looks the way it does)

- **`ClientMsg` decode THROWS on unknown tags; `HostMsg` falls back to `.unknown`.** Every new
  client→host verb must be gated on a `WireCapabilities` flag the host advertises (the
  `relayMembership` / `canFetchDiff` recipe). And every new `ClientMsg`/`SyncClient.Event`
  case breaks **four exhaustive switches**: `ConnectionHandler.messageLoop` (host),
  `SyncClient.messageLoop` (protocol), iOS `RemoteStore` (owner-routing + demo switches), and
  iOS `HostConnection` (event switch). The compiler only tells you about the iOS ones under
  xcodebuild.
- **swift-crypto ≠ CryptoKit in one visible way:** `Curve25519.*.PrivateKey` isn't `Sendable`
  on Linux — hence `DeviceIdentity`'s conditional `@unchecked Sendable` (SecureChannel.swift).
  Everything else was drop-in; the interop is *proven* by the vectors, not assumed.
- **corelibs-foundation's `URLSessionWebSocketTask` does not work on distro Linux** (it rides
  libcurl's experimental WS protocol, compiled out of Debian's libcurl). Symptom:
  `NSURLErrorDomain -1005` instantly at connect. Plain HTTPS via URLSession works fine (the
  PoP enrollment used it successfully). Hence the NIO-backed `RelayWebSocket` (`#else` leg of
  RelayTransport.swift): NIOSSL TLS → HTTP/1.1 upgrade → RFC 6455 **client** framing — client
  frames must be masked (`maskKey: .random()`), fragments reassembled, pings answered.
  `WireFraming.maxFrameSize` (16 MiB) equals NIO's decoder cap exactly; don't add headroom.
- **On Linux, `URLSession` lives in `FoundationNetworking`, `XMLParser` in `FoundationXML`** —
  every file touching them needs the `#if canImport(...)` import.
- **Three libcs, not two:** Darwin / Glibc / **Musl** (the static SDK). Gates that say
  `#if canImport(Glibc)` break under musl. Type spellings differ: `SOCK_STREAM` is `Int32`
  (Darwin), `__socket_type` (glibc — needs `.rawValue`), plain int (musl); `SHUT_RDWR` needs
  `Int32(...)`; there's no `SO_NOSIGPIPE` on Linux → `MSG_NOSIGNAL` per send
  (`writeNoSigpipe` in MCPApprovalServer.swift).
- **SwiftPM cross-platform manifest tricks that worked:** keep every package *dependency*
  declared on all platforms (identical `Package.resolved` everywhere — the Linux resolve never
  dirtied it) and condition the *products* with `.when(platforms:)`; compute the NucleicCore
  `exclude:` list per-platform for Apple-framework files; `#if os(Linux)` runs at
  manifest-evaluation time. Declare helper arrays **before** `let package` (top-level code
  executes in order). Node's strip-types runner (worker tests) rejects constructor parameter
  properties — that's why `RunnerPoolCore` declares fields explicitly.
- **The Linux stubs** (`Sources/NucleicCore/LinuxSupport.swift`) mirror the FULL externally
  referenced surfaces of `ContainerManager`/`MacVMManager` plus the value types from excluded
  files (`MacVMEntry`, `MacVMMaintenanceInfo`, `ContainerResourceSample`,
  `ContainerKillDiagnosis`, `UncheckedSendableBox`). The container **naming statics are
  duplicated** there (channel suffix, shared/control names) — flagged "keep in lockstep" in
  both files; if you touch naming in `Container/ContainerManager.swift`, touch the stub.
  Portable files that stayed compiled on Linux: `MacVMSpec/MacVMSurface/MacVMPackage`,
  `CommandInterceptor` (its only Apple import was `os`).
- **GRDB on Linux: works.** Glibc: `libsqlite3-dev`. Musl static: no sqlite in the sysroot —
  the build script compiles the amalgamation with Debian-equivalent flags
  (COLUMN_METADATA/FTS5/RTREE/SNAPSHOT/DBSTAT) and installs it into the SDK sysroot.
- **Static Linux SDK practicals:** version is per-toolchain (6.3.3 → bundle `0.1.0`; checksum
  from `https://www.swift.org/api/v1/install/releases.json`, platform `static-sdk`). The
  bundle carries BOTH arch sysroots (~2 GB unpacked) — extract selectively when disk-tight.
  `swift sdk install` of a local file needs no checksum flag; a URL does.
- **`Host.current()`, `Observation`, DispatchSource/DispatchIO, signal handling** all work on
  Linux as used. iCloud APIs (`startDownloadingUbiquitousItem`, `NSFileVersion`) don't exist —
  ProjectRelocator's iCloud passes are `#if canImport(Darwin)`-gated to no-ops.
- **AppStore runs headless** — the CLOUD_RUNTIME open question is answered: no AppKit in its
  transitive graph; `nucleicd` drives the same object graph as `NucleicApp.init` minus UI
  hooks and minus `ModelCatalog` (App-layer; a runner takes model/effort per-dispatch, and an
  empty `Welcome.modelCatalog` is the designed fallback).

### 11.3 Trip-wires for the next tasks

- **`createProject` (§0.2 item 1):** transfer import rejects unknown projects
  (`resolveTransferProject` — by UUID / root commit / normalized remote), so seeding projects
  is a prerequisite for BOTH dispatch paths, not just `startChat`.
- **Pairing proxy (§0.2 item 3d — done):** `RunnerContainer` DO proxies `GET /pairing` to
  loopback `:9200` (`NUCLEICD_CONTROL_PORT` in `cloud/nucleic-runner/src/container.ts`);
  nucleicd serves the payload there **once** (mint → serve → burn, then 410 — re-provisioning
  means a container restart), mirroring `--print-pairing`. `ControlEndpoint.swift` in the
  nucleicd target; the burn happens *before* the bytes go out, so a half-delivered payload
  counts as served rather than risking a double-armed secret.
- **Pool auth handshake:** `poolId = sha256(provisioning device's static key)` — the same
  string as that device's `HostID`/relay roomId, PoP context `nucleic-runner-enroll-v1:`
  (proofs can't replay across services). Re-enrolling rotates the pool secret.
- **Epoch fencing contract:** heartbeat 409 means "a newer boot of this slot exists" —
  nucleicd exits 75 (EX_TEMPFAIL) and the supervisor must NOT restart that epoch.
- **`swift test` on Linux builds the whole package graph** — a compile break in any target
  blocks the protocol suite; the Darwin-only test fixtures (`fake-claude`/`fake-grok`, which
  use `FileHandle.bytes`) are excluded from the Linux manifest for exactly this reason.
- **`ContainerServiceSettings.serviceEnabled` is hard-false on Linux.** Controlled projects —
  the runner's default project shape — pin the container service on via
  `reconcileContainerService`, and with it on, `SessionController.isSandboxed` routes every
  dispatch into the stub `ContainerManager`, which throws ("the Apple-containerization sandbox
  service does not exist on Linux"). The `#if os(Linux)` gate in `Project.swift` makes tier-0
  in-place execution (`RunSpec.container == nil`) the only path, per §4. Don't "fix" a dead
  dispatch by enabling the service on Linux — wire the RunnerPool sandbox provider (item 7)
  instead.
- **`swift build` takes ONE `--product`** — repeated flags silently keep only the last one
  (this shipped a stale `nucleicd` mid-verification once). Build products in separate
  invocations.

### 11.4 The additive-wire-change recipe (used for all runner verbs; reuse it)

1. New payload struct in `RunnerMessages.swift` (Sendable/Codable/Equatable, decode-defaulted
   optionals). 2. New `WireCapabilities` flag (decode-default false) if a client will *send* a
   new tag. 3. `ClientMsg`/`HostMsg` cases + `EnvelopeKey` + both codec switches in
   `MessageEnvelope.swift`. 4. `SyncClient.Event` + `messageLoop`. 5. Handle in
   `ConnectionHandler` (host) with a defaulted `SyncHostBridge` hook. 6. Handle-or-inert in
   iOS `RemoteStore` (×2) + `HostConnection`. 7. Round-trip + legacy-tolerance tests in
   `RunnerMessagesTests.swift`. 8. Build macOS AND iOS (xcodebuild) AND Linux.
