failproofai 1.0.10 → 1.0.11-beta.1
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/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- 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 +4 -4
- 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.meta +8 -0
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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 +4 -4
- 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.meta +8 -0
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +4 -4
- 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 -4
- 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 -2
- 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 +4 -4
- 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 -3
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -3
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +4 -4
- 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.meta +8 -0
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -4
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +8 -8
- 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/build-manifest.json +3 -3
- 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 +5 -5
- 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/build-manifest.json +3 -3
- 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 +5 -5
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +5 -5
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +9 -9
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0qxhecv._.js → [externals]__0d7i9ea._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[externals]__0sg9-3p._.js → [externals]__0hjaw70._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1_9ze9b._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1oenfmn._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ga9hs7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0moxohc._.js +22 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z2ll-a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +11 -11
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1autwno._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1v25n-6._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
- package/.next/standalone/.next/server/chunks/_1fz8m31._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1jrv3hg._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1nx8fim._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ogwigf._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1q27azx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0el6rg8._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0ol5pa3._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1rqmk27._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1w7tiq4._.js} +2 -2
- 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]__0oqrl71._.js → [root-of-the-server]__042u-jn._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-bm5zf._.js → [root-of-the-server]__0502lat._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_gn5iz._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f765xr._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01bmjsj._.js → [root-of-the-server]__0h38gzz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hoaz1g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ouobn4._.js → [root-of-the-server]__0mxmwtx._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__19evfi8._.js → [root-of-the-server]__0uescqv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1484zv2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__19dutr7._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u6sn69._.js → [root-of-the-server]__1ab4ifb._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fbrk56._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1um4yxn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wb4yt2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1y3n3cn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0yq5_3a._.js → _1ffxwxz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_042cgl1._.js → _1o95rf0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1zopuov._.js → _1thr5rf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_16pa9ht._.js → _1v1o2ya._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -14
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0nkkbfv._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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 +23 -23
- package/.next/standalone/.next/static/chunks/08tmc_7n-rba1.js +1 -0
- package/.next/standalone/.next/static/chunks/{3mjfvng11xnoo.js → 09tlrmucn6oq3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{078dofaxvjgpd.js → 0i69k917myh7b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03h3b-uomi7_d.js → 0u_nhsujkactp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j92xul6y24_v.js → 0zp51hzx62i-u.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3u1s8dsqiean5.js → 1fz7q0gtnmeod.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3ob2161ap37ul.js → 1j75le8wln-26.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2kj826h1w9r47.js → 1z4km7tw0_50h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1fqukzzowjflt.js → 239ay_80-xr8z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3nkc2jlgyygda.js → 2c9pc5xco6rnu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 3n46e45atic-m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0542cuk_va8g1.js → 3sg07wnn969xq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-11dn2pegmg9_9.js → turbopack-1opxka4dtjbc5.js} +1 -1
- package/.next/standalone/app/actions/install-hooks-web.ts +10 -2
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/256.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +17 -17
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +18 -18
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +3 -3
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +13 -13
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/base-server.js +1 -2
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +265 -53
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +177 -19
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +36 -17
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +14 -6
- package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/lib/route-cache-key.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +9 -11
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +4 -2
- package/.next/standalone/node_modules/next/dist/server/next-server.js +7 -3
- package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +32 -16
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +6 -1
- package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +9 -2
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +30 -29
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +18 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +14 -11
- package/.next/standalone/sdk/python/README.md +41 -0
- package/.next/standalone/sdk/python/docs/README.md +1 -0
- package/.next/standalone/sdk/python/docs/otel/README.md +36 -0
- package/.next/standalone/sdk/python/docs/otel/examples/agent_trace.py +30 -0
- package/.next/standalone/sdk/python/docs/otel/examples/quickstart.py +22 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +8 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +3 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/otel.py +189 -0
- package/.next/standalone/sdk/python/pyproject.toml +2 -1
- package/.next/standalone/sdk/python/tests/test_docs.py +16 -3
- package/.next/standalone/sdk/python/tests/test_integrations.py +1 -1
- package/.next/standalone/sdk/python/tests/test_otel.py +147 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +4 -1
- package/.next/standalone/sdk/python/uv.lock +258 -147
- package/.next/standalone/sdk/typescript/README.md +43 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +9 -2
- package/.next/standalone/sdk/typescript/package-lock.json +57 -5
- package/.next/standalone/sdk/typescript/package.json +2 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +9 -0
- package/.next/standalone/sdk/typescript/src/index.ts +2 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +4 -1
- package/.next/standalone/sdk/typescript/src/integrations/otel.ts +204 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +1 -1
- package/.next/standalone/sdk/typescript/test/otel.test.ts +125 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +653 -1077
- package/dist/cli.mjs +8174 -3930
- package/dist/worker.mjs +170 -62
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +14 -11
- package/scripts/launch-facts.ts +157 -0
- package/scripts/launch-ready.ts +228 -0
- package/scripts/launch-screen.ts +202 -0
- package/scripts/launch.ts +106 -49
- package/scripts/skew-log-filter.ts +42 -0
- package/src/audit/cli-login.ts +39 -59
- package/src/audit/cli.ts +308 -228
- package/src/audit/index.ts +45 -2
- package/src/audit/schedule-cli.ts +83 -120
- package/src/audit/types.ts +31 -0
- package/src/hooks/agent-selection.ts +155 -0
- package/src/hooks/backfill-cli.ts +151 -137
- package/src/hooks/backfill-request.ts +149 -0
- package/src/hooks/cloud-connection.ts +1 -1
- package/src/hooks/cloud-enrollment-cli.ts +78 -28
- package/src/hooks/cloud-introspect.ts +3 -0
- package/src/hooks/collector-config.ts +10 -1
- package/src/hooks/config-status.ts +606 -0
- package/src/hooks/configure-wizard.ts +915 -989
- package/src/hooks/daemon-service.ts +16 -6
- package/src/hooks/did-you-mean.ts +185 -0
- package/src/hooks/flush-cli.ts +33 -7
- package/src/hooks/fp-config.ts +96 -0
- package/src/hooks/fp-home.ts +11 -0
- package/src/hooks/fp-reset.ts +102 -95
- package/src/hooks/handler.ts +4 -0
- package/src/hooks/harness-cli.ts +87 -138
- package/src/hooks/install-prompt.ts +38 -477
- package/src/hooks/integrations.ts +1 -1
- package/src/hooks/jev-cli.ts +183 -196
- package/src/hooks/manager.ts +554 -358
- package/src/hooks/onboarding-attempt.ts +11 -9
- package/src/hooks/otel-cli.ts +344 -0
- package/src/hooks/otel-output.ts +24 -0
- package/src/hooks/otel-writers.ts +225 -0
- package/src/hooks/pack-cli.ts +219 -204
- package/src/hooks/report-body.ts +50 -0
- package/src/hooks/safe-config-write.ts +4 -2
- package/src/hooks/session-pause-cli.ts +93 -73
- package/src/hooks/tui.ts +849 -389
- package/src/hooks/uninstall-cli.ts +136 -149
- package/src/hooks/untraced-note.ts +52 -0
- package/.next/standalone/.next/server/chunks/[externals]__19_pzeq._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__20kzpkf._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0igxt32._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +0 -22
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +0 -6
- package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +0 -1
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_ssgManifest.js +0 -0
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `failproofai config` — the interactive setup launcher.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Four steps, in the order the machine needs them:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 4.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Selections REPLACE the enabled set at the chosen scope (the picker pre-checks
|
|
14
|
-
* whatever is already enabled, so unticking removes). Reuses the tested
|
|
15
|
-
* install/uninstall manager and the existing searchable policy picker.
|
|
6
|
+
* 1. Daemon — REQUIRED. Asks for sudo first, on a clean terminal.
|
|
7
|
+
* 2. Connect — one masked API-key field; Tab for open source.
|
|
8
|
+
* 3. Agents — which agents failproofai traces: hooks, collection and
|
|
9
|
+
* backfill all follow this one saved list.
|
|
10
|
+
* 4. Done — applied after the last answer; there is no separate review.
|
|
11
|
+
* Esc at any step before that changes nothing.
|
|
16
12
|
*
|
|
17
13
|
* ## Two ordering rules that are not cosmetic
|
|
18
14
|
*
|
|
@@ -21,10 +17,11 @@
|
|
|
21
17
|
* fired from underneath a rendered screen the prompt is invisible and the typed
|
|
22
18
|
* password lands in a redrawn frame.
|
|
23
19
|
*
|
|
24
|
-
* **
|
|
25
|
-
* requires
|
|
26
|
-
* rather than half-configured. Writing hooks first and discovering the
|
|
27
|
-
* will not start afterwards is the one ordering that cannot be undone
|
|
20
|
+
* **Everything is written at apply, the daemon before anything else.** Setup
|
|
21
|
+
* requires the daemon, so a failure has to leave the machine exactly as it was
|
|
22
|
+
* found rather than half-configured. Writing hooks first and discovering the
|
|
23
|
+
* service will not start afterwards is the one ordering that cannot be undone
|
|
24
|
+
* cleanly.
|
|
28
25
|
*/
|
|
29
26
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
30
27
|
import { homedir } from "node:os";
|
|
@@ -33,27 +30,33 @@ import { dirname, resolve, sep } from "node:path";
|
|
|
33
30
|
import { CORE_SOURCE } from "./pack-store";
|
|
34
31
|
|
|
35
32
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
collapseAfter,
|
|
34
|
+
colorsEnabled,
|
|
35
|
+
multiSelect,
|
|
36
|
+
OPEN_SOURCE,
|
|
37
|
+
optsFor,
|
|
38
|
+
paint,
|
|
39
|
+
promptCloudKey,
|
|
40
|
+
screenKit,
|
|
41
|
+
type CloudKeyAnswer,
|
|
41
42
|
type MultiChoice,
|
|
43
|
+
type OpenSource,
|
|
42
44
|
type TTYIn,
|
|
43
45
|
type TTYOut,
|
|
44
46
|
} from "./tui";
|
|
45
47
|
import {
|
|
46
48
|
DEFAULT_INGEST_URL,
|
|
49
|
+
readIngestCredential,
|
|
47
50
|
validateIngestKey,
|
|
48
51
|
} from "./collector-config";
|
|
49
52
|
import {
|
|
50
53
|
detectInstalledClis,
|
|
51
54
|
getIntegration,
|
|
52
55
|
settingsPathsFor,
|
|
53
|
-
unhookedHermesProfiles,
|
|
54
56
|
} from "./integrations";
|
|
55
57
|
import { INTEGRATION_TYPES, type IntegrationType, type HookScope } from "./types";
|
|
56
|
-
import { installHooks } from "./manager";
|
|
58
|
+
import { enforcingPolicyCount, installHooks, notEnforcingReason } from "./manager";
|
|
59
|
+
import { readInstalledPacks } from "./pack-manifest";
|
|
57
60
|
import { getConfigPathForScope, readScopedHooksConfig } from "./hooks-config";
|
|
58
61
|
import { discoverPolicyFiles, findSkippedPolicyFiles } from "./custom-hooks-loader";
|
|
59
62
|
import { trackHookEvent } from "./hook-telemetry";
|
|
@@ -62,7 +65,6 @@ import {
|
|
|
62
65
|
canElevate,
|
|
63
66
|
isDaemonSupportedPlatform,
|
|
64
67
|
installDaemonService,
|
|
65
|
-
daemonServiceFilePath,
|
|
66
68
|
daemonServiceStatus,
|
|
67
69
|
daemonServiceNeedsUpgrade,
|
|
68
70
|
daemonStatusCommand,
|
|
@@ -76,6 +78,7 @@ import {
|
|
|
76
78
|
} from "./daemon-service";
|
|
77
79
|
import { hookLogWarn } from "./hook-logger";
|
|
78
80
|
import {
|
|
81
|
+
maskToken,
|
|
79
82
|
readCloudCredentials,
|
|
80
83
|
resolveMachineId,
|
|
81
84
|
resolveMachineLabel,
|
|
@@ -85,12 +88,15 @@ import {
|
|
|
85
88
|
cloudBaseFor,
|
|
86
89
|
ingestUrlFor,
|
|
87
90
|
connectToCloud,
|
|
88
|
-
|
|
91
|
+
jevLines,
|
|
89
92
|
} from "./cloud-connection";
|
|
93
|
+
import { hasPermission, introspectKey, PERMISSION_EVENTS, PERMISSION_POLICIES } from "./cloud-introspect";
|
|
94
|
+
import { readConfig, readCredentials, updateConfig } from "./fp-config";
|
|
95
|
+
import { requestFreshStart } from "./backfill-request";
|
|
96
|
+
import { hookedAtUserScope } from "./agent-selection";
|
|
90
97
|
import {
|
|
91
98
|
detectSetupState,
|
|
92
99
|
isConfigured,
|
|
93
|
-
scopesFor,
|
|
94
100
|
type SetupTarget,
|
|
95
101
|
} from "./setup-state";
|
|
96
102
|
import { customPoliciesDir, launcherMarker } from "./fp-home";
|
|
@@ -134,6 +140,10 @@ export interface WizardAnswers {
|
|
|
134
140
|
machineLabel?: string;
|
|
135
141
|
/** Record decisions only, no session transcripts. */
|
|
136
142
|
noTranscripts?: boolean;
|
|
143
|
+
/** `--oss`: skip the connect step. A connection already here is left as it is. */
|
|
144
|
+
oss?: boolean;
|
|
145
|
+
/** `--agents`: the agents to trace, already validated. Skips the agents step. */
|
|
146
|
+
agents?: IntegrationType[];
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
/**
|
|
@@ -149,7 +159,9 @@ export type WizardAbort =
|
|
|
149
159
|
| "unsupported_platform"
|
|
150
160
|
| "running_as_sudo"
|
|
151
161
|
/** A cloud key was supplied and the server refused it. Unattended only. */
|
|
152
|
-
| "cloud_unverified"
|
|
162
|
+
| "cloud_unverified"
|
|
163
|
+
/** A key was supplied on the command line, and --url or FAILPROOFAI_CLOUD_URL cannot be used. */
|
|
164
|
+
| "bad_cloud_url";
|
|
153
165
|
|
|
154
166
|
export interface WizardResult {
|
|
155
167
|
applied: boolean;
|
|
@@ -160,7 +172,11 @@ export interface WizardResult {
|
|
|
160
172
|
clis?: IntegrationType[];
|
|
161
173
|
policies?: string[];
|
|
162
174
|
daemonInstalled?: boolean;
|
|
175
|
+
/** A connection was written by this run. */
|
|
163
176
|
connected?: boolean;
|
|
177
|
+
/** Whether the machine is connected once the run ends. */
|
|
178
|
+
mode?: "cloud" | "oss";
|
|
179
|
+
agents?: { selected: IntegrationType[]; added: IntegrationType[]; removed: IntegrationType[] };
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
async function emit(event: string, props: Record<string, unknown>): Promise<void> {
|
|
@@ -189,70 +205,221 @@ export function clisSupportingScope(scope: HookScope): IntegrationType[] {
|
|
|
189
205
|
return INTEGRATION_TYPES.filter((id) => getIntegration(id).scopes.includes(scope));
|
|
190
206
|
}
|
|
191
207
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Which agents a run starts with ticked, and what it compares the answer to.
|
|
210
|
+
*
|
|
211
|
+
* Three cases, told apart by what is on disk:
|
|
212
|
+
* - **rerun**: a saved selection exists. It is restored exactly, plus any
|
|
213
|
+
* agent detected now that no earlier setup saw — ticked, and tagged new.
|
|
214
|
+
* - **upgrade**: no saved selection, but hooks are installed (a machine set
|
|
215
|
+
* up before the selection existed). What is protected today is ticked, so
|
|
216
|
+
* finishing setup neither widens nor narrows it.
|
|
217
|
+
* - **first**: neither. The detected agents are ticked.
|
|
218
|
+
*
|
|
219
|
+
* Every agent is listed, detected or not (decision D1): an agent installed
|
|
220
|
+
* next week can be ticked today, and an empty machine is not stuck.
|
|
221
|
+
*/
|
|
222
|
+
export type AgentPlanMode = "first" | "rerun" | "upgrade";
|
|
223
|
+
|
|
224
|
+
export interface AgentPlan {
|
|
225
|
+
mode: AgentPlanMode;
|
|
226
|
+
/** Every agent: the detected ones first, then the rest. */
|
|
227
|
+
order: IntegrationType[];
|
|
228
|
+
detected: Set<IntegrationType>;
|
|
229
|
+
/** What the answer is compared to: the saved selection, or what is hooked today. */
|
|
230
|
+
baseline: Set<IntegrationType>;
|
|
231
|
+
/** Detected now and seen at no earlier setup. Re-runs only. */
|
|
232
|
+
fresh: Set<IntegrationType>;
|
|
233
|
+
/** Ticked when the step opens. */
|
|
234
|
+
initial: IntegrationType[];
|
|
235
|
+
/** The `seen` list to save: every agent detected at any setup so far. */
|
|
236
|
+
seen: IntegrationType[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function planAgents(input: {
|
|
240
|
+
detected: IntegrationType[];
|
|
241
|
+
saved?: { selected: string[]; seen: string[] };
|
|
242
|
+
hooked: IntegrationType[];
|
|
243
|
+
}): AgentPlan {
|
|
244
|
+
const known = (ids: string[]): IntegrationType[] =>
|
|
245
|
+
INTEGRATION_TYPES.filter((id) => ids.includes(id));
|
|
246
|
+
const detected = new Set(known(input.detected));
|
|
247
|
+
const order = [
|
|
196
248
|
...INTEGRATION_TYPES.filter((id) => detected.has(id)),
|
|
197
249
|
...INTEGRATION_TYPES.filter((id) => !detected.has(id)),
|
|
198
250
|
];
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
251
|
+
const ordered = (ids: Iterable<IntegrationType>): IntegrationType[] => {
|
|
252
|
+
const set = new Set(ids);
|
|
253
|
+
return order.filter((id) => set.has(id));
|
|
254
|
+
};
|
|
255
|
+
// Seen accumulates. Rebuilding it from this run's detection would let a PATH
|
|
256
|
+
// glitch forget an agent, and the next run would call it new — ticked — after
|
|
257
|
+
// somebody had deliberately unticked it.
|
|
258
|
+
const seen = ordered([...known(input.saved?.seen ?? []), ...detected]);
|
|
259
|
+
|
|
260
|
+
if (input.saved) {
|
|
261
|
+
const baseline = new Set(known(input.saved.selected));
|
|
262
|
+
const previouslySeen = new Set(known(input.saved.seen));
|
|
263
|
+
const fresh = new Set([...detected].filter((id) => !previouslySeen.has(id) && !baseline.has(id)));
|
|
264
|
+
return { mode: "rerun", order, detected, baseline, fresh, initial: ordered([...baseline, ...fresh]), seen };
|
|
265
|
+
}
|
|
266
|
+
const hooked = known(input.hooked);
|
|
267
|
+
if (hooked.length > 0) {
|
|
268
|
+
return { mode: "upgrade", order, detected, baseline: new Set(hooked), fresh: new Set(), initial: ordered(hooked), seen };
|
|
269
|
+
}
|
|
270
|
+
return { mode: "first", order, detected, baseline: new Set(), fresh: new Set(), initial: ordered(detected), seen };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The selection a run takes without asking: no terminal, or a `--token` run.
|
|
275
|
+
* The plan's starting ticks — and every agent when those are empty, which only
|
|
276
|
+
* happens on a machine where nothing is detected, hooked or saved. Hooking
|
|
277
|
+
* nothing there would leave setup finished and nothing guarded.
|
|
278
|
+
*/
|
|
279
|
+
export function defaultSelection(plan: AgentPlan): IntegrationType[] {
|
|
280
|
+
// A saved answer stands even when it is empty: removing every agent's hooks
|
|
281
|
+
// saves an empty selection, and an unattended re-run must not put them back.
|
|
282
|
+
if (plan.mode === "rerun") return plan.initial;
|
|
283
|
+
return plan.initial.length > 0 ? plan.initial : [...plan.order];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** What a chosen selection changes, against the plan's baseline. */
|
|
287
|
+
export function agentChanges(
|
|
288
|
+
plan: AgentPlan,
|
|
289
|
+
selected: IntegrationType[],
|
|
290
|
+
): { added: IntegrationType[]; removed: IntegrationType[] } {
|
|
291
|
+
const chosen = new Set(selected);
|
|
292
|
+
return {
|
|
293
|
+
// On a first run nothing was traced before, so nothing is "added" back.
|
|
294
|
+
added: plan.mode === "first" ? [] : plan.order.filter((id) => chosen.has(id) && !plan.baseline.has(id)),
|
|
295
|
+
removed: plan.order.filter((id) => !chosen.has(id) && plan.baseline.has(id)),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The picker's rows: one per agent, in the plan's order. */
|
|
300
|
+
export function agentChoices(plan: AgentPlan): MultiChoice<IntegrationType>[] {
|
|
301
|
+
return plan.order.map((id) => ({
|
|
302
|
+
label: getIntegration(id).displayName,
|
|
303
|
+
value: id,
|
|
304
|
+
checked: plan.initial.includes(id),
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The tag after a row, recomputed on every toggle: `new on this machine`,
|
|
310
|
+
* then what the toggle changes against the baseline, then `not installed`.
|
|
311
|
+
* No change tags on a first run — every tick would read "will be added".
|
|
312
|
+
*/
|
|
313
|
+
export function agentTag(
|
|
314
|
+
plan: AgentPlan,
|
|
315
|
+
c: ReturnType<typeof paint>,
|
|
316
|
+
): (id: IntegrationType, checked: boolean) => string | undefined {
|
|
317
|
+
return (id, checked) => {
|
|
318
|
+
if (plan.fresh.has(id)) return c.guide("new on this machine");
|
|
319
|
+
if (plan.mode !== "first") {
|
|
320
|
+
const before = plan.baseline.has(id);
|
|
321
|
+
if (checked && !before) return c.guide("will be added");
|
|
322
|
+
if (!checked && before) return c.warn("will be removed");
|
|
219
323
|
}
|
|
324
|
+
if (!plan.detected.has(id)) return c.ink3("not installed");
|
|
325
|
+
return undefined;
|
|
326
|
+
};
|
|
327
|
+
}
|
|
220
328
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
329
|
+
/** "A", "A and B", "A, B and C". */
|
|
330
|
+
function namesList(ids: IntegrationType[]): string {
|
|
331
|
+
const names = ids.map((id) => getIntegration(id).displayName);
|
|
332
|
+
return names.length <= 1 ? (names[0] ?? "") : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Wrap `text` after a 2-column glyph, continuing under it, within `width`. */
|
|
336
|
+
function wrapAfterGlyph(glyph: string, text: string, width: number): string[] {
|
|
337
|
+
const out: string[] = [];
|
|
338
|
+
let line = "";
|
|
339
|
+
for (const word of text.split(" ")) {
|
|
340
|
+
if (line && 2 + line.length + 1 + word.length > width) {
|
|
341
|
+
out.push(line);
|
|
342
|
+
line = word;
|
|
343
|
+
} else {
|
|
344
|
+
line = line ? `${line} ${word}` : word;
|
|
226
345
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
346
|
+
}
|
|
347
|
+
if (line) out.push(line);
|
|
348
|
+
return out.map((l, i) => (i === 0 ? `${glyph} ${l}` : ` ${l}`));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The AGENTS step, collapsed. Names are listed when nothing changed against
|
|
353
|
+
* the baseline; otherwise the count, then what will be added and removed. What a
|
|
354
|
+
* removal promises is only what the code does: no more hooks, and on a
|
|
355
|
+
* connected machine no more collection. Nothing already collected is deleted.
|
|
356
|
+
*/
|
|
357
|
+
export function agentsCollapsedLines(
|
|
358
|
+
kit: ReturnType<typeof screenKit>,
|
|
359
|
+
plan: AgentPlan,
|
|
360
|
+
selected: IntegrationType[],
|
|
361
|
+
collecting: boolean,
|
|
362
|
+
c: ReturnType<typeof paint>,
|
|
363
|
+
): string[] {
|
|
364
|
+
const { added, removed } = agentChanges(plan, selected);
|
|
365
|
+
const n = selected.length;
|
|
366
|
+
const tracing = `Tracing ${n} ${n === 1 ? "agent" : "agents"}`;
|
|
367
|
+
const lines = [kit.head("Agents")];
|
|
368
|
+
if (n === 0) {
|
|
369
|
+
lines.push(kit.caution("Tracing no agents.", "failproofai config"));
|
|
370
|
+
return lines;
|
|
371
|
+
}
|
|
372
|
+
if (added.length === 0 && removed.length === 0) {
|
|
373
|
+
const list = selected.map((id) => getIntegration(id).displayName).join(", ");
|
|
374
|
+
lines.push(...wrapAfterGlyph(c.guide("✓"), `${tracing}: ${list}`, kit.cols));
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
lines.push(kit.ok(tracing));
|
|
378
|
+
// The choice, not the result: this prints before apply, and an apply that
|
|
379
|
+
// stops at the daemon changes nothing (decision D3).
|
|
380
|
+
if (added.length > 0) lines.push(` Adding ${namesList(added)}.`);
|
|
381
|
+
if (removed.length > 0) {
|
|
382
|
+
const they = removed.length === 1 ? "it" : "they";
|
|
383
|
+
lines.push(
|
|
384
|
+
collecting
|
|
385
|
+
? ` Removing ${namesList(removed)}: ${they} will no longer be hooked or collected. Sessions already collected are kept.`
|
|
386
|
+
: ` Removing ${namesList(removed)}: ${they} will no longer be hooked.`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return lines;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Lives with the selection it seeds; re-exported where setup always had it. */
|
|
393
|
+
export { hookedAtUserScope };
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Take failproofai's hooks out of these agents' USER settings, and nothing
|
|
397
|
+
* else. Not `removeHooks`: that is the full uninstall, which also strips
|
|
398
|
+
* project and local hooks under the working directory and resets the policy
|
|
399
|
+
* configuration of every scope. Unticking an agent in setup must leave every
|
|
400
|
+
* `policies-config.json` exactly as it was.
|
|
401
|
+
*/
|
|
402
|
+
export function removeUserScopeHooks(clis: IntegrationType[], cwd: string): number {
|
|
403
|
+
let removed = 0;
|
|
404
|
+
for (const id of clis) {
|
|
405
|
+
const integration = getIntegration(id);
|
|
406
|
+
if (!integration.scopes.includes("user")) continue;
|
|
407
|
+
for (const settingsPath of settingsPathsFor(integration, "user", cwd)) {
|
|
408
|
+
// Hermes is asked even without a config file, so it can clear a plugin
|
|
409
|
+
// directory left behind by an interrupted install.
|
|
410
|
+
if (id !== "hermes" && !existsSync(settingsPath)) continue;
|
|
232
411
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Profile discovery is best-effort — never block the wizard.
|
|
412
|
+
removed += integration.removeHooksFromFile(settingsPath);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
hookLogWarn(
|
|
415
|
+
`could not remove failproofai hooks from ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
416
|
+
);
|
|
239
417
|
}
|
|
240
418
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
value: id,
|
|
244
|
-
checked: isDetected || installedHere,
|
|
245
|
-
section: isDetected ? "Detected" : "Not installed · set up ahead of time",
|
|
246
|
-
hint: installedHere
|
|
247
|
-
? "already configured"
|
|
248
|
-
: (partialHint ?? (isDetected ? undefined : "not on PATH")),
|
|
249
|
-
};
|
|
250
|
-
});
|
|
419
|
+
}
|
|
420
|
+
return removed;
|
|
251
421
|
}
|
|
252
422
|
|
|
253
|
-
|
|
254
|
-
const DIM_NOTE = "(auto-loaded)";
|
|
255
|
-
|
|
256
423
|
/**
|
|
257
424
|
* Persist the Custom checkbox into the scope's config, after installHooks has
|
|
258
425
|
* written it (installHooks copies the previous config forward, so writing
|
|
@@ -363,196 +530,6 @@ export function describeCustomPolicies(cwd: string): {
|
|
|
363
530
|
return { active, warnings, fileCount, scopes };
|
|
364
531
|
}
|
|
365
532
|
|
|
366
|
-
/**
|
|
367
|
-
* The wizard's one-line completion summary. Pure and exported so the widest
|
|
368
|
-
* real combination — every policy, every CLI, custom/daemon/reporting all
|
|
369
|
-
* present — can be pinned by a test without having to drive the whole wizard
|
|
370
|
-
* through a real chdir + on-disk custom-policy fixture.
|
|
371
|
-
*
|
|
372
|
-
* Kept inside a standard 80-column terminal: `writeLines` truncates with a
|
|
373
|
-
* hard cut and no ellipsis, so an over-long line doesn't just lose its tail
|
|
374
|
-
* — it reads as broken output. Naming all ten CLIs once took it to 182
|
|
375
|
-
* characters; the count alone carries the same information, and the user
|
|
376
|
-
* picked them two screens ago. A single grouped "· a, b, c" clause bounds the
|
|
377
|
-
* optional notes to one separator and short tags, rather than three
|
|
378
|
-
* independent " · " clauses stacking up.
|
|
379
|
-
*/
|
|
380
|
-
export function buildCompletionSummary(
|
|
381
|
-
policiesCount: number,
|
|
382
|
-
harnessesCount: number,
|
|
383
|
-
customEnabled: boolean | undefined,
|
|
384
|
-
daemonInstalled: boolean,
|
|
385
|
-
connected: boolean,
|
|
386
|
-
): string {
|
|
387
|
-
const extras: string[] = [];
|
|
388
|
-
if (customEnabled === true) extras.push("custom");
|
|
389
|
-
else if (customEnabled === false) extras.push("custom off");
|
|
390
|
-
if (daemonInstalled) extras.push("daemon");
|
|
391
|
-
if (connected) extras.push("reporting");
|
|
392
|
-
const extrasNote = extras.length > 0 ? ` · ${extras.join(", ")}` : "";
|
|
393
|
-
const harnesses = `${harnessesCount} harness${harnessesCount === 1 ? "" : "es"}`;
|
|
394
|
-
const line = (selection: string) =>
|
|
395
|
-
`Setup complete — ${selection} · ${harnesses}${extrasNote}`;
|
|
396
|
-
|
|
397
|
-
// Bound the WHOLE line, not just the names. `writeLines` truncates with a hard
|
|
398
|
-
// cut and no ellipsis, so 81 characters does not lose a tail — it reads as
|
|
399
|
-
// broken output. Naming is preferred and degrades to the count only when the
|
|
400
|
-
// full line will not fit, which is checked rather than guessed at: the extras
|
|
401
|
-
// clause grows too ("custom, daemon, reporting" is 25 characters), so a names
|
|
402
|
-
// budget alone was wrong for exactly the combinations that need it most.
|
|
403
|
-
return line(describeSelection(policiesCount));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* The budget this line has to fit in.
|
|
408
|
-
*
|
|
409
|
-
* 80 columns minus the 3-column gutter the existing summary tests already assert
|
|
410
|
-
* (`message.length + GUTTER <= 80`) — matching that convention rather than
|
|
411
|
-
* inventing a second one, because two different width rules for the same line is
|
|
412
|
-
* how one of them ends up wrong.
|
|
413
|
-
*/
|
|
414
|
-
const MAX_SUMMARY_COLUMNS = 77;
|
|
415
|
-
|
|
416
|
-
/** The column this line aligns under, matching ` Policies : `. */
|
|
417
|
-
const POLICY_LIST_INDENT = " ".repeat(15);
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* A taste of the set, not the set: two names and a count of the rest.
|
|
421
|
-
*
|
|
422
|
-
* The same shape `describeSelection` uses for bundles, for the same reason —
|
|
423
|
-
* naming everything turned a four-line review screen into a fifteen-line one,
|
|
424
|
-
* and a screen nobody reads to the bottom is worse at conveying what is about
|
|
425
|
-
* to happen than a short one. Two names say what KIND of thing these are;
|
|
426
|
-
* `failproofai policies list` says the rest, for the person who wants it.
|
|
427
|
-
*
|
|
428
|
-
* The whole review body is rendered dim by the prompt (`tui.ts`), so this
|
|
429
|
-
* reads as a subtitle to the count above it rather than competing with it.
|
|
430
|
-
*
|
|
431
|
-
* Degrades by dropping names rather than by overflowing: `writeLines`
|
|
432
|
-
* truncates with a hard cut and no ellipsis, so a line that runs over ends
|
|
433
|
-
* mid-slug and reads as a policy name that does not exist. Two long names
|
|
434
|
-
* (`sanitize-private-key-content` is 28) plus the prefix is within budget, but
|
|
435
|
-
* the guard is here rather than argued about, because a future rename is
|
|
436
|
-
* exactly the kind of change nobody re-measures.
|
|
437
|
-
*/
|
|
438
|
-
export function policyNamesLine(names: string[]): string[] {
|
|
439
|
-
if (names.length === 0) return [];
|
|
440
|
-
const budget = MAX_SUMMARY_COLUMNS - POLICY_LIST_INDENT.length;
|
|
441
|
-
for (const take of [2, 1]) {
|
|
442
|
-
const shown = names.slice(0, take);
|
|
443
|
-
const rest = names.length - shown.length;
|
|
444
|
-
const text = rest > 0 ? `${shown.join(", ")} +${rest}` : shown.join(", ");
|
|
445
|
-
if (text.length <= budget) return [`${POLICY_LIST_INDENT}${text}`];
|
|
446
|
-
}
|
|
447
|
-
return [];
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** The Policies line's value. With policies no longer chosen during setup, this
|
|
451
|
-
* reports what is enabled and nothing about how it got that way. */
|
|
452
|
-
function describeSelection(policiesCount: number): string {
|
|
453
|
-
return `${policiesCount} ${policiesCount === 1 ? "policy" : "policies"}`;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
export function reviewLines(state: {
|
|
458
|
-
/** What the scope step resolved to. Expands to one or two real scopes. */
|
|
459
|
-
target: SetupTarget;
|
|
460
|
-
clis: IntegrationType[];
|
|
461
|
-
policies: string[];
|
|
462
|
-
cwd: string;
|
|
463
|
-
/** The Custom checkbox. `undefined` = nothing to toggle, leave as-is. */
|
|
464
|
-
customEnabled?: boolean;
|
|
465
|
-
/**
|
|
466
|
-
* Whether a daemon will be installed by this run. False when one is already
|
|
467
|
-
* healthy (nothing to do) or the platform has no service manager — showing
|
|
468
|
-
* it any other way would promise work the apply will not perform.
|
|
469
|
-
*/
|
|
470
|
-
installDaemon?: boolean;
|
|
471
|
-
/** Whether an API key will be written and reporting turned on. */
|
|
472
|
-
connect?: boolean;
|
|
473
|
-
}): string[] {
|
|
474
|
-
const { target, clis, policies, cwd, customEnabled, installDaemon, connect } = state;
|
|
475
|
-
const scopes = scopesFor(target);
|
|
476
|
-
const where =
|
|
477
|
-
target === "both"
|
|
478
|
-
? `Global, plus this project (${homeify(cwd)})`
|
|
479
|
-
: target === "project"
|
|
480
|
-
? `This project (${homeify(cwd)})`
|
|
481
|
-
: "Everywhere (global)";
|
|
482
|
-
const lines: string[] = [];
|
|
483
|
-
const harnessNames = clis.map((c) => getIntegration(c).displayName);
|
|
484
|
-
lines.push(` Where : ${where}`);
|
|
485
|
-
lines.push(` Harnesses : ${harnessNames.length ? summarize(harnessNames, "harnesses") : "(none)"}`);
|
|
486
|
-
// Zero is a deliberate answer, not a failed step — say so, and say where to
|
|
487
|
-
// change it, so the review screen doesn't read like the wizard lost the
|
|
488
|
-
// selection. Hooks still install; only the builtin set is empty.
|
|
489
|
-
lines.push(
|
|
490
|
-
policies.length === 0
|
|
491
|
-
? " Policies : none enabled (add later: failproofai policies --install)"
|
|
492
|
-
: ` Policies : ${policies.length} enabled`,
|
|
493
|
-
);
|
|
494
|
-
// A taste of what they are, under the count. "15 enabled" alone is a number
|
|
495
|
-
// the user cannot check and, on the recommended path, did not choose; two
|
|
496
|
-
// names say what kind of thing it is without turning the review into a page.
|
|
497
|
-
if (policies.length > 0) {
|
|
498
|
-
lines.push(...policyNamesLine([...policies].sort()));
|
|
499
|
-
}
|
|
500
|
-
if (installDaemon && isDaemonSupportedPlatform()) {
|
|
501
|
-
lines.push(
|
|
502
|
-
` Daemon : failproofaid, installed as a system service running as you`,
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
lines.push(
|
|
506
|
-
connect
|
|
507
|
-
? " Reporting : on — policy decisions and session transcripts"
|
|
508
|
-
: " Reporting : off — nothing leaves this machine",
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
// Reflect the Custom decision, not just what is on disk. Reporting
|
|
512
|
-
// "1 file (project) (auto-loaded)" after the user had just unticked the row
|
|
513
|
-
// stated the opposite of what was about to happen.
|
|
514
|
-
const custom = describeCustomPolicies(cwd);
|
|
515
|
-
if (custom.active.length > 0) {
|
|
516
|
-
lines.push(
|
|
517
|
-
` Custom : ${custom.active.join(" · ")} ${
|
|
518
|
-
customEnabled === false ? "— DISABLED, will not load" : DIM_NOTE
|
|
519
|
-
}`,
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
for (const warning of custom.warnings) lines.push(` ${warning}`);
|
|
523
|
-
|
|
524
|
-
lines.push("");
|
|
525
|
-
lines.push(" This will update:");
|
|
526
|
-
// Deduplicated across scopes: a CLI that supports only user scope resolves to
|
|
527
|
-
// the same settings file under both halves of "Both", and listing it twice
|
|
528
|
-
// reads as two separate writes.
|
|
529
|
-
const seen = new Set<string>();
|
|
530
|
-
for (const scope of scopes) {
|
|
531
|
-
for (const cli of clis) {
|
|
532
|
-
const integration = getIntegration(cli);
|
|
533
|
-
if (!integration.scopes.includes(scope)) continue;
|
|
534
|
-
// Usually one path; Hermes lists one per profile so the operator sees
|
|
535
|
-
// every home dir that is about to be written.
|
|
536
|
-
for (const p of settingsPathsFor(integration, scope, cwd)) {
|
|
537
|
-
if (seen.has(p)) continue;
|
|
538
|
-
seen.add(p);
|
|
539
|
-
lines.push(` ${homeify(p)} ${integration.displayName} hooks`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
for (const scope of scopes) {
|
|
544
|
-
const configPath = getConfigPathForScope(scope, cwd);
|
|
545
|
-
if (seen.has(configPath)) continue;
|
|
546
|
-
seen.add(configPath);
|
|
547
|
-
lines.push(` ${homeify(configPath)} ${policies.length} policies`);
|
|
548
|
-
}
|
|
549
|
-
if (installDaemon && isDaemonSupportedPlatform()) {
|
|
550
|
-
const servicePath = daemonServiceFilePath();
|
|
551
|
-
if (servicePath) lines.push(` ${homeify(servicePath)} failproofaid service (needs root)`);
|
|
552
|
-
}
|
|
553
|
-
return lines;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
533
|
// ── First-run redirect ───────────────────────────────────────────────────────
|
|
557
534
|
|
|
558
535
|
function firstRunMarkerPath(): string {
|
|
@@ -670,7 +647,7 @@ export async function maybeFirstRunConfigure(
|
|
|
670
647
|
// and it must stay a hint rather than a failure: the command the user
|
|
671
648
|
// actually typed still runs.
|
|
672
649
|
stdout.write(
|
|
673
|
-
`\n
|
|
650
|
+
`\n${screenKit(optsFor(stdout)).caution("failproofai is not set up yet.", "failproofai config")}\n\n`,
|
|
674
651
|
);
|
|
675
652
|
return false;
|
|
676
653
|
}
|
|
@@ -681,7 +658,7 @@ export async function maybeFirstRunConfigure(
|
|
|
681
658
|
const lock = acquireOnboardingLock();
|
|
682
659
|
if (!lock) {
|
|
683
660
|
stdout.write(
|
|
684
|
-
`\n
|
|
661
|
+
`\n${screenKit(optsFor(stdout)).caution("Setup is already running in another terminal, so this one leaves it to finish.")}\n\n`,
|
|
685
662
|
);
|
|
686
663
|
return false;
|
|
687
664
|
}
|
|
@@ -730,31 +707,21 @@ export async function runConfigureWizard(
|
|
|
730
707
|
const stdin: TTYIn = io.stdin ?? process.stdin;
|
|
731
708
|
const stdout: TTYOut = io.stdout ?? process.stdout;
|
|
732
709
|
const cwd = process.cwd();
|
|
710
|
+
const started = Date.now();
|
|
733
711
|
// No terminal means no questions — not a refusal.
|
|
734
712
|
//
|
|
735
713
|
// `failproofai config` IS the authorisation: somebody typed the command whose
|
|
736
|
-
// entire job is to configure this machine.
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
// Safe to do here because the IMPLICIT path is guarded separately:
|
|
742
|
-
// `maybeFirstRunConfigure` has its own TTY check and returns before ever
|
|
743
|
-
// reaching this function, so setup never runs off the back of some other
|
|
744
|
-
// command on a headless box.
|
|
714
|
+
// entire job is to configure this machine. Safe because the IMPLICIT path is
|
|
715
|
+
// guarded separately: `maybeFirstRunConfigure` has its own TTY check and
|
|
716
|
+
// returns before ever reaching this function, so setup never runs off the back
|
|
717
|
+
// of some other command on a headless box.
|
|
745
718
|
const unattended = !stdin.isTTY || !stdout.isTTY;
|
|
746
719
|
// A key on the command line settles the whole run, not just the question it
|
|
747
|
-
// literally answers.
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
// machine up, connect it, send its data. There is nothing left to confirm —
|
|
751
|
-
// so asking "Connect to Cloud?", then the key, then "Ready to apply?" made
|
|
752
|
-
// the flag look like it had not been read. The only thing still worth
|
|
753
|
-
// stopping for is the sudo password, which is a CREDENTIAL rather than a
|
|
754
|
-
// question, and no flag can supply it.
|
|
720
|
+
// literally answers: set this machine up, connect it, send its data. The only
|
|
721
|
+
// thing still worth stopping for is the sudo password, which is a CREDENTIAL
|
|
722
|
+
// rather than a question, and no flag can supply it.
|
|
755
723
|
const preAnswered = Boolean(answers.token);
|
|
756
724
|
|
|
757
|
-
|
|
758
725
|
// Running the wizard itself under sudo configures the WRONG ACCOUNT, and
|
|
759
726
|
// does it silently: homedir() becomes /root, so the hooks land in root's
|
|
760
727
|
// settings, `daemonConfigured` is set for root, the daemon binary downloads
|
|
@@ -770,9 +737,6 @@ export async function runConfigureWizard(
|
|
|
770
737
|
"The one step that needs root (installing the service) asks for your password\n" +
|
|
771
738
|
"on its own.\n",
|
|
772
739
|
);
|
|
773
|
-
// Same reason as above: this configured nothing, and it is not a
|
|
774
|
-
// cancellation. `running_as_sudo` has been in `WizardAbort` since it was
|
|
775
|
-
// written and was never once assigned.
|
|
776
740
|
return { applied: false, abort: "running_as_sudo" };
|
|
777
741
|
}
|
|
778
742
|
|
|
@@ -780,10 +744,10 @@ export async function runConfigureWizard(
|
|
|
780
744
|
void emit("configure_started", {});
|
|
781
745
|
|
|
782
746
|
// failproofaid — the only evaluator on a configured machine — only runs on
|
|
783
|
-
// Linux and macOS. Checked before
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
747
|
+
// Linux and macOS. Checked before a single prompt is asked: completing setup
|
|
748
|
+
// anyway used to leave e.g. a Windows machine reading as configured while
|
|
749
|
+
// enforcing in-process with no fail-closed guarantee, which is worse than not
|
|
750
|
+
// being set up at all.
|
|
787
751
|
if (!isDaemonSupportedPlatform()) {
|
|
788
752
|
stdout.write(
|
|
789
753
|
`failproofai requires failproofaid, its background policy daemon, which runs on\n` +
|
|
@@ -797,614 +761,407 @@ export async function runConfigureWizard(
|
|
|
797
761
|
return { applied: false, abort: "unsupported_platform" };
|
|
798
762
|
}
|
|
799
763
|
|
|
800
|
-
|
|
764
|
+
const c = paint(colorsEnabled(stdout));
|
|
765
|
+
const kit = screenKit({ ...optsFor(stdout), fit: Boolean(stdout.isTTY), version: cliVersion });
|
|
766
|
+
const write = (lines: string[]): void => {
|
|
767
|
+
stdout.write(lines.map((line) => `${line}\n`).join(""));
|
|
768
|
+
};
|
|
769
|
+
// A grey line saying what apply is doing right now, removed once it is done.
|
|
770
|
+
// Without a terminal it stays, as a log line.
|
|
771
|
+
const transient = async <T,>(text: string, run: () => Promise<T>): Promise<T> => {
|
|
772
|
+
if (!stdout.isTTY) {
|
|
773
|
+
write([text]);
|
|
774
|
+
return run();
|
|
775
|
+
}
|
|
776
|
+
write([c.ink3(text)]);
|
|
777
|
+
try {
|
|
778
|
+
return await run();
|
|
779
|
+
} finally {
|
|
780
|
+
stdout.write("\x1b[1A\x1b[2K");
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// The logomark opens a config run on a terminal — one of the two places it
|
|
785
|
+
// appears at all.
|
|
786
|
+
if (stdout.isTTY) write(["", ...kit.logo()]);
|
|
787
|
+
write(["", kit.header("Set up this machine"), ""]);
|
|
801
788
|
|
|
802
789
|
const cancel = (): WizardResult => {
|
|
803
|
-
|
|
790
|
+
write(["", c.ink3("Cancelled. Nothing was changed.")]);
|
|
804
791
|
// Distinguished from the abort reasons: pressing Esc is not a failure, and
|
|
805
792
|
// a caller picking an exit code must not treat it as one.
|
|
806
793
|
return { applied: false, abort: "cancelled" };
|
|
807
794
|
};
|
|
808
795
|
|
|
809
|
-
//
|
|
796
|
+
// ── DAEMON ──────────────────────────────────────────────────────────────────
|
|
810
797
|
//
|
|
811
798
|
// First because it is the only step that needs a password: asking here means
|
|
812
|
-
// sudo prompts on a clean terminal, before any question has drawn a screen
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
// apply time stays non-interactive.
|
|
817
|
-
//
|
|
818
|
-
// Machine-level, so it is deliberately NOT gated on the scope chosen in the
|
|
819
|
-
// next step: one daemon serves every project on this machine.
|
|
820
|
-
//
|
|
821
|
-
// Always true here — the guard near the top of this function already
|
|
822
|
-
// refused setup on anything else. Kept as a real read (not a literal
|
|
823
|
-
// `true`) so this block still fails safe if that guard is ever moved.
|
|
824
|
-
const daemonSupported = isDaemonSupportedPlatform();
|
|
825
|
-
// An already-healthy daemon needs no install and no password. Re-running
|
|
826
|
-
// setup on a configured machine must not demand sudo for work that is
|
|
827
|
-
// already done.
|
|
828
|
-
// "running" only. A unit that exists but is stopped or crash-looping is
|
|
829
|
-
// exactly the machine that needs this run to reinstall it, and treating
|
|
830
|
-
// "installed" as good enough would skip the repair and then set
|
|
831
|
-
// `daemonConfigured` against a service that is not answering — which fails
|
|
832
|
-
// closed on every tool call.
|
|
833
|
-
// Running is not enough — it must also be the version this CLI ships.
|
|
834
|
-
//
|
|
835
|
-
// Skipping on "running" alone was right for the case it was written for
|
|
836
|
-
// (never demand a password for work already done), and exactly backwards
|
|
837
|
-
// during an upgrade: the OLD daemon is perfectly healthy, so setup skipped
|
|
838
|
-
// it and the stale version survived. That made "just re-run config" — the
|
|
839
|
-
// remedy every message points at — silently do nothing.
|
|
799
|
+
// sudo prompts on a clean terminal, before any question has drawn a screen.
|
|
800
|
+
// `sudo -v` caches the credential for the rest of the run, so the install at
|
|
801
|
+
// apply time stays non-interactive. Machine-level: one daemon serves every
|
|
802
|
+
// project on this machine.
|
|
840
803
|
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
// root to read, so a sudo cache older than five minutes says nothing about
|
|
854
|
-
// whether the daemon is up. Treating that as "stopped" made the wizard demand
|
|
855
|
-
// a password and unload/reload a perfectly healthy service — a real
|
|
856
|
-
// fail-closed window on a `daemonConfigured` machine, opened to fix nothing,
|
|
857
|
-
// and a direct breach of this file's own rule that setup must not demand sudo
|
|
858
|
-
// for work already done. So it is probed rather than assumed: the probe is a
|
|
859
|
-
// real hook evaluation over the socket, needs no privileges, and answers the
|
|
860
|
-
// question the status check was only standing in for.
|
|
861
|
-
const daemonMaybeUp =
|
|
862
|
-
daemonSupported && (daemonState === "running" || daemonState === "unknown") && daemonSkew === null;
|
|
804
|
+
// An already-healthy daemon needs no install and no password — but healthy
|
|
805
|
+
// means running, the version this CLI ships, AND able to answer. Running
|
|
806
|
+
// alone skipped the stale version during an upgrade; the right version alone
|
|
807
|
+
// waved through a unit whose worker dies on every spawn (`ExecStart` bakes in
|
|
808
|
+
// `process.execPath`, so an `nvm uninstall` breaks it). A real hook
|
|
809
|
+
// evaluation is the only check that tells those apart. `unknown` is macOS's
|
|
810
|
+
// "the state could not be READ" (launchd's system domain needs root), so it is
|
|
811
|
+
// probed rather than assumed down: reinstalling a healthy service opens a
|
|
812
|
+
// fail-closed window to fix nothing.
|
|
813
|
+
const daemonSkew = daemonVersionSkew();
|
|
814
|
+
const daemonState = daemonServiceStatus();
|
|
815
|
+
const daemonMaybeUp = (daemonState === "running" || daemonState === "unknown") && daemonSkew === null;
|
|
863
816
|
const daemonAnswers = daemonMaybeUp ? await probeDaemonEndToEnd() : false;
|
|
864
817
|
const daemonAlreadyRunning = daemonMaybeUp && daemonAnswers;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
* Keyed on a DEFINITE `running` reading, not on `daemonMaybeUp`: this is the
|
|
869
|
-
* branch that tears the service down before rebuilding, and the justification
|
|
870
|
-
* for that is knowing a live process is holding the singleton flock. An
|
|
871
|
-
* unreadable state is not that knowledge, and the plain install path already
|
|
872
|
-
* unloads before it writes.
|
|
873
|
-
*/
|
|
818
|
+
// Installed and running, but its worker cannot evaluate anything. Keyed on a
|
|
819
|
+
// DEFINITE `running`: this is the branch that tears the service down before
|
|
820
|
+
// rebuilding, justified only by knowing a live process holds the flock.
|
|
874
821
|
const daemonBroken = daemonState === "running" && daemonSkew === null && !daemonAnswers;
|
|
875
|
-
|
|
876
|
-
// A healthy daemon can still
|
|
877
|
-
// FAILPROOFAI_CLI_CMD existed, and nothing else
|
|
878
|
-
//
|
|
879
|
-
// Re-running setup is the one moment a user asks for their configuration to
|
|
880
|
-
// be brought up to date, so it is the moment to do it.
|
|
822
|
+
const daemonWanted = !daemonAlreadyRunning;
|
|
823
|
+
// A healthy daemon can still run a service definition written before
|
|
824
|
+
// FAILPROOFAI_CLI_CMD existed, and nothing else ever rewrites it: upgrading
|
|
825
|
+
// the npm package does not touch /etc/systemd/system.
|
|
881
826
|
let daemonUnitStale = daemonAlreadyRunning && daemonServiceNeedsUpgrade();
|
|
882
827
|
|
|
828
|
+
// `primeElevation` runs `sudo -v`, which PROMPTS. An unattended run has
|
|
829
|
+
// nobody to type a password, so it goes straight to the `sudo -n` the install
|
|
830
|
+
// uses anyway, and a machine that cannot elevate gets the commands instead of
|
|
831
|
+
// a hung terminal.
|
|
832
|
+
const elevate = (ask: string | null): boolean => {
|
|
833
|
+
if (unattended) {
|
|
834
|
+
if (ask) write([ask]);
|
|
835
|
+
return canElevate();
|
|
836
|
+
}
|
|
837
|
+
// The prompt is erased afterwards, with the line asking for it, so the step
|
|
838
|
+
// collapses to the one line that says how it went.
|
|
839
|
+
const rooted = collapseAfter(stdout, 10, () => {
|
|
840
|
+
if (ask) stdout.write(`${ask}\n`);
|
|
841
|
+
return primeElevation();
|
|
842
|
+
});
|
|
843
|
+
// Erased on failure too, so it is said again where the failure is.
|
|
844
|
+
if (!rooted && ask) write([ask]);
|
|
845
|
+
return rooted;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
write([kit.head("Daemon")]);
|
|
883
849
|
if (daemonWanted) {
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
// This block explained the warm-worker architecture to somebody who is
|
|
887
|
-
// about to type a password — three lines of mechanism answering a question
|
|
888
|
-
// nobody had. The install is happening either way; what they need is what
|
|
889
|
-
// it is and that it costs one sudo.
|
|
890
|
-
//
|
|
891
|
-
// The BROKEN case keeps its consequence, and only that: "every tool call is
|
|
850
|
+
// The broken case keeps its consequence on screen: "every tool call is
|
|
892
851
|
// denied" is the difference between a thirty-second fix and a support
|
|
893
|
-
// thread, so it
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
// does — and a machine that cannot elevate gets the commands below instead
|
|
903
|
-
// of a hung terminal. This is the same answer for both: sudo cannot be
|
|
904
|
-
// conjured away, only asked for at a moment somebody can answer.
|
|
905
|
-
if (!(unattended ? canElevate() : primeElevation())) {
|
|
852
|
+
// thread, so it is printed outside the region the sudo prompt is erased
|
|
853
|
+
// from.
|
|
854
|
+
if (daemonBroken) write(["failproofaid can't evaluate — every tool call is denied. Rebuilding needs sudo."]);
|
|
855
|
+
const ask = daemonBroken
|
|
856
|
+
? null
|
|
857
|
+
: daemonSkew
|
|
858
|
+
? `Updating failproofaid from ${daemonSkew.installed} to ${daemonSkew.expected} needs root once.`
|
|
859
|
+
: "Installing failproofaid needs root once.";
|
|
860
|
+
if (!elevate(ask)) {
|
|
906
861
|
// Required means required: write nothing at all, so a machine that could
|
|
907
|
-
// not be set up is left exactly as it was found
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
` Check what it needs: ${daemonStatusCommand() ?? "n/a"}\n\n`,
|
|
914
|
-
);
|
|
862
|
+
// not be set up is left exactly as it was found.
|
|
863
|
+
write([
|
|
864
|
+
kit.fail("Couldn't get root, so setup stopped before changing anything."),
|
|
865
|
+
` Re-run once you can use sudo: ${kit.cmd("failproofai config")}`,
|
|
866
|
+
` Check what it needs: ${kit.cmd(daemonStatusCommand() ?? "n/a")}`,
|
|
867
|
+
]);
|
|
915
868
|
void emit("configure_aborted", { reason: "needs_root" });
|
|
916
|
-
outro("Nothing was changed.", { ok: false }, stdout);
|
|
917
869
|
return { applied: false, abort: "needs_root" };
|
|
918
870
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
871
|
+
write([
|
|
872
|
+
kit.ok(
|
|
873
|
+
daemonBroken
|
|
874
|
+
? "Root granted to rebuild failproofaid"
|
|
875
|
+
: daemonSkew
|
|
876
|
+
? `Root granted to update failproofaid to ${daemonSkew.expected}`
|
|
877
|
+
: "Root granted to install failproofaid",
|
|
878
|
+
),
|
|
879
|
+
]);
|
|
880
|
+
} else {
|
|
881
|
+
write([kit.ok(`failproofaid ${cliVersion} is already running`)]);
|
|
882
|
+
if (daemonUnitStale) {
|
|
883
|
+
if (elevate("Its service definition is from an older version. Refreshing it needs root once.")) {
|
|
884
|
+
write([kit.ok("Root granted to refresh its service definition")]);
|
|
885
|
+
} else {
|
|
886
|
+
// NOT an abort: hooks are enforcing through a working daemon; only the
|
|
887
|
+
// scheduled audit is out of reach. Stopping setup over that would make
|
|
888
|
+
// an upgrade the thing that locked someone out of `failproofai config`.
|
|
889
|
+
write([kit.caution("No root, so its service definition stays as it is. Scheduled audits stay off until it is refreshed.")]);
|
|
890
|
+
daemonUnitStale = false;
|
|
891
|
+
}
|
|
935
892
|
}
|
|
936
|
-
} else if (daemonAlreadyRunning) {
|
|
937
|
-
stdout.write("failproofaid is already installed and running — leaving it alone.\n\n");
|
|
938
|
-
}
|
|
939
|
-
if (daemonSkew) {
|
|
940
|
-
stdout.write(
|
|
941
|
-
`failproofaid is ${daemonSkew.installed} but this CLI ships ${daemonSkew.expected} — ` +
|
|
942
|
-
`reinstalling it.\n\n`,
|
|
943
|
-
);
|
|
944
893
|
}
|
|
894
|
+
write([""]);
|
|
945
895
|
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
// ONE linear flow, and no opening fork.
|
|
896
|
+
// ── CONNECT TO CLOUD ────────────────────────────────────────────────────────
|
|
949
897
|
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
// by picking one. Recommended then silently took global scope, the detected
|
|
954
|
-
// CLIs, and fifteen policies nobody had seen.
|
|
898
|
+
// One masked field, with Tab as the way out. The key is checked here and
|
|
899
|
+
// written at apply, after the last question, so Esc anywhere before that
|
|
900
|
+
// still changes nothing (decision D3).
|
|
955
901
|
//
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
// guard is a real choice. It stopped being one when this package stopped
|
|
998
|
-
// shipping policies: hooks alone enforce nothing, so wiring them everywhere
|
|
999
|
-
// costs a config entry and changes no behaviour until a pack arrives. What it
|
|
1000
|
-
// buys is that an agent installed NEXT WEEK is guarded from its first tool
|
|
1001
|
-
// call, instead of running unguarded until somebody remembers to re-run setup
|
|
1002
|
-
// — and nobody remembers, because nothing tells them to.
|
|
1003
|
-
//
|
|
1004
|
-
// ALL twelve, not the detected ones, for exactly that reason. The cost is
|
|
1005
|
-
// honest and worth naming: hook config appears under `~/.cursor/`,
|
|
1006
|
-
// `~/.factory/` and the rest for agents that may never be installed.
|
|
1007
|
-
//
|
|
1008
|
-
// Which policies run, and on which of these agents, is chosen at
|
|
1009
|
-
// `failproofai policies add` — where the user is looking at a real list
|
|
1010
|
-
// instead of answering in the abstract.
|
|
1011
|
-
const clis: IntegrationType[] = [...clisSupportingScope(primaryScope)];
|
|
1012
|
-
|
|
1013
|
-
// 4 — Connect this machine? Last, because by this point the user has decided
|
|
1014
|
-
// what to protect, so "would you like to see it in a dashboard?" follows
|
|
1015
|
-
// naturally — asking up front interrupts setup with a question about a
|
|
1016
|
-
// product they may not have.
|
|
1017
|
-
//
|
|
1018
|
-
// A pasted API key rather than an interactive sign-in: it is the only form
|
|
1019
|
-
// that works on a headless box, in a container or over SSH, and it is the
|
|
1020
|
-
// same credential `failproofai config --connect` takes, so a machine set up
|
|
1021
|
-
// by the wizard and one set up by hand end up byte-identical on disk.
|
|
1022
|
-
//
|
|
1023
|
-
// Connecting turns on BOTH streams — policy decisions and session
|
|
1024
|
-
// transcripts. That is a real disclosure, not a footnote, so it is stated in
|
|
1025
|
-
// the body of the question itself rather than buried in an option hint.
|
|
1026
|
-
let connect: { url: string; token: string; machineId: string; machineLabel: string } | null = null;
|
|
1027
|
-
|
|
1028
|
-
{
|
|
1029
|
-
// Supplying a key IS asking to connect — there is no other reason to pass
|
|
1030
|
-
// one — so an unattended run with a token skips straight past the question,
|
|
1031
|
-
// and one without a token stays local, which is the same "Not now" branch a
|
|
1032
|
-
// person picks. Neither needs a separate flag to say so.
|
|
1033
|
-
const choice = answers.token
|
|
1034
|
-
// A key on the command line IS the answer to this question. Gating it on
|
|
1035
|
-
// there being no terminal was wrong: `--token` means "use this key", not
|
|
1036
|
-
// "use this key only if nobody is watching", and a run that asked anyway
|
|
1037
|
-
// made the flag look broken to the person who had just typed it.
|
|
1038
|
-
? "key"
|
|
1039
|
-
: unattended
|
|
1040
|
-
? "local"
|
|
1041
|
-
: await selectOne<"key" | "local">({
|
|
1042
|
-
message: "Connect this machine to FailproofAI Cloud?",
|
|
1043
|
-
// The hint says what you GET; the body says what LEAVES.
|
|
1044
|
-
//
|
|
1045
|
-
// Both have to be here and they are different jobs. "sends decisions +
|
|
1046
|
-
// transcripts" described the plumbing, which reads like a data-collection
|
|
1047
|
-
// notice rather than a feature — the reason anyone connects is to see
|
|
1048
|
-
// what their agents did, so the option says that.
|
|
1049
|
-
//
|
|
1050
|
-
// The disclosure stays in the BODY rather than moving into the hint,
|
|
1051
|
-
// which was the original design and is right: this screen is the only
|
|
1052
|
-
// place it is ever made. `describeOutcome` prints "hook activity" after
|
|
1053
|
-
// connecting and never mentions transcripts, so a person who does not
|
|
1054
|
-
// read it here does not read it at all. One line, not three, and the
|
|
1055
|
-
// specific nouns survive — "sessions" alone would not tell anyone their
|
|
1056
|
-
// file contents leave the machine.
|
|
1057
|
-
//
|
|
1058
|
-
// The get-started page, not the dashboard host.
|
|
1059
|
-
//
|
|
1060
|
-
// `app.befailproof.ai` is only useful to somebody who already has an
|
|
1061
|
-
// account, and this line addresses the person who has none — new to the
|
|
1062
|
-
// product, so no org either, not merely missing a key. The get-started
|
|
1063
|
-
// route walks them through creating both, so it answers the question
|
|
1064
|
-
// actually being asked. Naming the product rather than the artefact is
|
|
1065
|
-
// what makes it self-selecting: "No key?" reads as an error state to
|
|
1066
|
-
// someone who simply has not signed up yet.
|
|
1067
|
-
//
|
|
1068
|
-
// A marketing route rather than a dashboard one is also the safer thing
|
|
1069
|
-
// to hard-code here — it is the URL the site is expected to keep stable,
|
|
1070
|
-
// where an in-app path can be reorganised without anyone thinking to
|
|
1071
|
-
// update a string compiled into a CLI.
|
|
1072
|
-
//
|
|
1073
|
-
// The scope is gone from this screen: it is enforced at paste time, where
|
|
1074
|
-
// a wrong key is actually caught.
|
|
1075
|
-
body: [
|
|
1076
|
-
" Sessions include prompts, file contents and command output.",
|
|
1077
|
-
" New to FailproofAI? Create a key at https://befailproof.ai/get-started/",
|
|
1078
|
-
],
|
|
1079
|
-
// Cloud first, and therefore preselected: connecting is what most people
|
|
1080
|
-
// running this wizard came to do, and the local path stays one keystroke
|
|
1081
|
-
// away. Reversing these two is the whole change — neither option's copy
|
|
1082
|
-
// moved, so "stay local" is still stated as plainly as it was.
|
|
1083
|
-
choices: [
|
|
1084
|
-
{
|
|
1085
|
-
// What the cloud gives that this machine does not ALREADY have.
|
|
1086
|
-
//
|
|
1087
|
-
// "see what your agents did" was wrong: the local dashboard already
|
|
1088
|
-
// shows that, so it described something the user gets either way and
|
|
1089
|
-
// made connecting look redundant. The cloud's two jobs are a fleet
|
|
1090
|
-
// seen in one place and policies deployed to it from there — which is
|
|
1091
|
-
// also exactly what the key's two scopes buy (`events:add`,
|
|
1092
|
-
// `policies:pull`).
|
|
1093
|
-
label: "Paste an API key",
|
|
1094
|
-
value: "key",
|
|
1095
|
-
hint: "central monitoring · deploy policies from the dashboard",
|
|
1096
|
-
},
|
|
1097
|
-
{
|
|
1098
|
-
// "re-run config", NOT "--connect". That flag exists for scripted,
|
|
1099
|
-
// non-interactive setup; pointing a person who just declined at a
|
|
1100
|
-
// flag they would have to look up is worse than naming the command
|
|
1101
|
-
// they already ran.
|
|
1102
|
-
label: "Not now — stay local",
|
|
1103
|
-
value: "local",
|
|
1104
|
-
hint: "nothing leaves this machine · re-run config to add",
|
|
1105
|
-
},
|
|
1106
|
-
],
|
|
1107
|
-
stdin,
|
|
1108
|
-
stdout,
|
|
902
|
+
// Tab never disconnects (decision D4): on a connected machine it leaves the
|
|
903
|
+
// connection as it is, and `failproofai config --disconnect` is the one way to
|
|
904
|
+
// remove it. Enter on an empty field keeps the saved key exactly as it is too
|
|
905
|
+
// — no `connectToCloud`, which would reset collector settings — after
|
|
906
|
+
// checking that it still works.
|
|
907
|
+
const saved = savedConnection();
|
|
908
|
+
const urlChoice = resolveCloudUrl(answers.url, saved?.url);
|
|
909
|
+
// An unusable --url or FAILPROOFAI_CLOUD_URL is refused loudly, never a silent
|
|
910
|
+
// fall-back to the hosted service: whoever set it wants THAT endpoint, and
|
|
911
|
+
// reporting the machine elsewhere is what they did not ask. But only where a
|
|
912
|
+
// key would be checked or sent. `--oss`, a keyless unattended run and Tab send
|
|
913
|
+
// nothing, so stopping them over an endpoint they never use set up nothing.
|
|
914
|
+
const url = urlChoice.ok ? urlChoice.url : undefined;
|
|
915
|
+
const badUrl = urlChoice.ok ? undefined : urlChoice.message;
|
|
916
|
+
const host = url ? hostOf(url) : "no usable endpoint";
|
|
917
|
+
const source = urlChoice.ok ? urlChoice.source : undefined;
|
|
918
|
+
const refuseUrl = (message: string): WizardResult => {
|
|
919
|
+
write([kit.head("Connect to cloud"), kit.fail(message)]);
|
|
920
|
+
void emit("configure_aborted", { reason: "bad_cloud_url" });
|
|
921
|
+
// A failure, not a cancel: a run that asked to connect and set up nothing
|
|
922
|
+
// must not exit 0.
|
|
923
|
+
return { applied: false, abort: "bad_cloud_url" };
|
|
924
|
+
};
|
|
925
|
+
const machineId = resolveMachineId(answers.machineId);
|
|
926
|
+
const machineLabel = answers.machineLabel ?? saved?.machineLabel ?? resolveMachineLabel();
|
|
927
|
+
|
|
928
|
+
let typedOrg: string | undefined;
|
|
929
|
+
const planFor = (answer: CloudKeyAnswer | OpenSource): CloudPlan =>
|
|
930
|
+
answer === OPEN_SOURCE
|
|
931
|
+
? saved
|
|
932
|
+
? { kind: "kept", how: "skip", verified: false }
|
|
933
|
+
: { kind: "oss" }
|
|
934
|
+
: answer.kind === "saved"
|
|
935
|
+
? { kind: "kept", how: "saved", verified: answer.verified }
|
|
936
|
+
: { kind: "connect", token: answer.key, org: typedOrg, note: answer.note };
|
|
937
|
+
const cloudLines = (plan: CloudPlan): string[] =>
|
|
938
|
+
cloudCollapsedLines(kit, plan, {
|
|
939
|
+
host,
|
|
940
|
+
savedHost: saved ? hostOf(saved.url) : host,
|
|
941
|
+
savedOrg: saved?.org,
|
|
942
|
+
source,
|
|
1109
943
|
});
|
|
1110
|
-
if (choice === null) return cancel();
|
|
1111
|
-
|
|
1112
|
-
if (choice === "key") {
|
|
1113
|
-
// An already-enrolled machine has a URL and token that usually work.
|
|
1114
|
-
// Asking again is the seam that made connecting feel like two products.
|
|
1115
|
-
const existing = readCloudCredentials();
|
|
1116
|
-
let url: string | null = null;
|
|
1117
|
-
let token: string | null = null;
|
|
1118
|
-
// Reuse the enrolled id if there is one, else mint a stable key — never
|
|
1119
|
-
// the hostname, so two hosts with the same name do not merge. The hostname
|
|
1120
|
-
// becomes the human label instead.
|
|
1121
|
-
let machineId = resolveMachineId();
|
|
1122
|
-
let machineLabel = existing?.machineLabel ?? resolveMachineLabel();
|
|
1123
|
-
|
|
1124
|
-
if (existing && unattended && !answers.url) {
|
|
1125
|
-
// Already enrolled, and this run did not name a different place to
|
|
1126
|
-
// enrol. Reusing is what the person picks here too, and re-verifying a
|
|
1127
|
-
// working credential is the one thing an unattended re-run must not
|
|
1128
|
-
// turn into a failure.
|
|
1129
|
-
url = existing.url;
|
|
1130
|
-
token = answers.token ?? existing.token;
|
|
1131
|
-
machineId = answers.machineId ?? existing.machineId;
|
|
1132
|
-
machineLabel = answers.machineLabel ?? existing.machineLabel ?? machineLabel;
|
|
1133
|
-
} else if (existing && !unattended) {
|
|
1134
|
-
const reuse = await selectOne<"reuse" | "other">({
|
|
1135
|
-
message: `Use this machine's existing connection to ${existing.url}?`,
|
|
1136
|
-
choices: [
|
|
1137
|
-
{
|
|
1138
|
-
label: "Yes — reuse it",
|
|
1139
|
-
value: "reuse",
|
|
1140
|
-
hint: `as ${existing.machineLabel ?? existing.machineId}, same token`,
|
|
1141
|
-
},
|
|
1142
|
-
{ label: "No — different endpoint or key", value: "other", hint: "" },
|
|
1143
|
-
],
|
|
1144
|
-
stdin,
|
|
1145
|
-
stdout,
|
|
1146
|
-
});
|
|
1147
|
-
if (reuse === null) return cancel();
|
|
1148
|
-
if (reuse === "reuse") {
|
|
1149
|
-
url = existing.url;
|
|
1150
|
-
token = existing.token;
|
|
1151
|
-
machineId = existing.machineId;
|
|
1152
|
-
machineLabel = existing.machineLabel ?? machineLabel;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
944
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
// The env value goes through the SAME `validateCloudUrl` a typed one
|
|
1178
|
-
// did — it is not a trusted back door. http stays loopback-only, so a
|
|
1179
|
-
// bearer token still cannot be exported onto the wire in clear by
|
|
1180
|
-
// setting a variable.
|
|
1181
|
-
//
|
|
1182
|
-
// `--url` comes first. It was read into `answers.url` and then only
|
|
1183
|
-
// ever used as a CONDITION above, never as the URL, so
|
|
1184
|
-
// `config --token <key> --url <X>` connected to the env value or the
|
|
1185
|
-
// hosted default and ignored X — reporting a machine somewhere its
|
|
1186
|
-
// operator had explicitly said not to. The CLI fills `answers.url` from
|
|
1187
|
-
// `--url`, falling back to FAILPROOFAI_CLOUD_URL, so the source named
|
|
1188
|
-
// on screen is whichever of the two the value actually is.
|
|
1189
|
-
const envOverride = process.env.FAILPROOFAI_CLOUD_URL?.trim();
|
|
1190
|
-
const flagOverride = answers.url?.trim();
|
|
1191
|
-
const override = flagOverride || envOverride;
|
|
1192
|
-
const source = flagOverride && flagOverride !== envOverride ? "--url" : "FAILPROOFAI_CLOUD_URL";
|
|
1193
|
-
if (override) {
|
|
1194
|
-
const validated = validateCloudUrl(cloudBaseFor(override));
|
|
1195
|
-
if (!validated.ok) {
|
|
1196
|
-
// Loud, not silent-fallback-to-hosted: someone who exported this
|
|
1197
|
-
// wants THAT endpoint, and quietly reporting a machine to the
|
|
1198
|
-
// hosted service instead is the one outcome they did not ask for.
|
|
1199
|
-
stdout.write(
|
|
1200
|
-
`\n${source === "--url" ? "--url is" : "FAILPROOFAI_CLOUD_URL is set to"} "${override}", which cannot be used: ` +
|
|
1201
|
-
`${validated.reason}\n`,
|
|
1202
|
-
);
|
|
1203
|
-
return cancel();
|
|
945
|
+
const askCloud = (): Promise<CloudKeyAnswer | OpenSource | null> =>
|
|
946
|
+
promptCloudKey({
|
|
947
|
+
message: "Connect to cloud",
|
|
948
|
+
// Where the key goes, in the heading: the one place somebody can notice
|
|
949
|
+
// they are about to send a key somewhere they did not mean to.
|
|
950
|
+
meta: source ? `${host} · from ${source}` : host,
|
|
951
|
+
tabHint: saved ? "skip" : "use open source instead",
|
|
952
|
+
emptyError: saved ? "Paste a key, or press tab to skip." : "Paste a key, or press tab to use open source.",
|
|
953
|
+
saved: saved
|
|
954
|
+
? {
|
|
955
|
+
masked: maskToken(saved.token),
|
|
956
|
+
check: async () => {
|
|
957
|
+
const r = await checkCloudKey(saved.url, saved.token);
|
|
958
|
+
if (r.ok) {
|
|
959
|
+
const org = r.org ?? saved.org;
|
|
960
|
+
return { ok: true, line: `Your saved key works: ${maskToken(saved.token)}${org ? `, ${org}` : ""}` };
|
|
961
|
+
}
|
|
962
|
+
return r.refused
|
|
963
|
+
? { ok: false, line: `Your saved key no longer works: ${r.reason}` }
|
|
964
|
+
: { ok: false, usable: true, line: `Couldn't check your saved key: ${r.reason}` };
|
|
965
|
+
},
|
|
1204
966
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
967
|
+
: undefined,
|
|
968
|
+
check: async (key) => {
|
|
969
|
+
// Kept open, so Tab (or the saved key) still finishes setup.
|
|
970
|
+
if (!url) return { ok: false, line: badUrl ?? "No usable endpoint." };
|
|
971
|
+
const r = await checkCloudKey(url, key);
|
|
972
|
+
if (r.ok) {
|
|
973
|
+
typedOrg = r.org;
|
|
974
|
+
return { ok: true, line: r.note };
|
|
1213
975
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
if (token === null) {
|
|
1221
|
-
token = await promptText({
|
|
1222
|
-
// The destination is in the question now that it is no longer a
|
|
1223
|
-
// question of its own. It is the only remaining place a person can
|
|
1224
|
-
// notice they are about to send a key somewhere they did not mean.
|
|
1225
|
-
message: `API key for ${new URL(url).host}`,
|
|
1226
|
-
hint: "needs events:add · policies:pull enables managed policy too",
|
|
1227
|
-
// Masked: setup is routinely run while screen-sharing, and a pasted
|
|
1228
|
-
// key would otherwise sit in the scrollback of every recording.
|
|
1229
|
-
mask: true,
|
|
1230
|
-
validate: (v) => (v.length >= 8 ? null : "that looks too short to be a key"),
|
|
1231
|
-
stdin,
|
|
1232
|
-
stdout,
|
|
1233
|
-
});
|
|
1234
|
-
if (token === null) return cancel();
|
|
1235
|
-
}
|
|
976
|
+
return { ok: false, line: r.refused ? `That key was refused: ${r.reason}.` : `Couldn't check the key: ${r.reason}.` };
|
|
977
|
+
},
|
|
978
|
+
collapsed: (answer) => [...cloudLines(planFor(answer)), ""],
|
|
979
|
+
stdin,
|
|
980
|
+
stdout,
|
|
981
|
+
});
|
|
1236
982
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
} else {
|
|
1266
|
-
connect = { url, token, machineId, machineLabel };
|
|
1267
|
-
}
|
|
1268
|
-
} else {
|
|
1269
|
-
stdout.write("looks good.\n\n");
|
|
1270
|
-
connect = { url, token, machineId, machineLabel };
|
|
1271
|
-
}
|
|
983
|
+
let cloud: CloudPlan;
|
|
984
|
+
if (answers.oss) {
|
|
985
|
+
cloud = saved ? { kind: "kept", how: "skip", verified: false } : { kind: "oss" };
|
|
986
|
+
write([...cloudLines(cloud), ""]);
|
|
987
|
+
} else if (answers.token) {
|
|
988
|
+
// A key on the command line is a request to connect, and there is nowhere
|
|
989
|
+
// usable to send it.
|
|
990
|
+
if (!url) return refuseUrl(badUrl ?? "No usable endpoint.");
|
|
991
|
+
const r = await checkCloudKey(url, answers.token);
|
|
992
|
+
if (r.ok) {
|
|
993
|
+
cloud = { kind: "connect", token: answers.token, org: r.org, note: r.note };
|
|
994
|
+
write([...cloudLines(cloud), ""]);
|
|
995
|
+
} else if (unattended) {
|
|
996
|
+
// A key that does not work is a FAILED setup, not a prompt: a script that
|
|
997
|
+
// exited 0 here would leave a fleet believing it was reporting.
|
|
998
|
+
write([
|
|
999
|
+
kit.head("Connect to cloud", host),
|
|
1000
|
+
kit.fail(r.refused ? `That key was refused: ${r.reason}.` : `Couldn't check the key: ${r.reason}.`),
|
|
1001
|
+
]);
|
|
1002
|
+
void emit("configure_aborted", { reason: "cloud_unverified" });
|
|
1003
|
+
return { applied: false, abort: "cloud_unverified" };
|
|
1004
|
+
} else {
|
|
1005
|
+
// Somebody is at the keyboard, so a refused --token is a question again
|
|
1006
|
+
// rather than the end of setup.
|
|
1007
|
+
write([kit.fail(r.refused ? `The --token key was refused: ${r.reason}.` : `Couldn't check the --token key: ${r.reason}.`)]);
|
|
1008
|
+
const answer = await askCloud();
|
|
1009
|
+
if (answer === null) return cancel();
|
|
1010
|
+
cloud = planFor(answer);
|
|
1272
1011
|
}
|
|
1012
|
+
} else if (unattended) {
|
|
1013
|
+
// No key and nobody to ask: nothing about the connection changes.
|
|
1014
|
+
cloud = saved ? { kind: "kept", how: "skip", verified: false } : { kind: "oss" };
|
|
1015
|
+
write([...cloudLines(cloud), ""]);
|
|
1016
|
+
} else {
|
|
1017
|
+
// Said above the field, which stays usable for Tab and a saved key.
|
|
1018
|
+
if (badUrl) write([kit.fail(badUrl)]);
|
|
1019
|
+
const answer = await askCloud();
|
|
1020
|
+
if (answer === null) return cancel();
|
|
1021
|
+
cloud = planFor(answer);
|
|
1273
1022
|
}
|
|
1274
|
-
|
|
1275
|
-
//
|
|
1276
|
-
// The last question, and the only one a headless run skips outright rather
|
|
1277
|
-
// than answering from a flag. There is nothing to confirm when nobody is
|
|
1278
|
-
// watching, and the command itself was the confirmation.
|
|
1023
|
+
|
|
1024
|
+
// ── AGENTS ──────────────────────────────────────────────────────────────────
|
|
1279
1025
|
//
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1026
|
+
// One saved list drives hooks, collection and backfill. See `planAgents` for
|
|
1027
|
+
// which ticks a run starts with.
|
|
1028
|
+
const fp = readConfig();
|
|
1029
|
+
const hookedNow = INTEGRATION_TYPES.filter((id) => hookedAtUserScope(id, cwd));
|
|
1030
|
+
const plan = planAgents({ detected: detectInstalledClis(), saved: fp.agents, hooked: hookedNow });
|
|
1031
|
+
// Whether the daemon will be collecting once this run ends: a key is already
|
|
1032
|
+
// on disk, or this run writes one. Removal copy and the backfill request both
|
|
1033
|
+
// depend on it.
|
|
1034
|
+
const collectingAfter = cloud.kind === "connect" || readIngestCredential() !== null;
|
|
1035
|
+
const sessionsOn = cloud.kind === "connect" ? answers.noTranscripts !== true : fp.collector.sessions === true;
|
|
1036
|
+
|
|
1037
|
+
let selected: IntegrationType[];
|
|
1038
|
+
if (answers.agents && answers.agents.length > 0) {
|
|
1039
|
+
const named = new Set(answers.agents);
|
|
1040
|
+
selected = plan.order.filter((id) => named.has(id));
|
|
1041
|
+
write([...agentsCollapsedLines(kit, plan, selected, collectingAfter, c), ""]);
|
|
1042
|
+
} else if (unattended || preAnswered) {
|
|
1043
|
+
selected = defaultSelection(plan);
|
|
1044
|
+
write([...agentsCollapsedLines(kit, plan, selected, collectingAfter, c), ""]);
|
|
1045
|
+
} else {
|
|
1046
|
+
// "from last setup", not the reference's "from your last setup": with all
|
|
1047
|
+
// twelve agents listed, the longer meta runs the heading to 81 columns and
|
|
1048
|
+
// the end of it is cut off.
|
|
1049
|
+
const suffix =
|
|
1050
|
+
plan.mode === "rerun" ? " · from last setup" : plan.mode === "upgrade" ? " · as protected today" : "";
|
|
1051
|
+
const picked = await multiSelect<IntegrationType>({
|
|
1052
|
+
message: "Which agents should failproofai trace?",
|
|
1053
|
+
choices: agentChoices(plan),
|
|
1054
|
+
minSelected: 1,
|
|
1055
|
+
minMessage: "Pick at least one agent.",
|
|
1056
|
+
meta: (checked) => `${checked.length} of ${plan.order.length} selected${suffix}`,
|
|
1057
|
+
tag: agentTag(plan, c),
|
|
1058
|
+
collapsed: (values) => [
|
|
1059
|
+
...agentsCollapsedLines(kit, plan, plan.order.filter((id) => values.includes(id)), collectingAfter, c),
|
|
1060
|
+
"",
|
|
1061
|
+
],
|
|
1062
|
+
stdin,
|
|
1063
|
+
stdout,
|
|
1064
|
+
});
|
|
1065
|
+
if (picked === null) return cancel();
|
|
1066
|
+
selected = plan.order.filter((id) => picked.includes(id));
|
|
1067
|
+
}
|
|
1068
|
+
const { added, removed } = agentChanges(plan, selected);
|
|
1306
1069
|
|
|
1307
1070
|
// ── Apply ─────────────────────────────────────────────────────────────────
|
|
1308
1071
|
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1072
|
+
// The agents step was the last decision. OpenTelemetry is not part of setup:
|
|
1073
|
+
// it is opt-in, per agent, through `failproofai otel enable <agent>`.
|
|
1074
|
+
// ORDER MATTERS here:
|
|
1075
|
+
//
|
|
1076
|
+
// 1. The daemon first, because setup requires it: if it cannot be installed
|
|
1077
|
+
// this run must leave the machine exactly as it found it. Writing hooks
|
|
1078
|
+
// first and discovering the service will not start is the one ordering
|
|
1079
|
+
// whose failure cannot be undone cleanly.
|
|
1080
|
+
// 2. Hooks for the chosen agents, and off for the rest.
|
|
1081
|
+
// 3. The backfill request for agents added back, BEFORE the selection that
|
|
1082
|
+
// ticks them: the daemon handles a pending request ahead of its config
|
|
1083
|
+
// compare, so one landing a tick later would let an added agent resume
|
|
1084
|
+
// stale cursors first.
|
|
1085
|
+
// 4. The selection, BEFORE any new ingest key, or the first collection runs
|
|
1086
|
+
// every agent.
|
|
1087
|
+
// 5. The connection last.
|
|
1088
|
+
const done: string[] = [];
|
|
1315
1089
|
let daemonInstalled = daemonAlreadyRunning;
|
|
1316
1090
|
if (daemonWanted) {
|
|
1317
1091
|
// A unit that is running but cannot answer is torn down before it is
|
|
1318
|
-
// rebuilt
|
|
1319
|
-
//
|
|
1320
|
-
// the replacement needs — install over the top and the new unit starts,
|
|
1321
|
-
// loses the lock race, and the machine stays exactly as broken. This is
|
|
1322
|
-
// the production path for `uninstallDaemonService`, which until now had
|
|
1323
|
-
// none: it was defined, tested, documented in CLAUDE.md as called from
|
|
1324
|
-
// here, and referenced by nothing.
|
|
1092
|
+
// rebuilt: it holds the singleton flock the replacement needs, so
|
|
1093
|
+
// installing over it starts a unit that loses the lock race.
|
|
1325
1094
|
if (daemonBroken) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1095
|
+
await transient("Removing the failproofaid service that can't start…", async () => {
|
|
1096
|
+
try {
|
|
1097
|
+
await uninstallDaemonService();
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
// Non-fatal: the install below reports the outcome that decides.
|
|
1100
|
+
hookLogWarn(
|
|
1101
|
+
`could not remove the broken failproofaid service: ${err instanceof Error ? err.message : String(err)}`,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1336
1105
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1106
|
+
const daemonResult = await transient(daemonBroken ? "Reinstalling failproofaid…" : "Installing failproofaid…", () =>
|
|
1107
|
+
installDaemonService(),
|
|
1108
|
+
);
|
|
1339
1109
|
void emit("configure_daemon_install", {
|
|
1340
1110
|
installed: daemonResult.installed,
|
|
1341
|
-
// A bounded classification, never the raw reason:
|
|
1342
|
-
//
|
|
1343
|
-
// against a homedir()-derived path, so it routinely carries the OS
|
|
1344
|
-
// username and the local filesystem layout. The full text stays local
|
|
1345
|
-
// via the hookLogWarn below.
|
|
1111
|
+
// A bounded classification, never the raw reason: that string routinely
|
|
1112
|
+
// carries the OS username. The full text stays local via hookLogWarn.
|
|
1346
1113
|
reason: daemonResult.installed ? null : classifyDaemonInstallFailure(daemonResult.reason),
|
|
1347
1114
|
platform: process.platform,
|
|
1348
1115
|
});
|
|
1349
|
-
|
|
1350
1116
|
if (!daemonResult.installed) {
|
|
1351
1117
|
hookLogWarn(`failproofaid was not installed as a service: ${daemonResult.reason}`);
|
|
1352
1118
|
// Nothing user-facing has been written yet, so there is nothing to roll
|
|
1353
1119
|
// back — which is the entire reason this runs first.
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
);
|
|
1120
|
+
write([
|
|
1121
|
+
kit.fail("failproofaid could not be installed, so setup stopped before changing anything."),
|
|
1122
|
+
` ${daemonResult.reason ?? "unknown error"}`,
|
|
1123
|
+
` Once that is fixed, re-run: ${kit.cmd("failproofai config")}`,
|
|
1124
|
+
]);
|
|
1359
1125
|
void emit("configure_aborted", { reason: "daemon_failed" });
|
|
1360
|
-
outro("Nothing was changed.", { ok: false }, stdout);
|
|
1361
1126
|
return { applied: false, abort: "daemon_failed" };
|
|
1362
1127
|
}
|
|
1363
|
-
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
|
|
1368
|
-
// machine through: `waitForDaemonRunning()` asks systemd, `Ping` is
|
|
1369
|
-
// answered in `server.rs` without touching the worker, a null
|
|
1370
|
-
// `resolveWorkerCommand()` is best-effort, and `Worker::warm()` swallows
|
|
1371
|
-
// its own failure. Setting `daemonConfigured` against it denies every tool
|
|
1372
|
-
// call across all twelve CLIs, `UserPromptSubmit` included, so the user
|
|
1373
|
-
// cannot even ask their agent why.
|
|
1374
|
-
//
|
|
1375
|
-
// Checked HERE rather than inside `installDaemonService`, which can only
|
|
1376
|
-
// honestly report on the service: keeping them apart is also what lets the
|
|
1377
|
-
// install mechanics be tested against a stub binary.
|
|
1378
|
-
const probe = await probeDaemon();
|
|
1128
|
+
// Installed and running is the service manager's opinion. Setting
|
|
1129
|
+
// `daemonConfigured` against a daemon whose worker cannot run denies every
|
|
1130
|
+
// tool call across all twelve CLIs, `UserPromptSubmit` included, so a real
|
|
1131
|
+
// evaluation is asked for before anything else is written.
|
|
1132
|
+
const probe = await transient("Checking that failproofai answers…", () => probeDaemon());
|
|
1379
1133
|
if (!probe.ok) {
|
|
1380
1134
|
hookLogWarn(
|
|
1381
1135
|
`failproofaid was installed and running but did not answer a policy evaluation (${probe.reason})`,
|
|
1382
1136
|
);
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
" Then re-run: failproofai config\n\n",
|
|
1395
|
-
);
|
|
1137
|
+
write([
|
|
1138
|
+
kit.fail(
|
|
1139
|
+
probe.reason === "worker"
|
|
1140
|
+
? "failproofaid started but can't evaluate policies: its worker process could not be run."
|
|
1141
|
+
: "failproofaid started but could not be reached on its socket.",
|
|
1142
|
+
),
|
|
1143
|
+
" Setup stopped before changing anything: a machine that requires a daemon",
|
|
1144
|
+
" that cannot answer denies every tool call.",
|
|
1145
|
+
` Check it with: ${kit.cmd(daemonStatusCommand() ?? "systemctl status failproofaid")}`,
|
|
1146
|
+
` Then re-run: ${kit.cmd("failproofai config")}`,
|
|
1147
|
+
]);
|
|
1396
1148
|
void emit("configure_aborted", { reason: `daemon_not_answering_${probe.reason}` });
|
|
1397
|
-
outro("Nothing was changed.", { ok: false }, stdout);
|
|
1398
1149
|
return { applied: false, abort: "daemon_failed" };
|
|
1399
1150
|
}
|
|
1400
1151
|
daemonInstalled = true;
|
|
1152
|
+
done.push(
|
|
1153
|
+
kit.ok(
|
|
1154
|
+
daemonBroken
|
|
1155
|
+
? "failproofaid rebuilt and running"
|
|
1156
|
+
: daemonSkew
|
|
1157
|
+
? `failproofaid updated to ${cliVersion} and running`
|
|
1158
|
+
: "failproofaid installed and running",
|
|
1159
|
+
),
|
|
1160
|
+
);
|
|
1401
1161
|
} else if (daemonUnitStale) {
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
|
|
1405
|
-
// scheduled audit, nothing else.
|
|
1406
|
-
stdout.write("Refreshing the failproofaid service definition…\n");
|
|
1407
|
-
const upgrade = await ensureDaemonServiceCurrent();
|
|
1162
|
+
// Never instead of the install, and never able to abort a setup that is
|
|
1163
|
+
// otherwise fine: a failure here costs the scheduled audit, nothing else.
|
|
1164
|
+
const upgrade = await transient("Refreshing failproofaid's service definition…", () => ensureDaemonServiceCurrent());
|
|
1408
1165
|
void emit("configure_daemon_unit_refresh", {
|
|
1409
1166
|
outcome: upgrade.outcome,
|
|
1410
1167
|
daemon_running: upgrade.daemonRunning ?? true,
|
|
@@ -1412,149 +1169,120 @@ export async function runConfigureWizard(
|
|
|
1412
1169
|
});
|
|
1413
1170
|
if (upgrade.outcome === "failed") {
|
|
1414
1171
|
hookLogWarn(`failproofaid service definition could not be refreshed: ${upgrade.reason}`);
|
|
1172
|
+
done.push(kit.caution(`failproofaid's service definition could not be refreshed: ${upgrade.reason ?? "unknown error"}`));
|
|
1415
1173
|
if (upgrade.daemonRunning === false) {
|
|
1416
|
-
// The refresh stopped a daemon it could not start again
|
|
1417
|
-
//
|
|
1418
|
-
//
|
|
1419
|
-
// not drag a healthy machine down this branch.
|
|
1420
|
-
//
|
|
1421
|
-
// Leaving `daemonConfigured` set here is not "the audit stays off", it
|
|
1422
|
-
// is every tool call across all 12 CLIs denied against a socket
|
|
1423
|
-
// nothing is listening on, recoverable only by hand-editing
|
|
1424
|
-
// policies-config.json. So the machine goes back to in-process
|
|
1425
|
-
// evaluation — the same trade uninstallDaemonService makes, and for
|
|
1426
|
-
// the same reason.
|
|
1174
|
+
// The refresh stopped a daemon it could not start again. Leaving
|
|
1175
|
+
// `daemonConfigured` set would deny every tool call against a socket
|
|
1176
|
+
// nothing listens on, so the machine goes back to in-process evaluation.
|
|
1427
1177
|
daemonInstalled = false;
|
|
1428
1178
|
setDaemonConfigured(false);
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
"in-process evaluation rather than left denying every tool call. Hooks keep\n" +
|
|
1433
|
-
"enforcing. Re-run `failproofai config` to reinstall the service.\n\n",
|
|
1179
|
+
done.push(
|
|
1180
|
+
" It is no longer running, so this machine is back on in-process evaluation, and",
|
|
1181
|
+
` hooks keep enforcing. Reinstall it with ${kit.cmd("failproofai config")}`,
|
|
1434
1182
|
);
|
|
1435
1183
|
} else {
|
|
1436
|
-
|
|
1437
|
-
`\nThe service definition could not be refreshed:\n ${upgrade.reason ?? "unknown error"}\n` +
|
|
1438
|
-
"Hooks keep enforcing; scheduled audits stay off until it is.\n\n",
|
|
1439
|
-
);
|
|
1184
|
+
done.push(" Hooks keep enforcing; scheduled audits stay off until it is refreshed.");
|
|
1440
1185
|
}
|
|
1186
|
+
} else {
|
|
1187
|
+
done.push(kit.ok("failproofaid's service definition is up to date"));
|
|
1441
1188
|
}
|
|
1442
1189
|
}
|
|
1443
1190
|
|
|
1444
|
-
// The flag that makes hooks route through the daemon — and
|
|
1445
|
-
//
|
|
1446
|
-
// verified-running service, never on intent.
|
|
1447
|
-
// The version is recorded HERE, from the CLI that installed it — which is by
|
|
1448
|
-
// construction the version that was installed, since the download URL and the
|
|
1449
|
-
// binary filename are both derived from it.
|
|
1191
|
+
// The flag that makes hooks route through the daemon — and fail closed when
|
|
1192
|
+
// it is unreachable. Only ever set after a verified-running service.
|
|
1450
1193
|
if (daemonInstalled) {
|
|
1451
1194
|
setDaemonConfigured(true, cliVersion);
|
|
1452
|
-
//
|
|
1453
|
-
// longer referenced by anything. Keeps the previous version for an
|
|
1454
|
-
// offline rollback.
|
|
1195
|
+
// The unit points at the new binary now, so older ones are unreferenced.
|
|
1455
1196
|
pruneOldDaemonBinaries();
|
|
1456
1197
|
}
|
|
1457
1198
|
|
|
1458
|
-
|
|
1459
|
-
//
|
|
1460
|
-
//
|
|
1199
|
+
const connectedAfterPlan = cloud.kind === "connect" || cloud.kind === "kept";
|
|
1200
|
+
// Telemetry runs concurrently with the rest of apply (never rejects,
|
|
1201
|
+
// 5s-bounded) and is awaited before returning. `cli` carries the selected
|
|
1202
|
+
// agent ids, as it always has (and as hooks_installed does): a fixed list of
|
|
1203
|
+
// product ids, never user content. The agents_* fields add counts only.
|
|
1461
1204
|
const applied = emit("configure_applied", {
|
|
1462
|
-
target,
|
|
1463
|
-
scopes,
|
|
1464
|
-
cli:
|
|
1465
|
-
cli_count:
|
|
1466
|
-
policy_count:
|
|
1467
|
-
connected:
|
|
1205
|
+
target: "user",
|
|
1206
|
+
scopes: ["user"],
|
|
1207
|
+
cli: selected,
|
|
1208
|
+
cli_count: selected.length,
|
|
1209
|
+
policy_count: (readScopedHooksConfig("user", cwd).enabledPolicies ?? []).length,
|
|
1210
|
+
connected: cloud.kind === "connect",
|
|
1211
|
+
mode: connectedAfterPlan ? "cloud" : "oss",
|
|
1212
|
+
agents_selected: selected.length,
|
|
1213
|
+
agents_detected: plan.detected.size,
|
|
1214
|
+
agents_changed: added.length + removed.length > 0,
|
|
1468
1215
|
});
|
|
1469
1216
|
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
"configure-wizard",
|
|
1497
|
-
/* customPoliciesPath */ undefined,
|
|
1498
|
-
/* removeCustomHooks */ false,
|
|
1499
|
-
clisForScope,
|
|
1500
|
-
{ replace: true, quiet: true },
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
setCustomPoliciesEnabled(scope, cwd, customEnabled);
|
|
1504
|
-
}
|
|
1217
|
+
// Hooks, at user scope, for the chosen agents. Whatever policies are enabled
|
|
1218
|
+
// there are carried through untouched: `replace: true` would otherwise switch
|
|
1219
|
+
// off policies the user had turned on, and setup must never reduce
|
|
1220
|
+
// protection. Setup chooses no policies of its own any more.
|
|
1221
|
+
const policies = readScopedHooksConfig("user", cwd).enabledPolicies ?? [];
|
|
1222
|
+
// Nothing to hook is a real answer (a saved empty selection), and handing
|
|
1223
|
+
// installHooks an empty list is not a way to say it.
|
|
1224
|
+
if (selected.length > 0) await transient("Installing hooks…", () =>
|
|
1225
|
+
installHooks(
|
|
1226
|
+
policies,
|
|
1227
|
+
"user",
|
|
1228
|
+
cwd,
|
|
1229
|
+
/* includeBeta */ false,
|
|
1230
|
+
"configure-wizard",
|
|
1231
|
+
/* customPoliciesPath */ undefined,
|
|
1232
|
+
/* removeCustomHooks */ false,
|
|
1233
|
+
selected,
|
|
1234
|
+
{ replace: true, quiet: true },
|
|
1235
|
+
),
|
|
1236
|
+
);
|
|
1237
|
+
// And off for every agent not chosen that has them: the ones unticked now,
|
|
1238
|
+
// and any hooked since the last setup without being part of it.
|
|
1239
|
+
removeUserScopeHooks(
|
|
1240
|
+
INTEGRATION_TYPES.filter((id) => !selected.includes(id) && (plan.baseline.has(id) || hookedNow.includes(id))),
|
|
1241
|
+
cwd,
|
|
1242
|
+
);
|
|
1505
1243
|
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
|
|
1512
|
-
|
|
1244
|
+
// Agents traced again start fresh (see `requestFreshStart`). Only on a
|
|
1245
|
+
// rerun: its baseline is the selection the daemon actually filtered on. With
|
|
1246
|
+
// no saved list (upgrade, first) the daemon was collecting every agent, so an
|
|
1247
|
+
// agent "added" here was never untraced, has no stale cursors, and a request
|
|
1248
|
+
// would only make the daemon re-read everything it already sent.
|
|
1249
|
+
requestFreshStart(plan.mode === "rerun" ? added : [], collectingAfter && sessionsOn);
|
|
1250
|
+
|
|
1251
|
+
updateConfig({ agents: { selected, seen: plan.seen } });
|
|
1252
|
+
|
|
1253
|
+
// The connection, written after the daemon exists — the daemon runs the
|
|
1254
|
+
// collector, so a credential written for a service that is not there would be
|
|
1255
|
+
// a key on disk doing nothing. `connectToCloud` re-verifies each capability
|
|
1256
|
+
// and writes only what works.
|
|
1513
1257
|
let connected = false;
|
|
1514
|
-
if (connect) {
|
|
1258
|
+
if (cloud.kind === "connect" && url) {
|
|
1259
|
+
const token = cloud.token;
|
|
1515
1260
|
try {
|
|
1516
|
-
const outcome = await
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
// output. The separate `--connect` path always honoured it.
|
|
1527
|
-
sessions: answers.noTranscripts !== true,
|
|
1528
|
-
});
|
|
1261
|
+
const outcome = await transient("Connecting…", () =>
|
|
1262
|
+
connectToCloud({
|
|
1263
|
+
url,
|
|
1264
|
+
token,
|
|
1265
|
+
machineId,
|
|
1266
|
+
machineLabel,
|
|
1267
|
+
// Both streams unless the run said `--no-transcripts`.
|
|
1268
|
+
sessions: answers.noTranscripts !== true,
|
|
1269
|
+
}),
|
|
1270
|
+
);
|
|
1529
1271
|
connected = outcome.anyConfigured;
|
|
1530
|
-
|
|
1531
|
-
const shownAs =
|
|
1532
|
-
connect.machineLabel === connect.machineId
|
|
1533
|
-
? connect.machineId
|
|
1534
|
-
: `${connect.machineLabel} (${connect.machineId})`;
|
|
1535
|
-
for (const line of describeOutcome(outcome, shownAs, connect.url)) {
|
|
1536
|
-
stdout.write(`${line}\n`);
|
|
1537
|
-
}
|
|
1538
|
-
// Said when it took effect, as `--connect` says it: the default carries
|
|
1539
|
-
// prompts and file contents, so the opt-out is worth confirming.
|
|
1272
|
+
done.push(...connectDoneLines(kit, outcome, host, cloud.org));
|
|
1540
1273
|
if (outcome.ingest.ok && answers.noTranscripts === true) {
|
|
1541
|
-
|
|
1274
|
+
done.push(" Decisions only: session transcripts are not sent (--no-transcripts).");
|
|
1542
1275
|
}
|
|
1543
|
-
|
|
1544
|
-
// capabilities the server actually granted.
|
|
1545
|
-
void emit("configure_connect", {
|
|
1546
|
-
policy_ok: outcome.policy.ok,
|
|
1547
|
-
ingest_ok: outcome.ingest.ok,
|
|
1548
|
-
});
|
|
1276
|
+
void emit("configure_connect", { policy_ok: outcome.policy.ok, ingest_ok: outcome.ingest.ok });
|
|
1549
1277
|
} catch (err) {
|
|
1550
|
-
// Non-fatal, unlike the daemon: enforcement does not depend on the
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
hookLogWarn(
|
|
1554
|
-
`cloud connection was not written: ${err instanceof Error ? err.message : String(err)}`,
|
|
1555
|
-
);
|
|
1556
|
-
stdout.write("\nCould not connect — everything else applied. Retry with `failproofai config --connect`.\n");
|
|
1278
|
+
// Non-fatal, unlike the daemon: enforcement does not depend on the cloud.
|
|
1279
|
+
hookLogWarn(`cloud connection was not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
1280
|
+
done.push(kit.caution("Couldn't connect; everything else was applied.", "failproofai config"));
|
|
1557
1281
|
}
|
|
1282
|
+
} else if (cloud.kind === "kept" && cloud.how === "saved" && fp.mode !== "cloud") {
|
|
1283
|
+
// A home migrated from layout 1 carries keys but no mode, and would read
|
|
1284
|
+
// as open source everywhere that asks.
|
|
1285
|
+
updateConfig({ mode: "cloud" });
|
|
1558
1286
|
}
|
|
1559
1287
|
|
|
1560
1288
|
await applied;
|
|
@@ -1564,40 +1292,238 @@ export async function runConfigureWizard(
|
|
|
1564
1292
|
// And any record of an earlier failure is now false: this machine got set up.
|
|
1565
1293
|
clearOnboardingAttempt();
|
|
1566
1294
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
// are no longer occasional additions — see buildCompletionSummary's own doc
|
|
1570
|
-
// comment for why the widest combination still fits in 80 columns.
|
|
1571
|
-
outro(
|
|
1572
|
-
buildCompletionSummary(
|
|
1573
|
-
policies.length,
|
|
1574
|
-
clis.length,
|
|
1575
|
-
customEnabled,
|
|
1576
|
-
daemonInstalled,
|
|
1577
|
-
connected,
|
|
1578
|
-
),
|
|
1579
|
-
{ ok: true },
|
|
1580
|
-
stdout,
|
|
1581
|
-
);
|
|
1582
|
-
// Setup wires the hooks and deliberately chooses NO policies, so a machine
|
|
1583
|
-
// that has just finished it enforces almost nothing — and the summary line
|
|
1584
|
-
// says "0 policies" without saying what to do about it. Anyone else's pack is
|
|
1585
|
-
// typed the same way, which is the point: ours is named in full rather than
|
|
1586
|
-
// by a short name only we could use.
|
|
1587
|
-
if (policies.length === 0) {
|
|
1588
|
-
stdout.write(
|
|
1589
|
-
`\n Nothing is enforcing yet. Take ours, or anyone's:\n` +
|
|
1590
|
-
` failproofai policies add ${CORE_SOURCE}\n` +
|
|
1591
|
-
` failproofai policies show <owner>/<repo> (look first)\n\n`,
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1295
|
+
write(doneLines(kit, { elapsedMs: Date.now() - started, done, cwd, agents: selected.length, connected: connected || cloud.kind === "kept" }));
|
|
1296
|
+
|
|
1594
1297
|
return {
|
|
1595
1298
|
applied: true,
|
|
1596
|
-
target,
|
|
1597
|
-
scopes,
|
|
1598
|
-
clis,
|
|
1299
|
+
target: "user",
|
|
1300
|
+
scopes: ["user"],
|
|
1301
|
+
clis: selected,
|
|
1599
1302
|
policies,
|
|
1600
1303
|
daemonInstalled,
|
|
1601
1304
|
connected,
|
|
1305
|
+
mode: connected || cloud.kind === "kept" ? "cloud" : "oss",
|
|
1306
|
+
agents: { selected, added, removed },
|
|
1602
1307
|
};
|
|
1603
1308
|
}
|
|
1309
|
+
|
|
1310
|
+
// ── Cloud helpers ────────────────────────────────────────────────────────────
|
|
1311
|
+
|
|
1312
|
+
/** What the CONNECT step decided. Nothing is written until apply. */
|
|
1313
|
+
type CloudPlan =
|
|
1314
|
+
| { kind: "oss" }
|
|
1315
|
+
| { kind: "kept"; how: "skip" | "saved"; verified: boolean }
|
|
1316
|
+
| { kind: "connect"; token: string; org?: string; note?: string };
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* The connection already on this machine, from either credential: the policy
|
|
1320
|
+
* one and the collector's ingest key. Reading only the first, as setup used to,
|
|
1321
|
+
* showed a collector-only machine as unconnected.
|
|
1322
|
+
*/
|
|
1323
|
+
function savedConnection(): {
|
|
1324
|
+
url: string;
|
|
1325
|
+
token: string;
|
|
1326
|
+
org?: string;
|
|
1327
|
+
machineLabel?: string;
|
|
1328
|
+
} | null {
|
|
1329
|
+
const cloud = readCloudCredentials();
|
|
1330
|
+
const ingest = readIngestCredential();
|
|
1331
|
+
const token = cloud?.token ?? ingest?.key;
|
|
1332
|
+
if (!token) return null;
|
|
1333
|
+
const url = cloud?.url ?? cloudBaseFor(ingest?.url ?? DEFAULT_INGEST_URL);
|
|
1334
|
+
let org: string | undefined;
|
|
1335
|
+
try {
|
|
1336
|
+
const stored = readCredentials().org;
|
|
1337
|
+
org = stored?.name ?? stored?.slug;
|
|
1338
|
+
} catch {
|
|
1339
|
+
org = undefined;
|
|
1340
|
+
}
|
|
1341
|
+
return { url, token, org, machineLabel: cloud?.machineLabel };
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Where a key is sent: `--url`, else FAILPROOFAI_CLOUD_URL, else this machine's
|
|
1346
|
+
* existing connection, else the hosted service.
|
|
1347
|
+
*
|
|
1348
|
+
* Never asked for interactively: everybody on the hosted product has one right
|
|
1349
|
+
* answer, and asking made it look like a decision — which is how keys ended up
|
|
1350
|
+
* pasted into the URL field. The env value goes through the SAME validation a
|
|
1351
|
+
* typed one did, so http stays loopback-only. The source is named on screen,
|
|
1352
|
+
* because an env var is invisible at exactly the moment it matters.
|
|
1353
|
+
*/
|
|
1354
|
+
export function resolveCloudUrl(
|
|
1355
|
+
flag: string | undefined,
|
|
1356
|
+
existing: string | undefined,
|
|
1357
|
+
): { ok: true; url: string; source?: string } | { ok: false; message: string } {
|
|
1358
|
+
const env = process.env.FAILPROOFAI_CLOUD_URL?.trim();
|
|
1359
|
+
const fromFlag = flag?.trim();
|
|
1360
|
+
const override = fromFlag || env;
|
|
1361
|
+
if (!override) return { ok: true, url: existing ?? cloudBaseFor(DEFAULT_INGEST_URL) };
|
|
1362
|
+
const source = fromFlag && fromFlag !== env ? "--url" : "FAILPROOFAI_CLOUD_URL";
|
|
1363
|
+
const validated = validateCloudUrl(cloudBaseFor(override));
|
|
1364
|
+
if (!validated.ok) {
|
|
1365
|
+
return {
|
|
1366
|
+
ok: false,
|
|
1367
|
+
message: `${source === "--url" ? "--url is" : "FAILPROOFAI_CLOUD_URL is set to"} "${override}", which cannot be used: ${validated.reason}`,
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
return { ok: true, url: validated.url, source };
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function hostOf(url: string): string {
|
|
1374
|
+
try {
|
|
1375
|
+
return new URL(url).host;
|
|
1376
|
+
} catch {
|
|
1377
|
+
return url;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Check a key without writing anything or registering anything server-side.
|
|
1383
|
+
*
|
|
1384
|
+
* Introspect first: it says which organisation the key belongs to and what it
|
|
1385
|
+
* may do, with no side effect. A key that can neither send events nor pull
|
|
1386
|
+
* policies is refused; one that can do one of them is accepted with a note
|
|
1387
|
+
* saying which half is missing. A server too old for introspect is probed the
|
|
1388
|
+
* way setup always did, with an empty batch to the ingest endpoint.
|
|
1389
|
+
*
|
|
1390
|
+
* `refused: false` is "could not ask" (offline, a timeout, a 5xx) — a saved key
|
|
1391
|
+
* that fails that way may still be fine, so it is not treated as broken.
|
|
1392
|
+
*/
|
|
1393
|
+
export async function checkCloudKey(
|
|
1394
|
+
url: string,
|
|
1395
|
+
key: string,
|
|
1396
|
+
): Promise<{ ok: true; org?: string; note?: string } | { ok: false; refused: boolean; reason: string }> {
|
|
1397
|
+
const identity = await introspectKey(url, key);
|
|
1398
|
+
if (identity.kind === "rejected") return { ok: false, refused: true, reason: "the server did not accept it" };
|
|
1399
|
+
if (identity.kind === "unreachable") return { ok: false, refused: false, reason: identity.reason };
|
|
1400
|
+
if (identity.kind === "ok") {
|
|
1401
|
+
const events = hasPermission(identity.identity, PERMISSION_EVENTS);
|
|
1402
|
+
const policies = hasPermission(identity.identity, PERMISSION_POLICIES);
|
|
1403
|
+
const org = identity.identity.orgName ?? identity.identity.orgSlug;
|
|
1404
|
+
if (!events && !policies) {
|
|
1405
|
+
return { ok: false, refused: true, reason: `it is missing the ${PERMISSION_EVENTS} permission` };
|
|
1406
|
+
}
|
|
1407
|
+
const note = !events
|
|
1408
|
+
? `This key can't send events (${PERMISSION_EVENTS}), so nothing will be collected.`
|
|
1409
|
+
: !policies
|
|
1410
|
+
? `This key can't pull policies (${PERMISSION_POLICIES}), so cloud-managed policies won't arrive.`
|
|
1411
|
+
: undefined;
|
|
1412
|
+
return { ok: true, org, note };
|
|
1413
|
+
}
|
|
1414
|
+
const probe = await validateIngestKey({ url: ingestUrlFor(url), key });
|
|
1415
|
+
if (probe.ok) return { ok: true };
|
|
1416
|
+
const offline = /^could not reach|answered 5\d\d/.test(probe.reason);
|
|
1417
|
+
return { ok: false, refused: !offline, reason: probe.reason };
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** The CONNECT step, collapsed: what was checked, never yet what was written. */
|
|
1421
|
+
export function cloudCollapsedLines(
|
|
1422
|
+
kit: ReturnType<typeof screenKit>,
|
|
1423
|
+
plan: CloudPlan,
|
|
1424
|
+
at: { host: string; savedHost: string; savedOrg?: string; source?: string },
|
|
1425
|
+
): string[] {
|
|
1426
|
+
// An override is named where the key went: an env var is invisible at the
|
|
1427
|
+
// moment it matters, and a machine reporting somewhere unexpected is exactly
|
|
1428
|
+
// what nobody notices until they look for data that is not there.
|
|
1429
|
+
const head = kit.head("Connect to cloud", at.source ? `${at.host} · from ${at.source}` : undefined);
|
|
1430
|
+
switch (plan.kind) {
|
|
1431
|
+
case "oss":
|
|
1432
|
+
return [head, kit.ok("Using open source.")];
|
|
1433
|
+
case "kept":
|
|
1434
|
+
if (plan.how === "skip") return [head, kit.ok(`Not connecting. The connection to ${at.savedHost} is unchanged.`)];
|
|
1435
|
+
return plan.verified
|
|
1436
|
+
? [head, kit.ok(at.savedOrg ? `Connected as ${at.savedOrg} to ${at.savedHost}` : `Connected to ${at.savedHost}`)]
|
|
1437
|
+
: [head, kit.caution(`Keeping the connection to ${at.savedHost}. Its key couldn't be checked just now.`)];
|
|
1438
|
+
case "connect":
|
|
1439
|
+
return [
|
|
1440
|
+
head,
|
|
1441
|
+
kit.ok(plan.org ? `Key accepted for ${plan.org} on ${at.host}` : `Key accepted by ${at.host}`),
|
|
1442
|
+
...(plan.note ? [kit.caution(plan.note)] : []),
|
|
1443
|
+
];
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** What connecting wrote, for the DONE section. */
|
|
1448
|
+
function connectDoneLines(
|
|
1449
|
+
kit: ReturnType<typeof screenKit>,
|
|
1450
|
+
outcome: Awaited<ReturnType<typeof connectToCloud>>,
|
|
1451
|
+
host: string,
|
|
1452
|
+
org: string | undefined,
|
|
1453
|
+
): string[] {
|
|
1454
|
+
const name = outcome.org?.name ?? outcome.org?.slug ?? org;
|
|
1455
|
+
const as = name ? `Connected as ${name} to ${host}` : `Connected to ${host}`;
|
|
1456
|
+
// Jev's state rides along on every branch that connected: whether it is on,
|
|
1457
|
+
// available but off, or was cleared is decided by this connect and said
|
|
1458
|
+
// nowhere else.
|
|
1459
|
+
if (outcome.policy.ok && outcome.ingest.ok) return [kit.ok(as), ...jevLines(outcome)];
|
|
1460
|
+
if (outcome.ingest.ok) {
|
|
1461
|
+
return [kit.ok(as), kit.caution(`Cloud-managed policies won't arrive: ${outcome.policy.reason}`), ...jevLines(outcome)];
|
|
1462
|
+
}
|
|
1463
|
+
if (outcome.policy.ok) return [kit.ok(as), kit.caution(`Nothing is collected: ${outcome.ingest.reason}`), ...jevLines(outcome)];
|
|
1464
|
+
return [kit.caution(`Not connected: ${outcome.ingest.reason ?? outcome.policy.reason ?? "unknown error"}`, "failproofai config")];
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/** Packs that were declared and refused, and deny for it — as the policies listing counts them. */
|
|
1468
|
+
function failClosedPacks(): Array<{ id: string | null }> {
|
|
1469
|
+
try {
|
|
1470
|
+
return readInstalledPacks().errors.filter((err) => err.effect !== "observe" && !err.semanticOnly);
|
|
1471
|
+
} catch {
|
|
1472
|
+
return [];
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* The DONE section: what apply wrote, then whether anything is enforcing — the
|
|
1478
|
+
* one generic warning when it is not (decision D18) — and the custom-policy
|
|
1479
|
+
* files that will not load, which the removed review step used to show.
|
|
1480
|
+
*/
|
|
1481
|
+
export function doneLines(
|
|
1482
|
+
kit: ReturnType<typeof screenKit>,
|
|
1483
|
+
state: { elapsedMs: number; done: string[]; cwd: string; agents: number; connected: boolean },
|
|
1484
|
+
): string[] {
|
|
1485
|
+
const lines = [kit.head("Done", `in ${(state.elapsedMs / 1000).toFixed(1)}s`), ...state.done];
|
|
1486
|
+
const on = `${state.agents} ${state.agents === 1 ? "agent" : "agents"}`;
|
|
1487
|
+
if (notEnforcingReason(state.cwd)) {
|
|
1488
|
+
lines.push(
|
|
1489
|
+
kit.caution("Policies are not enforcing yet."),
|
|
1490
|
+
` Turn on ours: ${kit.cmd(`failproofai policies add ${CORE_SOURCE}`)}`,
|
|
1491
|
+
);
|
|
1492
|
+
} else {
|
|
1493
|
+
const { count, custom } = enforcingPolicyCount(state.cwd);
|
|
1494
|
+
if (count > 0 || custom) {
|
|
1495
|
+
lines.push(
|
|
1496
|
+
kit.ok(
|
|
1497
|
+
count === 0
|
|
1498
|
+
? `Your custom policies are enforcing on ${on}.`
|
|
1499
|
+
: custom
|
|
1500
|
+
? `${count} ${count === 1 ? "policy" : "policies"} and your custom policies are enforcing on ${on}.`
|
|
1501
|
+
: `${count} ${count === 1 ? "policy is" : "policies are"} enforcing on ${on}.`,
|
|
1502
|
+
),
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
// A pack that failed to load fails closed, which is why the machine reads
|
|
1506
|
+
// as enforcing at all when nothing above counted. Said in the words the
|
|
1507
|
+
// policies listing uses, never as custom policies that may not exist.
|
|
1508
|
+
const failed = failClosedPacks();
|
|
1509
|
+
if (failed.length > 0) {
|
|
1510
|
+
const [first] = failed;
|
|
1511
|
+
const many = failed.length > 1;
|
|
1512
|
+
lines.push(
|
|
1513
|
+
kit.fail(
|
|
1514
|
+
`${first.id ?? "A pack"}${many ? ` and ${failed.length - 1} more` : ""} failed to load, ` +
|
|
1515
|
+
`so the calls ${many ? "they cover" : "it covers"} are denied.`,
|
|
1516
|
+
first.id ? `failproofai policies remove ${first.id}` : "failproofai policies",
|
|
1517
|
+
),
|
|
1518
|
+
);
|
|
1519
|
+
} else if (count === 0 && !custom) {
|
|
1520
|
+
lines.push(kit.ok(`Policies are enforcing on ${on}.`));
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (!state.connected) lines.push(` Connect to cloud any time: ${kit.cmd("failproofai config")}`);
|
|
1524
|
+
for (const warning of describeCustomPolicies(state.cwd).warnings) {
|
|
1525
|
+
lines.push(kit.caution(warning.replace(/^! /, "")));
|
|
1526
|
+
}
|
|
1527
|
+
lines.push("");
|
|
1528
|
+
return lines;
|
|
1529
|
+
}
|