@sema-agent/core 7.53.0 → 7.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +291 -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 +7 -9
  104. package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
  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 +683 -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/parent-chain-guard.d.ts +10 -0
  195. package/dist/engine/session/parent-chain-guard.js +10 -0
  196. package/dist/engine/session/repo-utils.d.ts +5 -0
  197. package/dist/engine/session/repo-utils.js +9 -0
  198. package/dist/engine/session/storage-base.js +5 -2
  199. package/dist/index.d.ts +23 -9
  200. package/dist/index.js +19 -8
  201. package/dist/internal/harness.d.ts +1 -0
  202. package/dist/internal/harness.js +1 -0
  203. package/dist/internal/llm.d.ts +1 -1
  204. package/dist/internal/llm.js +1 -1
  205. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  206. package/dist/orchestration/run-workflow-tool.js +1 -0
  207. package/dist/orchestration/workflow-governance.d.ts +1 -1
  208. package/dist/orchestration/workflow.d.ts +3 -0
  209. package/dist/orchestration/workflow.js +2 -1
  210. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  211. package/dist/prompt-assembly/catalog/schema.js +3 -0
  212. package/dist/prompt-assembly/overlay.js +2 -2
  213. package/dist/prompt-assembly/select.js +2 -2
  214. package/dist/prompts/default.d.ts +1 -1
  215. package/dist/shell/lex.d.ts +87 -8
  216. package/dist/shell/lex.js +283 -49
  217. package/dist/stores/file/index.d.ts +8 -6
  218. package/dist/tools/artifact/artifact-tool.js +10 -0
  219. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  220. package/dist/tools/fs/bash-program-position.js +3 -10
  221. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  222. package/dist/tools/fs/fs-bash.d.ts +3 -3
  223. package/dist/tools/fs/fs-bash.js +72 -101
  224. package/dist/tools/fs/fs-read.d.ts +12 -0
  225. package/dist/tools/fs/fs-read.js +10 -9
  226. package/dist/tools/fs/fs-shared.d.ts +27 -31
  227. package/dist/tools/fs/fs-shared.js +12 -15
  228. package/dist/tools/fs/fs-write.js +1 -1
  229. package/dist/tools/fs/index.js +2 -2
  230. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  231. package/dist/tools/fs/read-deny-home.js +5 -4
  232. package/dist/tools/fs/read-deny.d.ts +16 -3
  233. package/dist/tools/fs/read-deny.js +12 -9
  234. package/dist/tools/fs/safety.d.ts +7 -6
  235. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  236. package/dist/tools/fs/shell-read-bindings.js +15 -16
  237. package/dist/tools/fs/shell-read-device.js +1 -1
  238. package/dist/tools/fs/shell-read-plan.js +36 -47
  239. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  240. package/dist/tools/fs/shell-read-word.js +2 -20
  241. package/dist/tools/fs/shell-reentry-text.js +1 -1
  242. package/dist/tools/web.d.ts +2 -2
  243. package/package.json +4 -1
  244. package/test/export-surface.snapshot.json +5654 -5520
  245. package/wire-changes/core@7.53.0.json +30 -8
  246. package/wire-changes/core@7.54.0.json +2870 -0
  247. package/wire-changes/core@7.54.1.json +37 -0
  248. package/dist/core/shell-scan.d.ts +0 -60
  249. package/dist/core/shell-scan.js +0 -183
