@sema-agent/core 7.18.0 → 7.20.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +276 -2
  2. package/dist/agents/subagent.js +12 -20
  3. package/dist/agents/teacher.js +2 -5
  4. package/dist/agents/verify.js +21 -10
  5. package/dist/brain/anthropic.js +8 -3
  6. package/dist/brain/open-responses.js +12 -4
  7. package/dist/brain/openai.js +7 -2
  8. package/dist/brain/reasoning.d.ts +13 -13
  9. package/dist/brain/reasoning.js +2 -2
  10. package/dist/brain/stream-engine.d.ts +4 -0
  11. package/dist/brain/stream-engine.js +6 -1
  12. package/dist/brain/stream-shared.d.ts +3 -0
  13. package/dist/brain/stream-shared.js +1 -0
  14. package/dist/core/ask-origin.d.ts +16 -0
  15. package/dist/core/ask-origin.js +8 -0
  16. package/dist/core/auto-mode-arming.d.ts +6 -0
  17. package/dist/core/auto-mode-arming.js +5 -5
  18. package/dist/core/auto-mode-defaults.d.ts +43 -15
  19. package/dist/core/auto-mode-defaults.js +4 -1
  20. package/dist/core/auto-mode.d.ts +74 -26
  21. package/dist/core/auto-mode.js +80 -11
  22. package/dist/core/checkpoint-store.d.ts +7 -0
  23. package/dist/core/checkpoint-store.js +2 -2
  24. package/dist/core/child-clamp-seats.d.ts +79 -0
  25. package/dist/core/child-clamp-seats.js +29 -0
  26. package/dist/core/engine-notice.d.ts +89 -41
  27. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  28. package/dist/core/fs-write-gate-policy.js +51 -4
  29. package/dist/core/gate-bounded-await.d.ts +38 -0
  30. package/dist/core/gate-bounded-await.js +53 -0
  31. package/dist/core/gate-fold.js +20 -48
  32. package/dist/core/gate-lanes.js +17 -9
  33. package/dist/core/gate-outcome.d.ts +34 -6
  34. package/dist/core/gate-outcome.js +7 -0
  35. package/dist/core/governance-codes.d.ts +16 -1
  36. package/dist/core/governance-codes.js +18 -0
  37. package/dist/core/hooks.d.ts +8 -8
  38. package/dist/core/memory-engine/engine.d.ts +14 -9
  39. package/dist/core/memory-engine/engine.js +21 -8
  40. package/dist/core/memory-engine/layout.d.ts +31 -0
  41. package/dist/core/memory-engine/layout.js +68 -3
  42. package/dist/core/memory-engine/types.d.ts +24 -0
  43. package/dist/core/memory.d.ts +19 -0
  44. package/dist/core/memory.js +12 -0
  45. package/dist/core/read-root-candidate.d.ts +12 -0
  46. package/dist/core/read-root-candidate.js +24 -0
  47. package/dist/core/remote-env.d.ts +32 -0
  48. package/dist/core/remote-env.js +4 -1
  49. package/dist/core/roles.d.ts +9 -1
  50. package/dist/core/roles.js +4 -0
  51. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  52. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  53. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  54. package/dist/core/runner/announce-once-ledger.js +8 -0
  55. package/dist/core/runner/classifier-ladder.d.ts +154 -11
  56. package/dist/core/runner/classifier-ladder.js +170 -10
  57. package/dist/core/runner/contracts.d.ts +46 -1
  58. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  59. package/dist/core/runner/denial-limit-arms.js +7 -6
  60. package/dist/core/runner/env-capability-census.d.ts +30 -0
  61. package/dist/core/runner/env-capability-census.js +38 -0
  62. package/dist/core/runner/forward-sink.d.ts +32 -0
  63. package/dist/core/runner/forward-sink.js +41 -0
  64. package/dist/core/runner/gate-exit.d.ts +4 -0
  65. package/dist/core/runner/gate-exit.js +4 -1
  66. package/dist/core/runner/git-status-frame.d.ts +0 -9
  67. package/dist/core/runner/listing-fold.d.ts +57 -0
  68. package/dist/core/runner/listing-fold.js +75 -0
  69. package/dist/core/runner/prepare-ask-lane.js +1 -0
  70. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  71. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  72. package/dist/core/runner/prepare-config-doors.js +8 -10
  73. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  74. package/dist/core/runner/prepare-execution-env.js +2 -0
  75. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  76. package/dist/core/runner/prepare-gate-stations.js +6 -2
  77. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  78. package/dist/core/runner/prepare-hands-readface.js +3 -5
  79. package/dist/core/runner/prepare-listings.d.ts +3 -0
  80. package/dist/core/runner/prepare-listings.js +4 -2
  81. package/dist/core/runner/prepare-memory.js +7 -1
  82. package/dist/core/runner/prepare-policy-chain.js +5 -5
  83. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  84. package/dist/core/runner/prepare-project-context.js +3 -2
  85. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  86. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  87. package/dist/core/runner/prepare-run-refs.js +5 -4
  88. package/dist/core/runner/prepare-task.js +8 -6
  89. package/dist/core/runner/resume-apply.js +3 -3
  90. package/dist/core/runner/run-clock-and-content.js +3 -7
  91. package/dist/core/runner/run-harness-handlers.js +10 -10
  92. package/dist/core/runner/run-leg.js +31 -16
  93. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  94. package/dist/core/runner/run-turn-boundary.js +44 -25
  95. package/dist/core/runner/runtask.js +2 -1
  96. package/dist/core/runner/synthetic-tools.d.ts +41 -29
  97. package/dist/core/runner/synthetic-tools.js +25 -40
  98. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  99. package/dist/core/runner/task-root-canonical.js +18 -0
  100. package/dist/core/runner/turn-attachments.d.ts +160 -53
  101. package/dist/core/runner/turn-attachments.js +153 -51
  102. package/dist/core/safe-notify.d.ts +5 -0
  103. package/dist/core/safe-notify.js +24 -2
  104. package/dist/core/sensitive-path-policy.d.ts +84 -17
  105. package/dist/core/sensitive-path-policy.js +37 -19
  106. package/dist/core/skill-listing-budget.d.ts +106 -0
  107. package/dist/core/skill-listing-budget.js +57 -0
  108. package/dist/core/skills-directory.js +17 -10
  109. package/dist/core/task-registry.js +2 -1
  110. package/dist/core/task-spec.d.ts +29 -2
  111. package/dist/core/tool-policy.d.ts +31 -5
  112. package/dist/core/tool-policy.js +45 -32
  113. package/dist/core/tool-spec.d.ts +37 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/core/write-protect.d.ts +105 -20
  116. package/dist/core/write-protect.js +89 -32
  117. package/dist/index.d.ts +8 -7
  118. package/dist/index.js +6 -5
  119. package/dist/orchestration/workflow-primitives.js +10 -20
  120. package/dist/stores/file/memory-store.d.ts +5 -2
  121. package/dist/stores/file/memory-store.js +8 -4
  122. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
  123. package/dist/tools/fs/bash-readonly-classifier.js +376 -41
  124. package/dist/tools/fs/fs-bash.d.ts +1 -1
  125. package/dist/tools/fs/fs-bash.js +19 -28
  126. package/dist/tools/fs/fs-shared.d.ts +22 -0
  127. package/dist/tools/fs/fs-shared.js +30 -0
  128. package/dist/tools/fs/index.d.ts +1 -1
  129. package/dist/tools/fs/index.js +1 -1
  130. package/dist/tools/fs/read-deny.d.ts +4 -3
  131. package/dist/tools/fs/safety.d.ts +15 -0
  132. package/dist/tools/fs/safety.js +7 -0
  133. package/package.json +2 -1
  134. package/test/export-surface.snapshot.json +31 -5
  135. package/wire-changes/core@7.18.0.json +1273 -0
  136. package/wire-changes/core@7.19.0.json +457 -0
  137. package/wire-changes/core@7.20.0.json +568 -0
