total-recall-brain 3.40.3 → 3.40.5

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 (41) hide show
  1. package/package.json +1 -1
  2. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +2 -0
  3. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +26 -0
  4. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +111 -7
  5. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  6. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +5 -0
  7. package/scaffold/.agent/skills/total-recall/plugins/auth/src/agent-login.mjs +127 -0
  8. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-api-rotation.mjs +140 -0
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +31 -0
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +1 -1
  11. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-gateway.mjs +21 -1
  12. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +40 -1
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +9 -12
  14. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +8 -7
  15. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  17. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +8 -7
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +13 -13
  20. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +340 -164
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +6 -5
  22. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +8 -2
  23. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +295 -178
  24. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +1 -1
  25. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +8 -7
  26. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +1 -1
  27. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +302 -168
  29. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +9 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +8 -5
  31. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +444 -27
  32. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +124 -1404
  33. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -2
  34. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -2
  35. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +313 -134
  36. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +8 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  38. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -3
  39. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +287 -12
  40. package/src/core/brain-updates.mjs +4 -1
  41. package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +0 -14
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "main": "cli.mjs",
7
7
  "scripts": {
8
- "test": "node --test test/auth.test.mjs && vitest run --exclude '**/auth.test.mjs'"
8
+ "test": "node --test test/*.test.mjs"
9
9
  },
10
10
  "keywords": [
11
11
  "total-recall-plugin",
@@ -122,6 +122,11 @@
122
122
  "description": "Inspect active session tokens",
123
123
  "effect": "read"
124
124
  },
