@sema-agent/core 7.33.8 → 7.35.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 (228) hide show
  1. package/CHANGELOG.md +462 -3
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/errors.d.ts +23 -1
  11. package/dist/brain/errors.js +19 -2
  12. package/dist/brain/open-responses.js +1 -0
  13. package/dist/brain/openai.js +1 -0
  14. package/dist/brain/status-sink.d.ts +7 -1
  15. package/dist/brain/stream-engine.d.ts +3 -0
  16. package/dist/brain/stream-engine.js +2 -1
  17. package/dist/brain/stream-shared.d.ts +3 -1
  18. package/dist/brain/stream-shared.js +16 -4
  19. package/dist/core/a2a.d.ts +3 -1
  20. package/dist/core/a2a.js +1 -1
  21. package/dist/core/ask-origin.d.ts +379 -154
  22. package/dist/core/ask-origin.js +224 -45
  23. package/dist/core/ask-question.js +2 -1
  24. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  25. package/dist/core/ask-unresolvable-notice.js +4 -3
  26. package/dist/core/auto-mode-arming.d.ts +17 -4
  27. package/dist/core/auto-mode-arming.js +31 -6
  28. package/dist/core/auto-mode-defaults.d.ts +16 -7
  29. package/dist/core/auto-mode-defaults.js +2 -1
  30. package/dist/core/auto-mode-directive.d.ts +18 -30
  31. package/dist/core/auto-mode-directive.js +9 -18
  32. package/dist/core/auto-mode-prompt.d.ts +62 -1
  33. package/dist/core/auto-mode-prompt.js +42 -4
  34. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  35. package/dist/core/auto-mode-rebuild.js +1 -0
  36. package/dist/core/auto-mode.d.ts +32 -3
  37. package/dist/core/auto-mode.js +80 -41
  38. package/dist/core/checkpoint-store.d.ts +54 -35
  39. package/dist/core/checkpoint-store.js +58 -3
  40. package/dist/core/child-clamp-seats.js +1 -0
  41. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  42. package/dist/core/classifier-unavailable-arm.js +17 -0
  43. package/dist/core/effective-path-target.d.ts +12 -0
  44. package/dist/core/effective-path-target.js +18 -7
  45. package/dist/core/engine-notice.d.ts +53 -2
  46. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  47. package/dist/core/fs-write-gate-policy.js +4 -1
  48. package/dist/core/gate-fold.js +19 -3
  49. package/dist/core/gate-lanes.js +116 -50
  50. package/dist/core/gate-outcome.d.ts +40 -7
  51. package/dist/core/gate-outcome.js +26 -4
  52. package/dist/core/governance-codes.d.ts +18 -2
  53. package/dist/core/governance-codes.js +23 -1
  54. package/dist/core/hooks.d.ts +53 -29
  55. package/dist/core/hooks.js +1 -3
  56. package/dist/core/mcp.js +1 -1
  57. package/dist/core/message-utils.d.ts +2 -1
  58. package/dist/core/message-utils.js +2 -1
  59. package/dist/core/mode-policy.d.ts +185 -0
  60. package/dist/core/mode-policy.js +184 -0
  61. package/dist/core/oracle-isolation.d.ts +3 -3
  62. package/dist/core/permission-rule-model.d.ts +20 -3
  63. package/dist/core/permission-rule-model.js +40 -3
  64. package/dist/core/permission-rule-provider.d.ts +22 -11
  65. package/dist/core/permission-rule-provider.js +17 -7
  66. package/dist/core/permission-rules.d.ts +23 -1
  67. package/dist/core/permission-rules.js +56 -9
  68. package/dist/core/persisted-rule-arms.d.ts +14 -10
  69. package/dist/core/persisted-rule-arms.js +13 -6
  70. package/dist/core/present-plan-tool.d.ts +1 -1
  71. package/dist/core/present-plan-tool.js +3 -3
  72. package/dist/core/read-only-shell.d.ts +19 -19
  73. package/dist/core/read-only-shell.js +38 -12
  74. package/dist/core/retired-keys.d.ts +9 -3
  75. package/dist/core/retired-keys.js +5 -1
  76. package/dist/core/runner/active-skill-scope.js +5 -4
  77. package/dist/core/runner/assemble-result.js +2 -2
  78. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  79. package/dist/core/runner/classifier-ladder.js +29 -0
  80. package/dist/core/runner/contracts.d.ts +62 -38
  81. package/dist/core/runner/denial-limit-arms.d.ts +35 -7
  82. package/dist/core/runner/denial-limit-arms.js +15 -9
  83. package/dist/core/runner/env-capability-census.js +1 -1
  84. package/dist/core/runner/file-history-request.d.ts +26 -0
  85. package/dist/core/runner/file-history-request.js +90 -0
  86. package/dist/core/runner/gate-exit.d.ts +30 -4
  87. package/dist/core/runner/gate-exit.js +28 -7
  88. package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
  89. package/dist/core/runner/permission-rule-lanes.js +55 -48
  90. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  91. package/dist/core/runner/persisted-path-rows.js +89 -0
  92. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  93. package/dist/core/runner/prepare-artifact.js +9 -3
  94. package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
  95. package/dist/core/runner/prepare-ask-lane.js +3 -3
  96. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  97. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  98. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  99. package/dist/core/runner/prepare-file-history.js +4 -90
  100. package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
  101. package/dist/core/runner/prepare-gate-stations.js +46 -29
  102. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  103. package/dist/core/runner/prepare-hands-readface.js +64 -53
  104. package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
  105. package/dist/core/runner/prepare-inherited-gate.js +45 -25
  106. package/dist/core/runner/prepare-park-ask.d.ts +6 -4
  107. package/dist/core/runner/prepare-park-ask.js +7 -5
  108. package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
  109. package/dist/core/runner/prepare-policy-chain.js +203 -180
  110. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  111. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  112. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  113. package/dist/core/runner/prepare-run-refs.js +9 -4
  114. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  115. package/dist/core/runner/prepare-safety-scan.js +19 -3
  116. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  117. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  118. package/dist/core/runner/prepare-task.js +56 -24
  119. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  120. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  121. package/dist/core/runner/read-root-grant.d.ts +9 -0
  122. package/dist/core/runner/read-root-grant.js +12 -0
  123. package/dist/core/runner/retired-runner-deps.js +1 -0
  124. package/dist/core/runner/run-brain-sinks.js +3 -1
  125. package/dist/core/runner/run-identity-wiring.js +1 -1
  126. package/dist/core/runner/run-leg.js +29 -18
  127. package/dist/core/runner/run-notification-lane.d.ts +3 -0
  128. package/dist/core/runner/run-notification-lane.js +3 -1
  129. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  130. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  131. package/dist/core/runner/run-turn-boundary.js +1 -1
  132. package/dist/core/runner/runtask.d.ts +1 -0
  133. package/dist/core/runner/runtask.js +8 -11
  134. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  135. package/dist/core/runner/session-mode-seats.js +73 -0
  136. package/dist/core/runner/session-rule-policy.js +3 -2
  137. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  138. package/dist/core/runner/stream-mode-verb.js +19 -0
  139. package/dist/core/runner/turn-attachments.d.ts +3 -3
  140. package/dist/core/runner/turn-attachments.js +1 -1
  141. package/dist/core/runner/write-protection-station.d.ts +7 -1
  142. package/dist/core/runner/write-protection-station.js +6 -3
  143. package/dist/core/runner-deps.d.ts +31 -34
  144. package/dist/core/runtime-caps.d.ts +12 -3
  145. package/dist/core/safety-axis-vocab.d.ts +2 -2
  146. package/dist/core/safety-axis-vocab.js +1 -1
  147. package/dist/core/secret-env.d.ts +30 -58
  148. package/dist/core/secret-env.js +149 -586
  149. package/dist/core/sensitive-path-policy.d.ts +4 -2
  150. package/dist/core/sensitive-path-policy.js +41 -31
  151. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  152. package/dist/core/task-event.d.ts +1 -1
  153. package/dist/core/task-result.d.ts +1 -1
  154. package/dist/core/task-spec.d.ts +41 -89
  155. package/dist/core/task-stream.d.ts +16 -0
  156. package/dist/core/terminal-cause.js +1 -0
  157. package/dist/core/tighten-task-spec.d.ts +2 -2
  158. package/dist/core/tighten-task-spec.js +12 -14
  159. package/dist/core/tool-catalog-entries.js +4 -3
  160. package/dist/core/tool-policy.d.ts +84 -30
  161. package/dist/core/tool-policy.js +76 -16
  162. package/dist/core/tool-registry.d.ts +38 -0
  163. package/dist/core/tool-registry.js +25 -1
  164. package/dist/core/tool-roster.d.ts +17 -2
  165. package/dist/core/tool-roster.js +11 -1
  166. package/dist/core/tool-spec.d.ts +8 -9
  167. package/dist/core/trace.d.ts +18 -6
  168. package/dist/core/types.d.ts +2 -1
  169. package/dist/core/wiring-manifest.d.ts +31 -5
  170. package/dist/core/wiring-manifest.js +28 -1
  171. package/dist/core/write-protect.d.ts +11 -1
  172. package/dist/core/write-protect.js +26 -15
  173. package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
  174. package/dist/engine/execution-env/node-execution-env.js +16 -3
  175. package/dist/engine/harness/agent-harness.js +2 -1
  176. package/dist/engine/harness/types.d.ts +6 -7
  177. package/dist/engine/llm/types.d.ts +2 -4
  178. package/dist/engine/loop/types.d.ts +7 -0
  179. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  180. package/dist/index.d.ts +18 -8
  181. package/dist/index.js +15 -6
  182. package/dist/orchestration/workflow-governance.js +1 -1
  183. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  184. package/dist/orchestration/workflow-size-guideline.js +2 -2
  185. package/dist/stores/file/index.d.ts +5 -3
  186. package/dist/stores/file/session-store.d.ts +8 -2
  187. package/dist/stores/file/session-store.js +5 -5
  188. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  189. package/dist/tools/artifact/artifact-tool.js +16 -1
  190. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  191. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  192. package/dist/tools/fs/containment-face.d.ts +9 -3
  193. package/dist/tools/fs/containment-face.js +2 -0
  194. package/dist/tools/fs/file-read-check.d.ts +50 -0
  195. package/dist/tools/fs/file-read-check.js +79 -0
  196. package/dist/tools/fs/fs-bash.d.ts +3 -3
  197. package/dist/tools/fs/fs-read.d.ts +1 -0
  198. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  199. package/dist/tools/fs/fs-search-tools.js +4 -4
  200. package/dist/tools/fs/fs-shared.js +20 -6
  201. package/dist/tools/fs/fs-write.d.ts +4 -3
  202. package/dist/tools/fs/fs-write.js +9 -9
  203. package/dist/tools/fs/index.d.ts +39 -3
  204. package/dist/tools/fs/index.js +38 -15
  205. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  206. package/dist/tools/fs/read-deny-home.js +225 -0
  207. package/dist/tools/fs/read-deny.d.ts +176 -34
  208. package/dist/tools/fs/read-deny.js +268 -61
  209. package/dist/tools/fs/safety.d.ts +6 -1
  210. package/dist/tools/fs/safety.js +18 -5
  211. package/dist/tools/fs/search.d.ts +3 -0
  212. package/dist/tools/fs/search.js +47 -9
  213. package/dist/tools/fs/shell-read-check.d.ts +52 -13
  214. package/dist/tools/fs/shell-read-check.js +199 -20
  215. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  216. package/dist/tools/fs/shell-read-establish.js +134 -21
  217. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  218. package/dist/tools/fs/shell-read-identity.js +21 -0
  219. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  220. package/dist/tools/fs/shell-read-judge.js +43 -18
  221. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  222. package/dist/tools/fs/shell-read-plan.js +26 -4
  223. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  224. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  225. package/package.json +1 -1
  226. package/test/export-surface.snapshot.json +117 -11
  227. package/wire-changes/core@7.34.0.json +2621 -0
  228. package/wire-changes/core@7.35.0.json +584 -0
