@nexrall/code-core 1.2.0 → 1.3.1
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/loop.d.ts +15 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +65 -15
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +22 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +18 -6
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +344 -66
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
import type { Message, AgentLoopOptions } from '../types';
|
|
2
|
+
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
3
|
+
/**
|
|
4
|
+
* Find the latest index ≤ maxIdx where history can be cut safely.
|
|
5
|
+
*
|
|
6
|
+
* A safe cut point is a **turn boundary**: an assistant message (which always
|
|
7
|
+
* begins a fresh turn after a user message). Cutting there guarantees that
|
|
8
|
+
* `messages[cut..]` starts with an assistant whose `tool_use` blocks are all
|
|
9
|
+
* answered by `tool_result`s that remain in the kept slice — so we never orphan
|
|
10
|
+
* a tool_result (which the API rejects). We deliberately allow cutting across
|
|
11
|
+
* tool_result-bearing user messages: the OLD implementation only cut at a
|
|
12
|
+
* *non*-tool_result user message, which never exists inside a single long
|
|
13
|
+
* agentic run (every user turn is a tool_result), so compaction was a no-op
|
|
14
|
+
* exactly when a long task needs it most.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findSafeCutIndex(messages: Message[], maxIdx: number): number;
|
|
2
17
|
export declare function runAgentLoop(initialMessages: Message[], options: AgentLoopOptions): Promise<Message[]>;
|
|
3
18
|
//# sourceMappingURL=loop.d.ts.map
|
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,EACjB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAmKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AAgRD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AA6ED,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+WpB"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveMaxIterations = resolveMaxIterations;
|
|
37
|
+
exports.findSafeCutIndex = findSafeCutIndex;
|
|
36
38
|
exports.runAgentLoop = runAgentLoop;
|
|
37
39
|
const client_1 = require("../api/client");
|
|
38
40
|
const executor_1 = require("../tools/executor");
|
|
@@ -89,13 +91,20 @@ function runToolHooks(entries, phase, toolName, input, workDir, result) {
|
|
|
89
91
|
for (const hook of entry.hooks ?? []) {
|
|
90
92
|
if (hook.type !== 'command' || !hook.command)
|
|
91
93
|
continue;
|
|
94
|
+
// Per-hook timeout. Default 60s (was a hard 10s that made the canonical
|
|
95
|
+
// "auto-run tests on PostToolUse" use-case useless — any real suite is
|
|
96
|
+
// slower). Configurable via `timeout_ms` on the hook, capped at 10min.
|
|
97
|
+
const hookTimeout = typeof hook.timeout_ms === 'number' && hook.timeout_ms > 0
|
|
98
|
+
? Math.min(hook.timeout_ms, 600000)
|
|
99
|
+
: 60000;
|
|
92
100
|
let r;
|
|
93
101
|
try {
|
|
94
102
|
r = (0, child_process_1.spawnSync)(hook.command, {
|
|
95
103
|
shell: true,
|
|
96
104
|
cwd: workDir,
|
|
97
|
-
timeout:
|
|
105
|
+
timeout: hookTimeout,
|
|
98
106
|
encoding: 'utf-8',
|
|
107
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
99
108
|
input: payload,
|
|
100
109
|
env: {
|
|
101
110
|
...process.env,
|
|
@@ -152,18 +161,29 @@ function runSimpleHooks(defs, workDir) {
|
|
|
152
161
|
// fix → …). A too-low cap makes the agent appear to "freeze" mid-task. Keep the
|
|
153
162
|
// default high and let projects raise it further via settings / env.
|
|
154
163
|
const DEFAULT_MAX_ITERATIONS = 500;
|
|
155
|
-
const MAX_ITERATIONS_CEILING = 2000; //
|
|
164
|
+
const MAX_ITERATIONS_CEILING = 2000; // default auto-continue backstop (no explicit opt-in)
|
|
165
|
+
const HARD_ITERATIONS_CAP = 100000; // absolute safety cap — even explicit opt-in can't exceed this
|
|
156
166
|
const STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runaway guard)
|
|
157
167
|
// Resolve the soft iteration budget. Precedence:
|
|
158
168
|
// options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
|
|
169
|
+
//
|
|
170
|
+
// The DEFAULT (nothing set) is clamped to MAX_ITERATIONS_CEILING so an ordinary
|
|
171
|
+
// run can never spin past 2000 rounds by accident. But an EXPLICIT value from
|
|
172
|
+
// any of the three opt-in channels is honoured up to HARD_ITERATIONS_CAP — this
|
|
173
|
+
// is what lets a genuinely long task (thousands of rounds) run when the user has
|
|
174
|
+
// deliberately asked for it, instead of dying at a hidden 2000 wall while the
|
|
175
|
+
// error message misleadingly tells them to "raise the limit".
|
|
159
176
|
function resolveMaxIterations(optionValue, settingsRaw) {
|
|
160
177
|
const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
|
|
161
178
|
const fromSettings = Number(settingsRaw.maxIterations);
|
|
162
|
-
const
|
|
179
|
+
const explicit = (typeof optionValue === 'number' && optionValue > 0) ? optionValue
|
|
163
180
|
: Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv
|
|
164
181
|
: Number.isFinite(fromSettings) && fromSettings > 0 ? fromSettings
|
|
165
|
-
:
|
|
166
|
-
|
|
182
|
+
: undefined;
|
|
183
|
+
if (explicit === undefined)
|
|
184
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
185
|
+
// Explicit opt-in: honour it, only bounded by the absolute safety cap.
|
|
186
|
+
return Math.min(Math.floor(explicit), HARD_ITERATIONS_CAP);
|
|
167
187
|
}
|
|
168
188
|
// When the soft budget is exhausted with work still pending, keep going instead
|
|
169
189
|
// of stopping. Precedence: options.autoContinue → env NEXRALL_AUTO_CONTINUE →
|
|
@@ -342,11 +362,20 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
342
362
|
});
|
|
343
363
|
// Extract final assistant text as the task output
|
|
344
364
|
const lastAssistant = [...result].reverse().find((m) => m.role === 'assistant');
|
|
345
|
-
|
|
365
|
+
let text = (lastAssistant?.content ?? [])
|
|
346
366
|
.filter((b) => b.type === 'text')
|
|
347
367
|
.map((b) => b.text)
|
|
348
368
|
.join('')
|
|
349
369
|
.trim();
|
|
370
|
+
// Cap the sub-agent's returned text so a verbose sub-task (e.g. "list every
|
|
371
|
+
// usage") can't blow up the PARENT's context in a single tool_result. Keep
|
|
372
|
+
// the head + tail — the conclusion is usually at the end.
|
|
373
|
+
const SUBTASK_MAX = 48000; // chars (~12k tokens)
|
|
374
|
+
if (text.length > SUBTASK_MAX) {
|
|
375
|
+
const head = text.slice(0, Math.floor(SUBTASK_MAX * 0.6));
|
|
376
|
+
const tail = text.slice(text.length - Math.floor(SUBTASK_MAX * 0.4));
|
|
377
|
+
text = `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
|
|
378
|
+
}
|
|
350
379
|
return { output: text || '(sub-task completed with no text output)' };
|
|
351
380
|
}
|
|
352
381
|
catch (err) {
|
|
@@ -382,14 +411,22 @@ function resolveAutoCompact(fromOptions, rawSettings) {
|
|
|
382
411
|
return s;
|
|
383
412
|
return true;
|
|
384
413
|
}
|
|
385
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Find the latest index ≤ maxIdx where history can be cut safely.
|
|
416
|
+
*
|
|
417
|
+
* A safe cut point is a **turn boundary**: an assistant message (which always
|
|
418
|
+
* begins a fresh turn after a user message). Cutting there guarantees that
|
|
419
|
+
* `messages[cut..]` starts with an assistant whose `tool_use` blocks are all
|
|
420
|
+
* answered by `tool_result`s that remain in the kept slice — so we never orphan
|
|
421
|
+
* a tool_result (which the API rejects). We deliberately allow cutting across
|
|
422
|
+
* tool_result-bearing user messages: the OLD implementation only cut at a
|
|
423
|
+
* *non*-tool_result user message, which never exists inside a single long
|
|
424
|
+
* agentic run (every user turn is a tool_result), so compaction was a no-op
|
|
425
|
+
* exactly when a long task needs it most.
|
|
426
|
+
*/
|
|
386
427
|
function findSafeCutIndex(messages, maxIdx) {
|
|
387
|
-
for (let i = Math.min(maxIdx, messages.length - 1); i
|
|
388
|
-
|
|
389
|
-
if (m.role !== 'user')
|
|
390
|
-
continue;
|
|
391
|
-
const hasToolResult = m.content.some((b) => b.type === 'tool_result');
|
|
392
|
-
if (!hasToolResult)
|
|
428
|
+
for (let i = Math.min(maxIdx, messages.length - 1); i >= 2; i--) {
|
|
429
|
+
if (messages[i].role === 'assistant')
|
|
393
430
|
return i;
|
|
394
431
|
}
|
|
395
432
|
return -1;
|
|
@@ -445,7 +482,13 @@ async function autoCompactMessages(messages, options) {
|
|
|
445
482
|
}
|
|
446
483
|
if (!summary)
|
|
447
484
|
return false;
|
|
448
|
-
|
|
485
|
+
// Replace the summarized head with a single user summary message. The cut is
|
|
486
|
+
// at a turn boundary (kept[0] is an assistant message — see findSafeCutIndex),
|
|
487
|
+
// so `user(summary) → assistant(kept[0])` is a valid, well-ordered sequence
|
|
488
|
+
// and no orphaned tool_result is left behind. We intentionally do NOT insert
|
|
489
|
+
// an assistant-ack here: that would put two assistant messages back-to-back
|
|
490
|
+
// (kept[0] is already an assistant), which the API rejects.
|
|
491
|
+
messages.splice(0, cut, { role: 'user', content: [{ type: 'text', text: `[Auto-compacted ${toSummarize.length} earlier messages]\n\nSummary of the earlier conversation so far:\n${summary}\n\nContinue the work from here.` }] });
|
|
449
492
|
// `kept` follows automatically since splice only replaced the head.
|
|
450
493
|
void kept;
|
|
451
494
|
return true;
|
|
@@ -710,8 +753,11 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
710
753
|
const toolResultBlocks = toolResults.map(({ block, result }) => ({
|
|
711
754
|
type: 'tool_result',
|
|
712
755
|
tool_use_id: block.id,
|
|
756
|
+
// When a tool returns BOTH an error and partial output (e.g. a bash
|
|
757
|
+
// command that timed out mid-test-run), surface both so the model can
|
|
758
|
+
// see WHY it hung — the failure detail is usually in the output tail.
|
|
713
759
|
content: result.error
|
|
714
|
-
? `Error: ${result.error}`
|
|
760
|
+
? (result.output ? `Error: ${result.error}\n\n${result.output}` : `Error: ${result.error}`)
|
|
715
761
|
: result.output ?? '',
|
|
716
762
|
is_error: result.error !== undefined,
|
|
717
763
|
}));
|
|
@@ -731,6 +777,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
731
777
|
content: toolResultContent,
|
|
732
778
|
};
|
|
733
779
|
messages.push(toolResultMessage);
|
|
780
|
+
// Incremental persistence hook: history is valid here (ends on a
|
|
781
|
+
// tool_result user turn). Let the caller checkpoint progress so a crash
|
|
782
|
+
// mid-run loses only the in-flight step, not the whole session.
|
|
783
|
+
options.onProgress?.(messages);
|
|
734
784
|
// Runaway guard: if every tool call in this round failed, count it. Enough
|
|
735
785
|
// consecutive all-failed rounds (e.g. a command that always errors, or the
|
|
736
786
|
// user denying every permission) means we're stuck — stop instead of
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA0WlB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
|
package/dist/api/client.js
CHANGED
|
@@ -316,7 +316,19 @@ async function streamChat(messages, options, onEvent) {
|
|
|
316
316
|
}
|
|
317
317
|
parser.feed(chunk.toString('utf-8'));
|
|
318
318
|
});
|
|
319
|
-
stream.on('end', () => {
|
|
319
|
+
stream.on('end', () => {
|
|
320
|
+
clearInterval(heartbeatWatchdog);
|
|
321
|
+
// A clean 'end' with ZERO model events means the connection was dropped before
|
|
322
|
+
// the backend produced anything (deploy restart, proxy reset). Without this,
|
|
323
|
+
// the fallback below returns an EMPTY assistant message and the agent loop
|
|
324
|
+
// treats it as a silent no-op — the turn just vanishes. Nothing was emitted,
|
|
325
|
+
// so a retry cannot duplicate output → tag retryable.
|
|
326
|
+
if (!sawModelEvent && !completedMessage) {
|
|
327
|
+
reject(Object.assign(new Error('Connection closed before the model responded. Retrying…'), { retryable: true }));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
320
332
|
// Treat stream destruction from abort as a clean exit, not an error
|
|
321
333
|
stream.on('error', (err) => {
|
|
322
334
|
clearInterval(heartbeatWatchdog);
|
|
@@ -324,6 +336,15 @@ async function streamChat(messages, options, onEvent) {
|
|
|
324
336
|
resolve();
|
|
325
337
|
return;
|
|
326
338
|
}
|
|
339
|
+
// Transport-level failures ("Premature close" / ECONNRESET / socket hang up)
|
|
340
|
+
// happen whenever the backend restarts mid-deploy or a proxy drops the socket.
|
|
341
|
+
// They are exactly as transient as an overloaded_error — if nothing has been
|
|
342
|
+
// emitted to the caller yet, retry the attempt transparently instead of
|
|
343
|
+
// surfacing "Stream failed: Premature close" and killing the whole turn.
|
|
344
|
+
if (!emittedToCaller) {
|
|
345
|
+
reject(Object.assign(err, { retryable: true }));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
327
348
|
reject(err);
|
|
328
349
|
});
|
|
329
350
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;AAgID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
|
|
@@ -118,7 +118,17 @@ function matchPattern(pattern, value) {
|
|
|
118
118
|
// "name:*" → prefix match (Claude Bash semantics)
|
|
119
119
|
if (pattern.endsWith(':*')) {
|
|
120
120
|
const prefix = pattern.slice(0, -2);
|
|
121
|
-
|
|
121
|
+
if (value === prefix)
|
|
122
|
+
return true;
|
|
123
|
+
// Token-boundary prefix: "npm run test:*" must match "npm run test foo"
|
|
124
|
+
// but NOT "npm run testify-malware". The char right after the prefix has
|
|
125
|
+
// to be a shell token boundary (whitespace / end), otherwise it's a
|
|
126
|
+
// different command that merely shares a textual prefix.
|
|
127
|
+
if (value.startsWith(prefix)) {
|
|
128
|
+
const next = value.charAt(prefix.length);
|
|
129
|
+
return next === '' || /\s/.test(next);
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
122
132
|
}
|
|
123
133
|
if (pattern.includes('*') || pattern.includes('?')) {
|
|
124
134
|
try {
|
|
@@ -186,15 +196,17 @@ function ruleMatches(rule, tool, input, workDir, opts) {
|
|
|
186
196
|
(aliases[ruleTool]?.includes(tool) ?? false);
|
|
187
197
|
if (!matchesTool)
|
|
188
198
|
return false;
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
199
|
+
// Chaining guard for ALLOW rules on bash: a patterned allow rule (prefix or
|
|
200
|
+
// glob) must never auto-approve a command that chains a SECOND command onto
|
|
201
|
+
// the matched one. A newline acts like `;`, and `;`, `&&`, `||`, `|`, `&`,
|
|
202
|
+
// `$(...)`, `` `...` `` all let "npm run test; rm -rf ~" ride an
|
|
192
203
|
// "Bash(npm run test:*)" allow rule straight past the user prompt.
|
|
193
|
-
// Deny rules stay aggressive (they SHOULD match
|
|
204
|
+
// Deny rules stay aggressive (they SHOULD match chained commands), and a
|
|
194
205
|
// bare "Bash" allow (pattern === null) is an explicit "allow everything".
|
|
195
206
|
if (opts?.forAllow && tool === 'bash' && pattern !== null && pattern !== '' && pattern !== '*') {
|
|
196
207
|
const cmd = typeof input.command === 'string' ? input.command : '';
|
|
197
|
-
|
|
208
|
+
// newline / ; / && / || / | / trailing & / command-substitution / backtick
|
|
209
|
+
if (/[\r\n;`]|&&|\|\||\||&\s*$|\$\(/.test(cmd))
|
|
198
210
|
return false;
|
|
199
211
|
}
|
|
200
212
|
if (pattern === null || pattern === '' || pattern === '*')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"
|
|
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;AAkrDtE,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,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
40
|
const https = __importStar(require("https"));
|
|
41
41
|
const http = __importStar(require("http"));
|
|
42
|
+
const dns = __importStar(require("dns"));
|
|
42
43
|
const child_process_1 = require("child_process");
|
|
43
44
|
const sandbox_1 = require("./sandbox");
|
|
44
45
|
const auth_1 = require("../auth");
|
|
@@ -58,10 +59,15 @@ const MAX_OUTPUT_CHARS = 100000; // bash / grep output cap (~100 KB)
|
|
|
58
59
|
// hidden in a fake heredoc/quote (e.g. `echo "<<EOF"\nrm -rf /\nEOF`) is still
|
|
59
60
|
// caught.
|
|
60
61
|
const BLOCKED_REGEXES = [
|
|
61
|
-
// Filesystem destruction — match rm/chmod with any whitespace between flags
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/rm\s+-rf\s
|
|
62
|
+
// Filesystem destruction — match rm/chmod with any whitespace between flags.
|
|
63
|
+
// Only root itself and BARE top-level system dirs are blocked; ordinary
|
|
64
|
+
// absolute subpaths (/tmp/x, /var/folders/x, /Users/me/proj/node_modules)
|
|
65
|
+
// must stay allowed — the old /rm\s+-rf\s+\// matched every absolute path.
|
|
66
|
+
// `-[a-z]*(?:rf|fr)[a-z]*` accepts -rf, -fr, -rfv, -vrf … (combined flag).
|
|
67
|
+
/rm\s+-[a-z]*(?:rf|fr)[a-z]*\s+\/(?:\*|\s|;|&|\||$)/, // rm -rf / · / * · root, bare
|
|
68
|
+
/rm\s+-[a-z]*(?:rf|fr)[a-z]*\s+\/(?:bin|sbin|etc|usr|var|lib|lib64|boot|sys|proc|dev|root|home|system|library|applications|opt|users)(?:\/)?(?:\*|\s|;|&|\||$)/, // rm -rf /etc … bare system dir
|
|
69
|
+
/rm\s+-[a-z]*(?:rf|fr)[a-z]*\s+~(?:\/)?(?:\*|\s|;|&|\||$)/, // rm -rf ~ · ~/ · ~/*
|
|
70
|
+
/rm\s+-[a-z]*(?:rf|fr)[a-z]*\s+\*/, // rm -rf *
|
|
65
71
|
/rm\s+--no-preserve-root/,
|
|
66
72
|
/chmod\s+-[rr]\s+777\s+\//,
|
|
67
73
|
/chown\s+-[rr]/,
|
|
@@ -69,15 +75,31 @@ const BLOCKED_REGEXES = [
|
|
|
69
75
|
/dd\s+if=\/dev\/(zero|random|urandom)/,
|
|
70
76
|
// Fork bomb
|
|
71
77
|
/:\(\)\s*\{.*:\|:&/,
|
|
72
|
-
// Pipe execution from network. [^|\n]* between the fetcher and the pipe
|
|
73
|
-
// covers the URL/flags ("curl https://x.sh | sh"), which the old \s*-only
|
|
74
|
-
// version missed entirely; no-space variants (curl|bash) are still caught.
|
|
75
|
-
/curl[^|\n]*\|\s*(bash|sh|python\d*|node|perl|ruby)\b/,
|
|
76
|
-
/wget[^|\n]*\|\s*(bash|sh|python\d*|node|perl|ruby)\b/,
|
|
77
|
-
/curl\s+.*-[oO]\s*-.*\|\s*(bash|sh)/, // curl -o - … | bash
|
|
78
78
|
// Credential theft
|
|
79
79
|
/cat\s+\/etc\/(shadow|passwd)/,
|
|
80
80
|
];
|
|
81
|
+
// Network-pipe rules (curl … | sh) get their own tier. Testing them against the
|
|
82
|
+
// FULL string caused constant false positives: the pattern showing up as DATA —
|
|
83
|
+
// a heredoc'd README (`cat > README.md <<EOF … install.sh | sh … EOF`), a
|
|
84
|
+
// commit message (`git commit -m "docs: explain curl … | sh risk"`), an echo —
|
|
85
|
+
// is never executed by this bash invocation. So these run against the command
|
|
86
|
+
// with heredoc bodies AND quoted strings stripped (stripDataSections).
|
|
87
|
+
// The one real execution path that lives INSIDE quotes — `bash -c "curl … |
|
|
88
|
+
// sh"` — is covered by the dedicated SHELL_DASH_C rule below, which only fires
|
|
89
|
+
// when the quoted pipe is the argument of an interpreter's -c flag.
|
|
90
|
+
// [^|\n]* between the fetcher and the pipe covers the URL/flags ("curl
|
|
91
|
+
// https://x.sh | sh"); no-space variants (curl|bash) are still caught.
|
|
92
|
+
// python is exempted for `-m json.tool` (harmless pretty-printer: the
|
|
93
|
+
// downloaded bytes are parsed as JSON, never executed).
|
|
94
|
+
const NETWORK_PIPE_REGEXES = [
|
|
95
|
+
/curl[^|\n]*\|\s*(bash|sh|python\d*(?!\s+-m\s+json\.tool)|node|perl|ruby)\b/,
|
|
96
|
+
/wget[^|\n]*\|\s*(bash|sh|python\d*(?!\s+-m\s+json\.tool)|node|perl|ruby)\b/,
|
|
97
|
+
/curl\s+.*-[oO]\s*-.*\|\s*(bash|sh)\b/, // curl -o - … | bash
|
|
98
|
+
];
|
|
99
|
+
// `bash -c "curl … | sh"` / `sh -c 'wget … | bash'` — quoted, but executed for
|
|
100
|
+
// real. Matched at a command position on the raw (heredoc-stripped) string so
|
|
101
|
+
// the quotes are still visible to the regex.
|
|
102
|
+
const SHELL_DASH_C_PIPE = /(?:^|[;&|`\n(]|\$\(|\bsudo\s+|\bexec\s+)\s*(?:bash|sh|zsh|dash)\s+(?:-\w+\s+)*-c\s+(["'])[^"']*(?:curl|wget)[^"']*\|[^"']*\1?/;
|
|
81
103
|
// Tier 2 — words that are dangerous only when INVOKED as a command. A plain
|
|
82
104
|
// \b-word match caused constant false positives once heredocs were allowed:
|
|
83
105
|
// `grep reboot README.md`, `git log --grep shutdown`, or a heredoc body that
|
|
@@ -90,11 +112,13 @@ const BLOCKED_COMMAND_WORDS = /(?:^|[;&|`\n(]|\$\(|\bsudo\s+|\bdoas\s+|\bexec\s+
|
|
|
90
112
|
// Heredoc bodies first (<<TAG / <<-TAG / <<'TAG' / <<"TAG" … up to the line
|
|
91
113
|
// that equals TAG), then double- and single-quoted strings. Intentionally
|
|
92
114
|
// naive — this feeds a blocklist heuristic, not an executor.
|
|
115
|
+
function stripHeredocs(command) {
|
|
116
|
+
// Heredoc: the (['"]?) quote group always participates, so the \2 tag
|
|
117
|
+
// backreference is safe (JS backrefs to non-participating groups match "").
|
|
118
|
+
return command.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ');
|
|
119
|
+
}
|
|
93
120
|
function stripDataSections(command) {
|
|
94
|
-
return command
|
|
95
|
-
// Heredoc: the (['"]?) quote group always participates, so the \2 tag
|
|
96
|
-
// backreference is safe (JS backrefs to non-participating groups match "").
|
|
97
|
-
.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ')
|
|
121
|
+
return stripHeredocs(command)
|
|
98
122
|
.replace(/"(?:[^"\\]|\\[\s\S])*"/g, '""')
|
|
99
123
|
.replace(/'[^']*'/g, "''");
|
|
100
124
|
}
|
|
@@ -104,6 +128,19 @@ function isCommandBlocked(command) {
|
|
|
104
128
|
if (re.test(lower))
|
|
105
129
|
return re.toString();
|
|
106
130
|
}
|
|
131
|
+
// Network-pipe rules see the command with data sections (heredoc bodies,
|
|
132
|
+
// quoted strings) removed — mentions in docs/commit messages are data, not
|
|
133
|
+
// something this invocation executes.
|
|
134
|
+
const noData = stripDataSections(lower);
|
|
135
|
+
for (const re of NETWORK_PIPE_REGEXES) {
|
|
136
|
+
if (re.test(noData))
|
|
137
|
+
return re.toString();
|
|
138
|
+
}
|
|
139
|
+
// …but an interpreter -c argument IS executed: check it on the raw string
|
|
140
|
+
// (heredoc-stripped only, quotes intact).
|
|
141
|
+
if (SHELL_DASH_C_PIPE.test(stripHeredocs(lower))) {
|
|
142
|
+
return SHELL_DASH_C_PIPE.toString();
|
|
143
|
+
}
|
|
107
144
|
if (BLOCKED_COMMAND_WORDS.test(stripDataSections(lower))) {
|
|
108
145
|
return BLOCKED_COMMAND_WORDS.toString();
|
|
109
146
|
}
|
|
@@ -297,21 +334,26 @@ const _bgShells = new Map();
|
|
|
297
334
|
let _bgCounter = 0;
|
|
298
335
|
function startBackgroundShell(command, displayCommand, workDir, note) {
|
|
299
336
|
const id = `bg_${++_bgCounter}`;
|
|
300
|
-
|
|
337
|
+
// detached:true → own process group so kill_shell can signal the whole tree
|
|
338
|
+
// (a background dev server usually spawns children that must die with it).
|
|
339
|
+
const child = (0, child_process_1.spawn)(command, { shell: true, cwd: workDir ?? process.cwd(), env: process.env, detached: true });
|
|
301
340
|
const shell = {
|
|
302
341
|
id, command: displayCommand, child, output: '', readCursor: 0, truncated: false,
|
|
303
342
|
status: 'running', exitCode: null,
|
|
304
343
|
};
|
|
344
|
+
// Rolling buffer: keep the LAST MAX_OUTPUT_CHARS instead of the first, so a
|
|
345
|
+
// long-lived watcher/dev-server's recent output (the useful part) is never
|
|
346
|
+
// permanently lost behind an early cap.
|
|
305
347
|
const append = (chunk) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
shell.output
|
|
348
|
+
shell.output += chunk;
|
|
349
|
+
if (shell.output.length > MAX_OUTPUT_CHARS) {
|
|
350
|
+
// Drop from the front but never behind the read cursor's logical position;
|
|
351
|
+
// readCursor is an absolute offset so we track how much we've discarded.
|
|
352
|
+
const overflow = shell.output.length - MAX_OUTPUT_CHARS;
|
|
353
|
+
shell.output = shell.output.slice(overflow);
|
|
354
|
+
shell.readCursor = Math.max(0, shell.readCursor - overflow);
|
|
311
355
|
shell.truncated = true;
|
|
312
356
|
}
|
|
313
|
-
else
|
|
314
|
-
shell.output += chunk;
|
|
315
357
|
};
|
|
316
358
|
child.stdout?.on('data', (c) => append(c.toString('utf-8')));
|
|
317
359
|
child.stderr?.on('data', (c) => append(c.toString('utf-8')));
|
|
@@ -357,18 +399,22 @@ async function killShell(input) {
|
|
|
357
399
|
return { error: `No background shell with id "${id}".` };
|
|
358
400
|
if (shell.status === 'running') {
|
|
359
401
|
shell.status = 'killed';
|
|
360
|
-
|
|
361
|
-
shell.child.kill('SIGTERM');
|
|
362
|
-
}
|
|
363
|
-
catch { /* ignore */ }
|
|
364
|
-
// Escalate to SIGKILL after 3 s if process is still alive
|
|
365
|
-
setTimeout(() => {
|
|
402
|
+
const killTree = (sig) => {
|
|
366
403
|
try {
|
|
367
|
-
if (shell.
|
|
368
|
-
shell.child.
|
|
404
|
+
if (shell.child.pid)
|
|
405
|
+
process.kill(-shell.child.pid, sig);
|
|
369
406
|
}
|
|
370
|
-
catch {
|
|
371
|
-
|
|
407
|
+
catch {
|
|
408
|
+
try {
|
|
409
|
+
shell.child.kill(sig);
|
|
410
|
+
}
|
|
411
|
+
catch { /* already dead */ }
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
killTree('SIGTERM');
|
|
415
|
+
// Escalate to SIGKILL after 3 s if process group is still alive
|
|
416
|
+
setTimeout(() => { if (shell.status === 'killed')
|
|
417
|
+
killTree('SIGKILL'); }, 3000);
|
|
372
418
|
}
|
|
373
419
|
return { output: `Background shell ${id} ${shell.status}.` };
|
|
374
420
|
}
|
|
@@ -416,43 +462,80 @@ async function bash(input, abortSignal, sandbox, workDir) {
|
|
|
416
462
|
r.output = `[${sandboxNote}]\n${r.output}`;
|
|
417
463
|
return r;
|
|
418
464
|
};
|
|
465
|
+
// detached:true puts the child in its own process group so we can signal the
|
|
466
|
+
// WHOLE tree (shell + its children — dev servers, test runners) on timeout /
|
|
467
|
+
// abort. Without it, kill() only hits the /bin/sh wrapper and leaves orphaned
|
|
468
|
+
// grandchildren holding ports (→ EADDRINUSE on the next run).
|
|
419
469
|
const child = (0, child_process_1.spawn)(effectiveCommand, {
|
|
420
470
|
shell: true,
|
|
421
471
|
cwd: workDir ?? process.cwd(),
|
|
422
472
|
env: process.env,
|
|
473
|
+
detached: true,
|
|
423
474
|
});
|
|
424
|
-
|
|
475
|
+
// Kill the child's entire process group (negative pid). Falls back to a
|
|
476
|
+
// plain kill if the group signal fails (e.g. process already gone).
|
|
477
|
+
const killTree = (sig) => {
|
|
478
|
+
try {
|
|
479
|
+
if (child.pid)
|
|
480
|
+
process.kill(-child.pid, sig);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
try {
|
|
484
|
+
child.kill(sig);
|
|
485
|
+
}
|
|
486
|
+
catch { /* already dead */ }
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
// Keep HEAD + TAIL rather than head-only: test runners print the failure
|
|
490
|
+
// summary at the END, so a large passing-then-failing suite must not lose its
|
|
491
|
+
// tail. We retain the first HEAD_CHARS and the last TAIL_CHARS.
|
|
492
|
+
const HEAD_CHARS = Math.floor(MAX_OUTPUT_CHARS * 0.25);
|
|
493
|
+
const TAIL_CHARS = MAX_OUTPUT_CHARS - HEAD_CHARS;
|
|
494
|
+
let head = '';
|
|
495
|
+
let tail = '';
|
|
496
|
+
let totalLen = 0;
|
|
425
497
|
let truncated = false;
|
|
426
498
|
let settled = false;
|
|
427
|
-
// Cap output in the data handler so we never accumulate more than MAX_OUTPUT_CHARS in RAM
|
|
428
499
|
const appendOutput = (chunk) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
500
|
+
totalLen += chunk.length;
|
|
501
|
+
if (head.length < HEAD_CHARS) {
|
|
502
|
+
const room = HEAD_CHARS - head.length;
|
|
503
|
+
head += chunk.slice(0, room);
|
|
504
|
+
chunk = chunk.slice(room);
|
|
505
|
+
if (!chunk)
|
|
506
|
+
return;
|
|
435
507
|
}
|
|
436
|
-
|
|
437
|
-
|
|
508
|
+
// Remaining goes into a rolling tail buffer.
|
|
509
|
+
tail += chunk;
|
|
510
|
+
if (tail.length > TAIL_CHARS) {
|
|
511
|
+
tail = tail.slice(tail.length - TAIL_CHARS);
|
|
512
|
+
truncated = true;
|
|
438
513
|
}
|
|
439
514
|
};
|
|
515
|
+
const collect = () => {
|
|
516
|
+
if (!truncated)
|
|
517
|
+
return head + tail;
|
|
518
|
+
const omitted = totalLen - head.length - tail.length;
|
|
519
|
+
return `${head}\n\n[… ${omitted} chars omitted (kept first ${HEAD_CHARS / 1024}KB + last ${Math.round(TAIL_CHARS / 1024)}KB) …]\n\n${tail}`;
|
|
520
|
+
};
|
|
440
521
|
const done = (code, signal, timedOut = false) => {
|
|
441
522
|
if (settled)
|
|
442
523
|
return;
|
|
443
524
|
settled = true;
|
|
444
|
-
const
|
|
525
|
+
const body = collect();
|
|
445
526
|
if (timedOut) {
|
|
446
|
-
|
|
527
|
+
// Return the partial output alongside the error — it usually holds the
|
|
528
|
+
// reason the command hung (a stuck test, a prompt, a slow step).
|
|
529
|
+
resolve(prependNote({ error: `Command timed out after ${timeoutMs}ms`, interrupted: true, output: body ? `Partial output before timeout:\n${body}` : undefined }));
|
|
447
530
|
}
|
|
448
531
|
else if (abortSignal?.aborted) {
|
|
449
|
-
resolve({ error: 'Command stopped by user', interrupted: true });
|
|
532
|
+
resolve({ error: 'Command stopped by user', interrupted: true, output: body || undefined });
|
|
450
533
|
}
|
|
451
534
|
else if (code !== 0) {
|
|
452
|
-
resolve(prependNote({ output:
|
|
535
|
+
resolve(prependNote({ output: body + `\n[Exit code: ${code ?? signal ?? 'unknown'}]` }));
|
|
453
536
|
}
|
|
454
537
|
else {
|
|
455
|
-
resolve(prependNote({ output:
|
|
538
|
+
resolve(prependNote({ output: body }));
|
|
456
539
|
}
|
|
457
540
|
};
|
|
458
541
|
child.stdout?.on('data', (chunk) => appendOutput(chunk.toString('utf-8')));
|
|
@@ -464,13 +547,13 @@ async function bash(input, abortSignal, sandbox, workDir) {
|
|
|
464
547
|
clearTimeout(timer);
|
|
465
548
|
resolve({ error: err.message });
|
|
466
549
|
});
|
|
467
|
-
// Timeout kill: SIGTERM
|
|
550
|
+
// Timeout kill: SIGTERM the whole group, escalate to SIGKILL after 3 s
|
|
468
551
|
const timer = setTimeout(() => {
|
|
469
552
|
if (settled)
|
|
470
553
|
return;
|
|
471
|
-
|
|
554
|
+
killTree('SIGTERM');
|
|
472
555
|
setTimeout(() => { if (!settled)
|
|
473
|
-
|
|
556
|
+
killTree('SIGKILL'); }, 3000);
|
|
474
557
|
done(null, null, true);
|
|
475
558
|
}, timeoutMs);
|
|
476
559
|
child.on('close', () => clearTimeout(timer));
|
|
@@ -478,7 +561,7 @@ async function bash(input, abortSignal, sandbox, workDir) {
|
|
|
478
561
|
const pollAbort = setInterval(() => {
|
|
479
562
|
if (abortSignal?.aborted && !settled) {
|
|
480
563
|
clearInterval(pollAbort);
|
|
481
|
-
|
|
564
|
+
killTree('SIGTERM');
|
|
482
565
|
done(null, null, false);
|
|
483
566
|
}
|
|
484
567
|
}, 200);
|
|
@@ -528,6 +611,11 @@ async function searchFiles(input, workDir) {
|
|
|
528
611
|
else {
|
|
529
612
|
// Content search — prefer ripgrep (faster, .gitignore-aware) when
|
|
530
613
|
// installed, fall back to plain grep with hardcoded excludes otherwise.
|
|
614
|
+
// maxBuffer is raised well past the default 1 MB: on a large repo a common
|
|
615
|
+
// pattern can emit tens of MB and the default silently truncates + sets
|
|
616
|
+
// status:null (ENOBUFS), which used to surface as an empty "search failed".
|
|
617
|
+
const SEARCH_MAX_BUFFER = 64 * 1024 * 1024; // 64 MB
|
|
618
|
+
const spawnOpts = { encoding: 'utf-8', timeout: DEFAULT_TIMEOUT_MS, maxBuffer: SEARCH_MAX_BUFFER };
|
|
531
619
|
let result;
|
|
532
620
|
if (ripgrepAvailable()) {
|
|
533
621
|
const args = ['--line-number', '--no-heading', '--color=never', '--hidden', '--glob', '!.git'];
|
|
@@ -537,8 +625,10 @@ async function searchFiles(input, workDir) {
|
|
|
537
625
|
args.push(`-C${contextLines}`);
|
|
538
626
|
if (include)
|
|
539
627
|
args.push('--glob', include);
|
|
628
|
+
// -m is PER-FILE in rg/grep; the real global cap is applied on the
|
|
629
|
+
// output below. Keep a generous per-file cap so no single file floods.
|
|
540
630
|
args.push('-m', '200', '--regexp', pattern, resolved);
|
|
541
|
-
result = (0, child_process_1.spawnSync)('rg', args,
|
|
631
|
+
result = (0, child_process_1.spawnSync)('rg', args, spawnOpts);
|
|
542
632
|
}
|
|
543
633
|
else {
|
|
544
634
|
const args = ['-rn', '--binary-files=without-match', '--color=never'];
|
|
@@ -549,17 +639,31 @@ async function searchFiles(input, workDir) {
|
|
|
549
639
|
if (include)
|
|
550
640
|
args.push(`--include=${include}`);
|
|
551
641
|
args.push('--exclude-dir=.git', '--exclude-dir=node_modules', '--exclude-dir=dist', '--exclude-dir=.next', '--exclude-dir=__pycache__', '--exclude-dir=.turbo', '--exclude-dir=coverage', '--exclude-dir=.cache', '-m', '200', pattern, resolved);
|
|
552
|
-
result = (0, child_process_1.spawnSync)('grep', args,
|
|
642
|
+
result = (0, child_process_1.spawnSync)('grep', args, spawnOpts);
|
|
553
643
|
}
|
|
554
644
|
let output = result.stdout ?? '';
|
|
555
645
|
const stderr = result.stderr ?? '';
|
|
646
|
+
// Distinguish real failure modes. spawnSync sets `.error` (not `.status`)
|
|
647
|
+
// for timeout (ETIMEDOUT), buffer overflow (ENOBUFS) and spawn failures.
|
|
648
|
+
const spawnErr = result.error;
|
|
649
|
+
if (spawnErr) {
|
|
650
|
+
if (spawnErr.code === 'ETIMEDOUT' || result.signal === 'SIGTERM') {
|
|
651
|
+
const partial = output ? `\n\nPartial results before timeout:\n${globalCapMatches(output)}` : '';
|
|
652
|
+
return { error: `Search timed out after ${Math.round(DEFAULT_TIMEOUT_MS / 1000)}s — narrow the path or pattern (or add an "include" filter).${partial}` };
|
|
653
|
+
}
|
|
654
|
+
if (spawnErr.code === 'ENOBUFS') {
|
|
655
|
+
// We still captured up to maxBuffer of output — return the capped head
|
|
656
|
+
// instead of failing blind.
|
|
657
|
+
return { output: `${globalCapMatches(output)}\n[Too many matches — output exceeded ${SEARCH_MAX_BUFFER / (1024 * 1024)}MB and was capped. Narrow the pattern/path.]` };
|
|
658
|
+
}
|
|
659
|
+
return { error: `search failed: ${spawnErr.message}` };
|
|
660
|
+
}
|
|
556
661
|
// Both rg and grep use exit code 1 = "no matches", 2 = error.
|
|
557
662
|
if (result.status === 1 && !output)
|
|
558
663
|
return { output: 'No matches found.' };
|
|
559
664
|
if (result.status !== 0 && result.status !== 1)
|
|
560
665
|
return { error: stderr || 'search failed' };
|
|
561
|
-
|
|
562
|
-
output = output.slice(0, MAX_OUTPUT_CHARS) + '\n[Output truncated at 100KB]';
|
|
666
|
+
output = globalCapMatches(output);
|
|
563
667
|
return { output: output || 'No matches found.' };
|
|
564
668
|
}
|
|
565
669
|
}
|
|
@@ -567,6 +671,29 @@ async function searchFiles(input, workDir) {
|
|
|
567
671
|
return { error: err.message };
|
|
568
672
|
}
|
|
569
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Enforce the documented global cap on grep/rg output: at most 200 matched
|
|
676
|
+
* lines (the tool contract) AND at most MAX_OUTPUT_CHARS bytes. The `-m 200`
|
|
677
|
+
* flag on rg/grep is PER-FILE, so a large repo can blow well past 200 total —
|
|
678
|
+
* this applies the real global ceiling after the fact.
|
|
679
|
+
*/
|
|
680
|
+
function globalCapMatches(output) {
|
|
681
|
+
if (!output)
|
|
682
|
+
return output;
|
|
683
|
+
const GLOBAL_MATCH_CAP = 200;
|
|
684
|
+
const lines = output.split('\n');
|
|
685
|
+
let capped = output;
|
|
686
|
+
let note = '';
|
|
687
|
+
if (lines.length > GLOBAL_MATCH_CAP) {
|
|
688
|
+
capped = lines.slice(0, GLOBAL_MATCH_CAP).join('\n');
|
|
689
|
+
note = `\n[Showing first ${GLOBAL_MATCH_CAP} of ${lines.length} matched lines — narrow the pattern/path for the rest.]`;
|
|
690
|
+
}
|
|
691
|
+
if (capped.length > MAX_OUTPUT_CHARS) {
|
|
692
|
+
capped = capped.slice(0, MAX_OUTPUT_CHARS);
|
|
693
|
+
note = `\n[Output truncated at ${MAX_OUTPUT_CHARS / 1024}KB.]`;
|
|
694
|
+
}
|
|
695
|
+
return capped + note;
|
|
696
|
+
}
|
|
570
697
|
function matchesGlob(name, pattern) {
|
|
571
698
|
// Convert simple glob (* → .*, ? → .) to regex
|
|
572
699
|
try {
|
|
@@ -694,6 +821,42 @@ function literalReplace(haystack, needle, replacement) {
|
|
|
694
821
|
return haystack;
|
|
695
822
|
return haystack.slice(0, idx) + replacement + haystack.slice(idx + needle.length);
|
|
696
823
|
}
|
|
824
|
+
// Max size an edit tool will load into memory. Guards against OOM on a huge file
|
|
825
|
+
// and mirrors read_file's cap (edits target source, not multi-MB assets).
|
|
826
|
+
const MAX_EDIT_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
827
|
+
/**
|
|
828
|
+
* Guard a path for in-place editing: reject missing files, oversize files, and
|
|
829
|
+
* binaries (a text edit on a binary corrupts it, and CRLF-normalize mangles
|
|
830
|
+
* embedded \r\n). Returns an error string, or the file's mode + content.
|
|
831
|
+
*/
|
|
832
|
+
function preflightEdit(resolved) {
|
|
833
|
+
let stat;
|
|
834
|
+
try {
|
|
835
|
+
stat = fs.statSync(resolved);
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
return { error: `File not found: ${resolved}. Read the file first to verify the path.` };
|
|
839
|
+
}
|
|
840
|
+
if (stat.isDirectory())
|
|
841
|
+
return { error: `Path is a directory, not a file: ${resolved}.` };
|
|
842
|
+
if (stat.size > MAX_EDIT_BYTES) {
|
|
843
|
+
return { error: `File too large to edit (${(stat.size / 1024 / 1024).toFixed(1)} MB, max ${MAX_EDIT_BYTES / 1024 / 1024} MB). Use bash (sed/awk) for very large files.` };
|
|
844
|
+
}
|
|
845
|
+
if (isBinaryFile(resolved)) {
|
|
846
|
+
return { error: `Refusing to edit binary file ${resolved} — a text replace would corrupt it.` };
|
|
847
|
+
}
|
|
848
|
+
return { mode: stat.mode, content: fs.readFileSync(resolved, 'utf-8') };
|
|
849
|
+
}
|
|
850
|
+
/** Atomic write that preserves the original file's permission bits (mode). */
|
|
851
|
+
function atomicWritePreservingMode(resolved, data, mode) {
|
|
852
|
+
const tmp = resolved + '.nexrall_tmp';
|
|
853
|
+
fs.writeFileSync(tmp, data, 'utf-8');
|
|
854
|
+
try {
|
|
855
|
+
fs.chmodSync(tmp, mode);
|
|
856
|
+
}
|
|
857
|
+
catch { /* non-fatal — keep going */ }
|
|
858
|
+
fs.renameSync(tmp, resolved);
|
|
859
|
+
}
|
|
697
860
|
// Normalize CRLF → LF so Windows files match LF old_strings.
|
|
698
861
|
function normalizeLF(s) {
|
|
699
862
|
return s.replace(/\r\n/g, '\n');
|
|
@@ -708,7 +871,10 @@ async function editFile(input, workDir) {
|
|
|
708
871
|
return { error: 'Missing required parameter: old_string' };
|
|
709
872
|
try {
|
|
710
873
|
const resolved = resolvePath(filePath, workDir);
|
|
711
|
-
const
|
|
874
|
+
const pre = preflightEdit(resolved);
|
|
875
|
+
if ('error' in pre)
|
|
876
|
+
return { error: pre.error };
|
|
877
|
+
const original = pre.content;
|
|
712
878
|
// Normalize CRLF → LF for matching; restore on write if needed
|
|
713
879
|
const wasCRLF = original.includes('\r\n');
|
|
714
880
|
const origNorm = normalizeLF(original);
|
|
@@ -724,11 +890,9 @@ async function editFile(input, workDir) {
|
|
|
724
890
|
let updated = literalReplace(origNorm, oldNorm, newNorm);
|
|
725
891
|
if (wasCRLF)
|
|
726
892
|
updated = updated.replace(/\n/g, '\r\n');
|
|
727
|
-
// Atomic write
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
fs.writeFileSync(tmp, updated, 'utf-8');
|
|
731
|
-
fs.renameSync(tmp, resolved);
|
|
893
|
+
// Atomic write (temp + rename) so a crash mid-write never leaves the file
|
|
894
|
+
// truncated, AND preserve the original mode bits (+x on scripts etc.).
|
|
895
|
+
atomicWritePreservingMode(resolved, updated, pre.mode);
|
|
732
896
|
const diff = buildDiff(filePath, oldNorm, newNorm, origNorm);
|
|
733
897
|
const linesBefore = origNorm.split('\n').length;
|
|
734
898
|
const linesAfter = updated.split('\n').length;
|
|
@@ -802,6 +966,95 @@ function stripHtml(html) {
|
|
|
802
966
|
}
|
|
803
967
|
const MAX_REDIRECTS = 10;
|
|
804
968
|
const FETCH_USER_AGENT = 'Nexrall-Code/1.0 (+https://nexrall.com)';
|
|
969
|
+
/** True if an IP literal is loopback / private / link-local / unique-local. */
|
|
970
|
+
function isPrivateIp(ip) {
|
|
971
|
+
const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
972
|
+
if (v4) {
|
|
973
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
974
|
+
if (a === 10)
|
|
975
|
+
return true; // 10.0.0.0/8
|
|
976
|
+
if (a === 127)
|
|
977
|
+
return true; // loopback
|
|
978
|
+
if (a === 0)
|
|
979
|
+
return true; // 0.0.0.0/8
|
|
980
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
981
|
+
return true; // 172.16.0.0/12
|
|
982
|
+
if (a === 192 && b === 168)
|
|
983
|
+
return true; // 192.168.0.0/16
|
|
984
|
+
if (a === 169 && b === 254)
|
|
985
|
+
return true; // link-local (cloud metadata!)
|
|
986
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
987
|
+
return true; // CGNAT 100.64.0.0/10
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
const low = ip.toLowerCase().replace(/^\[|\]$/g, '');
|
|
991
|
+
if (low === '::1' || low === '::')
|
|
992
|
+
return true; // IPv6 loopback / unspecified
|
|
993
|
+
if (low.startsWith('fe80'))
|
|
994
|
+
return true; // link-local
|
|
995
|
+
if (low.startsWith('fc') || low.startsWith('fd'))
|
|
996
|
+
return true; // unique-local fc00::/7
|
|
997
|
+
if (low.startsWith('::ffff:'))
|
|
998
|
+
return isPrivateIp(low.slice(7)); // IPv4-mapped
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* SSRF guard: reject a URL whose host is (or resolves to) a private / internal
|
|
1003
|
+
* address. Blocks the classic prompt-injection → cloud-metadata exfiltration
|
|
1004
|
+
* (http://169.254.169.254/…) and internal-service probing. Returns an error
|
|
1005
|
+
* string when the host is unsafe, or null when it's OK to fetch.
|
|
1006
|
+
*/
|
|
1007
|
+
async function ssrfCheck(parsedUrl) {
|
|
1008
|
+
// Escape hatch for local dev-server previews (fetch_url http://localhost:3000):
|
|
1009
|
+
// opt-in only, since it re-opens the SSRF surface.
|
|
1010
|
+
const env = (process.env.NEXRALL_ALLOW_LOCAL_FETCH ?? '').toLowerCase();
|
|
1011
|
+
if (env === '1' || env === 'true' || env === 'on')
|
|
1012
|
+
return null;
|
|
1013
|
+
const host = parsedUrl.hostname.replace(/^\[|\]$/g, '');
|
|
1014
|
+
const lower = host.toLowerCase();
|
|
1015
|
+
if (lower === 'localhost' || lower.endsWith('.localhost') || lower === 'metadata.google.internal') {
|
|
1016
|
+
return `Refusing to fetch internal host "${host}" (SSRF guard).`;
|
|
1017
|
+
}
|
|
1018
|
+
// Only SHORT-CIRCUIT on a form we can evaluate exactly: a valid dotted-quad
|
|
1019
|
+
// IPv4 or an IPv6 literal. Numeric shorthands like decimal (2130706433) or
|
|
1020
|
+
// octal (0177.0.0.1) must NOT be checked with isPrivateIp — they'd read as
|
|
1021
|
+
// "not private" and slip through, yet getaddrinfo expands them to 127.0.0.1 /
|
|
1022
|
+
// 169.254.169.254 at connect time. Those fall through to DNS below, which
|
|
1023
|
+
// canonicalizes them via the same resolver the real request uses.
|
|
1024
|
+
if (isValidDottedQuad(host) || host.includes(':')) {
|
|
1025
|
+
return isPrivateIp(host) ? `Refusing to fetch private/internal address "${host}" (SSRF guard).` : null;
|
|
1026
|
+
}
|
|
1027
|
+
// Hostname (or numeric shorthand) → resolve and reject if ANY resolved
|
|
1028
|
+
// address is private. getaddrinfo handles inet_aton forms (decimal/octal/hex),
|
|
1029
|
+
// so a disguised metadata IP resolves to its real address here and is caught.
|
|
1030
|
+
try {
|
|
1031
|
+
const addrs = await new Promise((res, rej) => {
|
|
1032
|
+
dns.lookup(host, { all: true }, (err, a) => (err ? rej(err) : res(a)));
|
|
1033
|
+
});
|
|
1034
|
+
if (!addrs.length)
|
|
1035
|
+
return null;
|
|
1036
|
+
for (const { address } of addrs) {
|
|
1037
|
+
if (isPrivateIp(address))
|
|
1038
|
+
return `Refusing to fetch "${host}" — it resolves to a private/internal address (${address}) (SSRF guard).`;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
// DNS failure. For a plain hostname, let the real request surface the network
|
|
1043
|
+
// error. But for a NUMERIC shorthand we couldn't resolve, fail closed — we
|
|
1044
|
+
// can't prove it's public and it might be a disguised private literal.
|
|
1045
|
+
if (/^[\d.]+$|^0[xX]/.test(host)) {
|
|
1046
|
+
return `Refusing to fetch numeric host "${host}" — could not verify it is a public address (SSRF guard).`;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
/** True only for a canonical dotted-quad IPv4 (4 octets, each 0–255). */
|
|
1052
|
+
function isValidDottedQuad(host) {
|
|
1053
|
+
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1054
|
+
if (!m)
|
|
1055
|
+
return false;
|
|
1056
|
+
return m.slice(1).every((o) => Number(o) <= 255);
|
|
1057
|
+
}
|
|
805
1058
|
async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
806
1059
|
const url = typeof input.url === 'string' ? input.url : '';
|
|
807
1060
|
if (!url)
|
|
@@ -819,10 +1072,33 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
|
819
1072
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
820
1073
|
return { error: `Unsupported protocol: ${parsedUrl.protocol}. Only http and https are supported.` };
|
|
821
1074
|
}
|
|
1075
|
+
// SSRF guard — runs on every hop (redirects recurse through fetchUrl), so a
|
|
1076
|
+
// public URL that 302-redirects to 169.254.169.254 is still blocked.
|
|
1077
|
+
const ssrfError = await ssrfCheck(parsedUrl);
|
|
1078
|
+
if (ssrfError)
|
|
1079
|
+
return { error: ssrfError };
|
|
1080
|
+
const allowLocal = ['1', 'true', 'on'].includes((process.env.NEXRALL_ALLOW_LOCAL_FETCH ?? '').toLowerCase());
|
|
822
1081
|
return new Promise((resolve) => {
|
|
823
1082
|
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
1083
|
+
// Re-validate the address the socket ACTUALLY connects to. ssrfCheck() above
|
|
1084
|
+
// resolves at check-time; this closes the TOCTOU / DNS-rebinding window where
|
|
1085
|
+
// a hostname flips to a private IP between the check and the connect.
|
|
1086
|
+
const guardedLookup = ((hostname, opts, cb) => {
|
|
1087
|
+
const callback = (typeof opts === 'function' ? opts : cb);
|
|
1088
|
+
const options = (typeof opts === 'function' ? {} : opts);
|
|
1089
|
+
return dns.lookup(hostname, options, (err, address, family) => {
|
|
1090
|
+
if (err)
|
|
1091
|
+
return callback(err, address, family);
|
|
1092
|
+
const addrs = Array.isArray(address) ? address.map((a) => a.address) : [address];
|
|
1093
|
+
if (!allowLocal && addrs.some((a) => isPrivateIp(a))) {
|
|
1094
|
+
return callback(Object.assign(new Error('SSRF guard: host resolved to a private address at connect time'), { code: 'ESSRFBLOCKED' }), address, family);
|
|
1095
|
+
}
|
|
1096
|
+
return callback(null, address, family);
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
824
1099
|
const req = transport.get(url, {
|
|
825
1100
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
1101
|
+
lookup: guardedLookup,
|
|
826
1102
|
headers: {
|
|
827
1103
|
'User-Agent': FETCH_USER_AGENT,
|
|
828
1104
|
'Accept-Encoding': 'identity', // opt-out of gzip/deflate — we read raw Buffer as UTF-8
|
|
@@ -919,7 +1195,10 @@ async function multiEdit(input, workDir) {
|
|
|
919
1195
|
return { error: 'Missing required parameter: edits (must be a non-empty array)' };
|
|
920
1196
|
try {
|
|
921
1197
|
const resolved = resolvePath(filePath, workDir);
|
|
922
|
-
const
|
|
1198
|
+
const pre = preflightEdit(resolved);
|
|
1199
|
+
if ('error' in pre)
|
|
1200
|
+
return { error: pre.error };
|
|
1201
|
+
const rawFile = pre.content;
|
|
923
1202
|
// Work in LF-normalised space so CRLF files match LF old_strings
|
|
924
1203
|
const wasCRLF = rawFile.includes('\r\n');
|
|
925
1204
|
let content = normalizeLF(rawFile);
|
|
@@ -945,11 +1224,10 @@ async function multiEdit(input, workDir) {
|
|
|
945
1224
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
946
1225
|
content = literalReplace(content, oldStr, newStr);
|
|
947
1226
|
}
|
|
948
|
-
// Restore original line endings before writing (atomic temp+rename)
|
|
1227
|
+
// Restore original line endings before writing (atomic temp+rename),
|
|
1228
|
+
// preserving the original mode bits (+x on scripts etc.).
|
|
949
1229
|
const finalContent = wasCRLF ? content.replace(/\n/g, '\r\n') : content;
|
|
950
|
-
|
|
951
|
-
fs.writeFileSync(tmp, finalContent, 'utf-8');
|
|
952
|
-
fs.renameSync(tmp, resolved);
|
|
1230
|
+
atomicWritePreservingMode(resolved, finalContent, pre.mode);
|
|
953
1231
|
return {
|
|
954
1232
|
output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
|
|
955
1233
|
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n'),
|
package/dist/types.d.ts
CHANGED
|
@@ -151,6 +151,14 @@ export interface AgentLoopOptions {
|
|
|
151
151
|
takePendingInput?: () => string[];
|
|
152
152
|
/** Notify that a queued follow-up was injected into the conversation (for UI echo/logging). */
|
|
153
153
|
onInjectedInput?: (text: string) => void;
|
|
154
|
+
/**
|
|
155
|
+
* Fired at every turn boundary inside the loop with a snapshot of the current
|
|
156
|
+
* message history. Lets the caller persist progress INCREMENTALLY so a crash
|
|
157
|
+
* (or `kill`) hours into a long run doesn't lose the whole session — only the
|
|
158
|
+
* in-flight step. The array is a live reference; treat it as read-only and
|
|
159
|
+
* copy if you need to retain it. Cheap: callers should debounce heavy writes.
|
|
160
|
+
*/
|
|
161
|
+
onProgress?: (messages: Message[]) => void;
|
|
154
162
|
/**
|
|
155
163
|
* Max agent iterations (model response + tool round) before the runaway-loop
|
|
156
164
|
* backstop trips. Overrides the NEXRALL_MAX_ITERATIONS env var and the
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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)",
|