@sema-agent/core 7.52.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 (357) hide show
  1. package/CHANGELOG.md +470 -0
  2. package/LICENSE +3 -1
  3. package/README.md +3 -1
  4. package/dist/agents/cross-session-envelope.d.ts +8 -0
  5. package/dist/agents/cross-session-envelope.js +6 -0
  6. package/dist/agents/cross-session-judge.d.ts +21 -1
  7. package/dist/agents/cross-session-judge.js +8 -0
  8. package/dist/agents/cross-session-ref.d.ts +5 -0
  9. package/dist/agents/cross-session-ref.js +1 -0
  10. package/dist/agents/org-delivery-gate.d.ts +122 -0
  11. package/dist/agents/org-delivery-gate.js +69 -0
  12. package/dist/agents/peer-held-queue.d.ts +9 -4
  13. package/dist/agents/peer-held-queue.js +6 -5
  14. package/dist/agents/peer-session-drain.d.ts +35 -6
  15. package/dist/agents/peer-session-drain.js +271 -182
  16. package/dist/agents/read-inbox-tool.d.ts +7 -0
  17. package/dist/agents/read-inbox-tool.js +5 -1
  18. package/dist/agents/send-message-tool.js +3 -4
  19. package/dist/agents/subagent.d.ts +6 -5
  20. package/dist/agents/subagent.js +117 -83
  21. package/dist/agents/tool-filter.d.ts +25 -4
  22. package/dist/agents/tool-filter.js +13 -3
  23. package/dist/brain/anthropic.d.ts +6 -1
  24. package/dist/brain/anthropic.js +4 -4
  25. package/dist/brain/model-presets.js +2 -1
  26. package/dist/brain/route-adjudicator.d.ts +6 -0
  27. package/dist/brain/routing.d.ts +24 -1
  28. package/dist/brain/routing.js +54 -19
  29. package/dist/brain/timeout.js +5 -4
  30. package/dist/core/a2a.d.ts +5 -2
  31. package/dist/core/a2a.js +33 -14
  32. package/dist/core/allow-entry.d.ts +3 -1
  33. package/dist/core/allow-entry.js +3 -2
  34. package/dist/core/approval-object.d.ts +87 -0
  35. package/dist/core/approval-object.js +85 -0
  36. package/dist/core/arg-summary.d.ts +3 -2
  37. package/dist/core/arg-summary.js +17 -11
  38. package/dist/core/ask-origin.d.ts +43 -27
  39. package/dist/core/ask-origin.js +20 -11
  40. package/dist/core/ask-question.d.ts +0 -1
  41. package/dist/core/ask-question.js +4 -67
  42. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  43. package/dist/core/auto-compaction.d.ts +106 -50
  44. package/dist/core/auto-compaction.js +69 -53
  45. package/dist/core/auto-mode-prompt.d.ts +4 -2
  46. package/dist/core/auto-mode-prompt.js +4 -3
  47. package/dist/core/canonical-json.d.ts +69 -0
  48. package/dist/core/canonical-json.js +130 -8
  49. package/dist/core/checkpoint-requirements.d.ts +13 -7
  50. package/dist/core/checkpoint-requirements.js +4 -1
  51. package/dist/core/checkpoint-store.d.ts +60 -36
  52. package/dist/core/checkpoint-store.js +7 -6
  53. package/dist/core/child-clamp-seats.d.ts +45 -6
  54. package/dist/core/child-clamp-seats.js +56 -4
  55. package/dist/core/compliance.js +3 -1
  56. package/dist/core/consolidate-scope.d.ts +1 -1
  57. package/dist/core/cross-boundary-taint.d.ts +213 -0
  58. package/dist/core/cross-boundary-taint.js +196 -0
  59. package/dist/core/decision-receipt.d.ts +44 -1
  60. package/dist/core/decision-receipt.js +16 -0
  61. package/dist/core/effective-path-target.d.ts +28 -6
  62. package/dist/core/effective-path-target.js +28 -2
  63. package/dist/core/engine-notice.d.ts +47 -4
  64. package/dist/core/engine-notice.js +17 -1
  65. package/dist/core/engine-storage-backend.d.ts +73 -0
  66. package/dist/core/engine-storage-backend.js +1 -0
  67. package/dist/core/file-history-store.d.ts +6 -2
  68. package/dist/core/file-history-store.js +1 -0
  69. package/dist/core/gate-fold.js +13 -4
  70. package/dist/core/gate-lanes.js +41 -34
  71. package/dist/core/gate-outcome.d.ts +18 -9
  72. package/dist/core/governance-codes.d.ts +9 -2
  73. package/dist/core/governance-codes.js +9 -0
  74. package/dist/core/hands-band-screen.d.ts +7 -0
  75. package/dist/core/hands-band-screen.js +32 -0
  76. package/dist/core/hooks.d.ts +21 -19
  77. package/dist/core/hooks.js +6 -5
  78. package/dist/core/locked-config.d.ts +110 -31
  79. package/dist/core/locked-config.js +155 -42
  80. package/dist/core/lsp.d.ts +1 -1
  81. package/dist/core/mcp-probe.js +1 -1
  82. package/dist/core/mcp-server-spec.d.ts +152 -17
  83. package/dist/core/mcp-server-spec.js +223 -40
  84. package/dist/core/mcp.d.ts +21 -14
  85. package/dist/core/mcp.js +59 -54
  86. package/dist/core/memory-engine/engine.d.ts +14 -19
  87. package/dist/core/memory-engine/engine.js +9 -6
  88. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  89. package/dist/core/memory-engine/export-bundle.js +1 -1
  90. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  91. package/dist/core/memory-engine/file-backend.js +1 -12
  92. package/dist/core/memory.d.ts +3 -3
  93. package/dist/core/mode-policy.d.ts +16 -26
  94. package/dist/core/mode-policy.js +5 -18
  95. package/dist/core/mods/kernel/dispatch.js +13 -15
  96. package/dist/core/mods/kernel/events.d.ts +9 -6
  97. package/dist/core/mods/kernel/events.js +2 -2
  98. package/dist/core/mods/kernel/receive.d.ts +41 -11
  99. package/dist/core/mods/kernel/receive.js +124 -54
  100. package/dist/core/mods/kernel/site-rules.d.ts +80 -15
  101. package/dist/core/mods/kernel/site-rules.js +232 -40
  102. package/dist/core/mods/stations.d.ts +0 -6
  103. package/dist/core/mods/stations.js +0 -7
  104. package/dist/core/mounted-mcp.d.ts +57 -0
  105. package/dist/core/mounted-mcp.js +71 -2
  106. package/dist/core/own-property-receipt.d.ts +13 -0
  107. package/dist/core/own-property-receipt.js +3 -1
  108. package/dist/core/parent-spec-seat.d.ts +12 -15
  109. package/dist/core/parent-spec-seat.js +14 -6
  110. package/dist/core/pause-registry.d.ts +35 -1
  111. package/dist/core/pause-registry.js +10 -0
  112. package/dist/core/permission-rule-consent.d.ts +18 -5
  113. package/dist/core/permission-rule-consent.js +13 -8
  114. package/dist/core/permission-rule-model.d.ts +87 -15
  115. package/dist/core/permission-rule-model.js +87 -8
  116. package/dist/core/permission-rule-org.d.ts +200 -33
  117. package/dist/core/permission-rule-org.js +217 -91
  118. package/dist/core/permission-rule-session.js +2 -2
  119. package/dist/core/permission-rule-store.d.ts +8 -0
  120. package/dist/core/permission-rule-store.js +1 -0
  121. package/dist/core/permission-rule-sync.js +3 -2
  122. package/dist/core/permission-rules.d.ts +7 -1
  123. package/dist/core/permission-rules.js +37 -8
  124. package/dist/core/read-only-shell.d.ts +1 -1
  125. package/dist/core/read-only-shell.js +30 -9
  126. package/dist/core/remote-env.d.ts +41 -1
  127. package/dist/core/remote-env.js +18 -0
  128. package/dist/core/resume-startup-cause.d.ts +18 -0
  129. package/dist/core/resume-startup-cause.js +12 -0
  130. package/dist/core/retention.js +7 -2
  131. package/dist/core/retired-keys.d.ts +8 -2
  132. package/dist/core/retired-keys.js +12 -1
  133. package/dist/core/roles.js +60 -10
  134. package/dist/core/rule-lane-reads.js +1 -1
  135. package/dist/core/runner/approval-feedback.d.ts +46 -0
  136. package/dist/core/runner/approval-feedback.js +41 -0
  137. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  138. package/dist/core/runner/compaction-call-options.js +2 -2
  139. package/dist/core/runner/contracts.d.ts +34 -41
  140. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  141. package/dist/core/runner/cross-boundary-lane.js +190 -0
  142. package/dist/core/runner/decide-continuation.d.ts +4 -4
  143. package/dist/core/runner/decide-continuation.js +3 -1
  144. package/dist/core/runner/gate-exit.d.ts +32 -12
  145. package/dist/core/runner/gate-exit.js +77 -34
  146. package/dist/core/runner/inherited-ask-grants.js +2 -2
  147. package/dist/core/runner/mcp-redial.d.ts +52 -10
  148. package/dist/core/runner/mcp-redial.js +38 -9
  149. package/dist/core/runner/permission-rule-lanes.d.ts +5 -2
  150. package/dist/core/runner/permission-rule-lanes.js +22 -10
  151. package/dist/core/runner/person-path-rows.d.ts +5 -3
  152. package/dist/core/runner/person-path-rows.js +2 -2
  153. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -4
  154. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  155. package/dist/core/runner/prepare-ask-lane.js +3 -3
  156. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  157. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  158. package/dist/core/runner/prepare-config-doors.d.ts +58 -15
  159. package/dist/core/runner/prepare-config-doors.js +167 -146
  160. package/dist/core/runner/prepare-context-lane.js +6 -3
  161. package/dist/core/runner/prepare-defer-classify.js +4 -7
  162. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  163. package/dist/core/runner/prepare-delegation-surface.js +8 -7
  164. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  165. package/dist/core/runner/prepare-execution-env.js +2 -1
  166. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  167. package/dist/core/runner/prepare-gate-stations.js +4 -3
  168. package/dist/core/runner/prepare-hands-readface.d.ts +3 -3
  169. package/dist/core/runner/prepare-hands-readface.js +3 -2
  170. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  171. package/dist/core/runner/prepare-inherited-gate.js +3 -2
  172. package/dist/core/runner/prepare-listings.d.ts +1 -1
  173. package/dist/core/runner/prepare-listings.js +4 -2
  174. package/dist/core/runner/prepare-memory-engine-session.d.ts +0 -6
  175. package/dist/core/runner/prepare-memory-engine-session.js +19 -6
  176. package/dist/core/runner/prepare-memory.d.ts +3 -2
  177. package/dist/core/runner/prepare-memory.js +6 -2
  178. package/dist/core/runner/prepare-offload-wrappers.d.ts +4 -4
  179. package/dist/core/runner/prepare-offload-wrappers.js +2 -1
  180. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  181. package/dist/core/runner/prepare-park-ask.js +9 -3
  182. package/dist/core/runner/prepare-policy-chain.d.ts +1 -1
  183. package/dist/core/runner/prepare-policy-chain.js +14 -19
  184. package/dist/core/runner/prepare-project-context.d.ts +1 -1
  185. package/dist/core/runner/prepare-project-context.js +6 -4
  186. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  187. package/dist/core/runner/prepare-prompt-assembly.js +20 -16
  188. package/dist/core/runner/prepare-prompt-inputs.d.ts +12 -6
  189. package/dist/core/runner/prepare-prompt-inputs.js +6 -3
  190. package/dist/core/runner/prepare-protocol-tools.d.ts +7 -5
  191. package/dist/core/runner/prepare-protocol-tools.js +19 -12
  192. package/dist/core/runner/prepare-question-face.d.ts +4 -2
  193. package/dist/core/runner/prepare-question-face.js +4 -3
  194. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  195. package/dist/core/runner/prepare-safety-scan.js +34 -27
  196. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  197. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  198. package/dist/core/runner/prepare-task.js +22 -19
  199. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  200. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  201. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -5
  202. package/dist/core/runner/prepare-wiring-manifest.js +9 -6
  203. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  204. package/dist/core/runner/resume-admission.d.ts +4 -1
  205. package/dist/core/runner/resume-admission.js +16 -14
  206. package/dist/core/runner/resume-apply.js +4 -4
  207. package/dist/core/runner/resume-internals-and-config.d.ts +3 -3
  208. package/dist/core/runner/resume-internals-and-config.js +3 -8
  209. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  210. package/dist/core/runner/resume-policy-outcome.js +18 -15
  211. package/dist/core/runner/resume-preflight.js +2 -2
  212. package/dist/core/runner/resume-review-outcome.js +3 -3
  213. package/dist/core/runner/retired-runner-deps.d.ts +5 -6
  214. package/dist/core/runner/retired-runner-deps.js +1 -4
  215. package/dist/core/runner/run-leg.js +97 -43
  216. package/dist/core/runner/run-recovery-lanes.js +5 -3
  217. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  218. package/dist/core/runner/run-terminal-adoption.js +21 -9
  219. package/dist/core/runner/run-turn-boundary.js +9 -3
  220. package/dist/core/runner/runtask.js +1 -1
  221. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  222. package/dist/core/runner/session-file-state-replay.js +3 -2
  223. package/dist/core/runner/skills-seat.d.ts +15 -0
  224. package/dist/core/runner/skills-seat.js +57 -0
  225. package/dist/core/runner/stream-steer-verb.js +2 -2
  226. package/dist/core/runner/sub-agent-deny-lists.d.ts +13 -11
  227. package/dist/core/runner/sub-agent-deny-lists.js +7 -5
  228. package/dist/core/runner/synthetic-tools.d.ts +5 -2
  229. package/dist/core/runner/synthetic-tools.js +2 -12
  230. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  231. package/dist/core/runner/tool-face-overlay.js +24 -15
  232. package/dist/core/runner-deps.d.ts +37 -22
  233. package/dist/core/session-policy-store.d.ts +19 -0
  234. package/dist/core/session-policy-store.js +22 -2
  235. package/dist/core/session-store.d.ts +35 -16
  236. package/dist/core/session-store.js +20 -12
  237. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  238. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  239. package/dist/core/shell-wrapper-table.d.ts +5 -3
  240. package/dist/core/shell-wrapper-table.js +1 -1
  241. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  242. package/dist/core/store-contracts/mailbox-store-contract.js +13 -0
  243. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  244. package/dist/core/store-contracts/session-store-contract.js +639 -0
  245. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  246. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  247. package/dist/core/task-event.d.ts +44 -6
  248. package/dist/core/task-notification.js +2 -2
  249. package/dist/core/task-registry-agent.js +1 -0
  250. package/dist/core/task-registry-shared.d.ts +5 -0
  251. package/dist/core/task-registry.d.ts +7 -0
  252. package/dist/core/task-registry.js +17 -0
  253. package/dist/core/task-result.d.ts +1 -1
  254. package/dist/core/task-spec.d.ts +7 -7
  255. package/dist/core/tighten-task-spec.d.ts +6 -4
  256. package/dist/core/tighten-task-spec.js +2 -2
  257. package/dist/core/tool-catalog-entries.d.ts +1 -1
  258. package/dist/core/tool-catalog-entries.js +52 -52
  259. package/dist/core/tool-face-reader.d.ts +49 -0
  260. package/dist/core/tool-face-reader.js +94 -0
  261. package/dist/core/tool-face.d.ts +17 -8
  262. package/dist/core/tool-face.js +1 -0
  263. package/dist/core/tool-policy.d.ts +67 -23
  264. package/dist/core/tool-policy.js +72 -60
  265. package/dist/core/tool-roster.d.ts +46 -28
  266. package/dist/core/tool-roster.js +91 -14
  267. package/dist/core/tool-spec-members.js +2 -2
  268. package/dist/core/tool-spec.d.ts +20 -4
  269. package/dist/core/tools.js +2 -0
  270. package/dist/core/trace.d.ts +24 -2
  271. package/dist/core/types.d.ts +3 -3
  272. package/dist/core/types.js +1 -1
  273. package/dist/core/untrusted-text.d.ts +23 -0
  274. package/dist/core/untrusted-text.js +10 -10
  275. package/dist/core/user-attachment.d.ts +56 -2
  276. package/dist/core/user-attachment.js +139 -56
  277. package/dist/core/wiring-manifest.d.ts +4 -2
  278. package/dist/engine/compaction/compaction.d.ts +2 -2
  279. package/dist/engine/compaction/utils.d.ts +2 -2
  280. package/dist/engine/harness/agent-harness.js +2 -1
  281. package/dist/engine/harness/types.d.ts +10 -4
  282. package/dist/engine/llm/types.d.ts +7 -2
  283. package/dist/engine/llm/types.js +2 -1
  284. package/dist/engine/loop/agent-loop.d.ts +12 -1
  285. package/dist/engine/loop/agent-loop.js +17 -5
  286. package/dist/engine/loop/types.d.ts +7 -1
  287. package/dist/engine/session/repo-utils.d.ts +5 -0
  288. package/dist/engine/session/repo-utils.js +9 -0
  289. package/dist/engine/session/storage-base.js +2 -2
  290. package/dist/index.d.ts +37 -17
  291. package/dist/index.js +31 -14
  292. package/dist/internal/harness.d.ts +1 -0
  293. package/dist/internal/harness.js +1 -0
  294. package/dist/internal/llm.d.ts +1 -1
  295. package/dist/internal/llm.js +1 -1
  296. package/dist/orchestration/goal.d.ts +24 -1
  297. package/dist/orchestration/goal.js +3 -2
  298. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  299. package/dist/orchestration/run-workflow-tool.js +1 -0
  300. package/dist/orchestration/workflow-governance.d.ts +1 -1
  301. package/dist/orchestration/workflow.d.ts +3 -0
  302. package/dist/orchestration/workflow.js +2 -1
  303. package/dist/prompt-assembly/artifact-store.d.ts +44 -9
  304. package/dist/prompt-assembly/artifact-store.js +38 -12
  305. package/dist/prompt-assembly/artifact.d.ts +12 -11
  306. package/dist/prompt-assembly/artifact.js +52 -21
  307. package/dist/prompt-assembly/assemble.d.ts +2 -0
  308. package/dist/prompt-assembly/assemble.js +2 -1
  309. package/dist/prompt-assembly/catalog/index.js +2 -1
  310. package/dist/prompt-assembly/catalog/memory.d.ts +13 -0
  311. package/dist/prompt-assembly/catalog/memory.js +6 -0
  312. package/dist/prompt-assembly/catalog/schema.d.ts +19 -2
  313. package/dist/prompt-assembly/catalog/schema.js +3 -0
  314. package/dist/prompt-assembly/composer.d.ts +2 -0
  315. package/dist/prompt-assembly/composer.js +1 -1
  316. package/dist/prompt-assembly/overlay.d.ts +21 -11
  317. package/dist/prompt-assembly/overlay.js +57 -19
  318. package/dist/prompt-assembly/select.d.ts +21 -6
  319. package/dist/prompt-assembly/select.js +41 -4
  320. package/dist/prompts/default.d.ts +1 -12
  321. package/dist/prompts/default.js +0 -3
  322. package/dist/shell/lex.d.ts +87 -8
  323. package/dist/shell/lex.js +283 -49
  324. package/dist/stores/file/index.d.ts +8 -6
  325. package/dist/stores/file/permission-rule-store.d.ts +1 -1
  326. package/dist/stores/file/permission-rule-store.js +2 -1
  327. package/dist/tools/artifact/artifact-tool.js +14 -3
  328. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  329. package/dist/tools/fs/bash-program-position.js +3 -10
  330. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  331. package/dist/tools/fs/fs-bash.d.ts +3 -3
  332. package/dist/tools/fs/fs-bash.js +72 -101
  333. package/dist/tools/fs/fs-read.d.ts +12 -0
  334. package/dist/tools/fs/fs-read.js +10 -9
  335. package/dist/tools/fs/fs-shared.d.ts +27 -31
  336. package/dist/tools/fs/fs-shared.js +12 -15
  337. package/dist/tools/fs/fs-write.js +1 -1
  338. package/dist/tools/fs/index.js +2 -2
  339. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  340. package/dist/tools/fs/read-deny-home.js +5 -4
  341. package/dist/tools/fs/read-deny.d.ts +16 -3
  342. package/dist/tools/fs/read-deny.js +12 -9
  343. package/dist/tools/fs/safety.d.ts +7 -6
  344. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  345. package/dist/tools/fs/shell-read-bindings.js +15 -16
  346. package/dist/tools/fs/shell-read-device.js +1 -1
  347. package/dist/tools/fs/shell-read-plan.js +36 -47
  348. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  349. package/dist/tools/fs/shell-read-word.js +2 -20
  350. package/dist/tools/fs/shell-reentry-text.js +1 -1
  351. package/dist/tools/web.d.ts +2 -2
  352. package/package.json +4 -1
  353. package/test/export-surface.snapshot.json +5651 -5457
  354. package/wire-changes/core@7.53.0.json +1968 -0
  355. package/wire-changes/core@7.54.0.json +2870 -0
  356. package/dist/core/shell-scan.d.ts +0 -60
  357. package/dist/core/shell-scan.js +0 -183
