# The Double Blind — verifiable privacy

Witbitz's core privacy claim: for an app built on it, **neither the platform operator nor the app's own owner can
read the users' data**. And — the part that makes it a *property* not a promise — that claim is meant to be
**checkable from the code**, not taken on faith.

This doc is the whole argument in one place: what holds today, exactly where it stops, and **Sealed Spaces** (§5) —
the opt-in tier that turns the remaining gap from a behavioural promise into a property of the build.

> See also: [async Spaces](./async-spaces.md) (the sealed ledger + decrypt-once render),
> [the Space link is the auth](./room-link-auth.md) (the keys live in the link), and
> [Verify it yourself](./verify.md) (run the checks).

---

## 1. Ciphertext at rest → the platform is blind

A Space's data is a snapshot **sealed under the room key `mk`** and stored as ciphertext; the platform never holds
`mk` (`agent/envelope.mjs` `seal`/`open`; the key is committed only as `commit(mk)`, [room-link-auth](./room-link-auth.md)).
At rest, to the operator, a Space is opaque bytes — storage and transport are **content-blind**.

There is **no operator recovery key.** Every record is sealed to the **room key alone** — the platform writes no
support or recovery recipient of any kind, so there is nothing readable-by-the-operator to read at rest. The double
blind is the only configuration that runs in production.

## 2. The decrypt-once render → the one plaintext door

