brainclaw 1.20.4 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-cloud.js +63 -0
- package/dist/cli.js +2 -3
- package/dist/commands/cloud.js +198 -0
- package/dist/commands/export.js +3 -3
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp-write-claims.js +162 -46
- package/dist/commands/mcp-write-entities.js +67 -0
- package/dist/commands/mcp.js +64 -1
- package/dist/commands/session-end.js +0 -102
- package/dist/commands/session-start.js +0 -23
- package/dist/commands/switch.js +41 -12
- package/dist/core/actions.js +25 -1
- package/dist/core/agent-files.js +19 -0
- package/dist/core/agentruns.js +68 -10
- package/dist/core/assignments.js +94 -19
- package/dist/core/claims.js +13 -24
- package/dist/core/config.js +58 -0
- package/dist/core/context-diff.js +28 -11
- package/dist/core/coordination.js +1 -3
- package/dist/core/entity-locator.js +404 -0
- package/dist/core/federation-attestation.js +96 -0
- package/dist/core/federation-canonical.js +95 -0
- package/dist/core/federation-hpke.js +213 -0
- package/dist/core/federation-inbound.js +187 -0
- package/dist/core/federation-keyring.js +241 -0
- package/dist/core/federation-message.js +5 -5
- package/dist/core/federation-outbox-v2.js +125 -0
- package/dist/core/federation-pairing.js +213 -0
- package/dist/core/federation-projection.js +336 -0
- package/dist/core/federation-relay.js +223 -0
- package/dist/core/federation-state.js +270 -0
- package/dist/core/identity.js +9 -1
- package/dist/core/ids.js +5 -0
- package/dist/core/io.js +39 -1
- package/dist/core/operations/relocate.js +40 -10
- package/dist/core/schema.js +24 -17
- package/dist/core/sequence.js +47 -6
- package/dist/core/store-resolution.js +99 -26
- package/dist/core/workspace-projects.js +23 -2
- package/dist/core/worktree.js +59 -1
- package/dist/facts.js +7 -7
- package/dist/facts.json +6 -6
- package/docs/cli.md +73 -40
- package/docs/concepts/federation-v2-rfc.md +275 -0
- package/docs/index.md +1 -0
- package/package.json +2 -2
- package/dist/cli/register-federation.js +0 -258
- package/dist/core/federation-cloud.js +0 -245
- package/dist/core/federation-outbox.js +0 -292
- package/dist/core/federation-signing.js +0 -115
package/docs/cli.md
CHANGED
|
@@ -36,7 +36,9 @@ All commands support these global options:
|
|
|
36
36
|
|
|
37
37
|
Set the active project for subsequent CLI and MCP commands. This eliminates the need to `cd` into a subproject directory in multi-project workspaces.
|
|
38
38
|
|
|
39
|
-
**Session-scoped by default (v1.10.0).** A plain `switch <project>` only affects the **calling agent's session** — it auto-creates a session if needed and never touches the shared pointer. This is what keeps two agents working in the same monorepo independent: neither clobbers the other.
|
|
39
|
+
**Session-scoped by default (v1.10.0).** A plain `switch <project>` only affects the **calling agent's session** — it auto-creates a session if needed and never touches the shared pointer. This is what keeps two agents working in the same monorepo independent: neither clobbers the other.
|
|
40
|
+
|
|
41
|
+
**What the read paths report (v1.21.0).** `switch --list` and the no-argument "show current" both derive from the *same* resolver that routes an actual write, and echo the selector that won as `scope` (show) / `active_source` (`--list`): `session`, `global`, `cwd_child` (you are standing inside a child project), `env_project` / `explicit` (named by `$BRAINCLAW_PROJECT` or `--cwd`), or `cwd` (nothing points anywhere — commands use the current directory). Before v1.21.0 these two read paths walked their own session-then-global ladder, so an agent inside a child project was shown the *shared* pointer while its writes went to the child. Reporting a project a write would not reach is the defect that behaviour existed to hide, so the reported project and the written project are now the same one by construction.
|
|
40
42
|
|
|
41
43
|
`--global` is the **only** path that writes (or, with `--clear`, removes) the shared, per-workspace `.brainclaw/active-project.json` that every agent on the host sees. Use it for an operator setting a workspace-wide default — not for per-agent work.
|
|
42
44
|
|
|
@@ -1747,46 +1749,9 @@ brainclaw push --remote origin --message "chore: push memory state" --json
|
|
|
1747
1749
|
|
|
1748
1750
|
## Federation
|
|
1749
1751
|
|
|
1750
|
-
The `federation` command group
|
|
1751
|
-
|
|
1752
|
-
### `brainclaw federation push <message>`
|
|
1753
|
-
|
|
1754
|
-
Push a test signal to the cloud. The signal is sent from the current project and agent to a target project or broadcast address.
|
|
1755
|
-
|
|
1756
|
-
| Option | Description |
|
|
1757
|
-
|---|---|
|
|
1758
|
-
| `--type <type>` | Signal type (default: `runtime_note`). Accepted values: `signal`, `handoff`, `candidate`, `runtime_note`, `board_snapshot` |
|
|
1759
|
-
| `--to-project <project>` | Target project name (default: `broadcast`) |
|
|
1760
|
-
| `--to-agent <agent>` | Target agent name |
|
|
1761
|
-
|
|
1762
|
-
```bash
|
|
1763
|
-
brainclaw federation push "Auth rollout complete" --to-project lodestar
|
|
1764
|
-
brainclaw federation push "Blocked on payments" --type runtime_note --to-agent copilot
|
|
1765
|
-
```
|
|
1766
|
-
|
|
1767
|
-
### `brainclaw federation pull`
|
|
1768
|
-
|
|
1769
|
-
Pull signals from the cloud inbox for the current agent.
|
|
1770
|
-
|
|
1771
|
-
| Option | Description |
|
|
1772
|
-
|---|---|
|
|
1773
|
-
| `--agent <name>` | Agent name to pull for (default: auto-detected) |
|
|
1774
|
-
| `--since <date>` | Only pull signals after this ISO date |
|
|
1775
|
-
| `--limit <n>` | Maximum number of signals to pull (default: 20) |
|
|
1776
|
-
|
|
1777
|
-
```bash
|
|
1778
|
-
brainclaw federation pull
|
|
1779
|
-
brainclaw federation pull --since 2026-04-01
|
|
1780
|
-
brainclaw federation pull --agent copilot --limit 50
|
|
1781
|
-
```
|
|
1782
|
-
|
|
1783
|
-
### `brainclaw federation status`
|
|
1752
|
+
The v1 `brainclaw federation` command group and the whole cloud egress path (`app.brainclaw.dev` push/pull, `cloud_sync` config, `BRAINCLAW_CLOUD_*` env vars) were removed in wave 1 of dec#156 / pln#651. There is no migration: the v1 format is abandoned, not deprecated. The v2 federation surface will be introduced by pln#651 wave 3 alongside the pairing CLI (`brainclaw cloud connect`).
|
|
1784
1753
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
```bash
|
|
1788
|
-
brainclaw federation status
|
|
1789
|
-
```
|
|
1754
|
+
Local cross-project federation (see below) is unaffected.
|
|
1790
1755
|
|
|
1791
1756
|
---
|
|
1792
1757
|
|
|
@@ -2242,4 +2207,72 @@ Show brainclaw context volume stats — tokens injected per agent and per MCP to
|
|
|
2242
2207
|
|---|---|
|
|
2243
2208
|
| `--json` | Output as JSON |
|
|
2244
2209
|
|
|
2210
|
+
### `brainclaw cloud status`
|
|
2211
|
+
|
|
2212
|
+
Show the federation v2 connection state for this workspace: linked cloud project, role,
|
|
2213
|
+
current key epoch, the epochs actually readable on this device, and the three sync states.
|
|
2214
|
+
|
|
2215
|
+
| Option | Description |
|
|
2216
|
+
|---|---|
|
|
2217
|
+
| `--json` | Output as JSON |
|
|
2218
|
+
|
|
2219
|
+
Sync state is **visible by design** (dec#154): a cloud-originated operation materializes in
|
|
2220
|
+
the local journal as `pending`, `synced` or `conflict`, and a conflict is presented for
|
|
2221
|
+
resolution rather than resolved by a silent last-write-wins. The counts are read from the
|
|
2222
|
+
outbox on disk, not from a cached counter — a status that echoed a number the code had
|
|
2223
|
+
itself incremented would reassure precisely when you are checking because you doubt.
|
|
2224
|
+
|
|
2225
|
+
`readable_epochs` likewise comes from the keyring on disk. It can be shorter than the
|
|
2226
|
+
epochs the state declares: a partial backup restore leaves that exact disagreement, and
|
|
2227
|
+
the honest answer is what can actually be decrypted here.
|
|
2228
|
+
|
|
2229
|
+
### `brainclaw cloud connect <invite-code> --url <url> --agent <id>`
|
|
2230
|
+
|
|
2231
|
+
Join a cloud project. This is a **key ceremony**, not a config write: it claims the invite,
|
|
2232
|
+
proves possession of this agent's Ed25519 identity, and attests the device's X25519
|
|
2233
|
+
encryption key with that same identity.
|
|
2234
|
+
|
|
2235
|
+
| Option | Description |
|
|
2236
|
+
|---|---|
|
|
2237
|
+
| `--url <url>` | Cloud deployment address (required) |
|
|
2238
|
+
| `--agent <id>` | Opaque agent identifier to enroll, 4–64 chars (required) |
|
|
2239
|
+
| `--json` | Output as JSON |
|
|
2240
|
+
|
|
2241
|
+
The human copies **only the invite code**, then compares **two fingerprints** with what the
|
|
2242
|
+
approver sees on their screen. No API key, no PEM, no agent_id, no environment variable
|
|
2243
|
+
(dec#8). Both fingerprints are printed in full, never truncated — a 16-character comparison
|
|
2244
|
+
collides far more easily than it looks.
|
|
2245
|
+
|
|
2246
|
+
The attestation is what stops a **phantom member**. Without it, the Cloud — which
|
|
2247
|
+
orchestrates the pairing — could insert its own key into the envelope list: end-to-end
|
|
2248
|
+
encryption whose key exchange is arbitrated by the very party it claims to neutralize.
|
|
2249
|
+
|
|
2250
|
+
A refused pairing writes **nothing** locally, so re-running is safe and leaves no orphan.
|
|
2251
|
+
|
|
2252
|
+
### `brainclaw cloud await --url <url>`
|
|
2253
|
+
|
|
2254
|
+
Observe the human approval and activate the local pairing.
|
|
2255
|
+
|
|
2256
|
+
Deliberately a **separate command**: approval depends on a person, whose delay is not
|
|
2257
|
+
bounded. A command that blocked indefinitely on a third party would be a poor citizen in a
|
|
2258
|
+
script, and an interrupted pairing must stay resumable. Polling does **not** mutate local
|
|
2259
|
+
state — reading a status must never change the workspace.
|
|
2260
|
+
|
|
2261
|
+
### `brainclaw cloud disconnect --url <url>`
|
|
2262
|
+
|
|
2263
|
+
Remove the local authorization and request remote revocation.
|
|
2264
|
+
|
|
2265
|
+
| Option | Description |
|
|
2266
|
+
|---|---|
|
|
2267
|
+
| `--url <url>` | Cloud deployment address (required) |
|
|
2268
|
+
| `--forget-keys` | Also erase this project's epoch keys — its sealed past becomes unreadable here |
|
|
2269
|
+
|
|
2270
|
+
Local state flips to revoked **even if the cloud is unreachable**: otherwise a lost device
|
|
2271
|
+
would stay authorized for want of a network, the opposite of what revocation must
|
|
2272
|
+
guarantee.
|
|
2273
|
+
|
|
2274
|
+
What disconnect does **not** do, stated rather than left implied: it does not erase data
|
|
2275
|
+
already pulled and decrypted locally, nor what other devices already hold. It withdraws an
|
|
2276
|
+
authorization; it does not rewrite the past (RFC §5.2).
|
|
2277
|
+
|
|
2245
2278
|
This keeps end-user installs aware of published npm releases without requiring a local tarball channel. To keep beta testers on a different channel, set `brainclaw_update_source` to `type: npm` with a different `dist_tag`, such as `prelaunch`.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# RFC joint — Fédération v2 : projection chiffrée, appariement attesté et board aveugle
|
|
2
|
+
|
|
3
|
+
> **Statut :** décision d'architecture à implémenter
|
|
4
|
+
> **Propriétaire :** core + Cloud, document canonique unique
|
|
5
|
+
> **Remplace :** l'étape 1 de pln#650 et celle de pln#102, et les propositions de transition du format antérieur
|
|
6
|
+
> **Dépendances :** dec#154, dec#155, dec#156, dec#8/cst#1–4 côté Cloud, décision promue depuis can_b16f6957, trp#1520
|
|
7
|
+
|
|
8
|
+
Ce fichier est le RFC commun : le dépôt Cloud le référence, il ne maintient pas un second protocole. Les deux implémentations prennent les types, vecteurs et règles de refus de ce document comme contrat unique.
|
|
9
|
+
|
|
10
|
+
## 1. Décision et invariant
|
|
11
|
+
|
|
12
|
+
La fédération v2 remplace entièrement le fil antérieur. Ce format est **abandonné**, pas déprécié : aucune lecture, négociation, conversion, réémission ni migration depuis cloud_sync ou BRAINCLAW_CLOUD_* n'est autorisée. Les enveloppes anciennes en attente sont jetables. L'activation ne peut résulter que d'un appariement explicite enregistré dans l'état local de connexion ; la présence d'une variable d'environnement ne vaut jamais consentement.
|
|
13
|
+
|
|
14
|
+
> Rien ne quitte l'hôte en clair en dehors du squelette non verbal explicitement classé, et rien n'entre dans la mémoire locale sans signature d'origine vérifiée. Les deux directions échouent fermées.
|
|
15
|
+
|
|
16
|
+
sealed est le seul transport de contenu. Une erreur de classification, validation, chiffrement, signature, révocation ou fraîcheur annule l'opération ; elle ne produit ni export tronqué ni matérialisation locale. Le profil public est opaque par défaut.
|
|
17
|
+
|
|
18
|
+
### Hors périmètre v1
|
|
19
|
+
|
|
20
|
+
- Le Cloud n'est pas la mémoire canonique : il conserve une projection, des opérations de contrôle et un état de transport.
|
|
21
|
+
- Le board ne reçoit aucune clé et ne déchiffre rien dans le navigateur v1.
|
|
22
|
+
- **La recherche reste locale.**
|
|
23
|
+
- La révocation ne prétend pas effacer une clé ou un texte déjà détenu ; elle protège seulement les données futures.
|
|
24
|
+
- Réécrire le passé signifie reprojeter depuis le local, jamais réécrire des ciphertexts dans le Cloud.
|
|
25
|
+
|
|
26
|
+
## 2. Acteurs, identifiants et conflit
|
|
27
|
+
|
|
28
|
+
| Élément | Règle v2 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Hôte local | Source de vérité des contenus, journal et mapping local → Cloud. Il ne délègue jamais une écriture de contenu au Cloud. |
|
|
31
|
+
| Cloud | Transport, autorisations, projection chiffrée, opérations de board et états pending/synced/conflict. Il ne déchiffre pas. |
|
|
32
|
+
| cloud_project_id | UUID opaque de l'espace Cloud. Le nom et le chemin locaux ne sont ni URL ni AAD publics. |
|
|
33
|
+
| id_opaque | UUID v4 client pour un objet local. Le mapping vers id reste local et ne traverse jamais le fil. |
|
|
34
|
+
| base_rev | Révision monotone par objet opaque. Prérequis de toute écriture/commande et borne anti-rollback du lecteur. |
|
|
35
|
+
| operation_id | UUID aléatoire par intention de transport, conservé au retry. Il porte l'idempotence, pas un hash de texte. |
|
|
36
|
+
| États visibles | Une opération est pending, synced ou conflict. Un conflit conserve base_rev, opération et proposition de résolution ; aucun last-write-wins silencieux. |
|
|
37
|
+
|
|
38
|
+
Une commande de board est la troisième classe d'appelants de dec#155 : elle ne reçoit qu'un identifiant opaque et un base_rev, jamais cwd, session ou contexte ambiant. Elle est matérialisée dans le journal local après vérification. Une divergence de révision est refusée et visible.
|
|
39
|
+
|
|
40
|
+
## 3. Enveloppe de fil v1
|
|
41
|
+
|
|
42
|
+
Chaque objet, révision, opération de board ou paquet de clés v2 utilise une enveloppe stricte. Il n'existe pas de payload inconnu au point d'egress.
|
|
43
|
+
|
|
44
|
+
~~~ts
|
|
45
|
+
type FederationEnvelopeV1 = {
|
|
46
|
+
schema: 'brainclaw.federation-envelope/v1';
|
|
47
|
+
meta: PublicMetaV1;
|
|
48
|
+
sealed: {
|
|
49
|
+
alg: 'HPKE-v1/X25519-HKDF-SHA256-CHACHA20POLY1305';
|
|
50
|
+
enc: string; // clé HPKE encapsulée, base64url canonique
|
|
51
|
+
nonce: string; // nonce AEAD, base64url canonique, unique par enc
|
|
52
|
+
ciphertext: string; // blob AEAD, base64url canonique
|
|
53
|
+
};
|
|
54
|
+
key_epoch: number;
|
|
55
|
+
origin_sig: {
|
|
56
|
+
alg: 'Ed25519';
|
|
57
|
+
key_id: string; // référence opaque d'une identité enregistrée
|
|
58
|
+
value: string; // signature base64url canonique
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type PublicMetaV1 = {
|
|
63
|
+
id_opaque: string;
|
|
64
|
+
kind: FederatedKind;
|
|
65
|
+
status: PublicStatus;
|
|
66
|
+
priority?: 'low' | 'medium' | 'high' | 'critical';
|
|
67
|
+
rank?: number;
|
|
68
|
+
deps: Array<{ from: string; to: string }>;
|
|
69
|
+
timestamp_bucket_jour: string; // YYYY-MM-DD UTC
|
|
70
|
+
base_rev: number;
|
|
71
|
+
aad: CanonicalAadV1;
|
|
72
|
+
wrap_hint: string; // référence opaque de paquet/roster, jamais un destinataire
|
|
73
|
+
transport: {
|
|
74
|
+
operation_id: string;
|
|
75
|
+
content_hash: string;
|
|
76
|
+
idempotency_key: string;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
~~~
|
|
80
|
+
|
|
81
|
+
key_epoch et origin_sig sont au niveau de l'enveloppe afin qu'ils restent visibles sans clé ; ils sont aussi liés aux octets signés. Aucun champ local ne peut être ajouté à meta. L'absence d'un champ optionnel est significative : priorité et rang ne sont pas inventés pour les objets qui n'en ont pas.
|
|
82
|
+
|
|
83
|
+
### 3.1 Octets canoniques, AAD et signature
|
|
84
|
+
|
|
85
|
+
Les sérialisations hachées, chiffrées ou signées sont JSON UTF-8 canonique : clés triées par code point, aucune espace, chaînes NFC, entiers finis sans notation exponentielle et base64url sans padding. Core et Cloud partagent les vecteurs de test ; ils ne réimplémentent pas chacun une quasi-canonicalisation.
|
|
86
|
+
|
|
87
|
+
L'AAD est une structure canonique, pas une chaîne concaténée ambiguë.
|
|
88
|
+
|
|
89
|
+
~~~ts
|
|
90
|
+
type CanonicalAadV1 = {
|
|
91
|
+
protocol: 'brainclaw/federation/v1';
|
|
92
|
+
cloud_project_id: string;
|
|
93
|
+
object_id: string; // id_opaque
|
|
94
|
+
base_rev: number;
|
|
95
|
+
object_type: FederatedKind;
|
|
96
|
+
schema: 'brainclaw.federation-envelope/v1';
|
|
97
|
+
};
|
|
98
|
+
~~~
|
|
99
|
+
|
|
100
|
+
La suite crypto v1 est DHKEM(X25519, HKDF-SHA-256), HKDF-SHA-256 et ChaCha20-Poly1305. enc, nonce, alg, key_epoch et l'AAD sont obligatoires. Toute répétition du couple de contexte de nonce est une erreur d'émission. Le déchiffrement utilise exactement cet AAD et échoue fermé au moindre octet différent.
|
|
101
|
+
|
|
102
|
+
L'entrée Ed25519 est :
|
|
103
|
+
|
|
104
|
+
~~~text
|
|
105
|
+
"brainclaw/federation-envelope/v1\0"
|
|
106
|
+
|| canonical(meta)
|
|
107
|
+
|| canonical(sealed) // alg, enc, nonce et ciphertext inclus
|
|
108
|
+
|| canonical(key_epoch)
|
|
109
|
+
~~~
|
|
110
|
+
|
|
111
|
+
Le raccourci « signature sur meta || ciphertext » couvre ainsi également les paramètres permettant d'interpréter le ciphertext. Une signature valide est vérifiée contre l'identité Ed25519 du roster de projet, pas contre un from autorapporté ni un en-tête HTTP.
|
|
112
|
+
|
|
113
|
+
### 3.2 Hash, idempotence et erreurs 409
|
|
114
|
+
|
|
115
|
+
content_hash est SHA-256(canonical(sealed)), rendu base64url. Il ne porte jamais un hash de texte clair. L'AEAD aléatoire empêche un essai de contenu à faible entropie d'être confirmé par hash côté Cloud.
|
|
116
|
+
|
|
117
|
+
idempotency_key est SHA-256(canonical(sealed) || operation_id || origin_sig.key_id). Le client crée operation_id aléatoirement avant le premier envoi et le réutilise pour tous ses retries. Le Cloud compare ces valeurs opaques ; il ne les recalcule pas à partir de contenu. Les réponses 409 STALE et 409 REV_CONFLICT emploient base_rev, operation_id et ces dérivés du ciphertext, jamais un content_hash historique sur texte clair.
|
|
118
|
+
|
|
119
|
+
## 4. Classification exhaustive d'egress
|
|
120
|
+
|
|
121
|
+
La classification comporte exactement trois classes : **clair**, **scellé** et **interdit de sortir**. « Non classé » n'est pas une quatrième classe : c'est un refus. Cette table est le contrat de l'étape 5, non une liste indicative.
|
|
122
|
+
|
|
123
|
+
### 4.1 Champs publics autorisés
|
|
124
|
+
|
|
125
|
+
Seuls les champs ci-dessous peuvent quitter l'hôte hors de sealed. Tous sont structurels, normalisés et explicitement construits ; id, noms et références locaux ne sont jamais recopiés.
|
|
126
|
+
|
|
127
|
+
| Champ clair | Origine / normalisation | Limite de confidentialité |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| id_opaque | UUID v4 client, mapping local conservé localement | Stable dans un projet Cloud, pas cross-projet |
|
|
130
|
+
| kind | Type de l'entité : plan, claim, handoff, etc. | Révèle le mix de types |
|
|
131
|
+
| status | État métier normalisé et, le cas échéant, état de sync public | Révèle avancement et conflits |
|
|
132
|
+
| priority | Enum de priorité quand l'entité en porte une | Révèle l'urgence relative |
|
|
133
|
+
| rank | Entier de séquence quand l'entité en porte un | Révèle l'ordre relatif |
|
|
134
|
+
| deps | Arêtes d'UUID opaques from → to, sans libellé ni chemin | Révèle le graphe |
|
|
135
|
+
| timestamp_bucket_jour | Jour UTC de l'événement/révision, jamais l'heure | Révèle une cadence quotidienne |
|
|
136
|
+
| base_rev | Compteur monotone par id_opaque | Révèle la fréquence des changements |
|
|
137
|
+
| key_epoch | Epoch crypto actif | Révèle les rotations, pas les clés |
|
|
138
|
+
| aad | Les six champs canoniques de §3.1 | Lie projet, objet, rév., type et schéma |
|
|
139
|
+
| wrap_hint | Référence opaque au paquet de clés de l'epoch | Ne contient ni nom ni empreinte de destinataire |
|
|
140
|
+
| origin_sig | key_id, algorithme et signature | Pseudonyme stable, nécessaire à la vérification |
|
|
141
|
+
| transport | operation_id, content_hash, idempotency_key dérivés du ciphertext | Retry sans oracle de texte clair |
|
|
142
|
+
|
|
143
|
+
Le champ status contient au plus un couple normalisé object/sync. sync vaut pending, synced ou conflict lorsqu'un état est connu du transport. Le Cloud n'infère jamais un état local absent : sans marqueur reçu, le board affiche « état local inconnu / hors ligne ». Un client local peut, lui, afficher pending avant émission.
|
|
144
|
+
|
|
145
|
+
### 4.2 Règles de classement des feuilles Zod
|
|
146
|
+
|
|
147
|
+
| Classe | Feuilles source | Traitement |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| Clair | seulement les états, priorité, rang et liens dont la projection les mappe explicitement vers §4.1 | Construire une valeur normalisée ; ne jamais recopier la feuille source |
|
|
150
|
+
| Scellé | titres, noms, text, descriptions, tags, lanes, scope_hint, rationale, verify_cmd, auteurs, modèles, provenance, commentaires, résultats, compteurs, heures précises et objets/maps libres | Placés dans le document AEAD. Un map unknown reste un unique leaf scellé : le projecteur public ne peut ni l'inspecter ni le fusionner |
|
|
151
|
+
| Interdit | chemins locaux, host_id, session_id, worktree_path, project_path, storage_dir, commandes, shell, PID, clés, secrets, variables d'environnement et configuration locale | Absent de meta **et** de sealed. Sa présence fait échouer la projection |
|
|
152
|
+
|
|
153
|
+
id et toutes les références locales *_id sont scellés ou omis, sauf lorsqu'une relation est expressément projetée comme arête dans deps après traduction des deux extrémités en UUID opaques. project_id local ne sort pas : cloud_project_id opaque ne vit que dans l'AAD. visibility est une règle locale de décision d'export, jamais une donnée fédérée.
|
|
154
|
+
|
|
155
|
+
### 4.3 Inventaire Zod fédérable, claims et handoffs inclus
|
|
156
|
+
|
|
157
|
+
Cette table définit l'ensemble exhaustif de schémas de contenu admis par v2. Une famille absente est **non projetable** : son schéma entier est interdit de sortie. Les règles communes de §4.2 s'appliquent à chaque ligne. La flèche signifie transformation, pas copie de champ local.
|
|
158
|
+
|
|
159
|
+
| Schéma source / chemins couverts | Clair explicitement projetable | Scellé | Interdit de sortir |
|
|
160
|
+
| --- | --- | --- | --- |
|
|
161
|
+
| ConstraintSchema + confirmations[] | id → id_opaque, kind=constraint, status, jour, base_rev | short_label, text, catégorie, scope, tags, plan_id, expiration, cycle de confirmation et chaque MemoryConfirmationEvent | related_paths, project_id, host_id, session_id |
|
|
162
|
+
| DecisionSchema + confirmations[] | id, kind=decision, résultat normalisé dans status, jour, révision | short_label, text, outcome, tags, plan_id, verified_at, verify_cmd, auteurs, provenance et confirmations | related_paths, project_id, host_id, session_id |
|
|
163
|
+
| TrapSchema + confirmations[] | id, kind=trap, status, jour, révision | libellé, texte, sévérité, catégorie de plateforme, tags, dates, verify_cmd, provenance et confirmations | related_paths, project_id, host_id, session_id |
|
|
164
|
+
| HandoffSchema, contract, review, snapshot | id, kind=handoff, status, liens transformés en deps, jour, révision | from, to, texte, récit, tags, contrat, revue, snapshot.diff, snapshot.diff_digest, auteurs, modèle, provenance, correction | related_paths, project_id, host_id, session_id, toute clé/chemin caché dans contrat ou snapshot, visibility |
|
|
165
|
+
| PlanItemSchema + PlanStepSchema | plans et étapes ont chacun un id opaque, kind, status, priority, depends_on → deps, rang si séquencé, jour, révision | texte des plans/étapes, type, assignee, projet, tags, effort, heures précises | related_paths, identifiants locaux d'assignee/projet hors deps |
|
|
166
|
+
| SequenceSchema + SequenceItemSchema | id, kind=sequence, status, rank, hard_after/soft_after → deps, jour, révision | name, description, lane, scope_hint, rationale, owner, tags et distinction sémantique des arêtes | project_id, host_id, session_id, ids de plan/étape non traduits |
|
|
167
|
+
| ClaimSchema | id, kind=claim, status, lien plan seulement s'il devient deps, jour, révision | agent, user, scope, description, mode de handoff, dates, modèle, références d'assignation et base_sha | project_id, host_id, session_id, worktree_path, paths, toute information de checkout |
|
|
168
|
+
| CandidateSchema, CandidateUseSchema, CandidateContradictionSchema | id, kind=candidate, status, lien plan traduit, jour, révision | texte, type, auteurs, origin, tags, from/to, usages, contradictions, narration, motif de promotion/résolution, provenance, sévérité | project_id, host_id, session_id, related_paths, visibility |
|
|
169
|
+
| RuntimeNoteSchema | id, kind=runtime_note, statut normalisé, jour, révision | agent, texte, note type, tags, expiration, modèle, provenance | project_id, host_id, session_id, visibility |
|
|
170
|
+
| InboxMessageSchema | id, kind=inbox_message, statut, liens explicitement transformés, jour, révision | from, to, type, texte, ref, payload, scope, thread, acquittements, auteurs, tags et longueurs | project_id, host_id, session_id, claim_id/assignment_id non traduits ; aucun payload n'est promu au clair |
|
|
171
|
+
| AssignmentSchema + AssignmentArtifactSchema | id, kind=assignment, statut normalisé, liens explicitement traduits, jour, révision | description, scope, lane, raisons, artefacts, erreurs, retries, TTL, tags et tous les temps exacts | agent_id, session_id, worktree_path, project_id local, identifiants de message/claim non traduits |
|
|
172
|
+
| AgentRunSchema | id, kind=agent_run, statut, liens explicitement traduits, jour, révision | description, scope, raison, artefacts, erreurs, tags et temps exacts | agent_id, session_id, project_id, worktree_path, command, shell, pid, provider_run_id |
|
|
173
|
+
| ActionRequiredSchema + response | id, kind=action_required, statut, liens explicitement traduits, jour, révision | titre, prompt, options, response_schema, réponse, agent, tags et raison | agent_id, session_id, tout payload contenant une valeur interdite, références locales non traduites |
|
|
174
|
+
| AiSurfaceTaskRequestSchema | id, kind=ai_task, statut, jour, révision | titre, instructions, surface cible, outputs, note de résultat, tags, auteur et modèle | project_id, session_id, related_paths |
|
|
175
|
+
| RuntimeEventSchema + LaneResultSchema | id, kind=runtime_event ou lane_result, statut normalisé, jour, révision | texte, metadata, raisons, artefacts, corps, verdict, tags, corrélations et modèle | agent_id, project_id, host_id, session_id, scope, related_paths, transport/commande locale et tout chemin de worktree |
|
|
176
|
+
| ProvenanceSchema / ProvenancePassthroughSchema | aucun | objet entier : auteurs, sessions, source et diagnostics restent dans le blob | tout chemin, host, session, projet local ou secret qu'il contient demeure interdit par validation récursive |
|
|
177
|
+
| MemoryConfirmationEventSchema | aucun autonome | objet entier | session_id |
|
|
178
|
+
|
|
179
|
+
L'autre moitié de l'inventaire est entièrement **interdite de sortie**, sans projecteur v2 : StateSchema, ConfigSchema et ses sous-schémas (notamment CloudSyncConfigSchema, RemoteSyncSchema, sécurité et détection de secrets), ProjectIdentityDocumentSchema, AgentIdentityDocumentSchema, AgentIdentityKeySchema, AgentProfileSchema, AgentInvokeSchema, profils/bootstrap, snapshots de session, intégrations, liens de projets, outils/capabilities, configurations de réputation et tout schéma de clés ou de stockage sûr. Les clés publiques d'identité de vérification vivent seulement dans le roster d'appariement, protocole de contrôle distinct des documents locaux.
|
|
180
|
+
|
|
181
|
+
### 4.4 Mécanisme d'application décidé pour l'étape 5
|
|
182
|
+
|
|
183
|
+
L'implémentation v2 a une seule source de vérité de classification : un FederationClassificationManifestV1 versionné, qui associe chaque chemin de feuille de chaque schéma fédérable du tableau à exactement une des trois classes. Un chemin dynamique record, unknown ou union permissive est un leaf unique classé ; il ne peut produire aucune sous-clé publique.
|
|
184
|
+
|
|
185
|
+
Trois filets complémentaires l'appliquent :
|
|
186
|
+
|
|
187
|
+
1. Un builder nominal et brandé toPublicProjection(entity, sealed) choisit les champs de meta un à un. Aucun spread n'est admis, et le type brandé ne peut être construit hors du module de projection.
|
|
188
|
+
2. Tous les émetteurs passent par un point de sortie unique qui parse FederationEnvelopeV1 avec Zod .strict(). En mode confidentiel, une clé inconnue ou un plaintext non classé refuse l'export ; elle n'est jamais silencieusement supprimée.
|
|
189
|
+
3. La CI produit une fixture golden byte-exact par créateur et une vérification de complétude. Elle importe les schémas Zod source, énumère leurs feuilles après déroulage des wrappers et exige une entrée unique du manifest pour chacune. Ajouter une feuille fait échouer la CI jusqu'à sa classification. Des sentinelles dans chaque feuille scellée et interdite ne doivent apparaître dans aucun JSON de fil.
|
|
190
|
+
|
|
191
|
+
Les trois filets sont nécessaires : Zod dépouille les clés inconnues par défaut, un test de sortie ne prouve pas les N constructeurs, et un spread d'entité reste typable en TypeScript tout en sérialisant les clés présentes à l'exécution.
|
|
192
|
+
|
|
193
|
+
## 5. Clés de projet, appariement et récupération
|
|
194
|
+
|
|
195
|
+
### 5.1 Chiffrement de projet et epochs
|
|
196
|
+
|
|
197
|
+
Chaque projet possède une paire X25519 de chiffrement par epoch. La clé publique est distribuée aux émetteurs ; ils peuvent sceller mais ne peuvent pas lire. La partie privée de l'epoch est remise, sous enveloppes HPKE, aux seuls appareils lecteurs autorisés. Chaque appareil possède sa propre paire X25519 dans le stockage sûr. Sa clé privée est distincte de la clé d'identité Ed25519 de ~/.brainclaw/keys/ : aucune clé ne se dérive de l'autre.
|
|
198
|
+
|
|
199
|
+
Le paquet de clés référencé par wrap_hint est une liste auditable d'enveloppements epoch-private-key → device-x25519-public-key, avec identité Ed25519 attestante et approbation autorisante. Il ne donne au board ni nom humain ni clé privée. Un écrivain n'obtient que la clé publique de projet et son accès de signature : « écrire sans lire » est une propriété d'architecture.
|
|
200
|
+
|
|
201
|
+
Le stockage local conserve le lien workspace ↔ cloud_project_id, l'identité d'appareil, le trousseau Map<epoch, key>, la position de sync, l'outbox v2 et les états visibles. Aucun secret ne va dans la configuration en clair. Le plafond est explicite : ~/.brainclaw/keys/ est lisible par les processus du même UID ; la sécurité Cloud ne dépasse pas celle du disque local. TPM, enclave et HSM sont une v2 ultérieure.
|
|
202
|
+
|
|
203
|
+
### 5.2 Cérémonie d'appairage attestée
|
|
204
|
+
|
|
205
|
+
Le parcours nominal brainclaw cloud connect est :
|
|
206
|
+
|
|
207
|
+
1. L'utilisateur ouvre un code ou lien d'invitation et crée un enrollment pending pour un cloud_project_id opaque.
|
|
208
|
+
2. L'appareil génère sa paire X25519, puis signe une attestation avec sa clé d'identité Ed25519 déjà enregistrée. L'attestation lie projet, enrollment_id, challenge frais, clé publique X25519 et leurs empreintes.
|
|
209
|
+
3. Un membre habilité voit les empreintes Ed25519 **et** X25519, vérifie la preuve de possession du challenge, le rôle demandé et approuve/refuse. Le Cloud refuse une clé de chiffrement sans chaîne vers une identité enregistrée : aucun membre fantôme n'est ajouté au roster.
|
|
210
|
+
4. Après approbation, le client reçoit les credentials liés au workspace et les enveloppes de clés autorisées. Il effectue un premier pull en lecture seule, validé mais non matérialisé dans la mémoire.
|
|
211
|
+
5. status affiche rôle, epoch courant et état de sync. disconnect supprime l'autorisation locale et demande la révocation distante ; il ne prétend pas effacer les anciens blobs ou clés déjà lus.
|
|
212
|
+
|
|
213
|
+
Le parcours nominal ne demande jamais de copier API key, PEM, agent_id ou variable d'environnement. Une API key manuelle, si elle reste nécessaire pour compatibilité, est documentée hors de ce parcours et ne déclenche aucun sync par sa seule présence. Chaque phase conserve un état reprenable ; un enrollment interrompu est repris ou expiré proprement, jamais laissé orphelin.
|
|
214
|
+
|
|
215
|
+
Avant le premier sync, le client affiche le rôle, la table de classification et l'inventaire de reconstruction de §7. Ces textes sont dérivés de ce RFC, pas réécrits dans une seconde notice Cloud.
|
|
216
|
+
|
|
217
|
+
### 5.3 Perte d'appareil et révocation forward-only
|
|
218
|
+
|
|
219
|
+
Un projet ne peut émettre sa première enveloppe v2 qu'après l'enrôlement de deux appareils de récupération indépendamment attestés. Ils peuvent appartenir à une même personne, mais leurs clés privées ne partagent pas un même stockage. Cette règle fournit un chemin de remplacement : un porteur restant approuve la nouvelle clé, lui enveloppe les epochs historiques autorisés, puis révoque l'appareil perdu.
|
|
220
|
+
|
|
221
|
+
La révocation a deux couches :
|
|
222
|
+
|
|
223
|
+
- l'autorisation Cloud est coupée immédiatement ; le membre révoqué ne peut plus pousser, tirer ni faire approuver une clé ;
|
|
224
|
+
- le Cloud exige alors un key_epoch suivant. Dès qu'un porteur autorisé est en ligne, il crée l'epoch, publie un roster excluant le révoqué et les futures écritures utilisent ce nouvel epoch.
|
|
225
|
+
|
|
226
|
+
Si tous les porteurs autorisés sont hors ligne, la latence cryptographique est **non bornée** : le Cloud bloque les nouvelles écritures plutôt que d'accepter l'ancien epoch, jusqu'au retour d'un porteur. L'ancien membre lit encore ce qu'il avait déjà déchiffré ou reçu sous l'ancien epoch ; ce fait doit être testé et affiché. Si tous les appareils de récupération sont perdus, les données scellées passées sont irrécupérables par conception : on peut initier un nouvel epoch et projet logique, jamais prétendre qu'un reset Cloud les restaure.
|
|
227
|
+
|
|
228
|
+
« Rechiffrer le passé » signifie produire de nouvelles enveloppes depuis une copie locale autorisée, avec nouveaux IDs/révisions si nécessaire. Ce n'est ni une réécriture de ciphertext stocké par le Cloud ni une rotation rétroactive.
|
|
229
|
+
|
|
230
|
+
## 6. Réception : signature, anti-rejeu et matérialisation
|
|
231
|
+
|
|
232
|
+
Avant toute écriture via saveCandidate, saveRuntimeNote ou un autre chemin de mémoire, le lecteur :
|
|
233
|
+
|
|
234
|
+
1. parse strictement l'enveloppe et contrôle schéma, AAD et epoch ;
|
|
235
|
+
2. résout origin_sig.key_id dans le roster attesté, puis vérifie Ed25519 sur les octets canoniques complets ;
|
|
236
|
+
3. contrôle rôle, révocation et wrap_hint, puis déchiffre AEAD ;
|
|
237
|
+
4. vérifie que le plaintext correspond au type et au mapping local attendu ;
|
|
238
|
+
5. applique l'anti-rejeu : base_rev doit être strictement supérieur au high-water mark persistant de l'objet, ou être le même envelope déjà connu par idempotency_key ; une révision inférieure est refusée ;
|
|
239
|
+
6. dédoublonne par idempotency_key, append le résultat vérifié au journal local, puis seulement matérialise.
|
|
240
|
+
|
|
241
|
+
Signature absente, invalide ou d'une autre identité, modification de status/priority/deps, epoch révoqué et rollback sont tous des refus sans enregistrement local. Le Cloud peut encore retarder ou omettre une enveloppe, mais ne peut pas injecter ni faire accepter une ancienne révision comme actuelle. Le curseur de feed est conservé pour l'efficacité ; le high-water mark signé par objet est la barrière de sûreté.
|
|
242
|
+
|
|
243
|
+
## 7. Contrat du board aveugle et inventaire de reconstruction
|
|
244
|
+
|
|
245
|
+
Sans clé, le board rend seulement une carte générique : icône kind, placeholder fixe « contenu scellé », badge status, priorité/rang éventuels, arêtes vers UUID opaques, jour, base_rev, epoch et état de transport. Il ne rend aucun titre, nom, description, tag, lane, auteur, scope ou aperçu de ciphertext. L'absence de marqueur de sync est visible comme « état local inconnu / hors ligne », jamais masquée en « synchronisé ».
|
|
246
|
+
|
|
247
|
+
Le déchiffrement navigateur est entièrement différé après v1. Même un membre autorisé voit le même placeholder sur le board v1 ; il utilise son client local pour lire et rechercher le contenu.
|
|
248
|
+
|
|
249
|
+
Cette opacité n'est pas une promesse d'anonymat ou d'inférence nulle. Avec le seul squelette public, un adversaire peut reconstituer :
|
|
250
|
+
|
|
251
|
+
| Déduction possible | Signaux publics |
|
|
252
|
+
| --- | --- |
|
|
253
|
+
| Taille d'équipe | nombre de signataires/périphériques attestés, recipients de roster, rythme des opérations |
|
|
254
|
+
| Mix de fournisseurs | empreintes corrélables à des identités externes, classes/rythmes de clients et rôles connus du Cloud |
|
|
255
|
+
| Vélocité | jours, volumes, base_rev, retries et transitions |
|
|
256
|
+
| Forme de roadmap | kind, rank, deps, priorités et statuts |
|
|
257
|
+
| Chemin critique | graphe, ordres et états bloqués |
|
|
258
|
+
| Volume | nombre, taille et cadence des enveloppes |
|
|
259
|
+
| Arborescence/hiérarchie | dépendances, rôles d'approbation, groupes de cartes et positions relatives |
|
|
260
|
+
|
|
261
|
+
Les utilisateurs approuvent cet inventaire lors du pairing. Padding, obfuscation de graphe, anonymat des signataires et recherche protégée sont des chantiers ultérieurs explicites, pas des propriétés implicites de l'AEAD.
|
|
262
|
+
|
|
263
|
+
## 8. Critères de conformité inter-dépôts
|
|
264
|
+
|
|
265
|
+
Avant activation v2, core et Cloud partagent et passent les mêmes vecteurs d'encodage canonique, AAD, HPKE, signature, content_hash, idempotence et refus. Le pack de surface couvre au minimum :
|
|
266
|
+
|
|
267
|
+
- sentinelles dans chaque champ scellé/interdit de chaque créateur, absentes de tout JSON sortant ;
|
|
268
|
+
- Cloud hostile : signature ou metadata forgée, donc zéro écriture mémoire locale ;
|
|
269
|
+
- ciphertext ancien valide et réordonnancement de metadata : refus anti-rollback et intégrité ;
|
|
270
|
+
- invitation, approbation, preuve de possession, interruption/reprise et refus d'une clé de chiffrement non attestée ;
|
|
271
|
+
- deux machines, outbox hors ligne et reprise sans doublon ;
|
|
272
|
+
- révocation forward-only, en affirmant que l'ancien porteur lit le passé détenu mais jamais le futur epoch ;
|
|
273
|
+
- board sans clé : graphe et états rendus, aucun libellé en clair ni faux état synced.
|
|
274
|
+
|
|
275
|
+
La topologie de test contient deux hôtes simulés et un workspace multi-projets, et utilise isolateAgentEnv() plutôt qu'un nettoyage ad hoc de l'environnement. Les assertions portent sur le disque et les octets effectivement envoyés ou reçus, pas seulement sur des helpers internes.
|
package/docs/index.md
CHANGED
|
@@ -31,6 +31,7 @@ Use this page as the entry point into the packaged Markdown documentation.
|
|
|
31
31
|
- [concepts/loop-engine.md](concepts/loop-engine.md)
|
|
32
32
|
- [concepts/ideation-loop.md](concepts/ideation-loop.md) — memory-confrontation ideation loop (v1.5.0+)
|
|
33
33
|
- [concepts/mcp-governance.md](concepts/mcp-governance.md)
|
|
34
|
+
- [concepts/federation-v2-rfc.md](concepts/federation-v2-rfc.md) — contrat joint core + Cloud pour la fédération chiffrée v2
|
|
34
35
|
|
|
35
36
|
## Reference
|
|
36
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Shared project memory for humans and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"@eslint/js": "^10.0.1",
|
|
86
86
|
"@types/node": "^26.0.1",
|
|
87
87
|
"ajv": "^8.20.0",
|
|
88
|
-
"c8": "^
|
|
88
|
+
"c8": "^12.0.0",
|
|
89
89
|
"eslint": "^10.5.0",
|
|
90
90
|
"tree-sitter-wasms": "^0.1.13",
|
|
91
91
|
"typescript": "^6.0.3",
|