@@ -0,0 +1,85 @@
1
+ import { canonicalize } from "./canonical-json.js";
2
+ import { licenseCovers } from "./tool-policy.js";
3
+ import { parkedSkillApprovalRefusal } from "./skill-grant.js";
4
+ import { toolIdentityOf } from "./tool-roster.js";
5
+ import { inlineUntrusted } from "./untrusted-text.js";
6
+ export const APPROVAL_TARGET_ROLES = Object.freeze(["protective", "declared"]);
7
+ export function approvalObjectOf(row) {
8
+ const { capabilityId, contractId, shapeDigest, implementationRevision, family, pathTarget, targets, boundGrantDigest } = row;
9
+ const carried = [capabilityId, contractId, shapeDigest, implementationRevision].some((m) => m !== undefined);
10
+ return {
11
+ ...(carried ? { tool: { capabilityId, contractId, shapeDigest, implementationRevision, family, pathTarget } } : {}),
12
+ ...(targets !== undefined ? { targets } : {}),
13
+ ...(boundGrantDigest !== undefined ? { grants: boundGrantDigest } : {}),
14
+ };
15
+ }
16
+ export function approvalTargetsOf(reading) {
17
+ const targets = {};
18
+ for (const role of APPROVAL_TARGET_ROLES) {
19
+ const view = reading?.[role];
20
+ if (view?.kind === "path" && (view.canonical !== undefined || view.unread === true))
21
+ targets[role] = view.canonical ?? null;
22
+ }
23
+ return Object.keys(targets).length > 0 ? { targets } : {};
24
+ }
25
+ export const APPROVAL_STATION_MEMBERS = Object.freeze({
26
+ claim: Object.freeze(["grants"]),
27
+ session: Object.freeze(["tool"]),
28
+ execution: Object.freeze(["tool", "grants", "targets"]),
29
+ });
30
+ const IDENTITY_MEMBERS = Object.freeze(["capabilityId", "contractId", "shapeDigest", "implementationRevision"]);
31
+ const shown = (s) => `"${inlineUntrusted(s, 240)}"`;
32
+ const vetoed = (name, why) => `The approved tool call "${name}" was not executed: ${why}, so the approval does not cover it (resume.target_moved). This approval is spent — the call has to be re-issued: its new card shows what it would act on now.`;
33
+ function faceKey(family, pt) {
34
+ const slot = pt === undefined ? null : { param: pt.param, access: pt.access, aliases: [...(pt.aliases ?? [])], base: pt.base ?? "cwd", absent: pt.absent ?? "none", patternParam: pt.patternParam ?? null, skillScopeEligible: pt.skillScopeEligible === true };
35
+ return canonicalize({ family: family ?? null, pathTarget: slot });
36
+ }
37
+ function judgeTool(row, live) {
38
+ const entry = live.tool;
39
+ if (entry === undefined)
40
+ return { member: "tool", disposition: "reopen", code: "resume.tool_unavailable", message: `the approved tool "${row.toolName}" is no longer available on resume — refusing to continue as if it ran; the checkpoint is reopened for a retry with the tool present` };
41
+ const bound = approvalObjectOf(row).tool;
42
+ if (bound === undefined)
43
+ return undefined;
44
+ const id = toolIdentityOf(entry);
45
+ const differ = IDENTITY_MEMBERS.filter((k) => bound[k] !== id[k]);
46
+ if (differ.length > 0) {
47
+ const named = differ.map((k) => (bound[k] === undefined ? `${k} (missing on the row)` : k)).join(", ");
48
+ return { member: "tool", disposition: "reopen", code: "resume.tool_contract_mismatch", message: `the approved tool "${row.toolName}" on this leg is not the tool the approval was minted against (${named} differ) — refusing to execute a replacement wearing the name; the checkpoint is reopened for a retry with the original tool present` };
49
+ }
50
+ if (faceKey(bound.family, bound.pathTarget) === faceKey(entry.family, entry.pathTarget))
51
+ return undefined;
52
+ return { member: "tool", disposition: "veto", code: "resume.target_moved", message: vetoed(row.toolName, "the tool's face (its display family or the path target it declares) changed after the approval") };
53
+ }
54
+ function judgeTargets(row, live) {
55
+ const bound = row.targets;
56
+ if (bound === undefined || ("decided" in live && !licenseCovers(row.boundInputHash, live.decided)))
57
+ return undefined;
58
+ const roles = typeof bound === "object" && bound !== null && !Array.isArray(bound) ? Object.entries(bound) : [];
59
+ const whole = roles.length > 0 && roles.every(([k, v]) => APPROVAL_TARGET_ROLES.includes(k) && (v === null || (typeof v === "string" && v.length > 0)));
60
+ if (!whole)
61
+ return { member: "targets", disposition: "veto", code: "resume.target_moved", message: vetoed(row.toolName, "the target member of the approval on its checkpoint row is damaged (not a reading → resolved location record)") };
62
+ const now = approvalTargetsOf(live.targetReading).targets ?? {};
63
+ const where = (p) => (p === null ? "did not resolve" : `resolved to ${shown(p)}`);
64
+ for (const [role, was] of roles) {
65
+ const is = now[role];
66
+ if (is === was)
67
+ continue;
68
+ return { member: "targets", disposition: "veto", code: "resume.target_moved", message: vetoed(row.toolName, is === undefined ? `where its target resolves (it ${where(was)} when the card was asked) could not be established on this leg` : `its target ${is === null ? "does not resolve" : `resolves to ${shown(is)}`} now and ${where(was)} when the card was asked`) };
69
+ }
70
+ return undefined;
71
+ }
72
+ function judgeGrants(row, live, station) {
73
+ const refusal = parkedSkillApprovalRefusal(row, live.args, live.skills ?? [], live.skillToolName ?? "", station);
74
+ if (refusal === undefined)
75
+ return undefined;
76
+ return station === "claim" ? { member: "grants", disposition: "refuse_pending", code: "checkpoint.invalid_outcome", message: refusal } : { member: "grants", disposition: "veto", message: refusal };
77
+ }
78
+ export function judgeApprovalObject(row, live, station) {
79
+ for (const member of APPROVAL_STATION_MEMBERS[station]) {
80
+ const miss = member === "tool" ? judgeTool(row, live) : member === "targets" ? judgeTargets(row, live) : judgeGrants(row, live, station === "claim" ? "claim" : "execution");
81
+ if (miss !== undefined)
82
+ return miss;
83
+ }
84
+ return "match";
85
+ }
@@ -2,8 +2,9 @@ import { type Egress } from "./untrusted-egress.js";
2
2
  /** A SHORT, SECRET-SCRUBBED primary-arg summary. Abbreviation REDUCES the surface; the `same-principal` tier's pass
3
3
  * is the leak boundary, on whichever field the summary came from (the value is PERSISTED + SSE-forwarded and may be
4
4
  * read by a platform operator in a multi-tenant deployment). Rules:
5
- * • command → the command NAME (skip leading `VAR=val` env-prefixes; bail to `undefined` on a quoted assignment
6
- * or a lone assignment; basename a path-form command; `+ …` if it had args).
5
+ * • command → the command NAME: the first segment's written command word past its control words and the
6
+ * assignments bash strips (bail to `undefined` on an unparseable text, a lone assignment, an assignment-shaped
7
+ * or computed command word; basename a path-form command; `+ …` if more follows).
7
8
  * • file path → BASENAME only (no host directory layout).
8
9
  * • url → `new URL` origin+pathname (drops `user:pass@` userinfo + query/fragment).
9
10
  * • else → pattern/query/description, else the first string field whose name is no credential name.
@@ -1,4 +1,5 @@
1
- import { writtenAssignmentPrefix } from "../tools/fs/bash-program-position.js";
1
+ import { readProgramPosition } from "../tools/fs/bash-program-position.js";
2
+ import { argvOf, lexShell } from "../shell/lex.js";
2
3
  import { isSensitiveRecordKey } from "./credential-vocabulary.js";
3
4
  import { egressText } from "./untrusted-egress.js";
4
5
  const ACTIVITY_ARG_MAX = 80;
@@ -17,17 +18,22 @@ export function primaryActivityArg(args) {
17
18
  const path = str("file_path") ?? str("filePath") ?? str("path") ?? str("notebook_path");
18
19
  const url = str("url");
19
20
  if (command !== undefined) {
20
- const tokens = command.trim().split(/\s+/);
21
- let i = 0;
22
- while (i < tokens.length && writtenAssignmentPrefix(tokens[i]) !== undefined) {
23
- if (/["'\\]/.test(tokens[i]))
24
- return undefined;
25
- i++;
26
- }
27
- if (i >= tokens.length)
21
+ const lexed = lexShell(command);
22
+ if ("parseFailure" in lexed)
23
+ return undefined;
24
+ const named = lexed.segments.map((segment) => {
25
+ const argv = argvOf(segment);
26
+ const pos = readProgramPosition(argv);
27
+ return { segment, word: pos.written === undefined ? undefined : argv[pos.written + pos.assignments.length] };
28
+ }).find((s) => s.word !== undefined);
29
+ if (named?.word === undefined)
30
+ return undefined;
31
+ const word = named.word;
32
+ if (word.assignment !== undefined || word.expansions.length > 0 || word.unresolved !== undefined)
28
33
  return undefined;
29
- const name = basename(tokens[i]);
30
- val = tokens.length > i + 1 ? `${name} …` : name;
34
+ const name = basename(word.folded);
35
+ const more = named.segment.tokens.indexOf(word) < named.segment.tokens.length - 1 || lexed.segments.slice(lexed.segments.indexOf(named.segment) + 1).some((s) => s.tokens.length > 0);
36
+ val = more ? `${name} …` : name;
31
37
  }
32
38
  else if (path !== undefined) {
33
39
  val = basename(path);
@@ -113,8 +113,8 @@ export interface AskOriginFacts {
113
113
  /** Which of the gate's OWN post-fold stations minted the standing ask, if one did — again the gate's
114
114
  * record of its own layer (a station replaces an `allow` wholesale, so at most one fires per pass),
115
115
  * never a read-back of `decisionReason: "safety"` off a decision a policy could have composed. `unstated` is
116
- * the last station: the mode's answer for a call to a tool that states no permission of its own when no layer
117
- * stated the allow — its origin is `policy` (no tighten word), its class `unstated`. */
116
+ * the last station: the mode's answer for a call to a tool that states no permission of its own when no answer
117
+ * covers the fold's allow — its origin is `policy` (no tighten word), its class `unstated`. */
118
118
  readonly tightened: "shell_gate" | "safety" | "unstated" | undefined;
