total-recall-brain 3.40.5 → 3.40.7
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/package.json +1 -1
- package/src/cli/init.mjs +118 -2
- package/src/server/keys.mjs +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.40.
|
|
3
|
+
"version": "3.40.7",
|
|
4
4
|
"description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/init.mjs
CHANGED
|
@@ -198,6 +198,95 @@ function printHelp() {
|
|
|
198
198
|
`);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// ─── Per-brain state exclusions ─────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Brain-local runtime state that must never be committed.
|
|
205
|
+
*
|
|
206
|
+
* `config/brain.json` holds a live plaintext PAT, so a repository that tracks
|
|
207
|
+
* it publishes a working credential on every commit. `secrets.enc` is the
|
|
208
|
+
* encrypted store, the `*.pid`/`leader.json` files name local processes, and
|
|
209
|
+
* `keys.jsonl` is the auth ledger. None of it is source: `init` and `brain
|
|
210
|
+
* ensure` rebuild it, and a teammate or CI clone has no business receiving it.
|
|
211
|
+
*
|
|
212
|
+
* Only the PROJECT layer needs this. The global brain lives in `~/.agent`,
|
|
213
|
+
* which is not a repository. Returns the exclusions it added.
|
|
214
|
+
*/
|
|
215
|
+
export function ensureProjectGitignore(projectRoot, { dryRun = false } = {}) {
|
|
216
|
+
const header = '# Total Recall brain state (machine-local: holds a live PAT)';
|
|
217
|
+
const entries = [
|
|
218
|
+
'.agent/skills/total-recall/config/brain.json',
|
|
219
|
+
'.agent/skills/total-recall/config/brain.json.bak-*',
|
|
220
|
+
'.agent/skills/total-recall/config/keys.jsonl',
|
|
221
|
+
'.agent/skills/total-recall/config/secrets.enc',
|
|
222
|
+
'.agent/skills/total-recall/config/secrets.enc.bak-*',
|
|
223
|
+
'.agent/skills/total-recall/config/leader.json',
|
|
224
|
+
'.agent/skills/total-recall/*.pid',
|
|
225
|
+
];
|
|
226
|
+
const file = path.join(projectRoot, '.gitignore');
|
|
227
|
+
let current = '';
|
|
228
|
+
try { current = fs.readFileSync(file, 'utf8'); } catch { /* absent: create it */ }
|
|
229
|
+
const missing = entries.filter(entry => !current.split(/\r?\n/).includes(entry));
|
|
230
|
+
if (!missing.length) return [];
|
|
231
|
+
if (dryRun) return missing;
|
|
232
|
+
const block = `${current && !current.endsWith('\n') ? '\n' : ''}${current ? '\n' : ''}${header}\n${missing.join('\n')}\n`;
|
|
233
|
+
fs.writeFileSync(file, current + block, 'utf8');
|
|
234
|
+
return missing;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Bootstrap token store resolution ───────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The brain directory whose key store the server answering `brainUrl` reads.
|
|
241
|
+
*
|
|
242
|
+
* A server process derives its store from `AGENT_DIR` alone (`core/config.mjs`
|
|
243
|
+
* → `brainDir`), and the launchd service starts it with the GLOBAL agent dir.
|
|
244
|
+
* So a token written into a project brain's `brain.json` is only usable when
|
|
245
|
+
* it was minted into that global store. Minting into the project's own store
|
|
246
|
+
* produced a token no running server would accept, while init still reported
|
|
247
|
+
* success — the failure surfaced later as `startup check` reporting
|
|
248
|
+
* `brain failed` (`instructions-http-401`) for every project.
|
|
249
|
+
*
|
|
250
|
+
* Loopback URLs therefore resolve to the store the local server reads. A
|
|
251
|
+
* remote brain's store is not reachable from here, so the target brain keeps
|
|
252
|
+
* its own store.
|
|
253
|
+
*
|
|
254
|
+
* Two constraints pull against each other and both must hold:
|
|
255
|
+
* 1. `AGENT_DIR` (and the `_TR_TEST_AGENT_DIR` fixture override) honours an
|
|
256
|
+
* explicitly selected root, so the serving store must not be derived by
|
|
257
|
+
* blindly re-reading it — when it is pinned at the project the two stores
|
|
258
|
+
* collapse and the bug returns untouched.
|
|
259
|
+
* 2. An explicit root owns its whole layout: init must never create
|
|
260
|
+
* `$HOME/.agent` behind a caller that named a different root (a test, or
|
|
261
|
+
* an isolated install). So `globalBrainDir` is used only when it is
|
|
262
|
+
* genuinely distinct AND already exists; otherwise the target store is
|
|
263
|
+
* kept rather than a new one being created.
|
|
264
|
+
*/
|
|
265
|
+
export function resolveServingBrainDir({ brainDir, isProject, brainUrl, globalBrainDir = null }) {
|
|
266
|
+
if (!isProject) return brainDir;
|
|
267
|
+
let loopback = false;
|
|
268
|
+
try {
|
|
269
|
+
const host = new URL(brainUrl).hostname;
|
|
270
|
+
loopback = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(host);
|
|
271
|
+
} catch { /* unparseable URL: treat as remote and keep the target store */ }
|
|
272
|
+
if (!loopback) return brainDir;
|
|
273
|
+
// `globalBrainDir` is the caller's explicit answer and is preferred. Only
|
|
274
|
+
// when it names nothing usable do we consider the home-relative global
|
|
275
|
+
// brain — and never when an explicit root owns the layout, because that
|
|
276
|
+
// root's server is the one being served and $HOME belongs to someone else.
|
|
277
|
+
const explicitRoot = Boolean(process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR);
|
|
278
|
+
const candidates = [globalBrainDir];
|
|
279
|
+
if (!explicitRoot) candidates.push(path.join(os.homedir(), '.agent', 'skills', 'total-recall'));
|
|
280
|
+
for (const candidate of candidates) {
|
|
281
|
+
if (!candidate || candidate === brainDir) continue;
|
|
282
|
+
// Only adopt a store that is already established; adopting a missing one
|
|
283
|
+
// would make init responsible for creating it, which constraint 2 forbids.
|
|
284
|
+
if (fs.existsSync(path.join(candidate, 'config', 'keys.jsonl'))
|
|
285
|
+
|| fs.existsSync(path.join(candidate, 'config', '.ssss'))) return candidate;
|
|
286
|
+
}
|
|
287
|
+
return brainDir;
|
|
288
|
+
}
|
|
289
|
+
|
|
201
290
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
202
291
|
|
|
203
292
|
export default async function init(args) {
|
|
@@ -207,6 +296,10 @@ export default async function init(args) {
|
|
|
207
296
|
const cwd = process.cwd();
|
|
208
297
|
const globalAgentDir = getGlobalAgentDir();
|
|
209
298
|
const globalBrainDir = path.join(globalAgentDir, 'skills', 'total-recall');
|
|
299
|
+
// The HOME-relative global brain, which is what a background server reads.
|
|
300
|
+
// `globalAgentDir` above honours AGENT_DIR and can therefore point at the
|
|
301
|
+
// project being initialised; the serving store must not be derived from it.
|
|
302
|
+
const servingGlobalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
|
|
210
303
|
|
|
211
304
|
// Determine target based on --project flag
|
|
212
305
|
const isProject = opts.project;
|
|
@@ -705,12 +798,26 @@ export default async function init(args) {
|
|
|
705
798
|
|
|
706
799
|
if (needsBootstrap) {
|
|
707
800
|
try {
|
|
708
|
-
const {
|
|
709
|
-
const keyData = issueKey('Default Local Developer Key', { scopes: ['*'] });
|
|
801
|
+
const { keyStoreFor } = await import('../server/keys.mjs');
|
|
710
802
|
brainCfg.url = brainCfg.url || 'http://localhost:3000';
|
|
803
|
+
// Mint the bootstrap token into the store the SERVER reads, which is
|
|
804
|
+
// the store under `AGENT_DIR` — not necessarily the brain this init run
|
|
805
|
+
// targets. A project brain's token is validated by whatever server
|
|
806
|
+
// process answers `brainCfg.url`, and that process resolves its store
|
|
807
|
+
// from `AGENT_DIR` (launchd starts it with the global one). Issuing
|
|
808
|
+
// into the project's own store wrote a token no running server could
|
|
809
|
+
// match: init reported "pre-authorized" and every request answered 401,
|
|
810
|
+
// so `startup check` said `brain failed` on project after project.
|
|
811
|
+
// When the target IS the store the server reads this is a no-op branch.
|
|
812
|
+
const servingBrainDir = resolveServingBrainDir({ brainDir, isProject, brainUrl: brainCfg.url, globalBrainDir: servingGlobalBrainDir });
|
|
813
|
+
const { issueKey } = keyStoreFor(servingBrainDir);
|
|
814
|
+
const keyData = issueKey('Default Local Developer Key', { scopes: ['*'] });
|
|
711
815
|
brainCfg.token = brainCfg.token || keyData.token;
|
|
712
816
|
writeFileSecure(brainJsonPath, JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
|
|
713
817
|
logOk(`Bootstrap configuration successfully generated and pre-authorized at ${brainCfg.url}`);
|
|
818
|
+
if (isProject && servingBrainDir !== brainDir) {
|
|
819
|
+
logOk(`Bootstrap token issued to the serving brain store at ${servingBrainDir}`);
|
|
820
|
+
}
|
|
714
821
|
if (brainCfg.project_id) {
|
|
715
822
|
logOk(`Assigned project UUID ${brainCfg.project_id} to project brain`);
|
|
716
823
|
}
|
|
@@ -896,6 +1003,15 @@ export default async function init(args) {
|
|
|
896
1003
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
897
1004
|
logOk(`Registered project "${projectName}" in global brain registry`);
|
|
898
1005
|
|
|
1006
|
+
// Keep brain state out of git. `brain.json` carries a live PAT, so a
|
|
1007
|
+
// repo that tracks it publishes a working credential on every commit.
|
|
1008
|
+
try {
|
|
1009
|
+
const added = ensureProjectGitignore(cwd, { dryRun: opts.dryRun });
|
|
1010
|
+
if (added.length) logOk(`Excluded ${added.length} brain-state path(s) in .gitignore`);
|
|
1011
|
+
} catch (ignoreErr) {
|
|
1012
|
+
logWarn(`Could not update .gitignore: ${ignoreErr.message}`);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
899
1015
|
// Auto-sync all .md files from the repo into the vault
|
|
900
1016
|
try {
|
|
901
1017
|
const { syncSingleRepo } = await import('../core/repo-sync.mjs');
|
package/src/server/keys.mjs
CHANGED
|
@@ -2,4 +2,30 @@
|
|
|
2
2
|
import { brainDir } from '../core/config.mjs';
|
|
3
3
|
import { loadPluginModule } from '../core/plugin-runtime.mjs';
|
|
4
4
|
const { createKeyStore } = await loadPluginModule('auth', 'access-keys');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The default store is the one under `AGENT_DIR`. That is the store a server
|
|
8
|
+
* process reads, because `core/config.mjs` derives `brainDir` from
|
|
9
|
+
* `AGENT_DIR` alone. Anything that mints a token a *server* must accept has to
|
|
10
|
+
* use this store — see `keyStoreFor`.
|
|
11
|
+
*/
|
|
5
12
|
export const { ALL_SCOPES, KNOWN_SCOPES, normalizeScopes, isExpiredKey, keyHasScope, keyHasAnyScope, loadKeys, issueKey, revokeKey, recordKeyUsage, isValidToken, findValidKeyByToken, loadRoles, setRole, listKeyUsage, recordKeyOutcome } = createKeyStore({ brainDir });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A key store bound to an explicit brain directory.
|
|
16
|
+
*
|
|
17
|
+
* `init` writes a bootstrap PAT into a brain's `config/brain.json`, and the
|
|
18
|
+
* server that answers that brain's URL validates it against whatever store its
|
|
19
|
+
* own `AGENT_DIR` produced. When the two differ, `init` used to mint the token
|
|
20
|
+
* into a store no running server reads: it printed "pre-authorized", and every
|
|
21
|
+
* request with that token then answered 401. Callers that need a token the
|
|
22
|
+
* server will accept resolve the serving brain first and pass it here.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} targetBrainDir absolute brain directory (…/skills/total-recall)
|
|
25
|
+
*/
|
|
26
|
+
export function keyStoreFor(targetBrainDir) {
|
|
27
|
+
if (typeof targetBrainDir !== 'string' || !targetBrainDir) {
|
|
28
|
+
throw new Error('Explicit auth key store brain directory required');
|
|
29
|
+
}
|
|
30
|
+
return createKeyStore({ brainDir: targetBrainDir });
|
|
31
|
+
}
|