@sema-agent/core 7.24.3 → 7.25.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 +101 -1
  2. package/dist/agents/peer-session-drain.d.ts +16 -1
  3. package/dist/agents/peer-session-drain.js +16 -1
  4. package/dist/agents/send-message-tool.d.ts +10 -2
  5. package/dist/agents/send-message-tool.js +18 -7
  6. package/dist/agents/subagent.js +13 -10
  7. package/dist/core/background-agent-store.d.ts +3 -3
  8. package/dist/core/checkpoint-store.d.ts +1 -0
  9. package/dist/core/delegation-frames.d.ts +5 -0
  10. package/dist/core/gate-fold.js +85 -0
  11. package/dist/core/gate-lanes.js +23 -1
  12. package/dist/core/gate-outcome.d.ts +8 -2
  13. package/dist/core/gate-outcome.js +3 -1
  14. package/dist/core/hooks.d.ts +29 -0
  15. package/dist/core/hooks.js +2 -0
  16. package/dist/core/mailbox-store.d.ts +5 -0
  17. package/dist/core/mailbox-store.js +1 -1
  18. package/dist/core/parent-spec-seat.d.ts +7 -1
  19. package/dist/core/parent-spec-seat.js +2 -0
  20. package/dist/core/permission-rule-consent.js +29 -47
  21. package/dist/core/permission-rule-provider.js +4 -3
  22. package/dist/core/permission-rule-store.d.ts +143 -7
  23. package/dist/core/permission-rule-store.js +165 -15
  24. package/dist/core/permission-rules.js +14 -10
  25. package/dist/core/rule-denial-sentence.d.ts +75 -0
  26. package/dist/core/rule-denial-sentence.js +16 -0
  27. package/dist/core/runner/contracts.d.ts +36 -2
  28. package/dist/core/runner/gate-exit.d.ts +14 -0
  29. package/dist/core/runner/gate-exit.js +4 -0
  30. package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
  31. package/dist/core/runner/prepare-delegation-surface.js +2 -1
  32. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  33. package/dist/core/runner/prepare-gate-stations.js +7 -4
  34. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  35. package/dist/core/runner/prepare-hands-readface.js +25 -10
  36. package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
  37. package/dist/core/runner/prepare-inherited-gate.js +11 -6
  38. package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
  39. package/dist/core/runner/prepare-policy-chain.js +42 -22
  40. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
  41. package/dist/core/runner/prepare-suspend-saga.js +4 -3
  42. package/dist/core/runner/prepare-task.js +10 -9
  43. package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
  44. package/dist/core/runner/prepare-wiring-manifest.js +8 -4
  45. package/dist/core/runner/resume-apply.js +2 -0
  46. package/dist/core/runner/run-harness-handlers.js +1 -0
  47. package/dist/core/runner/run-identity-wiring.d.ts +2 -2
  48. package/dist/core/runner/run-leg.js +28 -1
  49. package/dist/core/runner/run-notification-lane.d.ts +2 -2
  50. package/dist/core/runner/run-notification-lane.js +2 -1
  51. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  52. package/dist/core/runner/session-rule-policy.d.ts +73 -2
  53. package/dist/core/runner/session-rule-policy.js +121 -21
  54. package/dist/core/session-policy-store.d.ts +27 -3
  55. package/dist/core/session-policy-store.js +12 -4
  56. package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
  57. package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
  58. package/dist/core/store-contracts/peer-directory-contract.js +82 -0
  59. package/dist/core/task-event.d.ts +6 -0
  60. package/dist/core/task-notification.d.ts +79 -4
  61. package/dist/core/task-notification.js +90 -1
  62. package/dist/core/task-registry-agent.js +1 -2
  63. package/dist/core/task-registry-monitor.js +2 -1
  64. package/dist/core/task-registry-shared.d.ts +53 -5
  65. package/dist/core/task-registry-shared.js +13 -2
  66. package/dist/core/task-registry.js +8 -5
  67. package/dist/core/task-spec.d.ts +12 -5
  68. package/dist/core/tool-catalog-entries.js +1 -0
  69. package/dist/core/tool-spec.d.ts +1 -1
  70. package/dist/core/wiring-manifest.d.ts +2 -2
  71. package/dist/engine/loop/agent-loop.js +1 -1
  72. package/dist/engine/loop/types.d.ts +4 -0
  73. package/dist/index.d.ts +6 -3
  74. package/dist/index.js +4 -3
  75. package/dist/orchestration/workflow.js +12 -3
  76. package/dist/prompts/coordinator.d.ts +2 -1
  77. package/dist/stores/file/adoption/adopt.js +23 -1
  78. package/dist/stores/file/permission-rule-store.js +3 -1
  79. package/dist/stores/file/session-policy-store.d.ts +31 -0
  80. package/dist/stores/file/session-policy-store.js +87 -7
  81. package/dist/tools/fs/bash-program-position.d.ts +4 -26
  82. package/dist/tools/fs/bash-program-position.js +0 -11
  83. package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -309
  84. package/dist/tools/fs/bash-readonly-classifier.js +28 -766
  85. package/dist/tools/fs/fs-bash.d.ts +43 -207
  86. package/dist/tools/fs/fs-bash.js +20 -341
  87. package/dist/tools/fs/index.d.ts +12 -13
  88. package/dist/tools/fs/index.js +3 -4
  89. package/dist/tools/fs/read-deny.d.ts +25 -0
  90. package/dist/tools/fs/read-deny.js +19 -0
  91. package/dist/tools/fs/safety.js +2 -6
  92. package/dist/tools/fs/shell-read-check.d.ts +99 -0
  93. package/dist/tools/fs/shell-read-check.js +229 -0
  94. package/dist/tools/fs/shell-read-establish.d.ts +70 -115
  95. package/dist/tools/fs/shell-read-establish.js +216 -102
  96. package/dist/tools/fs/shell-read-judge.d.ts +231 -0
  97. package/dist/tools/fs/shell-read-judge.js +170 -0
  98. package/dist/tools/fs/shell-read-plan.d.ts +3 -0
  99. package/dist/tools/fs/shell-read-plan.js +82 -19
  100. package/dist/tools/monitor.d.ts +5 -5
  101. package/dist/tools/monitor.js +3 -5
  102. package/dist/tools/task-list.d.ts +4 -0
  103. package/package.json +1 -1
  104. package/test/export-surface.snapshot.json +19 -3
  105. package/wire-changes/core@7.24.4.json +54 -0
  106. package/wire-changes/core@7.25.0.json +671 -0