119
119
  /** The MANDATE mark inputs of this call — the tool SEAT's marks plus the gate's own per-call records
120
120
  * ({@link MandateMarks}), read from the same two places the lane's own derivation reads them. The
@@ -191,19 +191,15 @@ export type RuleOffersAbsence = (typeof RULE_OFFERS_ABSENCES)[number];
191
191
  * the reason is that a RULE is already speaking, not that the question is beyond rules, and a surface
192
192
  * that auto-answers by posture is not crossing a mandate when it answers one.
193
193
  *
194
- * FOUR named readings of the one pass, because four callers need four different things and taking
195
- * the wrong one is how this boundary drifts:
194
+ * THREE named readings of the one pass, because three callers need three different things and taking
195
+ * the wrong one is how this boundary drifts. None is the ENFORCEMENT reading: whether an allow rule clears the ask is the
196
+ * class row's `ruleMayClear` ({@link classifyAsk} — every door below is a class there), read once by the gate's allow layer.
196
197
  * · `word` — the DISCLOSED reason ({@link RuleOffersAbsence}), first door wins, in the display order the card has always
197
198
  * used (governance, the outranked allow, the hook, the ask rule, the two ancestor doors, then the marks). Display
198
199
  * only. A structural door the offer factory owns (no lane wired, a tool it does not speak for, a
199
200
  * task that cannot hold a rule) is decided BEFORE this and is deliberately silent;
200
201
  * · `mandated` — the FACT the ask carries as its own member. Read off the whole mandate set, so which
201
202
  * door is DISPLAYED cannot change it: an ask that is both outranked and mandated is mandated;
202
- * · `ruleMayClear` — the ENFORCEMENT reading the gate's clearing arms take. It is `mandated` plus the
203
- * person's explicit `ask` rule, and deliberately NOT `word !== undefined`: `allowRuleShadowed`
204
- * is an ACCOUNT of a refusal that already happened, display-only by contract, and reading it as a
205
- * blocker would let a policy forwarding that metadata refuse a call nothing is blocking. A
206
- * display word is never an authority;
207
203
  * · `mandate` — WHICH table word the bit stands on, only when the MARKS family is its whole reason (no
208
204
  * governance, hook or ancestor door, and no bit stamped without a word); absent, the bit's reason is
209
205
  * not one word.
@@ -211,7 +207,6 @@ export type RuleOffersAbsence = (typeof RULE_OFFERS_ABSENCES)[number];
211
207
  export declare function ruleClearanceOf(ask: RuleClearanceFacts | undefined, marks: Omit<MandateMarks, "probeMandated">): {
212
208
  word?: Exclude<RuleOffersAbsence, "lane_cannot_speak">;
213
209
  mandated: boolean;
214
- ruleMayClear: boolean;
215
210
  mandate?: PersistedRuleMandate;
216
211
  };
217
212
  /** The facts one ask carries about whether a rule could clear it — the members {@link ruleClearanceOf}
@@ -235,6 +230,8 @@ export interface RuleClearanceFacts {
235
230
  /** The engine-stamped probe mandate (the reversibility probe declared this call's demotion
236
231
  * structural — for the built-in shell probe, a read outside the session's allowed directories). */
