brainclaw 1.21.0 → 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/session-end.js +0 -102
- package/dist/commands/session-start.js +0 -23
- package/dist/core/claims.js +0 -18
- package/dist/core/coordination.js +1 -3
- 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/ids.js +5 -0
- package/dist/core/schema.js +0 -17
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/docs/cli.md +70 -39
- 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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fédération v2 — état local de connexion (pln#651 étape 3).
|
|
3
|
+
*
|
|
4
|
+
* Création propre, AUCUNE migration (dec#156) : ce module ne lit ni `cloud_sync` ni
|
|
5
|
+
* `BRAINCLAW_CLOUD_*`. Le chemin v1 a été démoli en étape 2, et le défaut vivant qu'il
|
|
6
|
+
* portait — la SEULE PRÉSENCE d'une variable d'environnement valant consentement au
|
|
7
|
+
* sync — ne doit pas se reconstituer ici. Le consentement est un fichier écrit par une
|
|
8
|
+
* cérémonie d'appairage explicite (étape 4), rien d'autre.
|
|
9
|
+
*
|
|
10
|
+
* ── OÙ VIT QUOI, ET POURQUOI ──────────────────────────────────────────────────
|
|
11
|
+
* `.brainclaw/coordination/federation/connection.json` (workspace, ce module)
|
|
12
|
+
* Le lien workspace ↔ cloud_project_id, l'identité d'appareil PUBLIQUE, les
|
|
13
|
+
* epochs connus, la position de sync, les états visibles. AUCUN SECRET.
|
|
14
|
+
*
|
|
15
|
+
* `~/.brainclaw/keys/` (federation-keyring.ts)
|
|
16
|
+
* Les clés privées. Hors du store, parce qu'un store de workspace se copie,
|
|
17
|
+
* se synchronise et — dans d'autres projets que celui-ci — se committe.
|
|
18
|
+
*
|
|
19
|
+
* La frontière est vérifiée par un test, pas seulement par cette phrase : le pack de
|
|
20
|
+
* l'étape 8 injecte une sentinelle et exige qu'aucun octet de clé privée n'atteigne
|
|
21
|
+
* `.brainclaw/`.
|
|
22
|
+
*
|
|
23
|
+
* ── LES TROIS ÉTATS SONT UNE EXIGENCE, PAS UN CONFORT (dec#154) ───────────────
|
|
24
|
+
* « Le Cloud est une projection et un relais ; le local est la source de vérité. » Une
|
|
25
|
+
* opération venue du Cloud se matérialise dans le journal local AVEC UN ÉTAT VISIBLE :
|
|
26
|
+
* pending / synced / conflict. Un état invisible transformerait le relais en autorité
|
|
27
|
+
* silencieuse — exactement ce que dec#154 refuse.
|
|
28
|
+
*/
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { memoryDir, writeFileAtomic } from './io.js';
|
|
32
|
+
import { generateId, nowISO } from './ids.js';
|
|
33
|
+
import { heldEpochs } from './federation-keyring.js';
|
|
34
|
+
import { counters as outboxCounters } from './federation-outbox-v2.js';
|
|
35
|
+
import { logger } from './logger.js';
|
|
36
|
+
const CONNECTION_FILE = 'connection.json';
|
|
37
|
+
export const FEDERATION_STATE_SCHEMA = 'brainclaw.federation-connection/v2';
|
|
38
|
+
/** Nombre d'appareils de récupération exigés avant la première enveloppe (RFC §5.3). */
|
|
39
|
+
export const REQUIRED_RECOVERY_DEVICES = 2;
|
|
40
|
+
// ── Emplacement ───────────────────────────────────────────────────────────────
|
|
41
|
+
export function connectionStatePath(cwd = process.cwd()) {
|
|
42
|
+
return path.join(memoryDir(cwd), 'coordination', 'federation', CONNECTION_FILE);
|
|
43
|
+
}
|
|
44
|
+
// ── Lecture ───────────────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Charge l'état de connexion, ou `undefined` si le workspace n'est pas appairé.
|
|
47
|
+
*
|
|
48
|
+
* FAIL-CLOSED SUR ÉTAT ILLISIBLE : un JSON corrompu renvoie `undefined` et journalise,
|
|
49
|
+
* il ne renvoie PAS un état par défaut. Un défaut fabriqué ferait croire à un appairage
|
|
50
|
+
* avec un `current_epoch` de 0, et l'appelant tenterait de sceller sous une clé
|
|
51
|
+
* inexistante. « Pas appairé » est une réponse sûre ; « appairé, epoch 0 » ne l'est pas.
|
|
52
|
+
*/
|
|
53
|
+
export function loadConnectionState(cwd = process.cwd()) {
|
|
54
|
+
const filepath = connectionStatePath(cwd);
|
|
55
|
+
if (!fs.existsSync(filepath))
|
|
56
|
+
return undefined;
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.warn(`État de connexion fédération illisible (${filepath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const state = raw;
|
|
66
|
+
if (state.schema !== FEDERATION_STATE_SCHEMA || !state.cloud_project_id || !state.device?.device_id) {
|
|
67
|
+
// Un schéma inconnu n'est PAS migré (dec#156) : la v1 est abandonnée, pas dépréciée.
|
|
68
|
+
logger.warn(`État de connexion fédération ignoré : schéma '${String(state.schema)}' non reconnu (attendu ${FEDERATION_STATE_SCHEMA}).`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return normalizeState(state, cwd);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Réconcilie l'état déclaré avec le DISQUE.
|
|
75
|
+
*
|
|
76
|
+
* `known_epochs` dit ce que l'appareil croit détenir ; `heldEpochs()` lit ce qu'il
|
|
77
|
+
* détient réellement. Le désaccord n'est pas théorique : une restauration partielle de
|
|
78
|
+
* sauvegarde, ou un `disconnect` interrompu, produit exactement cela. Faire confiance au
|
|
79
|
+
* JSON conduirait à tenter un déchiffrement sous une clé absente et à rendre l'erreur au
|
|
80
|
+
* mauvais endroit — loin de la cause.
|
|
81
|
+
*/
|
|
82
|
+
function normalizeState(state, cwd) {
|
|
83
|
+
const onDisk = heldEpochs(state.cloud_project_id);
|
|
84
|
+
const declared = state.keys?.known_epochs ?? [];
|
|
85
|
+
const missing = declared.filter((e) => !onDisk.includes(e));
|
|
86
|
+
if (missing.length > 0) {
|
|
87
|
+
logger.warn(`Trousseau incomplet pour ${state.cloud_project_id} : epoch(s) ${missing.join(', ')} déclaré(s) ` +
|
|
88
|
+
`mais absent(s) de ~/.brainclaw/keys/. Ces révisions ne sont pas déchiffrables sur cet appareil.`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
workspace_path: state.workspace_path ?? path.resolve(cwd),
|
|
93
|
+
peer_devices: state.peer_devices ?? [],
|
|
94
|
+
keys: {
|
|
95
|
+
current_epoch: state.keys?.current_epoch ?? 0,
|
|
96
|
+
// Le disque fait autorité sur ce qui est LISIBLE.
|
|
97
|
+
known_epochs: onDisk,
|
|
98
|
+
},
|
|
99
|
+
sync: {
|
|
100
|
+
feed_cursor: state.sync?.feed_cursor,
|
|
101
|
+
high_water: state.sync?.high_water ?? {},
|
|
102
|
+
last_pull_at: state.sync?.last_pull_at,
|
|
103
|
+
last_push_at: state.sync?.last_push_at,
|
|
104
|
+
},
|
|
105
|
+
counters: {
|
|
106
|
+
pending: state.counters?.pending ?? 0,
|
|
107
|
+
synced: state.counters?.synced ?? 0,
|
|
108
|
+
conflict: state.counters?.conflict ?? 0,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ── Écriture ──────────────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Persiste l'état de connexion de façon atomique.
|
|
115
|
+
*
|
|
116
|
+
* REFUSE D'ÉCRIRE UN SECRET : le contrôle ci-dessous n'est pas de la paranoïa décorative.
|
|
117
|
+
* `{...state, private_key}` compile, et une clé privée sérialisée dans un fichier du
|
|
118
|
+
* store serait ensuite copiée par tout ce qui copie un store. C'est le même raisonnement
|
|
119
|
+
* que les trois filets de l'étape 5 : le typage TypeScript est une BORNE INFÉRIEURE, pas
|
|
120
|
+
* une garantie d'exécution — chaque clé présente à l'exécution est sérialisée.
|
|
121
|
+
*/
|
|
122
|
+
export function saveConnectionState(state, cwd = process.cwd()) {
|
|
123
|
+
assertNoSecret(state);
|
|
124
|
+
const next = { ...state, updated_at: nowISO() };
|
|
125
|
+
const filepath = connectionStatePath(cwd);
|
|
126
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
127
|
+
writeFileAtomic(filepath, `${JSON.stringify(next, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
const SECRET_MARKERS = ['PRIVATE KEY', 'BEGIN OPENSSH', 'BEGIN RSA', 'BEGIN EC PARAMETERS'];
|
|
130
|
+
/**
|
|
131
|
+
* Refuse tout état contenant du matériel de clé privée, quel que soit le NOM du champ.
|
|
132
|
+
*
|
|
133
|
+
* Le contrôle porte sur le CONTENU sérialisé et non sur une liste de champs interdits :
|
|
134
|
+
* une liste de noms ne rattrape pas un champ ajouté demain, alors qu'un PEM privé porte
|
|
135
|
+
* toujours son en-tête. C'est le même choix que le filet 2 de l'étape 5 — fail-closed sur
|
|
136
|
+
* ce qui sort, pas allowlist sur ce qu'on a pensé à interdire.
|
|
137
|
+
*/
|
|
138
|
+
function assertNoSecret(state) {
|
|
139
|
+
const serialized = JSON.stringify(state);
|
|
140
|
+
for (const marker of SECRET_MARKERS) {
|
|
141
|
+
if (serialized.includes(marker)) {
|
|
142
|
+
throw new Error(`Refus d'écrire l'état de connexion : du matériel de clé privée ('${marker}') s'y trouve. ` +
|
|
143
|
+
`Les secrets vont dans ~/.brainclaw/keys/ via federation-keyring.ts, jamais dans le store de workspace.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Crée l'état de connexion initial d'un workspace fraîchement appairé.
|
|
149
|
+
*
|
|
150
|
+
* `stage: 'pending'` et non `'active'` : créer l'état ne vaut pas approbation. Le passage
|
|
151
|
+
* à `active` appartient à la cérémonie de l'étape 4, après preuve de possession ET
|
|
152
|
+
* approbation humaine. Un état créé optimiste rejouerait le défaut de la v1 — un artefact
|
|
153
|
+
* local suffisant à déclencher du sync.
|
|
154
|
+
*/
|
|
155
|
+
export function createConnectionState(params) {
|
|
156
|
+
const now = nowISO();
|
|
157
|
+
return {
|
|
158
|
+
schema: FEDERATION_STATE_SCHEMA,
|
|
159
|
+
cloud_project_id: params.cloudProjectId,
|
|
160
|
+
workspace_path: path.resolve(params.workspacePath ?? process.cwd()),
|
|
161
|
+
enrollment: { stage: 'pending', enrollment_id: params.enrollmentId, updated_at: now },
|
|
162
|
+
device: params.device,
|
|
163
|
+
peer_devices: [],
|
|
164
|
+
// 0 = « aucun epoch » et non « premier epoch ». Le premier epoch remis est le 1 ;
|
|
165
|
+
// sceller sous l'epoch 0 doit être impossible, pas silencieusement plausible.
|
|
166
|
+
keys: { current_epoch: 0, known_epochs: [] },
|
|
167
|
+
sync: { high_water: {} },
|
|
168
|
+
counters: { pending: 0, synced: 0, conflict: 0 },
|
|
169
|
+
created_at: now,
|
|
170
|
+
updated_at: now,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Identifiant d'appareil, indépendant de l'identité d'agent (RFC §5.1).
|
|
175
|
+
*
|
|
176
|
+
* Un agent peut tourner sur plusieurs machines et une machine porter plusieurs agents ;
|
|
177
|
+
* c'est l'APPAREIL qui détient une clé de déchiffrement et qu'on révoque en cas de perte.
|
|
178
|
+
* Réutiliser l'agent_id ferait qu'une révocation coupe l'agent partout à la fois.
|
|
179
|
+
*/
|
|
180
|
+
export function newDeviceId() {
|
|
181
|
+
return generateId('federation_devices');
|
|
182
|
+
}
|
|
183
|
+
// ── Anti-rejeu ────────────────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* Décide si une révision entrante est acceptable pour un objet donné.
|
|
186
|
+
*
|
|
187
|
+
* STRICTEMENT SUPÉRIEURE (RFC §6.5) : l'égalité est refusée ici, et le dédoublonnage
|
|
188
|
+
* d'une enveloppe déjà connue se fait par `idempotency_key` à l'étape 6 — deux
|
|
189
|
+
* mécanismes distincts pour deux questions distinctes. Confondre les deux ferait
|
|
190
|
+
* accepter un rejeu de même révision porteur d'un contenu différent.
|
|
191
|
+
*/
|
|
192
|
+
export function acceptsRevision(state, objectId, incomingRev) {
|
|
193
|
+
const seen = state.sync.high_water[objectId];
|
|
194
|
+
return seen === undefined || incomingRev > seen;
|
|
195
|
+
}
|
|
196
|
+
/** Avance la barrière anti-rejeu. Ne régresse JAMAIS, même si l'appelant le demande. */
|
|
197
|
+
export function recordRevision(state, objectId, rev) {
|
|
198
|
+
const seen = state.sync.high_water[objectId];
|
|
199
|
+
if (seen !== undefined && rev <= seen)
|
|
200
|
+
return state;
|
|
201
|
+
return { ...state, sync: { ...state.sync, high_water: { ...state.sync.high_water, [objectId]: rev } } };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Un projet ne peut émettre sa PREMIÈRE enveloppe v2 qu'après l'enrôlement de deux
|
|
205
|
+
* appareils de récupération indépendamment attestés (RFC §5.3).
|
|
206
|
+
*
|
|
207
|
+
* POURQUOI CETTE PORTE EXISTE ICI, dans l'état et non dans la commande : un enrôlement
|
|
208
|
+
* par appareil sans scénario de perte produit des workspaces DÉFINITIVEMENT illisibles.
|
|
209
|
+
* Si tous les porteurs sont perdus, le passé scellé est irrécupérable par construction —
|
|
210
|
+
* aucun reset côté Cloud ne le restaure. La porte est donc au plus près de la donnée qui
|
|
211
|
+
* la conditionne, pour qu'un second appelant ne puisse pas l'oublier.
|
|
212
|
+
*
|
|
213
|
+
* Les appareils révoqués ne comptent pas : deux porteurs dont un révoqué n'offrent aucun
|
|
214
|
+
* chemin de remplacement.
|
|
215
|
+
*/
|
|
216
|
+
export function recoveryReadiness(state) {
|
|
217
|
+
const all = [state.device, ...state.peer_devices];
|
|
218
|
+
const enrolled = all.filter((d) => d.recovery && !d.revoked_at).length;
|
|
219
|
+
if (enrolled >= REQUIRED_RECOVERY_DEVICES) {
|
|
220
|
+
return { ready: true, enrolled, required: REQUIRED_RECOVERY_DEVICES };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
ready: false,
|
|
224
|
+
enrolled,
|
|
225
|
+
required: REQUIRED_RECOVERY_DEVICES,
|
|
226
|
+
reason: `${enrolled}/${REQUIRED_RECOVERY_DEVICES} appareil(s) de récupération attesté(s). ` +
|
|
227
|
+
`Sans un second porteur, la perte de cet appareil rendrait le passé scellé irrécupérable — ` +
|
|
228
|
+
`aucune restauration côté cloud ne le ramènerait.`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Ce que `brainclaw cloud status` rend — le critère de sortie « les trois états de sync
|
|
233
|
+
* sont observables par une commande ».
|
|
234
|
+
*
|
|
235
|
+
* DEUX SOURCES, ET LES DEUX SONT LE DISQUE, PAS LA DÉCLARATION :
|
|
236
|
+
* `readable_epochs` vient de `heldEpochs()` via la réconciliation du chargement ;
|
|
237
|
+
* `sync` vient du comptage réel de l'outbox, pas des compteurs de `connection.json`.
|
|
238
|
+
*
|
|
239
|
+
* Les compteurs persistés restent un cache d'affichage bon marché pour les appelants qui
|
|
240
|
+
* n'ont pas besoin d'exactitude. Un STATUT, lui, est consulté précisément quand on doute :
|
|
241
|
+
* s'il relisait un compteur que le code a lui-même incrémenté, il n'observerait rien et
|
|
242
|
+
* rassurerait à tort au pire moment.
|
|
243
|
+
*/
|
|
244
|
+
export function summarizeConnection(cwd = process.cwd()) {
|
|
245
|
+
const state = loadConnectionState(cwd);
|
|
246
|
+
const sync = outboxCounters(cwd);
|
|
247
|
+
if (!state) {
|
|
248
|
+
return {
|
|
249
|
+
connected: false,
|
|
250
|
+
stage: 'unpaired',
|
|
251
|
+
current_epoch: 0,
|
|
252
|
+
readable_epochs: [],
|
|
253
|
+
sync,
|
|
254
|
+
recovery: { ready: false, enrolled: 0, required: REQUIRED_RECOVERY_DEVICES },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
connected: state.enrollment.stage === 'active',
|
|
259
|
+
cloud_project_id: state.cloud_project_id,
|
|
260
|
+
stage: state.enrollment.stage,
|
|
261
|
+
role: state.enrollment.role,
|
|
262
|
+
current_epoch: state.keys.current_epoch,
|
|
263
|
+
readable_epochs: state.keys.known_epochs,
|
|
264
|
+
device_fingerprint: state.device.x25519_fingerprint,
|
|
265
|
+
sync,
|
|
266
|
+
last_pull_at: state.sync.last_pull_at,
|
|
267
|
+
recovery: recoveryReadiness(state),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=federation-state.js.map
|
package/dist/core/ids.js
CHANGED
|
@@ -24,6 +24,11 @@ const PREFIXES = {
|
|
|
24
24
|
// prefix-based routing (dispatch_status). Canonical prefix is 'rtn'.
|
|
25
25
|
runtime_note: 'rtn',
|
|
26
26
|
runtime_notes: 'rtn',
|
|
27
|
+
// pln#651 étape 3 — identité d'APPAREIL de la fédération v2, distincte de l'identité
|
|
28
|
+
// d'agent. Entrée EXPLICITE et non laissée au fallback slice(0,3) : c'est ce fallback
|
|
29
|
+
// qui a produit la collision de can_b8d53d18 ('runtime_note' → 'run', déjà pris par
|
|
30
|
+
// agent_run). Il rend 'dev' ici par hasard, pas par contrat.
|
|
31
|
+
federation_devices: 'dev',
|
|
27
32
|
};
|
|
28
33
|
const ID_COUNTER_FILE = '.id-counter.json';
|
|
29
34
|
function counterPath(cwd, preferredDirName = '.brainclaw') {
|
package/dist/core/schema.js
CHANGED
|
@@ -1175,22 +1175,6 @@ export const RemoteSyncSchema = z.object({
|
|
|
1175
1175
|
ssh_key_path: z.string().optional(),
|
|
1176
1176
|
sync_strategy: z.enum(['pull-only', 'push-pull', 'pr-based']).default('push-pull'),
|
|
1177
1177
|
});
|
|
1178
|
-
export const CloudSyncConfigSchema = z.object({
|
|
1179
|
-
enabled: z.boolean().default(false),
|
|
1180
|
-
endpoint: z.string().default('https://app.brainclaw.dev'),
|
|
1181
|
-
api_key: z.string().optional(),
|
|
1182
|
-
/** Remote project this bridge federates into (scopes signed runtime writes). */
|
|
1183
|
-
project_id: z.string().optional(),
|
|
1184
|
-
/** Approved remote agent identity used to sign runtime writes (pln#100). */
|
|
1185
|
-
agent_id: z.string().optional(),
|
|
1186
|
-
agent_name: z.string().optional(),
|
|
1187
|
-
/**
|
|
1188
|
-
* Fail-closed toggle: when true, the bridge refuses to push a runtime write
|
|
1189
|
-
* unless it can sign it with an approved agent's Ed25519 key. Absent/false
|
|
1190
|
-
* keeps existing API-key-only setups working (signing is additive).
|
|
1191
|
-
*/
|
|
1192
|
-
require_signed: z.boolean().optional(),
|
|
1193
|
-
});
|
|
1194
1178
|
export const SessionSnapshotSchema = z.object({
|
|
1195
1179
|
schema_version: z.number().int().positive().optional(),
|
|
1196
1180
|
session_id: z.string(),
|
|
@@ -1520,7 +1504,6 @@ export const ConfigSchema = z.object({
|
|
|
1520
1504
|
target_audience: z.enum(['human', 'agent']).optional().default('human'),
|
|
1521
1505
|
openclaw_bridge: z.boolean().optional().default(false),
|
|
1522
1506
|
remote_sync: RemoteSyncSchema.optional(),
|
|
1523
|
-
cloud_sync: CloudSyncConfigSchema.optional(),
|
|
1524
1507
|
telemetry: z.literal(false),
|
|
1525
1508
|
allow_network: z.literal(false),
|
|
1526
1509
|
redaction: RedactionConfigSchema,
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.
|
|
2
|
+
// Source: brainclaw v1.22.0 on 2026-08-08T16:03:19.158Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.22.0",
|
|
5
|
+
"generated_at": "2026-08-08T16:03:19.158Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 67,
|
|
8
8
|
"published_count": 65,
|
|
@@ -474,7 +474,7 @@ export const FACTS = {
|
|
|
474
474
|
},
|
|
475
475
|
"bench": {
|
|
476
476
|
"schema": "brainclaw.bench.v1",
|
|
477
|
-
"generated_at": "2026-08-
|
|
477
|
+
"generated_at": "2026-08-08T16:03:17.056Z",
|
|
478
478
|
"node_version": "v24.18.0",
|
|
479
479
|
"platform": "linux-x64",
|
|
480
480
|
"repeats": 3,
|
|
@@ -483,7 +483,7 @@ export const FACTS = {
|
|
|
483
483
|
"name": "cold_onboard",
|
|
484
484
|
"volume": "empty",
|
|
485
485
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
486
|
-
"duration_ms_median":
|
|
486
|
+
"duration_ms_median": 73,
|
|
487
487
|
"payload_chars_median": 1640,
|
|
488
488
|
"payload_tokens_est_median": 410
|
|
489
489
|
},
|
|
@@ -491,15 +491,15 @@ export const FACTS = {
|
|
|
491
491
|
"name": "warm_work",
|
|
492
492
|
"volume": "medium",
|
|
493
493
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
494
|
-
"duration_ms_median":
|
|
495
|
-
"payload_chars_median":
|
|
496
|
-
"payload_tokens_est_median":
|
|
494
|
+
"duration_ms_median": 128,
|
|
495
|
+
"payload_chars_median": 2626,
|
|
496
|
+
"payload_tokens_est_median": 657
|
|
497
497
|
},
|
|
498
498
|
{
|
|
499
499
|
"name": "first_edit",
|
|
500
500
|
"volume": "medium",
|
|
501
501
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
502
|
-
"duration_ms_median":
|
|
502
|
+
"duration_ms_median": 14,
|
|
503
503
|
"payload_chars_median": 499,
|
|
504
504
|
"payload_tokens_est_median": 125
|
|
505
505
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.22.0",
|
|
3
|
+
"generated_at": "2026-08-08T16:03:19.158Z",
|
|
4
4
|
"tools": {
|
|
5
5
|
"count": 67,
|
|
6
6
|
"published_count": 65,
|
|
@@ -472,7 +472,7 @@
|
|
|
472
472
|
},
|
|
473
473
|
"bench": {
|
|
474
474
|
"schema": "brainclaw.bench.v1",
|
|
475
|
-
"generated_at": "2026-08-
|
|
475
|
+
"generated_at": "2026-08-08T16:03:17.056Z",
|
|
476
476
|
"node_version": "v24.18.0",
|
|
477
477
|
"platform": "linux-x64",
|
|
478
478
|
"repeats": 3,
|
|
@@ -481,7 +481,7 @@
|
|
|
481
481
|
"name": "cold_onboard",
|
|
482
482
|
"volume": "empty",
|
|
483
483
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
484
|
-
"duration_ms_median":
|
|
484
|
+
"duration_ms_median": 73,
|
|
485
485
|
"payload_chars_median": 1640,
|
|
486
486
|
"payload_tokens_est_median": 410
|
|
487
487
|
},
|
|
@@ -489,15 +489,15 @@
|
|
|
489
489
|
"name": "warm_work",
|
|
490
490
|
"volume": "medium",
|
|
491
491
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
492
|
-
"duration_ms_median":
|
|
493
|
-
"payload_chars_median":
|
|
494
|
-
"payload_tokens_est_median":
|
|
492
|
+
"duration_ms_median": 128,
|
|
493
|
+
"payload_chars_median": 2626,
|
|
494
|
+
"payload_tokens_est_median": 657
|
|
495
495
|
},
|
|
496
496
|
{
|
|
497
497
|
"name": "first_edit",
|
|
498
498
|
"volume": "medium",
|
|
499
499
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
500
|
-
"duration_ms_median":
|
|
500
|
+
"duration_ms_median": 14,
|
|
501
501
|
"payload_chars_median": 499,
|
|
502
502
|
"payload_tokens_est_median": 125
|
|
503
503
|
}
|
package/docs/cli.md
CHANGED
|
@@ -1749,46 +1749,9 @@ brainclaw push --remote origin --message "chore: push memory state" --json
|
|
|
1749
1749
|
|
|
1750
1750
|
## Federation
|
|
1751
1751
|
|
|
1752
|
-
The `federation` command group
|
|
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`).
|
|
1753
1753
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
Push a test signal to the cloud. The signal is sent from the current project and agent to a target project or broadcast address.
|
|
1757
|
-
|
|
1758
|
-
| Option | Description |
|
|
1759
|
-
|---|---|
|
|
1760
|
-
| `--type <type>` | Signal type (default: `runtime_note`). Accepted values: `signal`, `handoff`, `candidate`, `runtime_note`, `board_snapshot` |
|
|
1761
|
-
| `--to-project <project>` | Target project name (default: `broadcast`) |
|
|
1762
|
-
| `--to-agent <agent>` | Target agent name |
|
|
1763
|
-
|
|
1764
|
-
```bash
|
|
1765
|
-
brainclaw federation push "Auth rollout complete" --to-project lodestar
|
|
1766
|
-
brainclaw federation push "Blocked on payments" --type runtime_note --to-agent copilot
|
|
1767
|
-
```
|
|
1768
|
-
|
|
1769
|
-
### `brainclaw federation pull`
|
|
1770
|
-
|
|
1771
|
-
Pull signals from the cloud inbox for the current agent.
|
|
1772
|
-
|
|
1773
|
-
| Option | Description |
|
|
1774
|
-
|---|---|
|
|
1775
|
-
| `--agent <name>` | Agent name to pull for (default: auto-detected) |
|
|
1776
|
-
| `--since <date>` | Only pull signals after this ISO date |
|
|
1777
|
-
| `--limit <n>` | Maximum number of signals to pull (default: 20) |
|
|
1778
|
-
|
|
1779
|
-
```bash
|
|
1780
|
-
brainclaw federation pull
|
|
1781
|
-
brainclaw federation pull --since 2026-04-01
|
|
1782
|
-
brainclaw federation pull --agent copilot --limit 50
|
|
1783
|
-
```
|
|
1784
|
-
|
|
1785
|
-
### `brainclaw federation status`
|
|
1786
|
-
|
|
1787
|
-
Check cloud federation configuration: shows the configured cloud URL, whether the API key is set, and pings the cloud health endpoint if configured.
|
|
1788
|
-
|
|
1789
|
-
```bash
|
|
1790
|
-
brainclaw federation status
|
|
1791
|
-
```
|
|
1754
|
+
Local cross-project federation (see below) is unaffected.
|
|
1792
1755
|
|
|
1793
1756
|
---
|
|
1794
1757
|
|
|
@@ -2244,4 +2207,72 @@ Show brainclaw context volume stats — tokens injected per agent and per MCP to
|
|
|
2244
2207
|
|---|---|
|
|
2245
2208
|
| `--json` | Output as JSON |
|
|
2246
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
|
+
|
|
2247
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`.
|