failproofai 1.0.3 → 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 (251) 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.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
  42. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  43. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/index.html +1 -1
  46. package/.next/standalone/.next/server/app/index.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  48. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  56. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
  83. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
  99. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
  103. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
  106. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
  120. package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
  121. package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
  123. package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
  124. package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
  126. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
  128. package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
  130. package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
  135. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
  136. package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
  137. package/.next/standalone/app/actions/get-leaks.ts +100 -0
  138. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
  139. package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
  140. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
  141. package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
  142. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
  143. package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
  144. package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
  145. package/.next/standalone/app/audit/audit-styles.css +114 -1
  146. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
  147. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
  148. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
  149. package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
  150. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
  151. package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
  152. package/.next/standalone/lib/auth/api-server-client.ts +28 -0
  153. package/.next/standalone/lib/claude-sessions.ts +101 -27
  154. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  155. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  156. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  157. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  158. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
  159. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
  160. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
  161. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  162. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
  163. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
  164. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  165. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  166. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  167. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  168. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
  169. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  170. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
  171. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  172. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  173. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  174. package/.next/standalone/node_modules/next/package.json +11 -11
  175. package/.next/standalone/package.json +14 -13
  176. package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
  177. package/.next/standalone/sdk/python/README.md +8 -0
  178. package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
  179. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  180. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
  181. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
  182. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
  183. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
  184. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
  185. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
  186. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
  187. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
  188. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
  189. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
  190. package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
  191. package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
  192. package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
  193. package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
  194. package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
  195. package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
  196. package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
  197. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
  198. package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
  199. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
  200. package/.next/standalone/sdk/python/uv.lock +29 -29
  201. package/.next/standalone/server.js +1 -1
  202. package/README.md +13 -58
  203. package/dist/cli.mjs +5368 -8756
  204. package/dist/worker.mjs +539 -312
  205. package/lib/auth/api-server-client.ts +28 -0
  206. package/lib/claude-sessions.ts +101 -27
  207. package/package.json +14 -13
  208. package/pi-extension/index.ts +42 -4
  209. package/scripts/changelog-open.py +115 -0
  210. package/src/audit/cli.ts +134 -0
  211. package/src/audit/desktop-notify.ts +420 -0
  212. package/src/audit/harm-report.ts +101 -0
  213. package/src/audit/index.ts +141 -13
  214. package/src/audit/leak-fingerprint.ts +200 -0
  215. package/src/audit/leak-notice.ts +161 -0
  216. package/src/audit/leak-record.ts +232 -0
  217. package/src/audit/leak-scan.ts +292 -0
  218. package/src/audit/leak-store.ts +217 -0
  219. package/src/audit/macos-notifier.ts +310 -0
  220. package/src/audit/redact-example.ts +266 -14
  221. package/src/audit/report-harm.ts +12 -1
  222. package/src/audit/report.ts +6 -3
  223. package/src/audit/schedule-cli.ts +37 -0
  224. package/src/audit/scoring.ts +49 -0
  225. package/src/audit/types.ts +41 -0
  226. package/src/hooks/builtin-policies.ts +67 -2
  227. package/src/hooks/configure-wizard.ts +20 -0
  228. package/src/hooks/fp-config.ts +71 -11
  229. package/src/hooks/fp-home.ts +27 -0
  230. package/src/hooks/handler.ts +59 -1
  231. package/src/hooks/integrations.ts +47 -13
  232. package/src/hooks/notice.ts +155 -0
  233. package/src/hooks/uninstall-cli.ts +15 -0
  234. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
  235. package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
  236. package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
  238. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
  240. package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
  241. package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
  242. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
  243. package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
  244. package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
  245. package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
  246. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
  247. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
  248. package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
  249. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
  250. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
  251. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
@@ -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
@@ -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" },
@@ -35,6 +35,8 @@ import { loadAllCustomHooks } from "./custom-hooks-loader";
35
35
  import type { CustomHook } from "./policy-types";