125
+ {
126
+ "name": "agent",
127
+ "description": "Sign in an agent CLI that authenticates on its own terminal",
128
+ "effect": "destructive"
129
+ },
125
130
  {
126
131
  "name": "secret",
127
132
  "description": "Manage and inspect auth secrets in the Total Recall store",
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Agent CLI sign-in, as an auth-plugin capability.
3
+ *
4
+ * Some agent CLIs authenticate on their own terminal — Antigravity (`agy`) prints a Google consent
5
+ * URL and then blocks reading the authorization code. There was no way to do that through the auth
6
+ * plugin, so it was being hand-rolled beside it and the code ended up nowhere.
7
+ *
8
+ * The pty lives in scripts/agent-login-pty.py, owned by this plugin, because Node has no built-in
9
+ * pty and this plugin carries no pty library. This module is the plugin-facing surface: it resolves
10
+ * the agent, runs the helper, relays its events, and reports a result the CLI can act on.
11
+ *
12
+ * It never claims success from the transcript alone — the helper re-runs the CLI's own login probe
13
+ * and reports `ok` only when the session actually exists.
14
+ */
15
+
16
+ import { spawn } from 'node:child_process';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
21
+ const HELPER = path.join(HERE, '..', 'scripts', 'agent-login-pty.py');
22
+
23
+ /** Only the facts that differ between CLIs are declared. */
24
+ export const AGENT_LOGIN_PROFILES = {
25
+ agy: {
26
+ binary: 'agy',
27
+ consentHost: 'accounts.google.com',
28
+ probeArgs: 'models',
29
+ signedOutPattern: 'please sign in',
30
+ signIn: 'Run `agy` with no arguments, open the URL it prints, and approve with the Google account the agent should use.',
31
+ },
32
+ };
33
+
34
+ export function agentLoginProfile(agent) {
35
+ return AGENT_LOGIN_PROFILES[String(agent || '').trim().toLowerCase()];
36
+ }
37
+
38
+ /**
39
+ * @param {object} options
40
+ * @param {string} options.agent
41
+ * @param {string} [options.code] authorization code if already in hand
42
+ * @param {string} [options.codeFile] path polled for the code (unattended flows)
43
+ * @param {string} [options.urlFile] where to write the consent URL
44
+ * @param {number} [options.timeoutMs]
45
+ * @param {(event: object) => void} [options.onEvent]
46
+ */
47
+ export async function loginAgent({ agent, code, codeFile, urlFile, timeoutMs = 900_000, onEvent = () => {} }) {
48
+ const profile = agentLoginProfile(agent);
49
+ if (!profile) {
50
+ return {
51
+ ok: false,
52
+ agent,
53
+ error: `No sign-in flow is defined for '${agent}'. Known agents: ${Object.keys(AGENT_LOGIN_PROFILES).join(', ') || '(none)'}.`,
54
+ };
55
+ }
56
+
57
+ return new Promise((resolve) => {
58
+ const args = [
59
+ HELPER,
60
+ '--agent', agent,
61
+ '--binary', profile.binary,
62
+ '--consent-host', profile.consentHost,
63
+ '--probe-args', profile.probeArgs,
64
+ '--signed-out-pattern', profile.signedOutPattern,
65
+ '--timeout', String(Math.floor(timeoutMs / 1000)),
66
+ ];
67
+ if (code) args.push('--code', code);
68
+ if (codeFile) args.push('--code-file', codeFile);
69
+ if (urlFile) args.push('--url-file', urlFile);
70
+
71
+ const child = spawn('python3', args, { stdio: ['ignore', 'pipe', 'pipe'] });
72
+ let buffer = '';
73
+ let url = '';
74
+ let sawDone = false;
75
+ let probe = null;
76
+ let helperError = '';
77
+
78
+ child.stdout.on('data', (chunk) => {
79
+ buffer += chunk.toString();
80
+ const lines = buffer.split('\n');
81
+ buffer = lines.pop() || '';
82
+ for (const line of lines) {
83
+ if (!line.trim()) continue;
84
+ let event;
85
+ try {
86
+ event = JSON.parse(line);
87
+ } catch {
88
+ continue;
89
+ }
90
+ onEvent(event);
91
+ if (event.event === 'url') url = event.url || url;
92
+ if (event.event === 'probe') probe = event.probe || probe;
93
+ if (event.event === 'already_signed_in') probe = event.probe || probe;
94
+ if (event.event === 'done') {
95
+ sawDone = true;
96
+ resolve({
97
+ ok: Boolean(event.ok),
98
+ agent,
99
+ url,
100
+ probe,
101
+ alreadySignedIn: Boolean(probe && probe.signed_in),
102
+ reason: event.reason,
103
+ hint: event.ok ? undefined : profile.signIn,
104
+ });
105
+ }
106
+ }
107
+ });
108
+
109
+ child.stderr.on('data', (chunk) => { helperError += chunk.toString(); });
110
+
111
+ child.on('error', (error) => {
112
+ resolve({ ok: false, agent, error: `Could not start the sign-in helper: ${error.message}` });
113
+ });
114
+
115
+ child.on('close', (code_) => {
116
+ if (sawDone) return;
117
+ resolve({
118
+ ok: false,
119
+ agent,
120
+ url,
121
+ probe,
122
+ error: helperError.trim() || `The sign-in helper exited (${code_}) without reporting a result.`,
123
+ hint: profile.signIn,
124
+ });
125
+ });
126
+ });
127
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Provider-API rotation.
3
+ *
4
+ * Some providers let an account mint and revoke its own keys over their API, given a credential
5
+ * with management rights. Until this module existed, no such path existed in the plugin at all:
6
+ * rotation-capability classified keys as `provider_api` and described them as "fully automatic",
7
+ * but rotateAuto sent `provider_api` down the browser path with everything else, so a provider
8
+ * that could rotate itself in one request was instead driven through a console login. A
9
+ * management key therefore had no consumer anywhere in the code.
10
+ *
11
+ * Recipes are data: a provider declares the endpoints and which stored secret holds the
12
+ * management credential (provider-catalog.mjs `rotation.api`). Nothing here is provider-specific
13
+ * beyond those declarations, and no secret value is ever logged or returned.
14
+ */
15
+
16
+ /**
17
+ * @typedef {{
18
+ * create: { method: string, path: string, body?: (label: string) => object, value_path: string, id_path?: string, revoke_path?: string },
19
+ * revoke?: { method: string, path: (id: string, hash: string) => string },
20
+ * probe?: string,
21
+ * }} ProviderApiRecipe
22
+ */
23
+
24
+ /** Read a dotted path out of a response body. */
25
+ function pick(obj, dotted) {
26
+ return String(dotted || '').split('.').reduce((acc, part) => (acc == null ? acc : acc[part]), obj);
27
+ }
28
+
29
+ /**
30
+ * Rotate a key by calling the provider's own key-management API.
31
+ *
32
+ * Order matters and mirrors the supervised browser flow on purpose: mint the new credential, store
33
+ * it, and only then revoke the old one. If revoking happened first and storing failed, the account
34
+ * would be left with a live key nobody holds; this way a failure leaves an extra key rather than
35
+ * none.
36
+ *
37
+ * @param {object} deps
38
+ * @param {string} deps.brainDir
39
+ * @param {string} deps.key stored secret being rotated
40
+ * @param {string} deps.managementKeyName stored secret holding management rights
41
+ * @param {ProviderApiRecipe} deps.recipe
42
+ * @param {string} deps.baseUrl
43
+ * @param {(brain:string, name:string) => Promise<string|null>} deps.readSecret
44
+ * @param {(brain:string, key:string, value:string, opts:object) => Promise<object>} deps.storeSecret
45
+ * @param {(brain:string, name:string) => Promise<object|null>} deps.secretMeta
46
+ * @param {typeof fetch} [deps.fetchImpl]
47
+ */
48
+ export async function rotateViaProviderApi({
49
+ brainDir, key, managementKeyName, recipe, baseUrl,
50
+ readSecret, storeSecret, secretMeta, recordKeyId, fetchImpl = fetch,
51
+ }) {
52
+ const management = await readSecret(brainDir, managementKeyName);
53
+ if (!management) {
54
+ return {
55
+ ok: false,
56
+ error: `No management credential in the store: ${managementKeyName}. `
57
+ + 'This provider rotates itself over its API, so it needs a key with management rights '
58
+ + 'stored under that name; without one there is nothing to authenticate the request.',
59
+ needs: managementKeyName,
60
+ };
61
+ }
62
+
63
+ const auth = { Authorization: `Bearer ${management}` };
64
+ const label = `${key.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${new Date().toISOString().slice(0, 10)}`;
65
+
66
+ let created;
67
+ try {
68
+ const res = await fetchImpl(`${baseUrl}${recipe.create.path}`, {
69
+ method: recipe.create.method || 'POST',
70
+ headers: { ...auth, 'Content-Type': 'application/json' },
71
+ body: JSON.stringify(recipe.create.body ? recipe.create.body(label) : { name: label }),
72
+ });
73
+ const body = await res.json().catch(() => null);
74
+ if (!res.ok) {
75
+ return {
76
+ ok: false,
77
+ error: `Provider refused to mint a key (HTTP ${res.status}). `
78
+ + 'A credential without management rights gets 401 or 403 here, which is the usual cause.',
79
+ status: res.status,
80
+ };
81
+ }
82
+ const value = pick(body, recipe.create.value_path);
83
+ if (!value) return { ok: false, error: `Provider response carried no new key at ${recipe.create.value_path}` };
84
+ const id = recipe.create.id_path ? pick(body, recipe.create.id_path) : null;
85
+ created = { value, id, label };
86
+ } catch (error) {
87
+ return { ok: false, error: `Could not reach the provider API: ${error.message}` };
88
+ }
89
+
90
+ // Store first, revoke second.
91
+ let exports;
92
+ try {
93
+ const result = await storeSecret(brainDir, key, created.value, { actor: 'provider-api-rotation' });
94
+ exports = result && result.exports;
95
+ // Keep the provider's id for the key we just minted, so the next rotation can revoke it.
96
+ if (created.id && recordKeyId) await recordKeyId(brainDir, key, created.id);
97
+ } catch (error) {
98
+ return {
99
+ ok: false,
100
+ error: `Minted a key but could not store it (${error.message}); the old key is untouched `
101
+ + 'and the new one should be revoked by hand.',
102
+ minted: created.label,
103
+ };
104
+ }
105
+
106
+ const revoked = await revokeOld({ auth, recipe, created, secretMeta, brainDir, key, fetchImpl, baseUrl });
107
+
108
+ return {
109
+ ok: true,
110
+ key,
111
+ class: 'provider_api',
112
+ method: 'provider_api',
113
+ label: created.label,
114
+ revoked,
115
+ exports,
116
+ };
117
+ }
118
+
119
+ /** Best-effort revoke of the previous credential; never fails the rotation. */
120
+ async function revokeOld({ auth, recipe, created, secretMeta, brainDir, key, fetchImpl, baseUrl }) {
121
+ if (!recipe.revoke) return { attempted: false, reason: 'provider declares no revoke endpoint' };
122
+ const prior = await secretMeta(brainDir, key);
123
+ const priorId = prior?.provider_key_id || prior?.key_id || null;
124
+ if (!priorId) {
125
+ return {
126
+ attempted: false,
127
+ reason: 'the store does not record the previous key id, so the old key cannot be addressed; '
128
+ + 'revoke it in the provider console',
129
+ };
130
+ }
131
+ try {
132
+ const res = await fetchImpl(`${baseUrl}${recipe.revoke.path(priorId, created.id)}`, {
133
+ method: recipe.revoke.method || 'DELETE',
134
+ headers: auth,
135
+ });
136
+ return res.ok ? { attempted: true, revoked: true } : { attempted: true, revoked: false, status: res.status };
137
+ } catch (error) {
138
+ return { attempted: true, revoked: false, error: error.message };
139
+ }
140
+ }
@@ -15,6 +15,15 @@
15
15
  * class?: 'provider_api' | 'provider_browser' | 'self_generated' | 'manual',
16
16
  * high_risk?: boolean,
17
17
  * manual_keys?: string[],
18
+ * // A provider that lets the account mint and revoke its own keys over its API. The management
19
+ * // credential is a separate stored secret; without it the provider cannot rotate itself, which
20
+ * // is what made 'provider_api' a class with no implementation.
21
+ * management_key?: string,
22
+ * api?: {
23
+ * base_url: string,
24
+ * create: { method?: string, path: string, body?: (label: string) => object, value_path: string, id_path?: string },
25
+ * revoke?: { method?: string, path: (id: string, hash: string) => string },
26
+ * },
18
27
  * },
19
28
  * default_monthly_cap_usd?: number | null,
20
29
  * tiers?: { id: string, label: string, monthly_usd?: number | null }[],
@@ -93,6 +102,28 @@ export const PROVIDER_CATALOG = [
93
102
  env_keys: ['OPENROUTER_API_KEY'],
94
103
  notes: 'OpenAI-compatible base URL https://openrouter.ai/api/v1',
95
104
  },
105
+ // OpenRouter issues and revokes keys over its own API, but only to a credential that carries
106
+ // management rights. The inference key the store already holds cannot do it — the API answers
107
+ // is_management_key:false and 401 on GET /api/v1/keys — so the management credential lives
108
+ // under its own name and this declaration says where to find it.
109
+ rotation: {
110
+ class: 'provider_api',
111
+ management_key: 'OPENROUTER_MANAGEMENT_KEY',
112
+ api: {
113
+ base_url: 'https://openrouter.ai/api/v1',
114
+ create: {
115
+ method: 'POST',
116
+ path: '/keys',
117
+ body: (label) => ({ name: label }),
118
+ value_path: 'key',
119
+ id_path: 'data.hash',
120
+ },
121
+ revoke: {
122
+ method: 'DELETE',
123
+ path: (id) => `/keys/${id}`,
124
+ },
125
+ },
126
+ },
96
127
  default_monthly_cap_usd: 30,
97
128
  tiers: [{ id: 'payg', label: 'Pay as you go', monthly_usd: null }],
98
129
  },
