@sema-agent/core 7.49.5 → 7.50.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 (200) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agents/observer.js +3 -10
  3. package/dist/agents/subagent-steps.d.ts +6 -14
  4. package/dist/agents/subagent-steps.js +3 -17
  5. package/dist/brain/stream-shared.d.ts +1 -1
  6. package/dist/core/allow-entry.d.ts +1 -1
  7. package/dist/core/allow-entry.js +1 -1
  8. package/dist/core/ask-origin.d.ts +15 -11
  9. package/dist/core/ask-origin.js +6 -14
  10. package/dist/core/auto-mode-prompt.js +6 -2
  11. package/dist/core/checkpoint-store.d.ts +6 -4
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/context-edit.d.ts +2 -2
  14. package/dist/core/decision-receipt.d.ts +2 -2
  15. package/dist/core/decision-receipt.js +30 -11
  16. package/dist/core/delegation-frames.d.ts +66 -18
  17. package/dist/core/delegation-frames.js +22 -0
  18. package/dist/core/engine-notice.d.ts +7 -5
  19. package/dist/core/frame-settlement.d.ts +6 -1
  20. package/dist/core/gate-fold.js +34 -19
  21. package/dist/core/gate-lanes.js +15 -13
  22. package/dist/core/gate-outcome.d.ts +2 -2
  23. package/dist/core/governance-codes.js +2 -0
  24. package/dist/core/hooks.d.ts +28 -20
  25. package/dist/core/human-input-projection.js +2 -0
  26. package/dist/core/lsp-diagnostics.d.ts +3 -0
  27. package/dist/core/lsp-diagnostics.js +6 -0
  28. package/dist/core/lsp-session.d.ts +39 -17
  29. package/dist/core/lsp-session.js +118 -58
  30. package/dist/core/lsp.d.ts +83 -3
  31. package/dist/core/lsp.js +146 -16
  32. package/dist/core/mcp.js +2 -1
  33. package/dist/core/media-byte-cap.d.ts +14 -0
  34. package/dist/core/media-byte-cap.js +15 -3
  35. package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
  36. package/dist/core/memory-engine/consolidation-driver.js +54 -6
  37. package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
  38. package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
  39. package/dist/core/memory-engine/consolidation.d.ts +17 -0
  40. package/dist/core/memory-engine/consolidation.js +1 -0
  41. package/dist/core/memory-engine/distiller.d.ts +8 -2
  42. package/dist/core/memory-engine/distiller.js +8 -6
  43. package/dist/core/memory-engine/engine.d.ts +37 -4
  44. package/dist/core/memory-engine/engine.js +127 -38
  45. package/dist/core/memory-engine/file-backend.d.ts +8 -3
  46. package/dist/core/memory-engine/file-backend.js +44 -16
  47. package/dist/core/memory-engine/index.d.ts +2 -2
  48. package/dist/core/memory-engine/index.js +1 -1
  49. package/dist/core/memory-engine/landing-admission.d.ts +65 -0
  50. package/dist/core/memory-engine/landing-admission.js +68 -0
  51. package/dist/core/memory-engine/types.d.ts +23 -0
  52. package/dist/core/mode-policy.d.ts +3 -2
  53. package/dist/core/mode-policy.js +2 -0
  54. package/dist/core/permission-rule-model.d.ts +19 -13
  55. package/dist/core/permission-rule-model.js +7 -4
  56. package/dist/core/permission-rule-provider.d.ts +11 -0
  57. package/dist/core/permission-rules.d.ts +1 -1
  58. package/dist/core/permission-rules.js +5 -2
  59. package/dist/core/persisted-rule-arms.d.ts +15 -7
  60. package/dist/core/persisted-rule-arms.js +13 -6
  61. package/dist/core/reminder-mint.d.ts +5 -0
  62. package/dist/core/reminder-mint.js +1 -0
  63. package/dist/core/retired-keys.d.ts +8 -0
  64. package/dist/core/retired-keys.js +4 -0
  65. package/dist/core/rule-lane-reads.d.ts +4 -4
  66. package/dist/core/rule-lane-reads.js +10 -1
  67. package/dist/core/runner/construction-doors.d.ts +10 -4
  68. package/dist/core/runner/construction-doors.js +25 -2
  69. package/dist/core/runner/contracts.d.ts +20 -3
  70. package/dist/core/runner/env-capability-census.js +1 -1
  71. package/dist/core/runner/gate-exit.js +2 -1
  72. package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
  73. package/dist/core/runner/memory-consolidation-driver.js +5 -2
  74. package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
  75. package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
  76. package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
  77. package/dist/core/runner/permission-rule-lanes.js +13 -43
  78. package/dist/core/runner/person-path-rows.d.ts +244 -0
  79. package/dist/core/runner/person-path-rows.js +387 -0
  80. package/dist/core/runner/policy-throw.d.ts +34 -0
  81. package/dist/core/runner/policy-throw.js +25 -0
  82. package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
  83. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  84. package/dist/core/runner/prepare-artifact.js +2 -8
  85. package/dist/core/runner/prepare-ask-lane.d.ts +2 -1
  86. package/dist/core/runner/prepare-ask-lane.js +14 -16
  87. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  88. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  89. package/dist/core/runner/prepare-context-lane.js +2 -2
  90. package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
  91. package/dist/core/runner/prepare-gate-stations.js +1 -1
  92. package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
  93. package/dist/core/runner/prepare-hands-readface.js +57 -43
  94. package/dist/core/runner/prepare-lsp.d.ts +15 -13
  95. package/dist/core/runner/prepare-lsp.js +11 -11
  96. package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
  97. package/dist/core/runner/prepare-policy-chain.js +11 -10
  98. package/dist/core/runner/prepare-run-refs.js +1 -1
  99. package/dist/core/runner/prepare-task.js +24 -16
  100. package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
  101. package/dist/core/runner/prepare-turn-wiring.js +19 -8
  102. package/dist/core/runner/read-root-grant.d.ts +1 -25
  103. package/dist/core/runner/read-root-grant.js +1 -17
  104. package/dist/core/runner/retired-runner-deps.d.ts +2 -2
  105. package/dist/core/runner/run-brain-sinks.d.ts +1 -5
  106. package/dist/core/runner/run-brain-sinks.js +2 -4
  107. package/dist/core/runner/run-harness-handlers.js +5 -4
  108. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  109. package/dist/core/runner/run-identity-wiring.js +10 -1
  110. package/dist/core/runner/run-leg.d.ts +1 -1
  111. package/dist/core/runner/run-leg.js +22 -9
  112. package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
  113. package/dist/core/runner/run-settle-and-teardown.js +7 -4
  114. package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
  115. package/dist/core/runner/run-terminal-adoption.js +1 -1
  116. package/dist/core/runner/run-turn-boundary.js +2 -2
  117. package/dist/core/runner/runtask.js +8 -8
  118. package/dist/core/runner/session-file-state-replay.d.ts +14 -4
  119. package/dist/core/runner/session-file-state-replay.js +8 -0
  120. package/dist/core/runner/stream-steer-verb.js +3 -0
  121. package/dist/core/runner/tool-end-body.d.ts +5 -4
  122. package/dist/core/runner/tool-end-body.js +4 -4
  123. package/dist/core/runner/tool-output-projection.d.ts +6 -4
  124. package/dist/core/runner/tool-output-projection.js +66 -47
  125. package/dist/core/runner-deps.d.ts +6 -3
  126. package/dist/core/shared-memory/types.d.ts +2 -2
  127. package/dist/core/shell-read-silence.d.ts +23 -0
  128. package/dist/core/shell-read-silence.js +18 -0
  129. package/dist/core/synthetic-read-verdict.d.ts +80 -21
  130. package/dist/core/synthetic-read-verdict.js +89 -18
  131. package/dist/core/task-event.d.ts +113 -25
  132. package/dist/core/task-spec.d.ts +44 -0
  133. package/dist/core/task-tool-shape.d.ts +1 -1
  134. package/dist/core/tool-catalog-entries.js +16 -2
  135. package/dist/core/tool-policy.d.ts +25 -15
  136. package/dist/core/tool-policy.js +9 -13
  137. package/dist/core/trace.d.ts +3 -2
  138. package/dist/core/types.d.ts +3 -3
  139. package/dist/core/types.js +1 -1
  140. package/dist/core/untrusted-text.d.ts +2 -2
  141. package/dist/core/user-attachment.d.ts +164 -0
  142. package/dist/core/user-attachment.js +437 -0
  143. package/dist/engine/compaction/compaction.js +10 -3
  144. package/dist/engine/compaction/utils.js +6 -0
  145. package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
  146. package/dist/engine/execution-env/node-execution-env.js +125 -0
  147. package/dist/engine/harness/agent-harness.d.ts +12 -2
  148. package/dist/engine/harness/agent-harness.js +15 -3
  149. package/dist/engine/harness/types.d.ts +21 -0
  150. package/dist/engine/llm/content-block-render.d.ts +15 -0
  151. package/dist/engine/llm/content-block-render.js +10 -0
  152. package/dist/engine/llm/index.d.ts +1 -0
  153. package/dist/engine/llm/index.js +1 -0
  154. package/dist/engine/llm/types.d.ts +80 -4
  155. package/dist/engine/llm/types.js +43 -1
  156. package/dist/engine/session/import-validate.js +2 -0
  157. package/dist/index.d.ts +6 -4
  158. package/dist/index.js +2 -1
  159. package/dist/internal/harness-types.d.ts +1 -1
  160. package/dist/internal/llm.d.ts +2 -2
  161. package/dist/internal/llm.js +1 -1
  162. package/dist/orchestration/workflow.d.ts +4 -5
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompts/default.d.ts +1 -1
  165. package/dist/tools/artifact/artifact-tool.js +1 -1
  166. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  167. package/dist/tools/fs/dir-entries.d.ts +4 -10
  168. package/dist/tools/fs/dir-entries.js +4 -11
  169. package/dist/tools/fs/file-read-check.js +3 -3
  170. package/dist/tools/fs/fs-pdf.js +2 -4
  171. package/dist/tools/fs/fs-read.d.ts +47 -4
  172. package/dist/tools/fs/fs-read.js +63 -19
  173. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  174. package/dist/tools/fs/fs-search-tools.js +7 -2
  175. package/dist/tools/fs/fs-write.d.ts +1 -1
  176. package/dist/tools/fs/fs-write.js +7 -6
  177. package/dist/tools/fs/index.d.ts +7 -1
  178. package/dist/tools/fs/index.js +56 -34
  179. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  180. package/dist/tools/fs/read-deny-home.js +32 -22
  181. package/dist/tools/fs/read-deny.d.ts +14 -0
  182. package/dist/tools/fs/read-deny.js +8 -0
  183. package/dist/tools/fs/repo-map.d.ts +3 -2
  184. package/dist/tools/fs/repo-map.js +11 -3
  185. package/dist/tools/fs/safety.d.ts +41 -2
  186. package/dist/tools/fs/safety.js +12 -3
  187. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  188. package/dist/tools/fs/shell-read-check.js +44 -24
  189. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  190. package/dist/tools/fs/shell-read-face.d.ts +15 -0
  191. package/dist/tools/fs/shell-read-face.js +5 -0
  192. package/dist/tools/fs/shell-read-judge.d.ts +9 -4
  193. package/dist/tools/fs/shell-read-judge.js +1 -1
  194. package/dist/tools/loop-tick.d.ts +2 -2
  195. package/package.json +3 -1
  196. package/test/export-surface.snapshot.json +39 -1
  197. package/wire-changes/core@7.50.0.json +1699 -0
  198. package/wire-changes/core@7.50.1.json +65 -0
  199. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  200. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A tool policy that THREW — the one form its failure takes on each of the two surfaces it reaches, for every seat that
