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
@@ -16,6 +16,11 @@ import { INTEGRATION_TYPES, type IntegrationType } from "../hooks/types";
16
16
  import { ADAPTERS } from "./cli-adapters";
17
17
  import { AUDIT_DETECTORS } from "./detectors";
18
18
  import { severityForBuiltin } from "./features";
19
+ import { findSecrets, flattenToolInput } from "./leak-scan";
20
+ import { fingerprintSecret, fingerprintId } from "./leak-fingerprint";
21
+ import { readLeakIdentity, readLeakRecord, writeLeakRecord } from "./leak-store";
22
+ import { upsertFinding, describeMechanism, emptyRecord } from "./leak-record";
23
+ import { shortenPaths } from "./redact-example";
19
24
  import { readCachedTranscript, writeCachedTranscriptResult } from "./cache";
20
25
  import { initReplay, replayEvent, restoreReplay } from "./replay";
21
26
  import {
@@ -32,6 +37,64 @@ import {
32
37
 
33
38
  const TRANSCRIPT_CONCURRENCY = 8;
34
39
 
40
+ /**
41
+ * Raw transcript bytes allowed in flight at once.
42
+ *
43
+ * The concurrency above counts FILES, which is the wrong unit. `streamEventsFrom`
44
+ * returns an array — despite the name it materialises every event of a
45
+ * transcript into JS objects — and a 57 MB JSONL becomes several hundred MB of
46
+ * them. On a real 1.15 GB corpus, 7 files hold 224 MB and the largest is
47
+ * 57.5 MB, so eight workers could put a quarter of the corpus in memory
48
+ * simultaneously. That machine printed `Aborted(OOM)` 22 times in one run.
49
+ *
50
+ * This is NOT what fixed those lines, and the distinction is worth keeping
51
+ * honest: they were measured at ~22 per run, appear within the first two
52
+ * seconds, are unchanged by this budget at 48 MB or at 16 MB, and do not fail
53
+ * the run — its counts come back correct. Their source is still unidentified.
54
+ * This bounds a real and separate failure mode (eight of the seven largest
55
+ * files in flight at once); it should not be read as having fixed the other.
56
+ *
57
+ * 48 MB of raw input is the budget because the expansion factor from JSONL to
58
+ * parsed objects is roughly 5-10x, which keeps the peak inside a default heap
59
+ * with room for the eight small-file workers this is meant not to slow down.
60
+ * Chosen to bound memory, not to be exactly right: the failure it prevents is a
61
+ * crash, and the cost of being conservative is that one very large file scans
62
+ * alone for a moment.
63
+ */
64
+ const TRANSCRIPT_BYTE_BUDGET = 48 * 1024 * 1024;
65
+
66
+ /**
67
+ * Admission control by weight, so a worker waits for MEMORY as well as a slot.
68
+ *
69
+ * A file bigger than the whole budget is admitted alone rather than refused —
70
+ * otherwise the largest transcript on the machine could never be scanned, which
71
+ * is precisely the one most likely to hold something.
72
+ */
73
+ class ByteGate {
74
+ private inFlight = 0;
75
+ private waiting: Array<() => void> = [];
76
+
77
+ constructor(private readonly budget: number) {}
78
+
79
+ async acquire(bytes: number): Promise<void> {
80
+ const want = Math.max(0, bytes);
81
+ while (this.inFlight > 0 && this.inFlight + want > this.budget) {
82
+ await new Promise<void>((resolve) => this.waiting.push(resolve));
83
+ }
84
+ this.inFlight += want;
85
+ }
86
+
87
+ release(bytes: number): void {
88
+ this.inFlight -= Math.max(0, bytes);
89
+ if (this.inFlight < 0) this.inFlight = 0;
90
+ // Wake everyone and let each re-test its own weight: a small task behind a
91
+ // large one should not be held by a queue position it does not need.
92
+ const waiters = this.waiting;
93
+ this.waiting = [];
94
+ for (const wake of waiters) wake();
95
+ }
96
+ }
97
+
35
98
  /** Canonicalize a policy name to its short, qualified form for display
36
99
  * (`failproofai/foo` → `foo`). */
37
100
  function shortPolicyName(name: string): string {
@@ -113,6 +176,50 @@ interface ScanOutcome {
113
176
  resumed: boolean;
114
177
  }
115
178
 
179
+ const CREDENTIAL_RESEARCH_MARKERS: ReadonlyArray<RegExp> = [
180
+ /\b(?:secret|credential)[-_ ](?:scanner|detection|detector|pattern|regex|corpus)\b/i,
181
+ /\b(?:trufflehog|trufflesecurity|gitleaks|detect-secrets|secret-detection-rules)\b/i,
182
+ /\b(?:SECRET_PATTERNS|findSecrets|isDocsLiteral|leak-scan)\b/,
183
+ /\b(?:fixture|placeholder|synthetic|sample) (?:api )?(?:key|token|secret|credential)s?\b/i,
184
+ /\b(?:grep|rg|ripgrep|scan|hunt|search)\b[^\n]{0,240}\b(?:secret|credential|token)s?\b/i,
185
+ ];
186
+
187
+ const CREDENTIAL_RESEARCH_PURPOSE =
188
+ /\b(?:hunt|scan|search|research|test)(?:ing)?\b[^\n]{0,80}\b(?:secret|credential|token)s?\b|\b(?:secret|credential|token)s?\b[^\n]{0,80}\b(?:scanner|detector|detection|research|fixtures?|corpus)\b/i;
189
+
190
+ /**
191
+ * Deliberate secret-detector research is full of realistic fake credentials.
192
+ * Reporting those as leaks is the feedback loop that made one measured session
193
+ * contribute 455 of the 500 rows in its own audit.
194
+ *
195
+ * Require independent signals across tool INPUTS. Results are excluded from
196
+ * classification: a normal command can print hostile text, while commands and
197
+ * URLs state what the agent intentionally set out to inspect. Two signals keep
198
+ * an ordinary discussion that happens to say "secret scanner" from silencing a
199
+ * session; dedicated detector/test work reliably carries several.
200
+ */
201
+ export function isCredentialResearchSession(
202
+ events: NormalizedToolEvent[],
203
+ sessionDescription?: string,
204
+ ): boolean {
205
+ if (sessionDescription && CREDENTIAL_RESEARCH_PURPOSE.test(sessionDescription)) return true;
206
+ const matched = new Set<number>();
207
+ let evidenceEvents = 0;
208
+ for (const event of events) {
209
+ const intent = flattenToolInput(event.toolInput);
210
+ let eventMatched = false;
211
+ for (let i = 0; i < CREDENTIAL_RESEARCH_MARKERS.length; i++) {
212
+ if (CREDENTIAL_RESEARCH_MARKERS[i].test(intent)) {
213
+ matched.add(i);
214
+ eventMatched = true;
215
+ }
216
+ }
217
+ if (eventMatched) evidenceEvents++;
218
+ if (matched.size >= 2 || evidenceEvents >= 3) return true;
219
+ }
220
+ return false;
221
+ }
222
+
116
223
  async function scanOneTranscript(
117
224
  meta: TranscriptMetadata,
118
225
  resume?: { fromByte: number; detectorState: DetectorSessionState },
@@ -181,21 +288,27 @@ async function scanOneTranscript(
181
288
  // Capture the session's cwd from the first event that carried one — every
182
289
  // event in a single transcript shares the same cwd by construction.
183
290
  result.cwd = result.cwd || events[0].cwd || "";
291
+ const suppressLeakScan = isCredentialResearchSession(events, meta.sessionDescription);
292
+ if (suppressLeakScan) result.leakScanSuppressed = "credential-research";
184
293
 
185
294
  for (const event of events) {
186
- // Run audit detectors first (stateful, must see every event).
187
- for (const detector of AUDIT_DETECTORS) {
188
- const hit = detector.detect(event, sessionState);
189
- if (!hit) continue;
190
- recordHit(
191
- result,
192
- detector.name,
193
- event.timestamp,
194
- event.cwd,
195
- truncateExample(hit.example),
196
- );
197
- }
198
- // Then replay through every builtin policy.
295
+ // The 8 behavioural detectors are switched off with the rest of the old
296
+ // audit — see the header of `src/audit/scoring.ts`. The modules and their
297
+ // unit tests are untouched; nothing calls them. `sessionState` is still
298
+ // threaded through so a resumed scan keeps its shape and restoring this
299
+ // loop needs no plumbing.
300
+ // for (const detector of AUDIT_DETECTORS) {
301
+ // const hit = detector.detect(event, sessionState);
302
+ // if (!hit) continue;
303
+ // recordHit(
304
+ // result,
305
+ // detector.name,
306
+ // event.timestamp,
307
+ // event.cwd,
308
+ // truncateExample(hit.example),
309
+ // );
310
+ // }
311
+ // Replay through every builtin policy.
199
312
  let replayHits;
200
313
  try {
201
314
  replayHits = await replayEvent(event);
@@ -212,11 +325,129 @@ async function scanOneTranscript(
212
325
  truncateExample(example),
213
326
  );
214
327
  }
328
+
329
+ // Credential VALUES, which the replay above cannot report: it returns a
330
+ // decision per policy, never the text that matched. Scanned separately so
331
+ // the leak report can name WHICH key, and so detection can be tuned for
332
+ // precision without dragging the redactor's recall down with it.
333
+ if (!suppressLeakScan) recordLeaks(result, event);
215
334
  }
216
335
 
217
336
  return { result, bytesScanned, detectorState: sessionState, resumed };
218
337
  }
219
338
 
339
+ /**
340
+ * Fingerprint every credential in one event and attach the sightings.
341
+ *
342
+ * The raw value exists only inside this function. It is read out of the event,
343
+ * hashed into a salted id, rendered into a mask, and dropped — nothing that
344
+ * leaves here can reconstruct it.
345
+ *
346
+ * Input and result are scanned separately and tagged, because they are
347
+ * different exposures: an input is what the agent SENT (deniable at PreToolUse
348
+ * on 12/12 CLIs, and 39.5% of measured appearances) while a result is what it
349
+ * RECEIVED (blockable on 2/12, and only after the fact).
350
+ */
351
+ function recordLeaks(result: TranscriptAuditResult, event: NormalizedToolEvent): void {
352
+ const salt = leakSalt();
353
+ if (!salt) return;
354
+
355
+ const filePath = (event.toolInput as { file_path?: unknown }).file_path;
356
+ const shortPath = typeof filePath === "string" ? shortenPaths(filePath) : null;
357
+
358
+ const scan = (text: string, direction: "input" | "result"): void => {
359
+ for (const match of findSecrets(text)) {
360
+ (result.leaks ??= []).push({
361
+ id: fingerprintId(match.value, salt),
362
+ fingerprint: fingerprintSecret(match.value, match.rule),
363
+ name: match.name,
364
+ rule: match.rule,
365
+ shaped: match.shaped,
366
+ timestamp: event.timestamp,
367
+ cwd: event.cwd,
368
+ toolName: event.toolName,
369
+ direction,
370
+ path: shortPath,
371
+ });
372
+ }
373
+ };
374
+
375
+ scan(flattenToolInput(event.toolInput), "input");
376
+ if (event.toolResultText !== undefined) scan(event.toolResultText, "result");
377
+ }
378
+
379
+ /**
380
+ * The machine's HMAC salt, read once per process.
381
+ *
382
+ * Cached because `scanOneTranscript` runs this per EVENT — 340,918 of them on
383
+ * the measured corpus — and re-reading a file that many times would dominate
384
+ * the scan. `null` when the store is unreadable, which disables leak recording
385
+ * rather than failing the audit: a scan that cannot fingerprint is still a scan
386
+ * that can count.
387
+ */
388
+ let cachedSalt: string | null | undefined;
389
+ function leakSalt(): string | null {
390
+ if (cachedSalt === undefined) {
391
+ try {
392
+ cachedSalt = readLeakIdentity().salt;
393
+ } catch {
394
+ cachedSalt = null;
395
+ }
396
+ }
397
+ return cachedSalt;
398
+ }
399
+
400
+ /**
401
+ * Merge this scan's sightings into `~/.failproofai/audit/leaks.json`.
402
+ *
403
+ * Returns the ids seen for the FIRST time, which is the set worth telling a
404
+ * human about. Never throws: a scan that finds leaks it cannot write down has
405
+ * still found them, and the caller reports on the in-memory result either way.
406
+ */
407
+ export function persistLeaks(perTranscript: TranscriptAuditResult[], replaceExisting: boolean): string[] {
408
+ const fresh: string[] = [];
409
+ try {
410
+ const previous = readLeakRecord();
411
+ const previousIds = new Set(previous.findings.map((f) => f.id));
412
+ // A successful all-history scan is authoritative. Rebuilding is what lets
413
+ // detector precision fixes remove findings they now reject; merging made
414
+ // every false positive live for 90 days even after the code was fixed.
415
+ // Scoped or incomplete scans still merge because absence outside their
416
+ // coverage says nothing.
417
+ const record = replaceExisting
418
+ ? emptyRecord(previous.salt, new Date().toISOString())
419
+ : previous;
420
+ for (const t of perTranscript) {
421
+ for (const leak of t.leaks ?? []) {
422
+ const { isNew } = upsertFinding(record, {
423
+ id: leak.id,
424
+ fingerprint: leak.fingerprint,
425
+ name: leak.name,
426
+ rule: leak.rule,
427
+ // A vendor shape is what licenses an alert to name a console; a
428
+ // name-only match knows something leaked, not who issued it.
429
+ confidence: leak.shaped ? "doc-verified" : "name-only",
430
+ sighting: {
431
+ cli: t.cli,
432
+ sessionId: t.sessionId,
433
+ cwd: shortenPaths(leak.cwd),
434
+ at: leak.timestamp,
435
+ mechanism: describeMechanism(leak.toolName, leak.direction, leak.path),
436
+ },
437
+ });
438
+ // During an authoritative rebuild every insertion is new to the empty
439
+ // record, but only ids absent from the previous record are new to the
440
+ // HUMAN and eligible for a notification.
441
+ if (isNew && !previousIds.has(leak.id)) fresh.push(leak.id);
442
+ }
443
+ }
444
+ writeLeakRecord(record);
445
+ } catch {
446
+ // Bookkeeping failure must not fail the audit — see leak-store.ts.
447
+ }
448
+ return fresh;
449
+ }
450
+
220
451
  function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): string {
221
452
  if (event.toolName === "Bash") {
222
453
  const command = (event.toolInput as { command?: unknown }).command;
@@ -236,7 +467,7 @@ function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): s
236
467
  * came from the FIRST event in the file and the tail's came from the first
237
468
  * event after the offset, so the older one wins.
238
469
  */
239
- function mergeIncremental(
470
+ export function mergeIncremental(
240
471
  cached: TranscriptAuditResult,
241
472
  tail: TranscriptAuditResult,
242
473
  ): TranscriptAuditResult {
@@ -250,6 +481,12 @@ function mergeIncremental(
250
481
  examplesByName: {},
251
482
  rangeByName: { ...cached.rangeByName },
252
483
  };
484
+ if (cached.leakScanSuppressed || tail.leakScanSuppressed) {
485
+ out.leakScanSuppressed = "credential-research";
486
+ out.leaks = [];
487
+ } else {
488
+ out.leaks = [...(cached.leaks ?? []), ...(tail.leaks ?? [])];
489
+ }
253
490
  for (const [name, list] of Object.entries(cached.examplesByName)) {
254
491
  out.examplesByName[name] = [...list];
255
492
  }
@@ -432,18 +669,22 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
432
669
 
433
670
  // 1. Discover transcripts across all selected CLIs.
434
671
  const allTranscripts: TranscriptMetadata[] = [];
672
+ let discoveryErrors = 0;
435
673
  for (const cli of clis) {
436
674
  const adapter = ADAPTERS[cli];
437
675
  let list: TranscriptMetadata[];
438
676
  try {
439
677
  list = await adapter.listTranscripts({ projects: opts.projects, sinceMs });
440
678
  } catch {
679
+ discoveryErrors++;
441
680
  continue; // adapter failures shouldn't kill the whole audit
442
681
  }
443
682
  allTranscripts.push(...list);
444
683
  }
445
684
 
446
- // 2. Scan each transcript (cache-aware), 8 in parallel.
685
+ // 2. Scan each transcript (cache-aware), 8 at a time and within a memory
686
+ // budget — see TRANSCRIPT_BYTE_BUDGET for why a file count is the wrong unit.
687
+ const gate = new ByteGate(TRANSCRIPT_BYTE_BUDGET);
447
688
  let skipped = 0;
448
689
  let errors = 0;
449
690
  const tasks = allTranscripts.map((meta) => async (): Promise<TranscriptAuditResult> => {
@@ -457,6 +698,9 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
457
698
  : undefined;
458
699
  cachedPrefix = found?.kind === "resume" ? found.result : null;
459
700
  }
701
+ // Memory admission. Taken AFTER the cache check above, so a cache hit — the
702
+ // common case on a warm machine — never waits for a slot it does not use.
703
+ await gate.acquire(meta.sizeBytes);
460
704
  try {
461
705
  const scan = await scanOneTranscript(meta, resume);
462
706
  // A resumed scan produced hits for the TAIL only; the cached result holds
@@ -500,6 +744,11 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
500
744
  examplesByName: {},
501
745
  rangeByName: {},
502
746
  };
747
+ } finally {
748
+ // Released on every path, including the error one — a task that threw
749
+ // still freed its memory, and holding its weight would shrink the budget
750
+ // permanently over a long scan until nothing could be admitted at all.
751
+ gate.release(meta.sizeBytes);
503
752
  }
504
753
  });
505
754
 
@@ -527,8 +776,28 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
527
776
  eventsScanned += t.eventsScanned ?? 0;
528
777
  }
529
778
 
779
+ // Fold every sighting into the persistent record. This is the ONLY place the
780
+ // leak record is written: `scanOneTranscript` runs 8-way concurrent and a
781
+ // per-transcript write would be eight processes racing one file — the exact
782
+ // shape that lost updates when it was measured (2 concurrent writers, 5/5
783
+ // trials, the loser's entry gone permanently).
784
+ //
785
+ // `newFindingIds` is what a notice keys on: an id already in the record must
786
+ // not alert again however many fresh sightings it accumulates, and a
787
+ // credential seen for the first time must alert even though its rule has
788
+ // fired a thousand times before.
789
+ const coversAllHistory =
790
+ opts.clis === undefined &&
791
+ opts.projects === undefined &&
792
+ opts.since === undefined &&
793
+ discoveryErrors === 0 &&
794
+ errors === 0 &&
795
+ skipped === 0;
796
+ const newFindingIds = persistLeaks(perTranscript, coversAllHistory);
797
+
530
798
  const auditResult: AuditResult = {
531
799
  version: 2,
800
+ newLeakIds: newFindingIds,
532
801
  scannedAt: new Date(startedAt).toISOString(),
533
802
  scope: {
534
803
  cli: clis,
@@ -0,0 +1,200 @@
1
+ /**
2
+ * How a leaked credential is NAMED once it leaves the machine.
3
+ *
4
+ * Every other redactor in this codebase exists to destroy a value. This one
5
+ * exists to describe one, because the leak report has the opposite job: a
6
+ * finding the reader cannot tie to a row in a vendor console is a finding they
7
+ * cannot act on. `[REDACTED: GitHub token]` tells you nothing you can revoke.
8
+ *
9
+ * The rule is: **enough to identify, never enough to use.**
10
+ *
11
+ * ## Why the mask character matters, which is not obvious
12
+ *
13
+ * The three most natural masking characters — `X`, `x` and `0` — RE-MATCH every
14
+ * vendor prefix pattern we ship. `AKIA` + sixteen `X`s satisfies
15
+ * `AKIA[A-Z0-9]{16}`, so a masked AWS key is reported by failproofai as a live
16
+ * AWS key on the next scan. The same holds for a masked connection string,
17
+ * whose pattern keys on `://…@` and does not care what sits between. So the
18
+ * mask is `•` (U+2022), which is in no credential charset anywhere, and the
19
+ * unit tests assert that every masked form fails to re-match.
20
+ *
21
+ * That is not a hypothetical: this product measured its own audit output
22
+ * feeding back into its own corpus, one leaked credential becoming seven
23
+ * reported findings across four sessions.
24
+ *
25
+ * ## Why the last four, and why not always
26
+ *
27
+ * Last-4 is what vendor consoles display next to a key, so it is the token that
28
+ * lets a human find the right row and press revoke. But it is only safe when
29
+ * what remains is genuinely unguessable. For a 40-character GitHub token,
30
+ * showing `ghp_` and four trailing characters leaves ~32 unknown base62
31
+ * characters — no help to anyone. For a 13-character human-chosen password,
32
+ * last-4 is nearly a third of the secret, and the corpus's one confirmed-live
33
+ * password was exactly 13 characters at 3.19 bits per character.
34
+ *
35
+ * So the tail is shown only when the value clears BOTH bars: a recognised
36
+ * public vendor prefix (which proves it is a minted token, not a typed
37
+ * password) AND enough residual length. Everything else is described by class
38
+ * and length alone.
39
+ */
40
+
41
+ /** The mask glyph. Never `X`, `x` or `0` — see the module header. */
42
+ const MASK_CHAR = "•";
43
+
44
+ /** Longest run of mask glyphs rendered, so a 200-char token stays readable. */
45
+ const MAX_MASK_RUN = 8;
46
+
47
+ /**
48
+ * Minimum total length before any trailing characters are shown.
49
+ *
50
+ * 24 is chosen so that even the shortest minted token we recognise keeps well
51
+ * over 100 bits of residual entropy after the prefix and tail are removed.
52
+ * Human-chosen passwords essentially never reach it, which is the point.
53
+ */
54
+ const MIN_LEN_FOR_TAIL = 24;
55
+
56
+ /** How many trailing characters a vendor console typically shows. */
57
+ const TAIL_CHARS = 4;
58
+
59
+ /**
60
+ * Public, minted prefixes — the part of a credential that carries no secret.
61
+ *
62
+ * Each of these is published by its issuer and identical across every key they
63
+ * mint, so showing it discloses nothing while telling the reader which console
64
+ * to open. Ordered longest-first, so `sk-ant-api03-` wins over `sk-ant-` and
65
+ * `sk-`; the first match is the one rendered.
66
+ *
67
+ * Deliberately NOT the full detection vocabulary. This list only has to be
68
+ * right about what is safe to PRINT — a prefix missing from it costs a less
69
+ * specific label, never a disclosure.
70
+ */
71
+ const PUBLIC_PREFIXES: ReadonlyArray<readonly [string, string]> = [
72
+ ["sk-ant-api03-", "Anthropic API key"],
73
+ ["sk-ant-oat01-", "Anthropic OAuth token"],
74
+ ["sk-ant-", "Anthropic key"],
75
+ ["sk-proj-", "OpenAI project key"],
76
+ ["sk-svcacct-", "OpenAI service-account key"],
77
+ ["sk-admin-", "OpenAI admin key"],
78
+ ["sk-or-v1-", "OpenRouter key"],
79
+ ["github_pat_", "GitHub fine-grained token"],
80
+ ["ghp_", "GitHub personal access token"],
81
+ ["gho_", "GitHub OAuth token"],
82
+ ["ghu_", "GitHub user-to-server token"],
83
+ ["ghs_", "GitHub server-to-server token"],
84
+ ["ghr_", "GitHub refresh token"],
85
+ ["glpat-", "GitLab personal access token"],
86
+ ["xoxb-", "Slack bot token"],
87
+ ["xoxp-", "Slack user token"],
88
+ ["xapp-", "Slack app-level token"],
89
+ ["sk_live_", "Stripe live secret key"],
90
+ ["sk_test_", "Stripe test secret key"],
91
+ ["rk_live_", "Stripe restricted key"],
92
+ ["AKIA", "AWS access key ID"],
93
+ ["ASIA", "AWS temporary access key ID"],
94
+ ["AIza", "Google API key"],
95
+ ["hf_", "Hugging Face token"],
96
+ ["gsk_", "Groq API key"],
97
+ ["r8_", "Replicate token"],
98
+ ["xai-", "xAI key"],
99
+ ["npm_", "npm access token"],
100
+ ["dop_v1_", "DigitalOcean token"],
101
+ ["shpat_", "Shopify access token"],
102
+ ["sbp_", "Supabase token"],
103
+ ];
104
+
105
+ export interface LeakFingerprint {
106
+ /** What the reader sees: `ghp_••••••••4f2a` or `[13-char password]`. */
107
+ display: string;
108
+ /** Human label for the class, e.g. "GitHub personal access token". */
109
+ label: string;
110
+ /** Total character length of the original value. */
111
+ length: number;
112
+ /** True when a minted public prefix was recognised — i.e. we can name a
113
+ * console to revoke at. False means "we know it leaked, not who issued it". */
114
+ attributed: boolean;
115
+ }
116
+
117
+ /** The recognised public prefix for a value, longest match first. */
118
+ function matchPublicPrefix(value: string): readonly [string, string] | null {
119
+ for (const entry of PUBLIC_PREFIXES) {
120
+ if (value.startsWith(entry[0])) return entry;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Render a credential as something a human can recognise and nobody can use.
127
+ *
128
+ * `fallbackLabel` names the class when no public prefix is recognised — pass
129
+ * the detecting rule's own label (e.g. "assigned secret", "database password")
130
+ * so an unattributed finding still says what KIND of thing leaked.
131
+ */
132
+ export function fingerprintSecret(value: string, fallbackLabel = "secret"): LeakFingerprint {
133
+ const length = value.length;
134
+ const matched = matchPublicPrefix(value);
135
+
136
+ if (!matched) {
137
+ // No minted prefix: this is a password, a first-party key, or a format we
138
+ // do not recognise. Nothing but the class and the length is safe to print,
139
+ // and the length is what lets the owner recognise their own value.
140
+ return {
141
+ display: `[${length}-char ${fallbackLabel}]`,
142
+ label: fallbackLabel,
143
+ length,
144
+ attributed: false,
145
+ };
146
+ }
147
+
148
+ const [prefix, label] = matched;
149
+ const bodyLength = length - prefix.length;
150
+ const run = MASK_CHAR.repeat(Math.max(1, Math.min(MAX_MASK_RUN, bodyLength)));
151
+
152
+ // The tail is the actionable half — but only when what stays hidden is
153
+ // genuinely unguessable. See the module header.
154
+ if (length >= MIN_LEN_FOR_TAIL && bodyLength > TAIL_CHARS * 2) {
155
+ return {
156
+ display: `${prefix}${run}${value.slice(-TAIL_CHARS)}`,
157
+ label,
158
+ length,
159
+ attributed: true,
160
+ };
161
+ }
162
+
163
+ return { display: `${prefix}${run}`, label, length, attributed: true };
164
+ }
165
+
166
+ /**
167
+ * A stable identity for one distinct credential, for deduping across scans.
168
+ *
169
+ * Salted with a per-machine secret so the digest cannot be used as a lookup
170
+ * oracle: without the salt, an attacker holding a guessable candidate (and the
171
+ * corpus is full of dictionary passwords) could confirm it by comparing
172
+ * hashes. With it, the id is meaningful only on the machine that produced it.
173
+ *
174
+ * The salt therefore forecloses cross-machine dedupe until an org-scoped key
175
+ * exists — a deliberate trade recorded here so nobody reads a per-machine salt
176
+ * as an oversight.
177
+ */
178
+ /**
179
+ * Every id `fingerprintId` can produce, and nothing else.
180
+ *
181
+ * Ids become FILENAMES — one marker file per notified finding, one queue file
182
+ * per pending macOS banner — so an id is a path component, and a path component
183
+ * built from unvalidated input is a directory traversal. Measured: passing
184
+ * `"../../../../tmp/PWNED"` to `markLeakNoticeDelivered` created that file.
185
+ *
186
+ * Ids are HMAC hex today, so nothing in a normal run can carry a `/`. That is
187
+ * not the same as safe: the record is JSON read off disk, and `leaks.json` is a
188
+ * file a user (or anything running as them) can edit. The guard is one regex
189
+ * and it makes the property structural instead of incidental.
190
+ */
191
+ export function isFindingId(id: string): boolean {
192
+ return /^[0-9a-f]{16}$/.test(id);
193
+ }
194
+
195
+ export function fingerprintId(value: string, machineSalt: string): string {
196
+ // Lazily required so this module stays importable in the browser bundle,
197
+ // where the dashboard renders findings but never computes ids.
198
+ const { createHmac } = require("node:crypto") as typeof import("node:crypto");
199
+ return createHmac("sha256", machineSalt).update(value).digest("hex").slice(0, 16);
200
+ }