@@ -35,7 +35,7 @@ export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
35
35
  export type { ShellSegmentConnector } from "./bash-lexer.js";
36
36
  export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
37
37
  /**
38
- * The CLASSIFY face's default allow set: the shared list PLUS the verbs that are
38
+ * The verbs the CLASSIFY face admits ON TOP of the shared list ({@link bashClassifyDefaultAllow}) —
39
39
  * admissible only where the compound classifier's per-verb judgment runs, and NOT on the shared list's
40
40
  * other consumer faces:
41
41
  * · `find` — read-only only when no CC `aCy` dangerous predicate token is present (guard arm);
@@ -54,7 +54,18 @@ export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
54
54
  * poll-loop face inherits the guard arms for free: its body vetting delegates to the same compound
55
55
  * classifier.
56
56
  */
57
- export declare const BASH_CLASSIFY_DEFAULT_ALLOW: readonly string[];
57
+ export declare const BASH_CLASSIFY_EXTRA_ALLOW: readonly string[];
58
+ /** A verb only the classify face admits. */
59
+ export type BashClassifyExtraVerb = (typeof BASH_CLASSIFY_EXTRA_ALLOW)[number];
60
+ /**
61
+ * @contract shell.classify_allow.one_union_point — the classify face's default allow set is the union of
62
+ * two SPELLED-OUT tables ({@link BASH_READONLY_DEFAULT_ALLOW} ∪ {@link BASH_CLASSIFY_EXTRA_ALLOW}) and
63
+ * this function is its only assembly point; no call site composes its own. Both halves stay arrays of
64
+ * string literals so a static reader can enumerate them: a vocabulary derived in place
65
+ * (`[...TABLE, "extra"]`) is invisible to the release's wire-change manifest, so a verb added to or
66
+ * dropped from this security-relevant set would move nothing a downstream reader could see.
67
+ */
68
+ export declare function bashClassifyDefaultAllow(): readonly string[];
58
69
  /**
59
70
  * G2 — options for {@link parseLeadingCommandName}. Omitting them keeps the parser byte-identical to
60
71
  * what every existing caller (the `bash_readonly` declaration face, the coarse command-name policy, the
@@ -413,6 +424,21 @@ export interface CompoundReadonlyVerdict {
413
424
  * path is unchanged.
414
425
  */
415
426
  recursiveReadPaths?: readonly string[];
