@sema-agent/core 7.22.0 → 7.23.1

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 (60) hide show
  1. package/CHANGELOG.md +210 -1
  2. package/dist/core/auto-compaction.d.ts +2 -2
  3. package/dist/core/checkpoint-store.d.ts +1 -5
  4. package/dist/core/context-edit.js +1 -1
  5. package/dist/core/engine-notice.d.ts +13 -0
  6. package/dist/core/gate-bounded-await.d.ts +14 -4
  7. package/dist/core/gate-bounded-await.js +1 -1
  8. package/dist/core/gate-crash-trace.d.ts +7 -0
  9. package/dist/core/gate-crash-trace.js +3 -0
  10. package/dist/core/gate-fold.js +5 -4
  11. package/dist/core/gate-lanes.js +2 -1
  12. package/dist/core/governance-codes.d.ts +1 -1
  13. package/dist/core/governance-codes.js +2 -0
  14. package/dist/core/hooks.js +2 -1
  15. package/dist/core/park-selfcheck.js +1 -1
  16. package/dist/core/path-identity.d.ts +11 -4
  17. package/dist/core/path-identity.js +5 -2
  18. package/dist/core/read-root-candidate.js +11 -1
  19. package/dist/core/runner/assemble-result.d.ts +2 -7
  20. package/dist/core/runner/assemble-result.js +25 -8
  21. package/dist/core/runner/contracts.d.ts +15 -15
  22. package/dist/core/runner/gate-exit.d.ts +0 -5
  23. package/dist/core/runner/gate-exit.js +0 -3
  24. package/dist/core/runner/inherited-ask-grants.d.ts +2 -7
  25. package/dist/core/runner/inherited-ask-grants.js +1 -0
  26. package/dist/core/runner/prepare-ask-lane.js +1 -0
  27. package/dist/core/runner/prepare-context-lane.d.ts +43 -1
  28. package/dist/core/runner/prepare-context-lane.js +25 -4
  29. package/dist/core/runner/prepare-task.js +2 -2
  30. package/dist/core/runner/prepare-turn-wiring.js +2 -2
  31. package/dist/core/runner/run-harness-handlers.js +4 -2
  32. package/dist/core/runner/run-recovery-lanes.js +6 -3
  33. package/dist/core/runner/run-telemetry-and-budget-seats.js +11 -1
  34. package/dist/core/runner/write-protection-station.d.ts +7 -25
  35. package/dist/core/runner/write-protection-station.js +18 -0
  36. package/dist/core/task-registry.d.ts +0 -6
  37. package/dist/core/task-result.d.ts +28 -10
  38. package/dist/core/types.d.ts +1 -1
  39. package/dist/core/write-protect.d.ts +64 -12
  40. package/dist/core/write-protect.js +87 -34
  41. package/dist/engine/execution-env/node-execution-env.d.ts +19 -2
  42. package/dist/engine/execution-env/node-execution-env.js +5 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/tools/fs/bash-lexer.d.ts +12 -5
  45. package/dist/tools/fs/bash-lexer.js +5 -2
  46. package/dist/tools/fs/bash-program-position.d.ts +18 -0
  47. package/dist/tools/fs/bash-program-position.js +13 -1
  48. package/dist/tools/fs/bash-readonly-classifier.d.ts +10 -3
  49. package/dist/tools/fs/bash-readonly-classifier.js +70 -41
  50. package/dist/tools/fs/fs-bash.d.ts +22 -0
  51. package/dist/tools/fs/fs-bash.js +19 -12
  52. package/dist/tools/fs/fs-read.js +2 -1
  53. package/dist/tools/fs/fs-shared.d.ts +33 -7
  54. package/dist/tools/fs/fs-shared.js +12 -1
  55. package/dist/tools/fs/safety.d.ts +37 -0
  56. package/dist/tools/fs/safety.js +31 -3
  57. package/package.json +2 -2
  58. package/test/export-surface.snapshot.json +3 -1
  59. package/wire-changes/core@7.23.0.json +316 -0
  60. package/wire-changes/core@7.23.1.json +205 -0