3
+ * consults a policy chain (the gate's target reading, the adjudicate seat, the inherited-constraint station, the two
4
+ * re-checks of an approved edit).
5
+ *
6
+ * The MODEL reads one line of the thrown value inside the seat's deny: secret-shaped runs redacted, then folded to one line
7
+ * and capped (`inlineUntrusted`, which also neutralizes a reminder fence), so a policy's error text never carries a
8
+ * credential, a multi-kilobyte body or a forged frame into the conversation. The DEPLOYMENT hears the failure once, on the
9
+ * seat's hook-failure lane (`onError`, phase `hook`): an error whose message names the seat and the call and carries the
10
+ * class and the redacted text — and no `cause`, because a host's serializer walks a cause chain and the raw value would
11
+ * carry the unredacted text there.
12
+ */
13
+ import type { PermissionResult } from "../tool-policy.js";
14
+ /** The model's line of a thrown policy value: redacted BEFORE the cap (a cut must never leave half a token unredacted),
15
+ * one line, at most {@link PROBE_REASON_MAX} code points; never empty. */
16
+ export declare function policyThrowLine(err: unknown): string;
17
+ /** The deny of a judgment whose wait the task's signal ended — a timeout or a cancellation, nothing the policy said. */
18
+ export declare function policyCheckAbortedDeny(): PermissionResult;
19
+ /** The deny of a judgment that FAILED (threw or rejected) while `signal` bounded its wait: the aborted deny when the signal had
20
+ * already ended the wait (a cancellation — not reported), else the policy's own throw — its sentence and one line of what it
21
+ * threw — reported once through {@link reportPolicyThrow}. A seat that raced the judgment against its signal has told the two
22
+ * apart already and passes no signal. */
23
+ export declare function failedJudgmentDeny(err: unknown, signal: AbortSignal | undefined, report: (err: Error) => void, seat: string, call: {
24
+ readonly toolName: string;
25
+ readonly toolCallId: string;
26
+ }): PermissionResult;
27
+ /** Hand the deployment, through `report` (the seat's hook-failure lane), the failure of the policy that threw while `seat`
28
+ * judged `call`. A rejection that arrives with the judgment's `signal` already aborted is the task ending mid-judgment — a
29
+ * cancellation, not a policy defect — and is not reported (the deny it produced stands either way). A report lane that
30
+ * throws is swallowed: the fail-closed deny is decided before the report and must not depend on the sink. */
31
+ export declare function reportPolicyThrow(report: (err: Error) => void, seat: string, call: {
32
+ readonly toolName: string;
33
+ readonly toolCallId: string;
34
+ }, err: unknown, signal?: AbortSignal): void;
@@ -0,0 +1,25 @@
1
+ import { thrownLabel, thrownText } from "../thrown-value.js";
2
+ import { redactSecrets } from "../untrusted-egress.js";
3
+ import { inlineUntrusted, PROBE_REASON_MAX } from "../untrusted-text.js";
4
+ export function policyThrowLine(err) {
5
+ const line = inlineUntrusted(redactSecrets(thrownText(err)), PROBE_REASON_MAX);
6
+ return line === "" ? "(no message)" : line;
7
+ }
8
+ export function policyCheckAbortedDeny() {
9
+ return { action: "deny", message: "policy check aborted (task timed out or cancelled)" };
10
+ }
11
+ export function failedJudgmentDeny(err, signal, report, seat, call) {
12
+ if (signal?.aborted === true)
13
+ return policyCheckAbortedDeny();
14
+ reportPolicyThrow(report, seat, call, err);
15
+ return { action: "deny", message: `policy check threw — the tool policy chain failed while judging this call (not a timeout or cancellation), so it was denied fail-closed: ${policyThrowLine(err)}` };
16
+ }
17
+ export function reportPolicyThrow(report, seat, call, err, signal) {
18
+ if (signal?.aborted === true)
19
+ return;
20
+ try {
21
+ report(new Error(`${seat} threw while judging "${call.toolName}" (toolCallId ${call.toolCallId}); the call was denied fail-closed: ${redactSecrets(thrownLabel(err))}`));
22
+ }
23
+ catch {
24
+ }
25
+ }
@@ -1,4 +1,6 @@
1
1
  import { StoredSession, isSessionConflict } from "../session.js";
