@molecule/api-ai-tools 1.0.1 → 1.0.2
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 +48 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +33 -7
- package/dist/utilities.d.ts +37 -0
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +69 -1
- package/package.json +1 -1
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-
|
|
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.
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -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;
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
package/dist/utilities.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -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;
|
|
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;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"}
|
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
|
-
|
|
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,6 +117,42 @@ 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
157
|
/** Commands that dump environment variables — blocked to prevent secret leakage. */
|
|
90
158
|
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;
|