3257afe8c0
Tranché par la cascade plutôt qu'en attendant une réponse : le niveau 3 (ORM) ne dit rien des inbox, le niveau 2 non plus — `@ng-org/web` n'expose aucune méthode contenant « inbox » et la session n'en publie aucune. C'est donc le modèle du moteur qui décide, et il dit DEUX : un site porte une inbox sur son repo de store public et une autre sur son protégé (`engine/verifier/src/site.rs:127-152`), les seuls `AddInboxCap` du moteur, `new_store_default` n'en posant une que `if !private`. Elles sont adressées séparément jusque dans les enregistrements de contact, qui choisissent leur prédicat selon le profil visé — `ng:site_inbox` pour un profil public, `ng:protected_inbox` sinon (`engine/verifier/src/inbox_processor.rs:787,823-824`). `userInbox(id)` en exposait une : une cardinalité que cette bibliothèque avait inventée, et que le nom `walletInbox` avait contribué à masquer. Elle prend désormais le scope, et le store PRIVÉ n'en a pas — d'où `InboxScope` plutôt que `Scope` : demander l'inbox privée n'est pas une recherche qui ne rend rien, c'est une question sans référent dans le modèle, et le type l'interdit. `myInboxes` énumère les deux, `isOwnInbox` reconnaît les deux. Le shim garde un triple par (user, scope). 160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
200 lines
26 KiB
Markdown
200 lines
26 KiB
Markdown
# Brief — the virtual user boundary
|
||
|
||
**Status: specified 2026-07-30; all seven steps done** (four planned, three added as the work uncovered them). Sits alongside `2026-07-20-caps-emulation-alignment.md` (the wider caps chantier) and `2026-07-27-p1a-cap-surface.md` (the surface, implemented). This lot is about something more fundamental than either: **what a virtual user is allowed to reach.**
|
||
|
||
## Why this lot exists
|
||
|
||
A virtual user must **simulate the boundary of the future single-user wallet**. Today it does not: it is a grouping fiction — a shim account listing three index documents — and nothing enforces it. Every access function reaches any document of any identity, given a session id and a NURI.
|
||
|
||
Stated by the PO on 2026-07-30, on discovering the state:
|
||
|
||
> A virtual user must simulate the boundary of the future mono-user wallet. So the access functions must all be restricted to the virtual user currently "connected" (`setCurrentUser`). No cross-wallet access may be permitted, otherwise we are building on a fundamentally wrong model.
|
||
|
||
This is the same failure mode the whole caps chantier exists to prevent, one level down. P1a fixed the *shape* of reading (possession, not an ACL). It left the *reach* unbounded — and a consumer coded against an unbounded reach is coded against a world that will never exist, exactly like one coded against an ACL.
|
||
|
||
## The rule
|
||
|
||
> **The only reads/writes not confined to a virtual user are those that make multi-wallet operation possible at all** (e.g. the index of virtual users). — PO, 2026-07-30
|
||
|
||
And its sharpened form, which decides the hard cases:
|
||
|
||
> **Nothing common — only indexing mechanisms to make the virtual users work.** — PO, 2026-07-30
|
||
|
||
So an exemption must be *plumbing*, never *pooled user data*. The test: **does removing it stop the virtual users from functioning, or does it merely stop users from seeing each other's content?** Only the first justifies living outside a wallet. The shim passes (remove it and no wallet is resolvable); a shared index of user announcements does not (remove it and every wallet still works — you simply have to be given links).
|
||
|
||
Everything else is confined. The exemption list is short, explicitly named, and each entry has to justify itself against those two sentences — an exemption that merely *helps* is not an exemption.
|
||
|
||
## The premise that collapsed: there is no discovery
|
||
|
||
Recorded here because it removes a whole module rather than guarding it (PO, 2026-07-30 — see [`../readcap-and-nuri-model.md`](../readcap-and-nuri-model.md) §4ter-bis, where the principle is documented in full):
|
||
|
||
> **You cannot discover. You can only follow links.** NextGraph is local-first: publishing is *place the data in your public store* **and** *circulate the link* — into inboxes, or into somewhere already reachable by the people concerned. It is seen only by those who received the link. Private distribution is the same act plus the ReadCap.
|
||
|
||
`discovery.ts` therefore fails on **both** counts: it emulates a global-list capability the target will never have, and it is pooled user data across wallets. It is not a boundary to guard, it is a module to remove — with `watchShape('public')`'s discovery fold, `INDEX_ACCOUNT`, and the `submitToIndex` guard along with it.
|
||
|
||
What replaces it is not a mechanism but the model itself: a link reaches you through an **inbox**, or through a document you already hold. Which makes the inbox the bootstrap of the entire reachability graph — the reason its guard (below) and its automatic processing matter more than they first appear.
|
||
|
||
*Consequence for P1a's acceptance test, resolved*: `test/watch-shape.test.ts` (e) proved "a harvested bare reference reads nothing, the repo link reads the document" **on the discovery fold**. The property is independent of discovery and survives — `test/cross-user-access.test.ts` already proves it on the model's own terms (Bob follows a reference found in a document he holds), so (e) was dropped rather than re-based.
|
||
|
||
## The good news: the boundary already exists
|
||
|
||
**The keyring is the boundary.** A document is legitimately reachable when `capFor(doc)` answers — either because this wallet created it (its scope index refiles the cap, the emulated `AddRepo { read_cap }`) or because someone delivered the cap to it. No new notion is needed; the guard is written. What is missing is applying it.
|
||
|
||
And the surface to guard is small. Everything in the library reaches NextGraph through **four functions in two modules**:
|
||
|
||
- `docs.docCreate`, `docs.sparqlUpdate`, `docs.sparqlQuery`
|
||
- `subscribe`'s `ng.doc_subscribe`
|
||
|
||
Nothing else touches `ng`. (`open-repo` only tests whether `doc_subscribe` exists; `ng-proxy` is the app-facing proxy.)
|
||
|
||
## What is confined, and what is exempt
|
||
|
||
**Exempt — each one passes the rule:**
|
||
|
||
| Exemption | Why it makes multi-wallet operation possible |
|
||
|---|---|
|
||
| The store-root **pointer** + the **doc-shim**, to resolve THE CURRENT account | This is the index of virtual users. Without it no virtual user is resolvable at all. **Resolution only** — enumerating every account is not covered (see below). |
|
||
| The **reserved accounts** (the inbox anchor; `@index` is gone with `discovery.ts`) | They host infrastructure documents; they are not anybody's wallet. |
|
||
| **Depositing** into another wallet's inbox (write-only) | Without a cross-wallet write channel there is no sharing, hence no useful multi-wallet — and it carries no pooled data: a deposit is addressed to one wallet, not shared between them. This IS the NextGraph model: an inbox deposit is anonymous and sealed, and grants the depositor nothing in return. |
|
||
|
||
~~Reading a discovery index~~ — **withdrawn 2026-07-30**, on both counts: it emulates a capability that does not exist, and it is pooled user data. See *The premise that collapsed* above.
|
||
|
||
**Confined — none of these passes the rule:**
|
||
|
||
| Path | Today | Becomes |
|
||
|---|---|---|
|
||
| `docs.sparqlQuery` / `sparqlUpdate` (**exported from the SDK entry**) | any document, any wallet | guarded on the anchor: the cap must be held |
|
||
| `inbox.read` / `readSynced` / `watch` | **any inbox, including someone else's** | only inboxes belonging to the current wallet |
|
||
| `subscribeDoc` | any document | only documents whose cap is held |
|
||
| `storeRegistry.listEntityDocs` / `resolveReadGraphs` | fan-out over every account | **removed** — cross-wallet enumeration, and its former justification (feeding discovery) is gone too |
|
||
| `storeRegistry.allAccounts` / `loadShim` | enumerates every virtual user | **removed**, or reduced to the reserved-account resolution that infrastructure needs |
|
||
| `storeRegistry.ensureAccount(id)` | any id | the current identity, plus the reserved accounts |
|
||
| `readUnion` | ✅ already guarded (P1a) | unchanged |
|
||
|
||
## The breach P1a opened, and which this lot must close first
|
||
|
||
`inbox.read` has no guard, and since P1a it **absorbs caps into the reader's keyring**. So:
|
||
|
||
```ts
|
||
setCurrentUser("mallory");
|
||
await inbox.read(bobsInbox); // mallory pockets the caps addressed to Bob
|
||
```
|
||
|
||
Directed sharing is therefore defeatable by anyone who knows an inbox NURI. Strictly speaking this is not a regression — the inbox was never guarded — but before P1a it carried nothing that granted access, and now it does. **This is the first thing to fix**, and it is arguably P1a's own debt rather than this lot's.
|
||
|
||
Closing it needs a notion that does not exist yet: **"my inbox"**. Today an inbox is an arbitrary NURI supplied by the caller. Which is the same brick as the PO's other instruction, so they should land together:
|
||
|
||
> Processing inboxes is the polyfill's job, not the app's — as soon as the app connects a user/wallet, at **two levels**: the wallet inbox (where ReadCaps arrive) and the inbox of **every document the user owns**.
|
||
|
||
## Design notes for the implementation
|
||
|
||
**Scaffolding in the keyring, not in the exemption list.** The current account's three scope-index documents belong to its wallet, so they should be *in its keyring* rather than exempted. That keeps the exemption list down to what genuinely serves multi-wallet operation (the shim, the reserved accounts). Only the pointer and the doc-shim stay outside.
|
||
|
||
**Exemptions are named, never inferred.** A NURI is exempt because it is *the* shim document or *a* reserved account's document, resolved as such — never because it "looks like infrastructure". An inferred exemption is a hole.
|
||
|
||
**Write-only really means write-only.** Depositing into another wallet's inbox must not make that inbox readable, subscribable, or listable as a side effect. This is the one asymmetric permission in the model and it needs its own test.
|
||
|
||
**The guard belongs to a function, not to a position.** One named predicate ("may the current wallet reach this document?"), called at each of the four passage points — not four inline checks that drift apart.
|
||
|
||
## Hardening is the polyfill's responsibility, not a negotiation with the consumer
|
||
|
||
Stated by the PO on 2026-07-30, closing the question "what does the app actually use?":
|
||
|
||
> We do not need to know what the app uses: the polyfill must harden **everything it exposes**. That is its responsibility. **Nothing may allow its own mechanisms to be bypassed** — the virtual user in particular.
|
||
|
||
This settles how the lot proceeds, and it generalizes past it. A surface that lets a caller go around the wallet boundary does not merely risk misuse: it **teaches a model that will not exist**, which is the one thing this library exists to prevent. So an exposed function that can bypass a mechanism the polyfill provides is a defect *whether or not anyone calls it that way*, and "the consumer might depend on it" is not an argument for keeping it — if the consumer depends on it, the consumer depends on something the target will refuse.
|
||
|
||
Applies to every exported surface, including ones added later: **if it is exposed, it is guarded**.
|
||
|
||
## What this breaks
|
||
|
||
`docs.*` is exported from the SDK-identical entry and can reach any document; the cross-account fan-out enumerates every wallet; `discovery.*` disappears entirely. The consumer will have to change where it relied on any of them. That is the point, not a side effect: each one is the API starting to tell the truth about a boundary that will exist. Update `../migration-guide.md` accordingly.
|
||
|
||
## Order of work
|
||
|
||
1. ~~**Remove `discovery.***~~ — **DONE 2026-07-30.** `src/discovery.ts` and `test/discovery.test.ts` deleted; `INDEX_ACCOUNT`, `watchShape`'s public-scope fold and its discovery-index container subscription, `nurisFromRef`, the `submitToIndex` guard, and the e2e discovery block all removed. P1a's acceptance test did not need re-basing: `test/cross-user-access.test.ts` already proves the same property (a bare reference reads nothing, the link reads the document) on the model's own terms — following a link — so `watch-shape.test.ts` (e), which proved it on the discovery fold, was dropped. Docs realigned: the ADR is marked superseded, `read-model.md` now describes ONE regime (follow, never enumerate), and the root README's capability row records the removal.
|
||
2. ~~**"My inbox" + the inbox read guard**~~ — **DONE 2026-07-30.** `storeRegistry.userInbox(id)` gives every virtual user its own inbox document, created on first sight and recorded in the doc-shim under its own predicate (`shim:docInbox`), read by its OWN query so an account record written before this existed still resolves — the fixed account SELECT did not grow a fourth required field. `isOwnInbox(nuri)` is the predicate; `inbox.read` / `readSynced` (hence `watch`, which reads through it) refuse an inbox that is not the connected wallet's, and refuse outright when no identity is set. **Depositing stays open** — `post` / `shareCap` are untouched, because that is the one legitimate cross-wallet act. The shared `resolveInboxAnchor` (a reserved account's document, an inbox COMMON to every wallet) was removed: it was unused by the library and violated *nothing common*. Locked by `test/isolation-active.test.ts` *an inbox may be DEPOSITED into by anyone, and READ only by its owner*, which walks the exact breach — Alice deposits, cannot read back; Mallory knowing the NURI absorbs nothing; anonymous is refused; Bob reads his own and only then does the cap land.
|
||
|
||
*Not done, and deliberately*: per-DOCUMENT inboxes. ~~Upstream every document has one~~ — **false, corrected 2026-08-03**: no document has an inbox upstream, and neither does the private store (see step 7's correction). Here only the wallet does. **The PO has ruled they must come** (2026-07-30) — *"it can come in a second step, but it must come"* — so this is a commitment, not an option. The guard predicate (`isOwnInbox`) is where they plug in: it answers "is this inbox mine?", which extends to "…one of my documents' inboxes" without changing a single caller.
|
||
|
||
### Two defects this step surfaced — the first still open, the second closed by steps 5–6
|
||
|
||
**`userInbox(id)` is a directory, and directories do not exist.** It resolves ANY wallet's inbox from its identity id, and it is exported (`storeRegistry.*` is re-exported from the SDK entry). But you cannot look someone up in NextGraph — you cannot discover, you can only follow links. Their inbox NURI reaches you because *they gave it to you*, not because you resolved it from a name. Resolving **my own** inbox is legitimate plumbing; resolving **anyone's** is the same shape as the discovery index just removed. Fix: the public surface becomes "my inbox" (no argument), and reaching someone else's requires a NURI you were given. Resolution-by-id stays internal, for the shim and the tests.
|
||
|
||
**The keyring is not stored anywhere, and the shape is wrong — fix it now, not at P1b.** It is an in-memory `Map<accountKey, Map<Nuri, ReadCap>>`, rebuilt from scratch each session. Nothing persists a cap *as a cap*. PO directive, 2026-07-30:
|
||
|
||
> If NextGraph stores the key next to the document, then that is what we do. Without encryption we simply store a stand-in value. Stay as close as possible to how NextGraph works.
|
||
|
||
So this is not a P1b concern, it is a **shape** concern — the one thing this library exists to get right — and the stand-in key is stored exactly where the real one will be. Two storage sites, mirroring upstream:
|
||
|
||
- **My own documents** → the cap goes **beside the NURI in the scope container**, which is the emulation of `AddRepo { read_cap }` on a branch of the store. Today the container stores only `shim:contains <nuri>` and the cap is **re-derived** from that NURI; it gains a `shim:readCap` beside it. Deriving then stops being how a cap is *recovered* and becomes merely how the stand-in value is *minted* — the single function P1b replaces.
|
||
- **Caps I received** → into the emulation of `AddLink { read_cap }` on the **User branch** of the private store. Verified 2026-07-30 (see [`../readcap-and-nuri-model.md`](../readcap-and-nuri-model.md) §4quinquies): that register exists, it is explicitly for **external repos**, and its stated purpose is to *"share with all its device a new Link they received"* — wallet-resident and cross-device. So a received cap belongs **inside the virtual user**, like everything else. *(Two earlier versions of this note were wrong and are recorded in [`2026-07-30-users-stores-branches.md`](2026-07-30-users-stores-branches.md): the first blamed key derivation and proposed a "keyring document"; the second concluded no register existed at all and proposed browser-local storage — which would have put library data OUTSIDE even the physical user. Both were refuted; `AddLink` is the answer.)*
|
||
3. ~~**The guard at the four passage points**~~ — **DONE 2026-07-30.** `src/emulated-verifier/reach.ts` holds the boundary as **two rules on one criterion — possession — implemented in two places** (PO directive):
|
||
|
||
- **Rule 1, authorization**, at the passage points (`assertMayReach`, called from `docs.sparqlQuery` / `sparqlUpdate`): nothing reaches `ng` unless the connected user possesses the document's cap.
|
||
- **Rule 2, do not even attempt**, at the callers (`mustNotAttempt`, applied in `read-model.readUnion`, which now filters BEFORE opening or reading): a reader that holds no cap does not issue the operation at all. Upstream you cannot even address a repo you have no cap for, so asking about one is not "a read that will be refused" — it is a read with no meaning.
|
||
|
||
The redundancy is the point, and a test pins it: a caller that forgets rule 2 is still refused by rule 1, so a bookkeeping lapse fails loudly instead of succeeding quietly.
|
||
|
||
**Possession decides, never the shape of the reference in hand.** A caller legitimately manipulates a bare NURI while holding its cap elsewhere — references travel bare through content and indexes, the cap sits in what the user holds. `targetOf` first, so both forms answer alike. (An earlier reading of the directive checked the string for `:r:` instead; corrected on the PO's clarification.)
|
||
|
||
Exemptions are **declared**, never inferred from a NURI's shape (`declareInfrastructure`, called by the store-registry for the store-root pointer and the doc-shim — the index of virtual users, the only thing that passes the "remove it and no user resolves at all" test). A user also reaches its own three stores and its own inbox, or the boundary would lock it out of itself.
|
||
|
||
Not done: `subscribeDoc` is not yet guarded — it interacts with `ensureRepoOpen`, which opens documents before their cap is known on some cold-start paths. Left for step 4 with the barrier tests in view.
|
||
4. ~~**Remove the cross-account fan-out**~~ — **DONE 2026-07-30.** `listEntityDocs`, `resolveReadGraphs`, `allAccounts`, `loadShim` and the full-shim cache are gone. Nothing in the library used them any more once `discovery` was removed; only their own tests did. `subscribeDoc` is now guarded too (rule 1) — a subscription IS an access, since the push carries the document's state, so leaving it open would have been a door beside the gate.
|
||
|
||
### Machinery vs virtual user: two APIs, and only one is the app's
|
||
|
||
The PO's framing, which replaced the exemption list entirely:
|
||
|
||
> Clearly distinguish what is polyfill machinery (and therefore the PHYSICAL user) from what is a virtual user's operation. Use different functions, probably grouped in different namespaces — because one API is exposed to the app and the other must never be.
|
||
|
||
`src/shared-wallet/physical.ts` now holds `physicalCreate` / `physicalQuery` / `physicalUpdate`, with `ensurePhysicalRepoOpen` and `subscribePhysicalDoc` as their open/subscribe counterparts. They are unguarded, and **never exported from the package** — a test asserts it, because a regression there is silent and total.
|
||
|
||
The dividing line:
|
||
|
||
> Does this operate on the **index of virtual users** (the shim), or on the **content of one virtual user**? The first is machinery; everything else is the user's, and is confined.
|
||
|
||
So the store-root pointer, the doc-shim and the account records go through the machinery; a virtual user's stores, its inbox and its documents go through the guarded `docs.*`, even though the library is what calls them on the user's behalf.
|
||
|
||
**This is strictly stronger than the exemption list it replaces.** `declareInfrastructure` is deleted. The machinery no longer calls the guarded primitive and gets waved through — it calls a different primitive that was never guarded. There is no list to widen, to get wrong, or to infer from a NURI's shape, and the boundary now has no `if` in it that could be talked into saying yes.
|
||
|
||
5. ~~**The Link, and inbox processing on connection**~~ — **DONE 2026-07-30**, after the four steps above.
|
||
|
||
**The Link.** Giving access is a `Link` deposited into the recipient's inbox — upstream's word at all three stages (`InboxMsgContent::Link` for the message, `AddLink { read_cap }` for the filing, `RemoveLink` for the withdrawal). `shareCap` deposits one; the deposit kind is `…:inbox:link`.
|
||
|
||
**Applying it durably.** `storeRegistry.addLink` / `readLinks` emulate `AddLink` on the **User branch of the private store** — a distinct subject (`shim:userBranch`) in the private store document, kept separate from the `ldp:contains` listing exactly as upstream keeps the User branch separate from Main. Idempotent, so re-processing costs nothing.
|
||
|
||
**The split that matters**: `inbox.read` KEEPS a Link (in the session's keyring) but does not FILE it — reading a queue must not write to a user's store. `inbox.processInbox` *applies*: it reads, then files. That is what an inbox is upstream — **a queue you consume, not a store you re-read**.
|
||
|
||
**On connection.** `setCurrentUser` fires `connect.connectedUser()`: restore the already-applied Links from the User branch, then drain the inbox. Restore-first means a reconnecting user reads its shared documents immediately, without waiting on the queue. Fire-and-forget, because the setter is synchronous and every consumer calls it from synchronous code — the work announces itself through `CapRegistry.onChange`, which `watchShape` already listens to. `connectedUser()` is exported for a caller that needs to await it.
|
||
|
||
**Two things it deliberately does NOT do.** It does not **provision**: connecting an identity that does not exist creates nothing (`resolveAccount`, not `ensureAccount`) — otherwise connecting would mint a user's stores and caps as a background side effect, arming the whole emulation at a moment nothing controls. And it does not drain **per-document** inboxes, which do not exist yet.
|
||
|
||
Proven by `test/cross-user-access.test.ts`: a cap shared to Bob survives **with his inbox emptied** and every in-memory cap dropped — restored from the User branch, not from the queue.
|
||
|
||
*Cost noted*: `setCurrentUser` now has observable asynchronous effects (it reads, and it logs). Three log-assertion tests had to await `connectedUser()` before counting lines. That is the honest price of moving the obligation off the app, and it is worth naming rather than discovering.
|
||
|
||
6. ~~**The Store branch**~~ — **DONE 2026-08-03.** A document's cap is now STORED when it is created — `shim:readCap` on a `storeBranch` subject of the store document, the emulated `AddRepo { read_cap }` — and READ back by `listMyEntityDocs`, never recomputed. That closes the asymmetry left by the Link work, where received caps were filed durably while created ones were re-minted.
|
||
|
||
Two things this pinned down, both of which would have cost more later:
|
||
|
||
- **The listing and the keys stay separate**, as Main and Store branches are upstream: `contains` on one subject, `readCap` on another, written as two statements because upstream they are two commits.
|
||
- **Creation mints the cap exactly once.** It used to mint twice — once to write, once to hold — which agreed only because the stand-in value is a constant. With P1b's real key those would be two different keys, and a creator would hold one that does not open its own document. A test pins it, and another proves the cap is read rather than recomputed by corrupting the stored value and checking the corruption wins.
|
||
|
||
Honest about the emulation: upstream the Store branch carries **no triples at all** (`BranchCrdt::None`). Representing it as RDF is ours; what is faithful is the storage beside the document and the separation from the listing.
|
||
|
||
7. ~~**Per-document inboxes**~~ — **DONE 2026-08-03.** Upstream a repo carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`): an inbox is a keypair on the document whose PRIVATE half the owner holds, recorded with `AddInboxCap { repo_id, overlay, priv_key }` on the **User branch** — the same branch as `AddLink`, and with the same stated purpose (*"so that a user can share with all its device"*). So "which inboxes may I read" has exactly one answer, and it is the one place to look.
|
||
|
||
*(Renamed and split on 2026-08-03 — `documentInbox` became `openDocumentInbox` (own the inbox) + `documentInboxAddress` (find where to deposit). Conflating the two made per-document inboxes unusable by anyone but their owner; see [`2026-08-03-document-inbox-addressing.md`](2026-08-03-document-inbox-addressing.md).)*
|
||
|
||
`storeRegistry.openDocumentInbox(doc)` resolves — creating on first ask — the inbox of a document this user owns, recording the pair on its User branch. **Lazy**: minting an inbox document for every entity up front would double every `createEntityDoc` for inboxes most documents never receive anything in. `myInboxes()` enumerates both levels, `isOwnInbox` answers from the same record, and `connect.connectedUser` drains them all in one call.
|
||
|
||
The asymmetry holds at both levels, and a test walks it: **anyone deposits** into a document's inbox (that is how a third party reaches its owner at all), **only the owner reads** it.
|
||
|
||
**Correction 2026-08-03, twice.** *First:* the paragraph above overstated the upstream fact — an adversarial doc review caught it. *Then the correction itself overshot*, calling the step an ANTICIPATION: the engine **supports** an inbox on any repo (`inbox` is a field of every `Repo`, `AddInboxCapV0` is keyed by `repo_id`, and `update_inbox_cap_v0` applies it with no `is_store` check — `engine/verifier/src/verifier.rs:1920`). What no code path does is CREATE one for a document. Verified: `new_store_default` attaches an inbox only `if !private` (`engine/verifier/src/verifier.rs:2994`), and `doc_create` goes through `new_repo_default`, which leaves `inbox: None` (`engine/repo/src/repo.rs:574`). The **only** two `AddInboxCap` commits in the engine are for the public and protected STORE repos (`engine/verifier/src/site.rs:128,149`). So today **no document is GIVEN an inbox, and neither is the private store** — which says nothing about what may have one. The step stands as delivered, and it is aligned on the engine's model rather than betting past it.
|
||
|
||
## Relation to P1b
|
||
|
||
P1b (per-document encryption, closing the read paths that bypass the guard) largely **becomes** this lot, better framed. Encrypting each document while leaving the wallet boundary open would be securing the windows with the door open — and conversely, once every access is confined to the connected wallet, "the emulated key is derivable" stops being the load-bearing weakness. This lot should therefore precede P1b, or absorb it.
|