@molecule/api-ai-tools 1.0.1 → 1.0.3

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-08-04T01:47:38.726Z
6
+ Generated: 2026-08-13T21:34:34.535Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -356,6 +356,20 @@ function discoverSkills(backend: ExecutionBackend): Promise<SkillEntry[]>
356
356
 
357
357
  **Returns:** Array of discovered skills with name, description, and path
358
358
 
359
+ #### `isEnvFilePath(path)`
360
+
361
+ Whether a path is an env file, for which {@link redactSecrets}' full env-dump
362
+ treatment is appropriate rather than {@link redactSecretsInCode}. Matches
363
+ `.env`, `.env.<suffix>`, and `<name>.env`.
364
+
365
+ ```typescript
366
+ function isEnvFilePath(path: string): boolean
367
+ ```
368
+
369
+ - `path` — A workspace-relative or absolute file path.
370
+
371
+ **Returns:** `true` when the file is an env file.
372
+
359
373
  #### `isValidGlob(pattern)`
360
374
 
361
375
  Validate that a glob/include pattern is safe (no shell metacharacters that could
@@ -399,6 +413,12 @@ function pathArgError(path: unknown, tool: string): string | null
399
413
 
400
414
  Redact values of common secret/credential patterns in text output.
401
415
 
416
+ ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
417
+ NAME beside the value and therefore cannot tell a credential from ordinary
418
+ code that happens to use a keyword-ish identifier. Use this for `.env` reads
419
+ and command output (where an env dump is the actual threat); use
420
+ {@link redactSecretsInCode} for source-file content.
421
+
402
422
  ```typescript
403
423
  function redactSecrets(s: string): string
404
424
  ```
@@ -407,6 +427,33 @@ function redactSecrets(s: string): string
407
427
 
408
428
  **Returns:** A redacted copy safe to surface to end users or models.
409
429
 
430
+ #### `redactSecretsInCode(s)`
431
+
432
+ CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
433
+ the JSON `KEY: 'value'` passes.
434
+
435
+ Those passes match on the NAME next to a quoted value, so over source code they
436
+ replace legitimate content at enormous scale: `forgotPasswordEndpoint:
437
+ '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
438
+ password" string all became `'[REDACTED]'`. Because the agent writes back the
439
+ content it reads, that token then lands in the user's project — measured at
440
+ 10,952 of 27,919 flagship template files before this split.
441
+
442
+ No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
443
+ test helper is indistinguishable from a real credential by shape. So the
444
+ name-keyed passes simply do not run over code. Credentials in source are still
445
+ caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
446
+ detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
447
+ which is what actually catches a secret sitting under an innocuous name.
448
+
449
+ ```typescript
450
+ function redactSecretsInCode(s: string): string
451
+ ```
452
+
453
+ - `s` — Source-file content or other code-shaped text.
454
+
455
+ **Returns:** A redacted copy that preserves ordinary code verbatim.
456
+
410
457
  #### `resolvePath(path, projectRoot)`
411
458
 
412
459
  Normalize a path to be absolute within the project root.
@@ -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;AAoBnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA0nBxF"}
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,CAopBxF"}
package/dist/tools.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import { TOOL_SCHEMAS } from './schemas.js';
10
- import { checkBlockedCommand, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isValidGlob, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_SIZE, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, pathArgError, redactSecrets, resolvePath, shellQuote, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
10
+ import { checkBlockedCommand, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isEnvFilePath, isValidGlob, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_SIZE, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
11
11
  /**
12
12
  * Build a complete set of AI agent tools bound to an execution backend.
13
13
  *
@@ -71,6 +71,24 @@ export function buildTools(backend, config) {
71
71
  result = redactSecrets(result);
72
72
  return result;
73
73
  }
74
+ /**
75
+ * Sanitize FILE CONTENT for return to the model. Unlike {@link sanitizeOutput},
76
+ * this uses the code-safe redactor for ordinary source files — the name-keyed
77
+ * JSON passes destroy legitimate code, and the agent writes back what it reads,
78
+ * so an over-redacted read corrupts the user's project on the next write. Env
79
+ * files still get the full env-dump treatment, since that is where the shape
80
+ * those passes detect actually indicates a credential.
81
+ *
82
+ * @param s - Raw file contents.
83
+ * @param path - The file's path, used to decide the redaction grade.
84
+ * @returns Content safe to return to the model, with code preserved verbatim.
85
+ */
86
+ function sanitizeFileContent(s, path) {
87
+ let result = stripControlChars(s);
88
+ if (doRedact)
89
+ result = isEnvFilePath(path) ? redactSecrets(result) : redactSecretsInCode(result);
90
+ return result;
91
+ }
74
92
  // ── Diff computation ───────────────────────────────────────────
75
93
  /**
76
94
  * Compute a lightweight diff summary for telemetry and UI badges.
@@ -120,7 +138,7 @@ export function buildTools(backend, config) {
120
138
  return {
121
139
  error: `File too large (${Math.round(content.length / 1024)}KB). Maximum is ${MAX_READ_SIZE / 1024 / 1024}MB.`,
122
140
  };
123
- return { path, content: sanitizeOutput(content) };
141
+ return { path, content: sanitizeFileContent(content, path) };
124
142
  }
125
143
  catch (e) {
126
144
  // Backends (e.g. the docker provider's `cat`) already prefix "Failed to read <path>: ";
@@ -363,7 +381,11 @@ export function buildTools(backend, config) {
363
381
  try {
364
382
  const globArg = include ? `--include=${shellQuote(include)}` : '';
365
383
  const result = await backend.run(`grep -rn ${globArg} ${grepExcludeArgs} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)} 2>/dev/null || true`, { timeout: 10000 });
366
- const output = sanitizeOutput(result.stdout.trim());
384
+ // grep emits `<file>:<line>:<content>`, so redaction runs PER MATCH on the
385
+ // content alone: the file prefix would otherwise hide an env assignment from
386
+ // the line-anchored env pattern, and knowing each match's own path is what
387
+ // lets a hit inside a .env get full treatment while source stays verbatim.
388
+ const output = stripControlChars(result.stdout.trim());
367
389
  if (!output)
368
390
  return { pattern, path, matches: [] };
369
391
  const matches = output
@@ -372,8 +394,8 @@ export function buildTools(backend, config) {
372
394
  .map((line) => {
373
395
  const m = line.match(/^(.+?):(\d+):(.*)$/);
374
396
  return m
375
- ? { file: m[1], line: parseInt(m[2]), content: m[3] }
376
- : { file: '', line: 0, content: line };
397
+ ? { file: m[1], line: parseInt(m[2]), content: sanitizeFileContent(m[3], m[1]) }
398
+ : { file: '', line: 0, content: sanitizeFileContent(line, '') };
377
399
  });
378
400
  return { pattern, path, matches };
379
401
  }
@@ -506,7 +528,7 @@ export function buildTools(backend, config) {
506
528
  return { error: symlinkErr };
507
529
  try {
508
530
  const content = await backend.readFile(path);
509
- return { name, path, content: sanitizeOutput(content) };
531
+ return { name, path, content: sanitizeFileContent(content, path) };
510
532
  }
511
533
  catch (_error) {
512
534
  // File not present at the given path — fall through to the error return below
@@ -523,7 +545,11 @@ export function buildTools(backend, config) {
523
545
  continue;
524
546
  try {
525
547
  const content = await backend.readFile(skillPath);
526
- return { name, path: `${dir}/${name}/SKILL.md`, content: sanitizeOutput(content) };
548
+ return {
549
+ name,
550
+ path: `${dir}/${name}/SKILL.md`,
551
+ content: sanitizeFileContent(content, skillPath),
552
+ };
527
553
  }
528
554
  catch (_error) {
529
555
  // Skill file not present in this directory — try the next candidate
@@ -23,10 +23,47 @@ export declare const stripControlChars: (s: string) => string;
23
23
  /**
24
24
  * Redact values of common secret/credential patterns in text output.
25
25
  *
26
+ * ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
27
+ * NAME beside the value and therefore cannot tell a credential from ordinary
28
+ * code that happens to use a keyword-ish identifier. Use this for `.env` reads
29
+ * and command output (where an env dump is the actual threat); use
30
+ * {@link redactSecretsInCode} for source-file content.
31
+ *
26
32
  * @param s - Log or command output that may contain `.env`-style secrets.
27
33
  * @returns A redacted copy safe to surface to end users or models.
28
34
  */
29
35
  export declare function redactSecrets(s: string): string;
36
+ /**
37
+ * CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
38
+ * the JSON `KEY: 'value'` passes.
39
+ *
40
+ * Those passes match on the NAME next to a quoted value, so over source code they
41
+ * replace legitimate content at enormous scale: `forgotPasswordEndpoint:
42
+ * '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
43
+ * password" string all became `'[REDACTED]'`. Because the agent writes back the
44
+ * content it reads, that token then lands in the user's project — measured at
45
+ * 10,952 of 27,919 flagship template files before this split.
46
+ *
47
+ * No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
48
+ * test helper is indistinguishable from a real credential by shape. So the
49
+ * name-keyed passes simply do not run over code. Credentials in source are still
50
+ * caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
51
+ * detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
52
+ * which is what actually catches a secret sitting under an innocuous name.
53
+ *
54
+ * @param s - Source-file content or other code-shaped text.
55
+ * @returns A redacted copy that preserves ordinary code verbatim.
56
+ */
57
+ export declare function redactSecretsInCode(s: string): string;
58
+ /**
59
+ * Whether a path is an env file, for which {@link redactSecrets}' full env-dump
60
+ * treatment is appropriate rather than {@link redactSecretsInCode}. Matches
61
+ * `.env`, `.env.<suffix>`, and `<name>.env`.
62
+ *
63
+ * @param path - A workspace-relative or absolute file path.
64
+ * @returns `true` when the file is an env file.
65
+ */
66
+ export declare function isEnvFilePath(path: string): boolean;
30
67
  /**
31
68
  * Check if a command is blocked for security reasons. Returns error message or null if allowed.
32
69
  *
@@ -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;AAyDhD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK/C;AAaD;;;;;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;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"}
package/dist/utilities.js CHANGED
@@ -39,7 +39,33 @@ s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
39
39
  // e.g. MAILGUN_APIKEY), and SERVICE_ACCOUNT were the gaps; `_KEY` already covers
40
40
  // OPENAI_KEY / *_ROLE_KEY.
41
41
  const SECRET_KEYWORDS = 'SECRET|PASSWORD|PASSWD|PWD|TOKEN|API_KEY|APIKEY|PRIVATE_KEY|DATABASE_URL|REDIS_URL|AUTH|CREDENTIAL|ACCESS_KEY|SIGNING_KEY|ENCRYPTION_KEY|CONNECTION_STRING|SERVICE_ACCOUNT|DSN|SMTP_PASS|_KEY';
42
- const SECRET_KEY_PATTERN = new RegExp(`^(.*(?:${SECRET_KEYWORDS})[A-Z0-9_]*)=(.+)$`, 'gim');
42
+ /**
43
+ * A keyword-named `NAME=value` anywhere on a line — the ENV-DUMP grade pattern,
44
+ * used by {@link redactSecrets} for command output and `.env` reads. The permissive
45
+ * `.*` prefix is deliberate there: a leaked env var routinely arrives mid-line
46
+ * (`Error: DATABASE_URL=postgres://…` on stderr), and that output is shown to the
47
+ * model but never written back to a file, so over-matching costs nothing.
48
+ *
49
+ * The value must not open with `{` or `<`: an env value never does, but a JSX
50
+ * expression container or element always does. Without that guard this pattern ate
51
+ * `auth={authClient}` down to `auth=[REDACTED]`.
52
+ */
53
+ const SECRET_KEY_PATTERN = new RegExp(`^(.*(?:${SECRET_KEYWORDS})[A-Za-z0-9_]*)=(?![{<])(.+)$`, 'gim');
54
+ /**
55
+ * The same assignment anchored to a REAL env-assignment shape — line start (or
56
+ * `export `), a bare identifier, then `=` with no surrounding spaces. This is the
57
+ * CODE-SAFE form used by {@link redactSecretsInCode}.
58
+ *
59
+ * Anchoring matters because the loose form above matches any line with a keyword
60
+ * anywhere before an `=` and replaces the whole line tail. Over source code that
61
+ * destroyed ordinary lines, and since the executor writes back what it reads, the
62
+ * literal token landed in users' projects.
63
+ */
64
+ const SECRET_ENV_ASSIGNMENT = new RegExp(
65
+ // The leading name run allows EMPTY: a name that IS the keyword (`DATABASE_URL=`,
66
+ // `SECRET=`) has nothing before it, and requiring a character there silently
67
+ // un-masked exactly the plainest env vars.
68
+ `^((?:export[ \\t]+)?[A-Za-z0-9_]*(?:${SECRET_KEYWORDS})[A-Za-z0-9_]*)=(?![{<])(.+)$`, 'gim');
43
69
  /** Catch JSON-formatted env dumps like { KEY: 'value' } from node/python. */