@@ -258,7 +258,7 @@ export const ROTATION_RECIPES = {
258
258
 
259
259
  return null;
260
260
  },
261
- async revokeOld(page, { keepLabels = ['MASTER', 'FESTECH-PROD', 'FESTECH-LIVE', 'ULTRACHAT-PROD', 'GROCRM-PROD', 'JSN-PROD'] } = {}) {
261
+ async revokeOld(page, { keepLabels = ['MASTER', 'PROD', 'LIVE', 'STAGING'] } = {}) {
262
262
  return await page.evaluate(async (labelsToKeep) => {
263
263
  let deleted = 0;
264
264
  const deleteBtns = Array.from(document.querySelectorAll('button[aria-label=Delete]'));
@@ -10,6 +10,7 @@ import fs from 'fs';
10
10
  import path from 'path';
11
11
  import crypto from 'crypto';
12
12
  import os from 'os';
13
+ import { spawnSync } from 'node:child_process';
13
14
  import { encryptSecrets, decryptSecrets, encryptSecretsSync, decryptSecretsSync } from './crypto.mjs';
14
15
  import { DEFAULT_KEYCHAIN_SERVICE, readKeychainPassword } from './secrets-keychain.mjs';
15
16
  import { writeFileSecure, appendFileSecure } from './secure-file.mjs';
@@ -58,6 +59,7 @@ export function secretsPassword({
58
59
  env = process.env,
59
60
  readKeychain = readTrKeychainPassword,
60
61
  readEnvFile = readTrEnvFilePassword,
62
+ readLaunchAgent = readTrLaunchAgentPassword,
61
63
  } = {}) {
62
64
  const fromEnv = env.TR_SECRETS_PASSWORD || env.TR_MASTER_PASSWORD;
63
65
  if (fromEnv) return fromEnv;
@@ -70,7 +72,25 @@ export function secretsPassword({
70
72
  }
71
73
  if (fromKeychain) return fromKeychain;
72
74
  }
73
- return readEnvFile(env) || null;
75
+ return readEnvFile(env) || readLaunchAgent(env) || null;
76
+ }
77
+
78
+ /** Existing owner-only launchd carrier, also usable by noninteractive agents.
79
+ * No password is copied into another store, environment, argv or output.
80
+ */
81
+ export function readTrLaunchAgentPassword(env=process.env,{platform=process.platform,exec=spawnSync}={}) {
82
+ if(platform!=='darwin' || env.TR_SECRETS_NO_HOST_CARRIER==='1')return null;
83
+ const home=env.HOME || (env===process.env?os.homedir():null);
84
+ const file=env.TR_SECRETS_LAUNCH_AGENT || (home?path.join(home,'Library/LaunchAgents/com.totalrecall.brain.plist'):null);
85
+ if(!file)return null;
86
+ try {
87
+ const stat=fs.lstatSync(file),directory=fs.lstatSync(path.dirname(file));
88
+ const uid=process.getuid?.();
89
+ if(!stat.isFile() || !directory.isDirectory() || (stat.mode & 0o077)!==0 || (directory.mode & 0o022)!==0 || (uid!==undefined && (stat.uid!==uid || directory.uid!==uid)))return null;
90
+ const result=exec('/usr/bin/plutil',['-extract','EnvironmentVariables.TR_SECRETS_PASSWORD','raw','-o','-',file],{encoding:'utf8',timeout:5000,maxBuffer:65536});
91
+ if(result.status!==0)return null;
92
+ return (result.stdout || '').replace(/\r?\n$/,'') || null;
93
+ }catch{return null;}
74
94
  }
75
95
 
76
96
  /**
@@ -23,6 +23,7 @@ import { getProvider, providerForKeyName } from './provider-catalog.mjs';
23
23
  import { atomicWrite, safeStringify } from './vault.mjs';
24
24
  import { getRotationPlan, generateSecretValue } from './rotation-capability.mjs';
25
25
  import { getRecipe, valueLooksValid } from './provider-rotation-recipes.mjs';
26
+ import { rotateViaProviderApi } from './provider-api-rotation.mjs';
26
27
  import {
27
28
  launchRotationContext,
28
29
  openConsole,
@@ -440,7 +441,45 @@ export async function rotateAuto(brainDir, key, opts = {}) {
440
441
  return { ok: true, key, class: plan.class, method: 'self_generated', exports: result.exports };
441
442
  }
442
443
 
443
- if (plan.class === 'provider_browser' || plan.class === 'provider_api') {
444
+ // A provider that can mint and revoke its own keys must not be driven through a browser. This
445
+ // branch used to send provider_api down the browser path with everything else, which is why a
446
+ // management credential had no consumer anywhere in the code and a rotation that needed no human
447
+ // instead waited on a console login.
448
+ const provider = row.provider ? getProvider(row.provider) : providerForKeyName(key);
449
+ const api = provider?.rotation?.api;
450
+ if (plan.class === 'provider_api' && api?.create) {
451
+ const attempt = await rotateViaProviderApi({
452
+ brainDir,
453
+ key,
454
+ managementKeyName: provider.rotation.management_key,
455
+ recipe: api,
456
+ baseUrl: api.base_url,
457
+ // getSecret returns an object even for a key it does not hold, so 'truthy' is not the test.
458
+ // Reading a missing management key as present sent the request out with a bogus credential
459
+ // and surfaced the provider's 401, which reads as "wrong key" when the real answer is
460
+ // "no management key is stored at all".
461
+ readSecret: async (brain, name) => {
462
+ try {
463
+ const found = await getSecret(brain, name);
464
+ if (!found || found.found === false) return null;
465
+ const value = found.value ?? found;
466
+ return typeof value === 'string' && value ? value : null;
467
+ } catch { return null; }
468
+ },
469
+ storeSecret: (brain, name, value, o) => rotateSecretAndExport(brain, name, value, {
470
+ actor: o.actor, exportEnv: opts.exportEnv !== false,
471
+ }),
472
+ secretMeta: async (brain, name) => (await listSecretsMeta(brain)).find((r) => r.key === name) || null,
473
+ // Remember which key the provider minted, so the next rotation can revoke this one instead of
474
+ // reporting that the previous credential cannot be addressed.
475
+ recordKeyId: async (brain, name, id) => {
476
+ try { await updateSecretMeta(brain, name, { provider_key_id: id }); } catch { /* metadata is best-effort */ }
477
+ },
478
+ });
479
+ return { ...attempt, class: plan.class, method: 'provider_api' };
480
+ }
481
+
482
+ if (plan.class === 'provider_browser') {
444
483
  return { ...(await rotateViaBrowser(brainDir, key, opts)), class: plan.class, method: 'browser' };
445
484
  }
