@sema-agent/core 7.53.0 → 7.54.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 (244) hide show
  1. package/CHANGELOG.md +277 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -4
  104. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +639 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/repo-utils.d.ts +5 -0
  195. package/dist/engine/session/repo-utils.js +9 -0
  196. package/dist/engine/session/storage-base.js +2 -2
  197. package/dist/index.d.ts +23 -9
  198. package/dist/index.js +19 -8
  199. package/dist/internal/llm.d.ts +1 -1
  200. package/dist/internal/llm.js +1 -1
  201. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  202. package/dist/orchestration/run-workflow-tool.js +1 -0
  203. package/dist/orchestration/workflow-governance.d.ts +1 -1
  204. package/dist/orchestration/workflow.d.ts +3 -0
  205. package/dist/orchestration/workflow.js +2 -1
  206. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  207. package/dist/prompt-assembly/catalog/schema.js +3 -0
  208. package/dist/prompt-assembly/overlay.js +2 -2
  209. package/dist/prompt-assembly/select.js +2 -2
  210. package/dist/prompts/default.d.ts +1 -1
  211. package/dist/shell/lex.d.ts +87 -8
  212. package/dist/shell/lex.js +283 -49
  213. package/dist/stores/file/index.d.ts +8 -6
  214. package/dist/tools/artifact/artifact-tool.js +10 -0
  215. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  216. package/dist/tools/fs/bash-program-position.js +3 -10
  217. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  218. package/dist/tools/fs/fs-bash.d.ts +3 -3
  219. package/dist/tools/fs/fs-bash.js +72 -101
  220. package/dist/tools/fs/fs-read.d.ts +12 -0
  221. package/dist/tools/fs/fs-read.js +10 -9
  222. package/dist/tools/fs/fs-shared.d.ts +27 -31
  223. package/dist/tools/fs/fs-shared.js +12 -15
  224. package/dist/tools/fs/fs-write.js +1 -1
  225. package/dist/tools/fs/index.js +2 -2
  226. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  227. package/dist/tools/fs/read-deny-home.js +5 -4
  228. package/dist/tools/fs/read-deny.d.ts +16 -3
  229. package/dist/tools/fs/read-deny.js +12 -9
  230. package/dist/tools/fs/safety.d.ts +7 -6
  231. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  232. package/dist/tools/fs/shell-read-bindings.js +15 -16
  233. package/dist/tools/fs/shell-read-device.js +1 -1
  234. package/dist/tools/fs/shell-read-plan.js +36 -47
  235. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  236. package/dist/tools/fs/shell-read-word.js +2 -20
  237. package/dist/tools/fs/shell-reentry-text.js +1 -1
  238. package/dist/tools/web.d.ts +2 -2
  239. package/package.json +4 -1
  240. package/test/export-surface.snapshot.json +5654 -5520
  241. package/wire-changes/core@7.53.0.json +30 -8
  242. package/wire-changes/core@7.54.0.json +2870 -0
  243. package/dist/core/shell-scan.d.ts +0 -60
  244. package/dist/core/shell-scan.js +0 -183
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The device lane's cross-boundary WIRING for one leg: the tree's shared state ref, the session record that carries the state from
3
+ * one leg of a session to the next, and the gate seat whose ONE obligation reading the station and both exits share.
4
+ *
5
+ * WHERE THE STATE LIVES. The tree's state is one object, reached from the tree's opaque token (`RunInternals.crossBoundaryTree`,
6
+ * `ToolExecuteContext.crossBoundaryTreeForChildren`) through this module alone; each leg joins into it what it knows at prepare (its
7
+ * environment's declaration, its session's newest record, its posture) and every bit an exit sets lands there before the call
8
+ * runs. Each leg's session holds a record (`CROSS_BOUNDARY_RECORD_TYPE`) of the state its content was produced under — written only
9
+ * for an armed tree: at prepare and at an exit (before the call runs) when the state grew past what the session already says, and at
10
+ * a leg's end (before a suspend's row is put, when the leg settles) also when conversation content follows the newest record. A
11
+ * write that fails refuses its call (or its row) on a governed tree; a prepare's or a settle's, and any write on a tree that is not
12
+ * governed, mints `config.cross_boundary_record_unwritten`. A record covers the content before it: a leg that takes over a session
13
+ * whose content no record covers (none on the branch, or after the newest), on an armed tree, reads that content as off-target.
14
+ *
15
+ * THE OBLIGATION of a call is read off three things at the moment it is asked ({@link crossBoundaryObligation}): the legs the
16
+ * call travels (its roster row's declaration, or what the dispatching code gives per call), whether the environment the `env`
17
+ * leg reaches declares itself the target NOW, and the tree's state NOW.
18
+ */
19
+ import type { AgentTool } from "../../internal/harness.js";
20
+ import type { SessionTreeEntry } from "../../internal/harness-types.js";
21
+ import { type CrossBoundaryCheck, type CrossBoundaryPosture, type CrossBoundaryTreeToken } from "../cross-boundary-taint.js";
22
+ import { type ToolRoster } from "../tool-roster.js";
23
+ import type { RunnerDeps } from "../types.js";
24
+ /** A new tree with no bit set, and the token that names it — the one mint of a token. */
25
+ export declare function mintCrossBoundaryTree(): CrossBoundaryTreeToken;
26
+ /** The tree a leg joins: the one its handed token names, or a new one (a root leg) — synchronous, so the phases that hand the token
27
+ * down never wait on the session. A token the engine did not mint names no tree it can read: reported, and the leg's tree reads as
28
+ * the strictest state. */
29
+ export declare function joinedCrossBoundaryTree(handed: CrossBoundaryTreeToken | undefined, report: (message: string) => void): CrossBoundaryTreeToken;
30
+ /** One leg's cross-boundary wiring (see the module header). */
31
+ export interface CrossBoundaryLane {
32
+ /** The tree's token — the one the leg's children are handed. */
33
+ readonly token: CrossBoundaryTreeToken;
34
+ /** The station and the exits are ENGAGED on this leg: a posture is declared, the leg runs on a target, or its tree is governed —
35
+ * each fixed at prepare, so no live consumer flips between two readings mid-run. */
36
+ readonly stationEngaged: boolean;
37
+ /** The gate seat, minted once the leg's roster and tool instances exist; undefined on a leg whose station is not engaged. */
38
+ seat(view: CrossBoundaryLegView): CrossBoundaryCheck | undefined;
39
+ /** The seat minted by {@link seat}, for the resumed call's exit. */
40
+ readonly check: CrossBoundaryCheck | undefined;
41
+ /** Write the record the tree's state owes this leg's session: the new entry's id, or undefined when nothing was owed (or, on a tree
42
+ * that is not governed, the write failed and was reported); throws when it cannot be written on a governed tree. With `atLeaf` (a
43
+ * suspend: a leg's end), the record also covers the content after the newest one and is written only as the child of that leaf
44
+ * (the row then points at it). */
45
+ owe(atLeaf?: string): Promise<string | undefined>;
46
+ /** The tree is armed, so the session OWES the tree's record and a parked row says a reader must read it — on a tree that is not
47
+ * governed a failed write leaves the record missing (the notice says so), and the reader then reads the content after the newest
48
+ * record as off-target. */
49
+ recorded(): boolean;
50
+ /** The leg settles: the record that covers what it added is written, a failure reported loudly (the next leg then reads the
51
+ * content after the newest record as off-target). */
52
+ settle(): Promise<void>;
53
+ /** How many times this leg's record writes have waited on the session store — a station that judged before an exit reads again
54
+ * when the count moved across it (the exit's own form: judged again after a wait). */
55
+ storeWaits(): number;
56
+ }
57
+ /** What the seat reads per call: the live roster (the legs a row declares) and the mounted instances (a `"per_call"` resolver). */
58
+ export interface CrossBoundaryLegView {
59
+ readonly tools: readonly AgentTool[];
60
+ readonly roster: {
61
+ readonly current: ToolRoster | undefined;
62
+ };
63
+ }
64
+ export interface CrossBoundaryLaneInput {
65
+ /** The tree this leg joins ({@link joinedCrossBoundaryTree}), or none — a new tree. */
66
+ token?: CrossBoundaryTreeToken;
67
+ /** The posture door's reading (`undefined` = undeclared). */
68
+ posture: CrossBoundaryPosture | undefined;
69
+ /** The run fold: this leg's environment declares the target, or the row it resumes was minted on one. */
70
+ externalContentTargetActive: boolean;
71
+ /** The leg's own environment — read for its CURRENT declaration at each judgment. */
72
+ env: {
73
+ readonly externalContentTarget?: boolean;
74
+ };
75
+ session: {
76
+ getBranch(): Promise<ReadonlyArray<SessionTreeEntry>>;
77
+ getLeafId(): Promise<string | null>;
78
+ appendCustomEntry(customType: string, data?: unknown): Promise<string>;
79
+ };
80
+ resumeLeg: boolean;
81
+ sessionId: string;
82
+ runId: string;
83
+ hostTaskId: string;
84
+ deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer">;
85
+ }
86
+ /** The leg's lane: the tree ref joined with what this leg knows, the session's record read and (when the state grew) written. */
87
+ export declare function prepareCrossBoundaryLane(input: CrossBoundaryLaneInput): Promise<CrossBoundaryLane>;
@@ -0,0 +1,190 @@
1
+ import { BOUNDARY_LEGS, CROSS_BOUNDARY_RECORD_TYPE, CROSS_BOUNDARY_TAINT_BITS, STRICTEST_CROSS_BOUNDARY_TREE, crossBoundaryObligation, crossBoundaryPostureUndeclaredNotice, crossBoundaryRecordUnwrittenNotice, crossBoundaryRecordData, crossBoundaryTreeCovers, crossBoundaryTreeGoverned, joinCrossBoundaryTree, readCrossBoundaryRecord, } from "../cross-boundary-taint.js";
2
+ import { deliverEngineNotice } from "../engine-notice.js";
3
+ import { policyCannotAnswer } from "../real-approval.js";
4
+ import { thrownText } from "../thrown-value.js";
5
+ import { rosterEntryNamed } from "../tool-roster.js";
6
+ import { emitTrace } from "../trace.js";
7
+ const TREE_STATE = new WeakMap();
8
+ export function mintCrossBoundaryTree() {
9
+ const token = Object.freeze(Object.create(null));
10
+ TREE_STATE.set(token, { current: Object.freeze({}) });
11
+ return token;
12
+ }
13
+ export function joinedCrossBoundaryTree(handed, report) {
14
+ if (handed !== undefined && TREE_STATE.has(handed))
15
+ return handed;
16
+ const token = mintCrossBoundaryTree();
17
+ if (handed !== undefined) {
18
+ report("a cross-boundary tree token the engine did not mint was handed to this leg — it reads its tree as armed, observed and governed");
19
+ TREE_STATE.get(token).current = STRICTEST_CROSS_BOUNDARY_TREE;
20
+ }
21
+ return token;
22
+ }
23
+ const EVERY_LEG = Object.freeze([...BOUNDARY_LEGS]);
24
+ const bitsOf = (t) => CROSS_BOUNDARY_TAINT_BITS.filter((b) => t[b] === true);
25
+ function legsOfCall(view, legEnv, token, call, report) {
26
+ const roster = view.roster.current;
27
+ const row = roster !== undefined ? rosterEntryNamed(roster, call.toolName) : undefined;
28
+ if (row === undefined)
29
+ return { legs: EVERY_LEG, env: undefined };
30
+ if (row.boundaryLegs !== "per_call")
31
+ return { legs: row.boundaryLegs, env: legEnv };
32
+ const strictest = (why) => (report(`tool "${row.name}" declares its boundary legs per call, and its boundaryLegsFor ${why} — the call reads as every leg, off the target (the strictest reading)`), { legs: EVERY_LEG, env: undefined });
33
+ const resolve = view.tools.find((t) => t.name === row.name)?.boundaryLegsFor;
34
+ if (typeof resolve !== "function")
35
+ return strictest("is missing");
36
+ let given;
37
+ try {
38
+ given = resolve(call.args, { tree: token });
39
+ }
40
+ catch (err) {
41
+ return strictest(`threw (${thrownText(err)})`);
42
+ }
43
+ const { legs, env } = (typeof given === "object" && given !== null ? given : {});
44
+ if (!Array.isArray(legs) || !legs.every((l) => BOUNDARY_LEGS.some((b) => b === l)) || (env !== undefined && (typeof env !== "object" || env === null)))
45
+ return strictest("answered outside its type ({ legs: a list of boundary legs, env?: an environment })");
46
+ return { legs: legs, env: env ?? legEnv };
47
+ }
48
+ export async function prepareCrossBoundaryLane(input) {
49
+ const { posture, externalContentTargetActive, env, session, resumeLeg, sessionId, runId, hostTaskId, deps } = input;
50
+ const report = (message) => deps.onError?.(new Error(message), { phase: "config", sessionId });
51
+ const token = input.token ?? mintCrossBoundaryTree();
52
+ const ref = TREE_STATE.get(token);
53
+ if (ref === undefined)
54
+ throw new TypeError("prepareCrossBoundaryLane: the token names no tree (resolve it with joinedCrossBoundaryTree)");
55
+ let branch;
56
+ try {
57
+ branch = await session.getBranch();
58
+ }
59
+ catch (err) {
60
+ report(`the session's cross-boundary record could not be read (${thrownText(err)}) — this leg reads its tree as armed, observed and governed`);
61
+ }
62
+ const read = branch !== undefined ? readCrossBoundaryRecord(branch) : undefined;
63
+ if (read?.kind === "malformed")
64
+ report(`${read.why} — this leg reads its tree as armed, observed and governed`);
65
+ const fromRecord = read === undefined ? STRICTEST_CROSS_BOUNDARY_TREE : read.kind === "absent" ? {} : read.tree;
66
+ let recorded = read?.kind === "recorded" ? read.tree : {};
67
+ const own = { ...(externalContentTargetActive ? { targetExecutionArmed: true } : {}), ...(posture === "multi_tenant" ? { governed: true } : {}) };
68
+ let tree = joinCrossBoundaryTree(joinCrossBoundaryTree(ref.current, fromRecord), own);
69
+ if (tree.targetExecutionArmed === true && read !== undefined && read.kind !== "malformed" && read.uncoveredContent)
70
+ tree = joinCrossBoundaryTree(tree, { offTargetReadObserved: true });
71
+ ref.current = tree;
72
+ const stationEngaged = posture !== undefined || externalContentTargetActive || crossBoundaryTreeGoverned(tree);
73
+ const unwritten = (phase, err) => deliverEngineNotice(deps.onNotice, crossBoundaryRecordUnwrittenNotice({ sessionId, runId, phase, governed: crossBoundaryTreeGoverned(ref.current), cause: thrownText(err) }));
74
+ let writing = Promise.resolve();
75
+ let storeWaits = 0;
76
+ const write = (phase, atLeaf) => {
77
+ const next = writing.then(async () => {
78
+ let leafAt = atLeaf;
79
+ let cover = phase === "suspend" || phase === "settle";
80
+ let written;
81
+ for (;;) {
82
+ const state = ref.current;
83
+ if (state.targetExecutionArmed !== true)
84
+ return written;
85
+ const grown = !crossBoundaryTreeCovers(recorded, state);
86
+ if (!grown && !cover)
87
+ return written;
88
+ try {
89
+ if (!grown) {
90
+ storeWaits++;
91
+ const now = readCrossBoundaryRecord((await session.getBranch()));
92
+ if (now.kind !== "malformed" && !now.uncoveredContent) {
93
+ cover = false;
94
+ continue;
95
+ }
96
+ }
97
+ if (leafAt !== undefined) {
98
+ storeWaits++;
99
+ if ((await session.getLeafId()) !== leafAt)
100
+ throw new Error("the session moved past the row's leaf before its record could be written");
101
+ }
102
+ storeWaits++;
103
+ const entryId = await session.appendCustomEntry(CROSS_BOUNDARY_RECORD_TYPE, crossBoundaryRecordData(state));
104
+ if (typeof entryId !== "string" || entryId.length === 0)
105
+ throw new Error(`the session store answered the record's append with no entry id (${typeof entryId})`);
106
+ if (leafAt !== undefined && (storeWaits++, await session.getLeafId()) !== entryId)
107
+ throw new Error("the record the session store appended is not the session's new leaf");
108
+ recorded = state;
109
+ written = entryId;
110
+ if (leafAt !== undefined)
111
+ leafAt = entryId;
112
+ cover = false;
113
+ }
114
+ catch (err) {
115
+ if (crossBoundaryTreeGoverned(ref.current))
116
+ throw err;
117
+ unwritten(phase, err);
118
+ recorded = state;
119
+ return written;
120
+ }
121
+ }
122
+ });
123
+ writing = next.catch(() => undefined);
124
+ return next;
125
+ };
126
+ const owe = (atLeaf) => write(atLeaf !== undefined ? "suspend" : "exit", atLeaf);
127
+ const trace = (phase, call) => emitTrace(deps.tracer, () => ({ kind: "permission.cross_boundary_taint", version: 1, taskId: hostTaskId, runId, phase, taint: bitsOf(ref.current), governed: ref.current.governed === true, ...(call !== undefined ? { toolName: call.toolName, ...(call.toolCallId !== undefined ? { toolCallId: call.toolCallId } : {}) } : {}), ts: Date.now() }));
128
+ if (tree.targetExecutionArmed === true) {
129
+ trace(resumeLeg ? "resume" : "prepare");
130
+ if (posture === undefined && tree.governed !== true)
131
+ deliverEngineNotice(deps.onNotice, crossBoundaryPostureUndeclaredNotice({ sessionId, runId, tree }));
132
+ await write("prepare").catch((err) => unwritten("prepare", err));
133
+ }
134
+ let check;
135
+ const lane = {
136
+ token,
137
+ stationEngaged,
138
+ get check() {
139
+ return check;
140
+ },
141
+ seat(view) {
142
+ if (!stationEngaged)
143
+ return undefined;
144
+ const obligationOf = (call, say) => {
145
+ const { legs, env: reached } = legsOfCall(view, env, token, call, say);
146
+ try {
147
+ return crossBoundaryObligation(legs, reached?.externalContentTarget === true, ref.current);
148
+ }
149
+ catch (err) {
150
+ say(`the cross-boundary obligation of "${call.toolName}" could not be read (${thrownText(err)}) — the call reads as every leg, off the target`);
151
+ return crossBoundaryObligation(EVERY_LEG, false, ref.current);
152
+ }
153
+ };
154
+ const quiet = () => undefined;
155
+ check = {
156
+ obligation: (call) => obligationOf(call, report),
157
+ async exit(call, decidedBy) {
158
+ for (;;) {
159
+ const now = obligationOf(call, quiet);
160
+ if (now.mandate && policyCannotAnswer({ action: "ask", requiresRealApproval: true }, decidedBy)) {
161
+ return {
162
+ kind: "unconfirmed",
163
+ message: `tool "${call.toolName}" needs a person's confirmation: this run executes on a target device${ref.current.offTargetReadObserved === true ? " and has since let content from beyond it in, so a call that reaches beyond the device or back into it" : ", so a call that writes beyond the device"} is put to a person — this call was allowed by a configuration, not a person, and was not executed`,
164
+ };
165
+ }
166
+ if (now.record && ref.current.offTargetReadObserved !== true) {
167
+ ref.current = joinCrossBoundaryTree(ref.current, { offTargetReadObserved: true });
168
+ trace("observed", call);
169
+ }
170
+ const judged = ref.current;
171
+ try {
172
+ await owe();
173
+ }
174
+ catch (err) {
175
+ return { kind: "unrecorded", message: `tool "${call.toolName}" was not executed: the session's cross-boundary record could not be written before it ran (${thrownText(err)}) — on a governed tree a call that owes its record does not run without it; refused fail-closed` };
176
+ }
177
+ if (ref.current === judged)
178
+ return undefined;
179
+ }
180
+ },
181
+ };
182
+ return check;
183
+ },
184
+ owe,
185
+ storeWaits: () => storeWaits,
186
+ recorded: () => ref.current.targetExecutionArmed === true,
187
+ settle: () => write("settle").then(() => undefined, (err) => unwritten("settle", err)),
188
+ };
189
+ return lane;
190
+ }
@@ -121,9 +121,9 @@ export declare function mintDecideRecord(cp: Checkpoint, facts: HostDecisionFact
121
121
  }): GateOutcome;
