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