446
485
 
@@ -7,20 +7,17 @@ import { resolveSecretsPath, loadSecrets, saveSecrets, ensureMeta, appendAudit,
7
7
 
8
8
  const REQUEST_TIMEOUT_MS = 10_000;
9
9
 
10
- function serverPort(override) {
11
- // The leader's port is whatever it advertised on the mesh. Assuming the local
12
- // default made sync fail with "fetch failed" against a leader that was up and
13
- // reachable on a different port (the cloud brain serves on 3900, not 3000).
14
- const candidate = Number(override ?? process.env.TR_SERVER_PORT ?? process.env.PORT ?? 3000);
15
- return Number.isInteger(candidate) && candidate > 0 && candidate <= 65535 ? candidate : 3000;
10
+ function serverPort() {
11
+ const value = Number(process.env.TR_SERVER_PORT || process.env.PORT || 3000);
12
+ return Number.isInteger(value) && value > 0 && value <= 65535 ? value : 3000;
16
13
  }
17
14
 
18
- function meshUrl(ip, route, port) {
15
+ function meshUrl(ip, route) {
19
16
  const octets = String(ip || '').split('.').map(Number);
20
17
  if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255) || octets[0] !== 100 || octets[1] < 64 || octets[1] > 127) {
21
18
  throw new Error('Leader IP is outside the Tailscale CGNAT range');
22
19
  }
23
- return `http://${ip}:${serverPort(port)}${route}`;
20
+ return `http://${ip}:${serverPort()}${route}`;
24
21
  }
25
22
 
26
23
  export function getSecretsChecksum() {
@@ -42,8 +39,8 @@ export function secretsSyncMode(env = process.env) {
42
39
  * anything. The node's store is never replaced wholesale and the node keeps
43
40
  * its own master password.
44
41
  */
45
- export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch, port } = {}) {
46
- const base = meshUrl(leaderIp, '/api/secrets/mesh', port);
42
+ export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch } = {}) {
43
+ const base = meshUrl(leaderIp, '/api/secrets/mesh');
47
44
  const listResponse = await fetchImpl(base, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
48
45
  if (!listResponse.ok) throw new Error(`Leader returned ${listResponse.status}`);
49
46
  const { keys } = await listResponse.json();
@@ -79,6 +76,6 @@ export async function syncLoop() {
79
76
  if (await isLeader()) return;
80
77
  const leaderInfo = await getLeaderInfo();
81
78
  if (!leaderInfo?.ip) return;
82
- try { await syncGrantedSecrets(leaderInfo.ip, { port: leaderInfo.brain_port }); }
83
- catch (err) { logger.error('secrets-sync', 'Failed to sync granted secrets', { error: err.message, leader: leaderInfo.ip, port: leaderInfo.brain_port ?? 'default' }); }
79
+ try { await syncGrantedSecrets(leaderInfo.ip); }
80
+ catch (err) { logger.error('secrets-sync', 'Failed to sync granted secrets', { error: err.message }); }
84
81
  }
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "tr-plugin-code-quality",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-code-quality",
9
- "version": "0.2.1",
9
+ "version": "0.2.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.11.2"
13
13
  },
14
14
  "devDependencies": {
15
- "total-recall-brain": "3.40.1"
15
+ "total-recall-brain": "3.40.4"
16
16
  }
17
17
  },