122
122
  /**
123
123
  * Capture-time domain check for a decide outcome's TEXT payloads — the review lanes' `editedPlan` /
124
- * `reason` and the approval lane's deny `reason`. All are persisted (the recorded winner's payload,
125
- * compared when an `env_failed` reopen replays it) and all are fenced into the model-facing
126
- * continuation reminder. The declared wire type is a string; a value outside it reads one way in this
124
+ * `reason`, the approval lane's deny `reason` and its allow `feedback`. All are persisted (the recorded winner's payload,
125
+ * compared when an `env_failed` reopen replays it) and all reach the model-facing continuation (the plan and the
126
+ * notes fenced into its reminder, the feedback as the person's own words). The declared wire type is a string; a value outside it reads one way in this
127
127
  * process and another after a JSON round-trip, and reaches the reminder as whatever its stringification
128
128
  * happens to be. Worse, the pre-CAS tag-hygiene check and the fence BOTH reach the value through
129
129
  * `String.prototype.replace`: an object carrying its own `replace` is therefore read twice and may
@@ -133,7 +133,7 @@ export declare function mintDecideRecord(cp: Checkpoint, facts: HostDecisionFact
133
133
  * is named in the message only — `CheckpointError`'s structured `detail.field` is a closed set covering
134
134
  * the approval lane's payload names, and widening it is a wire-surface change of its own.
135
135
  */
