@sema-agent/core 7.19.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 (106) hide show
  1. package/CHANGELOG.md +183 -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 +3 -1
  6. package/dist/brain/open-responses.js +4 -2
  7. package/dist/brain/openai.js +3 -1
  8. package/dist/brain/stream-engine.d.ts +4 -0
  9. package/dist/brain/stream-engine.js +4 -0
  10. package/dist/core/auto-mode.d.ts +37 -18
  11. package/dist/core/auto-mode.js +1 -1
  12. package/dist/core/child-clamp-seats.d.ts +79 -0
  13. package/dist/core/child-clamp-seats.js +29 -0
  14. package/dist/core/engine-notice.d.ts +67 -49
  15. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  16. package/dist/core/fs-write-gate-policy.js +51 -4
  17. package/dist/core/gate-bounded-await.d.ts +38 -0
  18. package/dist/core/gate-bounded-await.js +53 -0
  19. package/dist/core/gate-fold.js +16 -48
  20. package/dist/core/gate-lanes.js +11 -3
  21. package/dist/core/gate-outcome.d.ts +9 -6
  22. package/dist/core/governance-codes.d.ts +16 -1
  23. package/dist/core/governance-codes.js +12 -0
  24. package/dist/core/hooks.d.ts +8 -8
  25. package/dist/core/memory-engine/engine.d.ts +14 -9
  26. package/dist/core/memory-engine/engine.js +21 -8
  27. package/dist/core/memory-engine/layout.d.ts +31 -0
  28. package/dist/core/memory-engine/layout.js +68 -3
  29. package/dist/core/memory-engine/types.d.ts +24 -0
  30. package/dist/core/memory.d.ts +19 -0
  31. package/dist/core/memory.js +12 -0
  32. package/dist/core/remote-env.d.ts +32 -0
  33. package/dist/core/remote-env.js +4 -1
  34. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  35. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  36. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  37. package/dist/core/runner/announce-once-ledger.js +8 -0
  38. package/dist/core/runner/classifier-ladder.d.ts +5 -0
  39. package/dist/core/runner/classifier-ladder.js +13 -2
  40. package/dist/core/runner/contracts.d.ts +44 -1
  41. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  42. package/dist/core/runner/denial-limit-arms.js +5 -4
  43. package/dist/core/runner/env-capability-census.d.ts +30 -0
  44. package/dist/core/runner/env-capability-census.js +38 -0
  45. package/dist/core/runner/forward-sink.d.ts +32 -0
  46. package/dist/core/runner/forward-sink.js +41 -0
  47. package/dist/core/runner/gate-exit.d.ts +4 -0
  48. package/dist/core/runner/gate-exit.js +4 -1
  49. package/dist/core/runner/git-status-frame.d.ts +0 -9
  50. package/dist/core/runner/listing-fold.d.ts +57 -0
  51. package/dist/core/runner/listing-fold.js +75 -0
  52. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  53. package/dist/core/runner/prepare-execution-env.js +2 -0
  54. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  55. package/dist/core/runner/prepare-gate-stations.js +6 -2
  56. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  57. package/dist/core/runner/prepare-hands-readface.js +3 -5
  58. package/dist/core/runner/prepare-listings.d.ts +3 -0
  59. package/dist/core/runner/prepare-listings.js +4 -2
  60. package/dist/core/runner/prepare-memory.js +7 -1
  61. package/dist/core/runner/prepare-policy-chain.js +5 -5
  62. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  63. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  64. package/dist/core/runner/prepare-run-refs.js +5 -4
  65. package/dist/core/runner/prepare-task.js +7 -5
  66. package/dist/core/runner/run-clock-and-content.js +3 -7
  67. package/dist/core/runner/run-harness-handlers.js +10 -10
  68. package/dist/core/runner/run-leg.js +28 -19
  69. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  70. package/dist/core/runner/run-turn-boundary.js +44 -33
  71. package/dist/core/runner/runtask.js +2 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +13 -39
  73. package/dist/core/runner/synthetic-tools.js +15 -59
  74. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  75. package/dist/core/runner/task-root-canonical.js +18 -0
  76. package/dist/core/runner/turn-attachments.d.ts +125 -49
  77. package/dist/core/runner/turn-attachments.js +124 -66
  78. package/dist/core/safe-notify.d.ts +5 -0
  79. package/dist/core/safe-notify.js +24 -2
  80. package/dist/core/sensitive-path-policy.d.ts +53 -27
  81. package/dist/core/sensitive-path-policy.js +16 -26
  82. package/dist/core/skill-listing-budget.d.ts +34 -40
  83. package/dist/core/skill-listing-budget.js +25 -26
  84. package/dist/core/skills-directory.js +17 -10
  85. package/dist/core/task-registry.js +2 -1
  86. package/dist/core/task-spec.d.ts +8 -7
  87. package/dist/core/tool-policy.d.ts +12 -1
  88. package/dist/core/tool-policy.js +39 -25
  89. package/dist/core/write-protect.d.ts +98 -18
  90. package/dist/core/write-protect.js +88 -34
  91. package/dist/index.d.ts +6 -5
  92. package/dist/index.js +5 -4
  93. package/dist/orchestration/workflow-primitives.js +10 -20
  94. package/dist/stores/file/memory-store.d.ts +5 -2
  95. package/dist/stores/file/memory-store.js +8 -4
  96. package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -2
  97. package/dist/tools/fs/bash-readonly-classifier.js +317 -35
  98. package/dist/tools/fs/fs-bash.d.ts +1 -1
  99. package/dist/tools/fs/fs-bash.js +12 -25
  100. package/dist/tools/fs/fs-shared.d.ts +22 -0
  101. package/dist/tools/fs/fs-shared.js +30 -0
  102. package/dist/tools/fs/index.d.ts +1 -1
  103. package/dist/tools/fs/index.js +1 -1
  104. package/package.json +2 -1
  105. package/test/export-surface.snapshot.json +19 -3
  106. package/wire-changes/core@7.20.0.json +568 -0