2
+ import { userAttachmentOf } from "../../internal/llm.js";
3
+ import { lastAttachmentFrameOf } from "../user-attachment.js";
2
4
  import { reconcileInterruptedSession } from "../session-reconcile.js";
3
5
  import { detectConflicts, settleBranchWriteBack } from "./branch-writeback.js";
4
6
  import { settleTeardownLeg } from "./teardown-bounded.js";
@@ -57,6 +59,11 @@ export async function prepareAcquireReconcile(input) {
57
59
  e.code = "resume_at.not_a_message";
58
60
  throw e;
59
61
  }
62
+ if (userAttachmentOf(msg) !== undefined) {
63
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" is an attachment frame that trails a user message; resume at that user message instead`);
64
+ e.code = "resume_at.not_a_message";
65
+ throw e;
66
+ }
60
67
  }
61
68
  if (spec.resumeAtMode === "before") {
62
69
  const role = entry.type === "message" ? entry.message.role : undefined;
@@ -84,7 +91,7 @@ export async function prepareAcquireReconcile(input) {
84
91
  throw e;
85
92
  }
86
93
  }
87
- const storage = session.getStorage(), leafBefore = await storage.getLeafId(), leafSet = spec.resumeAtMode === "before" ? entry.parentId : spec.resumeAt;
94
+ const storage = session.getStorage(), leafBefore = await storage.getLeafId(), leafSet = spec.resumeAtMode === "before" ? entry.parentId : entry.type === "message" && entry.message.role === "user" ? lastAttachmentFrameOf(await session.getEntries(), spec.resumeAt) : spec.resumeAt;
88
95
  await storage.setLeafId(leafSet);
89
96
  own.ids = new Set();
90
97
  branch = { storage, leafBefore, leafSet, own };
@@ -20,7 +20,8 @@ import { RosterBuilder } from "../tool-roster.js";
20
20
  import type { AgentTool, RunnerDeps } from "../types.js";
21
21
  import type { ContainmentFace } from "../../tools/fs/index.js";
