@molecule/api-ai-tools 1.11.0 → 1.12.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 +166 -4
- package/dist/command-position.d.ts +37 -0
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +164 -5
- package/dist/cut-run.d.ts.map +1 -1
- package/dist/cut-run.js +10 -6
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +8 -0
- package/dist/server-command.d.ts +9 -3
- package/dist/server-command.d.ts.map +1 -1
- package/dist/server-command.js +39 -4
- package/dist/tool-catalog.d.ts +32 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +76 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +44 -4
- package/dist/utilities.d.ts +46 -0
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +200 -29
- package/package.json +1 -1
package/dist/utilities.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { posix } from 'path';
|
|
8
|
-
import { commandSegments, locatedCommandSegments, statementArgumentEnd, withoutTimeoutWrapper, } from './command-position.js';
|
|
8
|
+
import { commandSegments, executedCommands, locatedCommandSegments, statementArgumentEnd, withoutTimeoutWrapper, } from './command-position.js';
|
|
9
9
|
import { classifyRunFailure } from './failure-kind.js';
|
|
10
10
|
import { commandRunsNoTests } from './no-test-run.js';
|
|
11
11
|
import { isDiagnosisRun, scopedRerunCommand, suiteCommandTarget, suiteRunInTheWay, withoutHeredocs, withSuiteConfig, } from './suite-overlap.js';
|
|
@@ -163,8 +163,41 @@ export function redactSecrets(s) {
|
|
|
163
163
|
export function redactSecretsInCode(s) {
|
|
164
164
|
return s.replace(SECRET_ENV_ASSIGNMENT, '$1=[REDACTED]');
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The placeholder the output filter substitutes for a hidden value. Copying it into
|
|
168
|
+
* application configuration changes the value rather than recovering it. The write
|
|
169
|
+
* tools check for added occurrences ({@link addedRedactionMask}); intentional literals
|
|
170
|
+
* require the caller's explicit per-call declaration.
|
|
171
|
+
*
|
|
172
|
+
* Only the shapes a value can take count: quoted (`'[REDACTED]'`, `"[REDACTED]"`) or
|
|
173
|
+
* assigned (`=[REDACTED]`). Prose that mentions the token is ordinary text.
|
|
174
|
+
*/
|
|
175
|
+
const REDACTION_MASK = /'\[REDACTED\]'|"\[REDACTED\]"|=[ \t]*\[REDACTED\]/g;
|
|
176
|
+
/**
|
|
177
|
+
* The redaction mask `next` adds over `previous`, or null when it adds none.
|
|
178
|
+
*
|
|
179
|
+
* `previous` is the file's previous content for `write_file`, and the replacement's own
|
|
180
|
+
* `old_string` for `edit_file` — an edit that carries an existing mask through (a file that
|
|
181
|
+
* already had one, read back and rewritten) adds no occurrence. A new occurrence may
|
|
182
|
+
* be copied output or intentional source; the caller declares the latter explicitly.
|
|
183
|
+
*
|
|
184
|
+
* @param next - The content about to be written (or the replacement's `new_string`).
|
|
185
|
+
* @param previous - The content it replaces, already on disk (or the `old_string`).
|
|
186
|
+
* @returns The added mask as written (`'[REDACTED]'`, `"[REDACTED]"`, `=[REDACTED]`), or null.
|
|
187
|
+
*/
|
|
188
|
+
export function addedRedactionMask(next, previous) {
|
|
189
|
+
const before = previous.match(REDACTION_MASK)?.length ?? 0;
|
|
190
|
+
const after = next.match(REDACTION_MASK) ?? [];
|
|
191
|
+
if (after.length <= before)
|
|
192
|
+
return null;
|
|
193
|
+
return after[before] ?? after[0] ?? '[REDACTED]';
|
|
194
|
+
}
|
|
166
195
|
/** Source-file extensions a printing command's output is read as code for. */
|
|
167
196
|
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|css|scss|html|vue|svelte|md|sql|hurl)$/;
|
|
197
|
+
/** Programs whose output is file NAMES or counts, never another file's content. */
|
|
198
|
+
const INSPECTION_PROGRAMS = new Set(['find', 'ls', 'wc']);
|
|
199
|
+
/** `find` options that run a program or write a file — not read-only inspection. */
|
|
200
|
+
const FIND_NOT_INSPECTION = /^-(?:exec|execdir|ok|okdir|delete|fls|fprint|fprint0|fprintf)/;
|
|
168
201
|
/**
|
|
169
202
|
* A literal argument, not a shell expansion. The shared parser owns statement boundaries;
|
|
170
203
|
* this deliberately small word grammar only decides whether an argument can read other data.
|
|
@@ -253,9 +286,46 @@ function sourcePrintFiles(program, args) {
|
|
|
253
286
|
* @returns True when the command prints source files, with only literal headings and filters.
|
|
254
287
|
*/
|
|
255
288
|
export function printsOnlySource(command) {
|
|
289
|
+
return printsOnly(command, false);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Whether a shell command only INSPECTS the workspace WITHOUT printing a file's contents:
|
|
293
|
+
* it prints source files (a pure literal read, {@link printsOnlySource}), lists names
|
|
294
|
+
* (`ls`, `find`), or counts lines (`wc`) — joined by `;`, `&&` or `|`, with literal
|
|
295
|
+
* `echo`/`printf` headings and `cd`.
|
|
296
|
+
*
|
|
297
|
+
* Its output is code or names, not an env dump, so it gets the code-safe redaction
|
|
298
|
+
* ({@link redactSecretsInCode}) instead of the name-keyed env-dump passes. Without this,
|
|
299
|
+
* ordinary code values such as `tokenKey: 'jwtToken'` in a mixed source read and
|
|
300
|
+
* `ls | head` command would be masked by the environment-dump rules.
|
|
301
|
+
*
|
|
302
|
+
* A RECURSIVE SEARCH IS NOT INSPECTION (`grep -rn x .`, `rg x src/`): its output is the
|
|
303
|
+
* content of whichever files matched, including `.env` files and JSON secret dumps, so it
|
|
304
|
+
* keeps the full grade. A path with no extension is not evidence that it is a directory —
|
|
305
|
+
* `/dev/stdin` and an ordinary `secrets` file read the same way.
|
|
306
|
+
*
|
|
307
|
+
* Env dumps keep the full grade: `.env` anywhere in the command, `env`/`printenv`, a
|
|
308
|
+
* non-literal or redirecting form, and an interpreter one-liner (`node -e`, `python -c`)
|
|
309
|
+
* all return false.
|
|
310
|
+
*
|
|
311
|
+
* @param command - The shell command.
|
|
312
|
+
* @returns True when the command only reads or lists workspace content.
|
|
313
|
+
*/
|
|
314
|
+
export function isReadOnlyInspection(command) {
|
|
315
|
+
return printsOnly(command, true);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The one segment grammar both grades share: literal words only, no wrapper or group, no
|
|
319
|
+
* backgrounding, and every byte between segments a separator.
|
|
320
|
+
*
|
|
321
|
+
* @param command - The shell command.
|
|
322
|
+
* @param inspection - Also accept name/count listing (`ls`, `find`, `wc`).
|
|
323
|
+
* @returns True when every segment is allowed and at least one prints something.
|
|
324
|
+
*/
|
|
325
|
+
function printsOnly(command, inspection) {
|
|
256
326
|
if (/\.env\b/.test(command))
|
|
257
327
|
return false;
|
|
258
|
-
let
|
|
328
|
+
let prints = false;
|
|
259
329
|
let previousEnd = 0;
|
|
260
330
|
for (const located of locatedCommandSegments(command)) {
|
|
261
331
|
const { segment, start, end, programOffset, separator, terminator } = located;
|
|
@@ -302,6 +372,13 @@ export function printsOnlySource(command) {
|
|
|
302
372
|
return false;
|
|
303
373
|
}
|
|
304
374
|
}
|
|
375
|
+
else if (inspection && INSPECTION_PROGRAMS.has(program) && separator !== '|') {
|
|
376
|
+
// Names and counts only — nothing here can be another file's content. `find` is the
|
|
377
|
+
// exception that can run a program or write a file.
|
|
378
|
+
if (program === 'find' && args.some((arg) => FIND_NOT_INSPECTION.test(arg)))
|
|
379
|
+
return false;
|
|
380
|
+
prints = true;
|
|
381
|
+
}
|
|
305
382
|
else {
|
|
306
383
|
const filter = separator === '|';
|
|
307
384
|
if (!(filter ? /^(?:head|tail|sort|uniq|wc|cut|nl|grep)$/ : /^(?:cat|head|tail|nl|sed|grep|rg)$/).test(program)) {
|
|
@@ -319,11 +396,11 @@ export function printsOnlySource(command) {
|
|
|
319
396
|
!files.every((file) => !file.startsWith('-') && SOURCE_FILE_RE.test(file))) {
|
|
320
397
|
return false;
|
|
321
398
|
}
|
|
322
|
-
|
|
399
|
+
prints = true;
|
|
323
400
|
}
|
|
324
401
|
}
|
|
325
402
|
}
|
|
326
|
-
return
|
|
403
|
+
return prints && /^[\s;]*$/.test(command.slice(previousEnd));
|
|
327
404
|
}
|
|
328
405
|
/**
|
|
329
406
|
* Whether a path is an env file, for which {@link redactSecrets}' full env-dump
|
|
@@ -1301,30 +1378,127 @@ export function suiteRunPriorityLaunch(command, launch) {
|
|
|
1301
1378
|
};
|
|
1302
1379
|
}
|
|
1303
1380
|
// ── Command blocking ──────────────────────────────────────────────────────────
|
|
1381
|
+
/** A word with one layer of matching shell quotes removed (`"/proc/self/environ"` → `/proc/self/environ`). */
|
|
1382
|
+
function shellWord(word) {
|
|
1383
|
+
return word.replace(/^(['"])(.*)\1$/, '$2');
|
|
1384
|
+
}
|
|
1385
|
+
/** `/proc/<pid>/environ` and `/proc/self/environ` — a process's environment, as the kernel exposes it. */
|
|
1386
|
+
const PROC_ENVIRON = /^\/proc\/(?:\d+|self)\/environ(?:\/|$)/;
|
|
1387
|
+
/** Anything under `/proc`: a process's environment is one of the files there. */
|
|
1388
|
+
const PROC_PATH = /^\/proc(?:\/|$)/;
|
|
1389
|
+
/** The molecule platform's secrets dir — `/etc/mol/env` carries the vault + platform secrets. */
|
|
1390
|
+
const MOL_SECRETS = /^\/etc\/mol(?:\/|$)/;
|
|
1391
|
+
/**
|
|
1392
|
+
* Whether one executed command reads a file that would print a process's environment or the
|
|
1393
|
+
* platform's secrets.
|
|
1394
|
+
*
|
|
1395
|
+
* This is asked of the commands the shell RUNS ({@link executedCommands}), never of the command
|
|
1396
|
+
* text: a file path quoted inside a grep pattern (`grep -E "ENV|/etc/mol" src`) is data a
|
|
1397
|
+
* comparison reads, not a file this command opens. The shapes below are the ones that print:
|
|
1398
|
+
* `cat` and its siblings of a `/proc/<pid>/environ`, a redirect's path handed to a reader
|
|
1399
|
+
* (`xxd < /proc/self/environ`), and reads of `/etc/mol`, which is an env-dump by another door.
|
|
1400
|
+
*
|
|
1401
|
+
* @param program - The command's program, directory removed.
|
|
1402
|
+
* @param args - Its arguments, as written.
|
|
1403
|
+
* @returns True when the command reads one of those files.
|
|
1404
|
+
*/
|
|
1405
|
+
function readsSecretFile(program, args) {
|
|
1406
|
+
const words = args.map(shellWord);
|
|
1407
|
+
const first = words[0] ?? '';
|
|
1408
|
+
switch (program) {
|
|
1409
|
+
case 'cat':
|
|
1410
|
+
return words.some((word) => /^\/etc\/environment(?:\/|\.|$)/.test(word) ||
|
|
1411
|
+
word.startsWith('/root/.bashrc') ||
|
|
1412
|
+
PROC_ENVIRON.test(word) ||
|
|
1413
|
+
MOL_SECRETS.test(word));
|
|
1414
|
+
case 'sed':
|
|
1415
|
+
case 'awk':
|
|
1416
|
+
case 'cp':
|
|
1417
|
+
return words.some((word) => PROC_ENVIRON.test(word) || MOL_SECRETS.test(word));
|
|
1418
|
+
case 'xargs':
|
|
1419
|
+
return words.some((word) => PROC_ENVIRON.test(word));
|
|
1420
|
+
case 'dd':
|
|
1421
|
+
// `dd if=/proc/1/environ` carries the path inside its own argument.
|
|
1422
|
+
return words.some((word) => /\/proc(?:\/|$)/.test(word) || MOL_SECRETS.test(word));
|
|
1423
|
+
case 'base64':
|
|
1424
|
+
case 'head':
|
|
1425
|
+
case 'less':
|
|
1426
|
+
case 'od':
|
|
1427
|
+
case 'strings':
|
|
1428
|
+
case 'tail':
|
|
1429
|
+
case 'xxd':
|
|
1430
|
+
return PROC_PATH.test(first) || words.some((word) => MOL_SECRETS.test(word));
|
|
1431
|
+
default:
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1304
1435
|
/**
|
|
1305
|
-
*
|
|
1306
|
-
* Also covers reads of `/etc/mol/…` (the molecule platform's secrets dir —
|
|
1307
|
-
* `/etc/mol/env` carries the vault + platform secrets inside a sandbox),
|
|
1308
|
-
* which is an env-dump by another door.
|
|
1436
|
+
* Whether one executed command prints the environment instead of running something in it.
|
|
1309
1437
|
*
|
|
1310
|
-
* `env` counts only when it DUMPS:
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1438
|
+
* `env` counts only when it DUMPS: {@link executedCommands} reports it as the program only when
|
|
1439
|
+
* nothing follows it, because `env -u KEY cmd`, `env KEY=value cmd` and `env -i cmd` run `cmd`
|
|
1440
|
+
* with a changed environment and print nothing — an executor reaches for them to prove a keyless
|
|
1441
|
+
* build still succeeds, and blocking those sent it looking for workarounds (X0 rehearsal 14).
|
|
1442
|
+
* `export` / `set` / `declare` print every variable they carry when given nothing to set.
|
|
1443
|
+
*
|
|
1444
|
+
* @param program - The command's program, directory removed.
|
|
1445
|
+
* @param args - Its arguments, as written.
|
|
1446
|
+
* @returns True when the command prints the environment.
|
|
1316
1447
|
*/
|
|
1317
|
-
|
|
1318
|
-
const
|
|
1448
|
+
function dumpsEnvironment(program, args) {
|
|
1449
|
+
const words = args.map(shellWord);
|
|
1450
|
+
if (program === 'env')
|
|
1451
|
+
return true;
|
|
1452
|
+
if (program === 'printenv')
|
|
1453
|
+
return true;
|
|
1454
|
+
if (program === 'export')
|
|
1455
|
+
return words.every((word) => word === '-p' || word === '--print');
|
|
1456
|
+
if (program === 'set')
|
|
1457
|
+
return words.length === 0;
|
|
1458
|
+
if (program === 'declare')
|
|
1459
|
+
return (words.length === 0 ||
|
|
1460
|
+
(words.every((word) => word.startsWith('-')) &&
|
|
1461
|
+
words.some((word) => /^-[A-Za-z]*[px]/.test(word))));
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
/** Interpreters an env-dumping one-liner is written in (python, node, ruby, perl). */
|
|
1465
|
+
const INTERPRETER_PROGRAMS = new Set([
|
|
1466
|
+
'node',
|
|
1467
|
+
'perl',
|
|
1468
|
+
'python',
|
|
1469
|
+
'python2',
|
|
1470
|
+
'python3',
|
|
1471
|
+
'ruby',
|
|
1472
|
+
]);
|
|
1473
|
+
/** An inline program (`-e` / `-c`) that reads the environment and prints it. */
|
|
1474
|
+
const INTERPRETER_ENV_DUMP = /(?:^|\s)(?:-e|-c)\s[^\n]*(?:os\.environ|process\.env|ENV\[|%ENV|ENVIRON)/i;
|
|
1475
|
+
/**
|
|
1476
|
+
* Whether one executed command dumps the environment from an inline interpreter program
|
|
1477
|
+
* (`node -e 'console.log(process.env)'`).
|
|
1478
|
+
*
|
|
1479
|
+
* @param program - The command's program, directory removed.
|
|
1480
|
+
* @param text - The program and its arguments as one line.
|
|
1481
|
+
* @returns True when the inline program reads the environment.
|
|
1482
|
+
*/
|
|
1483
|
+
function dumpsEnvironmentFromInterpreter(program, text) {
|
|
1484
|
+
return INTERPRETER_PROGRAMS.has(program) && INTERPRETER_ENV_DUMP.test(text);
|
|
1485
|
+
}
|
|
1319
1486
|
/** Block shell redirects from /proc environ. */
|
|
1320
1487
|
const BLOCKED_PROC_REDIRECT = /(?:<\s*\/proc\/(?:\d+|self)\/environ)/i;
|
|
1321
1488
|
/** Block shell redirects from the molecule platform secrets dir (`/etc/mol`). */
|
|
1322
1489
|
const BLOCKED_MOL_REDIRECT = /(?:<\s*\/etc\/mol)/i;
|
|
1323
|
-
/** Interpreter-based env dumping (python, node, ruby, perl). */
|
|
1324
|
-
const BLOCKED_INTERPRETER_ENV = /(?:python[23]?|node|ruby|perl)\s+(?:-e|-c)\s+[^\n]*(?:os\.environ|process\.env|ENV\[|%ENV|ENVIRON)/i;
|
|
1325
1490
|
/**
|
|
1326
1491
|
* Check if a command is blocked for security reasons. Returns error message or null if allowed.
|
|
1327
1492
|
*
|
|
1493
|
+
* The first check reads the commands the shell actually RUNS — the statements, a `sh -c`
|
|
1494
|
+
* string's body, and every command substitution ({@link executedCommands}) — so a `|` inside a
|
|
1495
|
+
* quoted grep alternation is data, not a pipeline: `grep -E "USER CALLS|ENV|BUTTON|passed|failed"
|
|
1496
|
+
* app.log` prints log lines, and the `ENV` in its pattern started nothing (X0
|
|
1497
|
+
* xr-molecule-flash-m331 blocked exactly that call as an environment dump, twice). What the
|
|
1498
|
+
* shell DOES run stays visible through the same walk: `ls | env`, `bash -c 'env'`,
|
|
1499
|
+
* `echo "$(printenv)"` and a nested `sh -c 'x; env'` are all commands, and so is `env` itself
|
|
1500
|
+
* when nothing follows it.
|
|
1501
|
+
*
|
|
1328
1502
|
* @param command - Shell command string proposed for execution.
|
|
1329
1503
|
* @returns A human-readable block reason, or `null` when the command is allowed.
|
|
1330
1504
|
*/
|
|
@@ -1341,13 +1515,14 @@ export function checkBlockedCommand(command) {
|
|
|
1341
1515
|
"check the project's provisioned env file to see WHICH services exist. Printing them only " +
|
|
1342
1516
|
'leaks secrets into the transcript. To check whether ONE variable is set without printing ' +
|
|
1343
1517
|
'it: `test -n "$NAME" && echo set || echo missing`.';
|
|
1344
|
-
|
|
1518
|
+
const statements = executedCommands(command);
|
|
1519
|
+
if (statements.some((s) => dumpsEnvironment(s.program, s.args) || readsSecretFile(s.program, s.args)))
|
|
1345
1520
|
return `Command blocked: dumping environment variables is not allowed.${envDumpSteer}`;
|
|
1346
1521
|
if (BLOCKED_PROC_REDIRECT.test(command))
|
|
1347
1522
|
return 'Command blocked: /proc/environ access is not allowed.' + envDumpSteer;
|
|
1348
1523
|
if (BLOCKED_MOL_REDIRECT.test(command))
|
|
1349
1524
|
return 'Command blocked: /etc/mol (platform secrets) access is not allowed.' + envDumpSteer;
|
|
1350
|
-
if (
|
|
1525
|
+
if (statements.some((s) => dumpsEnvironmentFromInterpreter(s.program, s.text)))
|
|
1351
1526
|
return `Command blocked: dumping the environment from an interpreter is not allowed.${envDumpSteer}`;
|
|
1352
1527
|
return null;
|
|
1353
1528
|
}
|
|
@@ -3882,11 +4057,8 @@ export function repeatingFailureNote(found, o) {
|
|
|
3882
4057
|
`repeats this one cause. Stop it and fix that cause first: \`exec_command: ${o.stop}\`, ` +
|
|
3883
4058
|
`then run the whole suite again. It is still going (${where}).${reproduce}`);
|
|
3884
4059
|
}
|
|
3885
|
-
//
|
|
3886
|
-
//
|
|
3887
|
-
// not. What it does NOT carry is the "Stop it if every test fails this way" clause — that one
|
|
3888
|
-
// belongs to a PROVEN cause the run is still walking (the sentence below), never to a lead:
|
|
3889
|
-
// xr-molecule-flash-m99's executor read it on a shared duration and voided the run.
|
|
4060
|
+
// The same predicate controls both an early return and stop advice. Without
|
|
4061
|
+
// that verdict, neither a repeated error nor a shared duration asks for a stop.
|
|
3890
4062
|
const keepGoing = `Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
|
|
3891
4063
|
`this one, and a run stopped at its first repeating cause costs a second whole run to find ` +
|
|
3892
4064
|
`them. It is still going (${where}).`;
|
|
@@ -3894,10 +4066,9 @@ export function repeatingFailureNote(found, o) {
|
|
|
3894
4066
|
return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
|
|
3895
4067
|
`so one cause is repeating:\n${rows}${more}\n` +
|
|
3896
4068
|
(o.editsInvalidateRun
|
|
3897
|
-
? `Work out the fix now: apply it when the run ends, not while it walks
|
|
3898
|
-
: `Start fixing it now, while the run keeps walking
|
|
3899
|
-
|
|
3900
|
-
`out on every test of every file so far).${reproduce} ${keepGoing}${thenRun}`);
|
|
4069
|
+
? `Work out the fix now: apply it when the run ends, not while it walks.`
|
|
4070
|
+
: `Start fixing it now, while the run keeps walking.`) +
|
|
4071
|
+
`${reproduce} ${keepGoing}${thenRun}`);
|
|
3901
4072
|
return (`${found.count} different tests in this run each failed after ~${found.seconds}s — the same ` +
|
|
3902
4073
|
`duration to within half a second. A shared duration is a lead, not a cause: a suite whose ` +
|
|
3903
4074
|
`config sets a timeout spends that timeout on every failure that waits for anything, so ` +
|