@@ -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)
@@ -446,18 +449,6 @@ function isGrepPatternFlagToken(tok) {
446
449
  }
447
450
  return /^-[A-Za-z0-9]*[ef]/.test(tok);
448
451
  }
449
- function isGrepPatternPayloadLongOption(tok) {
450
- if (!tok.includes("="))
451
- return false;
452
- const name = longOptionNameOf(tok);
453
- return name !== undefined && isLongOptionAbbrevOf(name, "regexp");
454
- }
455
- function isCutDelimiterPayloadLongOption(tok) {
456
- if (!tok.includes("="))
457
- return false;
458
- const name = longOptionNameOf(tok);
459
- return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
460
- }
461
452
  function isGrepClusterFileStdin(tok) {
462
453
  if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
463
454
  return false;
@@ -472,6 +463,283 @@ function isGrepClusterFileStdin(tok) {
472
463
  }
473
464
  return false;
474
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
+ }
475
743
  const SEPARATED_VALUE_OPTIONS = {
476
744
  head: { short: "cn", long: ["bytes", "lines"] },
477
745
  tail: { short: "cns", long: ["bytes", "lines", "sleep-interval", "pid", "max-unchanged-stats"] },
@@ -1248,17 +1516,6 @@ function isGrepFileStdinLongOption(tok) {
1248
1516
  return name !== undefined && isLongOptionAbbrevOf(name, "file");
1249
1517
  }
1250
1518
  const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
1251
- function grepClusterValueOwner(tok) {
1252
- if (!tok.startsWith("-") || tok.startsWith("--") || !CLUSTER_OPTION_CHAR.test(tok[1] ?? ""))
1253
- return undefined;
1254
- for (const ch of tok.slice(1)) {
1255
- if (ch === "e" || ch === "f")
1256
- return ch;
1257
- if (!CLUSTER_OPTION_CHAR.test(ch))
1258
- return undefined;
1259
- }
1260
- return undefined;
1261
- }
1262
1519
  function attachedOptionPayloads(tok) {
1263
1520
  const literal = literalTildeSpelling;
1264
1521
  const out = [];
@@ -1282,6 +1539,7 @@ function attachedOptionPayloads(tok) {
1282
1539
  }
1283
1540
  function collectSegmentBoundaryFindings(tokens, boundary) {
1284
1541
  const name = tokens.folded[0];
1542
+ const slotVerb = tokens.slotVerb ?? name;
1285
1543
  if (NO_PATH_OPERAND_COMMANDS.has(name))
1286
1544
  return [];
1287
1545
  const args = tokens.folded.slice(1);
@@ -1328,6 +1586,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1328
1586
  const eoo = args.indexOf("--");
1329
1587
  const patternSuppliedByFlag = name === "grep" && (eoo === -1 ? args : args.slice(0, eoo)).some(isGrepPatternFlagToken);
1330
1588
  let sawOperand = false;
1589
+ const argvOperandsStartAt = tokens.slotVerb !== undefined ? 1 : 0;
1590
+ let sawArgvOperand = false;
1331
1591
  let endOfOptions = false;
1332
1592
  for (let k = 0; k < args.length; k++) {
1333
1593
  const t = args[k];
@@ -1338,15 +1598,35 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1338
1598
  endOfOptions = true;
1339
1599
  continue;
1340
1600
  }
1341
- if (name === "cut" && (t === "-d" || t === "--delimiter" || t === "--output-delimiter")) {
1342
- k++;
1343
- continue;
1344
- }
1345
- if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
1346
- continue;
1347
- if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
1348
- continue;
1349
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
+ }
1350
1630
  for (const payload of attachedOptionPayloads(t))
1351
1631
  judgeOperand(payload, argGlobs(k));
1352
1632
  continue;
@@ -1354,6 +1634,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1354
1634
  }
1355
1635
  const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
1356
1636
  sawOperand = true;
1637
+ if (k >= argvOperandsStartAt)
1638
+ sawArgvOperand = true;
1357
1639
  if (t === "-") {
1358
1640
  if (isGrepPatternSlot)
1359
1641
  continue;
@@ -1584,7 +1866,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1584
1866
  const unlisted = tokenizeSegment(segments[si]);
1585
1867
  if (unlisted.folded.length === 0)
1586
1868
  continue;
1587
- 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]) });
1588
1870
  pushReach();