22
22
  import type { PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
23
- import { type ArtifactStoreRef } from "./read-root-grant.js";
23
+ import type { ArtifactStoreRef } from "./read-root-grant.js";
24
+ import type { ReadRootsSeat } from "./person-path-rows.js";
24
25
  import type { Prepared } from "./contracts.js";
25
26
  import type { ToolOccurrenceTable } from "./tool-occurrence.js";
26
27
  export interface PrepareArtifactInput {
@@ -48,8 +49,9 @@ export interface PrepareArtifactInput {
48
49
  /** borrowed-readonly — the canonical task root the fs fence contains reads to; undefined ⇒ no local-file publish. */
49
50
  rootCanonical: string | undefined;
50
51
  /** borrowed-readonly — the extra containment roots (write and read-only) the fence admits, read per call (the run's
51
- * live set: an in-flight read-root grant widens it). */
52
- additionalRootsCanonical: () => readonly string[];
52
+ * live set: an in-flight read-root grant widens it); `ready` settles once the run's newest tables are compiled — this
53
+ * reader is no call, and reads them only then. */
54
+ additionalRootsCanonical: (() => readonly string[]) & Pick<ReadRootsSeat, "ready">;
53
55
  /** borrowed-readonly — the read-deny matcher in force for the run, when any. */
54
56
  readDenyMatcher: PlacedReadDenyMatcher | undefined;
55
57
  /** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
@@ -5,9 +5,7 @@ import { RosterBuilder } from "../tool-roster.js";
5
5
  import { defineTool } from "../tools.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { physicalPathOfExisting } from "../physical-path.js";
8
- import { resolveKey, violationDetails, violationText } from "../../tools/fs/safety.js";
9
- import { rootAdmittingArtifactStore, withdrawStandingGrantsAdmitting } from "./read-root-grant.js";
10
- import { emitTrace } from "../trace.js";
8
+ import { resolveKey, rootAdmittingArtifactStore, violationDetails, violationText } from "../../tools/fs/safety.js";
11
9
  import { createArtifactTool } from "../../tools/artifact/artifact-tool.js";
12
10
  import { thrownText } from "../thrown-value.js";
13
11
  export function prepareArtifact(input) {
@@ -30,11 +28,6 @@ export function prepareArtifact(input) {
30
28
  const storePhysical = storageDir !== undefined && rootCanonical !== undefined ? physicalPathOfExisting(storageDir) : undefined;
31
29
  if (artifactStoreRef !== undefined)
32
30
  artifactStoreRef.current = storePhysical;
33
- if (artifactStoreRef !== undefined && storePhysical !== undefined) {
34
- for (const entry of withdrawStandingGrantsAdmitting(storePhysical, artifactStoreRef)) {
35
- emitTrace(deps.tracer, () => ({ kind: "config.additional_directory_skipped", version: 1, taskId, entry, field: "persistedReadRule", reason: `admits_artifact_store: the local artifact store ${storageDir} lies inside it or it inside the store — the Artifact tool's ledger and stored files stay outside every root the file tools reach`, ts: Date.now() }));
36
- }
37
- }
38
31
  if (storePhysical !== undefined && rootCanonical !== undefined) {
39
32
  const overlapping = rootAdmittingArtifactStore(storePhysical, [rootCanonical, ...additionalRootsCanonical()]);
40
33
  if (overlapping !== undefined)
@@ -49,6 +42,7 @@ export function prepareArtifact(input) {
49
42
  const readLocalFile = async (path, maxBytes, signal) => {
50
43
  if (rootCanonical === undefined)
51
44
  return { ok: false, reason: "refused", text: "Error (Artifact): this task has no filesystem root, so a local file cannot be published." };
45
+ await additionalRootsCanonical.ready(signal);
52
46
  const rk = await resolveKey(executionEnv, rootCanonical, path, signal, handsCwdRef?.current, additionalRootsCanonical, undefined, readDenyMatcher, resolvedReadFace);
53
47
  if (!rk.ok)
54
48
  return { ok: false, reason: "refused", text: violationText("Artifact", rk.violation), details: violationDetails(rk.violation) };
@@ -93,7 +93,8 @@ export interface PrepareAskLaneInput {
93
93
  hooks: Hooks | undefined;
94
94
  /** borrowed-readonly — the validated per-prepare hook seat bound (the deny observer's timeout). */
95
95
  hookTimeoutMs: number;
96
- /** borrowed-readonly — the own hook-crash reporter (the deny observer's swallow-guarded report sink). */
96
+ /** borrowed-readonly — the own hook-failure lane (`onError`, phase `hook`, swallow-guarded): the deny observer's report sink,
97
+ * the gate's crashing-PreToolUse sink and the adjudicate seat's report of a policy chain that threw. */
97
98
  notifyOwnHookCrash: (err: unknown) => void;
98
99
  /** borrowed-readonly — the hands phase's in-flight read-root widening (an approver's accepted `readRootGrant`), or undefined (hands-less). */
99
100
  grantReadRoot: GrantReadRoot | undefined;
@@ -3,13 +3,13 @@ import { primaryActivityArg } from "../arg-summary.js";
3
3
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
4
4
  import { advertisedWritableRootsSeat, exemptRootsSeat, attest, attestationOf, isLiveApproverSeat, resolveAsk } from "../tool-policy.js";
5
5
  import { boundInputHashOf } from "../canonical-json.js";
6
- import { composeCallSignal, raceAbort } from "./abort-race.js";
6
+ import { composeCallSignal, raceSettlementAgainstSignal } from "./abort-race.js";
7
+ import { failedJudgmentDeny, policyCheckAbortedDeny } from "./policy-throw.js";
7
8
  import { askMandateMember } from "../ask-origin.js";
8
9
  import { approvalCardMemberOf } from "../tool-roster.js";
9
10
  import { gateAskCarry, lateAskSettlementObserver } from "./denial-limit-arms.js";
10
11
  import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
11
12
  import { settleReadRootGrant } from "./read-root-grant.js";
12
- import { asThrownError } from "../thrown-value.js";
13
13
  function createDenyObserverNotifier(hooks, hookTimeoutMs, signal, report) {
14
14
  if (hooks?.permissionDenied === undefined)
15
15
  return undefined;
@@ -66,14 +66,19 @@ export function prepareAskLane(input) {
66
66
  return { askLane: undefined };
67
67
  const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
68
68
  const adjudicate = effectivePolicy
69
- ? (req, callSignal) => {
69
+ ? async (req, callSignal) => {
70
70
  const signal = composedCallSignal(callSignal);
71
71
  const stampedReq = () => ({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs), ...exemptRootsSeat(exemptRoots) });
72
- const withReading = stampCallTarget?.(stampedReq(), signal);
73
- return raceAbort(withReading === undefined ? Promise.resolve(effectivePolicy.check(stampedReq(), signal)) : withReading.then((r) => effectivePolicy.check(r, signal)), signal, () => ({
74
- action: "deny",
75
- message: "policy check aborted (task timed out or cancelled)",
76
- }));
72
+ const judged = (async () => {
73
+ const withReading = stampCallTarget?.(stampedReq(), signal);
74
+ return withReading === undefined ? effectivePolicy.check(stampedReq(), signal) : effectivePolicy.check(await withReading, signal);
75
+ })();
76
+ const raced = await raceSettlementAgainstSignal(judged, signal);
77
+ if (raced.tag === "value")
78
+ return raced.value;
79
+ if (raced.tag === "aborted")
80
+ return policyCheckAbortedDeny();
81
+ return failedJudgmentDeny(raced.error, undefined, notifyOwnHookCrash, "the tool policy chain", req);
77
82
  }
78
83
  : undefined;
79
84
  const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
@@ -138,12 +143,5 @@ export function prepareAskLane(input) {
138
143
  return resolved;
139
144
  };
140
145
  const notifyPermissionDenied = createDenyObserverNotifier(hooks, hookTimeoutMs, abortController.signal, notifyOwnHookCrash);
141
- const notifyHookError = (err) => {
142
- try {
143
- deps.onError?.(asThrownError(err), { phase: "hook", sessionId });
144
- }
145
- catch {
146
- }
147
- };
148
- return { askLane: { composedCallSignal, adjudicate, approvalPreviewOf, resolveAskBound, notifyPermissionDenied, notifyHookError } };
146
+ return { askLane: { composedCallSignal, adjudicate, approvalPreviewOf, resolveAskBound, notifyPermissionDenied, notifyHookError: notifyOwnHookCrash } };
149
147
  }
@@ -114,8 +114,9 @@ export interface PrepareCapsAndWorkflowInput {
114
114
  * arming facts (the deny bits, the face, the re-entrant arm) are bound onto it here. */
115
115
  permissionMode: PermissionModeController;
116
116
  /** borrowed-readonly — LATE-BOUND: the effective read-deny table the hands phase compiles (the rows the classifier's deny
117
- * paragraph renders); read at ARMING, which the driver runs after that phase — never at this phase's own call. */
118
- readDenyEntries: () => {
117
+ * paragraph renders); read at ARMING, which the driver runs after that phase — never at this phase's own call — once the
118
+ * run's newest tables are compiled (an arming is no call). */
119
+ readDenyEntries: () => Promise<{
119
120
  readonly entries: ReadonlyArray<{
120
121
  readonly pattern: string;
121
122
  readonly caseSensitive?: boolean;
@@ -124,7 +125,7 @@ export interface PrepareCapsAndWorkflowInput {
124
125
  readonly tool: string;
125
126
  readonly pattern: string;
126
127
  }>;
127
- };
128
+ }>;
128
129
  /** borrowed-readonly — the peer lane's mount verdict (the classifier's cross-session rule fills only while mounted). */
129
130
  peerLaneActive: boolean;
130
131
  /** borrowed-readonly — the leg's effective delegation fact (`effectiveDelegationFacts`): the classifier window shows its user lane as the launching agent's. */
@@ -226,7 +226,7 @@ export async function prepareCapsAndWorkflow(input) {
226
226
  }
227
227
  announceClassifierRoleDefaulted({ onNotice: deps.onNotice, seat: { ...classifierSeat, model: classifierModel }, ...(sessionId !== undefined ? { sessionId } : {}), ...(runId !== undefined ? { runId } : {}) });
228
228
  const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
229
- const table = readDenyEntries();
229
+ const table = await readDenyEntries();
230
230
  const derivedDenyRules = readDenyClassifierRules(table.entries, table.ruleRows);
231
231
  let classifierSystemPromptMemo;
232
232
  const classifierSystemPrompt = () => (classifierSystemPromptMemo ??= buildAutoModePrompt({ ...am, derivedDenyRules, ...(classifierLaneRule ? { crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : {}) }));
@@ -2,7 +2,7 @@ import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens } from "../../internal/h
2
2
  import { TOOL_TURN_REPAIR_DROPS } from "../../internal/llm.js";
3
3
  import { clearStaleToolResults, createClearedProjectionLedger, dropEmptyFailureAssistants, editBudget, replayClearedProjection, resolveTriggerWindow } from "../context-edit.js";
4
4
  import { guardBudget, insertTrimNotice, protectGitFrame, trimToBudget } from "../context-guard.js";
5
- import { AGGREGATE_MEDIA_BUDGET_BYTES, capAggregateMediaBytes } from "../media-byte-cap.js";
5
+ import { capAggregateMediaBytes, mediaBudgetBytes } from "../media-byte-cap.js";
6
6
  import { capAggregateToolResults } from "../tool-result-budget.js";
7
7
  import { createOffloadPersist } from "../tool-result-store.js";
8
8
  import { emitTrace } from "../trace.js";
@@ -50,7 +50,7 @@ export function prepareContextLane(input) {
50
50
  }))),
51
51
  });
