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.
Files changed (51) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli/register-cloud.js +63 -0
  3. package/dist/cli.js +2 -3
  4. package/dist/commands/cloud.js +198 -0
  5. package/dist/commands/export.js +3 -3
  6. package/dist/commands/init.js +11 -0
  7. package/dist/commands/mcp-write-claims.js +162 -46
  8. package/dist/commands/mcp-write-entities.js +67 -0
  9. package/dist/commands/mcp.js +64 -1
  10. package/dist/commands/session-end.js +0 -102
  11. package/dist/commands/session-start.js +0 -23
  12. package/dist/commands/switch.js +41 -12
  13. package/dist/core/actions.js +25 -1
  14. package/dist/core/agent-files.js +19 -0
  15. package/dist/core/agentruns.js +68 -10
  16. package/dist/core/assignments.js +94 -19
  17. package/dist/core/claims.js +13 -24
  18. package/dist/core/config.js +58 -0
  19. package/dist/core/context-diff.js +28 -11
  20. package/dist/core/coordination.js +1 -3
  21. package/dist/core/entity-locator.js +404 -0
  22. package/dist/core/federation-attestation.js +96 -0
  23. package/dist/core/federation-canonical.js +95 -0
  24. package/dist/core/federation-hpke.js +213 -0
  25. package/dist/core/federation-inbound.js +187 -0
  26. package/dist/core/federation-keyring.js +241 -0
  27. package/dist/core/federation-message.js +5 -5
  28. package/dist/core/federation-outbox-v2.js +125 -0
  29. package/dist/core/federation-pairing.js +213 -0
  30. package/dist/core/federation-projection.js +336 -0
  31. package/dist/core/federation-relay.js +223 -0
  32. package/dist/core/federation-state.js +270 -0
  33. package/dist/core/identity.js +9 -1
  34. package/dist/core/ids.js +5 -0
  35. package/dist/core/io.js +39 -1
  36. package/dist/core/operations/relocate.js +40 -10
  37. package/dist/core/schema.js +24 -17
  38. package/dist/core/sequence.js +47 -6
  39. package/dist/core/store-resolution.js +99 -26
  40. package/dist/core/workspace-projects.js +23 -2
  41. package/dist/core/worktree.js +59 -1
  42. package/dist/facts.js +7 -7
  43. package/dist/facts.json +6 -6
  44. package/docs/cli.md +73 -40
  45. package/docs/concepts/federation-v2-rfc.md +275 -0
  46. package/docs/index.md +1 -0
  47. package/package.json +2 -2
  48. package/dist/cli/register-federation.js +0 -258
  49. package/dist/core/federation-cloud.js +0 -245
  50. package/dist/core/federation-outbox.js +0 -292
  51. package/dist/core/federation-signing.js +0 -115