@@ -7,82 +7,14 @@ import { RollingTailBuffer } from "../../core/exec-output-tail.js";
7
7
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
8
8
  import { hasBackgroundShell } from "../../core/background-shell.js";
9
9
  import { delimitUntrusted } from "../../core/untrusted-text.js";
10
- import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
11
10
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
12
- import { imageMagicMatches, normalizeAbsPathLexically, violationDetails, withinAnyRoot } from "./safety.js";
11
+ import { imageMagicMatches, withinAnyRoot } from "./safety.js";
13
12
  import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
14
13
  import { ghRateLimitHint } from "./gh-rate-limit.js";
15
- import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
16
- import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
17
- import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
18
- import { BOUNDARY_SEAT_READS_DECLINE, CLASSIFY_SEAT_READS_DECLINE, NOT_AUTO_ALLOWED, seatDeclineEvidence, } from "./bash-program-position.js";
19
- import { readDenySeat, sensitivePathDenyList, SENSITIVE_PATH_DENY_SCOPE } from "./read-deny.js";
14
+ import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
15
+ import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed } from "./bash-readonly-classifier.js";
20
16
  import { toolFace } from "../../core/tool-catalog-entries.js";
21
17
  import { thrownText } from "../../core/thrown-value.js";
22
- const RECURSIVE_CAUSE_MAX_PATHS = 3;
23
- const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
24
- function operandFamily(paths) {
25
- const enc = (s) => s.replace(/%/g, "%25").replace(/;/g, "%3B").replace(/:/g, "%3A");
26
- return {
27
- shown: paths.slice(0, RECURSIVE_CAUSE_MAX_PATHS).map((p) => enc(inlineUntrusted(p, PROBE_CAUSE_PATH_MAX))),
28
- total: paths.length,
29
- };
30
- }
31
- export function bashReversibilityProbe(allow, boundary) {
32
- const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
33
- const judge = (args, command, resolved, offers) => {
34
- const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict, CLASSIFY_SEAT_DECLINE_EVIDENCE);
35
- const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {}, CLASSIFY_SEAT_DECLINE_EVIDENCE);
36
- const backgrounded = isBackgroundShellCall(args);
37
- const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
38
- if (backgrounded)
39
- return { reversible: false, ...boundaryGate(detailed), ...probeAccountOf(command, allowSet, resolved, detailed) };
40
- if (detailed.reason === undefined) {
41
- if (boundaryUnread(detailed)) {
42
- const account = probeAccountOf(command, allowSet, resolved, detailed);
43
- if (account.cause !== undefined)
44
- return { reversible: false, ...boundaryGate(detailed), ...account };
45
- return { reversible: false, ...outOfRootGate(), ...account };
46
- }
47
- return { reversible: true };
48
- }
49
- const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
50
- if (loop.reason === undefined)
51
- return { reversible: true };
52
- const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
53
- const loopScan = refusedCommandScan(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE);
54
- if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, loopScan))
55
- return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, loop), ...seatOffer(loop) };
56
- return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...probeAccountOf(command, allowSet, resolved, detailed) };
57
- };
58
- return (args) => {
59
- const command = shellCommandOf(args);
60
- if (command === undefined)
61
- return { reversible: false };
62
- const resolved = typeof boundary === "function" ? boundary() : boundary;
63
- if (resolved === undefined)
64
- return judge(args, command, undefined, true);
65
- return grantClearsSeat((c, b, o) => judge(args, c, b, o), command, resolved);
66
- };
67
- }
68
- function boundaryUnread(verdict) {
69
- return boundaryUndecided(verdict) || boundaryUnjudged(verdict);
70
- }
71
- function readRootCandidateOf(verdict) {
72
- const dir = verdict.outOfRootGrantCandidate;
73
- if (dir === undefined)
74
- return {};
75
- return { readRootCandidate: { dir, clearsThisAsk: true, ...(verdict.outOfRootGrantCoversExactPath === true ? { covers: "exact" } : {}) } };
76
- }
77
- function boundaryUndecided(verdict) {
78
- return verdict.undecidedPaths !== undefined && verdict.undecidedPaths.length > 0;
79
- }
80
- function boundaryDeclared(verdict) {
81
- return denyJudgeSpoke(verdict) || verdict.outOfRootRead === true;
82
- }
83
- function denyJudgeSpoke(verdict) {
84
- return verdict.readDenied === true || (verdict.recursiveReadPaths !== undefined && verdict.recursiveReadPaths.length > 0);
85
- }
86
18
  function shellCommandOf(args) {
87
19
  const command = args?.command;
88
20
  return typeof command === "string" ? command : undefined;
@@ -90,152 +22,23 @@ function shellCommandOf(args) {
90
22
  function isBackgroundShellCall(args) {
91
23
  return args?.run_in_background === true;
92
24
  }
93
- export const BOUNDARY_SEAT_DECLINE_EVIDENCE = seatDeclineEvidence(BOUNDARY_SEAT_READS_DECLINE);
94
- export const CLASSIFY_SEAT_DECLINE_EVIDENCE = seatDeclineEvidence(CLASSIFY_SEAT_READS_DECLINE);
95
- function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
96
- if (resolved === undefined)
97
- return {};
98
- const scan = refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence);
99
- if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, scan))
100
- return { mandated: true };
101
- const named = classifyOutOfRootReadGate(command, allowSet, resolved);
102
- if (named.gated || named.denied !== undefined)
103
- return { mandated: true };
104
- return declinedRung(scan);
105
- }
106
- function declinedRung(scan) {
107
- return scan.declined ? { boundaryDeclined: true } : {};
108
- }
109
- function refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence) {
110
- return verdict.refusedWhole === true ? refusedCommandMayRead(command, allowSet, resolved, declineEvidence) : { mayRead: false, declined: false };
111
- }
112
- function boundaryUnjudged(verdict) {
113
- return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
114
- }
115
- function unjudgedRead(command, allowSet, resolved, verdict, scan) {
116
- if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
117
- return true;
118
- if (verdict.refusedWhole !== true)
119
- return false;
120
- if (scan.mayRead)
121
- return true;
122
- if (resolved === undefined)
123
- return false;
124
- const named = classifyOutOfRootReadGate(command, allowSet, resolved);
125
- return named.gated || named.denied !== undefined;
126
- }
127
- function recursiveReadCause(detailed) {
128
- const recursive = detailed.recursiveReadPaths;
129
- if (recursive === undefined || recursive.length === 0)
130
- return undefined;
131
- const recursiveSet = new Set(recursive);
132
- const others = (detailed.undecidedPaths ?? []).filter((p) => !recursiveSet.has(p));
133
- return {
134
- code: RECURSIVE_READ_CAUSE_CODE,
135
- roots: operandFamily(recursive),
136
- ...(others.length > 0 ? { further: operandFamily(others) } : {}),
137
- };
138
- }
139
- function denyHitReason(finding) {
140
- if (finding === undefined)
141
- return {};
142
- const { path, pattern } = finding;
143
- const composed = (target, pat) => `reading "${target}" matches ${sensitivePathDenyList(pat)}.`;
144
- const room = PROBE_REASON_MAX - composed("", "").length;
145
- const patternRoom = Math.min(pattern.length, Math.max(Math.floor(room / 2), room - path.length));
146
- return { reason: composed(fitMiddle(path, room - patternRoom), fitMiddle(pattern, patternRoom)) };
147
- }
148
- export function fitMiddle(s, budget) {
149
- return s.length <= budget ? s : elideMiddle(s, budget);
150
- }
151
- export function elideMiddle(s, budget) {
152
- if (budget < 12)
153
- return sliceHeadSafe(s, Math.max(0, budget));
154
- const keep = budget - 1;
155
- const head = Math.ceil(keep / 2);
156
- return `${sliceHeadSafe(s, head)}\u2026${sliceTailSafe(s, keep - head)}`;
157
- }
158
- function denyFindingOf(command, allowSet, resolved, verdict) {
159
- if (verdict.readDenied === true && verdict.readDeniedPath !== undefined && verdict.readDeniedPattern !== undefined) {
160
- return { path: verdict.readDeniedPath, pattern: verdict.readDeniedPattern };
161
- }
162
- return resolved === undefined ? undefined : classifyOutOfRootReadGate(command, allowSet, resolved).denied;
163
- }
164
- function recursiveDenyReason(resolved, verdict) {
165
- const roots = verdict.recursiveReadPaths;
166
- const entries = resolved?.denyEntries;
167
- if (roots === undefined || roots.length === 0 || entries === undefined || entries.length === 0)
168
- return {};
169
- const composed = (root, rows) => `a recursive read form traverses "${root}", which this walk does not enumerate, past ${sensitivePathDenyList(rows)}.`;
170
- const root = roots[0] ?? "";
171
- const label = (rows) => rows.length < entries.length ? [...rows, `+${entries.length - rows.length} more`] : rows;
172
- let rows = entries.slice(0, MAX_DENY_ROWS_NAMED);
173
- while (rows.length > 1 && PROBE_REASON_MAX - composed("", label(rows)).length < MIN_TRAVERSAL_ROOT_ROOM)
174
- rows = rows.slice(0, -1);
175
- let shown = label(rows);
176
- if (PROBE_REASON_MAX - composed("", shown).length < MIN_TRAVERSAL_ROOT_ROOM) {
177
- const over = MIN_TRAVERSAL_ROOT_ROOM - (PROBE_REASON_MAX - composed("", shown).length);
178
- shown = label([fitMiddle(rows[0] ?? "", Math.max((rows[0] ?? "").length - over, 8))]);
179
- }
180
- return { reason: composed(fitMiddle(root, Math.max(PROBE_REASON_MAX - composed("", shown).length, 0)), shown) };
181
- }
182
- const MAX_DENY_ROWS_NAMED = 3;
183
- const MIN_TRAVERSAL_ROOT_ROOM = 24;
184
- function probeAccountOf(command, allowSet, resolved, verdict) {
185
- const cause = recursiveReadCause(verdict);
186
- const hit = denyHitReason(denyFindingOf(command, allowSet, resolved, verdict));
187
- return { ...(hit.reason !== undefined ? hit : recursiveDenyReason(resolved, verdict)), ...(cause !== undefined ? { cause } : {}) };
188
- }
189
- export function bashReadBoundaryProbe(boundary) {
190
- const allowSet = new Set(bashClassifyDefaultAllow());
191
- const judge = (command, resolved, offers) => {
192
- const seatVerdict = (verdict) => {
193
- if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, refusedCommandScan(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE))) {
194
- return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, verdict) };
195
- }
196
- if (boundaryUndecided(verdict)) {
197
- return { reversible: false, ...probeAccountOf(command, allowSet, resolved, verdict) };
198
- }
199
- if (verdict.reason === undefined)
200
- return { reversible: true };
201
- return undefined;
202
- };
203
- const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
204
- if (detailed.reason === undefined)
205
- return seatVerdict(detailed) ?? { reversible: true };
206
- const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
207
- if (loop.reason === undefined)
208
- return seatVerdict(loop) ?? { reversible: true };
209
- const compoundVerdict = seatVerdict(detailed);
210
- const loopVerdict = seatVerdict(loop);
211
- const mandated = [compoundVerdict, loopVerdict].find((verdict) => verdict?.mandated === true);
212
- const offerFrom = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
213
- if (mandated !== undefined)
214
- return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
215
- if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
216
- return { reversible: false, mandated: true, ...probeAccountOf(command, allowSet, resolved, detailed) };
217
- return compoundVerdict ?? loopVerdict ?? { reversible: true };
218
- };
25
+ export function bashReversibilityProbe(allow, boundary) {
26
+ const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
219
27
  return (args) => {
220
28
  const command = shellCommandOf(args);
221
29
  if (command === undefined)
222
30
  return { reversible: false };
223
- const resolved = typeof boundary === "function" ? boundary() : boundary;
224
- if (resolved === undefined)
31
+ if (isBackgroundShellCall(args))
32
+ return { reversible: false };
33
+ const home = typeof boundary === "function" ? boundary() : boundary;
34
+ const fold = home?.homeDir !== undefined ? { homeDir: home.homeDir } : undefined;
35
+ if (classifyCompoundReadonlyDetailed(command, allowSet, fold).reason === undefined)
36
+ return { reversible: true };
37
+ if (classifyBoundedReadonlyPollLoopDetailed(command, allowSet, fold).reason === undefined)
225
38
  return { reversible: true };
226
- return grantClearsSeat(judge, command, resolved);
39
+ return { reversible: false };
227
40
  };
228
41
  }
