@sema-agent/core 7.49.4 → 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 (200) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/dist/agents/observer.js +3 -10
  3. package/dist/agents/peer-directory.d.ts +8 -3
  4. package/dist/agents/peer-directory.js +5 -3
  5. package/dist/agents/subagent-steps.d.ts +6 -14
  6. package/dist/agents/subagent-steps.js +3 -17
  7. package/dist/brain/stream-shared.d.ts +1 -1
  8. package/dist/core/allow-entry.d.ts +1 -1
  9. package/dist/core/allow-entry.js +1 -1
  10. package/dist/core/ask-origin.d.ts +15 -11
  11. package/dist/core/ask-origin.js +6 -14
  12. package/dist/core/auto-mode-prompt.js +6 -2
  13. package/dist/core/checkpoint-store.d.ts +6 -4
  14. package/dist/core/checkpoint-store.js +1 -1
  15. package/dist/core/context-edit.d.ts +2 -2
  16. package/dist/core/decision-receipt.d.ts +2 -2
  17. package/dist/core/decision-receipt.js +30 -11
  18. package/dist/core/delegation-frames.d.ts +66 -18
  19. package/dist/core/delegation-frames.js +22 -0
  20. package/dist/core/engine-notice.d.ts +7 -5
  21. package/dist/core/frame-settlement.d.ts +6 -1
  22. package/dist/core/gate-fold.js +19 -13
  23. package/dist/core/gate-lanes.js +12 -11
  24. package/dist/core/gate-outcome.d.ts +2 -2
  25. package/dist/core/governance-codes.js +2 -0
  26. package/dist/core/hooks.d.ts +28 -20
  27. package/dist/core/human-input-projection.js +2 -0
  28. package/dist/core/lsp-diagnostics.d.ts +3 -0
  29. package/dist/core/lsp-diagnostics.js +6 -0
  30. package/dist/core/lsp-session.d.ts +39 -17
  31. package/dist/core/lsp-session.js +118 -58
  32. package/dist/core/lsp.d.ts +83 -3
  33. package/dist/core/lsp.js +146 -16
  34. package/dist/core/mcp.js +2 -1
  35. package/dist/core/media-byte-cap.d.ts +14 -0
  36. package/dist/core/media-byte-cap.js +15 -3
  37. package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
  38. package/dist/core/memory-engine/consolidation-driver.js +54 -6
  39. package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
  40. package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
  41. package/dist/core/memory-engine/consolidation.d.ts +17 -0
  42. package/dist/core/memory-engine/consolidation.js +1 -0
  43. package/dist/core/memory-engine/distiller.d.ts +8 -2
  44. package/dist/core/memory-engine/distiller.js +8 -6
  45. package/dist/core/memory-engine/engine.d.ts +37 -4
  46. package/dist/core/memory-engine/engine.js +127 -38
  47. package/dist/core/memory-engine/file-backend.d.ts +8 -3
  48. package/dist/core/memory-engine/file-backend.js +44 -16
  49. package/dist/core/memory-engine/index.d.ts +2 -2
  50. package/dist/core/memory-engine/index.js +1 -1
  51. package/dist/core/memory-engine/landing-admission.d.ts +65 -0
  52. package/dist/core/memory-engine/landing-admission.js +68 -0
  53. package/dist/core/memory-engine/types.d.ts +23 -0
  54. package/dist/core/mode-policy.d.ts +3 -2
  55. package/dist/core/mode-policy.js +2 -0
  56. package/dist/core/permission-rule-model.d.ts +19 -13
  57. package/dist/core/permission-rule-model.js +7 -4
  58. package/dist/core/permission-rule-provider.d.ts +11 -0
  59. package/dist/core/permission-rules.d.ts +1 -1
  60. package/dist/core/permission-rules.js +5 -2
  61. package/dist/core/persisted-rule-arms.d.ts +15 -7
  62. package/dist/core/persisted-rule-arms.js +13 -6
  63. package/dist/core/posix-mode-face.d.ts +10 -0
  64. package/dist/core/posix-mode-face.js +3 -0
  65. package/dist/core/reminder-mint.d.ts +5 -0
  66. package/dist/core/reminder-mint.js +1 -0
  67. package/dist/core/retired-keys.d.ts +8 -0
  68. package/dist/core/retired-keys.js +4 -0
  69. package/dist/core/rule-lane-reads.d.ts +4 -4
  70. package/dist/core/rule-lane-reads.js +10 -1
  71. package/dist/core/runner/construction-doors.d.ts +10 -4
  72. package/dist/core/runner/construction-doors.js +25 -2
  73. package/dist/core/runner/contracts.d.ts +20 -3
  74. package/dist/core/runner/env-capability-census.js +1 -1
  75. package/dist/core/runner/gate-exit.js +2 -1
  76. package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
  77. package/dist/core/runner/memory-consolidation-driver.js +5 -2
  78. package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
  79. package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
  80. package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
  81. package/dist/core/runner/permission-rule-lanes.js +13 -43
  82. package/dist/core/runner/person-path-rows.d.ts +244 -0
  83. package/dist/core/runner/person-path-rows.js +387 -0
  84. package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
  85. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  86. package/dist/core/runner/prepare-artifact.js +2 -8
  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 +6 -7
  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 +127 -1
  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/stores/file/permission-rule-store.d.ts +13 -1
  166. package/dist/stores/file/permission-rule-store.js +40 -21
  167. package/dist/tools/artifact/artifact-tool.js +1 -1
  168. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  169. package/dist/tools/fs/dir-entries.d.ts +4 -10
  170. package/dist/tools/fs/dir-entries.js +4 -11
  171. package/dist/tools/fs/file-read-check.js +3 -3
  172. package/dist/tools/fs/fs-pdf.js +2 -4
  173. package/dist/tools/fs/fs-read.d.ts +47 -4
  174. package/dist/tools/fs/fs-read.js +63 -19
  175. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  176. package/dist/tools/fs/fs-search-tools.js +7 -2
  177. package/dist/tools/fs/fs-write.d.ts +1 -1
  178. package/dist/tools/fs/fs-write.js +7 -6
  179. package/dist/tools/fs/index.d.ts +7 -1
  180. package/dist/tools/fs/index.js +56 -34
  181. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  182. package/dist/tools/fs/read-deny-home.js +32 -22
  183. package/dist/tools/fs/read-deny.d.ts +14 -0
  184. package/dist/tools/fs/read-deny.js +8 -0
  185. package/dist/tools/fs/repo-map.d.ts +3 -2
  186. package/dist/tools/fs/repo-map.js +11 -3
  187. package/dist/tools/fs/safety.d.ts +41 -2
  188. package/dist/tools/fs/safety.js +12 -3
  189. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  190. package/dist/tools/fs/shell-read-check.js +28 -15
  191. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  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.49.5.json +104 -0
  198. package/wire-changes/core@7.50.0.json +1699 -0
  199. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  200. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -1 +1 @@
