@sema-agent/core 7.37.1 → 7.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/dist/agents/observer.js +1 -1
  3. package/dist/agents/peer-notices.js +4 -4
  4. package/dist/agents/subagent.js +1 -1
  5. package/dist/agents/team-role.js +2 -2
  6. package/dist/brain/anthropic.js +10 -9
  7. package/dist/brain/errors.d.ts +26 -0
  8. package/dist/brain/errors.js +34 -0
  9. package/dist/brain/input-too-long.d.ts +0 -31
  10. package/dist/brain/input-too-long.js +1 -18
  11. package/dist/brain/open-responses.js +10 -9
  12. package/dist/brain/openai.js +10 -9
  13. package/dist/brain/status-sink.d.ts +1 -1
  14. package/dist/brain/stream-engine.js +10 -2
  15. package/dist/brain/stream-shared.d.ts +11 -3
  16. package/dist/brain/stream-shared.js +4 -2
  17. package/dist/brain/tool-call-id.d.ts +29 -8
  18. package/dist/brain/tool-call-id.js +36 -6
  19. package/dist/brain/tool-call-repair.js +1 -1
  20. package/dist/core/a2a-task-state.js +2 -2
  21. package/dist/core/artifact-host.js +6 -6
  22. package/dist/core/ask-origin.js +6 -6
  23. package/dist/core/ask-unresolvable-notice.js +1 -1
  24. package/dist/core/auto-mode.js +4 -4
  25. package/dist/core/background-agent-store.js +2 -2
  26. package/dist/core/checkpoint-execution-record.js +1 -1
  27. package/dist/core/checkpoint-store.d.ts +2 -2
  28. package/dist/core/checkpoint-store.js +2 -2
  29. package/dist/core/child-clamp-seats.js +3 -3
  30. package/dist/core/compliance.js +1 -1
  31. package/dist/core/context-edit.js +1 -1
  32. package/dist/core/context-guard.d.ts +0 -19
  33. package/dist/core/context-guard.js +0 -38
  34. package/dist/core/decision-receipt.d.ts +27 -1
  35. package/dist/core/decision-receipt.js +176 -1
  36. package/dist/core/engine-notice.d.ts +60 -8
  37. package/dist/core/engine-notice.js +2 -1
  38. package/dist/core/gate-fold.js +3 -2
  39. package/dist/core/gate-outcome.js +4 -4
  40. package/dist/core/governance-codes.d.ts +221 -13
  41. package/dist/core/governance-codes.js +104 -4
  42. package/dist/core/hook-seats.js +2 -2
  43. package/dist/core/hooks.d.ts +21 -2
  44. package/dist/core/hooks.js +4 -1
  45. package/dist/core/mailbox-store.js +3 -3
  46. package/dist/core/mcp-failure.js +4 -4
  47. package/dist/core/mcp-injection-drop.js +1 -1
  48. package/dist/core/memory-engine/consolidation.js +1 -1
  49. package/dist/core/memory-engine/delegation-settlement.d.ts +8 -0
  50. package/dist/core/memory-engine/delegation-settlement.js +7 -0
  51. package/dist/core/memory-engine/engine.d.ts +12 -2
  52. package/dist/core/memory-engine/engine.js +10 -6
  53. package/dist/core/memory-engine/export-bundle.js +1 -1
  54. package/dist/core/memory-engine/layout.d.ts +6 -6
  55. package/dist/core/memory-engine/layout.js +6 -8
  56. package/dist/core/memory-engine/session-write-home.d.ts +30 -0
  57. package/dist/core/memory-engine/session-write-home.js +34 -0
  58. package/dist/core/memory-engine/tools.js +1 -1
  59. package/dist/core/memory-engine/types.js +1 -1
  60. package/dist/core/memory.js +1 -1
  61. package/dist/core/mode-policy.d.ts +10 -1
  62. package/dist/core/mode-policy.js +15 -7
  63. package/dist/core/model-seat.js +1 -1
  64. package/dist/core/permission-rule-model.js +3 -3
  65. package/dist/core/permission-rule-store.js +2 -2
  66. package/dist/core/read-only-shell-table.js +8 -8
  67. package/dist/core/retention.js +2 -2
  68. package/dist/core/runner/assemble-result.js +15 -5
  69. package/dist/core/runner/classifier-ladder.js +1 -1
  70. package/dist/core/runner/denial-limit-arms.d.ts +2 -0
  71. package/dist/core/runner/denial-limit-arms.js +6 -2
  72. package/dist/core/runner/inherited-ask-grants.d.ts +6 -5
  73. package/dist/core/runner/inherited-ask-grants.js +3 -2
  74. package/dist/core/runner/prepare-artifact.d.ts +3 -2
  75. package/dist/core/runner/prepare-context-lane.d.ts +19 -15
  76. package/dist/core/runner/prepare-context-lane.js +11 -15
  77. package/dist/core/runner/prepare-gate-stations.js +5 -5
  78. package/dist/core/runner/prepare-hands-readface.d.ts +4 -3
  79. package/dist/core/runner/prepare-hands-readface.js +1 -0
  80. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  81. package/dist/core/runner/prepare-policy-chain.js +10 -10
  82. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -2
  83. package/dist/core/runner/run-harness-handlers.js +3 -0
  84. package/dist/core/runner/run-identity-wiring.d.ts +1 -1
  85. package/dist/core/runner/run-identity-wiring.js +0 -2
  86. package/dist/core/runner/run-leg.js +2 -2
  87. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  88. package/dist/core/runner/run-turn-boundary.js +1 -1
  89. package/dist/core/runner/session-mode-seats.d.ts +11 -3
  90. package/dist/core/runner/session-mode-seats.js +6 -0
  91. package/dist/core/runner/session-rule-policy.js +2 -3
  92. package/dist/core/runner/stream-steer-verb.js +1 -1
  93. package/dist/core/runner/tool-call-id-fold-seat.js +16 -9
  94. package/dist/core/runner/tool-turn-repair-seat.d.ts +11 -1
  95. package/dist/core/runner/tool-turn-repair-seat.js +35 -0
  96. package/dist/core/runner/turn-attachments.js +1 -1
  97. package/dist/core/runtime-caps.js +1 -1
  98. package/dist/core/safety-axis-vocab.d.ts +5 -0
  99. package/dist/core/safety-axis-vocab.js +5 -0
  100. package/dist/core/session-reconcile.d.ts +9 -7
  101. package/dist/core/session-reconcile.js +21 -1
  102. package/dist/core/shared-memory/types.js +1 -1
  103. package/dist/core/side-query.d.ts +6 -1
  104. package/dist/core/store-contracts/read-back-shape-contract.js +1 -1
  105. package/dist/core/swappable-deps.js +1 -1
  106. package/dist/core/task-notification.js +4 -4
  107. package/dist/core/task-registry-shared.js +1 -1
  108. package/dist/core/terminal-cause.d.ts +19 -0
  109. package/dist/core/tool-face.js +6 -6
  110. package/dist/core/tool-policy.d.ts +17 -11
  111. package/dist/core/tool-policy.js +38 -72
  112. package/dist/core/tool-roster.js +1 -1
  113. package/dist/core/tool-search-setting.js +1 -1
  114. package/dist/core/trace.d.ts +5 -4
  115. package/dist/core/types.d.ts +2 -2
  116. package/dist/core/types.js +1 -1
  117. package/dist/core/wiring-manifest.js +2 -2
  118. package/dist/core/write-protect.js +1 -1
  119. package/dist/engine/harness/agent-harness.js +10 -6
  120. package/dist/engine/llm/tool-turns.d.ts +93 -4
  121. package/dist/engine/llm/tool-turns.js +63 -9
  122. package/dist/engine/llm/types.d.ts +11 -0
  123. package/dist/index.d.ts +5 -3
  124. package/dist/index.js +4 -2
  125. package/dist/internal/llm.d.ts +2 -2
  126. package/dist/internal/llm.js +1 -1
  127. package/dist/orchestration/workflow-governance.js +2 -2
  128. package/dist/orchestration/workflow-types.js +2 -2
  129. package/dist/shell/lex.js +7 -7
  130. package/dist/tools/fs/bash-program-position.js +2 -2
  131. package/dist/tools/fs/fs-shared.d.ts +2 -1
  132. package/dist/tools/fs/fs-shared.js +1 -1
  133. package/dist/tools/fs/index.d.ts +13 -6
  134. package/dist/tools/fs/index.js +12 -4
  135. package/dist/tools/fs/read-deny-home.d.ts +98 -45
  136. package/dist/tools/fs/read-deny-home.js +294 -117
  137. package/dist/tools/fs/read-deny.d.ts +2 -2
  138. package/dist/tools/fs/read-deny.js +2 -2
  139. package/dist/tools/fs/safety.d.ts +2 -0
  140. package/dist/tools/fs/safety.js +2 -2
  141. package/dist/tools/fs/search.d.ts +4 -3
  142. package/dist/tools/fs/search.js +3 -3
  143. package/dist/tools/fs/shell-read-check.d.ts +9 -0
  144. package/dist/tools/fs/shell-read-check.js +3 -2
  145. package/dist/tools/fs/shell-read-establish.d.ts +3 -0
  146. package/dist/tools/fs/shell-read-establish.js +1 -1
  147. package/dist/tools/fs/shell-read-identity.d.ts +4 -3
  148. package/dist/tools/fs/shell-read-identity.js +2 -2
  149. package/dist/tools/fs/shell-read-judge.js +2 -2
  150. package/dist/tools/fs/shell-read-plan.js +2 -2
  151. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  152. package/package.json +1 -1
  153. package/test/export-surface.snapshot.json +17 -1
  154. package/wire-changes/core@7.38.0.json +664 -0
  155. package/wire-changes/core@7.38.1.json +56 -0
