@molecule/api-ai-tools 1.4.3 → 1.5.0

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-09-24T01:21:52.537Z
6
+ Generated: 2026-10-15T00:28:10.570Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -323,6 +323,14 @@ Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
323
323
  type DiscoveredSkill = SkillEntry
324
324
  ```
325
325
 
326
+ ### Classes
327
+
328
+ #### `ToolInputError`
329
+
330
+ A tool call the handler refuses with a message meant for the model as is —
331
+ thrown from deep inside a handler (a path check), returned by the guard as the
332
+ call's `error` without the generic "check your parameters" wrapping.
333
+
326
334
  ### Functions
327
335
 
328
336
  #### `buildAgentPrompt(ctx)`
@@ -503,6 +511,24 @@ function isEnvFilePath(path: string): boolean
503
511
 
504
512
  **Returns:** `true` when the file is an env file.
505
513
 
514
+ #### `isOutsideRoot(path, projectRoot)`
515
+
516
+ Whether a tool path points outside the project root once normalized (an
517
+ absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
518
+ clamps such a path to the root; a file tool must refuse it instead of
519
+ answering about the root — a `list_files /tmp/x` used to return the
520
+ workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
521
+ 2026-10-14).
522
+
523
+ ```typescript
524
+ function isOutsideRoot(path: string, projectRoot: string): boolean
525
+ ```
526
+
527
+ - `path` — The path the tool was given.
528
+ - `projectRoot` — The project root.
529
+
530
+ **Returns:** True when the path leaves the root.
531
+
506
532
  #### `isValidGlob(pattern)`
507
533
 
508
534
  Validate that a glob/include pattern is safe (no shell metacharacters that could
@@ -4,6 +4,13 @@
4
4
  *
5
5
  * @module
6
6
  */
7
+ /**
8
+ * A tool call the handler refuses with a message meant for the model as is —
9
+ * thrown from deep inside a handler (a path check), returned by the guard as the
10
+ * call's `error` without the generic "check your parameters" wrapping.
11
+ */
12
+ export declare class ToolInputError extends Error {
13
+ }
7
14
  /** The JSON-Schema-ish shape every tool in this package declares. */
8
15
  export interface NormalizableSchema {
9
16
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"input-normalizer.d.ts","sourceRoot":"","sources":["../src/input-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACpE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiDD,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAiD5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAaR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5E,OAAO,GAAE,YAAiB,GACzB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAsBxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,EACD,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAaxC"}
1
+ {"version":3,"file":"input-normalizer.d.ts","sourceRoot":"","sources":["../src/input-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACpE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiDD,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAiD5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAaR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5E,OAAO,GAAE,YAAiB,GACzB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAuBxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,EACD,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAaxC"}
@@ -5,6 +5,13 @@
5
5
  * @module
6
6
  */
7
7
  import { redactSecrets } from './utilities.js';
8
+ /**
9
+ * A tool call the handler refuses with a message meant for the model as is —
10
+ * thrown from deep inside a handler (a path check), returned by the guard as the
11
+ * call's `error` without the generic "check your parameters" wrapping.
12
+ */
13
+ export class ToolInputError extends Error {
14
+ }
8
15
  /**
9
16
  * Other names a model sends for a declared parameter, most likely first.
10
17
  *
@@ -157,6 +164,8 @@ export function guardToolExecute(schema, impl, options = {}) {
157
164
  return await impl(input, ...rest);
158
165
  }
159
166
  catch (e) {
167
+ if (e instanceof ToolInputError)
168
+ return { error: e.message };
160
169
  if (missing.length > 0)
161
170
  return { error: missingParamError(schema, input, missing) };
162
171
  const message = redactError(e instanceof Error ? e.message : String(e));
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA0T1C,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA+T1C,CAAA"}
package/dist/schemas.js CHANGED
@@ -79,7 +79,7 @@ export const TOOL_SCHEMAS = {
79
79
  },
80
80
  edit_file: {
81
81
  name: 'edit_file',
82
- description: 'Make targeted search-and-replace edits to a file. Preferred over write_file for small changes. Each old_string must match exactly once in the file. The result shows each edited region as it now reads, with line numbers, so there is no need to re-read the file to check an edit.',
82
+ description: 'Make targeted search-and-replace edits to a file. Preferred over write_file for small changes. Each old_string must match exactly once in the file, unless that replacement sets replace_all to change every occurrence (a rename, a repeated call). The result shows each edited region as it now reads, with line numbers, so there is no need to re-read the file to check an edit.',
83
83
  parameters: {
84
84
  type: 'object',
85
85
  properties: {
@@ -101,6 +101,10 @@ export const TOOL_SCHEMAS = {
101
101
  type: 'string',
102
102
  description: 'Replacement string.',
103
103
  },
104
+ replace_all: {
105
+ type: 'boolean',
106
+ description: 'Replace every occurrence of old_string instead of requiring exactly one (default false).',
107
+ },
104
108
  },
105
109
  required: ['old_string', 'new_string'],
106
110
  },
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAoB9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA+BnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAqzCxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAoB9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAgCnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAu3CxF"}
package/dist/tools.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import { addChangeSpan, changedSpan, countLines, renderEditExcerpt, renderSmallFile, } from './edit-excerpt.js';
10
- import { guardToolExecute, missingParamError } from './input-normalizer.js';
10
+ import { guardToolExecute, missingParamError, ToolInputError } from './input-normalizer.js';
11
11
  import { TOOL_SCHEMAS } from './schemas.js';
12
12
  /**
13
13
  * Total waiting, per background task, after which wait_for_task reports it
@@ -16,7 +16,7 @@ import { TOOL_SCHEMAS } from './schemas.js';
16
16
  const stuckAfterMs = () => Number(process.env.MOL_AI_BG_STUCK_AFTER_MS) || 5 * 60 * 1000;
17
17
  /** Cumulative wait_for_task time per task id, this process. */
18
18
  const waitedByTask = new Map();
19
- import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isEnvFilePath, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
19
+ import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
20
20
  /**
21
21
  * Build a complete set of AI agent tools bound to an execution backend.
22
22
  *
@@ -49,8 +49,30 @@ export function buildTools(backend, config) {
49
49
  const excludedDirs = (searchExcludedDirs && searchExcludedDirs.length > 0
50
50
  ? searchExcludedDirs
51
51
  : DEFAULT_SEARCH_EXCLUDED_DIRS).filter((d) => /^[A-Za-z0-9._-]+$/.test(d));
52
- const grepExcludeArgs = excludedDirs.map((d) => `--exclude-dir=${shellQuote(d)}`).join(' ');
53
- const findExcludeArgs = excludedDirs.map((d) => `-not -path ${shellQuote(`*/${d}/*`)}`).join(' ');
52
+ /**
53
+ * The excluded dirs for a search rooted at `path`. Inside an installed
54
+ * package the build output IS the code: excluding `dist` there returned
55
+ * nothing for `IconName` in `node_modules/@molecule/app-icons` (it lives in
56
+ * `dist/types.d.ts`; X1 run sol-m04).
57
+ *
58
+ * @param path - The resolved search root.
59
+ * @returns The dir names to skip below it.
60
+ */
61
+ const excludesFor = (path) => /(?:^|\/)node_modules(?:\/|$)/.test(path)
62
+ ? excludedDirs.filter((d) => d !== 'dist')
63
+ : excludedDirs;
64
+ const grepExcludeArgs = (path) => excludesFor(path)
65
+ .map((d) => `--exclude-dir=${shellQuote(d)}`)
66
+ .join(' ');
67
+ // Pruned BELOW the root only (-mindepth 1): a `-not -path '*/node_modules/*'`
68
+ // filter matched every result of a search rooted inside node_modules and
69
+ // returned none.
70
+ const findPruneArgs = (path) => {
71
+ const dirs = excludesFor(path);
72
+ if (dirs.length === 0)
73
+ return '';
74
+ return `-mindepth 1 \\( -type d \\( ${dirs.map((d) => `-name ${shellQuote(d)}`).join(' -o ')} \\) -prune \\) -o`;
75
+ };
54
76
  // ── Path helpers ───────────────────────────────────────────────
55
77
  /**
56
78
  * After a write or edit, parse the file and hand the error back IN THE SAME
@@ -81,7 +103,14 @@ export function buildTools(backend, config) {
81
103
  * @param path - Relative or absolute path requested by the tool input.
82
104
  * @returns A normalized path honoring `pathGuards` and the backend root.
83
105
  */
84
- function resolve(path) {
106
+ function resolve(path, use = 'file') {
107
+ if (pathGuards && typeof path === 'string' && isOutsideRoot(path, root)) {
108
+ throw new ToolInputError(use === 'cwd'
109
+ ? `${path} is outside the workspace (${root}), so it cannot be the working directory — ` +
110
+ `start the command with \`cd ${path} && \` instead.`
111
+ : `${path} is outside the workspace (${root}). The file tools only reach files inside it — ` +
112
+ `read or list anything else with exec_command (e.g. \`cat ${path}\` or \`ls ${path}\`).`);
113
+ }
85
114
  return pathGuards ? resolvePath(path, root) : path;