237
232
  probeMandated?: boolean;
233
+ /** The ask stands on the call's cross-boundary obligation (see {@link import("./tool-policy.js").PermissionResult}). */
234
+ crossBoundaryMandated?: boolean;
238
235
  /** An ancestor's approver answered "unavailable" for this call, or a durable mandate was floated here. */
239
236
  inheritedUnresolved?: boolean;
240
237
  /** The ask is being resolved at an ANCESTOR's frozen approver (the three inherited-lane mint sites
@@ -309,8 +306,11 @@ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<As
309
306
  * re-authorizes the agent, so the question is a property of the TARGET, not of anyone's hesitation.
310
307
  * - `target_unresolved` — the reading of a written target could not say where it leads on disk:
311
308
  * `probe_unanswered`'s doctrine one layer over.
309
+ * - `cross_boundary_taint` — the call's cross-boundary obligation is a person's confirmation (an org-side write from a tree
310
+ * that executes on a device of a multi-tenant deployment, or a device-bound call after an off-target read): a fact of
311
+ * the tree, not of the tool; read off the ask's own `crossBoundaryMandated` ({@link ruleClearanceOf}).
312
312
  */
313
- export declare const PERSISTED_RULE_MANDATES: readonly ["operator_always", "tool_marks", "probe_mandate", "probe_unanswered", "write_protection", "target_unresolved"];
313
+ export declare const PERSISTED_RULE_MANDATES: readonly ["operator_always", "tool_marks", "probe_mandate", "probe_unanswered", "write_protection", "target_unresolved", "cross_boundary_taint"];
314
314
  export type PersistedRuleMandate = (typeof PERSISTED_RULE_MANDATES)[number];