Binary file
@@ -0,0 +1,63 @@
1
+ import { runCloudStatus, runCloudConnect, runCloudAwait, runCloudDisconnect, } from '../commands/cloud.js';
2
+ /** Adresse du cloud. Fournie par `--url`, sans quoi la commande demande de la préciser. */
3
+ const DEFAULT_URL_HINT = 'https://<votre-déploiement>.workers.dev';
4
+ function fail(message) {
5
+ console.error(message);
6
+ process.exit(1);
7
+ }
8
+ /**
9
+ * Fédération v2 (pln#651 étapes 3 et 4).
10
+ *
11
+ * `connect` est une CÉRÉMONIE DE CLÉS, pas une écriture de configuration — voir
12
+ * src/core/federation-pairing.ts. L'humain ne manipule qu'un code d'invitation et compare
13
+ * deux empreintes ; aucune clé d'API, aucun PEM, aucune variable d'environnement (dec#8).
14
+ */
15
+ export function registerCloudCommands(program) {
16
+ const cloud = program
17
+ .command('cloud')
18
+ .description('Fédération cloud v2 : appairage attesté, état de connexion, epochs de clés');
19
+ cloud
20
+ .command('status')
21
+ .description("Affiche le projet lié, le rôle, l'epoch de clé courant et les trois états de sync")
22
+ .option('--json', 'Sortie JSON')
23
+ .action((options) => {
24
+ runCloudStatus({ json: options.json });
25
+ });
26
+ cloud
27
+ .command('connect <invite-code>')
28
+ .description("Rejoint un projet cloud : réclame l'invitation, prouve la possession de l'identité et atteste la clé de chiffrement de cet appareil")
29
+ .requiredOption('--url <url>', `Adresse du déploiement cloud (ex. ${DEFAULT_URL_HINT})`)
30
+ .requiredOption('--agent <id>', "Identifiant d'agent à enrôler (opaque, 4 à 64 caractères)")
31
+ .option('--json', 'Sortie JSON')
32
+ .action(async (inviteCode, options) => {
33
+ await runCloudConnect({
34
+ inviteCode,
35
+ url: options.url,
36
+ agentId: options.agent,
37
+ json: options.json,
38
+ }).catch((err) => fail(`Erreur : ${err instanceof Error ? err.message : String(err)}`));
39
+ });
40
+ cloud
41
+ .command('await')
42
+ .description("Constate l'approbation humaine et active l'appairage local")
43
+ // Commande DISTINCTE de connect : l'approbation dépend d'un humain dont le délai
44
+ // n'est pas borné. Bloquer indéfiniment sur un tiers ferait un mauvais citoyen dans
45
+ // un script, et une interruption doit rester reprenable.
46
+ .requiredOption('--url <url>', 'Adresse du déploiement cloud')
47
+ .action(async (options) => {
48
+ await runCloudAwait({ url: options.url })
49
+ .catch((err) => fail(`Erreur : ${err instanceof Error ? err.message : String(err)}`));
50
+ });
51
+ cloud
52
+ .command('disconnect')
53
+ .description("Retire l'autorisation locale et demande la révocation distante")
54
+ .requiredOption('--url <url>', 'Adresse du déploiement cloud')
55
+ // Effacer le trousseau rend DÉFINITIVEMENT illisible tout ce qui a été scellé sous
56
+ // ces epochs. Ce n'est donc pas le défaut : on le demande explicitement.
57
+ .option('--forget-keys', "Efface aussi les clés d'epoch de ce projet (le passé scellé devient illisible ici)")
58
+ .action(async (options) => {
59
+ await runCloudDisconnect({ url: options.url, forgetKeys: options.forgetKeys })
60
+ .catch((err) => fail(`Erreur : ${err instanceof Error ? err.message : String(err)}`));
61
+ });
62
+ }
63
+ //# sourceMappingURL=register-cloud.js.map
package/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import { registerPlanningCommands } from './cli/register-planning.js';
13
13
  import { registerCoordinationCommands } from './cli/register-coordination.js';
14
14
  import { registerReviewCommands } from './cli/register-review.js';
15
15
  import { registerMemoryContextCommands } from './cli/register-memory-context.js';
16
- import { registerFederationCommands } from './cli/register-federation.js';
17
16
  import { registerCodeMapCommands } from './cli/register-code-map.js';
17
+ import { registerCloudCommands } from './cli/register-cloud.js';
18
18
  const program = new Command();
