@sema-agent/core 7.17.6 → 7.17.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.17.7 — 2026-09-15
4
+
5
+ ### Narrowing — an INTERPRETER whose program is argument text: the argument-as-program family gains its fourth member (#791; @server @cli @test)
6
+ - **Before.** Three shapes were modelled: shell re-entry (`sh -c …` — the argument IS the command line), argv re-entry (`xargs cat` — the operands arrive on a stream) and a verb's script slot (`sed -n '1r <file>'`). The fourth was not. An interpreter's program can arrive as an option's value (`perl -e '…'`, `python3 -c '…'`, `node --eval '…'`), as the first operand (the awk family, whose program is mandatory unless `-f` names a file), or on STANDARD INPUT when the command line names none — and in each case the reads it performs are decided by text nobody parsed. An unparenthesized program is ONE folded token that is not path-shaped, so it fell through the ordinary operand scan and any absolute path inside it was never a candidate. Measured on the real load path under the default doctrine, each at zero asks and EXECUTED: `perl -e '…'`, `perl -pe '…' <file>`, `perl -ne '…' <file>`, `ruby -e '…'`, `php -r '…'`, `node -p '…'`, `node -pe '…'`, `<program> | tclsh`, `awk '{print}' <file>`, `awk 'NR==1' <file>`, `gawk '{print}' <file>`. Spellings carrying `(`/`)`/`$` (`python3 -c 'print(open("x").read())'`, `node -e '…readFileSync…'`) were already refused by the generic punctuation gate, which is why the defect looked narrower than it was.
7
+ - **After (one table, one mint point, the family's existing sentence).** A row per interpreter declares how it receives its program: the options whose value is program TEXT (`-e`, `-c`, `--eval`, `--run`, single-dash `-cmd`), the short letters that introduce it inside a bundle (`-pe`, `-lne`, `-uc`, `-0777e`), the options whose value NAMES the program (a script file, a module), the letters whose value is the rest of the token (so a bundle scan stops there rather than reading an `-e` out of a directory name), the letters that make THAT interpreter print and exit, which OPERAND holds the program and whether it is text or a file, and whether an invocation naming no program reads one from standard input. An invocation carrying program text is refused WHOLE with the family's template (`command "X" runs its argument as an interpreted program, which this walk does not judge — not auto-allowed`); one that names no program at all is refused with the second half of the same answer (`… reads the program it runs from its standard input …`). Twenty-six rows today — perl, ruby, python, node/nodejs, php, lua/luajit, tclsh, wish, expect, awk/gawk/mawk/nawk, jq, bc, dc, sqlite3, psql, osascript, Rscript, julia, ghci, groovy, bun — and a versioned spelling (`python3`, `python3.11`, `perl5.36`, `tclsh8.6`) is the same row.
8
+ - **Every doubt resolves in the REFUSING direction, and each way it can arise is declared.** A letter this file has no row for does NOT end a bundle scan (`perl -fe '…'` carries an `-e`), a DIGIT never ends one either (it is the numeric payload of the letter in front of it — `perl -0777e '…'`, `ruby -0e '…'`), a letter whose value can only be ATTACHED takes nothing from the next token (`perl -C -e '…'` runs the `-e`; reading `-e` as `-C`'s value swallowed it), a letter whose value IS the next token consumes it so it cannot be mistaken for an operand or for `--` (`perl -I -- -e '…'`; `python3 -W ignore` names no program and reads one from stdin), a lone `-` is the stdin marker wherever it stands including past `--` — except on a row whose operand is program text, where it is a DATA stream (`jq -f <filter> -`) — and a program-file option whose VALUE is `-` (`awk -f -`) is stdin rather than a file. An INITIALIZATION file is supplemental and does not name the program (`sqlite3 -init <file> <db> '<sql>'` still carries SQL, and without it still reads SQL from its input), an INTERACTIVE switch outranks both a banner and a named script (`lua -v -i`, `lua -i <script>`), and an interpreter whose operands are MODULES it loads never has one name the program (`ghci <module>` still reads commands from its input, and `ghci <module> -e '…'` still carries the `-e`).
9
+ - **Four rows the intake sweep filled in, each measured as a false refusal first.** A long option whose VALUE the row declares no longer turns the script behind it into a word of unknown arity (`node --import tsx app.ts`, `--loader`, `--experimental-loader`, `--require`, `--env-file`); a MODE that names what runs takes no value and reads no program from its input (`node --test`, and `--check` as a plain switch); a LINT switch leaves the file operand naming the program (`php -l <file>`); and the C shell family joins the shell re-entry spellings, since `csh -c '…'` and `tcsh -c '…'` run their argument exactly as `sh -c` does.
10
+ - **Where the interpreter stops reading its OWN options is declared too, in both directions.** Past the word that NAMES the program every remaining word belongs to that program — `node app.js -p 3000`, `python3 script.py -c config.json` and `python3 -m unittest -c` keep their old verdict — but only while every option behind it was one the row EXPLAINS. After an option of unknown arity the following word may be that option's value rather than the script, so the search for program text runs to the end instead: `ruby --encoding UTF-8 -e '…'` is the mandated ask it should be, while `node --inspect app.js` and `python3 -u script.py -c x` are untouched.
11
+ - **What is judged is PRESENCE, never CONTENT.** A quoted program is one word to this file's lexer and its contents are never parsed, so the only answerable question is "is there a program this walk did not read". `node -e 'console.log(1)'` therefore refuses with the rest: telling it apart from a program that opens a file needs a parser per language, which is the thing a lexical floor must not grow. This is the same doctrine that keeps the sed recognizer on sed's own closed grammar rather than on a character blacklist, and it is pinned in both directions.
12
+ - **Not this member, and pinned as such.** A program named as a FILE operand (`perl x.pl`, `python3 app.py`, `node app.js`, `ruby app.rb`, `tclsh script.tcl`, `awk -f prog.awk <file>`, `php -f page.php`) is a path the ordinary operand walk already judges and is untouched. So is a module (`python3 -m http.server`), an invocation that prints a banner and exits (`node --version`, `node -v`, `ruby -v`, `perl -V`, `python3 --version`, `php -v`, `lua -v`, `--help`), a fused option VALUE that merely contains a text letter (`perl -Ivendor script.pl`, `perl -i.bak script.pl`), and a text letter standing past POSIX end-of-options, where it is a FILE NAME (`perl -- -e`, `python3 -- -c`). A bundle is read left to right and STOPS at the first letter no row explains, because an option this file has no row for may consume the rest of the token.
13
+ - **Behaviour, for consumers, and the headless lane first.** Under BOTH doctrines an interpreter carrying an inline program now asks ONCE, mandated — a stored allow rule naming the whole command line is disclosed as shadowed and does not clear it. **Under the default doctrine these ran with ZERO asks before, so on a headless or auto-mode deployment — no approver wired — the card is a REFUSAL, not a prompt.** Under `"classify"` the same invocations already asked (no interpreter is on any reader table); what changes there is that the plain ask becomes a mandated one. The affected spellings are every one listed above plus the bare interpreter (`perl`, `ruby`, `python3`, `node`, `php`, `lua`, `tclsh`, `wish`, `expect`), an explicit stdin operand (`python3 -`), and an option-only invocation that is not a banner (`python3 -u`, `perl -w`).
14
+ - **Stated over-fire, measured rather than described — the awk family is where it lands.** Over a twenty-line corpus of everyday awk one-liners, SEVEN move from zero asks to one mandated ask: `awk '{print}' <file>`, `awk -F, '{print}' <file>`, `awk 'NR==1' <file>`, `awk 'NF' <file>`, `awk 'END{print NR}' <file>`, `gawk '{print}' <file>`, `mawk '{print}' <file>`. The other thirteen — every spelling whose program text carries `$`, `/` or a pipe, which is the overwhelming majority of real one-liners — were ALREADY mandated on both doctrines and are unchanged by this arm. awk is in the family because its program can read (`getline < "<file>"`), write (`print > "<file>"`) and spawn (`system(…)`), which is the hazard the family names; `awk -f prog.awk <file>` — the same work with the program in a file — is the unchanged spelling. Beyond awk the over-fire is the harmless inline program (`node -e 'console.log(1)'`), one approval each.
15
+ - **A third pass, the same root, and the point where it stops — stated rather than buried.** Three adversarial passes each found more spellings of one question ("where does this interpreter stop reading its own options, and what names its program"), and each was answered by declaring more per-row data: an attached-only value letter, an interactive switch, a supplemental initialization file, a module selector that ends option processing, a banner that owns its payload, a declared plain long option. The third pass answered the structural question directly, and the answer is a boundary rather than a bug: past the word that NAMES the program the remaining words are that program's own and are exempt — and a named module or script can define program-text slots of its own (`python3 -m timeit 0` compiles and runs `0`). No amount of per-interpreter option data reaches that class. Closing it means refusing every unmodelled argument tail, which would refuse `node app.js --port 3000` and `python3 script.py --flag` along with it — an adjudication about everyday interpreter invocations, not a fix, and it is left as one.
16
+ - **A second everyday line leaves the zero-ask floor with the jq row, and it is named rather than implied.** `jq -r .name pkg.json` and `jq . f` are mandated asks now: a jq filter is program text like any other, which is the family's rule read consistently. Measured and stated honestly in the other direction: unlike the rest of the table, jq's filter language has no general file-open and no command-execution builtin, so this row is doctrine consistency rather than a closed hazard. `jq -f <filter file> <data>` is the unchanged spelling.
17
+ - **Scope, honestly.** The table names the interpreters this file has a row for; one it does not — `deno` (its inline program is a SUBCOMMAND, `deno eval`, a shape this option/operand model does not express), `mono`, `dotnet`, `scala`, and anything else — is judged as any other unlisted program, exactly as before. Where a row's option grammar is incomplete the cost is one ask, not a silence, because an unknown option never ends a scan: `sqlite3 -init <file> <db>` counts the option's value as an operand and therefore refuses, and `perl -Mstrict <script>` refuses because perl's module argument can carry code. `psql`'s operands are connection arguments, so it is the one row where no operand ever names the program.
18
+
19
+ ### Narrowing — ONE launcher option grammar, read by every reader that has to find the program word (#792; @server @cli @test)
20
+ - **Before.** `time -p xargs cat <<< <path>` was vouched under the default doctrine while `xargs cat <<< <path>` was mandated. The launcher option grammar — the one table that declares which tokens belong to a control word rather than naming the program behind it — was read by the boundary walk's control-word strip and by the single-command boundary face, but NOT by the refused-command whitelist's own head scan. There `-p` was read as the program word, the segment was exempted as an ordinary unlisted program, and the argv re-entry standing behind it was never asked about. The same silence covered `time -f %e xargs cat <<< <path>` and `time -o <file> xargs cat <<< <path>`.
21
+ - **After (a reuse point, not a second table).** The whitelist's head scan reads the same declared grammar, so all three program-word readers give one answer, and beneath it a floor the walk has always had: an option-shaped word is never read as a program name. A spelling the grammar deliberately does not carry — a FUSED long option (`time --format=%e …`), or an option of a launcher with no row at all (`nice -n 5 …`, `env -u X …`) — therefore leaves which program runs unreadable and is refused, which is the answer the boundary walk already gave for that condition. The readers that resolve a launcher CHAIN (`env time -p …`) deliberately still refuse rather than resolve: resolving there would turn a whole refusal into an ordinary walked segment, which is the widening direction, while every reader that does read the table only ever finds MORE program to judge.
22
+ - **Behaviour, for consumers.** `time -p xargs cat <<< <path>` and its valued spellings are now one mandated ask on both doctrines (a refusal on a headless deployment), as the bare `xargs` spelling already was; `time -p cd <dir>` is recognized as the directory change it is; and `time --format=%e cat <file>` — a spelling the boundary walk already called unreadable — now gets that answer from the whitelist too instead of being quietly exempted. Unchanged and pinned: `time -p ls`, `time -p cat <file>`, `time -f %e cat <file>`, `time -p echo hi`, `time -o <file> ls`.
23
+ ### Narrowing — an APPLET DISPATCHER is a launcher, and the shell re-entry member is spelled by the shell (#791, the complement pass; @server @cli @test)
24
+ - **Before.** A multi-call binary runs the applet named in its first argument, and it was on no launcher table — so nobody unwrapped it, and the program text behind it folds into words that all resolve in root. Measured at ZERO asks under the default doctrine: `busybox sh -c "cat /etc/passwd"` and `busybox ash -c "…"`, while the bare `sh -c "cat /etc/passwd"` was mandated; `busybox awk '{print}' <file>` slipped the interpreter table the same way. Separately, three spellings of the shell itself were outside the shell re-entry pattern and vouched on their own: `ash -c "…"`, `mksh -c "…"`, `yash -c "…"`.
25
+ - **After.** The dispatcher joins the launcher table — it is already on the rule lane's own curated interpreter table, which is the list this file's launcher set is drawn from, so the pinned subset relation is unchanged — and the shell re-entry pattern spells the Almquist family and its descendants. `busybox cat <file>`, `busybox ls`, `busybox ls -la` and a bare `busybox` keep the verdict they had, and `busybox cat <outside>` is gated exactly as before. Stated over-fire: `busybox --help` puts an option where the applet name would be, which leaves what runs unreadable and refuses — one ask.
26
+ - Pins: `test/backlog791-interpreter-inline-program.test.ts` (174 cases — the real load path on both doctrines with a planted marker and its reverse control on all four delivery shapes, the mandated-ask lane under an exact covering rule, fifty-five inline-program spellings and twenty-five stdin ones pinned on the arm's OWN sentence, forty-one negative controls plus a twenty-nine-line everyday corpus, reachability at all three faces that judge a program's own words with the redirection words dropped from the argument list, the interpreter behind a control word / a launcher / a path prefix / an applet dispatcher, and the stated over-fire pinned rather than described; SIXTEEN mutation directions measured in the header, no two of which red the same set) and four new cases in `test/backlog766-argv-reentry.test.ts` (the control word's own option against the chain, and the everyday cells the grammar keeps).
27
+
3
28
  ## 7.17.6 — 2026-09-14
4
29
 
5
30
  ### Narrowing — a verb whose ARGUMENT IS A PROGRAM: the sed script slot is read for the files it names and the commands it runs (#769; @server @cli @test)
@@ -253,13 +253,19 @@ function isBareControlWord(toks, i, words) {
253
253
  }
254
254
  function tokensMoveBase(toks, allow) {
255
255
  let from = 0;
256
- while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES))
256
+ while (from < toks.folded.length && isBareControlWord(toks, from, SHELL_CONTROL_PREFIXES)) {
257
+ const controlWord = programBasename(toks.folded[from] ?? "");
257
258
  from++;
259
+ for (let span = launcherOptionSpan(controlWord, toks.folded, from); span > 0; span = launcherOptionSpan(controlWord, toks.folded, from))
260
+ from += span;
261
+ }
258
262
  const head = toks.folded[from];
259
263
  if (head === undefined)
260
264
  return false;
261
265
  if (toks.raw[from] !== head)
262
266
  return true;
267
+ if (head.startsWith("-") && head !== "-")
268
+ return true;
263
269
  if (head === "cd")
264
270
  return true;
265
271
  if (!COMMAND_LAUNCHERS.has(head))
@@ -267,7 +273,7 @@ function tokensMoveBase(toks, allow) {
267
273
  const unwrapped = unwrapLauncher(toks.folded.slice(from), allow);
268
274
  return unwrapped.kind === "refused" || (unwrapped.kind === "reader" && unwrapped.head === "cd");
269
275
  }
270
- const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi)?sh|eval|source|\.|parallel)$/;
276
+ const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
271
277
  const ARGV_REENTRY_PROGRAM_RE = /^xargs$/;
