OCP Core — normative specification (v1.0-draft)
The key words MUST, MUST NOT, SHOULD, MAY are to be interpreted as in RFC 2119.
The Zod schemas in ../src are the authoritative message shapes; the golden
vectors in ../vectors are the compatibility contract. This document holds the
behavioral rules the schemas cannot express.
1. Roles
Surface (renders chat, publishes context, executes surface-tools), Hub (owns sessions, identity, turns, tool routing, policy, audit), Companion (remote, authenticated I/O device bound to a Surface session), Provider (data/tools behind the Hub). Surfaces and Companions MUST NOT communicate directly — the Hub brokers everything.
2. Versioning & forward compatibility
- Every message carries
v=major.minor. Within a major version all changes are ADDITIVE-ONLY. - Receivers MUST ignore unknown message types and unknown envelope/payload fields. Receivers MUST NOT reject a structurally valid envelope because its type is unknown.
- Vendor extensions use
x-<vendor>.<name>message types and tool names.
3. Envelope
Every frame is {v, id, type, ts, sid?, replyTo?, payload} (schema:
envelope.ts). id MUST be unique per sender (ULID RECOMMENDED). replyTo
correlates a response/error/confirm to the request it answers.
Delivery is AT-LEAST-ONCE across reconnects: receivers MUST dedup on id.
4. Errors
One error message type (schema: errors.ts) with a registered code set.
replyTo MUST reference the failed request when one exists. Pairing failures
MUST be uniform (PAIR_DENIED) — implementations MUST NOT reveal whether a
token was expired, replayed, or malformed.
5. Sessions
session.opennegotiates capabilities; the Hub answerssession.linkedwith the INTERSECTION. Absent capability ⇒ defined fallback: notools→ Q&A only;stt/ttsnone→ modality handled by the peer or unavailable.- Heartbeats (
session.heartbeat) every 10 s in BOTH directions; a peer is stale after 2 misses (~25 s). - Companion drop →
session.suspendedwith a 60 s grace; rejoin requires the resume key AND a device-key signature. After grace →session.closed. - Surface drop → the session closes immediately (
surface_gone); nothing may apply without a live Surface to render approvals. - Hard TTLs: 30 min idle, 4 h absolute. All session state is revocable at any time (Surface UI, Hub device list, admin).
6. Context
context.set is a FULL REPLACE of the Surface's context, carrying a monotonic
rev. Hubs MAY reject serialized contexts > 32 KB with CONTEXT_TOO_LARGE;
Surfaces MUST truncate selection.text to ≤ 8 KB. resource.kind, id, and
selection.ref are opaque to the protocol; the Provider interprets them.
7. Identity tiers
T0 anonymous (public content, no writes, strict quotas) · T1 site-identified
(ocp.identify with a tenant-signed visitor JWT) · T2 hub-authenticated. A
paired Companion NEVER exceeds the tier of the Surface session it joined, and
its token may carry a lower ceiling. The Hub re-derives effective permission on
EVERY call: tier × scopes × resource ACL. Client claims are never authority.
8. Turns
turn.input→ streamedturn.delta_ +turn.citation_ →turn.done(echoingcontextRev) orturn.error.- ONE in-flight turn per session. A new
turn.inputwhile a turn streams IMPLIES cancellation: the Hub MUST emitturn.error{TURN_ABORTED}for the superseded turn before streaming the new one (voice barge-in). turn.cancelstops the identified turn; the Hub respondsturn.error{TURN_ABORTED}.- An in-flight turn does NOT survive a disconnect: after
session.resumedthe Hub MUST sendturn.error{TURN_ABORTED}for any turn that was streaming. - Audio input (
audioRef) requires the negotiatedaudiocapability.
9. Tools
MCP-shaped definitions (schema: tools.ts), two locations: hub and
surface. Manifests are hints — the Hub MUST re-check authorization on every
invocation. Tools with write: true MUST be gated by a confirm (§10). Surface
tool scope SHOULD be limited to the current resource.
10. Confirms
ui.request (confirm/choice/notice) is Hub-initiated and non-bypassable; it
may be satisfied from ANY connected peer. A confirm without a response by
expiresAt EXPIRES TO DENY. Every write MUST be preceded by a confirm; a
pending Surface-side diff whose Companion dropped stays pending for manual
approval — it MUST NOT auto-apply.
11. Transport & retention
All legs are wss/https. Audio MUST NOT persist beyond the turn. Transcripts are
retained per tenant policy, scoped by tier. JWS exp verification allows
±30 s clock skew.
12. Session state machine
Per peer leg, from the Hub's perspective:
Rules the diagram cannot show: a session's clock for ttl_absolute starts at
creation and is NEVER extended (token refresh keeps the TOKEN alive, not the
session); SUSPENDED applies only to the Companion leg — the Surface leg has
no grace; every transition into CLOSED MUST emit session.closed{reason} to
every still-connected peer and resolve all pending confirms to deny and all
pending actions to error.
13. Canonical flows
Pairing (Companion profile §2):
Turn with a gated write tool (§8–§10):
14. Security considerations
- Pairing is delegation with mandatory human sign-off. The QR token is a
bearer secret with a ≤90 s life and a single-use nonce burned atomically at
claim; possession alone never links — a human MUST approve on the Surface
(anti-QRLjacking), and the resulting companion token is
cnf-bound to the claiming device key. - Proof of possession everywhere it matters. Every companion connect and resume presents a fresh device-key signature over the current token hash with a bounded timestamp (±60 s). A stolen token without the device key is useless; a stolen resume key without both is too.
- Uniform denial. All pairing failures are
PAIR_DENIEDwith identical status/shape/timing budget — no oracle for expired vs replayed vs forged. - Expiry-to-deny. Unanswered confirms deny; dropped sessions deny pending confirms and error pending actions. No affirmative action ever results from silence.
- Takeover requires consent. A different approved device connecting over a live companion MUST be confirmed by the Surface alone — neither the incoming nor the incumbent device can authorize its own takeover.
- The client is never the authority. Tier, scopes, and ACLs are re-derived by the Hub per call; tool manifests are hints; context fields are untrusted input to the Provider (prompt-injection surface: providers SHOULD spotlight or delimit context-derived text).
- Public endpoints are rate-limited.
pair/claim(nonce guessing, DoS) strictly;pair/resultgenerously enough for its own poll cadence. - Attestation (App Attest / Play Integrity) upgrades trust in the device
claim; deployments MAY require it (
PAIR_DENIEDon absence/failure) and MUST record its verified/unverified status for audit. - Audit. pair start/claim/approve/deny/revoke, companion link/takeover, tool invocation, and confirm resolution MUST be auditable events.
Appendix A — registries
Message types (v1.0): error · session.open · session.linked ·
session.suspended · session.resumed · session.closed ·
session.heartbeat · session.refresh · session.refreshed · context.set ·
turn.input · turn.delta · turn.citation · turn.done · turn.cancel ·
turn.error · tools.advertise · action.request · action.result ·
ui.request · ui.response · pair.approval_request ·
pair.approval_response · pair.linked · pair.revoked.
Vendor extensions: x-<vendor>.<name>.
Error codes: AUTH_EXPIRED · SCOPE_DENIED · RATE_LIMITED ·
NONCE_USED · SESSION_GONE · TURN_ABORTED · CONTEXT_TOO_LARGE ·
PAIR_DENIED · UNSUPPORTED · INTERNAL.
Scopes: context.read · qa.ask · tools.invoke · pair.manage.
Companion default: context.read qa.ask. Surface (T2): all four.
Additions to any registry are minor-version changes; removals or semantic changes are major.