failproofai 1.0.0-beta.5 → 1.0.0-beta.7

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 (368) 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 +3 -3
  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/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +3 -1
  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 +2 -2
  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/_head.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +3 -1
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
  33. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -1
  37. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -2
  39. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
  41. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +3 -2
  43. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -2
  45. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  47. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/audit/page.js +5 -3
  50. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/index.html +1 -1
  53. package/.next/standalone/.next/server/app/index.rsc +15 -15
  54. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  55. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  56. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  57. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  58. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  59. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/page.js +3 -1
  61. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  64. package/.next/standalone/.next/server/app/policies/page.js +4 -2
  65. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -2
  69. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  73. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
  74. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  77. package/.next/standalone/.next/server/app/projects/page.js +4 -2
  78. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
  81. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +18 -0
  82. package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +6 -0
  83. package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
  84. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +53 -0
  85. package/.next/standalone/.next/server/app/settings/page.js +18 -0
  86. package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
  87. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
  88. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
  89. package/.next/standalone/.next/server/app-paths-manifest.json +2 -1
  90. package/.next/standalone/.next/server/chunks/{[externals]__1nl3dvw._.js → [externals]__0r-zamh._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g0vcwt._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0h3lfmp._.js → [root-of-the-server]__0ky_n0r._.js} +2 -2
  97. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__18d-9oi._.js → [root-of-the-server]__0lk_935._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p7s8-d._.js → [root-of-the-server]__0pbp9fe._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__10oa-v9._.js → [root-of-the-server]__0v2fpxe._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0sb_5m8._.js → [root-of-the-server]__131ebum._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-dh5jy._.js → [root-of-the-server]__1_ps0jw._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l4vuqm._.js → [root-of-the-server]__1a7i2q4._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +5 -0
  109. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +5 -0
  110. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +5 -0
  111. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
  112. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06bp_fg._.js +5 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__074qmkp._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c_qcgp._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kbc9bs._.js +3 -0
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +10 -10
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1a2hcb_._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rtdfof._.js → [root-of-the-server]__1rdruw4._.js} +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +5 -0
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +4 -0
  123. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rag5dg._.js → [root-of-the-server]__1yw368c._.js} +2 -2
  125. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +3 -0
  129. package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +3 -0
  131. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/_194yh9u._.js +3 -0
  133. package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +3 -0
  134. package/.next/standalone/.next/server/chunks/ssr/_1gmzkwj._.js +5 -0
  135. package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +3 -0
  137. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  138. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  140. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dwo94s.js +4 -0
  141. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +5 -0
  143. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  144. package/.next/standalone/.next/server/middleware.js +2 -2
  145. package/.next/standalone/.next/server/pages/404.html +1 -1
  146. package/.next/standalone/.next/server/pages/500.html +1 -1
  147. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  148. package/.next/standalone/.next/server/server-reference-manifest.json +54 -12
  149. package/.next/standalone/.next/static/chunks/0945ybz3fsvba.js +1 -0
  150. package/.next/standalone/.next/static/chunks/{0cr85wuc9gc9k.js → 0gr76ps8_zm26.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/{2cqk4a9s893oy.js → 0m-m7wgtxdoav.js} +1 -1
  152. package/.next/standalone/.next/static/chunks/{1tp9dex23zs8y.js → 11l6oh9cyb35a.js} +1 -1
  153. package/.next/standalone/.next/static/chunks/166f8qnh03jqc.js +6 -0
  154. package/.next/standalone/.next/static/chunks/1bm0q12hgs22l.css +2 -0
  155. package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +1 -0
  156. package/.next/standalone/.next/static/chunks/{2s9g1dsde_ts1.js → 2aej0vi1fm87m.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{1bkynffp3q1f7.js → 2bxld6-rym3ah.js} +22 -22
  158. package/.next/standalone/.next/static/chunks/{2v1rpzrh2i1wv.js → 2p2kz4aj1tooj.js} +1 -1
  159. package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +2 -0
  160. package/.next/standalone/.next/static/chunks/36pc3_8fpb6ic.js +1 -0
  161. package/.next/standalone/.next/static/chunks/{2k285z07qce47.js → 3t1wr_kx6brwc.js} +1 -1
  162. package/.next/standalone/app/actions/get-hooks-config.ts +2 -1
  163. package/.next/standalone/app/actions/get-scheduled-audit.ts +70 -0
  164. package/.next/standalone/app/actions/update-scheduled-audit.ts +44 -0
  165. package/.next/standalone/app/api/audit/run/route.ts +28 -0
  166. package/.next/standalone/app/api/audit/status/route.ts +13 -1
  167. package/.next/standalone/app/api/auth/status/route.ts +1 -1
  168. package/.next/standalone/app/audit/_components/auth-dialog.css +159 -0
  169. package/.next/standalone/app/audit/_components/auth-dialog.tsx +3 -0
  170. package/.next/standalone/app/audit/audit-styles.css +6 -149
  171. package/.next/standalone/app/settings/page.tsx +31 -0
  172. package/.next/standalone/app/settings/settings-client.tsx +488 -0
  173. package/.next/standalone/components/navbar.tsx +2 -0
  174. package/.next/standalone/lib/auth/api-server-client.ts +2 -3
  175. package/.next/standalone/lib/auth/auth-store.ts +4 -4
  176. package/.next/standalone/lib/codex-sessions.ts +2 -1
  177. package/.next/standalone/lib/dashboard-host.ts +101 -0
  178. package/.next/standalone/lib/install-check.ts +22 -7
  179. package/.next/standalone/lib/pi-sessions.ts +99 -14
  180. package/.next/standalone/lib/telemetry-enabled.ts +54 -0
  181. package/.next/standalone/lib/telemetry-id.ts +52 -1
  182. package/.next/standalone/lib/telemetry.ts +8 -4
  183. package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
  184. package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
  185. package/.next/standalone/node_modules/react/package.json +3 -3
  186. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
  187. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
  188. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
  189. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
  190. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
  191. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
  192. package/.next/standalone/node_modules/react-dom/package.json +4 -4
  193. package/.next/standalone/package.json +7 -6
  194. package/.next/standalone/proxy.ts +89 -1
  195. package/.next/standalone/server.js +1 -1
  196. package/bin/failproofai.mjs +202 -86
  197. package/dist/cli.mjs +17803 -15346
  198. package/dist/worker.mjs +1620 -257
  199. package/lib/auth/api-server-client.ts +2 -3
  200. package/lib/auth/auth-store.ts +4 -4
  201. package/lib/codex-sessions.ts +2 -1
  202. package/lib/dashboard-host.ts +101 -0
  203. package/lib/install-check.ts +22 -7
  204. package/lib/pi-sessions.ts +99 -14
  205. package/lib/telemetry-enabled.ts +54 -0
  206. package/lib/telemetry-id.ts +52 -1
  207. package/lib/telemetry.ts +8 -4
  208. package/package.json +7 -6
  209. package/scripts/install-telemetry.mjs +8 -1
  210. package/scripts/launch.ts +25 -3
  211. package/scripts/parse-script-args.ts +17 -1
  212. package/scripts/prune-standalone.mjs +20 -0
  213. package/scripts/translate-docs/readme-translator.ts +163 -4
  214. package/scripts/validate-mdx.ts +153 -13
  215. package/src/audit/audit-lock.ts +285 -0
  216. package/src/audit/audit-schedule.ts +111 -0
  217. package/src/audit/cache.ts +18 -4
  218. package/src/audit/cli-adapters/goose.ts +2 -1
  219. package/src/audit/cli-adapters/hermes.ts +28 -10
  220. package/src/audit/cli.ts +191 -35
  221. package/src/audit/dashboard-cache.ts +2 -1
  222. package/src/hooks/builtin-policies.ts +100 -3
  223. package/src/hooks/cloud-connection.ts +160 -14
  224. package/src/hooks/cloud-enrollment-cli.ts +119 -21
  225. package/src/hooks/cloud-enrollment.ts +87 -17
  226. package/src/hooks/cloud-introspect.ts +147 -0
  227. package/src/hooks/cloud-managed-policies.ts +33 -2
  228. package/src/hooks/collector-config.ts +25 -43
  229. package/src/hooks/configure-wizard.ts +750 -318
  230. package/src/hooks/custom-hooks-loader.ts +125 -11
  231. package/src/hooks/daemon-client.ts +83 -17
  232. package/src/hooks/daemon-download.ts +54 -2
  233. package/src/hooks/daemon-service.ts +605 -29
  234. package/src/hooks/first-run-gate.ts +39 -0
  235. package/src/hooks/fp-config.ts +497 -0
  236. package/src/hooks/fp-home.ts +355 -0
  237. package/src/hooks/fp-reset.ts +332 -0
  238. package/src/hooks/handler.ts +42 -1
  239. package/src/hooks/hook-activity-store.ts +35 -20
  240. package/src/hooks/hook-logger.ts +2 -1
  241. package/src/hooks/hook-telemetry.ts +5 -1
  242. package/src/hooks/hooks-config.ts +4 -3
  243. package/src/hooks/loader-utils.ts +166 -5
  244. package/src/hooks/manager.ts +5 -4
  245. package/src/hooks/onboarding-attempt.ts +179 -0
  246. package/src/hooks/onboarding-lock.ts +119 -0
  247. package/src/hooks/policy-types.ts +0 -7
  248. package/src/hooks/session-pause-cli.ts +15 -13
  249. package/src/hooks/session-pause.ts +36 -4
  250. package/src/hooks/setup-state.ts +256 -0
  251. package/src/hooks/tui.ts +4 -4
  252. package/src/hooks/worker-server.ts +53 -1
  253. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0szqzl8._.js +0 -3
  255. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +0 -3
  260. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +0 -4
  261. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__094nop3._.js +0 -3
  262. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +0 -4
  263. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +0 -4
  264. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ls0gsr._.js +0 -4
  265. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n_uvv0._.js +0 -3
  266. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +0 -4
  267. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +0 -4
  268. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +0 -3
  269. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1gak4pd._.js +0 -3
  270. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1j4u3yd._.js +0 -4
  271. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +0 -4
  272. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +0 -3
  273. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_12c_2cz.js +0 -3
  274. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +0 -5
  275. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +0 -5
  276. package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +0 -1
  277. package/.next/standalone/.next/static/chunks/1bw4dfzflelcv.js +0 -1
  278. package/.next/standalone/.next/static/chunks/34sjdw8lv46e3.js +0 -6
  279. package/.next/standalone/.next/static/chunks/3i27c3hcriawq.css +0 -1
  280. package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +0 -2
  281. package/.next/standalone/Cargo.lock +0 -2222
  282. package/.next/standalone/Cargo.toml +0 -9
  283. package/.next/standalone/crates/CLOUD_POLICIES.md +0 -136
  284. package/.next/standalone/crates/PROTOCOL.md +0 -122
  285. package/.next/standalone/crates/failproofaid/Cargo.toml +0 -25
  286. package/.next/standalone/crates/failproofaid/src/cloud_client.rs +0 -484
  287. package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +0 -921
  288. package/.next/standalone/crates/failproofaid/src/lock.rs +0 -81
  289. package/.next/standalone/crates/failproofaid/src/main.rs +0 -639
  290. package/.next/standalone/crates/failproofaid/src/paths.rs +0 -201
  291. package/.next/standalone/crates/failproofaid/src/server.rs +0 -502
  292. package/.next/standalone/crates/failproofaid/src/worker.rs +0 -400
  293. package/.next/standalone/crates/failproofaid/tests/daemon_e2e.rs +0 -241
  294. package/.next/standalone/crates/fpai-collect/Cargo.toml +0 -43
  295. package/.next/standalone/crates/fpai-collect/src/config.rs +0 -420
  296. package/.next/standalone/crates/fpai-collect/src/cursor.rs +0 -520
  297. package/.next/standalone/crates/fpai-collect/src/delivery.rs +0 -472
  298. package/.next/standalone/crates/fpai-collect/src/filetail.rs +0 -487
  299. package/.next/standalone/crates/fpai-collect/src/health.rs +0 -369
  300. package/.next/standalone/crates/fpai-collect/src/lib.rs +0 -34
  301. package/.next/standalone/crates/fpai-collect/src/redact.rs +0 -516
  302. package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +0 -279
  303. package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +0 -619
  304. package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +0 -156
  305. package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +0 -715
  306. package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +0 -158
  307. package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +0 -512
  308. package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +0 -511
  309. package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +0 -586
  310. package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +0 -318
  311. package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +0 -579
  312. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +0 -314
  313. package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +0 -448
  314. package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +0 -23
  315. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +0 -176
  316. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +0 -500
  317. package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +0 -315
  318. package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +0 -493
  319. package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +0 -201
  320. package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +0 -533
  321. package/.next/standalone/crates/fpai-collect/src/spool.rs +0 -535
  322. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +0 -390
  323. package/.next/standalone/crates/fpai-collect/src/supervisor.rs +0 -423
  324. package/.next/standalone/crates/fpai-collect/src/uploader.rs +0 -600
  325. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +0 -739
  326. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +0 -394
  327. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +0 -976
  328. package/.next/standalone/crates/fpai-collect/tests/config.rs +0 -286
  329. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +0 -1020
  330. package/.next/standalone/crates/fpai-collect/tests/delivery.rs +0 -281
  331. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +0 -1009
  332. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +0 -1130
  333. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +0 -622
  334. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +0 -855
  335. package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +0 -1478
  336. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +0 -1192
  337. package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +0 -290
  338. package/.next/standalone/crates/fpai-collect/tests/uploader.rs +0 -506
  339. package/.next/standalone/crates/fpai-ipc/Cargo.toml +0 -16
  340. package/.next/standalone/crates/fpai-ipc/src/envelope.rs +0 -175
  341. package/.next/standalone/crates/fpai-ipc/src/framing.rs +0 -177
  342. package/.next/standalone/crates/fpai-ipc/src/lib.rs +0 -10
  343. package/.next/standalone/crates/fpai-ipc/src/peer.rs +0 -80
  344. package/.next/standalone/docker-hook-sync/Dockerfile +0 -79
  345. package/.next/standalone/docker-hook-sync/entrypoint.sh +0 -109
  346. package/.next/standalone/integration-suite/Dockerfile +0 -47
  347. package/.next/standalone/integration-suite/README.md +0 -129
  348. package/.next/standalone/integration-suite/canary-policies.mjs +0 -45
  349. package/.next/standalone/integration-suite/capture-tokens.sh +0 -48
  350. package/.next/standalone/integration-suite/ci-entrypoint.sh +0 -179
  351. package/.next/standalone/integration-suite/inject-tokens.sh +0 -44
  352. package/.next/standalone/integration-suite/install-clis.sh +0 -136
  353. package/.next/standalone/integration-suite/probe-cli.sh +0 -248
  354. package/.next/standalone/integration-suite/report.js +0 -120
  355. package/.next/standalone/integration-suite/run.sh +0 -159
  356. package/.next/standalone/openclaw-plugin/index.js +0 -223
  357. package/.next/standalone/openclaw-plugin/openclaw.plugin.json +0 -6
  358. package/.next/standalone/openclaw-plugin/package.json +0 -12
  359. package/.next/standalone/osv-scanner.toml +0 -17
  360. package/.next/standalone/pi-extension/index.ts +0 -524
  361. package/.next/standalone/pi-extension/package.json +0 -12
  362. package/.next/standalone/postcss.config.mjs +0 -8
  363. package/.next/standalone/readme-arch-hq.gif +0 -0
  364. package/.next/standalone/rust-toolchain.toml +0 -4
  365. package/src/auth/cli.ts +0 -371
  366. /package/.next/standalone/.next/static/{LOuwMmfomO6hQYfMQYs4K → LUaLkKg40nSkHrIajexIk}/_buildManifest.js +0 -0
  367. /package/.next/standalone/.next/static/{LOuwMmfomO6hQYfMQYs4K → LUaLkKg40nSkHrIajexIk}/_clientMiddlewareManifest.js +0 -0
  368. /package/.next/standalone/.next/static/{LOuwMmfomO6hQYfMQYs4K → LUaLkKg40nSkHrIajexIk}/_ssgManifest.js +0 -0
@@ -1,20 +1,34 @@
1
1
  /**
2
2
  * `failproofai config` — the interactive setup launcher.
3
3
  *
4
- * A single guided flow that sets up the whole failproofai ecosystem, hiding the
5
- * scope / cli / two-layer machinery behind three plain questions:
6
- * 1. Where? global (user) vs this project
7
- * 2. Assistants? multi-select of agent CLIs (detected + install-ahead)
8
- * 3. Policies? multi-select of themed presets (combine any) or Everything
9
- * …then a Review screen that shows exactly which files change, and Apply.
4
+ * A single guided flow that sets up the whole failproofai ecosystem:
5
+ *
6
+ * 0. Daemon — REQUIRED. Asks for sudo first, on a clean terminal.
7
+ * 1. Where — inferred from cwd, then confirmed (global / project / both)
8
+ * 2. Policies — multi-select of themed presets (combine any) or Everything
9
+ * 3. Assistants— multi-select of agent CLIs (detected + install-ahead)
10
+ * 4. Connect — paste an API key, or stay fully local
11
+ * 5. Review — shows exactly which files change, then Apply.
10
12
  *
11
13
  * Selections REPLACE the enabled set at the chosen scope (the picker pre-checks
12
14
  * whatever is already enabled, so unticking removes). Reuses the tested
13
15
  * install/uninstall manager and the existing searchable policy picker.
16
+ *
17
+ * ## Two ordering rules that are not cosmetic
18
+ *
19
+ * **The daemon comes first because it is the only step that needs a password.**
20
+ * `sudo -v` must prompt on a clean terminal, before any TUI frame is drawn —
21
+ * fired from underneath a rendered screen the prompt is invisible and the typed
22
+ * password lands in a redrawn frame.
23
+ *
24
+ * **The daemon is INSTALLED first, before any user config is written.** Setup
25
+ * requires it, so a failure has to leave the machine exactly as it was found
26
+ * rather than half-configured. Writing hooks first and discovering the service
27
+ * will not start afterwards is the one ordering that cannot be undone cleanly.
14
28
  */
15
29
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
16
- import { homedir, hostname } from "node:os";
17
- import { resolve, sep } from "node:path";
30
+ import { homedir } from "node:os";
31
+ import { dirname, resolve, sep } from "node:path";
18
32
 
19
33
  import {
20
34
  selectOne,
@@ -47,30 +61,83 @@ import { discoverPolicyFiles, findSkippedPolicyFiles } from "./custom-hooks-load
47
61
  import { trackHookEvent } from "./hook-telemetry";
48
62
  import { getInstanceId } from "../../lib/telemetry-id";
49
63
  import {
64
+ canElevate,
50
65
  isDaemonSupportedPlatform,
51
66
  installDaemonService,
52
67
  daemonServiceFilePath,
68
+ daemonServiceStatus,
69
+ daemonServiceNeedsUpgrade,
70
+ daemonStatusCommand,
71
+ ensureDaemonServiceCurrent,
72
+ daemonVersionSkew,
53
73
  primeElevation,
54
74
  setDaemonConfigured,
75
+ probeDaemonEndToEnd,
76
+ uninstallDaemonService,
55
77
  } from "./daemon-service";
56
78
  import { hookLogWarn } from "./hook-logger";
57
79
  import {
58
80
  readCloudCredentials,
81
+ resolveMachineId,
82
+ resolveMachineLabel,
83
+ validateCloudUrl,
59
84
  verifyCloudCredentials,
60
85
  writeCloudCredentials,
61
86
  } from "./cloud-enrollment";
62
- import { cloudBaseFor, ingestUrlFor } from "./cloud-connection";
87
+ import {
88
+ cloudBaseFor,
89
+ ingestUrlFor,
90
+ connectToCloud,
91
+ describeOutcome,
92
+ } from "./cloud-connection";
93
+ import {
94
+ detectSetupState,
95
+ isConfigured,
96
+ buildTargetChoices,
97
+ scopesFor,
98
+ type SetupTarget,
99
+ } from "./setup-state";
100
+ import { customPoliciesDir, launcherMarker } from "./fp-home";
101
+ import { pruneOldDaemonBinaries } from "./daemon-download";
102
+ import { version as cliVersion } from "../../package.json";
103
+ import {
104
+ attemptHintLines,
105
+ blockerCleared,
106
+ clearOnboardingAttempt,
107
+ readOnboardingAttempt,
108
+ recordOnboardingAttempt,
109
+ type RetryProbe,
110
+ } from "./onboarding-attempt";
111
+ import { acquireOnboardingLock } from "./onboarding-lock";
63
112
 
64
113
  export interface WizardIO {
65
114
  stdin?: TTYIn;
66
115
  stdout?: TTYOut;
67
116
  }
68
117
 
118
+ /**
119
+ * Why a wizard run ended without applying. Distinguished so the caller can
120
+ * pick an exit code — a user who pressed Esc did nothing wrong (exit 0), a
121
+ * machine that could not install the required daemon did not get set up
122
+ * (exit 1), and a fleet script needs to tell those apart.
123
+ */
124
+ export type WizardAbort =
125
+ | "cancelled"
126
+ | "needs_root"
127
+ | "daemon_failed"
128
+ | "not_a_tty"
129
+ | "running_as_sudo";
130
+
69
131
  export interface WizardResult {
70
132
  applied: boolean;
71
- scope?: HookScope;
133
+ /** Present only when `applied` is false. */
134
+ abort?: WizardAbort;
135
+ target?: SetupTarget;
136
+ scopes?: HookScope[];
72
137
  clis?: IntegrationType[];
73
138
  policies?: string[];
139
+ daemonInstalled?: boolean;
140
+ connected?: boolean;
74
141
  }
75
142
 
76
143
  async function emit(event: string, props: Record<string, unknown>): Promise<void> {
@@ -93,21 +160,6 @@ function homeify(p: string): string {
93
160
 
94
161
  // ── Pure builders (exported for tests) ───────────────────────────────────────
95
162
 
96
- export function buildScopeChoices(cwd: string) {
97
- return [
98
- {
99
- label: "Everywhere I code",
100
- value: "user" as HookScope,
101
- hint: "global · applies in every project on this machine",
102
- },
103
- {
104
- label: "Just this project",
105
- value: "project" as HookScope,
106
- hint: homeify(cwd),
107
- },
108
- ];
109
- }
110
-
111
163
  /** The CLIs that can actually be configured at `scope`. Hermes and OpenClaw
112
164
  * are gateways with no project-level config, so they are user-scope only. */
113
165
  export function clisSupportingScope(scope: HookScope): IntegrationType[] {
@@ -338,7 +390,12 @@ export function describeCustomPolicies(cwd: string): {
338
390
  let fileCount = 0;
339
391
  const dirs: Array<{ dir: string; label: string }> = [
340
392
  { dir: resolve(cwd, ".failproofai", "policies"), label: "project" },
341
- { dir: resolve(homedir(), ".failproofai", "policies"), label: "global" },
393
+ // `customPoliciesDir()`, not layout 1's `~/.failproofai/policies`. This
394
+ // scanned the old location while `custom-hooks-loader.ts` loads from the
395
+ // new one, so the wizard reported "no personal policies" to users whose
396
+ // policies were being enforced, and would have reported the opposite after
397
+ // the layout reset moved them.
398
+ { dir: customPoliciesDir(), label: "global" },
342
399
  ];
343
400
  for (const { dir, label } of dirs) {
344
401
  const found = discoverPolicyFiles(dir);
@@ -358,22 +415,30 @@ export function describeCustomPolicies(cwd: string): {
358
415
  }
359
416
 
360
417
  export function reviewLines(state: {
361
- scope: HookScope;
418
+ /** What the scope step resolved to. Expands to one or two real scopes. */
419
+ target: SetupTarget;
362
420
  clis: IntegrationType[];
363
421
  policies: string[];
364
422
  cwd: string;
365
423
  /** The Custom checkbox. `undefined` = nothing to toggle, leave as-is. */
366
424
  customEnabled?: boolean;
367
425
  /**
368
- * Step 0's answer. The daemon is machine-level, so this is a decision the
369
- * user made explicitly rather than something inferred from the scope —
370
- * showing it any other way would promise a service the apply won't install.
426
+ * Whether a daemon will be installed by this run. False when one is already
427
+ * healthy (nothing to do) or the platform has no service manager — showing
428
+ * it any other way would promise work the apply will not perform.
371
429
  */
372
430
  installDaemon?: boolean;
431
+ /** Whether an API key will be written and reporting turned on. */
432
+ connect?: boolean;
373
433
  }): string[] {
374
- const { scope, clis, policies, cwd, customEnabled, installDaemon } = state;
434
+ const { target, clis, policies, cwd, customEnabled, installDaemon, connect } = state;
435
+ const scopes = scopesFor(target);
375
436
  const where =
376
- scope === "project" ? `This project (${homeify(cwd)})` : "Everywhere (global)";
437
+ target === "both"
438
+ ? `Global, plus this project (${homeify(cwd)})`
439
+ : target === "project"
440
+ ? `This project (${homeify(cwd)})`
441
+ : "Everywhere (global)";
377
442
  const lines: string[] = [];
378
443
  const assistantNames = clis.map((c) => getIntegration(c).displayName);
379
444
  lines.push(` Where : ${where}`);
@@ -391,6 +456,11 @@ export function reviewLines(state: {
391
456
  ` Daemon : failproofaid, installed as a system service running as you`,
392
457
  );
393
458
  }
459
+ lines.push(
460
+ connect
461
+ ? " Reporting : on — policy decisions and session transcripts"
462
+ : " Reporting : off — nothing leaves this machine",
463
+ );
394
464
 
395
465
  // Reflect the Custom decision, not just what is on disk. Reporting
396
466
  // "1 file (project) (auto-loaded)" after the user had just unticked the row
@@ -407,15 +477,29 @@ export function reviewLines(state: {
407
477
 
408
478
  lines.push("");
409
479
  lines.push(" This will update:");
410
- for (const cli of clis) {
411
- const integration = getIntegration(cli);
412
- // Usually one path; Hermes lists one per profile so the operator sees every
413
- // home dir that is about to be written.
414
- for (const p of settingsPathsFor(integration, scope, cwd)) {
415
- lines.push(` ${homeify(p)} ${integration.displayName} hooks`);
480
+ // Deduplicated across scopes: a CLI that supports only user scope resolves to
481
+ // the same settings file under both halves of "Both", and listing it twice
482
+ // reads as two separate writes.
483
+ const seen = new Set<string>();
484
+ for (const scope of scopes) {
485
+ for (const cli of clis) {
486
+ const integration = getIntegration(cli);
487
+ if (!integration.scopes.includes(scope)) continue;
488
+ // Usually one path; Hermes lists one per profile so the operator sees
489
+ // every home dir that is about to be written.
490
+ for (const p of settingsPathsFor(integration, scope, cwd)) {
491
+ if (seen.has(p)) continue;
492
+ seen.add(p);
493
+ lines.push(` ${homeify(p)} ${integration.displayName} hooks`);
494
+ }
416
495
  }
417
496
  }
418
- lines.push(` ${homeify(getConfigPathForScope(scope, cwd))} ${policies.length} policies`);
497
+ for (const scope of scopes) {
498
+ const configPath = getConfigPathForScope(scope, cwd);
499
+ if (seen.has(configPath)) continue;
500
+ seen.add(configPath);
501
+ lines.push(` ${homeify(configPath)} ${policies.length} policies`);
502
+ }
419
503
  if (installDaemon && isDaemonSupportedPlatform()) {
420
504
  const servicePath = daemonServiceFilePath();
421
505
  if (servicePath) lines.push(` ${homeify(servicePath)} failproofaid service (needs root)`);
@@ -426,7 +510,7 @@ export function reviewLines(state: {
426
510
  // ── First-run redirect ───────────────────────────────────────────────────────
427
511
 
428
512
  function firstRunMarkerPath(): string {
429
- return resolve(homedir(), ".failproofai", ".launcher-configured");
513
+ return launcherMarker();
430
514
  }
431
515
 
432
516
  export function hasSeenLauncher(): boolean {
@@ -435,30 +519,16 @@ export function hasSeenLauncher(): boolean {
435
519
 
436
520
  export function markLauncherSeen(): void {
437
521
  try {
438
- mkdirSync(resolve(homedir(), ".failproofai"), { recursive: true });
522
+ // Layout 2 puts this under state/ with the rest of the daemon-adjacent
523
+ // markers, so the parent is a directory deeper than the home and has to be
524
+ // created rather than assumed.
525
+ mkdirSync(dirname(firstRunMarkerPath()), { recursive: true });
439
526
  writeFileSync(firstRunMarkerPath(), "1", "utf8");
440
527
  } catch {
441
528
  // best-effort
442
529
  }
443
530
  }
444
531
 
445
- /**
446
- * Whether failproofai is already set up GLOBALLY (user scope) for any agent.
447
- * Deliberately ignores project scope: project-scoped hooks in whatever repo the
448
- * user happens to be in shouldn't suppress the one-time global welcome. The
449
- * marker file is the primary "seen" gate; this is the "already set up" shortcut.
450
- */
451
- function anyHooksInstalledGlobally(): boolean {
452
- for (const id of INTEGRATION_TYPES) {
453
- try {
454
- if (getIntegration(id).hooksInstalledInSettings("user")) return true;
455
- } catch {
456
- // ignore broken settings files
457
- }
458
- }
459
- return false;
460
- }
461
-
462
532
  /**
463
533
  * On the FIRST bare `failproofai` invocation, redirect the user into the
464
534
  * configure wizard instead of the dashboard. Returns true when it handled the
@@ -470,45 +540,139 @@ function anyHooksInstalledGlobally(): boolean {
470
540
  * • non-TTY (CI/pipe) → print a one-line hint, go to dashboard
471
541
  * • fresh + TTY → mark seen, run the wizard, done
472
542
  */
473
- export async function maybeFirstRunConfigure(io: WizardIO = {}): Promise<boolean> {
543
+ export interface FirstRunOptions {
544
+ /**
545
+ * Run the post-setup audit after a completed apply. The caller sets this to
546
+ * false when the command it is about to run is `audit` itself, which would
547
+ * otherwise scan the entire history twice back to back.
548
+ */
549
+ postSetupAudit?: boolean;
550
+ /**
551
+ * Run setup even though the machine reads as already configured.
552
+ *
553
+ * Set by the caller after a layout reset. `isConfigured()` is a union that
554
+ * counts live user-scope hooks in any agent CLI, and the reset deliberately
555
+ * leaves those settings files alone — so a machine whose policy config was
556
+ * just deleted still answers "configured", the wizard is skipped, and
557
+ * `markLauncherSeen()` below back-fills the marker so every later run skips
558
+ * it too. The result is hooks firing on every tool call against no policies,
559
+ * with nothing to say so.
560
+ */
561
+ force?: boolean;
562
+ }
563
+
564
+ /**
565
+ * The live values `blockerCleared` compares an earlier attempt against.
566
+ *
567
+ * Built here rather than imported into `onboarding-attempt.ts` so that module
568
+ * stays pure and unit-testable without a service manager or a sudo binary.
569
+ */
570
+ function retryProbe(): RetryProbe {
571
+ return {
572
+ canElevate: () => {
573
+ try {
574
+ return canElevate();
575
+ } catch {
576
+ // No sudo binary at all is a blocker that has not cleared.
577
+ return false;
578
+ }
579
+ },
580
+ daemonStatus: () => {
581
+ try {
582
+ return daemonServiceStatus();
583
+ } catch {
584
+ return "";
585
+ }
586
+ },
587
+ cliVersion,
588
+ };
589
+ }
590
+
591
+ export async function maybeFirstRunConfigure(
592
+ io: WizardIO = {},
593
+ opts: FirstRunOptions = {},
594
+ ): Promise<boolean> {
474
595
  if (process.env.FAILPROOFAI_NO_FIRST_RUN === "1") return false;
475
- if (hasSeenLauncher()) return false;
476
596
 
477
597
  const stdin: TTYIn = io.stdin ?? process.stdin;
478
598
  const stdout: TTYOut = io.stdout ?? process.stdout;
479
599
 
480
- if (anyHooksInstalledGlobally()) {
481
- markLauncherSeen();
600
+ // One state read covering all three "already set up" signals — a config
601
+ // file, live user-scope hooks, or the legacy marker. See `isConfigured`.
602
+ const state = detectSetupState();
603
+ if (isConfigured(state) && !opts.force) {
604
+ // Back-fill the marker for a machine that is demonstrably configured but
605
+ // predates it, so later runs settle this with a single stat instead of
606
+ // walking every integration's settings file on every invocation.
607
+ if (!state.hasLegacyMarker) markLauncherSeen();
608
+ return false;
609
+ }
610
+
611
+ // A previous attempt that ABORTED. Setup writes nothing on those paths, by
612
+ // design, so without this record the machine is indistinguishable from one
613
+ // that has never been offered setup — and the wizard relaunches on every
614
+ // command forever. `--force` (an explicit `failproofai config`) never reaches
615
+ // here, so asking for setup by name always gets it.
616
+ const attempt = opts.force ? null : readOnboardingAttempt();
617
+ if (attempt && !blockerCleared(attempt, retryProbe())) {
618
+ for (const line of attemptHintLines(attempt)) stdout.write(`${line}\n`);
482
619
  return false;
483
620
  }
484
621
 
485
622
  if (!stdin.isTTY || !stdout.isTTY) {
623
+ // Never launch a wizard nobody can answer. This is the CI / piped path,
624
+ // and it must stay a hint rather than a failure: the command the user
625
+ // actually typed still runs.
486
626
  stdout.write(
487
627
  `\n[failproofai] Not set up yet — run \`failproofai config\` to get started.\n\n`,
488
628
  );
489
629
  return false;
490
630
  }
491
631
 
492
- // Fire-and-forget: never block the wizard's first paint on telemetry.
493
- void emit("first_run_configure_shown", {});
494
- // runConfigureWizard marks the launcher as seen only if the user completes an
495
- // apply — so cancelling keeps redirecting here on the next bare run, and only
496
- // a finished setup sends the user to the dashboard afterwards.
497
- const result = await runConfigureWizard(io);
632
+ // Onboarding now fires on ANY command, so two terminals on a fresh machine
633
+ // is a real shape: both would draw a wizard, race on the same settings
634
+ // files, and both try to install the one system service. Only one gets to.
635
+ const lock = acquireOnboardingLock();
636
+ if (!lock) {
637
+ stdout.write(
638
+ `\n[failproofai] Setup is already running in another terminal — leaving it to finish.\n\n`,
639
+ );
640
+ return false;
641
+ }
642
+
643
+ try {
644
+ // Fire-and-forget: never block the wizard's first paint on telemetry.
645
+ void emit("first_run_configure_shown", {});
646
+ // runConfigureWizard marks the launcher as seen only if the user completes
647
+ // an apply — so cancelling keeps offering setup on the next run rather
648
+ // than silently never mentioning it again.
649
+ const result = await runConfigureWizard(io);
650
+ // Remember WHY, so the next command can hint instead of relaunching. Only
651
+ // on an abort: a completed apply clears the record below.
652
+ if (!result.applied && result.abort) {
653
+ recordOnboardingAttempt(result.abort, cliVersion, daemonServiceStatus());
654
+ }
498
655
 
499
656
  // Onboarding-only: after a completed first-run setup, run the audit pipeline
500
657
  // (scan + cache warm) before the caller boots the dashboard. The explicit
501
658
  // `failproofai config` command does NOT do this — only this first-run path.
502
659
  // Lazy-imported + best-effort; opt out with FAILPROOFAI_NO_AUTO_AUDIT=1.
503
- if (result.applied) {
504
- try {
505
- const { runPostSetupAudit } = await import("../audit/cli");
506
- await runPostSetupAudit();
507
- } catch {
508
- // the audit is a bonus — never let it break onboarding or the dashboard.
660
+ if (result.applied && opts.postSetupAudit !== false) {
661
+ try {
662
+ const { runPostSetupAudit } = await import("../audit/cli");
663
+ await runPostSetupAudit();
664
+ } catch {
665
+ // the audit is a bonus — never let it break onboarding or the dashboard.
666
+ }
509
667
  }
668
+ return true;
669
+ } finally {
670
+ // Released on every path, including a throw from the wizard itself —
671
+ // otherwise a crash mid-setup would leave a lock behind, and although the
672
+ // liveness check reclaims it, doing so needs the next run to reach that
673
+ // check rather than relying on it.
674
+ lock.release();
510
675
  }
511
- return true;
512
676
  }
513
677
 
514
678
  // ── The wizard ───────────────────────────────────────────────────────────────
@@ -551,104 +715,162 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
551
715
 
552
716
  const cancel = (): WizardResult => {
553
717
  outro("Cancelled — nothing was changed.", { ok: false }, stdout);
554
- return { applied: false };
718
+ // Distinguished from the abort reasons: pressing Esc is not a failure, and
719
+ // a caller picking an exit code must not treat it as one.
720
+ return { applied: false, abort: "cancelled" };
555
721
  };
556
722
 
557
- // 0 — The background daemon, before anything else.
723
+ // 0 — The background daemon. REQUIRED, and before anything else.
558
724
  //
559
- // First because it is the only step that needs a password: asking here
560
- // means sudo prompts on a clean terminal, before any question has drawn a
561
- // screen, instead of firing from underneath a rendered TUI where the prompt
562
- // is invisible and the typed password lands in a redrawn frame. `sudo -v`
725
+ // First because it is the only step that needs a password: asking here means
726
+ // sudo prompts on a clean terminal, before any question has drawn a screen,
727
+ // instead of firing from underneath a rendered TUI where the prompt is
728
+ // invisible and the typed password lands in a redrawn frame. `sudo -v`
563
729
  // caches the credential for the rest of the run, so the actual install at
564
730
  // apply time stays non-interactive.
565
731
  //
566
732
  // Machine-level, so it is deliberately NOT gated on the scope chosen in the
567
733
  // next step: one daemon serves every project on this machine.
568
- let daemonWanted = false;
569
- let elevated = false;
570
- if (isDaemonSupportedPlatform()) {
571
- const choice = await selectOne<"install" | "skip">({
572
- message: "Install the failproofaid background daemon?",
573
- choices: [
574
- {
575
- label: "Yes — keep policy evaluation warm",
576
- value: "install",
577
- hint: "a system service, started at boot, running as you · needs your password once",
578
- },
579
- {
580
- label: "Not now",
581
- value: "skip",
582
- hint: "policies still enforce, evaluated per hook call",
583
- },
584
- ],
585
- stdin,
586
- stdout,
587
- });
588
- if (choice === null) return cancel();
589
- daemonWanted = choice === "install";
590
-
591
- if (daemonWanted) {
592
- stdout.write("\nInstalling the failproofaid service needs root. Your password stays with sudo.\n");
593
- elevated = primeElevation();
594
- if (!elevated) {
595
- // Never fatal: the rest of setup is worth applying, and a machine with
596
- // no daemon is exactly the machine every release before this one had.
597
- stdout.write(
598
- "Could not get sudo — carrying on without the daemon.\n" +
599
- "Everything else still applies, and policies still enforce in-process.\n\n",
600
- );
601
- daemonWanted = false;
602
- }
734
+ //
735
+ // On a platform with no service manager there is nothing to install, so the
736
+ // requirement does not apply — requiring an impossible step would lock those
737
+ // users out of setup entirely rather than protecting anything.
738
+ const daemonSupported = isDaemonSupportedPlatform();
739
+ // An already-healthy daemon needs no install and no password. Re-running
740
+ // setup on a configured machine must not demand sudo for work that is
741
+ // already done.
742
+ // "running" only. A unit that exists but is stopped or crash-looping is
743
+ // exactly the machine that needs this run to reinstall it, and treating
744
+ // "installed" as good enough would skip the repair and then set
745
+ // `daemonConfigured` against a service that is not answering — which fails
746
+ // closed on every tool call.
747
+ // Running is not enough — it must also be the version this CLI ships.
748
+ //
749
+ // Skipping on "running" alone was right for the case it was written for
750
+ // (never demand a password for work already done), and exactly backwards
751
+ // during an upgrade: the OLD daemon is perfectly healthy, so setup skipped
752
+ // it and the stale version survived. That made "just re-run config" — the
753
+ // remedy every message points at — silently do nothing.
754
+ //
755
+ // And running the right VERSION is still not enough: it must be able to
756
+ // answer. `ExecStart` bakes in `process.execPath` and an absolute
757
+ // `dist/worker.mjs`, so an `nvm uninstall 20` after setup leaves a unit
758
+ // systemd calls active whose worker dies on every spawn. That machine reads
759
+ // as "already running", so this wizard — the documented remedy, and the only
760
+ // caller that can rebuild the unit — skipped it and left the box denying
761
+ // every tool call with no route back but hand-editing `config.toml`. A real
762
+ // hook evaluation is the only check that distinguishes the two.
763
+ const daemonSkew = daemonSupported ? daemonVersionSkew() : null;
764
+ const daemonUpToDate =
765
+ daemonSupported && daemonServiceStatus() === "running" && daemonSkew === null;
766
+ const daemonAnswers = daemonUpToDate ? await probeDaemonEndToEnd() : false;
767
+ const daemonAlreadyRunning = daemonUpToDate && daemonAnswers;
768
+ /** Installed and running, but its worker cannot evaluate anything. */
769
+ const daemonBroken = daemonUpToDate && !daemonAnswers;
770
+ let daemonWanted = daemonSupported && !daemonAlreadyRunning;
771
+ // A healthy daemon can still be running a service definition written before
772
+ // FAILPROOFAI_CLI_CMD existed, and nothing else on the machine will ever
773
+ // rewrite it: upgrading the npm package does not touch /etc/systemd/system.
774
+ // Re-running setup is the one moment a user asks for their configuration to
775
+ // be brought up to date, so it is the moment to do it.
776
+ let daemonUnitStale = daemonAlreadyRunning && daemonServiceNeedsUpgrade();
777
+
778
+ if (daemonWanted) {
779
+ stdout.write(
780
+ daemonBroken
781
+ ? "failproofaid is installed and running but cannot evaluate policies — its worker\n" +
782
+ "process will not start, which on this machine denies every tool call. Rebuilding\n" +
783
+ "the service needs root once. Your password goes to sudo, never to us.\n\n"
784
+ : "failproofai runs a small background service (failproofaid) so policy checks\n" +
785
+ "stay warm — without it every tool call pays a fresh startup, about 15x slower.\n" +
786
+ "Installing it needs root once. Your password goes to sudo, never to us.\n\n",
787
+ );
788
+ if (!primeElevation()) {
789
+ // Required means required: write nothing at all, so a machine that could
790
+ // not be set up is left exactly as it was found rather than carrying
791
+ // half a configuration. The commands are printed so an admin can do the
792
+ // privileged half by hand.
793
+ stdout.write(
794
+ "\nCould not get root, so setup stopped before changing anything.\n\n" +
795
+ " Re-run once you can use sudo: failproofai config\n" +
796
+ ` Check what it needs: ${daemonStatusCommand() ?? "n/a"}\n\n`,
797
+ );
798
+ void emit("configure_aborted", { reason: "needs_root" });
799
+ outro("Nothing was changed.", { ok: false }, stdout);
800
+ return { applied: false, abort: "needs_root" };
801
+ }
802
+ } else if (daemonUnitStale) {
803
+ stdout.write(
804
+ "failproofaid is running, but from a service definition written by an older\n" +
805
+ "version — it cannot start a scheduled audit. Refreshing it needs root once.\n\n",
806
+ );
807
+ if (!primeElevation()) {
808
+ // NOT an abort, unlike the install branch above. There is a working
809
+ // daemon here and hooks are enforcing; only the scheduled audit is out
810
+ // of reach. Stopping setup over that would make an upgrade the thing
811
+ // that locked someone out of `failproofai config`.
812
+ stdout.write(
813
+ "\nCould not get root, so the service definition was left as it is.\n" +
814
+ "Everything else is set up as normal; scheduled audits stay off until it is\n" +
815
+ `refreshed. Re-run \`failproofai config\` once you can use sudo.\n\n`,
816
+ );
817
+ daemonUnitStale = false;
603
818
  }
819
+ } else if (daemonAlreadyRunning) {
820
+ stdout.write("failproofaid is already installed and running — leaving it alone.\n\n");
821
+ }
822
+ if (daemonSkew) {
823
+ stdout.write(
824
+ `failproofaid is ${daemonSkew.installed} but this CLI ships ${daemonSkew.expected} — ` +
825
+ `reinstalling it.\n\n`,
826
+ );
604
827
  }
605
828
 
606
- // 1 — Where?
607
- const scope = await selectOne<HookScope>({
608
- message: "Where should this apply?",
609
- choices: buildScopeChoices(cwd),
610
- stdin,
611
- stdout,
612
- });
613
- if (scope === null) return cancel();
829
+ // 1 — Where? Inferred from cwd, then confirmed.
830
+ //
831
+ // Running from inside a project and running from a home directory are two
832
+ // different intents, and asking a context-free "global or project?" made the
833
+ // user restate something they had already expressed by choosing where to run
834
+ // the command. So the choices are built from what actually exists here — and
835
+ // labelled Update vs Set up accordingly — with the likelier target first.
836
+ const setupState = detectSetupState(cwd);
837
+ const targetChoices = buildTargetChoices(setupState);
614
838
 
615
- // 2 — Which assistants? An "Everything available" row protects every supported
616
- // CLI (detected + set-up-ahead); when ticked it wins over the individual boxes.
617
- const clisSel = await multiSelect<string>({
618
- message: "Which AI assistants should it protect?",
619
- choices: [
620
- {
621
- label: "Everything available",
622
- value: ALL_CLIS,
623
- // Counts only what this scope can actually take — expanding to all 12
624
- // under project scope is what crashed the apply on Hermes.
625
- hint: `protect all ${clisSupportingScope(scope).length} CLIs configurable here`,
626
- // A selector, not an assistant. Counting it gave "13 assistants" for
627
- // the 12 supported CLIs, and listed "Everything available" among them.
628
- summaryExclude: true,
629
- },
630
- ...buildAgentChoices(scope, cwd),
631
- ],
632
- minSelected: 1,
633
- summaryNoun: "assistants",
634
- hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ↵ confirm",
635
- stdin,
636
- stdout,
637
- });
638
- if (clisSel === null) return cancel();
639
- // Filter to what this scope supports in BOTH branches: "Everything
640
- // available" must not expand to CLIs that cannot take this scope, and a
641
- // locked row can't be ticked but belt-and-braces keeps the invariant local
642
- // to the one place `clis` is built.
643
- const supported = new Set(clisSupportingScope(scope));
644
- const clis: IntegrationType[] = (
645
- clisSel.includes(ALL_CLIS)
646
- ? [...INTEGRATION_TYPES]
647
- : (clisSel.filter((v) => v !== ALL_CLIS) as IntegrationType[])
648
- ).filter((id) => supported.has(id));
839
+ let target: SetupTarget;
840
+ if (targetChoices.length === 1) {
841
+ // From a home directory there is no project to configure, so there is no
842
+ // question to ask. Say what is about to happen rather than silently
843
+ // deciding it.
844
+ target = targetChoices[0].value;
845
+ stdout.write(`Configuring ${targetChoices[0].label.toLowerCase()} — ${targetChoices[0].hint}.\n\n`);
846
+ } else {
847
+ const chosen = await selectOne<SetupTarget>({
848
+ message: "What are we configuring?",
849
+ choices: targetChoices.map((c) => ({
850
+ label: c.label,
851
+ value: c.value,
852
+ hint: c.hint,
853
+ })),
854
+ stdin,
855
+ stdout,
856
+ });
857
+ if (chosen === null) return cancel();
858
+ target = chosen;
859
+ }
860
+ const scopes = scopesFor(target);
861
+ // The scope whose CURRENT state seeds the pickers below. With "Both" the
862
+ // project is the more specific of the two and the one the user is standing
863
+ // in, so it wins; anything it does not define still falls back to global at
864
+ // merge time, which is exactly the layering the policy loader already does.
865
+ const primaryScope: HookScope = scopes.includes("project") ? "project" : "user";
649
866
 
650
- // 3 — Which policies? Multi-select of themed presets — additive, so the
867
+ // 2 — Which policies? Multi-select of themed presets — additive, so the
651
868
  // enabled set is the union of every ticked bundle.
869
+ //
870
+ // Before the assistants step, because "what do you want guarded" is the
871
+ // question the user came here to answer; which CLIs to wire it into is
872
+ // plumbing that follows from it.
873
+ //
652
874
  // Seed the Custom checkbox from whatever the config already says, so the
653
875
  // wizard shows the current state rather than resetting it every run.
654
876
  const customEnabledBefore = readHooksConfig().customPoliciesEnabled !== false;
@@ -664,7 +886,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
664
886
  // renders "none". Hooks still install, so enforcement can be switched on
665
887
  // later without re-running setup.
666
888
  //
667
- // The assistants step above keeps its minimum deliberately: an empty CLI list
889
+ // The assistants step below keeps its minimum deliberately: an empty CLI list
668
890
  // does NOT mean "no assistants" there — `installHooksImpl` falls back to
669
891
  // ["claude"], so letting it through would silently install for Claude.
670
892
  const presets = await multiSelect<string>({
@@ -681,67 +903,119 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
681
903
  // locked-unchecked and must not write a disabling flag.
682
904
  const customEnabled = hasCustomFiles ? presets.includes(CUSTOM) : undefined;
683
905
 
684
- // 4 — Send data to AgentEye? Last, and only when a daemon was asked for,
685
- // since the daemon is what runs the collector.
906
+ // 3 — Which assistants? An "Everything available" row protects every supported
907
+ // CLI (detected + set-up-ahead); when ticked it wins over the individual boxes.
908
+ const clisSel = await multiSelect<string>({
909
+ message: "Which AI assistants should it protect?",
910
+ choices: [
911
+ {
912
+ label: "Everything available",
913
+ value: ALL_CLIS,
914
+ // Counts only what this scope can actually take — expanding to all 12
915
+ // under project scope is what crashed the apply on Hermes.
916
+ hint: `protect all ${clisSupportingScope(primaryScope).length} CLIs configurable here`,
917
+ // A selector, not an assistant. Counting it gave "13 assistants" for
918
+ // the 12 supported CLIs, and listed "Everything available" among them.
919
+ summaryExclude: true,
920
+ },
921
+ ...buildAgentChoices(primaryScope, cwd),
922
+ ],
923
+ minSelected: 1,
924
+ summaryNoun: "assistants",
925
+ hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ↵ confirm",
926
+ stdin,
927
+ stdout,
928
+ });
929
+ if (clisSel === null) return cancel();
930
+ // Filter to what the chosen scopes support in BOTH branches: "Everything
931
+ // available" must not expand to CLIs that cannot take any selected scope,
932
+ // and a locked row can't be ticked but belt-and-braces keeps the invariant
933
+ // local to the one place `clis` is built.
686
934
  //
687
- // Deliberately AFTER the enforcement questions rather than beside the daemon
688
- // one: by this point the user has decided what to protect, so "and would you
689
- // like to see it in a dashboard?" follows naturally, where asking up front
690
- // interrupts setup with a question about a product they may not have.
935
+ // The union across scopes, not the intersection: under "Both", a user-scope-
936
+ // only gateway like Hermes is still installable via the user half, and
937
+ // dropping it because project scope cannot take it would silently protect
938
+ // less than the user asked for. `installHooks` is called per scope below and
939
+ // skips what a given scope cannot take.
940
+ const supported = new Set(scopes.flatMap((s) => clisSupportingScope(s)));
941
+ const clis: IntegrationType[] = (
942
+ clisSel.includes(ALL_CLIS)
943
+ ? [...INTEGRATION_TYPES]
944
+ : (clisSel.filter((v) => v !== ALL_CLIS) as IntegrationType[])
945
+ ).filter((id) => supported.has(id));
946
+
947
+ // 4 — Connect this machine? Last, because by this point the user has decided
948
+ // what to protect, so "would you like to see it in a dashboard?" follows
949
+ // naturally — asking up front interrupts setup with a question about a
950
+ // product they may not have.
691
951
  //
692
- // Defaults to NO, and the transcript choice is a SEPARATE question from the
693
- // key, because a transcript carries prompts, file contents and whatever was
694
- // pasted into a terminal. Configuring ingest must never silently send those.
695
- let collector: { cred: { url: string; key: string }; sessions: boolean } | null = null;
696
- if (daemonWanted) {
697
- const send = await selectOne<"yes" | "no">({
698
- message: "Send session and hook data to AgentEye?",
952
+ // A pasted API key rather than an interactive sign-in: it is the only form
953
+ // that works on a headless box, in a container or over SSH, and it is the
954
+ // same credential `failproofai config --connect` takes, so a machine set up
955
+ // by the wizard and one set up by hand end up byte-identical on disk.
956
+ //
957
+ // Connecting turns on BOTH streams — policy decisions and session
958
+ // transcripts. That is a real disclosure, not a footnote, so it is stated in
959
+ // the body of the question itself rather than buried in an option hint.
960
+ let connect: { url: string; token: string; machineId: string; machineLabel: string } | null = null;
961
+
962
+ {
963
+ const choice = await selectOne<"key" | "local">({
964
+ message: "Connect this machine to Failproof Cloud?",
965
+ body: [
966
+ " Connecting reports this machine's policy decisions AND full session",
967
+ " transcripts — prompts, file contents and command output — to your",
968
+ " dashboard. Staying local sends nothing, anywhere, ever.",
969
+ ],
699
970
  choices: [
700
971
  {
701
- label: "Not now",
702
- value: "no",
703
- hint: "nothing leaves this machine",
972
+ label: "Not now — stay local",
973
+ value: "local",
974
+ hint: "policies still enforce · connect later with failproofai config --connect",
704
975
  },
705
976
  {
706
- label: "Yes — connect to a dashboard",
707
- value: "yes",
708
- hint: "needs an events:add API key",
977
+ label: "Paste an API key",
978
+ value: "key",
979
+ hint: "reports decisions and transcripts to your dashboard",
709
980
  },
710
981
  ],
711
982
  stdin,
712
983
  stdout,
713
984
  });
714
- if (send === null) return cancel();
715
-
716
- if (send === "yes") {
717
- // If this machine is ALREADY enrolled with Failproof Cloud, it has a URL
718
- // and a token that usually work for both capabilities. Asking for them
719
- // again is the seam that made connecting feel like two products: someone
720
- // runs `--connect`, sees an empty dashboard, and has no reason to think a
721
- // second credential exists. Offer what we already know, and let it be
722
- // overridden.
985
+ if (choice === null) return cancel();
986
+
987
+ if (choice === "key") {
988
+ // An already-enrolled machine has a URL and token that usually work.
989
+ // Asking again is the seam that made connecting feel like two products.
723
990
  const existing = readCloudCredentials();
724
991
  let url: string | null = null;
725
- let key: string | null = null;
992
+ let token: string | null = null;
993
+ // Reuse the enrolled id if there is one, else mint a stable key — never
994
+ // the hostname, so two hosts with the same name do not merge. The hostname
995
+ // becomes the human label instead.
996
+ let machineId = resolveMachineId();
997
+ let machineLabel = existing?.machineLabel ?? resolveMachineLabel();
726
998
 
727
999
  if (existing) {
728
1000
  const reuse = await selectOne<"reuse" | "other">({
729
1001
  message: `Use this machine's existing connection to ${existing.url}?`,
730
1002
  choices: [
731
1003
  {
732
- label: `Yes — reuse it`,
1004
+ label: "Yes — reuse it",
733
1005
  value: "reuse",
734
- hint: `as ${existing.machineId}, same token`,
1006
+ hint: `as ${existing.machineLabel ?? existing.machineId}, same token`,
735
1007
  },
736
- { label: "No — use a different endpoint and key", value: "other", hint: "" },
1008
+ { label: "No — different endpoint or key", value: "other", hint: "" },
737
1009
  ],
738
1010
  stdin,
739
1011
  stdout,
740
1012
  });
741
1013
  if (reuse === null) return cancel();
742
1014
  if (reuse === "reuse") {
743
- url = ingestUrlFor(existing.url);
744
- key = existing.token;
1015
+ url = existing.url;
1016
+ token = existing.token;
1017
+ machineId = existing.machineId;
1018
+ machineLabel = existing.machineLabel ?? machineLabel;
745
1019
  }
746
1020
  }
747
1021
 
@@ -750,41 +1024,58 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
750
1024
  message: "Failproof Cloud URL",
751
1025
  defaultValue: cloudBaseFor(DEFAULT_INGEST_URL),
752
1026
  hint: "Enter for the hosted endpoint",
753
- validate: (v) => (/^https?:\/\//.test(v) ? null : "must be an http(s) URL"),
1027
+ // The SAME guard `--connect` uses, not a looser lookalike. This was
1028
+ // `/^https?:\/\//`, which accepts `http://cloud.example.com` — and
1029
+ // the wizard is the documented primary path, so the interactive flow
1030
+ // put the machine's bearer token on the wire in clear while the flag
1031
+ // nobody uses refused to. `validateCloudUrl` permits http for
1032
+ // loopback only, which is what the local walkthrough needs.
1033
+ validate: (v) => {
1034
+ const check = validateCloudUrl(cloudBaseFor(v));
1035
+ return check.ok ? null : check.reason;
1036
+ },
754
1037
  stdin,
755
1038
  stdout,
756
1039
  });
757
1040
  if (entered === null) return cancel();
758
- // Asked for as a base and derived, so this and `--connect` take the
1041
+ // Asked for as a base and normalised, so this and `--connect` take the
759
1042
  // same thing. Someone pasting the older `/events` endpoint still works.
760
- url = ingestUrlFor(cloudBaseFor(entered));
1043
+ const validated = validateCloudUrl(cloudBaseFor(entered));
1044
+ if (!validated.ok) {
1045
+ // Unreachable while `validate` above rejects the same input; kept so
1046
+ // the two can never drift into disagreeing silently.
1047
+ stdout.write(`\n${validated.reason}\n`);
1048
+ return cancel();
1049
+ }
1050
+ url = validated.url;
761
1051
  }
762
1052
 
763
- if (key === null) {
764
- key = await promptText({
1053
+ if (token === null) {
1054
+ token = await promptText({
765
1055
  message: "API key",
766
- hint: "needs events:add",
767
- // Masked: setup is routinely run while screen-sharing, and a pasted key
768
- // would otherwise sit in the scrollback of every recording.
1056
+ hint: "needs events:add · policies:pull enables managed policy too",
1057
+ // Masked: setup is routinely run while screen-sharing, and a pasted
1058
+ // key would otherwise sit in the scrollback of every recording.
769
1059
  mask: true,
770
1060
  validate: (v) => (v.length >= 8 ? null : "that looks too short to be a key"),
771
1061
  stdin,
772
1062
  stdout,
773
1063
  });
774
- if (key === null) return cancel();
1064
+ if (token === null) return cancel();
775
1065
  }
776
1066
 
777
- // Check BEFORE writing anything. A typo'd key is otherwise only
778
- // discovered later as a silent pile of 401s parked in failed/, which
779
- // reads like a server problem rather than a typo.
1067
+ // Check BEFORE the review screen, so a typo is caught while the user is
1068
+ // still thinking about credentials rather than three screens later. The
1069
+ // apply step re-verifies and is what actually writes — nothing is
1070
+ // persisted here.
780
1071
  stdout.write("\nChecking the key… ");
781
- const check = await validateIngestKey({ url, key });
782
- if (!check.ok) {
783
- stdout.write(`\nThat did not work: ${check.reason}\n`);
1072
+ const probe = await validateIngestKey({ url: ingestUrlFor(url), key: token });
1073
+ if (!probe.ok) {
1074
+ stdout.write(`\nThat did not work: ${probe.reason}\n`);
784
1075
  const retry = await selectOne<"skip" | "anyway">({
785
- message: "Carry on without sending data?",
1076
+ message: "Carry on without connecting?",
786
1077
  choices: [
787
- { label: "Yes, skip it", value: "skip", hint: "re-run failproofai config later" },
1078
+ { label: "Yes, skip it", value: "skip", hint: "everything else still applies" },
788
1079
  { label: "Save it anyway", value: "anyway", hint: "if you know the server is just down" },
789
1080
  ],
790
1081
  stdin,
@@ -792,58 +1083,32 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
792
1083
  });
793
1084
  if (retry === null) return cancel();
794
1085
  if (retry === "skip") {
795
- stdout.write("Skipping data collection. Everything else still applies.\n\n");
1086
+ stdout.write("Staying local. Connect later with `failproofai config --connect`.\n\n");
796
1087
  } else {
797
- collector = { cred: { url, key }, sessions: false };
1088
+ connect = { url, token, machineId, machineLabel };
798
1089
  }
799
1090
  } else {
800
- stdout.write("looks good.\n");
801
- const sessions = await selectOne<"hooks" | "both">({
802
- message: "What should it send?",
803
- choices: [
804
- {
805
- label: "Policy decisions only",
806
- value: "hooks",
807
- hint: "which hooks fired and what they decided \u00b7 no file contents",
808
- },
809
- {
810
- label: "Decisions and full session transcripts",
811
- value: "both",
812
- hint: "includes prompts, file contents and command output",
813
- },
814
- ],
815
- stdin,
816
- stdout,
817
- });
818
- if (sessions === null) return cancel();
819
- collector = { cred: { url, key }, sessions: sessions === "both" };
820
-
821
- // The other half of the same connection. If this machine is not
822
- // enrolled for policy and the key turns out to carry `policies:pull`,
823
- // enrol it here rather than making the user discover a second command.
824
- // Silent when the key lacks the permission — an events-only key is a
825
- // perfectly reasonable thing to have, and a warning about a capability
826
- // nobody asked for is noise.
827
- if (!readCloudCredentials()) {
828
- const base = cloudBaseFor(url);
829
- const machineId = hostname();
830
- const enrol = await verifyCloudCredentials({ url: base, machineId, token: key });
831
- if (enrol.ok) {
832
- writeCloudCredentials({ url: base, machineId, token: key });
833
- stdout.write(
834
- `Also connected for cloud-managed policy (${enrol.policyCount} assigned, generation ${enrol.generation}).\n`,
835
- );
836
- }
837
- }
1091
+ stdout.write("looks good.\n\n");
1092
+ connect = { url, token, machineId, machineLabel };
838
1093
  }
839
1094
  }
840
1095
  }
841
-
842
-
843
1096
  // 5 — Review & apply
844
1097
  const decision = await selectOne<"apply" | "cancel">({
845
1098
  message: "Ready to apply?",
846
- body: reviewLines({ scope, clis, policies, cwd, customEnabled, installDaemon: daemonWanted }),
1099
+ body: reviewLines({
1100
+ target,
1101
+ clis,
1102
+ policies,
1103
+ cwd,
1104
+ customEnabled,
1105
+ // A stale-unit refresh rewrites the service file and restarts the
1106
+ // daemon, so it belongs on the list of things this run is about to
1107
+ // change. Leaving it off would make the confirmation screen of a
1108
+ // security tool quietly incomplete about a root-owned file.
1109
+ installDaemon: daemonWanted || daemonUnitStale,
1110
+ connect: connect !== null,
1111
+ }),
847
1112
  choices: [
848
1113
  { label: "Yes, apply now", value: "apply", hint: "write the config" },
849
1114
  { label: "Cancel", value: "cancel", hint: "quit, no changes" },
@@ -853,90 +1118,248 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
853
1118
  });
854
1119
  if (decision !== "apply") return cancel();
855
1120
 
856
- // Apply — REPLACE the enabled set at this scope.
1121
+ // ── Apply ─────────────────────────────────────────────────────────────────
1122
+ //
1123
+ // ORDER MATTERS. The daemon goes first, because setup requires it: if it
1124
+ // cannot be installed, this run must leave the machine exactly as it found
1125
+ // it rather than half-configured. Writing hooks first and discovering the
1126
+ // service will not start afterwards is the one ordering whose failure cannot
1127
+ // be undone cleanly — hooks would already be live, pointing at a machine
1128
+ // whose `daemonConfigured` flag we then could not honestly set.
1129
+ let daemonInstalled = daemonAlreadyRunning;
1130
+ if (daemonWanted) {
1131
+ // A unit that is running but cannot answer is torn down before it is
1132
+ // rebuilt, rather than installed over. Its `ExecStart` points at a binary
1133
+ // or interpreter that no longer works, and it holds the singleton flock
1134
+ // the replacement needs — install over the top and the new unit starts,
1135
+ // loses the lock race, and the machine stays exactly as broken. This is
1136
+ // the production path for `uninstallDaemonService`, which until now had
1137
+ // none: it was defined, tested, documented in CLAUDE.md as called from
1138
+ // here, and referenced by nothing.
1139
+ if (daemonBroken) {
1140
+ stdout.write("Removing the failproofaid service that cannot start…\n");
1141
+ try {
1142
+ await uninstallDaemonService();
1143
+ } catch (err) {
1144
+ // Non-fatal: the install below reports its own outcome, and it is that
1145
+ // outcome — not this one — that decides whether setup continues.
1146
+ hookLogWarn(
1147
+ `could not remove the broken failproofaid service: ${err instanceof Error ? err.message : String(err)}`,
1148
+ );
1149
+ }
1150
+ }
1151
+ stdout.write(daemonBroken ? "Reinstalling the failproofaid service…\n" : "Installing the failproofaid service…\n");
1152
+ const daemonResult = await installDaemonService();
1153
+ void emit("configure_daemon_install", {
1154
+ installed: daemonResult.installed,
1155
+ // A bounded classification, never the raw reason: on the failure path
1156
+ // that string is an errno message from writeFileSync/execFileSync
1157
+ // against a homedir()-derived path, so it routinely carries the OS
1158
+ // username and the local filesystem layout. The full text stays local
1159
+ // via the hookLogWarn below.
1160
+ reason: daemonResult.installed ? null : classifyDaemonInstallFailure(daemonResult.reason),
1161
+ platform: process.platform,
1162
+ });
1163
+
1164
+ if (!daemonResult.installed) {
1165
+ hookLogWarn(`failproofaid was not installed as a service: ${daemonResult.reason}`);
1166
+ // Nothing user-facing has been written yet, so there is nothing to roll
1167
+ // back — which is the entire reason this runs first.
1168
+ stdout.write(
1169
+ `\nThe failproofaid service could not be installed:\n ${daemonResult.reason ?? "unknown error"}\n\n` +
1170
+ "Setup stopped before changing anything. Once that is fixed, re-run:\n" +
1171
+ " failproofai config\n\n",
1172
+ );
1173
+ void emit("configure_aborted", { reason: "daemon_failed" });
1174
+ outro("Nothing was changed.", { ok: false }, stdout);
1175
+ return { applied: false, abort: "daemon_failed" };
1176
+ }
1177
+
1178
+ // Installed and running is the service manager's opinion, and it is not
1179
+ // what setup needs to know. `ExecStart` bakes in `process.execPath` and an
1180
+ // absolute `dist/worker.mjs`, so a unit can be perfectly active while the
1181
+ // worker behind it dies on every spawn — and every other check waves that
1182
+ // machine through: `waitForDaemonRunning()` asks systemd, `Ping` is
1183
+ // answered in `server.rs` without touching the worker, a null
1184
+ // `resolveWorkerCommand()` is best-effort, and `Worker::warm()` swallows
1185
+ // its own failure. Setting `daemonConfigured` against it denies every tool
1186
+ // call across all twelve CLIs, `UserPromptSubmit` included, so the user
1187
+ // cannot even ask their agent why.
1188
+ //
1189
+ // Checked HERE rather than inside `installDaemonService`, which can only
1190
+ // honestly report on the service: keeping them apart is also what lets the
1191
+ // install mechanics be tested against a stub binary.
1192
+ if (!(await probeDaemonEndToEnd())) {
1193
+ hookLogWarn("failproofaid was installed and running but did not answer a policy evaluation");
1194
+ stdout.write(
1195
+ "\nfailproofaid started but cannot evaluate policies — its worker process\n" +
1196
+ "could not be run. Setup stopped before changing anything, because a machine\n" +
1197
+ "configured to require a daemon that cannot answer denies every tool call.\n\n" +
1198
+ ` Check it with: ${daemonStatusCommand() ?? "systemctl status failproofaid"}\n` +
1199
+ " Then re-run: failproofai config\n\n",
1200
+ );
1201
+ void emit("configure_aborted", { reason: "daemon_not_answering" });
1202
+ outro("Nothing was changed.", { ok: false }, stdout);
1203
+ return { applied: false, abort: "daemon_failed" };
1204
+ }
1205
+ daemonInstalled = true;
1206
+ } else if (daemonUnitStale) {
1207
+ // Deliberately after the install branch and never instead of it: this only
1208
+ // ever runs on a machine whose daemon is already up, and it must not be
1209
+ // able to abort a setup that is otherwise fine. A failure here costs the
1210
+ // scheduled audit, nothing else.
1211
+ stdout.write("Refreshing the failproofaid service definition…\n");
1212
+ const upgrade = await ensureDaemonServiceCurrent();
1213
+ void emit("configure_daemon_unit_refresh", {
1214
+ outcome: upgrade.outcome,
1215
+ daemon_running: upgrade.daemonRunning ?? true,
1216
+ platform: process.platform,
1217
+ });
1218
+ if (upgrade.outcome === "failed") {
1219
+ hookLogWarn(`failproofaid service definition could not be refreshed: ${upgrade.reason}`);
1220
+ if (upgrade.daemonRunning === false) {
1221
+ // The refresh stopped a daemon it could not start again, and its own
1222
+ // rollback could not either. Explicitly `=== false`: every failure
1223
+ // that never touched the service leaves this undefined, and those must
1224
+ // not drag a healthy machine down this branch.
1225
+ //
1226
+ // Leaving `daemonConfigured` set here is not "the audit stays off", it
1227
+ // is every tool call across all 12 CLIs denied against a socket
1228
+ // nothing is listening on, recoverable only by hand-editing
1229
+ // policies-config.json. So the machine goes back to in-process
1230
+ // evaluation — the same trade uninstallDaemonService makes, and for
1231
+ // the same reason.
1232
+ daemonInstalled = false;
1233
+ setDaemonConfigured(false);
1234
+ stdout.write(
1235
+ `\nThe service definition could not be refreshed:\n ${upgrade.reason ?? "unknown error"}\n` +
1236
+ "failproofaid is no longer running, so this machine was switched back to\n" +
1237
+ "in-process evaluation rather than left denying every tool call. Hooks keep\n" +
1238
+ "enforcing. Re-run `failproofai config` to reinstall the service.\n\n",
1239
+ );
1240
+ } else {
1241
+ stdout.write(
1242
+ `\nThe service definition could not be refreshed:\n ${upgrade.reason ?? "unknown error"}\n` +
1243
+ "Hooks keep enforcing; scheduled audits stay off until it is.\n\n",
1244
+ );
1245
+ }
1246
+ }
1247
+ }
1248
+
1249
+ // The flag that makes hooks route through the daemon — and, on a machine
1250
+ // where the daemon is unreachable, fail closed. Only ever set after a
1251
+ // verified-running service, never on intent.
1252
+ // The version is recorded HERE, from the CLI that installed it — which is by
1253
+ // construction the version that was installed, since the download URL and the
1254
+ // binary filename are both derived from it.
1255
+ if (daemonInstalled) {
1256
+ setDaemonConfigured(true, cliVersion);
1257
+ // Only now — the unit points at the new binary, so older ones are no
1258
+ // longer referenced by anything. Keeps the previous version for an
1259
+ // offline rollback.
1260
+ pruneOldDaemonBinaries();
1261
+ }
1262
+
857
1263
  // Telemetry runs concurrently with the install (never rejects, 5s-bounded) so
858
1264
  // it doesn't add dead time between "apply" and the config actually writing,
859
1265
  // while still being awaited before the process can exit.
860
1266
  const applied = emit("configure_applied", {
861
- scope,
1267
+ target,
1268
+ scopes,
862
1269
  cli: clis,
863
1270
  cli_count: clis.length,
864
1271
  policy_count: policies.length,
865
1272
  source: presets.join("+"),
1273
+ connected: connect !== null,
866
1274
  });
1275
+
1276
+ // One install per chosen scope, with the CLI list narrowed to what THAT
1277
+ // scope can take.
1278
+ //
1279
+ // The comment here used to claim `installHooks` "already skips CLIs a given
1280
+ // scope cannot take". It does not — `installHooksImpl` validates every CLI
1281
+ // against the scope up front and THROWS `Scope "project" is not supported by
1282
+ // Hermes`. `clis` is the union across scopes (deliberately, so a user-scope-
1283
+ // only gateway is still installed via the user half), so under "Both" +
1284
+ // "Everything available" the project pass got handed hermes/openclaw and
1285
+ // died. Nothing catches it, and by then the daemon is installed,
1286
+ // `daemonConfigured` is set and user-scope hooks are written — so the run
1287
+ // aborted mid-apply, before any project config or the pasted cloud key.
1288
+ // `configure-wizard.test.ts` mocks `installHooks` wholesale, which is why the
1289
+ // real validation path was never exercised.
1290
+ //
867
1291
  // quiet: the wizard renders its own outro; replace: the chosen set becomes
868
- // the full enabled set at this scope (unticking removes).
869
- await installHooks(
870
- policies,
871
- scope,
872
- cwd,
873
- /* includeBeta */ false,
874
- "configure-wizard",
875
- /* customPoliciesPath */ undefined,
876
- /* removeCustomHooks */ false,
877
- clis,
878
- { replace: true, quiet: true },
879
- );
880
- setCustomPoliciesEnabled(scope, cwd, customEnabled);
881
-
882
- // Daemon setup — driven by step 0's answer, with the sudo credential it
883
- // already primed, so nothing here can stop to ask a question. A failure
884
- // (binary download blocked, no service manager, a daemon that won't stay
885
- // up) never fails the wizard: the rest of setup already applied, and this
886
- // machine simply stays on the in-process path, since `daemonConfigured` is
887
- // only set on success.
888
- let daemonInstalled = false;
889
- if (daemonWanted) {
890
- const daemonResult = await installDaemonService();
891
- if (daemonResult.installed) {
892
- setDaemonConfigured(true);
893
- daemonInstalled = true;
894
- } else {
895
- hookLogWarn(`failproofaid was not installed as a service: ${daemonResult.reason}`);
1292
+ // the full enabled set at that scope (unticking removes).
1293
+ for (const scope of scopes) {
1294
+ const supportedHere = new Set(clisSupportingScope(scope));
1295
+ const clisForScope = clis.filter((id) => supportedHere.has(id));
1296
+ if (clisForScope.length > 0) {
1297
+ await installHooks(
1298
+ policies,
1299
+ scope,
1300
+ cwd,
1301
+ /* includeBeta */ false,
1302
+ "configure-wizard",
1303
+ /* customPoliciesPath */ undefined,
1304
+ /* removeCustomHooks */ false,
1305
+ clisForScope,
1306
+ { replace: true, quiet: true },
1307
+ );
896
1308
  }
897
- void emit("configure_daemon_install", {
898
- installed: daemonResult.installed,
899
- // A bounded classification, never the raw reason: on the failure path
900
- // that string is an errno message from writeFileSync/execFileSync
901
- // against a homedir()-derived path, so it routinely carries the OS
902
- // username and the local filesystem layout ("/home/<user>/.config/
903
- // systemd/user/failproofaid.service"). The full text stays local via
904
- // the hookLogWarn above.
905
- reason: daemonResult.installed ? null : classifyDaemonInstallFailure(daemonResult.reason),
906
- platform: process.platform,
907
- });
1309
+ setCustomPoliciesEnabled(scope, cwd, customEnabled);
908
1310
  }
909
1311
 
910
- // Collector config, written only when the daemon actually came up: the
911
- // daemon is what runs the collector, so writing a credential for a service
912
- // that failed to install would leave a key on disk doing nothing.
913
- if (collector && daemonInstalled) {
1312
+ // Cloud connection, written after the daemon exists — the daemon is what
1313
+ // runs the collector, so a credential written for a service that is not
1314
+ // there would be a key on disk doing nothing.
1315
+ //
1316
+ // `connectToCloud` re-verifies each capability and writes only what actually
1317
+ // works, so a key revoked between the earlier probe and here degrades to a
1318
+ // reported partial rather than a connection this machine does not have.
1319
+ let connected = false;
1320
+ if (connect) {
914
1321
  try {
915
- writeIngestCredential(collector.cred);
916
- writeCollectorSettings({
917
- sessions: collector.sessions,
918
- hooks: true,
919
- hooksVerbosity: "decisions",
920
- redact: "minimal",
1322
+ const outcome = await connectToCloud({
1323
+ url: connect.url,
1324
+ token: connect.token,
1325
+ machineId: connect.machineId,
1326
+ machineLabel: connect.machineLabel,
1327
+ // Both streams, as disclosed at the connect question. This is the one
1328
+ // place that decision becomes a written setting.
1329
+ sessions: true,
1330
+ });
1331
+ connected = outcome.anyConfigured;
1332
+ // Show the human label with the id in parentheses when they differ.
1333
+ const shownAs =
1334
+ connect.machineLabel === connect.machineId
1335
+ ? connect.machineId
1336
+ : `${connect.machineLabel} (${connect.machineId})`;
1337
+ for (const line of describeOutcome(outcome, shownAs, connect.url)) {
1338
+ stdout.write(`${line}\n`);
1339
+ }
1340
+ // Never the key, the URL, or the count — only that it happened and which
1341
+ // capabilities the server actually granted.
1342
+ void emit("configure_connect", {
1343
+ policy_ok: outcome.policy.ok,
1344
+ ingest_ok: outcome.ingest.ok,
921
1345
  });
922
- // Never the key, the URL, or the count — only that it happened and what
923
- // shape of data the user agreed to send.
924
- void emit("configure_collector", { sessions: collector.sessions });
925
1346
  } catch (err) {
926
- // Non-fatal, like the daemon: the rest of setup is worth keeping, and a
927
- // machine with no collector behaves exactly as every release before this.
1347
+ // Non-fatal, unlike the daemon: enforcement does not depend on the
1348
+ // dashboard, and a machine with no connection behaves exactly as every
1349
+ // release before this one did.
928
1350
  hookLogWarn(
929
- `collector configuration was not written: ${err instanceof Error ? err.message : String(err)}`,
1351
+ `cloud connection was not written: ${err instanceof Error ? err.message : String(err)}`,
930
1352
  );
1353
+ stdout.write("\nCould not connect — everything else applied. Retry with `failproofai config --connect`.\n");
931
1354
  }
932
- } else if (collector && !daemonInstalled) {
933
- hookLogWarn("skipped collector configuration because the daemon was not installed");
934
1355
  }
935
1356
 
936
1357
  await applied;
937
- // Only now — a completed apply — is the launcher considered "seen", so the
938
- // first-run bare invocation stops redirecting here and opens the dashboard.
1358
+ // Only now — a completed apply — is the launcher considered "seen", so
1359
+ // first-run onboarding stops offering itself on every command.
939
1360
  markLauncherSeen();
1361
+ // And any record of an earlier failure is now false: this machine got set up.
1362
+ clearOnboardingAttempt();
940
1363
 
941
1364
  // Keep this inside a standard 80-column terminal. `writeLines` truncates with
942
1365
  // a hard cut and no ellipsis, so an over-long line doesn't just lose its tail
@@ -949,12 +1372,21 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
949
1372
  : customEnabled === false
950
1373
  ? " · custom policies DISABLED"
951
1374
  : "";
952
- const daemonNote = daemonInstalled ? " · background daemon enabled" : "";
1375
+ const daemonNote = daemonInstalled ? " · daemon on" : "";
1376
+ const cloudNote = connected ? " · reporting on" : "";
953
1377
  const assistants = `${clis.length} assistant${clis.length === 1 ? "" : "s"}`;
954
1378
  outro(
955
- `Setup complete — ${policies.length} policies${customNote} · ${assistants}${daemonNote}`,
1379
+ `Setup complete — ${policies.length} policies${customNote} · ${assistants}${daemonNote}${cloudNote}`,
956
1380
  { ok: true },
957
1381
  stdout,
958
1382
  );
959
- return { applied: true, scope, clis, policies };
1383
+ return {
1384
+ applied: true,
1385
+ target,
1386
+ scopes,
1387
+ clis,
1388
+ policies,
1389
+ daemonInstalled,
1390
+ connected,
1391
+ };
960
1392
  }