@sema-agent/core 7.49.5 → 7.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/CHANGELOG.md +138 -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 +19 -13
  21. package/dist/core/gate-lanes.js +12 -11
  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/prepare-acquire-reconcile.js +8 -1
  81. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  82. package/dist/core/runner/prepare-artifact.js +2 -8
  83. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  84. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  85. package/dist/core/runner/prepare-context-lane.js +2 -2
  86. package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
  87. package/dist/core/runner/prepare-gate-stations.js +1 -1
  88. package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
  89. package/dist/core/runner/prepare-hands-readface.js +57 -43
  90. package/dist/core/runner/prepare-lsp.d.ts +15 -13
  91. package/dist/core/runner/prepare-lsp.js +11 -11
  92. package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
  93. package/dist/core/runner/prepare-policy-chain.js +6 -7
  94. package/dist/core/runner/prepare-run-refs.js +1 -1
  95. package/dist/core/runner/prepare-task.js +24 -16
  96. package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
  97. package/dist/core/runner/prepare-turn-wiring.js +19 -8
  98. package/dist/core/runner/read-root-grant.d.ts +1 -25
  99. package/dist/core/runner/read-root-grant.js +1 -17
  100. package/dist/core/runner/retired-runner-deps.d.ts +2 -2
  101. package/dist/core/runner/run-brain-sinks.d.ts +1 -5
  102. package/dist/core/runner/run-brain-sinks.js +2 -4
  103. package/dist/core/runner/run-harness-handlers.js +5 -4
  104. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  105. package/dist/core/runner/run-identity-wiring.js +10 -1
  106. package/dist/core/runner/run-leg.d.ts +1 -1
  107. package/dist/core/runner/run-leg.js +22 -9
  108. package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
  109. package/dist/core/runner/run-settle-and-teardown.js +7 -4
  110. package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
  111. package/dist/core/runner/run-terminal-adoption.js +1 -1
  112. package/dist/core/runner/run-turn-boundary.js +2 -2
  113. package/dist/core/runner/runtask.js +8 -8
  114. package/dist/core/runner/session-file-state-replay.d.ts +14 -4
  115. package/dist/core/runner/session-file-state-replay.js +8 -0
  116. package/dist/core/runner/stream-steer-verb.js +3 -0
  117. package/dist/core/runner/tool-end-body.d.ts +5 -4
  118. package/dist/core/runner/tool-end-body.js +4 -4
  119. package/dist/core/runner/tool-output-projection.d.ts +6 -4
  120. package/dist/core/runner/tool-output-projection.js +66 -47
  121. package/dist/core/runner-deps.d.ts +6 -3
  122. package/dist/core/shared-memory/types.d.ts +2 -2
  123. package/dist/core/shell-read-silence.d.ts +23 -0
  124. package/dist/core/shell-read-silence.js +18 -0
  125. package/dist/core/synthetic-read-verdict.d.ts +80 -21
  126. package/dist/core/synthetic-read-verdict.js +89 -18
  127. package/dist/core/task-event.d.ts +113 -25
  128. package/dist/core/task-spec.d.ts +44 -0
  129. package/dist/core/task-tool-shape.d.ts +1 -1
  130. package/dist/core/tool-catalog-entries.js +16 -2
  131. package/dist/core/tool-policy.d.ts +25 -15
  132. package/dist/core/tool-policy.js +9 -13
  133. package/dist/core/trace.d.ts +3 -2
  134. package/dist/core/types.d.ts +3 -3
  135. package/dist/core/types.js +1 -1
  136. package/dist/core/untrusted-text.d.ts +2 -2
  137. package/dist/core/user-attachment.d.ts +164 -0
  138. package/dist/core/user-attachment.js +437 -0
  139. package/dist/engine/compaction/compaction.js +10 -3
  140. package/dist/engine/compaction/utils.js +6 -0
  141. package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
  142. package/dist/engine/execution-env/node-execution-env.js +125 -0
  143. package/dist/engine/harness/agent-harness.d.ts +12 -2
  144. package/dist/engine/harness/agent-harness.js +15 -3
  145. package/dist/engine/harness/types.d.ts +21 -0
  146. package/dist/engine/llm/content-block-render.d.ts +15 -0
  147. package/dist/engine/llm/content-block-render.js +10 -0
  148. package/dist/engine/llm/index.d.ts +1 -0
  149. package/dist/engine/llm/index.js +1 -0
  150. package/dist/engine/llm/types.d.ts +80 -4
  151. package/dist/engine/llm/types.js +43 -1
  152. package/dist/engine/session/import-validate.js +2 -0
  153. package/dist/index.d.ts +6 -4
  154. package/dist/index.js +2 -1
  155. package/dist/internal/harness-types.d.ts +1 -1
  156. package/dist/internal/llm.d.ts +2 -2
  157. package/dist/internal/llm.js +1 -1
  158. package/dist/orchestration/workflow.d.ts +4 -5
  159. package/dist/orchestration/workflow.js +2 -2
  160. package/dist/prompts/default.d.ts +1 -1
  161. package/dist/tools/artifact/artifact-tool.js +1 -1
  162. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  163. package/dist/tools/fs/dir-entries.d.ts +4 -10
  164. package/dist/tools/fs/dir-entries.js +4 -11
  165. package/dist/tools/fs/file-read-check.js +3 -3
  166. package/dist/tools/fs/fs-pdf.js +2 -4
  167. package/dist/tools/fs/fs-read.d.ts +47 -4
  168. package/dist/tools/fs/fs-read.js +63 -19
  169. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  170. package/dist/tools/fs/fs-search-tools.js +7 -2
  171. package/dist/tools/fs/fs-write.d.ts +1 -1
  172. package/dist/tools/fs/fs-write.js +7 -6
  173. package/dist/tools/fs/index.d.ts +7 -1
  174. package/dist/tools/fs/index.js +56 -34
  175. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  176. package/dist/tools/fs/read-deny-home.js +32 -22
  177. package/dist/tools/fs/read-deny.d.ts +14 -0
  178. package/dist/tools/fs/read-deny.js +8 -0
  179. package/dist/tools/fs/repo-map.d.ts +3 -2
  180. package/dist/tools/fs/repo-map.js +11 -3
  181. package/dist/tools/fs/safety.d.ts +41 -2
  182. package/dist/tools/fs/safety.js +12 -3
  183. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  184. package/dist/tools/fs/shell-read-check.js +28 -15
  185. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  186. package/dist/tools/fs/shell-read-judge.d.ts +9 -4
  187. package/dist/tools/fs/shell-read-judge.js +1 -1
  188. package/dist/tools/loop-tick.d.ts +2 -2
  189. package/package.json +3 -1
  190. package/test/export-surface.snapshot.json +39 -1
  191. package/wire-changes/core@7.50.0.json +1699 -0
  192. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  193. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -436,11 +436,10 @@ export interface RunWorkflowOptions {
436
436
  parentCwd?: string;
437
437
  /**
438
438
  * The SPAWNING tool call's id (`run_workflow`'s own `ToolExecuteContext.toolCallId`) — threaded into every
439
- * spawned agent's trusted internals (`RunInternals.parentToolCallId`) so each child runs AS a sub-agent:
440
- * its per-turn `task_progress` ticks actually EMIT (the runner gates them on `parentToolCallId`) and carry
441
- * this id for attribution, the same contract as a `createSubagentTool` delegation. Set by the `run_workflow`
442
- * tool wiring; a directly-started workflow (no launching tool call) leaves it unset — children then emit no
443
- * progress ticks (status quo), and no id is ever fabricated.
439
+ * spawned agent's trusted internals (`RunInternals.parentToolCallId`) so each child's frames carry this id
440
+ * for attribution, the same contract as a `createSubagentTool` delegation. Set by the `run_workflow` tool
441
+ * wiring; a directly-started workflow (no launching tool call) leaves it unset — its children's row frames
442
+ * (start, ticks, terminal tick) still go out, without the attribution, and no id is ever fabricated.
444
443
  */
445
444
  parentToolCallId?: string;
446
445
  /** The HOST task's taskId — threaded as every spawned agent's `RunInternals.parentTaskId` so its
@@ -1648,7 +1648,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1648
1648
  const runSpec = await reviewSpawnBeforeLaunch("ctx.agent", label, callKey, preReviewSpec, effectiveSignal);
1649
1649
  const enrichedForward = opts.onForwardEvent !== undefined
1650
1650
  ? (e) => {
1651
- opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
1651
+ opts.onForwardEvent(e.type === "task_progress" || e.type === "task_started" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
1652
1652
  }
1653
1653
  : undefined;
1654
1654
  const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd, callerRoot: opts.parentCwd } : {}), ...spawnAttribution, ...(await captureFloorSeatsNow()), ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
@@ -2013,7 +2013,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2013
2013
  const runSpec = await reviewSpawnBeforeLaunch("ctx.agentStream", label, callKey, preReviewSpec, effectiveSignal);
2014
2014
  const enrichedForwardS = opts.onForwardEvent !== undefined
2015
2015
  ? (e) => {
2016
- opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
2016
+ opts.onForwardEvent(e.type === "task_progress" || e.type === "task_started" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
2017
2017
  }
2018
2018
  : undefined;
2019
2019
  const runInternals = {
@@ -255,7 +255,7 @@ export declare const PROJECT_CONTEXT_FRAMING = "# Project context\nThe `<user_me
255
255
  * relation to the specific tool results or user messages in which they appear."
256
256
  * - LEAN (`wMy` @596743, heading **`# Harness`**) → "`<system-reminder>` tags in messages and tool
257
257
  * results are injected by the harness, not the user."
258
- * - THIRD ARM (`itp`'s `otp(model)` short-circuit → `gMy` @597111 (cli250.js:44459)): "The system may send updates,
258
+ * - THIRD ARM (`itp`'s `otp(model)` short-circuit → `gMy` @597111 (cli250.js:44486)): "The system may send updates,
259
259
  * reminders, or modifications to rules via mid-conversation system turns. These are
260
260
  * system-controlled, unlike function results." sema has NO counterpart and does not need one — the
261
261
  * arm is selected by a per-model latch (`otp` @597208) over CC's own model registry, which a BYOM
@@ -321,7 +321,7 @@ export function createArtifactTool(opts) {
321
321
  if (sharedReadApproved.has(url))
322
322
  return Promise.resolve({ ok: true });
323
323
  if (permissionMode !== undefined) {
324
- const verdict = arbitrateByMode(permissionMode(), classifyAsk({ action: "ask", requiresRealApproval: true, guard: "artifact_shared_read" }, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }));
324
+ const verdict = arbitrateByMode(permissionMode(), classifyAsk({ action: "ask", requiresRealApproval: true, guards: ["artifact_shared_read"] }, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }));
325
325
  if (verdict.kind === "released") {
326
326
  if (!releaseAnnounced && onNotice !== undefined) {
327
327
  releaseAnnounced = true;
@@ -19,7 +19,7 @@ import { type ProgramRow } from "./program-read-model.js";
19
19
  * auto-allow an irreversible clock jump on the full shell face).
20
20
  *
21
21
  * Rows 3-4 are the CC-anchored expansion — the members of CC's safe-command
22
- * set (pretty223.js:420513-420565 `e6s`; identical in the 220 corpus; the 220→245 campaign diff records
22
+ * set (2.1.296 `v3e` (cli296.js:249645); `e6s` in the 223 corpus, identical in 220; the 220→245 campaign diff records
23
23
  * no change on this face) that satisfy THIS list's curation bar (no write/mutation mode under any args)
24
24
  * without new stdin-floor/option-value modeling: pure status/computation printers (`cal`…`sleep` — none
25
25
  * reads stdin when bare, none takes a path it READS except realpath/readlink, which stay boundary-judged)
@@ -22,7 +22,7 @@
22
22
  * judging was cut short by an abort — the listing is a prefix, not the directory).
23
23
  */
24
24
  import type { ExecutionEnv } from "../../internal/harness-types.js";
25
- import type { FileError, FileKind, Result } from "../../internal/harness.js";
25
+ import type { FileError, FileInfo, FileKind, Result } from "../../internal/harness.js";
26
26
  import { type HomeViewEnv, type HomeViewsCarrier } from "./read-deny-home.js";
27
27
  /** The upstream at-mention's cap on listed entries. */
28
28
  export declare const DIRECTORY_LISTING_MAX_ENTRIES = 1000;
@@ -55,15 +55,9 @@ export interface DirectoryListing {
55
55
  /** An abort cut the judging short: `entries` / `more` / `omitted` describe a prefix of the directory. */
56
56
  incomplete: boolean;
57
57
  }
58
- /** List one level of `dir` (a canonical directory path). A listing the environment refuses is the error. */
59
- export declare function enumerateDirectory(env: ExecutionEnv, dir: string, judge: EntryDenyJudge | undefined, signal?: AbortSignal): Promise<Result<DirectoryListing, FileError>>;
60
- /**
61
- * A data label (a path, a file name) as a JSON string literal with every C0/C1 control, format character
62
- * and line / paragraph separator written as `\uXXXX` — lossless (JSON.parse gives the original back), and
63
- * one line whatever the name holds. JSON's own escaping alone leaves U+2028 / U+2029 / U+0085 and the
64
- * format characters raw.
65
- */
66
- export declare function quoteDataLabel(label: string): string;
58
+ /** List one level of `dir` (a canonical directory path). A listing the environment refuses is the error. `list` is how the
59
+ * entries are read (default: the environment's `listDir`; a carrier bound to a judged directory passes its bound listing). */
60
+ export declare function enumerateDirectory(env: ExecutionEnv, dir: string, judge: EntryDenyJudge | undefined, signal?: AbortSignal, list?: (dir: string) => Promise<Result<FileInfo[], FileError>>): Promise<Result<DirectoryListing, FileError>>;
67
61
  /** The listing as text: one entry per line (a directory with a trailing "/"; a name that could break or
68
62
  * hide in its line is written as {@link quoteDataLabel}), then "… and N more entries" when capped. */
69
63
  export declare function renderDirectoryListing(l: DirectoryListing): string;
@@ -1,4 +1,5 @@
1
1
  import { homeViewsOf } from "./read-deny-home.js";
2
+ import { DATA_LABEL_UNSAFE, quoteDataLabel } from "../../internal/llm.js";
2
3
  export const DIRECTORY_LISTING_MAX_ENTRIES = 1000;
3
4
  export function createEntryDenyJudge(env, deny, signal) {
4
5
  const views = homeViewsOf(env, deny, signal);
@@ -10,8 +11,8 @@ export function createEntryDenyJudge(env, deny, signal) {
10
11
  return view !== undefined ? deny.matchPath(view) : null;
11
12
  };
12
13
  }
13
- export async function enumerateDirectory(env, dir, judge, signal) {
14
- const listing = await env.listDir(dir, signal);
14
+ export async function enumerateDirectory(env, dir, judge, signal, list = (d) => env.listDir(d, signal)) {
15
+ const listing = await list(dir);
15
16
  if (!listing.ok)
16
17
  return listing;
17
18
  const infos = [...listing.value].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -44,16 +45,8 @@ export async function enumerateDirectory(env, dir, judge, signal) {
44
45
  },
45
46
  };
46
47
  }
47
- const LABEL_UNSAFE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
48
- export function quoteDataLabel(label) {
49
- let out = "";
50
- for (const ch of JSON.stringify(label)) {
51
- out += LABEL_UNSAFE.test(ch) ? `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}` : ch;
52
- }
53
- return out;
54
- }
55
48
  export function renderDirectoryListing(l) {
56
- const lines = l.entries.map((e) => `${LABEL_UNSAFE.test(e.name) ? quoteDataLabel(e.name) : e.name}${e.kind === "directory" ? "/" : ""}`);
49
+ const lines = l.entries.map((e) => `${DATA_LABEL_UNSAFE.test(e.name) ? quoteDataLabel(e.name) : e.name}${e.kind === "directory" ? "/" : ""}`);
57
50
  if (l.more > 0)
58
51
  lines.push(`… and ${l.more} more entries`);
59
52
  return lines.join("\n");
@@ -1,6 +1,6 @@
1
1
  import { effectivePathSpellingOf } from "../../core/effective-path-target.js";
2
2
  import { pathTargetOf } from "../../core/tool-registry.js";
3
- import { fitMiddle, READ_DENY_SENTENCE_PART_MAX } from "./read-deny.js";
3
+ import { fitMiddle, READ_DENY_SENTENCE_PART_MAX, seatForCall } from "./read-deny.js";
4
4
  import { resolveKey, violationDetails } from "./safety.js";
5
5
  import { readStationMemberOf } from "./shell-read-check.js";
6
6
  import { grantOfferFor, parentDirLexically } from "./shell-read-judge.js";
@@ -27,8 +27,8 @@ export function createFileReadStation(opts) {
27
27
  return null;
28
28
  const spelled = selection.spelled;
29
29
  return (async () => {
30
- const extra = opts.roots();
31
- const r = await resolveKey(opts.env, opts.rootCanonical, spelled, signal, selection.base, extra, undefined, opts.readDeny, opts.readFace);
30
+ const extra = seatForCall(opts.roots, callOpts.toolCallId)();
31
+ const r = await resolveKey(opts.env, opts.rootCanonical, spelled, signal, selection.base, extra, undefined, seatForCall(opts.readDeny, callOpts.toolCallId), opts.readFace);
32
32
  if (r.ok || r.violation.code !== "path_not_in_root" || r.violation.target === undefined || opts.excluded?.(r.violation.target) === true || opts.exempt?.(r.violation.target) === true)
33
33
  return VOUCH;
34
34
  const target = r.violation.target;
@@ -3,13 +3,14 @@ import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
3
3
  import { PDF_FALLBACK_RENDER_PAGES, PDF_INLINE_PAGE_THRESHOLD, PDF_MAX_EXTRACT_SIZE, PDF_MAX_PAGES_PER_READ, PDF_TARGET_RAW_SIZE, extractPdfPagesAsImages, extractPdfTextLayer, getPdfPageCount, parsePdfPageRange, pdfMagicMatches, } from "./pdf.js";
4
4
  import { resolveAutoDownsampler, enoentMessage, MAX_READ_BYTES } from "./fs-shared.js";
5
5
  import { planReminderDisclosure, renderReminderDisclosureTrailer } from "../../core/reminder-disclosure.js";
6
+ import { MARKED_BLOCK_AFTER_DATA } from "../../core/reminder-mint.js";
6
7
  function pdfDisclosureTrailer(body, d) {
7
8
  if (d === undefined)
8
9
  return "";
9
10
  const plan = planReminderDisclosure({ segments: [body], mark: d.mark, outlet: "pdf", defuseExactMark: false, throttle: { key: d.key, windows: d.windows } });
10
11
  d.effects.push(plan.effects);
11
12
  const trailer = renderReminderDisclosureTrailer(plan.verdict, d.mark);
12
- return trailer !== undefined ? `\n${trailer}` : "";
13
+ return trailer !== undefined ? `${MARKED_BLOCK_AFTER_DATA}${trailer}` : "";
13
14
  }
14
15
  export { pdfModelCapabilitiesOf } from "./pdf.js";
15
16
  async function pdfPagesToImageBlocks(pages, path, downsamplerOpt) {
@@ -47,9 +48,6 @@ export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt,
47
48
  return `Error (Read): ${await enoentMessage(env, key, cwd, signal, readDeny)}`;
48
49
  return `Error (Read): cannot stat PDF "${path}" to verify its size before reading: ${meta.error.message}`;
49
50
  }
50
- if (meta.value.kind === "directory") {
51
- return `Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`;
52
- }
53
51
  const size = preRead ? preRead.byteLength : meta.value.size;
54
52
  if (size > PDF_MAX_EXTRACT_SIZE) {
55
53
  return `Error (Read): PDF "${path}" exceeds the maximum size for PDF reading (${size} bytes > ${PDF_MAX_EXTRACT_SIZE}-byte / 100 MB cap).`;
@@ -7,6 +7,8 @@ import { type ImageDimensions } from "../../core/mcp.js";
7
7
  import { type PdfModelCapabilities } from "./pdf.js";
8
8
  import { type ReadImageDownsamplerOption, type CwdRef } from "./fs-shared.js";
9
9
  import { type ReminderDisclosureCounts, type ReminderDisclosureEffects, type ReminderDisclosureVerdict } from "../../core/reminder-disclosure.js";
10
+ import { type DirectoryListing } from "./dir-entries.js";
11
+ import { FileError, type BoundFileObject, type Result } from "../../internal/harness.js";
10
12
  /**
11
13
  * The Read tool, and the ONE read of a file for the model it is built on, split into three steps so every
12
14
  * carrier shares it:
@@ -42,8 +44,9 @@ import { type ReminderDisclosureCounts, type ReminderDisclosureEffects, type Rem
42
44
  export declare const READ_OUTCOMES: readonly ["attached", "partial", "unchanged", "not_attached"];
43
45
  export type ReadOutcome = (typeof READ_OUTCOMES)[number];
44
46
  /** The lane the read took. Absent on a refusal raised before the lane was known. A corrupt notebook served
45
- * as raw text is `text` (with the `notebook_fallback` note). */
46
- export type ReadKind = "text" | "notebook" | "image" | "pdf";
47
+ * as raw text is `text` (with the `notebook_fallback` note). `directory`: the path is a directory — judged in one
48
+ * place, before the lanes; the Read tool refuses it, a carrier that lists directories reads the kind. */
49
+ export type ReadKind = "text" | "notebook" | "image" | "pdf" | "directory";
47
50
  /** The engine's statements about one read, as data. The Read carrier's rendering is
48
51
  * {@link renderReadNoteForTool}; the frame carrier renders the same members its own way. */
49
52
  export type ReadNote = {
@@ -105,6 +108,12 @@ export type ReadNote = {
105
108
  | {
106
109
  kind: "disclosure";
107
110
  verdict: ReminderDisclosureVerdict;
111
+ }
112
+ /** A PDF read as a reference only (a bound read, {@link ReadForModelOptions.bound}): its size and page count, when known. */
113
+ | {
114
+ kind: "pdf_reference";
115
+ bytes?: number;
116
+ pages?: number;
108
117
  };
109
118
  /** The read-state entry a delivery records, minus the delivery's own facts (when, which transmission). */
110
119
  export interface ReadObservation {
@@ -136,6 +145,17 @@ export interface ReadForModelContext {
136
145
  taskId?: string;
137
146
  principal?: string;
138
147
  }
148
+ /** How a carrier other than the Read tool asks for the read. Absent members ⇒ exactly the Read tool's read. */
149
+ export interface ReadForModelOptions {
150
+ /** A BOUND read: the canonical key the carrier resolved and the object it holds open there ({@link ModelFileReader.openBound}),
151
+ * the one it had judged. A path that resolves to another key is refused; the kind, the size and every byte come from the
152
+ * held object, never from the path. A PDF is a reference only: its size from the held object, its page count from its
153
+ * bytes up to the whole-send size (`pdf_reference` note). */
154
+ bound?: {
155
+ key: string;
156
+ object: BoundFileObject;
157
+ };
158
+ }
139
159
  export interface ModelFileReaderOptions {
140
160
  env: ExecutionEnv;
141
161
  state: ReadFileState;
@@ -159,7 +179,30 @@ export interface ModelFileReaderOptions {
159
179
  disclosureWindows?: Map<string, number>;
160
180
  }
161
181
  export interface ModelFileReader {
162
- readForModel(args: unknown, ctx: ReadForModelContext): Promise<ReadForModelResult>;
182
+ readForModel(args: unknown, ctx: ReadForModelContext, opts?: ReadForModelOptions): Promise<ReadForModelResult>;
183
+ /** The path's resolution alone — the same step `readForModel` opens with (the read-deny table, the roots, the device
184
+ * and UNC refusals): its canonical key, or the Read tool's refusal. */
185
+ resolveTarget(path: string, ctx: ReadForModelContext): Promise<{
186
+ ok: true;
187
+ key: string;
188
+ } | {
189
+ ok: false;
190
+ refusal: ReadRefusal;
191
+ }>;
192
+ /** Open the object at a resolved key and hold it, for a carrier that judges and then reads that same object (the
193
+ * environment's `openBound`; an environment without it cannot bind, and the answer says so). The carrier closes it. */
194
+ openBound(key: string, signal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
195
+ /** One level of a held directory, every entry through the walk's per-entry read-deny judge. */
196
+ listDirectory(bound: {
197
+ key: string;
198
+ object: BoundFileObject;
199
+ }, signal?: AbortSignal): Promise<Result<DirectoryListing, FileError>>;
200
+ /** Plan the disclosure over external text this reader did not read (the same pipeline, throttle windows and
201
+ * counters); the effects commit with the result that carries them. */
202
+ planDisclosure(segments: string[], throttleKey: string): {
203
+ note?: ReadNote;
204
+ effects: ReminderDisclosureEffects;
205
+ };
163
206
  /** Apply the result's effects: the read-state entry (stamped `lastReadAt` and the transmission that
164
207
  * carried it) and the disclosure windows / counters. Once per result: a second commit throws. */
165
208
  commit(result: ReadForModelResult, at?: {
@@ -178,4 +221,4 @@ export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileSta
178
221
  taskId?: string;
179
222
  principal?: string;
180
223
  toolCallId?: string;
181
- }) => boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
224
+ }) => boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts, sharedReader?: ModelFileReader): AgentTool;
@@ -4,17 +4,22 @@ import { sha256, resolveKey, violationText, violationDetails, binaryMagicFormat,
4
4
  import { decodeTextBytes } from "./encoding.js";
5
5
  import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
6
6
  import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
7
- import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
7
+ import { countPdfPagesFromBytes, PDF_MAX_PAGES_PER_READ, PDF_TARGET_RAW_SIZE, pdfMagicMatches } from "./pdf.js";
8
8
  import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
9
9
  import { readPdfFile } from "./fs-pdf.js";
10
- import { openSystemReminder } from "../../core/reminder-mint.js";
10
+ import { seatForCall, seatReady } from "./read-deny.js";
11
+ import { thrownText } from "../../core/thrown-value.js";
12
+ import { MARKED_BLOCK_AFTER_DATA, openSystemReminder } from "../../core/reminder-mint.js";
11
13
  import { planReminderDisclosure, commitReminderDisclosureEffects, renderReminderDisclosureTrailer, } from "../../core/reminder-disclosure.js";
12
14
  import { toolFace } from "../../core/tool-catalog-entries.js";
13
15
  import { admitImageBytes, imageReadCap } from "./image-admission.js";
16
+ import { createEntryDenyJudge, enumerateDirectory } from "./dir-entries.js";
17
+ import { FileError } from "../../internal/harness.js";
14
18
  export const READ_OUTCOMES = Object.freeze(["attached", "partial", "unchanged", "not_attached"]);
19
+ const UNBINDABLE = "this environment cannot bind a read to the file that was checked, so it was not read";
15
20
  const NO_EFFECTS = () => ({ disclosure: [] });
16
21
  export function createModelFileReader(opts) {
17
- const { env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readDeny, readFace, reminderMark } = opts;
22
+ const { env, state, rootCanonical, cwdRef, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readFace, reminderMark, readDeny: readDenySeat, additionalRoots: rootsSeat } = opts;
18
23
  const windows = opts.disclosureWindows ?? new Map();
19
24
  const refuse = (envelope, kind) => ({
20
25
  outcome: "not_attached",
@@ -29,7 +34,13 @@ export function createModelFileReader(opts) {
29
34
  const verdict = plan.verdict;
30
35
  return { ...(verdict.reminder !== undefined || verdict.envelopeTags !== undefined ? { note: { kind: "disclosure", verdict } } : {}), effects: plan.effects };
31
36
  };
32
- const readForModel = async (args, ctx) => {
37
+ const resolve = async (path, ctx) => {
38
+ if (ctx.toolCallId === undefined)
39
+ await seatReady(rootsSeat, ctx.signal);
40
+ return resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, seatForCall(rootsSeat, ctx.toolCallId), bgOutputReadExemption === undefined ? undefined : (key) => bgOutputReadExemption(key, ctx), seatForCall(readDenySeat, ctx.toolCallId), readFace);
41
+ };
42
+ const readForModel = async (args, ctx, opts) => {
43
+ const readDeny = seatForCall(readDenySeat, ctx.toolCallId);
33
44
  const { offset, limit, pages } = (args ?? {});
34
45
  const path = fileArgPath(args);
35
46
  if (path === undefined)
@@ -42,29 +53,35 @@ export function createModelFileReader(opts) {
42
53
  details: { type: "file_unchanged", reason, file: { filePath: path, startLine, endLine, totalLines } },
43
54
  effects: NO_EFFECTS(),
44
55
  });
45
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, bgOutputReadExemption === undefined ? undefined : (key) => bgOutputReadExemption(key, ctx), readDeny, readFace);
56
+ const r = await resolve(path, ctx);
46
57
  if (!r.ok)
47
58
  return refuse(errorResult(violationText("Read", r.violation), violationDetails(r.violation)));
59
+ if (opts?.bound !== undefined && r.key !== opts.bound.key) {
60
+ return refuse(errorResult(`Error (Read): "${path.slice(0, 300)}" resolved to a different file while it was being checked, so it was not read.`));
61
+ }
62
+ const bound = opts?.bound;
63
+ const readBytes = (key, laneCap) => bound === undefined ? env.readBinaryFile(key, ctx.signal) : bound.object.readBytes(laneCap, ctx.signal);
64
+ const info = bound === undefined ? await env.fileInfo(r.key, ctx.signal) : await bound.object.size().then((s) => (s.ok ? { ok: true, value: { kind: bound.object.kind, size: s.value } } : s));
65
+ if (info.ok && info.value.kind === "directory") {
66
+ return refuse(errorResult(`Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`), "directory");
67
+ }
48
68
  const isNb = isNotebookPath(r.key);
49
69
  const imageMime = imageMimeForRead(r.key);
50
70
  if (imageMime !== undefined) {
51
- const meta = await env.fileInfo(r.key, ctx.signal);
71
+ const meta = info;
52
72
  if (!meta.ok) {
53
73
  const ex = await env.exists(r.key, ctx.signal);
54
74
  if (ex.ok && !ex.value)
55
75
  return refuse(errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal, readDeny)}`), "image");
56
76
  return refuse(errorResult(`Error (Read): cannot stat image "${path}" to verify its size before reading: ${meta.error.message}`), "image");
57
77
  }
58
- if (meta.value.kind === "directory") {
59
- return refuse(errorResult(`Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`), "image");
60
- }
61
78
  const downsampler = imageDownsampler === false ? undefined : (imageDownsampler ?? (await resolveAutoDownsampler()));
62
79
  const readCap = imageReadCap(downsampler);
63
80
  const tooLargeToRead = (bytes) => `Error (Read): image "${path}" is too large to read (${bytes} bytes > ${readCap}-byte cap).` +
64
81
  (downsampler ? "" : ` ${NO_DOWNSAMPLER_IMAGE_CAP_HINT}`);
65
82
  if (meta.value.size > readCap)
66
83
  return refuse(errorResult(tooLargeToRead(meta.value.size)), "image");
67
- const bin = await env.readBinaryFile(r.key, ctx.signal);
84
+ const bin = await readBytes(r.key, readCap);
68
85
  if (!bin.ok)
69
86
  return refuse(errorResult(`Error (Read): cannot read image "${path}": ${bin.error.message}`), "image");
70
87
  const admitted = await admitImageBytes(bin.value, imageMime, downsampler);
@@ -96,6 +113,25 @@ export function createModelFileReader(opts) {
96
113
  };
97
114
  }
98
115
  const pdfOf = async (preRead) => {
116
+ if (bound !== undefined) {
117
+ if (!info.ok)
118
+ return refuse(errorResult(`Error (Read): cannot size PDF "${path.slice(0, 300)}": ${info.error.message.slice(0, 200)}`), "pdf");
119
+ let held = preRead;
120
+ if (held === undefined && info.value.size <= PDF_TARGET_RAW_SIZE) {
121
+ const got = await readBytes(r.key, PDF_TARGET_RAW_SIZE);
122
+ if (!got.ok)
123
+ return refuse(errorResult(`Error (Read): cannot read PDF "${path.slice(0, 300)}": ${got.error.message.slice(0, 200)}`), "pdf");
124
+ held = got.value;
125
+ }
126
+ const pageCount = held !== undefined ? countPdfPagesFromBytes(held) : undefined;
127
+ return {
128
+ outcome: "attached",
129
+ kind: "pdf",
130
+ data: [],
131
+ notes: [{ kind: "pdf_reference", bytes: info.value.size, ...(pageCount !== undefined ? { pages: pageCount } : {}) }],
132
+ effects: NO_EFFECTS(),
133
+ };
134
+ }
99
135
  const effects = [];
100
136
  const out = await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, preRead, pdfCapabilities, readDeny, {
101
137
  mark: reminderMark,
@@ -113,11 +149,7 @@ export function createModelFileReader(opts) {
113
149
  return pdfOf(undefined);
114
150
  const binaryExt = hasBinaryExtension(r.key);
115
151
  const binaryExtRefusal = () => refuse(errorResult(`Error (Read): "${path}" appears to be a binary file (by extension); this tool reads UTF-8 text only.`), "text");
116
- const info = await env.fileInfo(r.key, ctx.signal);
117
152
  if (info.ok) {
118
- if (info.value.kind === "directory") {
119
- return refuse(errorResult(`Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`), "text");
120
- }
121
153
  if (info.value.size > SLICED_READ_MAX_BYTES) {
122
154
  if (binaryExt)
123
155
  return binaryExtRefusal();
@@ -137,7 +169,7 @@ export function createModelFileReader(opts) {
137
169
  return refuse(errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal, readDeny)}`), "text");
138
170
  return refuse(errorResult(`Error (Read): cannot determine the type of "${path.slice(0, 300)}" (stat failed: ${info.error.message.slice(0, 200)}); refusing to open it — if it is a FIFO/socket/device, reading it would hang. Retry if this was transient, or inspect it with Bash (e.g. \`file\`, \`ls -l\`).`), "text");
139
171
  }
140
- const readBin = await env.readBinaryFile(r.key, ctx.signal);
172
+ const readBin = await readBytes(r.key, !isNb && limit === undefined ? MAX_READ_BYTES : SLICED_READ_MAX_BYTES);
141
173
  if (!readBin.ok)
142
174
  return refuse(errorResult(`Error (Read): cannot read "${path}": ${readBin.error.message}`), "text");
143
175
  const readSize = readBin.value.byteLength;
@@ -302,6 +334,16 @@ export function createModelFileReader(opts) {
302
334
  const committed = new WeakSet();
303
335
  return {
304
336
  readForModel,
337
+ async resolveTarget(path, ctx) {
338
+ const r = await resolve(path, ctx).catch((err) => ({ ok: false, violation: { code: "invalid", message: thrownText(err) } }));
339
+ return r.ok ? { ok: true, key: r.key } : { ok: false, refusal: errorResult(violationText("Read", r.violation), violationDetails(r.violation)) };
340
+ },
341
+ openBound: (key, signal) => (env.openBound !== undefined ? env.openBound(key, signal) : Promise.resolve({ ok: false, error: new FileError("not_supported", UNBINDABLE, key) })),
342
+ listDirectory: (bound, signal) => enumerateDirectory(env, bound.key, readDenySeat !== undefined ? createEntryDenyJudge(env, readDenySeat, signal) : undefined, signal, () => bound.object.list(signal)),
343
+ planDisclosure(segments, throttleKey) {
344
+ const d = disclosureOf(segments, "read", throttleKey);
345
+ return { ...(d.note !== undefined ? { note: d.note } : {}), effects: d.effects };
346
+ },
305
347
  commit(result, at) {
306
348
  if (committed.has(result))
307
349
  throw new Error("ModelFileReader.commit: this read result was already committed — a delivery commits its read once");
@@ -348,6 +390,8 @@ export function renderReadNoteForTool(note, mark) {
348
390
  }
349
391
  case "disclosure":
350
392
  return renderReminderDisclosureTrailer(note.verdict, mark) ?? "";
393
+ case "pdf_reference":
394
+ return `[PDF not read: ${note.bytes !== undefined ? `${note.bytes} bytes` : "size unknown"}, ${note.pages !== undefined ? `${note.pages} page${note.pages === 1 ? "" : "s"}` : "page count unknown"}. Read it with the pages parameter, at most ${PDF_MAX_PAGES_PER_READ} pages per request.]`;
351
395
  }
352
396
  }
353
397
  export function renderReadToolResult(r, mark) {
@@ -373,7 +417,7 @@ export function renderReadToolResult(r, mark) {
373
417
  return {
374
418
  content: [
375
419
  ...(noCells !== undefined ? [{ type: "text", text: renderReadNoteForTool(noCells, mark) }] : r.data),
376
- ...(trailer !== undefined ? [{ type: "text", text: trailer }] : []),
420
+ ...(trailer !== undefined ? [{ type: "text", text: `${MARKED_BLOCK_AFTER_DATA}${trailer}` }] : []),
377
421
  ],
378
422
  details: r.details,
379
423
  };
@@ -383,14 +427,14 @@ export function renderReadToolResult(r, mark) {
383
427
  const header = noteOf("partial_page") ?? noteOf("partial_chars") ?? noteOf("range");
384
428
  const body = r.data[0]?.text ?? "";
385
429
  return {
386
- content: `${fallback !== undefined ? renderReadNoteForTool(fallback, mark) : ""}${header !== undefined ? renderReadNoteForTool(header, mark) : ""}${body}${trailer !== undefined ? `\n${trailer}` : ""}`,
430
+ content: `${fallback !== undefined ? renderReadNoteForTool(fallback, mark) : ""}${header !== undefined ? renderReadNoteForTool(header, mark) : ""}${body}${trailer !== undefined ? `${MARKED_BLOCK_AFTER_DATA}${trailer}` : ""}`,
387
431
  details: r.details,
388
432
  };
389
433
  }
390
434
  }
391
435
  }
392
- export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readDeny, readFace, reminderMark, reminderDisclosureCounts) {
393
- const reader = createModelFileReader({
436
+ export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readDeny, readFace, reminderMark, reminderDisclosureCounts, sharedReader) {
437
+ const reader = sharedReader ?? createModelFileReader({
394
438
  env,
395
439
  state,
396
440
  rootCanonical,
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import { type RootsSeat } from "./safety.js";
3
- import type { ReadDenyMatcher } from "./read-deny.js";
3
+ import { type ReadDenyMatcher } from "./read-deny.js";
4
4
  import type { ContainmentFace } from "./containment-face.js";
5
5
  import type { CwdRef } from "./fs-shared.js";
6
6
  /**
@@ -12,5 +12,5 @@ import type { CwdRef } from "./fs-shared.js";
12
12
  * honesty boundary.
13
13
  */
14
14
  export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
15
- export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef, readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean): AgentTool;
16
- export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef, readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean): AgentTool;
15
+ export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, rootsSeat?: RootsSeat, denySeat?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef, readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean): AgentTool;
16
+ export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, rootsSeat?: RootsSeat, denySeat?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef, readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean): AgentTool;
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { clipSpelling, pathFamilyOf, resolveKey, violationText, violationDetails, WIN32_NAMESPACE_REFUSAL_TEXT } from "./safety.js";
4
4
  import { runGrepDetailed, runGlobDetailed, invalidGlobTokens } from "./search.js";
5
+ import { seatForCall } from "./read-deny.js";
5
6
  import { toolFace } from "../../core/tool-catalog-entries.js";
6
7
  import { pathTargetBaseOf, splitAbsoluteGlobPattern } from "../../core/effective-path-target.js";
7
8
  import { pathTargetValue } from "../../core/tool-registry.js";
@@ -74,7 +75,7 @@ export function grepDetailFields(text, mode, offset, structuredRows) {
74
75
  }
75
76
  return detailFields;
76
77
  }
77
- export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef, readAdmission) {
78
+ export function createGrepTool(env, rootCanonical, rootsSeat, denySeat, readFace, cwdRef, readAdmission) {
78
79
  const declaredTarget = toolFace("grep").pathTarget;
79
80
  return defineTool({
80
81
  name: "Grep",
@@ -112,6 +113,8 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
112
113
  offset: Type.Optional(Type.Number({ description: "Skip the first N output lines (pagination partner of head_limit)." })),
113
114
  }),
114
115
  execute: async (args, ctx) => {
116
+ const readDeny = seatForCall(denySeat, ctx.toolCallId);
117
+ const additionalRoots = seatForCall(rootsSeat, ctx.toolCallId);
115
118
  const retired = args;
116
119
  for (const [name, repair] of [
117
120
  ["regex", "the `pattern` is ALWAYS evaluated as a regular expression (ripgrep semantics) — drop the key; to match text literally, escape the regex metacharacters in `pattern`"],
@@ -177,7 +180,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
177
180
  },
178
181
  });
179
182
  }
180
- export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef, readAdmission) {
183
+ export function createGlobTool(env, rootCanonical, rootsSeat, denySeat, readFace, cwdRef, readAdmission) {
181
184
  const declaredTarget = toolFace("glob").pathTarget;
182
185
  return defineTool({
183
186
  name: "Glob",
@@ -201,6 +204,8 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
201
204
  max_results: Type.Optional(Type.Number({ description: "Cap results (default 500)." })),
202
205
  }),
203
206
  execute: async (args, ctx) => {
207
+ const readDeny = seatForCall(denySeat, ctx.toolCallId);
208
+ const additionalRoots = seatForCall(rootsSeat, ctx.toolCallId);
204
209
  let pattern = args.pattern;
205
210
  let path = declaredTarget === undefined ? undefined : pathTargetValue(args, declaredTarget);
206
211
  const { max_results } = args;
@@ -2,7 +2,7 @@ import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { BeforeWriteHook, FileEditedHook, TrackFileEditHook } from "../../core/types.js";
3
3
  import { type ReadFileState } from "./safety.js";
4
4
  import type { ContainmentFace } from "./containment-face.js";
5
- import type { WriteDenyMatcher } from "./read-deny.js";
5
+ import { type WriteDenyMatcher } from "./read-deny.js";
6
6
  import { type CwdRef } from "./fs-shared.js";
7
7
  /**
8
8
  * The `beforeWrite` CONTENT hook, mounted here rather than in ToolPolicy because only this band sees
@@ -3,6 +3,7 @@ import { Type } from "typebox";
3
3
  import { defineTool, errorResult } from "../../core/tools.js";
4
4
  import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, READ_REFUSED_ESCAPE_HINT, } from "./safety.js";
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
+ import { seatForCall } from "./read-deny.js";
6
7
  import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, coerceWriteArguments, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
8
  import { toolFace } from "../../core/tool-catalog-entries.js";
8
9
  import { thrownText } from "../../core/thrown-value.js";
@@ -112,7 +113,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
112
113
  const path = fileArgPath(args);
113
114
  if (path === undefined)
114
115
  return { ok: true };
115
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
116
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, seatForCall(writeDeny, ctx.toolCallId), writeFace?.());
116
117
  if (!r.ok)
117
118
  return { ok: true };
118
119
  if (a.old_string === a.new_string)
@@ -150,7 +151,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
150
151
  const path = fileArgPath(args);
151
152
  if (path === undefined)
152
153
  return errorResult(`Error (Edit): file_path is required.`);
153
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
154
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, seatForCall(writeDeny, ctx.toolCallId), writeFace?.());
154
155
  if (!r.ok)
155
156
  return errorResult(violationText("Edit", r.violation), violationDetails(r.violation));
156
157
  if (!batch && a.old_string === a.new_string) {
@@ -344,7 +345,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
344
345
  return { ok: true };
345
346
  if (ipynbRedirect("Write", path))
346
347
  return { ok: true };
347
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
348
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, seatForCall(writeDeny, ctx.toolCallId), writeFace?.());
348
349
  if (!r.ok)
349
350
  return { ok: true };
350
351
  const exists = await env.exists(r.key, ctx.signal);
@@ -363,7 +364,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
363
364
  const ipynb = ipynbRedirect("Write", path);
364
365
  if (ipynb)
365
366
  return errorResult(ipynb);
366
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
367
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, seatForCall(writeDeny, ctx.toolCallId), writeFace?.());
367
368
  if (!r.ok)
368
369
  return errorResult(violationText("Write", r.violation), violationDetails(r.violation));
369
370
  const exists = await env.exists(r.key, ctx.signal);
@@ -460,7 +461,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
460
461
  return { ok: false, code: "invalid", message: NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE };
461
462
  if (mode !== "insert" && !a.cell_id)
462
463
  return { ok: false, code: "invalid", message: NOTEBOOK_CELL_ID_REQUIRED_MESSAGE };
463
- const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
464
+ const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, seatForCall(writeDeny, ctx.toolCallId), writeFace?.());
464
465
  if (!r.ok)
465
466
  return { ok: true };
466
467
  const notRead = requireRead(state, r.key);
@@ -480,7 +481,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
480
481
  return errorResult(NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE);
481
482
  if (mode !== "insert" && !cell_id)
482
483
  return errorResult(NOTEBOOK_CELL_ID_REQUIRED_MESSAGE);
483
- const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
484
+ const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, seatForCall(writeDeny, ctx.toolCallId), writeFace?.());
484
485
  if (!r.ok)
485
486
  return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
486
487
  const notRead = requireRead(state, r.key);
@@ -57,7 +57,7 @@ export interface HandsToolkitOptions {
57
57
  * (`createShellReadStation`). */
58
58
  shellReadAdmission?: ShellReadAdmission;
59
59
  /** The gate's file read station's ADMISSION (`FileReadStation.admitted`, file-read-check.ts), handed to Read / Grep /
60
- * Glob: an out-of-root target the gate put to a question and then let through is read once, on the canonical key the
60
+ * Glob / RepoMap: an out-of-root target the gate put to a question and then let through is read once, on the canonical key the
61
61
  * station judged. ABSENT ⇒ no station (a library-direct toolkit): the tools refuse every out-of-root target. */
62
62
  readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean;
63
63
  /** Commit-message attribution trailer for the `bash` git protocol. Default NONE — attribution is a
@@ -245,6 +245,12 @@ export interface HandsToolkitOptions {
245
245
  * mount may pass its own object or omit it (counting off — and the whole disclosure pipeline
246
246
  * is off anyway when `reminderMark` is absent). */
247
247
  reminderDisclosureCounts?: import("../../core/reminder-disclosure.js").ReminderDisclosureCounts;
248
+ /** Where the band hands out the leg's ONE file reader — the reader its Read tool reads through, with the same read
249
+ * state, throttle windows and counters — for the run's other carriers of a file to the model (a user's attachment).
250
+ * Written once at mount; absent ⇒ the reader stays the Read tool's own. */
251
+ modelFileReaderSeat?: {
252
+ current?: import("./fs-read.js").ModelFileReader;
253
+ };
248
254
  }
249
255
  /** Whether the band's WRITE half (Edit / Write / NotebookEdit, and the full `bash`) mounts for these options — the ONE
250
256
  * predicate {@link createHandsToolkit} branches on and prepare-task plans by (the memory phase must know, before the