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
|
@@ -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 };
|
package/src/core/runtime.mjs
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/src/core/surface.mjs
CHANGED
|
@@ -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
|
package/src/server/auth.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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);
|