failproofai 1.0.3 → 1.0.4-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-verify/route_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
  42. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  43. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/index.html +1 -1
  46. package/.next/standalone/.next/server/app/index.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  48. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  56. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
  83. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
  99. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
  103. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
  106. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
  120. package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
  121. package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
  123. package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
  124. package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
  126. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
  128. package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
  130. package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
  135. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
  136. package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
  137. package/.next/standalone/app/actions/get-leaks.ts +100 -0
  138. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
  139. package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
  140. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
  141. package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
  142. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
  143. package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
  144. package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
  145. package/.next/standalone/app/audit/audit-styles.css +114 -1
  146. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
  147. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
  148. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
  149. package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
  150. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
  151. package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
  152. package/.next/standalone/lib/auth/api-server-client.ts +28 -0
  153. package/.next/standalone/lib/claude-sessions.ts +101 -27
  154. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  155. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  156. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  157. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  158. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
  159. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
  160. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
  161. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  162. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
  163. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
  164. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  165. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  166. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  167. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  168. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
  169. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  170. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
  171. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  172. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  173. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  174. package/.next/standalone/node_modules/next/package.json +11 -11
  175. package/.next/standalone/package.json +14 -13
  176. package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
  177. package/.next/standalone/sdk/python/README.md +8 -0
  178. package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
  179. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  180. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
  181. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
  182. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
  183. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
  184. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
  185. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
  186. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
  187. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
  188. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
  189. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
  190. package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
  191. package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
  192. package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
  193. package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
  194. package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
  195. package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
  196. package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
  197. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
  198. package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
  199. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
  200. package/.next/standalone/sdk/python/uv.lock +29 -29
  201. package/.next/standalone/server.js +1 -1
  202. package/README.md +13 -58
  203. package/dist/cli.mjs +5368 -8756
  204. package/dist/worker.mjs +539 -312
  205. package/lib/auth/api-server-client.ts +28 -0
  206. package/lib/claude-sessions.ts +101 -27
  207. package/package.json +14 -13
  208. package/pi-extension/index.ts +42 -4
  209. package/scripts/changelog-open.py +115 -0
  210. package/src/audit/cli.ts +134 -0
  211. package/src/audit/desktop-notify.ts +420 -0
  212. package/src/audit/harm-report.ts +101 -0
  213. package/src/audit/index.ts +141 -13
  214. package/src/audit/leak-fingerprint.ts +200 -0
  215. package/src/audit/leak-notice.ts +161 -0
  216. package/src/audit/leak-record.ts +232 -0
  217. package/src/audit/leak-scan.ts +292 -0
  218. package/src/audit/leak-store.ts +217 -0
  219. package/src/audit/macos-notifier.ts +310 -0
  220. package/src/audit/redact-example.ts +266 -14
  221. package/src/audit/report-harm.ts +12 -1
  222. package/src/audit/report.ts +6 -3
  223. package/src/audit/schedule-cli.ts +37 -0
  224. package/src/audit/scoring.ts +49 -0
  225. package/src/audit/types.ts +41 -0
  226. package/src/hooks/builtin-policies.ts +67 -2
  227. package/src/hooks/configure-wizard.ts +20 -0
  228. package/src/hooks/fp-config.ts +71 -11
  229. package/src/hooks/fp-home.ts +27 -0
  230. package/src/hooks/handler.ts +59 -1
  231. package/src/hooks/integrations.ts +47 -13
  232. package/src/hooks/notice.ts +155 -0
  233. package/src/hooks/uninstall-cli.ts +15 -0
  234. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
  235. package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
  236. package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
  238. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
  240. package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
  241. package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
  242. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
  243. package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
  244. package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
  245. package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
  246. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
  247. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
  248. package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
  249. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
  250. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
  251. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The per-credential record — what the audit knows that `AuditCount` cannot say.