86
115
  }
87
116
  /**
@@ -102,8 +131,12 @@ export function buildTools(backend, config) {
102
131
  return null;
103
132
  }
104
133
  catch (_error) {
105
- // readlink unavailable or timed out — deny access conservatively
106
- return 'Unable to verify path safety — access denied';
134
+ // readlink unavailable or timed out — deny access conservatively, and say
135
+ // why: "access denied" read as a forbidden path, and builders spent call
136
+ // after call on paths that were fine while the sandbox was not answering.
137
+ return ('Unable to verify path safety: the sandbox did not answer the check within 5 s (it may be ' +
138
+ 'restarting or out of memory), so the path was refused to stay safe. Retry in a moment; if ' +
139
+ 'every file tool fails this way, the sandbox needs a restart.');
107
140
  }
108
141
  }
109
142
  /**
@@ -543,7 +576,13 @@ export function buildTools(backend, config) {
543
576
  if (!replacements?.length &&
544
577
  typeof input.old_string === 'string' &&
545
578
  typeof input.new_string === 'string') {
546
- replacements = [{ old_string: input.old_string, new_string: input.new_string }];
579
+ replacements = [
580
+ {
581
+ old_string: input.old_string,
582
+ new_string: input.new_string,
583
+ replace_all: input.replace_all,
584
+ },
585
+ ];
547
586
  }
548
587
  if (!replacements?.length)
549
588
  return {
@@ -568,7 +607,10 @@ export function buildTools(backend, config) {
568
607
  // Where each applied replacement landed, in the coordinates of the
569
608
  // latest content — rendered into the result so the model sees the edit.
570
609
  let spans = [];
571
- for (const { old_string: oldString, new_string: newString } of replacements) {
610
+ for (const { old_string: oldString, new_string: newString, replace_all: replaceAll,
611
+ // Models sometimes send the camelCase spelling inside an item, where
612
+ // the input normalizer (top-level keys only) does not reach.
613
+ replaceAll: replaceAllCamel, } of replacements) {
572
614
  // Validate each replacement is well-formed BEFORE touching content. A
573
615
  // weak model sometimes emits a batch element missing a field or with a
574
616
  // non-string value; left unchecked that either crashes (`undefined`
@@ -690,13 +732,22 @@ export function buildTools(backend, config) {
690
732
  `indentation — or include more surrounding context to make it unique.`,
691
733
  };
692
734
  }
735
+ if (count > 1 && (replaceAll === true || replaceAllCamel === true)) {
736
+ // Every occurrence, by split/join — never String.replace, whose
737
+ // replacement string treats ` if (count > 1) {
738
+ // Say WHERE,` and friends as patterns (see below).
739
+ const replaced = content.split(oldString).join(newString);
740
+ spans = addChangeSpan(spans, content, replaced);
741
+ content = replaced;
742
+ continue;
743
+ }
693
744
  if (count > 1) {
694
745
  // Say WHERE, so the retry can widen old_string around the one it means.
695
746
  const lines = [];
696
747
  for (let at = content.indexOf(oldString); at >= 0 && lines.length < 10; at = content.indexOf(oldString, at + 1))
697
748
  lines.push(content.slice(0, at).split('\n').length);
698
749
  return {
699
- error: `old_string found ${count} times in ${path} (starting at lines ${lines.join(', ')}${count > lines.length ? ', …' : ''}) — must be unique. Include more surrounding context from the one you mean, or make one edit per occurrence.`,
750
+ error: `old_string found ${count} times in ${path} (starting at lines ${lines.join(', ')}${count > lines.length ? ', …' : ''}) — must be unique. Include more surrounding context from the one you mean, or set replace_all: true on this replacement to change every occurrence.`,
700
751
  };
701
752
  }
702
753
  // Splice by index rather than `content.replace(oldString, newString)`:
@@ -776,7 +827,7 @@ export function buildTools(backend, config) {
776
827
  // `file:line:content` for matches, plus `--` group separators. Both
777
828
  // shapes are parsed below; without context the output is unchanged.
778
829
  const contextArg = contextLines > 0 ? `-C ${contextLines}` : '';
779
- const result = await backend.run(`grep -rn ${globArg} ${contextArg} ${grepExcludeArgs} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)} 2>/dev/null || true`, { timeout: 10000 });
830
+ const result = await backend.run(`grep -rn ${globArg} ${contextArg} ${grepExcludeArgs(path)} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)} 2>/dev/null || true`, { timeout: 10000 });
780
831
  // grep emits `<file>:<line>:<content>`, so redaction runs PER MATCH on the
781
832
  // content alone: the file prefix would otherwise hide an env assignment from
782
833
  // the line-anchored env pattern, and knowing each match's own path is what
@@ -835,7 +886,7 @@ export function buildTools(backend, config) {
835
886
  if (symlinkErr)
836
887
  return { error: symlinkErr };
837
888
  try {
838
- const result = await backend.run(`find ${shellQuote(path)} -name ${shellQuote(pattern)} ${findExcludeArgs} 2>/dev/null | head -${MAX_FIND_RESULTS}`, { timeout: 10000 });
889
+ const result = await backend.run(`find ${shellQuote(path)} ${findPruneArgs(path)} -name ${shellQuote(pattern)} -print 2>/dev/null | head -${MAX_FIND_RESULTS}`, { timeout: 10000 });
839
890
  const files = result.stdout.trim().split('\n').filter(Boolean);
840
891
  return { pattern, path, files };
841
892
  }
@@ -899,7 +950,7 @@ export function buildTools(backend, config) {
899
950
  },
900
951
  async exec_command(input) {
901
952
  const command = input.command;
902
- const cwd = input.cwd ? resolve(input.cwd) : root;
953
+ const cwd = input.cwd ? resolve(input.cwd, 'cwd') : root;
903
954
  // A model that sent the command under another name (`cmd`, `script`, …)
904
955
  // has already been normalized by guardToolExecute; reaching here with
905
956
  // nothing means there is genuinely no command. Say so in the terms that
@@ -106,6 +106,19 @@ export declare function checkBlockedCommand(command: string): string | null;
106
106
  * @returns A normalized absolute path confined to `projectRoot`.
107
107
  */