136
- export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason"): void;
136
+ export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason" | "feedback"): void;
137
137
  /**
138
138
  * The continuation prompt for a resumed run. The vendored harness only exposes
139
139
  * `prompt(text)` (it always appends a user message — there is no "continue from tool results" entry), so
@@ -8,6 +8,7 @@ import { facetOf, isGateKind, isQuestionGate } from "../pause-registry.js";
8
8
  import { humanRefusalSentence, policyCannotAnswer, screenApproverAttribution } from "../tool-policy.js";
9
9
  import { mintGateOutcome } from "./gate-exit.js";
10
10
  import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
11
+ import { writtenFeedback } from "./approval-feedback.js";
11
12
  import { engineCardOf } from "../tool-roster.js";
12
13
  export function resumeDecisionWasNegative(resume) {
13
14
  const o = resume.outcome;
@@ -52,7 +53,8 @@ export function sameWinner(incoming, persisted) {
52
53
  incoming.decision === persisted.decision &&
53
54
  deepJsonEqual(incoming.updatedInput, persisted.updatedInput) &&
54
55
  deepJsonEqual(incoming.answer, persisted.answer) &&
55
- (persisted.reason === undefined || incoming.reason === persisted.reason));
56
+ (persisted.reason === undefined || incoming.reason === persisted.reason) &&
57
+ (persisted.feedback === undefined || writtenFeedback(incoming.feedback) === writtenFeedback(persisted.feedback)));
56
58
  }
57
59
  export function pendingContentAskCallId(cp) {
58
60
  return cp.pendingAction.kind === "tool_approval" && engineCardOf(cp.pendingAction) === "question" ? cp.pendingAction.toolCallId : undefined;
@@ -118,12 +118,16 @@ export declare function createSettlementLedger(call: {
118
118
  */
