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
- 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
- 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."
- 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.
- Content-blind platform. Witbitz mediates but cannot read the content (sealed under the epoch key). Governance = membership + capabilities + attribution + audit + confinement — never content inspection.
- 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.
- 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 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)
JOIN(space, identity, admissionProof)→ the party fetches its sealedkeyGrant(s)(opened with its private key) + its membership credential (caps). Governed byadmissionPolicy.READ(space, cursor)→ entries sincecursor, each with itsepoch; the party decrypts with the epoch key it holds + verifies eachsig. Poll-shaped — a read teaches the server nothing.SUBMIT(space, entry)→ server checks admitted · hassubmit· sig valid ·epoch== current · within rate/size caps, assignsseq+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?)(needsadmit) → open a new epoch, seal its key to all members incl. the invitee, optionally seal granted past epochs.REVOKE(space, party)(needsrevoke) → 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.
Membership succession — what makes a PUT /members legal (a deliberate decision)
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
- 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 the agent-key admission pattern (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 (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.
Machine-readable source: bridge-protocol.md · every doc in one fetch: llms-full.txt (HTML)