18
18
  "node_modules/@gregiteen/ssss-cli": {
@@ -2595,9 +2595,9 @@
2595
2595
  }
2596
2596
  },
2597
2597
  "node_modules/total-recall-brain": {
2598
- "version": "3.40.1",
2599
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.1.tgz",
2600
- "integrity": "sha512-tZMnDHXsMT1sa698oF26Np/G4k+Cn4ZXpTdo+BTNOvUMYEhxWR+7Tl+upHLMhcEEPuEM0EiAObL53a9JELzGag==",
2598
+ "version": "3.40.4",
2599
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.4.tgz",
2600
+ "integrity": "sha512-/9JfUWyzXcgHu3kV/rqsOvclMY25PpfW4jSrunsCrkt2MPDUDfGD+z22AoA/OG/jSHE4ZvD/A9QV4IYE2+O99w==",
2601
2601
  "dev": true,
2602
2602
  "license": "MIT",
2603
2603
  "workspaces": [
@@ -2626,7 +2626,8 @@
2626
2626
  "bin": {
2627
2627
  "antigravity": "bin/antigravity.mjs",
2628
2628
  "total-recall": "bin/total-recall.mjs",
2629
- "total-recall-memory": "bin/total-recall-memory.mjs"
2629
+ "total-recall-memory": "bin/total-recall-memory.mjs",
2630
+ "vibebrain": "bin/total-recall.mjs"
2630
2631
  },
2631
2632
  "optionalDependencies": {
2632
2633
  "playwright": "^1.60.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-code-quality",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Total Recall code-quality plugin: shared gate runner core and per-repo gate configuration.",
6
6
  "license": "MIT",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "code-quality",
4
4
  "name": "Code Quality Gates",
5
- "version": "0.2.1",
5
+ "version": "0.2.2",
6
6
  "description": "Fail-closed deterministic code-quality gates, lint rules, and test compliance reporting.",
7
7
  "author": "Total Recall contributors",
8
8
  "license": "MIT",
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "tr-plugin-composable-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-composable-cli",
9
- "version": "0.2.1",
9
+ "version": "0.2.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.11.2"
13
13
  },
14
14
  "devDependencies": {
15
- "total-recall-brain": "3.40.1"
15
+ "total-recall-brain": "3.40.4"
16
16
  }
17
17
  },
18
18
  "node_modules/@gregiteen/ssss-cli": {
@@ -2595,9 +2595,9 @@
2595
2595
  }
2596
2596
  },