315
315
  /** Membership test for {@link PersistedRuleMandate} — the screen a row reader applies to a stored word. */
316
316
  export declare function isPersistedRuleMandate(v: unknown): v is PersistedRuleMandate;
@@ -323,6 +323,7 @@ declare const MANDATE_DISCLOSURE: {
323
323
  readonly tool_marks: "this tool carries egress/irreversibility/interaction marks (a mandated confirmation a rule cannot clear)";
324
324
  readonly write_protection: "this write's target is on the deployment's write-protection table (the agent's own configuration is guarded one look per write) — it is cleared by confirming this call, never by a standing allow rule";
325
325
  readonly target_unresolved: "where this write leads on disk could not be determined (a link on the way could not be examined, or the environment could not resolve the target), so whether it lands on a guarded path is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can";
326
+ readonly cross_boundary_taint: "this run executes on a device outside the deployment's trust boundary and the deployment is multi-tenant, so an org-side write — or, after an off-target read, a call bound for the device — is confirmed by the deployment's approver for this call: no standing allow rule and no automatic lane answers for it, and an allow the configuration gives (a blanket seat or a policy's answer) is refused";
326
327
  };
327
328
  /** The fence over the disclosure table: `never` while every word has a row. */
328
329
  export type DisclosureTableCoversEveryMandate = AssertAllKeysHandled<Exclude<PersistedRuleMandate, keyof typeof MANDATE_DISCLOSURE>>;
@@ -331,10 +332,9 @@ export type DisclosureTableCoversEveryMandate = AssertAllKeysHandled<Exclude<Per
331
332
  export declare function mandateDisclosureOf(mandate: PersistedRuleMandate | undefined): string;
