# nucleic-runner

The Antimatter runner control plane (design: [docs/ANTIMATTER_RUNNER.md](../../docs/ANTIMATTER_RUNNER.md)).
Provisions, wakes, scales, caps, and load-balances the Cloudflare Containers a runner — a
headless `nucleicd` host users dispatch agents onto as if it were a macOS host — runs in.
It never sees content: devices talk to the runner over the Noise-E2EE relay path
(`cloud/nucleic-edge`); this Worker only handles lifecycle metadata.

## Pieces

- `src/enroll.ts` — pool creation via X25519 proof-of-possession (context
  `nucleic-runner-enroll-v1:`; same algorithm as the relay's, different context so proofs can't
  cross services). `poolId = sha256(provisioning device's static key)` = its mesh `HostID`.
- `src/runnerToken.ts` — HMAC instance tokens (`payload.signature`), ~30 d, re-minted on every
  accepted heartbeat.
- `src/pool.ts` — `RunnerPoolCore`: instance registry, the load balancer (`placeSandbox`,
  `least-sessions` | `round-robin`), the **max containers** cap (Settings ▸ Antimatter ▸
  Runner), monotonic epoch fencing (zombie writers get 409), idle reaping via DO alarms.
- `src/container.ts` — `RunnerContainer` DO: thin start/stop/status/pairing-proxy over one
  container slot.
- `src/index.ts` — routing + auth; `RunnerPool` runtime binding.

## Routes

| Route | Auth |
|---|---|
| `POST /v1/runner/enroll/challenge`, `POST /v1/runner/enroll` | open (PoP is the gate) |
| `GET /v1/pool/status` · `PATCH /v1/pool/settings` · `POST /v1/pool/{provision,wake,stop}` · `GET /v1/pool/pairing` | pool credential (or admin + `?pool=`) |
| `POST /v1/pool/heartbeat` · `POST /v1/pool/sandbox/{acquire,release}` | instance token |
| `POST /v1/pool/revoke` | admin |

## Dev

```
npm install
npm test          # node:test, no wrangler needed
npm run typecheck
npm run check     # wrangler dry-run build
```

The `containers` block in `wrangler.jsonc` stays commented until the `nucleicd` Linux build
exists (CLOUD_RUNTIME Phase 2); everything else deploys and runs without it.