108
108
  export declare function resolvePath(path: string, projectRoot: string): string;
109
+ /**
110
+ * Whether a tool path points outside the project root once normalized (an
111
+ * absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
112
+ * clamps such a path to the root; a file tool must refuse it instead of
113
+ * answering about the root — a `list_files /tmp/x` used to return the
114
+ * workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
115
+ * 2026-10-14).
116
+ *
117
+ * @param path - The path the tool was given.
118
+ * @param projectRoot - The project root.
119
+ * @returns True when the path leaves the root.
120
+ */
121
+ export declare function isOutsideRoot(path: string, projectRoot: string): boolean;
109
122
  /**
110
123
  * Validate that a glob/include pattern is safe (no shell metacharacters that could
111
124
  * inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -256,6 +256,27 @@ export function resolvePath(path, projectRoot) {
256
256
  return projectRoot;
257
257
  return resolved;
258
258
  }
259
+ /**
260
+ * Whether a tool path points outside the project root once normalized (an
261
+ * absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
262
+ * clamps such a path to the root; a file tool must refuse it instead of
263
+ * answering about the root — a `list_files /tmp/x` used to return the
264
+ * workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
265
+ * 2026-10-14).
266
+ *
267
+ * @param path - The path the tool was given.
268
+ * @param projectRoot - The project root.
269
+ * @returns True when the path leaves the root.
270
+ */
271
+ export function isOutsideRoot(path, projectRoot) {
272
+ if (path === '' || path === '/')
273
+ return false;
274
+ const clean = path.replace(/\0/g, '');
275
+ const resolved = clean.startsWith('/')
276
+ ? posix.normalize(clean)
277
+ : posix.normalize(`${projectRoot}/${clean}`);
278
+ return resolved !== projectRoot && !resolved.startsWith(projectRoot + '/');
279
+ }
259
280
  /**
260
281
  * Validate that a glob/include pattern is safe (no shell metacharacters that could
261
282
  * inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",