119
119
  export declare function cloneObserverInput(input: unknown): unknown;
120
120
  /**
121
- * The input a gate exit decided on, as the call's record keeps it ({@link ToolGateResult.finalInput}): a deep,
122
- * FROZEN copy — never the live object the tool receives (a tool or a later layer writing into its own arguments
123
- * must not rewrite what the gate decided), and never writable by a reader; the deny observer gets a writable
124
- * {@link cloneObserverInput} of it. A graph `structuredClone` refuses (functions, handles) is kept as a shallow
125
- * copy frozen at its own top level only — its members are the live arguments' own, and freezing those would
126
- * freeze the call's input under the tool.
121
+ * The input a gate exit decided on, as the call's record keeps it ({@link ToolGateResult.finalInput}): a PLAIN-DATA PROJECTION of
122
+ * it, deep and FROZEN — never the live input (a tool, a later layer or the caller writing into its own arguments cannot rewrite
123
+ * what the gate decided), and never writable by a reader; the deny observer gets a writable {@link cloneObserverInput} of it. Read
124
+ * by the engine's one receipt ({@link RECORD_RECEIPT}), so nothing of the caller's runs: every record and list copied off its own
125
+ * enumerable data members, and every member that is not data replaced by a marker naming what it was (`[not recorded: …]`) — a
126
+ * Map, a Date, a class instance, a function or a bigint by a marker, never by reference; a symbol-keyed member's marker joins its
127
+ * container's list of them (`[not recorded: symbol-keyed members]`, under a key no data member holds). On an ALLOW exit too: the
128
+ * record is the projection while the call runs with the value itself — the gate's own copy (`ownArgs`: records and lists, a getter
129
+ * already read into data) recorded as it is save a bigint, the member closing a cycle and containers past the bound, which are
130
+ * markers. A value the receipt refuses whole (a Proxy, a function, a Map in place of the arguments) is recorded as its marker.
127
131
  */