332
333
  /**
333
334
  * The mandate provenance of one call, judged from the SAME mark inputs the gate is driven with —
334
- * the single source for "could a persisted allow rule clear this ask?". Allow rules silence the
335
- * classifier's questions, never a mandated one, and this predicate is the mandated-family half of
336
- * that boundary (the real-approval/governance half rides the decision's own `requiresRealApproval`
337
- * bit, which the org layer stamps):
335
+ * the WORD a mandated ask discloses for its marks (the class derivation reads the same marks as classes,
336
+ * and the allow layer reads the classes). Allow rules silence the classifier's questions, never a
337
+ * mandated one; this is the mark family's word for that boundary:
338
338
  * · `probeMandated` — this CALL's own reversibility probe declared its demotion STRUCTURAL
339
339
  * (`"probe_mandate"`), judged FIRST because it is the only per-call member here: the three below
340
340
  * are properties of the TOOL and are true of every call on the seat, so a mandate that is true of
@@ -355,10 +355,11 @@ export declare function mandateDisclosureOf(mandate: PersistedRuleMandate | unde
355
355
  * in {@link irreversibilityMandateOf} which {@link safetyAxisOf} shares: such a write is not an irreversible action, and
356
356
  * its park stays budgetable.
357
357
  *
358
- * Two consumers, one derivation: the gate's silencing arm (a matching rule is disclosed as outranked
359
- * instead of clearing the ask) and the runner's suggestion factory (a mandated ask offers no
360
- * "stop asking me this" option — a rule minted from it would never clear it). A drift between the
361
- * two would let a card offer a rule the lane then refuses to honor. The per-call member reaches both
358
+ * Two consumers, one derivation: the ask's stamped `mandated` / `mandate` members and the runner's suggestion factory (a
359
+ * mandated ask offers no "stop asking me this" option — a rule minted from it would never clear it). Every word here is also a
360
+ * class the allow layer reads, so an ask this derivation mandates is never cleared by a rule; the converse does not hold — a
361
+ * class with no word here (a caller policy's own guard on an ask without the real-approval bit) leaves the ask unmandated, and its
362
+ * card can offer a rule the allow layer then discloses as outranked. The per-call member reaches both
362
363
  * the same way every other per-call fact does: the gate stamps it on the surviving ask, the
363
364
  * synchronous sites read it off the decision they spread, and the park leg threads its own parameter.
364
365
  */
@@ -465,8 +466,10 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
465
466
  * `requires_user_interaction` is the SERVER's declaration on the called tool (`_meta["anthropic/requiresUserInteraction"] === true`): a