19
19
  function parseLeadingGlobalOptions(argv) {
20
20
  const result = {};
@@ -171,8 +171,8 @@ registerPlanningCommands(program);
171
171
  registerCoordinationCommands(program);
172
172
  registerReviewCommands(program);
173
173
  registerMemoryContextCommands(program);
174
- registerFederationCommands(program);
175
174
  registerCodeMapCommands(program);
175
+ registerCloudCommands(program);
176
176
  // ─── Command-order shim (pln#622 PR5) ────────────────────────────────────────
177
177
  // Commander renders `--help` in registration order. The per-family register
178
178
  // modules above would otherwise reorder the help output relative to the
@@ -290,7 +290,6 @@ const ORIGINAL_COMMAND_ORDER = [
290
290
  'switch',
291
291
  'who',
292
292
  'worktree',
293
- 'federation',
294
293
  // Gated by BRAINCLAW_ENABLE_CODEV: listed here so the order is right in both
295
294
  // modes — when not registered they are simply absent from the live array and
296
295
  // the sort skips them naturally.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `brainclaw cloud connect / status / disconnect` — fédération v2 (pln#651 étapes 3 et 4).
3
+ *
4
+ * `connect` n'est pas une écriture de configuration : c'est une CÉRÉMONIE DE CLÉS. Elle
5
+ * réclame une invitation, prouve la possession de l'identité Ed25519, atteste la clé de
6
+ * chiffrement X25519 de l'appareil, puis attend une approbation humaine qui compare des
7
+ * empreintes. Ce qui la rend indissociable de la distribution des clés est écrit dans
8
+ * src/core/federation-pairing.ts.
9
+ *
10
+ * L'HUMAIN NE COPIE QU'UN CODE D'INVITATION, et compare deux empreintes. Aucune clé
11
+ * d'API, aucun PEM, aucun agent_id, aucune variable d'environnement (dec#8).
12
+ *
13
+ * dec#154 exige que l'état de sync soit VISIBLE : un état pending/synced/conflict que seul
14
+ * le code consulte transformerait le relais cloud en autorité silencieuse. C'est ce que
15
+ * `status` rend.
16
+ */
17
+ import { summarizeConnection, loadConnectionState, saveConnectionState } from '../core/federation-state.js';
18
+ import { forgetProjectEpochs } from '../core/federation-keyring.js';
19
+ import { beginPairing, checkPairingApproval, completePairing, requestRevocation, PairingError, } from '../core/federation-pairing.js';
20
+ import { resolveEffectiveCwd } from '../core/store-resolution.js';
21
+ import { nowISO } from '../core/ids.js';
22
+ export function runCloudStatus(options = {}) {
23
+ const cwd = options.cwd ?? resolveEffectiveCwd();
24
+ const summary = summarizeConnection(cwd);
25
+ if (options.json) {
26
+ console.log(JSON.stringify(summary, null, 2));
27
+ return;
28
+ }
29
+ if (summary.stage === 'unpaired') {
30
+ console.log('Cloud : non appairé.');
31
+ console.log(" Aucun sync n'a lieu tant qu'aucun appairage n'a été fait explicitement.");
32
+ // Énoncé volontairement, parce que c'est le défaut que la v2 ferme : en v1 la seule
33
+ // présence de BRAINCLAW_CLOUD_API_KEY valait consentement, et jusqu'à 100 signaux
34
+ // étaient tirés puis écrits dans le store local à chaque démarrage de session.
35
+ return;
36
+ }
37
+ console.log(`Cloud : ${summary.stage}${summary.connected ? '' : ' (pas encore actif)'}`);
38
+ console.log(` Projet : ${summary.cloud_project_id ?? '—'}`);
39
+ console.log(` Rôle : ${summary.role ?? '—'}`);
40
+ console.log(` Appareil : ${summary.device_fingerprint?.slice(0, 16) ?? '—'}…`);
41
+ console.log(` Epoch : ${summary.current_epoch} (lisibles : ${summary.readable_epochs.join(', ') || 'aucun'})`);
42
+ console.log(` Sync : ${summary.sync.pending} en attente · ${summary.sync.synced} synchronisé(s) · ${summary.sync.conflict} conflit(s)`);
43
+ if (summary.last_pull_at)
44
+ console.log(` Dernier pull : ${summary.last_pull_at}`);
45
+ if (!summary.recovery.ready) {
46
+ console.log('');
47
+ console.log(` ⚠ Récupération : ${summary.recovery.reason}`);
48
+ }
49
+ if (summary.sync.conflict > 0) {
50
+ console.log('');
51
+ // Un conflit ne se résout JAMAIS par last-write-wins silencieux (dec#154) : il est
52
+ // présenté, avec une proposition, et attend une décision.
53
+ console.log(` ${summary.sync.conflict} opération(s) en conflit attendent une résolution explicite.`);
54
+ }
55
+ }
56
+ // ── Transport HTTP ────────────────────────────────────────────────────────────
57
+ /**
58
+ * Transport réel. Injecté partout ailleurs pour que la cérémonie soit exerçable sans
59
+ * réseau — les tests font tourner un cloud simulé qui VÉRIFIE réellement les signatures,
60
+ * ce qu'un serveur acceptant tout ne prouverait pas.
61
+ */
62
+ export function httpTransport(baseUrl) {
63
+ const url = (p) => `${baseUrl.replace(/\/+$/, '')}${p}`;
64
+ const call = async (method, path, body) => {
65
+ const res = await fetch(url(path), {
66
+ method,
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: body === undefined ? undefined : JSON.stringify(body),
69
+ });
70
+ // Une réponse non-JSON (page d'erreur de proxy, 502 HTML) ne doit pas lever une
71
+ // exception de parsing qui masquerait le vrai statut HTTP — c'est lui qui porte
72
+ // l'information utile.
73
+ let parsed = {};
74
+ try {
75
+ parsed = (await res.json());
76
+ }
77
+ catch { /* corps non-JSON */ }
78
+ return { status: res.status, body: parsed };
79
+ };
80
+ return {
81
+ post: (path, body) => call('POST', path, body),
82
+ get: (path) => call('GET', path),
83
+ };
84
+ }
85
+ export async function runCloudConnect(options) {
86
+ const cwd = options.cwd ?? resolveEffectiveCwd();
87
+ const transport = options.transport ?? httpTransport(options.url);
88
+ let handle;
89
+ try {
90
+ handle = await beginPairing({
91
+ inviteCode: options.inviteCode,
92
+ agentId: options.agentId,
93
+ transport,
94
+ cwd,
95
+ });
96
+ }
97
+ catch (err) {
98
+ if (err instanceof PairingError) {
99
+ console.error(`Appairage interrompu à l'étape « ${err.stage} » : ${err.message}`);
100
+ // Fail-closed : un appairage refusé ne laisse aucune trace locale, donc relancer la
101
+ // commande est sûr et ne laisse pas d'orphelin à nettoyer.
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ throw err;
106
+ }
107
+ if (options.json) {
108
+ console.log(JSON.stringify({
109
+ enrollment_id: handle.enrollment_id,
110
+ cloud_project_id: handle.cloud_project_id,
111
+ device_id: handle.device.device_id,
112
+ fingerprints: handle.fingerprints,
113
+ awaiting: 'human_approval',
114
+ }, null, 2));
115
+ return;
116
+ }
117
+ console.log('Preuve de possession acceptée. Attestation de clé enregistrée.');
118
+ console.log('');
119
+ console.log(' Projet cloud : ' + handle.cloud_project_id);
120
+ console.log(' Enrôlement : ' + handle.enrollment_id);
121
+ console.log('');
122
+ // CES DEUX EMPREINTES SONT LE CŒUR DE LA CÉRÉMONIE. La personne qui approuve voit les
123
+ // mêmes à l'écran ; leur comparaison hors bande est ce qui ferme l'attaque de l'homme
124
+ // du milieu sur l'appairage. Affichées EN ENTIER, pas tronquées : une comparaison sur
125
+ // 16 caractères se collisionne bien plus facilement qu'elle n'en a l'air.
126
+ console.log(" Empreinte d'identité (Ed25519) : " + handle.fingerprints.identity);
127
+ console.log(' Empreinte de chiffrement (X25519) : ' + handle.fingerprints.encryption);
128
+ console.log('');
129
+ console.log(' → Faites vérifier CES DEUX EMPREINTES par la personne qui approuve.');
130
+ console.log(" Si elles diffèrent de ce qu'elle voit, REFUSEZ : quelqu'un s'est interposé.");
131
+ console.log('');
132
+ console.log(" En attente d'approbation humaine. Constatez-la avec : brainclaw cloud await");
133
+ }
134
+ /**
135
+ * Constate l'approbation et bascule l'état local en actif.
136
+ *
137
+ * SÉPARÉ DE `connect` : l'approbation dépend d'un humain, dont le délai n'est pas borné.
138
+ * Une commande qui bloquerait indéfiniment sur un tiers serait un mauvais citoyen dans un
139
+ * script, et un appairage interrompu par un Ctrl-C doit rester reprenable.
140
+ */
141
+ export async function runCloudAwait(options) {
142
+ const cwd = options.cwd ?? resolveEffectiveCwd();
143
+ const state = loadConnectionState(cwd);
144
+ if (!state?.enrollment.enrollment_id) {
145
+ console.error("Aucun appairage en cours sur ce workspace. Lancez d'abord : brainclaw cloud connect");
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+ const transport = options.transport ?? httpTransport(options.url);
150
+ const result = await checkPairingApproval({ enrollmentId: state.enrollment.enrollment_id, transport, cwd });
151
+ if (!result.approved) {
152
+ console.log(`Toujours en attente (état distant : ${result.state}).`);
153
+ return;
154
+ }
155
+ const next = completePairing({ role: result.role, cwd });
156
+ console.log(`Appairage approuvé. Rôle : ${next.enrollment.role ?? '—'}.`);
157
+ console.log('');
158
+ // Le premier pull est en LECTURE SEULE et non destructif (RFC §5.2 phase 4) : rien
159
+ // n'est matérialisé dans la mémoire locale tant que la vérification d'origine n'existe
160
+ // pas. Le dire évite qu'on croie la synchronisation déjà active.
161
+ console.log(" Aucune donnée n'est encore matérialisée : la vérification à la réception");
162
+ console.log(" (signature d'origine, anti-rejeu) est livrée à l'étape suivante.");
163
+ }
164
+ export async function runCloudDisconnect(options) {
165
+ const cwd = options.cwd ?? resolveEffectiveCwd();
166
+ const state = loadConnectionState(cwd);
167
+ if (!state) {
168
+ console.log("Ce workspace n'est pas appairé — rien à déconnecter.");
169
+ return;
170
+ }
171
+ let revocationNote = "aucune révocation distante demandée (pas d'enrôlement connu)";
172
+ if (state.enrollment.enrollment_id) {
173
+ const transport = options.transport ?? httpTransport(options.url);
174
+ const res = await requestRevocation({ enrollmentId: state.enrollment.enrollment_id, transport });
175
+ revocationNote = res.revoked
176
+ ? 'autorisation distante révoquée'
177
+ : `révocation distante NON confirmée (${res.detail ?? 'raison inconnue'})`;
178
+ }
179
+ // L'état local passe en 'revoked' MÊME SI le cloud est injoignable : sinon un appareil
180
+ // perdu resterait autorisé faute de réseau, exactement l'inverse de ce qu'une
181
+ // révocation doit garantir.
182
+ saveConnectionState({ ...state, enrollment: { ...state.enrollment, stage: 'revoked', updated_at: nowISO() } }, cwd);
183
+ let keysNote = "trousseau conservé (utilisez --forget-keys pour l'effacer)";
184
+ if (options.forgetKeys) {
185
+ const removed = forgetProjectEpochs(state.cloud_project_id);
186
+ keysNote = `${removed} clé(s) d'epoch effacée(s) — le passé scellé de ce projet devient illisible ici`;
187
+ }
188
+ console.log('Déconnecté.');
189
+ console.log(` ${revocationNote}`);
190
+ console.log(` ${keysNote}`);
191
+ console.log('');
192
+ // Énoncé plutôt que tu : prétendre l'inverse serait une promesse que la cryptographie
193
+ // ne tient pas (RFC §5.2).
194
+ console.log(" Ce qui N'EST PAS effacé : les données déjà tirées et déchiffrées localement,");
195
+ console.log(" et ce que d'autres appareils détiennent déjà. Un disconnect retire une");
196
+ console.log(' autorisation ; il ne réécrit pas le passé.');
197
+ }
198
+ //# sourceMappingURL=cloud.js.map
@@ -6,7 +6,7 @@ import { loadConfig, saveConfig } from '../core/config.js';
6
6
  import { isAgentIntegrationName, upsertAgentIntegrationDeclaration } from '../core/agent-integrations.js';
7
7
  import { resolveInstructions, loadInstructions } from '../core/instructions.js';
8
8
  import { detectAiAgent } from '../core/ai-agent-detection.js';
9
- import { AGENT_EXPORT_REGISTRY, resolveExportTarget, resolveExportTargetByFormat, resolveLiveCompanionPath, writeExportFile, writeLiveCompanionFile, buildHygieneSection, describeAutoConfigWrite, writeExportCompanionFiles, collectExportGitignoreEntries, ensureGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, } from '../core/agent-files.js';
9
+ import { AGENT_EXPORT_REGISTRY, resolveExportTarget, resolveExportTargetByFormat, resolveLiveCompanionPath, writeExportFile, writeLiveCompanionFile, buildHygieneSection, describeAutoConfigWrite, writeExportCompanionFiles, collectExportGitignoreEntries, ensureGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, BRAINCLAW_PROTOCOL_ARTIFACT_IGNORES, } from '../core/agent-files.js';
10
10
  import { buildCoordinationSnapshot } from '../core/coordination.js';
11
11
  import { listClaims } from '../core/claims.js';
12
12
  import { listCandidates } from '../core/candidates.js';
@@ -61,7 +61,7 @@ export function runExport(options) {
61
61
  });
62
62
  if (liveResult)
63
63
  gitignoreEntries.push(liveResult.relativePath);
64
- ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES]);
64
+ ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES, ...BRAINCLAW_PROTOCOL_ARTIFACT_IGNORES]);
65
65
  declareAgentIntegrationFromTarget(cwd, target.agentName, 'manual');