128
132
  export declare function frozenInputSnapshot(input: unknown): unknown;
129
133
  /** What one `classic.PreToolUse` screening came to: an answer the receipt took (`undefined` = no opinion), or the
@@ -211,9 +215,10 @@ export interface GatePass {
211
215
  * behind the policy's answer could not land in between. Returns the gate result (a park's suspension or the
212
216
  * exit's record). Writer: the dispatch (`runToolGate`, which wires `runGateLanes`). Reader: the fold's tail. */
213
217
  readonly afterFold: (pass: GatePass) => Promise<ToolGateResult>;
214
- /** borrowed-mutable — the FINAL args, always the pass's own immutable value (`ownArgs`): the model's input as taken at
215
- * the entry, rewritten by the phase-1 hook, by the policy rewrite on the ask path and by the exits' rewrite fold.
216
- * Writers: the dispatch (phase 1), the lanes, the exits. */
218
+ /** borrowed-mutable — the FINAL args, the pass's own immutable value (`ownArgs`): the model's input as taken at the entry,
219
+ * rewritten by the phase-1 hook, by the policy rewrite on the ask path and by the exits' rewrite fold — save on a refusal
220
+ * of a value nothing took ({@link untakenValueExit}), where it holds that value as given and only the deny exit's receipt
221
+ * reads it. Writers: the dispatch (phase 1), the lanes, the exits. */
217
222
  currentInput: unknown;
218
223
  /** borrowed-mutable — the adjudication request. Minted over the pass's args so the record is whole from
219
224
  * the first station, and RE-MINTED by the adjudication station over the FINAL post-screen args (the
@@ -341,9 +346,24 @@ export declare function engineFailClosedExit(pass: GatePass, reason: string): Pr
341
346
  /** The refusal sentence for args the gate could not take into its own copy (`ownArgs` refused them), naming what. */
342
347
  export declare const unownedArgsMessage: (subject: string, reason: string) => string;
343
348
  /**
344
- * The model's args could not be taken into the pass's own copy (a function, a shared buffer, a typed array): nobody judged
345
- * them, so nothing may run them. The single deny exit (the observer fires), in the policy layer's word — the value is the
346
- * deployment's own (a host driving the gate; the runner's calls are always clonable).
349
+ * A value nothing took — the model's args the pass could not take into its own copy (`ownArgs`: a function, a shared buffer, a
350
+ * typed array; {@link unownedArgsMessage}), or a resolution's edit no approval can be bound to (`bindableArgsOf`) — is refused
351
+ * here, through the single deny exit (the observer fires): nobody judged it, so nothing may run it, and it is no args of the
352
+ * call, so no reading of the tool's runs over it. The refusal carries no judged bytes (no `updatedInput`): the pass holds the
353
+ * value as given, and the deny exit's one receipt records it (`finalInput`, its plain-data projection — one marker when the
354
+ * receipt refuses it whole: a Map, a Date, a class instance, a Proxy or a function in place of the arguments). In the refusing
355
+ * layer's word: the resolution's for an edit (the lanes mint its `presentation_failed` record), the policy layer's for the
356
+ * model's args ({@link unownedArgsExit}).
357
+ */
358
+ export declare function untakenValueExit(pass: GatePass, untaken: {
359
+ readonly value: unknown;
360
+ readonly message: string;
361
+ readonly deniedBy: Extract<DeniedBy, "policy" | "ask_resolution">;
362
+ }): Promise<ToolGateResult>;
363
+ /**
364
+ * The model's args could not be taken into the pass's own copy (a function, a shared buffer, a typed array): the dispatch seeded
365
+ * the pass with them as given, and they leave through the exit for a value nothing took, in the policy layer's word — the value is
366
+ * the deployment's own (a host driving the gate; the runner's calls are always clonable).
347
367
  */
