total-recall-brain 3.36.0 → 3.37.0

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.
@@ -29,8 +29,14 @@ export const HARNESS_SPECS = {
29
29
  name: 'Claude Code CLI',
30
30
  binary: 'claude',
31
31
  category: 'code_engineering',
32
- defaultFlags: ['--output-format', 'json', '--permission-mode', 'bypassPermissions', '--setting-sources', 'local', '--tools', '', '-p'],
32
+ defaultFlags: ['--output-format', 'json', '--permission-mode', 'bypassPermissions', '-p'],
33
33
  execType: 'flag_last',
34
+ // Options the harness accepts for the built-in tool set and settings layers.
35
+ toolsFlag: '--tools',
36
+ settingSourcesFlag: '--setting-sources',
37
+ // Headless subscription auth (`claude setup-token`), read from the secret
38
+ // store when the caller's environment does not already carry it.
39
+ authSecrets: ['CLAUDE_CODE_OAUTH_TOKEN'],
34
40
  description: 'Deep codebase editing, refactoring, and Unix command execution.'
35
41
  },
36
42
  codex: {
@@ -62,6 +68,79 @@ export const HARNESS_SPECS = {
62
68
  }
63
69
  };
64
70
 
71
+ /**
72
+ * Normalise a caller's tool selection.
73
+ * undefined | null | 'default' | 'all' -> null (the harness's full built-in set)
74
+ * 'none' | '' -> '' (no tools)
75
+ * 'Bash,Read' | ['Bash', 'Read'] -> 'Bash,Read'
76
+ */
77
+ export function normalizeToolSelection(tools) {
78
+ if (tools === undefined || tools === null) return null;
79
+ const list = (Array.isArray(tools) ? tools : String(tools).split(','))
80
+ .map((t) => String(t).trim())
81
+ .filter(Boolean);
82
+ if (list.length === 0 || (list.length === 1 && list[0] === 'none')) return '';
83
+ if (list.length === 1 && (list[0] === 'default' || list[0] === 'all')) return null;
84
+ return list.join(',');
85
+ }
86
+
87
+ /**
88
+ * Argument vector for a harness, before the task prompt.
89
+ * `tools` and `settingSources` are only accepted by harnesses that declare the
90
+ * matching flag; asking another harness for them is an error, not a silent no-op.
91
+ */
92
+ export function buildHarnessArgs(spec, { tools, settingSources } = {}) {
93
+ const args = [...spec.defaultFlags];
94
+ const extra = [];
95
+ const selection = normalizeToolSelection(tools);
96
+ if (selection !== null) {
97
+ if (!spec.toolsFlag) throw new Error(`Harness "${spec.name}" does not support choosing its tool set.`);
98
+ extra.push(spec.toolsFlag, selection);
99
+ }
100
+ if (settingSources) {
101
+ if (!spec.settingSourcesFlag) throw new Error(`Harness "${spec.name}" does not support --setting-sources.`);
102
+ extra.push(spec.settingSourcesFlag, String(settingSources));
103
+ }
104
+ if (!extra.length) return args;
105
+ // Prompt-mode flags such as `-p` stay last so the task follows them.
106
+ const last = args[args.length - 1];
107
+ return spec.execType === 'flag_last' && last && last.startsWith('-')
108
+ ? [...args.slice(0, -1), ...extra, last]
109
+ : [...args, ...extra];
110
+ }
111
+
112
+ /** POSIX single-quote a value for a remote shell command line. */
113
+ export function shellQuote(value) {
114
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
115
+ }
116
+
117
+ async function readStoreSecret(key) {
118
+ const { getSecret, defaultBrainDir } = await import('./secrets-store.mjs');
119
+ const res = await getSecret(defaultBrainDir(), key, { actor: 'agent-harness', action: 'harness-auth' });
120
+ return res.found ? res.value : null;
121
+ }
122
+
123
+ /**
124
+ * Environment for a harness process. Headless auth secrets the harness declares
125
+ * (for Claude Code, CLAUDE_CODE_OAUTH_TOKEN) are filled from the secret store
126
+ * when the base environment lacks them, so a session without a login keychain
127
+ * (SSH, launchd, cron) can still authenticate. A store that cannot be opened is
128
+ * not fatal: the harness falls back to its own login. Values are never logged.
129
+ */
130
+ export async function resolveHarnessEnv(spec, baseEnv = process.env, { readSecret = readStoreSecret } = {}) {
131
+ const env = { ...baseEnv };
132
+ for (const key of spec.authSecrets || []) {
133
+ if (env[key]) continue;
134
+ try {
135
+ const value = await readSecret(key);
136
+ if (value) env[key] = value;
137
+ } catch {
138
+ // Store locked or missing; leave auth to the harness.
139
+ }
140
+ }
141
+ return env;
142
+ }
143
+
65
144
  /**
66
145
  * Detect all installed and operational harnesses on this computer.
67
146
  */
