Witbitz docs HomeTrustAll docs

The Bridge Protocol (v2)

How a party — witbitz-native OR external — connects to a shared Space. This is the actual product: governed collaboration between sovereign Spaces. Companion to the Bridge overview and grounded in the live async Spaces engine, the double-blind privacy model, and identity and admission. 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

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 historydefault: 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 platform's core authority model — who may do what, on whose authority — and it reuses the capability pattern described in identity and admission.

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 (BUILT): pinned checkpoints detect a rewrite of your view (chainMatchesCheckpoint), and cross-party comparison detects EQUIVOCATION — a server serving two internally-consistent but different logs to two parties (it can't forge entries, but the party signature covers only the entry core, not the server's seq/at/prevHash, so it can reorder/drop + re-chain each fork and checkpoint it). Neither party can tell alone. When they gossip the checkpoints they pinned, checkpointConsistentWithLog finds the other's head isn't on my chain, and verifyEquivocationProof(serverPub, cpA, cpB) yields a self-contained proof — two conflicting checkpoints both genuinely the server's — that convicts the operator by its own two signatures, verifiable by anyone (agent/bridge.equivocation.test.mjs; a forged checkpoint can't frame an honest server). The gossip transport flow is now built + demonstrated (agent/bridge.gossip.test.mjs, mirrored in the browser client's auditAgainstPeer): each party pins the head the server showed it, exchanges it over a channel INDEPENDENT of the audited server (routing gossip through the Bridge would let it equivocate on the gossip too), and auto-flags by comparing the peer's head to its own log — and on a fork the two checkpoints are a third-party-verifiable proof (only the server's public key needed; neither party trusted, neither able to fabricate it). What remains product-side, not protocol: wiring the physical out-of-band channel in a deployment (the demo/live-e2e simulate the exchange; the honest live round-trip raises no alarm). At M4: promote to a full public transparency log (Merkle + independent witnesses) for public verifiability. — settled: checkpoints + equivocation-detection + gossip flow now, full transparency 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)

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

Every membership change (an ADMIT/REVOKE, or a re-cap) is authored client-side and PUT as a new record. Because that record is attacker-reachable input — not trusted server state — the platform verifies it is a legal successor of the current head, not merely "internally consistent and signed by some admit-holder." A record next succeeds the stored head cur iff all hold: 1. Same space, strictly-advancing epoch (next.epoch > cur.epoch). Monotonic ⇒ an older record can never be replayed, and two competing records at the same epoch can't fork the chain (first writer wins; the second is stale). 2. Signed by a party that holds admit in cur — authority is re-checked at apply time against the current head, never "in some earlier record." This is what defeats the pre-signed-transition attack: a party that signs a record while it holds admit, is then revoked, and submits the stale-but-valid record — by submission time it is no longer an admit-holder in cur, so the record is refused (whatever epoch it named). The signature is necessary, never sufficient. 3. ≥1 admit-holder remains (no-lockout). An admit-holder MAY re-cap or remove any member, including the founder — that is precisely what admit grants, and in practice admit is held only by a trusted party (in the asymmetric-bridge demo, never by the counterparty or the read-only assistant) — but it may not brick the space by leaving zero admit-holders.

Deliberately NOT enforced: key-completeness of next (a member left without the new epoch key simply can't decrypt — self-evident to that member, not a server concern) and prev-hash chaining of the membership sequence (equivocation between competing heads is caught by the checkpoint/gossip layer above, not by membership linkage). These are decisions, revisitable — but the difference between "authorized" and "authorized to do this specific transition" is closed by rules 1–3, each with an adversarial test (agent/bridge.revocation.test.mjs) and a mutation-gate proving it is load-bearing.

Founding (create) — the one write with no predecessor

create is the only write with no prior head to check against, so it stands on two rules: (a) the genesis record (epoch 0, ≥1 admit-holder) must be signed by the founder it names — so no one can found a space in another party's name (verifyCreate); (b) whoever creates an unclaimed id first owns it — re-creating an existing id is refused (409), never overwritten. Consequence for the product: a space id must be minted at create as a commitment to the founder, never pre-shared — if an id is guessable or leaks before first use, an attacker who creates first becomes founder and holds admit. (Binding the id cryptographically to the founder key — so a victim's id is un-squattable even if leaked — is the stronger form, a considered future hardening; today the guarantee is first-come-first-served on the id.)

Concurrency — two admit-holders, the same next epoch

Resolution is first-write-wins. The monotonic rule (§1) is not enough on its own — a naïve read-check-write would let two genuinely concurrent writers both read cur = N, both pass N+1 > N, and both write (last-write-wins, silently clobbering a revocation). So the epoch advance is an atomic compare-and-set: the store applies next only if the stored head is still the cur we verified against (DynamoDB ConditionExpression on the stored epoch); a concurrent second writer's CAS fails and it gets 409 conflict, to re-base on the new head and resubmit at the next epoch. So a revocation cannot be undone by racing it — not sequentially (the later writer fails the monotonic check) and not concurrently (the loser fails the CAS). Atomicity is proven deterministically by a handler unit test (a lost CAS → 409); a client e2e can only exercise it (fire both, assert exactly one lands — best-effort against network timing), which live-e2e.mjs does. What none of this settles is a malicious server that ignores its own CAS, or accepts a forged write, or shows different heads to different parties — that class (equivocation) is the job of the server-signed checkpoints parties pin + gossip (§Audit), not of the succession rules. Its detection is now built (checkpointConsistentWithLog / verifyEquivocationProof, with a two-reader demo test); what remains the parties' to run is the out-of-band gossip transport.

The meeting — turn model & liveness

Identity & admission

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 (Cut 1, 6/6) is the spine: a sealed shared ledger (crossToBridgeSUBMIT 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.

Machine-readable source: bridge-protocol.md · every doc in one fetch: llms-full.txt (HTML)