@@ -1,23 +1,106 @@
1
+ import { userInfo } from "node:os";
1
2
  import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
2
3
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js";
4
+ import { thrownText } from "../../core/thrown-value.js";
3
5
  import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
4
- export function declaredReadDenyHomes(env) {
5
- const home = env.homeDir;
6
- return home !== undefined && isAbsolutePathForm(home) ? [home] : [];
6
+ export const AUXILIARY_HOME_PROBE_MS = 2_000;
7
+ const said = new Set();
8
+ function sayOnce(kind, line) {
9
+ if (said.has(kind))
10
+ return;
11
+ said.add(kind);
12
+ try {
13
+ console.warn(line);
14
+ }
15
+ catch {
16
+ }
7
17
  }
8
- export async function readDenyHomesOf(env, signal) {
9
- return (await readDenyHomesAnswered(env, signal)).homes;
18
+ function isNoAccountRow(e) {
19
+ try {
20
+ const info = typeof e === "object" && e !== null ? e.info : undefined;
21
+ return typeof info === "object" && info !== null && info.code === "ENOENT";
22
+ }
23
+ catch {
24
+ return false;
25
+ }
10
26
  }
11
- async function readDenyHomesAnswered(env, signal) {
12
- const home = declaredReadDenyHomes(env)[0];
27
+ function valueText(v) {
28
+ try {
29
+ return JSON.stringify(v) ?? String(v);
30
+ }
31
+ catch {
32
+ return typeof v;
33
+ }
34
+ }
35
+ function accountHomeOf() {
36
+ try {
37
+ const home = userInfo().homedir;
38
+ if (typeof home === "string" && isAbsolutePathForm(home))
39
+ return { state: "home", home };
40
+ sayOnce("bad_value", `read-deny homes: the account home lookup answered ${valueText(home)}, not an absolute path — home-anchored read-deny rows are placed under the environment's declared home only.`);
41
+ return { state: "none" };
42
+ }
43
+ catch (e) {
44
+ if (isNoAccountRow(e))
45
+ return { state: "none" };
46
+ sayOnce("unanswered", `read-deny homes: the account home could not be read (${thrownText(e)}) — home-anchored read-deny rows hold their run at every location until the lookup answers.`);
47
+ return { state: "unanswered" };
48
+ }
49
+ }
50
+ function declaredHomeOf(env) {
51
+ const home = env.homeDir;
52
+ return home !== undefined && isAbsolutePathForm(home) ? home : undefined;
53
+ }
54
+ function declaredReadDenyHomesAnswered(env) {
55
+ const home = declaredHomeOf(env);
13
56
  if (home === undefined)
14
57
  return { homes: [], whole: true };
15
- const canonical = await env.canonicalPath(home, signal);
16
- if (canonical.ok)
17
- return { homes: canonical.value !== home ? [home, canonical.value] : [home], whole: true };
18
- return { homes: [home], whole: canonical.error.code === "not_found" || canonical.error.code === "not_directory" };
58
+ const account = accountHomeOf();
59
+ if (account.state === "unanswered")
60
+ return { homes: [], whole: false };
61
+ if (account.state === "none" || normalizeAbsPathLexically(account.home) === normalizeAbsPathLexically(home))
62
+ return { homes: [home], whole: true };
63
+ return { homes: [home, account.home], whole: true };
64
+ }
65
+ export function declaredReadDenyHomes(env) {
66
+ return declaredReadDenyHomesAnswered(env).homes;
67
+ }
68
+ function isDeclared(declared, home) {
69
+ return declared !== undefined && normalizeAbsPathLexically(home) === normalizeAbsPathLexically(declared);
19
70
  }
20
- export async function readDenyHomeLinksOf(env, entries, homes, signal) {
71
+ async function homeSpellingsOf(env, home, signal) {
72
+ try {
73
+ const canonical = await env.canonicalPath(home, signal);
74
+ if (canonical.ok)
75
+ return { spellings: canonical.value !== home ? [home, canonical.value] : [home], canonical: canonical.value, answered: true };
76
+ return { spellings: [home], answered: canonical.error.code === "not_found" || canonical.error.code === "not_directory" };
77
+ }
78
+ catch {
79
+ return { spellings: [home], answered: false };
80
+ }
81
+ }
82
+ export async function readDenyHomesOf(env, signal) {
83
+ const listed = declaredReadDenyHomesAnswered(env);
84
+ const [declared, ...auxiliary] = listed.homes;
85
+ if (!listed.whole || declared === undefined)
86
+ return [];
87
+ const [own, ...others] = await Promise.all([
88
+ homeSpellingsOf(env, declared, signal),
89
+ ...auxiliary.map((home) => until(homeSpellingsOf(env, home, signal), Date.now() + AUXILIARY_HOME_PROBE_MS, signal).then((r) => r ?? { spellings: [home], answered: false })),
90
+ ]);
91
+ if (!own.answered)
92
+ return [];
93
+ if (others.some((r) => !r.answered))
94
+ sayAuxiliary("its path probe did not answer");
95
+ return [...new Set([...own.spellings, ...others.flatMap((r) => r.spellings)])];
96
+ }
97
+ async function until(work, deadline, signal) {
98
+ return await withGateTimeout(work, Math.max(0, deadline - Date.now()), signal, "auxiliaryHome").catch(() => undefined);
99
+ }
100
+ function sayAuxiliary(what) {
101
+ sayOnce("auxiliary", `read-deny homes: a home beside the environment's declared home did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — its home-anchored read-deny rows sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`);
102
+ }
103
+ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline) {
21
104
  const rows = [];
22
105
  const familyOf = new Map();
23
106
  for (const { pattern, caseSensitive } of entries) {
@@ -35,7 +118,8 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal) {
35
118
  });
36
119
  rows.push({ pattern, caseSensitive: caseSensitive === true, spelled });
37
120
  }
38
- const resolved = new Map(await Promise.all([...familyOf].map(async ([at, family]) => [at, await whereItLeads(env, at, family, signal)])));
121
+ const leadOf = async (at, family) => (await whereItLeads(env, at, family, signal)) ?? ((await holdsNothingBelow(env, at, signal)) ? at : undefined);
122
+ const resolved = new Map(await Promise.all([...familyOf].map(async ([at, family]) => [at, deadline === undefined ? await leadOf(at, family) : await until(leadOf(at, family), deadline, signal)])));
39
123
  const out = new Map();
40
124
  const whole = [...resolved.values()].every((location) => location !== undefined);
41
125
  for (const { pattern, caseSensitive, spelled } of rows) {
@@ -52,6 +136,10 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal) {
52
136
  }
53
137
  return { linked: out, whole };
54
138
  }
139
+ async function holdsNothingBelow(env, at, signal) {
140
+ const probe = await withGateTimeout(Promise.resolve().then(() => env.canonicalPath(at, signal)), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHead").catch(() => undefined);
141
+ return probe !== undefined && !probe.ok && probe.error.code === "not_directory";
142
+ }
55
143
  export async function whereItLeads(env, at, family = pathFamilyOf({ root: at }), signal) {
56
144
  try {
57
145
  const key = await canonicalizeTarget(env, at, signal, family);
@@ -77,17 +165,189 @@ export async function whereItsAnsweredPrefixLeads(env, at, family = pathFamilyOf
77
165
  };
78
166
  return await withGateTimeout(walk(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "ruleRowHead").catch(() => undefined);
79
167
  }
168
+ const NO_CHAIN = { ancestors: [], whole: false };
169
+ async function chainAbove(env, identity, canonical, signal, deadline) {
170
+ let own;
171
+ const ancestors = [];
172
+ let whole = true;
173
+ for (let dir = normalizeAbsPathLexically(canonical), first = true;; first = false) {
174
+ const id = deadline === undefined ? await identityOf(env, identity, dir, signal) : await until(identityOf(env, identity, dir, signal), deadline, signal);
175
+ if (id === undefined && deadline !== undefined && Date.now() >= deadline) {
176
+ whole = false;
177
+ break;
178
+ }
179
+ if (id?.ok !== true)
180
+ whole = false;
181
+ else if (first)
182
+ own = { dir, id: id.value };
183
+ else
184
+ ancestors.push({ dir, id: id.value });
185
+ const parent = normalizeAbsPathLexically(`${dir}/..`);
186
+ if (parent === dir)
187
+ break;
188
+ dir = parent;
189
+ }
190
+ return { ...(own !== undefined ? { own } : {}), ancestors, whole };
191
+ }
192
+ async function identityOf(env, identity, path, signal) {
193
+ try {
194
+ return await identity.call(env, path, signal);
195
+ }
196
+ catch {
197
+ return undefined;
198
+ }
199
+ }
200
+ async function readAuxiliaryHome(env, home, opts, signal) {
201
+ const deadline = Date.now() + AUXILIARY_HOME_PROBE_MS;
202
+ const spelled = await until(homeSpellingsOf(env, home, signal), deadline, signal);
203
+ let reading;
204
+ if (spelled?.canonical === undefined) {
205
+ const answered = spelled?.answered === true;
206
+ reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked: new Map(), answered };
207
+ }
208
+ else {
209
+ const place = spelled.spellings;
210
+ const identity = env.fileIdentity;
211
+ const [chain, heads] = await Promise.all([
212
+ typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true }),
213
+ opts.entries !== undefined ? readDenyHomeLinksOf(env, opts.entries, place, signal, deadline) : Promise.resolve({ linked: new Map(), whole: true }),
214
+ ]);
215
+ reading = { place, chain, linked: heads.linked, answered: chain.whole && heads.whole };
216
+ }
217
+ if (!reading.answered)
218
+ sayAuxiliary("a probe did not answer");
219
+ return reading;
220
+ }
221
+ function homeViewSourceOver(env, auxiliary) {
222
+ const declared = declaredHomeOf(env);
223
+ const identity = env.fileIdentity;
224
+ let declaredChain;
225
+ const readDeclared = () => {
226
+ if (declared === undefined || typeof identity !== "function")
227
+ return Promise.resolve(NO_CHAIN);
228
+ if (declaredChain === undefined) {
229
+ const reading = withGateTimeout((async () => {
230
+ const spelled = await homeSpellingsOf(env, declared);
231
+ return spelled.canonical === undefined ? NO_CHAIN : await chainAbove(env, identity, spelled.canonical);
232
+ })(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() => NO_CHAIN);
233
+ declaredChain = reading;
234
+ void reading.then((c) => {
235
+ if (!c.whole && declaredChain === reading)
236
+ declaredChain = undefined;
237
+ });
238
+ }
239
+ return declaredChain;
240
+ };
241
+ return {
242
+ views(signal) {
243
+ let index;
244
+ const indexOf = () => (index ??= (async () => {
245
+ const chains = [await readDeclared(), ...(await Promise.all(auxiliary)).map((a) => a.chain)];
246
+ const out = new Map();
247
+ for (const { own } of chains)
248
+ if (own !== undefined && !out.has(own.id))
249
+ out.set(own.id, own.dir);
250
+ for (const { ancestors } of chains)
251
+ for (const { dir, id } of ancestors)
252
+ if (!out.has(id))
253
+ out.set(id, dir);
254
+ return out.size > 0 ? out : undefined;
255
+ })());
256
+ const views = new Map();
257
+ const viewOf = (path, leafIsFile) => {
258
+ const key = `${leafIsFile ? "f" : "d"}:${path}`;
259
+ let v = views.get(key);
260
+ if (v === undefined) {
261
+ v = (async () => {
262
+ const index = await indexOf();
263
+ if (index === undefined || typeof identity !== "function")
264
+ return undefined;
265
+ if (!leafIsFile) {
266
+ const id = await identityOf(env, identity, path, signal);
267
+ if (id === undefined)
268
+ return undefined;
269
+ if (id.ok) {
270
+ const at = index.get(id.value);
271
+ if (at !== undefined)
272
+ return at;
273
+ }
274
+ else if (id.error.code !== "not_found" && id.error.code !== "not_directory")
275
+ return undefined;
276
+ }
277
+ const parent = normalizeAbsPathLexically(`${path}/..`);
278
+ if (parent === path)
279
+ return undefined;
280
+ const up = await viewOf(parent, false);
281
+ return up === undefined ? undefined : joinForFamily(pathFamilyOf({ root: path }), up, path.slice(parent.length).replace(/^[\\/]+/, ""));
282
+ })();
283
+ views.set(key, v);
284
+ }
285
+ return v;
286
+ };
287
+ return {
288
+ async of(path, opts) {
289
+ if (typeof identity !== "function" || (declared === undefined && auxiliary.length === 0) || !isAbsolutePathForm(path))
290
+ return undefined;
291
+ const spelled = normalizeAbsPathLexically(path);
292
+ const view = await viewOf(spelled, opts?.leaf === "file");
293
+ return view === undefined || view === spelled ? undefined : view;
294
+ },
295
+ };
296
+ },
297
+ };
298
+ }
299
+ const PLACEMENT_VIEWS = new WeakMap();
300
+ function placementOf(matcher, homes, linked, whole) {
301
+ const list = Object.freeze([...homes]);
302
+ PLACEMENT_VIEWS.set(list, matcher.homeViews);
303
+ return { matcher, homes: list, linked, whole };
304
+ }
305
+ export function placementViewsOf(homes) {
306
+ return PLACEMENT_VIEWS.get(homes);
307
+ }
308
+ export function sayPlacementLost() {
309
+ try {
310
+ console.warn("read-deny homes: this tool band was handed a home list that is not the placement's own (an engine defect) — its home-anchored read-deny rows hold their run at every location, and only the declared home's views are read.");
311
+ }
312
+ catch {
313
+ }
314
+ }
315
+ export function homeViewsAt(env, homes) {
316
+ const declared = declaredHomeOf(env);
317
+ const auxiliary = [...new Set(homes.filter((h) => isAbsolutePathForm(h) && !isDeclared(declared, h)))];
318
+ return homeViewSourceOver(env, auxiliary.map((home) => readAuxiliaryHome(env, home, {})));
319
+ }
320
+ export function homeViewsOf(env, judge, signal) {
321
+ return (judge?.homeViews ?? homeViewsAt(env, [])).views(signal);
322
+ }
323
+ export function carryHomeViews(judge, source) {
324
+ return { ...judge, homeViews: source };
325
+ }
80
326
  export async function placeReadDenyAtHome(env, compile, homes, signal) {
81
- let at = homes;
82
- if (at === undefined) {
83
- const answer = await withGateTimeout(readDenyHomesAnswered(env, signal), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHomes");
84
- if (!answer.whole)
85
- return { matcher: compile([]), homes: [], linked: new Map(), whole: false };
86
- at = answer.homes;
327
+ const entries = compile([]).entries;
328
+ const declared = declaredHomeOf(env);
329
+ const heldAnywhere = (whole) => placementOf(carryHomeViews(compile([]), homeViewSourceOver(env, [])), [], new Map(), whole);
330
+ const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true };
331
+ if (!listed.whole)
332
+ return heldAnywhere(false);
333
+ const own = listed.homes.filter((h) => isDeclared(declared, h));
334
+ let declaredAt = [];
335
+ if (own.length > 0) {
336
+ const ownSpellings = await withGateTimeout(homeSpellingsOf(env, own[0], signal), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHomes");
337
+ if (!ownSpellings.answered)
338
+ return heldAnywhere(false);
339
+ declaredAt = [...new Set([...own, ...ownSpellings.spellings])];
340
+ }
341
+ const auxiliary = listed.homes.filter((h) => !isDeclared(declared, h)).map((home) => readAuxiliaryHome(env, home, { entries }, signal));
342
+ const [heads, readings] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary)]);
343
+ const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])];
344
+ const linked = new Map();
345
+ for (const map of [heads.linked, ...readings.map((r) => r.linked)]) {
346
+ for (const [pattern, locations] of map)
347
+ linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]);
87
348
  }
