failproofai 1.0.3 → 1.0.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.next/standalone/.grok/hooks/failproofai.json +172 -0
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/build-manifest.json +5 -5
  4. package/.next/standalone/.next/prerender-manifest.json +4 -4
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  23. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/index.html +1 -1
  39. package/.next/standalone/.next/server/app/index.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  42. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  43. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  49. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  55. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  65. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  66. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  70. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__0_gzk8q._.js} +1 -1
  73. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1zndo1i._.js} +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +5 -0
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +21 -14
  93. package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +3 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0wvusu0._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1b5jzxr._.js → [root-of-the-server]__16gamwq._.js} +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/{_0v8eebd._.js → _00z5uuq._.js} +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/{_1if9ixi._.js → _08f0jd2._.js} +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/{_0j6gd_k._.js → _1byck5v._.js} +2 -2
  126. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +155 -1
  127. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  130. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/lib_cline-projects_ts_13_hbdd._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +3 -0
  137. package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  139. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  140. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  141. package/.next/standalone/.next/server/pages/404.html +1 -1
  142. package/.next/standalone/.next/server/pages/500.html +1 -1
  143. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  144. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  145. package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 00gv0v91mycm-.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 01hvwwrhvbzyh.js} +1 -1
  147. package/.next/standalone/.next/static/chunks/0hzmxwb2pvk5p.js +6 -0
  148. package/.next/standalone/.next/static/chunks/{0nm11ysn0153h.js → 0j884dwnwtyrt.js} +1 -1
  149. package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +1 -0
  150. package/.next/standalone/.next/static/chunks/0qn9khv0oo9ex.js +1 -0
  151. package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 1_2jsatfuthrw.js} +1 -1
  152. package/.next/standalone/.next/static/chunks/1_bxotmf9qngs.js +1 -0
  153. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 1po3lirgk2riw.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{40lpt2o2b283e.js → 2bnf_4pgyk1rq.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 2bvwsyu6x__ia.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{3l36d73kj18zw.js → 3_--3ibia6ba1.js} +1 -1
  158. package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +2 -0
  159. package/.next/standalone/.next/static/chunks/{3pibxnbhjza7n.js → 3ws7eljkq25wg.js} +1 -1
  160. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-18qf4jodtft6b.js} +1 -1
  161. package/.next/standalone/.qwen/settings.json +251 -0
  162. package/.next/standalone/app/project/[name]/page.tsx +24 -4
  163. package/.next/standalone/assets/logos/cline.png +0 -0
  164. package/.next/standalone/assets/logos/grok-dark.svg +1 -0
  165. package/.next/standalone/assets/logos/grok-light.svg +1 -0
  166. package/.next/standalone/assets/logos/ori-dark.svg +1 -0
  167. package/.next/standalone/assets/logos/ori-light.svg +1 -0
  168. package/.next/standalone/assets/logos/qwen.svg +1 -0
  169. package/.next/standalone/lib/cli-registry.ts +33 -1
  170. package/.next/standalone/lib/cline-projects.ts +155 -0
  171. package/.next/standalone/lib/cline-sessions.ts +268 -0
  172. package/.next/standalone/lib/download-session.ts +37 -0
  173. package/.next/standalone/lib/grok-projects.ts +123 -0
  174. package/.next/standalone/lib/grok-sessions.ts +355 -0
  175. package/.next/standalone/lib/ori-projects.ts +141 -0
  176. package/.next/standalone/lib/ori-sessions.ts +221 -0
  177. package/.next/standalone/lib/projects.ts +27 -3
  178. package/.next/standalone/lib/qwen-projects.ts +106 -0
  179. package/.next/standalone/lib/qwen-sessions.ts +313 -0
  180. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  181. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  182. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  183. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  184. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
  185. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
  186. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  187. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  188. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  189. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  190. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +2 -2
  191. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  192. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
  193. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  194. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
  195. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  196. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  197. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  198. package/.next/standalone/node_modules/next/package.json +10 -10
  199. package/.next/standalone/package.json +11 -11
  200. package/.next/standalone/sdk/python/uv.lock +29 -29
  201. package/.next/standalone/server.js +1 -1
  202. package/README.md +38 -7
  203. package/bin/failproofai.mjs +24 -7
  204. package/dist/cli.mjs +20313 -17756
  205. package/dist/worker.mjs +8521 -6768
  206. package/lib/cli-registry.ts +33 -1
  207. package/lib/cline-projects.ts +155 -0
  208. package/lib/cline-sessions.ts +268 -0
  209. package/lib/download-session.ts +37 -0
  210. package/lib/grok-projects.ts +123 -0
  211. package/lib/grok-sessions.ts +355 -0
  212. package/lib/ori-projects.ts +141 -0
  213. package/lib/ori-sessions.ts +221 -0
  214. package/lib/projects.ts +27 -3
  215. package/lib/qwen-projects.ts +106 -0
  216. package/lib/qwen-sessions.ts +313 -0
  217. package/package.json +11 -11
  218. package/src/audit/cli-adapters/cline.ts +50 -0
  219. package/src/audit/cli-adapters/grok.ts +70 -0
  220. package/src/audit/cli-adapters/index.ts +24 -0
  221. package/src/audit/cli-adapters/ori.ts +55 -0
  222. package/src/audit/cli-adapters/qwen.ts +71 -0
  223. package/src/hooks/batch-expand.ts +380 -0
  224. package/src/hooks/batch-fanout.ts +142 -0
  225. package/src/hooks/builtin-policies.ts +6 -4
  226. package/src/hooks/enforcement-capability.ts +97 -0
  227. package/src/hooks/handler.ts +61 -5
  228. package/src/hooks/harness-cli.ts +3 -1
  229. package/src/hooks/integrations.ts +756 -2
  230. package/src/hooks/normalize-cli-payload.ts +126 -0
  231. package/src/hooks/policy-evaluator.ts +297 -6
  232. package/src/hooks/resolve-transcript-path.ts +10 -0
  233. package/src/hooks/risk-patterns.ts +26 -0
  234. package/src/hooks/tool-name-canonicalize.ts +64 -0
  235. package/src/hooks/types.ts +533 -2
  236. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +0 -5
  238. package/.next/standalone/.next/server/chunks/_1-akg6w._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +0 -3
  240. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +0 -3
  243. package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
  244. package/.next/standalone/.next/static/chunks/1-_zkmnm-jwei.js +0 -1
  245. package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
  246. package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
  247. package/.next/standalone/.next/static/chunks/40nr6ehj4p3cw.css +0 -2
  248. package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
  249. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_buildManifest.js +0 -0
  250. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_clientMiddlewareManifest.js +0 -0
  251. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_ssgManifest.js +0 -0
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Expand a CLI's BATCH tool call into per-element canonical scalar inputs.
3
+ *
4
+ * failproofai's builtins read SCALARS — `tool_input.command`, `.file_path`,
5
+ * `.pattern`, `.old_string`. Cline delivers ARRAYS: `run_commands{commands[]}`,
6
+ * `read_files{files[]}`, `search_codebase{queries[]}`, plus one multi-file blob
7
+ * in `apply_patch{input}`. With only a key rename, `block-sudo`,
8
+ * `block-env-files`, `block-secrets-write`, `block-force-push` and
9
+ * `block-read-outside-cwd` all read `undefined` and ALLOW SILENTLY — the
10
+ * inert-hook failure this repo has already shipped twice (grok, and our own
11
+ * Claude hooks running inside grok).
12
+ *
13
+ * Joining the array into one string is NOT a fix, and the reason is specific:
14
+ * `SECRET_FILE_RE` is `/\.(?:pem|key)$/`. Under any join only the LAST element
15
+ * can ever match, so a `.pem` at `files[0]` rides straight through. That is why
16
+ * this expands and the evaluator runs the policy set once per element.
17
+ *
18
+ * PURE: no evaluator import, no fs, no policy registry — so the audit path can
19
+ * call it without dragging the policy engine along.
20
+ *
21
+ * CONTAINMENT: returns null for every CLI except cline and every tool except
22
+ * cline's four containers. `null` means "take the existing single-shot path,
23
+ * byte-for-byte unchanged", so this cannot regress the other 15 integrations.
24
+ */
25
+ import type { IntegrationType } from "./types";
26
+ import {
27
+ ENV_FILE_PATH_RE,
28
+ SECRET_FILE_RE,
29
+ SECRET_FILE_ID_RSA_RE,
30
+ SECRET_FILE_CREDENTIALS_RE,
31
+ } from "./risk-patterns";
32
+
33
+ export interface FanoutElement {
34
+ /** Canonical SCALAR tool input: {command} | {file_path,…} | {pattern} |
35
+ * {file_path, old_string, new_string, patch}. */
36
+ input: Record<string, unknown>;
37
+ /** Short human locator; goes into the deny message and the activity row. */
38
+ label: string;
39
+ index: number;
40
+ /** True when this element is a COLLAPSE of several that the cap or the
41
+ * wall-clock budget refused to run apart. A weaker guarantee we state
42
+ * explicitly, never a silent skip. */
43
+ degraded?: boolean;
44
+ }
45
+
46
+ export interface BatchExpansion {
47
+ /** Canonical tool name (Bash | Read | Grep | Edit). */
48
+ tool: string;
49
+ elements: FanoutElement[];
50
+ /** The un-expanded original, so a batch-aware policy still sees the whole call. */
51
+ raw: Record<string, unknown>;
52
+ }
53
+
54
+ /**
55
+ * The separator used wherever a command list still has to collapse to one
56
+ * string. `" &&\n"` is DERIVED, not chosen:
57
+ * • `"\n"` alone silently disables `READ_LIKE_CMDS` — its boundary
58
+ * alternation `(?:^|;|&&|\|\||\|)` contains no newline, so a batched
59
+ * `cat /etc/passwd` stops looking read-like and `block-read-outside-cwd`
60
+ * never inspects it.
61
+ * • `" && "` alone manufactures a FALSE deny: a `.`-based lookahead can cross
62
+ * the boundary and fire on two commands neither of which triggers it alone.
63
+ * `" &&\n"` keeps every `&&`-aware segmenter alive while the newline stops
64
+ * `.`-based lookaheads crossing a boundary.
65
+ */
66
+ export const BATCH_JOIN = " &&\n";
67
+
68
+ function omit(o: Record<string, unknown>, key: string): Record<string, unknown> {
69
+ const { [key]: _dropped, ...rest } = o;
70
+ return rest;
71
+ }
72
+
73
+ export function expandBatchToolInput(
74
+ cli: IntegrationType,
75
+ toolName: string | undefined,
76
+ rawInput: unknown,
77
+ ): BatchExpansion | null {
78
+ if (cli !== "cline" || !toolName) return null;
79
+ if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) return null;
80
+ const raw = rawInput as Record<string, unknown>;
81
+
82
+ // An expansion to ZERO elements would evaluate nothing and report a clean
83
+ // allow — the exact inert-hook failure this exists to prevent. So an empty or
84
+ // mis-shaped container returns null and falls back to the collapse safety net
85
+ // in canonicalizeClineToolInput, which at least still reads SOMETHING.
86
+ const mk = (
87
+ parts: Array<{ input: Record<string, unknown>; label: string }>,
88
+ ): BatchExpansion | null =>
89
+ parts.length === 0
90
+ ? null
91
+ : { tool: toolName, raw, elements: parts.map((e, index) => ({ ...e, index })) };
92
+
93
+ switch (toolName) {
94
+ // run_commands {commands: ["cd '<dir>' && ls -la", …]}
95
+ case "Bash": {
96
+ const cmds = raw.commands;
97
+ if (!Array.isArray(cmds)) return null;
98
+ return mk(
99
+ cmds
100
+ .filter((c): c is string => typeof c === "string")
101
+ .map((c) => ({ input: { ...omit(raw, "commands"), command: c }, label: c })),
102
+ );
103
+ }
104
+ // read_files {files: [{path, start_line, …}, …]}
105
+ case "Read": {
106
+ const files = raw.files;
107
+ if (!Array.isArray(files)) return null;
108
+ return mk(
109
+ files.flatMap((f) => {
110
+ if (!f || typeof f !== "object" || Array.isArray(f)) return [];
111
+ const e = f as Record<string, unknown>;
112
+ if (typeof e.path !== "string") return [];
113
+ // Spread the entry so start_line/limit survive for policies that read
114
+ // them; `path` is KEPT too, because a cline-aware custom policy may
115
+ // already be reading it.
116
+ return [{ input: { ...e, file_path: e.path }, label: e.path }];
117
+ }),
118
+ );
119
+ }
120
+ // search_codebase {queries: ["alpha", …]}
121
+ case "Grep": {
122
+ const qs = raw.queries;
123
+ if (!Array.isArray(qs)) return null;
124
+ return mk(
125
+ qs
126
+ .filter((q): q is string => typeof q === "string")
127
+ .map((q) => ({ input: { ...omit(raw, "queries"), pattern: q }, label: q })),
128
+ );
129
+ }
130
+ // apply_patch {input: "*** Begin Patch …"} — byte-identical to the OpenAI
131
+ // apply_patch blob ori's `edit` tool carries, so this splitter is
132
+ // back-portable to ori and closes ori's documented multi-file KNOWN GAP.
133
+ case "Edit": {
134
+ const blob = raw.input;
135
+ if (typeof blob !== "string") return null;
136
+ return mk(
137
+ splitApplyPatch(blob).map((s) => ({
138
+ input: {
139
+ file_path: s.path,
140
+ old_string: s.oldText,
141
+ new_string: s.newText,
142
+ patch: s.subPatch,
143
+ cline_patch_op: s.op,
144
+ },
145
+ label: `${s.op} ${s.path}`,
146
+ })),
147
+ );
148
+ }
149
+ default:
150
+ return null;
151
+ }
152
+ }
153
+
154
+ export interface PatchSection {
155
+ op: "add" | "update" | "delete" | "move";
156
+ path: string;
157
+ oldText: string;
158
+ newText: string;
159
+ /** A standalone, re-emittable patch for just this file. */
160
+ subPatch: string;
161
+ }
162
+
163
+ const FILE_OP_RE = /^\*\*\* (Add File|Update File|Delete File|Move to): (.+)$/;
164
+ const OPS: Record<string, PatchSection["op"]> = {
165
+ "Add File": "add",
166
+ "Update File": "update",
167
+ "Delete File": "delete",
168
+ "Move to": "move",
169
+ };
170
+ const HEADERS: Record<PatchSection["op"], string> = {
171
+ add: "Add File",
172
+ update: "Update File",
173
+ delete: "Delete File",
174
+ move: "Move to",
175
+ };
176
+
177
+ interface OpenSection {
178
+ op: PatchSection["op"];
179
+ path: string;
180
+ body: string[];
181
+ }
182
+
183
+ function build(cur: OpenSection, op: PatchSection["op"], path: string): PatchSection {
184
+ const minus: string[] = [];
185
+ const plus: string[] = [];
186
+ for (const l of cur.body) {
187
+ if (l.startsWith("-")) minus.push(l.slice(1));
188
+ else if (l.startsWith("+")) plus.push(l.slice(1));
189
+ }
190
+ return {
191
+ op,
192
+ path,
193
+ oldText: minus.join("\n"),
194
+ newText: plus.join("\n"),
195
+ subPatch: ["*** Begin Patch", `*** ${HEADERS[op]}: ${path}`, ...cur.body, "*** End Patch"].join("\n"),
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Split an OpenAI apply_patch blob into one section per file it touches.
201
+ *
202
+ * Serves BOTH cline's `apply_patch` and ori's `edit` — the format is identical,
203
+ * and one implementation is what stops a third CLI adding a third regex.
204
+ */
205
+ export function splitApplyPatch(patch: string): PatchSection[] {
206
+ const out: PatchSection[] = [];
207
+ let cur: OpenSection | null = null;
208
+
209
+ for (const line of patch.split("\n")) {
210
+ const m = FILE_OP_RE.exec(line);
211
+ if (m) {
212
+ const op = OPS[m[1]!]!;
213
+ const path = m[2]!.trim();
214
+ if (op === "move") {
215
+ // `Move to:` is a RENAME MODIFIER on the preceding Update File, not an
216
+ // independent op. We emit it as its OWN element anyway — deliberately —
217
+ // so a rename INTO `.env` is seen by the path builtins. It inherits the
218
+ // in-progress body so old/new_string stay right, and the Update element
219
+ // is still emitted at the next flush. Cost: one rename yields two
220
+ // elements. That is the trade: a duplicate row, never a missed path.
221
+ // Flush the section being renamed FIRST, then emit the move, so paths
222
+ // come out in literal file order — `Update File: a` + `Move to: b`
223
+ // yields [a, b], never [b, a]. Getting this backwards silently changes
224
+ // which path `pickRiskiestPath` and `file_path = paths[0]` land on.
225
+ if (cur) {
226
+ out.push(build(cur, cur.op, cur.path));
227
+ out.push(build(cur, "move", path));
228
+ cur = null;
229
+ } else
230
+ out.push({
231
+ op: "move",
232
+ path,
233
+ oldText: "",
234
+ newText: "",
235
+ subPatch: `*** Begin Patch\n*** Move to: ${path}\n*** End Patch`,
236
+ });
237
+ continue;
238
+ }
239
+ if (cur) out.push(build(cur, cur.op, cur.path));
240
+ cur = { op, path, body: [] };
241
+ continue;
242
+ }
243
+ if (line.startsWith("*** ")) continue; // Begin Patch / End Patch / End of File
244
+ if (cur) cur.body.push(line);
245
+ }
246
+ if (cur) out.push(build(cur, cur.op, cur.path));
247
+ return out;
248
+ }
249
+
250
+ /** Every path an apply_patch blob touches, in file order. */
251
+ export function applyPatchFilePaths(patch: string): string[] {
252
+ return splitApplyPatch(patch).map((s) => s.path);
253
+ }
254
+
255
+ /**
256
+ * When a path list must still collapse to ONE (the overflow tail, and the
257
+ * non-fan-out safety net), pick the one a builtin is most likely to deny on
258
+ * rather than element [0]. The probes IMPORT the real builtin regexes — a
259
+ * second hand-maintained copy is how a bypass gets reintroduced by a builtin
260
+ * edit nobody thought to mirror. Falls back to paths[0] when nothing matches.
261
+ *
262
+ * NOT the primary mechanism, and it cannot be: it runs before params are bound,
263
+ * so `block-secrets-write`'s `additionalPatterns` and `block-read-outside-cwd`'s
264
+ * `allowPaths` have no influence on it, and neither does any custom policy.
265
+ * That is exactly why PreToolUse fans out instead of relying on this.
266
+ */
267
+ export function pickRiskiestPath(paths: string[]): string | undefined {
268
+ if (paths.length === 0) return undefined;
269
+ for (const re of [
270
+ ENV_FILE_PATH_RE,
271
+ SECRET_FILE_RE,
272
+ SECRET_FILE_ID_RSA_RE,
273
+ SECRET_FILE_CREDENTIALS_RE,
274
+ ]) {
275
+ const hit = paths.find((p) => re.test(p));
276
+ if (hit) return hit;
277
+ }
278
+ return paths[0];
279
+ }
280
+
281
+ /** Everything past the cap / budget, joined into ONE degraded element. It is
282
+ * still EVALUATED — a weaker guarantee we can state honestly, never a silent
283
+ * skip. */
284
+ export function collapseElements(tool: string, els: FanoutElement[]): FanoutElement {
285
+ const first = els[0]!;
286
+ const base = { label: `${els.length} collapsed`, index: first.index, degraded: true as const };
287
+ switch (tool) {
288
+ case "Bash":
289
+ return {
290
+ ...base,
291
+ input: {
292
+ ...first.input,
293
+ command: els.map((e) => String(e.input.command ?? "")).join(BATCH_JOIN),
294
+ },
295
+ };
296
+ case "Read":
297
+ case "Edit": {
298
+ const paths = els.map((e) => String(e.input.file_path ?? "")).filter(Boolean);
299
+ return {
300
+ ...base,
301
+ input: { ...first.input, file_path: pickRiskiestPath(paths), cline_collapsed_paths: paths },
302
+ };
303
+ }
304
+ case "Grep":
305
+ return {
306
+ ...base,
307
+ input: { ...first.input, pattern: els.map((e) => String(e.input.pattern ?? "")).join("|") },
308
+ };
309
+ default:
310
+ return { ...first, ...base };
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Cline → canonical, for every path that does NOT fan out: PostToolUse, audit
316
+ * replay, fail-closed shaping, and a policy reading `ctx.payload`. Derives the
317
+ * best single scalar and PRESERVES the arrays under `cline_*` so a batch-aware
318
+ * policy can still see everything.
319
+ *
320
+ * This is a SAFETY NET, not the enforcement mechanism: a collapse cannot make
321
+ * an anchored regex match every element, which is precisely why PreToolUse fans
322
+ * out instead. `pickRiskiestPath` is what makes the net worth having — element
323
+ * [0] would silently miss a `.env` at `files[1]`.
324
+ */
325
+ export function canonicalizeClineToolInput(
326
+ toolName: string,
327
+ input: Record<string, unknown>,
328
+ ): Record<string, unknown> {
329
+ switch (toolName) {
330
+ case "Bash": {
331
+ const cs = input.commands;
332
+ if (!Array.isArray(cs)) return input;
333
+ const commands = cs.filter((c): c is string => typeof c === "string");
334
+ if (commands.length === 0) return input;
335
+ return { ...omit(input, "commands"), command: commands.join(BATCH_JOIN), cline_commands: commands };
336
+ }
337
+ case "Read": {
338
+ const files = input.files;
339
+ if (!Array.isArray(files)) return input;
340
+ const paths = files
341
+ .map((f) =>
342
+ f && typeof f === "object" && !Array.isArray(f)
343
+ ? (f as Record<string, unknown>).path
344
+ : undefined,
345
+ )
346
+ .filter((p): p is string => typeof p === "string");
347
+ if (paths.length === 0) return input;
348
+ return {
349
+ ...omit(input, "files"),
350
+ file_path: pickRiskiestPath(paths),
351
+ cline_files: files,
352
+ cline_file_paths: paths,
353
+ };
354
+ }
355
+ case "Grep": {
356
+ const qs = input.queries;
357
+ if (!Array.isArray(qs)) return input;
358
+ const queries = qs.filter((q): q is string => typeof q === "string");
359
+ if (queries.length === 0) return input;
360
+ return { ...omit(input, "queries"), pattern: queries.join("|"), cline_queries: queries };
361
+ }
362
+ case "Edit": {
363
+ const blob = input.input;
364
+ if (typeof blob !== "string") return input;
365
+ const sections = splitApplyPatch(blob);
366
+ if (sections.length === 0) return input;
367
+ const paths = sections.map((s) => s.path);
368
+ return {
369
+ ...input,
370
+ patch: blob,
371
+ file_path: pickRiskiestPath(paths),
372
+ old_string: sections.map((s) => s.oldText).join("\n"),
373
+ new_string: sections.map((s) => s.newText).join("\n"),
374
+ cline_patch_files: paths,
375
+ };
376
+ }
377
+ default:
378
+ return input;
379
+ }
380
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Run the policy set once per element of an expanded batch tool call.
3
+ *
4
+ * Why this exists rather than a cleverer canonicalization: cline's tools carry
5
+ * ARRAYS, and collapsing an array into one scalar cannot preserve an ANCHORED
6
+ * regex. `SECRET_FILE_RE` is `/\.(?:pem|key)$/`, so under any join only the last
7
+ * path can match and a `.pem` at `files[0]` rides straight through — a silent
8
+ * allow, which is the failure mode this repo has already shipped twice.
9
+ *
10
+ * Combination rules:
11
+ * all allow, no notes → plain allow, identical to a non-batch call
12
+ * any deny → LOWEST element index wins, returned verbatim,
13
+ * short-circuiting the rest
14
+ * no deny, >=1 instruct → union deduped by (policy, reason), shaped ONCE
15
+ * no deny/instruct, >=1 note→ same
16
+ *
17
+ * The deny is returned verbatim because `evaluatePolicies` already shaped it for
18
+ * the calling CLI and already embedded the element locator (it received
19
+ * `opts.batch`). No re-shaping, and therefore nothing to keep in sync with the
20
+ * 16 per-CLI deny shapes.
21
+ */
22
+ import { evaluatePolicies, type EvaluationResult } from "./policy-evaluator";
23
+ import { collapseElements, type BatchExpansion, type FanoutElement } from "./batch-expand";
24
+ import { hookLogWarn } from "./hook-logger";
25
+ import type { HookEventType, SessionMetadata } from "./types";
26
+ import type { HooksConfig } from "./policy-types";
27
+
28
+ /** A shape guard, not the real one: 32 elements of registered policies sits
29
+ * comfortably inside daemon-client.ts's 30s RESPONSE budget. The wall clock
30
+ * below is what actually protects that budget. */
31
+ const MAX_ELEMENTS = 32;
32
+ const WALL_CLOCK_BUDGET_MS = 5_000;
33
+
34
+ export interface BatchOutcome {
35
+ size: number;
36
+ evaluated: number;
37
+ decidedIndex: number | null;
38
+ decidedLabel: string | null;
39
+ degraded: boolean;
40
+ }
41
+
42
+ type Entry = { policyName: string; reason: string };
43
+
44
+ /** Policy P saying the SAME thing about elements 3 and 7 contributes one line;
45
+ * P saying two different things contributes both. Keyed on (name, reason). */
46
+ function pushDeduped(into: Entry[], from: Entry[] | undefined): void {
47
+ for (const e of from ?? []) {
48
+ const k = `${e.policyName}\t${e.reason}`;
49
+ if (!into.some((x) => `${x.policyName}\t${x.reason}` === k)) into.push(e);
50
+ }
51
+ }
52
+
53
+ export async function evaluateExpandedBatch(
54
+ eventType: HookEventType,
55
+ payload: Record<string, unknown>,
56
+ session: SessionMetadata | undefined,
57
+ config: HooksConfig | undefined,
58
+ expansion: BatchExpansion,
59
+ dedupe: Set<string>,
60
+ ): Promise<EvaluationResult & { batch: BatchOutcome }> {
61
+ const started = Date.now();
62
+
63
+ let queue: FanoutElement[] =
64
+ expansion.elements.length > MAX_ELEMENTS
65
+ ? [
66
+ ...expansion.elements.slice(0, MAX_ELEMENTS),
67
+ collapseElements(expansion.tool, expansion.elements.slice(MAX_ELEMENTS)),
68
+ ]
69
+ : [...expansion.elements];
70
+
71
+ const instructs: Entry[] = [];
72
+ const allows: Entry[] = [];
73
+ let evaluated = 0;
74
+ let sawDegraded = false;
75
+
76
+ for (let i = 0; i < queue.length; i++) {
77
+ // Checked BETWEEN elements only — a policy already inside its own timeout
78
+ // race is not interruptible from here.
79
+ if (i > 0 && Date.now() - started > WALL_CLOCK_BUDGET_MS) {
80
+ const rest = queue.slice(i);
81
+ hookLogWarn(
82
+ `batch fan-out budget exceeded after ${i}/${queue.length} elements; the remaining ` +
83
+ `${rest.length} are being evaluated COLLAPSED — anchored patterns may not match`,
84
+ );
85
+ queue = [...queue.slice(0, i), collapseElements(expansion.tool, rest)];
86
+ }
87
+ const el = queue[i]!;
88
+ sawDegraded = sawDegraded || !!el.degraded;
89
+
90
+ const r = await evaluatePolicies(
91
+ eventType,
92
+ { ...payload, tool_input: el.input },
93
+ session,
94
+ config,
95
+ {
96
+ dedupe,
97
+ collectEntries: true,
98
+ batch: {
99
+ index: el.index,
100
+ count: expansion.elements.length,
101
+ label: el.label,
102
+ degraded: !!el.degraded,
103
+ all: expansion.elements.map((e) => e.input),
104
+ raw: expansion.raw,
105
+ },
106
+ },
107
+ );
108
+ evaluated++;
109
+
110
+ if (r.decision === "deny") {
111
+ return {
112
+ ...r,
113
+ batch: {
114
+ size: expansion.elements.length,
115
+ evaluated,
116
+ decidedIndex: el.index,
117
+ decidedLabel: el.label,
118
+ degraded: sawDegraded,
119
+ },
120
+ };
121
+ }
122
+ pushDeduped(instructs, r.instructEntries);
123
+ pushDeduped(allows, r.allowEntries);
124
+ }
125
+
126
+ // One shaping pass over the deduped union, through the SAME per-CLI tails
127
+ // every other integration uses — no shaping code was extracted to get here.
128
+ const shaped = await evaluatePolicies(eventType, payload, session, config, {
129
+ dedupe,
130
+ preEvaluated: { instructEntries: instructs, allowEntries: allows },
131
+ });
132
+ return {
133
+ ...shaped,
134
+ batch: {
135
+ size: expansion.elements.length,
136
+ evaluated,
137
+ decidedIndex: null,
138
+ decidedLabel: null,
139
+ degraded: sawDegraded,
140
+ },
141
+ };
142
+ }
@@ -16,6 +16,12 @@ import { POLICY_CATALOG } from "./policy-catalog";
16
16
  import { allow, deny, instruct } from "./policy-helpers";
17
17
  import { normalizePolicyName, registerPolicy } from "./policy-registry";
18
18
  import { hookLogWarn } from "./hook-logger";
19
+ import {
20
+ ENV_FILE_PATH_RE,
21
+ SECRET_FILE_RE,
22
+ SECRET_FILE_ID_RSA_RE,
23
+ SECRET_FILE_CREDENTIALS_RE,
24
+ } from "./risk-patterns";
19
25
 
20
26
  /**
21
27
  * Whether `resolved` lives under an agent CLI's home directory
@@ -192,7 +198,6 @@ const DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
192
198
  const CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
193
199
 
194
200
  // blockEnvFiles
195
- const ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
196
201
  const ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
197
202
 
198
203
  // blockSudo
@@ -444,9 +449,6 @@ const SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
444
449
  const SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"] as const;
445
450
 
446
451
  // blockSecretsWrite
447
- const SECRET_FILE_RE = /\.(?:pem|key)$/;
448
- const SECRET_FILE_ID_RSA_RE = /id_rsa/;
449
- const SECRET_FILE_CREDENTIALS_RE = /credentials/;
450
452
 
451
453
  // blockWorkOnMain
452
454
  const GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
@@ -253,6 +253,103 @@ export const ENFORCEMENT_CAPABILITY: Record<
253
253
  // the policy-evaluator comment both claim "Goose has NO Stop event"; that is
254
254
  // false at v1.43.0 (agent.rs:1956, :2840 emit_stop_hook_blocking).
255
255
  },
256
+
257
+ // grok 1.0.3 (1a29d5bc12). Every "block" row below was proven by a live
258
+ // probe, not read from grok's hooks doc — which is also where two of the
259
+ // three surprises came from (see types.ts): the doc's own tool name for the
260
+ // shell tool disagrees with the wire, and nothing documents that project
261
+ // hooks need a git repo. grok's capability advertisement corroborates the
262
+ // blocking set: its ACP initialize response carries
263
+ // x.ai/hooks: {blockingEvents:["pre_tool_use","stop","subagent_stop"],
264
+ // decisions:["deny","block"], stopSignals:[…]}
265
+ // — exactly the three rows marked "block" here.
266
+ grok: {
267
+ PreToolUse: "block", // VERIFIED live: {decision:"deny",reason} blocked `echo` and beat --yolo (permissionMode bypassPermissions); Claude's hookSpecificOutput shape did NOT block (A/B on the same hook)
268
+ Stop: "block", // VERIFIED live: {decision:"block",reason} forced another turn — the agent ran the required command, then stopped. ONLY on reason==="end_turn"; the session-shutdown fire is parsed and discarded upstream. Cap: 8 continuations/turn
269
+ SubagentStop: "block", // advertised in x.ai/hooks blockingEvents; not exercised by a probe
270
+ UserPromptSubmit: "observe", // fired live; not in blockingEvents
271
+ PostToolUse: "observe", // fired live; post-hoc, and not in blockingEvents
272
+ PostToolUseFailure: "observe",
273
+ SessionStart: "observe", // fired live
274
+ SessionEnd: "observe", // fired live
275
+ // The six below are the widened set. All OBSERVE by construction, not by
276
+ // caution: blockingEvents lists exactly three events, so grok cannot honour
277
+ // a deny on any of these no matter what we send.
278
+ StopFailure: "observe", // VERIFIED firing (a 429 turn): {error:"rate_limit", errorDetails, lastAssistantMessage}
279
+ Notification: "observe", // VERIFIED firing: notificationType "agent_error" — a type grok's own docs do not list
280
+ PermissionDenied: "observe", // config key accepted (count=14); firing not exercised — needs a real permission denial
281
+ SubagentStart: "observe", // config key accepted; firing not exercised — needs a subagent
282
+ PreCompact: "observe", // config key accepted; firing not exercised — needs compaction
283
+ PostCompact: "observe", // config key accepted; firing not exercised — needs compaction
284
+ },
285
+
286
+ // qwen-code 0.21.12. A near-pure Claude clone on the wire, so most rows
287
+ // inherit Claude's semantics — but only the two proven ones are "block".
288
+ qwen: {
289
+ PreToolUse: "block", // VERIFIED live: hookSpecificOutput.permissionDecision:"deny" blocked run_shell_command and beat -y (yolo); reason reached the model verbatim. ("ask" degrades to deny in headless and background subagents)
290
+ Stop: "block", // VERIFIED live: top-level {decision:"block",reason} forced another turn. NOTE stop_hook_active is true on the FIRST fire, so it is not a usable loop guard here
291
+ UserPromptSubmit: "observe", // docs accept a decision, unverified — and it fires per MODEL INVOCATION (4× in one observed turn), so treat as observation until probed
292
+ PostToolUse: "observe", // docs accept `decision`, unverified; post-hoc regardless
293
+ PostToolUseFailure: "observe",
294
+ PermissionRequest: "observe",
295
+ PermissionDenied: "observe",
296
+ SubagentStart: "observe",
297
+ SubagentStop: "observe", // sibling of Stop upstream, but not probed — left honest rather than assumed
298
+ PreCompact: "observe",
299
+ SessionStart: "observe", // fired live
300
+ SessionEnd: "observe",
301
+ // Widened set. Every one has a real executeHooks() dispatch site in the
302
+ // shipped bundle; the three marked VERIFIED were also observed firing.
303
+ TaskCreated: "block", // qwen's TodoCreated. VERIFIED firing ×4 with phase:"validation", where upstream docs state a {decision:"block"} PREVENTS the write and returns the reason to the model
304
+ TaskCompleted: "block", // qwen's TodoCompleted. VERIFIED firing ×4, same validation-phase contract
305
+ InstructionsLoaded: "observe", // VERIFIED firing: {file_path, memory_type, load_reason}
306
+ Notification: "observe", // VERIFIED firing: notification_type "auth_success"
307
+ StopFailure: "observe", // upstream documents it fire-and-forget — output and exit code ignored
308
+ PostCompact: "observe",
309
+ UserPromptExpansion: "observe",
310
+ },
311
+
312
+ // ── ori ───────────────────────────────────────────────────────────────────
313
+ // ori 0.12.0+68f9a36, probed LIVE against the built-in `@ori-runloop/
314
+ // agent-loop` harness driving nvidia/nemotron-3.5-lightning:free. Enforcement
315
+ // is the `approval-asker` / `unattended-approvals` extension points rather
316
+ // than a hook-event stream, so PreToolUse is the only row there can be: ori
317
+ // exposes no prompt-submit, post-tool, session or stop gate to subscribe to.
318
+ ori: {
319
+ PreToolUse: "block", // LIVE: {outcome:"deny"} from our approval-asker provision turned 3 tool.started into 3 tool.failed with ZERO tool.succeeded, and ori held the deny across the model's retries. MODE-GATED — read the note below before quoting this row
320
+ // DELIBERATELY NO OTHER ROWS. Two things this table cannot express, both
321
+ // verified live and both load-bearing:
322
+ // 1. ori's approval mode defaults to `self-drive`, and in that mode the
323
+ // DYNAMIC points are never called — so no deny is ever PRODUCED, and
324
+ // the row above never gets a chance to apply. Isolated three ways: no
325
+ // callback under self-drive; still none after claiming approval-policy
326
+ // with defaultAction:"ask"; but defaultAction:"reject" DID block every
327
+ // call, which proves the static point is wired and that self-drive
328
+ // skips specifically the dynamic asker. Coverage therefore requires
329
+ // `--approvals manual` (or /approvals in the TUI).
330
+ // 2. There is no Stop event at all, so the 5 require-*-before-stop
331
+ // builtins are INAPPLICABLE on ori, exactly as on Hermes and Goose.
332
+ },
333
+
334
+ // ── cline ─────────────────────────────────────────────────────────────────
335
+ // cline v3.0.60. Its hooks are event-NAMED scripts under a hooks directory,
336
+ // and THE EXIT CODE IS IGNORED on every event — so every row here is a claim
337
+ // about a stdout SHAPE, never about exit 2. cline's verdict schema has no
338
+ // `decision` / `block` / `permissionDecision` field at all, which is why every
339
+ // generic branch in policy-evaluator.ts is inert for it.
340
+ cline: {
341
+ PreToolUse: "block", // LIVE: {"cancel":true,"errorMessage":…} on stdout stopped a real run_commands call and the side effect never happened. CAVEAT, and it is a product decision rather than a footnote: `cancel` becomes {stop:true} -> applyStopControl THROWS ControlledStopError, ABORTING THE WHOLE RUN ("[abort] aborted by another client"), not just this tool call. A STRONGER action than a per-tool deny, not a weaker one
342
+ Stop: "observe", // TaskComplete maps here, and `cancel` is NOT a force-retry channel: the task has already completed, so a cancel kills a finished run rather than re-entering the loop. The 5 require-*-before-stop builtins are INAPPLICABLE on cline, exactly as on Hermes, Goose and ori
343
+ // DELIBERATELY NO OTHER ROWS. PostToolUse, UserPromptSubmit, TaskStart,
344
+ // TaskResume, TaskCancel, TaskError and SessionShutdown ARE installed — for
345
+ // the audit trail and for observe-mode custom policies — but nothing has
346
+ // been probed showing a verdict on any of them changes what the agent does,
347
+ // and ABSENT MEANS UNKNOWN in this file.
348
+ //
349
+ // FAIL-OPEN across the whole surface, and unlike ori there is no
350
+ // failureBehavior:"deny" to inherit: a timeout (120s default), a parse
351
+ // failure or a spawn error makes cline SKIP the hook and run the tool.
352
+ },
256
353
  };
257
354
 
258
355
  /**