@nexrall/code-core 1.4.23 → 1.4.25
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/agentTypes.d.ts +66 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +415 -31
- package/dist/agent/loop.d.ts +34 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +117 -8
- package/dist/agent/securityLint.d.ts +27 -0
- package/dist/agent/securityLint.d.ts.map +1 -0
- package/dist/agent/securityLint.js +195 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +24 -4
- package/package.json +1 -1
|
@@ -8,10 +8,74 @@ export interface AgentType {
|
|
|
8
8
|
/** System instructions (the markdown body below the frontmatter). */
|
|
9
9
|
prompt: string;
|
|
10
10
|
source: 'project' | 'global' | 'builtin' | 'plugin';
|
|
11
|
+
/**
|
|
12
|
+
* When true, this agent's write tools may only target TEST files.
|
|
13
|
+
*
|
|
14
|
+
* Needed because a tool allowlist is all-or-nothing per tool: granting
|
|
15
|
+
* `edit_file` grants it for every path. The `test-writer` agent must be able to
|
|
16
|
+
* write tests while being unable to "fix" production source to make a test
|
|
17
|
+
* pass — the single most common way a test-writing agent destroys signal — and
|
|
18
|
+
* a prompt instruction alone cannot guarantee that. Enforced in loop.ts's
|
|
19
|
+
* permission gate, where it is a real refusal rather than a request.
|
|
20
|
+
*/
|
|
21
|
+
testFilesOnly?: boolean;
|
|
11
22
|
}
|
|
12
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* A problem found while loading an agent definition.
|
|
25
|
+
*
|
|
26
|
+
* These used to be silently swallowed. Every one of them changed what an agent
|
|
27
|
+
* could DO — a mistyped tool name removed a capability, a missing frontmatter
|
|
28
|
+
* block removed the allowlist entirely — and the user was told nothing.
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentWarning {
|
|
31
|
+
/** Absolute path of the definition file the problem was found in. */
|
|
32
|
+
file: string;
|
|
33
|
+
/** Agent name, when one could be determined. */
|
|
34
|
+
agent: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Discover all agent types. Precedence: project > global > plugin > builtin.
|
|
39
|
+
*
|
|
40
|
+
* Results are returned in a STABLE order (builtins in declared order, then
|
|
41
|
+
* user-supplied ones alphabetically) rather than in discovery order. The
|
|
42
|
+
* system-prompt catalogue is built from this, and a set that reshuffles between
|
|
43
|
+
* runs would silently bust the prompt cache on the block it lives in — filesystem
|
|
44
|
+
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
45
|
+
*/
|
|
13
46
|
export declare function loadAgentTypes(workDir: string): AgentType[];
|
|
14
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* As `loadAgentTypes`, but also reports what was wrong with the definitions.
|
|
49
|
+
*
|
|
50
|
+
* Split in two so the common caller stays a one-liner while the CLI's `/agents`
|
|
51
|
+
* and the agent loop can surface problems. Warnings never hide an agent: a
|
|
52
|
+
* flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
|
|
53
|
+
* because making a user's agent vanish over a typo is its own kind of silent
|
|
54
|
+
* failure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadAgentTypesWithWarnings(workDir: string): {
|
|
57
|
+
types: AgentType[];
|
|
58
|
+
warnings: AgentWarning[];
|
|
59
|
+
};
|
|
60
|
+
/** The tool names a hand-written allowlist may use (exported for validation + tests). */
|
|
61
|
+
export declare function knownToolNames(): string[];
|
|
62
|
+
/** The built-in agents, for clients that want to show them alongside user-defined ones. */
|
|
63
|
+
export declare function builtinAgents(): AgentType[];
|
|
64
|
+
/**
|
|
65
|
+
* One line per agent for the system prompt's <available_subagents> block.
|
|
66
|
+
*
|
|
67
|
+
* Reports each agent's CAPABILITY CLASS rather than enumerating its allowlist.
|
|
68
|
+
* The full lists (~15 tool names each) went into every single request while
|
|
69
|
+
* telling the model nothing it needs in order to choose: what matters when
|
|
70
|
+
* delegating is "can this one edit files?" and "what is it for?", not whether
|
|
71
|
+
* `get_hover` happens to be included. With six builtins the verbatim lists cost
|
|
72
|
+
* ~670 tokens per request, most of it near-identical boilerplate that also
|
|
73
|
+
* weakens the signal it was meant to carry.
|
|
74
|
+
*
|
|
75
|
+
* The per-agent description already states its own restrictions in prose, and the
|
|
76
|
+
* allowlist is enforced at the permission gate regardless of what is advertised
|
|
77
|
+
* here — so this is purely a summary, never the mechanism.
|
|
78
|
+
*/
|
|
15
79
|
export declare function summariseAgents(types: AgentType[]): string;
|
|
16
80
|
export declare function findAgentType(types: AgentType[], name: string | undefined): AgentType | undefined;
|
|
17
81
|
//# sourceMappingURL=agentTypes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA6QD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA0JD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAqB5G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
|
package/dist/agent/agentTypes.js
CHANGED
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadAgentTypes = loadAgentTypes;
|
|
37
|
+
exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings;
|
|
38
|
+
exports.knownToolNames = knownToolNames;
|
|
39
|
+
exports.builtinAgents = builtinAgents;
|
|
37
40
|
exports.summariseAgents = summariseAgents;
|
|
38
41
|
exports.findAgentType = findAgentType;
|
|
39
42
|
const fs = __importStar(require("fs"));
|
|
@@ -43,11 +46,58 @@ const index_1 = require("../plugins/index");
|
|
|
43
46
|
// ── Built-in agent types ──────────────────────────────────────────────────────
|
|
44
47
|
// Shipped defaults; lowest precedence (project > global > builtin), so a user
|
|
45
48
|
// can override any of them with a same-name .nexrall/agents/<name>.md file.
|
|
49
|
+
// Shared read-only tool set.
|
|
50
|
+
//
|
|
51
|
+
// An allowlist only ever GRANTS — a name that doesn't exist on the current client
|
|
52
|
+
// is simply never offered to the model, so listing the VS Code language-server
|
|
53
|
+
// tools alongside the CLI ones is safe and gives each agent the best available
|
|
54
|
+
// capability on whichever client it runs.
|
|
55
|
+
//
|
|
56
|
+
// This exists because it was previously inlined per agent, and the one agent that
|
|
57
|
+
// had it (`reviewer`) listed five VS Code-only tools, leaving it with just five
|
|
58
|
+
// usable tools when run from the CLI — a silent capability gap that is very easy
|
|
59
|
+
// to reintroduce by copy-pasting a list.
|
|
60
|
+
const READ_ONLY_TOOLS = [
|
|
61
|
+
// Universal
|
|
62
|
+
'read_file', 'search_files', 'glob', 'list_directory', 'bash', 'bash_output',
|
|
63
|
+
// kill_shell belongs next to bash_output: an agent that can start a background
|
|
64
|
+
// process and poll it but never stop it leaks that process past its own
|
|
65
|
+
// lifetime. Stopping a shell you started is not a write to the repo.
|
|
66
|
+
'kill_shell',
|
|
67
|
+
'notebook_read', 'todo_write', 'todo_read',
|
|
68
|
+
// Skills are reusable prompt playbooks, and loop.ts advertises the skills
|
|
69
|
+
// catalogue to sub-agents at EVERY depth — so withholding the tool that loads
|
|
70
|
+
// one meant showing every sub-agent a menu it could not order from.
|
|
71
|
+
'use_skill',
|
|
72
|
+
// VS Code language server (ignored on the CLI)
|
|
73
|
+
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
74
|
+
'get_hover', 'get_diagnostics',
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Read-only + the network, for agents that must consult external sources.
|
|
78
|
+
*
|
|
79
|
+
* `web_search` is deliberately ABSENT despite the name of this list. It is a
|
|
80
|
+
* SERVER-SIDE tool: Anthropic executes it and returns the result inside the same
|
|
81
|
+
* assistant message, so loop.ts filters those blocks out before the permission
|
|
82
|
+
* gate ever runs. Listing it here would be theatre — it grants nothing (agents
|
|
83
|
+
* without it can still search) and denies nothing. Naming the absence is the
|
|
84
|
+
* only way to stop it being "helpfully" re-added.
|
|
85
|
+
*/
|
|
86
|
+
const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'fetch_url'];
|
|
87
|
+
/** Read-only + the write tools, for agents that produce code. */
|
|
88
|
+
const WRITE_TOOLS = [
|
|
89
|
+
...READ_ONLY_TOOLS,
|
|
90
|
+
'write_file', 'edit_file', 'multi_edit', 'create_directory', 'move_file', 'copy_file',
|
|
91
|
+
// notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
|
|
92
|
+
// knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
|
|
93
|
+
// test-writer silently could not touch notebooks.
|
|
94
|
+
'notebook_edit',
|
|
95
|
+
];
|
|
46
96
|
const BUILTIN_AGENTS = [
|
|
47
97
|
{
|
|
48
98
|
name: 'reviewer',
|
|
49
99
|
description: 'Read-only code reviewer — finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.',
|
|
50
|
-
tools:
|
|
100
|
+
tools: READ_ONLY_TOOLS,
|
|
51
101
|
source: 'builtin',
|
|
52
102
|
prompt: [
|
|
53
103
|
'You are a meticulous senior code reviewer. You NEVER modify files — you only read, search, and report.',
|
|
@@ -64,23 +114,209 @@ const BUILTIN_AGENTS = [
|
|
|
64
114
|
'then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale.',
|
|
65
115
|
].join('\n'),
|
|
66
116
|
},
|
|
117
|
+
// Promoted from the security-audit plugin to a builtin.
|
|
118
|
+
//
|
|
119
|
+
// Leaving it plugin-only was indefensible next to `reviewer` being builtin:
|
|
120
|
+
// reviewer's own prompt already tells it to look for security issues, so
|
|
121
|
+
// security IS treated as default work — yet the specialist agent for it was
|
|
122
|
+
// invisible unless the user happened to know the plugin existed. For an agent
|
|
123
|
+
// that WRITES code, "you only get a security review if you knew to install
|
|
124
|
+
// something" is the wrong default.
|
|
125
|
+
{
|
|
126
|
+
name: 'security-auditor',
|
|
127
|
+
description: 'Read-only security auditor — hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.',
|
|
128
|
+
tools: READ_ONLY_TOOLS,
|
|
129
|
+
model: 'pro',
|
|
130
|
+
source: 'builtin',
|
|
131
|
+
prompt: [
|
|
132
|
+
'You are a security auditor. You find real, exploitable flaws — not style issues.',
|
|
133
|
+
'',
|
|
134
|
+
'Method:',
|
|
135
|
+
'1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network',
|
|
136
|
+
' input, deserialization), then trace user-controlled data inward to where it is used.',
|
|
137
|
+
'2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.',
|
|
138
|
+
'3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;',
|
|
139
|
+
' Medium = needs unusual preconditions; Low = hardening.',
|
|
140
|
+
'',
|
|
141
|
+
'Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken',
|
|
142
|
+
'authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,',
|
|
143
|
+
'path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,',
|
|
144
|
+
'and crypto misuse (hand-rolled comparison, predictable randomness).',
|
|
145
|
+
'',
|
|
146
|
+
'Hard rules:',
|
|
147
|
+
'- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.',
|
|
148
|
+
'- NEVER print a discovered secret\'s value. Report its location and advise rotation.',
|
|
149
|
+
'- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.',
|
|
150
|
+
'- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.',
|
|
151
|
+
].join('\n'),
|
|
152
|
+
},
|
|
153
|
+
// The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
|
|
154
|
+
// search that would otherwise flood the parent's context. Defaults to the
|
|
155
|
+
// cheapest model on purpose — "find every caller of X" has no need of a
|
|
156
|
+
// frontier model, and this is the agent most likely to be spawned in bulk.
|
|
157
|
+
{
|
|
158
|
+
name: 'explorer',
|
|
159
|
+
description: 'Fast read-only codebase explorer — locates files, symbols, and call sites and reports concise findings. Use to keep bulk searching out of the main context. Cannot modify files.',
|
|
160
|
+
tools: READ_ONLY_TOOLS,
|
|
161
|
+
model: 'turbo',
|
|
162
|
+
source: 'builtin',
|
|
163
|
+
prompt: [
|
|
164
|
+
'You map code. You NEVER modify anything.',
|
|
165
|
+
'',
|
|
166
|
+
'Method:',
|
|
167
|
+
'1. Prefer structural search over text search where available (get_workspace_symbols, find_references,',
|
|
168
|
+
' go_to_definition); fall back to search_files/glob otherwise.',
|
|
169
|
+
'2. Read only the sections you need — use read_file with offset/limit on large files instead of',
|
|
170
|
+
' pulling in thousands of lines.',
|
|
171
|
+
'3. Follow the real call graph rather than guessing from names.',
|
|
172
|
+
'',
|
|
173
|
+
'Your ONLY output is a compact report: the file:line locations that matter, how they relate, and the',
|
|
174
|
+
'direct answer to the question you were given. This exists to keep bulk search OUT of the parent\'s',
|
|
175
|
+
'context, so do not paste large file contents back — cite locations and summarise. Say plainly when',
|
|
176
|
+
'something does not exist; a confident wrong answer is far worse than "not found".',
|
|
177
|
+
].join('\n'),
|
|
178
|
+
},
|
|
179
|
+
// Matches Claude Code's built-in `Plan`: research a change and return a
|
|
180
|
+
// strategy, deliberately WITHOUT write access so "make a plan" can never
|
|
181
|
+
// quietly become "start editing".
|
|
182
|
+
{
|
|
183
|
+
name: 'planner',
|
|
184
|
+
description: 'Read-only planning agent — researches a change and returns a concrete step-by-step implementation plan with risks and affected files. Cannot modify files.',
|
|
185
|
+
tools: RESEARCH_TOOLS,
|
|
186
|
+
model: 'pro',
|
|
187
|
+
source: 'builtin',
|
|
188
|
+
prompt: [
|
|
189
|
+
'You produce implementation plans. You NEVER modify files — planning and doing are separate steps,',
|
|
190
|
+
'and this agent exists so "plan it" cannot silently turn into "change it".',
|
|
191
|
+
'',
|
|
192
|
+
'Method:',
|
|
193
|
+
'1. Read the actual code before proposing anything. No plan may rest on an assumed API shape.',
|
|
194
|
+
'2. Find every affected call site (find_references / search_files) and list them.',
|
|
195
|
+
'3. Order the steps so the tree stays working after each one — types, then implementation, then',
|
|
196
|
+
' tests, then exports/registration.',
|
|
197
|
+
'',
|
|
198
|
+
'Output:',
|
|
199
|
+
'- Goal, in one sentence.',
|
|
200
|
+
'- Numbered steps, each with the exact files touched and what changes in them.',
|
|
201
|
+
'- Risks + the specific thing that could break, and how it would be detected.',
|
|
202
|
+
'- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).',
|
|
203
|
+
'- Anything genuinely ambiguous, stated as an open question rather than a silent assumption.',
|
|
204
|
+
].join('\n'),
|
|
205
|
+
},
|
|
206
|
+
// Promoted from the test-gen plugin. Needs write access — it produces test
|
|
207
|
+
// files — but is deliberately forbidden from touching source, because "make the
|
|
208
|
+
// tests pass" is the single most common way an agent destroys signal.
|
|
209
|
+
{
|
|
210
|
+
name: 'test-writer',
|
|
211
|
+
description: 'Writes tests that follow the project\'s existing conventions. May create/edit TEST files only — never production source.',
|
|
212
|
+
tools: WRITE_TOOLS,
|
|
213
|
+
// Enforced, not merely requested: the permission gate refuses a write whose
|
|
214
|
+
// path is not a test file. Without this the allowlist would grant edit_file
|
|
215
|
+
// for every path and the rule below would be a suggestion the model is free
|
|
216
|
+
// to rationalise its way past.
|
|
217
|
+
testFilesOnly: true,
|
|
218
|
+
source: 'builtin',
|
|
219
|
+
prompt: [
|
|
220
|
+
'You write tests. You may create and edit TEST files only.',
|
|
221
|
+
'',
|
|
222
|
+
'Hard rules — these are the ways test-writing agents destroy value, so they are non-negotiable:',
|
|
223
|
+
'- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.',
|
|
224
|
+
'- NEVER weaken, delete or skip an existing assertion or test.',
|
|
225
|
+
'- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.',
|
|
226
|
+
'',
|
|
227
|
+
'Method:',
|
|
228
|
+
'1. Read the existing tests FIRST and copy their conventions exactly — runner, file naming, layout,',
|
|
229
|
+
' assertion style, fixture/helper patterns. Never introduce a new framework.',
|
|
230
|
+
'2. Test observable behaviour and the contract, not private internals.',
|
|
231
|
+
'3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,',
|
|
232
|
+
' boundaries, error paths, concurrency where it applies.',
|
|
233
|
+
'4. No sleeps or wall-clock dependence — those produce the flaky tests that get deleted later.',
|
|
234
|
+
'5. RUN the tests you wrote and report the real output. Never claim a test passes without running it.',
|
|
235
|
+
].join('\n'),
|
|
236
|
+
},
|
|
237
|
+
// The DevOps gap — answered with a READ-ONLY advisor, not an operator.
|
|
238
|
+
//
|
|
239
|
+
// A "DevOps agent" with write/apply access is a genuinely different risk class
|
|
240
|
+
// from the others here: its mistakes are `kubectl delete`, a bad `terraform
|
|
241
|
+
// apply`, a broken deploy pipeline — often not revertible and affecting
|
|
242
|
+
// production rather than a working tree. So this one diagnoses and proposes a
|
|
243
|
+
// diff; a human applies it. That asymmetry is the whole design.
|
|
244
|
+
{
|
|
245
|
+
name: 'devops-advisor',
|
|
246
|
+
description: 'Read-only CI/CD, container, and infrastructure advisor — diagnoses pipelines, Dockerfiles, and k8s manifests and proposes concrete fixes as a diff. Never applies changes.',
|
|
247
|
+
tools: RESEARCH_TOOLS,
|
|
248
|
+
model: 'pro',
|
|
249
|
+
source: 'builtin',
|
|
250
|
+
prompt: [
|
|
251
|
+
'You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.',
|
|
252
|
+
'',
|
|
253
|
+
'Hard rules:',
|
|
254
|
+
'- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only',
|
|
255
|
+
' (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).',
|
|
256
|
+
' NEVER run anything that mutates infrastructure — no apply/delete/scale/rollout/restart/push,',
|
|
257
|
+
' no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.',
|
|
258
|
+
'- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.',
|
|
259
|
+
'',
|
|
260
|
+
'Method:',
|
|
261
|
+
'1. Read what actually exists — workflow files, Dockerfiles, manifests, kustomize overlays, the',
|
|
262
|
+
' deploy scripts — before drawing any conclusion. Never reason from what a stack "usually" looks like.',
|
|
263
|
+
'2. Follow the real path a change takes to production, and name the step that is broken or missing.',
|
|
264
|
+
'3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually',
|
|
265
|
+
' needs, image tags that do not match what is deployed, missing health probes, absent resource',
|
|
266
|
+
' limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and',
|
|
267
|
+
' migrations that must run before the new image is live.',
|
|
268
|
+
'',
|
|
269
|
+
'Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or',
|
|
270
|
+
'exact file content, and the command a human should run to apply and verify it.',
|
|
271
|
+
].join('\n'),
|
|
272
|
+
},
|
|
67
273
|
];
|
|
274
|
+
/**
|
|
275
|
+
* Every tool name a client may offer, for validating a hand-written allowlist.
|
|
276
|
+
*
|
|
277
|
+
* Deliberately a SEPARATE list rather than an import from tools/executor.ts:
|
|
278
|
+
* that module's TOOL_MAP is the CLI's local dispatch table and legitimately
|
|
279
|
+
* lacks `get_diagnostics` (intercepted by VS Code before the executor) and
|
|
280
|
+
* `web_search` (run server-side by Anthropic). Validating against it would
|
|
281
|
+
* reject two perfectly valid names. A canary test asserts every name used by the
|
|
282
|
+
* builtin agents appears here, so the two cannot drift apart unnoticed.
|
|
283
|
+
*/
|
|
284
|
+
const KNOWN_TOOL_NAMES = new Set([
|
|
285
|
+
'read_file', 'write_file', 'edit_file', 'multi_edit', 'list_directory', 'create_directory',
|
|
286
|
+
'move_file', 'copy_file', 'delete_file', 'search_files', 'glob',
|
|
287
|
+
'bash', 'bash_output', 'kill_shell',
|
|
288
|
+
'notebook_read', 'notebook_edit',
|
|
289
|
+
'todo_write', 'todo_read', 'memory_write', 'memory_read', 'use_skill',
|
|
290
|
+
'fetch_url', 'web_search', 'generate_image', 'stock_photo', 'open_in_browser',
|
|
291
|
+
'task',
|
|
292
|
+
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
293
|
+
'get_hover', 'get_diagnostics',
|
|
294
|
+
]);
|
|
295
|
+
/** Frontmatter keys this parser understands, for typo detection. */
|
|
296
|
+
const KNOWN_META_KEYS = new Set([
|
|
297
|
+
'name', 'description', 'tools', 'model', 'test_files_only', 'testfilesonly',
|
|
298
|
+
]);
|
|
299
|
+
const VALID_MODELS = ['turbo', 'pro', 'ultra'];
|
|
68
300
|
function parseFrontmatter(raw) {
|
|
69
301
|
const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
70
302
|
if (!m)
|
|
71
|
-
return { meta: {}, body: raw.trim() };
|
|
303
|
+
return { meta: {}, body: raw.trim(), ok: false };
|
|
72
304
|
const meta = {};
|
|
73
305
|
for (const line of m[1].split(/\r?\n/)) {
|
|
74
306
|
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
75
307
|
if (kv)
|
|
76
308
|
meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
77
309
|
}
|
|
78
|
-
return { meta, body: (m[2] ?? '').trim() };
|
|
310
|
+
return { meta, body: (m[2] ?? '').trim(), ok: true };
|
|
79
311
|
}
|
|
80
312
|
function parseModel(v) {
|
|
81
313
|
const s = (v ?? '').toLowerCase();
|
|
82
314
|
return s === 'turbo' || s === 'pro' || s === 'ultra' ? s : undefined;
|
|
83
315
|
}
|
|
316
|
+
/** Parse a frontmatter boolean, accepting the usual truthy spellings. */
|
|
317
|
+
function parseBool(v) {
|
|
318
|
+
return /^(true|yes|1|on)$/i.test((v ?? '').trim());
|
|
319
|
+
}
|
|
84
320
|
function parseToolList(v) {
|
|
85
321
|
if (!v)
|
|
86
322
|
return undefined;
|
|
@@ -91,62 +327,210 @@ function parseToolList(v) {
|
|
|
91
327
|
.filter(Boolean);
|
|
92
328
|
return tools.length ? tools : undefined;
|
|
93
329
|
}
|
|
94
|
-
function loadDir(dir, source, into) {
|
|
330
|
+
function loadDir(dir, source, into, warnings) {
|
|
95
331
|
let entries;
|
|
96
332
|
try {
|
|
97
|
-
entries = fs.readdirSync(dir
|
|
333
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
334
|
}
|
|
99
335
|
catch {
|
|
336
|
+
// Overwhelmingly "no .nexrall/agents here", which is the normal case and not
|
|
337
|
+
// worth a word. A genuine permission error is rare enough that the cost of
|
|
338
|
+
// staying quiet is lower than warning on every repo that has no agents.
|
|
100
339
|
return;
|
|
101
340
|
}
|
|
102
|
-
for (const
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
const full = path.join(dir, entry.name);
|
|
343
|
+
// Recurse into subfolders so definitions can be organised (agents/review/…),
|
|
344
|
+
// matching Claude Code. Identity still comes only from the name field / filename.
|
|
345
|
+
if (entry.isDirectory()) {
|
|
346
|
+
loadDir(full, source, into, warnings);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!entry.name.endsWith('.md'))
|
|
350
|
+
continue;
|
|
351
|
+
let raw;
|
|
103
352
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
353
|
+
raw = fs.readFileSync(full, 'utf-8');
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
warnings.push({ file: full, agent: path.basename(entry.name, '.md'), message: `could not be read (${err.message}) — this agent was skipped` });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const { meta, body, ok } = parseFrontmatter(raw);
|
|
360
|
+
const name = (meta.name || path.basename(entry.name, '.md')).trim();
|
|
361
|
+
if (!name)
|
|
362
|
+
continue;
|
|
363
|
+
// Earlier tiers win (project > global > plugin).
|
|
364
|
+
if (source !== 'project' && into.has(name))
|
|
365
|
+
continue;
|
|
366
|
+
const tools = parseToolList(meta.tools);
|
|
367
|
+
let effectiveTools = tools;
|
|
368
|
+
// ── FAIL CLOSED on a definition we could not parse ────────────────────────
|
|
369
|
+
//
|
|
370
|
+
// A file with no `---` frontmatter block used to yield tools === undefined,
|
|
371
|
+
// which means "no allowlist" — i.e. FULL access, including write_file, bash
|
|
372
|
+
// and delete_file. So the worse the file, the more power it got: the exact
|
|
373
|
+
// inversion you do not want in a permission system. A malformed definition
|
|
374
|
+
// is now read-only, which is both the safe reading and, for anyone hand-
|
|
375
|
+
// writing an agent, almost always the intended one.
|
|
376
|
+
if (!ok) {
|
|
377
|
+
effectiveTools = READ_ONLY_TOOLS;
|
|
378
|
+
warnings.push({
|
|
379
|
+
file: full,
|
|
380
|
+
agent: name,
|
|
381
|
+
message: 'has no valid YAML frontmatter (a `---` block must be the first thing in the file), so no ' +
|
|
382
|
+
'tool allowlist could be read. Treating it as READ-ONLY. Add frontmatter with a `tools:` line ' +
|
|
383
|
+
'to grant more.',
|
|
119
384
|
});
|
|
120
385
|
}
|
|
121
|
-
|
|
122
|
-
|
|
386
|
+
else {
|
|
387
|
+
if (!meta.description) {
|
|
388
|
+
warnings.push({
|
|
389
|
+
file: full,
|
|
390
|
+
agent: name,
|
|
391
|
+
message: 'has no `description:` — that text is the ONLY thing the model uses to decide when to delegate to this agent, so it will rarely be picked.',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (meta.model !== undefined && parseModel(meta.model) === undefined) {
|
|
395
|
+
warnings.push({
|
|
396
|
+
file: full,
|
|
397
|
+
agent: name,
|
|
398
|
+
message: `has model: "${meta.model}", which is not valid — use one of ${VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model.`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// The highest-value warning of the lot. An allowlist only ever GRANTS, so a
|
|
402
|
+
// misspelled name is not an error anywhere — the tool is simply never
|
|
403
|
+
// permitted, and the model is told "Permission denied", which points it at
|
|
404
|
+
// the user rather than at the typo.
|
|
405
|
+
const unknown = (tools ?? []).filter((t) => !KNOWN_TOOL_NAMES.has(t) && !t.includes('__'));
|
|
406
|
+
if (unknown.length) {
|
|
407
|
+
warnings.push({
|
|
408
|
+
file: full,
|
|
409
|
+
agent: name,
|
|
410
|
+
message: `lists unknown tool name(s): ${unknown.join(', ')}. An allowlist only grants, so these silently do nothing and the agent cannot use them. Tool names are lower_snake_case (read_file, search_files, glob, bash).`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
|
|
414
|
+
if (strayKeys.length) {
|
|
415
|
+
warnings.push({
|
|
416
|
+
file: full,
|
|
417
|
+
agent: name,
|
|
418
|
+
message: `has unrecognised frontmatter key(s): ${strayKeys.join(', ')} — these are ignored.`,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (!body) {
|
|
422
|
+
warnings.push({
|
|
423
|
+
file: full,
|
|
424
|
+
agent: name,
|
|
425
|
+
message: 'has an empty body — the text below the frontmatter IS the agent\'s system prompt, so it currently has no instructions.',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
123
428
|
}
|
|
429
|
+
into.set(name, {
|
|
430
|
+
name,
|
|
431
|
+
description: meta.description || `Custom ${name} agent`,
|
|
432
|
+
tools: effectiveTools,
|
|
433
|
+
model: parseModel(meta.model),
|
|
434
|
+
prompt: body,
|
|
435
|
+
source,
|
|
436
|
+
// Exposed to user/plugin definitions too — `test_files_only: true` (or
|
|
437
|
+
// `testFilesOnly`) lets anyone build a test-writing agent that genuinely
|
|
438
|
+
// cannot touch production source, rather than only the builtin getting
|
|
439
|
+
// that guarantee.
|
|
440
|
+
...(parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}),
|
|
441
|
+
});
|
|
124
442
|
}
|
|
125
443
|
}
|
|
126
|
-
/**
|
|
444
|
+
/**
|
|
445
|
+
* Discover all agent types. Precedence: project > global > plugin > builtin.
|
|
446
|
+
*
|
|
447
|
+
* Results are returned in a STABLE order (builtins in declared order, then
|
|
448
|
+
* user-supplied ones alphabetically) rather than in discovery order. The
|
|
449
|
+
* system-prompt catalogue is built from this, and a set that reshuffles between
|
|
450
|
+
* runs would silently bust the prompt cache on the block it lives in — filesystem
|
|
451
|
+
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
452
|
+
*/
|
|
127
453
|
function loadAgentTypes(workDir) {
|
|
454
|
+
return loadAgentTypesWithWarnings(workDir).types;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* As `loadAgentTypes`, but also reports what was wrong with the definitions.
|
|
458
|
+
*
|
|
459
|
+
* Split in two so the common caller stays a one-liner while the CLI's `/agents`
|
|
460
|
+
* and the agent loop can surface problems. Warnings never hide an agent: a
|
|
461
|
+
* flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
|
|
462
|
+
* because making a user's agent vanish over a typo is its own kind of silent
|
|
463
|
+
* failure.
|
|
464
|
+
*/
|
|
465
|
+
function loadAgentTypesWithWarnings(workDir) {
|
|
128
466
|
const out = new Map();
|
|
129
|
-
|
|
130
|
-
loadDir(path.join(
|
|
467
|
+
const warnings = [];
|
|
468
|
+
loadDir(path.join(workDir, '.nexrall', 'agents'), 'project', out, warnings);
|
|
469
|
+
loadDir(path.join(os.homedir(), '.nexrall', 'agents'), 'global', out, warnings);
|
|
131
470
|
for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'agents'))
|
|
132
|
-
loadDir(dir, 'plugin', out);
|
|
471
|
+
loadDir(dir, 'plugin', out, warnings);
|
|
133
472
|
for (const agent of BUILTIN_AGENTS) {
|
|
134
473
|
if (!out.has(agent.name))
|
|
135
474
|
out.set(agent.name, agent);
|
|
136
475
|
}
|
|
137
|
-
|
|
476
|
+
// Builtins first in their declared order (the common, cache-friendly case), then
|
|
477
|
+
// everything user-supplied alphabetically.
|
|
478
|
+
const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i) => [a.name, i]));
|
|
479
|
+
const types = [...out.values()].sort((a, b) => {
|
|
480
|
+
const ai = builtinOrder.get(a.name);
|
|
481
|
+
const bi = builtinOrder.get(b.name);
|
|
482
|
+
if (ai !== undefined && bi !== undefined)
|
|
483
|
+
return ai - bi;
|
|
484
|
+
if (ai !== undefined)
|
|
485
|
+
return -1;
|
|
486
|
+
if (bi !== undefined)
|
|
487
|
+
return 1;
|
|
488
|
+
return a.name.localeCompare(b.name);
|
|
489
|
+
});
|
|
490
|
+
return { types, warnings };
|
|
491
|
+
}
|
|
492
|
+
/** The tool names a hand-written allowlist may use (exported for validation + tests). */
|
|
493
|
+
function knownToolNames() {
|
|
494
|
+
return [...KNOWN_TOOL_NAMES].sort();
|
|
495
|
+
}
|
|
496
|
+
/** The built-in agents, for clients that want to show them alongside user-defined ones. */
|
|
497
|
+
function builtinAgents() {
|
|
498
|
+
return BUILTIN_AGENTS;
|
|
138
499
|
}
|
|
139
|
-
/**
|
|
500
|
+
/**
|
|
501
|
+
* One line per agent for the system prompt's <available_subagents> block.
|
|
502
|
+
*
|
|
503
|
+
* Reports each agent's CAPABILITY CLASS rather than enumerating its allowlist.
|
|
504
|
+
* The full lists (~15 tool names each) went into every single request while
|
|
505
|
+
* telling the model nothing it needs in order to choose: what matters when
|
|
506
|
+
* delegating is "can this one edit files?" and "what is it for?", not whether
|
|
507
|
+
* `get_hover` happens to be included. With six builtins the verbatim lists cost
|
|
508
|
+
* ~670 tokens per request, most of it near-identical boilerplate that also
|
|
509
|
+
* weakens the signal it was meant to carry.
|
|
510
|
+
*
|
|
511
|
+
* The per-agent description already states its own restrictions in prose, and the
|
|
512
|
+
* allowlist is enforced at the permission gate regardless of what is advertised
|
|
513
|
+
* here — so this is purely a summary, never the mechanism.
|
|
514
|
+
*/
|
|
140
515
|
function summariseAgents(types) {
|
|
141
516
|
if (!types.length)
|
|
142
517
|
return '';
|
|
143
518
|
return types
|
|
144
519
|
.map((t) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
520
|
+
const canWrite = !t.tools || t.tools.some((x) => WRITE_TOOL_HINTS.has(x));
|
|
521
|
+
const access = t.testFilesOnly
|
|
522
|
+
? 'writes TEST files only'
|
|
523
|
+
: canWrite ? 'can modify files' : 'read-only';
|
|
524
|
+
const model = t.model ? `, ${t.model} model` : '';
|
|
525
|
+
return `- ${t.name} (${access}${model}): ${t.description}`;
|
|
147
526
|
})
|
|
148
527
|
.join('\n');
|
|
149
528
|
}
|
|
529
|
+
/** Tool names that imply write access, for the summary line above. */
|
|
530
|
+
const WRITE_TOOL_HINTS = new Set([
|
|
531
|
+
'write_file', 'edit_file', 'multi_edit', 'notebook_edit',
|
|
532
|
+
'delete_file', 'move_file', 'copy_file',
|
|
533
|
+
]);
|
|
150
534
|
function findAgentType(types, name) {
|
|
151
535
|
if (!name)
|
|
152
536
|
return undefined;
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ export declare function resolveMaxIterations(optionValue: number | undefined, se
|
|
|
6
6
|
* dozen lines.
|
|
7
7
|
*/
|
|
8
8
|
export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
|
|
11
|
+
* tool — as opposed to the user declining it.
|
|
12
|
+
*
|
|
13
|
+
* The distinction matters to the model, which is why this is an exception rather
|
|
14
|
+
* than a `false`: both used to collapse into "Permission denied by user", so an
|
|
15
|
+
* agent blocked by its own allowlist (very often a mistyped tool name) was told
|
|
16
|
+
* the human had refused. The rational response to that is to ask again, which
|
|
17
|
+
* can never succeed. Carrying a reason lets the tool_result say what is actually
|
|
18
|
+
* true and what to do instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ToolNotAllowedError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* Reduce a sub-agent's message history to the text its parent should receive.
|
|
11
25
|
*
|
|
@@ -47,6 +61,26 @@ export declare function compactionThresholds(): {
|
|
|
47
61
|
export declare function estimateBodyBytes(messages: Message[]): number;
|
|
48
62
|
/** Tools that mutate the filesystem — used by the verification nudge (GAP D). */
|
|
49
63
|
export declare const WRITE_TOOL_NAMES: Set<string>;
|
|
64
|
+
/**
|
|
65
|
+
* May an agent restricted to `testFilesOnly` perform this tool call?
|
|
66
|
+
*
|
|
67
|
+
* A tool allowlist is all-or-nothing per tool: granting `edit_file` grants it for
|
|
68
|
+
* every path in the repo. The `test-writer` agent needs write access to produce
|
|
69
|
+
* tests, but must NOT be able to "fix" production source so a failing test goes
|
|
70
|
+
* green — the single most common way a test-writing agent destroys the signal it
|
|
71
|
+
* was asked to create. Its prompt says so; this makes it a refusal rather than a
|
|
72
|
+
* request.
|
|
73
|
+
*
|
|
74
|
+
* Pure + exported so the rules are testable directly, without running a real
|
|
75
|
+
* sub-agent.
|
|
76
|
+
*
|
|
77
|
+
* KNOWN LIMIT, stated rather than hidden: this gates the file TOOLS, not `bash`.
|
|
78
|
+
* A determined model could still write source via `bash: echo ... > src/x.ts`.
|
|
79
|
+
* Closing that means parsing shell redirection, which is not reliably doable — so
|
|
80
|
+
* this is a strong guardrail against the realistic failure mode, not a sandbox.
|
|
81
|
+
* Real isolation is the sandbox config (tools/sandbox.ts), a separate mechanism.
|
|
82
|
+
*/
|
|
83
|
+
export declare function allowsTestOnlyWrite(tool: string, input: Record<string, unknown> | undefined): boolean;
|
|
50
84
|
/** Heuristic: does a bash command look like it's running tests/build/lint/typecheck? (GAP D) */
|
|
51
85
|
export declare const VERIFY_CMD_RE: RegExp;
|
|
52
86
|
/**
|