1589
1871
  continue;
1590
1872
  }
@@ -1734,7 +2016,7 @@ function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reac
1734
2016
  if (si > 0 && reach?.[si]?.andJoined !== true)
1735
2017
  andRunStart = reach?.[si]?.orBroken === true ? si + 1 : si;
1736
2018
  const reachable = candidates.filter((c) => c.skippedCd < andRunStart);
1737
- const row = findingsRowFor(JSON.stringify([toks.folded, toks.raw]));
2019
+ const row = findingsRowFor(JSON.stringify([toks.folded, toks.raw, toks.slotVerb]));
1738
2020
  for (const { base: currentBase } of reachable) {
1739
2021
  for (const finding of findingsFor(row, toks, currentBase)) {
1740
2022
  if (finding.kind === "unresolvable") {
@@ -1929,7 +2211,7 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
1929
2211
  if (UNMODELLED_BASE_MOVERS.has(name))
1930
2212
  baseMoved = true;
1931
2213
  if (!allow.has(name) && name !== "cd") {
1932
- 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 });
1933
2215
  continue;
1934
2216
  }
1935
2217
  pushReader(i === 0 && name === written ? toks : { folded: [name, ...toks.folded.slice(i + 1)], raw: [toks.raw[i] ?? name, ...toks.raw.slice(i + 1)] });
@@ -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
  *
@@ -9,11 +9,11 @@ import { hasBackgroundShell } from "../../core/background-shell.js";
9
9
  import { delimitUntrusted } from "../../core/untrusted-text.js";
10
10
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
11
11
  import { imageMagicMatches, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
12
- import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
12
+ import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsolated } from "../../core/remote-env.js";
13
13
  import { ghRateLimitHint } from "./gh-rate-limit.js";
14
- import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
14
+ import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
15
15
  import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
16
- import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
16
+ import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
17
17
  import { toolFace } from "../../core/tool-catalog-entries.js";
18
18
  const RECURSIVE_CAUSE_MAX_PATHS = 3;
19
19
  const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
@@ -25,7 +25,7 @@ function operandFamily(paths) {
25
25
  };
26
26
  }
