Appearance
Why hosts do not receive raw OAuth tokens
This is a real architectural rule, not a polish detail. It is the reason the pairing grant explicitly excludes provider OAuth tokens, and the reason system.factotum (the local credentials actor) is the one component allowed to read or write OAuth credentials on a Device.
This page states the rule, the threat model, the boundary between what HiveCast does and what the local Device does, and the four code-review consequences.
The rule
A pairing grant only contains device-scoped credentials. It MUST NOT contain the principal's OAuth refresh tokens, OAuth access tokens, or OIDC ID tokens for any third-party identity provider.
There are two distinct OAuth families in this product, and they live on opposite sides of the network:
| OAuth family | Where the redirect lands | Where the token is stored | Crossings |
|---|---|---|---|
| HiveCast account login (Google, GitHub, Enterprise SSO) | https://hivecast.ai/api/auth/callback/<provider> | HiveCast system.auth credentials[] table, principal-scoped | The OIDC id_token is consumed cloud-side. Devices never see it. |
| Per-provider inference OAuth (Anthropic, Codex, OpenAI when interactive) | http://localhost:<port>/callback | Device-local: system.factotum writes <matrix-home>/credentials.json | The browser redirect lands on the Device; the token never leaves the Device. |
The first family is registered with each provider. Google does not let you register http://localhost:8765/callback as a redirect URI for production apps; the redirect URI is https://hivecast.ai/..., registered once per provider. So the OIDC code exchange happens on HiveCast.
The second family is the opposite. Anthropic's OAuth flow expects a loopback redirect (http://localhost:<port>/callback) and the resulting refresh token is meant to be stored on the user's machine. Sending it to the cloud would (a) cross trust boundaries with no benefit and (b) create a massive blast radius if HiveCast were compromised.
The threat model
What does a Device get if you give it a principal-level OAuth token?
- Persistent ability to act AS the principal against any service the principal is logged into via that provider
- Refresh tokens that survive principal session expiry
- Cross-product blast radius: a compromised Device could enumerate every service the principal has ever signed into via Google
What does a Device get with a device-scoped credential?
- A NATS user JWT scoped to one principal × one Space × one authorityRoot, expiring in 24 hours
- A heartbeat bearer token for one Host Link record, revocable independently
- Capabilities limited to the
scopes[]attached to that link
Per-Device blast radius. Per-Device revocation. The principal's broader identity and external-provider tokens stay where the user originally put them.
The boundary
WORKSTREAMS/matrix-web/USER-FLOWS-AND-AUTH-BOUNDARIES.md codifies the boundary as:
| Flow | MUST happen | Why |
|---|---|---|
| Google / GitHub / Enterprise SSO sign-in | HiveCast | Registered redirect URIs |
| Email verification | HiveCast | HiveCast sends + receives |
| Domain DNS verification | HiveCast | DNS check + sign |
| Account creation | HiveCast | Owns the root registry |
| Anthropic OAuth | LOCAL | Loopback redirect; token stays local |
| Codex / OpenAI OAuth | LOCAL | ~/.codex/auth.json is local |
| Ollama configuration | LOCAL | Ollama runs locally |
| Inference API calls | LOCAL | Tokens are local; API calls go directly |
| Credential storage | LOCAL | <matrix-home>/credentials.json, never uploaded |
The pairing grant is the bridge between those two columns. It carries only what is needed to talk to the cloud bus on this Device's behalf — nothing from the left column, nothing about other right-column credentials. The Device holds its own factotum store; the cloud holds its own principal-level credentials store. They never copy across.
Where this is enforced in code
Cloud side — the redeem response
auth.device.exchange and auth.pair.exchange produce a DeviceSetupExchangeResponse whose credentials map contains only:
natsUserJwt(device-scoped)natsUserSeed(device-scoped nkey)hostLinkToken(heartbeat bearer for this link)registryToken(optional, device registry write token)
There is no code path that reads from HostCredentialStore.list() (the principal-level credentials store) and writes those values into the exchange response. If you ever see principal.credentials flowing into DeviceSetupExchangeResponse, that's a bug — file it as a security regression.
Device side — Factotum is the boundary
system.factotum is the one actor on a Device that reads or writes provider credential files. The op surface (declared in system-factotum/dist/index.js:222-340 after build) is:
credential.store
credential.get
credential.list — never returns secrets, only metadata
credential.delete
credential.refresh
credential.import-detected — discovers ~/.claude/.credentials.json, ~/.codex/auth.json, env vars
credential.import — imports a previously detected source
credential.validate — proxies a minimal API call to verify
credential.lease — issues short-lived lease (NOT raw secret)
credential.materialize — exchanges lease for HTTP headers (only callable by trusted system actors)credential.lease / credential.materialize is the discipline that lets inference actors call third-party APIs without ever holding the raw secret. The actor calls credential.lease and gets back a leaseId, then hands that to the inference catalog which calls credential.materialize and gets HTTP headers ready to use. The raw key never crosses the bus.
Cloud-side counterpart
HostCredentialStore (in host-auth.ts:432-490) stores principal-level credentials cloud-side. Its store() and getNatsAccount() methods are how the cloud holds the principal's NATS account seed (used to mint device user JWTs). That account seed is never copied into a redeem response either; only its derived per-device user credentials are.
Four code-review consequences
When reviewing a PR that touches pairing, auth, or credentials, reject the following patterns even if "everything works":
A pairing grant carrying a non-NATS bearer token. If you see
googleAccessToken,anthropicRefreshToken,codexJwt, or anything similar inDeviceSetupExchangeResponse.credentials, that's a violation. Per-provider OAuth belongs in the local Factotum store on the Device, not in the pairing grant.An actor other than
system.factotumreading<matrix-home>/credentials.jsondirectly. If a chat actor, an inference catalog, or anything else opens that file, that's a violation. They go throughcredential.lease/credential.materializeinstead.A pairing flow that "imports" provider tokens from the principal. If
auth.device.exchangeever takesprovider/accessTokeninputs to forward to the Device, that is the wrong direction. Per-Device provider OAuth should be initiated from the Device's own browser viasystem.factotum's detect/import ops.HTTP headers including raw third-party tokens travelling on the bus. The lease/materialize pattern exists so the bus only ever carries lease IDs. Inference actors that call third-party APIs should only see fully formed
headers: Record<string, string>fromcredential.materialize— never rawapiKeystrings on bus payloads.
Mantra
The mantra to internalize is:
HiveCast credentials live cloud-side. Provider credentials live Device-side. The pairing grant is the bridge — it carries only Device-scoped, Host-Link-bound material in either direction.
If a feature seems to need raw OAuth tokens to cross the boundary, re-design the feature so it doesn't. There has not yet been a case where that re-design wasn't possible.
See also
- Pairing grant — the wire shape that this rule shapes.
- Security / Authentication — full OAuth provider matrix (separate
docs-securitypackage). - Security / Secrets / Factotum — the local credentials actor.
WORKSTREAMS/matrix-web/USER-FLOWS-AND-AUTH-BOUNDARIES.md— original policy doc.
Source:
WORKSTREAMS/matrix-web/USER-FLOWS-AND-AUTH-BOUNDARIES.md§ "Auth Boundary Rules";WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md§ "Correct Security Model" and § "Setup-exchange / device redeem response";projects/matrix-3/packages/system-factotum/(Factotum op surface and lease/materialize discipline);projects/matrix-3/packages/system-auth/src/host-auth.ts(no principal-token paths inHostLinkStoreorHostDeviceLinkStore).