229
- function grantClearsSeat(judge, command, resolved) {
230
- const verdict = judge(command, resolved, true);
231
- const dir = verdict.readRootCandidate?.dir;
232
- if (dir === undefined)
233
- return verdict;
234
- if (judge(command, { ...resolved, roots: [...resolved.roots, dir] }, false).reversible)
235
- return verdict;
236
- const { readRootCandidate: _withdrawn, ...withoutOffer } = verdict;
237
- return withoutOffer;
238
- }
239
42
  export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
240
43
  const EXIT1_INTERPRETATION = {
241
44
  grep: "No matches found",
@@ -811,7 +614,6 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
811
614
  const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
812
615
  const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
813
616
  const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
814
- const canonicalEscape = createShellCanonicalRecheck(env, taskOpts.readBoundary);
815
617
  return defineTool({
816
618
  name: "Bash",
817
619
  ...toolFace("bash"),
@@ -847,9 +649,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
847
649
  const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
848
650
  if (timeoutRefusal !== undefined)
849
651
  return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
850
- const escape = await canonicalEscape(command, ctx.signal);
851
- if (escape !== undefined)
852
- return escape;
652
+ const refused = await taskOpts.shellReadAdmission?.("Bash", ctx.toolCallId, command, ctx.signal);
653
+ if (refused !== undefined)
654
+ return refused;
853
655
  if (run_in_background) {
854
656
  if (!hasBackgroundShell(env)) {
855
657
  return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
@@ -1037,110 +839,10 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
1037
839
  },
1038
840
  });
1039
841
  }
1040
- import { createCanonicalReadBoundary, NOTHING_ANSWERED } from "./shell-read-establish.js";
1041
- export function createShellCanonicalRecheck(env, readBoundary, face) {
1042
- const recheckReaders = new Set(bashClassifyDefaultAllow());
1043
- let canonicalForRoots;
1044
- const canonicalBoundaryFor = (roots) => {
1045
- const key = roots.join("\u0000");
1046
- if (canonicalForRoots?.key !== key)
1047
- canonicalForRoots = { key, boundary: createCanonicalReadBoundary(env, roots) };
1048
- return canonicalForRoots.boundary;
1049
- };
1050
- return async (command, signal) => {
1051
- const resolvedBoundary = typeof readBoundary === "function" ? readBoundary() : readBoundary;
1052
- if (resolvedBoundary === undefined)
1053
- return undefined;
1054
- const containmentAsked = resolvedBoundary.face !== "open";
1055
- if (resolvedBoundary.denyMatch === undefined && !containmentAsked)
1056
- return undefined;
1057
- const faces = [
1058
- classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
1059
- classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
1060
- ];
1061
- const named = classifyOutOfRootReadGate(command, recheckReaders, resolvedBoundary);
1062
- const literalAll = [
1063
- ...new Set([
1064
- ...faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? []), ...(v.outOfRootPaths ?? [])]),
1065
- ...named.checked,
1066
- ...named.paths,
1067
- ]),
1068
- ];
1069
- const folded = (ps) => new Set(ps.map((p) => normalizeAbsPathLexically(p)));
1070
- const answered = {
1071
- containment: folded([...faces.flatMap((v) => v.outOfRootPaths ?? []), ...named.paths]),
1072
- deny: folded([...faces.flatMap((v) => (v.readDeniedPath !== undefined ? [v.readDeniedPath] : [])), ...(named.denied !== undefined ? [named.denied.path] : [])]),
1073
- };
1074
- const patterns = [...new Set([...faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]), ...named.undecided])];
1075
- return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal: literalAll, patterns }, resolvedBoundary.roots, face ?? FULL_SHELL_FACE_ESCAPE_PROSE, { deny: resolvedBoundary.denyMatch, containment: containmentAsked, denyAnswered: answered.deny, containmentAnswered: answered.containment }, signal);
1076
- };
1077
- }
1078
- async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, ask, signal) {
1079
- const resolved = await canonical.escapesAfterResolution(candidates, ask, signal);
1080
- if (resolved.denied !== undefined) {
1081
- return readDenyRefusal(resolved.denied.path, resolved.denied.pattern, face.tool);
1082
- }
1083
- if (resolved.unverifiable !== undefined) {
1084
- return readContainmentRefusal(`Error (${face.tool}): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
1085
- face.note);
1086
- }
1087
- if (resolved.escaping.length > 0) {
1088
- const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
1089
- return readContainmentRefusal(`Error (${face.tool}): ${face.subject} resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")}${face.verdictPhrase}. ` +
1090
- face.note, resolved.escaping);
1091
- }
1092
- return undefined;
1093
- }
1094
- const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
1095
- "paths it would actually read; a deny-listed read is refused here rather than escalated.";
1096
- function readContainmentRefusal(content, paths = []) {
1097
- return errorResult(content, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths });
1098
- }
1099
- function readDenyRefusal(target, pattern, tool = "Bash") {
1100
- const content = `Error (${tool}): reading "${target}" is refused: the target matches ${sensitivePathDenyList(pattern)}. ` +
1101
- `${SENSITIVE_PATH_DENY_SCOPE} ${DENY_REFUSAL_NOTE}`;
1102
- return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target, pattern }));
1103
- }
1104
- const READONLY_FACE_ESCAPE_PROSE = {
1105
- tool: "Bash",
1106
- subject: "a path this command reads",
1107
- verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
1108
- note: BASH_READONLY_CONFINEMENT_NOTE,
1109
- };
1110
- const FULL_SHELL_FACE_ESCAPE_PROSE = {
1111
- tool: "Bash",
1112
- subject: "a path this command names",
1113
- verdictPhrase: "",
1114
- note: "The read boundary judged this command's written spelling before the call was approved, and execution is past the approval step — a target reached only through symlink resolution is refused here rather than escalated.",
1115
- };
1116
- export const MONITOR_ESCAPE_PROSE = { ...FULL_SHELL_FACE_ESCAPE_PROSE, tool: "Monitor" };
1117
842
  export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1118
