failproofai 1.0.0-beta.0 → 1.0.0-beta.10

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 (357) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +2 -1
  3. package/.next/standalone/.next/build-manifest.json +6 -6
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/routes-manifest.json +6 -0
  7. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  8. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js +3 -1
  10. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  16. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  20. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page.js +3 -1
  22. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  25. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  26. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  27. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  28. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  29. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  31. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  33. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
  35. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -1
  39. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -2
  41. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
  43. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +3 -2
  45. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -2
  47. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/audit/page.js +5 -3
  53. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/index.html +1 -1
  56. package/.next/standalone/.next/server/app/index.rsc +15 -15
  57. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  59. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  60. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  61. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  62. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  63. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/page.js +3 -1
  65. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  68. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  69. package/.next/standalone/.next/server/app/policies/page.js +5 -3
  70. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  73. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  74. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -2
  75. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  76. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  77. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  78. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  79. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  80. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
  81. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  84. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  85. package/.next/standalone/.next/server/app/projects/page.js +4 -2
  86. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
  89. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +18 -0
  90. package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +6 -0
  91. package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
  92. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +53 -0
  93. package/.next/standalone/.next/server/app/settings/page.js +18 -0
  94. package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
  95. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
  96. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
  97. package/.next/standalone/.next/server/app-paths-manifest.json +2 -1
  98. package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
  99. package/.next/standalone/.next/server/chunks/{[externals]__1nl3dvw._.js → [externals]__0r-zamh._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g0vcwt._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0h3lfmp._.js → [root-of-the-server]__0ky_n0r._.js} +2 -2
  107. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__18d-9oi._.js → [root-of-the-server]__0lk_935._.js} +2 -2
  108. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__0pbp9fe._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__10oa-v9._.js → [root-of-the-server]__0v2fpxe._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0sb_5m8._.js → [root-of-the-server]__131ebum._.js} +3 -3
  112. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-dh5jy._.js → [root-of-the-server]__1_ps0jw._.js} +2 -2
  113. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l4vuqm._.js → [root-of-the-server]__1a7i2q4._.js} +2 -2
  114. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +5 -0
  119. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +5 -0
  120. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +5 -0
  121. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
  122. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
  123. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
  124. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04pwh4t._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06bp_fg._.js +5 -0
  128. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__074qmkp._.js +3 -0
  129. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  130. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__10butu6._.js +3 -0
  131. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1a2hcb_._.js +3 -0
  132. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__1rdruw4._.js} +2 -2
  134. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +5 -0
  136. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +4 -0
  137. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__1yw368c._.js} +2 -2
  139. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +3 -0
  140. package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +3 -0
  143. package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +3 -0
  144. package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +3 -0
  145. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +3 -0
  148. package/.next/standalone/.next/server/chunks/ssr/_1gmzkwj._.js +5 -0
  149. package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +3 -0
  150. package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +3 -0
  151. package/.next/standalone/.next/server/chunks/ssr/_1y4qr7u._.js +3 -0
  152. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  153. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  155. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dwo94s.js +4 -0
  156. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
  157. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +3 -0
  158. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +5 -0
  159. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  160. package/.next/standalone/.next/server/middleware.js +2 -2
  161. package/.next/standalone/.next/server/pages/404.html +1 -1
  162. package/.next/standalone/.next/server/pages/500.html +1 -1
  163. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  164. package/.next/standalone/.next/server/server-reference-manifest.json +74 -20
  165. package/.next/standalone/.next/static/chunks/00adubtgov21s.js +1 -0
  166. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  167. package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
  168. package/.next/standalone/.next/static/chunks/{1grd7y94-igz9.js → 0yudogeqddpzr.js} +1 -1
  169. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  170. package/.next/standalone/.next/static/chunks/1bm0q12hgs22l.css +2 -0
  171. package/.next/standalone/.next/static/chunks/{1d6ncspjagm53.js → 1fu90ch80qngv.js} +1 -1
  172. package/.next/standalone/.next/static/chunks/{2vr7aazw6k2-n.js → 1m416lie3qwsj.js} +1 -1
  173. package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +1 -0
  174. package/.next/standalone/.next/static/chunks/1ypqzvoal8j56.js +6 -0
  175. package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
  176. package/.next/standalone/.next/static/chunks/{29k6ef49zppxj.js → 2h4sbmp0ad78w.js} +1 -1
  177. package/.next/standalone/.next/static/chunks/2obwg4tterjpa.js +1 -0
  178. package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +2 -0
  179. package/.next/standalone/.next/static/chunks/{29ydr-8q0zis6.js → 30ufy4_e2n78e.js} +22 -22
  180. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  181. package/.next/standalone/.next/static/chunks/{1s-_swlc80y43.js → 3p4j0l1o_p-nm.js} +1 -1
  182. package/.next/standalone/.next/static/chunks/{0ip1n8xmgxarv.js → 3uxnxwxcctyj0.js} +1 -1
  183. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  184. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  185. package/.next/standalone/app/actions/get-hooks-config.ts +2 -1
  186. package/.next/standalone/app/actions/get-scheduled-audit.ts +70 -0
  187. package/.next/standalone/app/actions/update-scheduled-audit.ts +44 -0
  188. package/.next/standalone/app/api/audit/run/route.ts +28 -0
  189. package/.next/standalone/app/api/audit/status/route.ts +13 -1
  190. package/.next/standalone/app/api/auth/status/route.ts +1 -1
  191. package/.next/standalone/app/audit/_components/auth-dialog.css +159 -0
  192. package/.next/standalone/app/audit/_components/auth-dialog.tsx +3 -0
  193. package/.next/standalone/app/audit/audit-styles.css +6 -149
  194. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  195. package/.next/standalone/app/policies/hooks-client.tsx +81 -6
  196. package/.next/standalone/app/settings/page.tsx +31 -0
  197. package/.next/standalone/app/settings/settings-client.tsx +488 -0
  198. package/.next/standalone/components/navbar.tsx +2 -0
  199. package/.next/standalone/lib/auth/api-server-client.ts +2 -3
  200. package/.next/standalone/lib/auth/auth-store.ts +4 -4
  201. package/.next/standalone/lib/codex-sessions.ts +2 -1
  202. package/.next/standalone/lib/dashboard-host.ts +101 -0
  203. package/.next/standalone/lib/install-check.ts +22 -7
  204. package/.next/standalone/lib/pi-sessions.ts +99 -14
  205. package/.next/standalone/lib/telemetry-enabled.ts +54 -0
  206. package/.next/standalone/lib/telemetry-id.ts +52 -1
  207. package/.next/standalone/lib/telemetry.ts +8 -4
  208. package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
  209. package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
  210. package/.next/standalone/node_modules/react/package.json +3 -3
  211. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
  212. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
  213. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
  214. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
  215. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
  216. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
  217. package/.next/standalone/node_modules/react-dom/package.json +4 -4
  218. package/.next/standalone/package.json +17 -10
  219. package/.next/standalone/proxy.ts +89 -1
  220. package/.next/standalone/server.js +1 -1
  221. package/bin/failproofai.mjs +518 -79
  222. package/dist/cli.mjs +11484 -6046
  223. package/dist/worker.mjs +2019 -272
  224. package/lib/auth/api-server-client.ts +2 -3
  225. package/lib/auth/auth-store.ts +4 -4
  226. package/lib/codex-sessions.ts +2 -1
  227. package/lib/dashboard-host.ts +101 -0
  228. package/lib/install-check.ts +22 -7
  229. package/lib/pi-sessions.ts +99 -14
  230. package/lib/telemetry-enabled.ts +54 -0
  231. package/lib/telemetry-id.ts +52 -1
  232. package/lib/telemetry.ts +8 -4
  233. package/package.json +17 -10
  234. package/scripts/build-daemon-packages.mjs +210 -0
  235. package/scripts/daemon-platforms.mjs +42 -0
  236. package/scripts/install-telemetry.mjs +8 -1
  237. package/scripts/launch.ts +25 -3
  238. package/scripts/parse-script-args.ts +17 -1
  239. package/scripts/prune-standalone.mjs +26 -0
  240. package/scripts/publish-aliases.mjs +62 -41
  241. package/scripts/repro-npm-install.sh +155 -0
  242. package/scripts/translate-docs/readme-translator.ts +163 -4
  243. package/scripts/validate-mdx.ts +153 -13
  244. package/src/audit/audit-lock.ts +285 -0
  245. package/src/audit/audit-schedule.ts +111 -0
  246. package/src/audit/cache.ts +18 -4
  247. package/src/audit/cli-adapters/goose.ts +2 -1
  248. package/src/audit/cli-adapters/hermes.ts +28 -10
  249. package/src/audit/cli.ts +191 -35
  250. package/src/audit/dashboard-cache.ts +2 -1
  251. package/src/hooks/backfill-cli.ts +247 -0
  252. package/src/hooks/builtin-policies.ts +390 -2
  253. package/src/hooks/cloud-connection.ts +354 -0
  254. package/src/hooks/cloud-enrollment-cli.ts +325 -0
  255. package/src/hooks/cloud-enrollment.ts +243 -0
  256. package/src/hooks/cloud-introspect.ts +147 -0
  257. package/src/hooks/cloud-managed-policies.ts +169 -0
  258. package/src/hooks/collector-config.ts +234 -0
  259. package/src/hooks/configure-wizard.ts +915 -158
  260. package/src/hooks/custom-hooks-loader.ts +191 -33
  261. package/src/hooks/daemon-client.ts +136 -17
  262. package/src/hooks/daemon-download.ts +192 -30
  263. package/src/hooks/daemon-service.ts +1190 -92
  264. package/src/hooks/first-run-gate.ts +50 -0
  265. package/src/hooks/fp-config.ts +497 -0
  266. package/src/hooks/fp-home.ts +375 -0
  267. package/src/hooks/fp-reset.ts +464 -0
  268. package/src/hooks/handler.ts +185 -6
  269. package/src/hooks/hook-activity-store.ts +85 -20
  270. package/src/hooks/hook-logger.ts +2 -1
  271. package/src/hooks/hook-telemetry.ts +5 -1
  272. package/src/hooks/hooks-config.ts +4 -3
  273. package/src/hooks/loader-utils.ts +171 -8
  274. package/src/hooks/manager.ts +5 -4
  275. package/src/hooks/onboarding-attempt.ts +179 -0
  276. package/src/hooks/onboarding-lock.ts +119 -0
  277. package/src/hooks/policy-types.ts +28 -0
  278. package/src/hooks/session-pause-cli.ts +159 -0
  279. package/src/hooks/session-pause.ts +268 -0
  280. package/src/hooks/setup-state.ts +256 -0
  281. package/src/hooks/tui.ts +119 -4
  282. package/src/hooks/uninstall-cli.ts +345 -0
  283. package/src/hooks/worker-server.ts +65 -3
  284. package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
  285. package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
  286. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
  287. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +0 -3
  288. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +0 -3
  289. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +0 -3
  290. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +0 -3
  291. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +0 -3
  292. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +0 -3
  293. package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
  294. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0114ewg._.js +0 -4
  295. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0259h38._.js +0 -3
  296. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +0 -4
  297. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +0 -4
  298. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +0 -4
  299. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +0 -4
  300. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +0 -4
  301. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +0 -3
  302. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +0 -3
  303. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +0 -4
  304. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zj-87g._.js +0 -3
  305. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +0 -4
  306. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +0 -3
  307. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1-upb14.js +0 -3
  308. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +0 -5
  309. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +0 -5
  310. package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +0 -1
  311. package/.next/standalone/.next/static/chunks/0u9hc6oxx-gpj.js +0 -6
  312. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  313. package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
  314. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  315. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  316. package/.next/standalone/.next/static/chunks/3i27c3hcriawq.css +0 -1
  317. package/.next/standalone/.next/static/chunks/3wrgbih-516uj.js +0 -1
  318. package/.next/standalone/Cargo.lock +0 -428
  319. package/.next/standalone/Cargo.toml +0 -9
  320. package/.next/standalone/crates/PROTOCOL.md +0 -122
  321. package/.next/standalone/crates/failproofaid/Cargo.toml +0 -18
  322. package/.next/standalone/crates/failproofaid/src/lock.rs +0 -81
  323. package/.next/standalone/crates/failproofaid/src/main.rs +0 -83
  324. package/.next/standalone/crates/failproofaid/src/paths.rs +0 -172
  325. package/.next/standalone/crates/failproofaid/src/server.rs +0 -502
  326. package/.next/standalone/crates/failproofaid/src/worker.rs +0 -400
  327. package/.next/standalone/crates/failproofaid/tests/daemon_e2e.rs +0 -241
  328. package/.next/standalone/crates/fpai-ipc/Cargo.toml +0 -16
  329. package/.next/standalone/crates/fpai-ipc/src/envelope.rs +0 -175
  330. package/.next/standalone/crates/fpai-ipc/src/framing.rs +0 -177
  331. package/.next/standalone/crates/fpai-ipc/src/lib.rs +0 -10
  332. package/.next/standalone/crates/fpai-ipc/src/peer.rs +0 -80
  333. package/.next/standalone/docker-hook-sync/Dockerfile +0 -79
  334. package/.next/standalone/docker-hook-sync/entrypoint.sh +0 -109
  335. package/.next/standalone/integration-suite/Dockerfile +0 -47
  336. package/.next/standalone/integration-suite/README.md +0 -129
  337. package/.next/standalone/integration-suite/canary-policies.mjs +0 -45
  338. package/.next/standalone/integration-suite/capture-tokens.sh +0 -48
  339. package/.next/standalone/integration-suite/ci-entrypoint.sh +0 -179
  340. package/.next/standalone/integration-suite/inject-tokens.sh +0 -44
  341. package/.next/standalone/integration-suite/install-clis.sh +0 -136
  342. package/.next/standalone/integration-suite/probe-cli.sh +0 -248
  343. package/.next/standalone/integration-suite/report.js +0 -120
  344. package/.next/standalone/integration-suite/run.sh +0 -159
  345. package/.next/standalone/openclaw-plugin/index.js +0 -223
  346. package/.next/standalone/openclaw-plugin/openclaw.plugin.json +0 -6
  347. package/.next/standalone/openclaw-plugin/package.json +0 -12
  348. package/.next/standalone/osv-scanner.toml +0 -17
  349. package/.next/standalone/pi-extension/index.ts +0 -524
  350. package/.next/standalone/pi-extension/package.json +0 -12
  351. package/.next/standalone/postcss.config.mjs +0 -8
  352. package/.next/standalone/readme-arch-hq.gif +0 -0
  353. package/.next/standalone/rust-toolchain.toml +0 -4
  354. package/src/auth/cli.ts +0 -371
  355. /package/.next/standalone/.next/static/{7Lxtd352rSaHB-pmm6G8M → pBBcTvE6_VueYGbacy1kg}/_buildManifest.js +0 -0
  356. /package/.next/standalone/.next/static/{7Lxtd352rSaHB-pmm6G8M → pBBcTvE6_VueYGbacy1kg}/_clientMiddlewareManifest.js +0 -0
  357. /package/.next/standalone/.next/static/{7Lxtd352rSaHB-pmm6G8M → pBBcTvE6_VueYGbacy1kg}/_ssgManifest.js +0 -0
