failproofai 1.0.4-beta.1 → 1.0.4-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) 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]__0v1hz15._.js} +1 -1
  81. package/.next/standalone/.next/server/chunks/{[externals]__0_gzk8q._.js → [externals]__1nb206a._.js} +1 -1
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
  90. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/_0otft92._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +14 -21
  95. package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  99. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wvusu0._.js → [root-of-the-server]__0n_lxhg._.js} +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__16gamwq._.js → [root-of-the-server]__0yl9wb5._.js} +2 -2
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/{_1byck5v._.js → _0guxnh-._.js} +2 -2
  123. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +61 -168
  128. package/.next/standalone/.next/server/chunks/ssr/{_08f0jd2._.js → _1if9ixi._.js} +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  131. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
  132. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
  133. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  138. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  141. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  142. package/.next/standalone/.next/server/pages/404.html +1 -1
  143. package/.next/standalone/.next/server/pages/500.html +1 -1
  144. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  145. package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
  146. package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +1 -0
  147. package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0jpd8dv2vk930.css} +1 -1
  148. package/.next/standalone/.next/static/chunks/{1po3lirgk2riw.js → 0po8n4tyotjf2.js} +1 -1
  149. package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +2 -0
  150. package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +1 -0
  151. package/.next/standalone/.next/static/chunks/17tfki9x5rd_7.js +1 -0
  152. package/.next/standalone/.next/static/chunks/{1_bxotmf9qngs.js → 1i-i0pxg9q_8y.js} +1 -1
  153. package/.next/standalone/.next/static/chunks/{1_2jsatfuthrw.js → 2bxr2_2h4kaqq.js} +1 -1
  154. package/.next/standalone/.next/static/chunks/{01hvwwrhvbzyh.js → 2zafgs90r_leq.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{00gv0v91mycm-.js → 323cyb3a2s06y.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{0qn9khv0oo9ex.js → 364cqbpeg1y5i.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{2bvwsyu6x__ia.js → 38dqur43qthrl.js} +1 -1
  158. package/.next/standalone/.next/static/chunks/{3_--3ibia6ba1.js → 3l36d73kj18zw.js} +1 -1
  159. package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
  160. package/.next/standalone/.next/static/chunks/{2bnf_4pgyk1rq.js → 40lpt2o2b283e.js} +1 -1
  161. package/.next/standalone/.next/static/chunks/42gwu2bsz4ske.js +6 -0
  162. package/.next/standalone/.next/static/chunks/{turbopack-18qf4jodtft6b.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
  163. package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
  164. package/.next/standalone/app/actions/get-leaks.ts +109 -0
  165. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +140 -34
  166. package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
  167. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
  168. package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
  169. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
  170. package/.next/standalone/app/audit/_components/leak-section.tsx +192 -0
  171. package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
  172. package/.next/standalone/app/audit/audit-styles.css +132 -1
  173. package/.next/standalone/app/project/[name]/page.tsx +4 -24
  174. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
  175. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
  176. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
  177. package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
  178. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
  179. package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
  180. package/.next/standalone/lib/auth/api-server-client.ts +28 -0
  181. package/.next/standalone/lib/claude-sessions.ts +101 -27
  182. package/.next/standalone/lib/cli-registry.ts +1 -33
  183. package/.next/standalone/lib/download-session.ts +0 -37
  184. package/.next/standalone/lib/projects.ts +3 -27
  185. package/.next/standalone/lib/sqlite-reader.ts +78 -1
  186. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  187. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  188. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
  189. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
  190. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  191. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
  192. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
  193. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  194. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  195. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  196. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +2 -2
  197. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  198. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  199. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  200. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  201. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  202. package/.next/standalone/node_modules/next/package.json +11 -11
  203. package/.next/standalone/package.json +15 -14
  204. package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
  205. package/.next/standalone/sdk/python/README.md +8 -0
  206. package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
  207. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  208. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
  209. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
  210. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
  211. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
  212. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
  213. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
  214. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
  215. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
  216. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
  217. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
  218. package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
  219. package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
  220. package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
  221. package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
  222. package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
  223. package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
  224. package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
  225. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
  226. package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
  227. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
  228. package/.next/standalone/sdk/python/uv.lock +18 -18
  229. package/.next/standalone/server.js +1 -1
  230. package/README.md +19 -95
  231. package/bin/failproofai.mjs +8 -25
  232. package/dist/cli.mjs +20091 -25864
  233. package/dist/worker.mjs +4881 -6361
  234. package/lib/auth/api-server-client.ts +28 -0
  235. package/lib/claude-sessions.ts +101 -27
  236. package/lib/cli-registry.ts +1 -33
  237. package/lib/download-session.ts +0 -37
  238. package/lib/projects.ts +3 -27
  239. package/lib/sqlite-reader.ts +78 -1
  240. package/package.json +15 -14
  241. package/pi-extension/index.ts +61 -8
  242. package/scripts/changelog-open.py +115 -0
  243. package/src/audit/cache.ts +6 -0
  244. package/src/audit/cli-adapters/claude.ts +13 -0
  245. package/src/audit/cli-adapters/index.ts +0 -24
  246. package/src/audit/cli-login.ts +4 -1
  247. package/src/audit/cli.ts +164 -5
  248. package/src/audit/desktop-notify.ts +420 -0
  249. package/src/audit/harm-report.ts +101 -0
  250. package/src/audit/index.ts +284 -15
  251. package/src/audit/leak-fingerprint.ts +200 -0
  252. package/src/audit/leak-notice.ts +259 -0
  253. package/src/audit/leak-record.ts +232 -0
  254. package/src/audit/leak-scan.ts +268 -0
  255. package/src/audit/leak-store.ts +265 -0
  256. package/src/audit/macos-notifier.ts +310 -0
  257. package/src/audit/redact-example.ts +322 -15
  258. package/src/audit/report-harm.ts +12 -1
  259. package/src/audit/report.ts +6 -3
  260. package/src/audit/schedule-cli.ts +37 -0
  261. package/src/audit/scoring.ts +49 -0
  262. package/src/audit/types.ts +47 -0
  263. package/src/hooks/builtin-policies.ts +71 -8
  264. package/src/hooks/configure-wizard.ts +20 -0
  265. package/src/hooks/enforcement-capability.ts +0 -97
  266. package/src/hooks/fp-config.ts +71 -11
  267. package/src/hooks/fp-home.ts +27 -0
  268. package/src/hooks/handler.ts +84 -62
  269. package/src/hooks/harness-cli.ts +1 -3
  270. package/src/hooks/integrations.ts +63 -769
  271. package/src/hooks/normalize-cli-payload.ts +0 -126
  272. package/src/hooks/notice.ts +191 -0
  273. package/src/hooks/policy-evaluator.ts +6 -297
  274. package/src/hooks/resolve-transcript-path.ts +0 -10
  275. package/src/hooks/tool-name-canonicalize.ts +0 -64
  276. package/src/hooks/types.ts +2 -533
  277. package/src/hooks/uninstall-cli.ts +15 -0
  278. package/.next/standalone/.grok/hooks/failproofai.json +0 -172
  279. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +0 -5
  280. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +0 -3
  281. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +0 -3
  282. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +0 -3
  283. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +0 -3
  284. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +0 -3
  285. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +0 -3
  286. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +0 -3
  287. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +0 -3
  288. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +0 -3
  289. package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +0 -3
  290. package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +0 -3
  291. package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +0 -3
  292. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +0 -3
  293. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +0 -3
  294. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +0 -3
  295. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +0 -3
  296. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +0 -3
  297. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +0 -3
  298. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +0 -3
  299. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +0 -3
  300. package/.next/standalone/.next/server/chunks/ssr/_00z5uuq._.js +0 -3
  301. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
  302. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
  303. package/.next/standalone/.next/server/chunks/ssr/lib_cline-projects_ts_13_hbdd._.js +0 -3
  304. package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +0 -3
  305. package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +0 -3
  306. package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.js +0 -3
  307. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
  308. package/.next/standalone/.next/static/chunks/0hzmxwb2pvk5p.js +0 -6
  309. package/.next/standalone/.next/static/chunks/0j884dwnwtyrt.js +0 -1
  310. package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +0 -1
  311. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
  312. package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +0 -2
  313. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
  314. package/.next/standalone/.next/static/chunks/3ws7eljkq25wg.js +0 -69
  315. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
  316. package/.next/standalone/.qwen/settings.json +0 -251
  317. package/.next/standalone/assets/logos/cline.png +0 -0
  318. package/.next/standalone/assets/logos/grok-dark.svg +0 -1
  319. package/.next/standalone/assets/logos/grok-light.svg +0 -1
  320. package/.next/standalone/assets/logos/ori-dark.svg +0 -1
  321. package/.next/standalone/assets/logos/ori-light.svg +0 -1
  322. package/.next/standalone/assets/logos/qwen.svg +0 -1
  323. package/.next/standalone/lib/cline-projects.ts +0 -155
  324. package/.next/standalone/lib/cline-sessions.ts +0 -268
  325. package/.next/standalone/lib/grok-projects.ts +0 -123
  326. package/.next/standalone/lib/grok-sessions.ts +0 -355
  327. package/.next/standalone/lib/ori-projects.ts +0 -141
  328. package/.next/standalone/lib/ori-sessions.ts +0 -221
  329. package/.next/standalone/lib/qwen-projects.ts +0 -106
  330. package/.next/standalone/lib/qwen-sessions.ts +0 -313
  331. package/lib/cline-projects.ts +0 -155
  332. package/lib/cline-sessions.ts +0 -268
  333. package/lib/grok-projects.ts +0 -123
  334. package/lib/grok-sessions.ts +0 -355
  335. package/lib/ori-projects.ts +0 -141
  336. package/lib/ori-sessions.ts +0 -221
  337. package/lib/qwen-projects.ts +0 -106
  338. package/lib/qwen-sessions.ts +0 -313
  339. package/src/audit/cli-adapters/cline.ts +0 -50
  340. package/src/audit/cli-adapters/grok.ts +0 -70
  341. package/src/audit/cli-adapters/ori.ts +0 -55
  342. package/src/audit/cli-adapters/qwen.ts +0 -71
  343. package/src/hooks/batch-expand.ts +0 -380
  344. package/src/hooks/batch-fanout.ts +0 -142
  345. package/src/hooks/risk-patterns.ts +0 -26
  346. /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → P6icI2V2PvVuePJrqjHCr}/_buildManifest.js +0 -0
  347. /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → P6icI2V2PvVuePJrqjHCr}/_clientMiddlewareManifest.js +0 -0
  348. /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → P6icI2V2PvVuePJrqjHCr}/_ssgManifest.js +0 -0
@@ -42,6 +42,8 @@
42
42
  */
43
43
  import { homedir } from "node:os";
44
44
 
45
+ import type { AuditResult } from "./types";
46
+
45
47
  import { SECRET_PATTERNS } from "../hooks/builtin-policies";
46
48
 
47
49
  /** Longest example we let through, after redaction. */
@@ -132,18 +134,77 @@ export function maskTruncatedSecret(input: string): string {
132
134
  * identifier, including the camelCase `_authToken` that `npm config set`
133
135
  * writes. `KEY`, `PASS`, `AUTH`, `PAT` and `SIG` are also fragments of ordinary
134
136
  * words — `MONKEY_COUNT`, `PASSENGERS`, `AUTHOR`, `PATH`, `SIGNAL` — so they
135
- * only count as a whole `_`-delimited component.
137
+ * only count as a whole component, where the components come from
138
+ * `identifierComponents` and include camelCase humps as well as `_`.
136
139
  */
137
140
  const SECRET_NAME_SUBSTRINGS = [
138
141
  "TOKEN",
139
142
  "SECRET",
140
143
  "PASSWORD",
141
144
  "PASSWD",
145
+ "PASSPHRASE",
142
146
  "CREDENTIAL",
143
147
  "APIKEY",
144
148
  "PRIVATEKEY",
145
149
  ];
146
- const SECRET_NAME_COMPONENTS = ["KEY", "PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "SESSION", "COOKIE"];
150
+ const SECRET_NAME_COMPONENTS = ["PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "COOKIE"];
151
+
152
+ /**
153
+ * Names that mean "credential" only inside a COMPOUND identifier.
154
+ *
155
+ * Every entry here was measured as a dominant false positive on a real 2,664-
156
+ * transcript corpus, and the reason they are demotions rather than deletions is
157
+ * that the SHAPE layer already catches real vendor keys regardless of what they
158
+ * are called. The name layer exists for FIRST-PARTY secrets with no recognisable
159
+ * format — and those essentially always carry a qualified name
160
+ * (`COMPOSIO_API_KEY`, `DB_PASSWORD`), never a bare one. So requiring a compound
161
+ * costs almost nothing real and removes most of the noise.
162
+ *
163
+ * PWD `MYSQL_PWD` is MySQL's documented password variable; a bare `PWD`
164
+ * is the shell's working directory, on every second line of a
165
+ * captured session.
166
+ * KEY a bare `key=` is a React list prop, a map entry, a sort field.
167
+ * 80 findings on the measured corpus, none of them credentials.
168
+ *
169
+ * `SIG` was demoted here too and put back: a bare `sig=` in a URL query IS a
170
+ * request signature, and an existing test says so. It also never appeared in
171
+ * the measured noise, unlike the two above — the rule is that a demotion needs
172
+ * evidence, not a plausible story.
173
+ *
174
+ * `SESSION` is REMOVED from both lists rather than demoted, and the difference
175
+ * matters: demoting it would have changed nothing, because
176
+ * `DBUS_SESSION_BUS_ADDRESS` is already a compound. It was the single largest
177
+ * source of noise measured — 254 of 500 findings, every one of them
178
+ * `DBUS_SESSION_BUS_ADDRESS`, `XDG_SESSION_TYPE`, `SESSION_MANAGER`,
179
+ * `session_id` or `sessionUpdate`. A session IDENTIFIER is not a rotatable
180
+ * credential; a session SECRET still matches through `SECRET`, `TOKEN`, or
181
+ * `KEY` in `sessionKey`.
182
+ */
183
+ const COMPOUND_ONLY_COMPONENTS = ["PWD", "KEY"];
184
+
185
+ /**
186
+ * Split an identifier into its uppercased components, on `_`, `-`, `.` AND
187
+ * camelCase humps.
188
+ *
189
+ * The humps are the point. Splitting on `_` alone means `sessionKey`, `dbPass`,
190
+ * `basicAuth` and `authCookie` decompose to a single component that matches
191
+ * nothing, so their values shipped to the digest verbatim — 203 such names on
192
+ * this machine's corpus. They are not exotic: camelCase is what an identifier
193
+ * looks like everywhere except a shell environment, and the redactor is applied
194
+ * to transcripts full of TypeScript, JSON and Python.
195
+ *
196
+ * The second replace splits an acronym from the word that follows it, so
197
+ * `APIKey` yields `API` + `KEY` rather than one run of capitals that matches
198
+ * neither.
199
+ */
200
+ function identifierComponents(name: string): string[] {
201
+ return name
202
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
203
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
204
+ .toUpperCase()
205
+ .split(/[_\-.]+/)
206
+ .filter(Boolean);
207
+ }
147
208
 
148
209
  /**
149
210
  * `NAME=value`, with the value quoted or running to the next shell separator.
@@ -151,20 +212,158 @@ const SECRET_NAME_COMPONENTS = ["KEY", "PASS", "AUTH", "PAT", "SIG", "SIGNATURE"
151
212
  * The unquoted alternative excludes quotes as well as separators: a value that
152
213
  * runs to end-of-token inside an already-quoted string (`"…?sig=deadbeef"`)
153
214
  * would otherwise swallow the closing quote and leave the line unbalanced.
215
+ *
216
+ * Four shapes, because the first spelling of this pattern was `NAME=value` and
217
+ * only that — no whitespace around the `=`, no `:`, no quoted name — and the
218
+ * other three all reached the emailed digest UNREDACTED. `NAME = "value"` and
219
+ * `NAME: "value"` are what a config file, a YAML key and a JSON body look like,
220
+ * which is most of where credentials are actually written down; `"name": val`
221
+ * is the same line once an agent pretty-prints it. A value only survived that
222
+ * gap if it independently matched one of the vendor patterns in
223
+ * `SECRET_PATTERNS` — and 230 of 237 secret-named assignments measured across
224
+ * this machine's transcripts match none of them, so the assignment rule was the
225
+ * only thing standing between them and the digest.
226
+ *
227
+ * The separator is captured and re-emitted verbatim rather than normalised to
228
+ * `=`, so a redacted YAML line is still YAML and a redacted JSON line is still
229
+ * recognisable as the field it came from. Horizontal whitespace only: `\s`
230
+ * would let a `KEY:` at end-of-line swallow the newline and glue the next line
231
+ * onto the match.
232
+ *
233
+ * The `:` alternative refuses a `//` after it, because a URL scheme is the one
234
+ * `name:value` shape that is not an assignment. Without the guard `https` reads
235
+ * as the name and the rest of the URL as its value — and since a non-secret
236
+ * name returns the match UNCHANGED but still CONSUMES it, a `?token=…` sitting
237
+ * inside that URL was swallowed by the `https:` match and never examined. The
238
+ * failure mode is silent and inverted: adding a separator to catch more secrets
239
+ * stopped one already being caught.
240
+ */
241
+ /**
242
+ * The identifier is BOUNDED, and the bound is load-bearing rather than tidy.
243
+ *
244
+ * Unbounded (`[A-Za-z0-9_]*`), this is quadratic on a long unbroken token: the
245
+ * engine starts at every one of n positions, consumes the whole run, then
246
+ * backtracks a character at a time looking for the `=` or `:` that never comes.
247
+ * Measured on a plain 300 KB base64 blob — `"A".repeat(300000)` — it ran for
248
+ * over 20 SECONDS on one string. That is not an exotic input: agent transcripts
249
+ * carry base64 images, minified bundles and whole file contents as single
250
+ * lines, and the audit reads every one of them. A scheduled scan hitting a few
251
+ * of these would stall for minutes with nobody watching.
252
+ *
253
+ * 128 characters is far past any real environment-variable or field name, so
254
+ * bounding it costs nothing that exists and makes the scan linear in n.
154
255
  */
155
- const ASSIGNMENT_RE = /\b([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
256
+ const ASSIGNMENT_RE =
257
+ /(["']?)\b([A-Za-z_][A-Za-z0-9_]{0,127})\1([ \t]*(?::(?!\/\/)|=)[ \t]*)("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
156
258
 
157
259
  /** Credentials inline in a URL, on ANY scheme — `https://user:pass@host`. */
158
- const URL_CREDENTIALS_RE = /([a-z][a-z0-9+.\-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
260
+ // Every quantifier here is BOUNDED, for the same reason ASSIGNMENT_RE's is.
261
+ // Unbounded, `[a-z0-9+.\-]*` is quadratic on any long lowercase run: at each of
262
+ // n positions it consumes to the end hunting for a `://` that is not there,
263
+ // then backtracks a character at a time. Measured at 18 SECONDS on 100 KB of a
264
+ // single repeated letter — and this one runs over the WHOLE text, on every
265
+ // example the audit redacts. A URI scheme is at most a few characters (RFC 3986
266
+ // allows more, but nothing real uses it), and userinfo that runs past 256 bytes
267
+ // is not a credential anyone typed.
268
+ const URL_CREDENTIALS_RE = /([a-z][a-z0-9+.\-]{0,31}:\/\/)[^\s/:@]{1,256}:[^\s/@]{1,256}@/gi;
159
269
 
160
270
  /** `curl -u user:pass`, in both spellings and both separators. */
161
271
  const BASIC_AUTH_FLAG_RE = /((?:^|\s)(?:-u|--user)[\s=])\S+:\S+/g;
162
272
 
163
273
  /** True when an identifier's name says its value is a credential. */
274
+ /**
275
+ * Prefixes whose values a build tool INLINES INTO THE BROWSER BUNDLE.
276
+ *
277
+ * These are not conventionally public, they are mechanically public: Next.js,
278
+ * Vite, CRA, Expo, SvelteKit, Astro, Nuxt and Gatsby each substitute the value
279
+ * into the JavaScript they ship to every visitor. A credential named this way
280
+ * has already been published by the framework, to everyone, before failproofai
281
+ * ever saw it.
282
+ */
283
+ const CLIENT_BUNDLE_PREFIXES = [
284
+ "NEXT_PUBLIC",
285
+ "VITE",
286
+ "REACT_APP",
287
+ "EXPO_PUBLIC",
288
+ "NUXT_PUBLIC",
289
+ "GATSBY",
290
+ "STORYBOOK",
291
+ "PUBLIC",
292
+ ];
293
+
294
+ /** Markers that name a key as the publishable half of a pair. */
295
+ const PUBLISHED_MARKERS = /PUBLISHABLE|(^|_)PUBLIC_KEY$|(^|_)ANON_KEY$/;
296
+
297
+ /**
298
+ * True when the identifier says the value is meant to be public.
299
+ *
300
+ * `isSecretName` matches any component in `KEY`/`PASS`/`AUTH`/… — and EVERY
301
+ * publishable key on earth is named `*_KEY`. Measured before this existed, it
302
+ * returned true for all twelve of `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`,
303
+ * `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `VAPID_PUBLIC_KEY`, `PUBLISHABLE_KEY` and
304
+ * friends — including the ones carrying the literal word PUBLIC.
305
+ *
306
+ * Over-redaction is usually cheap: masking something that might be secret costs
307
+ * readability. This case is different, because we can PROVE the value is not a
308
+ * secret — so masking it reports a credential exposure that did not happen, and
309
+ * a security tool crying wolf about a browser-bundled config value is not a
310
+ * cheap error. This repo's own committed PostHog `phc_` key, documented in
311
+ * source as "Write-only (safe to commit)", is masked as an "assigned secret" by
312
+ * the rule this vetoes.
313
+ *
314
+ * Deliberately narrow. The marker must be a PREFIX (so a mid-name `PUBLIC` in
315
+ * `MY_PUBLIC_FACING_SECRET` does not disarm it) or an explicit publishable
316
+ * suffix. Words like ANON, BROWSER and SITE are excluded on their own — only
317
+ * `ANON_KEY`, which is Supabase's documented browser key, qualifies.
318
+ */
319
+ export function isPublishedByDesign(name: string): boolean {
320
+ const components = identifierComponents(name);
321
+ if (components.length === 0) return false;
322
+ const upper = components.join("_");
323
+ for (const prefix of CLIENT_BUNDLE_PREFIXES) {
324
+ if (upper === prefix || upper.startsWith(prefix + "_")) return true;
325
+ }
326
+ return PUBLISHED_MARKERS.test(upper);
327
+ }
328
+
329
+ /**
330
+ * How confidently the NAME alone says "credential".
331
+ *
332
+ * `strong` — the word means secret and nothing else: `DB_PASSWORD`,
333
+ * `STRIPE_SECRET`, `PRIVATE_KEY`. A short value here is still worth flagging;
334
+ * `hunter2` assigned to `PASSWORD` is a leaked password.
335
+ *
336
+ * `weak` — the word is ALSO ordinary programming vocabulary: `key`, `token`,
337
+ * `auth`, `sig`. Measured on a real 2,664-transcript machine these produced
338
+ * `keyType`, `tokenLimitCancelled`, `max_output_tokens`, `resultKey`,
339
+ * `configDirKey` — 394 findings from this layer, of which 389 had no vendor
340
+ * prefix and 227 were under 24 characters. Nothing that short is an API key.
341
+ * A weak name has to be backed by a value that actually looks like a secret.
342
+ */
343
+ export type SecretNameStrength = "strong" | "weak" | "none";
344
+
345
+ const STRONG_NAME_WORDS = ["SECRET", "PASSWORD", "PASSWD", "PASSPHRASE", "CREDENTIAL", "APIKEY", "PRIVATEKEY"];
346
+
347
+ export function secretNameStrength(name: string): SecretNameStrength {
348
+ if (!isSecretName(name)) return "none";
349
+ const upper = name.toUpperCase();
350
+ if (STRONG_NAME_WORDS.some((w) => upper.includes(w))) return "strong";
351
+ // `API_KEY` / `apiKey` split into components rather than matching APIKEY as a
352
+ // substring, and they are as unambiguous as the substring form.
353
+ const parts = identifierComponents(name);
354
+ if (parts.includes("API") && parts.includes("KEY")) return "strong";
355
+ return "weak";
356
+ }
357
+
164
358
  export function isSecretName(name: string): boolean {
359
+ // Runs first and beats every other signal: a value the framework compiles
360
+ // into the browser bundle is public no matter what the rest of the name says.
361
+ if (isPublishedByDesign(name)) return false;
165
362
  const upper = name.toUpperCase();
166
363
  if (SECRET_NAME_SUBSTRINGS.some((word) => upper.includes(word))) return true;
167
- return upper.split("_").some((part) => SECRET_NAME_COMPONENTS.includes(part));
364
+ const components = identifierComponents(name);
365
+ if (components.some((part) => SECRET_NAME_COMPONENTS.includes(part))) return true;
366
+ return components.length > 1 && components.some((part) => COMPOUND_ONLY_COMPONENTS.includes(part));
168
367
  }
169
368
 
170
369
  /**
@@ -193,15 +392,18 @@ export function isSecretName(name: string): boolean {
193
392
  * is the actionable half of the finding.
194
393
  */
195
394
  export function maskAssignedSecrets(input: string): string {
196
- let out = input.replace(ASSIGNMENT_RE, (match, name: string, value: string) => {
197
- if (!isSecretName(name)) return match;
198
- // An earlier pass already named this one, and it named it better.
199
- // `export ANTHROPIC_API_KEY=sk-ant-…` is masked by the vendor pattern as
200
- // "Anthropic API key"; re-masking it here would downgrade that to the
201
- // generic label and strip the marker's own tail as it went.
202
- if (value.startsWith("[REDACTED")) return match;
203
- return `${name}=[REDACTED: assigned secret]`;
204
- });
395
+ let out = input.replace(
396
+ ASSIGNMENT_RE,
397
+ (match, quote: string, name: string, separator: string, value: string) => {
398
+ if (!isSecretName(name)) return match;
399
+ // An earlier pass already named this one, and it named it better.
400
+ // `export ANTHROPIC_API_KEY=sk-ant-…` is masked by the vendor pattern as
401
+ // "Anthropic API key"; re-masking it here would downgrade that to the
402
+ // generic label and strip the marker's own tail as it went.
403
+ if (value.startsWith("[REDACTED")) return match;
404
+ return `${quote}${name}${quote}${separator}[REDACTED: assigned secret]`;
405
+ },
406
+ );
205
407
  out = out.replace(URL_CREDENTIALS_RE, "$1[REDACTED: URL credentials]@");
206
408
  out = out.replace(BASIC_AUTH_FLAG_RE, "$1[REDACTED: basic auth]");
207
409
  return out;
@@ -298,15 +500,120 @@ export function shortenPaths(input: string, home = homedir()): string {
298
500
  * digest as one row, and a raw newline there breaks the plain-text layout while
299
501
  * saying nothing the single line does not.
300
502
  */
503
+ /**
504
+ * Mask every secret, and nothing else.
505
+ *
506
+ * The three masking passes of `redactExample` without the path shortening, the
507
+ * whitespace collapse or the length cap. This is what a LOCAL renderer wants:
508
+ * on your own machine your own paths are the useful half of an example, and
509
+ * `~/…/db.ts` costs readability for no gain — nobody is protected from their own
510
+ * directory names. A credential on screen is the other half of that trade: it is
511
+ * one screenshot, one pasted issue or one screen-share away from being published,
512
+ * and unlike the path it can never be un-leaked.
513
+ *
514
+ * So the split is deliberate: everything that LEAVES the machine goes through
515
+ * `redactExample`; the terminal gets this.
516
+ */
517
+ export function maskSecretsOnly(input: string): string {
518
+ return maskAssignedSecrets(maskTruncatedSecret(maskSecrets(input)));
519
+ }
520
+
301
521
  export function redactExample(input: string, home = homedir()): string {
302
522
  // Assignment masking runs LAST of the three, so the two pattern-based passes
303
523
  // get first refusal on anything they can name precisely. A vendor prefix
304
524
  // yields "[REDACTED: Anthropic API key]"; falling through to this one would
305
525
  // have said only "assigned secret", which is true but less useful to read.
306
- const masked = maskAssignedSecrets(maskTruncatedSecret(maskSecrets(input)));
526
+ const masked = maskSecretsOnly(input);
307
527
  const shortened = shortenPaths(masked, home);
308
528
  const collapsed = shortened.replace(/\s+/g, " ").trim();
309
529
  return collapsed.length > REDACTED_EXAMPLE_MAX_CHARS
310
530
  ? `${collapsed.slice(0, REDACTED_EXAMPLE_MAX_CHARS - 1)}…`
311
531
  : collapsed;
312
532
  }
533
+
534
+ /**
535
+ * Redact every free-text field of an `AuditResult` that could carry a secret or
536
+ * name someone's disk.
537
+ *
538
+ * The counts, titles, timestamps and policy names are the substance of a report
539
+ * and none of them come from user data. What DOES come from user data is the
540
+ * example strings (slices of real commands), the per-example `cwd`, and the
541
+ * project lists — which are the same three things `redactExample` was written
542
+ * for, applied to the whole structure instead of one row.
543
+ *
544
+ * Used by every renderer that produces an artifact which can travel:
545
+ * `formatMarkdown` (the file the CLI calls a "Shareable report") and
546
+ * `formatJson` (whatever the caller pipes it into). The emailed digest reaches
547
+ * the same guarantee by a different route — `harm-report.ts` redacts each
548
+ * example as it selects it, because it also has to apply the reporting window.
549
+ *
550
+ * Returns a NEW object. The caller's result is left alone, so the dashboard and
551
+ * the cache keep the unredacted values they need to render a local view.
552
+ */
553
+ export function redactAuditResult(result: AuditResult, home = homedir()): AuditResult {
554
+ // Built field by field ON PURPOSE. This was `{...result}` plus three named
555
+ // rewrites, which meant it was a FIELD ALLOWLIST pretending to be a redactor:
556
+ // every field it did not name — including any field added later — passed
557
+ // through byte-identical, absolute home paths and all. It guards
558
+ // `formatJson`, i.e. an artifact the product invites the user to share.
559
+ //
560
+ // Enumerating every key means TypeScript rejects the build when `AuditResult`
561
+ // grows a field nobody decided about here, which is the only mechanism that
562
+ // survives a future contributor. `redactAuditResultKeys` in the tests reflects
563
+ // over the real object and fails if this list drifts, covering the case where
564
+ // a field is added as optional and the compiler stays quiet.
565
+ const redacted: AuditResult = {
566
+ // Scalars and counters: no paths, no user content, nothing to redact.
567
+ version: result.version,
568
+ scannedAt: result.scannedAt,
569
+ transcripts: result.transcripts,
570
+ totals: result.totals,
571
+ eventsScanned: result.eventsScanned,
572
+ enabledBuiltinNames: result.enabledBuiltinNames,
573
+ // Salted, machine-local ids. They carry no path and cannot be reversed to
574
+ // a credential, so they pass through — but the decision is recorded here
575
+ // rather than inherited from a spread.
576
+ newLeakIds: result.newLeakIds,
577
+
578
+ // Everything below carries a path or user content and is rewritten.
579
+ scope: {
580
+ cli: result.scope.cli,
581
+ since: result.scope.since,
582
+ projects: result.scope.projects === "all"
583
+ ? "all"
584
+ : result.scope.projects.map((p) => shortenPaths(p, home)),
585
+ },
586
+ results: result.results.map((row) => ({
587
+ ...row,
588
+ examples: row.examples.map((e) => ({
589
+ ...e,
590
+ example: redactExample(e.example, home),
591
+ cwd: shortenPaths(e.cwd, home),
592
+ })),
593
+ })),
594
+ projectsScanned: result.projectsScanned.map((p) => shortenPaths(p, home)),
595
+ };
596
+ return redacted;
597
+ }
598
+
599
+ /**
600
+ * Every top-level key `redactAuditResult` has consciously handled.
601
+ *
602
+ * The compiler catches a REQUIRED field added to `AuditResult`, because the
603
+ * object literal above would stop satisfying the type. It does not catch an
604
+ * OPTIONAL one — and the leak record is arriving as optional fields. So the
605
+ * test reflects over a real result and fails when a key appears here that is
606
+ * not in this list, which forces the decision to be made rather than defaulted.
607
+ */
608
+ export const REDACTED_AUDIT_RESULT_KEYS: ReadonlyArray<keyof AuditResult> = [
609
+ "version",
610
+ "scannedAt",
611
+ "scope",
612
+ "transcripts",
613
+ "results",
614
+ "totals",
615
+ "projectsScanned",
616
+ "eventsScanned",
617
+ "enabledBuiltinNames",
618
+ "newLeakIds",
619
+ ];
@@ -31,6 +31,7 @@ import { getValidAccessToken } from "../../lib/auth/auth-store";
31
31
  import { AuthApiError, submitAuditReport } from "../../lib/auth/api-server-client";
32
32
  import { readConfig } from "../hooks/fp-config";
33
33
  import { buildHarmReport } from "./harm-report";
34
+ import { activeFindings, readLeakRecord } from "./leak-store";
34
35
  import {
35
36
  ensureMachineIdentity,
36
37
  machineLabel,
@@ -101,7 +102,16 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
101
102
  return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
102
103
  }
103
104
 
104
- const report = buildHarmReport(result, identity.last_reported_at, intervalDays);
105
+ // The leak record is read here rather than carried on the AuditResult: the
106
+ // digest wants every credential still live in this window, not only the ones
107
+ // this particular scan happened to re-encounter. A key that leaked last month
108
+ // and was not touched by today's transcripts is still a key to rotate.
109
+ const report = buildHarmReport(
110
+ result,
111
+ identity.last_reported_at,
112
+ intervalDays,
113
+ activeFindings(readLeakRecord()),
114
+ );
105
115
  const hits = report.harmful.reduce((n, p) => n + p.hits, 0);
106
116
 
107
117
  try {
@@ -112,6 +122,7 @@ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome
112
122
  window_from: report.window_from,
113
123
  window_to: report.window_to,
114
124
  harmful: report.harmful,
125
+ leaks: report.leaks,
115
126
  });
116
127
 
117
128
  // Persist whatever the server says the next window starts at, INCLUDING when
@@ -10,6 +10,7 @@
10
10
  * install command + report path + star link.
11
11
  */
12
12
  import type { AuditCount, AuditResult, RunAuditOptions } from "./types";
13
+ import { maskSecretsOnly, redactAuditResult, redactExample, shortenPaths } from "./redact-example";
13
14
 
14
15
  const ANSI = {
15
16
  reset: "\x1B[0m",
@@ -105,7 +106,7 @@ function renderRow(r: AuditCount, opts: { showExamples?: boolean }): string[] {
105
106
  ` ${ANSI.dim}Last seen ${formatTimeAgo(r.lastSeen)} · ${r.projects} project${r.projects === 1 ? "" : "s"}${ANSI.reset}`,
106
107
  );
107
108
  if (opts.showExamples && r.examples[0]) {
108
- out.push(` ${ANSI.dim}Example: ${r.examples[0].example}${ANSI.reset}`);
109
+ out.push(` ${ANSI.dim}Example: ${maskSecretsOnly(r.examples[0].example)}${ANSI.reset}`);
109
110
  }
110
111
  if (r.installHint) {
111
112
  const arrowColor = r.enabledInConfig ? ANSI.green : ANSI.cyan;
@@ -226,7 +227,7 @@ export function formatText(result: AuditResult, opts: RunAuditOptions = {}): str
226
227
  }
227
228
 
228
229
  export function formatJson(result: AuditResult): string {
229
- return JSON.stringify(result, null, 2);
230
+ return JSON.stringify(redactAuditResult(result), null, 2);
230
231
  }
231
232
 
232
233
  /** Escape characters that would break a markdown table row. Pipes split
@@ -334,7 +335,9 @@ export function formatMarkdown(result: AuditResult): string {
334
335
  out.push("");
335
336
  }
336
337
  for (const e of r.examples) {
337
- out.push(`- \`${escapeBackticks(e.example)}\` _(${e.cwd || "?"}, ${formatTimeAgo(e.timestamp)})_`);
338
+ out.push(
339
+ `- \`${escapeBackticks(redactExample(e.example))}\` _(${shortenPaths(e.cwd) || "?"}, ${formatTimeAgo(e.timestamp)})_`,
340
+ );
338
341
  }
339
342
  out.push("");
340
343
  }
@@ -219,6 +219,14 @@ export function runScheduleStatus(): void {
219
219
  // so it is a destination in name only. Showing the address for one would tell
220
220
  // somebody their digests are going somewhere they are not.
221
221
  const live = auth && auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
222
+ // Beside the schedule, because they are the two halves of "what does this
223
+ // machine do on its own" — one decides whether it scans, the other whether it
224
+ // is allowed to interrupt you about what it found.
225
+ detail.push([
226
+ "leak notifications",
227
+ config.audit.notify ? green("on") : dim("off (failproofai audit --notify)"),
228
+ ]);
229
+
222
230
  detail.push(["reports to", live ? live.user.email : dim("— signed out")]);
223
231
  if (on && !live) {
224
232
  // The state the reporter surfaces as "signed-out". Named here for the same
@@ -346,3 +354,32 @@ export function asScheduleError(err: unknown): never {
346
354
  }
347
355
  throw err;
348
356
  }
357
+
358
+ /**
359
+ * Turn the desktop banner on or off.
360
+ *
361
+ * A separate command from `--schedule` rather than a flag on it, because they
362
+ * are separate decisions with different lifetimes: somebody who wants the
363
+ * weekly scan and not the banner should not have to re-state their schedule to
364
+ * say so, and somebody silencing the banner in an annoyed moment should not
365
+ * discover they also turned off the scan.
366
+ *
367
+ * Writes the same `audit.notify` key the dashboard toggle writes and the audit
368
+ * child reads, so the two surfaces cannot disagree.
369
+ */
370
+ export function runNotifyToggle(enable: boolean): void {
371
+ try {
372
+ updateConfig({ audit: { notify: enable } });
373
+ } catch (err) {
374
+ process.stderr.write(
375
+ `Could not update the setting: ${err instanceof Error ? err.message : String(err)}\n`,
376
+ );
377
+ process.exit(1);
378
+ }
379
+ process.stdout.write(
380
+ enable
381
+ ? `\n${green("on")} leak notifications — this machine will tell you when a scan finds a credential.\n\n`
382
+ : `\n${dim("off")} leak notifications — scans continue, and findings still appear in ` +
383
+ `\`failproofai audit\` and the emailed digest.\n\n`,
384
+ );
385
+ }
@@ -1,6 +1,55 @@
1
1
  /**
2
2
  * Score derivation for the audit dashboard.
3
3
  *
4
+ * ## SWITCHED OFF — nothing in the product calls any of this (2026-09-07)
5
+ *
6
+ * Every function below is intact and still covered by
7
+ * `__tests__/audit/scoring.test.ts`, deliberately: the score is commented out at
8
+ * its call sites, not deleted, so restoring it is un-commenting rather than
9
+ * rewriting. The call sites, all commented and cross-referenced back to here:
10
+ * `audit-dashboard.tsx` (the four `useMemo`s, the poster/how-to-improve/
11
+ * come-back-better props, and the `score`/`grade` PostHog properties),
12
+ * `audit-poster.tsx` (the `.poster-score` block, the PNG filename, the share
13
+ * payload), `how-to-improve-section.tsx` (the projected sub-heading),
14
+ * `share-templates.ts` (19 of 20 templates interpolated the number).
15
+ *
16
+ * ### Why
17
+ *
18
+ * Two reasons, one product and one correctness.
19
+ *
20
+ * The product reason is the owner's: the audit tried to do too many things
21
+ * shallowly, and grading a machine 0-100 with a letter tier was not the half
22
+ * worth keeping.
23
+ *
24
+ * The correctness reason is worse and is the one to read before switching it
25
+ * back on. **`deriveScore` never reads `enabledInConfig`** — only
26
+ * `projectedScore` does, at the filter below. So the number the dashboard
27
+ * rendered as "enable all N → projected {score}" was undeliverable by
28
+ * construction: a user who took the prescription, enabled the policies and
29
+ * re-ran got the SAME score with the projection gone. Advertised up to +22,
30
+ * delivered 0. There is also no time window, so a hit denied today is counted
31
+ * against the machine forever.
32
+ *
33
+ * Two further things measured at the same time, neither of them the score's
34
+ * fault but both worth knowing if this comes back:
35
+ *
36
+ * - `penaltyFor` consumes `row.hits`, i.e. OCCURRENCES. Occurrences amplify
37
+ * against distinct values by 10.7x-41x and non-uniformly by class, so the
38
+ * sum does not preserve rank order between classes. Fixing that needs a
39
+ * per-distinct-value carrier, which `AuditCount` does not have.
40
+ * - `syntheticRank`, `scoreToPercentile` and `COHORT_SIZE` had already lost
41
+ * their last caller before this change — the cohort they describe does not
42
+ * exist. They are dead either way.
43
+ *
44
+ * ### To restore
45
+ *
46
+ * Un-comment the call sites listed above (each carries its original inline) and
47
+ * put the score-bearing share copy back from the block at the bottom of
48
+ * `share-templates.ts`. Make `score`/`grade` required again in `ShareCtx` and in
49
+ * `audit-poster.tsx`'s `Props`, and the compiler will walk you through the rest.
50
+ * Fix the `enabledInConfig` defect first, or it will be advertising the same
51
+ * undeliverable number.
52
+ *
4
53
  * Score is on 0-100, mapped to letter grades that anchor the leaderboard
5
54
  * + tier prose. The thresholds match the reference design (assets/audit):
6
55
  *
@@ -45,6 +45,9 @@ export interface TranscriptMetadata {
45
45
  mtimeMs: number;
46
46
  /** Byte size of the transcript file. Used for cache invalidation. */
47
47
  sizeBytes: number;
48
+ /** Optional host-authored session purpose. Used only to distinguish
49
+ * deliberate credential-scanner research from evidence of a leak. */
50
+ sessionDescription?: string;
48
51
  }
49
52
 
50
53
  /** Per-session detector state. Detectors mutate this freely. */
@@ -119,6 +122,27 @@ export interface AuditCount {
119
122
  }
120
123
 
121
124
  /** Per-transcript scan result (also the per-transcript cache value). */
125
+ /** One credential sighting, already fingerprinted. */
126
+ export interface TranscriptLeak {
127
+ /** Salted, machine-local id for the DISTINCT value. */
128
+ id: string;
129
+ /** Masked rendering + class + length. Never the value. */
130
+ fingerprint: import("./leak-fingerprint").LeakFingerprint;
131
+ /** Identifier it was assigned to, when it came from an assignment. */
132
+ name: string | null;
133
+ /** Rule that found it. */
134
+ rule: string;
135
+ /** Whether a vendor shape matched — gates whether an alert may name a console. */
136
+ shaped: boolean;
137
+ timestamp: string;
138
+ cwd: string;
139
+ toolName: string;
140
+ /** Sent by the agent, or received by it. Different exposures, different fixes. */
141
+ direction: "input" | "result";
142
+ /** Home-shortened path the value was read from or written to, when there was one. */
143
+ path: string | null;
144
+ }
145
+
122
146
  export interface TranscriptAuditResult {
123
147
  /** Cache key: matches TranscriptMetadata.transcriptPath. */
124
148
  transcriptPath: string;
@@ -140,6 +164,20 @@ export interface TranscriptAuditResult {
140
164
  hitsByName: Record<string, number>;
141
165
  /** Up to 3 example commands per policy/detector (later coalesced upstream). */
142
166
  examplesByName: Record<string, { timestamp: string; cwd: string; example: string }[]>;
167
+ /**
168
+ * Credentials found in this transcript, one entry per SIGHTING.
169
+ *
170
+ * Deliberately separate from `hitsByName`, which counts by RULE and cannot
171
+ * enumerate distinct values. These carry the fingerprint rather than the
172
+ * credential — the raw value never leaves `scanOneTranscript`.
173
+ *
174
+ * Optional so a cached result written before this field existed still parses;
175
+ * absent reads as "this transcript predates leak scanning", not "clean".
176
+ */
177
+ leaks?: TranscriptLeak[];
178
+ /** The session was explicitly generating/researching credential fixtures,
179
+ * so its matches are examples rather than evidence of user leakage. */
180
+ leakScanSuppressed?: "credential-research";
143
181
  /** First/last timestamp per name. */
144
182
  rangeByName: Record<string, { first: string; last: string }>;
145
183
  }
@@ -179,6 +217,15 @@ export interface AuditResult {
179
217
  * Lets the dashboard answer "is this policy already on?" for
180
218
  * detector-mapped policies that may not have hit during this audit. */
181
219
  enabledBuiltinNames: string[];
220
+ /**
221
+ * Ids of credentials seen for the FIRST time in this scan.
222
+ *
223
+ * What a notice keys on. Not "findings this scan saw" — a key already in the
224
+ * record must not re-alert however many fresh sightings it picks up, and one
225
+ * seen for the first time must alert even though its rule has fired before.
226
+ * Optional so a cached result predating leak scanning still parses.
227
+ */
228
+ newLeakIds?: string[];
182
229
  }
183
230
 
184
231
  /** CLI-supplied options for `runAudit()`. Set by `bin/failproofai.mjs`. */