@@ -1,7 +1,6 @@
1
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
- import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
4
- export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
3
+ import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRunsShell, shellScanDeclineOf, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
5
4
  export const BASH_READONLY_DEFAULT_ALLOW = [
6
5
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
7
6
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -52,7 +51,8 @@ export function coarseReadonlyCheck(command, allow, options) {
52
51
  return `command "${parsed.name}" is not in the read-only allowlist`;
53
52
  return undefined;
54
53
  }
55
- const SHELL_SEGMENT_HARD_REJECT = /[<>$()`\n\r\\]/;
54
+ const SHELL_SEGMENT_QUOTE_BLIND_REJECT = /[<>()`\n\r\\]/;
55
+ const SHELL_EXPANSION_CHARS = /[$]/;
56
56
  export function carriesShellRedirection(text) {
57
57
  return SHELL_REDIRECTION_OPERATORS.test(text);
58
58
  }
@@ -78,10 +78,25 @@ function rejectedSansRedirection(source) {
78
78
  if (redirectionPrecedesConnector(source)) {
79
79
  return "a redirection followed by `|`, `&` or `;` is not allowed — the operator's own characters cannot be told apart from a command boundary";
80
80
  }
81
+ return hardRejectReason(source, true);
82
+ }
83
+ function hardRejectReason(source, tolerateRedirection) {
84
+ const reason = tolerateRedirection
85
+ ? "command/variable substitution, subshells, escapes, and line breaks are not allowed"
86
+ : "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
81
87
  for (const ch of source) {
82
- if (SHELL_SEGMENT_HARD_REJECT.test(ch) && !SHELL_REDIRECTION_OPERATORS.test(ch)) {
83
- return "command/variable substitution, subshells, escapes, and line breaks are not allowed";
84
- }
88
+ if (SHELL_SEGMENT_QUOTE_BLIND_REJECT.test(ch) && !(tolerateRedirection && SHELL_REDIRECTION_OPERATORS.test(ch)))
89
+ return reason;
90
+ }
91
+ if (!SHELL_EXPANSION_CHARS.test(source))
92
+ return undefined;
93
+ const mask = quoteMask(source);
94
+ for (let i = 0; i < source.length; i++) {
95
+ if (!SHELL_EXPANSION_CHARS.test(source[i]))
96
+ continue;
97
+ if (mask.balanced && mask.openedBy[i] === "'")
98
+ continue;
99
+ return reason;
85
100
  }
86
101
  return undefined;
87
102
  }
@@ -91,8 +106,10 @@ export function splitShellCompoundSegments(source, options) {
91
106
  if (refused !== undefined)
92
107
  return { reject: refused };
93
108
  }
94
- else if (SHELL_SEGMENT_HARD_REJECT.test(source)) {
95
- return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
109
+ else {
110
+ const refused = hardRejectReason(source, false);
111
+ if (refused !== undefined)
112
+ return { reject: refused };
96
113
  }
97
114
  const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
98
115
  const lexed = lexShellText(scanned, { mode: "strict" });
@@ -239,8 +256,8 @@ function tokensMoveBase(toks) {
239
256
  return pos.refusal !== undefined || pos.program?.basename === "cd";
240
257
  }
241
258
  const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
242
- export function refusedCommandMayRead(command, allow, boundary) {
243
- return !refusedCommandExempt(command, allow, boundary, 0);
259
+ export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
260
+ return !refusedCommandExempt(command, allow, boundary, declinesAreEvidence, 0);
244
261
  }
245
262
  const STDIN_ONLY_READER_FLAGS = new Map([
246
263
  ["cat", /^-[AbeEnstTuv]+$/],
@@ -262,7 +279,7 @@ function whitelistSegment(segment) {
262
279
  redirects: tokens.filter((t) => t.kind === "redirect"),
263
280
  };
264
281
  }
265
- function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
282
+ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, depth, baseMovedOutside = false) {
266
283
  if (depth > 4)
267
284
  return false;
268
285
  const lexed = lexShellText(command, { mode: "lenient" });
@@ -282,12 +299,21 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
282
299
  }
283
300
  segments.push(whitelistSegment(segment));
284
301
  }
302
+ let declined;
303
+ for (const { words } of segments) {
304
+ const pos = readProgramPosition(words);
305
+ if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE))
306
+ return false;
307
+ const decline = shellScanDeclineOf(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED);
308
+ if (decline !== undefined && declinesAreEvidence.has(decline))
309
+ return false;
310
+ declined ??= decline;
311
+ }
312
+ if (declined !== undefined)
313
+ return true;
285
314
  const baseMoved = baseMovedOutside ||
286
315
  segments.some(({ words }) => {
287
- const pos = readProgramPosition(words);
288
- if (pos.refusal !== undefined)
289
- return true;
290
- const runs = pos.program?.basename;
316
+ const runs = readProgramPosition(words).program?.basename;
291
317
  return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
292
318
  });
293
319
  for (const body of bodies) {
@@ -296,7 +322,7 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
296
322
  (walked.undecidedPaths?.length ?? 0) === 0 && (walked.unresolvedOperands?.length ?? 0) === 0 && (walked.recursiveReadPaths?.length ?? 0) === 0;
297
323
  if (fullyGreen)
298
324
  continue;
299
- if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
325
+ if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, depth + 1, baseMoved))
300
326
  return false;
301
327
  }