348
368
  export declare function unownedArgsExit(pass: GatePass, reason: string): Promise<ToolGateResult>;
349
369
  /** The hook's OWN deny in phase 1. A PreToolUse hook deny does NOT fire `permissionDenied`
@@ -2,13 +2,15 @@ import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
2
2
  import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
3
3
  import { wireOf } from "../tool-name-readings.js";
4
4
  import { isProtectedWrite } from "../tool-registry.js";
5
- import { attestationOf, decisionText } from "../tool-policy.js";
5
+ import { attestationOf, decisionText, licenseCovers } from "../tool-policy.js";
6
6
  import { pathDenialCardOf } from "../../tools/fs/safety.js";
7
- import { boundInputHashOf } from "../canonical-json.js";
8
- import { argsAsTheToolReads, argsReadFrom } from "../effective-path-target.js";
7
+ import { argsAsTheToolReads } from "../effective-path-target.js";
9
8
  import { askUnresolvableRemedySeat } from "../ask-unresolvable-notice.js";
10
9
  import { inlineUntrusted } from "../untrusted-text.js";
11
10
  import { formatHookFeedback } from "../reminder-mint.js";
11
+ import { types } from "node:util";
12
+ import { receive } from "../mods/kernel/receive.js";
13
+ import { describeBindingFault, MAX_DEPTH } from "../canonical-json.js";
12
14
  export function mintGateOutcome(facts) {
13
15
  const settlement = facts.settled !== undefined ? Object.freeze({ ...facts.settled.settlement, who: Object.freeze({ ...facts.settled.settlement.who }) }) : undefined;
14
16
  const attribution = facts.classifier !== undefined ? Object.freeze({ ...facts.classifier, ...(facts.classifier.fallback !== undefined ? { fallback: Object.freeze({ ...facts.classifier.fallback }) } : {}) }) : undefined;
@@ -82,12 +84,7 @@ export function createSettlementLedger(call) {
82
84
  `(\`attestation\`), never a resolver's member; denied fail-closed rather than recording a settlement nobody can attribute`, contractRefusal(origin));
83
85
  }
84
86
  const edited = resolved.action === "allow" && resolved.updatedInput !== undefined;
85
- const licensed = () => {
86
- if (edited)
87
- return record.inputDigest === boundInputHashOf(resolved.updatedInput);
88
- const askedDigest = boundInputHashOf(req.args);
89
- return record.inputDigest === askedDigest && (resolved.presentedInput === undefined || boundInputHashOf(resolved.presentedInput) === askedDigest);
90
- };
87
+ const licensed = () => edited ? licenseCovers(record.inputDigest, resolved.updatedInput) : licenseCovers(record.inputDigest, req.args) && (resolved.presentedInput === undefined || licenseCovers(record.inputDigest, resolved.presentedInput));
91
88
  const bound = record?.settlement !== undefined && record.origin === origin && sameClasses(record.classes, asked.classes) ? record.settlement : undefined;
92
89
  const refusal = bound !== undefined && SETTLEMENT_IS_REFUSAL[bound.kind] ? bound : undefined;
93
90
  if (resolved.action === "allow") {
@@ -128,7 +125,7 @@ export function createSettlementLedger(call) {
128
125
  return { decision, deniedBy: refusal ? "ask_resolution" : "policy" };
129
126
  },
130
127
  keepForBytes(runs) {
131
- if (settled !== undefined && settled.licensed !== boundInputHashOf(runs) && settled.licensed !== boundInputHashOf(argsReadFrom(runs)))
128
+ if (settled !== undefined && !licenseCovers(settled.licensed, runs))
132
129
  settled = undefined;
133
130
  },
134
131
  causeOf(decision) {
@@ -154,32 +151,63 @@ export function cloneObserverInput(input) {
154
151
  return input;
155
152
  }
156
153
  }
154
+ const RECORD_RECEIPT = Object.freeze({ accessors: "refuse", depth: MAX_DEPTH, others: "refuse", contain: true, isProxy: (v) => types.isProxy(v) });
157
155
  export function frozenInputSnapshot(input) {
158
- try {
159
- return deepFreeze(structuredClone(input));
156
+ const received = receive(input, RECORD_RECEIPT);
157
+ if (!received.ok)
158
+ return notRecorded(input, received.fault);
159
+ const record = received.value;
160
+ const symbolMarkers = new Map();
161
+ for (const fault of received.faults ?? []) {
162
+ const above = fault.path.slice(0, -1);
163
+ const holder = memberAt(record, above);
164
+ const last = fault.path[fault.path.length - 1];
165
+ if (typeof holder !== "object" || holder === null || last === undefined)
166
+ continue;
167
+ const rawHolder = memberAt(input, above);
168
+ const symbolKeyed = fault.kind === "hidden" && (Object.hasOwn(holder, last) || typeof rawHolder !== "object" || rawHolder === null || !Object.hasOwn(rawHolder, last));
169
+ if (symbolKeyed) {
170
+ const listed = symbolMarkers.get(holder);
171
+ if (listed === undefined)
172
+ symbolMarkers.set(holder, [notRecorded(input, fault)]);
173
+ else
174
+ listed.push(notRecorded(input, fault));
175
+ }
176
+ else
177
+ Object.defineProperty(holder, last, { value: notRecorded(input, fault), enumerable: true, writable: true, configurable: true });
160
178
  }
161
- catch {
162
- return Object.freeze(Array.isArray(input) ? [...input] : input !== null && typeof input === "object" ? { ...input } : input);
179
+ for (const [holder, markers] of symbolMarkers) {
180
+ let key = "[not recorded: symbol-keyed members]";
181
+ while (Object.hasOwn(holder, key))
182
+ key += "'";
183
+ Object.defineProperty(holder, key, { value: markers, enumerable: true, writable: true, configurable: true });
163
184
  }
185
+ return freezeRecord(record);
164
186
  }
165
- function deepFreeze(value) {
166
- if (value === null || typeof value !== "object" || Object.isFrozen(value))
167
- return value;
168
- Object.freeze(value);
169
- for (const key of Reflect.ownKeys(value)) {
170
- const d = Object.getOwnPropertyDescriptor(value, key);
171
- if (d !== undefined && "value" in d)
172
- deepFreeze(d.value);
187
+ function freezeRecord(record) {
188
+ const pending = [record];
189
+ for (let at = pending.pop(); at !== undefined || pending.length > 0; at = pending.pop()) {
190
+ if (typeof at !== "object" || at === null || Object.isFrozen(at))
191
+ continue;
192
+ Object.freeze(at);
193
+ for (const member of Object.values(at))
194
+ pending.push(member);
173
195
  }
174
- return value;
196
+ return record;
197
+ }
198
+ const notRecorded = (input, fault) => `[not recorded: ${describeBindingFault(input, fault)}]`;
199
+ function memberAt(value, path) {
200
+ let at = value;
201
+ for (const key of path)
202
+ at = typeof at === "object" && at !== null ? at[key] : undefined;
203
+ return at;
175
204
  }
176
205
  const PARK_FAILURE_CAUSE_MAX = 600;
177
- function withParkFailureCause(reason, parkFailed) {
206
+ function withParkFailureCause(reason, parkFailed, nobodyJudged) {
178
207
  if (parkFailed === undefined)
179
208
  return reason;
180
- return (`${reason} — note: a durable approval park was attempted for this call FIRST and could not be minted ` +
181
- `(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
182
- `left to say, not the reason the call stopped.`);
209
+ const note = `${reason} — note: a durable approval park was attempted for this call and could not be minted (${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)})`;
210
+ return nobodyJudged ? `${note}; with nobody to judge the call, that is why it stopped — it can be issued again once its park can be minted.` : `${note}.`;
183
211
  }
184
212
  export function passAskFace(pass) {
185
213
  return askFaceOf(wireOf("gate.askFace", pass.req));
@@ -192,11 +220,15 @@ export async function engineFailClosedExit(pass, reason) {
192
220
  return { block: true, reason: formatHookFeedback(reason, input.reminderMark), gate, finalInput, preToolContext: pass.preToolContext };
193
221
  }
194
222
  export const unownedArgsMessage = (subject, reason) => `${subject} could not be taken into the gate's own copy (${reason}) — denied fail-closed`;
195
- export async function unownedArgsExit(pass, reason) {
196
- pass.decision = { action: "deny", message: unownedArgsMessage(`the arguments of "${pass.toolName}"`, reason) };
197
- pass.deniedBy = "policy";
223
+ export async function untakenValueExit(pass, untaken) {
224
+ pass.currentInput = untaken.value;
225
+ pass.decision = { action: "deny", message: untaken.message };
226
+ pass.deniedBy = untaken.deniedBy;
198
227
  return await exitGate(pass);
199
228
  }
229
+ export async function unownedArgsExit(pass, reason) {
230
+ return await untakenValueExit(pass, { value: pass.currentInput, message: unownedArgsMessage(`the arguments of "${pass.toolName}"`, reason), deniedBy: "policy" });
231
+ }
200
232
  export function hookDenyExit(pass, deniedReason) {
201
233
  const { input } = pass;
202
234
  return {
@@ -236,11 +268,13 @@ export async function exitGate(pass) {
236
268
  const unresolvable = foldCause ?? unresolvableCauseOfSettlement(gate);
237
269
  const tableSeat = unresolvable !== undefined ? askUnresolvableRemedySeat(unresolvable) : undefined;
238
270
  const remedySeat = tableSeat === "absent" && input.askSeatForm === "declared_none" && pass.askClassification?.classes.has("content_question") !== true ? "declared_none" : tableSeat;
239
- const remedy = remedySeat !== undefined ? headlessApprovalRemedy({ mode: input.permissionMode?.(), classification: foldCause !== undefined ? ANCESTOR_UNRESOLVED_CLASSIFICATION : pass.askClassification, approverSeat: remedySeat, coveredWrite: isProtectedWrite(pass.req), ...(pass.shellUnplacedOptions !== undefined ? { unplacedOptions: pass.shellUnplacedOptions } : {}) }) : undefined;
240
- const denyReason = withParkFailureCause(`${decisionText(pass.decision) ?? `tool "${toolName}" denied by policy`}${remedy !== undefined ? ` — ${remedy}` : ""}`, pass.parkFailed);
271
+ const bindingRefusal = attestationOf(pass.decision, { toolCallId, toolName })?.bindingRefusal;
272
+ const remedy = bindingRefusal !== undefined ? undefined : remedySeat !== undefined ? headlessApprovalRemedy({ mode: input.permissionMode?.(), classification: foldCause !== undefined ? ANCESTOR_UNRESOLVED_CLASSIFICATION : pass.askClassification, approverSeat: remedySeat, coveredWrite: isProtectedWrite(pass.req), ...(pass.shellUnplacedOptions !== undefined ? { unplacedOptions: pass.shellUnplacedOptions } : {}) }) : undefined;
273
+ const denyReason = withParkFailureCause(`${decisionText(pass.decision) ?? `tool "${toolName}" denied by policy`}${remedy !== undefined ? ` — ${remedy}` : ""}`, pass.parkFailed, unresolvable !== undefined);
241
274
  if (unresolvable !== undefined) {
242
275
  const cause = unresolvable;
243
- pass.notifier.notify(() => input.onAskUnresolvable?.({ toolName, toolCallId, cause, parkLaneExisted: pass.suspendAsk !== undefined, ...(remedy !== undefined ? { remedy } : {}) }), "toolGate.onAskUnresolvable");
276
+ const noticeRemedy = bindingRefusal ?? remedy;
277
+ pass.notifier.notify(() => input.onAskUnresolvable?.({ toolName, toolCallId, cause, parkLaneExisted: pass.suspendAsk !== undefined, ...(noticeRemedy !== undefined ? { remedy: noticeRemedy } : {}) }), "toolGate.onAskUnresolvable");
244
278
  }
245
279
  const finalInput = frozenInputSnapshot(pass.currentInput);
246
280
  await notifyPermissionDenied({
@@ -269,6 +303,14 @@ export async function exitGate(pass) {
269
303
  pass.currentInput = argsAsTheToolReads(pass.req, rw);
270
304
  }
271
305
  ledger.keepForBytes(pass.currentInput);
306
+ const crossBoundary = await input.crossBoundaryCheck?.exit({ toolName, args: pass.currentInput, toolCallId }, ledger.settled?.settlement.who.party === "person" ? "person" : "policy");
307
+ if (crossBoundary !== undefined) {
308
+ pass.decision = { action: "deny", message: crossBoundary.message, decisionReason: "safety" };
309
+ pass.deniedBy = crossBoundary.kind === "unconfirmed" ? "ask_resolution" : "policy";
310
+ if (crossBoundary.kind === "unconfirmed")
311
+ ledger.set({ settlement: { kind: "blanket_allow_refused", who: { party: "none" }, when: Date.now() }, origin: ledger.settled?.origin ?? "safety_tighten" });
312
+ return exitGate(pass);
313
+ }
272
314
  const release = pass.arbitration?.kind === "released" ? pass.arbitration : (() => { if (ledger.settled !== undefined)
273
315
  return undefined; const a = pass.arbitration === undefined ? input.ancestorArbitrationOf?.(toolCallId) : undefined; return a?.kind === "released" ? a : undefined; })();
274
316
  return {
@@ -1,6 +1,6 @@
1
1
  import { primaryActivityArg } from "../arg-summary.js";
2
2
  import { askApproverIdentity, attest, attestationOf } from "../tool-policy.js";
3
- import { boundInputHashOf } from "../canonical-json.js";
3
+ import { exactBoundInputHashOf } from "../canonical-json.js";
4
4
  export function askGrantCovers(granted, asked) {
5
5
  return asked !== undefined && asked.length > 0 && granted.length > 0 && asked.every((c) => c === "ordinary" || granted.includes(c));
6
6
  }
@@ -34,5 +34,5 @@ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision
34
34
  const settlement = attestationOf(grant.settled, req)?.settlement;
35
35
  if (settlement === undefined || decision.action !== "ask")
36
36
  return reused;
37
- return attest(reused, req, { settlement, ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...(decision.classes !== undefined ? { classes: decision.classes } : {}), inputDigest: boundInputHashOf(grant.presented) });
37
+ return attest(reused, req, { settlement, ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...(decision.classes !== undefined ? { classes: decision.classes } : {}), inputDigest: exactBoundInputHashOf(grant.presented) });
38
38
  }
@@ -14,8 +14,8 @@
14
14
  *
15
15
  * Every read's outcome lands in the run's unit, per personal partition (the durable one — its user and project rows — and
16
16
  * the session one), and only where it is newer, by the order the reads STARTED: a read that answers late never replaces
17
- * a later one. The prepare read is the unit's first write. A reader that is not a call (the compaction summary's working-file
18
- * reader, the user-attachment seat's reader, the Artifact tool's local read, a read root's grant eligibility, the classifier's table, a language server's session read)
17
+ * a later one. The prepare read is the unit's first write. A reader that is not a call (the compaction's kept-tail
18
+ * normalizer, the user-attachment seat's reader (the compaction restore's too), the Artifact tool's local read, a read root's grant eligibility, the classifier's table, a language server's session read)
19
19
  * reads the unit's newest tables: while the rows the unit last took are compiling it waits for them (within
20
20
  * {@link PATH_TABLES_READY_BOUND_MS}), and a read of tables still compiling, or that could not be compiled, refuses —
21
21
  * never the tables of rows the unit has since replaced.
@@ -38,10 +38,9 @@ export interface PrepareAcquireReconcileInput {
38
38
  * suspension point BEFORE reconcile) and `suspendedBatch` (wake-reconcile SKIPS those calls —
39
39
  * they are resumed, not crash-interrupted). Absent for a fresh/non-durable run. */
40
40
  resume: Pick<PrepareResume, "leafId" | "suspendedBatch"> | undefined;
41
- /** borrowed-readonly here — the safety scan's effect registry (its mutation-owner table lives on
42
- * {@link import("./prepare-safety-scan.js").PrepareSafetyScanResult}); reconcile classifies an
43
- * orphaned call's retry-safety by it. */
44
- toolEffects: Map<string, ToolEffect>;
41
+ /** borrowed-readonly — the safety scan's pre-mount reading ({@link import("./prepare-safety-scan.js").PrepareSafetyScanResult.wakeEffects}):
42
+ * this reconcile runs before any tool mounts, so a contested name reads its least repeat-safe member; it classifies an orphan's retry-safety. */
43
+ wakeEffects: Map<string, ToolEffect>;
45
44
  /** borrowed-readonly — subagent transcript persistence: the trusted spawner's placement
46
45
  * declaration for THIS run's session ({@link RunInternals.sessionPlacement}), forwarded verbatim
47
46
  * into the create-form acquire so the store creates the child's transcript into its subagent