@@ -1,21 +1,137 @@
1
1
  import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
2
+ import { isAbsolutePathForm, normalizeAbsPathLexically, pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "./safety.js";
2
3
  import { PROBE_REASON_MAX } from "../../core/untrusted-text.js";
4
+ export const READ_DENY_HOME_WORD = "~";
5
+ export function readDenyPatternRun(pattern) {
6
+ const segments = pattern.split("/").filter(Boolean);
7
+ return segments[0] === READ_DENY_HOME_WORD ? { homeAnchored: true, run: segments.slice(1).join("/") } : { homeAnchored: false, run: pattern };
8
+ }
9
+ export function readDenyRunHead(run) {
10
+ const segments = run.split("/").filter(Boolean);
11
+ const wild = segments.findIndex((seg) => seg.includes("*"));
12
+ return wild === -1 ? segments.length : wild;
13
+ }
14
+ export const DENY_FACES = ["read", "write"];
15
+ export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
16
+ const leads = new Map();
17
+ const partsOf = (head) => {
18
+ const family = pathFamilyOf({ root: head });
19
+ const id = pathIdentityOf(head, family, head);
20
+ if (!id.ok)
21
+ return [head];
22
+ const unc = id.identity.startsWith("//");
23
+ const segments = id.identity.split("/").filter((seg) => seg !== "");
24
+ const parts = [];
25
+ for (let k = unc ? 2 : 1; k <= segments.length; k++)
26
+ parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family));
27
+ return parts.length > 0 ? parts : [head];
28
+ };
29
+ const partsByRow = rows.map((row) => (row.head === undefined ? [] : partsOf(row.head)));
30
+ for (const parts of partsByRow)
31
+ for (const part of parts)
32
+ if (!leads.has(part))
33
+ leads.set(part, leadsTo(part).catch(() => undefined));
34
+ const out = [];
35
+ for (const [i, row] of rows.entries()) {
36
+ out.push(row);
37
+ let parentLed;
38
+ let parentPart;
39
+ for (const part of partsByRow[i]) {
40
+ const led = await leads.get(part);
41
+ const extendsParent = led !== undefined && parentLed !== undefined && parentPart !== undefined && sameLocation(led, `${parentLed.replace(/[\\/]+$/, "")}/${part.slice(parentPart.replace(/[\\/]+$/, "").length).replace(/^[\\/]+/, "")}`);
42
+ const twin = extendsParent ? undefined : rowLedTo(row, part, led);
43
+ if (twin !== undefined)
44
+ out.push(twin);
45
+ parentLed = led;
46
+ parentPart = part;
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ function sameLocation(a, b) {
52
+ const family = pathFamilyOf({ root: a });
53
+ const x = pathIdentityOf(a, family, a);
54
+ const y = pathIdentityOf(b, family, b);
55
+ return x.ok && y.ok && x.identity === y.identity;
56
+ }
57
+ function rowLedTo(row, head, at) {
58
+ if (at === undefined)
59
+ return undefined;
60
+ const family = pathFamilyOf({ root: head });
61
+ const from = pathIdentityOf(at, family, head);
62
+ const to = pathIdentityOf(head, family, head);
63
+ if (!from.ok || !to.ok || from.identity === to.identity)
64
+ return undefined;
65
+ const base = from.identity;
66
+ const under = base.endsWith("/") ? base : `${base}/`;
67
+ const onto = to.identity.endsWith("/") ? to.identity.slice(0, -1) : to.identity;
68
+ return {
69
+ rule: row.rule,
70
+ tool: row.tool,
71
+ pattern: row.pattern,
72
+ faces: row.faces,
73
+ reaches: (absolutePath) => {
74
+ const id = pathIdentityOf(absolutePath, family, head);
75
+ if (!id.ok)
76
+ return false;
77
+ if (id.identity === base)
78
+ return row.reaches(to.identity);
79
+ return id.identity.startsWith(under) && row.reaches(`${onto}/${id.identity.slice(under.length)}`);
80
+ },
81
+ };
82
+ }
83
+ function ruleRowHit(rows, face, path) {
84
+ for (const r of rows)
85
+ if (r.faces.includes(face) && r.reaches(path))
86
+ return r;
87
+ return undefined;
88
+ }
89
+ export function compileWriteDeny(rules = []) {
90
+ const ruleRows = rules.filter((r) => r.faces.includes("write"));
91
+ const matchPath = (path) => {
92
+ const hit = ruleRowHit(ruleRows, "write", path);
93
+ return hit === undefined ? null : { pattern: hit.rule };
94
+ };
95
+ return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts) };
96
+ }
97
+ function matchTargetOver(matchPath, canonicalKey, lexicalView, opts) {
98
+ const canonical = matchPath(canonicalKey, opts);
99
+ if (canonical !== null)
100
+ return { ...canonical, matchedView: canonicalKey };
101
+ if (lexicalView !== undefined) {
102
+ const lexical = matchPath(lexicalView);
103
+ if (lexical !== null)
104
+ return { ...lexical, matchedView: lexicalView };
105
+ }
106
+ for (const hop of opts?.hops ?? []) {
107
+ const viaHop = matchPath(hop);
108
+ if (viaHop !== null)
109
+ return { ...viaHop, matchedView: hop };
110
+ }
111
+ return null;
112
+ }
3
113
  export function readDenySeat(matcher) {
4
- const entries = (matcher?.entries ?? []).map((e) => e.pattern);
114
+ const entries = [...(matcher?.entries ?? []).map((e) => e.pattern), ...(matcher?.ruleRows ?? []).map((r) => r.rule)];
5
115
  if (matcher === undefined || entries.length === 0)
6
116
  return { denyEntries: entries };
7
- return { denyMatch: (p) => matcher.matchPath(p)?.pattern ?? null, denyEntries: entries };
117
+ return {
118
+ denyMatch: (p, opts) => matcher.matchPath(p, opts)?.pattern ?? null,
119
+ denyAtOrBelow: (p) => (matcher.rowAtOrBelow !== undefined ? (matcher.rowAtOrBelow(p)?.pattern ?? null) : entries[0]),
120
+ denyEntries: entries,
121
+ };
8
122
  }
