failproofai 1.0.0 → 1.0.1-beta.0

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 (238) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +0 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +5 -5
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/routes-manifest.json +0 -6
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +4 -3
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +4 -3
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
  26. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
  28. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/status/route.js +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +4 -3
  32. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
  34. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
  36. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -3
  38. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +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/audit/page/server-reference-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/audit/page.js +5 -3
  42. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/index.html +1 -1
  45. package/.next/standalone/.next/server/app/index.rsc +15 -15
  46. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  47. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  48. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  49. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/page.js +4 -3
  51. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  54. package/.next/standalone/.next/server/app/policies/page.js +6 -5
  55. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page.js +5 -4
  59. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page.js +5 -4
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  71. package/.next/standalone/.next/server/app/settings/page.js +6 -4
  72. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  74. package/.next/standalone/.next/server/app-paths-manifest.json +0 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0jx6cp6._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0f-x2b0._.js} +1 -1
  90. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1f290jp._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0hwvl7e._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1g2ji1t._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1ctjdzl._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
  95. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0v5_91z._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
  113. package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
  117. package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
  118. package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  121. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  123. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
  128. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  129. package/.next/standalone/.next/server/pages/404.html +1 -1
  130. package/.next/standalone/.next/server/pages/500.html +1 -1
  131. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  132. package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
  133. package/.next/standalone/.next/static/chunks/00q6cfzuzpc85.js +1 -0
  134. package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
  135. package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
  137. package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
  139. package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
  140. package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
  141. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  142. package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
  144. package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
  145. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
  146. package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
  147. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
  148. package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
  149. package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
  150. package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
  151. package/.next/standalone/app/api/auth/status/route.ts +7 -17
  152. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
  153. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
  154. package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
  155. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  156. package/.next/standalone/app/audit/audit-styles.css +150 -26
  157. package/.next/standalone/app/globals.css +39 -1
  158. package/.next/standalone/app/settings/page.tsx +38 -21
  159. package/.next/standalone/app/settings/settings-client.tsx +583 -392
  160. package/.next/standalone/app/settings/settings.css +354 -0
  161. package/.next/standalone/components/navbar.tsx +33 -4
  162. package/.next/standalone/lib/auth/api-server-client.ts +51 -29
  163. package/.next/standalone/lib/auth/auth-store.ts +44 -59
  164. package/.next/standalone/package.json +12 -10
  165. package/.next/standalone/server.js +1 -1
  166. package/bin/failproofai.mjs +6 -0
  167. package/bin/failproofaid-shim.mjs +0 -0
  168. package/dist/cli.mjs +1889 -426
  169. package/dist/worker.mjs +27 -5
  170. package/lib/auth/api-server-client.ts +51 -29
  171. package/lib/auth/auth-store.ts +44 -59
  172. package/package.json +12 -10
  173. package/scripts/docs-audit.ts +493 -0
  174. package/scripts/translate-docs/cli.ts +16 -2
  175. package/scripts/translate-docs/mdx-translator.ts +6 -1
  176. package/scripts/translate-docs/readme-translator.ts +4 -2
  177. package/src/audit/cli-login.ts +330 -0
  178. package/src/audit/cli.ts +124 -6
  179. package/src/audit/dashboard-cache.ts +44 -8
  180. package/src/audit/harm-report.ts +260 -0
  181. package/src/audit/machine-store.ts +120 -0
  182. package/src/audit/redact-example.ts +312 -0
  183. package/src/audit/report-harm.ts +167 -0
  184. package/src/audit/schedule-cli.ts +352 -0
  185. package/src/hooks/builtin-policies.ts +26 -0
  186. package/src/hooks/daemon-service.ts +76 -0
  187. package/src/hooks/fp-config.ts +96 -5
  188. package/src/hooks/fp-home.ts +118 -8
  189. package/src/hooks/fp-reset.ts +79 -9
  190. package/src/hooks/migrations.ts +241 -4
  191. package/src/hooks/tui.ts +61 -3
  192. package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
  193. package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
  194. package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
  195. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
  196. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
  197. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
  198. package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
  199. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
  200. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
  201. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
  202. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
  203. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
  204. package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
  206. package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
  208. package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
  209. package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
  211. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
  212. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
  214. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
  215. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
  216. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
  219. package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
  220. package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
  221. package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
  222. package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
  223. package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
  225. package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
  226. package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
  227. package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
  228. package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
  229. package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
  230. package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
  231. package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
  232. package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
  233. package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
  234. package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
  235. package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
  236. /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
  237. /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
  238. /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_ssgManifest.js +0 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Turning an audit result into a harm report the api-server can act on.
