total-recall-brain 3.30.0 → 3.30.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.30.0",
3
+ "version": "3.30.1",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -181,6 +181,10 @@ npx total-recall compile
181
181
  ```
182
182
  Synchronously rebuild the entire memory index (derived embeddings) and regenerate prompt instruction shims (such as `INSTRUCTIONS.md`, `GEMINI.md`, `AGENTS.md`, etc.).
183
183
 
184
+ * **Rules are global + local.** Every project's shims carry the global brain's invariants, preferences and corrections plus the project's own; a project node with the same slug overrides the global one.
185
+ * `compile --global` (what `remember --global` runs) and `compile --all` also recompile every registered project, so a global rule reaches every repo at once.
186
+ * A client gets a shim only once it is connected (`total-recall connect claude-code|codex|antigravity|…` in that repo). No `CLAUDE.md` in a repo means Claude Code was never connected there.
187
+
184
188
  ### 5. Ingest OKF Bundle
185
189
  ```bash
186
190
  npx total-recall ingest okf <path> [options]
@@ -394,11 +398,23 @@ transcript. `set`, `rotate` and `rekey` all accept it.
394
398
  ### Master password and `rekey`
395
399
 
396
400
  The store is AES-256-GCM; the key is scrypt-derived (N=2^16, ~64 MB) from
397
- `TR_SECRETS_PASSWORD`. On macOS the password belongs in the **Keychain**
398
- (`security add-generic-password -s total-recall-secrets`), not in a dotfile —
399
- `rekey` picks the Keychain entry up automatically and rotates every carrier
400
- (LaunchAgent plist, env file, Keychain) in one transaction, verifying the
401
- retired password no longer decrypts before it commits.
401
+ the master password, resolved in this order:
402
+
403
+ 1. `TR_SECRETS_PASSWORD` / `TR_MASTER_PASSWORD` in the environment (a
404
+ LaunchAgent plist injects it for launchd jobs);
405
+ 2. macOS: the **Keychain** entry `total-recall-secrets`
406
+ (`security add-generic-password -s total-recall-secrets`). An ssh or mesh
407
+ session cannot read a locked login Keychain;
408
+ 3. Linux and headless hosts: the env file `~/.agent/tr.env` (or `TR_ENV_FILE`)
409
+ containing `export TR_SECRETS_PASSWORD='…'`. It is used only when it is a
410
+ regular file, mode 0600, owned by you, in a directory (keep `~/.agent` 0700)
411
+ nobody else can write. Do not also export it from `.bashrc` — every process
412
+ started from the shell would inherit it.
413
+
414
+ `rekey` rotates every carrier (LaunchAgent plist, `--env-file`, Keychain) in one
415
+ transaction, verifying the retired password no longer decrypts before it
416
+ commits. No carrier on a machine means every command fails with "Secrets file
417
+ is not valid JSON" and `mesh` falls back to local discovery.
402
418
 
403
419
  > **Processes hold the old password until restarted.** After a rekey, anything
404
420
  > still running with the retired value fails with `Failed to decrypt secrets
@@ -584,7 +600,7 @@ npm test # vitest run — whole suite
584
600
  npx vitest run <path> # one spec file
585
601
  ```
586
602
 
587
- Current baseline: **288 spec files / 1493 tests, ~160 s** on the Mac Mini.
603
+ Current baseline (3.30.1): **349 spec files / 2024 tests, ~160 s** on the Mac Mini.
588
604
 
589
605
  > **Never run the full suite, a full typecheck, or a production build on the
590
606
  > laptop.** These go to the Mac Mini or the droplet — local runs cause slowdowns
@@ -369,10 +369,19 @@ function apiDetails(opts) {
369
369
  models_url: `${base}/v1/models`,
370
370
  ssss_manifest_url: `${base}/api/ssss`,
371
371
  discovery_url: `${base}/.well-known/total-recall.json`,
372
- authorization_header: opts.token ? `Authorization: Bearer ${opts.token}` : 'Authorization: Bearer <PAT>'
372
+ // Never the token itself: this goes to stdout and --json, where it lands in
373
+ // terminal scrollback, CI logs and agent transcripts. Clients read the
374
+ // token from brain.json (0600).
375
+ authorization_header: opts.token ? `Authorization: Bearer ${maskToken(opts.token)}` : 'Authorization: Bearer <PAT>'
373
376
  };
374
377
  }
375
378
 
