OCP Companion profile — normative specification (v1.0-draft)

Extends Core. A Companion is an authenticated remote I/O device (typically a phone doing voice) bound to ONE Surface session.

1. Discovery

GET {iss}/.well-known/ocp.json (schema: wellknown.ts) over TLS. JWKS keys carry kid and rotate; Companions SHOULD cache per HTTP caching headers.

2. Pairing (delegated — the default)

  1. Surface → Hub pair/start (HTTPS, authenticated by whatever the Surface has). Hub mints {sid, nonce} (each ≥128-bit random) and a ≤90 s ES256 JWS (claims: pairing.ts#pairTokenClaimsSchema), rendered as ocp://pair?d=<b64url(JWS)> ONLY inside that Surface session.
  2. Companion scans → fetches .well-known → verifies the JWS OFFLINE (±30 s skew) → MUST require new URL(cap).origin === new URL(iss).origin.
  3. Companion → Hub pair/claim {jws, devicePubKey, attestation?, device} (HTTPS). The nonce is burned ATOMICALLY. Attestation MAY be required for T1+, MUST be for write scopes. All failures return uniform PAIR_DENIED.
  4. Hub → Surface pair.approval_request. Delegation MUST NOT activate before a human approves ON THE SURFACE (anti-QRLjacking).
  5. On approval the Hub mints a Companion session token (15 min, silently refreshed over the live WS) bound to sid + device key + tier + scopes; both peers receive pair.linked.
  6. The Surface MUST show a persistent connected indicator with one-click revoke; every pair/claim/approve/revoke is audit-logged.

Proof-of-possession: every subsequent connect and sensitive call is signed with the device key. A stolen token without the key is useless.

Token refresh: a Companion with a live, authenticated WS leg SHOULD send session.refresh before its token expires (recommended: at 2/3 of the TTL). The Hub replies session.refreshed {token, expiresIn} with a new token bound to the SAME cnf.jkt, tier, and scopes — no re-pairing, no new PoP over the established leg (possession was proven at connect). Reconnect/resume MUST present the newest token with a fresh PoP. A Hub MUST NOT extend a session's absolute TTL through refresh.

Account-bound variant (OPTIONAL): when the claim carries a Companion JWT whose subject equals the Surface session's subject, the Hub MAY auto-approve. Cross-subject delegation ALWAYS requires explicit on-Surface consent.

3. Sessions & scope

One active Companion per session; a second claim requires an explicit takeover confirm on the Surface. The Companion tier is capped by the Surface session's tier. Scopes are explicit (context.read, qa.ask, tools.invoke:<name>).

4. Voice

Default: mic/VAD/STT on-device → turn.input{text}; Hub streams text back for on-device TTS. stt/tts location is negotiated at session.open. Confirms render natively on the Companion and are equivalent to Surface confirms.