@@ -94,6 +173,12 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
94
173
  }
95
174
 
96
175
  const timeoutMs = options.timeoutMs || 180000; // 3 min default
176
+ // A dispatch is a single answer, not a workflow: tool-less and isolated from
177
+ // user/project settings unless the caller asks otherwise.
178
+ const harnessArgs = buildHarnessArgs(spec, {
179
+ tools: options.tools !== undefined ? options.tools : (spec.toolsFlag ? 'none' : undefined),
180
+ settingSources: options.settingSources !== undefined ? options.settingSources : (spec.settingSourcesFlag ? 'local' : undefined),
181
+ });
97
182
 
98
183
  // Remote mesh execution branch
99
184
  if (options.node) {
@@ -111,10 +196,11 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
111
196
  message: `Dispatching to ${spec.name} remotely on mesh node "${options.node}" (${targetNode.ip})...`
112
197
  });
113
198
 
114
- const flags = spec.defaultFlags.join(' ');
199
+ const flags = harnessArgs.map(shellQuote).join(' ');
200
+ const cd = options.cwd ? `cd ${shellQuote(options.cwd)} && ` : '';
115
201
  const remoteCmd = spec.execType === 'pipe_stdin'
116
- ? `echo ${JSON.stringify(taskPrompt)} | ${spec.binary} ${flags}`
117
- : `${spec.binary} ${flags} ${JSON.stringify(taskPrompt)}`;
202
+ ? `${cd}printf '%s' ${shellQuote(taskPrompt)} | ${spec.binary} ${flags}`
203
+ : `${cd}${spec.binary} ${flags} ${shellQuote(taskPrompt)}`;
118
204
  const execResult = await execMeshCommand(options.node, remoteCmd, {
119
205
  vaultRoot: options.vaultRoot,
120
206
  timeoutMs,
@@ -140,7 +226,8 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
140
226
  }
141
227
 
142
228
  const cwd = options.cwd || process.cwd();
143
- const args = [...spec.defaultFlags];
229
+ const args = [...harnessArgs];
230
+ const env = await resolveHarnessEnv(spec, process.env, options);
144
231
 
145
232
  if (spec.execType === 'subcommand' || spec.execType === 'flag_last') {
146
233
  args.push(taskPrompt);
@@ -156,7 +243,7 @@ export async function dispatchTask(harnessId, taskPrompt, options = {}) {
156
243
  cwd,
157
244
  stdio: spec.execType === 'pipe_stdin' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
158
245
  env: {
159
- ...process.env,
246
+ ...env,
160
247
  TR_HARNESS_DISPATCH: '1'
161
248
  }
162
249
  });
@@ -27,7 +27,7 @@ export function globalPluginsDir() {
27
27
  }
28
28
 
29
29
  export function bundledPluginsDir() {
30
- return path.join(PACKAGE_ROOT, 'plugins');
30
+ return process.env._TR_TEST_BUNDLED_PLUGINS_DIR || path.join(PACKAGE_ROOT, 'plugins');
31
31
  }
32
32
 
33
33
  /** The vault that owns records for plugins installed in `pluginsDir` (`<brain>/plugins` → `<brain>/memory-vault`). */
@@ -351,10 +351,11 @@ export function discoverPlugins(projectRoot = process.cwd()) {
351
351
  export function listBundledPlugins(projectRoot = process.cwd()) {
352
352
  const seen = new Set();
353
353
  const out = [];
354
- const dirs = [
355
- bundledPluginsDir(),
356
- path.join(PACKAGE_ROOT, '.agent', 'skills', 'total-recall', 'plugins')
357
- ];
354
+ // Under test the fixture directory is the only source, so results never
355
+ // depend on plugins installed in the developer's own brain.
356
+ const dirs = process.env._TR_TEST_BUNDLED_PLUGINS_DIR
357
+ ? [bundledPluginsDir()]
358
+ : [bundledPluginsDir(), path.join(PACKAGE_ROOT, '.agent', 'skills', 'total-recall', 'plugins')];
358
359
  for (const dir of dirs) {
359
360
  if (!fs.existsSync(dir)) continue;
360
361
  for (const p of readPluginsDir(dir, 'bundled')) {