66
66
  console.log(`✔ Written to ${target.relativePath} (${result.created ? 'created' : 'updated'})`);
67
67
  if (liveResult) {
@@ -102,7 +102,7 @@ function runExportDetect(cwd, options) {
102
102
  const gitignoreEntries = collectExportGitignoreEntries(cwd, target.relativePath, autoConfigs);
103
103
  if (liveResult)
104
104
  gitignoreEntries.push(liveResult.relativePath);
105
- ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES]);
105
+ ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES, ...BRAINCLAW_PROTOCOL_ARTIFACT_IGNORES]);
106
106
  declareAgentIntegrationFromTarget(cwd, target.agentName, detected ? 'detected' : 'manual');
107
107
  const source = detected ? `${detected.name} [${detected.detection_source}]` : 'fallback (no agent detected)';
108
108
  console.log(`✔ Detected: ${source}`);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import readline from 'node:readline/promises';
4
+ import { clearEnumerationMemo } from '../core/entity-locator.js';
4
5
  import { registerAgentIdentity, resolveDefaultAgentName, resolveExistingCurrentAgent } from '../core/agent-registry.js';
5
6
  import { MEMORY_DIR, memoryExists, ensureMemoryDir, memoryPath, writeFileAtomic } from '../core/io.js';
6
7
  import { emptyState, loadState, saveState } from '../core/state.js';
@@ -371,6 +372,16 @@ export async function runInit(options = {}) {
371
372
  console.log(`Tip: run 'brainclaw init' again later to refresh the detected agent's integration files on this project.`);
372
373
  }
373
374
  console.log(`Tip: in an agent session, call the bclaw_work MCP tool (intent: "consult") to load the shared memory; from a terminal, 'brainclaw context --json' does the same.`);
375
+ // A STORE JUST CAME INTO EXISTENCE, so the routing memo's candidate list is stale.
376
+ //
377
+ // `clearEnumerationMemo` was documented as being "for tests, and for any caller that has
378
+ // just created a store" — and that second caller did not exist (Fable audit found the
379
+ // claim describing intent rather than code). The consequence was small but real: for up
380
+ // to the memo TTL, a mutation routed right after `brainclaw init` / `bclaw_init_project`
381
+ // could not see the new project. Wiring it HERE rather than in the MCP handler covers
382
+ // every path that materialises a store, since both the CLI and the tool go through
383
+ // runInit.
384
+ clearEnumerationMemo();
374
385
  }
375
386
  function safeRunMachinePrereqs(agentName) {
376
387
  try {