Plaintext opens in exactly one *code path* — the certified render — but that path runs on **two** triggers: an
**AI turn** (open the ledger with a *present* member's `mk`, run the declared program, re-seal, **drop `mk`**) and
every **changed read** (the poll decrypts the new entries to return them, because the client is thin and decrypts
nothing itself — [async Spaces §3–4](./async-spaces.md)). Reads outnumber turns, so the honest exposure surface is
*the render*, not "only when the AI acts." The key reaches that render only from a **present** member's client, per
active window — never from storage — so the operator can act on the data only while a member is present, and learns
nothing at rest. Keeping it **one** auditable path is exactly what makes the attested tier tractable.

**The honest boundary, stated plainly.** Because the client is thin, `mk` travels to the render on *every*
read and write, and the server opens the ledger in place — a **decryption oracle** for the moments a member
is active. So *content-blind* means **at rest and against a passive (honest-but-curious) operator**, not one
that captures `mk` in flight; and because the whole ledger is sealed under one long-lived room key, an
operator that logged `mk` once could read that Space's history past and future — there is no per-Space
forward secrecy yet.

That boundary is the reason §5 exists.

## 3. Backendless → the app owner is blind too

The second blind is the one nobody else offers. A Witbitz app is a **static page with no backend of its own** that
connects *only* to the audited Witbitz API. So the app's *developer/owner* has no backend through which to read its
users either: the users' content is sealed to a key the owner doesn't hold, and the app has nowhere else to send
it. Data is encrypted **from** the owner, not merely **to** them.

To be exact — "backendless" is a claim about the *app*, not the platform. **Witbitz is still a backend** (one
shared, constrained, inspectable platform). What's removed is each developer's *own, opaque, app-specific* server.
So the trust surface isn't smaller in a hand-wavy "no infrastructure" sense — it's **consolidated**: one auditable
platform to check, instead of N unknown app backends. That consolidation is exactly what makes the footprint
declarable (§4).

## 4. Verifiable → check the code, not the operator

Because the app is a static page with a declared egress (it can only talk to the Witbitz API), its **entire data
footprint is inspectable and declarable**. You verify the code and a signature, not a "trust us" policy.

Four of those checks run in your own terminal **today** — the admission gate enforcing, the egress lock, the
content-blind store, and the deployed render being a reproducible build of published source.
**[Verify it yourself](./verify.md)** is the commands. What is *not* yet checkable about your own traffic is the
render being unobservable **in use**; see §6.

## 5. Sealed Spaces — the tier that removes the key from our reach

*Opt-in, and a **tier** — not a replacement.*

Today's Spaces are double-blind, but §2's blindness is **behavioural**: the operator is trusted not to look at a key
it does, briefly, receive. A single capture defeats it — a logging toggle, a compelled order, a compromised render
reads the entire history, past **and** future, because the key arrives on a schedule and nothing bounds one room's
exposure. A Sealed Space takes the key off that schedule. **The server never receives `mk`. There is no capture to
toggle.** The platform stores ciphertext and holds exactly one capability — the ability to **append**, never to
**read**.

### 5.1 The design — four changes, each assembling a primitive already in the platform

1. **The ledger becomes an append-only log of individually sealed entries** — not one snapshot re-sealed on every
   write. This is the load-bearing move: appending no longer requires reading, which is exactly what today fuses
   "write" to "hold the key."
2. **The client decrypts locally.** It syncs sealed history once, caches it on the device, then pulls sealed deltas
   by cursor — the same poll shape as today, just opaque to the server.
3. **A session-scoped, ephemeral actor does the reasoning.** On the first turn the client seals the assembled
   context *to the actor's session public key*; after that the actor receives only new messages. It never sees
   `mk`, holds nothing across sessions, and is evicted on idle.
4. **Write-back is a write-only capability.** The actor appends its reply by sealing to the Space's public write
   key (`sealTo` / `openBox`). It can add to the ledger without being able to open it.

**The net:** the platform routes ciphertext, stores ciphertext, and can append ciphertext. It cannot read the room.

### 5.2 What it protects — and what it doesn't

A security claim is only worth the threat model it names. Three columns, not two.

**Structurally protected:** at-rest capture (storage dumps, a compelled disk or database order, a logging toggle, a
compromised render — the platform holds only ciphertext and a write-only key); and **blast radius**, because
per-Space epoch keys mean one Space's compromise doesn't cascade and rotation bounds it going forward.

**Not protected**, stated plainly:

- **The live session** is plaintext to the ephemeral actor while it reasons. Sealed Spaces closes the *at-rest*
  hole; it does not make the AI compute over data it cannot read. That is the [attested tier](./the-attested-tier.md).
- **The endpoints.** A compromised member device sees plaintext; true of every end-to-end system.
- **Metadata** — who is in a Space, when they post, how large the messages are. Sealing content doesn't seal traffic
  shape.

Naming the third column is not a weakness. It is the honest meaning of *content-blind*: blind **at rest**, bounded
**in use**.

### 5.3 What it costs

- **No member-absent turns.** With no client present, nothing decrypts the context to feed the actor — so "the agent
  acts while everyone's away" ([delegated authority](./delegated-authority.md)) doesn't exist in a Sealed Space.
  This is why Sealed is a *choice*, not the default.
- **Prompt caching, likely lost.** An ephemeral per-session actor re-receives the full context each session, so
  server-side prefix caching probably goes away. **Likely the largest recurring cost; worth measuring before
  committing.** A **warm-actor** variant — residency billed, context held longer — is the dial that trades a little
  blindness back for cache.
- **Cold-device sync.** A new device pulls sealed history before the first render — a first-open cost, not per-turn.
- **Actor residency** is billed while a session is open; not scale-to-zero for the length of a burst.
- **Forward-only revocation.** Removing a member is an epoch re-key — the mechanism [the Bridge](./the-bridge.md)
  already runs. It cannot un-share what they already read; inherent to end-to-end, and worth saying out loud.

### 5.4 Status

**In hand:** per-entry sealed append and content-blind polling (scaffolded behind a flag, inert until enabled);
`sealTo`/`openBox` for write-only append; epoch re-key in the Bridge; the reproducible-build + egress-lock +
certificate stack. **To build:** the sealed client → actor session channel; ephemeral-actor lifecycle and idle
eviction; cursor-based sealed-delta sync as the default read path; epoch-key distribution to members without the
platform in the loop. **To measure first:** the real prompt-caching cost on a busy Space.

## 6. Status — what holds today

Being precise, because this is exactly where a claim can overreach:

| Property | Status |
|---|---|
| Ciphertext at rest (sealed ledger, platform holds no key) | **shipped** |
| No operator recovery key (sealed to the room key alone) | **shipped & CHECKABLE** — `POST {"op":"sealed"}` → `recipients:["room"]` ([verify §Test 5](./verify.md)) |
| Admission gate enforces (reads *and* writes refused without an allow-listed identity) | **shipped & CHECKABLE** ([verify §Tests 1–3](./verify.md)) |
| Decrypt-once render, `mk` from the client only, dropped after | **shipped** |
| Backendless static app (no backend of its own) | **shipped** |
| Egress-lock (CSP `connect-src`/`script-src` allowlist) | **shipped & ENFORCED** (block-mode) ([verify §Test 4](./verify.md)) |
| Client E2EE primitive (the sealed-blob envelope) | **shipped** |
| Signed build **certificate** (deployed build hash + egress, signed from ground truth) | **shipped & CHECKABLE** — `/cert.json` ([verify §Test 6](./verify.md)) |
| **Reproducible build** of published source (rebuild it → the deployed hash) | **shipped & CHECKABLE** — `/source.tar.gz` ([verify §Test 7](./verify.md)) |
| Sealed Spaces (the platform never receives `mk`) — §5 | **designed** — primitives in hand, the session channel is not built |
| Attested tier (operator provably blind **in use**) | **built & independently verifiable — NOT on production traffic** ([the attested tier](./the-attested-tier.md)) |
| Attested **client** (knowing the code running on the user's side) | **design** ([the attested client](./the-attested-client.md)) |

## 7. The moat — honestly

The moat is **not** "incumbents *can't* do this." They can: Apple's Private Cloud Compute ships attested confidential
inference today, and a competent team could match everything Witbitz has shipped in a quarter. Two things are
actually defensible:

- **Neutral + backendless.** A vendor cloud reads *to itself* by default; a Witbitz app has no backend of its own
  and isn't bound to one provider's stack. The trust surface is **consolidated to one auditable platform, not
  removed** — the honest claim, *not* "no backend" (Witbitz **is** the backend, singular and inspectable). Awkward
  for an incumbent whose business *is* the readable backend to adopt — but not something they're *barred* from.
- **The verifiability layer.** The signed certificate and the reproducible build have **shipped**, and the attested
  tier is **built and independently verifiable** — though not yet serving production traffic. So the honest position
  is no longer "a direction we're building toward": four checks run in a stranger's terminal today, and the fifth is
  demonstrated on hardware and waiting on operations rather than on invention. What remains genuinely unbuilt is
  Sealed Spaces (§5) and the attested **client**.

## 8. Code map

| Concern | Where |
|---|---|
| At-rest ledger seal (**symmetric, under `mk` only**) + key commitment | `agent/envelope.mjs` (`seal`, `open`, `commit`, `newRoomKey`); `agent/sessionStore.mjs` `encodeMemory` — one recipient, `mk`. **No operator/beta/admin recipient in prod.** |
| Recipient sealing (**ECDH `sealTo`/`openBox`**) — seals to a **member/agent** public key, *never an operator* | scoped agent reads (`agent/spaceScopedView.mjs`), Bridge epoch keys (`agent/bridgeMembership.mjs`), the collection-blob — a **separate** mechanism from the at-rest ledger above |
| Sealed ledger + decrypt-once render | `agent/sessionStore.mjs`, `agent/asyncTurn.mjs` |
| Content-blind poll (etag over ciphertext) | `agent/spacePoll.mjs` |
| Egress-lock CSP (**block-mode, enforced** on prod Spaces) | `spaces/tools/gen-space-csp.mjs` |
| The render, published and readable (~600 lines) | [github.com/kibitz-chat/witbitz-render](https://github.com/kibitz-chat/witbitz-render) |

---

*Sealed Spaces is not "trust us less." It is "you don't have to." The current tier remains for the workflows that
need an always-on agent; Sealed is for the rooms where the key should never leave your hands — and where you can
prove it didn't.*