2597
2597
  "node_modules/total-recall-brain": {
2598
- "version": "3.40.1",
2599
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.1.tgz",
2600
- "integrity": "sha512-tZMnDHXsMT1sa698oF26Np/G4k+Cn4ZXpTdo+BTNOvUMYEhxWR+7Tl+upHLMhcEEPuEM0EiAObL53a9JELzGag==",
2598
+ "version": "3.40.4",
2599
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.4.tgz",
2600
+ "integrity": "sha512-/9JfUWyzXcgHu3kV/rqsOvclMY25PpfW4jSrunsCrkt2MPDUDfGD+z22AoA/OG/jSHE4ZvD/A9QV4IYE2+O99w==",
2601
2601
  "dev": true,
2602
2602
  "license": "MIT",
2603
2603
  "workspaces": [
@@ -2626,7 +2626,8 @@
2626
2626
  "bin": {
2627
2627
  "antigravity": "bin/antigravity.mjs",
2628
2628
  "total-recall": "bin/total-recall.mjs",
2629
- "total-recall-memory": "bin/total-recall-memory.mjs"
2629
+ "total-recall-memory": "bin/total-recall-memory.mjs",
2630
+ "vibebrain": "bin/total-recall.mjs"
2630
2631
  },
2631
2632
  "optionalDependencies": {
2632
2633
  "playwright": "^1.60.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-composable-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Total Recall composable CLI plugin: create, compose, register, test and safely run custom verbs; every command listed in the instruction surfaces.",
6
6
  "scripts": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "composable-cli",
4
4
  "name": "Composable CLI",
5
- "version": "0.2.1",
5
+ "version": "0.2.2",
6
6
  "description": "Dynamic agent command composition engine with pipeline registration, versioning, and execution.",
7
7
  "author": "Total Recall contributors",
8
8
  "license": "MIT",
@@ -118,30 +118,30 @@
118
118
  }
119
119
  ],
120
120
  "card_face": {
121
- "skin": "studio-rack",
121
+ "skin": "cyber-forge",
122
122
  "theme": "vibrant",
123
123
  "headline": "Dynamic Agent Command Composition",
124
- "primary_color": "#14b8a6",
125
- "secondary_color": "#134e4a",
126
- "accent_color": "#2dd4bf",
124
+ "primary_color": "#0891b2",
125
+ "secondary_color": "#083344",
126
+ "accent_color": "#06b6d4",
127
127
  "touch_layout": "comfortable",
128
128
  "interactive": true,
129
129
  "quick_actions": [
130
130
  {
131
- "subcommand": "status",
132
- "label": "STATUS"
131
+ "subcommand": "library",
132
+ "label": "LIBRARY"
133
133
  },
134
134
  {
135
- "subcommand": "create",
136
- "label": "CREATE"
135
+ "subcommand": "list",
136
+ "label": "LIST"
137
137
  },
138
138
  {
139
- "subcommand": "read",
140
- "label": "READ"
139
+ "subcommand": "show",
140
+ "label": "SHOW"
141
141
  },
142
142
  {
143
- "subcommand": "update",
144
- "label": "UPDATE"
143
+ "subcommand": "history",
144
+ "label": "HISTORY"
145
145
  }
146
146
  ]
147
147
  },