@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.
@@ -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
@@ -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;AAuflB,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CAuWpB"}
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"}
@@ -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: 10000,
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; // absolute backstop — auto-continue never goes past this
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 candidate = (typeof optionValue === 'number' && optionValue > 0) ? optionValue
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
- : DEFAULT_MAX_ITERATIONS;
166
- return Math.min(Math.floor(candidate), MAX_ITERATIONS_CEILING);
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
- const text = (lastAssistant?.content ?? [])
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
- /** Find the latest index ≤ maxIdx where history can be cut safely (user msg, no tool_results). */
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 > 0; i--) {
388
- const m = messages[i];
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
- messages.splice(0, cut, { role: 'user', content: [{ type: 'text', text: `[Auto-compacted ${toSummarize.length} earlier messages]\n\nSummary of the earlier conversation:\n${summary}` }] }, { role: 'assistant', content: [{ type: 'text', text: 'Understood — I have the summary of our earlier work and will continue from there.' }] });
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
@@ -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,CAkVlB;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"}
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"}
@@ -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', () => { clearInterval(heartbeatWatchdog); resolve(); });
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;AAqHD;;;;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"}
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
- return value === prefix || value.startsWith(prefix);
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
- // Multi-line guard for ALLOW rules on bash: a patterned allow rule
190
- // (prefix or glob) must never auto-approve a multi-line command a newline
191
- // acts like `;`, so "npm run test\nrm -rf ~" would otherwise ride an
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 multi-line commands), and a
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
- if (/[\r\n]/.test(cmd))
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":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA46CtE,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"}
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"}
@@ -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
- /rm\s+-rf\s+\//, // rm -rf /… (any path under /)
63
- /rm\s+-rf\s+~/, // rm -rf ~
64
- /rm\s+-rf\s+\*/, // rm -rf *
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
- const child = (0, child_process_1.spawn)(command, { shell: true, cwd: workDir ?? process.cwd(), env: process.env });
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
- if (shell.truncated)
307
- return;
308
- const remaining = MAX_OUTPUT_CHARS - shell.output.length;
309
- if (chunk.length >= remaining) {
310
- shell.output += chunk.slice(0, remaining);
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
- try {
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.status === 'killed')
368
- shell.child.kill('SIGKILL');
404
+ if (shell.child.pid)
405
+ process.kill(-shell.child.pid, sig);
369
406
  }
370
- catch { /* ignore */ }
371
- }, 3000);
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
- let output = '';
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
- if (truncated)
430
- return;
431
- const remaining = MAX_OUTPUT_CHARS - output.length;
432
- if (chunk.length >= remaining) {
433
- output += chunk.slice(0, remaining);
434
- truncated = true;
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
- else {
437
- output += chunk;
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 truncNote = truncated ? '\n[Output truncated at 100KB]' : '';
525
+ const body = collect();
445
526
  if (timedOut) {
446
- resolve({ error: `Command timed out after ${timeoutMs}ms`, interrupted: true });
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: output + `\n[Exit code: ${code ?? signal ?? 'unknown'}]` + truncNote }));
535
+ resolve(prependNote({ output: body + `\n[Exit code: ${code ?? signal ?? 'unknown'}]` }));
453
536
  }
454
537
  else {
455
- resolve(prependNote({ output: output + truncNote }));
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 first, escalate to SIGKILL after 3 s if still alive
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
- child.kill('SIGTERM');
554
+ killTree('SIGTERM');
472
555
  setTimeout(() => { if (!settled)
473
- child.kill('SIGKILL'); }, 3000);
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
- child.kill('SIGTERM');
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, { encoding: 'utf-8', timeout: DEFAULT_TIMEOUT_MS });
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, { encoding: 'utf-8', timeout: DEFAULT_TIMEOUT_MS });
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
- if (output.length > MAX_OUTPUT_CHARS)
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 original = fs.readFileSync(resolved, 'utf-8');
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: write to a temp file then rename so a crash mid-write never
728
- // leaves the original file truncated or partially overwritten.
729
- const tmp = resolved + '.nexrall_tmp';
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 rawFile = fs.readFileSync(resolved, 'utf-8');
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
- const tmp = resolved + '.nexrall_tmp';
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
@@ -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.2.0",
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)",