ead5aececf
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
217 lines
17 KiB
Markdown
217 lines
17 KiB
Markdown
# Brief — aligner l'émulation des caps sur le vrai modèle NextGraph
|
||
|
||
**Brief (incubation) — 2026-07-20.** Voir la référence `docs/readcap-and-nuri-model.md`.
|
||
|
||
## Problème
|
||
|
||
`caps.ts` émule les droits de lecture comme une **ACL** (`Map<Nuri, Set<PrincipalId>>`,
|
||
`grantRead(doc, grantee)`) — **l'inversion** du vrai modèle NextGraph (possession
|
||
de clé). Conséquences : pas de notion de **référence cap-less**, grant/révocation
|
||
**instantanés et totaux** (au lieu de scellage durable + re-key), et une API
|
||
(`declareConnections`) que les consommateurs doivent **re-déclarer à chaque
|
||
session**. Cet écart empêche de bâtir correctement des modèles qui reposent sur la
|
||
vraie sémantique — notamment la **présence anonyme** (nommer/compter sans lire).
|
||
|
||
## Objectif : shape-fidelity, PAS sécurité
|
||
|
||
Le polyfill **n'égale PAS** la sécurité de NextGraph fini, et n'essaie pas. Le
|
||
wallet partagé + l'absence de crypto rendent l'émulation **volontairement
|
||
non-sécurisée** (tout est en clair, tout marqueur est forgeable) — véhicule de
|
||
dev/staging, pas un but. **Seul objectif** : exposer la **BONNE FORME** des
|
||
primitives futures pour que les consommateurs (Festipod) soient codés contre le
|
||
**modèle mental correct** et **n'aient pas à être réécrits** quand NextGraph sera fini.
|
||
|
||
Corollaire : **« pas de crypto » n'est pas un problème** ; ce qui compte est d'être
|
||
**dans la même logique, avec RIGUEUR**. Une critique « un attaquant lit le clair /
|
||
forge un marqueur » est **exacte mais hors-scope**. Ce qui est **inacceptable** =
|
||
exposer la **mauvaise forme** (ex. une ACL là où le réel est possession de clé) →
|
||
le consommateur code contre un modèle qui n'existera pas. **L'inversion ACL des
|
||
ReadCaps EST ce manque de rigueur** — le défaut central à corriger.
|
||
|
||
## Mécanisme d'enforcement : simulation crypto LÉGÈRE (anti-ACL, anti-raccourci)
|
||
|
||
Pour que la forme soit **réellement** possession-de-clé (et pas une ACL déguisée),
|
||
la donnée d'un doc est **stockée chiffrée** (chiffrement symétrique par-doc, même
|
||
léger) et le **ReadCap = la clé**. Invariant (cf. `docs/vision.md`) :
|
||
|
||
> un **`did` nu (sans ReadCap)** ne permet **PAS** de lire ; un **NURI avec
|
||
> ReadCap** est **suffisant et requis**.
|
||
|
||
Ça **empêche les raccourcis** que l'adversaire a relevés (#4/#6 : lire le clair,
|
||
`sparqlQuery`/`inbox.read` qui bypass le filtre) et **interdit** de retomber sur
|
||
une ACL — c'est le cœur de « même logique, avec rigueur ».
|
||
|
||
**Cible — PAS l'état actuel** : toutes les surfaces de lecture (`read-model`,
|
||
`read-filter`, `sparqlQuery`, `inbox.read`) devront passer par le
|
||
déchiffrement-avec-clé. **Aujourd'hui c'est FAUX** : `inbox.read` (`inbox.ts:198`)
|
||
et `sparqlQuery` brut (`docs.ts:82`) **bypass** le filtre, et le seul garde
|
||
(`caps.canRead`, `caps.ts:95`) est une **ACL set-membership** — l'inversion même
|
||
que la vision interdit. **Fermer ces voies = le cœur de P1.**
|
||
|
||
1. **Deux formes de référence distinctes** : cap-less (nomme/localise sans lire —
|
||
aligné sur le NURI sans `:k:`) vs cap-porteur (id + clé/token). Aujourd'hui
|
||
absent.
|
||
2. **Grant = livrer un cap-token à un destinataire** (émuler le scellage :
|
||
le destinataire *reçoit* le token dans son inbox ; c'est la **possession** du
|
||
token qui autorise la lecture — pas une ligne d'ACL vérifiée par principal).
|
||
3. **Enforcement par possession** : les lecteurs (`read-filter`, `use-shape`) ne
|
||
voient que ce dont ils **détiennent le token**, pas « ce dont ils sont dans le
|
||
set de readers ».
|
||
4. **Résoudre un cap-less** = nommer / prouver l'existence / compter, **sans**
|
||
exposer le contenu (support de la présence anonyme).
|
||
5. **Révocation = re-key** émulé : invalider l'ancien token, re-livrer un nouveau
|
||
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
|
||
|
||
- **TRANCHÉ (directive PO, 2026-07-21)** : on **simule la crypto** (donnée chiffrée
|
||
par-doc, cap = clé). La « sémantique seule » (registre de tokens) est **écartée** —
|
||
elle redevient une ACL et laisse lire le clair. Reste à trancher le **niveau** de
|
||
simulation (chiffrement réel léger vs projection read-model masquée), **avant P1**.
|
||
- Représentation NURI cap-less vs cap-porteur dans l'émulation (calquer `:k:`).
|
||
- Faut-il permettre le **fetch keyless** (résoudre un cap-less en existence/compte
|
||
sans le contenu) — dépend de ce que le vrai broker autorise (INFÉRÉ, non tracé).
|
||
- Migration d'API : `declareConnections`/`grantRead` → `seal(cap, recipient)` +
|
||
`inbox → caps reçus`. Casse les consommateurs (`declareConnections` app disparaît).
|
||
|
||
## P0 — Spike « keyless-resolve » (le verrou, AVANT tout P1)
|
||
|
||
**Question porteuse** : un détenteur d'une **référence cap-less** (`did:ng:o:{id}:v:{overlay}`, sans `:k:`) peut-il, **sans jamais lire le contenu** :
|
||
- **Q1 — Existence / fetch** : prouver/récupérer la présence des blocs (chiffrés) auprès du broker ? Ou le broker exige-t-il un ReadCap/membership pour servir les blocs ?
|
||
- **Q2 — Suppression** : distinguer « existe » de « supprimé » ? *(Le point FRAGILE : NextGraph est append-only CRDT — une désinscription = un **commit tombstone** qu'il faudrait **lire** pour connaître → potentiellement **la clé est requise**. Or le **décrément au leave** en dépend.)*
|
||
- **Q3 — Confidentialité** : la clé (`:k:`) reste-t-elle **requise** pour déchiffrer (le keyless ne donne jamais le contenu) ?
|
||
|
||
**Pourquoi c'est le verrou** : tout le **compteur anonyme** (compter/valider des réfs cap-less sans lire) ET le **décrément au leave** en dépendent. **Si NON** → « compteur anonyme par réf cap-less » est **inconstruisible en cible** → Festipod ne doit **pas** coder cette forme (réécriture garantie). **Si OUI** → P1 expose `resolveCapLess(nuri) → {exists|deleted}` (jamais de contenu), et l'émulation la simule fidèlement.
|
||
|
||
**Méthode** (bon marché, décisif) :
|
||
1. **Tracer** dans `nextgraph-rs` le chemin d'**autorisation de fetch** du broker/verifier : qui sert les blocs (`BlocksGet`/`TopicSync`/`OverlaySync`) ? un cap/membership est-il vérifié, ou `id+overlay` suffit ? l'overlay *outer* est-il public ? une suppression est-elle observable sans clé ?
|
||
2. *(Optionnel)* **test e2e décisif** (façon `e2e/reactivity-doc-subscribe.ts`) : B détient la réf cap-less, tente fetch/existence **sans** la clé, vérifie qu'il **n'accède pas** au contenu. Preuve empirique > source.
|
||
3. *(Ou)* confirmer avec le dev NextGraph — le plus rapide.
|
||
|
||
**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).
|
||
|
||
### 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
|
||
|
||
- **P1** — distinction cap-less / cap-porteur dans les NURI + le read-model
|
||
(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
|
||
à 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).
|
||
- **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
|
||
|
||
Un adversaire a réfuté le brief (6 constats). **À lire au filtre de l'Objectif ci-dessus** (forme, pas sécurité). Les critiques purement **sécurité** — contenu en clair lisible (#4), marqueurs forgeables — sont **ACCEPTÉES / hors-scope** : le polyfill ne cherche pas à les empêcher. Restent les vrais défauts de **FORME / rigueur** (à corriger), et une question de **modèle futur** (#5) :
|
||
|
||
1. **WriteCap oublié, et « possession » y est FAUX.** L'écriture est **membership/permissions** (`AddMember`) — une **liste d'autorisation**, pas de la possession de clé (réf §1) ; `ng-proxy.ts:28-48` garde chaque `sparql_update`. → garder une **piste WriteCap = membership** ; la **possession ne concerne QUE la lecture**.
|
||
2. **P2 « possession sans crypto » = l'ACL renommée.** Sans crypto, « qui détient quel token » = `Map<doc, Set<holder>>` = le `readers` actuel : **aucun delta observable**. Les vrais deltas sont **durabilité + cap-less + re-partage par le détenteur** — c'est ÇA le contenu de P2, pas « inverser l'ACL ».
|
||
3. **Révocation non-rétroactive INÉMULABLE** sans versioning : `read-model.ts:112-118` ne lit que l'état courant → « invalider l'ancien token » = retrait total = l'inverse du réel (l'ex-détenteur déchiffre les versions **antérieures**). → n'émuler que « plus de nouvelles lectures après re-key » + **documenter la non-rétroactivité comme non-émulable**.
|
||
4. **cap-less « sans exposer le contenu » ILLUSOIRE dans l'émulation** : contenu en **clair** dans le wallet partagé ; `sparqlQuery`/`inbox.read` **bypass** le filtre ; `read-filter.ts:30-35` est tout-ou-rien. → l'anonymat cap-less exige soit du **vrai crypto**, soit une **projection read-model masquée** (compter sans lire). « Remplacement pas refonte » est **surévalué**.
|
||
5. **Keyless-fetch = INFÉRÉ et load-bearing** : ajouter un **spike P0** qui le vérifie **avant** P1 (sinon le modèle — polyfill ET Festipod — est inconstruisible).
|
||
6. **Migration ≠ swap d'API.** `declareConnections` se re-joue chaque session parce que la map est éphémère ; des scellages durables déplacent le grant à l'**acceptation de connexion** + persistent « déjà scellé » — pas d'analogue de `protectedDocsOf` + la boucle de re-dérivation. **Re-architecture consommateur.**
|
||
7. *(Plausible)* livrer un cap par inbox async **ne re-déclenche pas** `watchShape` (souscrit aux docs de données, pas aux caps) → vues illisibles **périmées** jusqu'à un autre changement. → prévoir un signal de mutation de caps.
|
||
|
||
**Conséquence** : ajouter en tête **P0 (spike keyless-fetch)** et une **piste WriteCap distincte** ; requalifier P2 (le vrai contenu = durabilité + cap-less + re-partage, pas « inverser l'ACL ») ; acter que **sans crypto, la privacy de lecture n'est pas applicable** (choisir : vrai crypto vs projection masquée).
|
||
|
||
Liens : `readcap-and-nuri-model.md`, `packages/client/src/caps.ts`. Côté consommateur,
|
||
le brief Festipod « réaligner les inscriptions » dépend de ce chantier.
|