9
- export function sensitivePathDenyList(pattern) {
123
+ export function sensitivePathDenyList(pattern, face = "read") {
10
124
  const rows = typeof pattern === "string" ? [pattern] : pattern;
11
- return `the sensitive-path read deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
125
+ return `the ${face === "write" ? "write" : "sensitive-path read"} deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
12
126
  }
13
127
  export const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
128
+ export const WRITE_DENY_SCOPE = "A write deny row holds regardless of the containment roots and of the permission mode.";
14
129
  export function readDenySentence(finding) {
130
+ const write = finding.face === "write";
15
131
  const canonicalNote = finding.canonical !== undefined && finding.canonical !== finding.target ? `(canonical target: "${fitMiddle(finding.canonical, READ_DENY_SENTENCE_PART_MAX)}") ` : "";
16
132
  const lexicalHit = finding.matchedView !== undefined && finding.matchedView !== (finding.canonical ?? finding.target);
17
133
  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}`;
134
+ return `${finding.action ?? (write ? "writing" : "reading")} "${fitMiddle(finding.target, READ_DENY_SENTENCE_PART_MAX)}" ${canonicalNote}is refused: ${judged} ${sensitivePathDenyList(fitMiddle(finding.pattern, READ_DENY_SENTENCE_PART_MAX), write ? "write" : "read")}. ${write ? WRITE_DENY_SCOPE : SENSITIVE_PATH_DENY_SCOPE}`;
19
135
  }
20
136
  export const READ_DENY_SENTENCE_PART_MAX = PROBE_REASON_MAX;
21
137
  export function fitMiddle(s, budget) {
@@ -49,6 +165,7 @@ export const READ_FACE_BUILTIN_DENY_TABLE = [
49
165
  { pattern: ".local/share/keyrings", tier: "credentials" },
50
166
  { pattern: "Library/Keychains", tier: "credentials" },
51
167
  { pattern: ".credentials.json", tier: "credentials" },
168
+ { pattern: "credentials.env", tier: "credentials" },
52
169
  { pattern: ".codex/auth.json", tier: "credentials" },
53
170
  { pattern: ".config/github-copilot", tier: "credentials" },
54
171
  { pattern: ".gemini/oauth_creds.json", tier: "credentials" },
@@ -69,23 +186,23 @@ export const READ_FACE_BUILTIN_DENY_TABLE = [
69
186
  { pattern: "Library/Application Support/Exodus", tier: "wallet" },
70
187
  { pattern: "Library/Application Support/Ledger Live", tier: "wallet" },
71
188
  { pattern: "wallet.dat", tier: "wallet" },
72
- { pattern: ".sema/settings.json", tier: "agent-config" },
73
- { pattern: ".sema/settings.local.json", tier: "agent-config" },
74
- { pattern: ".sema.*", tier: "agent-config" },
75
- { pattern: ".claude/settings.json", tier: "agent-config" },
76
- { pattern: ".claude/settings.local.json", tier: "agent-config" },
77
- { pattern: ".claude.*", tier: "agent-config" },
78
- { pattern: ".mcp.json", tier: "agent-config" },
79
- { pattern: ".ai-agent/.env", tier: "agent-config" },
80
- { pattern: ".sema/engine-data/.env", tier: "agent-config" },
81
- { pattern: ".codex/config.toml", tier: "agent-config" },
82
- { pattern: ".cursor/mcp.json", tier: "agent-config" },
83
- { pattern: ".continue/config.json", tier: "agent-config" },
84
- { pattern: ".continue/config.yaml", tier: "agent-config" },
85
- { pattern: ".aider.conf.yml", tier: "agent-config" },
86
- { pattern: ".gemini/settings.json", tier: "agent-config" },
189
+ { pattern: "~/.sema/settings.json", tier: "agent-config" },
190
+ { pattern: "~/.sema/settings.local.json", tier: "agent-config" },
191
+ { pattern: "~/.sema.*", tier: "agent-config" },
192
+ { pattern: "~/.claude/settings.json", tier: "agent-config" },
193
+ { pattern: "~/.claude/settings.local.json", tier: "agent-config" },
194
+ { pattern: "~/.claude.*", tier: "agent-config" },
195
+ { pattern: "~/.mcp.json", tier: "agent-config" },
196
+ { pattern: "~/.ai-agent/.env", tier: "agent-config" },
197
+ { pattern: "~/.sema/engine-data/.env", tier: "agent-config" },
198
+ { pattern: "~/.codex/config.toml", tier: "agent-config" },
199
+ { pattern: "~/.cursor/mcp.json", tier: "agent-config" },
200
+ { pattern: "~/.continue/config.json", tier: "agent-config" },
201
+ { pattern: "~/.continue/config.yaml", tier: "agent-config" },
202
+ { pattern: "~/.aider.conf.yml", tier: "agent-config" },
203
+ { pattern: "~/.gemini/settings.json", tier: "agent-config" },
87
204
  ];
88
- export const READ_DENY_DEFAULT_TIERS = [];
205
+ export const READ_DENY_DEFAULT_TIERS = ["credentials", "agent-config"];
89
206
  export function resolveReadDenyBuiltins(config) {
90
207
  let activeTiers;
91
208
  if (config?.tiers === undefined) {
@@ -112,13 +229,21 @@ export function resolveReadDenyBuiltins(config) {
112
229
  }
113
230
  for (const name of config.exclude) {
114
231
  if (typeof name !== "string" || !READ_FACE_BUILTIN_DENY_TABLE.some((r) => r.pattern === name)) {
115
- throw new Error(`readDenyBuiltinExclude: ${JSON.stringify(name)} names no built-in row — row names are the canonical pattern texts of READ_FACE_BUILTIN_DENY_TABLE (e.g. ".ssh", ".config/gcloud").`);
232
+ const anchored = typeof name === "string" ? READ_FACE_BUILTIN_DENY_TABLE.find((r) => r.pattern === `${READ_DENY_HOME_WORD}/${name}`) : undefined;
233
+ throw new Error(`readDenyBuiltinExclude: ${JSON.stringify(name)} names no built-in row — ${anchored !== undefined ? `the row is anchored at the home directory and is named ${JSON.stringify(anchored.pattern)}; ` : ""}row names are the canonical pattern texts of READ_FACE_BUILTIN_DENY_TABLE (e.g. ".ssh", ".config/gcloud", "~/.sema/settings.json").`);
116
234
  }
117
235
  }
118
236
  excluded = new Set(config.exclude);
119
237
  }
120
238
  return READ_FACE_BUILTIN_DENY_TABLE.filter((r) => activeTiers.has(r.tier) && !excluded.has(r.pattern));
121
239
  }
240
+ export function readDenyBuiltinConfigOf(seats) {
241
+ resolveReadDenyBuiltins(seats);
242
+ return {
243
+ ...(seats.tiers !== undefined ? { tiers: [...seats.tiers] } : {}),
244
+ ...(seats.exclude !== undefined ? { exclude: [...seats.exclude] } : {}),
245
+ };
246
+ }
122
247
  export function activeReadDenyBuiltinTiers(config) {
123
248
  const rows = resolveReadDenyBuiltins(config);
124
249
  return READ_DENY_BUILTIN_TIERS.filter((t) => rows.some((r) => r.tier === t));
@@ -145,6 +270,30 @@ export function compileSegmentPattern(raw, fold) {
145
270
  return { raw, segments, foldAscii: fold === "ascii" };
146
271
  }
147
272
  export function matchSegmentPatterns(path, compiled, opts) {
273
+ const views = segmentViewsOf(path, opts);
274
+ const rooted = isAbsolutePathForm(path);
275
+ const lowered = new Map();
276
+ for (const pat of compiled) {
277
+ for (const view of views) {
278
+ const segs = pat.foldAscii ? (lowered.get(view) ?? lowered.set(view, view.map(asciiLower)).get(view)) : view;
279
+ const n = pat.segments.length;
280
+ const runAt = (i) => i + n <= segs.length && pat.segments.every((re, j) => re.test(segs[i + j] ?? ""));
281
+ if (pat.anchors !== undefined && rooted) {
282
+ for (const anchor of pat.anchors)
283
+ if (anchor.every((a, j) => segs[j] === a) && runAt(anchor.length))
284
+ return pat.raw;
285
+ continue;
286
+ }
287
+ if (pat.rootedOnly === true)
288
+ continue;
289
+ for (let i = 0; i + n <= segs.length; i++)
290
+ if (runAt(i))
291
+ return pat.raw;
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ function segmentViewsOf(path, opts) {
148
297
  const literalSegs = path.split(/[\\/]/).filter(Boolean);
149
298
  const win32Segs = [];
150
299
  for (const raw of literalSegs) {
@@ -164,20 +313,41 @@ export function matchSegmentPatterns(path, compiled, opts) {
164
313
  }
165
314
  const sameView = opts?.aliasResolved === true ||
166
315
  (win32Segs.length === literalSegs.length && win32Segs.every((s, i) => s === literalSegs[i]));
167
- const views = sameView ? [literalSegs] : [literalSegs, win32Segs];
168
- const lowered = new Map();
169
- for (const pat of compiled) {
170
- for (const view of views) {
171
- const segs = pat.foldAscii ? (lowered.get(view) ?? lowered.set(view, view.map(asciiLower)).get(view)) : view;
172
- const n = pat.segments.length;
173
- for (let i = 0; i + n <= segs.length; i++) {
174
- if (pat.segments.every((re, j) => re.test(segs[i + j] ?? "")))
175
- return pat.raw;
176
- }
177
- }
316
+ return sameView ? [literalSegs] : [literalSegs, win32Segs];
317
+ }
318
+ function rowMeetsPath(pat, segs, rooted) {
319
+ const n = pat.segments.length;
320
+ if (pat.anchors !== undefined && rooted) {
321
+ return pat.anchors.some((a) => {
322
+ const k = Math.min(segs.length, a.length + n);
323
+ for (let i = 0; i < k; i++)
324
+ if (i < a.length ? segs[i] !== a[i] : !pat.segments[i - a.length].test(segs[i]))
325
+ return false;
326
+ return true;
327
+ });
328
+ }
329
+ if (pat.rootedOnly === true)
330
+ return false;
331
+ for (let i = 0; i < segs.length; i++) {
332
+ const m = Math.min(n, segs.length - i);
333
+ if (pat.segments.slice(0, m).every((re, j) => re.test(segs[i + j])))
334
+ return true;
335
+ }
336
+ return false;
337
+ }
338
+ function rowAtOrBelowOf(rows, path) {
339
+ const rooted = isAbsolutePathForm(path);
340
+ const views = segmentViewsOf(path);
341
+ for (const pat of rows) {
342
+ for (const view of views)
343
+ if (rowMeetsPath(pat, pat.foldAscii ? view.map(asciiLower) : view, rooted))
344
+ return pat.raw;
178
345
  }
179
346
  return null;
180
347
  }
348
+ function homeAnchorSegments(home) {
349
+ return normalizeAbsPathLexically(home).split(/[\\/]/).filter(Boolean);
350
+ }
181
351
  function rgGlobEscape(pattern) {
182
352
  return pattern.replace(/[?[\]{}]/g, (c) => `[${c}]`);
183
353
  }
@@ -193,9 +363,10 @@ function normalizeEntry(entry, layer) {
193
363
  throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains a backslash — patterns are "/"-separated segment runs (both path families are matched); spell the segments with "/".`);
