@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 +10 -1
- package/dist/backends/local.d.ts.map +1 -1
- package/dist/backends/local.js +3 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +136 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +8 -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-09-
|
|
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,
|
|
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"}
|
package/dist/backends/local.js
CHANGED
|
@@ -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
|
},
|
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;AAsBnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,
|
|
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. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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/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;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;
|
|
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;
|