427
+ /**
428
+ * The ONE directory whose addition to the declared roots would make every objection this walk raised go
429
+ * away. It rides the SAME arm as the containment sentence and is dropped wherever that sentence is (a
430
+ * SHAPE refusal takes both — `tail -f <out-of-root>` stays refused after any grant), and on that arm the
431
+ * claim is MEASURED, not argued: the walk runs again with the proposed root in place and the directory is
432
+ * offered only if that walk has no objection left. `..`-traversing spellings are excluded on their own
433
+ * account ({@link outOfRootSpellings}): that is a disagreement between the folded text and the kernel,
434
+ * which no replay of the text can settle. VALUE: the deepest directory PROPERLY
435
+ * containing every {@link outOfRootPaths} entry, folded lexically — zero I/O, so a path that is itself a
436
+ * directory contributes its PARENT (`ls /etc/ssl` ⇒ `/etc`) where the reference implementation narrows
437
+ * with a stat; refused rather than widened when the result would be a family root or would contain the
438
+ * declared home or a declared {@link BashReadonlyRootBoundary.roots} entry. Meaning and spelling
439
+ * contract: {@link import("../../core/tool-spec.js").ReadRootGrantCandidate}.
440
+ */
441
+ outOfRootGrantCandidate?: string;
416
442
  }
417
443
  /**
418
444
  * The single minting point for the out-of-root-read approval option text, so a gate rendering
@@ -425,6 +451,18 @@ export interface CompoundReadonlyVerdict {
425
451
  * uses the host's native separator.
426
452
  */
427
453
  export declare function formatOutOfRootReadApprovalOption(directory: string): string;