194
364
  }
195
365
  const segments = shape.pattern.split("/").filter(Boolean);
196
- if (segments.length === 0) {
197
- throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments and would deny nothing. ` +
198
- `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*"); remove the entry or spell the segments.`);
366
+ const { homeAnchored, run } = readDenyPatternRun(segments.join("/"));
367
+ if (run.length === 0) {
368
+ throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments${homeAnchored ? " below the home word" : ""} and would ${homeAnchored ? "name the whole home directory" : "deny nothing"}. ` +
369
+ `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*", "~/.sema/settings.json"); remove the entry or spell the segments.`);
199
370
  }
200
371
  return { pattern: segments.join("/"), caseSensitive: shape.caseSensitive === true };
201
372
  }
@@ -211,13 +382,27 @@ export function persistedReadDenyEntryProblem(e) {
211
382
  if (pattern.includes("\\"))
212
383
  return "pattern contains a backslash";
213
384
  const segments = pattern.split("/").filter(Boolean);
214
- if (segments.length === 0)
385
+ if (readDenyPatternRun(segments.join("/")).run.length === 0)
215
386
  return "pattern has no path segments";
216
387
  if (segments.join("/") !== pattern)
217
388
  return "pattern is not in canonical a/b form";
218
389
  return null;
219
390
  }