3
+ *
4
+ * `AuditCount` aggregates BY RULE: it can report that `sanitize-api-keys` fired
5
+ * 47 times and show three 80-character examples. It cannot enumerate 47 distinct
6
+ * keys, say which of them is the same key seen twice, or answer "which CLI, in
7
+ * which project, at what time, and how did it get there". Every surface the
8
+ * narrowed product needs — the emailed digest, the dashboard's leak view, and
9
+ * the once-per-finding notification guard — asks exactly those questions, so
10
+ * none of them can be built on the rule-level record.
11
+ *
12
+ * ## Grouped by VALUE, not by occurrence — this is the whole bloat story
13
+ *
14
+ * The obvious shape is one row per sighting. Measured against the real corpus
15
+ * that is 10,301 rows, against roughly 266 distinct values, against about 25
16
+ * genuinely real credentials. Two orders of magnitude of duplication, because
17
+ * one pasted key fans out across every session that touched it — the single
18
+ * confirmed-live password in the corpus appeared in 11 files and 22 occurrences,
19
+ * partly because Claude's own checkpoint records replay the prompt that carried
20
+ * it.
21
+ *
22
+ * So the unit is the DISTINCT VALUE, with a bounded list of sightings attached
23
+ * as evidence. A finding seen a thousand times costs the same on disk as one
24
+ * seen twice: `MAX_SIGHTINGS` entries, plus a count. At the corpus's own numbers
25
+ * that is roughly 130 KB for the whole file, which is why this is one file and
26
+ * not a directory — a per-finding file would spend more on inodes and directory
27
+ * entries than on the findings.
28
+ *
29
+ * ## What is deliberately NOT stored
30
+ *
31
+ * The credential itself, ever. The record holds a salted id (for dedupe across
32
+ * scans) and a fingerprint (for a human to recognise the key in a console) and
33
+ * nothing that could reconstruct the value. `~/.failproofai/` is a directory the
34
+ * user did not ask for and cannot audit, so a leak scanner that caches the leaks
35
+ * has made the problem worse rather than better.
36
+ */
37
+ import type { IntegrationType } from "../hooks/types";
38
+ import type { LeakFingerprint } from "./leak-fingerprint";
39
+
40
+ /** Sightings kept per distinct value. Enough to answer 5W1H for the first and
41
+ * most recent exposure without storing the long tail nobody reads. */
42
+ export const MAX_SIGHTINGS = 5;
43
+
44
+ /** Findings not seen in this many days are dropped on the next write. A key
45
+ * whose transcripts have aged out of the scan window is not evidence of
46
+ * anything current, and keeping it forever grows the file without bound. */
47
+ export const FINDING_TTL_DAYS = 90;
48
+
49
+ /** Cap on distinct findings retained, newest-first. A machine with more than
50
+ * this is not going to triage them, and the census measured a name-only rule
51
+ * emitting thousands — the cap is what stops a bad matcher filling the disk. */
52
+ export const MAX_FINDINGS = 500;
53
+
54
+ /**
55
+ * How a credential came to be where it was found — the "how" of 5W1H.
56
+ *
57
+ * Derived from the tool event rather than authored, so it is always specific:
58
+ * "read from ~/…/.env" beats "detected in a file operation". The path is
59
+ * home-shortened by the caller before it reaches here.
60
+ */
61
+ export interface LeakMechanism {
62
+ /** Short past-tense phrase, e.g. "read from ~/…/.env". */
63
+ summary: string;
64
+ /** Canonical tool name (Bash, Read, Write, Edit…). */
65
+ toolName: string;
66
+ /** Where the value appeared: the tool's INPUT (the agent sent it) or its
67
+ * RESULT (the agent received it). Different exposures with different
68
+ * remedies — an input is deniable at PreToolUse, a result is not. */
69
+ direction: "input" | "result";
70
+ }
71
+
72
+ /** One exposure of one credential: the where/when/who of 5W1H. */
73
+ export interface LeakSighting {
74
+ /** WHO — the harness whose transcript carried it. */
75
+ cli: IntegrationType;
76
+ /** WHERE — session and project. `cwd` is already home-shortened. */
77
+ sessionId: string;
78
+ cwd: string;
79
+ /** WHEN — ISO-8601, from the transcript line. */
80
+ at: string;
81
+ /** HOW. */
82
+ mechanism: LeakMechanism;
83
+ }
84
+
85
+ /** One distinct credential, however many times it was seen. */
86
+ export interface LeakFinding {
87
+ /** Salted, machine-local, stable across scans. Never derived from the
88
+ * fingerprint, which is not unique. */
89
+ id: string;
90
+ /** WHAT — the masked rendering, its class, and its length. */
91
+ fingerprint: LeakFingerprint;
92
+ /** The identifier it was assigned to, when it came from an assignment
93
+ * (`COMPOSIO_API_KEY`). For the 230-of-237 first-party class this is the
94
+ * only actionable field there is, since no vendor console exists. */
95
+ name: string | null;
96
+ /** Which rule found it, for explaining and for suppression. */
97
+ rule: string;
98
+ /** How much we trust the attribution — mirrors the pattern census's tiers. */
99
+ confidence: "doc-verified" | "source-verified" | "partner-corroborated" | "observed-only" | "name-only";
100
+ firstSeen: string;
101
+ lastSeen: string;
102
+ /** Total exposures, which is NOT `sightings.length` — see MAX_SIGHTINGS. */
103
+ occurrences: number;
104
+ /** Bounded evidence, oldest first. */
105
+ sightings: LeakSighting[];
106
+ /** Set when the user dismisses it. Dismissed findings are retained (so they
107
+ * do not re-alert) but never notified about again. */
108
+ dismissedAt?: string;
109
+ /** Set when a notice for this finding has been delivered, so the next scan
110
+ * stays quiet. The delivery guard itself is a separate O_EXCL marker; this
111
+ * is the human-readable record of it. */
112
+ notifiedAt?: string;
113
+ }
114
+
115
+ export interface LeakRecord {
116
+ schemaVersion: number;
117
+ /** Per-machine HMAC salt for `fingerprintId`. Generated once, never sent. */
118
+ salt: string;
119
+ updatedAt: string;
120
+ findings: LeakFinding[];
121
+ }
122
+
123
+ export const LEAK_RECORD_SCHEMA_VERSION = 1;
124
+
125
+ /**
126
+ * Fold one new sighting into the record.
127
+ *
128
+ * Returns whether this is a **newly seen** credential, which is what the
129
+ * notification guard keys on: a finding whose id is already present must not
130
+ * alert again, however many new sightings it accumulates. Getting this backwards
131
+ * is the failure the census measured — keying on the rule-level examples, which
132
+ * `mergeIncremental` freezes at the first three, silently misses the arrival of
133
+ * a brand-new key while re-alerting for old ones.
134
+ */
135
+ export function upsertFinding(
136
+ record: LeakRecord,
137
+ incoming: Omit<LeakFinding, "firstSeen" | "lastSeen" | "occurrences" | "sightings"> & {
138
+ sighting: LeakSighting;
139
+ },
140
+ ): { record: LeakRecord; isNew: boolean } {
141
+ const { sighting, ...rest } = incoming;
142
+ const existing = record.findings.find((f) => f.id === rest.id);
143
+
144
+ if (!existing) {
145
+ record.findings.push({
146
+ ...rest,
147
+ firstSeen: sighting.at,
148
+ lastSeen: sighting.at,
149
+ occurrences: 1,
150
+ sightings: [sighting],
151
+ });
152
+ return { record, isNew: true };
153
+ }
154
+
155
+ existing.occurrences += 1;
156
+ if (sighting.at > existing.lastSeen) existing.lastSeen = sighting.at;
157
+ if (sighting.at < existing.firstSeen) existing.firstSeen = sighting.at;
158
+
159
+ // Keep the FIRST sightings rather than the most recent. The first exposure is
160
+ // the one that explains how the credential got into the transcript at all;
161
+ // later ones are usually the same value being replayed, and a checkpoint
162
+ // record replaying a prompt is not new information.
163
+ if (existing.sightings.length < MAX_SIGHTINGS) {
164
+ const seen = existing.sightings.some(
165
+ (s) => s.sessionId === sighting.sessionId && s.at === sighting.at,
166
+ );
167
+ if (!seen) existing.sightings.push(sighting);
168
+ }
169
+ return { record, isNew: false };
170
+ }
171
+
172
+ /**
173
+ * Drop what is no longer worth keeping, so the file cannot grow without bound.
174
+ *
175
+ * Order matters: age out first, then cap. Capping first would let a burst of
176
+ * stale findings evict fresh ones.
177
+ */
178
+ export function pruneRecord(record: LeakRecord, nowMs: number): LeakRecord {
179
+ const cutoff = new Date(nowMs - FINDING_TTL_DAYS * 86_400_000).toISOString();
180
+ const live = record.findings.filter((f) => f.lastSeen >= cutoff);
181
+ live.sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : a.lastSeen > b.lastSeen ? -1 : 0));
182
+ record.findings = live.slice(0, MAX_FINDINGS);
183
+ return record;
184
+ }
185
+
186
+ /** An empty record, for a machine that has never scanned. */
187
+ export function emptyRecord(salt: string, nowIso: string): LeakRecord {
188
+ return {
189
+ schemaVersion: LEAK_RECORD_SCHEMA_VERSION,
190
+ salt,
191
+ updatedAt: nowIso,
192
+ findings: [],
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Describe how a credential came to be in a tool event.
198
+ *
199
+ * Specific by construction: the path or command is what makes a finding
200
+ * recognisable to the person who caused it, and a generic phrase ("detected in
201
+ * tool input") is the kind of alert people learn to ignore.
202
+ */
203
+ export function describeMechanism(
204
+ toolName: string,
205
+ direction: "input" | "result",
206
+ shortPath: string | null,
207
+ ): LeakMechanism {
208
+ const where = shortPath ? ` ${shortPath}` : "";
209
+ let summary: string;
210
+ if (direction === "result") {
211
+ summary = shortPath ? `appeared in the output of reading${where}` : "appeared in tool output";
212
+ } else {
213
+ switch (toolName) {
214
+ case "Read":
215
+ summary = `read from${where || " a file"}`;
216
+ break;
217
+ case "Write":
218
+ summary = `written to${where || " a file"}`;
219
+ break;
220
+ case "Edit":
221
+ case "NotebookEdit":
222
+ summary = `edited into${where || " a file"}`;
223
+ break;
224
+ case "Bash":
225
+ summary = "passed in a shell command";
226
+ break;
227
+ default:
228
+ summary = `sent to the ${toolName} tool`;
229
+ }
230
+ }
231
+ return { summary, toolName, direction };
232
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Pulling credential VALUES out of a tool event.
3
+ *
4
+ * The audit already replays every event through the policy engine, but
5
+ * `ReplayHit` carries `{policyName, decision, reason}` and no matched text —
6
+ * the engine answers "did a rule fire", not "on what". That is the right answer
7
+ * for enforcement and the wrong one for a leak report, which has to say *which*
8
+ * key, so this walks the event itself.
9
+ *
10
+ * Running beside the replay rather than inside it is also what the pattern
11
+ * census concluded independently: detection wants a vocabulary tuned for
12
+ * PRECISION (a false positive becomes an email to a customer about a key that
13
+ * never leaked), while the redactor's vocabulary is tuned for RECALL (a false
14
+ * positive costs a few characters in a digest). Sharing one list would drag one
15
+ * of them to the other's error costs — measured, importing the detector's names
16
+ * into the redactor stopped 305 of 646 currently-masked names from being masked.
17
+ *
18
+ * ## Where it looks, and why both directions matter
19
+ *
20
+ * Tool INPUT and tool RESULT are separate exposures with separate remedies. An
21
+ * input is what the agent SENT — deniable at PreToolUse on all 12 CLIs, and
22
+ * 39.5% of measured credential appearances live there. A result is what the
23
+ * agent RECEIVED, blockable on 2 of 12 and only after the fact. The record keeps
24
+ * them apart so the report can tell a user which of the two happened.
25
+ */
26
+ import { SECRET_PATTERNS } from "../hooks/builtin-policies";
27
+ import { isSecretName } from "./redact-example";
28
+
29
+ /** One credential value found in one event. */
30
+ export interface SecretMatch {
31
+ /** The raw value. Held in memory only — the caller fingerprints it and must
32
+ * never persist or transmit this. */
33
+ value: string;
34
+ /** The identifier it was assigned to, when it came from an assignment. For
35
+ * the first-party class (230 of 237 secret-named assignments match no vendor
36
+ * pattern) this is the only actionable label there is. */
37
+ name: string | null;
38
+ /** Which rule found it — a vendor label, or "assigned secret". */
39
+ rule: string;
40
+ /** Whether the value carried a recognised vendor shape. Drives the record's
41
+ * confidence, and therefore whether an alert may name a console. */
42
+ shaped: boolean;
43
+ }
44
+
45
+ /**
46
+ * `NAME=value` in its four real spellings.
47
+ *
48
+ * Kept in step with `redact-example.ts`'s ASSIGNMENT_RE — same four shapes, same
49
+ * scheme guard — because a value the DETECTOR can report but the REDACTOR
50
+ * cannot mask is a leak in the report itself. The redactor may be broader; it
51
+ * must never be narrower. `__tests__/audit/leak-scan.test.ts` asserts that
52
+ * direction explicitly.
53
+ */
54
+ /**
55
+ * The identifier is BOUNDED, and the bound is load-bearing rather than tidy.
56
+ *
57
+ * Unbounded (`[A-Za-z0-9_]*`), this is quadratic on a long unbroken token: the
58
+ * engine starts at every one of n positions, consumes the whole run, then
59
+ * backtracks a character at a time looking for the `=` or `:` that never comes.
60
+ * Measured on a plain 300 KB base64 blob — `"A".repeat(300000)` — it ran for
61
+ * over 20 SECONDS on one string. That is not an exotic input: agent transcripts
62
+ * carry base64 images, minified bundles and whole file contents as single
63
+ * lines, and the audit reads every one of them. A scheduled scan hitting a few
64
+ * of these would stall for minutes with nobody watching.
65
+ *
66
+ * 128 characters is far past any real environment-variable or field name, so
67
+ * bounding it costs nothing that exists and makes the scan linear in n.
68
+ */
69
+ const ASSIGNMENT_RE =
70
+ /(["']?)\b([A-Za-z_][A-Za-z0-9_]{0,127})\1([ \t]*(?::(?!\/\/)|=)[ \t]*)("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
71
+
72
+ /** Shortest value worth reporting. Below this it is a flag, a boolean or a
73
+ * placeholder far more often than a credential. */
74
+ const MIN_VALUE_LEN = 8;
75
+
76
+ /**
77
+ * Values that are structurally incapable of being a leaked credential.
78
+ *
79
+ * Deliberately structural, never entropy: the corpus's one confirmed-live
80
+ * password measures 3.19 bits per character while 1.3 million UUIDs in the same
81
+ * corpus sit at 3.72, so any entropy threshold that catches the password also
82
+ * catches every UUID. These tests are decisive instead — a `$VAR` reference
83
+ * contains no secret by construction, and `process.env.X` is the CORRECT secure
84
+ * form, which the redactor currently flags 7,870 times.
85
+ */
86
+ function isNotACredential(value: string): boolean {
87
+ if (value.length < MIN_VALUE_LEN) return true;
88
+ // Shell/template indirection: the value is a reference, not a secret.
89
+ if (/^[$<{(`%]/.test(value)) return true;
90
+ if (value.includes("${") || value.includes("{{") || value.includes("%(")) return true;
91
+ // A code expression — `process.env.DISCORD_TOKEN` is how you SHOULD do it.
92
+ if (/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)+$/.test(value)) return true;
93
+ // Already masked, by us or by anyone else.
94
+ if (value.includes("[REDACTED") || /^[•*#.]+$/.test(value)) return true;
95
+ if (/(.)\1{5,}/.test(value)) return true; // xxxxxxxx, 00000000
96
+ if (/^(true|false|null|undefined|none)$/i.test(value)) return true;
97
+ if (/^-?\d+(\.\d+)?$/.test(value)) return true; // pure number
98
+ if (/^\/|^~\/|^\.\.?\//.test(value)) return true; // a path
99
+ return false;
100
+ }
101
+
102
+ /** Strip one layer of surrounding quotes. */
103
+ function unquote(v: string): string {
104
+ return (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))
105
+ ? v.slice(1, -1)
106
+ : v;
107
+ }
108
+
109
+ /**
110
+ * SHA-256 of credentials the vendors themselves publish in their documentation.
111
+ *
112
+ * The single highest-value refuter in the whole detector, and the only one that
113
+ * is decisive rather than heuristic. Measured on the real corpus: **456 of 457
114
+ * `AKIA` matches were AWS's own documentation literal** — 99.8% of that
115
+ * pattern's entire output, and 232 findings collapsing to zero. Across the
116
+ * pattern census, 44 of 113 patterns with five or more hits were at least 85%
117
+ * one literal, and 30 fired on nothing else at all.
118
+ *
119
+ * Stored as hashes so this file does not itself contain the values — a
120
+ * denylist written in plaintext is a file our own scanner flags, and one that
121
+ * lands in the corpus of every machine that installs us.
122
+ *
123
+ * A hash comparison cannot false-positive: either the value is byte-identical
124
+ * to a published sample or it is not. That is what makes this worth more than
125
+ * any amount of pattern tuning.
126
+ */
127
+ const DOCS_LITERALS = new Set([
128
+ "1a5d44a2dca19669d72edf4c4f1c27c4c1ca4b4408fbb17f6ce4ad452d78ddb3", // AWS access key ID
129
+ "78314b11be2e581549ac1c4f616563fad3fdf0c3b71678f6e2299182080e0598", // AWS secret access key
130
+ "cb20d34a7adda2f5d2043836051e242fd4c2b233e02c80c91fe9d5af9b100574", // AWS temporary key ID
131
+ "3543c50b0cb9cea8e55eb1f529b79f043cb250d485fde7b66cabbb5183add7da", // GitHub PAT sample
132
+ "1fae28c1b317300acfd90078c973567c8159015822b0c8b81508a5ec40517d20", // Google API key sample
133
+ "23219833092a3136d7ad178ccb62ebd6ac8cba54ff24c2cfcd37b84aba9e95cf", // SendGrid key sample
134
+ "872db7f4419592e397ef45230f910e06b48c36ed9970cf1f9e4fb896e69f129c", // Slack bot token sample
135
+ "78a08441f4314f0a2833cfe58c62e555a162264206982cda39f6804f5048f570", // Stripe live key sample
136
+ "2cafc0970149a84f3b9e62eaf169f36f59907a3b3e31f7b82e68c69cd27f7326", // Stripe test key sample
137
+ "b363ade84d56c43bebbc9339b4971e340e3f13f7f8d45580b53373d2763e9e33", // Twilio account SID sample
138
+ ]);
139
+
140
+ /** True when the value is a credential a vendor published on purpose. */
141
+ export function isDocsLiteral(value: string): boolean {
142
+ // Required lazily so this module stays importable from the browser bundle.
143
+ const { createHash } = require("node:crypto") as typeof import("node:crypto");
144
+ return DOCS_LITERALS.has(createHash("sha256").update(value).digest("hex"));
145
+ }
146
+
147
+ /**
148
+ * Text that is failproofai's OWN output being replayed back at us.
149
+ *
150
+ * Auditing for secrets writes secrets into the corpus the next audit reads.
151
+ * That is measured, not theoretical: the single largest organic cluster in
152
+ * 1.6 GB of transcripts was one file — a previous secret-audit run quoting back
153
+ * the credentials it had found (123 Atlassian tokens, 26 Telegram, 25 Slack,
154
+ * 18 GitHub) — and 108 of the 161 patterns that matched anything matched ONLY
155
+ * inside the investigation's own transcripts.
156
+ *
157
+ * Left unhandled the count climbs every scan, each report inflating the next.
158
+ * These markers appear in our own rendered output and essentially nowhere else.
159
+ */
160
+ const SELF_OUTPUT_MARKERS = [
161
+ "[REDACTED:",
162
+ "[redacted:",
163
+ "failproofai audit",
164
+ "assigned secret]",
165
+ ];
166
+
167
+ /** True when this text is our own report, not a transcript of real work. */
168
+ export function isSelfOutput(text: string): boolean {
169
+ return SELF_OUTPUT_MARKERS.some((m) => text.includes(m));
170
+ }
171
+
172
+ /**
173
+ * Literal prefixes that must appear before any vendor regex is worth running.
174
+ *
175
+ * A prefilter, and it is not an optimisation — it is what makes the pattern set
176
+ * shippable at all. The census measured the cost as LINEAR in pattern count:
177
+ * ~0.095s per pattern per 6 MB, so 288 patterns over a 1.6 GB corpus is roughly
178
+ * two hours, while the handful that ever match take five seconds. Scanning is
179
+ * already the audit's slowest phase and the whole point of adding vendors is
180
+ * that most of them never fire on any given machine.
181
+ *
182
+ * So: one cheap pass over the text asking "does any prefix appear at all", and
183
+ * the regexes only run when one does. Derived from `SECRET_PATTERNS` at module
184
+ * load rather than hand-maintained, because a hand-copied list that drifts from
185
+ * its source silently stops matching — the exact failure class this repo already
186
+ * has a tripwire test for elsewhere.
187
+ *
188
+ * Patterns with no literal prefix (a bare JWT, a connection string, the Telegram
189
+ * `\d{8,10}:` shape) yield no gate token and are always run; they are few, and
190
+ * a pattern that cannot be prefiltered is one whose cost has to be paid.
191
+ */
192
+ function literalPrefixOf(source: string): string | null {
193
+ // Walk the regex source until the first construct that is not a plain
194
+ // literal. Anything before that is text the input must contain verbatim.
195
+ let out = "";
196
+ for (let i = 0; i < source.length; i++) {
197
+ const c = source[i];
198
+ if (c === "\\") {
199
+ // An escape: only a handful are literal characters we can rely on.
200
+ const next = source[i + 1];
201
+ if (next === undefined) break;
202
+ if (/[dwsSDWbB]/.test(next)) break; // a class, not a literal
203
+ out += next;
204
+ i++;
205
+ continue;
206
+ }
207
+ if ("[](){}|?*+^$.".includes(c)) break;
208
+ out += c;
209
+ }
210
+ // Two characters is the shortest gate worth having: shorter than that and the
211
+ // prefilter matches nearly every input and buys nothing.
212
+ return out.length >= 2 ? out : null;
213
+ }
214
+
215
+ /** Built once. Each entry is `[pattern, label, gate]`; a null gate always runs. */
216
+ const GATED_PATTERNS: ReadonlyArray<readonly [RegExp, string, string | null]> =
217
+ SECRET_PATTERNS.map(([re, label]) => [re, label, literalPrefixOf(re.source)] as const);
218
+
219
+ /** Patterns worth running against this text. */
220
+ function applicablePatterns(text: string): ReadonlyArray<readonly [RegExp, string, string | null]> {
221
+ return GATED_PATTERNS.filter(([, , gate]) => gate === null || text.includes(gate));
222
+ }
223
+
224
+ /**
225
+ * Every credential value in one blob of text.
226
+ *
227
+ * Vendor SHAPES first, then secret-NAMED assignments, deduped by value so one
228
+ * key matched by both layers is reported once — under its vendor label, which
229
+ * is the one that can carry a revocation link. The two layers are near-disjoint
230
+ * in practice: 55.9% of real vendor-shaped credentials in the corpus have no
231
+ * secret-ish word within 60 characters, and 230 of 237 secret-named assignments
232
+ * match no vendor pattern at all.
233
+ *
234
+ * Both refuters run before anything is reported: our own replayed output is
235
+ * dropped whole, and every published documentation sample is dropped by hash.
236
+ */
237
+ export function findSecrets(text: string): SecretMatch[] {
238
+ if (!text) return [];
239
+ // Our own report replayed into a transcript is not a leak — see isSelfOutput.
240
+ if (isSelfOutput(text)) return [];
241
+ const byValue = new Map<string, SecretMatch>();
242
+
243
+ for (const [pattern, label] of applicablePatterns(text)) {
244
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
245
+ for (const m of text.matchAll(re)) {
246
+ const value = m[0];
247
+ if (isDocsLiteral(value)) continue;
248
+ if (!byValue.has(value)) byValue.set(value, { value, name: null, rule: label, shaped: true });
249
+ }
250
+ }
251
+
252
+ for (const m of text.matchAll(ASSIGNMENT_RE)) {
253
+ const name = m[2];
254
+ const value = unquote(m[4]);
255
+ if (!isSecretName(name) || isNotACredential(value) || isDocsLiteral(value)) continue;
256
+ const existing = byValue.get(value);
257
+ if (existing) {
258
+ // A vendor-shaped value that also has a name: keep the vendor rule (it
259
+ // can name a console) and gain the name (it says which of the user's
260
+ // variables to change).
261
+ existing.name ??= name;
262
+ continue;
263
+ }
264
+ byValue.set(value, { value, name, rule: "assigned secret", shaped: false });
265
+ }
266
+
267
+ return [...byValue.values()];
268
+ }
269
+
270
+ /**
271
+ * Serialize a tool input for scanning.
272
+ *
273
+ * Every string leaf, not just `command`: the measured surface includes file
274
+ * bodies on Write, replacement text on Edit, headers on WebFetch and arbitrary
275
+ * MCP payloads. `JSON.stringify` would work but escapes quotes, which breaks
276
+ * the assignment grammar on exactly the JSON-config shape that carries most
277
+ * first-party keys — so the leaves are joined raw.
278
+ */
279
+ export function flattenToolInput(input: unknown, depth = 0): string {
280
+ if (depth > 6 || input == null) return "";
281
+ if (typeof input === "string") return input;
282
+ if (typeof input === "number" || typeof input === "boolean") return String(input);
283
+ if (Array.isArray(input)) return input.map((v) => flattenToolInput(v, depth + 1)).join("\n");
284
+ if (typeof input === "object") {
285
+ return Object.entries(input as Record<string, unknown>)
286
+ // Keep the key adjacent to the value so `{"api_key": "…"}` still reads as
287
+ // an assignment to the grammar above.
288
+ .map(([k, v]) => `${k}=${flattenToolInput(v, depth + 1)}`)
289
+ .join("\n");
290
+ }
291
+ return "";
292
+ }