36
36
  import { persistHookActivity } from "./hook-activity-store";
37
37
  import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
38
+ import { shapeNotice, canDeliverNotice, leakNoticeText } from "./notice";
39
+ import { pendingLeakNotice, markLeakNoticeDelivered } from "../audit/leak-notice";
38
40
  import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
39
41
  import { resolveCwd } from "./resolve-cwd";
40
42
  import { resolvePermissionMode } from "./resolve-permission-mode";
@@ -104,6 +106,48 @@ export interface HookEventOutcome {
104
106
  stderr: string;
105
107
  }
106
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
+
107
151
  export interface EvaluateHookEventOptions {
108
152
  /**
109
153
  * Await pending telemetry POSTs before returning. Defaults to `true`,
@@ -757,7 +801,21 @@ export async function evaluateHookEvent(
757
801
  }
758
802
  }
759
803
 
760
- 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;
761
819
  } finally {
762
820
  if (opts?.awaitTelemetryFlush ?? true) {
763
821
  // Await any un-awaited (`void trackHookEvent(...)`) events fired during
@@ -828,7 +828,7 @@ function buildOpenCodePluginShim(binaryPath: string, scope: HookScope): string {
828
828
  // Re-generate via: failproofai policies --install --cli opencode
829
829
  // Plugin shim that bridges OpenCode's plugin API to the failproofai binary.
830
830
  // See: https://opencode.ai/docs/plugins/
831
- import { spawnSync } from "node:child_process";
831
+ import { spawn } from "node:child_process";
832
832
 
833
833
  // Map opencode bus-event types → canonical failproofai event names.
834
834
  // (The binary sees PascalCase — the binary's --cli=opencode flag is for
@@ -891,18 +891,52 @@ function canonicalizeToolInput(canonicalToolName, args) {
891
891
  const FAILPROOFAI_BIN = ${escapedBin};
892
892
  const USE_NPX = ${useNpx};
893
893
 
894
- function runFailproofai(eventName, payload, directory) {
894
+ /**
895
+ * Run failproofai for one event WITHOUT blocking opencode's event loop.
896
+ *
897
+ * This was spawnSync, and opencode loads the plugin in-process in the TUI —
898
+ * so every hook froze rendering and input for the subprocess's full duration,
899
+ * with a 60s ceiling. The verdicts are unchanged: every caller already awaits
900
+ * applyDecision, so the deny still lands before the tool runs. What changes
901
+ * is that the wait is now a promise rather than a blocked thread, and the TUI
902
+ * keeps painting while it happens.
903
+ *
904
+ * Fail-open on spawn error, and on the timeout — a policy that never ran must
905
+ * not read as a deny.
906
+ */
907
+ async function runFailproofai(eventName, payload, directory) {
895
908
  const cmd = USE_NPX ? "npx" : FAILPROOFAI_BIN;
896
909
  const args = USE_NPX
897
910
  ? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode"]
898
911
  : ["--hook", eventName, "--cli", "opencode"];
899
- const r = spawnSync(cmd, args, {
900
- input: JSON.stringify(payload),
901
- encoding: "utf8",
902
- timeout: 60_000,
903
- cwd: directory,
912
+ return await new Promise((resolveRun) => {
913
+ let child;
914
+ try {
915
+ child = spawn(cmd, args, { cwd: directory });
916
+ } catch {
917
+ resolveRun({ exitCode: 0, stdout: "", stderr: "" });
918
+ return;
919
+ }
920
+ let stdout = "";
921
+ let stderr = "";
922
+ let settled = false;
923
+ const finish = (exitCode) => {
924
+ if (settled) return;
925
+ settled = true;
926
+ clearTimeout(timer);
927
+ resolveRun({ exitCode, stdout, stderr });
928
+ };
929
+ const timer = setTimeout(() => {
930
+ try { child.kill("SIGKILL"); } catch { /* already gone */ }
931
+ finish(0);
932
+ }, 60_000);
933
+ child.stdout.on("data", (d) => { stdout += d; });
934
+ child.stderr.on("data", (d) => { stderr += d; });
935
+ child.on("error", () => finish(0));
936
+ child.on("close", (code) => finish(code ?? 0));
937
+ child.stdin.on("error", () => {});
938
+ child.stdin.end(JSON.stringify(payload));
904
939
  });
905
- return { exitCode: r.status ?? 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
906
940
  }
907
941
 
908
942
  async function applyDecision(result, ctx, eventName) {
@@ -967,7 +1001,7 @@ export default async function failproofaiPlugin({ client, directory }) {
967
1001
  }
968
1002
  }
969
1003
  if (!prompt) prompt = (info.text || info.content || props.text || "").toString();
970
- const r = runFailproofai("UserPromptSubmit", {
1004
+ const r = await runFailproofai("UserPromptSubmit", {
971
1005
  session_id: sessionID, cwd: directory, hook_event_name: "UserPromptSubmit", prompt,
972
1006
  }, directory);
973
1007
  await applyDecision(r, { client, sessionID }, "UserPromptSubmit");
@@ -978,7 +1012,7 @@ export default async function failproofaiPlugin({ client, directory }) {
978
1012
  if (!claudeEvent) return;
979
1013
  const props = event.properties || {};
980
1014
  const sessionID = props.sessionID || (props.session && props.session.id) || props.id;
981
- const r = runFailproofai(claudeEvent, {
1015
+ const r = await runFailproofai(claudeEvent, {
982
1016
  session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
983
1017
  }, directory);
984
1018
  await applyDecision(r, { client, sessionID }, claudeEvent);
@@ -987,7 +1021,7 @@ export default async function failproofaiPlugin({ client, directory }) {
987
1021
  // First-class PreToolUse hook. Note: tool args live on output.args (mutable).
988
1022
  "tool.execute.before": async (input, output) => {
989
1023
  const canonicalTool = canonicalizeTool(input.tool);
990
- const r = runFailproofai("PreToolUse", {
1024
+ const r = await runFailproofai("PreToolUse", {
991
1025
  session_id: input.sessionID,
992
1026
  cwd: directory,
993
1027
  tool_name: canonicalTool,
@@ -1000,7 +1034,7 @@ export default async function failproofaiPlugin({ client, directory }) {
1000
1034
  // First-class PostToolUse hook. Note: tool args live on input.args here.
1001
1035
  "tool.execute.after": async (input, output) => {
1002
1036
  const canonicalTool = canonicalizeTool(input.tool);
1003
- const r = runFailproofai("PostToolUse", {
1037
+ const r = await runFailproofai("PostToolUse", {
1004
1038
  session_id: input.sessionID,
1005
1039
  cwd: directory,
1006
1040
  tool_name: canonicalTool,
@@ -1014,7 +1048,7 @@ export default async function failproofaiPlugin({ client, directory }) {
1014
1048
  // Cleaner deny UX for prompted tools — mutate output.status instead of throwing.
1015
1049
  "permission.ask": async (input, output) => {
1016
1050
  const canonicalTool = canonicalizeTool(input.tool);
1017
- const r = runFailproofai("PermissionRequest", {
1051
+ const r = await runFailproofai("PermissionRequest", {
1018
1052
  session_id: input.sessionID,
1019
1053
  cwd: directory,
1020
1054
  tool_name: canonicalTool || input.command || "permission",
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Putting a short message in front of a HUMAN, inside the agent they are using.
3
+ *
4
+ * A notice is not a verdict. `allow` / `deny` / `instruct` all answer "what
5
+ * happens to this tool call"; a notice answers nothing — it is one line of text
6
+ * for the person, delivered alongside whatever the verdict turned out to be.
7
+ * That is why it is a separate field rather than a fourth decision: shaping it
8
+ * into stdout beside a verdict ad hoc is how the copilot nested-vs-flat
9
+ * `additionalContext` bug happened.
10
+ *
11
+ * ## Why the channel differs per CLI, and why it is not `additionalContext`
12
+ *
13
+ * Measured across all 12 CLIs by live probe. The obvious channel —
14
+ * `hookSpecificOutput.additionalContext` — reaches the MODEL, not the user, and
15
+ * a model is a paraphraser: over eight real Claude turns it mentioned an
16
+ * informational notice 8/8, but an earlier notice that asked the agent to *act*
17
+ * was disclaimed as a suspected prompt injection 6/6 ("instructions arriving
18
+ * that way aren't something I'll act on unasked"). A security notice routed
19
+ * through something that may decline to repeat it is not a delivered notice.
20
+ *
21
+ * So each CLI gets the channel that was proven to paint text on the user's
22
+ * terminal, and the ones with no such channel get nothing rather than a
23
+ * pretend delivery:
24
+ *
25
+ * claude `{"systemMessage":…}` renders `Stop says: …`; model-invisible
26
+ * (a neutral probe answered `NONE`)
27
+ * codex `{"systemMessage":…}` renders `• Stop (completed) says: …`
28
+ * copilot exit 0 + stderr renders as a `!` session warning
29
+ * factory plain stdout renders under a `Hooks Stop` block
30
+ *
31
+ * opencode, pi and openclaw have real channels too, but they live inside the
32
+ * plugin shims we ship rather than in a hook's stdout, so they are wired there.
33
+ * cursor, devin, goose, antigravity and hermes have no channel that reaches a
34
+ * human without an unsupported escape hatch — they get nothing here, and their
35
+ * users get the finding from `failproofai audit` and the emailed digest.
36
+ */
37
+ import type { IntegrationType } from "./types";
38
+
39
+ /** How a notice is delivered on one CLI. */
40
+ type NoticeChannel = "systemMessage" | "stderr" | "stdout" | "none";
41
+
42
+ /**
43
+ * Per-CLI channel, from live probes rather than documentation.
44
+ *
45
+ * A CLI absent from this map delivers nothing — deliberately. Guessing a
46
+ * channel produces the worst outcome available: output that looks delivered
47
+ * from our side and reaches nobody, so we would report a leak as "notified"
48
+ * that the user never saw.
49
+ */
50
+ const NOTICE_CHANNEL: Partial<Record<IntegrationType, NoticeChannel>> = {
51
+ claude: "systemMessage",
52
+ codex: "systemMessage",
53
+ copilot: "stderr",
54
+ factory: "stdout",
55
+ };
56
+
57
+ /**
58
+ * Claude truncates `systemMessage` at 4000 characters on the cloud-forwarding
59
+ * path, and a notice that gets cut mid-sentence reads as a broken tool. Ours is
60
+ * two lines by design; this is a backstop, not a budget.
61
+ */
62
+ const MAX_NOTICE_CHARS = 900;
63
+
64
+ export interface ShapedNotice {
65
+ /** Merged into the outcome's stdout, or empty. */
66
+ stdout: string;
67
+ /** Merged into the outcome's stderr, or empty. */
68
+ stderr: string;
69
+ }
70
+
71
+ /**
72
+ * Render a notice for one CLI, merging it into any verdict JSON already bound
73
+ * for stdout.
74
+ *
75
+ * `existingStdout` matters: a Stop-gate deny already emits a JSON object on
76
+ * several CLIs, and emitting a second object would produce two JSON documents
77
+ * on one stream — which every host parses as a syntax error and drops, taking
78
+ * the deny with it. So the notice is merged INTO that object when there is one.
79
+ */
80
+ export function shapeNotice(
81
+ cli: IntegrationType | undefined,
82
+ notice: string,
83
+ existingStdout = "",
84
+ ): ShapedNotice {
85
+ const empty: ShapedNotice = { stdout: existingStdout, stderr: "" };
86
+ if (!notice.trim() || !cli) return empty;
87
+
88
+ const channel = NOTICE_CHANNEL[cli] ?? "none";
89
+ const text = notice.length > MAX_NOTICE_CHARS
90
+ ? notice.slice(0, MAX_NOTICE_CHARS - 1) + "…"
91
+ : notice;
92
+
93
+ switch (channel) {
94
+ case "systemMessage": {
95
+ // Merge into the verdict object when one exists, so the stream stays a
96
+ // single JSON document.
97
+ if (existingStdout.trim().startsWith("{")) {
98
+ try {
99
+ const parsed = JSON.parse(existingStdout) as Record<string, unknown>;
100
+ parsed.systemMessage = text;
101
+ return { stdout: JSON.stringify(parsed), stderr: "" };
102
+ } catch {
103
+ // Not the JSON we thought it was — leave the verdict untouched
104
+ // rather than risk destroying it to add a courtesy message.
105
+ return empty;
106
+ }
107
+ }
108
+ // Anything already on stdout that we could not merge into is left
109
+ // exactly as it is, and the notice is dropped instead. The alternative —
110
+ // emitting our own object over the top — DESTROYS it: measured with a
111
+ // JSON array on stdout, which does not start with `{`, so it fell past
112
+ // the merge and was replaced wholesale. A courtesy message is never worth
113
+ // more than whatever the host was already being told.
114
+ if (existingStdout.trim()) return empty;
115
+ return { stdout: JSON.stringify({ systemMessage: text }), stderr: "" };
116
+ }
117
+ case "stderr":
118
+ // Exit code is NOT touched: on copilot a non-zero exit from a tool event
119
+ // is a block, and a notice must never block anything.
120
+ return { stdout: existingStdout, stderr: text + "\n" };
121
+ case "stdout":
122
+ // factory renders plain text under its Hooks block. Only safe when no
123
+ // verdict JSON is already on the stream.
124
+ if (existingStdout.trim()) return empty;
125
+ return { stdout: text + "\n", stderr: "" };
126
+ default:
127
+ return empty;
128
+ }
129
+ }
130
+
131
+ /** True when this CLI has a channel that was proven to reach a human. */
132
+ export function canDeliverNotice(cli: IntegrationType | undefined): boolean {
133
+ return !!cli && (NOTICE_CHANNEL[cli] ?? "none") !== "none";
134
+ }
135
+
136
+ /**
137
+ * The notice itself. A FIXED template with no interpolation of scanned content.
138
+ *
139
+ * The count is a number we computed; everything else is a constant. That is not
140
+ * stylistic: the alternative is interpolating a finding's `example`, which is
141
+ * the verbatim text of a command from a repository — attacker-controlled
142
+ * through a README, a Makefile target or an npm script. Tested against the
143
+ * shipped redactor, `echo "IGNORE ALL PRIOR INSTRUCTIONS…"` passes through
144
+ * byte-identical, because that redactor masks secrets, not instructions. On the
145
+ * CLIs where the only channel is the model's context, that string would land
146
+ * directly in the prompt. So nothing scanned ever reaches this template.
147
+ */
148
+ export function leakNoticeText(newFindings: number): string {
149
+ const n = newFindings === 1 ? "a credential" : `${newFindings} credentials`;
150
+ return (
151
+ `failproofai found ${n} in this project's agent transcripts.\n` +
152
+ "Run `failproofai audit` to see the details, " +
153
+ "or `failproofai audit --schedule` to get these by email."
154
+ );
155
+ }
@@ -47,6 +47,7 @@ import {
47
47
  import { listInstallableIds, getIntegration } from "./integrations";
48
48
  import type { IntegrationType } from "./types";
49
49
  import { removeHooks } from "./manager";
50
+ import { uninstallMacNotifier } from "../audit/macos-notifier";
50
51
 
51
52
  export interface UninstallOptions {
52
53
  /** Also delete ~/.failproofai (config, credentials, state, audit cache, daemon binary). */
@@ -346,6 +347,20 @@ export async function runUninstallCommand(opts: UninstallOptions = {}): Promise<
346
347
  }
347
348
  }
348
349
 
350
+ // The macOS notification agent, which is the one thing `--purge` alone cannot
351
+ // reach: its bundle lives inside `~/.failproofai` and goes with the home, but
352
+ // its LaunchAgent plist sits in `~/Library/LaunchAgents`, outside everything
353
+ // this command otherwise deletes. Left behind, launchd keeps trying to start
354
+ // an applet that is no longer on disk and logs the failure at every login —
355
+ // precisely the class of leftover an uninstall exists to prevent.
356
+ //
357
+ // No-ops on every other platform, and needs no elevation on macOS: a per-user
358
+ // agent is the user's own to load and unload. Tied to the daemon's removal
359
+ // because it exists only to deliver what the daemon's scheduled audit finds.
360
+ if (opts.purge || (found.serviceInstalled && (opts.purge || opts.yes))) {
361
+ uninstallMacNotifier();
362
+ }
363
+
349
364
  if (opts.purge && found.homeExists) {
350
365
  // Last, and only after the service is down: the daemon binary and its
351
366
  // socket live here, and deleting them out from under a running unit is how
@@ -1,3 +0,0 @@
1
- module.exports=[18622,(a,b,c)=>{b.exports=a.x("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js",()=>require("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js"))},20635,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/action-async-storage.external.js",()=>require("next/dist/server/app-render/action-async-storage.external.js"))},24725,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/after-task-async-storage.external.js",()=>require("next/dist/server/app-render/after-task-async-storage.external.js"))},43285,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/dynamic-access-async-storage.external.js",()=>require("next/dist/server/app-render/dynamic-access-async-storage.external.js"))},56704,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-async-storage.external.js",()=>require("next/dist/server/app-render/work-async-storage.external.js"))},32319,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-unit-async-storage.external.js",()=>require("next/dist/server/app-render/work-unit-async-storage.external.js"))},59043,(a,b,c)=>{b.exports=a.x("next/dist/server/runtime-reacts.external.js",()=>require("next/dist/server/runtime-reacts.external.js"))},31677,a=>{"use strict";var b=a.i(5050);let c=(0,b.createServerReference)("0021127018a217bb03f53322147dee0b96bd0077f2",b.callServer,void 0,b.findSourceMapURL,"getTelemetryConfig");a.s(["getTelemetryConfig",0,c])},3171,a=>{"use strict";var b=a.i(87924),c=a.i(72131),d=a.i(31677),e=a.i(66036);a.s(["default",0,function({error:a,reset:f}){return(0,c.useEffect)(()=>{(0,d.getTelemetryConfig)().then(b=>{(0,e.setClientTelemetryConfig)(b),(0,e.captureClientEvent)("client_error",{error_message:a.message,error_name:a.name,error_digest:a.digest,boundary:"global"})}).catch(()=>{})},[a]),(0,b.jsx)("html",{children:(0,b.jsx)("body",{children:(0,b.jsx)("main",{style:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:"#031035",color:"#f8fafc",fontFamily:"system-ui, sans-serif"},children:(0,b.jsxs)("div",{style:{textAlign:"center",padding:"2rem",border:"1px solid rgba(239,68,68,0.4)",borderRadius:"0.5rem",maxWidth:"500px"},children:[(0,b.jsx)("h2",{style:{color:"#ef4444",marginBottom:"0.5rem",fontSize:"1.25rem"},children:"Something went wrong"}),(0,b.jsx)("p",{style:{color:"#94a3b8",marginBottom:"1.5rem"},children:a.message||"An unexpected error occurred."}),(0,b.jsx)("button",{onClick:f,style:{padding:"0.5rem 1.25rem",background:"#3b82f6",color:"white",border:"none",borderRadius:"0.375rem",cursor:"pointer",fontSize:"0.875rem"},children:"Try again"})]})})})})}])},66036,a=>{"use strict";a.s(["captureClientEvent",0,function(a,b){},"setClientTelemetryConfig",0,function(a){}])},46058,(a,b,c)=>{"use strict";function d(a){if("function"!=typeof WeakMap)return null;var b=new WeakMap,c=new WeakMap;return(d=function(a){return a?c:b})(a)}c._=function(a,b){if(!b&&a&&a.__esModule)return a;if(null===a||"object"!=typeof a&&"function"!=typeof a)return{default:a};var c=d(b);if(c&&c.has(a))return c.get(a);var e={__proto__:null},f=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var g in a)if("default"!==g&&Object.prototype.hasOwnProperty.call(a,g)){var h=f?Object.getOwnPropertyDescriptor(a,g):null;h&&(h.get||h.set)?Object.defineProperty(e,g,h):e[g]=a[g]}return e.default=a,c&&c.set(a,e),e}},5050,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={callServer:function(){return f.callServer},createServerReference:function(){return h.createServerReference},findSourceMapURL:function(){return g.findSourceMapURL}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f=a.r(20611),g=a.r(1722),h=a.r(38783)},20611,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"callServer",{enumerable:!0,get:function(){return g}});let d=a.r(72131),e=a.r(88347),f=a.r(90841);async function g(a,b){return new Promise((c,g)=>{(0,d.startTransition)(()=>{(0,f.dispatchAppRouterAction)({type:e.ACTION_SERVER_ACTION,actionId:a,actionArgs:b,resolve:c,reject:g})})})}("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},1722,(a,b,c)=>{"use strict";let d;Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"findSourceMapURL",{enumerable:!0,get:function(){return d}});("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},88347,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d,e,f={ACTION_HMR_REFRESH:function(){return l},ACTION_NAVIGATE:function(){return i},ACTION_REFRESH:function(){return h},ACTION_RESTORE:function(){return j},ACTION_SERVER_ACTION:function(){return m},ACTION_SERVER_PATCH:function(){return k},PrefetchKind:function(){return n},ScrollBehavior:function(){return o}};for(var g in f)Object.defineProperty(c,g,{enumerable:!0,get:f[g]});let h="refresh",i="navigate",j="restore",k="server-patch",l="hmr-refresh",m="server-action";var n=((d={}).AUTO="auto",d.FULL="full",d),o=((e={})[e.Default=0]="Default",e[e.NoScroll=1]="NoScroll",e);("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},90841,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={dispatchAppRouterAction:function(){return i},dispatchGestureState:function(){return j},refreshOnInstantNavigationUnlock:function(){return h},useActionQueue:function(){return k}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f=a.r(46058)._(a.r(72131)),g=a.r(67009);a.r(88347);function h(){}function i(a){!0;throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0})}function j(a){!0;throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0})}function k(a){let[b,c]=f.default.useState(a.state),[d,e]=(0,f.useOptimistic)(b),h=(0,f.useMemo)(()=>d,[d]);return(0,g.isThenable)(h)?(0,f.use)(h):h}("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},42602,(a,b,c)=>{"use strict";b.exports=a.r(18622)},9270,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.AppRouterContext},36313,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.HooksClientContext},18341,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.ServerInsertedHtml},35112,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactDOM},87924,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactJsxRuntime},38783,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactServerDOMTurbopackClient},72131,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].React},67009,(a,b,c)=>{"use strict";function d(a){return null!==a&&"object"==typeof a&&"then"in a&&"function"==typeof a.then}Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"isThenable",{enumerable:!0,get:function(){return d}})}];
2
-
3
- //# sourceMappingURL=%5Broot-of-the-server%5D__1b5jzxr._.js.map