220
- export function compileReadDeny(additions = [], layer = "additions", builtin) {
391
+ export function checkedReadDenyHomes(homes, seat = "readDenyHomes") {
392
+ if (!Array.isArray(homes) || !homes.every((h) => typeof h === "string" && isAbsolutePathForm(h))) {
393
+ let shown;
394
+ try {
395
+ shown = JSON.stringify(homes) ?? String(homes);
396
+ }
397
+ catch {
398
+ shown = String(homes);
399
+ }
400
+ throw new Error(`${seat}: expected an array of absolute ${seat === "readDenyHomes" ? "home paths" : "locations"}, got ${shown} — a home-anchored row ("~/…") is placed ${seat === "readDenyHomes" ? "under each" : "at each"}, so a relative or empty spelling would guard a place that is not the home's.`);
401
+ }
402
+ return homes;
403
+ }
404
+ export function compileReadDeny(additions = [], layer = "additions", builtin, rules = [], homes = [], linked = new Map()) {
405
+ const ruleRows = rules.filter((r) => r.faces.includes("read"));
221
406
  const normalized = [];
222
407
  const seen = new Set();
223
408
  const push = (e) => {
@@ -231,44 +416,66 @@ export function compileReadDeny(additions = [], layer = "additions", builtin) {
231
416
  push(normalizeEntry(row.pattern, "built-in"));
232
417
  for (const entry of additions)
233
418
  push(normalizeEntry(entry, layer));
419
+ const homeSegments = [...new Set(checkedReadDenyHomes(homes))].map(homeAnchorSegments);
234
420
  const compiled = normalized.map((e) => {
235
- const c = compileSegmentPattern(e.pattern, e.caseSensitive ? "none" : "ascii");
421
+ const { homeAnchored, run } = readDenyPatternRun(e.pattern);
422
+ const fold = e.caseSensitive ? "none" : "ascii";
423
+ const c = compileSegmentPattern(run, fold);
236
424
  if (c === null)
237
425
  throw new Error(`readDenyPatterns: pattern ${JSON.stringify(e.pattern)} compiled to zero segments.`);
238
- return c;
426
+ if (!homeAnchored || homeSegments.length === 0)
427
+ return { ...c, raw: e.pattern };
428
+ return { ...c, raw: e.pattern, anchors: homeSegments.map((segs) => (fold === "ascii" ? segs.map(asciiLower) : segs)) };
429
+ });
430
+ if (!(linked instanceof Map))
431
+ throw new Error(`readDenyHomeLinks: expected a Map from a row's pattern to its locations, got ${typeof linked}.`);
432
+ const linkedPlacements = [];
433
+ normalized.forEach((e, i) => {
434
+ const locations = linked.get(e.pattern);
435
+ const { homeAnchored, run } = readDenyPatternRun(e.pattern);
436
+ if (locations === undefined || !homeAnchored)
437
+ return;
438
+ const c = compiled[i];
439
+ const rest = c.segments.slice(readDenyRunHead(run));
440
+ for (const location of new Set(checkedReadDenyHomes(locations, "readDenyHomeLinks"))) {
441
+ const segs = homeAnchorSegments(location);
442
+ linkedPlacements.push({ raw: e.pattern, segments: rest, foldAscii: c.foldAscii, anchors: [c.foldAscii ? segs.map(asciiLower) : segs], rootedOnly: true });
443
+ }
239
444
  });
445
+ const judged = linkedPlacements.length === 0 ? compiled : [...compiled, ...linkedPlacements];
240
446
  const rgExclusionGlobs = [];
241
447
  const rgProbeGlobs = [];
242
- for (const e of normalized) {
448
+ normalized.forEach((e, i) => {
449
+ if (compiled[i].anchors !== undefined)
450
+ return;
243
451
  const flag = e.caseSensitive ? "--glob" : "--iglob";
244
- const g = rgGlobEscape(e.pattern);
452
+ const g = rgGlobEscape(readDenyPatternRun(e.pattern).run);
245
453
  rgExclusionGlobs.push({ flag, glob: `!**/${g}` }, { flag, glob: `!**/${g}/**` });
246
454
  rgProbeGlobs.push({ flag, glob: `**/${g}` }, { flag, glob: `**/${g}/**` });
247
- }
455
+ });
456
+ const placed = judged.filter((c) => c.anchors !== undefined);
457
+ const unexcludableUnder = (scope) => placed.length > 0 && (!isAbsolutePathForm(scope) || rowAtOrBelowOf(placed, normalizeAbsPathLexically(scope)) !== null);
458
+ const rowAtOrBelow = (path) => {
459
+ const hit = rowAtOrBelowOf(judged, path);
460
+ if (hit !== null)
461
+ return { pattern: hit };
462
+ return ruleRows.length === 0 ? null : { pattern: ruleRows[0].rule };
463
+ };
248
464
  const matchPath = (path, opts) => {
249
- const hit = matchSegmentPatterns(path, compiled, opts);
250
- return hit === null ? null : { pattern: hit };
465
+ const hit = matchSegmentPatterns(path, judged, opts);
466
+ if (hit !== null)
467
+ return { pattern: hit };
468
+ const rule = ruleRows.length === 0 || opts?.walked === true ? undefined : ruleRowHit(ruleRows, "read", path);
469
+ return rule === undefined ? null : { pattern: rule.rule };
251
470
  };
252
471
  return {
253
472
  entries: normalized,
473
+ ...(ruleRows.length > 0 ? { ruleRows } : {}),
254
474
  matchPath,
255
- matchTarget(canonicalKey, lexicalView, opts) {
256
- const canonical = matchPath(canonicalKey, opts);
257
- if (canonical !== null)
258
- return { ...canonical, matchedView: canonicalKey };
259
- if (lexicalView !== undefined) {
260
- const lexical = matchPath(lexicalView);
261
- if (lexical !== null)
262
- return { ...lexical, matchedView: lexicalView };
263
- }
264
- for (const hop of opts?.hops ?? []) {
265
- const viaHop = matchPath(hop);
266
- if (viaHop !== null)
267
- return { ...viaHop, matchedView: hop };
268
- }
269
- return null;
270
- },
475
+ matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts),
271
476
  rgExclusionGlobs,
272
477
  rgProbeGlobs,
478
+ unexcludableUnder,
479
+ rowAtOrBelow,
273
480
  };
274
481
  }
@@ -93,7 +93,7 @@ export type ReadFileState = Map<string, ReadEntry>;
93
93
  export declare function sha256(content: string): string;
94
94
  /** A failed path resolution / invariant check, surfaced to the model as a self-correctable tool error. */
95
95
  export interface FsViolation {
96
- code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid" | "read_path_denied";
96
+ code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid" | "read_path_denied" | "write_path_denied";
97
97
  /** Set on the `not_read` violation raised by an AUTO-TRUNCATED read
98
98
  * ({@link ReadEntry.isPartialView}) rather than by a never-read file. Both carry the same CC-verbatim
99
99
  * message; only this one needs {@link PARTIAL_VIEW_READ_ESCAPE_HINT} appended, because for it a plain
@@ -378,6 +378,10 @@ export declare function isPathIdentity(s: string, family: PathFamily | undefined
378
378
  /** Is the identity a DIRECTORY a rule may speak for — anything but the tree's bare root (`/`, a drive root
379
379
  * `/c`, a share root `//host/share`), which admits nothing? */
380
380
  export declare function isIdentityDir(identity: PathIdentity, family: PathFamily | undefined): boolean;
381
+ /** A spelling of an identity that the environment resolves as a path of the family, and that reads as that family on its
382
+ * own ({@link isWinFormPath}) — the one way back from the comparison form: posix, the identity; a win drive identity
383
+ * `/c/rest` ⇒ `c:\rest`; a win UNC identity `//host/share/rest` ⇒ `\\host\share\rest`. */
384
+ export declare function spellingOfIdentity(identity: PathIdentity, family: PathFamily | undefined): string;
381
385
  /** Does the spelling carry a `.` or `..` segment under the family's separators (a backslash is a name byte on
382
386
  * POSIX)? A rule body with one admits nothing: folding it could make it name a second directory. */
383
387
  export declare function carriesDotSegment(spelled: string, family: PathFamily | undefined): boolean;
@@ -444,6 +448,7 @@ export type RootsSeat = readonly string[] | (() => readonly string[] | undefined
444
448
  * violation. Defense-in-depth at the tool layer; NOT a substitute for a sandboxed `executionEnv`.
445
449
  */
446
450
  export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRoots?: RootsSeat, exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
451
+ readonly face?: "write";
447
452
  matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
448
453
  readonly aliasResolved?: boolean;
449
454
  }): {
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readDenySentence } from "./read-deny.js";
3
+ import { homeViewsOf } from "./read-deny-home.js";
3
4
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
4
5
  import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
5
6
  import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
@@ -399,6 +400,13 @@ export function isIdentityDir(identity, family) {
399
400
  return identity !== "/";
400
401
  return identity.length > identityAnchorOf(identity).length;
401
402
  }
403
+ export function spellingOfIdentity(identity, family) {
404
+ if (family !== "win")
405
+ return identity;
406
+ if (isUncIdentity(identity))
407
+ return identity.replace(/\//g, "\\");
408
+ return `${identity.slice(1, 2)}:${(identity.length > 2 ? identity.slice(2) : "/").replace(/\//g, "\\")}`;
409
+ }
402
410
  export function carriesDotSegment(spelled, family) {
403
411
  return (family === "win" ? /(^|[\\/])\.{1,2}([\\/]|$)/ : /(^|\/)\.{1,2}(\/|$)/).test(spelled);
404
412
  }
@@ -458,15 +466,20 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
458
466
  return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
459
467
  }
460
468
  if (readDeny !== undefined) {
461
- const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), {
462
- aliasResolved: canon.aliasResolved === true,
463
- });
469
+ const judged = { aliasResolved: canon.aliasResolved === true };
470
+ let hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), judged);
471
+ if (hit === null) {
472
+ const homeView = await homeViewsOf(env, signal).of(key);
473
+ if (homeView !== undefined)
474
+ hit = readDeny.matchTarget(key, homeView, judged);
475
+ }
464
476
  if (hit !== null) {
477
+ const write = readDeny.face === "write";
465
478
  return {
466
479
  ok: false,
467
480
  violation: {
468
- code: "read_path_denied",
469
- message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}) }),
481
+ code: write ? "write_path_denied" : "read_path_denied",
482
+ message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}), ...(write ? { face: "write" } : {}) }),
470
483
  target: key,
