docs: l'inbox par document n'est pas une anticipation, c'est une capacité du moteur

Glissement à corriger : de « le moteur ne le FAIT pas » (vrai) j'ai conclu
« c'est notre anticipation » (faux). Le moteur le PERMET, et de façon générique
par conception :

- `inbox: Option<PrivKey>` est un champ de TOUT `Repo` (`repo.rs:126`), pas
  d'une structure de store ;
- `AddInboxCapV0` est clé par `repo_id` — « Repo the Inbox is opened for » ;
- `update_inbox_cap_v0` l'applique via `self.repos.get_mut(repo_id)` sans
  AUCUNE vérification `is_store` (`verifier.rs:1920`) ;
- et à tout moment, `AddInboxCap` étant un commit de branche User dont le type
  documente le cas de mise à jour.

Ce qui est vrai est plus étroit : aucun chemin de code n'en CRÉE une pour un
document. « Ne fait pas » n'est pas « ne peut pas » — c'est précisément
l'interdit que ma propre règle pose, et je l'ai enfreint en le formulant.

Donc l'inbox par document s'aligne sur le modèle du moteur (niveau 1) ; ce qui
est de nous est la surface JS, aucune n'étant exposée aux niveaux 2 et 3.
Corrigé dans store-registry.ts, nextgraph-current-state.md et les deux briefs.
This commit is contained in:
Sylvain Duchesne
2026-08-03 17:55:46 +02:00
parent 88efe4fea4
commit 2406afec8b
4 changed files with 29 additions and 28 deletions
@@ -192,7 +192,7 @@ So the store-root pointer, the doc-shim and the account records go through the m
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. 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 — this step ANTICIPATES, it does not mirror.** The paragraph above overstated the upstream fact, and an adversarial doc review caught it. 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 has an inbox, and neither does the private store**. What is genuinely verified is the *shape*: `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`), so the record accommodates an inbox on any repo. The step stands as delivered — a per-document inbox is what the consumer needs and what the record's shape supports — but it must be read as a bet on where upstream goes, not as an emulation of what it already does. **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 ## Relation to P1b
@@ -16,7 +16,7 @@
> >
> **Where the address lives.** On the document's emulated **Header branch** (`urn:ng-eventually:shim:headerBranch`), beside the content rather than in it — the same subject-as-compartment shape already used for the Store and User branches. `read-model` now drops every subject under the reserved `urn:ng-eventually:` namespace (`src/machinery.ts`), so the address cannot surface as one of the entity's properties. That filter is by SUBJECT, so it covers every emulated compartment present and future. > **Where the address lives.** On the document's emulated **Header branch** (`urn:ng-eventually:shim:headerBranch`), beside the content rather than in it — the same subject-as-compartment shape already used for the Store and User branches. `read-model` now drops every subject under the reserved `urn:ng-eventually:` namespace (`src/machinery.ts`), so the address cannot surface as one of the entity's properties. That filter is by SUBJECT, so it covers every emulated compartment present and future.
> >
> **The open question, answered.** *"Is 'a document has an inbox addressable by any holder' upstream, or this library's invention?"* — **the library's**, and it is now written down as such. Verified: no document has an inbox upstream, and neither does the private store; the only two `AddInboxCap` commits in the engine are for the public and protected STORE repos (`engine/verifier/src/site.rs:128,149`), `new_store_default` attaches one only `if !private` (`engine/verifier/src/verifier.rs:2994`), and `doc_create` leaves `inbox: None` (`engine/repo/src/repo.rs:574`). What IS upstream is the shape that makes this a defensible anticipation rather than a fiction: `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`), so the record accommodates an inbox on any repo. The half-split is upstream's too — a depositor seals with the inbox PUBLIC key (`engine/net/src/types.rs:4299`) and only the owner holds the private half — which is why an address is public by nature and belongs on the document, not on the owner's User branch. > **The open question, answered.** *"Is 'a document has an inbox addressable by any holder' upstream, or this library's invention?"* — **upstream, as a capability**; ours only as a JS surface. The engine lets any repo have an inbox: `inbox: Option<PrivKey>` is a field of every `Repo` (`engine/repo/src/repo.rs:126`), `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`), and `update_inbox_cap_v0` applies it with **no `is_store` check** (`engine/verifier/src/verifier.rs:1920`), at any time (User-branch commit, `commit.rs:1043-1050`, whose type documents the update case). What no code path does is CREATE one for a document: `new_store_default` attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None` (`repo.rs:574`), and the engine's only two `AddInboxCap` commits are for the public and protected STORE repos (`site.rs:128,149`). "Does not" is not "cannot" — an earlier version of this note called the feature an anticipation on exactly that confusion. The half-split is upstream's too — a depositor seals with the inbox PUBLIC key (`engine/net/src/types.rs:4299`) and only the owner holds the private half — which is why an address is public by nature and belongs on the document, not on the owner's User branch.
> >
> **Also fixed, and it was the root of the reported symptom.** `openDocumentInbox` (formerly `documentInbox`) called by a non-owner used to mint a parallel inbox and record it for the caller — no error, deposits lost. It now refuses, on OWNERSHIP (read from the Store branches), not on cap possession: a cap can be received, and a recipient must not be able to redirect the owner's deposits to itself. > **Also fixed, and it was the root of the reported symptom.** `openDocumentInbox` (formerly `documentInbox`) called by a non-owner used to mint a parallel inbox and record it for the caller — no error, deposits lost. It now refuses, on OWNERSHIP (read from the Store branches), not on cap possession: a cap can be received, and a recipient must not be able to redirect the owner's deposits to itself.
+13 -5
View File
@@ -121,11 +121,19 @@ documents, and not the private store. `new_store_default` attaches one solely `i
`AddInboxCap` commits in the whole engine are the two in `engine/verifier/src/site.rs:128,149` `AddInboxCap` commits in the whole engine are the two in `engine/verifier/src/site.rs:128,149`
— one for the public store repo, one for the protected one. — one for the public store repo, one for the protected one.
The *register* is nonetheless per-repo: `AddInboxCapV0 { repo_id, overlay, priv_key }` **But the engine SUPPORTS an inbox on any repo — "does not" and "cannot" are different
(`engine/repo/src/types.rs:1973`) records **which repo** an inbox is opened for, so the statements.** `inbox: Option<PrivKey>` is a field of EVERY `Repo`
shape accommodates an inbox on any repo. Nothing creates one, which is a different (`engine/repo/src/repo.rs:126`), not of a store structure. `AddInboxCapV0` is keyed by
statement from the shape forbidding it — and it is why this lib's per-document inbox is `repo_id` (`engine/repo/src/types.rs:1973`) — *"Repo the Inbox is opened for"*. And
an ANTICIPATION of that shape, not an emulation of something upstream already does. `update_inbox_cap_v0` applies it via `self.repos.get_mut(repo_id)` with **no `is_store`
check of any kind** (`engine/verifier/src/verifier.rs:1920`). It is generic by
construction, and available at any time: `AddInboxCap` is a User-branch commit
(`engine/repo/src/commit.rs:1043-1050`) whose type documents the late case — *"DEPS to
the previous AddInboxCap commit(s) if it is an update"*.
So a per-document inbox is **not an anticipation**: it is an engine capability that no
code path exercises automatically and that no level-2 or level-3 API exposes. This lib
implements it aligned on the engine's model.
A non-editor can deposit into an inbox without being invited as an editor; the owner A non-editor can deposit into an inbox without being invited as an editor; the owner
moderates. NURI: `did:ng:d:<inbox_id>`. Content: the `InboxMsgContent` enum (`ContactDetails`, moderates. NURI: `did:ng:d:<inbox_id>`. Content: the `InboxMsgContent` enum (`ContactDetails`,
+14 -21
View File
@@ -1099,28 +1099,21 @@ export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri> {
* same branch that carries `AddLink`. So "which inboxes may I read" is answered by the * same branch that carries `AddLink`. So "which inboxes may I read" is answered by the
* User branch, and that is what this emulates. * User branch, and that is what this emulates.
* *
* **This ANTICIPATES: no document has an inbox upstream today.** `new_store_default` * **The engine SUPPORTS this; nothing exercises it automatically.** Those are two
* attaches one only `if !private` (`engine/verifier/src/verifier.rs:2994`), and * different statements, and conflating them is what made an earlier version of this
* `doc_create` goes through `new_repo_default`, which leaves `inbox: None` * comment call the feature an "anticipation". It is not. `inbox: Option<PrivKey>` is a
* (`engine/repo/src/repo.rs:574`) — the only two `AddInboxCap` commits in the engine * field of EVERY `Repo` (`engine/repo/src/repo.rs:126`), not of a store structure;
* are for the public and protected STORE repos (`engine/verifier/src/site.rs:128,149`). * `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`); and
* What is verified is the SHAPE: the record is keyed by `repo_id`, so it accommodates * `update_inbox_cap_v0` applies it with `self.repos.get_mut(repo_id)` and **no
* an inbox on any repo. What is not verified is that anything upstream will create one * `is_store` check of any kind** (`engine/verifier/src/verifier.rs:1920`). Generic by
* per document. At migration this either becomes native or stays emulated here; either * construction, and at any time (see the User-branch note above).
* way the consumer-facing act is unchanged.
* *
* **Callable at ANY time, not only at creation — and that is the faithful shape.** * What is true is narrower: no code path CREATES one for a document — `new_store_default`
* `AddInboxCap` is a User-branch commit (`CommitBody::on_user_branch`, * attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None`
* `engine/repo/src/commit.rs:1043-1050`), i.e. an entry in a stream that stays open over * (`repo.rs:574`), and the only two `AddInboxCap` commits in the engine are for the
* time, and its type documents the late case explicitly: *"DEPS to the previous * public and protected STORE repos (`engine/verifier/src/site.rs:128,149`). So the
* AddInboxCap commit(s) if it is an **update**. in this case, repo_id should match"* * capability exists and is simply unexposed above level 1: this function is aligned on
* (`engine/repo/src/types.rs:1969-1971`). The verifier applies it to an already-loaded * the engine's model, it does not bet past it.
* repo (`update_inbox_cap_v0` → `repo.inbox = Some(...)`, `verifier.rs:1920`). That
* `new_store_default` attaches the two STORE inboxes at site creation is how those two
* happen to be made — not a constraint that an inbox may only exist from birth. So do
* NOT "simplify" this into a `createEntityDoc(…, { withInbox })` flag: it would narrow
* the model, and an owner deciding later that a document should receive is a case the
* engine supports.
* *
* Lazy on purpose, for the same reason: creating an inbox document for every entity up * Lazy on purpose, for the same reason: creating an inbox document for every entity up
* front would double every `createEntityDoc` for inboxes most documents never receive * front would double every `createEntityDoc` for inboxes most documents never receive