@nexrall/code-core 1.4.24 → 1.4.26
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/dist/agent/agentRegistry.d.ts +45 -0
- package/dist/agent/agentRegistry.d.ts.map +1 -0
- package/dist/agent/agentRegistry.js +138 -0
- package/dist/agent/agentTypes.d.ts +31 -0
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +182 -33
- package/dist/agent/loop.d.ts +14 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +176 -18
- package/dist/agent/planMode.d.ts +20 -0
- package/dist/agent/planMode.d.ts.map +1 -0
- package/dist/agent/planMode.js +252 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +14 -0
- package/dist/plugins/index.d.ts +20 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +44 -3
- package/dist/plugins/installer.d.ts +40 -2
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +85 -14
- package/dist/plugins/sources.d.ts +49 -0
- package/dist/plugins/sources.d.ts.map +1 -0
- package/dist/plugins/sources.js +197 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
|
|
36
|
+
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = void 0;
|
|
37
37
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
38
38
|
exports.createLimiter = createLimiter;
|
|
39
39
|
exports.extractSubTaskText = extractSubTaskText;
|
|
@@ -59,6 +59,8 @@ const executor_1 = require("../tools/executor");
|
|
|
59
59
|
const agentTypes_1 = require("./agentTypes");
|
|
60
60
|
const skills_1 = require("./skills");
|
|
61
61
|
const rules_1 = require("../permissions/rules");
|
|
62
|
+
const planMode_1 = require("./planMode");
|
|
63
|
+
const agentRegistry_1 = require("./agentRegistry");
|
|
62
64
|
const sandbox_1 = require("../tools/sandbox");
|
|
63
65
|
const index_1 = require("../plugins/index");
|
|
64
66
|
const testIntegrity_1 = require("./testIntegrity");
|
|
@@ -406,6 +408,24 @@ const SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0
|
|
|
406
408
|
/** Cap on the text a sub-task hands back, so one verbose sub-agent can't blow up
|
|
407
409
|
* the PARENT's context in a single tool_result. */
|
|
408
410
|
const SUBTASK_MAX = 48000; // chars (~12k tokens)
|
|
411
|
+
/**
|
|
412
|
+
* Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
|
|
413
|
+
* tool — as opposed to the user declining it.
|
|
414
|
+
*
|
|
415
|
+
* The distinction matters to the model, which is why this is an exception rather
|
|
416
|
+
* than a `false`: both used to collapse into "Permission denied by user", so an
|
|
417
|
+
* agent blocked by its own allowlist (very often a mistyped tool name) was told
|
|
418
|
+
* the human had refused. The rational response to that is to ask again, which
|
|
419
|
+
* can never succeed. Carrying a reason lets the tool_result say what is actually
|
|
420
|
+
* true and what to do instead.
|
|
421
|
+
*/
|
|
422
|
+
class ToolNotAllowedError extends Error {
|
|
423
|
+
constructor(message) {
|
|
424
|
+
super(message);
|
|
425
|
+
this.name = 'ToolNotAllowedError';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
exports.ToolNotAllowedError = ToolNotAllowedError;
|
|
409
429
|
/**
|
|
410
430
|
* Slice `s` to at most `max` UTF-16 units without splitting a surrogate pair.
|
|
411
431
|
*
|
|
@@ -511,11 +531,74 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
511
531
|
return { error: 'Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it.' };
|
|
512
532
|
}
|
|
513
533
|
// Resolve an optional custom agent type (subagent_type).
|
|
514
|
-
|
|
515
|
-
|
|
534
|
+
//
|
|
535
|
+
// `agentTypes` is a snapshot taken once at the top of runAgentLoop, before the
|
|
536
|
+
// model said anything. That made "write .nexrall/agents/x.md, then use it"
|
|
537
|
+
// impossible within a single turn: the file existed on disk, but this lookup
|
|
538
|
+
// consulted a list captured before it was written, and the model was told the
|
|
539
|
+
// agent did not exist — which reads as "creating it failed".
|
|
540
|
+
//
|
|
541
|
+
// So on a MISS ONLY, re-read from disk before giving up. The hit path (every
|
|
542
|
+
// normal call) still costs zero syscalls, and the miss path costs ~4 mostly-
|
|
543
|
+
// ENOENT stats — against a sub-agent that is about to run for seconds to
|
|
544
|
+
// minutes. Note runAgentLoop re-reads for the sub-agent anyway, so the old
|
|
545
|
+
// behaviour was already inconsistent: fresh for the child, stale for the lookup.
|
|
546
|
+
// ── Resolve a resume target ─────────────────────────────────────────────────
|
|
547
|
+
//
|
|
548
|
+
// `resume_agent_id` continues a previous sub-agent. The stored transcript
|
|
549
|
+
// carries the agent's NAME, and that name — not the one the model passed — is
|
|
550
|
+
// what gets authorised below.
|
|
551
|
+
//
|
|
552
|
+
// This matters because an id would otherwise be a permanent capability: deny
|
|
553
|
+
// `task(explorer)` today and a model holding yesterday's explorer id could
|
|
554
|
+
// still resume it, with the deny rule looking like it was applied. Re-deriving
|
|
555
|
+
// the name from storage also stops a mismatched `subagent_type` from being
|
|
556
|
+
// used to launder a denied agent under an allowed name.
|
|
557
|
+
const resumeId = typeof input.resume_agent_id === 'string' ? input.resume_agent_id.trim() : '';
|
|
558
|
+
const resumed = resumeId ? (0, agentRegistry_1.getAgent)(resumeId) : undefined;
|
|
559
|
+
if (resumeId && !resumed) {
|
|
560
|
+
return {
|
|
561
|
+
error: `No resumable sub-agent with id "${resumeId}". Ids live only for the current session and the ` +
|
|
562
|
+
'oldest are dropped when too many accumulate, so this one has expired or never existed. ' +
|
|
563
|
+
'Start a fresh sub-task with a self-contained prompt instead.',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const requestedType = resumed
|
|
567
|
+
? (resumed.agentName ?? '')
|
|
568
|
+
: (typeof input.subagent_type === 'string' ? input.subagent_type : '');
|
|
569
|
+
// ── Enforce `deny: ["task(<name>)"]` ─────────────────────────────────────────
|
|
570
|
+
//
|
|
571
|
+
// This is the load-bearing check; filtering the catalogue in runAgentLoop only
|
|
572
|
+
// stops the agent being SUGGESTED. It must run BEFORE resolution, because the
|
|
573
|
+
// reload-on-miss path below deliberately re-reads from disk UNFILTERED — a
|
|
574
|
+
// denied agent is absent from the snapshot, would therefore "miss", and would
|
|
575
|
+
// then be found by that reload and run. Denying by omission is not denying.
|
|
576
|
+
//
|
|
577
|
+
// Phrased as a policy refusal, not "unknown type": the model must not respond
|
|
578
|
+
// by trying to create the agent file it thinks is missing.
|
|
579
|
+
if (requestedType) {
|
|
580
|
+
const decision = (0, rules_1.evaluatePermission)((0, rules_1.loadSettings)(options.workDir).permissions, 'task', { subagent_type: requestedType }, options.workDir);
|
|
581
|
+
if (decision === 'deny') {
|
|
582
|
+
return {
|
|
583
|
+
error: `The sub-agent "${requestedType}" is disabled by a permission rule in this project ` +
|
|
584
|
+
`(permissions.deny in settings.json). This is a deliberate policy choice, not a missing file — ` +
|
|
585
|
+
'do not create it and do not retry. Do the work yourself, or use a different sub-agent.',
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
let agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
|
|
590
|
+
let knownTypes = agentTypes;
|
|
591
|
+
if (requestedType && !agent) {
|
|
592
|
+
knownTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
|
|
593
|
+
agent = (0, agentTypes_1.findAgentType)(knownTypes, requestedType);
|
|
594
|
+
}
|
|
516
595
|
if (requestedType && !agent) {
|
|
517
|
-
const known =
|
|
518
|
-
return {
|
|
596
|
+
const known = knownTypes.map((a) => a.name).join(', ') || '(none defined)';
|
|
597
|
+
return {
|
|
598
|
+
error: `Unknown subagent_type "${requestedType}". Available types: ${known}.\n` +
|
|
599
|
+
'If you just created .nexrall/agents/' + requestedType + '.md, make sure the write finished in an ' +
|
|
600
|
+
'EARLIER tool call than this one — a file written in the same batch may not be on disk yet.',
|
|
601
|
+
};
|
|
519
602
|
}
|
|
520
603
|
// A custom agent's persona is delivered through the project-instructions
|
|
521
604
|
// channel (authoritative in the system prompt), layered above the project's
|
|
@@ -525,19 +608,36 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
525
608
|
(options.nexrallMd ? `\n\n---\n\n${options.nexrallMd}` : '')
|
|
526
609
|
: options.nexrallMd;
|
|
527
610
|
// Optional tool allowlist — deny anything outside it for this sub-agent.
|
|
611
|
+
//
|
|
612
|
+
// A refusal here is reported through `deniedReason` rather than the generic
|
|
613
|
+
// "Permission denied by user", which was actively misleading: the user denied
|
|
614
|
+
// nothing, and a model told that will re-ask for approval instead of noticing
|
|
615
|
+
// that the agent's own allowlist (often a typo'd tool name) is what stopped it.
|
|
528
616
|
const allowed = agent?.tools ? new Set(agent.tools) : null;
|
|
529
617
|
const gatedPermission = async (req) => {
|
|
530
|
-
if (allowed && !allowed.has(req.tool))
|
|
531
|
-
|
|
618
|
+
if (allowed && !allowed.has(req.tool)) {
|
|
619
|
+
throw new ToolNotAllowedError(`The "${agent.name}" sub-agent is not allowed to use \`${req.tool}\` — it is not in that agent's ` +
|
|
620
|
+
'tool allowlist. This is a restriction of the agent definition, NOT a user decision: do not ask ' +
|
|
621
|
+
'for approval, use one of the tools you do have, or report back that the task needs a different agent.');
|
|
622
|
+
}
|
|
532
623
|
// Path-scoped write restriction (agent.testFilesOnly) — see
|
|
533
624
|
// allowsTestOnlyWrite for the reasoning and its known limit.
|
|
534
|
-
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
|
|
535
|
-
|
|
625
|
+
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input)) {
|
|
626
|
+
throw new ToolNotAllowedError(`The "${agent.name}" sub-agent may only write to TEST files, so \`${req.tool}\` was refused for this ` +
|
|
627
|
+
'path. Do not try to work around it: if production code must change, say so in your report instead.');
|
|
628
|
+
}
|
|
536
629
|
return options.requestPermission(req);
|
|
537
630
|
};
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
631
|
+
// ── Resume: continue a previous sub-agent instead of starting cold ──────────
|
|
632
|
+
//
|
|
633
|
+
// The new prompt is appended as another user turn to the stored transcript, so
|
|
634
|
+
// the agent keeps every file it read and every conclusion it reached. Without
|
|
635
|
+
// this, "now also check the auth path" means re-describing the entire job and
|
|
636
|
+
// re-reading everything — the most common and most expensive kind of waste in
|
|
637
|
+
// a delegated workflow.
|
|
638
|
+
const subMessages = resumed
|
|
639
|
+
? [...resumed.messages, { role: 'user', content: [{ type: 'text', text: prompt }] }]
|
|
640
|
+
: [{ role: 'user', content: [{ type: 'text', text: prompt }] }];
|
|
541
641
|
// A dedicated abort signal for this sub-agent, distinct from the parent's own
|
|
542
642
|
// options.abortSignal (user hit Ctrl+C). Set to true either when the parent
|
|
543
643
|
// aborts OR when the stall timeout below fires, whichever happens first —
|
|
@@ -560,6 +660,9 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
560
660
|
_agentScope: `sub_${++_subTaskCounter}`, // isolated todo store per sub-agent
|
|
561
661
|
editorContext: null, // fresh isolated context for sub-agent
|
|
562
662
|
model: agent?.model ?? options.model,
|
|
663
|
+
// Plan mode is inherited, never relaxed. If the main agent could spawn a
|
|
664
|
+
// sub-agent that writes, the lock would be one `task` call from useless.
|
|
665
|
+
planMode: options.planMode,
|
|
563
666
|
nexrallMd: subNexrallMd,
|
|
564
667
|
abortSignal: subAbort,
|
|
565
668
|
requestPermission: gatedPermission,
|
|
@@ -622,7 +725,23 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
622
725
|
}
|
|
623
726
|
// Normal completion: the final assistant message is the sub-agent's answer.
|
|
624
727
|
const text = capSubTaskText(extractSubTaskText(result, true));
|
|
625
|
-
|
|
728
|
+
// Store the transcript so a follow-up can continue this agent rather than
|
|
729
|
+
// re-running it from scratch, and tell the parent the id.
|
|
730
|
+
//
|
|
731
|
+
// Only on NORMAL completion. A timed-out or failed run is deliberately not
|
|
732
|
+
// resumable: its transcript ends mid-thought, often mid-tool-call, and
|
|
733
|
+
// resuming from that state invites the model to build on work whose status
|
|
734
|
+
// it cannot determine. Those paths already salvage their partial output as
|
|
735
|
+
// TEXT, which is the safe way to carry that information forward.
|
|
736
|
+
const agentId = resumed
|
|
737
|
+
? ((0, agentRegistry_1.updateAgent)(resumed.id, result), resumed.id)
|
|
738
|
+
: (0, agentRegistry_1.rememberAgent)(agent?.name ?? null, (typeof input.description === 'string' && input.description.trim()) || prompt.slice(0, 80), result);
|
|
739
|
+
const body = text || '(sub-task completed with no text output)';
|
|
740
|
+
return {
|
|
741
|
+
output: `${body}\n\n[resumable: this sub-agent is "${agentId}". To ask IT a follow-up — keeping ` +
|
|
742
|
+
'everything it already read and concluded — call task again with resume_agent_id="' + agentId +
|
|
743
|
+
'" instead of writing a new prompt from scratch.]',
|
|
744
|
+
};
|
|
626
745
|
}
|
|
627
746
|
catch (err) {
|
|
628
747
|
// Same salvage rule as the timeout path above, for the other way a sub-agent
|
|
@@ -1293,17 +1412,36 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1293
1412
|
const hooks = loadHooks(options.workDir);
|
|
1294
1413
|
const depth = options._depth ?? 0;
|
|
1295
1414
|
const agentScope = options._agentScope ?? 'root';
|
|
1415
|
+
// Settings are read before the agent catalogue because a `deny` rule can
|
|
1416
|
+
// switch a sub-agent off, and an agent that may not run must not be
|
|
1417
|
+
// advertised (see below).
|
|
1418
|
+
const settings = (0, rules_1.loadSettings)(options.workDir);
|
|
1296
1419
|
// Discover custom sub-agent types. Only the top-level agent is told the
|
|
1297
1420
|
// catalogue (sub-agents can't spawn further), but every level resolves types.
|
|
1298
|
-
|
|
1421
|
+
//
|
|
1422
|
+
// Denied agents are filtered OUT of the catalogue rather than left in it to be
|
|
1423
|
+
// refused on dispatch. Listing an agent you have forbidden trains the model to
|
|
1424
|
+
// spend a tool call discovering the refusal, and "available types: …" on the
|
|
1425
|
+
// error path would name it again. Enforcement still happens at dispatch
|
|
1426
|
+
// (runSubTask) — this is the cosmetic half; that is the load-bearing half.
|
|
1427
|
+
const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir).filter((t) => (0, rules_1.evaluatePermission)(settings.permissions, 'task', { subagent_type: t.name }, options.workDir) !== 'deny');
|
|
1299
1428
|
const agentsCatalogue = depth === 0 ? (0, agentTypes_1.summariseAgents)(agentTypes) : '';
|
|
1300
1429
|
// Skills catalogue — unlike agentsCatalogue, available at every depth: a skill is
|
|
1301
1430
|
// just a reusable prompt template (via use_skill), not another spawn point, so
|
|
1302
1431
|
// sub-agents benefit from the same playbooks without the recursion concerns that
|
|
1303
1432
|
// gate agentsCatalogue to the top level.
|
|
1304
1433
|
const skillsCatalogue = (0, skills_1.summariseSkills)((0, skills_1.loadSkills)(options.workDir));
|
|
1434
|
+
// Plan mode instructions ride the project-instructions channel, which is
|
|
1435
|
+
// authoritative in the system prompt. Prepended rather than appended: the
|
|
1436
|
+
// lock has to be read before the project conventions it overrides.
|
|
1437
|
+
//
|
|
1438
|
+
// Telling the model is not the enforcement (checkPlanMode at the call site is)
|
|
1439
|
+
// — it exists so the model spends the turn planning instead of discovering the
|
|
1440
|
+
// lock one refused tool call at a time.
|
|
1441
|
+
const planAwareNexrallMd = options.planMode
|
|
1442
|
+
? planMode_1.PLAN_MODE_INSTRUCTIONS + (options.nexrallMd ? `\n\n---\n\n${options.nexrallMd}` : '')
|
|
1443
|
+
: options.nexrallMd;
|
|
1305
1444
|
// Optional OS-level bash sandbox (opt-in via settings.json "sandbox").
|
|
1306
|
-
const settings = (0, rules_1.loadSettings)(options.workDir);
|
|
1307
1445
|
const sandboxCfg = (0, sandbox_1.parseSandboxConfig)(settings.raw.sandbox) ?? undefined;
|
|
1308
1446
|
// Soft iteration budget + optional auto-continue past it (see resolvers above).
|
|
1309
1447
|
const maxIterations = resolveMaxIterations(options.maxIterations, settings.raw);
|
|
@@ -1520,7 +1658,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1520
1658
|
effort: options.effort,
|
|
1521
1659
|
env: options.env,
|
|
1522
1660
|
editorContext: options.editorContext,
|
|
1523
|
-
nexrallMd:
|
|
1661
|
+
nexrallMd: planAwareNexrallMd,
|
|
1524
1662
|
clientType: options.clientType,
|
|
1525
1663
|
abortSignal: options.abortSignal,
|
|
1526
1664
|
extraTools: options.mcpManager?.getAnthropicTools(),
|
|
@@ -1745,17 +1883,37 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1745
1883
|
options.onToolResult(name, result);
|
|
1746
1884
|
return { block: { ...block, id }, result };
|
|
1747
1885
|
}
|
|
1886
|
+
// ── Plan mode: a session-wide read-only lock ────────────────────────
|
|
1887
|
+
//
|
|
1888
|
+
// Checked BEFORE requestPermission on purpose. Routing it through the
|
|
1889
|
+
// permission prompt would ask the user to approve something that is
|
|
1890
|
+
// not theirs to approve in that moment, and a model told "denied by
|
|
1891
|
+
// user" reliably re-asks. This refusal instead says plainly that no
|
|
1892
|
+
// answer here can unlock it.
|
|
1893
|
+
if (options.planMode) {
|
|
1894
|
+
const refusal = (0, planMode_1.checkPlanMode)(name, input);
|
|
1895
|
+
if (refusal) {
|
|
1896
|
+
result = { error: refusal.message };
|
|
1897
|
+
options.onToolResult(name, result);
|
|
1898
|
+
return { block: { ...block, id }, result };
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1748
1901
|
// Request permission
|
|
1749
1902
|
const description = humanDescription(name, input);
|
|
1750
1903
|
let permitted;
|
|
1904
|
+
// A definition-level refusal carries its own explanation and must not be
|
|
1905
|
+
// flattened into the generic user-denial message below.
|
|
1906
|
+
let deniedReason = null;
|
|
1751
1907
|
try {
|
|
1752
1908
|
permitted = await options.requestPermission({ tool: name, input, description });
|
|
1753
1909
|
}
|
|
1754
|
-
catch {
|
|
1910
|
+
catch (err) {
|
|
1755
1911
|
permitted = false;
|
|
1912
|
+
if (err instanceof ToolNotAllowedError)
|
|
1913
|
+
deniedReason = err.message;
|
|
1756
1914
|
}
|
|
1757
1915
|
if (!permitted) {
|
|
1758
|
-
result = { error: 'Permission denied by user' };
|
|
1916
|
+
result = { error: deniedReason ?? 'Permission denied by user' };
|
|
1759
1917
|
}
|
|
1760
1918
|
else if (name === 'task') {
|
|
1761
1919
|
// Gated so a burst of `task` blocks in one message becomes a QUEUE
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PlanModeRefusal {
|
|
2
|
+
/** Machine-readable reason, for tests and telemetry. */
|
|
3
|
+
reason: 'mutating-tool' | 'bash-not-read-only';
|
|
4
|
+
/** Message shown to the MODEL. Must stop it retrying or asking for approval. */
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Is this bash command provably read-only?
|
|
9
|
+
*
|
|
10
|
+
* "Provably" is doing real work: unknown verbs are refused, not guessed at.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isReadOnlyCommand(command: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Should this tool call be refused because the session is in plan mode?
|
|
15
|
+
* Returns null when the call is allowed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkPlanMode(tool: string, input: Record<string, unknown>): PlanModeRefusal | null;
|
|
18
|
+
/** Text appended to the system prompt while plan mode is active. */
|
|
19
|
+
export declare const PLAN_MODE_INSTRUCTIONS: string;
|
|
20
|
+
//# sourceMappingURL=planMode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planMode.d.ts","sourceRoot":"","sources":["../../src/agent/planMode.ts"],"names":[],"mappings":"AAwHA,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,MAAM,EAAE,eAAe,GAAG,oBAAoB,CAAC;IAC/C,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAgE1D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,eAAe,GAAG,IAAI,CAoCxB;AAED,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,QAqBvB,CAAC"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Plan mode ───────────────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// A session-wide read-only lock. The model may research as deeply as it likes
|
|
5
|
+
// and must finish by PROPOSING a plan; it cannot change anything until a human
|
|
6
|
+
// leaves plan mode. Claude Code ships the same idea, and the reason to copy it
|
|
7
|
+
// is that "read the codebase, then tell me what you'd do" is otherwise
|
|
8
|
+
// unenforceable — you are relying on the model choosing not to edit, which is a
|
|
9
|
+
// promise, not a guarantee.
|
|
10
|
+
//
|
|
11
|
+
// The design rule here is FAIL CLOSED. Everything that is not provably
|
|
12
|
+
// side-effect-free is refused. A false refusal costs the user one message
|
|
13
|
+
// ("this needs plan mode off"); a false permit silently mutates a repo the user
|
|
14
|
+
// believed was frozen. Those are not symmetric, so ambiguity always loses.
|
|
15
|
+
//
|
|
16
|
+
// Note this is a DIFFERENT axis from the sub-agent tool allowlist in
|
|
17
|
+
// agentTypes.ts. That restricts one delegated worker; this restricts the whole
|
|
18
|
+
// session including the main agent and every sub-agent it spawns.
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.PLAN_MODE_INSTRUCTIONS = void 0;
|
|
21
|
+
exports.isReadOnlyCommand = isReadOnlyCommand;
|
|
22
|
+
exports.checkPlanMode = checkPlanMode;
|
|
23
|
+
/**
|
|
24
|
+
* Tools that cannot alter anything outside the conversation.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately an ALLOWLIST. A denylist would mean every tool added later is
|
|
27
|
+
* writable-by-default in plan mode, and the person adding it has no reason to
|
|
28
|
+
* think about plan mode at all — the failure would ship silently. With an
|
|
29
|
+
* allowlist, a new tool is refused until someone deliberately classifies it,
|
|
30
|
+
* and the refusal is visible the first time anybody tries it.
|
|
31
|
+
*/
|
|
32
|
+
const READ_ONLY_TOOLS = new Set([
|
|
33
|
+
'read_file', 'search_files', 'glob', 'list_directory', 'notebook_read',
|
|
34
|
+
// Planning bookkeeping. todo_write persists only to the session's own todo
|
|
35
|
+
// list, not the repo, so it stays available — a plan mode that cannot draft
|
|
36
|
+
// a checklist is missing the point of plan mode.
|
|
37
|
+
'todo_write', 'todo_read',
|
|
38
|
+
// Reading memory is fine; memory_write is NOT here on purpose. Memory is
|
|
39
|
+
// durable state that survives the session, so writing it is a real mutation
|
|
40
|
+
// even though no file in the repo changes.
|
|
41
|
+
'memory_read',
|
|
42
|
+
'use_skill',
|
|
43
|
+
'fetch_url', 'web_search',
|
|
44
|
+
// Delegating research is ALLOWED, and this is safe rather than a hole:
|
|
45
|
+
// runSubTask passes planMode down, so the sub-agent is under the same lock and
|
|
46
|
+
// its own write tools are refused by this very function one level deeper.
|
|
47
|
+
//
|
|
48
|
+
// Worth allowing rather than blanket-refusing, because plan mode exists to
|
|
49
|
+
// support deep research and the explorer agent is the cheapest way to do bulk
|
|
50
|
+
// searching without flooding the main context. Blocking `task` would have made
|
|
51
|
+
// the mode's headline use case worse, for no security gain.
|
|
52
|
+
'task',
|
|
53
|
+
// VS Code language server — all pure queries.
|
|
54
|
+
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
55
|
+
'get_hover', 'get_diagnostics',
|
|
56
|
+
// Reading output from an ALREADY-running background shell. Starting one is
|
|
57
|
+
// gated with the rest of bash below; draining a buffer is not a new effect.
|
|
58
|
+
'bash_output',
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* Commands allowed through `bash` in plan mode.
|
|
62
|
+
*
|
|
63
|
+
* bash is the hard case: it is one tool that spans `git log` and `rm -rf /`,
|
|
64
|
+
* so plan mode is only as strong as this list. Hence a strict allowlist of
|
|
65
|
+
* verbs known to be read-only, plus the argument screening in
|
|
66
|
+
* `isReadOnlyCommand` for the several that are read-only ONLY in some forms.
|
|
67
|
+
*/
|
|
68
|
+
const READ_ONLY_BASH = new Set([
|
|
69
|
+
'ls', 'cat', 'head', 'tail', 'wc', 'file', 'stat', 'du', 'df', 'pwd', 'which',
|
|
70
|
+
'type', 'echo', 'printf', 'basename', 'dirname', 'realpath', 'readlink',
|
|
71
|
+
'grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack', 'find', 'fd', 'locate',
|
|
72
|
+
'diff', 'comm', 'cmp', 'sort', 'uniq', 'cut', 'tr', 'column', 'jq', 'yq',
|
|
73
|
+
'date', 'whoami', 'hostname', 'uname', 'env', 'printenv', 'id', 'groups',
|
|
74
|
+
'ps', 'top', 'uptime', 'free', 'node', 'python', 'python3',
|
|
75
|
+
'tree', 'less', 'more', 'nl', 'tac', 'strings', 'md5sum', 'sha256sum',
|
|
76
|
+
'true', 'false', 'test', 'sleep', 'seq', 'expr',
|
|
77
|
+
]);
|
|
78
|
+
/**
|
|
79
|
+
* Subcommands that are read-only for tools whose safety depends on the verb.
|
|
80
|
+
*
|
|
81
|
+
* `git` is the reason this exists: `git log` and `git push --force` share one
|
|
82
|
+
* binary, so classifying by executable name alone is useless here.
|
|
83
|
+
*/
|
|
84
|
+
const READ_ONLY_SUBCOMMANDS = {
|
|
85
|
+
git: new Set([
|
|
86
|
+
'log', 'show', 'diff', 'status', 'blame', 'branch', 'tag', 'remote',
|
|
87
|
+
'describe', 'rev-parse', 'rev-list', 'ls-files', 'ls-tree', 'ls-remote',
|
|
88
|
+
'cat-file', 'shortlog', 'reflog', 'whatchanged', 'grep', 'config',
|
|
89
|
+
'check-ignore', 'merge-base', 'name-rev', 'count-objects', 'verify-commit',
|
|
90
|
+
]),
|
|
91
|
+
// Package managers: only their query verbs.
|
|
92
|
+
npm: new Set(['ls', 'list', 'view', 'info', 'outdated', 'why', 'config', 'ping', 'search']),
|
|
93
|
+
pnpm: new Set(['ls', 'list', 'view', 'info', 'outdated', 'why', 'config']),
|
|
94
|
+
yarn: new Set(['list', 'info', 'why', 'config']),
|
|
95
|
+
cargo: new Set(['tree', 'metadata', 'search']),
|
|
96
|
+
go: new Set(['list', 'version', 'env', 'vet', 'doc']),
|
|
97
|
+
docker: new Set(['ps', 'images', 'logs', 'inspect', 'version', 'info', 'top', 'port', 'diff', 'stats']),
|
|
98
|
+
kubectl: new Set(['get', 'describe', 'logs', 'explain', 'top', 'version', 'api-resources', 'api-versions']),
|
|
99
|
+
// gh: read verbs only. `gh run rerun`, `gh pr merge` etc. are excluded, and
|
|
100
|
+
// the nested-verb check in isReadOnlyCommand handles `gh pr view` vs `gh pr merge`.
|
|
101
|
+
gh: new Set(['browse']),
|
|
102
|
+
};
|
|
103
|
+
/** Second-level read verbs for CLIs shaped as `<tool> <noun> <verb>`. */
|
|
104
|
+
const READ_ONLY_NESTED = {
|
|
105
|
+
gh: new Set(['view', 'list', 'status', 'diff', 'checks']),
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Shell metacharacters that can smuggle a second command past verb inspection.
|
|
109
|
+
*
|
|
110
|
+
* We could parse the chain and check each segment — `destructive.ts` does
|
|
111
|
+
* something like that — but plan mode is a deliberate, temporary restriction:
|
|
112
|
+
* the honest answer to "can I run this compound pipeline while frozen?" is
|
|
113
|
+
* "no, and it costs you nothing to wait". Rejecting the whole shape is far
|
|
114
|
+
* easier to get right than parsing shell grammar, and being wrong here means
|
|
115
|
+
* writing to a repo the user thinks is locked.
|
|
116
|
+
*
|
|
117
|
+
* `|` is included: `cat x | tee y` writes, and so does `... | sh`.
|
|
118
|
+
*/
|
|
119
|
+
const SHELL_CONTROL = /[\n\r;&|`]|\$\(|>>|>|<\(/;
|
|
120
|
+
function firstWords(command) {
|
|
121
|
+
return command.trim().split(/\s+/).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Is this bash command provably read-only?
|
|
125
|
+
*
|
|
126
|
+
* "Provably" is doing real work: unknown verbs are refused, not guessed at.
|
|
127
|
+
*/
|
|
128
|
+
function isReadOnlyCommand(command) {
|
|
129
|
+
const cmd = command.trim();
|
|
130
|
+
if (!cmd)
|
|
131
|
+
return false;
|
|
132
|
+
// Any chaining/redirection/substitution — refuse the whole thing.
|
|
133
|
+
if (SHELL_CONTROL.test(cmd))
|
|
134
|
+
return false;
|
|
135
|
+
const words = firstWords(cmd);
|
|
136
|
+
if (!words.length)
|
|
137
|
+
return false;
|
|
138
|
+
// `VAR=x cmd` — skip leading environment assignments to find the real verb.
|
|
139
|
+
let i = 0;
|
|
140
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]))
|
|
141
|
+
i++;
|
|
142
|
+
if (i >= words.length)
|
|
143
|
+
return false;
|
|
144
|
+
let verb = words[i];
|
|
145
|
+
// Strip a path prefix: /usr/bin/git → git
|
|
146
|
+
const slash = verb.lastIndexOf('/');
|
|
147
|
+
if (slash >= 0)
|
|
148
|
+
verb = verb.slice(slash + 1);
|
|
149
|
+
// `sudo anything` is refused outright regardless of the verb behind it: plan
|
|
150
|
+
// mode is a promise about this machine, and privilege escalation is exactly
|
|
151
|
+
// where a mistaken allow is least recoverable.
|
|
152
|
+
if (verb === 'sudo' || verb === 'doas' || verb === 'su')
|
|
153
|
+
return false;
|
|
154
|
+
const rest = words.slice(i + 1).filter((w) => !w.startsWith('-'));
|
|
155
|
+
const subs = READ_ONLY_SUBCOMMANDS[verb];
|
|
156
|
+
if (subs) {
|
|
157
|
+
const sub = rest[0];
|
|
158
|
+
if (!sub) {
|
|
159
|
+
// Bare `git` / `npm` just prints help — harmless.
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (subs.has(sub)) {
|
|
163
|
+
// `git config --global x y` WRITES. Only the read form (no value) passes.
|
|
164
|
+
if (verb === 'git' && sub === 'config') {
|
|
165
|
+
const args = words.slice(i + 2).filter((w) => !w.startsWith('-'));
|
|
166
|
+
return args.length <= 1;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const nested = READ_ONLY_NESTED[verb];
|
|
171
|
+
if (nested && rest[1] && nested.has(rest[1]))
|
|
172
|
+
return true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (!READ_ONLY_TOOLS.has(verb) && !READ_ONLY_BASH.has(verb))
|
|
176
|
+
return false;
|
|
177
|
+
// Interpreters can execute arbitrary code inline; only allow the trivial
|
|
178
|
+
// read-only forms (`node --version`). `node -e "fs.rmSync(...)"` must not pass.
|
|
179
|
+
if (verb === 'node' || verb === 'python' || verb === 'python3') {
|
|
180
|
+
return words.slice(i + 1).every((w) => w === '--version' || w === '-V' || w === '-v');
|
|
181
|
+
}
|
|
182
|
+
// `find` can execute and delete.
|
|
183
|
+
if (verb === 'find' && words.some((w) => w === '-exec' || w === '-execdir' || w === '-delete')) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// Reading a file with `test`/`stat` is fine, but `tee` is not in the list at
|
|
187
|
+
// all, and `echo`/`printf` are only safe because redirection is already
|
|
188
|
+
// rejected by SHELL_CONTROL above.
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Should this tool call be refused because the session is in plan mode?
|
|
193
|
+
* Returns null when the call is allowed.
|
|
194
|
+
*/
|
|
195
|
+
function checkPlanMode(tool, input) {
|
|
196
|
+
if (tool === 'bash') {
|
|
197
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
198
|
+
// Starting a background process in a "frozen" session is a side effect that
|
|
199
|
+
// outlives the refusal, so it is refused even for an otherwise-safe verb.
|
|
200
|
+
if (input.run_in_background === true) {
|
|
201
|
+
return {
|
|
202
|
+
reason: 'bash-not-read-only',
|
|
203
|
+
message: 'Plan mode is ON: background processes cannot be started. Finish researching and propose ' +
|
|
204
|
+
'your plan; the user will leave plan mode if they want it carried out.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (isReadOnlyCommand(command))
|
|
208
|
+
return null;
|
|
209
|
+
return {
|
|
210
|
+
reason: 'bash-not-read-only',
|
|
211
|
+
message: `Plan mode is ON, so \`bash\` is limited to provably read-only commands and this one was not ` +
|
|
212
|
+
'recognised as such (chained commands, redirection and command substitution are always refused). ' +
|
|
213
|
+
'Do NOT ask the user to approve it and do NOT try a variation to get around this — it is a ' +
|
|
214
|
+
'session-wide lock, not a per-call prompt. Use read-only inspection instead, and put the command ' +
|
|
215
|
+
'in your plan as a step to run once plan mode is off.',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (READ_ONLY_TOOLS.has(tool))
|
|
219
|
+
return null;
|
|
220
|
+
return {
|
|
221
|
+
reason: 'mutating-tool',
|
|
222
|
+
message: `Plan mode is ON, so \`${tool}\` is unavailable — this session may not modify anything yet. ` +
|
|
223
|
+
'Do NOT ask for approval: no answer the user gives at this prompt can unlock it, because it is a ' +
|
|
224
|
+
'session-wide lock they control directly. Research with read-only tools and finish by proposing ' +
|
|
225
|
+
'a concrete plan (files to change, in order, and how to verify). The user will exit plan mode to ' +
|
|
226
|
+
'let you carry it out.',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Text appended to the system prompt while plan mode is active. */
|
|
230
|
+
exports.PLAN_MODE_INSTRUCTIONS = [
|
|
231
|
+
'# PLAN MODE IS ACTIVE',
|
|
232
|
+
'',
|
|
233
|
+
'This session is READ-ONLY. You cannot edit files, write files, run mutating commands, or start',
|
|
234
|
+
'background processes. This is enforced outside your control — it is not a permission prompt, and',
|
|
235
|
+
'the user cannot approve an exception from inside this turn.',
|
|
236
|
+
'',
|
|
237
|
+
'Therefore:',
|
|
238
|
+
'- NEVER ask "shall I go ahead and make this change?" — you cannot, whatever the answer.',
|
|
239
|
+
'- NEVER attempt a mutating tool "just to see" — it will be refused and wastes the turn.',
|
|
240
|
+
'- NEVER try to reach a blocked command another way (a different flag, a script, an interpreter).',
|
|
241
|
+
'',
|
|
242
|
+
'Your job is to research thoroughly, then deliver a plan:',
|
|
243
|
+
'1. What you found — the specific files, functions and line numbers that matter.',
|
|
244
|
+
'2. What you propose to change — file by file, in dependency order.',
|
|
245
|
+
'3. Risks and unknowns — what could break, what you could not verify, what you assumed.',
|
|
246
|
+
'4. How it will be verified — the exact build/test/lint command that proves it works.',
|
|
247
|
+
'',
|
|
248
|
+
'Be concrete. "Update the auth logic" is not a plan; "add a `refreshToken` field to AuthConfig in',
|
|
249
|
+
'auth/index.ts:42, then thread it through client.ts:118" is. The user will review the plan and',
|
|
250
|
+
'leave plan mode to have it carried out.',
|
|
251
|
+
].join('\n');
|
|
252
|
+
//# sourceMappingURL=planMode.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,11 @@ export * from './mcp/manager';
|
|
|
17
17
|
export * from './checkpoint/manager';
|
|
18
18
|
export * from './commands/loader';
|
|
19
19
|
export * from './agent/agentTypes';
|
|
20
|
+
export * from './agent/planMode';
|
|
21
|
+
export * from './agent/agentRegistry';
|
|
20
22
|
export * from './permissions/rules';
|
|
21
23
|
export * from './permissions/destructive';
|
|
22
24
|
export * from './plugins/index';
|
|
23
25
|
export * from './plugins/installer';
|
|
26
|
+
export * from './plugins/sources';
|
|
24
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -33,8 +33,11 @@ __exportStar(require("./mcp/manager"), exports);
|
|
|
33
33
|
__exportStar(require("./checkpoint/manager"), exports);
|
|
34
34
|
__exportStar(require("./commands/loader"), exports);
|
|
35
35
|
__exportStar(require("./agent/agentTypes"), exports);
|
|
36
|
+
__exportStar(require("./agent/planMode"), exports);
|
|
37
|
+
__exportStar(require("./agent/agentRegistry"), exports);
|
|
36
38
|
__exportStar(require("./permissions/rules"), exports);
|
|
37
39
|
__exportStar(require("./permissions/destructive"), exports);
|
|
38
40
|
__exportStar(require("./plugins/index"), exports);
|
|
39
41
|
__exportStar(require("./plugins/installer"), exports);
|
|
42
|
+
__exportStar(require("./plugins/sources"), exports);
|
|
40
43
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;AA8ID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
|
|
@@ -174,6 +174,20 @@ function matchValues(tool, input, workDir) {
|
|
|
174
174
|
case 'stock_photo':
|
|
175
175
|
vals.push(str(input.path));
|
|
176
176
|
break;
|
|
177
|
+
// Sub-agent dispatch. Matching on the agent NAME is what makes
|
|
178
|
+
// `deny: ["task(explorer)"]` mean "this repo may not use the explorer
|
|
179
|
+
// agent" — the Claude-Code way of switching off a built-in you don't want.
|
|
180
|
+
//
|
|
181
|
+
// Without this case, `task` fell through to `default`, which reads
|
|
182
|
+
// `input.path` — a key the task tool does not have. Every patterned task
|
|
183
|
+
// rule therefore matched nothing and silently did nothing, which is the
|
|
184
|
+
// worst failure mode for a deny rule: it looks configured and isn't.
|
|
185
|
+
//
|
|
186
|
+
// A bare `task` rule (no pattern) still matches any dispatch, so
|
|
187
|
+
// `deny: ["task"]` disables sub-agents wholesale.
|
|
188
|
+
case 'task':
|
|
189
|
+
vals.push(str(input.subagent_type));
|
|
190
|
+
break;
|
|
177
191
|
default:
|
|
178
192
|
pushPath(str(input.path));
|
|
179
193
|
break;
|