@@ -11,12 +11,75 @@
11
11
  *
12
12
  * The ESM shim includes hooks API exports.
13
13
  */
14
- import { readFile, writeFile, unlink, access } from "fs/promises";
15
- import { resolve, dirname, relative } from "path";
14
+ import { readFile, writeFile, unlink, access, mkdir, lstat } from "fs/promises";
15
+ import { tmpdir } from "os";
16
+ import { resolve, dirname, relative, join } from "path";
16
17
  import { pathToFileURL } from "url";
18
+ import { createHash } from "crypto";
19
+ import { shimsDir } from "./fp-home";
20
+ import { hookLogWarn } from "./hook-logger";
17
21
 
18
22
  export const TMP_SUFFIX = ".__failproofai_tmp__.mjs";
19
23
 
24
+ /**
25
+ * Anything this loader generated, by name.
26
+ *
27
+ * The temporary tree is written BESIDE the user's sources, because that is the
28
+ * only place a rewritten relative import still resolves. That was harmless
29
+ * while the name was fixed — an abnormally terminated hook left at most one
30
+ * stale file per source, overwritten on the next load — but the name now
31
+ * carries a pid and a sequence number, so every kill (a CLI hook timeout, a
32
+ * Ctrl-C) leaves a uniquely-named file behind forever.
33
+ *
34
+ * Two consequences, and the second is the visible one: the directory fills up,
35
+ * and `findSkippedPolicyFiles` reports each leftover back to the user as a
36
+ * policy file that will not load — an accusation about a file failproofai
37
+ * wrote itself.
38
+ */
39
+ const TMP_ARTIFACT_RE = /\.__failproofai_tmp__\./;
40
+
41
+ /** True for a file this loader generated rather than one the user wrote. */
42
+ export function isTmpArtifact(name: string): boolean {
43
+ return TMP_ARTIFACT_RE.test(name);
44
+ }
45
+
46
+ /**
47
+ * How stale a generated file must be before a sweep will remove it.
48
+ *
49
+ * Another process may be mid-load right now, and its temporary tree is live
50
+ * until its `import()` resolves. Well past the 10s a module load is allowed,
51
+ * so a sweep can never delete a file another loader is still using.
52
+ */
53
+ const TMP_SWEEP_MIN_AGE_MS = 60_000;
54
+
55
+ /**
56
+ * Remove generated files left behind by a load that was killed.
57
+ *
58
+ * Best-effort and never throws: this runs on the hook path, and failing to
59
+ * tidy up is not worth failing an evaluation over.
60
+ */
61
+ export async function sweepStaleTmpArtifacts(dir: string, now = Date.now()): Promise<number> {
62
+ let removed = 0;
63
+ try {
64
+ const { readdir, stat } = await import("fs/promises");
65
+ for (const name of await readdir(dir)) {
66
+ if (!isTmpArtifact(name)) continue;
67
+ const path = resolve(dir, name);
68
+ try {
69
+ const info = await stat(path);
70
+ if (now - info.mtimeMs < TMP_SWEEP_MIN_AGE_MS) continue;
71
+ await unlink(path);
72
+ removed++;
73
+ } catch {
74
+ /* raced with another sweep, or not ours to remove */
75
+ }
76
+ }
77
+ } catch {
78
+ /* unreadable directory — nothing to sweep */
79
+ }
80
+ return removed;
81
+ }
82
+
20
83
  /** Regex to find local relative import specifiers (ESM). */
