docs: l'overlay est store-scopé ; retrait de la fausse piste "membership"
readcap-and-nuri-model — nouvelle section sur l'OVERLAY, le concept qui manquait à la référence. C'est l'espace réseau d'un STORE : deux formes (outer = BLAKE3 public du store_id, calculable par tous ; inner = BLAKE3 keyed par le ReadCapSecret, réservé aux détenteurs de la clé). Le `✌️` d'un NURI de DOCUMENT porte l'overlay de son store — VÉRIFIÉ de bout en bout, avec une contre-preuve mécanique : dans Store, get/put/del/has passent tous `&self.overlay_id`, donc tous les documents d'un store partagent le namespace de blocs et un overlay par-document est structurellement impossible. Conséquence documentée, qui contraint tout modèle de présence anonyme : le `✌️` est un pseudonyme stable et permanent de la personne, présent dans toute référence cap-less vers n'importe lequel de ses documents protected. Le même bit d'information sert à dédupliquer sans lire ET à tracer — indissociables. vision — correction d'une forme fausse. Le document affirmait « écriture = membership/permissions », en miroir de « lecture = possession de clé ». Faux : il n'y a pas de notion d'appartenance dans le modèle, uniquement des clés et des URLs. Toute forme en member/role/permission est une MAUVAISE forme. brief caps — la section « périmètre élargi : WriteCap = membership » est retirée, conservée barrée comme garde-fou, avec la leçon de méthode qui vaut plus qu'elle : lire l'état courant de nextgraph-rs pour en DÉDUIRE la forme cible est une erreur — le source contient de l'échafaudage inerte (AddMember, PermissionV0, verify_sig jamais appelé hors tests). Le source sert à vérifier un mécanisme, jamais à inférer une intention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -62,6 +62,79 @@ que la vision interdit. **Fermer ces voies = le cœur de P1.**
|
|||||||
5. **Révocation = re-key** émulé : invalider l'ancien token, re-livrer un nouveau
|
5. **Révocation = re-key** émulé : invalider l'ancien token, re-livrer un nouveau
|
||||||
aux autorisés restants ; **non-rétroactif**.
|
aux autorisés restants ; **non-rétroactif**.
|
||||||
|
|
||||||
|
## ~~Périmètre élargi : le WriteCap (= membership)~~ — RETIRÉ (2026-07-21)
|
||||||
|
|
||||||
|
**Cette section était fausse et est conservée barrée comme garde-fou.** Elle
|
||||||
|
importait une notion de *membership* lue dans l'**état courant** de `nextgraph-rs`
|
||||||
|
(`AddMember`, `PermissionV0`, `member_pubkey`) et la promouvait en **forme cible**.
|
||||||
|
Or (a) ces types sont un **échafaudage inerte** au runtime — `verify_sig` /
|
||||||
|
`verify_perm` ne sont appelés que dans des tests unitaires, les `Repo` sont
|
||||||
|
construits avec `members: HashMap::new()` ; et (b) le modèle cible **n'a pas de
|
||||||
|
notion d'appartenance du tout** : uniquement des **clés et des URLs**, symétrique
|
||||||
|
et asymétrique. Une forme en `member`/`role`/`permission` est donc exactement la
|
||||||
|
**mauvaise forme** que ce brief existe pour empêcher.
|
||||||
|
|
||||||
|
**La leçon de méthode, qui vaut plus que la section retirée** : lire l'état
|
||||||
|
courant de NextGraph pour en **déduire** la forme cible est une erreur — l'état
|
||||||
|
courant contient de l'inachevé qu'il ne faut pas figer dans le polyfill. Le source
|
||||||
|
sert à vérifier un **mécanisme** existant, jamais à inférer une **intention**.
|
||||||
|
|
||||||
|
Contenu erroné conservé ci-dessous à titre de trace :
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Section retirée</summary>
|
||||||
|
|
||||||
|
**Pourquoi c'est ici et pas ailleurs.** Le brief était d'abord centré ReadCap ; un
|
||||||
|
constat adverse a montré qu'il **ne compose pas** avec son consommateur : le brief
|
||||||
|
Festipod « inscriptions par Set » a besoin de **dédupliquer** les participations
|
||||||
|
(un user = une participation par événement), et la seule base non-applicative
|
||||||
|
disponible est la **signature d'auteur du commit** — donc un primitif d'**écriture**,
|
||||||
|
pas de lecture. Un polyfill qui n'expose que la forme du ReadCap laisse le
|
||||||
|
consommateur inventer sa propre dédup applicative → exactement la mauvaise forme.
|
||||||
|
|
||||||
|
**La forme réelle (VÉRIFIÉE, cf. `readcap-and-nuri-model.md` §1)** — et elle est
|
||||||
|
**asymétrique** de la lecture, ce qui est le point le plus facile à rater :
|
||||||
|
|
||||||
|
- **Lecture = possession d'une clé.** Pas d'ACL. Qui détient, lit.
|
||||||
|
- **Écriture = membership + permissions** (`AddMember`, `AddPermission` sur le
|
||||||
|
`RootBranch`). C'est **bel et bien une liste d'autorisation** — pas de la
|
||||||
|
possession. Émuler l'écriture « par possession de token » serait aussi faux que
|
||||||
|
l'ACL de lecture actuelle, en miroir.
|
||||||
|
- **Les commits SONT signés** par un `UserId` (clé **technique**, distincte du
|
||||||
|
profil) — donc un identifiant de dédup **existe** nativement, sans pseudonyme
|
||||||
|
applicatif.
|
||||||
|
- **Mais vérifier une signature exige d'être membre du repo** (accès au
|
||||||
|
`member_pubkey`). Un tiers non-membre voit un commit signé sans pouvoir
|
||||||
|
l'attribuer.
|
||||||
|
- **Le dépôt d'inbox n'est PAS authentifié** (sealed box anonyme) : un `from`
|
||||||
|
déclaré est du contenu, pas une preuve.
|
||||||
|
|
||||||
|
**Ce que ça impose au polyfill.** Exposer `membership` comme primitif **distinct**
|
||||||
|
de la possession de cap, avec au minimum : ajouter/retirer un membre d'un repo,
|
||||||
|
lire la members map **quand on est membre**, et **vérifier l'auteur d'un commit**
|
||||||
|
(→ un digest d'auteur, **par-overlay donc par-store**). C'est ce dernier point qui
|
||||||
|
débloque la dédup côté Festipod.
|
||||||
|
|
||||||
|
**La conséquence de forme, à documenter explicitement** (sinon le consommateur se
|
||||||
|
trompe de modèle) : le digest d'auteur étant **par-store**, le choix du découpage
|
||||||
|
en stores **est** le choix du niveau de corrélation. Un store **stable par-user**
|
||||||
|
donne un identifiant traçable **cross-événement** ; un store **par-événement**
|
||||||
|
donne un pseudonyme **local à l'événement** — dédup possible, corrélation
|
||||||
|
impossible. Festipod a besoin du second. Le polyfill doit donc rendre ce découpage
|
||||||
|
**exprimable**, pas le figer.
|
||||||
|
|
||||||
|
**Reste ouvert** : « le créateur d'un événement peut-il être membre du store qui
|
||||||
|
contient les participations, sans pour autant en détenir la clé de lecture ? » —
|
||||||
|
c'est-à-dire membership (écriture/vérification) et possession (lecture)
|
||||||
|
réellement **orthogonales**. Si NextGraph les couple, la dédup vérifiée et
|
||||||
|
l'anonymat s'excluent, et c'est le brief Festipod qui doit trancher ce qu'il
|
||||||
|
sacrifie. **À vérifier avant de façonner l'API.**
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
*(Question devenue sans objet : il n'y a pas de membership. La dédup ne passe pas
|
||||||
|
par une vérification de signature — voir le brief Festipod « inscriptions ».)*
|
||||||
|
|
||||||
## Questions ouvertes
|
## Questions ouvertes
|
||||||
|
|
||||||
- **TRANCHÉ (directive PO, 2026-07-21)** : on **simule la crypto** (donnée chiffrée
|
- **TRANCHÉ (directive PO, 2026-07-21)** : on **simule la crypto** (donnée chiffrée
|
||||||
@@ -91,14 +164,39 @@ que la vision interdit. **Fermer ces voies = le cœur de P1.**
|
|||||||
**Livrable** : YES/NO/PARTIAL par Q1/Q2/Q3 + le primitif exact (file:line) + la forme d'API à exposer (si OUI), ou le constat que le compteur change (si NON).
|
**Livrable** : YES/NO/PARTIAL par Q1/Q2/Q3 + le primitif exact (file:line) + la forme d'API à exposer (si OUI), ou le constat que le compteur change (si NON).
|
||||||
**Décision gatée** : OUI → P1 ; NON → le brief inscriptions revoit le compteur (pas anonyme, ou autre primitif).
|
**Décision gatée** : OUI → P1 ; NON → le brief inscriptions revoit le compteur (pas anonyme, ou autre primitif).
|
||||||
|
|
||||||
|
### Verdict du spike (2026-07-21) — VÉRIFIÉ dans `nextgraph-rs`
|
||||||
|
|
||||||
|
| | Réponse | Preuve |
|
||||||
|
|---|---|---|
|
||||||
|
| **Q1 — existence/fetch sans cap** | **OUI, partiel** | Les lectures ne sont pas cap-gatées : `blocks_get.rs`, `blocks_exist.rs`, `topic_sync_req.rs` servent les blocs sans exiger ReadCap ni membership. **Seul garde : l'overlay**. Nuance non levée : overlay *inner* vs *outer* (`expose_outer`, défaut `false`) — **sonde en cours**. |
|
||||||
|
| **Q2 — détecter une suppression sans clé** | **NON** | Broker append-only ; une suppression est un **commit tombstone chiffré** (`RemoveRepo`), no-op côté verifier. Sans clé on observe « une activité », jamais « une suppression ». |
|
||||||
|
| **Q3 — confidentialité** | **OUI** | Blocs stockés en ciphertext ; la clé est `#[serde(skip)]` (`types.rs`), dérivée du `ReadCapSecret`. Le keyless ne donne **jamais** le contenu. |
|
||||||
|
|
||||||
|
**Ce que ça décide.**
|
||||||
|
- **P1 est débloqué** : `resolveCapLess(nuri) → { exists }` est la bonne forme — mais **`{ exists | deleted }` ne l'est PAS**. Ne pas exposer d'état `deleted`, ce serait inventer une capacité que la cible n'aura jamais (précisément le mode d'échec que ce brief combat).
|
||||||
|
- **Le retrait doit être un message, pas une observation.** Côté consommateur : un *nudge* explicite. Le polyfill n'a **rien** à émuler pour ça — juste à ne pas prétendre le contraire.
|
||||||
|
- **Reste gaté** : la nuance d'overlay (Q1). Si un non-membre ne peut pas rejoindre l'overlay d'un store *protected* tiers, le fetch keyless est inatteignable **en pratique** malgré un chemin d'autorisation ouvert — et le compteur anonyme retombe sur du déclaratif.
|
||||||
|
|
||||||
## Esquisse de phases
|
## Esquisse de phases
|
||||||
|
|
||||||
- **P1** — distinction cap-less / cap-porteur dans les NURI + le read-model
|
- **P1** — distinction cap-less / cap-porteur dans les NURI + le read-model
|
||||||
(résoudre un cap-less = nommer/compter, pas lire).
|
(résoudre un cap-less = nommer/compter/prouver l'existence, **pas** lire, **pas**
|
||||||
|
de `deleted`). Inclut la **fermeture des bypass** (`inbox.read`, `sparqlQuery`)
|
||||||
|
sans laquelle la distinction n'est pas tenue.
|
||||||
- **P2** — remplacer l'ACL par un modèle de **possession de token** (grant = livrer
|
- **P2** — remplacer l'ACL par un modèle de **possession de token** (grant = livrer
|
||||||
à un destinataire ; enforcement = possession).
|
à un destinataire ; enforcement = possession). *Requalifié par la revue adverse :
|
||||||
|
le vrai contenu de P2 est **durabilité + cap-less + re-partage par le détenteur**,
|
||||||
|
pas « inverser l'ACL » — sans crypto, inverser ne produit aucun delta observable.*
|
||||||
- **P3** — révocation par re-key (invalidation + re-livraison, non-rétroactive).
|
- **P3** — révocation par re-key (invalidation + re-livraison, non-rétroactive).
|
||||||
- **P4** — adapter l'API consommateur + `migration-guide.md`.
|
- **PW** — **WriteCap = membership** : primitif distinct (add/remove member,
|
||||||
|
members map lisible par un membre, **vérification d'auteur de commit** → digest
|
||||||
|
par-store), et découpage en stores **exprimable** par le consommateur.
|
||||||
|
*Indépendant de P1–P3 ; **bloquant pour la dédup Festipod**, donc à ordonnancer
|
||||||
|
tôt si c'est ce besoin-là qui presse.*
|
||||||
|
- **P4** — adapter l'API consommateur + `migration-guide.md`. *La revue adverse
|
||||||
|
requalifie ce lot : ce n'est pas un swap d'API mais une **re-architecture
|
||||||
|
consommateur** (le grant se déplace vers l'acceptation de connexion et devient
|
||||||
|
persistant ; `declareConnections` disparaît).*
|
||||||
|
|
||||||
## Revue adverse (2026-07-20) — à intégrer
|
## Revue adverse (2026-07-20) — à intégrer
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,56 @@ regexes `net/types.rs` :
|
|||||||
- `did:ng:o:{repo}:c:{commit}:k:{clé}` (`RE_COMMIT` types.rs:73)
|
- `did:ng:o:{repo}:c:{commit}:k:{clé}` (`RE_COMMIT` types.rs:73)
|
||||||
- liste `RE_OBJECTS` `…:[cj]:{id}:k:{clé}…:l:{locator}` (types.rs:64)
|
- liste `RE_OBJECTS` `…:[cj]:{id}:k:{clé}…:l:{locator}` (types.rs:64)
|
||||||
|
|
||||||
L'`overlay` est **dérivé publiquement** de l'id du repo (`OverlayId::outer(store_id)`,
|
Le segment `:v:` est l'**overlay**, qui a sa propre section ci-dessous — c'est le
|
||||||
`:259`) → id+overlay ne fuitent **aucun secret**.
|
point le plus lourd de conséquences pour les modèles de présence anonyme.
|
||||||
|
|
||||||
|
## 4bis. L'overlay est l'espace réseau d'un STORE — jamais d'un document
|
||||||
|
|
||||||
|
**L'overlay est l'unité d'adressage réseau d'un store.** Chez le broker, les blocs
|
||||||
|
sont rangés sous une clé `(overlay, block_id)`, et les pairs se synchronisent
|
||||||
|
*dans* un overlay. Deux formes par store :
|
||||||
|
|
||||||
|
| | Dérivation | Qui peut le calculer |
|
||||||
|
|---|---|---|
|
||||||
|
| **outer** | `OverlayId::outer(store_id)` = BLAKE3 **public** | tout le monde (le store_id suffit) |
|
||||||
|
| **inner** | `OverlayId::inner(store_id, readcap_secret)` = BLAKE3 **keyed** | seulement qui détient la clé de lecture du store |
|
||||||
|
|
||||||
|
Cohérent avec le reste du modèle : pas de rôle ni de liste, seulement « détiens-tu
|
||||||
|
la clé qui permet de dériver cet identifiant ». `outer` = le nom public du store,
|
||||||
|
`inner` = son nom privé.
|
||||||
|
|
||||||
|
**Le `:v:` d'un NURI de DOCUMENT porte l'overlay de son STORE** (VÉRIFIÉ, chaîne
|
||||||
|
lue de bout en bout) : `NuriV0::repo_graph_name(repo_id, overlay_id)` formate
|
||||||
|
`o:{repo_id}:v:{overlay_id}` ; dans `doc_create` la valeur injectée est
|
||||||
|
`store.outer_overlay()` — le store **contenant**, jamais le `repo_id`. Un `Repo` ne
|
||||||
|
porte **aucun** champ overlay (seulement `store: Arc<Store>`) ; c'est `Store` qui
|
||||||
|
porte `overlay_id`. **Contre-preuve mécanique** : dans `Store`, `get`/`put`/`del`/`has`
|
||||||
|
passent tous `&self.overlay_id` au block storage — tous les documents d'un store
|
||||||
|
partagent le namespace de blocs, donc un overlay par-document est structurellement
|
||||||
|
impossible.
|
||||||
|
|
||||||
|
### La conséquence à connaître : le `:v:` est un pseudonyme stable
|
||||||
|
|
||||||
|
**Tous les documents d'une même personne dans son store protected portent le MÊME
|
||||||
|
`:v:`** = `outer(protected_store_id)`. Donc une référence cap-less — précisément
|
||||||
|
celle qu'on utilise pour « nommer sans donner à lire » — **expose l'appartenance
|
||||||
|
au store**, c'est-à-dire un **identifiant pseudonyme stable et permanent de la
|
||||||
|
personne**. Le store_id lui-même ne fuit pas (BLAKE3 non inversible), donc ça ne
|
||||||
|
dit pas *qui* ; mais c'est un **handle constant**, le même partout et pour
|
||||||
|
toujours, corrélable par quiconque collecte des références cap-less.
|
||||||
|
|
||||||
|
**Le couplage qui en résulte, et qui contraint tout modèle de présence anonyme** :
|
||||||
|
ce même `:v:` est *simultanément* (a) ce qui permet de **dédupliquer** des
|
||||||
|
références sans les lire — deux références de même `:v:` viennent de la même
|
||||||
|
personne — et (b) ce qui permet de **tracer** cette personne d'un contexte à
|
||||||
|
l'autre. **C'est le même bit d'information.** On ne peut pas obtenir la dédup sans
|
||||||
|
concéder le traçage, ni supprimer le traçage sans perdre la dédup — sauf à changer
|
||||||
|
le découpage en stores, ce qui déplace le curseur mais ne supprime pas l'arbitrage.
|
||||||
|
|
||||||
|
*Nuances.* Le `:v:` du NURI est l'overlay **outer**, alors que le trafic
|
||||||
|
client↔broker et le stockage local utilisent l'**inner** — valeur différente, mais
|
||||||
|
tirée du store elle aussi, donc la propriété tient dans les deux cas. Un store
|
||||||
|
`Dialog` renvoie un `Inner`, toujours store-scopé.
|
||||||
|
|
||||||
INFÉRÉ : un détenteur **sans clé** peut vraisemblablement **récupérer les blocs
|
INFÉRÉ : un détenteur **sans clé** peut vraisemblablement **récupérer les blocs
|
||||||
chiffrés** (avec l'overlay, toujours cap-less) → vérifier l'**existence** d'un doc
|
chiffrés** (avec l'overlay, toujours cap-less) → vérifier l'**existence** d'un doc
|
||||||
|
|||||||
+10
-2
@@ -40,8 +40,16 @@ lecture = **détenir la clé**, exactement comme en cible.
|
|||||||
|
|
||||||
## Conséquences de forme (à respecter partout)
|
## Conséquences de forme (à respecter partout)
|
||||||
|
|
||||||
- **Lecture = possession d'une clé** (ReadCap = `{id, clé}`). Un id nu ne lit pas.
|
- **Tout est clés et URLs.** Il n'y a **pas** de notion d'appartenance, de rôle ni
|
||||||
- **Écriture = membership/permissions** (primitif **distinct** — PAS de la possession).
|
de liste d'autorisation dans le modèle : uniquement de la cryptographie
|
||||||
|
symétrique et asymétrique, des URIs, et qui détient quelle clé. Toute forme
|
||||||
|
exposée qui ressemble à une ACL, un `member`, un `role` ou une `permission` est
|
||||||
|
une **mauvaise forme**, quel que soit l'échafaudage qu'on peut lire par ailleurs
|
||||||
|
dans l'état courant de NextGraph.
|
||||||
|
- **Lecture = possession de la clé de lecture** (ReadCap = `{id, clé}`). Un id nu
|
||||||
|
(un `did` sans ReadCap) ne lit pas.
|
||||||
|
- **Écriture = possession de la clé d'écriture** — une clé **distincte** de celle
|
||||||
|
de lecture, donc un axe distinct, mais **de la possession elle aussi**.
|
||||||
- **Partage d'un cap = le sceller à un destinataire** (livraison **durable**, au
|
- **Partage d'un cap = le sceller à un destinataire** (livraison **durable**, au
|
||||||
moment du partage — PAS une ACL re-déclarée à chaque session).
|
moment du partage — PAS une ACL re-déclarée à chaque session).
|
||||||
- **Révocation = re-key** (nouvelle clé ; les anciens détenteurs gardent l'ancien
|
- **Révocation = re-key** (nouvelle clé ; les anciens détenteurs gardent l'ancien
|
||||||
|
|||||||
Reference in New Issue
Block a user