@nexrall/code-core 1.4.23 → 1.4.24
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 +35 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +241 -6
- package/dist/agent/loop.d.ts +20 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +55 -0
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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,43 @@ 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
|
+
* Discover all agent types. Precedence: project > global > plugin > builtin.
|
|
25
|
+
*
|
|
26
|
+
* Results are returned in a STABLE order (builtins in declared order, then
|
|
27
|
+
* user-supplied ones alphabetically) rather than in discovery order. The
|
|
28
|
+
* system-prompt catalogue is built from this, and a set that reshuffles between
|
|
29
|
+
* runs would silently bust the prompt cache on the block it lives in — filesystem
|
|
30
|
+
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
31
|
+
*/
|
|
13
32
|
export declare function loadAgentTypes(workDir: string): AgentType[];
|
|
14
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* One line per agent for the system prompt's <available_subagents> block.
|
|
35
|
+
*
|
|
36
|
+
* Reports each agent's CAPABILITY CLASS rather than enumerating its allowlist.
|
|
37
|
+
* The full lists (~15 tool names each) went into every single request while
|
|
38
|
+
* telling the model nothing it needs in order to choose: what matters when
|
|
39
|
+
* delegating is "can this one edit files?" and "what is it for?", not whether
|
|
40
|
+
* `get_hover` happens to be included. With six builtins the verbatim lists cost
|
|
41
|
+
* ~670 tokens per request, most of it near-identical boilerplate that also
|
|
42
|
+
* weakens the signal it was meant to carry.
|
|
43
|
+
*
|
|
44
|
+
* The per-agent description already states its own restrictions in prose, and the
|
|
45
|
+
* allowlist is enforced at the permission gate regardless of what is advertised
|
|
46
|
+
* here — so this is purely a summary, never the mechanism.
|
|
47
|
+
*/
|
|
15
48
|
export declare function summariseAgents(types: AgentType[]): string;
|
|
16
49
|
export declare function findAgentType(types: AgentType[], name: string | undefined): AgentType | undefined;
|
|
17
50
|
//# sourceMappingURL=agentTypes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAuBA,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;
|
|
1
|
+
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAuBA,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;AA4RD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAmB3D;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
|
@@ -43,11 +43,37 @@ const index_1 = require("../plugins/index");
|
|
|
43
43
|
// ── Built-in agent types ──────────────────────────────────────────────────────
|
|
44
44
|
// Shipped defaults; lowest precedence (project > global > builtin), so a user
|
|
45
45
|
// can override any of them with a same-name .nexrall/agents/<name>.md file.
|
|
46
|
+
// Shared read-only tool set.
|
|
47
|
+
//
|
|
48
|
+
// An allowlist only ever GRANTS — a name that doesn't exist on the current client
|
|
49
|
+
// is simply never offered to the model, so listing the VS Code language-server
|
|
50
|
+
// tools alongside the CLI ones is safe and gives each agent the best available
|
|
51
|
+
// capability on whichever client it runs.
|
|
52
|
+
//
|
|
53
|
+
// This exists because it was previously inlined per agent, and the one agent that
|
|
54
|
+
// had it (`reviewer`) listed five VS Code-only tools, leaving it with just five
|
|
55
|
+
// usable tools when run from the CLI — a silent capability gap that is very easy
|
|
56
|
+
// to reintroduce by copy-pasting a list.
|
|
57
|
+
const READ_ONLY_TOOLS = [
|
|
58
|
+
// Universal
|
|
59
|
+
'read_file', 'search_files', 'glob', 'list_directory', 'bash', 'bash_output',
|
|
60
|
+
'notebook_read', 'todo_write', 'todo_read',
|
|
61
|
+
// VS Code language server (ignored on the CLI)
|
|
62
|
+
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
63
|
+
'get_hover', 'get_diagnostics',
|
|
64
|
+
];
|
|
65
|
+
/** Read-only + the network, for agents that must consult external sources. */
|
|
66
|
+
const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'web_search', 'fetch_url'];
|
|
67
|
+
/** Read-only + the write tools, for agents that produce code. */
|
|
68
|
+
const WRITE_TOOLS = [
|
|
69
|
+
...READ_ONLY_TOOLS,
|
|
70
|
+
'write_file', 'edit_file', 'multi_edit', 'create_directory', 'move_file', 'copy_file',
|
|
71
|
+
];
|
|
46
72
|
const BUILTIN_AGENTS = [
|
|
47
73
|
{
|
|
48
74
|
name: 'reviewer',
|
|
49
75
|
description: 'Read-only code reviewer — finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.',
|
|
50
|
-
tools:
|
|
76
|
+
tools: READ_ONLY_TOOLS,
|
|
51
77
|
source: 'builtin',
|
|
52
78
|
prompt: [
|
|
53
79
|
'You are a meticulous senior code reviewer. You NEVER modify files — you only read, search, and report.',
|
|
@@ -64,6 +90,162 @@ const BUILTIN_AGENTS = [
|
|
|
64
90
|
'then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale.',
|
|
65
91
|
].join('\n'),
|
|
66
92
|
},
|
|
93
|
+
// Promoted from the security-audit plugin to a builtin.
|
|
94
|
+
//
|
|
95
|
+
// Leaving it plugin-only was indefensible next to `reviewer` being builtin:
|
|
96
|
+
// reviewer's own prompt already tells it to look for security issues, so
|
|
97
|
+
// security IS treated as default work — yet the specialist agent for it was
|
|
98
|
+
// invisible unless the user happened to know the plugin existed. For an agent
|
|
99
|
+
// that WRITES code, "you only get a security review if you knew to install
|
|
100
|
+
// something" is the wrong default.
|
|
101
|
+
{
|
|
102
|
+
name: 'security-auditor',
|
|
103
|
+
description: 'Read-only security auditor — hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.',
|
|
104
|
+
tools: READ_ONLY_TOOLS,
|
|
105
|
+
model: 'pro',
|
|
106
|
+
source: 'builtin',
|
|
107
|
+
prompt: [
|
|
108
|
+
'You are a security auditor. You find real, exploitable flaws — not style issues.',
|
|
109
|
+
'',
|
|
110
|
+
'Method:',
|
|
111
|
+
'1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network',
|
|
112
|
+
' input, deserialization), then trace user-controlled data inward to where it is used.',
|
|
113
|
+
'2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.',
|
|
114
|
+
'3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;',
|
|
115
|
+
' Medium = needs unusual preconditions; Low = hardening.',
|
|
116
|
+
'',
|
|
117
|
+
'Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken',
|
|
118
|
+
'authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,',
|
|
119
|
+
'path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,',
|
|
120
|
+
'and crypto misuse (hand-rolled comparison, predictable randomness).',
|
|
121
|
+
'',
|
|
122
|
+
'Hard rules:',
|
|
123
|
+
'- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.',
|
|
124
|
+
'- NEVER print a discovered secret\'s value. Report its location and advise rotation.',
|
|
125
|
+
'- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.',
|
|
126
|
+
'- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.',
|
|
127
|
+
].join('\n'),
|
|
128
|
+
},
|
|
129
|
+
// The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
|
|
130
|
+
// search that would otherwise flood the parent's context. Defaults to the
|
|
131
|
+
// cheapest model on purpose — "find every caller of X" has no need of a
|
|
132
|
+
// frontier model, and this is the agent most likely to be spawned in bulk.
|
|
133
|
+
{
|
|
134
|
+
name: 'explorer',
|
|
135
|
+
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.',
|
|
136
|
+
tools: READ_ONLY_TOOLS,
|
|
137
|
+
model: 'turbo',
|
|
138
|
+
source: 'builtin',
|
|
139
|
+
prompt: [
|
|
140
|
+
'You map code. You NEVER modify anything.',
|
|
141
|
+
'',
|
|
142
|
+
'Method:',
|
|
143
|
+
'1. Prefer structural search over text search where available (get_workspace_symbols, find_references,',
|
|
144
|
+
' go_to_definition); fall back to search_files/glob otherwise.',
|
|
145
|
+
'2. Read only the sections you need — use read_file with offset/limit on large files instead of',
|
|
146
|
+
' pulling in thousands of lines.',
|
|
147
|
+
'3. Follow the real call graph rather than guessing from names.',
|
|
148
|
+
'',
|
|
149
|
+
'Your ONLY output is a compact report: the file:line locations that matter, how they relate, and the',
|
|
150
|
+
'direct answer to the question you were given. This exists to keep bulk search OUT of the parent\'s',
|
|
151
|
+
'context, so do not paste large file contents back — cite locations and summarise. Say plainly when',
|
|
152
|
+
'something does not exist; a confident wrong answer is far worse than "not found".',
|
|
153
|
+
].join('\n'),
|
|
154
|
+
},
|
|
155
|
+
// Matches Claude Code's built-in `Plan`: research a change and return a
|
|
156
|
+
// strategy, deliberately WITHOUT write access so "make a plan" can never
|
|
157
|
+
// quietly become "start editing".
|
|
158
|
+
{
|
|
159
|
+
name: 'planner',
|
|
160
|
+
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.',
|
|
161
|
+
tools: RESEARCH_TOOLS,
|
|
162
|
+
model: 'pro',
|
|
163
|
+
source: 'builtin',
|
|
164
|
+
prompt: [
|
|
165
|
+
'You produce implementation plans. You NEVER modify files — planning and doing are separate steps,',
|
|
166
|
+
'and this agent exists so "plan it" cannot silently turn into "change it".',
|
|
167
|
+
'',
|
|
168
|
+
'Method:',
|
|
169
|
+
'1. Read the actual code before proposing anything. No plan may rest on an assumed API shape.',
|
|
170
|
+
'2. Find every affected call site (find_references / search_files) and list them.',
|
|
171
|
+
'3. Order the steps so the tree stays working after each one — types, then implementation, then',
|
|
172
|
+
' tests, then exports/registration.',
|
|
173
|
+
'',
|
|
174
|
+
'Output:',
|
|
175
|
+
'- Goal, in one sentence.',
|
|
176
|
+
'- Numbered steps, each with the exact files touched and what changes in them.',
|
|
177
|
+
'- Risks + the specific thing that could break, and how it would be detected.',
|
|
178
|
+
'- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).',
|
|
179
|
+
'- Anything genuinely ambiguous, stated as an open question rather than a silent assumption.',
|
|
180
|
+
].join('\n'),
|
|
181
|
+
},
|
|
182
|
+
// Promoted from the test-gen plugin. Needs write access — it produces test
|
|
183
|
+
// files — but is deliberately forbidden from touching source, because "make the
|
|
184
|
+
// tests pass" is the single most common way an agent destroys signal.
|
|
185
|
+
{
|
|
186
|
+
name: 'test-writer',
|
|
187
|
+
description: 'Writes tests that follow the project\'s existing conventions. May create/edit TEST files only — never production source.',
|
|
188
|
+
tools: WRITE_TOOLS,
|
|
189
|
+
// Enforced, not merely requested: the permission gate refuses a write whose
|
|
190
|
+
// path is not a test file. Without this the allowlist would grant edit_file
|
|
191
|
+
// for every path and the rule below would be a suggestion the model is free
|
|
192
|
+
// to rationalise its way past.
|
|
193
|
+
testFilesOnly: true,
|
|
194
|
+
source: 'builtin',
|
|
195
|
+
prompt: [
|
|
196
|
+
'You write tests. You may create and edit TEST files only.',
|
|
197
|
+
'',
|
|
198
|
+
'Hard rules — these are the ways test-writing agents destroy value, so they are non-negotiable:',
|
|
199
|
+
'- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.',
|
|
200
|
+
'- NEVER weaken, delete or skip an existing assertion or test.',
|
|
201
|
+
'- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.',
|
|
202
|
+
'',
|
|
203
|
+
'Method:',
|
|
204
|
+
'1. Read the existing tests FIRST and copy their conventions exactly — runner, file naming, layout,',
|
|
205
|
+
' assertion style, fixture/helper patterns. Never introduce a new framework.',
|
|
206
|
+
'2. Test observable behaviour and the contract, not private internals.',
|
|
207
|
+
'3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,',
|
|
208
|
+
' boundaries, error paths, concurrency where it applies.',
|
|
209
|
+
'4. No sleeps or wall-clock dependence — those produce the flaky tests that get deleted later.',
|
|
210
|
+
'5. RUN the tests you wrote and report the real output. Never claim a test passes without running it.',
|
|
211
|
+
].join('\n'),
|
|
212
|
+
},
|
|
213
|
+
// The DevOps gap — answered with a READ-ONLY advisor, not an operator.
|
|
214
|
+
//
|
|
215
|
+
// A "DevOps agent" with write/apply access is a genuinely different risk class
|
|
216
|
+
// from the others here: its mistakes are `kubectl delete`, a bad `terraform
|
|
217
|
+
// apply`, a broken deploy pipeline — often not revertible and affecting
|
|
218
|
+
// production rather than a working tree. So this one diagnoses and proposes a
|
|
219
|
+
// diff; a human applies it. That asymmetry is the whole design.
|
|
220
|
+
{
|
|
221
|
+
name: 'devops-advisor',
|
|
222
|
+
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.',
|
|
223
|
+
tools: RESEARCH_TOOLS,
|
|
224
|
+
model: 'pro',
|
|
225
|
+
source: 'builtin',
|
|
226
|
+
prompt: [
|
|
227
|
+
'You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.',
|
|
228
|
+
'',
|
|
229
|
+
'Hard rules:',
|
|
230
|
+
'- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only',
|
|
231
|
+
' (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).',
|
|
232
|
+
' NEVER run anything that mutates infrastructure — no apply/delete/scale/rollout/restart/push,',
|
|
233
|
+
' no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.',
|
|
234
|
+
'- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.',
|
|
235
|
+
'',
|
|
236
|
+
'Method:',
|
|
237
|
+
'1. Read what actually exists — workflow files, Dockerfiles, manifests, kustomize overlays, the',
|
|
238
|
+
' deploy scripts — before drawing any conclusion. Never reason from what a stack "usually" looks like.',
|
|
239
|
+
'2. Follow the real path a change takes to production, and name the step that is broken or missing.',
|
|
240
|
+
'3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually',
|
|
241
|
+
' needs, image tags that do not match what is deployed, missing health probes, absent resource',
|
|
242
|
+
' limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and',
|
|
243
|
+
' migrations that must run before the new image is live.',
|
|
244
|
+
'',
|
|
245
|
+
'Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or',
|
|
246
|
+
'exact file content, and the command a human should run to apply and verify it.',
|
|
247
|
+
].join('\n'),
|
|
248
|
+
},
|
|
67
249
|
];
|
|
68
250
|
function parseFrontmatter(raw) {
|
|
69
251
|
const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
@@ -81,6 +263,10 @@ function parseModel(v) {
|
|
|
81
263
|
const s = (v ?? '').toLowerCase();
|
|
82
264
|
return s === 'turbo' || s === 'pro' || s === 'ultra' ? s : undefined;
|
|
83
265
|
}
|
|
266
|
+
/** Parse a frontmatter boolean, accepting the usual truthy spellings. */
|
|
267
|
+
function parseBool(v) {
|
|
268
|
+
return /^(true|yes|1|on)$/i.test((v ?? '').trim());
|
|
269
|
+
}
|
|
84
270
|
function parseToolList(v) {
|
|
85
271
|
if (!v)
|
|
86
272
|
return undefined;
|
|
@@ -116,6 +302,11 @@ function loadDir(dir, source, into) {
|
|
|
116
302
|
model: parseModel(meta.model),
|
|
117
303
|
prompt: body,
|
|
118
304
|
source,
|
|
305
|
+
// Exposed to user/plugin definitions too — `test_files_only: true` (or
|
|
306
|
+
// `testFilesOnly`) lets anyone build a test-writing agent that genuinely
|
|
307
|
+
// cannot touch production source, rather than only the builtin getting
|
|
308
|
+
// that guarantee.
|
|
309
|
+
...(parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}),
|
|
119
310
|
});
|
|
120
311
|
}
|
|
121
312
|
catch {
|
|
@@ -123,7 +314,15 @@ function loadDir(dir, source, into) {
|
|
|
123
314
|
}
|
|
124
315
|
}
|
|
125
316
|
}
|
|
126
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* Discover all agent types. Precedence: project > global > plugin > builtin.
|
|
319
|
+
*
|
|
320
|
+
* Results are returned in a STABLE order (builtins in declared order, then
|
|
321
|
+
* user-supplied ones alphabetically) rather than in discovery order. The
|
|
322
|
+
* system-prompt catalogue is built from this, and a set that reshuffles between
|
|
323
|
+
* runs would silently bust the prompt cache on the block it lives in — filesystem
|
|
324
|
+
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
325
|
+
*/
|
|
127
326
|
function loadAgentTypes(workDir) {
|
|
128
327
|
const out = new Map();
|
|
129
328
|
loadDir(path.join(workDir, '.nexrall', 'agents'), 'project', out);
|
|
@@ -134,19 +333,55 @@ function loadAgentTypes(workDir) {
|
|
|
134
333
|
if (!out.has(agent.name))
|
|
135
334
|
out.set(agent.name, agent);
|
|
136
335
|
}
|
|
137
|
-
|
|
336
|
+
// Builtins first in their declared order (the common, cache-friendly case), then
|
|
337
|
+
// everything user-supplied alphabetically.
|
|
338
|
+
const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i) => [a.name, i]));
|
|
339
|
+
return [...out.values()].sort((a, b) => {
|
|
340
|
+
const ai = builtinOrder.get(a.name);
|
|
341
|
+
const bi = builtinOrder.get(b.name);
|
|
342
|
+
if (ai !== undefined && bi !== undefined)
|
|
343
|
+
return ai - bi;
|
|
344
|
+
if (ai !== undefined)
|
|
345
|
+
return -1;
|
|
346
|
+
if (bi !== undefined)
|
|
347
|
+
return 1;
|
|
348
|
+
return a.name.localeCompare(b.name);
|
|
349
|
+
});
|
|
138
350
|
}
|
|
139
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* One line per agent for the system prompt's <available_subagents> block.
|
|
353
|
+
*
|
|
354
|
+
* Reports each agent's CAPABILITY CLASS rather than enumerating its allowlist.
|
|
355
|
+
* The full lists (~15 tool names each) went into every single request while
|
|
356
|
+
* telling the model nothing it needs in order to choose: what matters when
|
|
357
|
+
* delegating is "can this one edit files?" and "what is it for?", not whether
|
|
358
|
+
* `get_hover` happens to be included. With six builtins the verbatim lists cost
|
|
359
|
+
* ~670 tokens per request, most of it near-identical boilerplate that also
|
|
360
|
+
* weakens the signal it was meant to carry.
|
|
361
|
+
*
|
|
362
|
+
* The per-agent description already states its own restrictions in prose, and the
|
|
363
|
+
* allowlist is enforced at the permission gate regardless of what is advertised
|
|
364
|
+
* here — so this is purely a summary, never the mechanism.
|
|
365
|
+
*/
|
|
140
366
|
function summariseAgents(types) {
|
|
141
367
|
if (!types.length)
|
|
142
368
|
return '';
|
|
143
369
|
return types
|
|
144
370
|
.map((t) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
371
|
+
const canWrite = !t.tools || t.tools.some((x) => WRITE_TOOL_HINTS.has(x));
|
|
372
|
+
const access = t.testFilesOnly
|
|
373
|
+
? 'writes TEST files only'
|
|
374
|
+
: canWrite ? 'can modify files' : 'read-only';
|
|
375
|
+
const model = t.model ? `, ${t.model} model` : '';
|
|
376
|
+
return `- ${t.name} (${access}${model}): ${t.description}`;
|
|
147
377
|
})
|
|
148
378
|
.join('\n');
|
|
149
379
|
}
|
|
380
|
+
/** Tool names that imply write access, for the summary line above. */
|
|
381
|
+
const WRITE_TOOL_HINTS = new Set([
|
|
382
|
+
'write_file', 'edit_file', 'multi_edit', 'notebook_edit',
|
|
383
|
+
'delete_file', 'move_file', 'copy_file',
|
|
384
|
+
]);
|
|
150
385
|
function findAgentType(types, name) {
|
|
151
386
|
if (!name)
|
|
152
387
|
return undefined;
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -47,6 +47,26 @@ export declare function compactionThresholds(): {
|
|
|
47
47
|
export declare function estimateBodyBytes(messages: Message[]): number;
|
|
48
48
|
/** Tools that mutate the filesystem — used by the verification nudge (GAP D). */
|
|
49
49
|
export declare const WRITE_TOOL_NAMES: Set<string>;
|
|
50
|
+
/**
|
|
51
|
+
* May an agent restricted to `testFilesOnly` perform this tool call?
|
|
52
|
+
*
|
|
53
|
+
* A tool allowlist is all-or-nothing per tool: granting `edit_file` grants it for
|
|
54
|
+
* every path in the repo. The `test-writer` agent needs write access to produce
|
|
55
|
+
* tests, but must NOT be able to "fix" production source so a failing test goes
|
|
56
|
+
* green — the single most common way a test-writing agent destroys the signal it
|
|
57
|
+
* was asked to create. Its prompt says so; this makes it a refusal rather than a
|
|
58
|
+
* request.
|
|
59
|
+
*
|
|
60
|
+
* Pure + exported so the rules are testable directly, without running a real
|
|
61
|
+
* sub-agent.
|
|
62
|
+
*
|
|
63
|
+
* KNOWN LIMIT, stated rather than hidden: this gates the file TOOLS, not `bash`.
|
|
64
|
+
* A determined model could still write source via `bash: echo ... > src/x.ts`.
|
|
65
|
+
* Closing that means parsing shell redirection, which is not reliably doable — so
|
|
66
|
+
* this is a strong guardrail against the realistic failure mode, not a sandbox.
|
|
67
|
+
* Real isolation is the sandbox config (tools/sandbox.ts), a separate mechanism.
|
|
68
|
+
*/
|
|
69
|
+
export declare function allowsTestOnlyWrite(tool: string, input: Record<string, unknown> | undefined): boolean;
|
|
50
70
|
/** Heuristic: does a bash command look like it's running tests/build/lint/typecheck? (GAP D) */
|
|
51
71
|
export declare const VERIFY_CMD_RE: RegExp;
|
|
52
72
|
/**
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAyKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AAkED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAgBlF;AAiLD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,UAAU,UAAO,GAAG,MAAM,CAYjF;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAc,GAAG,MAAM,CAKtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAoBpE;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -42,6 +42,7 @@ exports.summariseSubTaskProgress = summariseSubTaskProgress;
|
|
|
42
42
|
exports.contextWindowFor = contextWindowFor;
|
|
43
43
|
exports.compactionThresholds = compactionThresholds;
|
|
44
44
|
exports.estimateBodyBytes = estimateBodyBytes;
|
|
45
|
+
exports.allowsTestOnlyWrite = allowsTestOnlyWrite;
|
|
45
46
|
exports.findSafeCutIndex = findSafeCutIndex;
|
|
46
47
|
exports.transcriptOf = transcriptOf;
|
|
47
48
|
exports.createLedger = createLedger;
|
|
@@ -528,6 +529,10 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
528
529
|
const gatedPermission = async (req) => {
|
|
529
530
|
if (allowed && !allowed.has(req.tool))
|
|
530
531
|
return false;
|
|
532
|
+
// Path-scoped write restriction (agent.testFilesOnly) — see
|
|
533
|
+
// allowsTestOnlyWrite for the reasoning and its known limit.
|
|
534
|
+
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
|
|
535
|
+
return false;
|
|
531
536
|
return options.requestPermission(req);
|
|
532
537
|
};
|
|
533
538
|
const subMessages = [
|
|
@@ -757,6 +762,56 @@ function resolveVerificationNudge(rawSettings) {
|
|
|
757
762
|
}
|
|
758
763
|
/** Tools that mutate the filesystem — used by the verification nudge (GAP D). */
|
|
759
764
|
exports.WRITE_TOOL_NAMES = new Set(['write_file', 'edit_file', 'multi_edit', 'delete_file', 'move_file', 'copy_file', 'notebook_edit']);
|
|
765
|
+
/**
|
|
766
|
+
* May an agent restricted to `testFilesOnly` perform this tool call?
|
|
767
|
+
*
|
|
768
|
+
* A tool allowlist is all-or-nothing per tool: granting `edit_file` grants it for
|
|
769
|
+
* every path in the repo. The `test-writer` agent needs write access to produce
|
|
770
|
+
* tests, but must NOT be able to "fix" production source so a failing test goes
|
|
771
|
+
* green — the single most common way a test-writing agent destroys the signal it
|
|
772
|
+
* was asked to create. Its prompt says so; this makes it a refusal rather than a
|
|
773
|
+
* request.
|
|
774
|
+
*
|
|
775
|
+
* Pure + exported so the rules are testable directly, without running a real
|
|
776
|
+
* sub-agent.
|
|
777
|
+
*
|
|
778
|
+
* KNOWN LIMIT, stated rather than hidden: this gates the file TOOLS, not `bash`.
|
|
779
|
+
* A determined model could still write source via `bash: echo ... > src/x.ts`.
|
|
780
|
+
* Closing that means parsing shell redirection, which is not reliably doable — so
|
|
781
|
+
* this is a strong guardrail against the realistic failure mode, not a sandbox.
|
|
782
|
+
* Real isolation is the sandbox config (tools/sandbox.ts), a separate mechanism.
|
|
783
|
+
*/
|
|
784
|
+
function allowsTestOnlyWrite(tool, input) {
|
|
785
|
+
// Non-write tools are unaffected: reading, searching and running tests are all
|
|
786
|
+
// essential to writing a test.
|
|
787
|
+
//
|
|
788
|
+
// WRITE_TOOL_NAMES deliberately excludes `create_directory`: isTestFile matches
|
|
789
|
+
// FILE paths, so a legitimate `create_directory('test/helpers')` would be
|
|
790
|
+
// refused and the agent could not scaffold the tree it needs — while an empty
|
|
791
|
+
// directory cannot damage production code, and files placed in it are still
|
|
792
|
+
// checked individually.
|
|
793
|
+
if (!exports.WRITE_TOOL_NAMES.has(tool))
|
|
794
|
+
return true;
|
|
795
|
+
// EVERY path the call could affect must be a test file, not just `path`:
|
|
796
|
+
// move_file takes {source, dest} and copy_file {source, destination}, so
|
|
797
|
+
// checking `path` alone would let `move_file src/index.ts -> /tmp/x` through and
|
|
798
|
+
// remove production code by relocating it.
|
|
799
|
+
//
|
|
800
|
+
// `source` is skipped for notebook_edit specifically, where it is the CELL
|
|
801
|
+
// CONTENT rather than a path — treating a blob of code as a path would refuse
|
|
802
|
+
// every legitimate notebook edit.
|
|
803
|
+
const pathKeys = tool === 'notebook_edit'
|
|
804
|
+
? ['path']
|
|
805
|
+
: ['path', 'source', 'dest', 'destination'];
|
|
806
|
+
const candidates = pathKeys
|
|
807
|
+
.map((k) => input?.[k])
|
|
808
|
+
.filter((v) => typeof v === 'string' && v.length > 0);
|
|
809
|
+
// An unrecognised write shape (no path-like argument at all) is refused rather
|
|
810
|
+
// than allowed through, so a future tool cannot silently become a hole here.
|
|
811
|
+
if (candidates.length === 0)
|
|
812
|
+
return false;
|
|
813
|
+
return candidates.every((p) => (0, testIntegrity_1.isTestFile)(p));
|
|
814
|
+
}
|
|
760
815
|
/** Heuristic: does a bash command look like it's running tests/build/lint/typecheck? (GAP D) */
|
|
761
816
|
exports.VERIFY_CMD_RE = /\b(npm|yarn|pnpm)\s+(run\s+)?(test|build|lint|typecheck|tsc)\b|\bpytest\b|\bgo\s+(test|vet|build)\b|\btsc\b|\beslint\b|\bcargo\s+(test|build|check)\b/i;
|
|
762
817
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SecurityFinding {
|
|
2
|
+
/** Machine-readable class, e.g. 'hardcoded-secret'. */
|
|
3
|
+
kind: string;
|
|
4
|
+
/** One-line explanation aimed at the model, phrased as what to do. */
|
|
5
|
+
message: string;
|
|
6
|
+
/** 1-based line number within the written content. */
|
|
7
|
+
line: number;
|
|
8
|
+
/** The offending line, truncated and with any secret value redacted. */
|
|
9
|
+
sample: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Scan file content for flagrant security problems.
|
|
13
|
+
*
|
|
14
|
+
* Returns at most `max` findings (default 5) — enough to be useful, few enough
|
|
15
|
+
* that the note appended to a tool result stays readable and cheap.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkSecurity(content: string, max?: number): SecurityFinding[];
|
|
18
|
+
/**
|
|
19
|
+
* Render findings as a note to append to a successful write's tool result.
|
|
20
|
+
*
|
|
21
|
+
* Phrased as a review comment rather than an error: the write HAS happened, and
|
|
22
|
+
* the model is being asked to look again. Deliberately explicit that it may be a
|
|
23
|
+
* false positive — otherwise the model tends to "fix" flagged-but-correct code,
|
|
24
|
+
* which is its own kind of damage.
|
|
25
|
+
*/
|
|
26
|
+
export declare function securityNoteText(findings: SecurityFinding[]): string;
|
|
27
|
+
//# sourceMappingURL=securityLint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"securityLint.d.ts","sourceRoot":"","sources":["../../src/agent/securityLint.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AA6GD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,eAAe,EAAE,CA6CzE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CAepE"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Inline security lint ─────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// The gap this closes: six checks already run automatically on every write —
|
|
5
|
+
// editCompleteness, crossFile, testIntegrity, claimEvidence, flaky, destructive —
|
|
6
|
+
// and not one of them is about security. For an agent that WRITES production code,
|
|
7
|
+
// nothing stopped it committing a hardcoded credential, an `eval` over
|
|
8
|
+
// user-controlled input, or a SQL string built by concatenation. The only security
|
|
9
|
+
// review available was an agent the user had to know to ask for.
|
|
10
|
+
//
|
|
11
|
+
// DESIGN: WARN, NEVER BLOCK.
|
|
12
|
+
//
|
|
13
|
+
// This runs on every single write, so a false positive is expensive — it would
|
|
14
|
+
// train the model (and the user) to ignore the channel, or worse, stall a
|
|
15
|
+
// legitimate edit. `editCompleteness` can afford to refuse outright because
|
|
16
|
+
// "// ... rest unchanged" is unambiguous; "this looks like SQL injection" is not.
|
|
17
|
+
// So findings are appended to the tool RESULT as a note the model sees and can act
|
|
18
|
+
// on, and the write still succeeds.
|
|
19
|
+
//
|
|
20
|
+
// Consequently every pattern here is tuned for PRECISION over recall. A check that
|
|
21
|
+
// fires on ordinary code was removed rather than loosened. Deep analysis is the
|
|
22
|
+
// `security-auditor` agent's job; this only catches the flagrant cases at the
|
|
23
|
+
// moment they are written.
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.checkSecurity = checkSecurity;
|
|
26
|
+
exports.securityNoteText = securityNoteText;
|
|
27
|
+
/** Max characters of the offending line to echo back. */
|
|
28
|
+
const SAMPLE_MAX = 160;
|
|
29
|
+
/**
|
|
30
|
+
* Redact anything that looks like a literal secret value before echoing a line.
|
|
31
|
+
*
|
|
32
|
+
* The whole point of flagging a hardcoded credential is to get it removed — so
|
|
33
|
+
* this must not copy the value into the transcript (and from there into logs, or
|
|
34
|
+
* the next request's context) on the way to reporting it.
|
|
35
|
+
*/
|
|
36
|
+
function redact(line) {
|
|
37
|
+
const masked = line
|
|
38
|
+
// key = "value" / key: 'value' → keep the key, mask the value
|
|
39
|
+
.replace(/(['"`]?[\w.-]*(?:secret|password|passwd|token|api[_-]?key|apikey|auth|credential|private[_-]?key)[\w.-]*['"`]?\s*[:=]\s*)(['"`])([^'"`]{4,})\2/gi, (_m, head, q) => `${head}${q}[REDACTED]${q}`)
|
|
40
|
+
// Bare high-entropy provider tokens appearing anywhere on the line
|
|
41
|
+
.replace(/\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, '[REDACTED]');
|
|
42
|
+
return masked.length > SAMPLE_MAX ? masked.slice(0, SAMPLE_MAX) + '…' : masked;
|
|
43
|
+
}
|
|
44
|
+
/** Strip string/comment noise that causes false positives, keeping length stable. */
|
|
45
|
+
function isLikelyCommentLine(line) {
|
|
46
|
+
return /^\s*(\/\/|\*|#|--|<!--)/.test(line);
|
|
47
|
+
}
|
|
48
|
+
const RULES = [
|
|
49
|
+
// ── Hardcoded credentials ───────────────────────────────────────────────────
|
|
50
|
+
// Provider-prefixed tokens are near-zero false positive: the prefixes are
|
|
51
|
+
// registered formats, not something that occurs naturally in source. Checked
|
|
52
|
+
// inside comments too — a key commented out is still a committed key.
|
|
53
|
+
{
|
|
54
|
+
kind: 'hardcoded-secret',
|
|
55
|
+
re: /\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})\b/,
|
|
56
|
+
message: 'Looks like a real API key/token committed to source. Move it to an environment variable and rotate the exposed key.',
|
|
57
|
+
includeComments: true,
|
|
58
|
+
},
|
|
59
|
+
// NOTE: the `private-key` check is NOT here — it needs to span multiple lines
|
|
60
|
+
// (BEGIN header on one, base64 body on the next), which this per-line loop
|
|
61
|
+
// cannot express. It runs separately in checkSecurity below.
|
|
62
|
+
{
|
|
63
|
+
kind: 'hardcoded-password',
|
|
64
|
+
// An ASSIGNMENT of a credential-ish name to a non-trivial literal.
|
|
65
|
+
//
|
|
66
|
+
// Tightened after measuring against the real backend, where the looser version
|
|
67
|
+
// fired on `missingSecret:'FIREBASE_TOKEN'` — code that NAMES a secret in an
|
|
68
|
+
// error message, the opposite of leaking one. So a value that is itself just a
|
|
69
|
+
// SCREAMING_SNAKE identifier (an env-var name) is excluded, along with the
|
|
70
|
+
// usual placeholder vocabulary. The value must also look like actual secret
|
|
71
|
+
// material: mixed case or digits, not a lone lowercase word.
|
|
72
|
+
re: /(?:password|passwd|secret|api[_-]?key|apikey|access[_-]?token)['"`]?\s*[:=]\s*['"`](?![A-Z0-9_]+['"`])(?!.*(?:\$\{|process\.env|os\.environ|example|changeme|placeholder|redacted|xxx|test|dummy|fake|sample|your[_-]?|<|\*{3}))(?=[^'"`]*[0-9A-Z])[^'"`\s]{10,}['"`]/,
|
|
73
|
+
message: 'Hardcoded credential literal. Read it from the environment/secret store instead, and rotate the exposed value.',
|
|
74
|
+
includeComments: true,
|
|
75
|
+
},
|
|
76
|
+
// ── Injection ───────────────────────────────────────────────────────────────
|
|
77
|
+
{
|
|
78
|
+
kind: 'dynamic-eval',
|
|
79
|
+
// The negative lookbehind for `.` is what makes this usable: `redisClient.eval`
|
|
80
|
+
// (a Redis Lua script), `page.eval` (Playwright), `vm.eval` and friends are
|
|
81
|
+
// METHOD calls on an object and have nothing to do with JavaScript's global
|
|
82
|
+
// eval. Without it, the real backend's Redis idempotency script was flagged.
|
|
83
|
+
// Only a bare `eval(` / `new Function(` with a non-literal argument counts.
|
|
84
|
+
re: /(?<![.\w$])(?:eval|new\s+Function)\s*\(\s*(?!['"`][^'"`]*['"`]\s*\))[^)]*[a-zA-Z_$][\w$]*/,
|
|
85
|
+
message: 'eval / new Function on a non-literal value executes arbitrary code if that value is ever user-controlled. Use an explicit parser or a lookup table.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
kind: 'sql-injection',
|
|
89
|
+
// Only fires when the interpolated expression is plausibly REQUEST-DERIVED.
|
|
90
|
+
//
|
|
91
|
+
// The obvious pattern — any `${...}` inside a SQL string — was measured
|
|
92
|
+
// against the real backend and flagged 15 of 183 files, essentially all of
|
|
93
|
+
// them safe and idiomatic: `${sets.join(', ')}` for a dynamic UPDATE, `${field}`
|
|
94
|
+
// for a server-chosen column, `${CONSUMPTION}` for a module constant. At that
|
|
95
|
+
// hit rate the warning is pure noise, and noise is worse than silence because
|
|
96
|
+
// it teaches everyone to skip the channel.
|
|
97
|
+
//
|
|
98
|
+
// So the interpolation must name something that plausibly came from the
|
|
99
|
+
// outside: req/request/params/query/body/input/user/args, or a bare
|
|
100
|
+
// `'...' + ident`. This trades recall for precision on purpose — thorough SQL
|
|
101
|
+
// review is the security-auditor agent's job, not an inline regex's.
|
|
102
|
+
re: /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\b[^;'"`]{0,160}(?:\$\{\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|['"`]\s*\+\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|%\s*\(\s*(?:request|params?|query|body|input|user)\b)/i,
|
|
103
|
+
message: 'SQL built by interpolating a request-derived value. Use a parameterised query ($1 / ? placeholders) — this is the classic injection sink.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
kind: 'command-injection',
|
|
107
|
+
// Shell execution with an interpolated or concatenated argument.
|
|
108
|
+
re: /\b(?:exec|execSync|spawnSync?|system|popen|os\.system|subprocess\.(?:call|run|Popen))\s*\(\s*(?:[`'"][^`'"]*(?:\$\{|['"]\s*\+)|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
109
|
+
message: 'Shell command built from a variable. Pass arguments as an array (no shell), or validate against an allowlist — a value containing ; or $() becomes command execution.',
|
|
110
|
+
},
|
|
111
|
+
// ── Transport / verification ────────────────────────────────────────────────
|
|
112
|
+
{
|
|
113
|
+
kind: 'tls-verification-disabled',
|
|
114
|
+
re: /(?:rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/,
|
|
115
|
+
message: 'TLS certificate verification is disabled, which removes protection against man-in-the-middle attacks. Trust a specific CA instead if the cert is self-signed.',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
/**
|
|
119
|
+
* Scan file content for flagrant security problems.
|
|
120
|
+
*
|
|
121
|
+
* Returns at most `max` findings (default 5) — enough to be useful, few enough
|
|
122
|
+
* that the note appended to a tool result stays readable and cheap.
|
|
123
|
+
*/
|
|
124
|
+
function checkSecurity(content, max = 5) {
|
|
125
|
+
if (!content)
|
|
126
|
+
return [];
|
|
127
|
+
const findings = [];
|
|
128
|
+
const lines = content.split(/\r?\n/);
|
|
129
|
+
// Private keys are matched across lines, unlike every other rule.
|
|
130
|
+
//
|
|
131
|
+
// A real PEM block is inherently multi-line: the BEGIN header sits on one line
|
|
132
|
+
// and the base64 body on the next. Requiring both on ONE line (which the
|
|
133
|
+
// line-by-line loop below does) meant the single highest-severity finding here
|
|
134
|
+
// only fired for keys embedded in a "\n"-escaped string literal, and missed the
|
|
135
|
+
// far more common case of a key pasted in verbatim.
|
|
136
|
+
const pemIdx = lines.findIndex((l) => /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH|PGP)?\s*PRIVATE KEY-----/.test(l));
|
|
137
|
+
if (pemIdx !== -1) {
|
|
138
|
+
// Require actual key material nearby, so a bare header — placeholder text in a
|
|
139
|
+
// config UI, documentation, a PEM parser — does not trip it.
|
|
140
|
+
const following = lines.slice(pemIdx, pemIdx + 4).join('\n');
|
|
141
|
+
if (/[A-Za-z0-9+/]{40,}/.test(following.replace(/-----[^-]+-----/g, ''))) {
|
|
142
|
+
findings.push({
|
|
143
|
+
kind: 'private-key',
|
|
144
|
+
message: 'A private key with real key material is being written into source. Store it outside the repo (secret manager / env var) and rotate it.',
|
|
145
|
+
line: pemIdx + 1,
|
|
146
|
+
sample: '-----BEGIN PRIVATE KEY----- [REDACTED]',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// One finding per (kind, line) at most, and one finding per kind overall —
|
|
151
|
+
// a file with fifty interpolated queries should say "SQL injection" once, not
|
|
152
|
+
// fill the model's context with fifty copies of the same advice.
|
|
153
|
+
const seenKinds = new Set();
|
|
154
|
+
for (let i = 0; i < lines.length && findings.length < max; i++) {
|
|
155
|
+
const line = lines[i];
|
|
156
|
+
if (!line || line.length > 2000)
|
|
157
|
+
continue; // minified/bundled — not hand-written source
|
|
158
|
+
const commentish = isLikelyCommentLine(line);
|
|
159
|
+
for (const rule of RULES) {
|
|
160
|
+
if (seenKinds.has(rule.kind))
|
|
161
|
+
continue;
|
|
162
|
+
if (commentish && !rule.includeComments)
|
|
163
|
+
continue;
|
|
164
|
+
if (!rule.re.test(line))
|
|
165
|
+
continue;
|
|
166
|
+
seenKinds.add(rule.kind);
|
|
167
|
+
findings.push({ kind: rule.kind, message: rule.message, line: i + 1, sample: redact(line.trim()) });
|
|
168
|
+
break; // at most one rule per line
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return findings;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Render findings as a note to append to a successful write's tool result.
|
|
175
|
+
*
|
|
176
|
+
* Phrased as a review comment rather than an error: the write HAS happened, and
|
|
177
|
+
* the model is being asked to look again. Deliberately explicit that it may be a
|
|
178
|
+
* false positive — otherwise the model tends to "fix" flagged-but-correct code,
|
|
179
|
+
* which is its own kind of damage.
|
|
180
|
+
*/
|
|
181
|
+
function securityNoteText(findings) {
|
|
182
|
+
// Opt-out, mirroring NEXRALL_ALLOW_ELIDED_WRITE. Someone working in a codebase
|
|
183
|
+
// that trips a rule constantly (a SQL-builder library, a crypto implementation,
|
|
184
|
+
// a test-fixture directory full of fake keys) needs a way to silence this
|
|
185
|
+
// without disabling the write path itself.
|
|
186
|
+
if (process.env.NEXRALL_SECURITY_LINT === 'off')
|
|
187
|
+
return '';
|
|
188
|
+
if (!findings.length)
|
|
189
|
+
return '';
|
|
190
|
+
const lines = findings.map((f) => ` • line ${f.line} [${f.kind}]: ${f.message}\n ${f.sample}`);
|
|
191
|
+
return (`\n\n⚠ SECURITY REVIEW (${findings.length} finding${findings.length > 1 ? 's' : ''}) — the write succeeded; check these before moving on:\n` +
|
|
192
|
+
lines.join('\n') +
|
|
193
|
+
`\nIf a finding is a false positive (test fixture, placeholder, intentionally dynamic), say so and continue — do NOT rewrite correct code to silence it.`);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=securityLint.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './tools/executor';
|
|
|
5
5
|
export * from './agent/loop';
|
|
6
6
|
export * from './agent/testIntegrity';
|
|
7
7
|
export * from './agent/editCompleteness';
|
|
8
|
+
export * from './agent/securityLint';
|
|
8
9
|
export * from './agent/crossFile';
|
|
9
10
|
export * from './agent/flaky';
|
|
10
11
|
export * from './agent/claimEvidence';
|
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,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,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ __exportStar(require("./tools/executor"), exports);
|
|
|
21
21
|
__exportStar(require("./agent/loop"), exports);
|
|
22
22
|
__exportStar(require("./agent/testIntegrity"), exports);
|
|
23
23
|
__exportStar(require("./agent/editCompleteness"), exports);
|
|
24
|
+
__exportStar(require("./agent/securityLint"), exports);
|
|
24
25
|
__exportStar(require("./agent/crossFile"), exports);
|
|
25
26
|
__exportStar(require("./agent/flaky"), exports);
|
|
26
27
|
__exportStar(require("./agent/claimEvidence"), exports);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAiuEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -44,6 +44,7 @@ const child_process_1 = require("child_process");
|
|
|
44
44
|
const sandbox_1 = require("./sandbox");
|
|
45
45
|
const auth_1 = require("../auth");
|
|
46
46
|
const editCompleteness_1 = require("../agent/editCompleteness");
|
|
47
|
+
const securityLint_1 = require("../agent/securityLint");
|
|
47
48
|
const memory_1 = require("../agent/memory");
|
|
48
49
|
const skills_1 = require("../agent/skills");
|
|
49
50
|
const testIntegrity_1 = require("../agent/testIntegrity");
|
|
@@ -413,8 +414,16 @@ async function writeFile(input, workDir) {
|
|
|
413
414
|
atomicWrite(resolved, content, existingMode);
|
|
414
415
|
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
415
416
|
const lines = content.split('\n').length;
|
|
417
|
+
// Inline security review of what was just written.
|
|
418
|
+
//
|
|
419
|
+
// Six gates already run automatically on every write (elision, cross-file
|
|
420
|
+
// breakage, test integrity, claim evidence, flaky tests, destructive commands)
|
|
421
|
+
// and none of them was about security — so an agent that writes production code
|
|
422
|
+
// could commit a hardcoded credential or an injection sink with nothing to say
|
|
423
|
+
// otherwise. This warns; it never blocks (see securityLint.ts for why).
|
|
424
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
|
|
416
425
|
if (isNew) {
|
|
417
|
-
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
|
|
426
|
+
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
|
|
418
427
|
}
|
|
419
428
|
const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
|
|
420
429
|
// Reward-hacking guard: a write_file that OVERWRITES an existing test file can
|
|
@@ -426,7 +435,7 @@ async function writeFile(input, workDir) {
|
|
|
426
435
|
const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
|
|
427
436
|
if (ti.suspicious)
|
|
428
437
|
tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
|
|
429
|
-
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
|
|
438
|
+
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
|
|
430
439
|
}
|
|
431
440
|
catch (err) {
|
|
432
441
|
return { error: err.message };
|
|
@@ -1408,7 +1417,12 @@ async function editFile(input, workDir) {
|
|
|
1408
1417
|
const linesBefore = origNorm.split('\n').length;
|
|
1409
1418
|
const linesAfter = updated.split('\n').length;
|
|
1410
1419
|
const xfile = crossFileBreakageWarning(resolved, origNorm, normalizeLF(updated), workDir ?? process.cwd());
|
|
1411
|
-
|
|
1420
|
+
// Security-lint only the NEWLY INSERTED text, not the whole file. Scanning the
|
|
1421
|
+
// full file would re-report pre-existing findings on every unrelated edit —
|
|
1422
|
+
// noise that has nothing to do with the change being made, and the fastest way
|
|
1423
|
+
// to teach everyone to ignore the warning.
|
|
1424
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
|
|
1425
|
+
return { output: `Edited ${resolved} (${linesBefore} → ${linesAfter} lines)\n\n${diff}${xfile}${sec}` };
|
|
1412
1426
|
}
|
|
1413
1427
|
catch (err) {
|
|
1414
1428
|
return { error: err.message };
|
|
@@ -1747,6 +1761,9 @@ async function multiEdit(input, workDir) {
|
|
|
1747
1761
|
let content = normalizeLF(rawFile);
|
|
1748
1762
|
const originalNorm = content; // captured before edits mutate `content`
|
|
1749
1763
|
const diffs = [];
|
|
1764
|
+
// Inserted text from every edit, collected for the security lint below so it
|
|
1765
|
+
// scans only what this call ADDED rather than the whole file.
|
|
1766
|
+
const insertedText = [];
|
|
1750
1767
|
for (let i = 0; i < edits.length; i++) {
|
|
1751
1768
|
const edit = edits[i];
|
|
1752
1769
|
// Normalize incoming old/new strings too
|
|
@@ -1766,6 +1783,7 @@ async function multiEdit(input, workDir) {
|
|
|
1766
1783
|
// Pass the pre-replace content so line numbers reflect the file state at
|
|
1767
1784
|
// the point this edit was applied (not the final state after all edits).
|
|
1768
1785
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
1786
|
+
insertedText.push(newStr);
|
|
1769
1787
|
content = literalReplace(content, oldStr, newStr);
|
|
1770
1788
|
}
|
|
1771
1789
|
// Restore original line endings before writing (atomic temp+rename),
|
|
@@ -1773,9 +1791,11 @@ async function multiEdit(input, workDir) {
|
|
|
1773
1791
|
const finalContent = wasCRLF ? content.replace(/\n/g, '\r\n') : content;
|
|
1774
1792
|
atomicWritePreservingMode(resolved, finalContent, pre.mode);
|
|
1775
1793
|
const xfile = crossFileBreakageWarning(resolved, originalNorm, content, workDir ?? process.cwd());
|
|
1794
|
+
// Lint only the newly inserted text (see the same reasoning in editFile).
|
|
1795
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join('\n')));
|
|
1776
1796
|
return {
|
|
1777
1797
|
output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
|
|
1778
|
-
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile,
|
|
1798
|
+
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile + sec,
|
|
1779
1799
|
};
|
|
1780
1800
|
}
|
|
1781
1801
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.24",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|