3
+ *
4
+ * Runs only after a SCHEDULED scan (`failproofai audit --scheduled`), only when
5
+ * the user has switched emailed reports on, and only ever from the audit child —
6
+ * never the daemon, which holds no human credential precisely so that refresh
7
+ * rotation stays inside the audit lock. See `crates/failproofaid/src/audit_lane.rs`.
8
+ *
9
+ * ## What counts as harm
10
+ *
11
+ * The policies the engine would have BLOCKED, plus the ones that caught a secret
12
+ * on its way into the model's context. In terms of `severityForBuiltin`, that is
13
+ * `deny` and `sanitize` — `block-*` and `sanitize-*` — and NOT `warn-`,
14
+ * `prefer-` or `require-`, which are hygiene.
15
+ *
16
+ * One name is added by hand, and it is worth explaining rather than hiding:
17
+ * `severityForBuiltin` derives severity from the NAME PREFIX, so
18
+ * `protect-env-vars` reads as `warn` despite being a policy that blocks `env` /
19
+ * `printenv` outright. Its whole subject is an agent reaching for the
20
+ * environment, which is the "read my keys" case this feature exists to report.
21
+ * Inheriting a scoring heuristic's blind spot into a security digest would be
22
+ * the wrong kind of consistency.
23
+ *
24
+ * ## The window, and the trap in `--since`
25
+ *
26
+ * `RunAuditOptions.since` filters on transcript MTIME, and that is right for
27
+ * what it does — it decides which files to open. It is WRONG as a window for
28
+ * this: a session left open for a month has a fresh mtime, so `--since 7d`
29
+ * hands back that whole transcript including month-old events, and the first
30
+ * digest would report everything the agent has ever done as though it happened
31
+ * this week.
32
+ *
33
+ * So the window is applied HERE, per event, against the timestamps `AuditCount`
34
+ * already carries — `lastSeen` to decide whether a policy fired in the window at
35
+ * all, and each example's own `timestamp` to decide which examples belong to it.
36
+ * The scan itself stays unfiltered.
37
+ *
38
+ * ## Counts are approximate; the window boundary is not
39
+ *
40
+ * `AuditCount.hits` is a total over everything scanned, and there is no
41
+ * per-event breakdown to subtract from it — the cache stores counts, not event
42
+ * lists. Rather than report a total that spans the wrong period, a policy whose
43
+ * activity straddles the window boundary reports the number of EXAMPLES that
44
+ * fall inside it, which is a real count of real events even though it is capped
45
+ * at three. A policy entirely inside the window reports its true total. The
46
+ * server's threshold reads these, so undercounting is the safe direction: it can
47
+ * delay a digest, never invent one.
48
+ */
49
+ import type { AuditCount, AuditResult } from "./types";
50
+ import { redactExample } from "./redact-example";
51
+
52
+ /** Severities that mean "the engine would have stopped this". */
53
+ const HARMFUL_SEVERITIES = new Set(["deny", "sanitize"]);
54
+
55
+ /**
56
+ * Policies whose severity misreads their intent. See the module docs.
57
+ *
58
+ * Kept as an explicit list rather than by rewriting `severityForBuiltin`,
59
+ * because that function feeds the SCORE's gentle/medium buckets and changing it
60
+ * would silently move every historical score.
61
+ */
62
+ const ALSO_HARMFUL = new Set(["protect-env-vars"]);
63
+
64
+ /** One policy's harmful activity inside the window, as the wire expects it. */
65
+ export interface ReportedPolicy {
66
+ policy: string;
67
+ category: string;
68
+ title: string;
69
+ hits: number;
70
+ first_seen?: string;
71
+ last_seen?: string;
72
+ examples: string[];
73
+ }
74
+
75
+ export interface HarmReport {
76
+ window_from?: string;
77
+ window_to: string;
78
+ harmful: ReportedPolicy[];
79
+ }
80
+
81
+ /** `failproofai/block-rm-rf` → `block-rm-rf`. */
82
+ function shortName(name: string): string {
83
+ const slash = name.indexOf("/");
84
+ return slash === -1 ? name : name.slice(slash + 1);
85
+ }
86
+
87
+ export function isHarmful(count: AuditCount): boolean {
88
+ if (count.source !== "builtin") return false;
89
+ const short = shortName(count.name);
90
+ return HARMFUL_SEVERITIES.has(count.severity) || ALSO_HARMFUL.has(short);
91
+ }
92
+
93
+ /** Parse an ISO timestamp, or null if it is absent or unusable. */
94
+ function ts(value: string | undefined): number | null {
95
+ if (!value) return null;
96
+ const n = Date.parse(value);
97
+ return Number.isFinite(n) ? n : null;
98
+ }
99
+
100
+ /**
101
+ * Select the harmful policies whose activity falls inside `[from, to]`.
102
+ *
103
+ * `from` undefined means "everything up to `to`", which is now only reachable
104
+ * by an explicit caller — `buildHarmReport` always supplies a bound. See the
105
+ * note there for why.
106
+ *
107
+ * `includeUnplaceable` decides what happens to a policy with NO usable
108
+ * timestamps. It cannot be placed, and the two failure directions are not
109
+ * equal: on a first report, dropping it loses a real finding; on a later one,
110
+ * including it re-reports something already covered. Silence about something
111
+ * new is the worse of the two and repetition is merely annoying, so each window
112
+ * gets the answer that fails the way it can afford to.
113
+ */
114
+ export function selectHarmful(
115
+ result: AuditResult,
116
+ from: Date | undefined,
117
+ to: Date,
118
+ opts: { includeUnplaceable?: boolean } = {},
119
+ ): ReportedPolicy[] {
120
+ const includeUnplaceable = opts.includeUnplaceable ?? from === undefined;
121
+ const fromMs = from ? from.getTime() : null;
122
+ const toMs = to.getTime();
123
+ const out: ReportedPolicy[] = [];
124
+
125
+ for (const count of result.results) {
126
+ if (!isHarmful(count)) continue;
127
+
128
+ const last = ts(count.lastSeen);
129
+ const first = ts(count.firstSeen);
130
+
131
+ // Nothing since the watermark — this policy's whole history predates the
132
+ // window.
133
+ if (fromMs !== null && last !== null && last <= fromMs) continue;
134
+ // Fired entirely after the window closed (a clock skew, or a scan that
135
+ // raced an event). It belongs to the next report, not this one.
136
+ if (first !== null && first > toMs) continue;
137
+
138
+ const inWindow = count.examples.filter((e) => {
139
+ const at = ts(e.timestamp);
140
+ if (at === null) return includeUnplaceable;
141
+ if (fromMs !== null && at <= fromMs) return false;
142
+ return at <= toMs;
143
+ });
144
+
145
+ const unplaceable = last === null && first === null;
146
+ if (unplaceable && !includeUnplaceable) continue;
147
+
148
+ // Wholly inside the window → the real total. Straddling EITHER edge → the
149
+ // examples that actually fall inside, which undercounts but never invents.
150
+ //
151
+ // Both edges, and the upper one is not symmetry for its own sake. This used
152
+ // to test the lower bound alone, so a policy that started inside the window
153
+ // and was still firing after it closed reported `count.hits` — every hit,
154
+ // including the ones after `to`, while its examples were filtered to the
155
+ // window. Those hits then fell inside the NEXT report's window too, since
156
+ // the watermark advances to `to`, and were counted a second time. A digest
157
+ // that reports tomorrow's findings today and again tomorrow is worse than
158
+ // one that is late.
159
+ //
160
+ // An UNPLACEABLE policy that survived the check above reports its full
161
+ // count: there is nothing to narrow it with, and having decided to include
162
+ // it, reporting zero would be a row claiming nothing happened. It is only
163
+ // reachable on a first report, where over-reporting is the direction that
164
+ // was chosen deliberately.
165
+ const afterLowerEdge = fromMs === null || (first !== null && first > fromMs);
166
+ const beforeUpperEdge = last !== null && last <= toMs;
167
+ const wholly = unplaceable || (afterLowerEdge && beforeUpperEdge);
168
+ // A straddling policy falls back to its in-window EXAMPLES, and the audit
169
+ // keeps at most three of them per policy, chosen in whatever order the
170
+ // transcripts happened to be walked. On a machine that has been running
171
+ // agents for months those three are routinely all old — so a policy that
172
+ // fired an hour ago scored zero and was dropped, and because `firstSeen`
173
+ // stays before the watermark forever, it was dropped from every later report
174
+ // too. Not a delayed digest: a feature that goes quiet on exactly the
175
+ // machines with the most to report.
176
+ //
177
+ // `beforeUpperEdge` having survived the `last <= fromMs` skip above means
178
+ // `lastSeen` itself sits inside the window, and that timestamp IS a real
179
+ // event. One is the floor it proves, which keeps the "never invent a hit"
180
+ // rule intact while making the row exist.
181
+ const floor = beforeUpperEdge ? 1 : 0;
182
+ const hits = wholly ? count.hits : Math.max(inWindow.length, floor);
183
+ if (hits <= 0) continue;
184
+
185
+ out.push({
186
+ policy: shortName(count.name),
187
+ category: count.category,
188
+ title: count.displayTitle ?? "",
189
+ hits,
190
+ first_seen: count.firstSeen,
191
+ last_seen: count.lastSeen,
192
+ examples: inWindow.map((e) => redactExample(e.example)).filter((e) => e.length > 0),
193
+ });
194
+ }
195
+
196
+ // Most active first, so a digest truncated by anything downstream keeps the
197
+ // rows that matter.
198
+ out.sort((a, b) => b.hits - a.hits);
199
+ return out;
200
+ }
201
+
202
+ /**
203
+ * Build the report body for one scan.
204
+ *
205
+ * `window_to` is the scan's own `scannedAt` rather than "now": it is the instant
206
+ * the evidence was gathered, and using a later clock reading would advance the
207
+ * watermark past events that happened while the scan was still running — events
208
+ * no report would ever cover.
209
+ *
210
+ * ## A first report is bounded to one interval, not to all of history
211
+ *
212
+ * With no watermark the obvious window is "everything", and that is what this
213
+ * did until it was run against a real machine: the first report covered 230
214
+ * sessions and 22,059 tool calls and came out at **5,815 findings**. Every
215
+ * number in it was true and the digest was still wrong — somebody's first email
216
+ * would describe their agent's entire recorded history as though it were this
217
+ * week's news, and would trip the critical bypass on day one for essentially
218
+ * everyone.
219
+ *
220
+ * A digest is a statement about RECENT behaviour, so the first one covers the
221
+ * same period every later one does: `interval_days` back from the scan. The
222
+ * older findings are not lost, they are simply not news — they are on the
223
+ * dashboard, which is where a full history belongs.
224
+ *
225
+ * `includeUnplaceable` still follows "is this the first report", not "is there a
226
+ * lower bound", so a policy carrying no usable timestamps is reported once on a
227
+ * new machine rather than silently dropped by the bound this now always sets.
228
+ */
229
+ export function buildHarmReport(
230
+ result: AuditResult,
231
+ lastReportedAt: string | undefined,
232
+ intervalDays: number,
233
+ ): HarmReport {
234
+ const to = new Date(Date.parse(result.scannedAt));
235
+ const windowTo = Number.isFinite(to.getTime()) ? to : new Date();
236
+ const watermark = ts(lastReportedAt);
237
+ const isFirstReport = watermark === null;
238
+
239
+ const oneInterval = Math.max(1, intervalDays) * 86_400_000;
240
+ const fallbackFrom = windowTo.getTime() - oneInterval;
241
+
242
+ // The watermark is the SERVER's clock; `windowTo` is this machine's. A
243
+ // backwards jump between them — NTP correcting a fast RTC, a VM restored from
244
+ // a snapshot, a dual-boot machine that wrote localtime to the hardware clock
245
+ // — leaves `from` LATER than `to`, and `selectHarmful` then matches nothing
246
+ // at all. That drops every finding silently and permanently: the watermark
247
+ // only ever moves forward, so the window never re-opens, while the run's
248
+ // outcome line still reads normal. The scheduling lane already repairs this
249
+ // class of jump; the reporting half did not. One interval back is a digest
250
+ // that is narrower than it should be, rather than one that is empty forever.
251
+ const from = new Date(
252
+ isFirstReport || watermark >= windowTo.getTime() ? fallbackFrom : watermark,
253
+ );
254
+
255
+ return {
256
+ window_from: from.toISOString(),
257
+ window_to: windowTo.toISOString(),
258
+ harmful: selectHarmful(result, from, windowTo, { includeUnplaceable: isFirstReport }),
259
+ };
260
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `~/.failproofai/audit/machine.json` — this machine's report identity.
3
+ *
4
+ * Two fields, and they are together because they share one property: both must
5
+ * outlive a sign-out.
6
+ *
7
+ * - `machine_id` is what the api-server keys reports on. Regenerate it and the
8
+ * server sees a brand-new machine, which burns a slot off the account's cap
9
+ * on every logout and splits one box's history into two.
10
+ * - `last_reported_at` is how far the last digest reached. Reset it and the
11
+ * next report re-covers months of history, and the user gets a digest of
12
+ * everything that ever happened as though it just did.
13
+ *
14
+ * That is why this is a separate file from `session.json` rather than two more
15
+ * keys in it: signing out deletes the session, and neither of these may go with
16
+ * it. `HOME_CLASSES` classifies this `identity` — never deleted, alongside
17
+ * `cursors/` and the telemetry id — while the tokens beside it are `user-typed`
18
+ * and come and go.
19
+ *
20
+ * ## The id is minted here, not borrowed
21
+ *
22
+ * `state/telemetry-id` is already a stable per-machine random id and would have
23
+ * been free to reuse. It is deliberately not reused: that id is the anonymous
24
+ * PostHog person, and sending it alongside a verified email address would link
25
+ * the two the moment somebody turns emailed reports on. Opting into a digest
26
+ * should not de-anonymise telemetry, so this feature gets its own id and the
27
+ * two never meet.
28
+ */
29
+ import { existsSync, readFileSync, rmSync } from "node:fs";
30
+ import { hostname } from "node:os";
31
+ import { randomUUID } from "node:crypto";
32
+
33
+ import { writeJsonAtomically } from "../../lib/atomic-write";
34
+ import { auditMachineFile } from "../hooks/fp-home";
35
+
36
+ export interface MachineIdentity {
37
+ /** Random, minted on first use. Opaque to the server. */
38
+ machine_id: string;
39
+ /** ISO-8601. Absent until the first digest is delivered. */
40
+ last_reported_at?: string;
41
+ /** When this id was minted. Diagnostics only. */
42
+ created_at: string;
43
+ }
44
+
45
+ export function readMachineIdentity(home?: string): MachineIdentity | null {
46
+ const path = auditMachineFile(home);
47
+ if (!existsSync(path)) return null;
48
+ try {
49
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<MachineIdentity>;
50
+ if (typeof parsed.machine_id !== "string" || !parsed.machine_id) return null;
51
+ return {
52
+ machine_id: parsed.machine_id,
53
+ last_reported_at:
54
+ typeof parsed.last_reported_at === "string" ? parsed.last_reported_at : undefined,
55
+ created_at: typeof parsed.created_at === "string" ? parsed.created_at : new Date(0).toISOString(),
56
+ };
57
+ } catch {
58
+ // Absent, unreadable and malformed all read as "no identity yet". The caller
59
+ // mints a new one, which costs a slot off the cap and a re-covered window —
60
+ // bad, but recoverable, and strictly better than refusing to report at all
61
+ // because one file got truncated.
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Read the identity, creating it on first call.
68
+ *
69
+ * Only ever called from the reporting path, so a machine that never opts into
70
+ * emailed reports never gets an id at all — there is nothing to mint one for.
71
+ */
72
+ export function ensureMachineIdentity(home?: string): MachineIdentity {
73
+ const existing = readMachineIdentity(home);
74
+ if (existing) return existing;
75
+ const fresh: MachineIdentity = {
76
+ machine_id: randomUUID(),
77
+ created_at: new Date().toISOString(),
78
+ };
79
+ writeJsonAtomically(auditMachineFile(home), fresh);
80
+ return fresh;
81
+ }
82
+
83
+ /**
84
+ * Record how far the last DELIVERED digest reached.
85
+ *
86
+ * The value is the server's `next_window_from`, not the window this run
87
+ * scanned. The server is authoritative because it knows which reports actually
88
+ * produced an email — a report held by the cooldown, or one whose send failed,
89
+ * must not advance the watermark or its findings are silently dropped from every
90
+ * future digest.
91
+ */
92
+ export function recordReportWatermark(nextWindowFrom: string, home?: string): void {
93
+ const current = ensureMachineIdentity(home);
94
+ writeJsonAtomically(auditMachineFile(home), {
95
+ ...current,
96
+ last_reported_at: nextWindowFrom,
97
+ } satisfies MachineIdentity);
98
+ }
99
+
100
+ export function deleteMachineIdentity(home?: string): void {
101
+ const path = auditMachineFile(home);
102
+ if (existsSync(path)) rmSync(path, { force: true });
103
+ }
104
+
105
+ /**
106
+ * A display name for this machine — its hostname.
107
+ *
108
+ * Shown in the digest so somebody with three boxes can tell which one is
109
+ * misbehaving, which is the whole reason it is sent. Falls back to `undefined`
110
+ * rather than a placeholder: the server keeps whatever label it already has when
111
+ * one is omitted, so guessing here would overwrite a good name with a bad one.
112
+ */
113
+ export function machineLabel(): string | undefined {
114
+ try {
115
+ const h = hostname().trim();
116
+ return h.length > 0 ? h : undefined;
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ }