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)
- 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 asocp://pair?d=<b64url(JWS)>ONLY inside that Surface session. - Companion scans → fetches
.well-known→ verifies the JWS OFFLINE (±30 s skew) → MUST requirenew URL(cap).origin === new URL(iss).origin. - 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 uniformPAIR_DENIED. - Hub → Surface
pair.approval_request. Delegation MUST NOT activate before a human approves ON THE SURFACE (anti-QRLjacking). - 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 receivepair.linked. - 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.