@luckydraw/cumulus 1.0.3 → 1.0.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.
- package/CHANGELOG.md +19 -0
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +53 -18
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +133 -0
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/jobs.d.ts +158 -0
- package/dist/gateway/jobs.d.ts.map +1 -0
- package/dist/gateway/jobs.js +497 -0
- package/dist/gateway/jobs.js.map +1 -0
- package/dist/gateway/scheduler.d.ts.map +1 -1
- package/dist/gateway/scheduler.js +9 -30
- package/dist/gateway/scheduler.js.map +1 -1
- package/dist/gateway/server.d.ts +16 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +183 -2
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +10 -0
- package/dist/gateway/setup.js.map +1 -1
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +21 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/gateway.d.ts +35 -45
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +98 -117
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/tool-inventory.d.ts +140 -0
- package/dist/lib/tool-inventory.d.ts.map +1 -0
- package/dist/lib/tool-inventory.js +317 -0
- package/dist/lib/tool-inventory.js.map +1 -0
- package/docs/conditional-continuation.md +102 -147
- package/docs/web-app-agent-guide.md +74 -3
- package/examples/web-app-agent/README.md +40 -8
- package/examples/web-app-agent/thread-config.visitor.example.json +30 -2
- package/package.json +1 -1
package/dist/lib/gateway.js
CHANGED
|
@@ -26,6 +26,7 @@ import { retrieve } from './retriever.js';
|
|
|
26
26
|
import { SessionManager } from './session.js';
|
|
27
27
|
import { StreamProcessor, shouldExternalizeUserInput, externalizeUserInput, } from './stream-processor.js';
|
|
28
28
|
import { scaffoldFromTemplate } from './templates.js';
|
|
29
|
+
import { applyCapabilityGates, compileDisallowedTools, getToolInventory, promptCapabilities, toolAvailable, } from './tool-inventory.js';
|
|
29
30
|
import { getEagerTools } from './tools/index.js';
|
|
30
31
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
31
32
|
const CUMULUS_DIR = process.env.CUMULUS_DIR || path.join(os.homedir(), '.cumulus');
|
|
@@ -151,11 +152,11 @@ To CHANGE a file, use the built-in Edit/Write tools.
|
|
|
151
152
|
- Do NOT patch files by writing shell scripts (sed, awk, python, echo/cat heredocs, etc.). Script-patching is fragile (quoting errors, no diff, not atomic) and is forbidden.
|
|
152
153
|
|
|
153
154
|
BACKGROUND WORK:
|
|
154
|
-
Your turn runs in a subprocess that EXITS when the turn ends
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
155
|
+
Your turn runs in a subprocess that EXITS when the turn ends, so a command you background yourself (\`cmd &\`) is killed as soon as you stop talking — on EVERY turn, not occasionally.
|
|
156
|
+
- For work that outlasts your turn, use run_job(command, label). The gateway owns it, so it has no pipes to break and is not in your turn's process tree. When it exits you are woken with its exit code and the tail of its output.
|
|
157
|
+
- Write the command plainly: no \`&\`, no \`> log 2>&1\`, no \`nohup\`/\`setsid\`. run_job already does all of that, and adding your own breaks the log capture.
|
|
158
|
+
- Then END YOUR TURN. Do not wait for it and do not poll — you are woken automatically. Use list_jobs and job_log(id) to check on a running job, cancel_job(id) to stop one.
|
|
159
|
+
- A command that finishes in a couple of minutes needs none of this — run it in the foreground and wait.
|
|
159
160
|
|
|
160
161
|
TOOLS:
|
|
161
162
|
- read_file: Read any file (text or PDF) — returns a summary + chunk table-of-contents and stores the content for future search. Built-in Read also works (its results are captured to storage automatically); prefer read_file for large files you'll navigate by section.
|
|
@@ -236,35 +237,20 @@ Example:
|
|
|
236
237
|
3. **Never fix inline** — a bug unrelated to your current task gets its own todo, not a context switch
|
|
237
238
|
4. **Reflect redirects** — update the todo list before executing on a user-directed change of plan
|
|
238
239
|
5. **Debugging** — treat each hypothesis as a todo; test systematically rather than chasing the first suspicious lead`;
|
|
239
|
-
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
240
|
+
/*
|
|
241
|
+
* The watcher recipe (task 116) lived here: a copy-pasteable detached
|
|
242
|
+
* watcher + schedule_trigger deadline, seeded into every thread's content
|
|
243
|
+
* store. Deleted by task 139, not deprecated — `run_job` makes the mechanism
|
|
244
|
+
* it taught unnecessary, and two ways to do background work is precisely the
|
|
245
|
+
* shape Rule #8 forbids.
|
|
246
|
+
*
|
|
247
|
+
* It was also actively harmful where it could not be run: topic-free
|
|
248
|
+
* operational boilerplate is what survives the relevance floor when a query
|
|
249
|
+
* matches nothing else, so on an app's visitor thread it was retrieved for
|
|
250
|
+
* unrelated questions and taught a model to POST to the gateway carrying the
|
|
251
|
+
* admin key (task 134). Task 134 gated the seeding; this removes the thing
|
|
252
|
+
* being seeded.
|
|
248
253
|
*/
|
|
249
|
-
export const WATCHER_RECIPE_TEMPLATE = `Watcher recipe — how a detached background job continues your work when it finishes (background job continuation).
|
|
250
|
-
|
|
251
|
-
Use when a long-running command's completion should wake this thread. Arm BOTH legs before the turn ends.
|
|
252
|
-
|
|
253
|
-
LEG 1 — WATCHER (wakes you the moment the job finishes). The job and its reporter run as one detached group with its OWN redirect — without it the watcher dies by SIGPIPE before it can report:
|
|
254
|
-
|
|
255
|
-
{ npm run build > /tmp/job.log 2>&1; rc=$?; C=\${CUMULUS_DIR:-$HOME/.cumulus}/gateway.config.json; curl -s -X POST "http://127.0.0.1:$(jq -r '.port // 8090' $C)/api/agents/inject" -H "X-API-Key: $(jq -r '.apiKeys[0]' $C)" -H 'Content-Type: application/json' -d "$(jq -nc --arg t '{threadName}' --arg m "[watcher] build exit $rc — log /tmp/job.log. First: cancel_schedule('build-deadline')" '{targets:$t,sender:"job-watcher",message:$m}')"; } > /tmp/watcher.log 2>&1 &
|
|
256
|
-
|
|
257
|
-
Replace \`npm run build\` with your command; keep every redirect exactly as shown. Fire on OUTCOME, not on success — the report carries the exit code and the log path so the woken turn can diagnose without re-running. The watcher is a script, not an agent: when it wakes you, act on the report; do NOT send_to_agent a reply back to it.
|
|
258
|
-
|
|
259
|
-
LEG 2 — DEADLINE (guarantees a wake even if the watcher itself died; stored in thread config, so it survives gateway restarts, which the watcher does not):
|
|
260
|
-
|
|
261
|
-
schedule_trigger({id: "build-deadline", trigger: "once", at: "<ISO 8601, generously past the expected finish>", message: "The build watcher never reported back. Check /tmp/job.log and whether the process is still alive."})
|
|
262
|
-
|
|
263
|
-
cancel_schedule("build-deadline") as your first action when the watcher wakes you.`;
|
|
264
|
-
/** Resolve the watcher recipe for a specific thread. */
|
|
265
|
-
export function renderWatcherRecipe(threadName) {
|
|
266
|
-
return WATCHER_RECIPE_TEMPLATE.replace(/\{threadName\}/g, threadName);
|
|
267
|
-
}
|
|
268
254
|
/**
|
|
269
255
|
* Committed budget for the resolved static template, in estimateTokens units
|
|
270
256
|
* (task 117). The template reached ~3,400 tokens/turn by accretion — every
|
|
@@ -278,35 +264,26 @@ export function renderWatcherRecipe(threadName) {
|
|
|
278
264
|
*/
|
|
279
265
|
export const STATIC_PROMPT_TOKEN_BUDGET = 1_800;
|
|
280
266
|
/**
|
|
281
|
-
*
|
|
282
|
-
* so two CONCURRENT seeds of the same thread would both pass the check and write
|
|
283
|
-
* twice — the memo makes the second caller join the first's promise instead.
|
|
284
|
-
* Cleared alongside the thread cache so a deleted-then-recreated thread re-seeds;
|
|
285
|
-
* across process restarts the content-hash dedupe makes re-seeding free.
|
|
286
|
-
*/
|
|
287
|
-
const seededRecipes = new Map();
|
|
288
|
-
/**
|
|
289
|
-
* Can this thread actually run the background-work recipe? (task 134)
|
|
267
|
+
* Can this thread actually run background work? (task 134, retained by 139)
|
|
290
268
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* on cdda's visitor threads at 2 of 24 turns, and on turn 1 of every thread it
|
|
297
|
-
* is the only stored item there is. What it then teaches a visitor's assistant
|
|
298
|
-
* is an HTTP call carrying the gateway's admin key, to a model whose only
|
|
299
|
-
* honest route is its own tools.
|
|
269
|
+
* Originally the gate on seeding the watcher recipe; now the gate on the
|
|
270
|
+
* `run_job` endpoint, which is the same question with more at stake. run_job
|
|
271
|
+
* is arbitrary shell execution reached over MCP, so without this a thread whose
|
|
272
|
+
* merged config denies `Bash` could run shell anyway — the bypass class task
|
|
273
|
+
* 136 closed for `read_file`.
|
|
300
274
|
*
|
|
301
275
|
* Same derived-predicate shape as task 110's isAgentSender: the condition that
|
|
302
|
-
* decides whether
|
|
303
|
-
* is
|
|
304
|
-
*
|
|
276
|
+
* decides whether background work is USEFUL is the condition that decides
|
|
277
|
+
* whether it is offered, so the two cannot drift apart. Deliberately NOT keyed
|
|
278
|
+
* on namespace — namespace is a proxy, executability is the property, and
|
|
305
279
|
* keying on it would need an option threaded through six spawn paths that fails
|
|
306
280
|
* OPEN when one is missed (task 113 is the recorded case of exactly that).
|
|
307
281
|
*/
|
|
308
282
|
export function canRunBackgroundWork(config) {
|
|
309
|
-
|
|
283
|
+
// Task 137: delegates to toolAvailable so an ALLOWLIST is honoured too. Checking
|
|
284
|
+
// disallowedTools alone (as this did originally) reports true for a thread whose
|
|
285
|
+
// allowlist simply omits Bash — it would still be seeded a recipe it cannot run.
|
|
286
|
+
return toolAvailable('Bash', config);
|
|
310
287
|
}
|
|
311
288
|
/**
|
|
312
289
|
* Root confinement for the `read_file` MCP tool (task 136).
|
|
@@ -334,38 +311,6 @@ export function readFileRootsForThread(config, threadCwd) {
|
|
|
334
311
|
return undefined;
|
|
335
312
|
return [threadCwd];
|
|
336
313
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Seed situational recipe docs into a thread's content store (task 116), gated
|
|
339
|
-
* on the thread being able to run them (task 134).
|
|
340
|
-
*
|
|
341
|
-
* Called from sendMessage, NOT getOrCreateThread: the latter has 14 call sites,
|
|
342
|
-
* several of them store-only paths (transcript ingest, history reads, thread
|
|
343
|
-
* listing) that never spawn a turn, and it sees only the thread config —
|
|
344
|
-
* a global `disallowedTools` would be invisible to a gate placed there.
|
|
345
|
-
* The gate lives HERE rather than at the call site so a future caller cannot
|
|
346
|
-
* reintroduce the seed by forgetting to check. Errors never fail the turn.
|
|
347
|
-
*/
|
|
348
|
-
export function seedThreadRecipes(content, threadName, config) {
|
|
349
|
-
if (!canRunBackgroundWork(config))
|
|
350
|
-
return Promise.resolve();
|
|
351
|
-
const existing = seededRecipes.get(threadName);
|
|
352
|
-
if (existing)
|
|
353
|
-
return existing;
|
|
354
|
-
const seeding = (async () => {
|
|
355
|
-
try {
|
|
356
|
-
await content.store(renderWatcherRecipe(threadName), {
|
|
357
|
-
sourceType: 'reference',
|
|
358
|
-
summary: `Watcher recipe: detached background-job continuation one-liner (watcher + schedule_trigger deadline) for thread ${threadName}`,
|
|
359
|
-
metadata: { recipe: 'watcher' },
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
console.error('[Gateway] Failed to seed thread recipes:', err);
|
|
364
|
-
}
|
|
365
|
-
})();
|
|
366
|
-
seededRecipes.set(threadName, seeding);
|
|
367
|
-
return seeding;
|
|
368
|
-
}
|
|
369
314
|
// ─── Thread cache ────────────────────────────────────────────────────────────
|
|
370
315
|
const threadCache = new Map();
|
|
371
316
|
// ─── Helper Functions ────────────────────────────────────────────────────────
|
|
@@ -682,8 +627,11 @@ export function generateSystemPrompt(count, tokens, sessionId, recentMessages, r
|
|
|
682
627
|
.replace('{alwaysIncludeContext}', alwaysIncludeContext)
|
|
683
628
|
.replace('{recentContext}', recentContext)
|
|
684
629
|
.replace('{retrievedContext}', retrievedContext)
|
|
685
|
-
//
|
|
686
|
-
//
|
|
630
|
+
// Added by task 109 so the watcher recipe could address this thread by exact
|
|
631
|
+
// name. That recipe is gone (task 139) and the shipped template no longer uses
|
|
632
|
+
// the placeholder, but a CUSTOM template legitimately might — so the
|
|
633
|
+
// substitution stays and only the reason changes. Global: a template may
|
|
634
|
+
// reference it more than once.
|
|
687
635
|
.replace(/\{threadName\}/g, threadName ?? 'this thread'));
|
|
688
636
|
}
|
|
689
637
|
/**
|
|
@@ -717,6 +665,35 @@ export function claudeModelArgs(threadPin, claudeModels) {
|
|
|
717
665
|
const resolved = resolveClaudeModel(threadPin, claudeModels);
|
|
718
666
|
return resolved ? ['--model', resolved] : [];
|
|
719
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* The full argv for a Claude CLI turn (task 137).
|
|
670
|
+
*
|
|
671
|
+
* Extracted from `sendMessage` for the same reason as `claudeModelArgs` and
|
|
672
|
+
* `resolveClaudeModel` above: the composition was only assertable by spawning a real
|
|
673
|
+
* subprocess, so nothing verified that a compiled allowlist actually reaches the spawn.
|
|
674
|
+
*
|
|
675
|
+
* Owns one measured invariant: `--disallowedTools` is VARIADIC, so it must come last.
|
|
676
|
+
* A positional argument after it is silently swallowed as extra deny rules — measured
|
|
677
|
+
* (`Permission deny rule "the" matches no known tool`). Append nothing after this call.
|
|
678
|
+
*/
|
|
679
|
+
export function claudeSpawnArgs(params) {
|
|
680
|
+
return appendDisallowedToolsArgs([
|
|
681
|
+
'--print',
|
|
682
|
+
'--verbose',
|
|
683
|
+
'--output-format',
|
|
684
|
+
'stream-json',
|
|
685
|
+
'--permission-mode',
|
|
686
|
+
'bypassPermissions',
|
|
687
|
+
'--mcp-config',
|
|
688
|
+
params.mcpConfigPath,
|
|
689
|
+
'--strict-mcp-config',
|
|
690
|
+
'--input-format',
|
|
691
|
+
'stream-json',
|
|
692
|
+
'--effort',
|
|
693
|
+
params.effort || 'high',
|
|
694
|
+
...claudeModelArgs(params.claudeModel, params.claudeModels),
|
|
695
|
+
], params.disallowedTools);
|
|
696
|
+
}
|
|
720
697
|
/**
|
|
721
698
|
* Resolve which provider a thread's model id runs on (task 118) — pure, same
|
|
722
699
|
* rationale as `resolveClaudeModel`: the decision must be assertable without
|
|
@@ -1029,15 +1006,13 @@ export async function getOrCreateThread(threadName, _basePath) {
|
|
|
1029
1006
|
// config — the gate needs the merged one.
|
|
1030
1007
|
return thread;
|
|
1031
1008
|
}
|
|
1032
|
-
/** Clear a thread from the cache
|
|
1009
|
+
/** Clear a thread from the cache */
|
|
1033
1010
|
export function clearThreadCache(threadName) {
|
|
1034
1011
|
if (threadName) {
|
|
1035
1012
|
threadCache.delete(threadName);
|
|
1036
|
-
seededRecipes.delete(threadName);
|
|
1037
1013
|
}
|
|
1038
1014
|
else {
|
|
1039
1015
|
threadCache.clear();
|
|
1040
|
-
seededRecipes.clear();
|
|
1041
1016
|
}
|
|
1042
1017
|
}
|
|
1043
1018
|
// ─── Core Pipeline ───────────────────────────────────────────────────────────
|
|
@@ -1231,11 +1206,12 @@ export async function sendMessage(options) {
|
|
|
1231
1206
|
const globalConfig = await loadGlobalConfig();
|
|
1232
1207
|
const threadConfig = await loadThreadConfig(threadName);
|
|
1233
1208
|
const mergedConfig = mergeConfigs(globalConfig, threadConfig);
|
|
1234
|
-
// Task
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
|
|
1209
|
+
// Task 137: drop prompt sections whose tools this thread cannot reach. Computed ONCE
|
|
1210
|
+
// here because three sites consume the template (the prompt, the 088 capture, and the
|
|
1211
|
+
// direct-provider instructions) — task 113's lesson is that per-site derivation is how
|
|
1212
|
+
// one of N paths silently keeps the old behaviour. Identity when nothing is gated, so
|
|
1213
|
+
// every thread on this box is byte-unaffected.
|
|
1214
|
+
const gatedPromptTemplate = applyCapabilityGates(systemPromptTemplate ?? SYSTEM_PROMPT_TEMPLATE, promptCapabilities(mergedConfig));
|
|
1239
1215
|
const alwaysInclude = await readAlwaysIncludeFiles(mergedConfig, threadCwd);
|
|
1240
1216
|
// ── Model resolution ──
|
|
1241
1217
|
// Thread config `model` field determines the execution path:
|
|
@@ -1320,7 +1296,7 @@ export async function sendMessage(options) {
|
|
|
1320
1296
|
systemPromptLength: 0,
|
|
1321
1297
|
};
|
|
1322
1298
|
// ── System prompt ──
|
|
1323
|
-
const systemPrompt = generateSystemPrompt(stats.count, stats.totalTokens, thread.session.getSessionId(), recentConversation, retrievedContext, alwaysInclude.formattedContext,
|
|
1299
|
+
const systemPrompt = generateSystemPrompt(stats.count, stats.totalTokens, thread.session.getSessionId(), recentConversation, retrievedContext, alwaysInclude.formattedContext, gatedPromptTemplate, threadName);
|
|
1324
1300
|
// Update debug with system prompt breakdown
|
|
1325
1301
|
const systemPromptTokens = estimateTokens(systemPrompt);
|
|
1326
1302
|
const ragTokens = retrievalResult ? estimateTokens(retrievedContext) : 0;
|
|
@@ -1339,7 +1315,7 @@ export async function sendMessage(options) {
|
|
|
1339
1315
|
// ── Prompt capture (task 088): assemble the observable sections ──
|
|
1340
1316
|
// The pure template (empty components) isolates the instructions section.
|
|
1341
1317
|
const promptCaptureEnabled = mergedConfig.promptCapture !== false;
|
|
1342
|
-
const instructionsOnly = generateSystemPrompt(stats.count, stats.totalTokens, thread.session.getSessionId(), [], '', '',
|
|
1318
|
+
const instructionsOnly = generateSystemPrompt(stats.count, stats.totalTokens, thread.session.getSessionId(), [], '', '', gatedPromptTemplate, threadName);
|
|
1343
1319
|
// CLI shape by default; the HF branch overwrites path/instructions below.
|
|
1344
1320
|
let promptCapturePath = 'cli';
|
|
1345
1321
|
const promptCaptureSections = [
|
|
@@ -1544,7 +1520,7 @@ export async function sendMessage(options) {
|
|
|
1544
1520
|
// Build system blocks with cache_control on static parts
|
|
1545
1521
|
const systemBlocks = [];
|
|
1546
1522
|
// Static instructions (cacheable)
|
|
1547
|
-
const instructions =
|
|
1523
|
+
const instructions = gatedPromptTemplate;
|
|
1548
1524
|
const modelIdentity = `You are ${threadModel}. You are NOT Claude, NOT Opus, NOT Sonnet, NOT Haiku. If asked what model you are, say "${threadModel}". `;
|
|
1549
1525
|
const hfInstructionsText = modelIdentity +
|
|
1550
1526
|
instructions
|
|
@@ -1788,7 +1764,13 @@ export async function sendMessage(options) {
|
|
|
1788
1764
|
tools: coreTools,
|
|
1789
1765
|
mcpTools: mcpToolEntries,
|
|
1790
1766
|
eagerAll: true, // HF models can't use ToolSearch — send all tools upfront
|
|
1791
|
-
|
|
1767
|
+
// Task 137: the allowlist must bind here too, or a visitor thread pinned to an
|
|
1768
|
+
// HF/OpenAI model would silently ignore it — the fail-open, one-of-N-paths gap
|
|
1769
|
+
// task 113 recorded. No probe needed on this path: the registry about to be
|
|
1770
|
+
// handed to the loop IS the exact inventory.
|
|
1771
|
+
disallowedTools: mergedConfig.allowedTools
|
|
1772
|
+
? compileDisallowedTools(mergedConfig.allowedTools, [...coreTools, ...mcpToolEntries].map(t => t.definition.name), mergedConfig.disallowedTools)
|
|
1773
|
+
: mergedConfig.disallowedTools,
|
|
1792
1774
|
onToken: (() => {
|
|
1793
1775
|
// Filter suppresses <todo> and <thinking> blocks from reaching the frontend (unless debug)
|
|
1794
1776
|
const filter = onToken
|
|
@@ -1878,22 +1860,22 @@ export async function sendMessage(options) {
|
|
|
1878
1860
|
}
|
|
1879
1861
|
// ── Local execution: spawn Claude on this machine ──
|
|
1880
1862
|
const mcpConfigPath = generateMcpConfig(thread.threadPath, thread.session.getSessionId(), threadName, sharedMcpPort, extraMcpServers, gatewayAgentsConfig, readFileRootsForThread(mergedConfig, threadCwd));
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1863
|
+
const resolvedClaudePath = claudePath || resolveClaudeCli();
|
|
1864
|
+
// Task 137: compile an allowlist into --disallowedTools. The CLI's --allowedTools is an
|
|
1865
|
+
// AUTO-APPROVE list and does not restrict (measured under three permission modes), and
|
|
1866
|
+
// under --permission-mode bypassPermissions it is a guaranteed no-op — so deny-by-default
|
|
1867
|
+
// has to be expressed as a denylist. Gated on the field being present so an unconstrained
|
|
1868
|
+
// thread never pays for the inventory probe (a subprocess spawn) at all.
|
|
1869
|
+
const effectiveDisallowedTools = mergedConfig.allowedTools
|
|
1870
|
+
? compileDisallowedTools(mergedConfig.allowedTools, await getToolInventory(resolvedClaudePath), mergedConfig.disallowedTools)
|
|
1871
|
+
: mergedConfig.disallowedTools;
|
|
1872
|
+
const args = claudeSpawnArgs({
|
|
1889
1873
|
mcpConfigPath,
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
...claudeModelArgs(mergedConfig.claudeModel, options.claudeModels),
|
|
1896
|
-
], mergedConfig.disallowedTools);
|
|
1874
|
+
effort: mergedConfig.effort,
|
|
1875
|
+
claudeModel: mergedConfig.claudeModel,
|
|
1876
|
+
claudeModels: options.claudeModels,
|
|
1877
|
+
disallowedTools: effectiveDisallowedTools,
|
|
1878
|
+
});
|
|
1897
1879
|
// Filter out Claude env vars
|
|
1898
1880
|
const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CLAUDE') && key !== 'CLAUDECODE'));
|
|
1899
1881
|
// Check if already aborted
|
|
@@ -1901,7 +1883,6 @@ export async function sendMessage(options) {
|
|
|
1901
1883
|
cleanupMcpConfig(mcpConfigPath);
|
|
1902
1884
|
throw new Error('Aborted');
|
|
1903
1885
|
}
|
|
1904
|
-
const resolvedClaudePath = claudePath || resolveClaudeCli();
|
|
1905
1886
|
// Windows: spawn() can't directly exec .cmd/.bat shims (CVE-2024-27980).
|
|
1906
1887
|
// Routing through cmd.exe is required for the npm-installed `claude.cmd`.
|
|
1907
1888
|
const isWindowsShim = process.platform === 'win32' && /\.(cmd|bat|ps1)$/i.test(resolvedClaudePath);
|