302
328
  const stdinFileJudgedInside = (word) => {
@@ -353,8 +379,6 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
353
379
  if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
354
380
  return false;
355
381
  const pos = readProgramPosition(words);
356
- if (pos.refusal !== undefined)
357
- return false;
358
382
  for (const k of words.folded.keys()) {
359
383
  const word = words.folded[k];
360
384
  if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
@@ -365,24 +389,16 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
365
389
  if (program === undefined) {
366
390
  if (pos.launchers.length === 0)
367
391
  continue;
368
- if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
369
- return false;
370
392
  if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
371
393
  return false;
372
394
  continue;
373
395
  }
374
396
  if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
375
397
  return false;
376
- if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
377
- return false;
378
- if (pos.launchers.length > 0) {
379
- if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
398
+ if (pos.launchers.length > 0 && !listed(program.basename)) {
399
+ if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
380
400
  return false;
381
- if (!listed(program.basename)) {
382
- if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
383
- return false;
384
- continue;
385
- }
401
+ continue;
386
402
  }
387
403
  const head = program.basename;
388
404
  if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
@@ -455,12 +471,12 @@ function longOptionNameOf(tok) {
455
471
  function isLongOptionAbbrevOf(name, full) {
456
472
  return full.startsWith(name);
457
473
  }
458
- function isGrepPatternFlagToken(tok) {
474
+ function suppliesFirstOperandSlot(slot, tok) {
459
475
  if (tok.startsWith("--")) {
460
476
  const long = longOptionNameOf(tok);
461
- return long !== undefined && (isLongOptionAbbrevOf(long, "regexp") || isLongOptionAbbrevOf(long, "file"));
477
+ return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionAbbrevOf(long, full));
462
478
  }
463
- return /^-[A-Za-z0-9]*[ef]/.test(tok);
479
+ return new RegExp(`^-[A-Za-z0-9]*[${slot.suppliedByLetters}]`).test(tok);
464
480
  }
465
481
  function isGrepClusterFileStdin(tok) {
466
482
  if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
@@ -534,6 +550,7 @@ const VERB_OPTION_SLOTS = {
534
550
  regexp: "pattern",
535
551
  },
536
552
  longOptionalValue: { color: "label", colour: "label", context: "count" },
553
+ firstOperand: { kind: "pattern", suppliedByLetters: "ef", suppliedByLongNames: ["regexp", "file"] },
537
554
  longPlain: [
538
555
  "basic-regexp", "byte-offset", "count", "dereference-recursive", "extended-regexp",
539
556
  "files-with-matches", "files-without-match", "fixed-strings", "help", "ignore-case", "initial-tab",
@@ -579,6 +596,7 @@ const VERB_OPTION_SLOTS = {
579
596
  shortPlain: "nrEszu",
580
597
  longValue: { expression: "script", file: "path", "line-length": "count" },
581
598
  longOptionalValue: { "in-place": "label" },
599
+ firstOperand: { kind: "script", suppliedByLetters: "ef", suppliedByLongNames: ["expression", "file"] },
582
600
  longPlain: [
583
601
  "debug", "follow-symlinks", "help", "null-data", "posix", "quiet", "regexp-extended", "sandbox",
584
602
  "separate", "silent", "unbuffered", "version", "zero-terminated",
@@ -1670,7 +1688,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1670
1688
  }
1671
1689
  else {
1672
1690
  const eoo = args.indexOf("--");
1673
- const patternSuppliedByFlag = name === "grep" && (eoo === -1 ? args : args.slice(0, eoo)).some(isGrepPatternFlagToken);
1691
+ const firstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
1692
+ const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t));
1674
1693
  let sawOperand = false;
1675
1694
  let sawArgvOperand = false;
1676
1695
  let endOfOptions = false;
@@ -1718,12 +1737,12 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1718
1737
  continue;
1719
1738
  }
1720
1739
  }
1721
- const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
1740
+ const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand;
1722
1741
  sawOperand = true;
1723
1742
  if (k >= argvOperandsStartAt)
1724
1743
  sawArgvOperand = true;
1725
1744
  if (t === "-") {
1726
- if (isGrepPatternSlot)
1745
+ if (inFirstOperandSlot)
1727
1746
  continue;
1728
1747
  const m = RECURSIVE_READ_FORMS[slotVerb];
1729
1748
  if (m === undefined || m.dashIsStdin === true) {
@@ -1734,8 +1753,14 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1734
1753
  positionalOperands.push(t);
1735
1754
  continue;
1736
1755
  }
1737
- if (isGrepPatternSlot)
1756
+ if (inFirstOperandSlot) {
1757
+ const named = slotValueCandidates(t, firstOperandSlot.kind);
1758
+ for (const c of named)
1759
+ judgeOperand(c, argGlobs(k));
1760
+ if (named.length > 0 && k >= argvOperandsStartAt)
1761
+ positionalOperands.push(t);
1738
1762
  continue;
1763
+ }
1739
1764
  if (k >= argvOperandsStartAt)
1740
1765
  positionalOperands.push(t);
1741
1766
  judgeOperand(t, argGlobs(k));
@@ -1987,7 +2012,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1987
2012
  if (!(pipeFed[si] ?? false)) {
1988
2013
  const floor = STDIN_FILE_FLOOR[name];
1989
2014
  const restArgs = toks.slice(1);
1990
- const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
2015
+ const floorFirstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
2016
+ const floorSlotSuppliedByFlag = floorFirstOperandSlot !== undefined && restArgs.some((t) => suppliesFirstOperandSlot(floorFirstOperandSlot, t));
1991
2017
  let sawNonFlagOperand = false;
1992
2018
  let hasStdinDash = false;
1993
2019
  let operandCount = 0;
@@ -2013,10 +2039,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
2013
2039
  if (t.startsWith("-") && t !== "-")
2014
2040
  continue;
2015
2041
  }
2016
- const isGrepPatternPosition = name === "grep" && !grepPatternSuppliedByFlag && !sawNonFlagOperand;
2042
+ const inFirstOperandSlot = floorFirstOperandSlot !== undefined && !floorSlotSuppliedByFlag && !sawNonFlagOperand;
2017
2043
  sawNonFlagOperand = true;
2018
2044
  operandCount++;
2019
- if (isGrepPatternPosition)
2045
+ if (inFirstOperandSlot)
2020
2046
  continue;
2021
2047
  if (t === "-") {
2022
2048
  hasStdinDash = true;
@@ -2329,10 +2355,13 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
2329
2355
  if (baseMoved)
2330
2356
  return { gated: true, paths: [] };
2331
2357
  const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
2332
- return verdict.outOfRootRead === true ? { gated: true, paths: verdict.outOfRootPaths ?? [] } : NOT_GATED;
2358
+ const denied = boundary.denyMatch === undefined ? false : evaluateReadBoundary(readers, boundary, false, readerReach).readDenied === true;
2359
+ if (verdict.outOfRootRead === true)
2360
+ return { gated: true, paths: verdict.outOfRootPaths ?? [], ...(denied ? { denied: true } : {}) };
2361
+ return denied ? { gated: false, paths: [], denied: true } : NOT_GATED;
2333
2362
  };
2334
2363
  const direct = judge(command);
2335
- if (direct.gated)
2364
+ if (direct.gated || direct.denied === true)
2336
2365
  return direct;
2337
2366
  const shape = POLL_LOOP_SHAPE.exec(command.trim());
2338
2367
  return shape !== null ? judge(shape[3]) : NOT_GATED;
@@ -3,6 +3,7 @@ import { type TaskRegistry } from "../../core/task-registry.js";
3
3
  import { type CwdRef } from "./fs-shared.js";
4
4
  import type { ReversibilityVerdict } from "../../core/types.js";
5
5
  import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
6
+ import { type ShellScanDecline } from "./bash-program-position.js";
6
7
  /**
7
8
  * A parsed-command classifier for the `bash` tool, exposed as a
8
9
  * `ToolSpec.reversibilityProbe`. A full shell is treated as egress+irreversible by DEFAULT; this probe is the
@@ -60,6 +61,27 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
60
61
  * shape classifier here changes what happens to the residual shell risk, never that reading.
61
62
  */
62
63
  export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
64
+ /**
65
+ * ONE rule, TWO TABLES: which {@link ShellScanDecline} a SEAT reads as read evidence. A decline names no word,
66
+ * so it is never the boundary DECLARING anything about a path.
67
+ *
68
+ * The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
69
+ * is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
70
+ * xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
71
+ * SHAPE classifier, which refuses all these shapes anyway: the only question left is whether a stored answer
72
+ * may retire the ask. It keeps NEITHER — empty by the argument, not by omission.
73
+ * THE COST, stated rather than mitigated: once that ask is retired, an out-of-root or deny-listed read HIDDEN
74
+ * inside such a command is not asked again. A command's words are not the argv its programs receive (a stream
75
+ * is re-split by its consumer, an unread option may move the working directory first, stdin text is a command
76
+ * line nobody parsed), so a scan of them cannot be the guarantee it was claimed to be — three independently
77
+ * exploitable shapes measured that. "A read the boundary objects to is never retired by a stored rule" does
78
+ * NOT hold on the classify seat for this class, and holds unchanged elsewhere; below it is the execution
79
+ * environment's sandbox. One word-level check survives on both seats and is not part of the trade: a token the
80
+ * scan DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
81
+ */
82
+ export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
83
+ /** The classify doctrine's table — see above; empty by the argument. */
84
+ export declare const CLASSIFY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
63
85
  /**
64
86
  * The READ-BOUNDARY seat of the full shell: a `ToolSpec.reversibilityProbe` that answers exactly ONE
65
87
  * question — does the deployment's read boundary object to where this command reads? — and nothing about
@@ -13,7 +13,8 @@ import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsola
13
13
  import { ghRateLimitHint } from "./gh-rate-limit.js";
14
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 { bashClassifyDefaultAllow, 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, } from "./bash-readonly-classifier.js";
17
+ import { NOT_AUTO_ALLOWED, SHELL_SCAN_DECLINES } from "./bash-program-position.js";
17
18
  import { toolFace } from "../../core/tool-catalog-entries.js";
18
19
  const RECURSIVE_CAUSE_MAX_PATHS = 3;
19
20
  const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
@@ -27,8 +28,8 @@ function operandFamily(paths) {
27
28
  export function bashReversibilityProbe(allow, boundary) {
28
29
  const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
29
30
  const judge = (args, command, resolved, offers) => {
30
- const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict);
31
- const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {});
31
+ const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict, CLASSIFY_SEAT_DECLINE_EVIDENCE);
32
+ const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {}, CLASSIFY_SEAT_DECLINE_EVIDENCE);
32
33
  const backgrounded = isBackgroundShellCall(args);
33
34
  const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
34
35
  if (backgrounded)
@@ -46,7 +47,7 @@ export function bashReversibilityProbe(allow, boundary) {
46
47
  if (loop.reason === undefined)
47
48
  return { reversible: true };
48
49
  const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
49
- if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop))
50
+ if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE))
50
51
  return { reversible: false, mandated: true, ...seatOffer(loop) };
51
52
  const refusedCause = recursiveReadCause(detailed);
52
53
  return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...(refusedCause !== undefined ? { cause: refusedCause } : {}) };
@@ -86,24 +87,30 @@ function shellCommandOf(args) {
86
87
  function isBackgroundShellCall(args) {
87
88
  return args?.run_in_background === true;
88
89
  }
89
- function readBoundaryMandate(command, allowSet, resolved, verdict) {
90
+ export const BOUNDARY_SEAT_DECLINE_EVIDENCE = new Set(SHELL_SCAN_DECLINES);
91
+ export const CLASSIFY_SEAT_DECLINE_EVIDENCE = new Set();
92
+ function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
90
93
  if (resolved === undefined)
91
94
  return {};
92
- if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict))
95
+ if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, declineEvidence))
93
96
  return { mandated: true };
94
- return classifyOutOfRootReadGate(command, allowSet, resolved).gated ? { mandated: true } : {};
97
+ const named = classifyOutOfRootReadGate(command, allowSet, resolved);
98
+ return named.gated || named.denied === true ? { mandated: true } : {};
95
99
  }
96
100
  function boundaryUnjudged(verdict) {
97
101
  return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
98
102
  }
99
- function unjudgedRead(command, allowSet, resolved, verdict) {
103
+ function unjudgedRead(command, allowSet, resolved, verdict, declineEvidence) {
100
104
  if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
101
105
  return true;
102
106
  if (verdict.refusedWhole !== true)
103
107
  return false;
104
- if (refusedCommandMayRead(command, allowSet, resolved))
108
+ if (refusedCommandMayRead(command, allowSet, resolved, declineEvidence))
105
109
  return true;
106
- return resolved !== undefined && classifyOutOfRootReadGate(command, allowSet, resolved).gated;
110
+ if (resolved === undefined)
111
+ return false;
112
+ const named = classifyOutOfRootReadGate(command, allowSet, resolved);
113
+ return named.gated || named.denied === true;
107
114
  }
108
115
  function recursiveReadCause(detailed) {
109
116
  const recursive = detailed.recursiveReadPaths;
@@ -121,7 +128,7 @@ export function bashReadBoundaryProbe(boundary) {
121
128
  const allowSet = new Set(bashClassifyDefaultAllow());
122
129
  const judge = (command, resolved, offers) => {
123
130
  const seatVerdict = (verdict) => {
124
- if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict)) {
131
+ if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE)) {
125
132
  const cause = recursiveReadCause(verdict);
126
133
  return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
127
134
  }
@@ -145,7 +152,7 @@ export function bashReadBoundaryProbe(boundary) {
145
152
  const offerFrom = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
146
153
  if (mandated !== undefined)
147
154
  return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
148
- if (readBoundaryMandate(command, allowSet, resolved, detailed).mandated === true)
155
+ if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
149
156
  return { reversible: false, mandated: true };
150
157
  return compoundVerdict ?? loopVerdict ?? { reversible: true };
151
158
  };
@@ -231,7 +231,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
231
231
  if (prevNb && !prevNb.isPartialView && prevNb.hash === hash && prevNb.view && prevNb.view.start === 1 && prevNb.view.end === total) {
232
232
  return fileUnchangedResult(`[${path}: unchanged since you last read it (lines 1-${total} of ${total}); content omitted to save context]`, "unchanged-since-last-read", 1, total, total);
233
233
  }
234
- state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now() });
234
+ state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now(), transmittedByToolCallId: ctx.toolCallId });
235
235
  const bodyBlocks = rendered.blocks.length > 0 ? rendered.blocks : [{ type: "text", text: "[notebook has 0 cells]" }];
236
236
  const nbDisclosure = discloseReminderShaped({
237
237
  segments: rendered.blocks.filter((b) => b.type === "text").map((b) => b.text),
@@ -307,6 +307,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
307
307
  ...(pageMarker !== undefined ? { isPartialView: true } : {}),
308
308
  view: { start, end },
309
309
  lastReadAt: Date.now(),
310
+ transmittedByToolCallId: ctx.toolCallId,
310
311
  });
311
312
  if (total === 0)
312
313
  return `${openSystemReminder(reminderMark)}Warning: the file exists but the contents are empty.</system-reminder>`;
@@ -375,9 +375,7 @@ export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
375
375
  * TWO CALLERS, ONE RULE, which is why the marker is optional. The foreground result path KNOWS the
376
376
  * marker it minted and passes it (exact, unambiguous). A backgrounded command's spool is assembled by a
377
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.
378
+ * so it passes none and the marker is recovered per span from the sentinel plus its hex suffix.
381
379
  */
382
380
  export declare function stripCwdSentinel(stderr: string, marker?: string): {
383
381
  stderr: string;
@@ -504,10 +502,38 @@ export declare function inheritSessionReadFileState(target: ReadFileState, inher
504
502
  * coordinate (canonicalized the same way the Read/Edit/Write tools populate it), so a plain lookup
505
503
  * suffices; no re-hash/re-register needed since the entry's content genuinely has not changed.
506
504
  */
507
- export declare function applyCompactionToReadFileState(state: ReadFileState, attachedComplete: ReadonlyArray<{
508
- path: string;
509
- content: string;
510
- }>, preserveKeys?: ReadonlyArray<string>): void;
505
+ /**
506
+ * The SECOND way a transmission leaves the model's view, and why this function is named for context
507
+ * SHRINK rather than compaction: the request-layer stale-result pass replaces an old tool result's
508
+ * content with a cleared marker — no summary, no re-attachment, nothing changed in the session — and
509
+ * for a Read result the effect on the entry is identical: the body its dedup stub points at is no
510
+ * longer anywhere the model can see, so the entry must retire with it.
511
+ *
512
+ * Selection is by the TRANSMISSION, not by path — an entry a LATER Read re-transmitted carries that
513
+ * later call's id and so survives a clear of the earlier one, which a path-keyed retirement could not
514
+ * tell apart. KNOWN IMPRECISION: a call id is not unique over a run (the clear ledger keys occurrences
515
+ * by id + message timestamp + ordinal for that reason) and the finer key is not reachable at the Read
516
+ * mint, which does not know the timestamp of the message its result will land in. A reused id can
517
+ * therefore retire one entry too many, costing a re-transmission and a tightened read-before-edit gate
518
+ * — the same tightening the summarized arm accepts wholesale. The converse is not reachable: an entry
519
+ * carries the id of the last Read that transmitted it, so if that result was cleared its id is in the list.
520
+ */
521
+ export type ReadFileStateShrink =
522
+ /** A landed compaction: `attachedComplete` re-entered the context whole; `preserveKeys` stayed visible in the kept tail. */
523
+ {
524
+ kind: "summarized";
525
+ attachedComplete: ReadonlyArray<{
526
+ path: string;
527
+ content: string;
528
+ }>;
529
+ preserveKeys?: ReadonlyArray<string>;
530
+ }
531
+ /** A request-layer stale-result clear: these tool calls' results are markers now. Nothing re-entered. */
532
+ | {
533
+ kind: "tool-results-cleared";
534
+ clearedToolCallIds: ReadonlyArray<string>;
535
+ };
536
+ export declare function applyContextShrinkToReadFileState(state: ReadFileState, shrink: ReadFileStateShrink): void;
511
537
  /** CC 2.1.198 verbatim skeleton: the ENOENT error body for a
512
538
  * missing Read/Edit target — `File does not exist. Note: your current working directory is <cwd>.`
513
539
  * plus a `Did you mean <sibling>?` correction when the parent directory holds a near-name (case
@@ -318,7 +318,18 @@ export async function inheritSessionReadFileState(target, inherited, judgeKey) {
318
318
  target.set(judged, entry);
319
319
  }
320
320
  }
321
- export function applyCompactionToReadFileState(state, attachedComplete, preserveKeys = []) {
321
+ export function applyContextShrinkToReadFileState(state, shrink) {
322
+ if (shrink.kind === "tool-results-cleared") {
323
+ if (shrink.clearedToolCallIds.length === 0)
324
+ return;
325
+ const cleared = new Set(shrink.clearedToolCallIds);
326
+ for (const [k, v] of [...state]) {
327
+ if (v.transmittedByToolCallId !== undefined && cleared.has(v.transmittedByToolCallId))
328
+ state.delete(k);
329
+ }
330
+ return;
331
+ }
332
+ const { attachedComplete, preserveKeys = [] } = shrink;
322
333
  const preserve = new Set(preserveKeys);
323
334
  for (const [k, v] of [...state]) {
324
335
  if (v.seededFromContext !== true && !preserve.has(k))
@@ -77,6 +77,13 @@ export interface ReadEntry {
77
77
  * disk (hash mismatch) always serves real content and the dedup disarms permanently.
78
78
  * Optional/additive: absent = normal entry. */
79
79
  seededFromContext?: boolean;
80
+ /** The id of the tool call whose RESULT carried the body this entry dedups against: when the
81
+ * request-layer stale-result pass clears THAT transmission, the entry retires with it
82
+ * (`applyContextShrinkToReadFileState`, `tool-results-cleared` arm). Recorded only by the Read
83
+ * lanes that TRANSMIT a body — a write-back's content is the model's own arguments and a
84
+ * seeded/re-attached entry never rode a tool result, so neither carries this and neither is
85
+ * retired by a clear. Optional/additive: absent (an older checkpoint) never matches a cleared id. */
86
+ transmittedByToolCallId?: string;
80
87
  }
81
88
  /** Per-task record of which files have been read, keyed by canonical path: owned by prepare-task,
82
89
  * closure-captured by the tools, and serialized into the durable checkpoint. */
@@ -385,6 +392,36 @@ export declare function isShellRootedSpellingUnmapped(family: PathFamily | undef
385
392
  * ⇒ `\\\\host\\share\\x`), for a lexical normalizer that recognises the backslash form only; every other spelling is
386
393
  * returned as is. */
387
394
  export declare function nativeUncSpellingOf(family: PathFamily | undefined, spelled: string): string;
395
+ /**
396
+ * Fold a TRAILING separator run off a spelling that is about to become a KEY: `<dir>/adir`, `<dir>/adir/`
397
+ * and `<dir>/adir//` name one directory and must mint one key. The existing-path arm folds them already,
398
+ * as a side effect of asking `realpath`; the arm that mints a key for a path that does NOT exist yet
399
+ * rejoined the model's own tail verbatim, so the same not-yet-created directory arrived under two and
400
+ * three keys — and a byte-wise reader (the per-file read state, the write gate's per-key ledger) treats
401
+ * two keys as two files.
402
+ *
403
+ * Called on the ANSWER, at the canonicalizer's single exit, and never on a spelling about to be probed.
404
+ * Two things break if it moves earlier. A trailing separator is an ASSERTION that the target is a
405
+ * directory and the platform enforces it (`<existing regular file>/` answers `not_directory`, reported
406
+ * here as a refusal), so folding before the probe spends that refusal and the slash-suffixed spelling of
407
+ * a file resolves to the file. And the ancestor walk chooses which ancestor to canonicalize by SPLITTING
408
+ * the spelling it was handed: a leaf stripped of its separator first is skipped as though it had been
409
+ * resolved, so `<root>/link/` — a link to a directory outside the root — comes back as the in-root
410
+ * spelling instead of the destination. Folding the answer costs neither.
411
+ *
412
+ * A spelling whose remainder is the bare ANCHOR keeps the separator the platform writes on a root — `/`,
413
+ * `C:\`, `\\host\share\`. That run IS the root's own spelling rather than a tail nobody wrote, and
414
+ * stripping it renames the spelling to nothing, to a DRIVE-RELATIVE location, or to a share root written
415
+ * unlike every other root. The question is asked in the module's own vocabulary ({@link pathIdentityOf} +
416
+ * {@link isIdentityDir}: is anything left BELOW the anchor?), so the three families need no arm each.
417
+ * Interior runs are NOT touched here: the ancestor walk folds them by probing, and a run in the middle of
418
+ * a name is the platform's to read.
419
+ *
420
+ * Scanned backwards rather than matched: `/[\\/]+$/` against a model-supplied spelling is quadratic when a
421
+ * long separator run precedes a non-separator (measured: ~5s for 32k leading slashes, on the caller's
422
+ * thread), and a path spelling is unbounded input.
423
+ */
424
+ export declare function foldTrailingSeparators(spelled: string, family: PathFamily): string;
388
425
  /**
389
426
  * Join a RELATIVE-OR-ROOTED spelling to a base under the family, without folding (`..` across a symlinked
390
427
  * parent is the env's to resolve). win = `path.win32` semantics: `/x`/`\x` lands on the base's drive or
@@ -371,11 +371,17 @@ function mintIdentity(anchor, segments, stripTail) {
371
371
  out.pop();
372
372
  continue;
373
373
  }
374
- const stripped = stripTail ? seg.replace(/[. ]+$/, "") : seg;
374
+ const stripped = stripTail ? seg.slice(0, trailingDotSpaceStart(seg)) : seg;
375
375
  out.push(stripped.length > 0 ? stripped : seg);
376
376
  }
377
377
  return (out.length === 0 ? anchor || "/" : `${anchor}/${out.join("/")}`);
378
378
  }
379
+ function trailingDotSpaceStart(seg) {
380
+ let end = seg.length;
381
+ while (end > 0 && (seg[end - 1] === "." || seg[end - 1] === " "))
382
+ end -= 1;
383
+ return end;
384
+ }
379
385
  function identityAnchorOf(identity) {
380
386
  return identity.startsWith("//") ? identity.split("/").slice(0, 4).join("/") : identity.slice(0, 2);
381
387
  }
@@ -403,6 +409,18 @@ export function isShellRootedSpellingUnmapped(family, spelled) {
403
409
  export function nativeUncSpellingOf(family, spelled) {
404
410
  return family === "win" && isUncSpelling(spelled) ? `\\\\${spelled.slice(2).replace(/\//g, "\\")}` : spelled;
405
411
  }
412
+ export function foldTrailingSeparators(spelled, family) {
413
+ let end = spelled.length;
414
+ while (end > 0 && (spelled[end - 1] === "/" || (family === "win" && spelled[end - 1] === "\\")))
415
+ end -= 1;
416
+ if (end === spelled.length)
417
+ return spelled;
418
+ const stripped = spelled.slice(0, end);
419
+ const identity = pathIdentityOf(stripped, family);
420
+ if (!identity.ok || !isIdentityDir(identity.identity, family))
421
+ return spelled;
422
+ return stripped;
423
+ }
406
424
  export function joinForFamily(family, base, rel) {
407
425
  if (family === "posix")
408
426
  return `${base.replace(/\/+$/, "")}/${rel}`;
@@ -500,7 +518,8 @@ export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
500
518
  throw e;
501
519
  });
502
520
  try {
503
- return await withGateTimeout(probe, DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "pathCanonicalization");
521
+ const answer = await withGateTimeout(probe, DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "pathCanonicalization");
522
+ return answer.ok ? { ...answer, key: foldTrailingSeparators(answer.key, family) } : answer;
504
523
  }
505
524
  catch (err) {
506
525
  if (seenThrow && err === thrownByProbe)
@@ -546,7 +565,16 @@ async function probeRealTarget(env, path, target, family, signal) {
546
565
  if (!link.ok) {
547
566
  return { ok: false, message: `cannot read symlink "${path}": ${link.error.message}`, unresolvedSymlink: true };
548
567
  }
549
- const linkTarget = isAbsoluteForFamily(family, link.value) ? link.value : joinForFamily(family, parentDir(abs), link.value);
568
+ const screenedTarget = win32NamespaceScreen(link.value, family);
569
+ if (!screenedTarget.ok) {
570
+ return {
571
+ ok: false,
572
+ message: `cannot resolve symlink "${path}": its target "${clipSpelling(link.value)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[screenedTarget.refuse]}; refused (${screenedTarget.refuse}).`,
573
+ refused: screenedTarget.refuse,
574
+ };
575
+ }
576
+ const hopSpelling = family === "win" ? screenedTarget.spelled : link.value;
577
+ const linkTarget = isAbsoluteForFamily(family, hopSpelling) ? hopSpelling : joinForFamily(family, parentDir(abs), hopSpelling);
550
578
  const reabs = await env.absolutePath(linkTarget, signal);
551
579
  if (!reabs.ok) {
552
580
  return { ok: false, message: `cannot resolve symlink target "${linkTarget}" for "${path}": ${reabs.error.message}`, unresolvedSymlink: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.22.0",
3
+ "version": "7.23.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "scripts": {
50
50
  "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.build.json --emitDeclarationOnly --removeComments false",
51
- "typecheck": "tsc --noEmit",
51
+ "typecheck": "tsc --noEmit && echo \"typecheck OK (tsc --noEmit clean)\"",
52
52
  "pretest": "npm run gate:debts",
53
53
  "test": "vitest run --exclude \"**/*.local.test.ts\"",
54
54
  "test:watch": "vitest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
3
  "_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
4
- "count": 2394,
4
+ "count": 2395,
5
5
  "exports": {
6
6
  "A2ATaskState": "type",
7
7
  "A2ATaskStateReversal": "type",
@@ -1615,6 +1615,7 @@
1615
1615
  "WriteProtectionMount": "interface",
1616
1616
  "WriteProtectionTargetView": "type",
1617
1617
  "WriteProtectionVerdict": "type",
1618
+ "WriteProtectionViewAbsence": "interface",
1618
1619
  "WriteReceipt": "interface",
1619
1620
  "XARGS_READ_ONLY_TARGETS": "variable",
1620
1621
  "ackAdoptionConfig": "function",
@@ -4011,6 +4012,7 @@
4011
4012
  "WriteProtectionMount": "advanced",
4012
4013
  "WriteProtectionTargetView": "advanced",
4013
4014
  "WriteProtectionVerdict": "advanced",
4015
+ "WriteProtectionViewAbsence": "advanced",
4014
4016
  "WriteReceipt": "internal",
4015
4017
  "XARGS_READ_ONLY_TARGETS": "advanced",
4016
4018
  "ackAdoptionConfig": "advanced",