# The Bridge Protocol (v2)

*How a party — witbitz-native OR external — connects to a shared Space. This is the actual product: comedians, couples, negotiation are each one application of it. Companion to [the Bridge (M5)](./bridge-m5.md) + [the vision](./witbitz-vision.md); grounded in the live [M2 engine](./async-spaces-m2.md), the [E2EE collection blob](./e2ee-collection-blob.md), and [privacy certification (M4)](./privacy-certification.md). v1 drafted 2026-07-25; **v2 (this) refined the same day** — added key epochs, trustless audit via checkpoints, and capabilities.*

## Thesis
A Bridge is a **governed relationship between sovereign Spaces** — an interop + governance boundary, not a walled garden. The parties may run **different agents, different humans, different systems**; witbitz governs the connection without owning both ends (the email/OAuth move). Witbitz is not the room — it's the **governance**: who may connect, on whose authority, what may cross, with what proof.

## Model
- **Space** — a sovereign room; content sealed under its own key; ≥1 members; optionally an agent. Private rooms *and* the shared space are Spaces.
- **Shared Space** — a Space multiple parties connect to (the meeting venue). Its content is sealed under an **epoch key** `sharedMk_e` (see *Key epochs*).
- **Party** — one side = an **identity** (a keypair) + optionally an **agent** + **human members**, holding a **capability set**. Two implementations of one contract: **native** (a witbitz Space) and **external** (someone else's agent over MCP/A2A/signed-HTTPS + their humans).
- **Bridge** — the governed relationship = a shared Space + its admitted parties (with capabilities) + the policy (admission, history, audit, revocation).

## The five invariants — get these right and the rest is mechanism
1. **Confinement is structural.** An epoch key opens *only* the shared space, *only* for that epoch. Private rooms are under different keys entirely → an external/hostile party **cannot** reach them. Not "we won't show you"; "you don't hold the key."
2. **Attribution by signature.** Every entry is signed by the party's key; verified against the public membership record. Provable authorship; no party can speak as another.
3. **Content-blind platform.** Witbitz mediates but **cannot read** the content (sealed under the epoch key). Governance = membership + capabilities + attribution + audit + confinement — **never** content inspection.
4. **Source-side egress.** "Nothing leaves a Space unless allowed" is enforced **at the source** — each party approves its own outbound loads *before* sealing. The platform never reads content to enforce.
5. **Governed membership + trustless audit.** Admission/revocation are capability-gated and re-key the space; the log is signed per-entry (attribution) *and* checkpointed by the server (integrity you don't have to take on trust).

### Where each invariant actually lives (the honest table)
| Invariant | Enforced by |
|---|---|
| Confinement | the **key boundary** — epoch key ≠ private `mk`; structural, not policy |
| Attribution | the **platform**, verifying each entry's client signature vs the membership pubkey — *on ciphertext* |
| Audit integrity | **client signatures** (authorship) + a **server-signed checkpoint** parties *pin & gossip* (tamper-detection) |
| Membership & who-may-do-what | the **platform**, checking the party's **capabilities** |
| What content crosses | the **source party** (approve → seal). The platform is blind and cannot police content, by design |

## Key epochs — because membership changes
The shared space is a sequence of **epochs**. Epoch `e` has its own key `sharedMk_e`, **sealed to each current member's public key** (recipient-key model, reuses `sealRecipientKey`; the server stores only ciphertext-per-member). Every entry is tagged with the epoch it was sealed under.

**Every membership change — a join OR a revoke — is an epoch boundary:** it mints a fresh `sharedMk_{e+1}` sealed to the *new* member set. Consequences:
- **Revocation gets real forward-secrecy** — the removed party is not sealed the new epoch key, so it can read no *future* content. It keeps whatever past epochs it already held (you can't un-see what you saw).
- **A joiner sees from its join-epoch forward, not the history** — *default: no back-history* (you're invited into a conversation, not handed its transcript). The admission policy MAY explicitly seal specific past-epoch keys to a joiner (e.g. a couple choosing to bring their therapist up to speed). — **settled: default no-history, policy can grant.**

## Membership & capabilities (the governance surface — public metadata, content-blind)
```
Membership {
  space, epoch,                       // the current epoch
  members: [{
    party, pubKey,                    // party id = fingerprint(pubKey)
    caps: subset of [read, submit, admit, revoke],
    admittedBy, admittedAt,
    keyGrants: { epoch -> sealed(sharedMk_epoch, pubKey) }   // WHICH epochs this party can open
  }],
  admissionPolicy,                    // who may admit: owner | all-members-consent | designated
  historyPolicy,                      // default: join grants current epoch only
}
```
**Capabilities** express who-may-do-what: an **observer** (`read` only — an auditor/supervisor), a normal **party** (`read+submit`), an **admitter** (`+admit/revoke`). `SUBMIT`, `ADMIT`, `REVOKE` are each gated on the caller's caps. This is the vision's core — *who may do what, on whose authority* — and it reuses the [capability layer](./data-flow-and-threat-model.md).

## Audit & transparency — the invariant only holds if you needn't trust *us*
Two guarantees, cleanly separated:
- **Attribution** — the **client** signs `{space, party, kind, actor, epoch, body, clientHeadSeq, nonce}`. Proves authorship, independent of ordering (`clientHeadSeq` = the seq the client last saw = causal context, advisory).
- **Log integrity** — the **server** maintains the hash chain (`prevHash` = hash of the prior entry) and periodically signs a **checkpoint** `{space, epoch, headSeq, headHash, at, serverSig}`. Parties **pin** checkpoints; a server that later reorders/drops entries can't match a checkpoint someone already holds. Parties **gossip** checkpoints out-of-band to catch *equivocation* (the server showing different heads to different parties).

**Now:** pinned checkpoints detect a rewrite of *your* view. **At M4:** promote the checkpoint to a full **public transparency log** (Merkle tree + independent witnesses) for *public* verifiability. — **settled: checkpoints now, full log at M4.**

## The entry (the unit that crosses)
```
Entry {
  party, kind:'load'|'post', actor:'agent'|'human',
  epoch,                              // which key sealed it
  body: sealed(content, sharedMk_epoch),   // CIPHERTEXT — platform never sees plaintext
  clientHeadSeq, nonce, sig,          // client signature over the above (attribution)
  seq, at, prevHash,                  // server-assigned order + chain (integrity)
}
```
`load` = a contributed position; `post` = a message/turn — same op, distinguished by `kind`. Body is fully opaque; any structure lives *inside* the sealed content (a clear-text type would leak).

## The operations (transport-agnostic)
- **`JOIN(space, identity, admissionProof)`** → the party fetches its sealed `keyGrant(s)` (opened with its private key) + its membership credential (caps). Governed by `admissionPolicy`.
- **`READ(space, cursor)`** → entries since `cursor`, each with its `epoch`; the party decrypts with the epoch key it holds + verifies each `sig`. **Poll-shaped — a read teaches the server nothing.**
- **`SUBMIT(space, entry)`** → server checks *admitted · has `submit` · sig valid · `epoch` == current · within rate/size caps*, assigns `seq`+`at`+`prevHash`, appends. Returns `{seq}`. (= load AND post.)
- **`MEMBERS(space)`** → the public membership record (parties, pubkeys, caps, policies, current epoch) so any party can verify signatures and know who's on the other side.
- **`CHECKPOINT(space)`** → the latest server-signed checkpoint, to pin & gossip.
- **`ADMIT(space, invitee-pubKey, caps, historyGrant?)`** *(needs `admit`)* → open a new epoch, seal its key to all members incl. the invitee, optionally seal granted past epochs.
- **`REVOKE(space, party)`** *(needs `revoke`)* → open a new epoch sealed to the *remaining* members. The revoked party keeps its old grants, loses the future.

A native party and an external party are two *implementations* of this contract.

## The meeting — turn model & liveness
- **No central orchestrator.** Each party's agent **reads the shared thread and acts on its own volition** (async): a native comedian on its own Space's turn; an external agent by polling + deciding. **Humans post directly, anytime.** The server only serializes order (`seq`) — it never decides whose turn it is.
- **Liveness** — poll stays the (blind) content read; add a **content-free webhook/push** (*"activity in space X, epoch e, head n"* — no content) so external parties don't busy-poll. Reuses M2's away-notification plan.

## Identity & admission
- **Base identity = a keypair** (Ed25519/ECDSA); party id = `fingerprint(pubKey)`; every entry signed by it. Native parties reuse **[agent-key admission](./data-flow-and-threat-model.md)** (cert-bound keys); external parties present pubkey + admission proof.
- **Admission proof** = a host-signed **invite grant** naming the invited pubkey + caps, or existing-members' **consent** (per `admissionPolicy`).
- **Designed to wrap** — a **DID / verifiable credential** or **OIDC** identity can later *bind* to the keypair; we don't pick a federation now (the key is enough to ship, the wrapper is additive).

## Bindings (the contract is the invariant; the wire is an adapter)
**Native** (the witbitz Lambda `/space`, extended) + **signed HTTPS/webhook** — the universal external binding, **shipped first** (poll `READ`; POST `SUBMIT`/`JOIN`; each request signed with the party key). **MCP** (shared space as resources + a `submit` tool) and **A2A** follow as those standards settle.

## Abuse bounds
Per-party **blind** caps — submits/minute, bytes/entry, total entries — all metadata, enforced without reading content.

## Settled decisions
Content-blind platform + **source-side policy** (no central content inspection) · **key epochs**, joiner **no-history by default** (policy can grant) · **checkpoints now, public transparency log at M4** · **capabilities** `{read,submit,admit,revoke}` in v1 · keypair base identity (DID/OIDC wraps later) · signed-HTTPS binding first (MCP/A2A follow) · poll reads + content-free push.

## Mapping to the engine — Cut 1 → the protocol
[`agent/bridge.mjs`](../agent/bridge.mjs) (Cut 1, 6/6) is the spine: a sealed shared ledger (`crossToBridge` ≈ `SUBMIT` a load; `bridgeLog` ≈ audit; `bridgeDigest` ≈ a `READ` view), content-blind (wrong key opens nothing). To become the protocol it needs:
- **(a)** address by **shared-space id**, not `bridgeId(roomA,roomB)` — the one place N=2 is baked in;
- **(b)** **key epochs** — per-epoch keys sealed per member; entries epoch-tagged;
- **(c)** **membership + capabilities + `JOIN`/`ADMIT`/`REVOKE`** (reuse `sealRecipientKey`);
- **(d)** **signed attribution** (client `sig`) + **server `prevHash` chain + signed `CHECKPOINT`**;
- **(e)** the **native | external party interface**.

**Cut 2** = implement `JOIN / READ / SUBMIT / MEMBERS / CHECKPOINT` (+ `ADMIT`/`REVOKE`) with epochs, capabilities, and the signed-HTTPS binding + a **stub external party** for the interop test. **Cut 3** = the advocate persona + escalation eval + end-to-end (native couple) + external-party smoke.

## Deferred (designed for, not built)
Public transparency log + witnesses (M4) · DID/OIDC identity wrapping · MCP + A2A bindings · source-side policy agents (DLP) · the co-host turn with an *external* agent · gossip transport for equivocation-catching.