843
  const timeoutCaps = resolveBashTimeoutCaps(opts);
1119
844
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
1120
845
  const sample = [...allow].slice(0, 6).join(", ");
1121
- const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
1122
- const denySeat = readDenySeat(opts?.readDeny);
1123
- const readBoundary = { roots: readRoots, cwd: rootCanonical, ...denySeat };
1124
- const overflowSpoolFence = createShellOverflowSpoolFence(env);
1125
- const readsOnlyEngineOverflowSpool = async (verdict) => {
1126
- if (verdict.outOfRootRead !== true)
1127
- return false;
1128
- if ((verdict.unresolvedOperands ?? []).length > 0)
1129
- return false;
1130
- const paths = verdict.outOfRootPaths ?? [];
1131
- if (paths.length === 0)
1132
- return false;
1133
- for (const p of [...paths, ...(verdict.outOfRootSpellings ?? [])]) {
1134
- const real = await canonicalBoundary.canonicalOf(p);
1135
- for (const view of [real, p])
1136
- if (view !== undefined && denySeat.denyMatch?.(normalizeAbsPathLexically(view)) != null)
1137
- return false;
1138
- if (!(await overflowSpoolFence(real ?? p)))
1139
- return false;
1140
- }
1141
- return true;
1142
- };
1143
- const canonicalBoundary = createCanonicalReadBoundary(env, readRoots);
1144
846
  return defineTool({
1145
847
  name: "Bash",
1146
848
  ...toolFace("bash-readonly"),
@@ -1148,7 +850,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1148
850
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
1149
851
  "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
1150
852
  "refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
1151
- (boundaryIsTheSandbox(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
853
+ (boundaryIsTheSandbox(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to or a recursive read traverses. ") +
1152
854
  "Still subject to the deployment's approval policy.",
1153
855
  parameters: Type.Object({
1154
856
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
@@ -1162,32 +864,9 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1162
864
  const reason = coarseReadonlyCheck(command, allow);
1163
865
  if (reason)
1164
866
  return errorResult(`Error (Bash): ${reason}`);
1165
- const boundary = classifySimpleCommandReadBoundary(command, readBoundary);
1166
- let releasedByGrant = [];
1167
- if (boundary.readDeniedPath !== undefined && boundary.readDeniedPattern !== undefined)
1168
- return readDenyRefusal(boundary.readDeniedPath, boundary.readDeniedPattern);
1169
- const traversal = boundary.recursiveReadPaths ?? [];
1170
- if (traversal.length > 0) {
1171
- const shown = traversal.slice(0, RECURSIVE_CAUSE_MAX_PATHS);
1172
- return readContainmentRefusal(`Error (Bash): this lexical check cannot bound what a recursive read form in this command traverses, and a deny list is in force — ` +
1173
- `${traversal.length} path(s) taken as traversal roots: ${shown.join(", ")}${traversal.length > shown.length ? `, and ${traversal.length - shown.length} more` : ""}. ` +
1174
- `Use the Grep tool to search a directory tree — it applies the same deny list to every file it reads. ` +
1175
- `${BASH_READONLY_CONFINEMENT_NOTE}`);
1176
- }
1177
- if (boundary.reason !== undefined) {
1178
- const rescueCandidates = [...(boundary.outOfRootPaths ?? []), ...(boundary.outOfRootSpellings ?? [])];
1179
- const rescued = boundary.outOfRootRead === true &&
1180
- (boundary.unresolvedOperands ?? []).length === 0 &&
1181
- (await canonicalBoundary.allResolveInside(rescueCandidates, ctx.signal));
1182
- if (rescued)
1183
- releasedByGrant = rescueCandidates;
1184
- if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
1185
- return readContainmentRefusal(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, boundary.outOfRootPaths ?? []);
1186
- }
1187
- }
1188
- const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? []), ...releasedByGrant], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, { deny: denySeat.denyMatch, containment: true, denyAnswered: NOTHING_ANSWERED, containmentAnswered: NOTHING_ANSWERED }, ctx.signal);
1189
- if (escape !== undefined)
1190
- return escape;
867
+ const refused = await opts?.shellReadAdmission?.("Bash", ctx.toolCallId, command, ctx.signal);
868
+ if (refused !== undefined)
869
+ return refused;
1191
870
  const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
1192
871
  return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true, undefined, readOnlyTick);
1193
872
  },
@@ -11,7 +11,7 @@ export * from "./fs-search-tools.js";
11
11
  export * from "./bash-readonly-classifier.js";
12
12
  export * from "./fs-bash.js";
13
13
  export * from "./read-deny.js";
14
- import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
14
+ import type { ShellReadAdmission } from "./shell-read-check.js";
15
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";
@@ -42,19 +42,18 @@ export interface HandsToolkitOptions {
42
42
  readOnly?: boolean;
43
43
  /** Override the `bash_readonly` command allowlist (default {@link BASH_READONLY_DEFAULT_ALLOW}). */
44
44
  bashReadonlyAllow?: readonly string[];
45
- /** The deployment's shell READ BOUNDARY, for the FULL `bash` face's execute-time canonical recheck
46
- * (see `createBashTool`'s `readBoundary`). The caller passes the SAME seat it gives the gate's shell
47
- * probes, so the boundary that judges a command's written spelling before approval and the leg that
48
- * re-judges its symlink-resolved targets before execution are one declaration. Ignored in `readOnly`
49
- * mode — `bash_readonly` builds its own: containment from the roots it is confined to, DENY from the same
50
- * compiled table this band hands the structured read faces.
45
+ /** The gate's shell read station's pre-spawn ADMISSION (`ShellReadAdmission`, shell-read-check.ts), handed to
46
+ * BOTH shell faces this band mounts: what a shell tool runs is what the station judged before approval, and
47
+ * the admission re-establishes the same candidates right before the spawn. The station itself — ONE parse of
48
+ * the command, its candidates established on the filesystem, judged by the read deny seat and the read
49
+ * roots — is the gate's (`ToolGateInput.shellReadCheck`, built by the Runner's hands phase for every shell
50
+ * face and every doctrine); this band judges nothing on its own.
51
51
  *
52
- * ABSENT ⇒ the leg does not run and costs no I/O: with no boundary declared there is no "outside" for
53
- * a read to be outside of, which is the same reading the probe seats take of an absent boundary. It
54
- * deliberately does NOT fall back to `additionalRoots`/`rootCanonical`: those are the WRITE side's
55
- * fence, they exclude `additionalReadRoots`, and standing a second root table up here would refuse
56
- * reads this deployment sanctions while claiming to enforce the boundary it was never given. */
57
- shellReadBoundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined);
52
+ * ABSENT ⇒ no station judged this mount (a library-direct toolkit with no gate): the full shell runs
53
+ * unconfined as it always did, and `bash_readonly` confines NOTHING — its containment IS the station, so a
54
+ * consumer mounting the read-only band outside the Runner must run `runToolGate` with a station of its own
55
+ * (`createShellReadStation`). */
56
+ shellReadAdmission?: ShellReadAdmission;
58
57
  /** Commit-message attribution trailer for the `bash` git protocol. Default NONE — attribution is a
59
58
  * deployment identity asset, so the branded deployment sets it; pass `"Name <email>"` to advertise
60
59
  * one, `false` (≡ unset) for no trailer section. */
@@ -11,7 +11,7 @@ export * from "./fs-search-tools.js";
11
11
  export * from "./bash-readonly-classifier.js";
12
12
  export * from "./fs-bash.js";
13
13
  export * from "./read-deny.js";
14
- import { BASH_READONLY_DEFAULT_ALLOW, } from "./bash-readonly-classifier.js";
14
+ import { BASH_READONLY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
15
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";
@@ -71,8 +71,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
71
71
  ...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
72
72
  ...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
73
73
  ...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
74
- ...(readFaceRoots !== undefined ? { additionalRoots: readFaceRoots } : {}),
75
- readDeny,
74
+ ...(opts.shellReadAdmission !== undefined ? { shellReadAdmission: opts.shellReadAdmission } : {}),
76
75
  ...(opts.onToolProgress !== undefined ? { onToolProgress: opts.onToolProgress } : {}),
77
76
  })
78
77
  : createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
@@ -87,7 +86,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
87
86
  ...(opts.queuedHumanInput !== undefined ? { queuedHumanInput: opts.queuedHumanInput } : {}),
88
87
  ...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}),
