@molecule/api-ai-tools 1.0.6 → 1.0.8

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-13T03:42:47.996Z
6
+ Generated: 2026-09-18T06:57:26.872Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -68,6 +68,15 @@ interface ExecutionBackend {
68
68
  /** The root directory for all operations (e.g. '/workspace' or '/Users/.../project'). */
69
69
  readonly projectRoot: string
70
70
 
71
+ /**
72
+ * Marks backends that operate DIRECTLY on the host filesystem (the local
73
+ * backend), as opposed to a contained sandbox. Consumers use it to judge
74
+ * how loud to be about disabled safety guards — a `..` path or symlink
75
+ * escape on a host backend reaches the operator's real files, while the
76
+ * same escape in a sandbox stays contained.
77
+ */
78
+ readonly hostFs?: boolean
79
+
71
80
  /** Read a file's content as UTF-8 string. */
72
81
  readFile(path: string): Promise<string>
73
82
 
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/backends/local.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAmDxE"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/backends/local.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAsDxE"}
@@ -16,6 +16,9 @@ import { dirname } from 'path';
16
16
  export function createLocalBackend(projectRoot) {
17
17
  return {
18
18
  projectRoot,
19
+ // Marks this backend as operating directly on the HOST filesystem —
20
+ // buildTools() uses it to warn loudly when path/symlink guards are off.
21
+ hostFs: true,
19
22
  async readFile(path) {
20
23
  return readFile(path, 'utf8');
21
24
  },
@@ -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;AAG9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsBnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA8qBxF"}
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;AAG9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsBnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA2zBxF"}
package/dist/tools.js CHANGED
@@ -18,6 +18,21 @@ import { checkBlockedCommand, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, i
18
18
  export function buildTools(backend, config) {
19
19
  const { include, exclude, pathGuards = true, symlinkGuards = false, redactSecrets: doRedact = true, blockDangerousCommands = false, blockCommand, execTimeoutMs = 120_000, commandBudgetMs, searchExcludedDirs, onAfterWrite, onFileDiff, onFileChange, } = config ?? {};
20
20
  const root = backend.projectRoot;
21
+ // A host-filesystem backend with escape guards off is the combination the
22
+ // audit flagged: `pathGuards` off lets `..` paths out of the workspace,
23
+ // and `symlinkGuards` off lets a workspace SYMLINK redirect reads/writes
24
+ // anywhere on the host (npm-workspace layouts are full of such symlinks).
25
+ // Defaults are deliberately unchanged — but the state must be LOUD, not
26
+ // silent: one warn per buildTools() call, naming the exact gaps.
27
+ if (backend.hostFs && (!pathGuards || !symlinkGuards)) {
28
+ const off = [!pathGuards ? '`pathGuards`' : null, !symlinkGuards ? '`symlinkGuards`' : null]
29
+ .filter(Boolean)
30
+ .join(' and ');
31
+ console.warn(`[ai-tools] ${off} disabled on a LOCAL-HOST backend (root ${backend.projectRoot}) — ` +
32
+ 'model-driven file operations can escape the workspace via `..` paths or symlinks ' +
33
+ 'into the host filesystem. Contained agent runs should use the sandbox backend; ' +
34
+ 'host runs should enable both guards.');
35
+ }
21
36
  // One synchronized excluded-dir set for BOTH search tools (VS Code
22
37
  // `search.exclude` semantics). Names are validated defensively — they are
23
38
  // interpolated into shell commands, so anything outside the safe charset is
@@ -89,6 +104,102 @@ export function buildTools(backend, config) {
89
104
  result = isEnvFilePath(path) ? redactSecrets(result) : redactSecretsInCode(result);
90
105
  return result;
91
106
  }
107
+ // ── Empty-read verification ────────────────────────────────────
108
+ /**
109
+ * The file's size in bytes as the backend's own shell reports it, or `null`
110
+ * when that could not be established (the probe failed, timed out, or printed
111
+ * something unparseable).
112
+ *
113
+ * `null` means "I could not look" and is deliberately DISTINCT from `0` ("I
114
+ * looked and the file is empty") — conflating the two is the very failure this
115
+ * probe exists to prevent. The last integer in stdout is taken rather than the
116
+ * whole string, so a consumer that wraps commands in environment sourcing
117
+ * cannot break the parse with stray output.
118
+ *
119
+ * @param path - The already-resolved, symlink-checked path.
120
+ * @returns The byte count, or null when it could not be determined.
121
+ */
122
+ async function probeFileSize(path) {
123
+ try {
124
+ const result = await backend.run(`wc -c < ${shellQuote(path)}`, { timeout: 10_000 });
125
+ if (result.exitCode !== 0)
126
+ return null;
127
+ const match = /(\d+)\s*$/.exec(result.stdout);
128
+ if (!match)
129
+ return null;
130
+ const bytes = Number.parseInt(match[1], 10);
131
+ return Number.isFinite(bytes) ? bytes : null;
132
+ }
133
+ catch (_error) {
134
+ // The probe is the cross-check for an ALREADY-suspect read; if it cannot
135
+ // run we must report "unverified", never "empty" — returning 0 here would
136
+ // reintroduce exactly the conflation this function exists to remove.
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * Decide what an EMPTY string from `backend.readFile` actually means.
142
+ *
143
+ * A backend read can come back empty without throwing: a sandbox exec whose
144
+ * output stream ends early still reports the process's own exit code, an HTTP
145
+ * file read can return a 200 with no body, and a concurrent writer can be
146
+ * observed mid-truncate. Observed in production on 2026-09-16 (X0 rehearsal
147
+ * 84): several `read_file` calls on two real components returned no content
148
+ * while an exec shell showed both files present at 3,617 and 25,746 bytes; a
149
+ * later read of the same paths succeeded, so the failure was transient.
150
+ *
151
+ * Returning `{ content: '' }` for that is indistinguishable from a successful
152
+ * read of an empty file — and a model that believes a file is empty writes it
153
+ * from scratch, destroying work it never saw. So an empty read is verified
154
+ * against the file's real size before it is reported as content: genuinely
155
+ * empty is returned as empty and SAID so; non-empty is retried once and then
156
+ * reported as a FAILED read; unverifiable is also a failure, never an empty
157
+ * file.
158
+ *
159
+ * @param path - The already-resolved, symlink-checked path.
160
+ * @param tool - The calling tool's name, for the error message.
161
+ * @returns The file's real content (possibly empty, with a note), or an error.
162
+ */
163
+ async function classifyEmptyRead(path, tool) {
164
+ const bytes = await probeFileSize(path);
165
+ if (bytes === 0) {
166
+ return {
167
+ content: '',
168
+ note: `${path} exists and is EMPTY (0 bytes) — that is the file's real content, not a failed read.`,
169
+ };
170
+ }
171
+ if (bytes === null) {
172
+ return {
173
+ error: `${tool} obtained NO CONTENT for ${path}, and could not confirm the file's size ` +
174
+ `(the \`wc -c ${path}\` probe failed), so this may be a FAILED read rather than an ` +
175
+ `empty file. Do NOT write or edit ${path} from memory or from the plan — that would ` +
176
+ `overwrite a file you have never seen. Call read_file again, or run \`cat ${path}\` ` +
177
+ `with exec_command, and only proceed once you actually have its contents.`,
178
+ };
179
+ }
180
+ // Non-empty on disk: the read failed, and the observed failure is transient —
181
+ // so retry it once here rather than spending an executor turn on it.
182
+ try {
183
+ const retry = await backend.readFile(path);
184
+ if (typeof retry === 'string' && retry !== '') {
185
+ return {
186
+ content: retry,
187
+ note: `The first read of ${path} returned nothing; this content came from an immediate retry.`,
188
+ };
189
+ }
190
+ }
191
+ catch (_error) {
192
+ // The retry's own failure adds nothing to the message below — the size
193
+ // probe is the authoritative fact, and it already said the file has bytes.
194
+ }
195
+ return {
196
+ error: `${tool} obtained NO CONTENT for ${path}, but the file is ${bytes} bytes on disk — the ` +
197
+ `read FAILED, this is not an empty file (a retry returned nothing either). Do NOT write ` +
198
+ `or edit ${path} from memory or from the plan — that would overwrite a file you have ` +
199
+ `never seen. Call read_file again, or run \`cat ${path}\` with exec_command, and only ` +
200
+ `proceed once you actually have its contents.`,
201
+ };
202
+ }
92
203
  // ── Diff computation ───────────────────────────────────────────
93
204
  /**
94
205
  * Compute a lightweight diff summary for telemetry and UI badges.
@@ -134,6 +245,22 @@ export function buildTools(backend, config) {
134
245
  return { error: symlinkErr };
135
246
  try {
136
247
  const content = await backend.readFile(path);
248
+ // A read that produced no usable text is verified against the file's real
249
+ // size before it can be reported as content (see classifyEmptyRead). The
250
+ // non-string arm covers a backend whose transport handed back an empty
251
+ // body as `undefined`; `content.length` below would otherwise raise an
252
+ // unactionable "Cannot read properties of undefined".
253
+ if (typeof content !== 'string' || content === '') {
254
+ const verdict = await classifyEmptyRead(path, 'read_file');
255
+ if ('error' in verdict)
256
+ return verdict;
257
+ return {
258
+ path,
259
+ content: sanitizeFileContent(verdict.content, path),
260
+ ...(verdict.content === '' ? { empty: true } : {}),
261
+ ...(verdict.note ? { note: verdict.note } : {}),
262
+ };
263
+ }
137
264
  if (content.length > MAX_READ_SIZE)
138
265
  return {
139
266
  error: `File too large (${Math.round(content.length / 1024)}KB). Maximum is ${MAX_READ_SIZE / 1024 / 1024}MB.`,
@@ -251,6 +378,15 @@ export function buildTools(backend, config) {
251
378
  return { error: symlinkErr };
252
379
  try {
253
380
  let content = await backend.readFile(path);
381
+ // A transiently-empty read here made every old_string "not found", which
382
+ // steers the model to re-read and then rewrite a file it never saw. Same
383
+ // verification as read_file: empty is only believed once it is confirmed.
384
+ if (typeof content !== 'string' || content === '') {
385
+ const verdict = await classifyEmptyRead(path, 'edit_file');
386
+ if ('error' in verdict)
387
+ return verdict;
388
+ content = verdict.content;
389
+ }
254
390
  const oldContent = content;
255
391
  for (const { old_string: oldString, new_string: newString } of replacements) {
256
392
  // Validate each replacement is well-formed BEFORE touching content. A
package/dist/types.d.ts CHANGED
@@ -10,6 +10,14 @@
10
10
  export interface ExecutionBackend {
11
11
  /** The root directory for all operations (e.g. '/workspace' or '/Users/.../project'). */
12
12
  readonly projectRoot: string;
13
+ /**
14
+ * Marks backends that operate DIRECTLY on the host filesystem (the local
15
+ * backend), as opposed to a contained sandbox. Consumers use it to judge
16
+ * how loud to be about disabled safety guards — a `..` path or symlink
17
+ * escape on a host backend reaches the operator's real files, while the
18
+ * same escape in a sandbox stays contained.
19
+ */
20
+ readonly hostFs?: boolean;
13
21
  /** Read a file's content as UTF-8 string. */
14
22
  readFile(path: string): Promise<string>;
15
23
  /** Write content to a file. Creates parent directories as needed. */
@@ -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":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAE5B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IAEzB,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEvC,qEAAqE;IACrE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvD,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvC,mCAAmC;IACnC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC,CAAA;IAEnF;;;OAGG;IACH,GAAG,CACD,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB;;;;;WAKG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GACA,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjE;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB,wEAAwE;IACxE,UAAU,CAAC,EAAE,OAAO,CAAA;IAEpB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,iFAAiF;IACjF,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,2FAA2F;IAC3F,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAEhC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAE1E;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAE7B,uGAAuG;IACvG,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAE9C,yEAAyE;IACzE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAE3C,wFAAwF;IACxF,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CAChD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAA;IACxC,IAAI,EAAE,MAAM,CAAA;CACb;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;IAEjB,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IAEnB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,EAAE,CAAA;IAEf,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjB,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAA;IAE/B,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAEE,CAAA;AAyFhD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD;AAsBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBlE;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAID,yCAAyC;AACzC,eAAO,MAAM,aAAa,QAAkB,CAAA;AAC5C,8CAA8C;AAC9C,eAAO,MAAM,cAAc,QAAmB,CAAA;AAC9C,kDAAkD;AAClD,eAAO,MAAM,eAAe,QAAa,CAAA;AACzC,0BAA0B;AAC1B,eAAO,MAAM,kBAAkB,KAAK,CAAA;AACpC,wBAAwB;AACxB,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,mHAW/B,CAAA;AAEV;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAmBnE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAyBf"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAEE,CAAA;AAyFhD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD;AA2BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBlE;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAID,yCAAyC;AACzC,eAAO,MAAM,aAAa,QAAkB,CAAA;AAC5C,8CAA8C;AAC9C,eAAO,MAAM,cAAc,QAAmB,CAAA;AAC9C,kDAAkD;AAClD,eAAO,MAAM,eAAe,QAAa,CAAA;AACzC,0BAA0B;AAC1B,eAAO,MAAM,kBAAkB,KAAK,CAAA;AACpC,wBAAwB;AACxB,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,mHAW/B,CAAA;AAEV;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAmBnE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAyBf"}
package/dist/utilities.js CHANGED
@@ -156,6 +156,9 @@ export function isEnvFilePath(path) {
156
156
  // ── Command blocking ──────────────────────────────────────────────────────────
157
157
  /**
158
158
  * Commands that dump environment variables — blocked to prevent secret leakage.
159
+ * Also covers reads of `/etc/mol/…` (the molecule platform's secrets dir —
160
+ * `/etc/mol/env` carries the vault + platform secrets inside a sandbox),
161
+ * which is an env-dump by another door.
159
162
  *
160
163
  * `env` counts only when it DUMPS: bare, or with the `-0` / `--null` output
161
164
  * flags, followed by the end of the command or a separator / pipe / redirect.
@@ -164,9 +167,11 @@ export function isEnvFilePath(path) {
164
167
  * a keyless build still succeeds, and blocking those sent it looking for
165
168
  * workarounds (X0 rehearsal 14).
166
169
  */
167
- const BLOCKED_COMMANDS = /(?:^|[;&|`]\s*|(?:sh|bash|zsh|dash)\s+-c\s+['"]?\s*)(?:\/usr\/bin\/)?(?:\benv(?:\s+(?:-0|--null|--))?\s*(?:$|[;&|>)`'"])|\bprintenv\b|\bexport\s*$|\bset\s*$|\bdeclare\s+-x|cat\s+\/etc\/environment|cat\s+\/root\/\.bashrc|cat\s+\/proc\/\d+\/environ|cat\s+\/proc\/self\/environ|strings\s+\/proc|xargs[^;&|\n]*\/proc\/[^;&|\n]*environ|less\s+\/proc|head\s+\/proc|tail\s+\/proc|xxd\s+\/proc|od\s+\/proc|base64\s+\/proc|dd\s[^\n]*\/proc|sed\s[^\n]*\/proc\/[^\n]*environ|awk\s[^\n]*\/proc\/[^\n]*environ|cp\s[^\n]*\/proc\/[^\n]*environ)/i;
170
+ const BLOCKED_COMMANDS = /(?:^|[;&|`]\s*|(?:sh|bash|zsh|dash)\s+-c\s+['"]?\s*)(?:\/usr\/bin\/)?(?:\benv(?:\s+(?:-0|--null|--))?\s*(?:$|[;&|>)`'"])|\bprintenv\b|\bexport\s*$|\bset\s*$|\bdeclare\s+-x|cat\s+\/etc\/environment|cat\s+\/root\/\.bashrc|cat\s+\/proc\/\d+\/environ|cat\s+\/proc\/self\/environ|strings\s+\/proc|xargs[^;&|\n]*\/proc\/[^;&|\n]*environ|less\s+\/proc|head\s+\/proc|tail\s+\/proc|xxd\s+\/proc|od\s+\/proc|base64\s+\/proc|dd\s[^\n]*\/proc|sed\s[^\n]*\/proc\/[^\n]*environ|awk\s[^\n]*\/proc\/[^\n]*environ|cp\s[^\n]*\/proc\/[^\n]*environ|(?:cat|less|head|tail|xxd|od|base64|strings|dd|cp|sed|awk)\s[^\n;&|]*\/etc\/mol(?:\/|\b))/i;
168
171
  /** Block shell redirects from /proc environ. */
169
172
  const BLOCKED_PROC_REDIRECT = /(?:<\s*\/proc\/(?:\d+|self)\/environ)/i;
173
+ /** Block shell redirects from the molecule platform secrets dir (`/etc/mol`). */
174
+ const BLOCKED_MOL_REDIRECT = /(?:<\s*\/etc\/mol)/i;
170
175
  /** Interpreter-based env dumping (python, node, ruby, perl). */
171
176
  const BLOCKED_INTERPRETER_ENV = /(?:python[23]?|node|ruby|perl)\s+(?:-e|-c)\s+[^\n]*(?:os\.environ|process\.env|ENV\[|%ENV|ENVIRON)/i;
172
177
  /**
@@ -187,6 +192,8 @@ export function checkBlockedCommand(command) {
187
192
  return `Command blocked: dumping environment variables is not allowed.${envDumpSteer}`;
188
193
  if (BLOCKED_PROC_REDIRECT.test(command))
189
194
  return 'Command blocked: /proc/environ access is not allowed.' + envDumpSteer;
195
+ if (BLOCKED_MOL_REDIRECT.test(command))
196
+ return 'Command blocked: /etc/mol (platform secrets) access is not allowed.' + envDumpSteer;
190
197
  if (BLOCKED_INTERPRETER_ENV.test(command))
191
198
  return `Command blocked: dumping the environment from an interpreter is not allowed.${envDumpSteer}`;
192
199
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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",