|
| 1 | +# ADR-019: Cloud SDK Trust Policy Defaults |
| 2 | + |
| 3 | +- **Date**: 2026-03-08 |
| 4 | +- **Status**: Accepted |
| 5 | +- **Affects**: SDK |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +`@sideband/cloud`'s `connect()` wraps `relayClientNegotiator`, which requires a `trustPolicy` |
| 10 | +telling it how to handle first connections and identity mismatches. Three policies exist: |
| 11 | + |
| 12 | +- `"auto"` — accept and pin on first connection; silently re-pin on identity mismatch (TOFR) |
| 13 | +- `"pinned-only"` — require a pre-existing pin; hard-reject on mismatch or if the store is empty |
| 14 | +- `"prompt"` — invoke caller-provided callbacks on first connection and on mismatch (full TOFU) |
| 15 | + |
| 16 | +The cloud relay authenticates daemons at registration time via API key — the control plane holds |
| 17 | +the daemon's `identityPublicKey` from `daemon.register`. A caller connecting via `quickConnectCode` |
| 18 | +has an additional trust proof: the QC code itself proves the operator intentionally shared access. |
| 19 | + |
| 20 | +## Decision |
| 21 | + |
| 22 | +**Default: `"auto"` for both auth paths.** |
| 23 | + |
| 24 | +- **Quick Connect**: `"auto"` is correct. The QC code IS the out-of-band trust proof — |
| 25 | + prompting for a fingerprint immediately after entry adds friction without security benefit. |
| 26 | + On identity mismatch, re-pinning is intentional: a fresh QC code proves the operator |
| 27 | + regenerated access, covering the common "wiped `~/.sideband/`" case. |
| 28 | + |
| 29 | +- **Account path**: `"auto"` is a pragmatic interim default. The intended future default is |
| 30 | + `"pinned-only"` verified against a control-plane-provided `expectedFingerprint` from |
| 31 | + `daemon.listAccessible`. That fingerprint is not yet plumbed through the current API, so |
| 32 | + `"pinned-only"` would reject first-time account connections against an empty store. |
| 33 | + `"auto"` keeps the account path functional until `expectedFingerprint` is available. |
| 34 | + |
| 35 | +The relay's API key authentication provides a meaningful baseline: only registered daemons can |
| 36 | +receive SBRP sessions. `"auto"` adds TOFR (Trust On First Registration) on top — weaker than |
| 37 | +strict TOFU but not unconditional trust. |
| 38 | + |
| 39 | +**Future direction**: when `daemon.listAccessible` provides `identityPublicKey`, the account |
| 40 | +path will default to `"pinned-only"` using the CP-provided fingerprint — silent verification |
| 41 | +with strong identity guarantees. A key-rotation fallback (prompt on mismatch) will handle the |
| 42 | +`~/.sideband/` wipe case. When that lands, this ADR should be superseded. |
| 43 | + |
| 44 | +**Callback no-op rule**: `onFirstConnection` and `onIdentityMismatch` are accepted by the |
| 45 | +type system for all policies but only invoked when `trustPolicy === "prompt"`. Passing them |
| 46 | +with `"auto"` or `"pinned-only"` has no effect. |
| 47 | + |
| 48 | +## Invariants |
| 49 | + |
| 50 | +- `"pinned-only"` MUST NOT be used as a default without a pre-populated identity store or |
| 51 | + a CP-provided expected fingerprint — it rejects immediately on an empty store. |
| 52 | +- Callbacks MUST be required at the type level when `trustPolicy: "prompt"` (enforced via |
| 53 | + discriminated union in `ConnectOptions`). |
| 54 | +- The trust policy used by `CloudClientNegotiator.negotiate()` is always the caller-supplied |
| 55 | + value or the default computed at negotiate-time — never cached across reconnects. |
| 56 | + |
| 57 | +## Alternatives Considered |
| 58 | + |
| 59 | +**Auth-mode-aware default (QC → `"auto"`, account → `"pinned-only"`)** |
| 60 | +Rejected: `"pinned-only"` fails on first account connection — the pin is established by the |
| 61 | +first successful connection, so an empty store at that point silently breaks the happy path. |
| 62 | + |
| 63 | +**Default to `"prompt"` on account path** |
| 64 | +Rejected: the type system mandates callbacks for `"prompt"` mode, forcing every caller to |
| 65 | +supply them even without interactive trust management intent. |
| 66 | + |
| 67 | +## References |
| 68 | + |
| 69 | +- `packages/cloud/src/connect.ts` — `ConnectOptions`, `CloudClientNegotiator.negotiate()` |
| 70 | +- `packages/peer/src/negotiator/client.ts` — `relayClientNegotiator`, `trustPolicy` implementation |
| 71 | +- ADR-017: Cloud SDK Design (auth modes, `getConnectionParams`) |
0 commit comments