@@ -0,0 +1,94 @@
1
+ import { dataKind, perToolEmptyName, perToolEntryRefusal, quotedName, receivePerToolDeclaration } from "./mcp-server-spec.js";
2
+ import { isPlainRecord } from "./mods/kernel/receive.js";
3
+ import { faceProblem } from "./runner/tool-face-overlay.js";
4
+ import { inlineUntrusted } from "./untrusted-text.js";
5
+ const TOOL_FACE_MEMBERS = { family: true, pathTarget: true, renderHints: true, ruleFace: true };
6
+ const PATH_TARGET_KINDS = { param: "string", aliases: "list", access: "string", skillScopeEligible: "boolean", base: "string", absent: "string", patternParam: "string" };
7
+ const PATH_TARGET_REQUIRED = ["param", "access"];
8
+ const RENDER_HINT_KINDS = { userFacingName: "string", activity: "string", summaryParams: "list", resultCards: "list", approvalCard: "string" };
9
+ const RULE_FACE_KINDS = { primaryParams: "list", params: "list" };
10
+ function kindProblem(label, value, kind) {
11
+ if (value === undefined)
12
+ return undefined;
13
+ if (kind === "list")
14
+ return Array.isArray(value) && value.every((name) => typeof name === "string") ? undefined : `${label} is ${dataKind(value)}, not a list of strings`;
15
+ return typeof value === kind ? undefined : `${label} is ${dataKind(value)}, not a ${kind}`;
16
+ }
17
+ function membersOf(raw, label, names) {
18
+ if (!isPlainRecord(raw))
19
+ return `${label} is ${dataKind(raw)}, not an object`;
20
+ const unknown = Object.keys(raw).find((member) => !names.includes(member));
21
+ return unknown === undefined ? raw : `${label} has the unknown member ${quotedName(unknown)} (the members are ${names.join(", ")})`;
22
+ }
23
+ function ownMember(record, name) {
24
+ return Object.hasOwn(record, name) ? record[name] : undefined;
25
+ }
26
+ function readMembers(raw, label, kinds, required = []) {
27
+ const record = membersOf(raw, label, Object.keys(kinds));
28
+ if (typeof record === "string")
29
+ return record;
30
+ for (const member of required)
31
+ if (ownMember(record, member) === undefined)
32
+ return `${label}.${member} is missing`;
33
+ const entries = [];
34
+ for (const member of Object.keys(record)) {
35
+ const kind = kinds[member];
36
+ const value = record[member];
37
+ const problem = kindProblem(`${label}.${member}`, value, kind);
38
+ if (problem !== undefined)
39
+ return problem;
40
+ if (value === undefined || (kind === "boolean" && value === false))
41
+ continue;
42
+ entries.push([member, kind === "list" ? [...value] : value]);
43
+ }
44
+ return Object.fromEntries(entries);
45
+ }
46
+ function readOneToolFace(raw) {
47
+ const face = membersOf(raw, "the face", Object.keys(TOOL_FACE_MEMBERS));
48
+ if (typeof face === "string")
49
+ return face;
50
+ const records = { pathTarget: [PATH_TARGET_KINDS, PATH_TARGET_REQUIRED], renderHints: [RENDER_HINT_KINDS, []], ruleFace: [RULE_FACE_KINDS, []] };
51
+ const entries = [];
52
+ for (const member of Object.keys(face)) {
53
+ const value = face[member];
54
+ if (value === undefined)
55
+ continue;
56
+ if (member === "family") {
57
+ const problem = kindProblem("family", value, "string");
58
+ if (problem !== undefined)
59
+ return problem;
60
+ entries.push([member, value]);
61
+ continue;
62
+ }
63
+ const [kinds, required] = records[member];
64
+ const record = readMembers(value, member, kinds, required);
65
+ if (typeof record === "string")
66
+ return record;
67
+ entries.push([member, record]);
68
+ }
69
+ const typed = Object.fromEntries(entries);
70
+ return faceProblem(typed, undefined) ?? typed;
71
+ }
72
+ export function readToolFaces(declared) {
73
+ if (declared === undefined)
74
+ return { ok: true };
75
+ const received = receivePerToolDeclaration(declared, "toolFaces", "tool");
76
+ if (!received.ok)
77
+ return { ok: false, problem: received.problem };
78
+ const entries = [];
79
+ for (const [tool, raw] of Object.entries(received.perTool)) {
80
+ if (tool.length === 0)
81
+ return { ok: false, problem: perToolEmptyName("toolFaces", "tool") };
82
+ let face;
83
+ try {
84
+ face = readOneToolFace(raw);
85
+ }
86
+ catch {
87
+ face = "a member threw while it was read";
88
+ }
89
+ if (typeof face === "string")
90
+ return { ok: false, problem: perToolEntryRefusal("toolFaces", "tool", tool, inlineUntrusted(face, 240)) };
91
+ entries.push([tool, face]);
92
+ }
93
+ return entries.length > 0 ? { ok: true, faces: Object.fromEntries(entries) } : { ok: true };
94
+ }
@@ -198,6 +198,9 @@ export interface ToolRenderHints {
198
198
  */
199
199
  export interface ToolFaceDeclaration {
200
200
  family: ToolFamily;
201
+ /** The legs every call of the definition travels, or `"per_call"` when the code that dispatches it gives them per call. REQUIRED:
202
+ * a definition that states none does not compile — no default, and no fail-closed reading as outside. */
203
+ boundaryLegs: import("./cross-boundary-taint.js").BoundaryLegsDeclaration;
201
204
  effect?: import("./tool-spec.js").ToolEffect;
202
205
  egress?: true;
203
206
  irreversibility?: "never" | "maybe" | "always";
@@ -232,6 +235,12 @@ export interface ToolFaceDeclaration {
232
235
  */
233
236
  export declare const TOOL_LEG_KINDS: readonly ["delegated", "teammate", "goal", "observer", "structured"];
234
237
  export type ToolLegKind = (typeof TOOL_LEG_KINDS)[number];
238
+ /**
239
+ * The leg kinds that share their spawner's tool surface: a teammate is handed tools its spawner holds in its own right (the team's mailbox,
240
+ * scheduler and task list), so the layers above the leg decide its protocol rows and the leg's own lists do not. A protocol row that no kind
241
+ * of this table names is the leg's own channel — a finishing or hand-back tool its spawner never held — and no layer decides it.
242
+ */
243
+ export declare const SPAWNER_SURFACE_LEG_KINDS: readonly ToolLegKind[];
235
244
  /**
236
245
  * One layer of a tool selector, as data: an allowed-tools list (absent = no allowlist on this layer) and a denied-tools list,
237
246
  * each a frozen copy of the entries a list was written with. A run's own layer is its `allowTools` and `excludeTools`; the
@@ -244,8 +253,9 @@ export interface ToolSelectorLayer {
244
253
  /**
245
254
  * The compiled tool selector of one run — the one answer to "is this tool available on this run", read at the roster valve
246
255
  * and on every later roster change (an MCP refresh or re-dial, the deferred-search lane), and handed down as the ceiling of
247
- * every run this one spawns (`layers`). A tool is admitted when it is a protocol tool of this run's leg kind, or when every
248
- * layer admits it: the layer's allowlist (if any) reaches it and its denylist does not.
256
+ * every run this one spawns (`layers`). A tool is admitted when it is the leg's own protocol channel, or when every layer
257
+ * admits it — the layer's allowlist (if any) reaches it and its denylist does not — or, for a protocol tool of a leg that shares
258
+ * its spawner's tool surface ({@link SPAWNER_SURFACE_LEG_KINDS}), when every layer ABOVE the leg's own admits it.
249
259
  */
250
260
  export interface ToolSelector {
251
261
  readonly layers: readonly ToolSelectorLayer[];
@@ -280,13 +290,12 @@ export interface ToolCatalogEntry {
280
290
  face: ToolFaceDeclaration;
281
291
  /** A `modelGate` class the default-bundle assembler stamps on this definition's default arm. */
282
292
  modelGate?: string;
283
- /** The leg kinds whose legs rely on this tool as part of their protocol: on such a leg the tool's availability is not a
284
- * selector's to decide (an allowlist need not name it), and a denylist that reaches it there is a contradiction the run
285
- * refuses at prepare. */
293
+ /** The leg kinds whose legs rely on this tool as part of their protocol: the leg's OWN lists do not decide it (an allowlist need not
294
+ * name it), and a denylist of the leg's own that reaches it is a contradiction the run refuses at prepare. Whether the layers ABOVE
295
+ * the leg decide it follows the row's kinds ({@link SPAWNER_SURFACE_LEG_KINDS}): a row some kind of which shares its spawner's tool
296
+ * surface is held exactly when the spawner may hold it; any other row is the leg's own channel, which no layer decides — an inherited
297
+ * deny that reaches it is the same contradiction. */
286
298
  protocol?: readonly ToolLegKind[];
287
- /** A protocol row the leg shares with the run that spawned it (the team task-list family a teammate shares with its leader): on its
288
- * leg the leg's OWN lists do not decide it, but the ceiling still does — the leg holds it exactly when its spawner may. */
289
- protocolUnderCeiling?: true;
290
299
  }
291
300
  /**
292
301
  * The band-membership table over the mount tags: a host derives its hand band as the definitions listed under a
@@ -25,6 +25,7 @@ export const RENDER_HINT_MAX_CHARS = 64;
25
25
  export const RENDER_HINT_ACTIVITY_MAX_CHARS = 32;
26
26
  export const RENDER_HINT_MAX_LIST = 16;
27
27
  export const TOOL_LEG_KINDS = Object.freeze(["delegated", "teammate", "goal", "observer", "structured"]);
28
+ export const SPAWNER_SURFACE_LEG_KINDS = Object.freeze(["teammate"]);
28
29
  export const MOUNT_TAG_IN_HANDS_BAND = {
29
30
  hands: true,
30
31
  backgroundTasks: true,
@@ -2,6 +2,7 @@ import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
2
2
  import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
3
3
  import { type Settlement } from "./gate-outcome.js";
4
4
  export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
5
+ import { type BindingFault } from "./canonical-json.js";
5
6
  import { type DecisionReason } from "./decision-receipt.js";
6
7
  export { isEngineRefusal } from "./decision-receipt.js";
7
8
  import { type RemovalGuardSeat } from "../tools/fs/removal-targets.js";
@@ -236,7 +237,8 @@ export interface EngineAttestation {
236
237
  readonly origin?: AskOrigin;
237
238
  /** The settled ask's class set (`ASK_CLASSES` order), when the minting station held one. */
238
239
  readonly classes?: readonly import("./ask-origin.js").AskClass[];
239
- /** `boundInputHashOf` of the arguments the settlement licenses: an approver's edit, the snapshot shown, or the asked args. */
240
+ /** `exactBoundInputHashOf` of the arguments the settlement licenses (an approver's edit, the snapshot shown, or the asked
241
+ * args); absent when they have none (outside the bindable domain) — such a record licenses no bytes. */
240
242
  readonly inputDigest?: string;
241
243
  /** The FORM of a classifier round that did not rule (a classifier deny's cause). */
242
244
  readonly classifierCause?: import("./gate-outcome.js").ClassifierDenyCause;
@@ -249,6 +251,9 @@ export interface EngineAttestation {
249
251
  /** Where a path deny row refused the call — the row, the view of the target it met, the face the call is on. The gate's
250
252
  * exit mints the refusal's card from it; the judge that found the row is its only writer. */
251
253
  readonly pathDenial?: import("../tools/fs/safety.js").PathDenialEvidence;
254
+ /** A seat refused the call's arguments because no approval can be bound to them ({@link bindableArgsOf}): the refusal's
255
+ * sentence, carried by the refusal itself — the gate's deny exit reads it (the remedy it gives) instead of judging again. */
256
+ readonly bindingRefusal?: string;
252
257
  }
253
258
  /** The members a station attests — everything but the call binding. */
254
259
  export type AttestationMembers = Omit<EngineAttestation, "toolCallId" | "toolName">;
@@ -422,6 +427,11 @@ export type PermissionResult = {
422
427
  * in the one direction the stamp cannot reach (a caller composing its own ask) the claim is
423
428
  * tightening-only: it can make an ask outlive a rule, never turn one into an allow. */
424
429
  probeMandated?: true;
430
+ /** This ask stands on the call's CROSS-BOUNDARY obligation — a person's confirmation
431
+ * ({@link import("./cross-boundary-taint.js").crossBoundaryObligation}); the station mints it beside `requiresRealApproval`, the
432
+ * decision receipt adds that bit to a policy's or hook's own. Its one carrier: the class (`cross_boundary`), the rule-silence word (`cross_boundary_taint`) and the
433
+ * real-approval bit are all read off this member. Tightening-only: a policy self-declaring it opts its own ask out of automatic answering. */
434
+ crossBoundaryMandated?: true;
425
435
  /** This ask is MANDATED — no persisted allow rule and no STANDING ANSWER (a release remembered from an
426
436
  * earlier call, at any scope, a session included) may retire it; only an answer produced for THIS call
427
437
  * clears it — a person, a hook, a deployment's blanket `onAsk: "allow"` seat, or the live deciders the
@@ -592,6 +602,37 @@ export interface AskRuleEvidence {
592
602
  }
593
603
  /** The human/model-readable text of a decision. */
594
604
  export declare function decisionText(d: PermissionResult): string | undefined;
605
+ /** Does a license (a record's `inputDigest`) cover the bytes that `runs`? The ONE judge — the gate's ledger keeps a settlement for
606
+ * the bytes it licensed by it, and the unstated question asks whether an answer was given for these bytes by it. A license for
607
+ * bytes covers what the tool reads them as ({@link argsReadFrom}: the one recorded step a reading took to `runs`). Compared by
608
+ * the EXACT digest only: bytes outside the bindable domain have none and are covered by no license. */
609
+ export declare function licenseCovers(licensed: string | undefined, runs: unknown): boolean;
610
+ /**
611
+ * THE judge of whether an approval can be bound to arguments, and the arguments it binds — asked wherever an approval is about to
612
+ * be asked or adopted, and nowhere else: the ask resolver (its no-approver arm too) and the approval policy before anyone is asked,
613
+ * the resolver where it adopts a person's edit, the gate's approved-edit loop (an edit any resolver hands back), and — through the
614
+ * same judge and sentence — the durable decide's capture of one before the claim ({@link capturedJsonOf}). Bindable = a record of
615
+ * plain JSON data nested at most 256 levels deep ({@link bindableValueOf}, exactly the values with an exact digest — the SAME reading every
616
+ * license and mint takes), judged on the value as given with nothing of the caller's run (a getter is never called: a value read
617
+ * through one has no fixed bytes). Ok ⇒ the judge's frozen copy, engine-owned (`args` itself when owned already); else the ONE
618
+ * sentence ({@link bindingRefusalOf}).
619
+ */
620
+ export declare function bindableArgsOf<T>(toolName: string, args: T): {
621
+ ok: true;
622
+ value: T;
623
+ } | {
624
+ ok: false;
625
+ refusal: string;
626
+ };
627
+ /** THE sentence refusing arguments no approval can be bound to, naming what the judge met and where (`args` is what it was given). */
628
+ export declare function bindingRefusalOf(toolName: string, args: unknown, fault: BindingFault): string;
629
+ /** Was the call ANSWERED for the bytes that run — does `d` carry an engine record ({@link attestationOf}) of a settled wait
630
+ * whose license covers `runs`? A person's or the deployment approver's yes over these arguments is; a layer's allow is not,
631
+ * whatever it says about itself, and neither is a yes given for other bytes (a later layer that rewrote them voids it here). */
632
+ export declare function answeredFor(d: PermissionResult, call: {
633
+ toolCallId: string;
634
+ toolName: string;
635
+ }, runs: unknown): boolean;
595
636
  /**
596
637
  * Gates tool calls before they execute (the production safety layer for "processing/changing data").
597
638
  *
@@ -878,9 +919,10 @@ export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintCha
878
919
  * different frozen deny data now digest differently.
879
920
  */
880
921
  export declare function constraintChainDigest(chain: readonly ConstraintChainEntry[]): string;
881
- /** Every member of the decision union — a member the union gains and this list lacks is a type error at `satisfies`.
882
- * The receipt walks it: a contract member not held as an own enumerable data property is refused if a descriptor on the
883
- * prototype chain holds one (an `undefined` data value spells absence). Not re-exported from `src/index.ts`. */
922
+ /** Every member of the decision union, read off the member × arm table ({@link DECISION_MEMBER_ARMS}: a member the union gains
923
+ * and the table lacks is a type error there). The receipt walks it: a contract member not held as an own enumerable data
924
+ * property is refused if a descriptor on the prototype chain holds one (an `undefined` data value spells absence), and a
925
+ * present one must sit on an arm its row holds. Not re-exported from `src/index.ts`. */
884
926
  export type DecisionMember = PermissionResult extends infer U ? (U extends unknown ? keyof U : never) : never;
885
927
  export declare const DECISION_MEMBERS: readonly DecisionMember[];
886
928
  /**
@@ -949,8 +991,10 @@ export type NamedToolPolicy = ToolPolicy & {
949
991
  * which simply opts out of the audit). */
950
992
  export declare function toolPolicyNameSets(p: ToolPolicy | undefined): readonly ToolPolicyNameSets[];
951
993
  /**
952
- * Name-based allow/deny. `deny` always wins. If `allow` is provided, only those tools are permitted
953
- * (everything else denied). Without `allow`, everything not in `deny` is permitted. Each entry is read by the rule
994
+ * Name-based allow/deny. `deny` always wins. If `allow` is provided, only those tools may run (everything else denied) — a
995
+ * NARROWING: a tool the list names gets no objection, never consent, so a `passthrough`-seat tool it names still meets the
996
+ * mode's unstated question; consent is an allow entry (`RunnerDeps.allowedTools` / `TaskSpec.allowedTools`). Without
997
+ * `allow`, everything not in `deny` gets no objection. Each entry is read by the rule
954
998
  * syntax (`ruleSpellingOf`): a name, or `Tool()` / `Tool(*)` — the bare `Tool`, as every list reader reads them, once however
955
999
  * many spellings write it; any other spelling is not a tool name (`onInvalidName`), and neither is the wildcard `*` (`*`,
956
1000
  * `*()`, `*(*)`) in `deny`: it names no tool, and the entry would be kept and never match. In `allow` it is one entry no tool
@@ -975,13 +1019,6 @@ export declare function createAllowDenyPolicy(opts: {
975
1019
  list: "allow" | "deny";
976
1020
  message: string;
977
1021
  }) => void;
978
- /**
979
- * What an `allow` list HIT means. `"grant"` (default): the deployment named this tool as permitted — a statement
980
- * (`decisionReason: "rule"`) that satisfies the fold's unstated rule for a passthrough-seat tool. `"narrowing"`: the
981
- * list only bounds WHICH tools a lane may see (an agent definition's `allowTools`); a hit is scope, not consent, and
982
- * answers a bare allow — the mode's unstated question still goes to a person. The deny list is unaffected.
983
- */
984
- scope?: "grant" | "narrowing";
985
1022
  }): NamedToolPolicy;
986
1023
  /**
987
1024
  * What `createApprovalPolicy`'s `approve` may resolve to: a boolean, or the `AskOutcome` OBJECT minus the
@@ -1030,7 +1067,8 @@ export declare function createApprovalPolicy(opts: {
1030
1067
  approve: (req: ToolCallRequest, signal?: AbortSignal) => ApprovalSeatOutcome | Promise<ApprovalSeatOutcome>;
1031
1068
  /** Always-denied tools. */
1032
1069
  deny?: string[];
1033
- /** Always-allowed tools (only meaningful with `denyByDefault`). */
1070
+ /** The exceptions to `denyByDefault` (only meaningful with it): tools that get no objection without an approval. A narrowing,
1071
+ * not consent — a `passthrough`-seat tool named here still meets the mode's unstated question; consent is an allow entry. */
1034
1072
  autoAllow?: string[];
1035
1073
  /** When true, deny anything not in `requireApproval` or `autoAllow`. Default false. */
1036
1074
  denyByDefault?: boolean;
@@ -1050,12 +1088,11 @@ export declare function createApprovalPolicy(opts: {
1050
1088
  * short-circuit — scanning continues, because a later policy may still `deny` (which outranks ask).
1051
1089
  * With no deny, the first `ask` wins; with neither, `allow`.
1052
1090
  *
1053
- * **An allow is STATED or not.** A layer that names the call (an allow rule, a list entry, an approver's yes, a mode's
1054
- * release) says WHY on its allow (`decisionReason`); a bare allow is "no objection" — an empty table, a list the call is
1055
- * not on, a layer that does not know the tool. The combined allow carries the FIRST stated reason in layer order, and a
1056
- * statement binds the arguments its layer judged: a layer that rewrites the arguments voids every statement before it (its
1057
- * own allow, if it states one, speaks for the new arguments). For a tool whose permission seat is `passthrough` an
1058
- * unstated allow is not a permission — the gate puts the mode's question to it; for every other tool the two forms run alike.
1091
+ * **A layer's allow is no objection, never consent.** What a combined allow carries beyond that is the engine RECORDS its
1092
+ * layers' stations minted (an approver's yes inside a layer, an ancestor classifier's attribution), accumulated per member
1093
+ * group; a yes binds the bytes it was given (its license), so a layer that rewrites the arguments afterwards leaves a yes that
1094
+ * no longer covers what runs. For a tool whose permission seat is `passthrough` the gate puts the mode's question to a
1095
+ * combined allow unless an answer covers the bytes that run ({@link answeredFor}) — the allow layer answers it there.
1059
1096
  *
1060
1097
  * **Idempotency requirement:** because the fold continues past an `ask` to look for a later `deny`, a
1061
1098
  * policy's `check()` can be invoked even after an earlier policy returned `ask`. `check()` MUST be
@@ -1847,8 +1884,8 @@ export declare function refusalOf<D extends PermissionResult>(deny: D, judged: u
1847
1884
  /**
1848
1885
  * A PreToolUse hook's `ask` folded over a verdict by `deny > ask > allow` — the ONE law for the gate's first screening and for
1849
1886
  * an approved edit's re-screen. A deny stands. Over an allow, the hook's own ask, RECONSTRUCTED rather than spread (its message,
1850
- * its real-approval demand, its rewrite) under the hook provenance the engine stamps. Over an ask, that ask keeps every member,
1851
- * takes the hook provenance and the hook's real-approval demand, and its message carries the hook's beside its own — a person
1887
+ * its real-approval demand and cross-boundary member, its rewrite) under the hook provenance the engine stamps. Over an ask, that ask keeps every member,
1888
+ * takes the hook provenance and the hook's real-approval demand and cross-boundary member, and its message carries the hook's beside its own — a person
1852
1889
  * clearing one ask that stands for two gates sees both reasons.
1853
1890
  */
1854
1891
  export declare function foldHookAsk(verdict: Exclude<PermissionResult, {
@@ -1986,10 +2023,14 @@ export type ResolverArmResult = (Extract<ResolvedAsk, {
1986
2023
  action: "allow";
1987
2024
  }> & {
1988
2025
  settlement?: Settlement;
1989
- }) | (Extract<ResolvedAsk, {
2026
+ bindingRefusal?: undefined;
2027
+ })
2028
+ /** `bindingRefusal`: the arm refused arguments no approval can be bound to — the record carries the sentence. */
2029
+ | (Extract<ResolvedAsk, {
1990
2030
  action: "deny";
1991
2031
  }> & {
1992
2032
  settlement: Settlement;
2033
+ bindingRefusal?: string;
1993
2034
  });
1994
2035
  /**
1995
2036
  * The DIRECTIONAL format controls, as a closed set (see {@link AskRequest.hasBidiControls}):
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
- import { withArgsReading } from "./effective-path-target.js";
3
+ import { argsReadFrom, carryArgsReadFrom, withArgsReading } from "./effective-path-target.js";
4
4
  import { brandPolicyAskClass } from "./ask-class.js";
5
5
  import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
6
6
  import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
@@ -12,12 +12,12 @@ import { join, normalize as normalizePath, sep } from "node:path";
12
12
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
13
13
  import { protectivePathTargetOf, shellFaceToolNames } from "./tool-registry.js";
14
14
  import { heldBy } from "./tool-name-readings.js";
15
- import { boundInputHashOf } from "./canonical-json.js";
15
+ import { bindableValueOf, boundInputHashOf, describeBindingFault, exactBoundInputHashOf } from "./canonical-json.js";
16
16
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
17
17
  import { describeRejectedValue } from "./tool-search-setting.js";
18
18
  import { egressText, egressThrownText } from "./untrusted-egress.js";
19
19
  import { assertNoRetiredKeys, assertNoRetiredToolPolicyKeys, RETIRED_DECISION_MEMBERS, RETIRED_TOOL_POLICY_KEYS } from "./retired-keys.js";
20
- import { isEngineRefusal, nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
20
+ import { DECISION_MEMBER_ARMS, isEngineRefusal, nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
21
21
  export { isEngineRefusal } from "./decision-receipt.js";
22
22
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "./permission-rules.js";
23
23
  import { ruleSpellingOf } from "./permission-rule-syntax.js";
@@ -45,6 +45,7 @@ const ATTESTATION_GROUP_OF = {
45
45
  unresolvableCause: "unresolvableCause",
46
46
  foldedClasses: "foldedClasses",
47
47
  pathDenial: "pathDenial",
48
+ bindingRefusal: "bindingRefusal",
48
49
  };
49
50
  const ATTESTATION_MEMBERS = Object.keys(ATTESTATION_GROUP_OF);
50
51
  const groupLead = (g) => ATTESTATION_MEMBERS.find((k) => ATTESTATION_GROUP_OF[k] === g);
@@ -146,6 +147,28 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
146
147
  export function decisionText(d) {
147
148
  return d.message;
148
149
  }
150
+ export function licenseCovers(licensed, runs) {
151
+ return licensed !== undefined && (licensed === exactBoundInputHashOf(runs) || licensed === exactBoundInputHashOf(argsReadFrom(runs)));
152
+ }
153
+ export function bindableArgsOf(toolName, args) {
154
+ const bound = bindableValueOf(args);
155
+ if (!bound.ok)
156
+ return { ok: false, refusal: bindingRefusalOf(toolName, args, bound.fault) };
157
+ if (typeof args === "object" && args !== null && OWNED_ARGS.has(args))
158
+ return { ok: true, value: args };
159
+ if (typeof bound.value === "object" && bound.value !== null)
160
+ OWNED_ARGS.add(bound.value);
161
+ return { ok: true, value: bound.value };
162
+ }
163
+ export function bindingRefusalOf(toolName, args, fault) {
164
+ return (`no approval can be bound to the arguments of "${toolName}": they hold ${inlineUntrusted(describeBindingFault(args, fault), 300)} — an approval binds arguments that are ` +
165
+ `a record of plain JSON data (records, dense lists, strings, finite numbers, booleans, null; no getter, hidden member, Proxy, ` +
166
+ `function, symbol, bigint or object of another kind; no cycle or shared object) nested at most 256 levels deep; denied fail-closed`);
167
+ }
168
+ export function answeredFor(d, call, runs) {
169
+ const record = attestationOf(d, call);
170
+ return record?.settlement !== undefined && licenseCovers(record.inputDigest, runs);
171
+ }
149
172
  const TOOL_POLICY_CARRIED_MEMBERS = ["projection", "pathRules", "writeGuardRows", "nameSets"];
150
173
  export function carriedMembersOf(p) {
151
174
  assertNoRetiredKeys(p, RETIRED_TOOL_POLICY_KEYS, "ToolPolicy");
@@ -259,12 +282,7 @@ export function constraintChainDigest(chain) {
259
282
  return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
260
283
  }
261
284
  const ALLOW = Object.freeze({ action: "allow" });
262
- const ALLOW_BY_RULE = Object.freeze({ action: "allow", decisionReason: "rule" });
263
- export const DECISION_MEMBERS = Object.keys({
264
- action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, classifierNoRuling: 0, allowRuleShadowed: 0, probeReason: 0,
265
- probeCause: 0, probeMandated: 0, mandated: 0, mandate: 0, probeReadRootCandidate: 0, matchedAskRule: 0, ruleEvidence: 0, segmentCoverage: 0,
266
- origin: 0, askClass: 0, classes: 0, guards: 0, ruleUnreadable: 0, authoredBy: 0, foldedAskClasses: 0, attestation: 0,
267
- });
285
+ export const DECISION_MEMBERS = Object.keys(DECISION_MEMBER_ARMS);
268
286
  const HOOK_RESULT_MEMBERS = [...DECISION_MEMBERS, "additionalContext"];
269
287
  const HOOK_SEAM_UNREAD = new Set(["decisionReason"]);
270
288
  const HOOK_SEAM_OPTIONAL = new Set(["action"]);
@@ -315,7 +333,10 @@ export function refuseOutOfContractDecision(d, opts) {
315
333
  return refusedOutOfContract(`a permission decision's members could not be detached from the layer that returned them (${ownedRewrite.reason}) — ` +
316
334
  `the engine keeps its own copy of what it read; denied fail-closed`);
317
335
  }
318
- const receipt = Object.freeze(attestation !== undefined ? { ...detached.value, attestation } : detached.value);
336
+ carryArgsReadFrom(contract.updatedInput, rewrite);
337
+ const held = detached.value;
338
+ const value = held.crossBoundaryMandated === true && held.requiresRealApproval !== true ? { ...held, requiresRealApproval: true } : held;
339
+ const receipt = Object.freeze(attestation !== undefined ? { ...value, attestation } : value);
319
340
  for (const v of Object.values(receipt))
320
341
  if (Array.isArray(v))
321
342
  Object.freeze(v);
@@ -355,6 +376,7 @@ export function toolPolicyNameSets(p) {
355
376
  return Array.isArray(sets) ? sets : [];
356
377
  }
357
378
  export function createAllowDenyPolicy(opts) {
379
+ assertNoRetiredKeys(opts, RETIRED_ALLOW_DENY_POLICY_OPTIONS, "createAllowDenyPolicy");
358
380
  const invalid = [];
359
381
  const screen = (entries, list) => {
360
382
  if (entries === undefined)
@@ -444,10 +466,13 @@ export function createAllowDenyPolicy(opts) {
444
466
  if (allow && !nameListHolds(allow, req) && !namespacedCoveringHit(allowCovering, req)) {
445
467
  return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
446
468
  }
447
- return allow && opts.scope !== "narrowing" ? ALLOW_BY_RULE : ALLOW;
469
+ return ALLOW;
448
470
  },
449
471
  };
450
472
  }
473
+ const RETIRED_ALLOW_DENY_POLICY_OPTIONS = Object.freeze({
474
+ scope: "an allow list is only ever a narrowing now (a tool it does not name is denied; a tool it names gets no objection, not consent) — drop the option, and put the consent in the allow layer: `RunnerDeps.allowedTools` for the deployment's, `TaskSpec.allowedTools` for a request's",
475
+ });
451
476
  export function createApprovalPolicy(opts) {
452
477
  if (opts.approvalTimeoutMs !== undefined && !Number.isFinite(opts.approvalTimeoutMs)) {
453
478
  throw new Error(`createApprovalPolicy: approvalTimeoutMs must be a finite number of ms (got ${opts.approvalTimeoutMs}) — omit it to wait indefinitely`);
@@ -481,17 +506,17 @@ export function createApprovalPolicy(opts) {
481
506
  return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
482
507
  }
483
508
  if (nameListHolds(need, req) || namespacedCoveringHit(needCovering, req)) {
484
- const attestOver = (inputDigest) => (d, settlement) => ({ ...d, attestation: mintAttestation(req, { settlement, origin: "policy", inputDigest }) });
509
+ const attestOver = (inputDigest) => (d, settlement, bindingRefusal) => ({ ...d, attestation: mintAttestation(req, { settlement, origin: "policy", ...(inputDigest !== undefined ? { inputDigest } : {}), ...(bindingRefusal !== undefined ? { bindingRefusal } : {}) }) });
485
510
  const unasked = unaskedSeatAnswer(req, signal, { aborted: `approval aborted for "${req.toolName}" (run or turn interrupted)`, unreachable: `approval for "${req.toolName}" was not requested: nobody can be asked about this call` }, "policy");
486
511
  if (unasked !== undefined)
487
512
  return unasked;
488
- const shown = tryCloneArgs(req.args);
489
- if (!shown.ok) {
490
- return attestOver(boundInputHashOf(req.args))({ action: "deny", message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${shown.reason}) — denied fail-closed` }, settledByNobody("presentation_failed"));
491
- }
492
- const askedDigest = boundInputHashOf(shown.value);
513
+ const bound = bindableArgsOf(req.toolName, req.args);
514
+ if (!bound.ok)
515
+ return attestOver(undefined)({ action: "deny", message: bound.refusal }, settledByNobody("presentation_failed"), bound.refusal);
516
+ const shown = { value: structuredClone(bound.value) };
517
+ const askedDigest = exactBoundInputHashOf(shown.value);
493
518
  const attest = attestOver(askedDigest);
494
- const heldWhileWaiting = () => boundInputHashOf(req.args) === askedDigest;
519
+ const heldWhileWaiting = () => licenseCovers(askedDigest, req.args);
495
520
  const changedWhilePending = () => attest({
496
521
  action: "deny",
497
522
  message: `the arguments of "${req.toolName}" changed while its approval was pending — denied fail-closed ` +
@@ -544,7 +569,7 @@ export function createApprovalPolicy(opts) {
544
569
  return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByDecider("person", "refused", undefined));
545
570
  }
546
571
  if (nameListHolds(auto, req) || namespacedCoveringHit(autoCovering, req))
547
- return ALLOW_BY_RULE;
572
+ return ALLOW;
548
573
  if (opts.denyByDefault) {
549
574
  return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
550
575
  }
@@ -587,7 +612,6 @@ export function combinePolicies(...policies) {
587
612
  let asked;
588
613
  let current = Object.freeze({ ...req });
589
614
  let rewrite;
590
- let stated;
591
615
  let records;
592
616
  let ruleAskText;
593
617
  let probeMandateSeen = false;
@@ -614,10 +638,6 @@ export function combinePolicies(...policies) {
614
638
  if (d.updatedInput !== undefined) {
615
639
  current = Object.freeze(await withArgsReading(current, d.updatedInput, signal));
616
640
  rewrite = d;
617
- stated = d.action === "allow" ? d.decisionReason : undefined;
618
- }
619
- else if (d.action === "allow" && stated === undefined && d.decisionReason !== undefined) {
620
- stated = d.decisionReason;
621
641
  }
622
642
  if (d.action === "allow" && d.attestation !== undefined)
623
643
  records = mergeAttestation(req, records, d.attestation);
@@ -662,8 +682,7 @@ export function combinePolicies(...policies) {
662
682
  const out = merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
663
683
  return folded.size > 0 ? attest(out, req, { foldedClasses: [...folded] }) : out;
664
684
  }
665
- const base = rewrite ?? ALLOW;
666
- const allowed = stated !== undefined && base.decisionReason !== stated ? { ...base, decisionReason: stated } : base;
685
+ const allowed = rewrite ?? ALLOW;
667
686
  return records === undefined ? allowed : { ...allowed, attestation: records };
668
687
  },
669
688
  };
@@ -713,7 +732,7 @@ export function createCoarseCommandNamePolicy(opts) {
713
732
  if (allow && !allow.has(parsed.name)) {
714
733
  return fallback(`command "${parsed.name}" is not in the allowlist`);
715
734
  }
716
- return allow ? ALLOW_BY_RULE : ALLOW;
735
+ return ALLOW;
717
736
  },
718
737
  }, "sandbox_local");
719
738
  }
@@ -899,7 +918,7 @@ export function refusalOf(deny, judged) {
899
918
  export function foldHookAsk(verdict, hookAsk) {
900
919
  if (verdict.action === "deny")
901
920
  return verdict;
902
- const mandate = hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {};
921
+ const mandate = { ...(hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}), ...(hookAsk.crossBoundaryMandated === true ? { crossBoundaryMandated: true } : {}) };
903
922
  if (verdict.action === "allow") {
904
923
  return { action: "ask", ...(hookAsk.message !== undefined ? { message: hookAsk.message } : {}), decisionReason: "hook", ...mandate, ...(hookAsk.updatedInput !== undefined ? { updatedInput: hookAsk.updatedInput } : {}) };
905
924
  }
@@ -1077,12 +1096,12 @@ export function carriesBidiControls(value, limits) {
1077
1096
  }
1078
1097
  }
1079
1098
  export async function resolveAsk(req, onAsk, signal, onLateSettlement) {
1080
- const askedDigest = boundInputHashOf(req.args);
1081
- const { settlement, ...r } = await resolveAskArms(req, onAsk, signal, onLateSettlement);
1099
+ const askedDigest = exactBoundInputHashOf(req.args);
1100
+ const { settlement, bindingRefusal, ...r } = await resolveAskArms(req, onAsk, signal, onLateSettlement);
1082
1101
  if (settlement === undefined)
1083
1102
  return r;
1084
- const licensedDigest = r.action === "allow" && r.updatedInput !== undefined ? boundInputHashOf(r.updatedInput) : r.presentedInput !== undefined ? boundInputHashOf(r.presentedInput) : askedDigest;
1085
- return attest(r, req, { settlement, ...(req.origin !== undefined ? { origin: req.origin } : {}), ...(req.classes !== undefined ? { classes: req.classes } : {}), inputDigest: licensedDigest });
1103
+ const licensedDigest = r.action === "allow" && r.updatedInput !== undefined ? exactBoundInputHashOf(r.updatedInput) : r.presentedInput !== undefined ? exactBoundInputHashOf(r.presentedInput) : askedDigest;
1104
+ return attest(r, req, { settlement, ...(req.origin !== undefined ? { origin: req.origin } : {}), ...(req.classes !== undefined ? { classes: req.classes } : {}), ...(licensedDigest !== undefined ? { inputDigest: licensedDigest } : {}), ...(bindingRefusal !== undefined ? { bindingRefusal } : {}) });
1086
1105
  }
1087
1106
  function raceAskWaitAgainstSignal(wait, signal) {
1088
1107
  const settledWait = wait.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
@@ -1198,10 +1217,9 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
1198
1217
  `re-adjudication an approver's edit gets; denied fail-closed (approve or refuse what was shown, and route edits ` +
1199
1218
  `through the run's approver seat)`, settledByNobody("approver_contract"));
1200
1219
  }
1201
- const edit = ownArgs(suppliedEdit);
1202
- if (!edit.ok) {
1203
- return refused(`the approved edit for "${toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`, settledByNobody("presentation_failed"));
1204
- }
1220
+ const edit = bindableArgsOf(toolName, suppliedEdit);
1221
+ if (!edit.ok)
1222
+ return { kind: "refused", message: edit.refusal, settlement: settledByNobody("presentation_failed"), binding: true };
1205
1223
  return { kind: "allow", edit: { value: edit.value }, settlement: settledByDecider(decider, "allowed", attribution.approver), grant };
1206
1224
  }
1207
1225
  async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
@@ -1211,37 +1229,25 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1211
1229
  return { action: "allow", decisionReason: "mode" };
1212
1230
  }
1213
1231
  if (onAsk === undefined || onAsk === "deny") {
1232
+ const bound = bindableArgsOf(req.toolName, req.args);
1214
1233
  return {
1215
1234
  action: "deny",
1216
- message: headlessDenialMessage(req.toolName, req.message),
1235
+ message: bound.ok ? headlessDenialMessage(req.toolName, req.message) : bound.refusal,
1217
1236
  decisionReason: "mode",
1218
1237
  settlement: settledByNobody("no_approver"),
1238
+ ...(bound.ok ? {} : { bindingRefusal: bound.refusal }),
1219
1239
  };
1220
1240
  }
1221
1241
  if (signal?.aborted) {
1222
1242
  return { action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, decisionReason: "mode", settlement: settledByNobody("task_aborted") };
1223
1243
  }
1224
- const presented = ownArgs(req.args);
1225
- if (!presented.ok) {
1226
- return {
1227
- action: "deny",
1228
- message: `approval for "${req.toolName}" could not present the args safely (${presented.reason}) — denied fail-closed`,
1229
- decisionReason: "mode",
1230
- settlement: settledByNobody("presentation_failed"),
1231
- };
1232
- }
1244
+ const presented = bindableArgsOf(req.toolName, req.args);
1245
+ if (!presented.ok)
1246
+ return { action: "deny", message: presented.refusal, decisionReason: "mode", settlement: settledByNobody("presentation_failed"), bindingRefusal: presented.refusal };
1233
1247
  const offered = ownReadRootCandidate(req.readRootCandidate);
1234
1248
  let ok;
1235
1249
  try {
1236
- const approverView = tryCloneArgs(presented.value);
1237
- if (!approverView.ok) {
1238
- return {
1239
- action: "deny",
1240
- message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${approverView.reason}) — denied fail-closed`,
1241
- decisionReason: "mode",
1242
- settlement: settledByNobody("presentation_failed"),
1243
- };
1244
- }
1250
+ const approverView = { value: structuredClone(presented.value) };
1245
1251
  const bidi = carriesBidiControls(presented.value) || carriesBidiControls(req.preview);
1246
1252
  const { hasBidiControls: _carried, ...bare } = req;
1247
1253
  const wait = Promise.resolve(onAsk({
@@ -1295,7 +1301,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1295
1301
  if (typeof ok === "object" && ok !== null) {
1296
1302
  const reading = readApproverOutcomeObject(ok, req.toolName, "readjudicated");
1297
1303
  if (reading.kind === "refused")
1298
- return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement };
1304
+ return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement, ...(reading.binding === true ? { bindingRefusal: reading.message } : {}) };
1299
1305
  if (reading.kind === "allow" && reading.settlement.who.party === "policy" && req.requiresRealApproval === true) {
1300
1306
  return { ...unjudgedAllowRefused(req, "policy"), ...readRootGrantSeat(reading.grant, offered, false) };
1301
1307
  }