466
467
  * permission question, unlike `content_question` (the reserved question tool's content-routing signal) — it carries that class's arbitration
467
468
  * row cell by cell, because the reference implementation judges the declaration ahead of its allow-rule lookup and its bypass release.
469
+ * `cross_boundary` is an ask carrying `crossBoundaryMandated` (the gate's cross-boundary station mints it with the real-approval bit; the
470
+ * decision receipt adds the bit to a policy's or hook's own): no rule, classifier, sandbox or mode answers it.
468
471
  * `unstated` is the mode's answer for a call nobody stated: a tool whose permission seat is `passthrough` (it states no
469
- * permission of its own — a remote peer's tool) reached the end of the fold with an allow no layer stated.
472
+ * permission of its own — a remote peer's tool) reached the end of the fold with an allow no answer covers.
470
473
  * `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
471
474
  * (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
472
475
  * answers it; the classifier is never asked again).
@@ -480,7 +483,7 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
480
483
  * `rule_unreadable` is a rule that may govern the call and cannot be read for it: the organization's snapshot, a partition of
481
484
  * the person's store, or this call against a deny / ask row (the lexer's word, a content row, the read station's rows).
482
485
  */
483
- export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "dangerous_delete", "unverifiable_delete", "unverifiable_shell_text", "transcript_write", "org_rule", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "target_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
486
+ export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "dangerous_delete", "unverifiable_delete", "unverifiable_shell_text", "transcript_write", "org_rule", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "cross_boundary", "operator_forced", "shell_floor", "target_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
484
487
  export type AskClass = (typeof ASK_CLASSES)[number];
485
488
  /** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
486
489
  export declare function isAskClass(v: unknown): v is AskClass;
@@ -504,7 +507,8 @@ export interface ModeArbitrationRow {
504
507
  * The mode × class table. `bypassImmune` is the one new column (nothing said it before); the classifier and
505
508
  * sandbox columns are the fold of the two former per-word lane tables (only `probe` moved: the classifier may
506
509
  * now answer it — the read boundary's unestablishable spelling is fed to it as a deny-table row instead), and
507
- * `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
510
+ * `ruleMayClear` is the allow layer's ONE reading: an allow member clears a standing ask exactly when the folded row says so
511
+ * (every class a real-approval bit, a governance verdict, a hook, a mark or an ask rule brings says no).
508
512
  * `probe`'s `classifierEligible` is the OPEN read face's reading: a narrowed (`roots`) face withholds the classifier from the class,
509
513
  * folded where the ask's row is ({@link foldArbitrationRow}).
510
514
  * `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
@@ -610,6 +614,13 @@ export declare const MODE_ARBITRATION: {
610
614
  readonly ruleMayClear: false;
611
615
  readonly hookAskFloor: true;
612
616
  };
617
+ readonly cross_boundary: {
618
+ readonly bypassImmune: true;
619
+ readonly classifierEligible: false;
620
+ readonly sandboxAdmission: false;
621
+ readonly ruleMayClear: false;
622
+ readonly hookAskFloor: false;
623
+ };
613
624
  readonly operator_forced: {
614
625
  readonly bypassImmune: true;
615
626
  readonly classifierEligible: true;
@@ -751,6 +762,9 @@ export interface AskClassification {
751
762
  readonly classes: ReadonlySet<AskClass>;
752
763
  readonly askClass: AskClass;
753
764
  readonly row: ModeArbitrationRow;
765
+ /** The tool face the classification was derived on ({@link AskClassFacts.askFace}); absent ⇒ the face-less reading, the `other`
766
+ * face's. The mode point and the headless remedy read the face here and nowhere else ({@link bypassImmuneOf}). */
767
+ readonly askFace?: AskFace;
754
768
  }
755
769
  /** The two members an ask's classification puts on the approval request and the park row: the disclosed word and
756
770
  * the whole set, the set as an array in {@link ASK_CLASSES} order (so `classes[0] === askClass`). */
@@ -859,14 +873,16 @@ export type Arbitration = {
859
873
  * The ONE mode read point. `dontAsk` refuses every ask — one value for every class — but a hook's that no member of the
860
874
  * set floors (`hookAskFloor`: a hook's ask beside a rule's or a safety check's ask is refused, as the reference's full
861
875
  * pipeline refuses it; a hook's ask alone is the reference's forced decision, which never reaches that conversion);
862
- * `bypassPermissions` refuses a write whose target's reading could not say where it leads where that class stays immune
863
- * (every face but the shell's: a person cannot be asked — the folded row says it, {@link foldArbitrationRow}), keeps every
864
- * immune class and releases the rest: the shell face releases that class ({@link RELEASED_ON_SHELL_FACE}), the reference
865
- * implementation's answer for a command whose written target could not be resolved;
876
+ * `bypassPermissions` refuses a write whose target's reading could not say where it leads on a face where THAT class stays
877
+ * immune (every face but the shell's: a person cannot be asked — the class's own reading on the face the classification records,
878
+ * {@link bypassImmuneOn}; another immune class beside it does not make it immune), keeps every immune class and releases the
879
+ * rest: the shell face releases that class ({@link RELEASED_ON_SHELL_FACE}), the reference implementation's answer for a
880
+ * command whose written target could not be resolved — so on that face an immune class beside it keeps the question;
866
881
  * `acceptEdits` answers an ORDINARY ask itself on a call the caller vouches is one of the mode's file commands
867
882
  * (`call.fileCommand`, `accepted`); `plan`, `default` and `auto` hand every ask on (plan's own questions were raised by
868
883
  * its write gate; the reference implementation refuses nothing at this point under plan — auto's classifier reads the
869
- * row at its own station). Reads the class SET and the folded row — never the disclosed word.
884
+ * row at its own station). Reads the class SET on the face the classification records ({@link bypassImmuneOf}) and the folded
885
+ * row's face-free `hookAskFloor` — never the row's `bypassImmune`, never the disclosed word.
870
886
  */
871
887
  export declare function arbitrateByMode(mode: PermissionMode, c: AskClassification, disclosure?: {
872
888
  origin?: AskOrigin;
@@ -25,7 +25,7 @@ export function stampAskOrigin(decision, facts, classification) {
25
25
  }
26
26
  export const RULE_OFFERS_ABSENCES = Object.freeze(["mandated", "allow_outranked", "ask_rule_matched", "lane_cannot_speak"]);
27
27
  export function ruleClearanceOf(ask, marks) {
28
- const marksWord = persistedRuleMandateOf({ ...marks, probeMandated: ask?.probeMandated === true });
28
+ const marksWord = ask?.crossBoundaryMandated === true ? "cross_boundary_taint" : persistedRuleMandateOf({ ...marks, probeMandated: ask?.probeMandated === true });
29
29
  const foldedDoor = (ask?.foldedAskClasses ?? []).some((c) => !MODE_ARBITRATION[c].ruleMayClear);
30
30
  const doorOutsideTable = ask?.requiresRealApproval === true || ask?.decisionReason === "hook" || ask?.decisionReason === "mode" || foldedDoor || ask?.authoredBy === "deployment" || ask?.inheritedUnresolved === true || ask?.ancestorResolved === true;
31
31
  const mandated = ask?.mandated === true || doorOutsideTable || marksWord !== undefined;
@@ -50,7 +50,7 @@ export function ruleClearanceOf(ask, marks) {
50
50
  return "mandated";
51
51
  return mandated ? "mandated" : undefined;
52
52
  })();
53
- return { ...(word !== undefined ? { word } : {}), mandated, ruleMayClear: !mandated && ask?.matchedAskRule === undefined, ...(mandate !== undefined ? { mandate } : {}) };
53
+ return { ...(word !== undefined ? { word } : {}), mandated, ...(mandate !== undefined ? { mandate } : {}) };
54
54
  }
55
55
  export function askMandateMember(ask) {
56
56
  const { mandated, mandate } = ruleClearanceOf(ask, {});
@@ -58,8 +58,9 @@ export function askMandateMember(ask) {
58
58
  return mandated ? { mandated: true, ...(mandate !== undefined ? { mandate } : {}), ...authored } : authored;
59
59
  }
60
60
  export function ownedClearanceFacts(d) {
61
- const { mandated, requiresRealApproval, decisionReason, matchedAskRule, probeMandated, authoredBy, foldedAskClasses } = d;
61
+ const { mandated, requiresRealApproval, decisionReason, matchedAskRule, probeMandated, crossBoundaryMandated, authoredBy, foldedAskClasses } = d;
62
62
  return {
63
+ ...(crossBoundaryMandated === true ? { crossBoundaryMandated } : {}),
63
64
  ...(mandated === true ? { mandated } : {}),
64
65
  ...(Array.isArray(foldedAskClasses) && foldedAskClasses.length > 0 ? { foldedAskClasses: [...foldedAskClasses] } : {}),
65
66
  ...(authoredBy === "deployment" ? { authoredBy } : {}),
@@ -106,7 +107,7 @@ export const ORIGIN_IMPLIES_REAL_APPROVAL = {
106
107
  safety_tighten: false,
107
108
  policy: false,
108
109
  };
109
- export const PERSISTED_RULE_MANDATES = Object.freeze(["operator_always", "tool_marks", "probe_mandate", "probe_unanswered", "write_protection", "target_unresolved"]);
110
+ export const PERSISTED_RULE_MANDATES = Object.freeze(["operator_always", "tool_marks", "probe_mandate", "probe_unanswered", "write_protection", "target_unresolved", "cross_boundary_taint"]);
110
111
  const PERSISTED_RULE_MANDATE_SET = new Set(PERSISTED_RULE_MANDATES);
111
112
  export function isPersistedRuleMandate(v) {
112
113
  return PERSISTED_RULE_MANDATE_SET.has(v);
@@ -118,6 +119,7 @@ const MANDATE_DISCLOSURE = {
118
119
  tool_marks: "this tool carries egress/irreversibility/interaction marks (a mandated confirmation a rule cannot clear)",
119
120
  write_protection: "this write's target is on the deployment's write-protection table (the agent's own configuration is guarded one look per write) — it is cleared by confirming this call, never by a standing allow rule",
120
121
  target_unresolved: "where this write leads on disk could not be determined (a link on the way could not be examined, or the environment could not resolve the target), so whether it lands on a guarded path is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can",
122
+ cross_boundary_taint: "this run executes on a device outside the deployment's trust boundary and the deployment is multi-tenant, so an org-side write — or, after an off-target read, a call bound for the device — is confirmed by the deployment's approver for this call: no standing allow rule and no automatic lane answers for it, and an allow the configuration gives (a blanket seat or a policy's answer) is refused",
121
123
  };
122
124
  export function mandateDisclosureOf(mandate) {
123
125
  return mandate === undefined ? "an explicit ask rule matched this call (a person's or the deployment's ask-me-each-time outranks a standing allow rule)" : MANDATE_DISCLOSURE[mandate];
@@ -183,6 +185,7 @@ export const ASK_CLASSES = Object.freeze([
183
185
  "denial_limit_fallback",
184
186
  "classifier_no_ruling",
185
187
  "requires_user_interaction",
188
+ "cross_boundary",
186
189
  "operator_forced",
187
190
  "shell_floor",
188
191
  "target_unresolved",
@@ -215,6 +218,7 @@ export const MODE_ARBITRATION = {
215
218
  denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
216
219
  classifier_no_ruling: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
217
220
  requires_user_interaction: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
221
+ cross_boundary: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
218
222
  operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
219
223
  shell_floor: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
220
224
  target_unresolved: { bypassImmune: true, classifierEligible: false, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true },
@@ -236,8 +240,11 @@ const RELEASED_ON_SHELL_FACE = new Set(["target_unresolved"]);
236
240
  function bypassImmuneOn(c, face) {
237
241
  return MODE_ARBITRATION[c].bypassImmune && !(face === "shell" && RELEASED_ON_SHELL_FACE.has(c));
238
242
  }
243
+ function bypassImmuneOf(c) {
244
+ return [...c.classes].some((k) => bypassImmuneOn(k, c.askFace));
245
+ }
239
246
  const CLASSIFIER_WITHHELD_ON_ROOTS = new Set(["probe"]);
240
- export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "unverifiable_shell_text", "transcript_write", "org_rule", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
247
+ export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "unverifiable_shell_text", "transcript_write", "org_rule", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction", "cross_boundary"]);
241
248
  const RELEASABLE_BY_FACE = Object.freeze({
242
249
  shell: Object.freeze(ASK_CLASSES.filter((c) => !bypassImmuneOn(c, "shell"))),
243
250
  other: Object.freeze(ASK_CLASSES.filter((c) => !bypassImmuneOn(c, "other"))),
@@ -280,6 +287,8 @@ export function decisionAskClassesOf(d) {
280
287
  out.add("operator_forced");
281
288
  if (d.probeMandated === true)
282
289
  out.add("probe");
290
+ if (d.crossBoundaryMandated === true)
291
+ out.add("cross_boundary");
283
292
  if (d.decisionReason === "hook")
284
293
  out.add("hook");
285
294
  if (d.decisionReason === "mode")
@@ -323,7 +332,7 @@ export function classifyAsk(d, facts) {
323
332
  classes.add("requires_user_interaction");
324
333
  if (marks.shellGated === true && marks.irreversibility === "always")
325
334
  classes.add("shell_floor");
326
- const toolMarked = marks.shellGated !== true && (marks.egress === true || marks.irreversibility === "always" || marks.irreversibility === "maybe");
335
+ const toolMarked = marks.egress === true || (marks.shellGated !== true && (marks.irreversibility === "always" || marks.irreversibility === "maybe"));
327
336
  if (toolMarked) {
328
337
  const sources = facts.toolMarkSources;
329
338
  if (sources === undefined || sources.size === 0 || sources.has("deployment"))
@@ -352,7 +361,7 @@ export function classifyAsk(d, facts) {
352
361
  if (classes.size === 0)
353
362
  classes.add("ordinary");
354
363
  const askClass = ASK_CLASSES.find((c) => classes.has(c));
355
- return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace, facts.askFace) };
364
+ return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace, facts.askFace), ...(facts.askFace !== undefined ? { askFace: facts.askFace } : {}) };
356
365
  }
357
366
  export function headlessDenialMessage(toolName, askMessage) {
358
367
  return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
@@ -366,12 +375,12 @@ export function headlessApprovalRemedy(input) {
366
375
  const c = input.classification;
367
376
  if (input.mode === "acceptEdits" && input.unplacedOptions !== undefined && (c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"))))
368
377
  return `${fileCommandOptionsSentence(input.unplacedOptions.program)} Spell the ${input.unplacedOptions.program} without option words for the mode to answer it, or ${approver}`;
369
- if (c?.row.bypassImmune === true)
378
+ if (c !== undefined && bypassImmuneOf(c))
370
379
  return `no permission mode releases this ask (${c.askClass}): ${approver}, or change the call`;
371
380
  if (input.mode === "plan" && c?.classes.has("mode") === true)
372
381
  return `the plan permission mode asks before every write and every call to a tool the engine does not define that is not read-only — leave plan mode (\`setPermissionMode\` to another mode, or approve the plan) to run it, or ${approver}`;
373
382
  if (c !== undefined && c.classes.size === 1 && c.classes.has("unstated"))
374
- return `no rule, policy or hook granted this tool, and it states no permission of its own — grant it with an allow rule (a permission rule \`allow\` for its name, or an allow-list entry), run with \`permissionMode: "bypassPermissions"\`, or ${approver}`;
383
+ return `no allow entry, stored allow rule or hook granted this tool, and it states no permission of its own — grant it with an allow entry for its name (\`RunnerDeps.allowedTools\` / \`TaskSpec.allowedTools\`), run with \`permissionMode: "bypassPermissions"\`, or ${approver}`;
375
384
  const onlyOrdinary = c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"));
376
385
  if (input.coveredWrite && onlyOrdinary) {
377
386
  if (input.mode === "acceptEdits")
@@ -391,9 +400,9 @@ export function arbitrateByMode(mode, c, disclosure, call) {
391
400
  case "dontAsk":
392
401
  return has("hook") && !c.row.hookAskFloor ? { kind: "ask" } : { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
393
402
  case "bypassPermissions":
394
- if (has("target_unresolved") && c.row.bypassImmune)
403
+ if (has("target_unresolved") && bypassImmuneOn("target_unresolved", c.askFace))
395
404
  return { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
396
- if (c.row.bypassImmune)
405
+ if (bypassImmuneOf(c))
397
406
  return { kind: "ask" };
398
407
  const reason = [...MODE_RELEASE_REASON_SET].find((r) => has(r));
399
408
  return {
@@ -154,7 +154,6 @@ export type QuestionOutcomeReading =
154
154
  * meant to withhold. Neither guess is safe, so neither is made.
155
155
  */
156
156
  export declare function classifyQuestionOutcome(outcome: OnQuestionOutcome): QuestionOutcomeReading;
157
- export declare function canonicalizeCapturedPlainData(node: unknown, seen?: WeakSet<object>): boolean;
158
157
  /**
159
158
  * Narrow an {@link OnQuestionOutcome} to its no-interaction arm — the ONLY value that licenses
160
159
  * re-routing a question to a later decider, so it is read strictly (the discriminant must be present
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { uuidv7 } from "../engine/session/uuid.js";
3
- import { boundInputHashOf } from "./canonical-json.js";
3
+ import { boundInputHashOf, capturedJsonOf } from "./canonical-json.js";
4
4
  import { defineTool, errorResult } from "./tools.js";
5
5
  import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
6
6
  import { toolFace } from "./tool-catalog-entries.js";
@@ -25,73 +25,10 @@ export function classifyQuestionOutcome(outcome) {
25
25
  return { shape: "unavailable" };
26
26
  if (!carriesAnswers)
27
27
  return { shape: "contradictory" };
28
- let captured;
29
- try {
30
- captured = structuredClone(answers);
31
- }
32
- catch {
28
+ const captured = capturedJsonOf(answers, "list");
29
+ if (!captured.ok)
33
30
  return { shape: "contradictory" };
34
- }
35
- if (!isCanonicalAnswerTree(captured))
36
- return { shape: "contradictory" };
37
- return { shape: "answered", answer: { answers: captured } };
38
- }
39
- function isCanonicalAnswerTree(answers) {
40
- return canonicalizeCapturedPlainData(answers);
41
- }
42
- function isUnknownArray(v) {
43
- return Array.isArray(v);
44
- }
45
- export function canonicalizeCapturedPlainData(node, seen = new WeakSet()) {
46
- if (node === null)
47
- return true;
48
- switch (typeof node) {
49
- case "string":
50
- case "boolean":
51
- return true;
52
- case "number":
53
- return Number.isFinite(node);
54
- case "object":
55
- break;
56
- default:
57
- return false;
58
- }
59
- const obj = node;
60
- if (seen.has(obj))
61
- return false;
62
- seen.add(obj);
63
- if (isUnknownArray(obj)) {
64
- const arr = obj;
65
- for (let i = 0; i < arr.length; i++) {
66
- if (!Object.prototype.hasOwnProperty.call(arr, i))
67
- return false;
68
- if (arr[i] === undefined)
69
- return false;
70
- if (Object.is(arr[i], -0))
71
- arr[i] = 0;
72
- if (!canonicalizeCapturedPlainData(arr[i], seen))
73
- return false;
74
- }
75
- if (Object.keys(arr).length !== arr.length)
76
- return false;
77
- }
78
- else {
79
- const proto = Object.getPrototypeOf(obj);
80
- if (proto !== Object.prototype && proto !== null)
81
- return false;
82
- const rec = obj;
83
- for (const key of Object.keys(rec)) {
84
- if (rec[key] === undefined) {
85
- delete rec[key];
86
- continue;
87
- }
88
- if (Object.is(rec[key], -0))
89
- rec[key] = 0;
90
- if (!canonicalizeCapturedPlainData(rec[key], seen))
91
- return false;
92
- }
93
- }
94
- return true;
31
+ return { shape: "answered", answer: { answers: captured.value } };
95
32
  }
96
33
  export function isQuestionUnavailable(outcome) {
97
34
  return classifyQuestionOutcome(outcome).shape === "unavailable";
@@ -48,8 +48,9 @@ export interface AskUnresolvableFacts {
48
48
  * did not take the call — it declined or failed; `false`: no park lane at all, the usual shape). */
49
49
  readonly parkLaneExisted: boolean;
50
50
  /** Present for the causes that say nobody could be asked ({@link askUnresolvableRemedySeat}): the caller-side change that
51
- * would have let the call run, as the gate minted it for the refusal's tool text (`headlessApprovalRemedy`, ask-origin.ts)
52
- * — carried verbatim, never re-composed here. */
51
+ * would have let the call run, as the gate minted it for the refusal's tool text (`headlessApprovalRemedy`, ask-origin.ts;
52
+ * for arguments no approval can be bound to, the binding's own sentence, which is that text — the remedy's approver half would
53
+ * not let them run) — carried verbatim, never re-composed here. */
53
54
  readonly remedy?: string;
54
55
  }
55
56
  /**