52
52
  const mediaCapped = capAggregateMediaBytes(capped, {
53
- limitBytes: deps.mediaByteCapBytes ?? AGGREGATE_MEDIA_BUDGET_BYTES,
53
+ limitBytes: mediaBudgetBytes(deps.mediaByteCapBytes),
54
54
  onStripped: (info) => {
55
55
  const onMediaStripped = deps.onMediaStripped;
56
56
  if (onMediaStripped !== undefined)
@@ -155,9 +155,9 @@ export interface PrepareGateStationsInput {
155
155
  handsCwdRef: CwdRef | undefined;
156
156
  /** borrowed-readonly — the leg's target-reading stamp, or undefined (hands-less): the gate stamps its request with it. */
157
157
  stampCallTarget: CallTargetStamp | undefined;
158
- /** borrowed-readonly — the leg's read-deny table in force, read at a release: the active built-in tiers and the rows
159
- * beside them (the deployment's and the task's patterns, the read-face rule rows). Empty ⇒ no table. */
160
- readDenyInForce: () => {
158
+ /** borrowed-readonly — the read-deny table the released call was judged with, read at its release: the active built-in
159
+ * tiers and the rows beside them (the deployment's and the task's patterns, the read-face rule rows). Empty ⇒ no table. */
160
+ readDenyInForce: (toolCallId: string) => {
161
161
  readonly tiers: readonly string[];
162
162
  readonly rows: readonly string[];
163
163
  };
@@ -50,7 +50,7 @@ export function prepareGateStations(input) {
50
50
  const gatePreToolUse = ownGatePreToolUse !== undefined ? (name, args, ctx) => ownGatePreToolUse(name, args, { ...ctx, permissionMode: permissionMode.observed }) : undefined;
51
51
  const releasesAnnounced = new Map();
52
52
  const onModeReleased = (info) => {
53
- const table = info.readUnestablished === true ? input.readDenyInForce() : undefined;
53
+ const table = info.readUnestablished === true ? input.readDenyInForce(info.toolCallId) : undefined;
54
54
  if (table !== undefined && table.tiers.length + table.rows.length > 0) {
55
55
  const named = [...(table.tiers.length > 0 ? [`built-in tiers ${table.tiers.join(", ")}`] : []), ...table.rows.slice(0, 8), ...(table.rows.length > 8 ? [`${table.rows.length - 8} more`] : [])].join("; ");
56
56
  deliverEngineNotice(deps.onNotice, {
@@ -1,6 +1,6 @@
1
1
  import { type BeforeWriteHook, type CwdRef, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
2
2
  import { type ReadFileState } from "../../tools/fs/safety.js";
3
- import { type PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
3
+ import { type PathTablesFacade, type PersonPathRows, type ReadRootsSeat } from "./person-path-rows.js";
4
4
  import type { HeadLeads } from "../path-rule-binding.js";
5
5
  import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
6
6
  import type { ShellReadBoundary } from "../../tools/fs/shell-read-judge.js";
@@ -56,19 +56,17 @@ export interface PrepareHandsReadFaceInput {
56
56
  /** borrowed-readonly — the registry scope (`internals?.registryScope ?? spec.principal ??
57
57
  * "default"`, computed in the driver); the band's task-registry scope. */
58
58
  taskScope: string;
59
- /** borrowed-readonly — the deny tables' permission-rule rows: the face-keyed path deny rules of the leg's caller policy and
60
- * of every ancestor's, each bound where it was installed (`toolPolicyPathRules`), and the person's deny rows; joined to the
61
- * read face's matcher and to the shell station's write face. */
59
+ /** borrowed-readonly — the leg's own deny-table rows: the face-keyed path deny rules of the leg's caller policy and of every
60
+ * ancestor's, each bound where it was installed (`toolPolicyPathRules`); the person's join them per read ({@link personRows}). */
62
61
  denyRuleRows: readonly PathRuleDenyRow[];
62
+ /** borrowed-readonly — the run's one reader of the person's rules: the run's tables are compiled here over it. */
63
+ personRows: PersonPathRows;
63
64
  /** borrowed-readonly — the leg's one resolution of where rule heads lead (the rows' twins, the same the gate's lanes read). */
64
65
  headLeads: HeadLeads;
65
66
  /** borrowed-readonly — the ONE judgment a shell command's write targets meet for the mode's file-command placement: the
66
67
  * leg's write judgment over the write-protection table and the chain's guarded write rows (read per judgment — it is
67
68
  * compiled at the manifest phase, before any gate is mounted). Absent ⇒ none (a standalone phase drive). */
68
69
  writeTargetJudge?: () => ShellReadBoundary["writeTargetJudge"];
69
- /** borrowed-readonly — the directories of the person's persisted `Read(//dir/**)` allow rows: read directories of this
70
- * run, canonicalized and admitted exactly as `additionalReadDirectories` entries are. */
71
- persistedReadDirectories?: readonly string[];
72
70
  /** borrowed-readonly — the leg's mode seat, read LIVE by the write face ({@link writeFaceReader}): its mode's face
73
71
  * is the resolver's row 3′ on every write. */
74
72
  permissionMode: {
@@ -160,6 +158,11 @@ export interface PrepareHandsReadFaceInput {
160
158
  * band's Read/notebook/PDF trailer outlets bump keys on it at tool-execute time (the driver owns
161
159
  * the object and folds it into result stats). */
162
160
  reminderDisclosureCounts: import("../reminder-disclosure.js").ReminderDisclosureCounts;
161
+ /** borrowed-mutable — where the band hands out the leg's one file reader (the Read tool's), for the leg's other
162
+ * carriers of a file (attachment frames): written once at mount, left empty when hands do not mount. */
163
+ modelFileReaderSeat?: {
164
+ current?: import("../../tools/fs/fs-read.js").ModelFileReader;
165
+ };
163
166
  /** borrowed-readonly — the first-touch history hook prepare built (present iff the run records file history into a
164
167
  * wired store and mounts a real fs env); threaded to the write band's trackFileEdit seat. */
165
168
  trackFileEdit?: import("../types.js").TrackFileEditHook;
@@ -218,6 +221,8 @@ export interface PrepareHandsReadFaceResult {
218
221
  /** owned — the same station's pre-spawn ADMISSION, published for the P11 face that spawns a shell command
219
222
  * and mounts LATER than the band (`Monitor`): the SAME function the band's shell faces got. ⇔ a real shell. */
220
223
  shellReadAdmission: ShellReadAdmission | undefined;
224
+ /** owned — the file read station's admission (`FileReadStation.admitted`) the band's read tools got, for the read tools mounted later (LSP). */
225
+ readAdmission: ((toolCallId: string | undefined, canonicalKey: string) => boolean) | undefined;
221
226
  /** write-back (D-3 hold) — the run's RESOLVED read face where hands mount; undefined ⇔ no hands
222
227
  * (the hands-less value lives in `handsLessResolvedFace`). The driver assigns this onto its
223
228
  * outer `let` for the call-time closures (ctx enrichment / checkpoint mint / envFacts). */
@@ -258,9 +263,13 @@ export interface PrepareHandsReadFaceResult {
258
263
  /** owned — the canonical containment root, published so the working-file ATTACHMENT
259
264
  * reader applies the same containment the hands tools do. Undefined ⇔ hands-less. */
260
265
  attachmentRootCanonical: string | undefined;
261
- /** owned — the ONE compiled deny judge per prepare (built-ins ∪ deployment ∪
262
- * task additions), carrying the home views of its placement. Undefined ⇔ hands-less (its consumers only exist with hands). */
263
- readDenyMatcher: PlacedReadDenyMatcher | undefined;
266
+ /** owned — the run's read deny table ({@link pathTables}' facade: the run's latest, `forCall` a call's own). Undefined ⇔
267
+ * hands-less (its consumers only exist with hands). */
268
+ readDenyMatcher: PathTablesFacade["readDeny"] | undefined;
269
+ /** owned — the read face's root set per judgment: write roots, read roots with grants, the person's read directories. */
270
+ readRoots: ReadRootsSeat;
271
+ /** owned — the run's path tables ({@link PathTablesFacade}); undefined ⇔ hands-less. */
272
+ pathTables: PathTablesFacade | undefined;
264
273
  /** owned — the shell-seat PROVENANCE set: the shell-running tools whose tier the engine's shell seat (read
265
274
  * boundary + shape tier + shellGate floor) is the SOLE POSITIVE source of — no explicit deployment mark on that name.
266
275
  * The one seat every consumer reads (risk grading, the gate's tighten attribution, the persisted-rule mandate
@@ -2,17 +2,19 @@ import { emitTrace } from "../trace.js";
2
2
  import { StubExecutionEnv } from "../stub-env.js";
3
3
  import { hasBackgroundShell } from "../background-shell.js";
4
4
  import { defaultTaskRegistry } from "../task-registry.js";
5
- import { bashReversibilityProbe, activeReadDenyBuiltinTiers, readDenyBuiltinConfigOf, compileReadDeny, compileWriteDeny, rowsWhereTheyLead, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, backgroundOutputReadExemption, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
- import { resolveKey, sha256 } from "../../tools/fs/safety.js";
7
- import { placeReadDenyAtHome } from "../../tools/fs/read-deny-home.js";
5
+ import { bashReversibilityProbe, activeReadDenyBuiltinTiers, readDenyBuiltinConfigOf, compileReadDeny, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, backgroundOutputReadExemption, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
+ import { resolveKey, rootAdmittingArtifactStore, sha256 } from "../../tools/fs/safety.js";
7
+ import { thrownText } from "../thrown-value.js";
8
+ import { carryReadFace } from "../../tools/fs/read-deny-home.js";
9
+ import { EMPTY_READ_ROOTS, afterCallTables, denySeatsOf, onPinnedRead, pathTablesCompile, readRootsOver } from "./person-path-rows.js";
8
10
  import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js";
9
11
  import { createShellReadStation } from "../../tools/fs/shell-read-check.js";
10
- import { composeReadChecks, createFileReadStation } from "../../tools/fs/file-read-check.js";
12
+ import { createFileReadStation } from "../../tools/fs/file-read-check.js";
11
13
  import { isShellFaceToolName, predictMountedToolNames, readTargetToolNames } from "../tool-registry.js";
12
14
  import { normalizeFileText } from "../../tools/fs/encoding.js";
13
15
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
14
16
  import { deliverEngineNotice } from "../types.js";
15
- import { readRootGrantOver, rootAdmittingArtifactStore } from "./read-root-grant.js";
17
+ import { readRootGrantOver } from "./read-root-grant.js";
16
18
  import { sessionSeatAnchorHolds } from "./session-read-file-states.js";
17
19
  import { rebaseWorkspacePath } from "./workspace-path.js";
18
20
  import { RosterBuilder } from "../tool-roster.js";
@@ -101,6 +103,7 @@ export function resolveHandsLessReadFace(input) {
101
103
  shellReadCheck: undefined,
102
104
  shellReadStationArmed: false,
103
105
  shellReadAdmission: undefined,
106
+ readAdmission: undefined,
104
107
  resolvedReadFace: undefined,
105
108
  resolvedWriteFace: undefined,
106
109
  readDenyAdditionsNormalized,
@@ -113,6 +116,8 @@ export function resolveHandsLessReadFace(input) {
113
116
  artifactStoreRef: undefined,
114
117
  attachmentRootCanonical: undefined,
115
118
  readDenyMatcher: undefined,
119
+ readRoots: EMPTY_READ_ROOTS,
120
+ pathTables: undefined,
116
121
  envHandToolNames: new Set(),
117
122
  shellGatedTools: new Set(),
118
123
  sealReadStateSeat: undefined,
@@ -148,6 +153,8 @@ export async function prepareHandsMount(input) {
148
153
  const artifactStoreRef = { current: undefined };
149
154
  let attachmentRootCanonical;
150
155
  let readDenyMatcher;
156
+ let pathTables;
157
+ let readRoots = EMPTY_READ_ROOTS;
151
158
  if (handsEnabled) {
152
159
  const rootCanonical = taskRootCanonical;
153
160
  attachmentRootCanonical = rootCanonical;
@@ -178,10 +185,6 @@ export async function prepareHandsMount(input) {
178
185
  };
179
186
  await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
180
187
  await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
181
- const standingCanonical = [];
182
- await canonicalizeExtraDirs(input.persistedReadDirectories, "persistedReadRule", standingCanonical);
183
- artifactStoreRef.standing = { keys: standingCanonical.filter((k) => !additionalReadRootsCanonical.includes(k) && !additionalRootsCanonical.includes(k)), readRoots: additionalReadRootsCanonical };
184
- additionalReadRootsCanonical.push(...standingCanonical);
185
188
  if (advertisedWritableDirs !== undefined) {
186
189
  if (spec.envFacts?.scratchpadDir)
187
190
  await advertisedWritableDirs.admit({ path: spec.envFacts.scratchpadDir, advertiser: "scratchpad", materializeMarker: ".sema-scratchpad" });
@@ -194,7 +197,7 @@ export async function prepareHandsMount(input) {
194
197
  readFileStateForCheckpoint = readFileState;
195
198
  seedContextFiles = async (files) => {
196
199
  for (const f of files) {
197
- const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
200
+ const rk = await readRoots.ready().then(() => resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, readRoots(), undefined, undefined, resolvedReadFace));
198
201
  if (rk.ok)
199
202
  seedReadFileStateFromContext(readFileState, rk.key, f.content);
200
203
  }
@@ -220,9 +223,12 @@ export async function prepareHandsMount(input) {
220
223
  const seedReadFaceSection = resume?.seed.readFace;
221
224
  const readDenyAdditions = unionAddOnly("readDenyPatterns", deps.readDenyPatterns, spec.readDenyPatterns, seedReadFaceSection?.denyEntries);
222
225
  const readDenyBuiltinCfg = readDenyBuiltinConfigOf({ tiers: deps.readDenyBuiltinTiers, exclude: deps.readDenyBuiltinExclude });
223
- const denyRuleRows = rowsWhereTheyLead(input.denyRuleRows, await input.headLeads.resolve(input.denyRuleRows));
224
- const readDenyPlacement = await placeReadDenyAtHome(executionEnv, (homes, linked) => compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg, denyRuleRows, homes, linked), undefined, undefined, deps.agentConfigDir);
225
- readDenyMatcher = readDenyPlacement.matcher;
226
+ const canonicalize = async (dirs) => { const out = []; for (const d of dirs)
227
+ await canonicalizeExtraDirs([d], "persistedReadRule", out).catch((err) => deps.onError?.(new Error(`persistedReadRule entry skipped (its lookup threw): ${d}: ${thrownText(err)}`), { phase: "config", sessionId })); return out; };
228
+ const tables = (pathTables = await input.personRows.attachTables(pathTablesCompile({ env: executionEnv, additions: readDenyAdditions, builtin: readDenyBuiltinCfg, legRows: input.denyRuleRows, leads: input.headLeads, configRoot: deps.agentConfigDir, canonicalize })));
229
+ readDenyMatcher = tables.readDeny;
230
+ readRoots = readRootsOver({ own: () => [...additionalRootsCanonical, ...additionalReadRootsCanonical], tables, ready: input.personRows.tablesReady, store: () => artifactStoreRef.current,
231
+ onHeld: (entry, store) => emitTrace(deps.tracer, () => ({ kind: "config.additional_directory_skipped", version: 1, taskId: hostTaskId, entry, field: "persistedReadRule", reason: `admits_artifact_store: the local artifact store ${store} lies inside it or it inside the store — the Artifact tool's ledger and stored files stay outside every root the file tools reach`, ts: Date.now() })) });
226
232
  readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinCfg);
227
233
  readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
228
234
  let liveReadFace = resolveContainmentFace({
@@ -247,7 +253,7 @@ export async function prepareHandsMount(input) {
247
253
  seatLeafId = undefined;
248
254
  }
249
255
  const judgeKey = async (path) => {
250
- const rk = await resolveKey(executionEnv, rootCanonical, path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, liveReadFace);
256
+ const rk = await readRoots.ready().then(() => resolveKey(executionEnv, rootCanonical, path, undefined, undefined, readRoots(), undefined, undefined, liveReadFace));
251
257
  return rk.ok ? rk.key : undefined;
252
258
  };
253
259
  if (resume === undefined && spec.sessionId !== undefined) {
@@ -262,8 +268,12 @@ export async function prepareHandsMount(input) {
262
268
  const prior = await session.buildContext().catch(() => undefined);
263
269
  for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
264
270
  const rk = await judgeKey(rec.path);
265
- if (rk !== undefined)
271
+ if (rk === undefined)
272
+ continue;
273
+ if ("content" in rec)
266
274
  seedReadFileStateFromTranscript(readFileState, rk, rec.content, rec.at);
275
+ else
276
+ readFileState.set(rk, { ...rec.observation, lastReadAt: rec.at });
267
277
  }
268
278
  }
269
279
  }
@@ -290,30 +300,36 @@ export async function prepareHandsMount(input) {
290
300
  sessionSeats.reanchor(sessionId, readFileState, ownCommittedTailEntryId);
291
301
  }
292
302
  : undefined;
293
- const denySeat = readDenySeat(readDenyMatcher);
294
- const writeDeny = compileWriteDeny(denyRuleRows);
295
- const writeDenySeat = writeDeny.ruleRows.length > 0 ? { writeDenyMatch: (p) => writeDeny.matchPath(p)?.pattern ?? null } : {};
296
- const shellReadBoundary = () => ({
297
- roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
298
- ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
299
- ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
300
- ...denySeat,
301
- ...writeDenySeat,
302
- writeRoots: [rootCanonical, ...additionalRootsCanonical],
303
- ...(resolvedWriteFace !== undefined ? { writeFace: resolvedWriteFace.inForce() } : {}),
304
- ...((judge) => (judge !== undefined ? { writeTargetJudge: judge } : {}))(input.writeTargetJudge?.()),
305
- ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
306
- });
303
+ const shellReadBoundary = (toolCallId) => {
304
+ const { read: denySeat, write: writeDenySeat } = denySeatsOf(tables.forCall(toolCallId));
305
+ return {
306
+ roots: [rootCanonical, ...readRoots.forCall(toolCallId)()],
307
+ ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
308
+ ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
309
+ ...denySeat,
310
+ ...writeDenySeat,
311
+ ...(input.personRows.unreadFor(toolCallId) ? { unreadPartitions: true } : {}),
312
+ writeRoots: [rootCanonical, ...additionalRootsCanonical],
313
+ ...(resolvedWriteFace !== undefined ? { writeFace: resolvedWriteFace.inForce() } : {}),
314
+ ...((judge) => (judge !== undefined ? { writeTargetJudge: judge } : {}))(input.writeTargetJudge?.()),
315
+ ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
316
+ };
317
+ };
307
318
  if (handsIncludeShell) {
308
319
  const spoolFence = handsReadOnly ? createShellOverflowSpoolFence(executionEnv) : undefined;
309
- const readonlyBoundary = () => spoolFence !== undefined ? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, ...writeDenySeat, alsoInside: spoolFence } : undefined;
320
+ const readonlyBoundary = (toolCallId) => {
321
+ if (spoolFence === undefined)
322
+ return undefined;
323
+ const { read: denySeat, write: writeDenySeat } = denySeatsOf(tables.forCall(toolCallId));
324
+ return { roots: [rootCanonical, ...readRoots.forCall(toolCallId)()], cwd: rootCanonical, ...denySeat, ...writeDenySeat, ...(input.personRows.unreadFor(toolCallId) ? { unreadPartitions: true } : {}), alsoInside: spoolFence };
325
+ };
310
326
  shellReadTools = new Set(shellFaceHere);
311
327
  shellReadStation = createShellReadStation({
312
328
  env: executionEnv,
313
329
  tools: shellReadTools,
314
330
  seatOf: () => (handsReadOnly ? "readonly" : "full_shell"),
315
- boundaryOf: () => readonlyBoundary() ?? shellReadBoundary(), rootsCanonical: true,
316
- homeViews: readDenyPlacement.matcher.homeViews,
331
+ boundaryOf: (_tool, _cwd, toolCallId) => readonlyBoundary(toolCallId) ?? shellReadBoundary(toolCallId), rootsCanonical: true,
332
+ homeViews: tables.readDeny.homeViews,
317
333
  onAdmissionRefused: (toolCallId) => void input.toolOccurrences.refuse(toolCallId, "read_boundary"),
318
334
  });
319
335
  }
@@ -322,8 +338,8 @@ export async function prepareHandsMount(input) {
322
338
  fileReadStation = createFileReadStation({
323
339
  env: executionEnv,
324
340
  rootCanonical,
325
- roots: () => [...additionalRootsCanonical, ...additionalReadRootsCanonical],
326
- ...(readDenyMatcher !== undefined ? { readDeny: readDenyMatcher } : {}),
341
+ roots: readRoots,
342
+ readDeny: tables.readDeny,
327
343
  ...(liveReadFace !== undefined ? { readFace: liveReadFace } : {}),
328
344
  ...("homeDir" in executionEnv && executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
329
345
  tools: readTargetToolNames(),
@@ -333,17 +349,12 @@ export async function prepareHandsMount(input) {
333
349
  }
334
350
  const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
335
351
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
336
- additionalReadRoots: additionalReadRootsCanonical,
337
- ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
338
- ...(denyRuleRows.length > 0 ? { pathDenyRules: denyRuleRows } : {}),
339
- readDenyHomes: readDenyPlacement.homes,
340
- readDenyHomeLinks: readDenyPlacement.linked,
341
- ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
342
- ...(readDenyBuiltinCfg.exclude !== undefined ? { readDenyBuiltinExclude: readDenyBuiltinCfg.exclude } : {}),
352
+ readDenyHomes: carryReadFace([], { readDeny: tables.readDeny, writeDeny: tables.writeDeny, readRoots }),
343
353
  readFace: liveReadFace,
344
354
  writeFaceLive: resolvedWriteFace.inForce,
345
355
  reminderMark: input.reminderMark,
346
356
  reminderDisclosureCounts: input.reminderDisclosureCounts,
357
+ ...(input.modelFileReaderSeat !== undefined ? { modelFileReaderSeat: input.modelFileReaderSeat } : {}),
347
358
  ...(input.trackFileEdit !== undefined ? { trackFileEdit: input.trackFileEdit } : {}),
348
359
  ...(input.onFileEdited !== undefined ? { onFileEdited: input.onFileEdited } : {}),
349
360
  includeShell: handsIncludeShell,
@@ -387,7 +398,7 @@ export async function prepareHandsMount(input) {
387
398
  deps.onError?.(new Error(`spec.tools ${shadowed.length > 1 ? "entries" : "entry"} ${names} collide${shadowed.length > 1 ? "" : "s"} with the built-in hands tool band — the built-in wins (registered later; the harness tool Map is last-write-wins) and the caller tool never dispatches this task. Rename the caller tool, or don't inject executionEnv if you intend to replace the band.`), { phase: "config", sessionId });
388
399
  }
389
400
  }
390
- roster.mountAll(band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))), { source: "builtin", mountedBy: "hands" });
401
+ roster.mountAll(band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(afterCallTables(input.personRows, t)))), { source: "builtin", mountedBy: "hands" });
391
402
  const shellFloor = effectiveShellGate;
392
403
  if (!(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
393
404
  const installShellSeat = (name) => {
@@ -413,9 +424,10 @@ export async function prepareHandsMount(input) {
413
424
  wsSnapshot,
414
425
  rebaseWsPath,
415
426
  backgroundTaskToolsActive,
416
- shellReadCheck: composeReadChecks([...(shellReadStation !== undefined ? [{ check: shellReadStation.check, tools: shellReadTools }] : []), ...(fileReadStation !== undefined ? [fileReadStation] : [])]),
427
+ shellReadCheck: onPinnedRead(input.personRows, [...(shellReadStation !== undefined ? [{ check: shellReadStation.check, tools: shellReadTools }] : []), ...(fileReadStation !== undefined ? [fileReadStation] : [])]),
417
428
  shellReadStationArmed: shellReadStation !== undefined,
418
429
  shellReadAdmission: shellReadStation?.admit,
430
+ readAdmission: fileReadStation?.admitted,
419
431
  resolvedReadFace,
420
432
  resolvedWriteFace,
421
433
  handsLessResolvedWriteFace,
@@ -428,6 +440,8 @@ export async function prepareHandsMount(input) {
428
440
  artifactStoreRef,
429
441
  attachmentRootCanonical,
430
442
  readDenyMatcher,
443
+ readRoots,
444
+ pathTables,
431
445
  envHandToolNames,
432
446
  shellGatedTools,
433
447
  sealReadStateSeat,