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.
Files changed (128) hide show
  1. package/bin/total-recall.mjs +19 -4
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +8 -5
  6. package/frontend/src/api/auth.ts +0 -3
  7. package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
  9. package/frontend/src/pages/LoginPage.tsx +1 -41
  10. package/frontend/src/pages/PluginsPage.tsx +14 -7
  11. package/package.json +7 -5
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  27. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  33. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  38. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
  42. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
  44. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
  84. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
  85. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  86. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
  87. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
  88. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  90. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  91. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  92. package/src/cli/command.mjs +23 -18
  93. package/src/cli/config.mjs +3 -0
  94. package/src/cli/connect.mjs +12 -116
  95. package/src/cli/daemon.mjs +23 -1
  96. package/src/cli/harness.mjs +0 -1
  97. package/src/cli/plugin/copies.mjs +8 -5
  98. package/src/cli/plugin/create.mjs +56 -3
  99. package/src/cli/plugin/index.mjs +40 -5
  100. package/src/cli/plugin/monetization.mjs +232 -12
  101. package/src/cli/preflight.mjs +161 -0
  102. package/src/cli/rebuild.mjs +22 -0
  103. package/src/cli/reset-password.mjs +3 -2
  104. package/src/cli/skill.mjs +8 -2
  105. package/src/cli/status.mjs +25 -7
  106. package/src/core/automatic-brain-updates.mjs +9 -3
  107. package/src/core/brain-updates.mjs +63 -5
  108. package/src/core/cli-commands.mjs +2 -0
  109. package/src/core/context-compiler.mjs +8 -1
  110. package/src/core/default-plugins.mjs +1 -1
  111. package/src/core/global-brain-updates.mjs +21 -1
  112. package/src/core/mesh-brain-updates.mjs +5 -2
  113. package/src/core/meta-harness.mjs +32 -79
  114. package/src/core/plugin-dependencies.mjs +6 -1
  115. package/src/core/plugin-runner.mjs +6 -1
  116. package/src/core/plugin-store.mjs +67 -1
  117. package/src/core/plugin-updates.mjs +1 -1
  118. package/src/core/portable-skills-maintenance.mjs +19 -0
  119. package/src/core/preflight-gate.mjs +77 -0
  120. package/src/core/preflight-receipt.mjs +123 -0
  121. package/src/core/required-plugins.mjs +7 -0
  122. package/src/core/runtime.mjs +0 -3
  123. package/src/core/skills-registry.mjs +33 -3
  124. package/src/core/surface.mjs +38 -1
  125. package/src/server/auth.mjs +41 -1
  126. package/src/server/memory-app.mjs +2 -1
  127. package/src/server/routes/auth.mjs +25 -1
  128. package/src/server/routes/plugins.mjs +17 -4
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Preflight receipt: the attestation that an agent read the mandatory skills and
3
+ * received a ready capsule for one repository. It is an ephemeral per-machine
4
+ * file (mode 0600), not application state: every hook reads it on every tool
5
+ * call, so it stays a plain file. Gate decisions are what go to the event log.
6
+ *
7
+ * The file location and fields are unchanged from the original `tr-preflight`
8
+ * command, so receipts written before this module remain valid.
9
+ */
10
+ import crypto from 'node:crypto';
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ export const SKILLS = ['.agent/skills/total-recall/SKILL.md', '.agent/skills/ssss/SKILL.md'];
18
+ export const DEFAULT_MAX_AGE_MS = 30 * 60 * 1000;
19
+ export const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
20
+
21
+ const BIN = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'total-recall.mjs');
22
+
23
+ /**
24
+ * The nearest ancestor holding `.agent`, else the directory itself, with symlinks
25
+ * resolved. A harness may report a symlinked path (`/var` for `/private/var`)
26
+ * where the shell reports the real one; both must find the same receipt.
27
+ */
28
+ export function findRoot(cwd = process.cwd()) {
29
+ const resolve = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
30
+ const start = resolve(cwd);
31
+ let dir = start;
32
+ for (let i = 0; i < 40; i += 1) {
33
+ if (fs.existsSync(path.join(dir, '.agent'))) return dir;
34
+ const parent = path.dirname(dir);
35
+ if (parent === dir) break;
36
+ dir = parent;
37
+ }
38
+ return start;
39
+ }
40
+
41
+ const rootKey = (root) => crypto.createHash('sha256').update(root).digest('hex').slice(0, 16);
42
+ const stateDir = (home) => path.join(home, '.agent', 'tr-preflight');
43
+ export const receiptPath = (root, home = os.homedir()) => path.join(stateDir(home), `${rootKey(root)}.json`);
44
+ export const consultPath = (root, home = os.homedir()) => path.join(stateDir(home), `consult-${rootKey(root)}.stamp`);
45
+
46
+ const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
47
+
48
+ /** Hashes of the mandatory skills, or the list of ones that are missing. */
49
+ export function skillHashes(root) {
50
+ const hashes = {};
51
+ const missing = [];
52
+ for (const rel of SKILLS) {
53
+ const full = path.join(root, rel);
54
+ if (fs.existsSync(full)) hashes[rel] = sha256(full);
55
+ else missing.push(rel);
56
+ }
57
+ return { hashes, missing };
58
+ }
59
+
60
+ export function readReceipt(root, home = os.homedir()) {
61
+ try { return JSON.parse(fs.readFileSync(receiptPath(root, home), 'utf8')); } catch { return null; }
62
+ }
63
+
64
+ /**
65
+ * @returns {{ ok: boolean, problems: string[], receipt: object|null, ageMs: number|null }}
66
+ * `problems` read the same as the original command so existing guidance still fits.
67
+ */
68
+ export function checkReceipt(root, { now = Date.now(), maxAgeMs, home = os.homedir() } = {}) {
69
+ const { hashes, missing } = skillHashes(root);
70
+ if (missing.length) return { ok: false, problems: missing.map((rel) => `missing mandatory skill: ${rel}`), receipt: null, ageMs: null };
71
+ const receipt = readReceipt(root, home);
72
+ if (!receipt) return { ok: false, problems: ['no preflight receipt for this repository'], receipt: null, ageMs: null };
73
+ const problems = [];
74
+ const ageMs = now - Date.parse(receipt.at || 0);
75
+ // The window is decided when the receipt is written, so the hot path never loads configuration.
76
+ const limit = maxAgeMs ?? (Number.isFinite(receipt.max_age_ms) && receipt.max_age_ms > 0 ? receipt.max_age_ms : DEFAULT_MAX_AGE_MS);
77
+ if (!Number.isFinite(ageMs) || ageMs > limit) problems.push('preflight receipt is stale; re-run --ack');
78
+ for (const [rel, hash] of Object.entries(hashes)) {
79
+ if (!receipt.skills || receipt.skills[rel] !== hash) problems.push(`${rel} changed since the receipt; re-read and re-run --ack`);
80
+ }
81
+ if (receipt.ready !== true) problems.push('receipt does not record ready:true');
82
+ return { ok: problems.length === 0, problems, receipt, ageMs: Number.isFinite(ageMs) ? ageMs : null };
83
+ }
84
+
85
+ /** Run this package's own CLI, so `ack` uses exactly the semantics every caller gets. */
86
+ function runSelf(args, cwd) {
87
+ const r = spawnSync(process.execPath, [BIN, ...args], { cwd, encoding: 'utf8', timeout: 60000 });
88
+ return { ok: r.status === 0, out: r.stdout || '', err: r.stderr || '', code: r.status };
89
+ }
90
+
91
+ /**
92
+ * Record a receipt after proving the capsule is ready and the rules verify.
93
+ * `run` is injectable so the specs need no child process.
94
+ */
95
+ export function acknowledge({ root, task = '', actions = 'read,edit,test', maxAgeMs, run = runSelf, now = Date.now(), home = os.homedir() }) {
96
+ const { hashes, missing } = skillHashes(root);
97
+ if (missing.length) return { ok: false, problems: missing.map((rel) => `missing mandatory skill: ${rel}`) };
98
+ const context = run(['context', task || '(no task given)', '--action', actions], root);
99
+ if (!/ready:true/.test(context.out)) {
100
+ return { ok: false, problems: ['total-recall context did not return ready:true',
101
+ context.code === 2 ? 'exit 2: capsule overflow; raise --budget or curate applicability, then retry' : `context exit=${context.code ?? '?'}`] };
102
+ }
103
+ const version = (context.out.match(/version:([0-9a-f]+)/) || [])[1] || null;
104
+ const rules = run(['rules', 'verify'], root);
105
+ if (!rules.ok) return { ok: false, problems: ['total-recall rules verify failed', (rules.out || rules.err).trim().split('\n').slice(-3).join(' | ')] };
106
+ const receipt = { at: new Date(now).toISOString(), repo: root, task, actions, ready: true, capsule_version: version, skills: hashes, rules_verify: 'ok',
107
+ ...(Number.isFinite(maxAgeMs) && maxAgeMs > 0 ? { max_age_ms: maxAgeMs } : {}) };
108
+ const file = receiptPath(root, home);
109
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
110
+ fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
111
+ return { ok: true, problems: [], receipt, file };
112
+ }
113
+
114
+ /** Marks that Total Recall was consulted just now; read by the repository-sweep policy. */
115
+ export function touchConsult(root, { now = Date.now(), home = os.homedir() } = {}) {
116
+ const file = consultPath(root, home);
117
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
118
+ fs.writeFileSync(file, String(now), { mode: 0o600 });
119
+ }
120
+
121
+ export function lastConsultMs(root, { home = os.homedir() } = {}) {
122
+ try { return Number(fs.readFileSync(consultPath(root, home), 'utf8')) || 0; } catch { return 0; }
123
+ }
@@ -152,6 +152,13 @@ export function auditProjectPluginCopies(projectRoot, { sharedDir = globalPlugin
152
152
  row.sourceVersion = row.sourceDir ? version(row.sourceDir) : null;
153
153
  row.sharedVersion = row.sharedDir ? version(row.sharedDir) : null;
154
154
  row.edited = row.sourceDir ? !sameTree(copyDir, row.sourceDir) : null;
155
+ // A copy declaring an older release than the shared install cannot be a deliberate local edit —
156
+ // it predates the shared plugin. It is a leftover from the release that vendored the defaults into
157
+ // every brain, and because discovery prefers a brain's own copy it SHADOWS the shared install and
158
+ // breaks the repository: a stale auth manifest fails validation, so every total-recall command in
159
+ // that repo dies with "Required plugin auth is missing or invalid". Same staleness rule the updater
160
+ // already applies when it refreshes a vendored copy.
161
+ row.stale = Boolean(row.version && row.sharedVersion && comparePackageVersions(row.version, row.sharedVersion) < 0);
155
162
  copies.push(row);
156
163
  }
157
164
  return { copies, count: copies.length, sourceDir };
@@ -162,10 +162,8 @@ export function loadRuntimeConfig(_configPath) {
162
162
  const checkPaths = [
163
163
  instructionsFilePath(),
164
164
  path.join(surfaceDir(), 'AGENTS.md'),
165
- path.join(surfaceDir(), 'GEMINI.md'),
166
165
  path.join(agentDir, 'INSTRUCTIONS.md'),
167
166
  path.join(agentDir, 'AGENTS.md'),
168
- path.join(agentDir, 'GEMINI.md'),
169
167
  path.join(path.dirname(agentDir), 'INSTRUCTIONS.md'),
170
168
  path.join(path.dirname(agentDir), 'AGENTS.md'),
171
169
  path.join(process.cwd(), 'INSTRUCTIONS.md'),
@@ -173,7 +171,6 @@ export function loadRuntimeConfig(_configPath) {
173
171
  path.join(process.cwd(), 'Claude.md'),
174
172
  // legacy mistaken brainDir locations (kept as last resort)
175
173
  path.join(brainDir, 'INSTRUCTIONS.md'),
176
- path.join(brainDir, 'GEMINI.md'),
177
174
  path.join(brainDir, 'AGENTS.md'),
178
175
  ];
179
176
  for (const p of checkPaths) {
@@ -456,6 +456,11 @@ export function deploySkill(brainDir, skillIdOrPath, opts = {}) {
456
456
  if (layered && fs.existsSync(destDir) && fs.lstatSync(destDir).isSymbolicLink()) {
457
457
  throw new Error(`Refusing layered deploy into symlink: ${destDir}`);
458
458
  }
459
+ if (opts.dryRun) return { success: true, dryRun: true, skillId: resolved.id, destDir,
460
+ sourcePath: resolved.sourcePath, layered, existed, routeCore: Boolean(opts.routeCore), actions: [
461
+ { op: 'replace-managed', from: layered && existed ? path.join(resolved.sourcePath, 'core') : resolved.sourcePath, to: layered && existed ? path.join(destDir, 'core') : destDir },
462
+ ...(opts.routeCore && layered ? [{ op: 'route-core', path: path.join(destDir, 'SKILL.md') }] : []),
463
+ ] };
459
464
  const replacement = layered && existed
460
465
  ? replaceSkillDir(path.join(resolved.sourcePath, 'core'), path.join(destDir, 'core'), {
461
466
  requiredFile: null,
@@ -463,6 +468,14 @@ export function deploySkill(brainDir, skillIdOrPath, opts = {}) {
463
468
  })
464
469
  : replaceSkillDir(resolved.sourcePath, destDir, { managedFiles: priorInstall?.managed_files || [] });
465
470
 
471
+ if (opts.routeCore && layered) {
472
+ const wrapper = path.join(destDir, 'SKILL.md');
473
+ const current = fs.readFileSync(wrapper, 'utf8');
474
+ const marker = '<!-- total-recall:managed-core-route:v1 -->';
475
+ if (!current.includes(marker)) fs.writeFileSync(wrapper, current.replace(/\s*$/, '') +
476
+ `\n\n${marker}\nRead the [maintained portable core](core/SKILL.md) for current protocol contracts; preserve repository-specific instructions above.\n`);
477
+ }
478
+
466
479
  let adaptResult = { adapted: false };
467
480
  if (opts.adapt && (!layered || !existed)) {
468
481
  const openwikiDir =
@@ -536,7 +549,7 @@ function installDetail(entry, inst) {
536
549
  const liveHash = exists ? hashSkillContent(inst.path) : null;
537
550
  const liveCoreHash = layered && exists ? hashSkillLayer(inst.path, 'core') : null;
538
551
  const liveRepoHash = layered && exists ? hashSkillLayer(inst.path, 'repo') : null;
539
- const drift = layered
552
+ const drift = !exists ? true : layered
540
553
  ? liveCoreHash !== (entry?.core_hash || inst.core_hash)
541
554
  : legacyUnderLayered
542
555
  ? false
@@ -548,7 +561,9 @@ function installDetail(entry, inst) {
548
561
  return {
549
562
  ...inst,
550
563
  exists,
564
+ required: !inst.obsolete,
551
565
  live_hash: liveHash,
566
+ verification: !exists ? 'missing' : legacyUnderLayered ? 'not_adopted' : drift ? 'drifted' : 'verified',
552
567
  ...(layered ? {
553
568
  core_hash: liveCoreHash,
554
569
  repo_hash: liveRepoHash,
@@ -576,7 +591,7 @@ export function skillStatus(brainDir, skillId) {
576
591
  } catch (err) {
577
592
  // One unreadable install (a symlinked layer, a moved repo) must not hide
578
593
  // the status of every other install.
579
- return { ...inst, exists: fs.existsSync(inst.path), error: err.message, drift: false };
594
+ return { ...inst, exists: fs.existsSync(inst.path), error: err.message, drift: true, verification: 'error' };
580
595
  }
581
596
  });
582
597
 
@@ -593,7 +608,11 @@ export function skillStatus(brainDir, skillId) {
593
608
  localPath,
594
609
  installs: installDetails,
595
610
  install_count: installDetails.length,
596
- any_drift: installDetails.some((i) => i.drift),
611
+ any_drift: installDetails.some((i) => !i.obsolete && i.drift),
612
+ verified: Boolean(entry && localPath) && installDetails.every((i) => i.obsolete || (i.verification === 'verified' && (!i.contract || i.contract.ok))),
613
+ missing_count: installDetails.filter((i) => !i.obsolete && !i.exists).length,
614
+ obsolete_count: installDetails.filter((i) => i.obsolete).length,
615
+ error_count: installDetails.filter((i) => i.error).length,
597
616
  any_contract_failure: installDetails.some((i) => i.contract && !i.contract.ok),
598
617
  };
599
618
  }
@@ -1269,6 +1288,17 @@ export function syncSkillTwoWay(brainDir, skillId, opts = {}) {
1269
1288
  }
1270
1289
 
1271
1290
  const { entry, locations } = collectSkillLocations(brainDir, skillId);
1291
+ const missingInstalls = loadRegistry(brainDir).installs.filter((inst) =>
1292
+ inst.skill_id === skillId && !inst.obsolete && !fs.existsSync(path.join(inst.path, 'SKILL.md')),
1293
+ );
1294
+ if (missingInstalls.length) {
1295
+ return {
1296
+ skillId, in_sync: false, dryRun,
1297
+ error: 'Recorded skill destinations are missing; reconcile existing repositories and deploy explicitly before synchronization',
1298
+ missing: missingInstalls.map((inst) => ({ path: inst.path, repo: inst.repo, repository_exists: Boolean(inst.repo && fs.existsSync(inst.repo)) })),
1299
+ locations: locations.length, actions: [],
1300
+ };
1301
+ }
1272
1302
  if (!locations.length) {
1273
1303
  return { skillId, skipped: true, reason: 'no live copies found' };
1274
1304
  }
@@ -500,6 +500,30 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
500
500
  const fullContent = prependDirectives(`${mdcHeader}${baseline}`, rulesBlock);
501
501
 
502
502
  try {
503
+ const normShim = path.normalize(shimPath);
504
+ // Antigravity rules deduplication: .agents/rules/AGENTS.md should be a relative
505
+ // symlink to ../../AGENTS.md so Antigravity resolves realpath and loads it once.
506
+ if (normShim.endsWith(path.normalize('.agents/rules/AGENTS.md'))) {
507
+ if (!fs.existsSync(shimDir)) {
508
+ fs.mkdirSync(shimDir, { recursive: true });
509
+ }
510
+ const relTarget = path.join('..', '..', 'AGENTS.md');
511
+ try {
512
+ const stat = fs.lstatSync(shimPath, { throwIfNoEntry: false });
513
+ if (stat) {
514
+ if (!stat.isSymbolicLink() || fs.readlinkSync(shimPath) !== relTarget) {
515
+ fs.unlinkSync(shimPath);
516
+ fs.symlinkSync(relTarget, shimPath);
517
+ }
518
+ } else {
519
+ fs.symlinkSync(relTarget, shimPath);
520
+ }
521
+ return true;
522
+ } catch {
523
+ // Fall through to regular write if symlink cannot be created
524
+ }
525
+ }
526
+
503
527
  const stat = fs.lstatSync(shimPath, { throwIfNoEntry: false });
504
528
  if (stat) {
505
529
  if (stat.isSymbolicLink()) {
@@ -552,7 +576,6 @@ const CLIENT_SHIMS = {
552
576
  roo: ['.roo/rules/total-recall.md', '.clinerules/total-recall.md'],
553
577
  'roo-code': ['.roo/rules/total-recall.md', '.clinerules/total-recall.md'],
554
578
  antigravity: ['AGENTS.md', '.agents/rules/AGENTS.md'],
555
- gemini: ['GEMINI.md', '.agents/rules/GEMINI.md'],
556
579
  codex: ['AGENTS.md'],
557
580
  grok: ['AGENTS.md'],
558
581
  replit: ['replit.md'],
@@ -647,6 +670,20 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
647
670
  // plugin-store.mjs likewise), so the peel was never correct for either.
648
671
  const baseDir = agentDir;
649
672
 
673
+ // Eradicate deprecated Gemini shims so they do not consume context budgets or inject obsolete rules
674
+ const deprecatedGeminiPaths = [
675
+ path.join(baseDir, 'GEMINI.md'),
676
+ path.join(baseDir, '.agent', 'GEMINI.md'),
677
+ path.join(baseDir, '.agents', 'rules', 'GEMINI.md')
678
+ ];
679
+ for (const depPath of deprecatedGeminiPaths) {
680
+ try {
681
+ if (fs.existsSync(depPath)) {
682
+ fs.unlinkSync(depPath);
683
+ }
684
+ } catch {}
685
+ }
686
+
650
687
  // Determine which client shims to write.
651
688
  // `baseDir/.agent/skills/...` only holds for a PROJECT brain. For the global
652
689
  // brain baseDir already IS the agent dir, so that path became
@@ -6,4 +6,44 @@ import { findValidKeyByToken, keyHasAnyScope, recordKeyUsage, recordKeyOutcome }
6
6
  import { logger } from '../core/logger.mjs';
7
7
  const owner = await loadPluginModule('auth', 'server-auth');
8
8
  export const BCRYPT_COST = owner.BCRYPT_COST;
9
- export const { loadSecurityConfig, apiRateLimiter, authRateLimiter, sandboxRateLimiter, ingestRateLimiter, isLoopbackIp, isMeshIp, isLocalRequest, requireHttps, requireAuthOrLocal, corsOptions, requireAuth, requireScope, requireSandboxEnabled, sessionCookieOptions, loginHandler, changePasswordHandler, mintStepUpToken, verifyStepUpToken, verifyDashboardPassword, logoutHandler, signSessionToken, dashboardAuthState } = owner.createServerAuth({ brainDir, authBrainDir: process.env.TR_SECRETS_BRAIN || brainDir, nodeEnv, watchdog, findValidKeyByToken, keyHasAnyScope, recordKeyUsage, recordKeyOutcome, logger });
9
+ const base = owner.createServerAuth({ brainDir, authBrainDir: process.env.TR_SECRETS_BRAIN || brainDir, nodeEnv, watchdog, findValidKeyByToken, keyHasAnyScope, recordKeyUsage, recordKeyOutcome, logger });
10
+ export const { loadSecurityConfig, apiRateLimiter, authRateLimiter, sandboxRateLimiter, ingestRateLimiter, isLoopbackIp, isMeshIp, isLocalRequest, requireHttps, requireAuthOrLocal, corsOptions, requireScope, requireSandboxEnabled, sessionCookieOptions, loginHandler, changePasswordHandler, mintStepUpToken, verifyStepUpToken, verifyDashboardPassword, logoutHandler, signSessionToken, dashboardAuthState } = base;
11
+
12
+ const hostName = (value) => String(value || '').replace(/^https?:\/\//, '').replace(/[/?#].*$/, '').replace(/:\d+$/, '').replace(/^\[(.*)\]$/, '$1');
13
+
14
+ let alwaysRequired = false;
15
+
16
+ /** Headless hosts (the memory-only runtime) never offer the open-local mode. */
17
+ export function enforceLogin() {
18
+ alwaysRequired = true;
19
+ }
20
+
21
+ /**
22
+ * Dashboard login is optional for a local install. It is required when
23
+ * `dashboard.require_login` is true, or, when unset, once a dashboard password
24
+ * exists. When not required, only a genuinely local browser or CLI is let in:
25
+ * loopback socket, loopback Host header and no foreign Origin (which blocks DNS
26
+ * rebinding and cross-site requests). Bearer tokens and every other client
27
+ * (mesh, tunnel, remote) still authenticate normally.
28
+ */
29
+ export function loginRequired() {
30
+ if (alwaysRequired) return true;
31
+ const configured = loadSecurityConfig().dashboard?.require_login;
32
+ return typeof configured === 'boolean' ? configured : dashboardAuthState().configured;
33
+ }
34
+
35
+ function isOpenLocalRequest(req) {
36
+ if (req.headers.authorization) return false;
37
+ if (!isLocalRequest(req)) return false;
38
+ if (!isLoopbackIp(hostName(req.headers.host))) return false;
39
+ const origin = req.headers.origin;
40
+ return !origin || isLoopbackIp(hostName(origin));
41
+ }
42
+
43
+ export function requireAuth(req, res, next) {
44
+ if (!loginRequired() && isOpenLocalRequest(req)) {
45
+ req.auth = { type: 'dashboard_session', local: true };
46
+ return next();
47
+ }
48
+ return base.requireAuth(req, res, next);
49
+ }
@@ -4,7 +4,7 @@ import cors from 'cors';
4
4
  import cookieParser from 'cookie-parser';
5
5
  import fs from 'node:fs';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { requireHttps, corsOptions, apiRateLimiter, requireAuthOrLocal } from './auth.mjs';
7
+ import { requireHttps, corsOptions, apiRateLimiter, requireAuthOrLocal, enforceLogin } from './auth.mjs';
8
8
  import { memoryRouter } from './routes/memory.mjs';
9
9
  import { authRouter } from './routes/auth.mjs';
10
10
  import { keysRouter } from './routes/keys.mjs';
@@ -17,6 +17,7 @@ import { getNodes } from '../core/vault-cache.mjs';
17
17
  const { version } = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8'));
18
18
 
19
19
  export function createMemoryApp() {
20
+ enforceLogin();
20
21
  const app = express();
21
22
  app.disable('x-powered-by');
22
23
  app.set('trust proxy', 'loopback');
@@ -1,7 +1,31 @@
1
1
  /** Required auth plugin adapter for dashboard authentication routes. */
2
+ import express from 'express';
2
3
  import * as auth from '../auth.mjs';
3
4
  import { logger } from '../../core/logger.mjs';
4
5
  import { BRAIN_DIR } from './_shared.mjs';
5
6
  import { loadPluginModule } from '../../core/plugin-runtime.mjs';
6
7
  const { createAuthRouter } = await loadPluginModule('auth', 'auth-router');
7
- export const authRouter = createAuthRouter({ auth, logger, BRAIN_DIR });
8
+ const pluginRouter = createAuthRouter({ auth, logger, BRAIN_DIR });
9
+
10
+ /**
11
+ * Total Recall has no Google sign-in: the plugin serves it for apps that do,
12
+ * so this router refuses those routes and drops Google from provider discovery.
13
+ */
14
+ export const authRouter = express.Router();
15
+ authRouter.use('/auth/google', (_req, res) => res.status(404).json({ error: 'Google sign-in is not available in Total Recall' }));
16
+ authRouter.use('/auth/providers', (req, res, next) => {
17
+ if (req.method !== 'GET') return next();
18
+ const json = res.json.bind(res);
19
+ res.json = (body) => {
20
+ if (body && typeof body === 'object') delete body.google;
21
+ return json(body);
22
+ };
23
+ next();
24
+ });
25
+ authRouter.use('/auth/status', (req, res, next) => {
26
+ if (req.method !== 'GET') return next();
27
+ const json = res.json.bind(res);
28
+ res.json = (body) => json({ ...body, login_required: auth.loginRequired() });
29
+ next();
30
+ });
31
+ authRouter.use(pluginRouter);
@@ -307,15 +307,28 @@ router.patch("/api/plugins/:id/card-face", requireAuth, requireScope("config:wri
307
307
  /**
308
308
  * POST /api/plugins/:id/run body: { subcommand?, args? }
309
309
  * Runs the plugin's CLI handler in a child process. Executing plugin code is a
310
- * write-level action.
310
+ * write-level action unless the installed manifest explicitly declares the
311
+ * exact operation read-only. Alternate execution and credential input remain
312
+ * write-level regardless of that declaration.
311
313
  */
312
- router.post("/api/plugins/:id/run", requireAuth, requireScope("config:write"), async (req, res) => {
314
+ router.post("/api/plugins/:id/run", requireAuth, (req, res, next) => {
315
+ const { subcommand, args = [], stdin, execution } = req.body || {};
316
+ try {
317
+ const plugin = getPluginById(req.params.id, projectRoot());
318
+ const operation = plugin?.valid && plugin.manifest?.cli?.subcommands?.find(item => item.name === subcommand);
319
+ res.locals.executionPlugin = plugin;
320
+ const read = typeof subcommand === 'string' && Array.isArray(args) && args.every(arg => typeof arg === 'string') &&
321
+ operation?.effect === 'read' && stdin === undefined && !execution &&
322
+ !operation.args?.some(arg => arg.secret);
323
+ return requireScope(read ? 'config:read' : 'config:write')(req, res, next);
324
+ } catch (error) { badRequest(res, error.message); }
325
+ }, async (req, res) => {
313
326
  try {
314
327
  const { subcommand = "", args = [], stdin, execution } = req.body || {};
315
328
  if (typeof subcommand !== "string" || !Array.isArray(args) || args.some((a) => typeof a !== "string")) {
316
329
  return badRequest(res, "subcommand must be a string and args an array of strings");
317
330
  }
318
- const plugin = getPluginById(req.params.id, projectRoot());
331
+ const plugin = res.locals.executionPlugin;
319
332
  if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
320
333
  if (!plugin.valid) return badRequest(res, `Plugin ${plugin.id} has an invalid manifest: ${plugin.errors.join("; ")}`);
321
334
  if (!plugin.manifest?.cli?.handler) return badRequest(res, `Plugin ${plugin.id} does not declare a CLI handler`);
@@ -330,7 +343,7 @@ router.post("/api/plugins/:id/run", requireAuth, requireScope("config:write"), a
330
343
  return res.json({ success: result.ok, pluginId: plugin.id, ...result });
331
344
  }
332
345
 
333
- const result = await runPluginCommand(plugin, { subcommand, args, ...(stdin !== undefined ? { stdinText: stdin } : {}), cwd: projectRoot(), secretsBrainDir: process.env.TR_SECRETS_BRAIN || globalBrainDir || BRAIN_DIR });
346
+ const result = await runPluginCommand(plugin, { subcommand, args, ...(stdin !== undefined ? { stdinText: stdin } : {}), cwd: projectRoot(), secretsBrainDir: process.env.TR_SECRETS_BRAIN || globalBrainDir || BRAIN_DIR, maxOutputBytes: 4 * 1024 * 1024 });
334
347
  res.json({ success: result.ok, pluginId: plugin.id, ...result });
335
348
  } catch (err) {
336
349
  badRequest(res, err.message);