454
+ /**
455
+ * The deepest directory containing both, or `undefined` when they share none (two path families).
456
+ *
457
+ * LINEAR, and that is the point: both paths are already folded, so the answer is their common prefix cut
458
+ * at a separator. Climbing one directory at a time re-folds the whole remaining path each step — the same
459
+ * answer at quadratic cost, measured at 4.9 s of synchronous work for one 32 KB command naming a
460
+ * 16 000-segment operand, on a walk that runs BEFORE the gate arms its probe deadline. The family-aware
461
+ * comparator keeps the last word: a prefix it cannot confirm contains both paths yields no candidate.
462
+ * Exported for its unit tests — the family confirmation is reachable from a command only through shapes
463
+ * the compound grammar refuses first, so a command-level pin cannot tell a live guard from an absent one.
464
+ */
465
+ export declare function deepestCommonDir(a: string, b: string): string | undefined;
428
466
  /** Resolve one operand against the boundary's base, WITHOUT touching the filesystem. Returns undefined
429
467
  * when the token cannot be resolved statically — the fail-closed direction, reported as its own
430
468
  * demotion rather than silently treated as in-root.
@@ -1,4 +1,4 @@
1
- import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
1
+ import { hasWin32AliasedComponent, isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
2
2
  import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
3
3
  import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
4
4
  export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
@@ -8,7 +8,10 @@ export const BASH_READONLY_DEFAULT_ALLOW = [
8
8
  "cal", "uptime", "id", "groups", "nproc", "locale", "free", "realpath", "readlink",
9
9
  "true", "false", "seq", "expr", "type", "sleep", "diff", "cmp", "comm",
10
10
  ];
11
- export const BASH_CLASSIFY_DEFAULT_ALLOW = [...BASH_READONLY_DEFAULT_ALLOW, "find", "sed", "cd"];
11
+ export const BASH_CLASSIFY_EXTRA_ALLOW = ["find", "sed", "cd"];
12
+ export function bashClassifyDefaultAllow() {
13
+ return [...BASH_READONLY_DEFAULT_ALLOW, ...BASH_CLASSIFY_EXTRA_ALLOW];
14
+ }
12
15
  const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
13
16
  function hasBareShellOperator(s, quotedOperatorsAreText) {
14
17
  if (!quotedOperatorsAreText)
@@ -135,6 +138,49 @@ export function formatOutOfRootReadApprovalOption(directory) {
135
138
  const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
136
139
  return `Yes, allow reading from ${leaf || directory}/ from this project`;
137
140
  }
141
+ function parentDirLexically(p) {
142
+ return normalizeAbsPathLexically(`${p}/..`);
143
+ }
144
+ export function deepestCommonDir(a, b) {
145
+ if (a === b)
146
+ return a;
147
+ const shorter = Math.min(a.length, b.length);
148
+ let end = 0;
149
+ let i = 0;
150
+ while (i < shorter && a[i] === b[i]) {
151
+ if (a[i] === "/" || a[i] === "\\")
152
+ end = i;
153
+ i++;
154
+ }
155
+ if (i === shorter && (a[shorter] === "/" || a[shorter] === "\\" || b[shorter] === "/" || b[shorter] === "\\"))
156
+ end = shorter;
157
+ const cut = a.slice(0, end);
158
+ const candidate = cut.includes("/") || cut.includes("\\") ? cut : a.slice(0, end + 1);
159
+ if (candidate.length === 0 || !withinAnyRoot([candidate], a) || !withinAnyRoot([candidate], b))
160
+ return undefined;
161
+ return candidate;
162
+ }
163
+ function outOfRootGrantCandidateOf(paths, boundary) {
164
+ if (paths.length === 0)
165
+ return undefined;
166
+ if (paths.some(hasWin32AliasedComponent))
167
+ return undefined;
168
+ let candidate = parentDirLexically(paths[0]);
169
+ for (const path of paths.slice(1)) {
170
+ if (candidate === undefined)
171
+ return undefined;
172
+ candidate = deepestCommonDir(candidate, parentDirLexically(path));
173
+ }
174
+ if (candidate === undefined)
175
+ return undefined;
176
+ if (parentDirLexically(candidate) === candidate)
177
+ return undefined;
178
+ if (boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir))
179
+ return undefined;
180
+ if (boundary.roots.some((root) => withinAnyRoot([candidate], root)))
181
+ return undefined;
182
+ return candidate;
183
+ }
138
184
  const NO_PATH_OPERAND_COMMANDS = new Set([
139
185
  "pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname",
140
186
  "cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
@@ -403,18 +449,6 @@ function isGrepPatternFlagToken(tok) {
403
449
  }
404
450
  return /^-[A-Za-z0-9]*[ef]/.test(tok);
405
451
  }
406
- function isGrepPatternPayloadLongOption(tok) {
407
- if (!tok.includes("="))
408
- return false;
409
- const name = longOptionNameOf(tok);
410
- return name !== undefined && isLongOptionAbbrevOf(name, "regexp");
411
- }
412
- function isCutDelimiterPayloadLongOption(tok) {
413
- if (!tok.includes("="))
414
- return false;
415
- const name = longOptionNameOf(tok);
416
- return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
417
- }
418
452
  function isGrepClusterFileStdin(tok) {
419
453
  if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
420
454
  return false;
@@ -429,6 +463,283 @@ function isGrepClusterFileStdin(tok) {
429
463
  }
430
464
  return false;
431
465
  }
466
+ function valueNamesAFile(kind) {
467
+ return kind === "path" || kind === "indirect" || kind === "form" || kind === "header" || kind === "url";
468
+ }
469
+ function widestValueKind(kinds) {
470
+ if (kinds.has("indirect"))
471
+ return "indirect";
472
+ if (kinds.has("path"))
473
+ return "path";
474
+ if (kinds.has("form"))
475
+ return "form";
476
+ if (kinds.has("header"))
477
+ return "header";
478
+ if (kinds.has("url"))
479
+ return "url";
480
+ return [...kinds][0];
481
+ }
482
+ const VERB_OPTION_SLOTS = {
483
+ head: {
484
+ shortValue: { c: "count", n: "count" },
485
+ shortPlain: "qvz",
486
+ longValue: { bytes: "count", lines: "count" },
487
+ longOptionalValue: {},
488
+ longPlain: ["help", "quiet", "silent", "verbose", "version", "zero-terminated"],
489
+ },
490
+ tail: {
491
+ shortValue: { b: "count", c: "count", n: "count", s: "count" },
492
+ shortPlain: "fFqvz",
493
+ longValue: { bytes: "count", lines: "count", "max-unchanged-stats": "count", pid: "count", "sleep-interval": "count" },
494
+ longOptionalValue: { follow: "label" },
495
+ longPlain: ["help", "quiet", "retry", "silent", "verbose", "version", "zero-terminated"],
496
+ },
497
+ cut: {
498
+ shortValue: { b: "count", c: "count", d: "delimiter", f: "count" },
499
+ shortPlain: "nsz",
500
+ longValue: { bytes: "count", characters: "count", delimiter: "delimiter", fields: "count", "output-delimiter": "delimiter" },
501
+ longOptionalValue: {},
502
+ longPlain: ["complement", "help", "only-delimited", "version", "zero-terminated"],
503
+ },
504
+ grep: {
505
+ shortValue: { A: "count", B: "count", C: "count", D: "label", d: "label", e: "pattern", f: "path", m: "count" },
506
+ shortPlain: "0123456789abcEFGhHiIlLnoPqrRsUvVwxyzZ",
507
+ longValue: {
508
+ "after-context": "count",
509
+ "before-context": "count",
510
+ "binary-files": "label",
511
+ devices: "label",
512
+ directories: "label",
513
+ exclude: "name-pattern",
514
+ "exclude-dir": "name-pattern",
515
+ "exclude-from": "path",
516
+ file: "path",
517
+ "group-separator": "label",
518
+ include: "name-pattern",
519
+ label: "label",
520
+ "max-count": "count",
521
+ regexp: "pattern",
522
+ },
523
+ longOptionalValue: { color: "label", colour: "label", context: "count" },
524
+ longPlain: [
525
+ "basic-regexp", "byte-offset", "count", "dereference-recursive", "extended-regexp",
526
+ "files-with-matches", "files-without-match", "fixed-strings", "help", "ignore-case", "initial-tab",
527
+ "invert-match", "line-buffered", "line-number", "line-regexp", "no-filename", "no-group-separator",
528
+ "no-messages", "null", "null-data", "only-matching", "perl-regexp", "quiet", "recursive", "silent",
529
+ "text", "version", "with-filename", "word-regexp",
530
+ ],
531
+ },
532
+ diff: {
533
+ shortValue: { C: "count", D: "label", F: "pattern", I: "pattern", L: "label", S: "path", U: "count", W: "count", X: "path", x: "name-pattern" },
534
+ shortPlain: "abBcdeEHilnNpqrstTuvwyZ",
535
+ longValue: {
536
+ exclude: "name-pattern",
537
+ "exclude-from": "path",
538
+ "from-file": "path",
539
+ "horizon-lines": "count",
540
+ ifdef: "label",
541
+ "ignore-matching-lines": "pattern",
542
+ label: "label",
543
+ "show-function-line": "pattern",
544
+ "starting-file": "path",
545
+ tabsize: "count",
546
+ "to-file": "path",
547
+ width: "count",
548
+ },
549
+ longOptionalValue: { color: "label", context: "count", unified: "count" },
550
+ longPlain: [
551
+ "brief", "ed", "expand-tabs", "help", "ignore-all-space", "ignore-blank-lines", "ignore-case",
552
+ "ignore-space-change", "ignore-tab-expansion", "ignore-trailing-space", "initial-tab", "minimal",
553
+ "new-file", "normal", "paginate", "recursive", "report-identical-files", "rcs", "show-c-function",
554
+ "side-by-side", "speed-large-files", "suppress-blank-empty", "text", "version",
555
+ ],
556
+ },
557
+ cmp: {
558
+ shortValue: { i: "count", n: "count" },
559
+ shortPlain: "blsv",
560
+ longValue: { bytes: "count", "ignore-initial": "count" },
561
+ longOptionalValue: {},
562
+ longPlain: ["help", "print-bytes", "quiet", "silent", "verbose", "version"],
563
+ },
564
+ sed: {
565
+ shortValue: { e: "script", f: "path" },
566
+ shortPlain: "nrEszu",
567
+ longValue: { expression: "script", file: "path", "line-length": "count" },
568
+ longOptionalValue: { "in-place": "label" },
569
+ longPlain: [
570
+ "debug", "follow-symlinks", "help", "null-data", "posix", "quiet", "regexp-extended", "sandbox",
571
+ "separate", "silent", "unbuffered", "version", "zero-terminated",
572
+ ],
573
+ },
574
+ date: {
575
+ shortValue: { d: "count", f: "path", r: "path", s: "count" },
576
+ shortPlain: "uR",
577
+ longValue: { date: "count", file: "path", reference: "path", "rfc-3339": "count", set: "count" },
578
+ longOptionalValue: { "iso-8601": "label" },
579
+ longPlain: ["debug", "help", "rfc-822", "rfc-email", "universal", "utc", "version"],
580
+ },
581
+ tar: {
582
+ shortValue: { b: "count", C: "path", f: "path", T: "path", X: "path" },
583
+ shortPlain: "acdhjJkmOpPrStuvwWxzZ",
584
+ longValue: {
585
+ "blocking-factor": "count",
586
+ directory: "path",
587
+ exclude: "name-pattern",
588
+ "exclude-from": "path",
589
+ file: "path",
590
+ "files-from": "path",
591
+ "strip-components": "count",
592
+ },
593
+ longOptionalValue: {},
594
+ longPlain: [
595
+ "absolute-names", "append", "create", "dereference", "extract", "get", "help", "interactive",
596
+ "keep-old-files", "list", "preserve-permissions", "sparse", "to-stdout", "touch", "update",
597
+ "verbose", "verify", "version",
598
+ ],
599
+ },
600
+ curl: {
601
+ shortValue: {
602
+ A: "label", b: "indirect", C: "count", c: "path", D: "path", d: "indirect", E: "path", e: "label",
603
+ F: "form", H: "header", K: "path", m: "count", o: "path", P: "label", Q: "label", r: "count",
604
+ T: "path", t: "label", U: "label", u: "label", w: "indirect", X: "label", x: "label", Y: "count",
605
+ y: "count", z: "path",
606
+ },
607
+ shortPlain: "#0123456aBfgGhiIjJklLMnNOpqRsSvVZ",
608
+ longValue: {
609
+ cacert: "path",
610
+ capath: "path",
611
+ cert: "path",
612
+ config: "path",
613
+ "connect-timeout": "count",
614
+ cookie: "indirect",
615
+ "cookie-jar": "path",
616
+ data: "indirect",
617
+ "data-ascii": "indirect",
618
+ "data-binary": "indirect",
619
+ "data-raw": "label",
620
+ "data-urlencode": "form",
621
+ "dump-header": "path",
622
+ form: "form",
623
+ "form-string": "label",
624
+ header: "header",
625
+ key: "path",
626
+ "max-time": "count",
627
+ "netrc-file": "path",
628
+ output: "path",
629
+ proxy: "label",
630
+ range: "count",
631
+ referer: "label",
632
+ request: "label",
633
+ resolve: "label",
634
+ trace: "path",
635
+ "time-cond": "path",
636
+ "trace-ascii": "path",
637
+ "upload-file": "path",
638
+ url: "url",
639
+ user: "label",
640
+ "user-agent": "label",
641
+ "write-out": "indirect",
642
+ },
643
+ longOptionalValue: {},
644
+ longPlain: [
645
+ "append", "compressed", "disable", "fail", "get", "globoff", "head", "help", "include", "insecure",
646
+ "junk-session-cookies", "list-only", "location", "manual", "netrc", "next", "no-buffer", "parallel",
647
+ "proxytunnel", "remote-header-name", "remote-name", "remote-time", "show-error", "silent",
648
+ "use-ascii", "verbose", "version",
649
+ ],
650
+ },
651
+ };
652
+ function readVerbOptionSlot(name, tok) {
653
+ const row = VERB_OPTION_SLOTS[name];
654
+ if (row === undefined)
655
+ return undefined;
656
+ if (!tok.startsWith("-") || tok === "-" || tok === "--")
657
+ return { form: "unexplained" };
658
+ if (tok.startsWith("--")) {
659
+ const long = longOptionNameOf(tok);
660
+ if (long === undefined)
661
+ return { form: "unexplained" };
662
+ const eq = tok.indexOf("=");
663
+ const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
664
+ const withValue = (kind) => attached !== undefined ? { form: "attached", value: attached, kind } : { form: "separated", kind };
665
+ const exact = row.longValue[long];
666
+ if (exact !== undefined)
667
+ return withValue(exact);
668
+ const exactOptional = row.longOptionalValue[long];
669
+ if (exactOptional !== undefined)
670
+ return attached !== undefined ? { form: "attached", value: attached, kind: exactOptional } : { form: "no-value" };
671
+ if (row.longPlain.includes(long))
672
+ return attached === undefined ? { form: "no-value" } : { form: "unexplained" };
673
+ const valueNames = [
674
+ ...Object.keys(row.longValue),
675
+ ...(attached !== undefined ? Object.keys(row.longOptionalValue) : []),
676
+ ].filter((full) => isLongOptionAbbrevOf(long, full));
677
+ const plainNames = [
678
+ ...row.longPlain,
679
+ ...(attached === undefined ? Object.keys(row.longOptionalValue) : []),
680
+ ].filter((full) => isLongOptionAbbrevOf(long, full));
681
+ if (valueNames.length === 0)
682
+ return plainNames.length > 0 && attached === undefined ? { form: "no-value" } : { form: "unexplained" };
683
+ const kinds = new Set(valueNames.map((full) => (row.longValue[full] ?? row.longOptionalValue[full])));
684
+ if (attached !== undefined)
685
+ return { form: "attached", value: attached, kind: widestValueKind(kinds) };
686
+ if (plainNames.length > 0 || kinds.size !== 1)
687
+ return { form: "unexplained" };
688
+ return withValue([...kinds][0]);
689
+ }
690
+ for (let i = 1; i < tok.length; i++) {
691
+ const ch = tok[i];
692
+ const kind = row.shortValue[ch];
693
+ if (kind !== undefined) {
694
+ const rest = tok.slice(i + 1);
695
+ return rest !== "" ? { form: "attached", value: rest, kind } : { form: "separated", kind };
696
+ }
697
+ if (!row.shortPlain.includes(ch))
698
+ return { form: "unexplained" };
699
+ }
700
+ return { form: "no-value" };
701
+ }
702
+ function localFileUrlPath(value) {
703
+ const m = /^file:(?:\/\/[^/?#]*)?(\/[^?#]*)/i.exec(value.trim());
704
+ if (m === null)
705
+ return undefined;
706
+ const raw = m[1];
707
+ try {
708
+ return decodeURIComponent(raw);
709
+ }
710
+ catch {
711
+ return raw;
712
+ }
713
+ }
714
+ function formSpecFileNames(value) {
715
+ const out = [];
716
+ const after = (s) => {
717
+ const at = s.search(/[@<]/);
718
+ if (at !== -1 && at < s.length - 1)
719
+ out.push(s.slice(at + 1));
720
+ };
721
+ after(value);
722
+ for (const part of value.split(/[;,]/))
723
+ after(part);
724
+ return [...new Set(out)];
725
+ }
726
+ function slotValueCandidates(value, kind) {
727
+ if (!valueNamesAFile(kind))
728
+ return [];
729
+ if (kind === "url") {
730
+ const local = localFileUrlPath(value);
731
+ return local !== undefined ? [local] : [];
732
+ }
733
+ if (kind === "form")
734
+ return formSpecFileNames(value).map(literalTildeSpelling);
735
+ const named = value.startsWith("@") && value.length > 1 ? literalTildeSpelling(value.slice(1)) : undefined;
736
+ if (kind === "header")
737
+ return named !== undefined ? [value, named] : [];
738
+ const out = [value];
739
+ if (kind === "indirect" && named !== undefined)
740
+ out.push(named);
741
+ return out;
742
+ }
432
743
  const SEPARATED_VALUE_OPTIONS = {
433
744
  head: { short: "cn", long: ["bytes", "lines"] },
434
745
  tail: { short: "cns", long: ["bytes", "lines", "sleep-interval", "pid", "max-unchanged-stats"] },
@@ -1205,17 +1516,6 @@ function isGrepFileStdinLongOption(tok) {
1205
1516
  return name !== undefined && isLongOptionAbbrevOf(name, "file");
1206
1517
  }
1207
1518
  const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
1208
- function grepClusterValueOwner(tok) {
1209
- if (!tok.startsWith("-") || tok.startsWith("--") || !CLUSTER_OPTION_CHAR.test(tok[1] ?? ""))
1210
- return undefined;
1211
- for (const ch of tok.slice(1)) {
1212
- if (ch === "e" || ch === "f")
1213
- return ch;
1214
- if (!CLUSTER_OPTION_CHAR.test(ch))
1215
- return undefined;
1216
- }
1217
- return undefined;
1218
- }
1219
1519
  function attachedOptionPayloads(tok) {
1220
1520
  const literal = literalTildeSpelling;
1221
1521
  const out = [];
@@ -1239,6 +1539,7 @@ function attachedOptionPayloads(tok) {
1239
1539
  }
1240
1540
  function collectSegmentBoundaryFindings(tokens, boundary) {
1241
1541
  const name = tokens.folded[0];
1542
+ const slotVerb = tokens.slotVerb ?? name;
1242
1543
  if (NO_PATH_OPERAND_COMMANDS.has(name))
1243
1544
  return [];
1244
1545
  const args = tokens.folded.slice(1);
@@ -1285,6 +1586,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1285
1586
  const eoo = args.indexOf("--");
1286
1587
  const patternSuppliedByFlag = name === "grep" && (eoo === -1 ? args : args.slice(0, eoo)).some(isGrepPatternFlagToken);
1287
1588
  let sawOperand = false;
1589
+ const argvOperandsStartAt = tokens.slotVerb !== undefined ? 1 : 0;
1590
+ let sawArgvOperand = false;
1288
1591
  let endOfOptions = false;
1289
1592
  for (let k = 0; k < args.length; k++) {
1290
1593
  const t = args[k];
@@ -1295,15 +1598,35 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1295
1598
  endOfOptions = true;
1296
1599
  continue;
1297
1600
  }
1298
- if (name === "cut" && (t === "-d" || t === "--delimiter" || t === "--output-delimiter")) {
1299
- k++;
1300
- continue;
1301
- }
1302
- if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
1303
- continue;
1304
- if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
1305
- continue;
1306
1601
  if (t.startsWith("-") && t !== "-") {
1602
+ const slot = readVerbOptionSlot(slotVerb, t);
1603
+ if (sawArgvOperand) {
1604
+ judgeOperand(t, argGlobs(k));
1605
+ if (slot !== undefined && slot.form === "separated") {
1606
+ const ahead = args[k + 1];
1607
+ if (ahead !== undefined && !expandingArgs.has(k + 1)) {
1608
+ for (const c of slotValueCandidates(ahead, slot.kind))
1609
+ judgeOperand(c, argGlobs(k + 1));
1610
+ }
1611
+ }
1612
+ }
1613
+ if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand)) {
1614
+ if (slot.form === "attached") {
1615
+ for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
1616
+ judgeOperand(c, argGlobs(k));
1617
+ }
1618
+ else if (slot.form === "separated") {
1619
+ const value = args[++k];
1620
+ if (value !== undefined && !expandingArgs.has(k)) {
1621
+ const named = slotValueCandidates(value, slot.kind);
1622
+ for (const c of named)
1623
+ judgeOperand(c, argGlobs(k));
1624
+ if (named.length > 0)
1625
+ positionalOperands.push(value);
1626
+ }
1627
+ }
1628
+ continue;
1629
+ }
1307
1630
  for (const payload of attachedOptionPayloads(t))
1308
1631
  judgeOperand(payload, argGlobs(k));
1309
1632
  continue;
@@ -1311,6 +1634,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1311
1634
  }
1312
1635
  const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
1313
1636
  sawOperand = true;
1637
+ if (k >= argvOperandsStartAt)
1638
+ sawArgvOperand = true;
1314
1639
  if (t === "-") {
1315
1640
  if (isGrepPatternSlot)
1316
1641
  continue;
@@ -1541,7 +1866,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1541
1866
  const unlisted = tokenizeSegment(segments[si]);
1542
1867
  if (unlisted.folded.length === 0)
1543
1868
  continue;
1544
- foldedSegments.push({ folded: [LAUNCHER_SCAN_HEAD, ...unlisted.folded], raw: [LAUNCHER_SCAN_HEAD, ...unlisted.raw] });
1869
+ foldedSegments.push({ folded: [LAUNCHER_SCAN_HEAD, ...unlisted.folded], raw: [LAUNCHER_SCAN_HEAD, ...unlisted.raw], slotVerb: programBasename(unlisted.folded[0]) });
1545
1870
  pushReach();
1546
1871
  continue;
1547
1872
  }
@@ -1632,10 +1957,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1632
1957
  shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
1633
1958
  }
1634
1959
  if (boundary !== undefined) {
1635
- const walk = evaluateReadBoundary(foldedSegments, boundary, foldedReach);
1960
+ const walk = evaluateReadBoundary(foldedSegments, boundary, true, foldedReach);
1636
1961
  if (shapeRefusal === undefined)
1637
1962
  return walk;
1638
- const { reason: _walkSentence, ...structure } = walk;
1963
+ const { reason: _walkSentence, outOfRootGrantCandidate: _walkGrantCandidate, ...structure } = walk;
1639
1964
  return { ...structure, reason: shapeRefusal };
1640
1965
  }
1641
1966
  if (shapeRefusal !== undefined)
@@ -1646,7 +1971,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1646
1971
  return {};
1647
1972
  }
1648
1973
  const BASE_CANDIDATE_MAX = 256;
1649
- function evaluateReadBoundary(foldedSegments, boundary, reach) {
1974
+ function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reach) {
1650
1975
  const seen = { outside: new Set(), inside: new Set(), undecided: new Set(), recursive: new Set(), unresolved: new Set(), checkedSpelled: new Set(), undecidedSpelled: new Set(), outsideSpelled: new Set() };
1651
1976
  const outside = [];
1652
1977
  const inside = [];
@@ -1691,7 +2016,7 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
1691
2016
  if (si > 0 && reach?.[si]?.andJoined !== true)
1692
2017
  andRunStart = reach?.[si]?.orBroken === true ? si + 1 : si;
1693
2018
  const reachable = candidates.filter((c) => c.skippedCd < andRunStart);
1694
- const row = findingsRowFor(JSON.stringify([toks.folded, toks.raw]));
2019
+ const row = findingsRowFor(JSON.stringify([toks.folded, toks.raw, toks.slotVerb]));
1695
2020
  for (const { base: currentBase } of reachable) {
1696
2021
  for (const finding of findingsFor(row, toks, currentBase)) {
1697
2022
  if (finding.kind === "unresolvable") {
@@ -1804,10 +2129,20 @@ function evaluateReadBoundary(foldedSegments, boundary, reach) {
1804
2129
  return { ...checkedField, ...undecidedField };
1805
2130
  const paths = outside.map((o) => `"${o.path}"`).join(", ");
1806
2131
  const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
2132
+ const grantCandidate = (() => {
2133
+ if (!mintGrantCandidate || outOfRootSpellings.length > 0)
2134
+ return undefined;
2135
+ const proposed = outOfRootGrantCandidateOf(outside.map((o) => o.path), boundary);
2136
+ if (proposed === undefined)
2137
+ return undefined;
2138
+ const replay = evaluateReadBoundary(foldedSegments, { ...boundary, roots: [...boundary.roots, proposed] }, false, reach);
2139
+ return replay.reason === undefined && (replay.undecidedPaths ?? []).length === 0 ? proposed : undefined;
2140
+ })();
1807
2141
  return {
1808
2142
  reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} ${NOT_AUTO_ALLOWED}`,
1809
2143
  outOfRootRead: true,
1810
2144
  outOfRootPaths: outside.map((o) => o.path),
2145
+ ...(grantCandidate !== undefined ? { outOfRootGrantCandidate: grantCandidate } : {}),
1811
2146
  ...outsideField,
1812
2147
  ...checkedField,
1813
2148
  ...undecidedField,
@@ -1825,7 +2160,7 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
1825
2160
  if (familyRefusal !== undefined)
1826
2161
  return { reason: familyRefusal, refusedWhole: true };
1827
2162
  }
1828
- return evaluateReadBoundary([toks], boundary);
2163
+ return evaluateReadBoundary([toks], boundary, true);
1829
2164
  }
1830
2165
  const NOT_GATED = { gated: false, paths: [] };
1831
2166
  const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}~]/;
@@ -1876,7 +2211,7 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
1876
2211
  if (UNMODELLED_BASE_MOVERS.has(name))
1877
2212
  baseMoved = true;
1878
2213
  if (!allow.has(name) && name !== "cd") {
1879
- pushReader({ folded: [LAUNCHER_SCAN_HEAD, ...toks.folded.slice(i)], raw: [LAUNCHER_SCAN_HEAD, ...toks.raw.slice(i)] });
2214
+ pushReader({ folded: [LAUNCHER_SCAN_HEAD, ...toks.folded.slice(i)], raw: [LAUNCHER_SCAN_HEAD, ...toks.raw.slice(i)], slotVerb: name });
1880
2215
  continue;
1881
2216
  }
1882
2217
  pushReader(i === 0 && name === written ? toks : { folded: [name, ...toks.folded.slice(i + 1)], raw: [toks.raw[i] ?? name, ...toks.raw.slice(i + 1)] });
@@ -1885,7 +2220,7 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
1885
2220
  return NOT_GATED;
1886
2221
  if (baseMoved)
1887
2222
  return { gated: true, paths: [] };
1888
- const verdict = evaluateReadBoundary(readers, containmentOnly, readerReach);
2223
+ const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
1889
2224
  return verdict.outOfRootRead === true ? { gated: true, paths: verdict.outOfRootPaths ?? [] } : NOT_GATED;
1890
2225
  };
1891
2226
  const direct = judge(command);
@@ -100,7 +100,7 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
100
100
  * add a mandate to an ask, never originate one). The poll-loop face's structured verdict
101
101
  * ({@link classifyBoundedReadonlyPollLoopDetailed}) is read the same three ways as the compound face's.
102
102
  *
103
- * The reader set is the engine's classify set ({@link BASH_CLASSIFY_DEFAULT_ALLOW}) — here it is NOT an
103
+ * The reader set is the engine's classify set ({@link bashClassifyDefaultAllow}) — here it is NOT an
104
104
  * auto-allow list but the set of programs whose operands the faces read; a wider set means more reads are
105
105
  * judged, never that more runs are vouched.
106
106
  *