Files
ng-eventually/docs/readcap-and-nuri-model.md
T
Sylvain Duchesne ead5aececf 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
2026-07-27 12:07:24 +02:00

10 KiB

Modèle ReadCap & NURI de NextGraph — et l'émulation caps du polyfill

Établi 2026-07-20, VÉRIFIÉ par lecture directe du cœur Rust nextgraph-rs (sauf points marqués INFÉRÉ). Les file:line sont datés — les numéros de ligne sont volatils, se repérer par symbole/regex.

But : donner la vérité-terrain du modèle de droits d'accès NextGraph, pour aligner l'émulation caps.ts du polyfill (aujourd'hui une ACL — l'inverse du modèle réel). C'est la base de l'item « aligner ReadCap/WriteCap avec NextGraph ».


1. Un ReadCap = possession d'une clé, PAS une ACL par-identité

Un ReadCap est fondamentalement une clé cryptographique que l'on détient, pas une entrée d'ACL liée à un wallet. « Qui détient la clé peut lire. »

  • Structure : ReadCap = ObjectRef = BlockRef { id: BlockId, key: SymKey } (engine/repo/src/types.rs:461, 463-471, 557, 565).
    • id: BlockId = digest BLAKE3 (adresse de l'objet chiffré).
    • key: SymKey = ChaCha20Key([u8;32]) = la clé de déchiffrement. Détenir le couple → le broker sert les blocs chiffrés par id, on déchiffre localement avec key.
  • Granularité : par commit/objet l'ObjectRef est le cap ; pour une branche → commit de définition ; pour un repo → RootBranch ; pour un store → cap du repo racine (types.rs:559-565). ReadCapSecret = la moitié clé (:567-570).
  • Il n'y a PAS de read-ACL. L'appartenance/permissions d'un repo (RootBranch, AddMember, AddPermission) gouvernent l'écriture/admin, pas la lecture. La lecture n'est gardée que par la possession de la clé.

2. Accorder la lecture = sceller la clé au destinataire

« Grant » = livrer le cap scellé (crypto_box seal, chiffrement à clé publique anonyme) à la pubkey d'inbox du destinataire — seul lui l'ouvre avec sa clé privée.

  • Message d'inbox scellé : InboxMsgBody.msg = crypto_box::seal(... to_inbox ...), ouvert avec la clé secrète d'inbox (engine/net/src/types.rs:4272, 4299, 4319).
  • Le payload peut porter un cap : ContactDetails.read_cap: Option<ReadCap> (« if user wants to share the content of profile ») (net/types.rs:4232-4233) → grant dirigé (scellé à un destinataire).
  • Variante non-dirigée : RepoLinkV0.read_cap = un lien partageable que quiconque le reçoit peut ouvrir (net/types.rs:5061-5078).

Le « ciblage wallet » vit donc dans l'enveloppe de scellage, pas dans le cap : le cap reste {id, clé}, possession-based.

3. Révocation = re-key (grossier, non-rétroactif)

On ne « reprend » pas une clé livrée. Révoquer = re-chiffrer avec une nouvelle clé et ne la re-sceller qu'aux autorisés restants.

  • « Capabilities are not durable: they can be refreshed by members and previously shared Caps become obsolete/revoked… if [a member] doesn't subscribe, they lose access after the refresh » (net/types.rs:5055-5058).
  • Mécanisme : RootCapRefresh / BranchCapRefresh (repo/src/commit.rs:616,630; perms types.rs:1748-1749).
  • Conséquences : grossier (échelle repo/branche), non-rétroactif (ce qui a été lu avant reste connu de l'ex-détenteur ; il ne déchiffre que les versions antérieures au refresh).
  • Livraison durable d'un cap = PermaCap — encore TODO (repo/types.rs:578).

4. Grammaire NURI : cap-less vs cap-porteur (le segment :k:)

Le discriminant est le segment :k:{clé} : présent = cap-porteur ; absent = cap-less (nomme/localise sans donner le droit de lire). C'est de première classe dans le type : NuriV0.target (des ids) et access/objects (le cap) sont des champs séparés — un NURI d'id parse avec access: vec![] (engine/net/src/app_protocol.rs:53-62, 99-118, 181-195, 659-677).

Cap-less (id + overlay éventuel, pas de clé) — formatters app_protocol.rs, regexes net/types.rs :

  • did:ng:o:{repo_id} (:315, RE_REPO_O types.rs:52)
  • did:ng:o:{repo_id}:v:{overlay_id} (:263, RE_REPO types.rs:55)
  • did:ng:o:{repo_id}:v:{overlay_id}:b:{branch_id} (RE_BRANCH types.rs:58)
  • did:ng:o:{repo_id}:c:{commit_id} (:355)
  • did:ng:b:{branch} / h:{topic} / v:{overlay} / d:{inbox} (:327,323,319,359)

Cap-porteur (embarque la clé) :

  • did:ng:j:{id}:k:{clé} — read cap d'objet/fichier (repo/types.rs:511, RE_FILE_READ_CAP types.rs:49)
  • 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)

Le segment :v: est l'overlay, qui a sa propre section ci-dessous — c'est le 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 chiffrés (avec l'overlay, toujours cap-less) → vérifier l'existence d'un doc sans lire son contenu. Le chemin d'autorisation de fetch broker pour un détenteur sans clé n'a pas été tracé — à confirmer avant de s'en servir.

5. Ce que le polyfill émule (caps.ts) — et où ça diverge

packages/client/src/caps.ts modélise readers: Map<Nuri, Set<PrincipalId>> + grantRead(doc, grantee) (:29-30, 41-42) — une ACL de principals par document, soit l'INVERSION exacte du modèle réel (clé). Divergences :

Réel NextGraph Émulation caps.ts
Nature possession de clé ACL (set de principals)
Grant sceller la clé (crypto_box) à l'inbox ajouter un principal au set
Durabilité durable (clé livrée une fois) éphémère (Map vide à chaque session → re-déclarée)
Révocation re-key grossier, non-rétroactif retrait du set : instantané et total
Granularité repo / branche / commit / objet un cap par doc-NURI
Réf. sans droit NURI cap-less (sans :k:) pas de notion (l'ACL dit qui peut)

Face app : declareConnections (côté consommateur) qui re-déclare « mes connexions lisent mes entités protected » à chaque session est un artefact de cette ACL éphémère — sans objet dans le modèle réel (les scellages y sont durables ; on scelle par-doc au partage, pas par-session).

6. Implications pour les consommateurs (ex. Festipod)

  • « scope protected = mon réseau peut lire » n'est pas une ACL vérifiée par le broker : c'est « j'ai scellé ma read key à chacune de mes connexions ». Le modèle mental « scope = ACL » est faux au niveau NextGraph.
  • Références anonymes possibles : mettre un NURI cap-less dans une collection tierce laisse le tiers nommer/compter sans lire l'identité ; sceller le cap-porteur séparément aux seuls autorisés. (Base d'un modèle de présence « participation auto-possédée + Set curé cap-less + cap scellé aux connexions ».)
  • Alignement à faire : quand les vraies opérations de cap seront disponibles, remplacer l'ACL émulée par du scellage de clé durable par-doc, et declareConnections-comme-ACL-ré-déclarée disparaît.

Réserves / lacunes

  • file:line datés (2026-07) — re-vérifier par symbole ; le core bouge.
  • INFÉRÉ : fetch broker keyless (existence sans clé) — non tracé au runtime.
  • Non tracé : exécution complète de RootCapRefresh côté verifier (verifier/src/commits/mod.rs:616), stockage wallet de private_store_read_cap (repo/types.rs:945,976).