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.open negotiates capabilities; the Hub answers session.linked with the INTERSECTION. Absent capability ⇒ defined fallback: no tools → Q&A only; stt/tts none → 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.suspended with 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 → streamed turn.delta_ + turn.citation_ → turn.done (echoing contextRev) or turn.error.
  • ONE in-flight turn per session. A new turn.input while a turn streams IMPLIES cancellation: the Hub MUST emit turn.error{TURN_ABORTED} for the superseded turn before streaming the new one (voice barge-in).
  • turn.cancel stops the identified turn; the Hub responds turn.error{TURN_ABORTED}.
  • An in-flight turn does NOT survive a disconnect: after session.resumed the Hub MUST send turn.error{TURN_ABORTED} for any turn that was streaming.
  • Audio input (audioRef) requires the negotiated audio capability.

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:

rendering diagram…

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):

rendering diagram…

Turn with a gated write tool (§8–§10):

rendering diagram…

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_DENIED with 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/result generously enough for its own poll cadence.
  • Attestation (App Attest / Play Integrity) upgrades trust in the device claim; deployments MAY require it (PAIR_DENIED on 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.