failproofai 1.0.4-beta.1 → 1.0.4-beta.2

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 (340) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +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/run/route_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-verify/route_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  42. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
  43. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  44. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/index.html +1 -1
  47. package/.next/standalone/.next/server/app/index.rsc +15 -15
  48. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  49. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  50. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  53. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  57. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  58. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  63. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  74. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  77. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  78. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/chunks/{[externals]__1zndo1i._.js → [externals]__1lh7m5d._.js} +1 -1
  81. package/.next/standalone/.next/server/chunks/{[externals]__0_gzk8q._.js → [externals]__1rqkg_y._.js} +1 -1
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
  88. package/.next/standalone/.next/server/chunks/_08w6xzm._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
  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 +14 -21
  93. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wvusu0._.js → [root-of-the-server]__0n_lxhg._.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]__0rgu2r3._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
  117. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +48 -168
  123. package/.next/standalone/.next/server/chunks/ssr/{_08f0jd2._.js → _1if9ixi._.js} +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  126. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
  127. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  133. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  136. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  137. package/.next/standalone/.next/server/pages/404.html +1 -1
  138. package/.next/standalone/.next/server/pages/500.html +1 -1
  139. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
  141. package/.next/standalone/.next/static/chunks/{1_bxotmf9qngs.js → 010bv1w6j171t.js} +1 -1
  142. package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
  143. package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
  144. package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +2 -0
  145. package/.next/standalone/.next/static/chunks/{00gv0v91mycm-.js → 0wz8yftk18ts2.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
  147. package/.next/standalone/.next/static/chunks/{0qn9khv0oo9ex.js → 1pb1oztsbwcss.js} +1 -1
  148. package/.next/standalone/.next/static/chunks/2aquitk72k2op.js +1 -0
  149. package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
  150. package/.next/standalone/.next/static/chunks/{1po3lirgk2riw.js → 2ej3b8gk5ittu.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/{01hvwwrhvbzyh.js → 2zafgs90r_leq.js} +1 -1
  152. package/.next/standalone/.next/static/chunks/{1_2jsatfuthrw.js → 32spub4wqjem-.js} +1 -1
  153. package/.next/standalone/.next/static/chunks/{3_--3ibia6ba1.js → 3l36d73kj18zw.js} +1 -1
  154. package/.next/standalone/.next/static/chunks/{2bvwsyu6x__ia.js → 3m4upvybtrexd.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
  156. package/.next/standalone/.next/static/chunks/{2bnf_4pgyk1rq.js → 40lpt2o2b283e.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{turbopack-18qf4jodtft6b.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
  158. package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
  159. package/.next/standalone/app/actions/get-leaks.ts +100 -0
  160. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
  161. package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
  162. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
  163. package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
  164. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
  165. package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
  166. package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
  167. package/.next/standalone/app/audit/audit-styles.css +114 -1
  168. package/.next/standalone/app/project/[name]/page.tsx +4 -24
  169. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
  170. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
  171. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
  172. package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
  173. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
  174. package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
  175. package/.next/standalone/lib/auth/api-server-client.ts +28 -0
  176. package/.next/standalone/lib/claude-sessions.ts +101 -27
  177. package/.next/standalone/lib/cli-registry.ts +1 -33
  178. package/.next/standalone/lib/download-session.ts +0 -37
  179. package/.next/standalone/lib/projects.ts +3 -27
  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/pages-turbo.runtime.prod.js +1 -1
  183. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
  184. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  185. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
  186. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
  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/start-server.js +1 -1
  193. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  194. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  195. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  196. package/.next/standalone/node_modules/next/package.json +11 -11
  197. package/.next/standalone/package.json +15 -14
  198. package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
  199. package/.next/standalone/sdk/python/README.md +8 -0
  200. package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
  201. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  202. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
  203. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
  204. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
  205. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
  206. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
  207. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
  208. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
  209. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
  210. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
  211. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
  212. package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
  213. package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
  214. package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
  215. package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
  216. package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
  217. package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
  218. package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
  219. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
  220. package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
  221. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
  222. package/.next/standalone/sdk/python/uv.lock +18 -18
  223. package/.next/standalone/server.js +1 -1
  224. package/README.md +19 -95
  225. package/bin/failproofai.mjs +7 -24
  226. package/dist/cli.mjs +19910 -25855
  227. package/dist/worker.mjs +4818 -6344
  228. package/lib/auth/api-server-client.ts +28 -0
  229. package/lib/claude-sessions.ts +101 -27
  230. package/lib/cli-registry.ts +1 -33
  231. package/lib/download-session.ts +0 -37
  232. package/lib/projects.ts +3 -27
  233. package/package.json +15 -14
  234. package/pi-extension/index.ts +42 -4
  235. package/scripts/changelog-open.py +115 -0
  236. package/src/audit/cli-adapters/index.ts +0 -24
  237. package/src/audit/cli.ts +134 -0
  238. package/src/audit/desktop-notify.ts +420 -0
  239. package/src/audit/harm-report.ts +101 -0
  240. package/src/audit/index.ts +141 -13
  241. package/src/audit/leak-fingerprint.ts +200 -0
  242. package/src/audit/leak-notice.ts +161 -0
  243. package/src/audit/leak-record.ts +232 -0
  244. package/src/audit/leak-scan.ts +292 -0
  245. package/src/audit/leak-store.ts +217 -0
  246. package/src/audit/macos-notifier.ts +310 -0
  247. package/src/audit/redact-example.ts +266 -14
  248. package/src/audit/report-harm.ts +12 -1
  249. package/src/audit/report.ts +6 -3
  250. package/src/audit/schedule-cli.ts +37 -0
  251. package/src/audit/scoring.ts +49 -0
  252. package/src/audit/types.ts +41 -0
  253. package/src/hooks/builtin-policies.ts +71 -8
  254. package/src/hooks/configure-wizard.ts +20 -0
  255. package/src/hooks/enforcement-capability.ts +0 -97
  256. package/src/hooks/fp-config.ts +71 -11
  257. package/src/hooks/fp-home.ts +27 -0
  258. package/src/hooks/handler.ts +64 -62
  259. package/src/hooks/harness-cli.ts +1 -3
  260. package/src/hooks/integrations.ts +49 -769
  261. package/src/hooks/normalize-cli-payload.ts +0 -126
  262. package/src/hooks/notice.ts +155 -0
  263. package/src/hooks/policy-evaluator.ts +6 -297
  264. package/src/hooks/resolve-transcript-path.ts +0 -10
  265. package/src/hooks/tool-name-canonicalize.ts +0 -64
  266. package/src/hooks/types.ts +2 -533
  267. package/src/hooks/uninstall-cli.ts +15 -0
  268. package/.next/standalone/.grok/hooks/failproofai.json +0 -172
  269. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +0 -5
  270. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +0 -3
  271. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +0 -3
  272. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +0 -3
  273. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +0 -3
  274. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +0 -3
  275. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +0 -3
  276. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +0 -3
  277. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +0 -3
  278. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +0 -3
  279. package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +0 -3
  280. package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +0 -3
  281. package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +0 -3
  282. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +0 -3
  283. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +0 -3
  284. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +0 -3
  285. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +0 -3
  286. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gamwq._.js +0 -3
  287. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +0 -3
  288. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +0 -3
  289. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +0 -3
  290. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +0 -3
  291. package/.next/standalone/.next/server/chunks/ssr/_00z5uuq._.js +0 -3
  292. package/.next/standalone/.next/server/chunks/ssr/_1byck5v._.js +0 -23
  293. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
  294. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
  295. package/.next/standalone/.next/server/chunks/ssr/lib_cline-projects_ts_13_hbdd._.js +0 -3
  296. package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +0 -3
  297. package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +0 -3
  298. package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.js +0 -3
  299. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
  300. package/.next/standalone/.next/static/chunks/0hzmxwb2pvk5p.js +0 -6
  301. package/.next/standalone/.next/static/chunks/0j884dwnwtyrt.js +0 -1
  302. package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +0 -1
  303. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
  304. package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +0 -2
  305. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
  306. package/.next/standalone/.next/static/chunks/3ws7eljkq25wg.js +0 -69
  307. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
  308. package/.next/standalone/.qwen/settings.json +0 -251
  309. package/.next/standalone/assets/logos/cline.png +0 -0
  310. package/.next/standalone/assets/logos/grok-dark.svg +0 -1
  311. package/.next/standalone/assets/logos/grok-light.svg +0 -1
  312. package/.next/standalone/assets/logos/ori-dark.svg +0 -1
  313. package/.next/standalone/assets/logos/ori-light.svg +0 -1
  314. package/.next/standalone/assets/logos/qwen.svg +0 -1
  315. package/.next/standalone/lib/cline-projects.ts +0 -155
  316. package/.next/standalone/lib/cline-sessions.ts +0 -268
  317. package/.next/standalone/lib/grok-projects.ts +0 -123
  318. package/.next/standalone/lib/grok-sessions.ts +0 -355
  319. package/.next/standalone/lib/ori-projects.ts +0 -141
  320. package/.next/standalone/lib/ori-sessions.ts +0 -221
  321. package/.next/standalone/lib/qwen-projects.ts +0 -106
  322. package/.next/standalone/lib/qwen-sessions.ts +0 -313
  323. package/lib/cline-projects.ts +0 -155
  324. package/lib/cline-sessions.ts +0 -268
  325. package/lib/grok-projects.ts +0 -123
  326. package/lib/grok-sessions.ts +0 -355
  327. package/lib/ori-projects.ts +0 -141
  328. package/lib/ori-sessions.ts +0 -221
  329. package/lib/qwen-projects.ts +0 -106
  330. package/lib/qwen-sessions.ts +0 -313
  331. package/src/audit/cli-adapters/cline.ts +0 -50
  332. package/src/audit/cli-adapters/grok.ts +0 -70
  333. package/src/audit/cli-adapters/ori.ts +0 -55
  334. package/src/audit/cli-adapters/qwen.ts +0 -71
  335. package/src/hooks/batch-expand.ts +0 -380
  336. package/src/hooks/batch-fanout.ts +0 -142
  337. package/src/hooks/risk-patterns.ts +0 -26
  338. /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
  339. /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
  340. /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
@@ -16,12 +16,6 @@ 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";
25
19
 
26
20
  /**
27
21
  * Whether `resolved` lives under an agent CLI's home directory
@@ -132,15 +126,80 @@ const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
132
126
 
133
127
  // sanitizeApiKeys
134
128
  const API_KEY_PATTERNS: Array<[RegExp, string]> = [
129
+ // ── Anthropic ── prefix doc-verified: platform.claude.com says a key
130
+ // "starts with `sk-ant-`" and its own example is `sk-ant-api03-...`.
131
+ [/sk-ant-api03-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
132
+ // Minted by the Claude CLI's OAuth flow and billed against a Pro/Max
133
+ // subscription rather than the API account — a different thing to revoke.
134
+ [/sk-ant-oat01-[A-Za-z0-9\-_]{20,}/, "Anthropic OAuth token"],
135
135
  [/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
136
+
137
+ // ── OpenAI ── the body charset EXCLUDES `-` and `_` on purpose: with them,
138
+ // this rule's run would bridge the hyphen in `sk-proj-` and report every
139
+ // project key a second time under the wrong label.
136
140
  [/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
137
- [/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
138
- [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
141
+ [/sk-svcacct-[A-Za-z0-9\-_]{20,}/, "OpenAI service-account key"],
142
+ // Highest blast radius in the family: it authenticates the organisation
143
+ // admin surface, and lives at a DIFFERENT console from the other keys.
144
+ [/sk-admin-[A-Za-z0-9\-_]{20,}/, "OpenAI admin key"],
145
+
146
+ // ── OpenAI-compatible gateways ──
147
+ [/sk-or-v1-[A-Za-z0-9\-_]{20,}/, "OpenRouter API key"],
148
+ // A BARE `sk-` CANNOT BE ATTRIBUTED, and calling it OpenAI is what this rule
149
+ // used to do. LiteLLM's own docs say its virtual keys "must start with sk-",
150
+ // and DeepSeek plus every other OpenAI-compatible proxy mints the same shape
151
+ // — so on a machine running a gateway (this one does) the old label sent the
152
+ // user to the wrong console to revoke a key that was never OpenAI's. The
153
+ // honest label names the family, not a vendor.
154
+ [/sk-[A-Za-z0-9]{20,}/, "OpenAI-compatible key (issuer unknown)"],
155
+
156
+ // ── GitHub ── all five documented types. github.blog's token-format post
157
+ // names ghp/gho/ghu/ghs/ghr; only two of them shipped before.
139
158
  [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
159
+ [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
160
+ [/gho_[A-Za-z0-9]{36}/, "GitHub OAuth token"],
161
+ [/ghu_[A-Za-z0-9]{36}/, "GitHub user-to-server token"],
162
+ [/ghs_[A-Za-z0-9]{36}/, "GitHub server-to-server token"],
163
+ [/ghr_[A-Za-z0-9]{36}/, "GitHub refresh token"],
164
+ [/glpat-[A-Za-z0-9\-_]{20,}/, "GitLab personal access token"],
165
+
166
+ // ── AWS ── the ID is the PUBLIC half; the paired 40-char secret is the
167
+ // credential. Reported anyway because an exposed ID names the account and is
168
+ // what a user searches their console for. `ASIA` (temporary STS) was missing.
140
169
  [/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
170
+ [/ASIA[A-Z0-9]{16}/, "AWS temporary access key ID"],
171
+
172
+ // ── Payments ── Stripe documents `pk_*` as "Safe to expose: Yes", so only
173
+ // the secret and restricted halves appear here.
141
174
  [/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
142
175
  [/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
176
+ [/rk_live_[A-Za-z0-9]{24,}/, "Stripe restricted key"],
177
+
178
+ // ── Google ── `AIza` also covers Firebase Web and Maps browser keys, which
179
+ // are public by design, so the label stays non-committal about severity.
143
180
  [/AIza[0-9A-Za-z\-_]{35}/, "Google API key"],
181
+
182
+ // ── Messaging ── the Telegram bot token was matched by ZERO of the shipped
183
+ // patterns and was found only because a phone-number regex tripped over it;
184
+ // two live-looking ones sit in this machine's corpus right now. Slack's
185
+ // prefixes were in the Rust collector but not here — one of nine such
186
+ // divergences between the two engines.
187
+ [/\d{8,10}:[A-Za-z0-9_-]{35}/, "Telegram bot token"],
188
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
189
+ [/xapp-[0-9]-[A-Za-z0-9-]{10,}/, "Slack app-level token"],
190
+
191
+ // ── Model hosts ──
192
+ [/hf_[A-Za-z0-9]{30,}/, "Hugging Face token"],
193
+ [/gsk_[A-Za-z0-9]{40,}/, "Groq API key"],
194
+ [/r8_[A-Za-z0-9]{30,}/, "Replicate token"],
195
+ [/xai-[A-Za-z0-9]{40,}/, "xAI API key"],
196
+
197
+ // ── Registries ──
198
+ [/npm_[A-Za-z0-9]{36}/, "npm access token"],
199
+ [/dop_v1_[a-f0-9]{64}/, "DigitalOcean token"],
200
+ [/shpat_[a-fA-F0-9]{32}/, "Shopify access token"],
201
+ [/sb_secret_[A-Za-z0-9\-_]{20,}/, "Supabase secret key"],
202
+ [/sbp_[a-f0-9]{40}/, "Supabase personal token"],
144
203
  ];
145
204
 
146
205
  // sanitizeConnectionStrings
@@ -198,6 +257,7 @@ const DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
198
257
  const CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
199
258
 
200
259
  // blockEnvFiles
260
+ const ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
201
261
  const ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
202
262
 
203
263
  // blockSudo
@@ -449,6 +509,9 @@ const SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
449
509
  const SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"] as const;
450
510
 
451
511
  // blockSecretsWrite
512
+ const SECRET_FILE_RE = /\.(?:pem|key)$/;
513
+ const SECRET_FILE_ID_RSA_RE = /id_rsa/;
514
+ const SECRET_FILE_CREDENTIALS_RE = /credentials/;
452
515
 
453
516
  // blockWorkOnMain
454
517
  const GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
@@ -74,6 +74,7 @@ import {
74
74
  probeDaemonEndToEnd,
75
75
  uninstallDaemonService,
76
76
  } from "./daemon-service";
77
+ import { installMacNotifier } from "../audit/macos-notifier";
77
78
  import { hookLogWarn } from "./hook-logger";
78
79
  import {
79
80
  readCloudCredentials,
@@ -1442,6 +1443,25 @@ export async function runConfigureWizard(
1442
1443
  // longer referenced by anything. Keeps the previous version for an
1443
1444
  // offline rollback.
1444
1445
  pruneOldDaemonBinaries();
1446
+
1447
+ // The delivery path for what the scheduled audit finds. macOS only, and a
1448
+ // no-op everywhere else: a Linux box's audit talks to the session bus
1449
+ // itself, while on a Mac the daemon runs outside the GUI session and cannot
1450
+ // reach Notification Center at all — see `audit/macos-notifier.ts`.
1451
+ //
1452
+ // Installed here, with the daemon, because it exists only to deliver what
1453
+ // the daemon's scheduled scan produces, and removed alongside it by
1454
+ // `uninstall`. Silent, and not a step in the wizard: it needs no password,
1455
+ // asks nothing, and narrating it would be an apology for the delivery half
1456
+ // of a feature the user just switched on. What they are actually asked —
1457
+ // does this machine scan, and may it interrupt — are real settings, and
1458
+ // both are theirs to change afterwards.
1459
+ //
1460
+ // Its failure is not the wizard's failure. A Mac where `osacompile` is
1461
+ // missing or launchd refuses the job must still finish installing its
1462
+ // daemon, its hooks and its policies, which are the parts that enforce
1463
+ // anything; the notice in-CLI still reaches them either way.
1464
+ installMacNotifier();
1445
1465
  }
1446
1466
 
1447
1467
  // Telemetry runs concurrently with the install (never rejects, 5s-bounded) so
@@ -253,103 +253,6 @@ 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
- },
353
256
  };
354
257
 
355
258
  /**
@@ -325,12 +325,53 @@ export interface FpConfig {
325
325
  * refresh token expiring must not silently switch off a background feature
326
326
  * somebody configured months ago.
327
327
  *
328
- * OFF by default, and the asymmetry with `telemetry.enabled` above is
329
- * deliberate: the audit reads the CONTENTS of every session transcript on
330
- * disk — prompts, file contents, command output — so nothing scans on a
331
- * timer until somebody asks for it.
328
+ * ON for a configured machine that has never stated an opinion, OFF for
329
+ * every machine we could not read an opinion from — which is not the same
330
+ * default twice, and the difference is the whole design.
331
+ *
332
+ * The scan reads the CONTENTS of every session transcript on disk, so the
333
+ * conservative instinct is to keep it off until asked. But the thing it
334
+ * looks for now is a leaked credential, and a credential nobody scans for
335
+ * is a credential nobody revokes: a switch that is off by default is a
336
+ * feature that, for most users, never runs. Setup is where the machine's
337
+ * owner is told what it does, so completing setup is the opinion.
338
+ *
339
+ * Hence the split. Once `config.json` exists AND carries an `audit` table,
340
+ * this is on unless it says exactly `false`. Absent file, unparseable JSON,
341
+ * or no `audit` table at all still read as OFF, because those are the three
342
+ * ways of not knowing, and "we could not tell" must never turn a transcript
343
+ * scan on. `crates/failproofaid/src/audit_lane.rs` makes the identical
344
+ * distinction over the identical bytes — the daemon and this reader must
345
+ * agree, or the settings page describes a machine that is doing something
346
+ * else.
347
+ *
348
+ * Turning it on sends NOTHING on its own: the digest is gated on
349
+ * `reportsConsentedAt` below, not on this switch, so a machine that flips
350
+ * on at upgrade scans locally and mails nobody.
332
351
  */
333
352
  auto: boolean;
353
+ /**
354
+ * Raise a desktop notification when a scan finds a credential.
355
+ *
356
+ * Separate from `auto` because they answer different questions: `auto` is
357
+ * whether the machine scans, this is whether it interrupts. Somebody who
358
+ * wants the scan and not the banner is a coherent person, and without this
359
+ * their only way to stop the banner is to stop the scan.
360
+ *
361
+ * ON by default, and the direction is the point: this fires at most once
362
+ * per distinct credential ever (`leak-notice.ts` claims each finding with
363
+ * an O_EXCL marker), so the volume it can reach is bounded by how many
364
+ * secrets are actually leaking. A silent default would make the common
365
+ * case — a user who never opens the dashboard and never gave an email —
366
+ * a machine that finds the key and tells nobody.
367
+ *
368
+ * It does NOT silence the in-CLI notice. That one appears inside a session
369
+ * the user is already driving, costs two lines, and is the only channel
370
+ * left when there is no desktop to notify — a headless box, a container, an
371
+ * SSH session. Turning off every channel at once is not an option this
372
+ * offers, because the resulting state is indistinguishable from broken.
373
+ */
374
+ notify: boolean;
334
375
  /**
335
376
  * When this machine's owner agreed that a scheduled scan may send what it
336
377
  * finds off the box, as epoch ms. Absent means they never did.
@@ -402,7 +443,9 @@ export const DEFAULT_CONFIG: FpConfig = {
402
443
  environment: "local",
403
444
  },
404
445
  telemetry: { enabled: true },
405
- audit: { auto: false, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
446
+ // `auto: false` is the no-file case specifically — see the field's doc for
447
+ // why a machine that HAS a config reads the other way.
448
+ audit: { auto: false, notify: true, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
406
449
  };
407
450
 
408
451
  /**
@@ -495,7 +538,11 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
495
538
  const daemon = (parsed.daemon ?? {}) as Record<string, unknown>;
496
539
  const collector = (parsed.collector ?? {}) as Record<string, unknown>;
497
540
  const telemetry = (parsed.telemetry ?? {}) as Record<string, unknown>;
498
- const audit = (parsed.audit ?? {}) as Record<string, unknown>;
541
+ // Presence, not just contents: `audit.auto` reads differently when the
542
+ // table is absent than when it is present-but-silent. See the field's doc.
543
+ const auditPresent =
544
+ !!parsed.audit && typeof parsed.audit === "object" && !Array.isArray(parsed.audit);
545
+ const audit = (auditPresent ? parsed.audit : {}) as Record<string, unknown>;
499
546
  return {
500
547
  // Anything unrecognised reads as `oss`. The failure direction matters: a
501
548
  // corrupt config must not be able to turn cloud reporting ON.
@@ -525,12 +572,20 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
525
572
  // reads as on — the shipped default, and what a config with no
526
573
  // [telemetry] block at all means.
527
574
  telemetry: { enabled: telemetry.enabled !== false },
528
- // Mirror image of telemetry above: only an explicit `true` switches the
529
- // scheduled scan on. Absent, misspelled, or `"yes"` all read as off,
530
- // because the failure direction here is a machine that starts reading
531
- // every transcript it can find on a timer nobody set.
575
+ // A configured machine with no stated opinion scans; a machine we could
576
+ // not read an opinion FROM does not. `auditPresent` is what separates
577
+ // them, and it is the reason this is not simply `!== false`: with no
578
+ // `audit` table there is no opinion to have been stated, only an absence,
579
+ // and an absence must not start reading every transcript on disk. Same
580
+ // three-way split as `audit_lane.rs`, over the same file.
532
581
  audit: {
533
- auto: audit.auto === true,
582
+ auto: auditPresent && audit.auto !== false,
583
+ // Default ON, off only on an explicit `false` — a notification the user
584
+ // never sees is the same as no detection at all. Ungated on
585
+ // `auditPresent`, unlike `auto`: this is read at the moment a
586
+ // notification is about to fire, which means a scan already ran and the
587
+ // question is only whether to speak.
588
+ notify: audit.notify !== false,
534
589
  intervalDays: readIntervalDays(audit.interval_days),
535
590
  // A finite number or nothing. A garbage value reads as absent, which is
536
591
  // the direction that sends nothing.
@@ -584,6 +639,7 @@ const OWNED_CONFIG_KEYS: readonly (readonly string[])[] = [
584
639
  ["collector", "sources", "*", "extra_paths"],
585
640
  ["telemetry", "enabled"],
586
641
  ["audit", "auto"],
642
+ ["audit", "notify"],
587
643
  ["audit", "interval_days"],
588
644
  ["audit", "reports_consented_at"],
589
645
  ];
@@ -688,6 +744,10 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
688
744
  // total rather than conditional.
689
745
  audit: {
690
746
  auto: config.audit.auto,
747
+ // Emitted unconditionally, like `auto`. A switch that only appears in the
748
+ // file once you have used it is a switch nobody discovers, and this one
749
+ // exists to be found by somebody the banner annoyed.
750
+ notify: config.audit.notify,
691
751
  interval_days: config.audit.intervalDays,
692
752
  // Written only once there IS consent, so an untouched machine's config
693
753
  // does not grow a key implying it was asked. It is in
@@ -299,6 +299,31 @@ export const auditSessionFile = (home?: string) => resolve(auditDir(home), "sess
299
299
  */
300
300
  export const auditMachineFile = (home?: string) => resolve(auditDir(home), "machine.json");
301
301
 
302
+ /**
303
+ * The leak record: one entry per DISTINCT credential seen in a transcript.
304
+ *
305
+ * `derived` — every finding in it is reproducible by rescanning, so a reset
306
+ * costs a rescan and nothing else. What is NOT reproducible lives next door in
307
+ * `auditLeakIdentityFile`, because the two have opposite reset semantics and
308
+ * this directory's own header says to classify the children, never the parent.
309
+ */
310
+ export const auditLeaksFile = (home?: string) => resolve(auditDir(home), "leaks.json");
311
+
312
+ /**
313
+ * The two things about a leak that a rescan cannot recreate: the HMAC salt that
314
+ * gives each finding its stable id, and the ids the user has dismissed.
315
+ *
316
+ * `identity`, and both halves for the same reason. Regenerate the salt and every
317
+ * id changes, so every previously-seen credential reads as brand new and the
318
+ * machine re-alerts for all of them at once. Drop the dismissals and the
319
+ * findings a human already judged not-a-secret come back — which is worse than
320
+ * a missed alert, because it teaches them the tool does not listen.
321
+ *
322
+ * Small by construction: a 32-byte salt and a list of 16-char ids.
323
+ */
324
+ export const auditLeakIdentityFile = (home?: string) =>
325
+ resolve(auditDir(home), "leak-identity.json");
326
+
302
327
 
303
328
  // ── fp-cloud-cli ───────────────────────────────────────────────────────────────────
304
329
 
@@ -628,6 +653,7 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
628
653
  // history the user was already told about. Kept OUT of `auditSessionFile`
629
654
  // precisely so both survive a sign-out.
630
655
  { path: auditMachineFile, class: "identity" },
656
+ { path: auditLeakIdentityFile, class: "identity" },
631
657
 
632
658
  // ── May be dropped: rebuilt on demand ──
633
659
  // NOTE: `auditDir` itself is deliberately absent. Layout 4 made it MIXED — it
@@ -637,6 +663,7 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
637
663
  // the delete list.
638
664
  { path: auditDashboardFile, class: "derived" },
639
665
  { path: auditCacheDir, class: "derived" },
666
+ { path: auditLeaksFile, class: "derived" },
640
667
  { path: auditScheduleFile, class: "derived" },
641
668
  { path: collectorHealthFile, class: "derived" },
642
669
  { path: codexSessionPathsFile, class: "derived" },
@@ -15,7 +15,6 @@ import type {
15
15
  HermesHookEventType,
16
16
  OpenClawHookEventType,
17
17
  AntigravityHookEventType,
18
- QwenHookEventType,
19
18
  } from "./types";
20
19
  import {
21
20
  CODEX_EVENT_MAP,
@@ -24,12 +23,9 @@ import {
24
23
  HERMES_EVENT_MAP,
25
24
  OPENCLAW_EVENT_MAP,
26
25
  ANTIGRAVITY_EVENT_MAP,
27
- QWEN_EVENT_MAP,
28
26
  } from "./types";
29
27
  import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize";
30
- import { expandBatchToolInput } from "./batch-expand";
31
- import { evaluateExpandedBatch } from "./batch-fanout";
32
- import { normalizeCliPayload, resolveEffectiveCli } from "./normalize-cli-payload";
28
+ import { normalizeCliPayload } from "./normalize-cli-payload";
33
29
  import type { PolicyFunction, PolicyResult, HooksConfig } from "./policy-types";
34
30
  import { readMergedHooksConfig } from "./hooks-config";
35
31
  import { registerBuiltinPolicies } from "./builtin-policies";
@@ -39,6 +35,8 @@ import { loadAllCustomHooks } from "./custom-hooks-loader";
39
35
  import type { CustomHook } from "./policy-types";
40
36
  import { persistHookActivity } from "./hook-activity-store";
41
37
  import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
38
+ import { shapeNotice, canDeliverNotice, leakNoticeText } from "./notice";
39
+ import { pendingLeakNotice, markLeakNoticeDelivered } from "../audit/leak-notice";
42
40
  import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
43
41
  import { resolveCwd } from "./resolve-cwd";
44
42
  import { resolvePermissionMode } from "./resolve-permission-mode";
@@ -97,13 +95,6 @@ export function canonicalizeEventType(raw: string, cli: IntegrationType): HookEv
97
95
  const mapped = ANTIGRAVITY_EVENT_MAP[raw as AntigravityHookEventType];
98
96
  if (mapped) return mapped;
99
97
  }
100
- if (cli === "qwen") {
101
- // Seventeen of qwen's nineteen events are already canonical; this maps the
102
- // two that are not — TodoCreated/TodoCompleted, which are qwen's spelling
103
- // of TaskCreated/TaskCompleted. Verified live against qwen-code 0.21.12.
104
- const mapped = QWEN_EVENT_MAP[raw as QwenHookEventType];
105
- if (mapped) return mapped;
106
- }
107
98
  // claude / copilot / unknown — already PascalCase, pass through.
108
99
  // HOOK_EVENT_TYPES type-checks downstream.
109
100
  return raw as HookEventType;
@@ -115,6 +106,48 @@ export interface HookEventOutcome {
115
106
  stderr: string;
116
107
  }
117
108
 
109
+ /**
110
+ * Merge a pending leak notice into an outcome, once.
111
+ *
112
+ * Anchored on Stop wherever the host renders there, and SessionStart only where
113
+ * it does not: Stop fires once per turn, AFTER the answer, so it never
114
+ * interrupts what the user was reading — and at session open the background
115
+ * scan may not have produced the finding yet.
116
+ *
117
+ * Every failure is swallowed. A courtesy message must never be the reason a
118
+ * hook misbehaves, and the finding is on disk either way.
119
+ */
120
+ function attachLeakNotice(
121
+ eventType: string,
122
+ cli: IntegrationType | undefined,
123
+ outcome: HookEventOutcome,
124
+ ): HookEventOutcome {
125
+ try {
126
+ if (!canDeliverNotice(cli)) return outcome;
127
+ // canDeliverNotice() above already proved cli is set; narrow for the call.
128
+ if (!cli) return outcome;
129
+ const canonical = canonicalizeEventType(eventType, cli);
130
+ if (canonical !== "Stop" && canonical !== "SessionStart") return outcome;
131
+
132
+ const pending = pendingLeakNotice();
133
+ if (pending.count === 0) return outcome;
134
+
135
+ const shaped = shapeNotice(cli, leakNoticeText(pending.count), outcome.stdout);
136
+ if (shaped.stdout === outcome.stdout && !shaped.stderr) return outcome;
137
+
138
+ // Claimed only once the notice is actually on the stream, so a crash
139
+ // between the two re-notifies rather than silently swallowing the alert.
140
+ markLeakNoticeDelivered(pending.ids);
141
+ return {
142
+ exitCode: outcome.exitCode,
143
+ stdout: shaped.stdout,
144
+ stderr: outcome.stderr + shaped.stderr,
145
+ };
146
+ } catch {
147
+ return outcome;
148
+ }
149
+ }
150
+
118
151
  export interface EvaluateHookEventOptions {
119
152
  /**
120
153
  * Await pending telemetry POSTs before returning. Defaults to `true`,
@@ -192,7 +225,7 @@ async function runObserved(
192
225
  */
193
226
  export async function evaluateHookEvent(
194
227
  eventType: string,
195
- declaredCli: IntegrationType = "claude",
228
+ cli: IntegrationType = "claude",
196
229
  stdinPayload: string,
197
230
  opts?: EvaluateHookEventOptions,
198
231
  ): Promise<HookEventOutcome> {
@@ -213,28 +246,12 @@ export async function evaluateHookEvent(
213
246
  hookLogWarn(`payload parse failed for ${eventType} (${stdinPayload.length} bytes)`);
214
247
  void trackHookEvent(getInstanceId(), "hook_payload_parse_error", {
215
248
  event_type: eventType,
216
- cli: declaredCli,
249
+ cli,
217
250
  payload_size: stdinPayload.length,
218
251
  });
219
252
  }
220
253
  }
221
254
 
222
- // grok executes other CLIs' hook configs — including the
223
- // `<cwd>/.claude/settings.json` our own claude install writes — passing
224
- // `--cli claude` while piping ITS camelCase payload. Resolve the CLI whose
225
- // contract actually governs this event BEFORE anything reads `cli`, so both
226
- // halves land on grok's path: the tool maps (without which every builtin
227
- // reads undefined) and the response shape (grok ignores Claude's
228
- // hookSpecificOutput deny — verified by A/B against a live session).
229
- // No-op for every other CLI. See normalize-cli-payload.ts:isGrokEnvelope.
230
- const cli = resolveEffectiveCli(declaredCli, parsed);
231
- if (cli !== declaredCli) {
232
- hookLogWarn(
233
- `payload for ${eventType} is a ${cli} envelope but --cli says ${declaredCli}; ` +
234
- `evaluating with the ${cli} contract`,
235
- );
236
- }
237
-
238
255
  normalizeCliPayload(cli, parsed);
239
256
 
240
257
  // Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
@@ -256,11 +273,6 @@ export async function evaluateHookEvent(
256
273
  // passes are idempotent because the camelCase keys won't match a
257
274
  // snake_case input.
258
275
  const rawInput = parsed.tool_input;
259
- // Read the RAW batch BEFORE canonicalization collapses it. The expander
260
- // emits canonical scalars directly, so there is no ambiguity about whether
261
- // canonicalization ran once, twice, or on the wrong shape. Returns null for
262
- // every CLI except cline, so the other 15 take the unchanged path below.
263
- const batchExpansion = expandBatchToolInput(cli, canonicalToolName, rawInput);
264
276
  const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
265
277
  if (canonicalInput !== rawInput) {
266
278
  parsed.tool_input = canonicalInput;
@@ -677,32 +689,8 @@ export async function evaluateHookEvent(
677
689
  );
678
690
  }
679
691
 
680
- // Telemetry keys already emitted in THIS invocation. Threaded into the
681
- // evaluator so a batch fan-out reports one event per real fault rather than
682
- // one per element. A null set (every non-batch call) changes nothing.
683
- const telemetryDedupe = new Set<string>();
684
-
685
- // Evaluate policies (use canonical PascalCase event type).
686
- //
687
- // Fan out only on PreToolUse: it is the only cline payload shape captured
688
- // live, and the only event where a per-element verdict changes anything.
689
- // forceDecision is excluded — its synthetic policy denies unconditionally,
690
- // so N elements would produce N identical denies and a meaningless locator.
691
- const result =
692
- batchExpansion && canonicalEventType === "PreToolUse" && !opts?.forceDecision
693
- ? await evaluateExpandedBatch(
694
- canonicalEventType,
695
- parsed,
696
- session,
697
- config,
698
- batchExpansion,
699
- telemetryDedupe,
700
- )
701
- // Deliberately NOT passed the dedupe set: it exists only to collapse
702
- // per-element telemetry inside a fan-out, and a single-shot call has
703
- // exactly one element. Passing it would change this call's shape for
704
- // all 16 CLIs to no effect.
705
- : await evaluatePolicies(canonicalEventType, parsed, session, config);
692
+ // Evaluate policies (use canonical PascalCase event type)
693
+ const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
706
694
  const durationMs = Math.round(performance.now() - startTime);
707
695
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
708
696
 
@@ -813,7 +801,21 @@ export async function evaluateHookEvent(
813
801
  }
814
802
  }
815
803
 
816
- return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
804
+ // Attach a pending leak notice, if this event is one that can carry it.
805
+ //
806
+ // Emitted HERE rather than in `handleHookEvent`, which is what fixes the
807
+ // bug this replaced: `bin/failproofai.mjs`'s `isDaemonConfigured()` branch
808
+ // writes `result.stdout`/`result.stderr` and EXITS INSIDE THAT BRANCH — it
809
+ // never falls through to `handleHookEvent`. Setup requires the daemon, so
810
+ // the previous notice was dead code on every configured machine, and had
811
+ // been since it shipped. Both paths return this object, so producing the
812
+ // notice here reaches both with no change to either caller.
813
+ const withNotice = attachLeakNotice(eventType, cli, {
814
+ exitCode: result.exitCode,
815
+ stdout: result.stdout,
816
+ stderr: result.stderr,
817
+ });
818
+ return withNotice;
817
819
  } finally {
818
820
  if (opts?.awaitTelemetryFlush ?? true) {
819
821
  // Await any un-awaited (`void trackHookEvent(...)`) events fired during
@@ -60,7 +60,7 @@ import {
60
60
  * the two lists are identical by reading the Rust source, because nothing else
61
61
  * connects them.
62
62
  *
63
- * Fourteen keys, fifteen sources: `claude` covers both the main and the subagent
63
+ * Twelve keys, thirteen sources: `claude` covers both the main and the subagent
64
64
  * transcript formats, which share a root — an extra path holding Claude
65
65
  * transcripts holds their subagents too.
66
66
  */
@@ -77,8 +77,6 @@ export const HARNESS_KEYS = [
77
77
  "opencode",
78
78
  "devin",
79
79
  "hermes",
80
- "grok",
81
- "qwen",
82
80
  ] as const;
83
81
 
84
82
  export type HarnessKey = (typeof HARNESS_KEYS)[number];