88
- const atHomes = compile(at);
89
- const { linked, whole } = await readDenyHomeLinksOf(env, atHomes.entries, at, signal);
90
- return { matcher: linked.size === 0 ? atHomes : compile(at, linked), homes: at, linked, whole };
349
+ const matcher = linked.size === 0 ? compile(at) : compile(at, linked);
350
+ return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, auxiliary)), at, linked, heads.whole);
91
351
  }
92
352
  export function readDenyPlacedLater(unplaced, place) {
93
353
  let current = unplaced;
@@ -108,6 +368,17 @@ export function readDenyPlacedLater(unplaced, place) {
108
368
  if (!whole)
109
369
  await again();
110
370
  };
371
+ const settled = async () => {
372
+ if (pending !== undefined)
373
+ await pending;
374
+ return current;
375
+ };
376
+ const homeViews = {
377
+ views(signal) {
378
+ let views;
379
+ return { of: async (path, opts) => await (await (views ??= settled().then((m) => m.homeViews.views(signal)))).of(path, opts) };
380
+ },
381
+ };
111
382
  const matcher = {
112
383
  get entries() {
113
384
  return current.entries;
@@ -125,101 +396,7 @@ export function readDenyPlacedLater(unplaced, place) {
125
396
  },
126
397
  unexcludableUnder: (scope) => current.unexcludableUnder?.(scope) ?? false,
127
398
  rowAtOrBelow: (path) => current.rowAtOrBelow?.(path) ?? null,
399
+ homeViews,
128
400
  };
129
401
  return { matcher, ready };
130
402
  }
131
- const HOME_CHAINS = new WeakMap();
132
- function homeChainOf(env) {
133
- const identity = env.fileIdentity;
134
- const home = declaredReadDenyHomes(env)[0];
135
- if (typeof identity !== "function" || home === undefined)
136
- return undefined;
137
- let perEnv = HOME_CHAINS.get(env);
138
- if (perEnv === undefined)
139
- HOME_CHAINS.set(env, (perEnv = new Map()));
140
- const known = perEnv;
141
- let chain = known.get(home);
142
- if (chain === undefined) {
143
- const reading = withGateTimeout((async () => {
144
- const canonical = await env.canonicalPath(home);
145
- if (!canonical.ok)
146
- return { index: undefined, whole: false };
147
- const out = new Map();
148
- let whole = true;
149
- for (let dir = normalizeAbsPathLexically(canonical.value);;) {
150
- const id = await identityOf(env, identity, dir);
151
- if (id?.ok === true) {
152
- if (!out.has(id.value))
153
- out.set(id.value, dir);
154
- }
155
- else
156
- whole = false;
157
- const parent = normalizeAbsPathLexically(`${dir}/..`);
158
- if (parent === dir)
159
- break;
160
- dir = parent;
161
- }
162
- return { index: out.size > 0 ? out : undefined, whole };
163
- })(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() => ({ index: undefined, whole: false }));
164
- chain = reading;
165
- known.set(home, reading);
166
- const drop = () => {
167
- if (known.get(home) === reading)
168
- known.delete(home);
169
- };
170
- void reading.then((c) => (c.whole ? undefined : drop()));
171
- }
172
- return chain.then((c) => c.index);
173
- }
174
- async function identityOf(env, identity, path, signal) {
175
- try {
176
- return await identity.call(env, path, signal);
177
- }
178
- catch {
179
- return undefined;
180
- }
181
- }
182
- export function homeViewsOf(env, signal) {
183
- const chain = homeChainOf(env);
184
- const identity = env.fileIdentity;
185
- const views = new Map();
186
- const viewOf = (path, leafIsFile) => {
187
- const key = `${leafIsFile ? "f" : "d"}:${path}`;
188
- let v = views.get(key);
189
- if (v === undefined) {
190
- v = (async () => {
191
- const index = await chain;
192
- if (index === undefined || typeof identity !== "function")
193
- return undefined;
194
- if (!leafIsFile) {
195
- const id = await identityOf(env, identity, path, signal);
196
- if (id === undefined)
197
- return undefined;
198
- if (id.ok) {
199
- const at = index.get(id.value);
200
- if (at !== undefined)
201
- return at;
202
- }
203
- else if (id.error.code !== "not_found" && id.error.code !== "not_directory")
204
- return undefined;
205
- }
206
- const parent = normalizeAbsPathLexically(`${path}/..`);
207
- if (parent === path)
208
- return undefined;
209
- const up = await viewOf(parent, false);
210
- return up === undefined ? undefined : joinForFamily(pathFamilyOf({ root: path }), up, path.slice(parent.length).replace(/^[\\/]+/, ""));
211
- })();
212
- views.set(key, v);
213
- }
214
- return v;
215
- };
216
- return {
217
- async of(path, opts) {
218
- if (chain === undefined || !isAbsolutePathForm(path))
219
- return undefined;
220
- const spelled = normalizeAbsPathLexically(path);
221
- const view = await viewOf(spelled, opts?.leaf === "file");
222
- return view === undefined || view === spelled ? undefined : view;
223
- },
224
- };
225
- }
@@ -392,8 +392,8 @@ export declare function checkedReadDenyHomes(homes: unknown, seat?: string): rea
392
392
  * layer or knob.
393
393
  *
394
394
  * `homes` — the spellings of the execution environment's home directory a home-anchored entry
395
- * ({@link READ_DENY_HOME_WORD}) is placed under: the declared home and, where the environment's canonical
396
- * spelling differs, that one too (the faces judge canonical keys beside the spelled views). Absolute
395
+ * ({@link READ_DENY_HOME_WORD}) is placed under: the declared home, the account home and, where the environment's
396
+ * canonical spelling of one differs, that one too (the faces judge canonical keys beside the spelled views). Absolute
397
397
  * spellings only — any other shape refuses loudly, naming `readDenyHomes`. EMPTY = the environment declares no
398
398
  * home: a home-anchored entry then matches its run anywhere — a row that cannot be placed holds wider, never narrower.
399
399
  *
@@ -11,7 +11,7 @@ export function readDenyRunHead(run) {
11
11
  const wild = segments.findIndex((seg) => seg.includes("*"));
12
12
  return wild === -1 ? segments.length : wild;
13
13
  }
14
- export const DENY_FACES = ["read", "write"];
14
+ export const DENY_FACES = Object.freeze(["read", "write"]);
15
15
  export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
16
16
  const leads = new Map();
17
17
  const partsOf = (head) => {
@@ -144,7 +144,7 @@ export function elideMiddle(s, budget) {
144
144
  const head = Math.ceil(keep / 2);
145
145
  return `${sliceHeadSafe(s, head)}\u2026${sliceTailSafe(s, keep - head)}`;
146
146
  }
147
- export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet", "agent-config"];
147
+ export const READ_DENY_BUILTIN_TIERS = Object.freeze(["credentials", "shell-history", "browser", "wallet", "agent-config"]);
148
148
  export const READ_FACE_BUILTIN_DENY_TABLE = [
149
149
  { pattern: ".ssh", tier: "credentials" },
150
150
  { pattern: "id_rsa*", tier: "credentials" },
@@ -1,3 +1,4 @@
1
+ import { type HomeViewSource } from "./read-deny-home.js";
1
2
  import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
2
3
  import type { ExecutionEnv } from "../../internal/harness-types.js";
3
4
  import { protectivePathTargetOf, type ToolCallLike } from "../../core/tool-registry.js";
@@ -449,6 +450,7 @@ export type RootsSeat = readonly string[] | (() => readonly string[] | undefined
449
450
  */
450
451
  export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRoots?: RootsSeat, exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
451
452
  readonly face?: "write";
453
+ readonly homeViews?: HomeViewSource;
452
454
  matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
453
455
  readonly aliasResolved?: boolean;
454
456
  }): {
@@ -312,7 +312,7 @@ export function isUncSpelling(p) {
312
312
  export function isAbsoluteForFamily(family, p) {
313
313
  return family === "win" ? /^[A-Za-z]:[\\/]/.test(p) || isUncSpelling(p) : p.startsWith("/");
314
314
  }
315
- export const WIN32_NAMESPACE_REFUSALS = ["device_namespace", "unmappable_namespace", "unc_on_posix_tree", "drive_relative"];
315
+ export const WIN32_NAMESPACE_REFUSALS = Object.freeze(["device_namespace", "unmappable_namespace", "unc_on_posix_tree", "drive_relative"]);
316
316
  export const WIN32_NAMESPACE_REFUSAL_TEXT = {
317
317
  device_namespace: "is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file)",
318
318
  unmappable_namespace: "is a Win32 extended-length namespace path with no DOS-path equivalent (only \\\\?\\<drive>: and \\\\?\\UNC\\ forms name files)",
@@ -469,7 +469,7 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
469
469
  const judged = { aliasResolved: canon.aliasResolved === true };
470
470
  let hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), judged);
471
471
  if (hit === null) {
472
- const homeView = await homeViewsOf(env, signal).of(key);
472
+ const homeView = await homeViewsOf(env, readDeny, signal).of(key);
473
473
  if (homeView !== undefined)
474
474
  hit = readDeny.matchTarget(key, homeView, judged);
475
475
  }
@@ -1,4 +1,5 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness-types.js";
2
+ import { type HomeViewsCarrier } from "./read-deny-home.js";
2
3
  /** The JS scanner's own output ceiling, in bytes of emitted content rows. Pinned to the
3
4
  * ripgrep leg's figure ({@link MAX_EXEC_OUTPUT_BYTES}, the exec pipe's rolling-tail bound) so the two
4
5
  * legs cannot disagree about how much one search may return; see the collection site in {@link jsGrep}
@@ -130,7 +131,7 @@ export declare function walk(env: ExecutionEnv, root: string, start: string, ign
130
131
  matchPath(path: string): {
131
132
  pattern: string;
132
133
  } | null;
133
- }): Promise<WalkResult>;
134
+ } & HomeViewsCarrier): Promise<WalkResult>;
134
135
  /** The JS-leg withholding disclosure: an empty result must stay distinguishable from
135
136
  * searched-and-empty, so this note rides EVERY output shape when something was pruned. Distinct from
136
137
  * {@link walkCaveat}: pruning is deliberate withholding, not incompleteness.
@@ -157,8 +158,8 @@ export interface ReadDenyWithheld {
157
158
  count?: number;
158
159
  patterns?: readonly string[];
159
160
  }
160
- /** The deny judge seam every search leg shares (structural subset of ReadDenyMatcher). */
161
- export interface ReadDenyJudge {
161
+ /** The deny judge seam every search leg shares (structural subset of ReadDenyMatcher), with its placement's home views. */
162
+ export interface ReadDenyJudge extends HomeViewsCarrier {
162
163
  matchPath(path: string): {
163
164
  pattern: string;
164
165
  } | null;
@@ -228,7 +228,7 @@ export async function walk(env, root, start, ignore, signal, deny) {
228
228
  let aborted = false;
229
229
  let denyPruned = 0;
230
230
  const denyPatterns = [];
231
- const views = deny !== undefined ? homeViewsOf(env, signal) : undefined;
231
+ const views = deny !== undefined ? homeViewsOf(env, deny, signal) : undefined;
232
232
  const denyHit = async (path, leaf) => {
233
233
  let hit = deny?.matchPath(path);
234
234
  if (!hit && views !== undefined) {
@@ -1156,7 +1156,7 @@ function rgRecordAbsolute(base) {
1156
1156
  return (p) => (base === undefined || family === undefined || isAbsoluteForFamily(family, p) ? p : joinForFamily(family, base, p.replace(/^\.[\\/]/, "")));
1157
1157
  }
1158
1158
  async function rgOutputDenyTripwireAt(env, stdout, mode, deny, opts, signal) {
1159
- const views = homeViewsOf(env, signal);
1159
+ const views = homeViewsOf(env, deny, signal);
1160
1160
  const absolute = rgRecordAbsolute(opts.base);
1161
1161
  const placed = new Map();
1162
1162
  for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
@@ -1343,7 +1343,7 @@ export async function runGrepDetailed(env, root, p, signal, deny) {
1343
1343
  return false;
1344
1344
  if (deny.unexcludableUnder(scope))
1345
1345
  return true;
1346
- const view = await homeViewsOf(env, signal).of(scope);
1346
+ const view = await homeViewsOf(env, deny, signal).of(scope);
1347
1347
  return view !== undefined && deny.unexcludableUnder(view);
1348
1348
  };
1349
1349
  if (!(await unexcludableUnder()) && (await detectRipgrep(env)))
@@ -20,6 +20,7 @@
20
20
  import type { ExecutionEnv } from "../../internal/harness-types.js";
21
21
  import { violationDetails } from "./safety.js";
22
22
  import { type ShellReadPlan } from "./shell-read-plan.js";
23
+ import { type HomeViewSource } from "./read-deny-home.js";
23
24
  import { type EstablishedRead, type ShellReadBoundary, type ShellReadDisposition, type ShellReadFinding, type ShellReadSeat } from "./shell-read-judge.js";
24
25
  import type { ReadRootGrantCandidate } from "../../core/tool-spec.js";
25
26
  import type { ToolCallLike } from "../../core/tool-registry.js";
@@ -145,6 +146,9 @@ export interface ShellReadStationOptions {
145
146
  /** `true` ⇔ every root `boundaryOf` returns is already a canonical identity (the Runner's): the station never
146
147
  * re-resolves one, so a root swapped for a link after it was declared or granted cannot bring in the link's target. */
147
148
  readonly rootsCanonical?: boolean;
149
+ /** The home views of the placement the boundary's deny seats were compiled at (read-deny-home.ts) — the Runner's. Absent,
150
+ * the declared home alone. */
151
+ readonly homeViews?: HomeViewSource;
148
152
  }
149
153
  /** What a consumer hands {@link judgeShellRead}: the command as written, the environment whose filesystem
150
154
  * answers where its words land (symlinks resolved, patterns expanded — a `NodeExecutionEnv` over a real
@@ -191,6 +195,11 @@ export interface ShellReadJudgment {
191
195
  * LAXER where only a filesystem sees the escape (an in-root symlink pointing out vouches) and answers a
192
196
  * pattern on its spelling alone, so the two diverge in both directions.
193
197
  *
198
+ * A read target is also judged at its HOME VIEW (a path spelled through a firmlink or a bind mount of a home, respelled
199
+ * at the home): this judgment is its own placement — the views read the environment's declared home and the account
200
+ * home the account database answers once, for this call (`readDenyHomesOf`'s homes); the account home's probes wait at
201
+ * most a short bound of their own.
202
+ *
194
203
  * It carries NO deadline. Bounding an environment-facing wait and substituting a fail-closed verdict is the
195
204
  * gate FOLD's policy on the gate's one deadline seat; the station has never held one on either entry, and a
196
205
  * second default here would decide the same number twice and turn a slow-but-correct filesystem's true
@@ -6,6 +6,7 @@ import { NOT_AUTO_ALLOWED, programPositionRefusalSentence, readProgramPosition }
6
6
  import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
7
7
  import { planShellReads } from "./shell-read-plan.js";
8
8
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
9
+ import { declaredReadDenyHomes, homeViewsAt } from "./read-deny-home.js";
9
10
  import { programArgs, spellsOption } from "./shell-read-write-targets.js";
10
11
  import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
11
12
  import { seatOf } from "../../core/tool-name-readings.js";
@@ -326,7 +327,7 @@ async function runShellReadStation(tool, command, declared, seat, readers, estab
326
327
  }
327
328
  export async function judgeShellRead(input) {
328
329
  const readers = input.readers ?? new Set(bashClassifyDefaultAllow());
329
- const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.env, input.signal);
330
+ const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots, { homeViews: homeViewsAt(input.env, declaredReadDenyHomes(input.env)) }), input.env, input.signal);
330
331
  const offer = normalizeReadRootCandidate(run.verdict.readRootCandidate);
331
332
  return {
332
333
  plan: run.plan,
@@ -349,7 +350,7 @@ export function createShellReadStation(opts) {
349
350
  const key = roots.join("\0");
350
351
  let e = establishers.get(key);
351
352
  if (e === undefined) {
352
- e = createShellReadEstablisher(opts.env, roots, { rootsCanonical: opts.rootsCanonical === true });
353
+ e = createShellReadEstablisher(opts.env, roots, { rootsCanonical: opts.rootsCanonical === true, ...(opts.homeViews !== undefined ? { homeViews: opts.homeViews } : {}) });
353
354
  establishers.set(key, e);
354
355
  }
355
356
  return e;
@@ -40,6 +40,7 @@
40
40
  import type { ExecutionEnv } from "../../internal/harness-types.js";
41
41
  import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
42
42
  import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
43
+ import type { HomeViewSource } from "./read-deny-home.js";
43
44
  /**
44
45
  * How many directories one glob operand (or one traversal root) may enumerate, and how many names it may
45
46
  * match, before the leg gives up and answers `unestablishable`.
@@ -82,7 +83,9 @@ export declare function establishShellReadsLexically(plan: ShellReadPlan, bounda
82
83
  * The filesystem-resolving establisher for one environment and one root set. `readRoots` are the roots
83
84
  * as declared (canonical by contract); they are resolved once more per instance because the comparison is
84
85
  * against `realpath` output on both sides — an aliased root would otherwise refuse every read under itself.
86
+ * `homeViews`: the home views of the placement the deny seats were compiled at (read-deny-home.ts); absent, the declared home's.
85
87
  */
86
88
  export declare function createShellReadEstablisher(env: ExecutionEnv, readRoots: readonly string[], opts?: {
87
89
  readonly rootsCanonical?: boolean;
90
+ readonly homeViews?: HomeViewSource;
88
91
  }): ShellReadEstablisher;
@@ -534,7 +534,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
534
534
  if (lexicalOnly)
535
535
  return establishShellReadsLexically(plan, boundary);
536
536
  const roots = await canonicalRoots(signal);
537
- return await withHomeViews(env, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
537
+ return await withHomeViews(env, opts, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
538
538
  },
539
539
  reestablish: async (previous, boundary, signal) => {
540
540
  if (lexicalOnly)