379
+ export function maskToken(token) {
380
+ const value = String(token || '');
381
+ if (value.length <= 8) return '<PAT>';
382
+ return `${value.slice(0, 6)}… (full token in config/brain.json)`;
383
+ }
384
+
376
385
  function detectObsidianVault() {
377
386
  // macOS: Obsidian stores vault paths in its app support config
378
387
  const obsidianConfig = path.join(
@@ -43,7 +43,8 @@ export function resolveUsagePath(brainDir) {
43
43
  /**
44
44
  * The master password: the environment first, then — on macOS — the Keychain
45
45
  * entry `secret rekey` already maintains (service `total-recall-secrets`,
46
- * overridable with TR_SECRETS_KEYCHAIN_SERVICE).
46
+ * overridable with TR_SECRETS_KEYCHAIN_SERVICE), then the host env file
47
+ * (see readTrEnvFilePassword).
47
48
  *
48
49
  * Only an interactive shell profile used to read the Keychain, so a
49
50
  * non-interactive process on the same Mac — a mesh `exec`, a release script —
@@ -52,13 +53,56 @@ export function resolveUsagePath(brainDir) {
52
53
  * disables the fallback.
53
54
  */
54
55
  let keychainPassword;
55
- export function secretsPassword({ env = process.env, readKeychain = readTrKeychainPassword } = {}) {
56
+ export function secretsPassword({
57
+ env = process.env,
58
+ readKeychain = readTrKeychainPassword,
59
+ readEnvFile = readTrEnvFilePassword,
60
+ } = {}) {
56
61
  const fromEnv = env.TR_SECRETS_PASSWORD || env.TR_MASTER_PASSWORD;
57
62
  if (fromEnv) return fromEnv;
58
- if (env.TR_SECRETS_NO_KEYCHAIN === '1') return null;
59
- if (readKeychain !== readTrKeychainPassword) return readKeychain(env) || null;
60
- if (keychainPassword === undefined) keychainPassword = readKeychain(env) || null;
61
- return keychainPassword;
63
+ if (env.TR_SECRETS_NO_KEYCHAIN !== '1') {
64
+ let fromKeychain;
65
+ if (readKeychain !== readTrKeychainPassword) fromKeychain = readKeychain(env) || null;
66
+ else {
67
+ if (keychainPassword === undefined) keychainPassword = readKeychain(env) || null;
68
+ fromKeychain = keychainPassword;
69
+ }
70
+ if (fromKeychain) return fromKeychain;
71
+ }
72
+ return readEnvFile(env) || null;
73
+ }
74
+
75
+ /**
76
+ * The host's env-file carrier: `TR_ENV_FILE`, else `~/.agent/tr.env` — the file
77
+ * `auto-pull.sh` sources and `secret rekey --env-file` rotates. Only a shell
78
+ * that sources it used to see the password, so a non-interactive process on a
79
+ * Linux host (an agent's tool shell, cron, a systemd unit) could not open the
80
+ * store its own user owns. The file is honoured only when it is a regular file
81
+ * (not a symlink) that this user owns and no one else can read or write, in a
82
+ * directory this user owns that no one else can write to.
83
+ */
84
+ export function readTrEnvFilePassword(env = process.env) {
85
+ const file = env.TR_ENV_FILE || path.join(env.HOME || os.homedir(), '.agent', 'tr.env');
86
+ try {
87
+ // lstat: a symlink could point the carrier at a file someone else controls.
88
+ const stat = fs.lstatSync(file);
89
+ if (!stat.isFile() || (stat.mode & 0o077) !== 0) return null;
90
+ // A directory others can write to lets them replace the file.
91
+ const dir = fs.statSync(path.dirname(file));
92
+ if ((dir.mode & 0o022) !== 0) return null;
93
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
94
+ if (uid !== null && (stat.uid !== uid || dir.uid !== uid)) return null;
95
+ const line = fs
96
+ .readFileSync(file, 'utf8')
97
+ .split(/\r?\n/)
98
+ .find((entry) => /^\s*(?:export\s+)?TR_SECRETS_PASSWORD\s*=/.test(entry));
99
+ if (!line) return null;
100
+ let value = line.slice(line.indexOf('=') + 1).trim();
101
+ if (value.length >= 2 && /^(['"]).*\1$/.test(value)) value = value.slice(1, -1);
102
+ return value || null;
103
+ } catch {
104
+ return null;
105
+ }
62
106
  }
63
107
 
64
108
  function readTrKeychainPassword(env) {