471
484
  pattern: hit.pattern,
472
485
  },
@@ -170,6 +170,8 @@ export interface ReadDenyJudge {
170
170
  flag: "--glob" | "--iglob";
171
171
  glob: string;
172
172
  }>;
173
+ /** read-deny.ts `ReadDenyMatcher.unexcludableUnder`: a scope where an entry the rg globs cannot exclude may lie. */
174
+ unexcludableUnder?(scope: string): boolean;
173
175
  }
174
176
  /** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit.
175
177
  * `deny` prunes the walk (note appended to every output shape); `denyOut` is the
@@ -286,6 +288,7 @@ type GrepOutputMode = "content" | "files_with_matches" | "count";
286
288
  */
287
289
  export declare function rgOutputDenyTripwire(stdout: string, mode: GrepOutputMode, judge: Pick<ReadDenyJudge, "matchPath">, opts?: {
288
290
  dropIncompleteTail?: boolean;
291
+ base?: string;
289
292
  }): {
290
293
  trip: false;
291
294
  } | {
@@ -1,6 +1,7 @@
1
1
  import { delimitUntrusted } from "../../core/untrusted-text.js";
2
2
  import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
3
3
  import { hasBinaryExtension, isAbsoluteForFamily, isWinFormPath, joinForFamily, pathFamilyOf } from "./safety.js";
4
+ import { homeViewsOf } from "./read-deny-home.js";
4
5
  import { thrownText } from "../../core/thrown-value.js";
5
6
  const WALK_MAX_FILES = 5000;
6
7
  const WALK_MAX_DEPTH = 32;
@@ -227,8 +228,14 @@ export async function walk(env, root, start, ignore, signal, deny) {
227
228
  let aborted = false;
228
229
  let denyPruned = 0;
229
230
  const denyPatterns = [];
230
- const denyHit = (path) => {
231
- const hit = deny?.matchPath(path);
231
+ const views = deny !== undefined ? homeViewsOf(env, signal) : undefined;
232
+ const denyHit = async (path, leaf) => {
233
+ let hit = deny?.matchPath(path);
234
+ if (!hit && views !== undefined) {
235
+ const view = await views.of(path, leaf !== undefined ? { leaf } : undefined);
236
+ if (view !== undefined)
237
+ hit = deny.matchPath(view);
238
+ }
232
239
  if (!hit)
233
240
  return false;
234
241
  denyPruned++;
@@ -265,7 +272,7 @@ export async function walk(env, root, start, ignore, signal, deny) {
265
272
  if (info.kind === "directory") {
266
273
  if (ignore(rel(info.path), true) || visited.has(info.path))
267
274
  continue;
268
- if (denyHit(info.path))
275
+ if (await denyHit(info.path))
269
276
  continue;
270
277
  if (depth < WALK_MAX_DEPTH)
271
278
  stack.push({ dir: info.path, depth: depth + 1 });
@@ -275,7 +282,7 @@ export async function walk(env, root, start, ignore, signal, deny) {
275
282
  else if (info.kind === "file") {
276
283
  if (ignore(rel(info.path), false))
277
284
  continue;
278
- if (denyHit(info.path))
285
+ if (await denyHit(info.path, "file"))
279
286
  continue;
280
287
  if (typeof info.mtimeMs === "number")
281
288
  mtimes.set(info.path, info.mtimeMs);
@@ -1131,18 +1138,40 @@ function formatRgRecords(records, p, caveat = "") {
1131
1138
  export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
1132
1139
  if (stdout.length === 0)
1133
1140
  return { trip: false };
1141
+ const absolute = rgRecordAbsolute(opts.base);
1134
1142
  for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
1135
1143
  if (record.path === undefined) {
1136
1144
  if (record.text === "--")
1137
1145
  continue;
1138
1146
  return { trip: true, reason: "ambiguous-record" };
1139
1147
  }
1140
- const hit = judge.matchPath(record.path);
1148
+ const hit = judge.matchPath(absolute(record.path));
1141
1149
  if (hit !== null)
1142
1150
  return { trip: true, reason: "deny-hit", pattern: hit.pattern };
1143
1151
  }
1144
1152
  return { trip: false };
1145
1153
  }
1154
+ function rgRecordAbsolute(base) {
1155
+ const family = base !== undefined ? pathFamilyOf({ root: base }) : undefined;
1156
+ return (p) => (base === undefined || family === undefined || isAbsoluteForFamily(family, p) ? p : joinForFamily(family, base, p.replace(/^\.[\\/]/, "")));
1157
+ }
1158
+ async function rgOutputDenyTripwireAt(env, stdout, mode, deny, opts, signal) {
1159
+ const views = homeViewsOf(env, signal);
1160
+ const absolute = rgRecordAbsolute(opts.base);
1161
+ const placed = new Map();
1162
+ for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
1163
+ if (record.path === undefined)
1164
+ continue;
1165
+ const path = absolute(record.path);
1166
+ if (placed.has(path))
1167
+ continue;
1168
+ const view = await views.of(path, { leaf: "file" });
1169
+ if (view !== undefined)
1170
+ placed.set(path, view);
1171
+ }
1172
+ const judge = placed.size === 0 ? deny : { matchPath: (p) => deny.matchPath(p) ?? (placed.has(p) ? deny.matchPath(placed.get(p)) : null) };
1173
+ return rgOutputDenyTripwire(stdout, mode, judge, opts);
1174
+ }
1146
1175
  async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
1147
1176
  const denyOut = {};
1148
1177
  const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
@@ -1212,7 +1241,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1212
1241
  const partial = err.partialStdout ?? "";
1213
1242
  if (partial.trim().length > 0) {
1214
1243
  if (deny !== undefined) {
1215
- const trip = rgOutputDenyTripwire(partial, mode, deny, { dropIncompleteTail: true });
1244
+ const trip = await rgOutputDenyTripwireAt(env, partial, mode, deny, { dropIncompleteTail: true, base: root }, signal);
1216
1245
  if (trip.trip)
1217
1246
  return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1218
1247
  }
@@ -1253,7 +1282,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1253
1282
  if (exitCode >= 2) {
1254
1283
  if (stdout.trim().length > 0) {
1255
1284
  if (deny !== undefined) {
1256
- const trip = rgOutputDenyTripwire(stdout, mode, deny, { dropIncompleteTail: true });
1285
+ const trip = await rgOutputDenyTripwireAt(env, stdout, mode, deny, { dropIncompleteTail: true, base: root }, signal);
1257
1286
  if (trip.trip)
1258
1287
  return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1259
1288
  }
@@ -1268,7 +1297,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
1268
1297
  return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
1269
1298
  }
1270
1299
  if (deny !== undefined) {
1271
- const trip = rgOutputDenyTripwire(stdout, mode, deny);
1300
+ const trip = await rgOutputDenyTripwireAt(env, stdout, mode, deny, { base: root }, signal);
1272
1301
  if (trip.trip)
1273
1302
  return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
1274
1303
  }
@@ -1308,7 +1337,16 @@ async function rgDenyExistenceProbe(env, root, deny, target, signal) {
1308
1337
  }
1309
1338
  }
1310
1339
  export async function runGrepDetailed(env, root, p, signal, deny) {
1311
- if (await detectRipgrep(env))
1340
+ const scope = p.path ? (isAbsoluteForFamily(pathFamilyOf({ root }), p.path) ? p.path : joinForFamily(pathFamilyOf({ root }), root, p.path)) : root;
1341
+ const unexcludableUnder = async () => {
1342
+ if (deny?.unexcludableUnder === undefined)
1343
+ return false;
1344
+ if (deny.unexcludableUnder(scope))
1345
+ return true;
1346
+ const view = await homeViewsOf(env, signal).of(scope);
1347
+ return view !== undefined && deny.unexcludableUnder(view);
1348
+ };
1349
+ if (!(await unexcludableUnder()) && (await detectRipgrep(env)))
1312
1350
  return rgGrepDetailed(env, root, p, signal, deny);
1313
1351
  const denyOut = {};
1314
1352
  const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);