Skip to content

Pairing

How a paired device joins the Fabric and what it tells the cloud about itself. Two flows are supported: the historical token-paste pairing (v1) and the OAuth Device Authorization Grant flow per RFC 8628 — shipped 2026-05-06 in commit e1ef78d (feat(fabric/auth): RFC 8628 OAuth device-grant pairing flow — Phase 13a-1; merged as 7d4f203). Phase 13a-1 is the recommended path for new pairings; token-paste stays in place for back-compat with already-paired devices.

What "paired" actually means

A paired device is a long-lived devices row plus a vault_recipients row of kind=device. The cloud holds:

  • the device's age public key (for vault re-wrap)
  • a 30-day device JWT issued at pair-complete (for WebSocket auth)
  • a heartbeat-published capability digest (for routing)

The device holds:

  • its age private key at ~/.config/optimalos/keys/device.key (mode 0600; TPM/Secure-Enclave sealing in Phase 12-2)
  • the cloud's origin URL
  • the device JWT in ~/.config/optimalos/credentials.json (mode 0600)

The cloud never sees plaintext credentials, never sees the device's private key, and never opens an inbound port to the device.

Token-paste flow (v1)

Three actors: the issuing browser (already unlocked at https://fabric.optimal.miami/), the cloud, and the device (running optimal pair).

Browser                     Cloud (Hetzner)               Device
  │                              │                            │
  │  POST /api/auth/pair-init    │                            │
  ├────────────────────────────▶ │                            │
  │  { ttl: 600 }                │                            │
  │  ◀──────────────────────────┤                            │
  │  { token, expiresAt }        │                            │
  │                              │                            │
  │   user copies token,         │                            │
  │   reads it on the device     │                            │
  │                              │                            │
  │                              │   `optimal pair --token …`│
  │                              │  ◀──────────────────────── ┤
  │                              │   POST /api/auth/pair-complete
  │                              │   { token, devicePubkey,   │
  │                              │     capabilities }         │
  │                              ├──────────────────────────▶ │
  │                              │   { deviceId, deviceJWT }  │
  │                              │                            │
  │                              │   eager re-wrap of all     │
  │                              │   vault_entries to include │
  │                              │   the new device recipient │

Concrete steps:

  1. Issue a pairing token from the browser. Open the dashboard, hit Add device, copy the resulting 10-minute token. Under the hood: POST /api/auth/pair-init returns a JWT signed with JWT_SIGNING_KEY, kind=pairing, ttl=600, and an invite claim that records the issuing browser's pubkey.
  2. Run optimal pair on the device. The CLI generates a fresh age keypair if one is not already at ~/.config/optimalos/keys/device.key, collects local capabilities (RAM, installed harnesses, OS), and posts to /api/auth/pair-complete along with the token.
  3. Cloud validates. The token must be live and unconsumed (pairing_tokens table tracks single-use). Cloud writes a devices row and a vault_recipients row of kind=device, then issues a 30-day device JWT.
  4. Eager re-wrap. Cloud (and the issuing browser, on next dashboard load) re-wraps every vault_entries row so the new device recipient can decrypt them. Atomicity is best-effort today; T8 in the threat audit tracks the Postgres RPC promotion.
  5. Device opens its WebSocket. optimalos-agent.service (or bun run dev in development) dials wss://fabric.optimal.miami/ws/device, presents the JWT in Sec-WebSocket-Protocol, and starts heart-beating every 20 seconds.

After step 5 the device shows up in the Sessions tab as an eligible target for the harnesses it has installed.

Capability declaration

Each heartbeat envelope publishes a capabilities block. The cloud upserts it into openclaw_instances and the (Phase 12-1) scheduler reads from there.

ts
{
  type: "heartbeat",
  payload: {
    deviceId: "uuid",
    capabilities: ["has-gpu", "harness:claude-code", "ram:16g", "always-on", "has-returnpro-creds"],
    installedHarnesses: ["claude-code", "openclaw"],
    ramFreeMb: 11_240,
    loadAvg: [0.32, 0.41, 0.39],
    diskFreeGb: 612,
    uptimeSec: 184_223,
    optimalosAgentVersion: "0.10c-1.1"
  }
}

Capability tag conventions:

Tag prefixMeaning
harness:<id>The named harness adapter is installed and detect.sh exits 0.
ram:<n>gTotal physical RAM, rounded down. Used by the (pending) capability scheduler.
has-gpuAny GPU detected; finer-grained tags TBD when the Phase 11-2 adapters surface real GPU needs.
has-<creds>Vault has at least one entry whose label matches a known credential bundle (e.g. has-returnpro-creds).
always-onThe device is mains-powered and does not sleep (used as a tie-breaker).

Custom tags are fine. The scheduler's requires list uses set membership against the union of all tags a device publishes.

Inspecting paired devices

From any signed-in browser at https://fabric.optimal.miami/:

  • Sessions tab (Phase 14-1 thin slice): shows online devices and the harnesses each can run.
  • Vault dashboard (Phase 10a-5): per-recipient revoke. Revoking a device recipient soft-revokes the vault_recipients row and triggers eager re-wrap.

From the CLI (auth via the same OPTIMAL_FABRIC_TOKEN env var as the rest of optimal vault):

bash
optimal vault recipients
# kind  pubkey       device_id  revoked_at
# browser  age1…   -            -
# device   age1…   <uuid>       -
# recovery age1…   -            -

Revoking a device

The flow is symmetric to pair:

  1. Click Revoke on the device row in the dashboard. This sets vault_recipients.revoked_at = now() and triggers eager re-wrap excluding that recipient.
  2. New ciphertext written from this point forward is no longer decryptable by the revoked device.
  3. T4 cross-check + 13b-1 cascade (both shipped): the device's 30-day JWT is still cryptographically valid, but authMiddleware cross-checks vault_recipients.revoked_at with a 60s cache (T4 closeout, commit 5ff9ba4), and WSMultiplex.closeRevokedConnectionByPubkey (commit cbd6bbd, Phase 13b-1) tears down any live WS session for that device within the same request that flipped revoked_at. Stale JWT no longer survives until natural expiry.
  4. If you suspect actual key compromise (laptop stolen, daemon binary tampered with), rotate JWT_SIGNING_KEY in /opt/optimalos/secrets.env on Hetzner and restart optimalos.service. That invalidates every JWT, browser and device alike, forcing a full re-pair.

Failure modes

SymptomLikely causeFix
pair-complete returns 410 GoneToken expired (10 minute TTL exceeded) or already redeemedIssue a fresh token from the browser.
pair-complete returns 401Token signature invalid; usually means JWT_SIGNING_KEY was rotated between issuance and redemptionRe-issue.
pair-complete returns 500Migration drift or DB unreachableCheck Hetzner journalctl -u optimalos; usually a Supabase connectivity blip.
Device JWT works for a minute, then 401sJWT_SIGNING_KEY was rotated (intentionally or via redeploy that wiped secrets.env)Re-pair.
Heartbeat does not show up in browserCloudflared or optimalos.service restarted while the device was mid-WebSocketDaemon will reconnect with exponential backoff (1s to 30s cap). Wait, then check journalctl -u optimalos-agent on the device.
pair-init returns 401The issuing browser's session expiredSign in again at /, then retry.

What's coming

  • Phase 12-2: TPM / Secure-Enclave device key sealing. Replaces mode 0600 disk storage of device.key.

Shipped since this doc was last revised

  • Phase 12-1: capability-aware routing scheduler. Shipped 2026-05-05 (35c7f70); patched 2026-05-08 (c125cfa). RAM-aware scoring + harness/capability hard filter + lexicographic tie-break. See multiplex.md.
  • Phase 13a-1: OAuth Device Authorization Grant (RFC 8628). Shipped 2026-05-06 in commit e1ef78d (merged 7d4f203). Replaces token-paste with a phone-friendly user-code flow. Better for non-CLI devices and removes the "type a long token" UX papercut. Token-paste remains the fallback for already-paired devices and CLI-only pairings.
  • Phase 13b-1: Full revocation cascade + WebAuthn-gated destructive ops. Shipped 2026-05-07 (commit cbd6bbd, merge b100b03). Adds the daemon-side closeRevokedConnectionByPubkey cascade (src/server/ws-multiplex.ts:334), cockpit revoke UI, and last-of-kind guards so the operator can't accidentally revoke their last recovery recipient.

Source

  • Charter §5 (session schema), §7 (transport), §8 (device daemon), §9 (capability routing): ~/.openclaw/workspace/optimalOS/docs/superpowers/specs/2026-05-03-fabric-charter.md
  • Vault design §5.3 (pair-a-new-device flow): ~/.optimalos/transfers/fabric-design/02-vault-design.md
  • Auth routes: ~/.openclaw/workspace/optimalOS/src/routes/auth.ts
  • Device daemon: ~/.openclaw/workspace/optimalOS/src/daemon/
  • Threat audit (T4 device-JWT cross-check, §3-D, §3-E, §3-O): ~/.optimalos/transfers/fabric-design/06-vault-auth-threat-rerun.md

Built by Carlos Lenis in Miami