44
70
  const SECRET_JSON_DQ = new RegExp(`(['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)"(?:[^"\\\\]|\\\\.)*"`, 'gi');
45
71
  const SECRET_JSON_SQ = new RegExp(`(['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)'(?:[^'\\\\]|\\\\.)*'`, 'gi');
@@ -76,6 +102,12 @@ const jsonValueReplacer = (quote) => (match, prefix) => {
76
102
  /**
77
103
  * Redact values of common secret/credential patterns in text output.
78
104
  *
105
+ * ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
106
+ * NAME beside the value and therefore cannot tell a credential from ordinary
107
+ * code that happens to use a keyword-ish identifier. Use this for `.env` reads
108
+ * and command output (where an env dump is the actual threat); use
109
+ * {@link redactSecretsInCode} for source-file content.
110
+ *
79
111
  * @param s - Log or command output that may contain `.env`-style secrets.
80
112
  * @returns A redacted copy safe to surface to end users or models.
81
113
  */
@@ -85,9 +117,54 @@ export function redactSecrets(s) {
85
117
  .replace(SECRET_JSON_DQ, jsonValueReplacer('"'))
86
118
  .replace(SECRET_JSON_SQ, jsonValueReplacer("'"));
87
119
  }
120
+ /**
121
+ * CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
122
+ * the JSON `KEY: 'value'` passes.
123
+ *
124
+ * Those passes match on the NAME next to a quoted value, so over source code they
125
+ * replace legitimate content at enormous scale: `forgotPasswordEndpoint:
126
+ * '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
127
+ * password" string all became `'[REDACTED]'`. Because the agent writes back the
128
+ * content it reads, that token then lands in the user's project — measured at
129
+ * 10,952 of 27,919 flagship template files before this split.
130
+ *
131
+ * No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
132
+ * test helper is indistinguishable from a real credential by shape. So the
133
+ * name-keyed passes simply do not run over code. Credentials in source are still
134
+ * caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
135
+ * detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
136
+ * which is what actually catches a secret sitting under an innocuous name.
137
+ *
138
+ * @param s - Source-file content or other code-shaped text.
139
+ * @returns A redacted copy that preserves ordinary code verbatim.
140
+ */
141
+ export function redactSecretsInCode(s) {
142
+ return s.replace(SECRET_ENV_ASSIGNMENT, '$1=[REDACTED]');
143
+ }
144
+ /**
145
+ * Whether a path is an env file, for which {@link redactSecrets}' full env-dump
146
+ * treatment is appropriate rather than {@link redactSecretsInCode}. Matches
147
+ * `.env`, `.env.<suffix>`, and `<name>.env`.
148
+ *
149
+ * @param path - A workspace-relative or absolute file path.
150
+ * @returns `true` when the file is an env file.
151
+ */
152
+ export function isEnvFilePath(path) {
153
+ const base = path.split('/').pop() ?? '';
154
+ return base === '.env' || base.startsWith('.env.') || base.endsWith('.env');
155
+ }
88
156
  // ── Command blocking ──────────────────────────────────────────────────────────
89
- /** Commands that dump environment variables — blocked to prevent secret leakage. */
90
- const BLOCKED_COMMANDS = /(?:^|[;&|`]\s*|(?:sh|bash|zsh|dash)\s+-c\s+['"]?\s*)(?:\/usr\/bin\/)?(?:\benv\b|\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;
157
+ /**
158
+ * Commands that dump environment variables — blocked to prevent secret leakage.
159
+ *
160
+ * `env` counts only when it DUMPS: bare, or with the `-0` / `--null` output
161
+ * flags, followed by the end of the command or a separator / pipe / redirect.
162
+ * `env -u KEY cmd`, `env KEY=value cmd` and `env -i cmd` run a command with a
163
+ * changed environment and print nothing; an executor reaches for them to prove
164
+ * a keyless build still succeeds, and blocking those sent it looking for
165
+ * workarounds (X0 rehearsal 14).
166
+ */
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;
91
168
  /** Block shell redirects from /proc environ. */
92
169
  const BLOCKED_PROC_REDIRECT = /(?:<\s*\/proc\/(?:\d+|self)\/environ)/i;
93
170
  /** Interpreter-based env dumping (python, node, ruby, perl). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
@@ -31,7 +31,7 @@
31
31
  "@molecule/api-ai": "^1.0.1"
32
32
  },
33
33
  "devDependencies": {
34
- "@molecule/api-ai": "1.0.1",
34
+ "@molecule/api-ai": "1.2.0",
35
35
  "@types/node": "26.1.2",
36
36
  "typescript": "6.0.3",
37
37
  "vitest": "4.1.10"