1
- export { createAssistantMessageEventStream, ensureToolTurnInvariants, MODEL_OUTPUT_INVALID_CAUSES, snapshotActorAssertion, stampStreamDrops, streamDropsOf, stripEngineMetadata, TOOL_TURN_INCOMPLETE_CAUSES, TOOL_TURN_REPAIR_DROPS } from "../engine/llm/index.js";
1
+ export { createAssistantMessageEventStream, DATA_LABEL_UNSAFE, ensureToolTurnInvariants, isTextBlock, MODEL_OUTPUT_INVALID_CAUSES, quoteDataLabel, renderContentBlock, renderUserAttachmentView, snapshotActorAssertion, stampStreamDrops, streamDropsOf, stripEngineMetadata, TOOL_TURN_INCOMPLETE_CAUSES, TOOL_TURN_REPAIR_DROPS, userAttachmentOf } from "../engine/llm/index.js";
@@ -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
@@ -113,15 +113,23 @@ export type RuleAdoptionFile = {
113
113
  export declare function readRuleAdoptionFile(dir: string): RuleAdoptionFile | undefined;
114
114
  /** Atomically publish the adoption marker (same write-temp → fsync → rename discipline as the store). */
115
115
  export declare function writeRuleAdoptionFile(dir: string, content: RuleAdoptionFile): void;
116
+ /** What a directory vetting reads from its host. Absent ⇒ this process's platform, and a skipped judgment goes unsaid. */
117
+ interface DirVetting {
118
+ /** The platform whose permission bits are read (default: this process's — see {@link posixModeBitsMeaningful}). */
119
+ readonly platform?: NodeJS.Platform;
120
+ /** Said when the group/other-writable judgment was NOT made because the platform reports no permission bits. */
121
+ readonly modeBitsUnjudged?: (dir: string) => void;
122
+ }
116
123
  /** One principal's rule file. */
117
124
  declare class FileDurableRulePartition implements WritableDurableRulePartition {
118
125
  private readonly dir;
119
126
  private readonly file;
120
127
  private readonly acquireWriteLock;
121
128
  private readonly onError?;
129
+ private readonly vetting?;
122
130
  readonly durability: StoreDurability;
123
131
  readonly fidelity: StoreFidelity;
124
- constructor(dir: string, file: string, acquireWriteLock: () => void, onError?: ((message: string) => void) | undefined);
132
+ constructor(dir: string, file: string, acquireWriteLock: () => void, onError?: ((message: string) => void) | undefined, vetting?: DirVetting | undefined);
125
133
  /** Read and validate. Any refusal answers with an empty set AND says so — never a silent empty store. The answer's
126
134
  * sentence names no location ({@link refusalText}); the disclosure that accompanies it does. */
127
135
  private read;
@@ -198,9 +206,13 @@ export declare class FileDurableRulePartitionProvider implements DurableRulePart
198
206
  private readonly onError?;
199
207
  private lock;
200
208
  private orgPersistence;
209
+ /** What every directory vetting of this instance reads; it says an unjudged mode check ONCE per instance. */
210
+ private readonly vetting;
201
211
  constructor(dir: string, onError?: ((message: string) => void) | undefined, opts?: {
202
212
  /** The data root whose adoption marker gates construction (design/183 I6). Absent ⇒ no root gate. */
203
213
  dataRoot?: string;
214
+ /** The platform whose permission bits the directory vetting reads. Absent ⇒ this process's platform. */
215
+ platform?: NodeJS.Platform;
204
216
  });
205
217
  /** The org partition's durable home in this directory (design/182 §7.4 contract; `org.json`). Shares
206
218
  * this provider's writer lock — an install is a write. */
@@ -2,6 +2,7 @@ import { closeSync, constants as FS, existsSync, fsyncSync, lstatSync, mkdirSync
2
2
  import { join } from "node:path";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { isRuleBehavior } from "../../core/permission-rule-model.js";
5
+ import { posixModeBitsMeaningful } from "../../core/posix-mode-face.js";
5
6
  import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, unknownRuleWriteDeltaError, } from "../../core/permission-rule-store.js";
6
7
  import { canonicalize } from "../../core/canonical-json.js";
7
8
  import { BootLock, FileStoreLockError } from "./fs-atomic.js";
@@ -118,13 +119,17 @@ const EMPTY_READ = { rules: [], tombstones: [], rev: 0 };
118
119
  function checksumOf(body) {
119
120
  return `sha256:${createHash("sha256").update(canonicalize(body), "utf8").digest("hex")}`;
120
121
  }
121
- function unsafeDirFault(dir) {
122
+ function unsafeDirFault(dir, vetting) {
122
123
  mkdirSync(dir, { recursive: true, mode: 0o700 });
123
124
  const st = lstatSync(dir);
124
125
  if (st.isSymbolicLink() || !st.isDirectory())
125
126
  return "is a symlink or not a directory";
126
127
  if (typeof process.getuid === "function" && st.uid !== process.getuid())
127
128
  return "is owned by another user";
129
+ if (!posixModeBitsMeaningful(vetting?.platform)) {
130
+ vetting?.modeBitsUnjudged?.(dir);
131
+ return undefined;
132
+ }
128
133
  if ((st.mode & 0o022) !== 0)
129
134
  return `is writable by group or others (mode ${(st.mode & 0o777).toString(8)}) — anyone who can write there can grant themselves permissions`;
130
135
  return undefined;
@@ -135,15 +140,18 @@ function assertSafeDir(dir) {
135
140
  throw new Error(`permission-rule store directory ${dir} ${fault}; refusing to use it`);
136
141
  }
137
142
  function refusalText(onError, sentence, location) {
143
+ discloseTo(onError, `${sentence} (at ${location})`);
144
+ return `${sentence}. The location is reported to the store's error disclosure (onError), not in this message.`;
145
+ }
146
+ function discloseTo(onError, message) {
138
147
  try {
139
- onError?.(`${sentence} (at ${location})`);
148
+ onError?.(message);
140
149
  }
141
150
  catch {
142
151
  }
143
- return `${sentence}. The location is reported to the store's error disclosure (onError), not in this message.`;
144
152
  }
145
- function assertSafeDirAtRequest(dir, onError, act) {
146
- const fault = unsafeDirFault(dir);
153
+ function assertSafeDirAtRequest(dir, onError, act, vetting) {
154
+ const fault = unsafeDirFault(dir, vetting);
147
155
  if (fault !== undefined)
148
156
  throw new Error(refusalText(onError, `the permission-rule store directory ${fault}; refusing to ${act} it`, dir));
149
157
  }
@@ -171,13 +179,15 @@ class FileDurableRulePartition {
171
179
  file;
172
180
  acquireWriteLock;
173
181
  onError;
182
+ vetting;
174
183
  durability = "durable";
175
184
  fidelity = "json";
176
- constructor(dir, file, acquireWriteLock, onError) {
185
+ constructor(dir, file, acquireWriteLock, onError, vetting) {
177
186
  this.dir = dir;
178
187
  this.file = file;
179
188
  this.acquireWriteLock = acquireWriteLock;
180
189
  this.onError = onError;
190
+ this.vetting = vetting;
181
191
  }
182
192
  read() {
183
193
  const bucket = "this owner's permission-rule bucket";
@@ -185,7 +195,7 @@ class FileDurableRulePartition {
185
195
  const unreadable = (sentence, location = this.file) => ({ unreadable: refusalText(this.onError, sentence, location) });
186
196
  let raw;
187
197
  try {
188
- const dirFault = unsafeDirFault(this.dir);
198
+ const dirFault = unsafeDirFault(this.dir, this.vetting);
189
199
  if (dirFault !== undefined)
190
200
  return unreadable(`the permission-rule store directory ${dirFault}; loading zero rules`, this.dir);
191
201
  const st = lstatSync(this.file);
@@ -228,11 +238,7 @@ class FileDurableRulePartition {
228
238
  return { file: parsed };
229
239
  }
230
240
  disclose(message) {
231
- try {
232
- this.onError?.(message);
233
- }
234
- catch {
235
- }
241
+ discloseTo(this.onError, message);
236
242
  }
237
243
  screenDurableRows(file) {
238
244
  const isSession = (scope) => scope?.kind === "session";
@@ -246,7 +252,7 @@ class FileDurableRulePartition {
246
252
  return { rules, tombstones, quarantined };
247
253
  }
248
254
  write(next) {
249
- assertSafeDirAtRequest(this.dir, this.onError, "write through");
255
+ assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting);
250
256
  atomicPublish(this.dir, this.file, JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2));
251
257
  }
252
258
  async list() {
@@ -392,11 +398,22 @@ export class FileDurableRulePartitionProvider {
392
398
  onError;
393
399
  lock;
394
400
  orgPersistence;
401
+ vetting;
395
402
  constructor(dir, onError, opts) {
396
403
  this.dir = dir;
397
404
  this.onError = onError;
398
405
  if (opts?.dataRoot !== undefined)
399
406
  assertAdoptionBootGate(opts.dataRoot, "FileDurableRulePartitionProvider");
407
+ let said = false;
408
+ this.vetting = {
409
+ platform: opts?.platform,
410
+ modeBitsUnjudged: (where) => {
411
+ if (said)
412
+ return;
413
+ said = true;
414
+ discloseTo(this.onError, `the permission-rule store directory is not checked for being writable by other accounts: this platform reports no POSIX permission bits, so who else can write there is not verified here (at ${where})`);
415
+ },
416
+ };
400
417
  }
401
418
  adoptionMarker() {
402
419
  const judged = judgeRuleAdoptionFile(this.dir);
@@ -410,13 +427,13 @@ export class FileDurableRulePartitionProvider {
410
427
  return this.fileOrgPersistence();
411
428
  }
412
429
  fileOrgPersistence() {
413
- this.orgPersistence ??= new FileOrgRuleStatePersistence(this.dir, this.acquireWriteLock, this.onError);
430
+ this.orgPersistence ??= new FileOrgRuleStatePersistence(this.dir, this.acquireWriteLock, this.onError, this.vetting);
414
431
  return this.orgPersistence;
415
432
  }
416
433
  acquireWriteLock = () => {
417
434
  if (this.lock !== undefined)
418
435
  return;
419
- assertSafeDirAtRequest(this.dir, this.onError, "write through");
436
+ assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting);
420
437
  const lock = new BootLock(join(this.dir, "PERMISSION-RULES-WRITER-LOCK"));
421
438
  try {
422
439
  lock.acquire();
@@ -437,11 +454,11 @@ export class FileDurableRulePartitionProvider {
437
454
  return this.readOnlySourceView(principalRuleFileName(principal), principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4);
438
455
  }
439
456
  }
440
- return new FileDurableRulePartition(this.dir, join(this.dir, principalRuleFileName(principal)), this.acquireWriteLock, this.onError);
457
+ return new FileDurableRulePartition(this.dir, join(this.dir, principalRuleFileName(principal)), this.acquireWriteLock, this.onError, this.vetting);
441
458
  }
442
459
  readOnlySourceView(sourceName, targetName, flipped) {
443
460
  const pick = flipped ? targetName : existsSync(join(this.dir, sourceName)) ? sourceName : targetName;
444
- return readOnlyRuleStoreView(new FileDurableRulePartition(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError));
461
+ return readOnlyRuleStoreView(new FileDurableRulePartition(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError, this.vetting));
445
462
  }
446
463
  forLocalOwner() {
447
464
  const adoption = this.adoptionMarker();
@@ -451,7 +468,7 @@ export class FileDurableRulePartitionProvider {
451
468
  return this.readOnlySourceView(LOCAL_OWNER_FILE, principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4);
452
469
  }
453
470
  }
454
- return new FileDurableRulePartition(this.dir, join(this.dir, LOCAL_OWNER_FILE), this.acquireWriteLock, this.onError);
471
+ return new FileDurableRulePartition(this.dir, join(this.dir, LOCAL_OWNER_FILE), this.acquireWriteLock, this.onError, this.vetting);
455
472
  }
456
473
  dispose() {
457
474
  this.lock?.release();
@@ -462,12 +479,14 @@ class FileOrgRuleStatePersistence {
462
479
  dir;
463
480
  acquireWriteLock;
464
481
  onError;
482
+ vetting;
465
483
  file;
466
484
  writeChain = Promise.resolve();
467
- constructor(dir, acquireWriteLock, onError) {
485
+ constructor(dir, acquireWriteLock, onError, vetting) {
468
486
  this.dir = dir;
469
487
  this.acquireWriteLock = acquireWriteLock;
470
488
  this.onError = onError;
489
+ this.vetting = vetting;
471
490
  this.file = join(dir, ORG_STATE_FILE);
472
491
  }
473
492
  readSync() {
@@ -477,7 +496,7 @@ class FileOrgRuleStatePersistence {
477
496
  const ioFault = (err) => `could not be read (${err?.code ?? err?.name ?? "unknown"})`;
478
497
  let dirFault;
479
498
  try {
480
- dirFault = unsafeDirFault(this.dir);
499
+ dirFault = unsafeDirFault(this.dir, this.vetting);
481
500
  }
482
501
  catch (err) {
483
502
  return refuse(`the permission-rule store directory ${ioFault(err)}`, `${this.dir}: ${thrownText(err)}`);
@@ -524,7 +543,7 @@ class FileOrgRuleStatePersistence {
524
543
  }
525
544
  installOrgStateSync(state) {
526
545
  this.acquireWriteLock();
527
- assertSafeDirAtRequest(this.dir, this.onError, "write through");
546
+ assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting);
528
547
  state = jsonForm(state);
529
548
  const cur = this.readSync();
530
549
  if (cur !== undefined && state.revisionHighWater < cur.revisionHighWater) {
@@ -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;