89
88
  ...(additionalRoots !== undefined ? { additionalRoots } : {}),
90
- ...(opts.shellReadBoundary !== undefined ? { readBoundary: opts.shellReadBoundary } : {}),
89
+ ...(opts.shellReadAdmission !== undefined ? { shellReadAdmission: opts.shellReadAdmission } : {}),
91
90
  ...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
92
91
  ...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
93
92
  ...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
@@ -89,6 +89,31 @@ export declare function readDenySeat(matcher: ReadDenyMatcher | undefined): Read
89
89
  export declare function sensitivePathDenyList(pattern: string | readonly string[]): string;
90
90
  /** The deny table's REACH: a deny row holds INSIDE the containment roots, so no root grant answers it. */
91
91
  export declare const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
92
+ /**
93
+ * THE deny refusal sentence — five segments, one mint, every read face: the ACTION, the ABSOLUTE target as the
94
+ * caller spelled it (with its canonical target in parentheses when the two differ), the DISPOSITION (a deny row is
95
+ * refused, never escalated), the table BY NAME with the PATTERN verbatim ({@link sensitivePathDenyList}), and
96
+ * one sentence of why ({@link SENSITIVE_PATH_DENY_SCOPE}). `matchedView` names the view the pattern actually hit
97
+ * when that is the requested SPELLING rather than the target — under a symlink or case alias the two strings
98
+ * differ, only one matches, and naming the other sends the reader chasing a path the pattern does not match.
99
+ * A face prefixes its own `Error (<tool>): ` and may append its own closing clause; the finding itself is
100
+ * never re-spelled.
101
+ */
102
+ export declare function readDenySentence(finding: {
103
+ readonly action?: string;
104
+ readonly target: string;
105
+ readonly canonical?: string;
106
+ readonly pattern: string;
107
+ readonly matchedView?: string;
108
+ }): string;
109
+ /** The per-part budget of the deny sentence — the gate's own card-account cap, not a second number. */
110
+ export declare const READ_DENY_SENTENCE_PART_MAX = 200;
111
+ /** `s` verbatim when it fits `budget`, else elided in its MIDDLE ({@link elideMiddle}). */
112
+ export declare function fitMiddle(s: string, budget: number): string;
113
+ /** `s` shortened to `budget` by removing its MIDDLE, marked — cut through the repo's surrogate-safe
114
+ * primitive, never a raw `slice` (a raw cut leaves half an astral code point on a card and in a durable
115
+ * row). Under a dozen characters the HEAD is taken. */
116
+ export declare function elideMiddle(s: string, budget: number): string;
92
117
  /**
93
118
  * The closed set of built-in tiers. A tier is the deployment's selection unit
94
119
  * ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to
@@ -1,3 +1,5 @@
1
+ import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
2
+ import { PROBE_REASON_MAX } from "../../core/untrusted-text.js";
1
3
  export function readDenySeat(matcher) {
2
4
  const entries = (matcher?.entries ?? []).map((e) => e.pattern);
3
5
  if (matcher === undefined || entries.length === 0)
@@ -9,6 +11,23 @@ export function sensitivePathDenyList(pattern) {
9
11
  return `the sensitive-path read deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
10
12
  }
11
13
  export const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
14
+ export function readDenySentence(finding) {
15
+ const canonicalNote = finding.canonical !== undefined && finding.canonical !== finding.target ? `(canonical target: "${fitMiddle(finding.canonical, READ_DENY_SENTENCE_PART_MAX)}") ` : "";
16
+ const lexicalHit = finding.matchedView !== undefined && finding.matchedView !== (finding.canonical ?? finding.target);
17
+ const judged = lexicalHit ? `the requested path's spelling ("${fitMiddle(finding.matchedView, READ_DENY_SENTENCE_PART_MAX)}") matches` : "the target matches";
18
+ return `${finding.action ?? "reading"} "${fitMiddle(finding.target, READ_DENY_SENTENCE_PART_MAX)}" ${canonicalNote}is refused: ${judged} ${sensitivePathDenyList(fitMiddle(finding.pattern, READ_DENY_SENTENCE_PART_MAX))}. ${SENSITIVE_PATH_DENY_SCOPE}`;
19
+ }
20
+ export const READ_DENY_SENTENCE_PART_MAX = PROBE_REASON_MAX;
21
+ export function fitMiddle(s, budget) {
22
+ return s.length <= budget ? s : elideMiddle(s, budget);
23
+ }
24
+ export function elideMiddle(s, budget) {
25
+ if (budget < 12)
26
+ return sliceHeadSafe(s, Math.max(0, budget));
27
+ const keep = budget - 1;
28
+ const head = Math.ceil(keep / 2);
29
+ return `${sliceHeadSafe(s, head)}\u2026${sliceTailSafe(s, keep - head)}`;
30
+ }
12
31
  export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet", "agent-config"];
13
32
  export const READ_FACE_BUILTIN_DENY_TABLE = [
14
33
  { pattern: ".ssh", tier: "credentials" },
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { sensitivePathDenyList, SENSITIVE_PATH_DENY_SCOPE } from "./read-deny.js";
2
+ import { readDenySentence } from "./read-deny.js";
3
3
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
4
4
  import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
5
5
  import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
@@ -461,15 +461,11 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
461
461
  aliasResolved: canon.aliasResolved === true,
462
462
  });
463
463
  if (hit !== null) {
464
- const lexicalHit = hit.matchedView !== undefined && hit.matchedView !== key;
465
- const judged = lexicalHit
466
- ? `the requested path's spelling ("${hit.matchedView}") matches`
467
- : `the target matches`;
468
464
  return {
469
465
  ok: false,
470
466
  violation: {
471
467
  code: "read_path_denied",
472
- message: `reading "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}is refused: ${judged} ${sensitivePathDenyList(hit.pattern)}. ${SENSITIVE_PATH_DENY_SCOPE}`,
468
+ message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}) }),
473
469
  target: key,
474
470
  pattern: hit.pattern,
475
471
  },