272
278
  export function refusedCommandMayRead(command, allow, boundary) {
273
279
  return !refusedCommandExempt(command, allow, boundary, 0);
@@ -474,11 +480,15 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
474
480
  .filter((t) => t !== "");
475
481
  });
476
482
  const headIndex = (toks) => {
483
+ const folded = toks.map(fold);
477
484
  let i = 0;
478
485
  while (i < toks.length) {
479
- const w = fold(toks[i]);
486
+ const w = folded[i];
480
487
  if (ASSIGNMENT_WORD.test(w) || SHELL_CONTROL_PREFIXES.has(w) || SHELL_BLOCK_WORDS.has(w) || /^[{}]$/.test(w)) {
481
488
  i++;
489
+ const controlWord = programBasename(w);
490
+ for (let span = launcherOptionSpan(controlWord, folded, i); span > 0; span = launcherOptionSpan(controlWord, folded, i))
491
+ i += span;
482
492
  continue;
483
493
  }
484
494
  if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(w)) {
@@ -580,14 +590,21 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
580
590
  const headWord = fold(toks[i]);
581
591
  if (headWord.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(headWord))
582
592
  return false;
593
+ if (headWord.startsWith("-") && headWord !== "-")
594
+ return false;
583
595
  let head = programOf(headWord);
584
- if (COMMAND_LAUNCHERS.has(head)) {
596
+ const launcherUnwrap = COMMAND_LAUNCHERS.has(head) ? unwrapLauncher(toks.slice(i).map(fold), allow) : undefined;
597
+ if (launcherUnwrap?.kind === "refused")
598
+ return false;
599
+ const runs = launcherUnwrap === undefined ? { head, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
600
+ if (runs.head !== undefined && runs.index !== undefined &&
601
+ argumentAsProgramRefusal(runs.head, runs.head, toks.slice(i + runs.index + 1).map(fold)) !== undefined)
602
+ return false;
603
+ if (launcherUnwrap !== undefined) {
585
604
  const wrapped = runProgramOf(toks, i);
586
605
  if (wrapped === undefined || /[$`]/.test(wrapped) || SHELL_REENTRY_PROGRAM_RE.test(wrapped))
587
606
  return false;
588
- const unwrapped = unwrapLauncher(toks.slice(i).map(fold), allow);
589
- if (unwrapped.kind === "refused")
590
- return false;
607
+ const unwrapped = launcherUnwrap;
591
608
  if (argvReentryUnjudged(unwrapped))
592
609
  return false;
593
610
  if (unwrapped.kind === "unlisted") {
@@ -1159,14 +1176,273 @@ const SCRIPT_SLOT_VERBS = new Map([
1159
1176
  return readings.every((reading) => reading === "unparsed");
1160
1177
  }],
1161
1178
  ]);
1179
+ const INLINE_PROGRAM_TEXT = "runs its argument as an interpreted program";
1180
+ const INLINE_PROGRAM_STDIN = "reads the program it runs from its standard input";
1181
+ const INTERPRETER_BANNER_OPTIONS = new Set(["--version", "--help", "--usage", "-version", "-help"]);
1182
+ const BLANK_INLINE = {
1183
+ longText: new Set(), longFile: new Set(), longNamesProgram: new Set(), longSupplemental: new Set(),
1184
+ longValue: new Set(), longPlain: new Set(), fileOptionEndsOptions: false,
1185
+ textLetters: "", fileLetters: "", separatedValueLetters: "", attachedValueLetters: "",
1186
+ plainLetters: "", bannerLetters: "", interactiveLetters: "",
1187
+ programOperand: { at: 0, isText: false },
1188
+ };
1189
+ const PERL_INLINE = {
1190
+ ...BLANK_INLINE, textLetters: "eEmM", separatedValueLetters: "I", attachedValueLetters: "iCFxDV",
1191
+ plainLetters: "nplawcsStTUWX", bannerLetters: "vVh", stdinWhenUnnamed: true,
1192
+ };
1193
+ const RUBY_INLINE = {
1194
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "IrECFKT", plainLetters: "nplawcsSU",
1195
+ bannerLetters: "vh", stdinWhenUnnamed: true,
1196
+ };
1197
+ const PYTHON_INLINE = {
1198
+ ...BLANK_INLINE, textLetters: "c", fileLetters: "m", separatedValueLetters: "WXQ",
1199
+ plainLetters: "uBEIsSqbdvxOtR", bannerLetters: "Vh?", interactiveLetters: "i",
1200
+ fileOptionEndsOptions: true, stdinWhenUnnamed: true,
1201
+ };
1202
+ const NODE_INLINE = {
1203
+ ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
1204
+ longValue: new Set(["--import", "--loader", "--experimental-loader", "--require", "--env-file", "--conditions", "--max-old-space-size"]),
1205
+ longNamesProgram: new Set(["--test"]),
1206
+ longPlain: new Set(["--inspect", "--inspect-brk", "--no-warnings", "--trace-warnings", "--watch", "--enable-source-maps", "--experimental-modules", "--no-deprecation", "--check"]),
1207
+ separatedValueLetters: "r", plainLetters: "c", bannerLetters: "vh", interactiveLetters: "i",
1208
+ stdinWhenUnnamed: true,
1209
+ };
1210
+ const PHP_INLINE = {
1211
+ ...BLANK_INLINE, longText: new Set(["--run", "--process-begin", "--process-code", "--process-end"]),
1212
+ textLetters: "rRBE", fileLetters: "fF", separatedValueLetters: "dcz", plainLetters: "nqswl",
1213
+ bannerLetters: "vhi", interactiveLetters: "a", stdinWhenUnnamed: true,
1214
+ };
1215
+ const LUA_INLINE = {
1216
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "l", bannerLetters: "vh",
1217
+ interactiveLetters: "i", stdinWhenUnnamed: true,
1218
+ };
1219
+ const TCL_INLINE = { ...BLANK_INLINE, stdinWhenUnnamed: true };
1220
+ const EXPECT_INLINE = {
1221
+ ...BLANK_INLINE, textLetters: "c", separatedValueLetters: "Db", plainLetters: "dN",
1222
+ bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: true,
1223
+ };
1224
+ const AWK_INLINE = {
1225
+ ...BLANK_INLINE, longText: new Set(["--source"]), textLetters: "e",
1226
+ longFile: new Set(["--file", "--exec"]), fileLetters: "fE", separatedValueLetters: "vF",
1227
+ bannerLetters: "V", programOperand: { at: 0, isText: true }, stdinWhenUnnamed: false,
1228
+ };
1229
+ const JQ_INLINE = {
1230
+ ...BLANK_INLINE, longFile: new Set(["--from-file"]), fileLetters: "f", separatedValueLetters: "L",
1231
+ plainLetters: "nrjcsSaCMRe", bannerLetters: "h", programOperand: { at: 0, isText: true },
1232
+ stdinWhenUnnamed: false,
1233
+ };
1234
+ const BC_INLINE = {
1235
+ ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e", plainLetters: "lqsw",
1236
+ bannerLetters: "vh", interactiveLetters: "i", programOperand: undefined, stdinWhenUnnamed: true,
1237
+ };
1238
+ const DC_INLINE = {
1239
+ ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e",
1240
+ longFile: new Set(["--file"]), fileLetters: "f", bannerLetters: "hV",
1241
+ programOperand: undefined, stdinWhenUnnamed: true,
1242
+ };
1243
+ const SQLITE_INLINE = {
1244
+ ...BLANK_INLINE, longText: new Set(["-cmd"]), longSupplemental: new Set(["-init"]),
1245
+ programOperand: { at: 1, isText: true }, stdinWhenUnnamed: true,
1246
+ };
1247
+ const PSQL_INLINE = {
1248
+ ...BLANK_INLINE, longText: new Set(["--command"]), textLetters: "c",
1249
+ longFile: new Set(["--file"]), fileLetters: "f", separatedValueLetters: "hpUdoF",
1250
+ plainLetters: "aeEqtxXwW", bannerLetters: "lV?", programOperand: undefined, stdinWhenUnnamed: true,
1251
+ };
1252
+ const OSASCRIPT_INLINE = {
1253
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "ls", plainLetters: "i", bannerLetters: "h",
1254
+ stdinWhenUnnamed: true,
1255
+ };
1256
+ const RSCRIPT_INLINE = {
1257
+ ...BLANK_INLINE, textLetters: "e", bannerLetters: "h", stdinWhenUnnamed: false,
1258
+ };
1259
+ const JULIA_INLINE = {
1260
+ ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "eE",
1261
+ separatedValueLetters: "JLp", plainLetters: "q", bannerLetters: "vh", interactiveLetters: "i",
1262
+ stdinWhenUnnamed: true,
1263
+ };
1264
+ const GHCI_INLINE = {
1265
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "io", programOperand: undefined,
1266
+ stdinWhenUnnamed: true,
1267
+ };
1268
+ const GROOVY_INLINE = {
1269
+ ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "Dcl", plainLetters: "n",
1270
+ bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: false,
1271
+ };
1272
+ const BUN_INLINE = {
1273
+ ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
1274
+ separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
1275
+ };
1276
+ const INTERPRETER_INLINE_PROGRAMS = new Map([
1277
+ ["perl", PERL_INLINE],
1278
+ ["ruby", RUBY_INLINE],
1279
+ ["python", PYTHON_INLINE],
1280
+ ["node", NODE_INLINE],
1281
+ ["nodejs", NODE_INLINE],
1282
+ ["php", PHP_INLINE],
1283
+ ["lua", LUA_INLINE],
1284
+ ["luajit", LUA_INLINE],
1285
+ ["tclsh", TCL_INLINE],
1286
+ ["wish", TCL_INLINE],
1287
+ ["expect", EXPECT_INLINE],
1288
+ ["awk", AWK_INLINE],
1289
+ ["gawk", AWK_INLINE],
1290
+ ["mawk", AWK_INLINE],
1291
+ ["nawk", AWK_INLINE],
1292
+ ["jq", JQ_INLINE],
1293
+ ["bc", BC_INLINE],
1294
+ ["dc", DC_INLINE],
1295
+ ["sqlite", SQLITE_INLINE],
1296
+ ["psql", PSQL_INLINE],
1297
+ ["osascript", OSASCRIPT_INLINE],
1298
+ ["Rscript", RSCRIPT_INLINE],
1299
+ ["julia", JULIA_INLINE],
1300
+ ["ghci", GHCI_INLINE],
1301
+ ["groovy", GROOVY_INLINE],
1302
+ ["bun", BUN_INLINE],
1303
+ ]);
1304
+ function interpreterKey(name) {
1305
+ return name.replace(/\d+(?:\.\d+)*$/, "");
1306
+ }
1307
+ function withoutRedirectionWords(args) {
1308
+ const out = [];
1309
+ for (let i = 0; i < args.length; i++) {
1310
+ const t = args[i];
1311
+ if (/^(?:<<<|<<|<|>>?|&>>?)$/.test(t)) {
1312
+ i++;
1313
+ continue;
1314
+ }
1315
+ if (/^(?:>>?|<)&\d*$/.test(t))
1316
+ continue;
1317
+ out.push(t);
1318
+ }
1319
+ return out;
1320
+ }
1321
+ function inlineProgramClause(model, args) {
1322
+ let endOfOptions = false;
1323
+ let banner = false;
1324
+ let interactive = false;
1325
+ let named = false;
1326
+ let stdin = false;
1327
+ let explained = true;
1328
+ let operands = 0;
1329
+ scan: for (let i = 0; i < args.length; i++) {
1330
+ const arg = args[i];
1331
+ if (!endOfOptions && arg === "--") {
1332
+ endOfOptions = true;
1333
+ continue;
1334
+ }
1335
+ if (arg === "-") {
1336
+ if (model.programOperand?.isText !== true)
1337
+ stdin = true;
1338
+ continue;
1339
+ }
1340
+ if (!endOfOptions && arg.startsWith("-")) {
1341
+ const eq = arg.indexOf("=");
1342
+ const whole = eq < 0 ? arg : arg.slice(0, eq);
1343
+ const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
1344
+ if (model.longText.has(whole))
1345
+ return INLINE_PROGRAM_TEXT;
1346
+ if (model.longFile.has(whole)) {
1347
+ if (valueOf() === "-") {
1348
+ stdin = true;
1349
+ continue;
1350
+ }
1351
+ named = true;
1352
+ if (explained && model.fileOptionEndsOptions)
1353
+ break scan;
1354
+ continue;
1355
+ }
1356
+ if (model.longNamesProgram.has(whole)) {
1357
+ named = true;
1358
+ continue;
1359
+ }
1360
+ if (model.longSupplemental.has(whole)) {
1361
+ valueOf();
1362
+ continue;
1363
+ }
1364
+ if (INTERPRETER_BANNER_OPTIONS.has(whole)) {
1365
+ banner = true;
1366
+ continue;
1367
+ }
1368
+ if (model.longValue.has(whole)) {
1369
+ valueOf();
1370
+ continue;
1371
+ }
1372
+ if (model.longPlain.has(whole))
1373
+ continue;
1374
+ if (arg.startsWith("--")) {
1375
+ if (eq < 0)
1376
+ explained = false;
1377
+ continue;
1378
+ }
1379
+ for (let k = 1; k < arg.length; k++) {
1380
+ const ch = arg[k];
1381
+ if (ch >= "0" && ch <= "9")
1382
+ continue;
1383
+ if (model.textLetters.includes(ch))
1384
+ return INLINE_PROGRAM_TEXT;
1385
+ if (model.fileLetters.includes(ch)) {
1386
+ const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
1387
+ if (value === "-") {
1388
+ stdin = true;
1389
+ break;
1390
+ }
1391
+ named = true;
1392
+ if (explained && model.fileOptionEndsOptions)
1393
+ break scan;
1394
+ break;
1395
+ }
1396
+ if (model.attachedValueLetters.includes(ch)) {
1397
+ if (model.bannerLetters.includes(ch))
1398
+ banner = true;
1399
+ break;
1400
+ }
1401
+ if (model.bannerLetters.includes(ch)) {
1402
+ banner = true;
1403
+ continue;
1404
+ }
1405
+ if (model.interactiveLetters.includes(ch)) {
1406
+ interactive = true;
1407
+ continue;
1408
+ }
1409
+ if (model.separatedValueLetters.includes(ch)) {
1410
+ if (k + 1 >= arg.length)
1411
+ i++;
1412
+ break;
1413
+ }
1414
+ if (!model.plainLetters.includes(ch))
1415
+ explained = false;
1416
+ }
1417
+ continue;
1418
+ }
1419
+ operands++;
1420
+ if (!named && explained && model.programOperand !== undefined && operands === model.programOperand.at + 1) {
1421
+ if (model.programOperand.isText)
1422
+ return INLINE_PROGRAM_TEXT;
1423
+ named = true;
1424
+ break;
1425
+ }
1426
+ }
1427
+ if (stdin || interactive)
1428
+ return INLINE_PROGRAM_STDIN;
1429
+ if (named || banner)
1430
+ return undefined;
1431
+ return model.stdinWhenUnnamed ? INLINE_PROGRAM_STDIN : undefined;
1432
+ }
1162
1433
  function unjudgedProgramTextRefusal(written, clause) {
1163
1434
  return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
1164
1435
  }
1165
- function scriptSlotRefusal(written, name, args) {
1166
- const recognizer = SCRIPT_SLOT_VERBS.get(name);
1167
- if (recognizer === undefined || !recognizer(args))
1436
+ function argumentAsProgramRefusal(written, name, args) {
1437
+ const slot = SCRIPT_SLOT_VERBS.get(name);
1438
+ if (slot !== undefined && slot(args)) {
1439
+ return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
1440
+ }
1441
+ const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
1442
+ if (interpreter === undefined)
1168
1443
  return undefined;
1169
- return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
1444
+ const clause = inlineProgramClause(interpreter, withoutRedirectionWords(args));
1445
+ return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
1170
1446
  }
1171
1447
  function guardedVerbFormReason(name, toks, raw) {
1172
1448
  if (name === "find") {
@@ -1590,9 +1866,9 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1590
1866
  return { reason: launcherUnwrap.reason, refusedWhole: true };
1591
1867
  const runs = launcherUnwrap === undefined ? { head: name, index: 0 } : { head: launcherUnwrap.head, index: launcherUnwrap.index };
1592
1868
  if (runs?.head !== undefined && runs.index !== undefined) {
1593
- const slotRefusal = scriptSlotRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
1594
- if (slotRefusal !== undefined)
1595
- return { reason: slotRefusal, refusedWhole: true };
1869
+ const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, folded.slice(from + runs.index + 1));
1870
+ if (familyRefusal !== undefined)
1871
+ return { reason: familyRefusal, refusedWhole: true };
1596
1872
  }
1597
1873
  if (allow.has(name)) {
1598
1874
  if (pathPrefixed || from > 0)
@@ -1958,9 +2234,9 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
1958
2234
  if (runs.kind === "refused")
1959
2235
  return { reason: runs.reason, refusedWhole: true };
1960
2236
  if (runs.head !== undefined && runs.index !== undefined) {
1961
- const slotRefusal = scriptSlotRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
1962
- if (slotRefusal !== undefined)
1963
- return { reason: slotRefusal, refusedWhole: true };
2237
+ const familyRefusal = argumentAsProgramRefusal(runs.head, runs.head, toks.folded.slice(runs.index + past + 1));
2238
+ if (familyRefusal !== undefined)
2239
+ return { reason: familyRefusal, refusedWhole: true };
1964
2240
  }
1965
2241
  return evaluateReadBoundary([toks], boundary);
1966
2242
  }
@@ -1976,7 +2252,7 @@ function whitelistReadsTheSameWords(command, outsideHeredocBodies) {
1976
2252
  }
1977
2253
  const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
1978
2254
  const COMMAND_LAUNCHERS = new Set([
1979
- "env", "command", "builtin", "exec", "noglob", "xargs",
2255
+ "env", "command", "builtin", "exec", "noglob", "xargs", "busybox",
1980
2256
  "sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
1981
2257
  "nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
1982
2258
  "setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.17.6",
3
+ "version": "7.17.7",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -0,0 +1,81 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.17.6",
4
+ "from": "3dfd4ff66bfd1c6ea217fa251d2f28df79052f87",
5
+ "to": "adb5afdddf8b01be7b9b8a8394251eea7fe2ee19",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [
10
+ {
11
+ "id": "F1",
12
+ "source": "behaviorFlips.summary",
13
+ "text": "Narrowing — the execute-time canonical recheck now answers for a GLOB's expansion (#775; B-109; test [7201]): the full Bash face hands the boundary's `undecidedPaths` (the operands the shell will expand) to the same expander the `bash_readonly` face already used, so a pattern that expands to a target outside the roots is refused before spawn; an expansion that cannot be established (uncompilable segment, >256 directories, >8192 names, an unreadable directory) is refused too."
14
+ },
15
+ {
16
+ "id": "F2",
17
+ "source": "behaviorFlips.summary",
18
+ "text": "Narrowing — a `..` taken AFTER a symlink is judged where the kernel takes it (#770; B-110; test [7203]): the lexical walk folds `..` for its containment verdict as before, but the recheck now receives the operand's UNFOLDED spelling and resolves it segment by segment (realpath the prefix, THEN take the parent), so `<link>/../x` is refused when the link's target parent is outside every read root."
19
+ },
20
+ {
21
+ "id": "F3",
22
+ "source": "behaviorFlips.summary",
23
+ "text": "Narrowing — `sed`'s SCRIPT SLOT is judged as an argument-as-program (#769; test [7180] candidate 7): a sed script that names a file or runs a command (`r`/`R`/`w`/`W`/`e` commands, `s///w FILE`, `s///e`) or a script the walk cannot see (`-f FILE`/`--file=`) refuses the command whole (mandated), under the same rule as shell re-entry and argv re-entry."
24
+ },
25
+ {
26
+ "id": "F5",
27
+ "source": "behaviorFlips.summary",
28
+ "text": "Narrowing — the execute-time read boundary asks what an environment DECLARES, not what interface it implements (#782; B-115; test [7214]): the canonical recheck is skipped only when the environment declares `capabilities.isolation: true` (the sandbox is the boundary, C-R37 γ); a lane that implements the remote interface but declares no isolation (server `REMOTE_EXEC=host`, SSH hosts) now runs the recheck like an in-process env."
29
+ }
30
+ ],
31
+ "closedSetMembers": [],
32
+ "typeKeys": [
33
+ {
34
+ "type": "CompoundReadonlyVerdict",
35
+ "at": [
36
+ "src/tools/fs/bash-readonly-classifier.ts:644"
37
+ ],
38
+ "added": [
39
+ "checkedSpellings",
40
+ "outOfRootSpellings",
41
+ "undecidedSpellings"
42
+ ],
43
+ "removed": [],
44
+ "breaking": false,
45
+ "criteriaCovered": false
46
+ }
47
+ ],
48
+ "declarationMoves": [],
49
+ "mintMoves": [],
50
+ "criteriaObserves": [
51
+ {
52
+ "kind": "typeMember",
53
+ "name": "AskRequest.ruleOffersAbsence",
54
+ "assertions": [
55
+ "G1.a",
56
+ "G2.a",
57
+ "G3.a",
58
+ "G5.a"
59
+ ]
60
+ },
61
+ {
62
+ "kind": "typeMember",
63
+ "name": "HarvestReport.rejections",
64
+ "assertions": [
65
+ "G4.a"
66
+ ]
67
+ }
68
+ ],
69
+ "consumersHint": {
70
+ "cli": [
71
+ "CompoundReadonlyVerdict"
72
+ ],
73
+ "server": [
74
+ "CompoundReadonlyVerdict"
75
+ ],
76
+ "sdk": [],
77
+ "note": "scanned cli@60ea7972 server@940568c6 sdk@6910789 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
78
+ },
79
+ "breakingCount": 0,
80
+ "breakingItems": []
81
+ }