21
84
  const LOCAL_IMPORT_RE = /(?:import\s+(?:[\s\S]*?\s+from\s+)?|export\s+(?:[\s\S]*?\s+from\s+))(['"])(\.\.?\/[^'"]+)\1/g;
22
85
 
@@ -74,11 +137,57 @@ export async function resolveLocalImport(
74
137
  * Exports the full public API of failproofai: customPolicies, allow, deny, instruct,
75
138
  * getCustomHooks, clearCustomHooks.
76
139
  */
140
+ /**
141
+ * Create the ESM shim that lets a policy file's `import ... from 'failproofai'`
142
+ * resolve, and return where it landed.
143
+ *
144
+ * NOT beside `dist/index.js`, which is where this used to go. That directory
145
+ * belongs to whoever installed the package, and on a system-wide install
146
+ * (`sudo npm i -g`, a container image, a shared build host, a CI runner) it is
147
+ * root-owned — so every non-root user running a hook failed with EACCES, the
148
+ * policy never loaded, and the hook exited 0. Builtins kept firing while
149
+ * cloud-managed and custom policies silently stopped, so the machine looked
150
+ * protected. That is the worst available failure for a policy engine.
151
+ *
152
+ * Two locations, and the ORDER OF OPERATIONS matters more than the choice:
153
+ *
154
+ * 1. `<home>/state/shims` — created at 0700 and owned by the user running the
155
+ * hook. Unlike a shared `/tmp`, no other local user can pre-plant a file at
156
+ * a path we are about to `import`.
157
+ * 2. `os.tmpdir()` — only when the first is unusable.
158
+ *
159
+ * **The write lives inside the same `try` as the mkdir on purpose.** `mkdir`
160
+ * with `recursive` RESOLVES on a directory that already exists, whatever its
161
+ * mode — so a `state/shims` left behind unwritable (a container that ran the
162
+ * CLI as root and then dropped to a non-root user; this very function creates
163
+ * that chain) would sail past a mkdir-only guard and throw EACCES on the write,
164
+ * reproducing the exact fail-open this function exists to fix. Any failure of
165
+ * either step falls through to the fallback, and says so — a silent slide into
166
+ * the weaker path is the same class of bug again.
167
+ */
168
+ async function writeShim(
169
+ dir: string,
170
+ name: string,
171
+ code: string,
172
+ exclusive: boolean,
173
+ ): Promise<string> {
174
+ const shimPath = join(dir, name);
175
+ // 0600 explicitly: an unset mode is `0666 & ~umask`, and `umask 000` is
176
+ // routine in containers — which in the fallback means a world-writable file
177
+ // in shared /tmp that this process then imports.
178
+ await writeFile(shimPath, code, {
179
+ encoding: "utf-8",
180
+ mode: 0o600,
181
+ ...(exclusive ? { flag: "wx" as const } : {}),
182
+ });
183
+ return shimPath;
184
+ }
185
+
77
186
  export async function createEsmShim(
78
187
  distIndex: string,
79
188
  distUrl: string,
189
+ tmpSuffix = TMP_SUFFIX,
80
190
  ): Promise<{ shimPath: string; shimUrl: string }> {
81
- const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
82
191
  const shimCode = [
83
192
  `import _cjs from '${distUrl}';`,
84
193
  `export const customPolicies = _cjs.customPolicies;`,
@@ -89,7 +198,35 @@ export async function createEsmShim(
89
198
  `export const instruct = _cjs.instruct;`,
90
199
  `export default _cjs;`,
91
200
  ].join("\n");
92
- await writeFile(shimPath, shimCode, "utf-8");
201
+ // The name still carries `tmpSuffix` (pid + load sequence + a random id).
202
+ // `fingerprintTemporaryTree` normalises exactly that substring away before
203
+ // hashing, so the policy module cache still hits; a name it could not
204
+ // normalise would force a cold module load on every single hook call.
205
+ const name = `failproofai-dist${tmpSuffix}.shim.mjs`;
206
+
207
+ try {
208
+ const dir = shimsDir();
209
+ await mkdir(dir, { recursive: true, mode: 0o700 });
210
+ // `mkdir` accepts a pre-existing SYMLINK here and a plain write would
211
+ // follow it out of the home. Confirm we got a real directory we own,
212
+ // unreadable to anyone else, before writing something we are about to
213
+ // execute.
214
+ const st = await lstat(dir);
215
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
216
+ if (!st.isDirectory() || st.isSymbolicLink() || (uid !== null && st.uid !== uid) || st.mode & 0o077) {
217
+ throw new Error(`${dir} is not a private directory owned by this user`);
218
+ }
219
+ const shimPath = await writeShim(dir, name, shimCode, false);
220
+ return { shimPath, shimUrl: pathToFileURL(shimPath).href };
221
+ } catch (err) {
222
+ hookLogWarn(
223
+ `policy shim directory unusable (${err instanceof Error ? err.message : String(err)}); falling back to ${tmpdir()}`,
224
+ );
225
+ }
226
+
227
+ // Predictable name in a shared directory, so O_EXCL: refuse rather than write
228
+ // through something another user planted.
229
+ const shimPath = await writeShim(tmpdir(), name, shimCode, true);
93
230
  return { shimPath, shimUrl: pathToFileURL(shimPath).href };
94
231
  }
95
232
 
@@ -101,6 +238,17 @@ export async function rewriteFileTree(
101
238
  entryPath: string,
102
239
  distUrl: string | null,
103
240
  distIndex: string | null,
241
+ tmpSuffix = TMP_SUFFIX,
242
+ /**
243
+ * SHA-256 the ENTRY file's raw bytes must match, re-checked here at the moment
244
+ * they are read for rewriting and import. Cloud-managed policies pass their
245
+ * pinned digest so the bytes that get imported are the same bytes that were
246
+ * verified — closing a TOCTOU where an earlier caller hashed the file, then
247
+ * this function independently re-read it (a window in which a same-user
248
+ * attacker could swap the bytes so a verified file imports unverified code).
249
+ * Only the entry is content-addressed; transitive local imports are not.
250
+ */
251
+ entrySha256?: string | null,
104
252
  ): Promise<string[]> {
105
253
  const queue: string[] = [entryPath];
106
254
  const visited = new Set<string>();
@@ -108,7 +256,7 @@ export async function rewriteFileTree(
108
256
 
109
257
  let esmShimUrl: string | null = null;
110
258
  if (distIndex && distUrl) {
111
- const shim = await createEsmShim(distIndex, distUrl);
259
+ const shim = await createEsmShim(distIndex, distUrl, tmpSuffix);
112
260
  tmpFiles.push(shim.shimPath);
113
261
  esmShimUrl = shim.shimUrl;
114
262
  }
@@ -118,7 +266,22 @@ export async function rewriteFileTree(
118
266
  if (visited.has(filePath)) continue;
119
267
  visited.add(filePath);
120
268
 
121
- let code = await readFile(filePath, "utf-8");
269
+ let code: string;
270
+ if (entrySha256 && filePath === entryPath) {
271
+ // Read the raw bytes, verify the pin against THEM, then decode and rewrite
272
+ // that same in-memory content — filePath is never read again, so the bytes
273
+ // imported below are exactly the bytes verified here.
274
+ const buf = await readFile(filePath);
275
+ const actual = createHash("sha256").update(buf).digest("hex");
276
+ if (actual !== entrySha256) {
277
+ throw new Error(
278
+ `cloud-managed policy failed integrity re-verification at load: expected ${entrySha256}, got ${actual}`,
279
+ );
280
+ }
281
+ code = buf.toString("utf-8");
282
+ } else {
283
+ code = await readFile(filePath, "utf-8");
284
+ }
122
285
 
123
286
  // Rewrite 'failproofai' or legacy 'claudeye' imports to the ESM shim (or direct CJS for require)
124
287
  if (esmShimUrl) {
@@ -148,7 +311,7 @@ export async function rewriteFileTree(
148
311
  if (!visited.has(resolved) && !queue.includes(resolved)) {
149
312
  queue.push(resolved);
150
313
  }
151
- let relPath = relative(dir, resolved + TMP_SUFFIX).split("\\").join("/");
314
+ let relPath = relative(dir, resolved + tmpSuffix).split("\\").join("/");
152
315
  if (!relPath.startsWith(".")) relPath = "./" + relPath;
153
316
  rewrites.set(specifier, relPath);
154
317
  }
@@ -163,7 +326,7 @@ export async function rewriteFileTree(
163
326
  code = code.replace(new RegExp(`"${escaped}"`, "g"), `"${replacement}"`);
164
327
  }
165
328
 
166
- const tmpPath = filePath + TMP_SUFFIX;
329
+ const tmpPath = filePath + tmpSuffix;
167
330
  await writeFile(tmpPath, code, "utf-8");
168
331
  tmpFiles.push(tmpPath);
169
332
  }
@@ -24,6 +24,7 @@ import { trackHookEvent } from "./hook-telemetry";
24
24
  import { getInstanceId, hashToId } from "../../lib/telemetry-id";
25
25
  import { CliError } from "../cli-error";
26
26
  import { hookLogWarn } from "./hook-logger";
27
+ import { customPoliciesDir, globalPolicyConfigFile } from "./fp-home";
27
28
 
28
29
  const VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
29
30
 
@@ -645,7 +646,7 @@ export async function listHooks(cwd?: string): Promise<void> {
645
646
  } else {
646
647
  console.log("\n Run `failproofai policies --install` to get started.");
647
648
  }
648
- console.log(" Config: ~/.failproofai/policies-config.json\n");
649
+ console.log(` Config: ${globalPolicyConfigFile()}\n`);
649
650
  } else if (installedScopes.length === 1) {
650
651
  // State B: Single scope — table with header row
651
652
  const scope = installedScopes[0];
@@ -657,7 +658,7 @@ export async function listHooks(cwd?: string): Promise<void> {
657
658
  for (const policy of regularPolicies) printSimpleRow(policy);
658
659
  printBetaSection(printSimpleRow);
659
660
 
660
- console.log("\n Config: ~/.failproofai/policies-config.json\n");
661
+ console.log(`\n Config: ${globalPolicyConfigFile()}\n`);
661
662
  } else {
662
663
  // State C: Multiple scopes — column table
663
664
  const COL = 9;
@@ -701,7 +702,7 @@ export async function listHooks(cwd?: string): Promise<void> {
701
702
  for (const policy of betaPolicies) printMultiScopeRow(policy);
702
703
  }
703
704
 
704
- console.log("\n Config: ~/.failproofai/policies-config.json");
705
+ console.log(`\n Config: ${globalPolicyConfigFile()}`);
705
706
 
706
707
  // Multi-scope warning
707
708
  const scopeNames = installedScopes.join(", ");
@@ -765,7 +766,7 @@ export async function listHooks(cwd?: string): Promise<void> {
765
766
  // hook path was loading them.
766
767
  const base = findProjectConfigDir(cwd ?? process.cwd());
767
768
  const projectDir = resolve(base, ".failproofai", "policies");
768
- const userDir = resolve(homedir(), ".failproofai", "policies");
769
+ const userDir = customPoliciesDir();
769
770
  const sameDir = userDir === projectDir;
770
771
  const conventionDirs: { label: string; dir: string }[] = [
771
772
  { label: sameDir ? "Project + User" : "Project", dir: projectDir },
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Why the last first-run setup stopped, and whether it is worth offering again.
3
+ *
4
+ * # The problem this exists for
5
+ *
6
+ * Setup is deliberately all-or-nothing: every abort path writes NOTHING, so a
7
+ * machine that could not be configured is left exactly as it was found rather
8
+ * than carrying half a configuration (a `daemonConfigured` flag with no daemon
9
+ * behind it denies every tool call across all twelve CLIs). That invariant is
10
+ * right and is not changed here.
11
+ *
12
+ * But it left onboarding with no memory. `isConfigured()` reads three signals —
13
+ * a global policy config, live user-scope hooks, the legacy marker — and an
14
+ * abort sets none of them, so "never tried" and "tried twenty times and could
15
+ * not finish" are indistinguishable. On a machine without passwordless sudo,
16
+ * every single command relaunched the whole wizard, forever.
17
+ *
18
+ * # What this is NOT
19
+ *
20
+ * Not a fourth "configured" signal. Nothing here may make `isConfigured()` true.
21
+ * A machine that failed setup is unconfigured and must keep saying so — to the
22
+ * hook path, to `--status`, and to the wizard when it is asked for by name.
23
+ * This only changes whether the wizard is *offered unprompted*.
24
+ *
25
+ * # Re-offering
26
+ *
27
+ * A hint that never becomes an offer again is its own failure: the common case
28
+ * is someone who hits the sudo prompt, gives up, and comes back later able to
29
+ * elevate. So each reason carries a cheap local check for whether its blocker
30
+ * is gone, and the wizard offers itself again the moment one is:
31
+ *
32
+ * needs_root -> elevation is now possible without a prompt
33
+ * daemon_failed -> the service manager now reports something different
34
+ * cancelled -> the CLI version changed; an upgrade is a fair moment to
35
+ * ask again, and nothing else about a deliberate cancel
36
+ * should re-nag
37
+ *
38
+ * Every probe is local and takes milliseconds. None of them runs on the hook
39
+ * path — `--hook` never reaches the first-run gate at all.
40
+ */
41
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
42
+ import { dirname } from "node:path";
43
+ import { onboardingAttemptFile } from "./fp-home";
44
+ import type { WizardAbort } from "./configure-wizard";
45
+
46
+ /** Bumped only for a deliberate shape change; an unreadable record is ignored. */
47
+ const SCHEMA_VERSION = 1;
48
+
49
+ export interface OnboardingAttempt {
50
+ schemaVersion: number;
51
+ /** Why the wizard stopped. */
52
+ reason: WizardAbort;
53
+ /** The CLI that made the attempt, so an upgrade can re-offer. */
54
+ cliVersion: string;
55
+ /**
56
+ * What the service manager said at the time. Compared, never trusted — the
57
+ * point is only whether it has CHANGED since.
58
+ */
59
+ daemonStatus: string;
60
+ /** Epoch ms, for support and for nothing else. */
61
+ at: number;
62
+ }
63
+
64
+ export function readOnboardingAttempt(): OnboardingAttempt | null {
65
+ const path = onboardingAttemptFile();
66
+ if (!existsSync(path)) return null;
67
+ try {
68
+ const raw = JSON.parse(readFileSync(path, "utf8")) as Partial<OnboardingAttempt>;
69
+ if (raw.schemaVersion !== SCHEMA_VERSION) return null;
70
+ if (typeof raw.reason !== "string" || typeof raw.cliVersion !== "string") return null;
71
+ return {
72
+ schemaVersion: SCHEMA_VERSION,
73
+ reason: raw.reason as WizardAbort,
74
+ cliVersion: raw.cliVersion,
75
+ daemonStatus: typeof raw.daemonStatus === "string" ? raw.daemonStatus : "",
76
+ at: typeof raw.at === "number" ? raw.at : 0,
77
+ };
78
+ } catch {
79
+ // A corrupt record reads as "no record", which degrades to today's
80
+ // behaviour — offering the wizard. Never to "configured".
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /** Best-effort: a machine that cannot record this simply keeps being offered. */
86
+ export function recordOnboardingAttempt(
87
+ reason: WizardAbort,
88
+ cliVersion: string,
89
+ daemonStatus: string,
90
+ now: number = Date.now(),
91
+ ): void {
92
+ try {
93
+ const path = onboardingAttemptFile();
94
+ mkdirSync(dirname(path), { recursive: true });
95
+ const record: OnboardingAttempt = {
96
+ schemaVersion: SCHEMA_VERSION,
97
+ reason,
98
+ cliVersion,
99
+ daemonStatus,
100
+ at: now,
101
+ };
102
+ writeFileSync(path, JSON.stringify(record, null, 2), "utf8");
103
+ } catch {
104
+ // best-effort
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Called on a completed apply. A stale record would keep suppressing the offer
110
+ * on a machine that has since been reconfigured by hand and un-configured again.
111
+ */
112
+ export function clearOnboardingAttempt(): void {
113
+ try {
114
+ rmSync(onboardingAttemptFile(), { force: true });
115
+ } catch {
116
+ // best-effort
117
+ }
118
+ }
119
+
120
+ export interface RetryProbe {
121
+ /** Elevation is possible without prompting. */
122
+ canElevate: () => boolean;
123
+ /** What the service manager says right now. */
124
+ daemonStatus: () => string;
125
+ /** This CLI's version. */
126
+ cliVersion: string;
127
+ }
128
+
129
+ /**
130
+ * Whether the thing that stopped setup last time has since changed.
131
+ *
132
+ * `true` means offer the wizard again; `false` means a one-line hint. An
133
+ * unrecognised reason returns `true` — an unknown blocker is one we cannot
134
+ * prove is still present, and the safe direction is offering setup rather than
135
+ * silently withholding it.
136
+ */
137
+ export function blockerCleared(attempt: OnboardingAttempt, probe: RetryProbe): boolean {
138
+ switch (attempt.reason) {
139
+ case "needs_root":
140
+ return probe.canElevate();
141
+ case "daemon_failed":
142
+ // Any movement at all: not-installed -> stopped, stopped -> running, or a
143
+ // platform that has gained a service manager. Comparing rather than
144
+ // demanding "running" is what lets a partially-repaired machine be
145
+ // offered setup again instead of waiting for a state it cannot reach on
146
+ // its own.
147
+ return probe.daemonStatus() !== attempt.daemonStatus || probe.canElevate();
148
+ case "cancelled":
149
+ // A deliberate stop. Re-asking on the next command is exactly the nagging
150
+ // this module removes — but an upgrade is a new thing to say, so it is
151
+ // allowed to ask once more.
152
+ return probe.cliVersion !== attempt.cliVersion;
153
+ case "not_a_tty":
154
+ case "running_as_sudo":
155
+ // Both are properties of the invocation, not of the machine, so the next
156
+ // one may well be fine.
157
+ return true;
158
+ default:
159
+ return true;
160
+ }
161
+ }
162
+
163
+ /** The one line shown instead of relaunching the wizard. */
164
+ export function attemptHintLines(attempt: OnboardingAttempt): string[] {
165
+ const why: Record<string, string> = {
166
+ needs_root: "it needs root to install the failproofaid service",
167
+ daemon_failed: "the failproofaid service could not be started",
168
+ cancelled: "it was cancelled",
169
+ not_a_tty: "there was no terminal to ask in",
170
+ running_as_sudo: "it was run under sudo",
171
+ };
172
+ const detail = why[attempt.reason] ?? "it did not finish";
173
+ return [
174
+ ``,
175
+ `[failproofai] Setup is not finished — ${detail}.`,
176
+ ` Run \`failproofai config\` when you are ready.`,
177
+ ``,
178
+ ];
179
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * A single-holder guard around first-run onboarding.
3
+ *
4
+ * Onboarding used to fire only on a bare `failproofai`. It now fires on any
5
+ * command an unconfigured machine runs, which makes concurrent runs a real
6
+ * shape rather than a theoretical one: two terminals, `failproofai audit` in
7
+ * each, and both draw a wizard. They would then race on the same settings
8
+ * files and, worse, both try to install the system service — two `sudo`
9
+ * prompts and two `systemctl enable` calls against one unit.
10
+ *
11
+ * ## Why liveness, not a timeout
12
+ *
13
+ * The obvious lock carries a timestamp and is treated as stale after N
14
+ * seconds. That cannot work here: the holder is a human answering questions,
15
+ * so any N short enough to recover promptly from a crash is also short enough
16
+ * to evict someone who walked away mid-setup. So the lock records a PID and is
17
+ * considered held only while that process is actually alive — which recovers
18
+ * instantly from a crash or a Ctrl-C, and never evicts a live wizard no matter
19
+ * how long it sits.
20
+ *
21
+ * ## Failure direction
22
+ *
23
+ * Every failure here resolves to "do not onboard". A lock we cannot take is
24
+ * indistinguishable from one already held, and skipping onboarding costs a
25
+ * user one printed hint, where wrongly proceeding costs a duplicated install.
26
+ * The command the user typed always runs either way.
27
+ */
28
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
29
+ import { homedir } from "node:os";
30
+ import { resolve } from "node:path";
31
+ import { onboardingLockFile, stateDir } from "./fp-home";
32
+
33
+ interface LockBody {
34
+ pid: number;
35
+ startedAt: number;
36
+ }
37
+
38
+ export function onboardingLockPath(home: string = homedir()): string {
39
+ return onboardingLockFile(home);
40
+ }
41
+
42
+ /**
43
+ * Whether `pid` is a live process this user can signal.
44
+ *
45
+ * `kill(pid, 0)` performs the permission and existence checks without
46
+ * delivering a signal. `EPERM` means the process exists but belongs to another
47
+ * user — still alive, so still holding the lock.
48
+ */
49
+ function processAlive(pid: number): boolean {
50
+ if (!Number.isInteger(pid) || pid <= 0) return false;
51
+ try {
52
+ process.kill(pid, 0);
53
+ return true;
54
+ } catch (err) {
55
+ return (err as NodeJS.ErrnoException)?.code === "EPERM";
56
+ }
57
+ }
58
+
59
+ function readLock(path: string): LockBody | null {
60
+ try {
61
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<LockBody>;
62
+ if (typeof parsed.pid !== "number") return null;
63
+ return { pid: parsed.pid, startedAt: Number(parsed.startedAt) || 0 };
64
+ } catch {
65
+ // Absent, unreadable, or truncated by a crash mid-write. All three mean
66
+ // "no live holder we can prove", and the liveness check below is what
67
+ // actually decides.
68
+ return null;
69
+ }
70
+ }
71
+
72
+ export interface OnboardingLock {
73
+ /** Release the lock. Safe to call more than once. */
74
+ release(): void;
75
+ }
76
+
77
+ /**
78
+ * Take the onboarding lock, or return `null` if another live process holds it.
79
+ *
80
+ * There is a small window between the liveness check and the write in which
81
+ * two processes could both decide the lock is free. That is accepted
82
+ * deliberately: closing it needs `O_EXCL` plus stale-file recovery, which
83
+ * reintroduces exactly the timeout problem this design avoids — and the
84
+ * consequence of losing that race is two wizards, which is the situation
85
+ * before this existed, not something worse.
86
+ */
87
+ export function acquireOnboardingLock(home: string = homedir()): OnboardingLock | null {
88
+ const path = onboardingLockPath(home);
89
+ try {
90
+ const held = readLock(path);
91
+ // Our own PID appearing here means a previous run in this same process
92
+ // already holds it — re-entering is fine and must not deadlock.
93
+ if (held && held.pid !== process.pid && processAlive(held.pid)) return null;
94
+
95
+ mkdirSync(stateDir(home), { recursive: true });
96
+ const body: LockBody = { pid: process.pid, startedAt: Date.now() };
97
+ writeFileSync(path, JSON.stringify(body), "utf8");
98
+
99
+ let released = false;
100
+ return {
101
+ release() {
102
+ if (released) return;
103
+ released = true;
104
+ try {
105
+ // Only remove a lock still recorded as ours: if this process was
106
+ // evicted as dead and another wizard took over, deleting the file
107
+ // would release someone else's lock.
108
+ const current = readLock(path);
109
+ if (!current || current.pid === process.pid) rmSync(path, { force: true });
110
+ } catch {
111
+ /* best effort — a stray lock is reclaimed by the liveness check */
112
+ }
113
+ },
114
+ };
115
+ } catch {
116
+ // Unwritable home, read-only filesystem, whatever it is: treat as held.
117
+ return null;
118
+ }
119
+ }
@@ -80,8 +80,36 @@ export interface LlmConfig {
80
80
  model?: string;
81
81
  }
82
82
 
83
+ /**
84
+ * The collector's non-secret settings. The ingest credential deliberately does
85
+ * NOT live here: this file is written with a bare `writeFileSync`, so it
86
+ * inherits the umask and lands at 0664 inside a 0775 `~/.failproofai/` — an API
87
+ * key here would be readable by every local user. It lives in
88
+ * `~/.failproofai/ingest.json` at 0600 instead.
89
+ *
90
+ * Read by the Rust daemon (`crates/fpai-collect/src/config.rs`); the field
91
+ * names must stay in step with the `Settings` struct there.
92
+ */
93
+ export interface CollectorConfig {
94
+ /**
95
+ * Ship agent session transcripts. Defaults to false and is a SEPARATE opt-in
96
+ * from configuring a key, because transcripts carry prompts, file contents
97
+ * and whatever was pasted into a terminal.
98
+ */
99
+ sessions?: boolean;
100
+ /** Ship hook activity. Carries decisions and tool names, never file contents. */
101
+ hooks?: boolean;
102
+ /** `decisions` keeps every deny/instruct exact and aggregates the ~99% allows. */
103
+ hooksVerbosity?: "all" | "decisions" | "off";
104
+ redact?: "minimal" | "off";
105
+ /** Label stamped on every event. Must not contain a comma — ingest skips those lines. */
106
+ environment?: string;
107
+ }
108
+
83
109
  export interface HooksConfig {
84
110
  enabledPolicies: string[];
111
+ /** Collector settings; see {@link CollectorConfig}. Absent means unconfigured. */
112
+ collector?: CollectorConfig;
85
113
  llm?: LlmConfig;
86
114
  policyParams?: Record<string, Record<string, unknown>>;
87
115
  /** Explicit custom policy files, loaded in array order. */