apex-code 0.0.1-alpha.8 → 0.0.4

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 (634) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +9 -8
  3. package/dist/cli/args.d.ts +26 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +72 -10
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +2 -1
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/cli/mcp-command.d.ts +9 -0
  11. package/dist/cli/mcp-command.d.ts.map +1 -0
  12. package/dist/cli/mcp-command.js +53 -0
  13. package/dist/cli/mcp-command.js.map +1 -0
  14. package/dist/cli/project-trust.d.ts.map +1 -1
  15. package/dist/cli/project-trust.js +3 -1
  16. package/dist/cli/project-trust.js.map +1 -1
  17. package/dist/cli/startup-ui.d.ts.map +1 -1
  18. package/dist/cli/startup-ui.js +2 -1
  19. package/dist/cli/startup-ui.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +94 -5
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +3 -2
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +45 -18
  26. package/dist/config.js.map +1 -1
  27. package/dist/core/agent-session-services.d.ts +3 -0
  28. package/dist/core/agent-session-services.d.ts.map +1 -1
  29. package/dist/core/agent-session-services.js +1 -0
  30. package/dist/core/agent-session-services.js.map +1 -1
  31. package/dist/core/agent-session.d.ts +183 -15
  32. package/dist/core/agent-session.d.ts.map +1 -1
  33. package/dist/core/agent-session.js +728 -117
  34. package/dist/core/agent-session.js.map +1 -1
  35. package/dist/core/auth-storage.d.ts.map +1 -1
  36. package/dist/core/auth-storage.js +6 -7
  37. package/dist/core/auth-storage.js.map +1 -1
  38. package/dist/core/bash-executor.d.ts.map +1 -1
  39. package/dist/core/bash-executor.js +7 -0
  40. package/dist/core/bash-executor.js.map +1 -1
  41. package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
  42. package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
  43. package/dist/core/checkpoints/git-checkpoints.js +251 -0
  44. package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
  45. package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
  46. package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
  47. package/dist/core/checkpoints/session-checkpoints.js +36 -0
  48. package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
  49. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  50. package/dist/core/compaction/branch-summarization.js +7 -3
  51. package/dist/core/compaction/branch-summarization.js.map +1 -1
  52. package/dist/core/compaction/compaction.d.ts +9 -3
  53. package/dist/core/compaction/compaction.d.ts.map +1 -1
  54. package/dist/core/compaction/compaction.js +59 -36
  55. package/dist/core/compaction/compaction.js.map +1 -1
  56. package/dist/core/defaults.d.ts +1 -0
  57. package/dist/core/defaults.d.ts.map +1 -1
  58. package/dist/core/defaults.js +9 -0
  59. package/dist/core/defaults.js.map +1 -1
  60. package/dist/core/delegation/runtime.d.ts +6 -5
  61. package/dist/core/delegation/runtime.d.ts.map +1 -1
  62. package/dist/core/delegation/runtime.js +4 -3
  63. package/dist/core/delegation/runtime.js.map +1 -1
  64. package/dist/core/durable-state/sqlite.d.ts.map +1 -1
  65. package/dist/core/durable-state/sqlite.js +26 -2
  66. package/dist/core/durable-state/sqlite.js.map +1 -1
  67. package/dist/core/exec.d.ts.map +1 -1
  68. package/dist/core/exec.js +5 -2
  69. package/dist/core/exec.js.map +1 -1
  70. package/dist/core/extensions/index.d.ts +2 -2
  71. package/dist/core/extensions/index.d.ts.map +1 -1
  72. package/dist/core/extensions/index.js +1 -1
  73. package/dist/core/extensions/index.js.map +1 -1
  74. package/dist/core/extensions/loader.d.ts.map +1 -1
  75. package/dist/core/extensions/loader.js +123 -50
  76. package/dist/core/extensions/loader.js.map +1 -1
  77. package/dist/core/extensions/runner.d.ts +5 -0
  78. package/dist/core/extensions/runner.d.ts.map +1 -1
  79. package/dist/core/extensions/runner.js +45 -1
  80. package/dist/core/extensions/runner.js.map +1 -1
  81. package/dist/core/extensions/source-runtime.d.ts +14 -0
  82. package/dist/core/extensions/source-runtime.d.ts.map +1 -0
  83. package/dist/core/extensions/source-runtime.js +56 -0
  84. package/dist/core/extensions/source-runtime.js.map +1 -0
  85. package/dist/core/extensions/types.d.ts +60 -11
  86. package/dist/core/extensions/types.d.ts.map +1 -1
  87. package/dist/core/extensions/types.js +3 -0
  88. package/dist/core/extensions/types.js.map +1 -1
  89. package/dist/core/footer-data-provider.d.ts +1 -1
  90. package/dist/core/footer-data-provider.d.ts.map +1 -1
  91. package/dist/core/footer-data-provider.js +1 -1
  92. package/dist/core/footer-data-provider.js.map +1 -1
  93. package/dist/core/formatter-lifecycle.d.ts +69 -0
  94. package/dist/core/formatter-lifecycle.d.ts.map +1 -0
  95. package/dist/core/formatter-lifecycle.js +360 -0
  96. package/dist/core/formatter-lifecycle.js.map +1 -0
  97. package/dist/core/hooks/command-handler.d.ts +19 -0
  98. package/dist/core/hooks/command-handler.d.ts.map +1 -0
  99. package/dist/core/hooks/command-handler.js +112 -0
  100. package/dist/core/hooks/command-handler.js.map +1 -0
  101. package/dist/core/hooks/http-handler.d.ts +10 -0
  102. package/dist/core/hooks/http-handler.d.ts.map +1 -0
  103. package/dist/core/hooks/http-handler.js +36 -0
  104. package/dist/core/hooks/http-handler.js.map +1 -0
  105. package/dist/core/hooks/loader.d.ts +29 -0
  106. package/dist/core/hooks/loader.d.ts.map +1 -0
  107. package/dist/core/hooks/loader.js +139 -0
  108. package/dist/core/hooks/loader.js.map +1 -0
  109. package/dist/core/hooks/runtime.d.ts +13 -0
  110. package/dist/core/hooks/runtime.d.ts.map +1 -0
  111. package/dist/core/hooks/runtime.js +63 -0
  112. package/dist/core/hooks/runtime.js.map +1 -0
  113. package/dist/core/hooks/types.d.ts +83 -0
  114. package/dist/core/hooks/types.d.ts.map +1 -0
  115. package/dist/core/hooks/types.js +24 -0
  116. package/dist/core/hooks/types.js.map +1 -0
  117. package/dist/core/keybindings.d.ts +20 -19
  118. package/dist/core/keybindings.d.ts.map +1 -1
  119. package/dist/core/keybindings.js +26 -5
  120. package/dist/core/keybindings.js.map +1 -1
  121. package/dist/core/mcp/config.d.ts +16 -0
  122. package/dist/core/mcp/config.d.ts.map +1 -0
  123. package/dist/core/mcp/config.js +166 -0
  124. package/dist/core/mcp/config.js.map +1 -0
  125. package/dist/core/mcp/connector.d.ts +35 -0
  126. package/dist/core/mcp/connector.d.ts.map +1 -0
  127. package/dist/core/mcp/connector.js +105 -0
  128. package/dist/core/mcp/connector.js.map +1 -0
  129. package/dist/core/mcp/contract.d.ts +19 -0
  130. package/dist/core/mcp/contract.d.ts.map +1 -0
  131. package/dist/core/mcp/contract.js +89 -0
  132. package/dist/core/mcp/contract.js.map +1 -0
  133. package/dist/core/mcp/mcp-tool.d.ts +32 -0
  134. package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
  135. package/dist/core/mcp/mcp-tool.js +171 -0
  136. package/dist/core/mcp/mcp-tool.js.map +1 -0
  137. package/dist/core/mcp/metadata-cache.d.ts +29 -0
  138. package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
  139. package/dist/core/mcp/metadata-cache.js +99 -0
  140. package/dist/core/mcp/metadata-cache.js.map +1 -0
  141. package/dist/core/mcp/oauth/authorize.d.ts +21 -0
  142. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
  143. package/dist/core/mcp/oauth/authorize.js +32 -0
  144. package/dist/core/mcp/oauth/authorize.js.map +1 -0
  145. package/dist/core/mcp/oauth/discover.d.ts +27 -0
  146. package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
  147. package/dist/core/mcp/oauth/discover.js +116 -0
  148. package/dist/core/mcp/oauth/discover.js.map +1 -0
  149. package/dist/core/mcp/oauth/flow.d.ts +31 -0
  150. package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
  151. package/dist/core/mcp/oauth/flow.js +175 -0
  152. package/dist/core/mcp/oauth/flow.js.map +1 -0
  153. package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
  154. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
  155. package/dist/core/mcp/oauth/mcp-token.js +129 -0
  156. package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
  157. package/dist/core/mcp/oauth/pkce.d.ts +12 -0
  158. package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
  159. package/dist/core/mcp/oauth/pkce.js +11 -0
  160. package/dist/core/mcp/oauth/pkce.js.map +1 -0
  161. package/dist/core/mcp/oauth/register.d.ts +16 -0
  162. package/dist/core/mcp/oauth/register.d.ts.map +1 -0
  163. package/dist/core/mcp/oauth/register.js +42 -0
  164. package/dist/core/mcp/oauth/register.js.map +1 -0
  165. package/dist/core/mcp/oauth/token-client.d.ts +33 -0
  166. package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
  167. package/dist/core/mcp/oauth/token-client.js +65 -0
  168. package/dist/core/mcp/oauth/token-client.js.map +1 -0
  169. package/dist/core/mcp/runtime.d.ts +20 -0
  170. package/dist/core/mcp/runtime.d.ts.map +1 -0
  171. package/dist/core/mcp/runtime.js +37 -0
  172. package/dist/core/mcp/runtime.js.map +1 -0
  173. package/dist/core/mcp/schema.d.ts +22 -0
  174. package/dist/core/mcp/schema.d.ts.map +1 -0
  175. package/dist/core/mcp/schema.js +20 -0
  176. package/dist/core/mcp/schema.js.map +1 -0
  177. package/dist/core/mcp/server-manager.d.ts +43 -0
  178. package/dist/core/mcp/server-manager.d.ts.map +1 -0
  179. package/dist/core/mcp/server-manager.js +142 -0
  180. package/dist/core/mcp/server-manager.js.map +1 -0
  181. package/dist/core/mcp/types.d.ts +100 -0
  182. package/dist/core/mcp/types.d.ts.map +1 -0
  183. package/dist/core/mcp/types.js +8 -0
  184. package/dist/core/mcp/types.js.map +1 -0
  185. package/dist/core/model-config.d.ts +5 -0
  186. package/dist/core/model-config.d.ts.map +1 -1
  187. package/dist/core/model-config.js +3 -1
  188. package/dist/core/model-config.js.map +1 -1
  189. package/dist/core/model-resolver.d.ts +1 -0
  190. package/dist/core/model-resolver.d.ts.map +1 -1
  191. package/dist/core/model-resolver.js +14 -8
  192. package/dist/core/model-resolver.js.map +1 -1
  193. package/dist/core/models-store.d.ts.map +1 -1
  194. package/dist/core/models-store.js +2 -1
  195. package/dist/core/models-store.js.map +1 -1
  196. package/dist/core/package-manager.d.ts.map +1 -1
  197. package/dist/core/package-manager.js +22 -14
  198. package/dist/core/package-manager.js.map +1 -1
  199. package/dist/core/permissions/gate.d.ts.map +1 -1
  200. package/dist/core/permissions/gate.js +45 -4
  201. package/dist/core/permissions/gate.js.map +1 -1
  202. package/dist/core/permissions/operations.d.ts +20 -0
  203. package/dist/core/permissions/operations.d.ts.map +1 -0
  204. package/dist/core/permissions/operations.js +9 -0
  205. package/dist/core/permissions/operations.js.map +1 -0
  206. package/dist/core/permissions/policy-command.d.ts +42 -0
  207. package/dist/core/permissions/policy-command.d.ts.map +1 -0
  208. package/dist/core/permissions/policy-command.js +44 -0
  209. package/dist/core/permissions/policy-command.js.map +1 -0
  210. package/dist/core/permissions/responder.d.ts +49 -2
  211. package/dist/core/permissions/responder.d.ts.map +1 -1
  212. package/dist/core/permissions/responder.js +20 -8
  213. package/dist/core/permissions/responder.js.map +1 -1
  214. package/dist/core/permissions/rules.d.ts.map +1 -1
  215. package/dist/core/permissions/rules.js +11 -1
  216. package/dist/core/permissions/rules.js.map +1 -1
  217. package/dist/core/permissions/store.d.ts +18 -0
  218. package/dist/core/permissions/store.d.ts.map +1 -1
  219. package/dist/core/permissions/store.js +89 -8
  220. package/dist/core/permissions/store.js.map +1 -1
  221. package/dist/core/pi-manifest.d.ts.map +1 -1
  222. package/dist/core/pi-manifest.js +2 -1
  223. package/dist/core/pi-manifest.js.map +1 -1
  224. package/dist/core/policy-executor.d.ts +47 -0
  225. package/dist/core/policy-executor.d.ts.map +1 -0
  226. package/dist/core/policy-executor.js +228 -0
  227. package/dist/core/policy-executor.js.map +1 -0
  228. package/dist/core/policy-loader.d.ts +72 -0
  229. package/dist/core/policy-loader.d.ts.map +1 -0
  230. package/dist/core/policy-loader.js +195 -0
  231. package/dist/core/policy-loader.js.map +1 -0
  232. package/dist/core/project-trust.d.ts +1 -1
  233. package/dist/core/project-trust.d.ts.map +1 -1
  234. package/dist/core/project-trust.js.map +1 -1
  235. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  236. package/dist/core/remote-catalog-provider.js +2 -1
  237. package/dist/core/remote-catalog-provider.js.map +1 -1
  238. package/dist/core/resource-loader.d.ts.map +1 -1
  239. package/dist/core/resource-loader.js +3 -2
  240. package/dist/core/resource-loader.js.map +1 -1
  241. package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
  242. package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
  243. package/dist/core/sandbox/bwrap-arguments.js +136 -0
  244. package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
  245. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  246. package/dist/core/sandbox/child-entry.js +3 -1
  247. package/dist/core/sandbox/child-entry.js.map +1 -1
  248. package/dist/core/sandbox/cli-launch.d.ts +84 -4
  249. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  250. package/dist/core/sandbox/cli-launch.js +115 -16
  251. package/dist/core/sandbox/cli-launch.js.map +1 -1
  252. package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
  253. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  254. package/dist/core/sandbox/cli-supervisor.js +50 -2
  255. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  256. package/dist/core/sandbox/full-access.d.ts +28 -0
  257. package/dist/core/sandbox/full-access.d.ts.map +1 -0
  258. package/dist/core/sandbox/full-access.js +63 -0
  259. package/dist/core/sandbox/full-access.js.map +1 -0
  260. package/dist/core/sandbox/git-identity.d.ts +57 -0
  261. package/dist/core/sandbox/git-identity.d.ts.map +1 -0
  262. package/dist/core/sandbox/git-identity.js +78 -0
  263. package/dist/core/sandbox/git-identity.js.map +1 -0
  264. package/dist/core/sandbox/host-approval.d.ts +42 -0
  265. package/dist/core/sandbox/host-approval.d.ts.map +1 -0
  266. package/dist/core/sandbox/host-approval.js +99 -0
  267. package/dist/core/sandbox/host-approval.js.map +1 -0
  268. package/dist/core/sandbox/linux-backend.d.ts +14 -0
  269. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  270. package/dist/core/sandbox/linux-backend.js +158 -80
  271. package/dist/core/sandbox/linux-backend.js.map +1 -1
  272. package/dist/core/sandbox/macos-backend.d.ts +5 -0
  273. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  274. package/dist/core/sandbox/macos-backend.js +164 -5
  275. package/dist/core/sandbox/macos-backend.js.map +1 -1
  276. package/dist/core/sandbox/network-proxy.d.ts +19 -0
  277. package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
  278. package/dist/core/sandbox/network-proxy.js +93 -19
  279. package/dist/core/sandbox/network-proxy.js.map +1 -1
  280. package/dist/core/sandbox/policy.d.ts +8 -0
  281. package/dist/core/sandbox/policy.d.ts.map +1 -1
  282. package/dist/core/sandbox/policy.js +29 -12
  283. package/dist/core/sandbox/policy.js.map +1 -1
  284. package/dist/core/sandbox/profiles.d.ts +25 -0
  285. package/dist/core/sandbox/profiles.d.ts.map +1 -0
  286. package/dist/core/sandbox/profiles.js +23 -0
  287. package/dist/core/sandbox/profiles.js.map +1 -0
  288. package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
  289. package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
  290. package/dist/core/sandbox/rpc/command-client.js +86 -0
  291. package/dist/core/sandbox/rpc/command-client.js.map +1 -0
  292. package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
  293. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
  294. package/dist/core/sandbox/rpc/command-proxy.js +120 -0
  295. package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
  296. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
  297. package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
  298. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
  299. package/dist/core/sandbox/rpc/framing.d.ts +35 -0
  300. package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
  301. package/dist/core/sandbox/rpc/framing.js +114 -0
  302. package/dist/core/sandbox/rpc/framing.js.map +1 -0
  303. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
  304. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
  305. package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
  306. package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
  307. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
  308. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
  309. package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
  310. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
  311. package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
  312. package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
  313. package/dist/core/sandbox/supervisor-temp.js +21 -0
  314. package/dist/core/sandbox/supervisor-temp.js.map +1 -0
  315. package/dist/core/sandbox/supervisor.d.ts +12 -1
  316. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  317. package/dist/core/sandbox/supervisor.js +15 -1
  318. package/dist/core/sandbox/supervisor.js.map +1 -1
  319. package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
  320. package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
  321. package/dist/core/sandbox/terminal-handoff.js +221 -0
  322. package/dist/core/sandbox/terminal-handoff.js.map +1 -0
  323. package/dist/core/sandbox/terminal-size.d.ts +13 -0
  324. package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
  325. package/dist/core/sandbox/terminal-size.js +23 -2
  326. package/dist/core/sandbox/terminal-size.js.map +1 -1
  327. package/dist/core/sdk.d.ts +24 -2
  328. package/dist/core/sdk.d.ts.map +1 -1
  329. package/dist/core/sdk.js +63 -4
  330. package/dist/core/sdk.js.map +1 -1
  331. package/dist/core/session-export.d.ts +4 -0
  332. package/dist/core/session-export.d.ts.map +1 -0
  333. package/dist/core/session-export.js +32 -0
  334. package/dist/core/session-export.js.map +1 -0
  335. package/dist/core/session-manager.d.ts +13 -1
  336. package/dist/core/session-manager.d.ts.map +1 -1
  337. package/dist/core/session-manager.js +34 -6
  338. package/dist/core/session-manager.js.map +1 -1
  339. package/dist/core/settings-diagnostics.d.ts +9 -0
  340. package/dist/core/settings-diagnostics.d.ts.map +1 -0
  341. package/dist/core/settings-diagnostics.js +21 -0
  342. package/dist/core/settings-diagnostics.js.map +1 -0
  343. package/dist/core/settings-manager.d.ts +172 -1
  344. package/dist/core/settings-manager.d.ts.map +1 -1
  345. package/dist/core/settings-manager.js +160 -11
  346. package/dist/core/settings-manager.js.map +1 -1
  347. package/dist/core/skills.d.ts.map +1 -1
  348. package/dist/core/skills.js +51 -33
  349. package/dist/core/skills.js.map +1 -1
  350. package/dist/core/slash-commands.d.ts.map +1 -1
  351. package/dist/core/slash-commands.js +4 -1
  352. package/dist/core/slash-commands.js.map +1 -1
  353. package/dist/core/system-prompt.d.ts.map +1 -1
  354. package/dist/core/system-prompt.js +8 -2
  355. package/dist/core/system-prompt.js.map +1 -1
  356. package/dist/core/tools/background-shell.d.ts +39 -0
  357. package/dist/core/tools/background-shell.d.ts.map +1 -0
  358. package/dist/core/tools/background-shell.js +81 -0
  359. package/dist/core/tools/background-shell.js.map +1 -0
  360. package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
  361. package/dist/core/tools/bash-command-segments.js +2 -0
  362. package/dist/core/tools/bash-command-segments.js.map +1 -1
  363. package/dist/core/tools/bash.d.ts +44 -16
  364. package/dist/core/tools/bash.d.ts.map +1 -1
  365. package/dist/core/tools/bash.js +264 -32
  366. package/dist/core/tools/bash.js.map +1 -1
  367. package/dist/core/tools/contract-snapshot.d.ts +58 -0
  368. package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
  369. package/dist/core/tools/contract-snapshot.js +51 -0
  370. package/dist/core/tools/contract-snapshot.js.map +1 -0
  371. package/dist/core/tools/contract.d.ts +53 -5
  372. package/dist/core/tools/contract.d.ts.map +1 -1
  373. package/dist/core/tools/contract.js +22 -2
  374. package/dist/core/tools/contract.js.map +1 -1
  375. package/dist/core/tools/delegate.d.ts +6 -4
  376. package/dist/core/tools/delegate.d.ts.map +1 -1
  377. package/dist/core/tools/delegate.js +21 -11
  378. package/dist/core/tools/delegate.js.map +1 -1
  379. package/dist/core/tools/edit-diff.d.ts +48 -5
  380. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  381. package/dist/core/tools/edit-diff.js +217 -8
  382. package/dist/core/tools/edit-diff.js.map +1 -1
  383. package/dist/core/tools/edit.d.ts +8 -0
  384. package/dist/core/tools/edit.d.ts.map +1 -1
  385. package/dist/core/tools/edit.js +72 -10
  386. package/dist/core/tools/edit.js.map +1 -1
  387. package/dist/core/tools/find.d.ts.map +1 -1
  388. package/dist/core/tools/find.js +5 -2
  389. package/dist/core/tools/find.js.map +1 -1
  390. package/dist/core/tools/grep.d.ts.map +1 -1
  391. package/dist/core/tools/grep.js +5 -2
  392. package/dist/core/tools/grep.js.map +1 -1
  393. package/dist/core/tools/index.d.ts +14 -2
  394. package/dist/core/tools/index.d.ts.map +1 -1
  395. package/dist/core/tools/index.js +12 -0
  396. package/dist/core/tools/index.js.map +1 -1
  397. package/dist/core/tools/ls.d.ts.map +1 -1
  398. package/dist/core/tools/ls.js +5 -2
  399. package/dist/core/tools/ls.js.map +1 -1
  400. package/dist/core/tools/lsp.d.ts +19 -1
  401. package/dist/core/tools/lsp.d.ts.map +1 -1
  402. package/dist/core/tools/lsp.js +163 -10
  403. package/dist/core/tools/lsp.js.map +1 -1
  404. package/dist/core/tools/path-permission.d.ts +3 -0
  405. package/dist/core/tools/path-permission.d.ts.map +1 -1
  406. package/dist/core/tools/path-permission.js +16 -4
  407. package/dist/core/tools/path-permission.js.map +1 -1
  408. package/dist/core/tools/path-utils.d.ts +27 -2
  409. package/dist/core/tools/path-utils.d.ts.map +1 -1
  410. package/dist/core/tools/path-utils.js +221 -4
  411. package/dist/core/tools/path-utils.js.map +1 -1
  412. package/dist/core/tools/powershell.d.ts +16 -0
  413. package/dist/core/tools/powershell.d.ts.map +1 -0
  414. package/dist/core/tools/powershell.js +39 -0
  415. package/dist/core/tools/powershell.js.map +1 -0
  416. package/dist/core/tools/read.d.ts.map +1 -1
  417. package/dist/core/tools/read.js +20 -11
  418. package/dist/core/tools/read.js.map +1 -1
  419. package/dist/core/tools/test.d.ts +54 -9
  420. package/dist/core/tools/test.d.ts.map +1 -1
  421. package/dist/core/tools/test.js +239 -19
  422. package/dist/core/tools/test.js.map +1 -1
  423. package/dist/core/tools/write.d.ts +7 -0
  424. package/dist/core/tools/write.d.ts.map +1 -1
  425. package/dist/core/tools/write.js +64 -9
  426. package/dist/core/tools/write.js.map +1 -1
  427. package/dist/core/trust-manager.d.ts.map +1 -1
  428. package/dist/core/trust-manager.js +2 -1
  429. package/dist/core/trust-manager.js.map +1 -1
  430. package/dist/core/verification-lifecycle.d.ts +77 -0
  431. package/dist/core/verification-lifecycle.d.ts.map +1 -0
  432. package/dist/core/verification-lifecycle.js +146 -0
  433. package/dist/core/verification-lifecycle.js.map +1 -0
  434. package/dist/core/workspace/artifacts.d.ts +45 -0
  435. package/dist/core/workspace/artifacts.d.ts.map +1 -0
  436. package/dist/core/workspace/artifacts.js +165 -0
  437. package/dist/core/workspace/artifacts.js.map +1 -0
  438. package/dist/core/workspace/comparison.d.ts +27 -0
  439. package/dist/core/workspace/comparison.d.ts.map +1 -0
  440. package/dist/core/workspace/comparison.js +94 -0
  441. package/dist/core/workspace/comparison.js.map +1 -0
  442. package/dist/core/workspace/git-observer.d.ts +48 -0
  443. package/dist/core/workspace/git-observer.d.ts.map +1 -0
  444. package/dist/core/workspace/git-observer.js +346 -0
  445. package/dist/core/workspace/git-observer.js.map +1 -0
  446. package/dist/core/workspace/projection.d.ts +3 -0
  447. package/dist/core/workspace/projection.d.ts.map +1 -0
  448. package/dist/core/workspace/projection.js +56 -0
  449. package/dist/core/workspace/projection.js.map +1 -0
  450. package/dist/core/workspace/state.d.ts +133 -0
  451. package/dist/core/workspace/state.d.ts.map +1 -0
  452. package/dist/core/workspace/state.js +96 -0
  453. package/dist/core/workspace/state.js.map +1 -0
  454. package/dist/extensions/llama/client.d.ts +6 -0
  455. package/dist/extensions/llama/client.d.ts.map +1 -1
  456. package/dist/extensions/llama/client.js +7 -0
  457. package/dist/extensions/llama/client.js.map +1 -1
  458. package/dist/extensions/llama/index.d.ts.map +1 -1
  459. package/dist/extensions/llama/index.js +2 -0
  460. package/dist/extensions/llama/index.js.map +1 -1
  461. package/dist/extensions/llama/provider.d.ts +3 -1
  462. package/dist/extensions/llama/provider.d.ts.map +1 -1
  463. package/dist/extensions/llama/provider.js +29 -4
  464. package/dist/extensions/llama/provider.js.map +1 -1
  465. package/dist/index.d.ts +8 -5
  466. package/dist/index.d.ts.map +1 -1
  467. package/dist/index.js +7 -4
  468. package/dist/index.js.map +1 -1
  469. package/dist/main.d.ts +6 -0
  470. package/dist/main.d.ts.map +1 -1
  471. package/dist/main.js +58 -23
  472. package/dist/main.js.map +1 -1
  473. package/dist/migrations.d.ts.map +1 -1
  474. package/dist/migrations.js +4 -3
  475. package/dist/migrations.js.map +1 -1
  476. package/dist/modes/acp/server.d.ts +55 -0
  477. package/dist/modes/acp/server.d.ts.map +1 -0
  478. package/dist/modes/acp/server.js +244 -0
  479. package/dist/modes/acp/server.js.map +1 -0
  480. package/dist/modes/acp/translate.d.ts +37 -0
  481. package/dist/modes/acp/translate.d.ts.map +1 -0
  482. package/dist/modes/acp/translate.js +50 -0
  483. package/dist/modes/acp/translate.js.map +1 -0
  484. package/dist/modes/index.d.ts +1 -0
  485. package/dist/modes/index.d.ts.map +1 -1
  486. package/dist/modes/index.js +1 -0
  487. package/dist/modes/index.js.map +1 -1
  488. package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
  489. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  490. package/dist/modes/interactive/components/assistant-message.js +182 -23
  491. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  492. package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
  493. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  494. package/dist/modes/interactive/components/bash-execution.js +20 -0
  495. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  496. package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
  497. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  498. package/dist/modes/interactive/components/custom-editor.js +20 -9
  499. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  500. package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
  501. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  502. package/dist/modes/interactive/components/extension-selector.js +7 -3
  503. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  504. package/dist/modes/interactive/components/footer.d.ts +19 -0
  505. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  506. package/dist/modes/interactive/components/footer.js +70 -10
  507. package/dist/modes/interactive/components/footer.js.map +1 -1
  508. package/dist/modes/interactive/components/model-selector.d.ts +10 -3
  509. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  510. package/dist/modes/interactive/components/model-selector.js +56 -14
  511. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  512. package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
  513. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
  514. package/dist/modes/interactive/components/permission-preview.js +33 -0
  515. package/dist/modes/interactive/components/permission-preview.js.map +1 -0
  516. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  517. package/dist/modes/interactive/components/session-selector.js +2 -2
  518. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  519. package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
  520. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  521. package/dist/modes/interactive/components/settings-selector.js +128 -56
  522. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  523. package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
  524. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
  525. package/dist/modes/interactive/components/settings-submenu.js +165 -0
  526. package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
  527. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  528. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  529. package/dist/modes/interactive/components/thinking-selector.js +74 -17
  530. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  531. package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
  532. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  533. package/dist/modes/interactive/components/tool-execution.js +64 -10
  534. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  535. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  536. package/dist/modes/interactive/external-editor.js +2 -1
  537. package/dist/modes/interactive/external-editor.js.map +1 -1
  538. package/dist/modes/interactive/interactive-mode.d.ts +45 -0
  539. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  540. package/dist/modes/interactive/interactive-mode.js +395 -43
  541. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  542. package/dist/modes/interactive/theme/theme.d.ts +12 -0
  543. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  544. package/dist/modes/interactive/theme/theme.js +20 -9
  545. package/dist/modes/interactive/theme/theme.js.map +1 -1
  546. package/dist/modes/json-event.d.ts +16 -13
  547. package/dist/modes/json-event.d.ts.map +1 -1
  548. package/dist/modes/json-event.js +20 -6
  549. package/dist/modes/json-event.js.map +1 -1
  550. package/dist/modes/print-mode.d.ts.map +1 -1
  551. package/dist/modes/print-mode.js +27 -12
  552. package/dist/modes/print-mode.js.map +1 -1
  553. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  554. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  555. package/dist/modes/rpc/rpc-client.js +7 -0
  556. package/dist/modes/rpc/rpc-client.js.map +1 -1
  557. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  558. package/dist/modes/rpc/rpc-mode.js +3 -0
  559. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  560. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  561. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  562. package/dist/modes/rpc/rpc-types.js.map +1 -1
  563. package/dist/package-manager-cli.d.ts +1 -0
  564. package/dist/package-manager-cli.d.ts.map +1 -1
  565. package/dist/package-manager-cli.js +196 -10
  566. package/dist/package-manager-cli.js.map +1 -1
  567. package/dist/utils/frontmatter.d.ts.map +1 -1
  568. package/dist/utils/frontmatter.js +2 -1
  569. package/dist/utils/frontmatter.js.map +1 -1
  570. package/dist/utils/management-http.d.ts +5 -3
  571. package/dist/utils/management-http.d.ts.map +1 -1
  572. package/dist/utils/management-http.js +14 -10
  573. package/dist/utils/management-http.js.map +1 -1
  574. package/dist/utils/shell.d.ts +3 -0
  575. package/dist/utils/shell.d.ts.map +1 -1
  576. package/dist/utils/shell.js +23 -9
  577. package/dist/utils/shell.js.map +1 -1
  578. package/dist/utils/syntax-highlight.d.ts +1 -0
  579. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  580. package/dist/utils/syntax-highlight.js +60 -1
  581. package/dist/utils/syntax-highlight.js.map +1 -1
  582. package/dist/utils/text.d.ts +8 -0
  583. package/dist/utils/text.d.ts.map +1 -0
  584. package/dist/utils/text.js +9 -0
  585. package/dist/utils/text.js.map +1 -0
  586. package/dist/utils/version-check.d.ts.map +1 -1
  587. package/dist/utils/version-check.js +6 -4
  588. package/dist/utils/version-check.js.map +1 -1
  589. package/docs/compaction.md +19 -0
  590. package/docs/custom-provider.md +12 -9
  591. package/docs/environment-variables.md +1 -1
  592. package/docs/extensions.md +35 -7
  593. package/docs/json.md +7 -2
  594. package/docs/keybindings.md +9 -9
  595. package/docs/llama-cpp.md +2 -0
  596. package/docs/models.md +8 -2
  597. package/docs/packages.md +2 -2
  598. package/docs/quickstart.md +1 -1
  599. package/docs/rpc.md +32 -3
  600. package/docs/sdk.md +32 -2
  601. package/docs/settings.md +23 -7
  602. package/docs/skills.md +4 -3
  603. package/docs/terminal-setup.md +23 -7
  604. package/docs/usage.md +10 -5
  605. package/docs/windows.md +24 -2
  606. package/examples/extensions/border-status-editor.ts +1 -1
  607. package/examples/extensions/built-in-tool-renderer.ts +8 -4
  608. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  609. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  610. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  611. package/examples/extensions/git-checkpoint.ts +24 -36
  612. package/examples/extensions/gondolin/package-lock.json +2 -2
  613. package/examples/extensions/gondolin/package.json +1 -1
  614. package/examples/extensions/minimal-mode.ts +2 -2
  615. package/examples/extensions/notify.ts +3 -1
  616. package/examples/extensions/permission-gate.ts +1 -1
  617. package/examples/extensions/plan-mode/index.ts +1 -1
  618. package/examples/extensions/rpc-demo.ts +1 -1
  619. package/examples/extensions/sandbox/package-lock.json +2 -2
  620. package/examples/extensions/sandbox/package.json +1 -1
  621. package/examples/extensions/subagent/README.md +1 -1
  622. package/examples/extensions/subagent/index.ts +6 -1
  623. package/examples/extensions/titlebar-spinner.ts +1 -1
  624. package/examples/extensions/with-deps/package-lock.json +2 -2
  625. package/examples/extensions/with-deps/package.json +1 -1
  626. package/examples/rpc-extension-ui.ts +1 -1
  627. package/examples/sdk/06-extensions.ts +1 -1
  628. package/examples/sdk/README.md +1 -1
  629. package/npm-shrinkwrap.json +439 -405
  630. package/package.json +17 -17
  631. package/dist/server/create-harness.d.ts +0 -26
  632. package/dist/server/create-harness.d.ts.map +0 -1
  633. package/dist/server/create-harness.js +0 -107
  634. package/dist/server/create-harness.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACrC,2IAA2I;IAC3I,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAC;IAC5D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,yFAAyF;IACzF,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,oHAAkH;AAClH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAKrG","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAA4C,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEnG,OAAO,KAAK,EAAqB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACrC,2IAA2I;IAC3I,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAC;IAC5D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,yFAAyF;IACzF,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAwCD,oHAAkH;AAClH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAmDvB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAKrG","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { type PermissionSpec, resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionPreview, PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/**\n * A denial, plus whatever the user said to do instead.\n *\n * The reason becomes the blocked tool result the model reads, so guidance is\n * bounded here rather than pasted whole: it is user-entered free text on a path\n * that reaches the transcript.\n */\nconst MAX_GUIDANCE_CHARS = 400;\n\n/**\n * Run the tool's preview producer, if it declares one, on the ask branch only.\n *\n * A producer reads a file to describe a change, so a call the user was never\n * asked about must not pay for it. A producer that throws degrades to a stated\n * reason rather than to silence: `readPreparedPath` throws precisely when the\n * target changed identity since authorization, which is the case a reader most\n * needs to see.\n */\nfunction producePreview(spec: PermissionSpec, params: unknown): PermissionPreview | undefined {\n\tif (!spec.previewCall) return undefined;\n\ttry {\n\t\treturn spec.previewCall(params as never);\n\t} catch (error) {\n\t\treturn { kind: \"unavailable\", reason: error instanceof Error ? error.message : String(error) };\n\t}\n}\n\nfunction describeDecline(toolName: string, guidance: string | undefined): string {\n\tconst trimmed = guidance?.trim().slice(0, MAX_GUIDANCE_CHARS);\n\tif (!trimmed) return `${toolName} was declined.`;\n\treturn `${toolName} was declined. The user asked for this instead. ${trimmed}`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tif (spec.prepareCall) spec.prepareCall(params as never);\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\tpreview: producePreview(spec, params),\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t\t// Only offer a session grant the persist branch below would actually write.\n\t\tsessionScope: ruleForCall !== null ? { description: spec.describe(ruleForCall) } : undefined,\n\t});\n\t// `persist` means the same thing on both branches: write the rule that decides\n\t// this exact call the way the user just decided it. Reading it only on the allow\n\t// branch is what let \"Reject always\" refuse once and then ask again.\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: answer.allow ? \"allow\" : \"deny\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: describeDecline(toolName, answer.guidance) };\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
@@ -18,10 +18,45 @@ function describeDecision(contract, toolName, ruleContent) {
18
18
  return contract.permission.describe(ruleContent);
19
19
  return `${toolName} is not permitted by the current permission configuration.`;
20
20
  }
21
+ /**
22
+ * A denial, plus whatever the user said to do instead.
23
+ *
24
+ * The reason becomes the blocked tool result the model reads, so guidance is
25
+ * bounded here rather than pasted whole: it is user-entered free text on a path
26
+ * that reaches the transcript.
27
+ */
28
+ const MAX_GUIDANCE_CHARS = 400;
29
+ /**
30
+ * Run the tool's preview producer, if it declares one, on the ask branch only.
31
+ *
32
+ * A producer reads a file to describe a change, so a call the user was never
33
+ * asked about must not pay for it. A producer that throws degrades to a stated
34
+ * reason rather than to silence: `readPreparedPath` throws precisely when the
35
+ * target changed identity since authorization, which is the case a reader most
36
+ * needs to see.
37
+ */
38
+ function producePreview(spec, params) {
39
+ if (!spec.previewCall)
40
+ return undefined;
41
+ try {
42
+ return spec.previewCall(params);
43
+ }
44
+ catch (error) {
45
+ return { kind: "unavailable", reason: error instanceof Error ? error.message : String(error) };
46
+ }
47
+ }
48
+ function describeDecline(toolName, guidance) {
49
+ const trimmed = guidance?.trim().slice(0, MAX_GUIDANCE_CHARS);
50
+ if (!trimmed)
51
+ return `${toolName} was declined.`;
52
+ return `${toolName} was declined. The user asked for this instead. ${trimmed}`;
53
+ }
21
54
  /** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */
22
55
  export async function evaluateToolCall(toolName, params, options) {
23
56
  const contract = resolveToolContract(options.getContract, toolName);
24
57
  const spec = contract.permission;
58
+ if (spec.prepareCall)
59
+ spec.prepareCall(params);
25
60
  const snapshot = await options.store.snapshot();
26
61
  if (snapshot.errors.length > 0) {
27
62
  const sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(", ");
@@ -47,19 +82,25 @@ export async function evaluateToolCall(toolName, params, options) {
47
82
  }
48
83
  const ruleForCall = spec.ruleForCall(params);
49
84
  const answer = await responder.ask({
85
+ preview: producePreview(spec, params),
50
86
  toolName,
51
87
  description: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,
88
+ // Only offer a session grant the persist branch below would actually write.
89
+ sessionScope: ruleForCall !== null ? { description: spec.describe(ruleForCall) } : undefined,
52
90
  });
53
- if (!answer.allow) {
54
- return { block: true, reason: `${toolName} was declined.` };
55
- }
91
+ // `persist` means the same thing on both branches: write the rule that decides
92
+ // this exact call the way the user just decided it. Reading it only on the allow
93
+ // branch is what let "Reject always" refuse once and then ask again.
56
94
  if (answer.persist && ruleForCall !== null) {
57
95
  await options.store.apply({
58
96
  type: "addRules",
59
97
  destination: "session",
60
- rules: [{ toolName, behavior: "allow", ruleContent: ruleForCall }],
98
+ rules: [{ toolName, behavior: answer.allow ? "allow" : "deny", ruleContent: ruleForCall }],
61
99
  });
62
100
  }
101
+ if (!answer.allow) {
102
+ return { block: true, reason: describeDecline(toolName, answer.guidance) };
103
+ }
63
104
  return { block: false };
64
105
  }
65
106
  /** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */
@@ -1 +1 @@
1
- {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { type PermissionSpec, resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionPreview, PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/**\n * A denial, plus whatever the user said to do instead.\n *\n * The reason becomes the blocked tool result the model reads, so guidance is\n * bounded here rather than pasted whole: it is user-entered free text on a path\n * that reaches the transcript.\n */\nconst MAX_GUIDANCE_CHARS = 400;\n\n/**\n * Run the tool's preview producer, if it declares one, on the ask branch only.\n *\n * A producer reads a file to describe a change, so a call the user was never\n * asked about must not pay for it. A producer that throws degrades to a stated\n * reason rather than to silence: `readPreparedPath` throws precisely when the\n * target changed identity since authorization, which is the case a reader most\n * needs to see.\n */\nfunction producePreview(spec: PermissionSpec, params: unknown): PermissionPreview | undefined {\n\tif (!spec.previewCall) return undefined;\n\ttry {\n\t\treturn spec.previewCall(params as never);\n\t} catch (error) {\n\t\treturn { kind: \"unavailable\", reason: error instanceof Error ? error.message : String(error) };\n\t}\n}\n\nfunction describeDecline(toolName: string, guidance: string | undefined): string {\n\tconst trimmed = guidance?.trim().slice(0, MAX_GUIDANCE_CHARS);\n\tif (!trimmed) return `${toolName} was declined.`;\n\treturn `${toolName} was declined. The user asked for this instead. ${trimmed}`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tif (spec.prepareCall) spec.prepareCall(params as never);\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\tpreview: producePreview(spec, params),\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t\t// Only offer a session grant the persist branch below would actually write.\n\t\tsessionScope: ruleForCall !== null ? { description: spec.describe(ruleForCall) } : undefined,\n\t});\n\t// `persist` means the same thing on both branches: write the rule that decides\n\t// this exact call the way the user just decided it. Reading it only on the allow\n\t// branch is what let \"Reject always\" refuse once and then ask again.\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: answer.allow ? \"allow\" : \"deny\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: describeDecline(toolName, answer.guidance) };\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /** Typed facts prepared by a tool-owned permission spec and consumed by execution. */
2
+ export interface CanonicalPath {
3
+ readonly kind: "canonical-path";
4
+ readonly value: string;
5
+ }
6
+ export interface FileIdentity {
7
+ readonly device: number;
8
+ readonly inode: number;
9
+ }
10
+ export type PreparedPathOperation = {
11
+ readonly kind: "path-existing";
12
+ readonly path: CanonicalPath;
13
+ readonly identity: FileIdentity;
14
+ } | {
15
+ readonly kind: "path-new";
16
+ readonly path: CanonicalPath;
17
+ };
18
+ export declare function setPreparedPathOperation(params: object, operation: PreparedPathOperation): void;
19
+ export declare function getPreparedPathOperation(params: object): PreparedPathOperation | undefined;
20
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/operations.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAEtF,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,qBAAqB,GAC9B;IACA,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;CAC/B,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC5B,CAAC;AAIL,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,qBAAqB,GAAG,IAAI,CAE/F;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAE1F","sourcesContent":["/** Typed facts prepared by a tool-owned permission spec and consumed by execution. */\n\nexport interface CanonicalPath {\n\treadonly kind: \"canonical-path\";\n\treadonly value: string;\n}\n\nexport interface FileIdentity {\n\treadonly device: number;\n\treadonly inode: number;\n}\n\nexport type PreparedPathOperation =\n\t| {\n\t\t\treadonly kind: \"path-existing\";\n\t\t\treadonly path: CanonicalPath;\n\t\t\treadonly identity: FileIdentity;\n\t }\n\t| {\n\t\t\treadonly kind: \"path-new\";\n\t\t\treadonly path: CanonicalPath;\n\t };\n\nconst preparedPathOperations = new WeakMap<object, PreparedPathOperation>();\n\nexport function setPreparedPathOperation(params: object, operation: PreparedPathOperation): void {\n\tpreparedPathOperations.set(params, operation);\n}\n\nexport function getPreparedPathOperation(params: object): PreparedPathOperation | undefined {\n\treturn preparedPathOperations.get(params);\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /** Typed facts prepared by a tool-owned permission spec and consumed by execution. */
2
+ const preparedPathOperations = new WeakMap();
3
+ export function setPreparedPathOperation(params, operation) {
4
+ preparedPathOperations.set(params, operation);
5
+ }
6
+ export function getPreparedPathOperation(params) {
7
+ return preparedPathOperations.get(params);
8
+ }
9
+ //# sourceMappingURL=operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../../src/core/permissions/operations.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAuBtF,MAAM,sBAAsB,GAAG,IAAI,OAAO,EAAiC,CAAC;AAE5E,MAAM,UAAU,wBAAwB,CAAC,MAAc,EAAE,SAAgC,EAAQ;IAChG,sBAAsB,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAAA,CAC9C;AAED,MAAM,UAAU,wBAAwB,CAAC,MAAc,EAAqC;IAC3F,OAAO,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAAA,CAC1C","sourcesContent":["/** Typed facts prepared by a tool-owned permission spec and consumed by execution. */\n\nexport interface CanonicalPath {\n\treadonly kind: \"canonical-path\";\n\treadonly value: string;\n}\n\nexport interface FileIdentity {\n\treadonly device: number;\n\treadonly inode: number;\n}\n\nexport type PreparedPathOperation =\n\t| {\n\t\t\treadonly kind: \"path-existing\";\n\t\t\treadonly path: CanonicalPath;\n\t\t\treadonly identity: FileIdentity;\n\t }\n\t| {\n\t\t\treadonly kind: \"path-new\";\n\t\t\treadonly path: CanonicalPath;\n\t };\n\nconst preparedPathOperations = new WeakMap<object, PreparedPathOperation>();\n\nexport function setPreparedPathOperation(params: object, operation: PreparedPathOperation): void {\n\tpreparedPathOperations.set(params, operation);\n}\n\nexport function getPreparedPathOperation(params: object): PreparedPathOperation | undefined {\n\treturn preparedPathOperations.get(params);\n}\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * PS.1: the permission authority for configured commands (verification and
3
+ * formatter policies from the VF.2 loader). Tool calls reach the gate through
4
+ * `evaluateToolCall`; a configured command has no tool name and no rule
5
+ * grammar, so it cannot use that path. This is the second entry point to the
6
+ * *same* authority, not a second authority: it reuses `resolveWithMode`, so
7
+ * plan mode's `exec`/`fs.write` floor and the bypass escape hatch behave
8
+ * identically for a formatter and for the write tool.
9
+ *
10
+ * Two differences from `evaluateToolCall` are deliberate. A `deny` policy is
11
+ * refused before any mode is consulted, because the policy permission is the
12
+ * configuration author's ceiling and `bypassPermissions` must not raise it.
13
+ * And an approved `ask` never persists a rule, because there is no tool name to
14
+ * persist it against.
15
+ */
16
+ import type { PolicyPermission } from "../policy-loader.ts";
17
+ import type { Capability } from "../tools/contract.ts";
18
+ import type { GateDecision } from "./gate.ts";
19
+ import type { PermissionResponder } from "./responder.ts";
20
+ import type { PermissionMode } from "./store.ts";
21
+ /** The typed facts a configured command presents for authorization. */
22
+ export interface ConfiguredCommandOperation {
23
+ policyId: string;
24
+ executable: string;
25
+ argv: readonly string[];
26
+ cwd: string;
27
+ /** Declared write patterns already intersected with the policy's pathScope, or undefined for a non-mutating command. */
28
+ writeScope: readonly string[] | undefined;
29
+ capabilities: ReadonlySet<Capability>;
30
+ permission: PolicyPermission;
31
+ }
32
+ export interface ConfiguredCommandAuthorizationOptions {
33
+ getMode: () => PermissionMode | Promise<PermissionMode>;
34
+ /** Absent in a non-interactive session: an `ask` policy then fails closed. */
35
+ responder?: PermissionResponder;
36
+ /** Resolves a responder at call time because the interactive UI binds after session construction. */
37
+ getResponder?: () => PermissionResponder | undefined;
38
+ }
39
+ /** Injected into the verification tracker and the formatter runner so neither imports the gate. */
40
+ export type AuthorizeConfiguredCommand = (operation: ConfiguredCommandOperation) => Promise<GateDecision>;
41
+ export declare function authorizeConfiguredCommand(operation: ConfiguredCommandOperation, options: ConfiguredCommandAuthorizationOptions): Promise<GateDecision>;
42
+ //# sourceMappingURL=policy-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-command.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/policy-command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,uEAAuE;AACvE,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,wHAAwH;IACxH,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC1C,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;IACtC,UAAU,EAAE,gBAAgB,CAAC;CAC7B;AAED,MAAM,WAAW,qCAAqC;IACrD,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD,8EAA8E;IAC9E,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,mGAAmG;AACnG,MAAM,MAAM,0BAA0B,GAAG,CAAC,SAAS,EAAE,0BAA0B,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAM1G,wBAAsB,0BAA0B,CAC/C,SAAS,EAAE,0BAA0B,EACrC,OAAO,EAAE,qCAAqC,GAC5C,OAAO,CAAC,YAAY,CAAC,CAyBvB","sourcesContent":["/**\n * PS.1: the permission authority for configured commands (verification and\n * formatter policies from the VF.2 loader). Tool calls reach the gate through\n * `evaluateToolCall`; a configured command has no tool name and no rule\n * grammar, so it cannot use that path. This is the second entry point to the\n * *same* authority, not a second authority: it reuses `resolveWithMode`, so\n * plan mode's `exec`/`fs.write` floor and the bypass escape hatch behave\n * identically for a formatter and for the write tool.\n *\n * Two differences from `evaluateToolCall` are deliberate. A `deny` policy is\n * refused before any mode is consulted, because the policy permission is the\n * configuration author's ceiling and `bypassPermissions` must not raise it.\n * And an approved `ask` never persists a rule, because there is no tool name to\n * persist it against.\n */\n\nimport type { PolicyPermission } from \"../policy-loader.ts\";\nimport type { Capability } from \"../tools/contract.ts\";\nimport type { GateDecision } from \"./gate.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport type { PermissionMode } from \"./store.ts\";\n\n/** The typed facts a configured command presents for authorization. */\nexport interface ConfiguredCommandOperation {\n\tpolicyId: string;\n\texecutable: string;\n\targv: readonly string[];\n\tcwd: string;\n\t/** Declared write patterns already intersected with the policy's pathScope, or undefined for a non-mutating command. */\n\twriteScope: readonly string[] | undefined;\n\tcapabilities: ReadonlySet<Capability>;\n\tpermission: PolicyPermission;\n}\n\nexport interface ConfiguredCommandAuthorizationOptions {\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/** Absent in a non-interactive session: an `ask` policy then fails closed. */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\n/** Injected into the verification tracker and the formatter runner so neither imports the gate. */\nexport type AuthorizeConfiguredCommand = (operation: ConfiguredCommandOperation) => Promise<GateDecision>;\n\nfunction describe(operation: ConfiguredCommandOperation): string {\n\treturn [operation.executable, ...operation.argv].join(\" \");\n}\n\nexport async function authorizeConfiguredCommand(\n\toperation: ConfiguredCommandOperation,\n\toptions: ConfiguredCommandAuthorizationOptions,\n): Promise<GateDecision> {\n\tif (operation.permission === \"deny\") {\n\t\treturn { block: true, reason: `Policy ${operation.policyId} is configured with permission \"deny\".` };\n\t}\n\n\tconst base = operation.permission === \"allow\" ? \"allow\" : \"ask\";\n\tconst resolution = resolveWithMode(await options.getMode(), { behavior: base }, operation.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: `Policy ${operation.policyId} is not permitted in the current permission mode.` };\n\t}\n\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Policy ${operation.policyId} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst answer = await responder.ask({ toolName: operation.policyId, description: describe(operation) });\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `Policy ${operation.policyId} was declined.` };\n\t}\n\treturn { block: false };\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * PS.1: the permission authority for configured commands (verification and
3
+ * formatter policies from the VF.2 loader). Tool calls reach the gate through
4
+ * `evaluateToolCall`; a configured command has no tool name and no rule
5
+ * grammar, so it cannot use that path. This is the second entry point to the
6
+ * *same* authority, not a second authority: it reuses `resolveWithMode`, so
7
+ * plan mode's `exec`/`fs.write` floor and the bypass escape hatch behave
8
+ * identically for a formatter and for the write tool.
9
+ *
10
+ * Two differences from `evaluateToolCall` are deliberate. A `deny` policy is
11
+ * refused before any mode is consulted, because the policy permission is the
12
+ * configuration author's ceiling and `bypassPermissions` must not raise it.
13
+ * And an approved `ask` never persists a rule, because there is no tool name to
14
+ * persist it against.
15
+ */
16
+ import { resolveWithMode } from "./modes.js";
17
+ function describe(operation) {
18
+ return [operation.executable, ...operation.argv].join(" ");
19
+ }
20
+ export async function authorizeConfiguredCommand(operation, options) {
21
+ if (operation.permission === "deny") {
22
+ return { block: true, reason: `Policy ${operation.policyId} is configured with permission "deny".` };
23
+ }
24
+ const base = operation.permission === "allow" ? "allow" : "ask";
25
+ const resolution = resolveWithMode(await options.getMode(), { behavior: base }, operation.capabilities);
26
+ if (resolution.behavior === "allow")
27
+ return { block: false };
28
+ if (resolution.behavior === "deny") {
29
+ return { block: true, reason: `Policy ${operation.policyId} is not permitted in the current permission mode.` };
30
+ }
31
+ const responder = options.getResponder?.() ?? options.responder;
32
+ if (!responder) {
33
+ return {
34
+ block: true,
35
+ reason: `Policy ${operation.policyId} requires approval, and no responder is available in this session.`,
36
+ };
37
+ }
38
+ const answer = await responder.ask({ toolName: operation.policyId, description: describe(operation) });
39
+ if (!answer.allow) {
40
+ return { block: true, reason: `Policy ${operation.policyId} was declined.` };
41
+ }
42
+ return { block: false };
43
+ }
44
+ //# sourceMappingURL=policy-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-command.js","sourceRoot":"","sources":["../../../src/core/permissions/policy-command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA2B7C,SAAS,QAAQ,CAAC,SAAqC,EAAU;IAChE,OAAO,CAAC,SAAS,CAAC,UAAU,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CAC3D;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC/C,SAAqC,EACrC,OAA8C,EACtB;IACxB,IAAI,SAAS,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;QACrC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,SAAS,CAAC,QAAQ,wCAAwC,EAAE,CAAC;IACtG,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAChE,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IAExG,IAAI,UAAU,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC7D,IAAI,UAAU,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QACpC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,SAAS,CAAC,QAAQ,mDAAmD,EAAE,CAAC;IACjH,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,OAAO,CAAC,SAAS,CAAC;IAChE,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,OAAO;YACN,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,UAAU,SAAS,CAAC,QAAQ,oEAAoE;SACxG,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACvG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACnB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,SAAS,CAAC,QAAQ,gBAAgB,EAAE,CAAC;IAC9E,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,CACxB","sourcesContent":["/**\n * PS.1: the permission authority for configured commands (verification and\n * formatter policies from the VF.2 loader). Tool calls reach the gate through\n * `evaluateToolCall`; a configured command has no tool name and no rule\n * grammar, so it cannot use that path. This is the second entry point to the\n * *same* authority, not a second authority: it reuses `resolveWithMode`, so\n * plan mode's `exec`/`fs.write` floor and the bypass escape hatch behave\n * identically for a formatter and for the write tool.\n *\n * Two differences from `evaluateToolCall` are deliberate. A `deny` policy is\n * refused before any mode is consulted, because the policy permission is the\n * configuration author's ceiling and `bypassPermissions` must not raise it.\n * And an approved `ask` never persists a rule, because there is no tool name to\n * persist it against.\n */\n\nimport type { PolicyPermission } from \"../policy-loader.ts\";\nimport type { Capability } from \"../tools/contract.ts\";\nimport type { GateDecision } from \"./gate.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport type { PermissionMode } from \"./store.ts\";\n\n/** The typed facts a configured command presents for authorization. */\nexport interface ConfiguredCommandOperation {\n\tpolicyId: string;\n\texecutable: string;\n\targv: readonly string[];\n\tcwd: string;\n\t/** Declared write patterns already intersected with the policy's pathScope, or undefined for a non-mutating command. */\n\twriteScope: readonly string[] | undefined;\n\tcapabilities: ReadonlySet<Capability>;\n\tpermission: PolicyPermission;\n}\n\nexport interface ConfiguredCommandAuthorizationOptions {\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/** Absent in a non-interactive session: an `ask` policy then fails closed. */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\n/** Injected into the verification tracker and the formatter runner so neither imports the gate. */\nexport type AuthorizeConfiguredCommand = (operation: ConfiguredCommandOperation) => Promise<GateDecision>;\n\nfunction describe(operation: ConfiguredCommandOperation): string {\n\treturn [operation.executable, ...operation.argv].join(\" \");\n}\n\nexport async function authorizeConfiguredCommand(\n\toperation: ConfiguredCommandOperation,\n\toptions: ConfiguredCommandAuthorizationOptions,\n): Promise<GateDecision> {\n\tif (operation.permission === \"deny\") {\n\t\treturn { block: true, reason: `Policy ${operation.policyId} is configured with permission \"deny\".` };\n\t}\n\n\tconst base = operation.permission === \"allow\" ? \"allow\" : \"ask\";\n\tconst resolution = resolveWithMode(await options.getMode(), { behavior: base }, operation.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: `Policy ${operation.policyId} is not permitted in the current permission mode.` };\n\t}\n\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Policy ${operation.policyId} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst answer = await responder.ask({ toolName: operation.policyId, description: describe(operation) });\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `Policy ${operation.policyId} was declined.` };\n\t}\n\treturn { block: false };\n}\n"]}
@@ -1,20 +1,67 @@
1
1
  /** Interactive escalation for an `ask`-resolved permission decision. */
2
+ /**
3
+ * What the prompt shows about the change it is authorizing.
4
+ *
5
+ * `unavailable` is a first-class variant rather than an absent field, because a
6
+ * reader who is shown nothing cannot tell "this changes nothing" from "we could
7
+ * not read it". Only the second one should stop them approving.
8
+ */
9
+ export type PermissionPreview = {
10
+ kind: "diff";
11
+ path: string;
12
+ lines: readonly string[];
13
+ omittedLines: number;
14
+ } | {
15
+ kind: "summary";
16
+ lines: readonly string[];
17
+ } | {
18
+ kind: "unavailable";
19
+ reason: string;
20
+ };
2
21
  export interface PermissionAskRequest {
3
22
  toolName: string;
4
23
  /** Human-readable rendering of what would be persisted, from the tool's own describe(). */
5
24
  description: string;
25
+ /**
26
+ * Present only when the tool's `ruleForCall()` yields something to persist.
27
+ *
28
+ * The gate silently ignores `persist` when there is no rule, so offering the
29
+ * choice in that case promised a grant nothing would write.
30
+ */
31
+ sessionScope?: {
32
+ description: string;
33
+ };
34
+ /**
35
+ * Produced by the tool's `previewCall`, after the gate decides to ask and never
36
+ * before. Absent when the tool declares no producer.
37
+ */
38
+ preview?: PermissionPreview;
6
39
  }
7
40
  export interface PermissionAnswer {
8
41
  allow: boolean;
9
- /** "Always allow this" — persist a session-source rule via the tool's own ruleForCall(). */
42
+ /**
43
+ * Persist a session-source rule via the tool's own ruleForCall(), matching the
44
+ * answer: an allow writes an allow rule, a refusal writes a deny one. Either
45
+ * way it ends with the session.
46
+ */
10
47
  persist?: boolean;
48
+ /**
49
+ * On a denial, what the user wants done instead. The gate bounds it and carries
50
+ * it in `GateDecision.reason`, which the agent loop already turns into the
51
+ * blocked tool result, so no second message queue is involved.
52
+ */
53
+ guidance?: string;
11
54
  }
12
55
  export interface PermissionResponder {
13
56
  ask(request: PermissionAskRequest): Promise<PermissionAnswer>;
14
57
  }
15
58
  /** The subset of ExtensionUIContext (core/extensions/types.ts) this responder needs. */
16
59
  export interface SelectUI {
17
- select(title: string, options: string[]): Promise<string | undefined>;
60
+ select(title: string, options: string[], opts?: {
61
+ preview?: PermissionPreview;
62
+ }): Promise<string | undefined>;
63
+ /** Absent on hosts that cannot collect free text; the guidance choice is then not offered. */
64
+ input?(title: string, placeholder?: string): Promise<string | undefined>;
18
65
  }
19
66
  /**
20
67
  * Builds a PermissionResponder over the same live, mid-session prompt primitive
@@ -1 +1 @@
1
- {"version":3,"file":"responder.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/responder.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,8FAA4F;IAC5F,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IACnC,GAAG,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9D;AAMD,wFAAwF;AACxF,MAAM,WAAW,QAAQ;IACxB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,QAAQ,GAAG,mBAAmB,CAc5E","sourcesContent":["/** Interactive escalation for an `ask`-resolved permission decision. */\n\nexport interface PermissionAskRequest {\n\ttoolName: string;\n\t/** Human-readable rendering of what would be persisted, from the tool's own describe(). */\n\tdescription: string;\n}\n\nexport interface PermissionAnswer {\n\tallow: boolean;\n\t/** \"Always allow this\" — persist a session-source rule via the tool's own ruleForCall(). */\n\tpersist?: boolean;\n}\n\nexport interface PermissionResponder {\n\task(request: PermissionAskRequest): Promise<PermissionAnswer>;\n}\n\nconst ALLOW_ONCE = \"Allow once\";\nconst ALWAYS_ALLOW = \"Always allow\";\nconst DENY = \"Deny\";\n\n/** The subset of ExtensionUIContext (core/extensions/types.ts) this responder needs. */\nexport interface SelectUI {\n\tselect(title: string, options: string[]): Promise<string | undefined>;\n}\n\n/**\n * Builds a PermissionResponder over the same live, mid-session prompt primitive\n * extensions already use (ExtensionUIContext.select) — no new rendering\n * dependency, per the roadmap's \"Explicitly not building\" a second TUI stack.\n *\n * Deliberately answers only `{allow, persist}`. It never constructs a rule string\n * itself: generating one from `ruleForCall()` and persisting it is the gate's job\n * (permissions/gate.ts), so the tool that owns the grammar is the only thing that\n * ever writes a rule (ADR 0010).\n */\nexport function createInteractiveResponder(ui: SelectUI): PermissionResponder {\n\treturn {\n\t\tasync ask({ toolName, description }) {\n\t\t\tconst choice = await ui.select(`Permission required — ${toolName}: ${description}`, [\n\t\t\t\tALLOW_ONCE,\n\t\t\t\tALWAYS_ALLOW,\n\t\t\t\tDENY,\n\t\t\t]);\n\t\t\tif (choice === ALLOW_ONCE) return { allow: true };\n\t\t\tif (choice === ALWAYS_ALLOW) return { allow: true, persist: true };\n\t\t\t// DENY, or no selection (cancelled/dismissed) — fail closed.\n\t\t\treturn { allow: false };\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"responder.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/responder.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IACnC,GAAG,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC9D;AAQD,wFAAwF;AACxF,MAAM,WAAW,QAAQ;IACxB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC9G,8FAA8F;IAC9F,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,QAAQ,GAAG,mBAAmB,CA2B5E","sourcesContent":["/** Interactive escalation for an `ask`-resolved permission decision. */\n\n/**\n * What the prompt shows about the change it is authorizing.\n *\n * `unavailable` is a first-class variant rather than an absent field, because a\n * reader who is shown nothing cannot tell \"this changes nothing\" from \"we could\n * not read it\". Only the second one should stop them approving.\n */\nexport type PermissionPreview =\n\t| { kind: \"diff\"; path: string; lines: readonly string[]; omittedLines: number }\n\t| { kind: \"summary\"; lines: readonly string[] }\n\t| { kind: \"unavailable\"; reason: string };\n\nexport interface PermissionAskRequest {\n\ttoolName: string;\n\t/** Human-readable rendering of what would be persisted, from the tool's own describe(). */\n\tdescription: string;\n\t/**\n\t * Present only when the tool's `ruleForCall()` yields something to persist.\n\t *\n\t * The gate silently ignores `persist` when there is no rule, so offering the\n\t * choice in that case promised a grant nothing would write.\n\t */\n\tsessionScope?: { description: string };\n\t/**\n\t * Produced by the tool's `previewCall`, after the gate decides to ask and never\n\t * before. Absent when the tool declares no producer.\n\t */\n\tpreview?: PermissionPreview;\n}\n\nexport interface PermissionAnswer {\n\tallow: boolean;\n\t/**\n\t * Persist a session-source rule via the tool's own ruleForCall(), matching the\n\t * answer: an allow writes an allow rule, a refusal writes a deny one. Either\n\t * way it ends with the session.\n\t */\n\tpersist?: boolean;\n\t/**\n\t * On a denial, what the user wants done instead. The gate bounds it and carries\n\t * it in `GateDecision.reason`, which the agent loop already turns into the\n\t * blocked tool result, so no second message queue is involved.\n\t */\n\tguidance?: string;\n}\n\nexport interface PermissionResponder {\n\task(request: PermissionAskRequest): Promise<PermissionAnswer>;\n}\n\nconst ALLOW_ONCE = \"Allow once\";\n/** Named for what the gate actually writes: an `addRules` update whose destination is \"session\". */\nconst ALLOW_SESSION = \"Allow for this session\";\nconst REJECT_WITH_GUIDANCE = \"Reject and say what to do instead\";\nconst DENY = \"Deny\";\n\n/** The subset of ExtensionUIContext (core/extensions/types.ts) this responder needs. */\nexport interface SelectUI {\n\tselect(title: string, options: string[], opts?: { preview?: PermissionPreview }): Promise<string | undefined>;\n\t/** Absent on hosts that cannot collect free text; the guidance choice is then not offered. */\n\tinput?(title: string, placeholder?: string): Promise<string | undefined>;\n}\n\n/**\n * Builds a PermissionResponder over the same live, mid-session prompt primitive\n * extensions already use (ExtensionUIContext.select) — no new rendering\n * dependency, per the roadmap's \"Explicitly not building\" a second TUI stack.\n *\n * Deliberately answers only `{allow, persist}`. It never constructs a rule string\n * itself: generating one from `ruleForCall()` and persisting it is the gate's job\n * (permissions/gate.ts), so the tool that owns the grammar is the only thing that\n * ever writes a rule (ADR 0010).\n */\nexport function createInteractiveResponder(ui: SelectUI): PermissionResponder {\n\treturn {\n\t\tasync ask({ toolName, description, sessionScope, preview }) {\n\t\t\tconst options = [ALLOW_ONCE];\n\t\t\tif (sessionScope) options.push(ALLOW_SESSION);\n\t\t\tif (ui.input) options.push(REJECT_WITH_GUIDANCE);\n\t\t\toptions.push(DENY);\n\n\t\t\t// The preview travels as data. Drawing it belongs to whatever is hosting\n\t\t\t// the prompt, which keeps this file free of a rendering dependency.\n\t\t\tconst choice = await ui.select(\n\t\t\t\t`Permission required — ${toolName}: ${description}`,\n\t\t\t\toptions,\n\t\t\t\tpreview ? { preview } : undefined,\n\t\t\t);\n\t\t\tif (choice === ALLOW_ONCE) return { allow: true };\n\t\t\tif (choice === ALLOW_SESSION) return { allow: true, persist: true };\n\t\t\tif (choice === REJECT_WITH_GUIDANCE && ui.input) {\n\t\t\t\tconst guidance = await ui.input(`What should Apex do instead of ${toolName}?`);\n\t\t\t\t// A dismissed guidance prompt is still a denial. Failing closed here\n\t\t\t\t// matches every other path out of this function.\n\t\t\t\treturn guidance?.trim() ? { allow: false, guidance: guidance.trim() } : { allow: false };\n\t\t\t}\n\t\t\t// DENY, or no selection (cancelled/dismissed) — fail closed.\n\t\t\treturn { allow: false };\n\t\t},\n\t};\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  /** Interactive escalation for an `ask`-resolved permission decision. */
2
2
  const ALLOW_ONCE = "Allow once";
3
- const ALWAYS_ALLOW = "Always allow";
3
+ /** Named for what the gate actually writes: an `addRules` update whose destination is "session". */
4
+ const ALLOW_SESSION = "Allow for this session";
5
+ const REJECT_WITH_GUIDANCE = "Reject and say what to do instead";
4
6
  const DENY = "Deny";
5
7
  /**
6
8
  * Builds a PermissionResponder over the same live, mid-session prompt primitive
@@ -14,16 +16,26 @@ const DENY = "Deny";
14
16
  */
15
17
  export function createInteractiveResponder(ui) {
16
18
  return {
17
- async ask({ toolName, description }) {
18
- const choice = await ui.select(`Permission required — ${toolName}: ${description}`, [
19
- ALLOW_ONCE,
20
- ALWAYS_ALLOW,
21
- DENY,
22
- ]);
19
+ async ask({ toolName, description, sessionScope, preview }) {
20
+ const options = [ALLOW_ONCE];
21
+ if (sessionScope)
22
+ options.push(ALLOW_SESSION);
23
+ if (ui.input)
24
+ options.push(REJECT_WITH_GUIDANCE);
25
+ options.push(DENY);
26
+ // The preview travels as data. Drawing it belongs to whatever is hosting
27
+ // the prompt, which keeps this file free of a rendering dependency.
28
+ const choice = await ui.select(`Permission required — ${toolName}: ${description}`, options, preview ? { preview } : undefined);
23
29
  if (choice === ALLOW_ONCE)
24
30
  return { allow: true };
25
- if (choice === ALWAYS_ALLOW)
31
+ if (choice === ALLOW_SESSION)
26
32
  return { allow: true, persist: true };
33
+ if (choice === REJECT_WITH_GUIDANCE && ui.input) {
34
+ const guidance = await ui.input(`What should Apex do instead of ${toolName}?`);
35
+ // A dismissed guidance prompt is still a denial. Failing closed here
36
+ // matches every other path out of this function.
37
+ return guidance?.trim() ? { allow: false, guidance: guidance.trim() } : { allow: false };
38
+ }
27
39
  // DENY, or no selection (cancelled/dismissed) — fail closed.
28
40
  return { allow: false };
29
41
  },
@@ -1 +1 @@
1
- {"version":3,"file":"responder.js","sourceRoot":"","sources":["../../../src/core/permissions/responder.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAkBxE,MAAM,UAAU,GAAG,YAAY,CAAC;AAChC,MAAM,YAAY,GAAG,cAAc,CAAC;AACpC,MAAM,IAAI,GAAG,MAAM,CAAC;AAOpB;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CAAC,EAAY,EAAuB;IAC7E,OAAO;QACN,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE;YACpC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,2BAAyB,QAAQ,KAAK,WAAW,EAAE,EAAE;gBACnF,UAAU;gBACV,YAAY;gBACZ,IAAI;aACJ,CAAC,CAAC;YACH,IAAI,MAAM,KAAK,UAAU;gBAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YAClD,IAAI,MAAM,KAAK,YAAY;gBAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACnE,+DAA6D;YAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAAA,CACxB;KACD,CAAC;AAAA,CACF","sourcesContent":["/** Interactive escalation for an `ask`-resolved permission decision. */\n\nexport interface PermissionAskRequest {\n\ttoolName: string;\n\t/** Human-readable rendering of what would be persisted, from the tool's own describe(). */\n\tdescription: string;\n}\n\nexport interface PermissionAnswer {\n\tallow: boolean;\n\t/** \"Always allow this\" — persist a session-source rule via the tool's own ruleForCall(). */\n\tpersist?: boolean;\n}\n\nexport interface PermissionResponder {\n\task(request: PermissionAskRequest): Promise<PermissionAnswer>;\n}\n\nconst ALLOW_ONCE = \"Allow once\";\nconst ALWAYS_ALLOW = \"Always allow\";\nconst DENY = \"Deny\";\n\n/** The subset of ExtensionUIContext (core/extensions/types.ts) this responder needs. */\nexport interface SelectUI {\n\tselect(title: string, options: string[]): Promise<string | undefined>;\n}\n\n/**\n * Builds a PermissionResponder over the same live, mid-session prompt primitive\n * extensions already use (ExtensionUIContext.select) — no new rendering\n * dependency, per the roadmap's \"Explicitly not building\" a second TUI stack.\n *\n * Deliberately answers only `{allow, persist}`. It never constructs a rule string\n * itself: generating one from `ruleForCall()` and persisting it is the gate's job\n * (permissions/gate.ts), so the tool that owns the grammar is the only thing that\n * ever writes a rule (ADR 0010).\n */\nexport function createInteractiveResponder(ui: SelectUI): PermissionResponder {\n\treturn {\n\t\tasync ask({ toolName, description }) {\n\t\t\tconst choice = await ui.select(`Permission required — ${toolName}: ${description}`, [\n\t\t\t\tALLOW_ONCE,\n\t\t\t\tALWAYS_ALLOW,\n\t\t\t\tDENY,\n\t\t\t]);\n\t\t\tif (choice === ALLOW_ONCE) return { allow: true };\n\t\t\tif (choice === ALWAYS_ALLOW) return { allow: true, persist: true };\n\t\t\t// DENY, or no selection (cancelled/dismissed) — fail closed.\n\t\t\treturn { allow: false };\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"responder.js","sourceRoot":"","sources":["../../../src/core/permissions/responder.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAoDxE,MAAM,UAAU,GAAG,YAAY,CAAC;AAChC,oGAAoG;AACpG,MAAM,aAAa,GAAG,wBAAwB,CAAC;AAC/C,MAAM,oBAAoB,GAAG,mCAAmC,CAAC;AACjE,MAAM,IAAI,GAAG,MAAM,CAAC;AASpB;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CAAC,EAAY,EAAuB;IAC7E,OAAO;QACN,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE;YAC3D,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,CAAC;YAC7B,IAAI,YAAY;gBAAE,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC9C,IAAI,EAAE,CAAC,KAAK;gBAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEnB,yEAAyE;YACzE,oEAAoE;YACpE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAC7B,2BAAyB,QAAQ,KAAK,WAAW,EAAE,EACnD,OAAO,EACP,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CACjC,CAAC;YACF,IAAI,MAAM,KAAK,UAAU;gBAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YAClD,IAAI,MAAM,KAAK,aAAa;gBAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACpE,IAAI,MAAM,KAAK,oBAAoB,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;gBACjD,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,kCAAkC,QAAQ,GAAG,CAAC,CAAC;gBAC/E,qEAAqE;gBACrE,iDAAiD;gBACjD,OAAO,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;YAC1F,CAAC;YACD,+DAA6D;YAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAAA,CACxB;KACD,CAAC;AAAA,CACF","sourcesContent":["/** Interactive escalation for an `ask`-resolved permission decision. */\n\n/**\n * What the prompt shows about the change it is authorizing.\n *\n * `unavailable` is a first-class variant rather than an absent field, because a\n * reader who is shown nothing cannot tell \"this changes nothing\" from \"we could\n * not read it\". Only the second one should stop them approving.\n */\nexport type PermissionPreview =\n\t| { kind: \"diff\"; path: string; lines: readonly string[]; omittedLines: number }\n\t| { kind: \"summary\"; lines: readonly string[] }\n\t| { kind: \"unavailable\"; reason: string };\n\nexport interface PermissionAskRequest {\n\ttoolName: string;\n\t/** Human-readable rendering of what would be persisted, from the tool's own describe(). */\n\tdescription: string;\n\t/**\n\t * Present only when the tool's `ruleForCall()` yields something to persist.\n\t *\n\t * The gate silently ignores `persist` when there is no rule, so offering the\n\t * choice in that case promised a grant nothing would write.\n\t */\n\tsessionScope?: { description: string };\n\t/**\n\t * Produced by the tool's `previewCall`, after the gate decides to ask and never\n\t * before. Absent when the tool declares no producer.\n\t */\n\tpreview?: PermissionPreview;\n}\n\nexport interface PermissionAnswer {\n\tallow: boolean;\n\t/**\n\t * Persist a session-source rule via the tool's own ruleForCall(), matching the\n\t * answer: an allow writes an allow rule, a refusal writes a deny one. Either\n\t * way it ends with the session.\n\t */\n\tpersist?: boolean;\n\t/**\n\t * On a denial, what the user wants done instead. The gate bounds it and carries\n\t * it in `GateDecision.reason`, which the agent loop already turns into the\n\t * blocked tool result, so no second message queue is involved.\n\t */\n\tguidance?: string;\n}\n\nexport interface PermissionResponder {\n\task(request: PermissionAskRequest): Promise<PermissionAnswer>;\n}\n\nconst ALLOW_ONCE = \"Allow once\";\n/** Named for what the gate actually writes: an `addRules` update whose destination is \"session\". */\nconst ALLOW_SESSION = \"Allow for this session\";\nconst REJECT_WITH_GUIDANCE = \"Reject and say what to do instead\";\nconst DENY = \"Deny\";\n\n/** The subset of ExtensionUIContext (core/extensions/types.ts) this responder needs. */\nexport interface SelectUI {\n\tselect(title: string, options: string[], opts?: { preview?: PermissionPreview }): Promise<string | undefined>;\n\t/** Absent on hosts that cannot collect free text; the guidance choice is then not offered. */\n\tinput?(title: string, placeholder?: string): Promise<string | undefined>;\n}\n\n/**\n * Builds a PermissionResponder over the same live, mid-session prompt primitive\n * extensions already use (ExtensionUIContext.select) — no new rendering\n * dependency, per the roadmap's \"Explicitly not building\" a second TUI stack.\n *\n * Deliberately answers only `{allow, persist}`. It never constructs a rule string\n * itself: generating one from `ruleForCall()` and persisting it is the gate's job\n * (permissions/gate.ts), so the tool that owns the grammar is the only thing that\n * ever writes a rule (ADR 0010).\n */\nexport function createInteractiveResponder(ui: SelectUI): PermissionResponder {\n\treturn {\n\t\tasync ask({ toolName, description, sessionScope, preview }) {\n\t\t\tconst options = [ALLOW_ONCE];\n\t\t\tif (sessionScope) options.push(ALLOW_SESSION);\n\t\t\tif (ui.input) options.push(REJECT_WITH_GUIDANCE);\n\t\t\toptions.push(DENY);\n\n\t\t\t// The preview travels as data. Drawing it belongs to whatever is hosting\n\t\t\t// the prompt, which keeps this file free of a rendering dependency.\n\t\t\tconst choice = await ui.select(\n\t\t\t\t`Permission required — ${toolName}: ${description}`,\n\t\t\t\toptions,\n\t\t\t\tpreview ? { preview } : undefined,\n\t\t\t);\n\t\t\tif (choice === ALLOW_ONCE) return { allow: true };\n\t\t\tif (choice === ALLOW_SESSION) return { allow: true, persist: true };\n\t\t\tif (choice === REJECT_WITH_GUIDANCE && ui.input) {\n\t\t\t\tconst guidance = await ui.input(`What should Apex do instead of ${toolName}?`);\n\t\t\t\t// A dismissed guidance prompt is still a denial. Failing closed here\n\t\t\t\t// matches every other path out of this function.\n\t\t\t\treturn guidance?.trim() ? { allow: false, guidance: guidance.trim() } : { allow: false };\n\t\t\t}\n\t\t\t// DENY, or no selection (cancelled/dismissed) — fail closed.\n\t\t\treturn { allow: false };\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/rules.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,yFASrB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,8FAA8F;IAC9F,IAAI,CAAC,EAAE,cAAc,CAAC;CACtB;AA8BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,OAAO,EACxD,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,cAAc,CAAC,OAAO,CAAC,EAC7B,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,GACrB,oBAAoB,CAkBtB","sourcesContent":["/**\n * The permission rule model (ADR 0004). Pure and tool-agnostic: this module never\n * inspects a `ruleContent` grammar itself — it only calls the tool's own\n * `PermissionSpec.matches()`, per ADR 0010's rule that a tool owns both directions\n * of its grammar and the engine never accumulates tool-specific matching.\n */\n\nimport type { Static, TSchema } from \"typebox\";\nimport type { PermissionBehavior, PermissionSpec } from \"../tools/contract.ts\";\n\n/**\n * Highest to lowest precedence. `flag` outranks config because it is a deliberate\n * per-run operator override; `cliArg` sits below config because it is a per-run\n * *default*, not an override. See ADR 0004 for why each source sits where it does.\n */\nexport const PERMISSION_SOURCES = [\n\t\"policy\",\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"cliArg\",\n\t\"command\",\n\t\"session\",\n] as const;\n\nexport type PermissionSource = (typeof PERMISSION_SOURCES)[number];\n\nexport interface PermissionRule {\n\tsource: PermissionSource;\n\tbehavior: PermissionBehavior;\n\ttoolName: string;\n\t/** Undefined matches every call to this tool — a tool-wide rule. */\n\truleContent?: string;\n}\n\nexport interface PermissionResolution {\n\tbehavior: PermissionBehavior;\n\t/** The winning rule, or undefined when no rule matched and the tool's own default applied. */\n\trule?: PermissionRule;\n}\n\nconst SOURCE_RANK = new Map<PermissionSource, number>(PERMISSION_SOURCES.map((source, index) => [source, index]));\n\nfunction ruleMatchesCall<TParams extends TSchema>(\n\trule: PermissionRule,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): boolean {\n\tif (rule.ruleContent === undefined) return true;\n\treturn spec.matches(rule.ruleContent, params);\n}\n\n/**\n * Deterministic tie-break for two matching rules at the *same* source, independent\n * of array order: a rule with `ruleContent` (specific) outranks a tool-wide rule\n * with none (blanket); between two specific rules, the longer `ruleContent` wins\n * (a proxy for \"more specific\"); a final lexical comparison guarantees a total,\n * order-independent order for the residual case of equal-length content.\n */\nfunction compareSameSourceRules(a: PermissionRule, b: PermissionRule): number {\n\tconst aSpecific = a.ruleContent !== undefined;\n\tconst bSpecific = b.ruleContent !== undefined;\n\tif (aSpecific !== bSpecific) return aSpecific ? -1 : 1; // specific wins (negative = a first)\n\tif (!aSpecific) return 0; // both blanket at the same source: no further distinction needed\n\tconst lengthDelta = (b.ruleContent as string).length - (a.ruleContent as string).length;\n\tif (lengthDelta !== 0) return lengthDelta;\n\treturn (a.ruleContent as string).localeCompare(b.ruleContent as string);\n}\n\n/**\n * Resolve the effective behavior for one call. The highest-precedence *matching*\n * rule wins, whatever its behavior — `deny` does not automatically outrank `allow`.\n * With no matching rule, the tool's own `defaultBehavior` applies.\n */\nexport function resolvePermission<TParams extends TSchema>(\n\trules: readonly PermissionRule[],\n\ttoolName: string,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): PermissionResolution {\n\tlet winner: PermissionRule | undefined;\n\tfor (const candidate of rules) {\n\t\tif (candidate.toolName !== toolName) continue;\n\t\tif (!ruleMatchesCall(candidate, spec, params)) continue;\n\t\tif (!winner) {\n\t\t\twinner = candidate;\n\t\t\tcontinue;\n\t\t}\n\t\tconst candidateRank = SOURCE_RANK.get(candidate.source) ?? Number.POSITIVE_INFINITY;\n\t\tconst winnerRank = SOURCE_RANK.get(winner.source) ?? Number.POSITIVE_INFINITY;\n\t\tif (candidateRank < winnerRank) {\n\t\t\twinner = candidate;\n\t\t} else if (candidateRank === winnerRank && compareSameSourceRules(candidate, winner) < 0) {\n\t\t\twinner = candidate;\n\t\t}\n\t}\n\treturn winner ? { behavior: winner.behavior, rule: winner } : { behavior: spec.defaultBehavior };\n}\n"]}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/rules.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,yFASrB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,8FAA8F;IAC9F,IAAI,CAAC,EAAE,cAAc,CAAC;CACtB;AAoCD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,OAAO,EACxD,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,cAAc,CAAC,OAAO,CAAC,EAC7B,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,GACrB,oBAAoB,CAoBtB","sourcesContent":["/**\n * The permission rule model (ADR 0004). Pure and tool-agnostic: this module never\n * inspects a `ruleContent` grammar itself — it only calls the tool's own\n * `PermissionSpec.matches()`, per ADR 0010's rule that a tool owns both directions\n * of its grammar and the engine never accumulates tool-specific matching.\n */\n\nimport type { Static, TSchema } from \"typebox\";\nimport type { PermissionBehavior, PermissionSpec } from \"../tools/contract.ts\";\n\n/**\n * Highest to lowest precedence. `flag` outranks config because it is a deliberate\n * per-run operator override; `cliArg` sits below config because it is a per-run\n * *default*, not an override. See ADR 0004 for why each source sits where it does.\n */\nexport const PERMISSION_SOURCES = [\n\t\"policy\",\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"cliArg\",\n\t\"command\",\n\t\"session\",\n] as const;\n\nexport type PermissionSource = (typeof PERMISSION_SOURCES)[number];\n\nexport interface PermissionRule {\n\tsource: PermissionSource;\n\tbehavior: PermissionBehavior;\n\ttoolName: string;\n\t/** Undefined matches every call to this tool — a tool-wide rule. */\n\truleContent?: string;\n}\n\nexport interface PermissionResolution {\n\tbehavior: PermissionBehavior;\n\t/** The winning rule, or undefined when no rule matched and the tool's own default applied. */\n\trule?: PermissionRule;\n}\n\nconst SOURCE_RANK = new Map<PermissionSource, number>(PERMISSION_SOURCES.map((source, index) => [source, index]));\n\nfunction ruleMatchesCall<TParams extends TSchema>(\n\trule: PermissionRule,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): boolean {\n\tif (spec.isUnknown?.(params)) {\n\t\tif (rule.behavior === \"allow\") return false;\n\t\tif (rule.behavior === \"deny\") return true;\n\t}\n\tif (rule.ruleContent === undefined) return true;\n\tif (rule.behavior === \"deny\")\n\t\treturn spec.matchesDeny?.(rule.ruleContent, params) ?? spec.matches(rule.ruleContent, params);\n\treturn spec.matches(rule.ruleContent, params);\n}\n\n/**\n * Deterministic tie-break for two matching rules at the *same* source, independent\n * of array order: a rule with `ruleContent` (specific) outranks a tool-wide rule\n * with none (blanket); between two specific rules, the longer `ruleContent` wins\n * (a proxy for \"more specific\"); a final lexical comparison guarantees a total,\n * order-independent order for the residual case of equal-length content.\n */\nfunction compareSameSourceRules(a: PermissionRule, b: PermissionRule): number {\n\tconst aSpecific = a.ruleContent !== undefined;\n\tconst bSpecific = b.ruleContent !== undefined;\n\tif (aSpecific !== bSpecific) return aSpecific ? -1 : 1; // specific wins (negative = a first)\n\tif (!aSpecific) return 0; // both blanket at the same source: no further distinction needed\n\tconst lengthDelta = (b.ruleContent as string).length - (a.ruleContent as string).length;\n\tif (lengthDelta !== 0) return lengthDelta;\n\treturn (a.ruleContent as string).localeCompare(b.ruleContent as string);\n}\n\n/**\n * Resolve the effective behavior for one call. The highest-precedence *matching*\n * rule wins, whatever its behavior — `deny` does not automatically outrank `allow`.\n * With no matching rule, the tool's own `defaultBehavior` applies.\n */\nexport function resolvePermission<TParams extends TSchema>(\n\trules: readonly PermissionRule[],\n\ttoolName: string,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): PermissionResolution {\n\tlet winner: PermissionRule | undefined;\n\tfor (const candidate of rules) {\n\t\tif (candidate.toolName !== toolName) continue;\n\t\tif (!ruleMatchesCall(candidate, spec, params)) continue;\n\t\tif (!winner) {\n\t\t\twinner = candidate;\n\t\t\tcontinue;\n\t\t}\n\t\tconst candidateRank = SOURCE_RANK.get(candidate.source) ?? Number.POSITIVE_INFINITY;\n\t\tconst winnerRank = SOURCE_RANK.get(winner.source) ?? Number.POSITIVE_INFINITY;\n\t\tif (candidateRank < winnerRank) {\n\t\t\twinner = candidate;\n\t\t} else if (candidateRank === winnerRank && compareSameSourceRules(candidate, winner) < 0) {\n\t\t\twinner = candidate;\n\t\t}\n\t}\n\treturn winner\n\t\t? { behavior: winner.behavior, rule: winner }\n\t\t: { behavior: spec.defaultBehaviorFor?.(params) ?? spec.defaultBehavior };\n}\n"]}
@@ -21,8 +21,16 @@ export const PERMISSION_SOURCES = [
21
21
  ];
22
22
  const SOURCE_RANK = new Map(PERMISSION_SOURCES.map((source, index) => [source, index]));
23
23
  function ruleMatchesCall(rule, spec, params) {
24
+ if (spec.isUnknown?.(params)) {
25
+ if (rule.behavior === "allow")
26
+ return false;
27
+ if (rule.behavior === "deny")
28
+ return true;
29
+ }
24
30
  if (rule.ruleContent === undefined)
25
31
  return true;
32
+ if (rule.behavior === "deny")
33
+ return spec.matchesDeny?.(rule.ruleContent, params) ?? spec.matches(rule.ruleContent, params);
26
34
  return spec.matches(rule.ruleContent, params);
27
35
  }
28
36
  /**
@@ -69,6 +77,8 @@ export function resolvePermission(rules, toolName, spec, params) {
69
77
  winner = candidate;
70
78
  }
71
79
  }
72
- return winner ? { behavior: winner.behavior, rule: winner } : { behavior: spec.defaultBehavior };
80
+ return winner
81
+ ? { behavior: winner.behavior, rule: winner }
82
+ : { behavior: spec.defaultBehaviorFor?.(params) ?? spec.defaultBehavior };
73
83
  }
74
84
  //# sourceMappingURL=rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../../src/core/permissions/rules.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IACjC,QAAQ;IACR,MAAM;IACN,OAAO;IACP,SAAS;IACT,MAAM;IACN,QAAQ;IACR,SAAS;IACT,SAAS;CACA,CAAC;AAkBX,MAAM,WAAW,GAAG,IAAI,GAAG,CAA2B,kBAAkB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAElH,SAAS,eAAe,CACvB,IAAoB,EACpB,IAA6B,EAC7B,MAAuB,EACb;IACV,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;AAAA,CAC9C;AAED;;;;;;GAMG;AACH,SAAS,sBAAsB,CAAC,CAAiB,EAAE,CAAiB,EAAU;IAC7E,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC;IAC9C,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC;IAC9C,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,qCAAqC;IAC7F,IAAI,CAAC,SAAS;QAAE,OAAO,CAAC,CAAC,CAAC,iEAAiE;IAC3F,MAAM,WAAW,GAAI,CAAC,CAAC,WAAsB,CAAC,MAAM,GAAI,CAAC,CAAC,WAAsB,CAAC,MAAM,CAAC;IACxF,IAAI,WAAW,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC;IAC1C,OAAQ,CAAC,CAAC,WAAsB,CAAC,aAAa,CAAC,CAAC,CAAC,WAAqB,CAAC,CAAC;AAAA,CACxE;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAChC,KAAgC,EAChC,QAAgB,EAChB,IAA6B,EAC7B,MAAuB,EACA;IACvB,IAAI,MAAkC,CAAC;IACvC,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,SAAS,CAAC,QAAQ,KAAK,QAAQ;YAAE,SAAS;QAC9C,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC;YAAE,SAAS;QACxD,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,GAAG,SAAS,CAAC;YACnB,SAAS;QACV,CAAC;QACD,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,iBAAiB,CAAC;QACpF,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,iBAAiB,CAAC;QAC9E,IAAI,aAAa,GAAG,UAAU,EAAE,CAAC;YAChC,MAAM,GAAG,SAAS,CAAC;QACpB,CAAC;aAAM,IAAI,aAAa,KAAK,UAAU,IAAI,sBAAsB,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1F,MAAM,GAAG,SAAS,CAAC;QACpB,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC;AAAA,CACjG","sourcesContent":["/**\n * The permission rule model (ADR 0004). Pure and tool-agnostic: this module never\n * inspects a `ruleContent` grammar itself — it only calls the tool's own\n * `PermissionSpec.matches()`, per ADR 0010's rule that a tool owns both directions\n * of its grammar and the engine never accumulates tool-specific matching.\n */\n\nimport type { Static, TSchema } from \"typebox\";\nimport type { PermissionBehavior, PermissionSpec } from \"../tools/contract.ts\";\n\n/**\n * Highest to lowest precedence. `flag` outranks config because it is a deliberate\n * per-run operator override; `cliArg` sits below config because it is a per-run\n * *default*, not an override. See ADR 0004 for why each source sits where it does.\n */\nexport const PERMISSION_SOURCES = [\n\t\"policy\",\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"cliArg\",\n\t\"command\",\n\t\"session\",\n] as const;\n\nexport type PermissionSource = (typeof PERMISSION_SOURCES)[number];\n\nexport interface PermissionRule {\n\tsource: PermissionSource;\n\tbehavior: PermissionBehavior;\n\ttoolName: string;\n\t/** Undefined matches every call to this tool — a tool-wide rule. */\n\truleContent?: string;\n}\n\nexport interface PermissionResolution {\n\tbehavior: PermissionBehavior;\n\t/** The winning rule, or undefined when no rule matched and the tool's own default applied. */\n\trule?: PermissionRule;\n}\n\nconst SOURCE_RANK = new Map<PermissionSource, number>(PERMISSION_SOURCES.map((source, index) => [source, index]));\n\nfunction ruleMatchesCall<TParams extends TSchema>(\n\trule: PermissionRule,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): boolean {\n\tif (rule.ruleContent === undefined) return true;\n\treturn spec.matches(rule.ruleContent, params);\n}\n\n/**\n * Deterministic tie-break for two matching rules at the *same* source, independent\n * of array order: a rule with `ruleContent` (specific) outranks a tool-wide rule\n * with none (blanket); between two specific rules, the longer `ruleContent` wins\n * (a proxy for \"more specific\"); a final lexical comparison guarantees a total,\n * order-independent order for the residual case of equal-length content.\n */\nfunction compareSameSourceRules(a: PermissionRule, b: PermissionRule): number {\n\tconst aSpecific = a.ruleContent !== undefined;\n\tconst bSpecific = b.ruleContent !== undefined;\n\tif (aSpecific !== bSpecific) return aSpecific ? -1 : 1; // specific wins (negative = a first)\n\tif (!aSpecific) return 0; // both blanket at the same source: no further distinction needed\n\tconst lengthDelta = (b.ruleContent as string).length - (a.ruleContent as string).length;\n\tif (lengthDelta !== 0) return lengthDelta;\n\treturn (a.ruleContent as string).localeCompare(b.ruleContent as string);\n}\n\n/**\n * Resolve the effective behavior for one call. The highest-precedence *matching*\n * rule wins, whatever its behavior — `deny` does not automatically outrank `allow`.\n * With no matching rule, the tool's own `defaultBehavior` applies.\n */\nexport function resolvePermission<TParams extends TSchema>(\n\trules: readonly PermissionRule[],\n\ttoolName: string,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): PermissionResolution {\n\tlet winner: PermissionRule | undefined;\n\tfor (const candidate of rules) {\n\t\tif (candidate.toolName !== toolName) continue;\n\t\tif (!ruleMatchesCall(candidate, spec, params)) continue;\n\t\tif (!winner) {\n\t\t\twinner = candidate;\n\t\t\tcontinue;\n\t\t}\n\t\tconst candidateRank = SOURCE_RANK.get(candidate.source) ?? Number.POSITIVE_INFINITY;\n\t\tconst winnerRank = SOURCE_RANK.get(winner.source) ?? Number.POSITIVE_INFINITY;\n\t\tif (candidateRank < winnerRank) {\n\t\t\twinner = candidate;\n\t\t} else if (candidateRank === winnerRank && compareSameSourceRules(candidate, winner) < 0) {\n\t\t\twinner = candidate;\n\t\t}\n\t}\n\treturn winner ? { behavior: winner.behavior, rule: winner } : { behavior: spec.defaultBehavior };\n}\n"]}
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../../src/core/permissions/rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The permission rule model (ADR 0004). Pure and tool-agnostic: this module never\n * inspects a `ruleContent` grammar itself — it only calls the tool's own\n * `PermissionSpec.matches()`, per ADR 0010's rule that a tool owns both directions\n * of its grammar and the engine never accumulates tool-specific matching.\n */\n\nimport type { Static, TSchema } from \"typebox\";\nimport type { PermissionBehavior, PermissionSpec } from \"../tools/contract.ts\";\n\n/**\n * Highest to lowest precedence. `flag` outranks config because it is a deliberate\n * per-run operator override; `cliArg` sits below config because it is a per-run\n * *default*, not an override. See ADR 0004 for why each source sits where it does.\n */\nexport const PERMISSION_SOURCES = [\n\t\"policy\",\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"cliArg\",\n\t\"command\",\n\t\"session\",\n] as const;\n\nexport type PermissionSource = (typeof PERMISSION_SOURCES)[number];\n\nexport interface PermissionRule {\n\tsource: PermissionSource;\n\tbehavior: PermissionBehavior;\n\ttoolName: string;\n\t/** Undefined matches every call to this tool — a tool-wide rule. */\n\truleContent?: string;\n}\n\nexport interface PermissionResolution {\n\tbehavior: PermissionBehavior;\n\t/** The winning rule, or undefined when no rule matched and the tool's own default applied. */\n\trule?: PermissionRule;\n}\n\nconst SOURCE_RANK = new Map<PermissionSource, number>(PERMISSION_SOURCES.map((source, index) => [source, index]));\n\nfunction ruleMatchesCall<TParams extends TSchema>(\n\trule: PermissionRule,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): boolean {\n\tif (spec.isUnknown?.(params)) {\n\t\tif (rule.behavior === \"allow\") return false;\n\t\tif (rule.behavior === \"deny\") return true;\n\t}\n\tif (rule.ruleContent === undefined) return true;\n\tif (rule.behavior === \"deny\")\n\t\treturn spec.matchesDeny?.(rule.ruleContent, params) ?? spec.matches(rule.ruleContent, params);\n\treturn spec.matches(rule.ruleContent, params);\n}\n\n/**\n * Deterministic tie-break for two matching rules at the *same* source, independent\n * of array order: a rule with `ruleContent` (specific) outranks a tool-wide rule\n * with none (blanket); between two specific rules, the longer `ruleContent` wins\n * (a proxy for \"more specific\"); a final lexical comparison guarantees a total,\n * order-independent order for the residual case of equal-length content.\n */\nfunction compareSameSourceRules(a: PermissionRule, b: PermissionRule): number {\n\tconst aSpecific = a.ruleContent !== undefined;\n\tconst bSpecific = b.ruleContent !== undefined;\n\tif (aSpecific !== bSpecific) return aSpecific ? -1 : 1; // specific wins (negative = a first)\n\tif (!aSpecific) return 0; // both blanket at the same source: no further distinction needed\n\tconst lengthDelta = (b.ruleContent as string).length - (a.ruleContent as string).length;\n\tif (lengthDelta !== 0) return lengthDelta;\n\treturn (a.ruleContent as string).localeCompare(b.ruleContent as string);\n}\n\n/**\n * Resolve the effective behavior for one call. The highest-precedence *matching*\n * rule wins, whatever its behavior — `deny` does not automatically outrank `allow`.\n * With no matching rule, the tool's own `defaultBehavior` applies.\n */\nexport function resolvePermission<TParams extends TSchema>(\n\trules: readonly PermissionRule[],\n\ttoolName: string,\n\tspec: PermissionSpec<TParams>,\n\tparams: Static<TParams>,\n): PermissionResolution {\n\tlet winner: PermissionRule | undefined;\n\tfor (const candidate of rules) {\n\t\tif (candidate.toolName !== toolName) continue;\n\t\tif (!ruleMatchesCall(candidate, spec, params)) continue;\n\t\tif (!winner) {\n\t\t\twinner = candidate;\n\t\t\tcontinue;\n\t\t}\n\t\tconst candidateRank = SOURCE_RANK.get(candidate.source) ?? Number.POSITIVE_INFINITY;\n\t\tconst winnerRank = SOURCE_RANK.get(winner.source) ?? Number.POSITIVE_INFINITY;\n\t\tif (candidateRank < winnerRank) {\n\t\t\twinner = candidate;\n\t\t} else if (candidateRank === winnerRank && compareSameSourceRules(candidate, winner) < 0) {\n\t\t\twinner = candidate;\n\t\t}\n\t}\n\treturn winner\n\t\t? { behavior: winner.behavior, rule: winner }\n\t\t: { behavior: spec.defaultBehaviorFor?.(params) ?? spec.defaultBehavior };\n}\n"]}
@@ -62,21 +62,39 @@ export interface CreateFilePermissionRuleStoreOptions {
62
62
  agentDir?: string;
63
63
  /** Overrides the managed policy file path. Defaults to $APEX_CODE_POLICY_PATH, else an OS-specific system location. */
64
64
  policyPath?: string;
65
+ /**
66
+ * The supervisor's captured host `policy` and `user` scopes. Defaults to
67
+ * $APEX_CODE_POLICY_SNAPSHOT_PATH, which only a sandbox supervisor sets. When present
68
+ * it replaces both `policyPath` and the `user` file for reading; `apply()` still
69
+ * writes a `user` update to this process's own agent directory, which is how an
70
+ * in-session approval persists without the child's copy becoming an authority source.
71
+ */
72
+ policySnapshotPath?: string;
65
73
  /** Test seam: inject backends for the three writable file-backed sources instead of touching real files. */
66
74
  backends?: Partial<Record<FileBackedSource, AuthStorageBackend>>;
67
75
  /** Immutable argv layers (`flag` / `cliArg`), validated by the CLI before construction. */
68
76
  initialRules?: readonly PermissionRule[];
77
+ /** Resolved before construction. False excludes both project-controlled scopes. */
78
+ projectTrusted?: boolean;
69
79
  }
70
80
  /** File-backed store for policy/local/project/user, in-memory for command/session. */
71
81
  export declare class FilePermissionRuleStore implements PermissionRuleStore {
72
82
  private readonly backends;
73
83
  private readonly policyPath;
84
+ private readonly policySnapshotPath?;
74
85
  private readonly initialRules;
75
86
  private readonly runtimeRules;
76
87
  private readonly runtimeModes;
88
+ private readonly projectTrusted;
89
+ private readonly fileScopes;
90
+ private snapshotScope?;
91
+ private readonly fileScopeUpdates;
77
92
  constructor(options: CreateFilePermissionRuleStoreOptions);
93
+ private capturedFileBackedScope;
78
94
  private readFileBackedScope;
79
95
  private readPolicy;
96
+ private readPolicySnapshot;
97
+ private capturedPolicyAndUser;
80
98
  snapshot(): Promise<PermissionStoreSnapshot>;
81
99
  apply(update: PermissionUpdate): Promise<void>;
82
100
  private applyRuntime;