27
27
  export function bashReversibilityProbe(allow, boundary) {
28
- const allowSet = new Set(allow ?? BASH_CLASSIFY_DEFAULT_ALLOW);
28
+ const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
29
29
  return (args) => {
30
30
  const command = shellCommandOf(args);
31
31
  if (command === undefined)
@@ -109,7 +109,7 @@ function recursiveReadCause(detailed) {
109
109
  };
110
110
  }
111
111
  export function bashReadBoundaryProbe(boundary) {
112
- const allowSet = new Set(BASH_CLASSIFY_DEFAULT_ALLOW);
112
+ const allowSet = new Set(bashClassifyDefaultAllow());
113
113
  return (args) => {
114
114
  const command = shellCommandOf(args);
115
115
  if (command === undefined)
@@ -402,23 +402,6 @@ function createEpilogueFilter(marker) {
402
402
  return out + buf;
403
403
  };
404
404
  }
405
- function readCwdEpilogue(stderr, marker) {
406
- let out = "";
407
- let rest = stderr;
408
- let cwd;
409
- for (;;) {
410
- const open = rest.indexOf(marker);
411
- if (open === -1)
412
- break;
413
- const close = rest.indexOf(marker, open + marker.length);
414
- if (close === -1)
415
- break;
416
- out += rest.slice(0, open);
417
- cwd = rest.slice(open + marker.length, close);
418
- rest = rest.slice(close + marker.length);
419
- }
420
- return { stderr: out + rest, cwd };
421
- }
422
405
  function describeSafely(sink, body) {
423
406
  if (sink === undefined)
424
407
  return;
@@ -582,7 +565,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
582
565
  let { stderr } = res.value;
583
566
  const { stdout, exitCode } = res.value;
584
567
  if (cwdRef) {
585
- const epilogue = readCwdEpilogue(stderr, cwdMarker);
568
+ const epilogue = stripCwdSentinel(stderr, cwdMarker);
586
569
  stderr = epilogue.stderr;
587
570
  if (epilogue.cwd !== undefined) {
588
571
  const captured = epilogue.cwd.trim();
@@ -736,7 +719,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
736
719
  const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
737
720
  const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
738
721
  const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
739
- const recheckReaders = new Set(BASH_CLASSIFY_DEFAULT_ALLOW);
722
+ const recheckReaders = new Set(bashClassifyDefaultAllow());
740
723
  let canonicalForRoots;
741
724
  const canonicalBoundaryFor = (roots) => {
742
725
  const key = roots.join("\u0000");
@@ -1070,6 +1053,7 @@ function pathSegmentsFrom(p, from) {
1070
1053
  }
1071
1054
  function createCanonicalReadBoundary(env, readRoots) {
1072
1055
  const lexicalOnly = boundaryIsTheSandbox(env);
1056
+ const resolverIsAuthoritative = canonicalPathIsAuthoritative(env);
1073
1057
  let rootsOnce;
1074
1058
  const canonicalRoots = async (signal) => {
1075
1059
  rootsOnce ??= (async () => {
@@ -1229,8 +1213,11 @@ function createCanonicalReadBoundary(env, readRoots) {
1229
1213
  let unresolvable;
1230
1214
  const judge = async (p) => {
1231
1215
  const real = await resolve(p, signal);
1232
- if (real === undefined)
1216
+ if (real === undefined) {
1217
+ if (resolverIsAuthoritative)
1218
+ unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
1233
1219
  return;
1220
+ }
1234
1221
  if (!real.exact && real.missing !== true) {
1235
1222
  unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
1236
1223
  return;
@@ -361,6 +361,28 @@ export declare function shellRecoveryHint(path: string, readOnly: boolean | unde
361
361
  * (NOT NotebookEdit — CC ships it without the trailer). */
362
362
  export declare const FILE_STATE_TRAILER = " (file state is current in your context \u2014 no need to Read it back)";
363
363
  export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
364
+ /**
365
+ * Take the engine's own cwd epilogue out of a finished stderr, and report the working directory the
366
+ * command left behind (the payload of the LAST span).
367
+ *
368
+ * EVERY span, and the LAST one — not the first, and not just one. A command that turns on shell tracing
369
+ * produces more than one (the shell echoes the epilogue's own source, marker and all, before running
370
+ * it), and under a one-pair rule the FIRST pair was that echo, whose payload is the unexpanded
371
+ * `' "$(pwd)" '`; a `cd` that plainly worked was then dropped while the real record stayed in the stderr
372
+ * a person reads. An opener with no close is NOT ours to eat — the command's own text is left exactly
373
+ * as it arrived.
374
+ *
375
+ * TWO CALLERS, ONE RULE, which is why the marker is optional. The foreground result path KNOWS the
376
+ * marker it minted and passes it (exact, unambiguous). A backgrounded command's spool is assembled by a
377
+ * different lane entirely — the shell row outlives the run that launched it and never had the marker —
378
+ * so it passes none and the marker is recovered per span from the sentinel plus its hex suffix. Before
379
+ * this was shared, only the foreground path stripped, and the completion notification handed a person
380
+ * the raw internal marker with the directory inside it.
381
+ */
382
+ export declare function stripCwdSentinel(stderr: string, marker?: string): {
383
+ stderr: string;
384
+ cwd: string | undefined;
385
+ };
364
386
  /** Convert a model-supplied millisecond timeout to our internal seconds, clamped. Exported for the
365
387
  * cc-parity unit (model sees ms, internal stays seconds — no ×1000 error).
366
388
  * ⚠️ This returns the EFFECTIVE budget, not the request — capping at a call site destroys the
@@ -247,6 +247,36 @@ export function shellRecoveryHint(path, readOnly) {
247
247
  }
248
248
  export const FILE_STATE_TRAILER = " (file state is current in your context — no need to Read it back)";
249
249
  export const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
250
+ const CWD_SENTINEL_SUFFIX_CHARS = 16;
251
+ function cwdMarkerAt(text, open) {
252
+ const after = open + CWD_SENTINEL.length;
253
+ const suffix = /^[0-9a-f]{16}/.exec(text.slice(after, after + CWD_SENTINEL_SUFFIX_CHARS));
254
+ return suffix === null ? undefined : CWD_SENTINEL + suffix[0];
255
+ }
256
+ export function stripCwdSentinel(stderr, marker) {
257
+ let out = "";
258
+ let rest = stderr;
259
+ let cwd;
260
+ let from = 0;
261
+ for (;;) {
262
+ const open = rest.indexOf(marker ?? CWD_SENTINEL, from);
263
+ if (open === -1)
264
+ break;
265
+ const span = marker ?? cwdMarkerAt(rest, open);
266
+ if (span === undefined) {
267
+ from = open + CWD_SENTINEL.length;
268
+ continue;
269
+ }
270
+ const close = rest.indexOf(span, open + span.length);
271
+ if (close === -1)
272
+ break;
273
+ out += rest.slice(0, open);
274
+ cwd = rest.slice(open + span.length, close);
275
+ rest = rest.slice(close + span.length);
276
+ from = 0;
277
+ }
278
+ return { stderr: out + rest, cwd };
279
+ }
250
280
  export function msTimeoutToSec(timeoutMs, caps = resolveBashTimeoutCaps()) {
251
281
  const { defaultSec, maxSec } = bashTimeoutCapsSec(caps);
252
282
  if (timeoutMs === undefined || !Number.isFinite(timeoutMs))
@@ -12,7 +12,7 @@ export * from "./bash-readonly-classifier.js";
12
12
  export * from "./fs-bash.js";
13
13
  export * from "./read-deny.js";
14
14
  import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
15
- export { BASH_CLASSIFY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
15
+ export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
16
16
  import { type ReadDenyEntry } from "./read-deny.js";
17
17
  import { type ContainmentFace } from "./containment-face.js";
18
18
  export * from "./containment-face.js";
@@ -12,7 +12,7 @@ export * from "./bash-readonly-classifier.js";
12
12
  export * from "./fs-bash.js";
13
13
  export * from "./read-deny.js";
14
14
  import { BASH_READONLY_DEFAULT_ALLOW, } from "./bash-readonly-classifier.js";
15
- export { BASH_CLASSIFY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
15
+ export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
16
16
  import { compileReadDeny } from "./read-deny.js";
17
17
  import { deploymentFaceClampNotice, resolveContainmentFace } from "./containment-face.js";
18
18
  export * from "./containment-face.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.19.0",
3
+ "version": "7.20.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -84,6 +84,7 @@
84
84
  "gate:tool-names": "node scripts/verify-tool-name-tables.mjs",
85
85
  "gate:tool-docs": "node scripts/gen-tool-spec-fields.mjs --check",
86
86
  "gate:backlog-ledger": "node scripts/verify-backlog-ledger.mjs",
87
+ "gate:child-clamp-seats": "node scripts/verify-child-clamp-seats.mjs",
87
88
  "gate:docs-tool-names": "node scripts/verify-docs-tool-names.mjs",
88
89
  "gate:comment-ratio": "vitest run test/comment-ratio-ratchet.test.ts",
89
90
  "gate:negative-controls": "vitest run test/gate-negative-controls.test.ts",