total-recall-brain 3.41.2 → 3.41.4
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/bin/total-recall.mjs +19 -4
- package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
- package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +8 -5
- package/frontend/src/api/auth.ts +0 -3
- package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
- package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
- package/frontend/src/pages/LoginPage.tsx +1 -41
- package/frontend/src/pages/PluginsPage.tsx +14 -7
- package/package.json +7 -5
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
- package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
- package/src/cli/command.mjs +23 -18
- package/src/cli/config.mjs +3 -0
- package/src/cli/connect.mjs +12 -116
- package/src/cli/daemon.mjs +23 -1
- package/src/cli/harness.mjs +0 -1
- package/src/cli/plugin/copies.mjs +8 -5
- package/src/cli/plugin/create.mjs +56 -3
- package/src/cli/plugin/index.mjs +40 -5
- package/src/cli/plugin/monetization.mjs +232 -12
- package/src/cli/preflight.mjs +161 -0
- package/src/cli/rebuild.mjs +22 -0
- package/src/cli/reset-password.mjs +3 -2
- package/src/cli/skill.mjs +8 -2
- package/src/cli/status.mjs +25 -7
- package/src/core/automatic-brain-updates.mjs +9 -3
- package/src/core/brain-updates.mjs +63 -5
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/context-compiler.mjs +8 -1
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/global-brain-updates.mjs +21 -1
- package/src/core/mesh-brain-updates.mjs +5 -2
- package/src/core/meta-harness.mjs +32 -79
- package/src/core/plugin-dependencies.mjs +6 -1
- package/src/core/plugin-runner.mjs +6 -1
- package/src/core/plugin-store.mjs +67 -1
- package/src/core/plugin-updates.mjs +1 -1
- package/src/core/portable-skills-maintenance.mjs +19 -0
- package/src/core/preflight-gate.mjs +77 -0
- package/src/core/preflight-receipt.mjs +123 -0
- package/src/core/required-plugins.mjs +7 -0
- package/src/core/runtime.mjs +0 -3
- package/src/core/skills-registry.mjs +33 -3
- package/src/core/surface.mjs +38 -1
- package/src/server/auth.mjs +41 -1
- package/src/server/memory-app.mjs +2 -1
- package/src/server/routes/auth.mjs +25 -1
- package/src/server/routes/plugins.mjs +17 -4
|
@@ -86,7 +86,13 @@ export function meshNodeKey(hostname) {
|
|
|
86
86
|
function normalizeNode(node, self = false) {
|
|
87
87
|
return {
|
|
88
88
|
hostname: normalizeHostname(node?.DNSName) || node?.HostName || null,
|
|
89
|
+
// The name the machine reports for itself, which is what the control
|
|
90
|
+
// server lists it under. It differs from the DNS name whenever the node was
|
|
91
|
+
// renamed (a droplet listed as `portfolio-site` answers as `cloud.mesh…`),
|
|
92
|
+
// and it is not unique, so it is an identity to match, never a key.
|
|
93
|
+
os_hostname: node?.HostName || null,
|
|
89
94
|
ip: node?.TailscaleIPs?.[0] || null,
|
|
95
|
+
ips: Array.isArray(node?.TailscaleIPs) ? node.TailscaleIPs : [],
|
|
90
96
|
online: !!node?.Online,
|
|
91
97
|
self,
|
|
92
98
|
os: node?.OS || null,
|
|
@@ -226,7 +232,9 @@ function enrichPeerWithEntity(peer, ent, extra = {}) {
|
|
|
226
232
|
: [];
|
|
227
233
|
return {
|
|
228
234
|
hostname: peer.hostname,
|
|
235
|
+
os_hostname: peer.os_hostname || null,
|
|
229
236
|
ip: peer.ip || ent?.ip || null,
|
|
237
|
+
ips: Array.isArray(peer.ips) && peer.ips.length ? peer.ips : [peer.ip || ent?.ip].filter(Boolean),
|
|
230
238
|
lan_ip: peer.lan_ip || ent?.lan_ip || null,
|
|
231
239
|
brain_port: ent?.brain_port || null,
|
|
232
240
|
online: !!peer.online,
|
|
@@ -308,47 +316,178 @@ function findEntityForSelf(self, vaultRoot) {
|
|
|
308
316
|
return entityForPeer(self, entities, otherHosts);
|
|
309
317
|
}
|
|
310
318
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const wanted = String(nameOrAddress || '').trim();
|
|
314
|
-
if (!wanted) return null;
|
|
315
|
-
const key = meshNodeKey(wanted);
|
|
316
|
-
const nodes = listEnrichedMeshNodes(vaultRoot);
|
|
319
|
+
const isAddressLike = (value) => value.includes(':') || /^\d{1,3}(\.\d{1,3}){3}$/.test(value);
|
|
320
|
+
const cleanName = (value) => String(value).trim().replace(/\.$/, '').toLowerCase();
|
|
317
321
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
function meshNodeError(code, message) {
|
|
323
|
+
const err = new Error(message);
|
|
324
|
+
err.code = code;
|
|
325
|
+
return err;
|
|
326
|
+
}
|
|
321
327
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
328
|
+
/** A control-server entry no live peer or entity accounts for. */
|
|
329
|
+
function registryOnlyNode(entry) {
|
|
330
|
+
const addresses = (entry.ipAddresses || []).map(String);
|
|
331
|
+
const name = entry.givenName || entry.name || null;
|
|
332
|
+
return {
|
|
333
|
+
hostname: name,
|
|
334
|
+
os_hostname: entry.name || null,
|
|
335
|
+
ip: addresses[0] || null,
|
|
336
|
+
ips: addresses,
|
|
337
|
+
lan_ip: null,
|
|
338
|
+
online: entry.online === true,
|
|
339
|
+
self: false,
|
|
340
|
+
title: name,
|
|
341
|
+
has_entity: false,
|
|
342
|
+
vault_only: false,
|
|
343
|
+
registry_only: true,
|
|
344
|
+
registry_id: entry.id == null ? null : String(entry.id),
|
|
345
|
+
access: null,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
325
348
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Every node reference can be resolved against, each with the identities an
|
|
351
|
+
* operator might type: what the local daemon reports (DNS name, the machine's
|
|
352
|
+
* own hostname, every address), what the vault records (title, LAN address),
|
|
353
|
+
* and what the control server lists (`name`, `givenName`, `id`).
|
|
354
|
+
*
|
|
355
|
+
* Registry entries join a node by a shared address. An entry nothing claims
|
|
356
|
+
* becomes a node of its own, so a machine only the control server knows about
|
|
357
|
+
* still resolves; it carries no access, which callers report as such.
|
|
358
|
+
*/
|
|
359
|
+
function meshNodeCandidates(nodes = [], registry = null) {
|
|
360
|
+
const entries = Array.isArray(registry) ? registry : [];
|
|
361
|
+
const claimed = new Set();
|
|
362
|
+
const identities = (node, entry) => {
|
|
363
|
+
const names = [node.hostname, node.os_hostname, node.title, entry?.name, entry?.givenName].filter(Boolean);
|
|
364
|
+
return {
|
|
365
|
+
addresses: new Set(
|
|
366
|
+
[node.ip, node.lan_ip, ...(node.ips || []), ...(entry?.ipAddresses || [])]
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
.map((v) => String(v).toLowerCase()),
|
|
369
|
+
),
|
|
370
|
+
names: new Set(names.map(cleanName)),
|
|
371
|
+
keys: new Set(names.map(meshNodeKey).filter(Boolean)),
|
|
372
|
+
id: entry?.id == null || entry.id === '' ? null : String(entry.id),
|
|
373
|
+
};
|
|
374
|
+
};
|
|
329
375
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
(
|
|
334
|
-
(r.name && meshNodeKey(r.name) === key)
|
|
376
|
+
const candidates = nodes.map((node) => {
|
|
377
|
+
const own = new Set([node.ip, ...(node.ips || [])].filter(Boolean).map(String));
|
|
378
|
+
const index = entries.findIndex(
|
|
379
|
+
(e, i) => !claimed.has(i) && (e.ipAddresses || []).some((ip) => own.has(String(ip))),
|
|
335
380
|
);
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
381
|
+
if (index !== -1) claimed.add(index);
|
|
382
|
+
return { node, ids: identities(node, index === -1 ? null : entries[index]) };
|
|
383
|
+
});
|
|
384
|
+
entries.forEach((entry, i) => {
|
|
385
|
+
if (claimed.has(i)) return;
|
|
386
|
+
const node = registryOnlyNode(entry);
|
|
387
|
+
candidates.push({ node, ids: identities(node, entry) });
|
|
388
|
+
});
|
|
389
|
+
return candidates;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Resolve a node reference against every identity `mesh nodes` can print.
|
|
394
|
+
*
|
|
395
|
+
* Precedence, first tier with a match wins: exact address, exact whole name,
|
|
396
|
+
* first-label key (never for an address, whose first label is shared by every
|
|
397
|
+
* node on the range), then the control server's node id.
|
|
398
|
+
*
|
|
399
|
+
* A machine's own hostname is not unique (two phones both report `localhost`),
|
|
400
|
+
* so a reference that matches two machines in the deciding tier is refused
|
|
401
|
+
* rather than resolved to whichever came first: `exec` would otherwise run on
|
|
402
|
+
* the wrong host. A vault-only record that shares a match with a live or
|
|
403
|
+
* registered node is set aside first — Headscale reassigns a freed address, so
|
|
404
|
+
* a stale record at that address describes the machine that held it before.
|
|
405
|
+
*
|
|
406
|
+
* Pure: callers supply the nodes and, optionally, the registry.
|
|
407
|
+
*
|
|
408
|
+
* @returns {object|null} the matching node; throws MESH_NODE_AMBIGUOUS
|
|
409
|
+
*/
|
|
410
|
+
export function matchMeshNode(nameOrAddress, nodes = [], registry = null) {
|
|
411
|
+
const wanted = String(nameOrAddress || '').trim();
|
|
412
|
+
if (!wanted) return null;
|
|
413
|
+
const lower = wanted.toLowerCase();
|
|
414
|
+
const exact = cleanName(wanted);
|
|
415
|
+
const key = isAddressLike(wanted) ? '' : meshNodeKey(wanted);
|
|
416
|
+
const candidates = meshNodeCandidates(nodes, registry);
|
|
417
|
+
|
|
418
|
+
const tiers = [
|
|
419
|
+
(ids) => ids.addresses.has(lower),
|
|
420
|
+
(ids) => ids.names.has(exact),
|
|
421
|
+
(ids) => !!key && ids.keys.has(key),
|
|
422
|
+
(ids) => ids.id === wanted,
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
for (const matches of tiers) {
|
|
426
|
+
const hits = candidates.filter((c) => matches(c.ids));
|
|
427
|
+
if (!hits.length) continue;
|
|
428
|
+
const current = hits.filter((c) => !c.node.vault_only);
|
|
429
|
+
const pool = current.length ? current : hits;
|
|
430
|
+
if (pool.length > 1) {
|
|
431
|
+
const names = pool.map((c) => `${c.node.hostname || '?'} (${c.node.ip || 'no address'})`);
|
|
432
|
+
throw meshNodeError(
|
|
433
|
+
'MESH_NODE_AMBIGUOUS',
|
|
434
|
+
`"${wanted}" matches more than one mesh node: ${names.join(', ')}. Use its address or full name.`,
|
|
435
|
+
);
|
|
346
436
|
}
|
|
437
|
+
return pool[0].node;
|
|
347
438
|
}
|
|
348
|
-
|
|
349
439
|
return null;
|
|
350
440
|
}
|
|
351
441
|
|
|
442
|
+
/** Find an enriched node (live + vault, plus `registry` when given) synchronously. */
|
|
443
|
+
export function findMeshNode(nameOrAddress, vaultRoot = meshVaultRoot(), registry = null) {
|
|
444
|
+
return matchMeshNode(nameOrAddress, listEnrichedMeshNodes(vaultRoot), registry);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function fetchMeshRegistry(brainDir) {
|
|
448
|
+
const { headscaleFetch } = await import('./headscale-client.mjs');
|
|
449
|
+
const data = await headscaleFetch('/api/v1/node', {}, brainDir);
|
|
450
|
+
return Array.isArray(data?.nodes) ? data.nodes : [];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The one resolver every command that takes a node goes through.
|
|
455
|
+
*
|
|
456
|
+
* Local discovery and the vault answer first, so a name this machine already
|
|
457
|
+
* knows never needs the control-server credential. Only on a miss is the
|
|
458
|
+
* control server asked — that is where registry-only names and node ids live.
|
|
459
|
+
* When it cannot be asked, the not-found error says why, so a locked secrets
|
|
460
|
+
* store is never reported as a machine that does not exist.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} nameOrAddress
|
|
463
|
+
* @param {{vaultRoot?: string, brainDir?: string, nodes?: object[], fetchRegistry?: Function}} [options]
|
|
464
|
+
* @returns {Promise<object>} the node; throws MESH_NODE_NOT_FOUND or MESH_NODE_AMBIGUOUS
|
|
465
|
+
*/
|
|
466
|
+
export async function resolveMeshNode(nameOrAddress, options = {}) {
|
|
467
|
+
const wanted = String(nameOrAddress || '').trim();
|
|
468
|
+
if (!wanted) throw meshNodeError('MESH_NODE_NOT_FOUND', 'A mesh node name or address is required.');
|
|
469
|
+
const nodes = options.nodes || listEnrichedMeshNodes(options.vaultRoot || meshVaultRoot());
|
|
470
|
+
|
|
471
|
+
const local = matchMeshNode(wanted, nodes);
|
|
472
|
+
if (local) return local;
|
|
473
|
+
|
|
474
|
+
let registry = null;
|
|
475
|
+
let registryError = null;
|
|
476
|
+
try {
|
|
477
|
+
registry = await (options.fetchRegistry || fetchMeshRegistry)(options.brainDir);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
registryError = err;
|
|
480
|
+
}
|
|
481
|
+
const found = registry ? matchMeshNode(wanted, nodes, registry) : null;
|
|
482
|
+
if (found) return found;
|
|
483
|
+
|
|
484
|
+
throw meshNodeError(
|
|
485
|
+
'MESH_NODE_NOT_FOUND',
|
|
486
|
+
`No mesh node matches "${wanted}".` +
|
|
487
|
+
(registryError ? ` The control server could not be asked: ${registryError.message}` : ''),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
352
491
|
/**
|
|
353
492
|
* Record how to reach a node.
|
|
354
493
|
*
|
|
@@ -358,8 +497,14 @@ export function findMeshNode(nameOrAddress, vaultRoot = meshVaultRoot(), registr
|
|
|
358
497
|
*/
|
|
359
498
|
export async function setMeshNodeAccess(nameOrAddress, accessPatch, options = {}) {
|
|
360
499
|
const vaultRoot = options.vaultRoot || meshVaultRoot();
|
|
361
|
-
|
|
362
|
-
|
|
500
|
+
let node;
|
|
501
|
+
try {
|
|
502
|
+
node = await resolveMeshNode(nameOrAddress, { vaultRoot, brainDir: options.brainDir });
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (err.code === 'MESH_NODE_NOT_FOUND') return { written: false, reason: 'node-not-found', error: err.message };
|
|
505
|
+
if (err.code === 'MESH_NODE_AMBIGUOUS') return { written: false, reason: 'node-ambiguous', error: err.message };
|
|
506
|
+
throw err;
|
|
507
|
+
}
|
|
363
508
|
|
|
364
509
|
const entities = listMeshNodeEntities(vaultRoot);
|
|
365
510
|
const otherHosts = new Set(
|
|
@@ -679,11 +824,50 @@ export async function patchOwnMeshNode(options = {}) {
|
|
|
679
824
|
* @param {boolean} [options.loginShell=true]
|
|
680
825
|
* @returns {Promise<{ node: string, hostname: string, ip: string, exitCode: number, success: boolean, stdout: string, stderr: string }>}
|
|
681
826
|
*/
|
|
827
|
+
/**
|
|
828
|
+
* Is `node` the machine running this command?
|
|
829
|
+
*
|
|
830
|
+
* Every host reaches itself through its own sshd, so `mesh ssh`/`mesh exec`
|
|
831
|
+
* aimed at self is a misconfiguration, not a remote call: it burns an auth
|
|
832
|
+
* round-trip, fails under `BatchMode` when this host holds no key for its own
|
|
833
|
+
* account, and hides the real intent (work that belongs to the local machine).
|
|
834
|
+
*
|
|
835
|
+
* Identity comes only from what the local daemon and registry report: the
|
|
836
|
+
* `self` flag, an exact address, or a whole hostname under any of its MagicDNS
|
|
837
|
+
* forms. Short labels are compared for HOSTNAMES ONLY — applied to an address
|
|
838
|
+
* they would compare the first octet, which every node on a shared range has
|
|
839
|
+
* in common, and match unrelated peers. Never key on a device name, a tailnet
|
|
840
|
+
* domain or an address range.
|
|
841
|
+
*/
|
|
842
|
+
export function isSelfNode(node, selfOverride) {
|
|
843
|
+
if (!node) return false;
|
|
844
|
+
if (node.self) return true;
|
|
845
|
+
const self = selfOverride === undefined ? getMeshSelf() : selfOverride;
|
|
846
|
+
if (!self) return false;
|
|
847
|
+
|
|
848
|
+
const isAddress = (value) => value.includes(':') || /^\d{1,3}(\.\d{1,3}){3}$/.test(value);
|
|
849
|
+
const clean = (v) => String(v || '').replace(/\.$/, '').toLowerCase();
|
|
850
|
+
const selfValues = [self.hostname, self.ip, self.name].filter(Boolean).map(clean);
|
|
851
|
+
const selfAddresses = new Set(selfValues.filter(isAddress));
|
|
852
|
+
const selfNames = new Set(selfValues.filter((v) => !isAddress(v)));
|
|
853
|
+
const selfLabels = new Set([...selfNames].map((v) => v.split('.')[0]));
|
|
854
|
+
|
|
855
|
+
return [node.hostname, node.ip, node.name].filter(Boolean).map(clean).some((candidate) => {
|
|
856
|
+
if (selfAddresses.has(candidate)) return true;
|
|
857
|
+
if (selfNames.has(candidate)) return true;
|
|
858
|
+
// A hostname may arrive fully qualified or bare; an address may not.
|
|
859
|
+
return !isAddress(candidate) && selfLabels.has(candidate.split('.')[0]);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
682
863
|
export async function execMeshCommand(target, command, options = {}) {
|
|
683
864
|
const { vaultRoot = meshVaultRoot(), timeoutMs = 60_000, loginShell = true } = options;
|
|
684
|
-
const node =
|
|
685
|
-
if (
|
|
686
|
-
throw new Error(
|
|
865
|
+
const node = await resolveMeshNode(target, { vaultRoot, brainDir: options.brainDir });
|
|
866
|
+
if (isSelfNode(node)) {
|
|
867
|
+
throw new Error(
|
|
868
|
+
`"${node.hostname}" is this machine, and the mesh never tunnels a node into itself. `
|
|
869
|
+
+ 'Run the command locally; the mesh CLI addresses peers.',
|
|
870
|
+
);
|
|
687
871
|
}
|
|
688
872
|
const resolved = resolveNodeAccess(node);
|
|
689
873
|
if (!resolved.complete) {
|
|
@@ -172,9 +172,14 @@ router.post('/api/mesh/access', requireAuth, requireScope('config:write'), async
|
|
|
172
172
|
const result = await setMeshNodeAccess(target, patch, { vaultRoot: meshVaultRoot() });
|
|
173
173
|
if (!result.written) {
|
|
174
174
|
const notFound = result.reason === 'node-not-found';
|
|
175
|
-
|
|
175
|
+
const ambiguous = result.reason === 'node-ambiguous';
|
|
176
|
+
res.status(notFound ? 404 : ambiguous ? 409 : 500).json({
|
|
176
177
|
success: false,
|
|
177
|
-
message: notFound
|
|
178
|
+
message: notFound
|
|
179
|
+
? `No mesh node matches "${target}".`
|
|
180
|
+
: ambiguous
|
|
181
|
+
? result.error
|
|
182
|
+
: 'The vault write was rejected.',
|
|
178
183
|
reason: result.reason || null,
|
|
179
184
|
});
|
|
180
185
|
return;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Command Fleet
|
|
2
|
+
|
|
3
|
+
All nine adapters share one plugin: Claude Code, Codex, Antigravity, Cursor,
|
|
4
|
+
DSH, Grok, Muse, Pi and Hermes. `runtime <agent> ...` exposes their native runtime
|
|
5
|
+
commands; DSH observations and Hermes gateway management live here as well.
|
|
6
|
+
|
|
7
|
+
`total-recall cli-agents setup` connects all nine native instruction surfaces.
|
|
8
|
+
The declared setup operation also runs during daemon-owned plugin refresh.
|
|
9
|
+
Every dispatched agent receives a freshly checked Total Recall context, complete
|
|
10
|
+
core help, installed plugin operations and composed commands. Setup failure or
|
|
11
|
+
an unready context stops dispatch.
|
|
12
|
+
|
|
13
|
+
`docs-refresh`, `docs-ensure`, `docs-status`, `docs-read` and `docs-search` manage
|
|
14
|
+
downloaded official documentation rather than a list of links. Content is hashed,
|
|
15
|
+
bounded and refreshed after 24 hours. Maintenance uses the existing Total Recall
|
|
16
|
+
daemon's plugin task, refreshing one stale adapter per run. Failed pages remain
|
|
17
|
+
visible as failures and cannot certify complete coverage; successful cached
|
|
18
|
+
content remains readable. `setup --node <recorded-node>` selects maintenance
|
|
19
|
+
placement without creating a second scheduler.
|
|
20
|
+
|
|
21
|
+
Standalone Total Recall owner for CLI-agent discovery and explicit mesh command execution. Install with `total-recall plugin install <path-or-private-git-url>`. Installation rebuilds the plugin inventory, CLI library and connected instruction surfaces. No model account is created or selected automatically.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
total-recall cli-agents catalog --json
|
|
25
|
+
total-recall cli-agents nodes --json
|
|
26
|
+
total-recall cli-agents inspect --node <recorded-node> --json
|
|
27
|
+
total-recall cli-agents run-command --stdin --json < request.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`request.json` selects one installed plugin operation, its literal arguments, a node and an agent:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"pluginId": "git-sentinel",
|
|
35
|
+
"subcommand": "status",
|
|
36
|
+
"args": [],
|
|
37
|
+
"execution": {
|
|
38
|
+
"agent": "codex",
|
|
39
|
+
"node": "local",
|
|
40
|
+
"permissions": { "read": true, "write": false, "send": false, "destructive": false }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For remote nodes, supply `execution.cwd` as an explicit absolute workspace. `agent: "direct"` runs the actual installed Total Recall CLI without a model. Remote access comes from recorded mesh entities and verified SSH configuration; unavailable nodes or binaries produce errors. Secrets are never accepted in an agent prompt. Existing account metadata is inspected without printing credentials or substituting provider API keys.
|
|
46
|
+
|
|
47
|
+
The four permissions are strict boolean **command-effect admission controls**. They allow or deny the selected plugin command before dispatch. They are not a network policy or an OS sandbox for every action a model might propose. Supported adapter write modes are scoped through the vendor CLI; vendor approval and sandbox defaults stay enabled. The host's authenticated authorization still applies.
|
|
48
|
+
|
|
49
|
+
A completed model turn is distinct from a completed plugin operation. Structured results return `commandExecutionVerified: false` for agent turns; only direct CLI execution returns a command exit receipt. Muse preserves its actual headless JSONL log and documented process exit status rather than treating session-protocol notifications as headless output. Do not infer working accounts, provider delivery or successful mutations from discovery or fixture tests.
|
|
50
|
+
|
|
51
|
+
## Primary contracts checked 2026-10-14
|
|
52
|
+
|
|
53
|
+
| Adapter | Contract |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| Claude Code | [Headless](https://code.claude.com/docs/en/headless) |
|
|
56
|
+
| Codex | [Non-interactive mode](https://developers.openai.com/codex/noninteractive) |
|
|
57
|
+
| Antigravity | [Headless CLI](https://antigravity.google/docs/cli/headless/) |
|
|
58
|
+
| Cursor | [CLI parameters](https://docs.cursor.com/en/cli/reference/parameters) |
|
|
59
|
+
| DSH | [Official CLI source](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md) |
|
|
60
|
+
| Grok | [Headless scripting](https://docs.x.ai/build/cli/headless-scripting) |
|
|
61
|
+
| Muse | [Headless and CI](https://dev.meta.ai/docs/muse-code/extending) |
|
|
62
|
+
| Pi | [Official CLI reference](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/cli.md) |
|
|
63
|
+
| Hermes | [Official command reference](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/reference/cli-commands.md) |
|
|
64
|
+
|
|
65
|
+
Cursor discovery uses `cursor-agent`; an ambiguous `agent` binary is not assumed to be Cursor. Missing installations are listed as unavailable. Unknown authentication requires explicit agent selection and remains unverified until a real run. Tests exercise literal stdin, bounded child-process cleanup, command admission, recorded SSH access, account checks and completed-result parsing. Run suites on the configured sanctioned test node.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Meta Harness Capability Brick
|
|
2
|
+
|
|
3
|
+
A zero-dependency, host-free pure ES module providing universal agent harness discovery, two-stage complexity classification, cost-vs-frontier routing, gate cascade evaluation, and execution envelope construction.
|
|
4
|
+
|
|
5
|
+
## Why This Brick Exists
|
|
6
|
+
|
|
7
|
+
Modern software engineering with autonomous AI harnesses suffers from two structural traps:
|
|
8
|
+
1. **The Correction Penalty**: Sub-frontier models on complex architectural tasks burn more tokens retrying than frontier models take in a single shot.
|
|
9
|
+
2. **Confirmation Bias**: An agent that writes flawed code often passes its own validation review unless evaluated by a separate model family.
|
|
10
|
+
|
|
11
|
+
This brick encapsulates the core deterministic intelligence needed to route, coordinate, and score agent harnesses without binding your application to any host runtime or secrets store.
|
|
12
|
+
|
|
13
|
+
## Installation / Vendoring
|
|
14
|
+
|
|
15
|
+
When vendoring into another repository using Total Recall:
|
|
16
|
+
```bash
|
|
17
|
+
total-recall plugin vendor meta-harness
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or import the capability directly in any standard ES module environment:
|
|
21
|
+
```javascript
|
|
22
|
+
import {
|
|
23
|
+
createMetaHarness,
|
|
24
|
+
ADAPTER_CATALOG,
|
|
25
|
+
classifyTaskComplexity,
|
|
26
|
+
selectOptimalHarness,
|
|
27
|
+
evaluateGateCascade,
|
|
28
|
+
computeEmpiricalScorecard
|
|
29
|
+
} from './capability/index.mjs';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Core API
|
|
33
|
+
|
|
34
|
+
### `classifyTaskComplexity(taskDescription, options)`
|
|
35
|
+
Analyzes instructions for architectural indicators, security keywords, blast radius, and length:
|
|
36
|
+
```javascript
|
|
37
|
+
const complexity = classifyTaskComplexity("Refactor auth system to WebAuthn with test verification");
|
|
38
|
+
// Returns: { score: 95, tier: 'frontier', isFrontierRecommended: true, factors: [...] }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `selectOptimalHarness(taskDescription, availableHarnesses, quotaStatus)`
|
|
42
|
+
Computes cost-vs-quality utility across available CLI runners:
|
|
43
|
+
```javascript
|
|
44
|
+
const selection = selectOptimalHarness("Fix typo in README.md", ['agy', 'dsh', 'ollama']);
|
|
45
|
+
// Returns: { selected: 'ollama', complexity: { tier: 'local' }, candidates: [...] }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `evaluateGateCascade(currentHarness, gateResults)`
|
|
49
|
+
FrugalGPT gate cascade: keeps cheap models on simple tasks, escalating only upon verified failure:
|
|
50
|
+
```javascript
|
|
51
|
+
const cascade = evaluateGateCascade('ollama', { testsPassed: false, linterPassed: true });
|
|
52
|
+
// Returns: { escalationRequired: true, recommendedHarness: 'dsh', reason: '...' }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `computeEmpiricalScorecard(historyRuns)`
|
|
56
|
+
Aggregates pass rates, failure frequencies, and average latencies across runs:
|
|
57
|
+
```javascript
|
|
58
|
+
const scorecard = computeEmpiricalScorecard(runs);
|
|
59
|
+
```
|