failproofai 1.0.0-beta.1 → 1.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +2 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error/page/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 +3 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/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 +3 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/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 +5 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/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 +3 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
- package/.next/standalone/.next/server/app/policies/page.js +5 -3
- 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 +4 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/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 +6 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/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 +4 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +18 -0
- package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +6 -0
- package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +53 -0
- package/.next/standalone/.next/server/app/settings/page.js +18 -0
- package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app-paths-manifest.json +2 -1
- package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[externals]__1nl3dvw._.js → [externals]__0r-zamh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g0vcwt._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0h3lfmp._.js → [root-of-the-server]__0ky_n0r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__18d-9oi._.js → [root-of-the-server]__0lk_935._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__0pbp9fe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__10oa-v9._.js → [root-of-the-server]__0v2fpxe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0sb_5m8._.js → [root-of-the-server]__131ebum._.js} +4 -4
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-dh5jy._.js → [root-of-the-server]__1_ps0jw._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l4vuqm._.js → [root-of-the-server]__1a7i2q4._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +5 -0
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +5 -0
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +5 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06bp_fg._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__074qmkp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1a2hcb_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1gwr8l-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__1rdruw4._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__1yw368c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__20ie7me._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0n4bc3i._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gmzkwj._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dwo94s.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +5 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +2 -2
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +74 -20
- package/.next/standalone/.next/static/chunks/{2ovmv4esqalpb.js → 0-m1vctj2jny6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1b3a1scouf40o.js → 04y6igmx5olt6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{35584kn409ad1.js → 06abmgw7cvecc.js} +22 -22
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/0on899o88y54u.js +1 -0
- package/.next/standalone/.next/static/chunks/0p_y4ndalc68e.js +6 -0
- package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
- package/.next/standalone/.next/static/chunks/0ug1y_6-uwu5t.js +1 -0
- package/.next/standalone/.next/static/chunks/13vrxdsdihqd_.css +2 -0
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/19lrnqxn7ni7i.js +1 -0
- package/.next/standalone/.next/static/chunks/{30is-6r00sbd7.js → 1a9qlmo5s678i.js} +1 -1
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +1 -0
- package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +2 -0
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/{07ix3lb4gib0x.js → 37nu_nos28w1x.js} +1 -1
- package/.next/standalone/.next/static/chunks/{191njgoik03fm.js → 43ylh_2r15k-w.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/actions/get-hooks-config.ts +2 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +70 -0
- package/.next/standalone/app/actions/update-scheduled-audit.ts +44 -0
- package/.next/standalone/app/api/audit/run/route.ts +28 -0
- package/.next/standalone/app/api/audit/status/route.ts +13 -1
- package/.next/standalone/app/api/auth/status/route.ts +1 -1
- package/.next/standalone/app/audit/_components/auth-dialog.css +159 -0
- package/.next/standalone/app/audit/_components/auth-dialog.tsx +3 -0
- package/.next/standalone/app/audit/audit-styles.css +6 -149
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/policies/hooks-client.tsx +81 -6
- package/.next/standalone/app/settings/page.tsx +31 -0
- package/.next/standalone/app/settings/settings-client.tsx +488 -0
- package/.next/standalone/components/navbar.tsx +2 -0
- package/.next/standalone/lib/auth/api-server-client.ts +2 -3
- package/.next/standalone/lib/auth/auth-store.ts +4 -4
- package/.next/standalone/lib/codex-sessions.ts +2 -1
- package/.next/standalone/lib/dashboard-host.ts +101 -0
- package/.next/standalone/lib/install-check.ts +22 -7
- package/.next/standalone/lib/pi-sessions.ts +99 -14
- package/.next/standalone/lib/telemetry-enabled.ts +54 -0
- package/.next/standalone/lib/telemetry-id.ts +52 -1
- package/.next/standalone/lib/telemetry.ts +8 -4
- package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
- package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
- package/.next/standalone/node_modules/react/package.json +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/package.json +4 -4
- package/.next/standalone/package.json +23 -10
- package/.next/standalone/proxy.ts +111 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +580 -81
- package/dist/cli.mjs +12974 -7447
- package/dist/worker.mjs +2020 -272
- package/lib/auth/api-server-client.ts +2 -3
- package/lib/auth/auth-store.ts +4 -4
- package/lib/codex-sessions.ts +2 -1
- package/lib/dashboard-host.ts +101 -0
- package/lib/install-check.ts +22 -7
- package/lib/pi-sessions.ts +99 -14
- package/lib/telemetry-enabled.ts +54 -0
- package/lib/telemetry-id.ts +52 -1
- package/lib/telemetry.ts +8 -4
- package/package.json +23 -10
- package/scripts/build-daemon-packages.mjs +247 -0
- package/scripts/daemon-platforms.mjs +42 -0
- package/scripts/install-telemetry.mjs +8 -1
- package/scripts/launch.ts +25 -3
- package/scripts/parse-script-args.ts +27 -1
- package/scripts/prune-standalone.mjs +26 -0
- package/scripts/publish-aliases.mjs +62 -41
- package/scripts/repro-npm-install.sh +155 -0
- package/scripts/translate-docs/readme-translator.ts +163 -4
- package/scripts/validate-mdx.ts +153 -13
- package/src/audit/audit-lock.ts +285 -0
- package/src/audit/audit-schedule.ts +111 -0
- package/src/audit/cache.ts +18 -4
- package/src/audit/cli-adapters/goose.ts +2 -1
- package/src/audit/cli-adapters/hermes.ts +28 -10
- package/src/audit/cli.ts +191 -35
- package/src/audit/dashboard-cache.ts +2 -1
- package/src/hooks/backfill-cli.ts +247 -0
- package/src/hooks/builtin-policies.ts +390 -2
- package/src/hooks/cloud-connection.ts +354 -0
- package/src/hooks/cloud-enrollment-cli.ts +325 -0
- package/src/hooks/cloud-enrollment.ts +243 -0
- package/src/hooks/cloud-introspect.ts +147 -0
- package/src/hooks/cloud-managed-policies.ts +169 -0
- package/src/hooks/collector-config.ts +234 -0
- package/src/hooks/configure-wizard.ts +930 -158
- package/src/hooks/custom-hooks-loader.ts +191 -33
- package/src/hooks/daemon-client.ts +144 -17
- package/src/hooks/daemon-download.ts +264 -30
- package/src/hooks/daemon-service.ts +1030 -72
- package/src/hooks/first-run-gate.ts +50 -0
- package/src/hooks/fp-config.ts +497 -0
- package/src/hooks/fp-home.ts +375 -0
- package/src/hooks/fp-reset.ts +464 -0
- package/src/hooks/handler.ts +185 -6
- package/src/hooks/hook-activity-store.ts +85 -20
- package/src/hooks/hook-logger.ts +2 -1
- package/src/hooks/hook-telemetry.ts +5 -1
- package/src/hooks/hooks-config.ts +4 -3
- package/src/hooks/loader-utils.ts +171 -8
- package/src/hooks/manager.ts +5 -4
- package/src/hooks/onboarding-attempt.ts +179 -0
- package/src/hooks/onboarding-lock.ts +119 -0
- package/src/hooks/policy-types.ts +28 -0
- package/src/hooks/session-pause-cli.ts +159 -0
- package/src/hooks/session-pause.ts +268 -0
- package/src/hooks/setup-state.ts +256 -0
- package/src/hooks/tui.ts +119 -4
- package/src/hooks/uninstall-cli.ts +349 -0
- package/src/hooks/worker-server.ts +65 -3
- package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0114ewg._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ig795d._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1yo1gyz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_14q3-7b.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +0 -5
- package/.next/standalone/.next/static/chunks/0fdx7ikab08-g.js +0 -6
- package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +0 -1
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/1d71o1_tav721.js +0 -1
- package/.next/standalone/.next/static/chunks/24gs8ip6_spcb.js +0 -1
- package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/3i27c3hcriawq.css +0 -1
- package/.next/standalone/Cargo.lock +0 -428
- package/.next/standalone/Cargo.toml +0 -9
- package/.next/standalone/crates/PROTOCOL.md +0 -122
- package/.next/standalone/crates/failproofaid/Cargo.toml +0 -18
- package/.next/standalone/crates/failproofaid/src/lock.rs +0 -81
- package/.next/standalone/crates/failproofaid/src/main.rs +0 -83
- package/.next/standalone/crates/failproofaid/src/paths.rs +0 -172
- package/.next/standalone/crates/failproofaid/src/server.rs +0 -502
- package/.next/standalone/crates/failproofaid/src/worker.rs +0 -400
- package/.next/standalone/crates/failproofaid/tests/daemon_e2e.rs +0 -241
- package/.next/standalone/crates/fpai-ipc/Cargo.toml +0 -16
- package/.next/standalone/crates/fpai-ipc/src/envelope.rs +0 -175
- package/.next/standalone/crates/fpai-ipc/src/framing.rs +0 -177
- package/.next/standalone/crates/fpai-ipc/src/lib.rs +0 -10
- package/.next/standalone/crates/fpai-ipc/src/peer.rs +0 -80
- package/.next/standalone/docker-hook-sync/Dockerfile +0 -79
- package/.next/standalone/docker-hook-sync/entrypoint.sh +0 -109
- package/.next/standalone/integration-suite/Dockerfile +0 -47
- package/.next/standalone/integration-suite/README.md +0 -129
- package/.next/standalone/integration-suite/canary-policies.mjs +0 -45
- package/.next/standalone/integration-suite/capture-tokens.sh +0 -48
- package/.next/standalone/integration-suite/ci-entrypoint.sh +0 -179
- package/.next/standalone/integration-suite/inject-tokens.sh +0 -44
- package/.next/standalone/integration-suite/install-clis.sh +0 -136
- package/.next/standalone/integration-suite/probe-cli.sh +0 -248
- package/.next/standalone/integration-suite/report.js +0 -120
- package/.next/standalone/integration-suite/run.sh +0 -159
- package/.next/standalone/openclaw-plugin/index.js +0 -223
- package/.next/standalone/openclaw-plugin/openclaw.plugin.json +0 -6
- package/.next/standalone/openclaw-plugin/package.json +0 -12
- package/.next/standalone/osv-scanner.toml +0 -17
- package/.next/standalone/pi-extension/index.ts +0 -524
- package/.next/standalone/pi-extension/package.json +0 -12
- package/.next/standalone/postcss.config.mjs +0 -8
- package/.next/standalone/readme-arch-hq.gif +0 -0
- package/.next/standalone/rust-toolchain.toml +0 -4
- package/src/auth/cli.ts +0 -371
- /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → BZDo0aAHzDwh5ntxgIgpc}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → BZDo0aAHzDwh5ntxgIgpc}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → BZDo0aAHzDwh5ntxgIgpc}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One connection to Failproof Cloud, with two capabilities.
|
|
3
|
+
*
|
|
4
|
+
* # Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Enrolment (#632) and collection (#640) were built independently and each
|
|
7
|
+
* arrived with its own credential file, its own URL and its own setup step:
|
|
8
|
+
*
|
|
9
|
+
* pull policy -> ~/.failproofai/cloud.json <base>/enforcement/v1/...
|
|
10
|
+
* push events -> ~/.failproofai/ingest.json <base>/events
|
|
11
|
+
*
|
|
12
|
+
* Same server, same organisation, usually the same key. Asked twice, a user
|
|
13
|
+
* who connects for policy sees an empty dashboard and reasonably concludes the
|
|
14
|
+
* product is broken — the second setup step is invisible until someone goes
|
|
15
|
+
* looking for it.
|
|
16
|
+
*
|
|
17
|
+
* So the URL and token are asked for ONCE and both capabilities are configured
|
|
18
|
+
* from them. The two files stay separate on disk — that is a real security
|
|
19
|
+
* boundary, since `cloud.json` and `ingest.json` can hold different keys with
|
|
20
|
+
* different permissions, and the daemon reads them independently — but nothing
|
|
21
|
+
* above this module has to know that.
|
|
22
|
+
*
|
|
23
|
+
* # Capabilities are verified and reported INDEPENDENTLY
|
|
24
|
+
*
|
|
25
|
+
* A key may carry `policies:pull` and not `events:add`, or the reverse. The
|
|
26
|
+
* honest outcome is then a partial connection with a precise reason, not an
|
|
27
|
+
* all-or-nothing failure: refusing to enrol for policy because the dashboard
|
|
28
|
+
* would be empty protects nothing, and silently skipping ingest reproduces the
|
|
29
|
+
* exact confusion this module removes.
|
|
30
|
+
*/
|
|
31
|
+
import {
|
|
32
|
+
cloudCredentialPath,
|
|
33
|
+
validateCloudUrl,
|
|
34
|
+
verifyCloudCredentials,
|
|
35
|
+
writeCloudCredentials,
|
|
36
|
+
type CloudCredentials,
|
|
37
|
+
} from "./cloud-enrollment";
|
|
38
|
+
import { updateConfig, readCredentials, writeCredentials } from "./fp-config";
|
|
39
|
+
import {
|
|
40
|
+
introspectKey,
|
|
41
|
+
hasPermission,
|
|
42
|
+
describeOrg,
|
|
43
|
+
PERMISSION_EVENTS,
|
|
44
|
+
PERMISSION_POLICIES,
|
|
45
|
+
} from "./cloud-introspect";
|
|
46
|
+
import {
|
|
47
|
+
ingestPath,
|
|
48
|
+
validateIngestKey,
|
|
49
|
+
writeCollectorSettings,
|
|
50
|
+
writeIngestCredential,
|
|
51
|
+
type CollectorSettings,
|
|
52
|
+
} from "./collector-config";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The path ingest is served on, appended to the cloud base URL.
|
|
56
|
+
*
|
|
57
|
+
* `/v1/events` rather than `/events`, and the difference is load-bearing for
|
|
58
|
+
* the hosted deployment. The server mounts its route list TWICE — flat at `/`
|
|
59
|
+
* and nested under `/v1` — so both reach the same handler when you talk to the
|
|
60
|
+
* server directly. But on the dashboard hostname a reverse proxy routes only
|
|
61
|
+
* `/v1/*` and `/enforcement/v1/*` to the server and sends EVERYTHING ELSE to
|
|
62
|
+
* the Next.js app. So `https://app.befailproof.ai/events` does not reach ingest
|
|
63
|
+
* at all; it reaches a web app that will happily answer.
|
|
64
|
+
*
|
|
65
|
+
* `/v1/events` is therefore the only path that works on all three shapes: the
|
|
66
|
+
* dashboard hostname, the server hostname, and a Compose deployment with no
|
|
67
|
+
* proxy at all. That is what lets one `--connect <origin>` configure BOTH
|
|
68
|
+
* capabilities from a single URL.
|
|
69
|
+
*/
|
|
70
|
+
export const INGEST_PATH = "/v1/events";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The ingest endpoint for a cloud base URL.
|
|
74
|
+
*
|
|
75
|
+
* Ingest wants a COMPLETE endpoint while enrolment wants a base, which is the
|
|
76
|
+
* whole reason two URLs were being asked for. One is derivable from the other,
|
|
77
|
+
* so only the base is ever asked for.
|
|
78
|
+
*/
|
|
79
|
+
export function ingestUrlFor(baseUrl: string): string {
|
|
80
|
+
return `${baseUrl.replace(/\/$/, "")}${INGEST_PATH}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The cloud base for whatever URL someone actually typed.
|
|
85
|
+
*
|
|
86
|
+
* Accepts either form. People paste the ingest endpoint — it is what the older
|
|
87
|
+
* prompt asked for, and what any existing runbook or `ingest.json` contains —
|
|
88
|
+
* and rejecting it as "not a base URL" would be pedantry about a URL we can
|
|
89
|
+
* read perfectly well.
|
|
90
|
+
*/
|
|
91
|
+
export function cloudBaseFor(url: string): string {
|
|
92
|
+
const trimmed = url.replace(/\/$/, "");
|
|
93
|
+
// Strip either ingest path. People paste what an older setup asked for, what
|
|
94
|
+
// is already in their config, or what a colleague sent them — and `/events`
|
|
95
|
+
// was the documented value for the whole life of the product before `/v1`.
|
|
96
|
+
// Refusing a URL we can read perfectly well is pedantry; silently treating
|
|
97
|
+
// `https://host/events` as an ORIGIN would be worse, because every derived
|
|
98
|
+
// URL would then carry a stray `/events` segment.
|
|
99
|
+
for (const suffix of [INGEST_PATH, "/events"]) {
|
|
100
|
+
if (trimmed.endsWith(suffix)) return trimmed.slice(0, -suffix.length);
|
|
101
|
+
}
|
|
102
|
+
return trimmed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ConnectInput {
|
|
106
|
+
/**
|
|
107
|
+
* Cloud base URL. Re-validated with `validateCloudUrl` on entry rather than
|
|
108
|
+
* trusted — this used to say "already validated by `validateCloudUrl`", and
|
|
109
|
+
* the wizard did not.
|
|
110
|
+
*/
|
|
111
|
+
url: string;
|
|
112
|
+
token: string;
|
|
113
|
+
machineId: string;
|
|
114
|
+
/** Human-facing display name for this machine. Defaults to the hostname. */
|
|
115
|
+
machineLabel?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Send session transcripts.
|
|
118
|
+
*
|
|
119
|
+
* Both callers — the wizard and `--connect` — now pass `true` unless the
|
|
120
|
+
* user opts out, because transcripts are what makes a dashboard worth
|
|
121
|
+
* connecting to. This function itself still requires it EXPLICITLY and
|
|
122
|
+
* treats `undefined` as off: a library that silently opts a caller into
|
|
123
|
+
* shipping prompts and file contents is the wrong default at this layer,
|
|
124
|
+
* whatever the product default above it happens to be.
|
|
125
|
+
*/
|
|
126
|
+
sessions?: boolean;
|
|
127
|
+
environment?: string;
|
|
128
|
+
/** Injected by tests so nothing reaches the network. */
|
|
129
|
+
verifyPolicy?: typeof verifyCloudCredentials;
|
|
130
|
+
verifyIngest?: typeof validateIngestKey;
|
|
131
|
+
introspect?: typeof introspectKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface CapabilityOutcome {
|
|
135
|
+
ok: boolean;
|
|
136
|
+
reason?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ConnectOutcome {
|
|
140
|
+
policy: CapabilityOutcome & { policyCount?: number; generation?: number };
|
|
141
|
+
ingest: CapabilityOutcome;
|
|
142
|
+
/** Which organisation the key belongs to. Absent on a pre-introspect server. */
|
|
143
|
+
org?: { id?: string; slug?: string; name?: string };
|
|
144
|
+
/** True when at least one capability was configured. */
|
|
145
|
+
anyConfigured: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Verify both capabilities, then write only what actually works.
|
|
150
|
+
*
|
|
151
|
+
* Verification happens BEFORE any write, per capability. A credential file that
|
|
152
|
+
* does not work is worse than none, because `--status` then reports a
|
|
153
|
+
* connection the machine does not have.
|
|
154
|
+
*/
|
|
155
|
+
export async function connectToCloud(input: ConnectInput): Promise<ConnectOutcome> {
|
|
156
|
+
// Checked HERE, not only at the call sites. `ConnectInput.url` is documented
|
|
157
|
+
// as "already validated by `validateCloudUrl`", and that was true of exactly
|
|
158
|
+
// one of the two callers: `--connect` validated, and the interactive wizard —
|
|
159
|
+
// the documented primary path — matched `/^https?:\/\//` and handed the raw
|
|
160
|
+
// string straight to `validateIngestKey` and then to here. So the flow most
|
|
161
|
+
// people use sent the machine's bearer token to any `http://` host in clear.
|
|
162
|
+
// A precondition worth stating in a docstring is worth enforcing at the
|
|
163
|
+
// boundary that depends on it.
|
|
164
|
+
const checked = validateCloudUrl(input.url);
|
|
165
|
+
if (!checked.ok) {
|
|
166
|
+
return {
|
|
167
|
+
policy: { ok: false, reason: checked.reason },
|
|
168
|
+
ingest: { ok: false, reason: checked.reason },
|
|
169
|
+
anyConfigured: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const creds: CloudCredentials = {
|
|
174
|
+
url: input.url,
|
|
175
|
+
machineId: input.machineId,
|
|
176
|
+
token: input.token,
|
|
177
|
+
machineLabel: input.machineLabel,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const verifyPolicy = input.verifyPolicy ?? verifyCloudCredentials;
|
|
181
|
+
const verifyIngest = input.verifyIngest ?? validateIngestKey;
|
|
182
|
+
const introspect = input.introspect ?? introspectKey;
|
|
183
|
+
const ingestUrl = ingestUrlFor(input.url);
|
|
184
|
+
|
|
185
|
+
// ── Ask the key what it is, before using it for anything ──────────────────
|
|
186
|
+
//
|
|
187
|
+
// One request answers three questions that are otherwise indistinguishable
|
|
188
|
+
// until much later: is the key accepted, which ORG does it report into, and
|
|
189
|
+
// does it carry the permissions this machine needs. Probing the endpoints
|
|
190
|
+
// (below) can only ever answer the first, and only by inference.
|
|
191
|
+
//
|
|
192
|
+
// A rejected key stops here — probing two more endpoints with a credential
|
|
193
|
+
// the server has already refused tells us nothing and costs two more
|
|
194
|
+
// unauthenticated database lookups on a surface with no rate limiting.
|
|
195
|
+
const identity = await introspect(input.url, input.token);
|
|
196
|
+
if (identity.kind === "rejected") {
|
|
197
|
+
return {
|
|
198
|
+
policy: { ok: false, reason: "the server did not accept that key" },
|
|
199
|
+
ingest: { ok: false, reason: "the server did not accept that key" },
|
|
200
|
+
anyConfigured: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// `unsupported` — an AgentEye older than the introspect endpoint. Fall
|
|
205
|
+
// through to probing, which is what every release before this one did. The
|
|
206
|
+
// CLI ships independently of whatever server a customer runs, so a good key
|
|
207
|
+
// on an older deployment must still connect.
|
|
208
|
+
const known = identity.kind === "ok" ? identity.identity : null;
|
|
209
|
+
|
|
210
|
+
// Skip a probe the key provably cannot pass. This is not an optimisation: a
|
|
211
|
+
// permission failure surfaces from `/events` as a 403 whose message reads
|
|
212
|
+
// like a server problem, where introspect can say exactly which permission is
|
|
213
|
+
// missing and for which org.
|
|
214
|
+
const canIngest = known ? hasPermission(known, PERMISSION_EVENTS) : true;
|
|
215
|
+
const canPolicy = known ? hasPermission(known, PERMISSION_POLICIES) : true;
|
|
216
|
+
|
|
217
|
+
const missing = (permission: string) =>
|
|
218
|
+
`that key does not carry \`${permission}\`` +
|
|
219
|
+
(known ? ` (it is valid for ${describeOrg(known)})` : "");
|
|
220
|
+
|
|
221
|
+
// Both checks run regardless of the other's result: reporting "cannot pull
|
|
222
|
+
// policy" while staying silent about ingest would send someone to fix one
|
|
223
|
+
// permission and hit the next one afterwards.
|
|
224
|
+
const [policyResult, ingestResult] = await Promise.all([
|
|
225
|
+
canPolicy
|
|
226
|
+
? verifyPolicy(creds)
|
|
227
|
+
: Promise.resolve({ ok: false as const, reason: missing(PERMISSION_POLICIES) }),
|
|
228
|
+
canIngest
|
|
229
|
+
? verifyIngest({ url: ingestUrl, key: input.token })
|
|
230
|
+
: Promise.resolve({ ok: false as const, reason: missing(PERMISSION_EVENTS) }),
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const outcome: ConnectOutcome = {
|
|
234
|
+
policy: policyResult.ok
|
|
235
|
+
? { ok: true, policyCount: policyResult.policyCount, generation: policyResult.generation }
|
|
236
|
+
: { ok: false, reason: policyResult.reason },
|
|
237
|
+
ingest: ingestResult.ok ? { ok: true } : { ok: false, reason: ingestResult.reason },
|
|
238
|
+
anyConfigured: false,
|
|
239
|
+
org: known
|
|
240
|
+
? { id: known.orgId, slug: known.orgSlug, name: known.orgName }
|
|
241
|
+
: undefined,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (policyResult.ok) {
|
|
245
|
+
writeCloudCredentials(creds);
|
|
246
|
+
outcome.anyConfigured = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Mode is the hard gate, set only once a capability actually verified.
|
|
250
|
+
// "cloud" is never inferred from a token merely being present: a machine
|
|
251
|
+
// that has not proven it can reach the server must stay provably silent.
|
|
252
|
+
if (policyResult.ok || ingestResult.ok) {
|
|
253
|
+
updateConfig({ mode: "cloud" });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (ingestResult.ok) {
|
|
257
|
+
writeIngestCredential({ url: ingestUrl, key: input.token });
|
|
258
|
+
const settings: CollectorSettings = {
|
|
259
|
+
// Hook activity is what makes the dashboard show anything at all, and it
|
|
260
|
+
// carries decisions and tool names — never file contents.
|
|
261
|
+
hooks: true,
|
|
262
|
+
// Explicit only — see the `sessions` doc on ConnectInput. Callers decide
|
|
263
|
+
// the product default; this layer never infers one.
|
|
264
|
+
sessions: input.sessions === true,
|
|
265
|
+
environment: input.environment,
|
|
266
|
+
// So the daemon stamps this machine's id on every collected event and the
|
|
267
|
+
// dashboard groups by machine, not by per-project agent_id.
|
|
268
|
+
machineId: input.machineId,
|
|
269
|
+
};
|
|
270
|
+
writeCollectorSettings(settings);
|
|
271
|
+
outcome.anyConfigured = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Last, and only once something actually configured: recording an org for a
|
|
275
|
+
// machine that connected to nothing would be a claim about where data goes
|
|
276
|
+
// from a machine that sends none. Merged into whatever the two writers above
|
|
277
|
+
// just wrote, so it cannot race them.
|
|
278
|
+
if (outcome.anyConfigured && known && (known.orgId || known.orgSlug || known.orgName)) {
|
|
279
|
+
writeCredentials({
|
|
280
|
+
...readCredentials(),
|
|
281
|
+
org: { id: known.orgId, slug: known.orgSlug, name: known.orgName },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return outcome;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* How to describe the outcome to someone at a terminal.
|
|
290
|
+
*
|
|
291
|
+
* The partial cases are the ones worth wording carefully: each names the
|
|
292
|
+
* missing permission, because "connected, but the dashboard will stay empty"
|
|
293
|
+
* without a cause is the message that sends people to support.
|
|
294
|
+
*/
|
|
295
|
+
export function describeOutcome(outcome: ConnectOutcome, machineId: string, url: string): string[] {
|
|
296
|
+
const lines: string[] = [];
|
|
297
|
+
|
|
298
|
+
// Named on every branch that connected, including the partial ones. A key
|
|
299
|
+
// pasted from the wrong organisation authenticates perfectly and reports into
|
|
300
|
+
// somewhere the user is not looking; the org is the only thing on screen that
|
|
301
|
+
// makes that visible, and it is worth nothing if it only prints on success.
|
|
302
|
+
const org = outcome.org
|
|
303
|
+
? outcome.org.name && outcome.org.slug
|
|
304
|
+
? `${outcome.org.name} (${outcome.org.slug})`
|
|
305
|
+
: (outcome.org.name ?? outcome.org.slug ?? outcome.org.id)
|
|
306
|
+
: undefined;
|
|
307
|
+
const into = org ? ` into ${org}` : "";
|
|
308
|
+
|
|
309
|
+
if (outcome.policy.ok && outcome.ingest.ok) {
|
|
310
|
+
lines.push(`Connected to ${url} as ${machineId}${into}.`);
|
|
311
|
+
const n = outcome.policy.policyCount ?? 0;
|
|
312
|
+
lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (generation ${outcome.policy.generation ?? 0}).`);
|
|
313
|
+
lines.push(` Dashboard hook activity will be sent to ${ingestUrlFor(url)}.`);
|
|
314
|
+
return lines;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (outcome.policy.ok && !outcome.ingest.ok) {
|
|
318
|
+
const n = outcome.policy.policyCount ?? 0;
|
|
319
|
+
lines.push(`Connected to ${url} as ${machineId}${into}, for policy only.`);
|
|
320
|
+
lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (generation ${outcome.policy.generation ?? 0}).`);
|
|
321
|
+
lines.push(` Dashboard NOT configured: ${outcome.ingest.reason}`);
|
|
322
|
+
lines.push("");
|
|
323
|
+
lines.push(" Enforcement works. Nothing will appear in the dashboard until this");
|
|
324
|
+
lines.push(" key also carries `events:add`, or you re-run --connect with one that does.");
|
|
325
|
+
return lines;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!outcome.policy.ok && outcome.ingest.ok) {
|
|
329
|
+
lines.push(`Connected to ${url} as ${machineId}${into}, for dashboard reporting only.`);
|
|
330
|
+
lines.push(` Dashboard hook activity will be sent to ${ingestUrlFor(url)}.`);
|
|
331
|
+
lines.push(` Cloud policy NOT configured: ${outcome.policy.reason}`);
|
|
332
|
+
lines.push("");
|
|
333
|
+
lines.push(" This machine keeps enforcing its LOCAL policies and will report what");
|
|
334
|
+
lines.push(" they decide, but will not receive centrally-managed ones.");
|
|
335
|
+
return lines;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
lines.push(`Could not connect to ${url}.`);
|
|
339
|
+
lines.push(` Policy: ${outcome.policy.reason}`);
|
|
340
|
+
lines.push(` Dashboard: ${outcome.ingest.reason}`);
|
|
341
|
+
return lines;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Paths written, for a closing note. Only those that exist. */
|
|
345
|
+
export function configuredPaths(outcome: ConnectOutcome): string[] {
|
|
346
|
+
const paths: string[] = [];
|
|
347
|
+
if (outcome.policy.ok) paths.push(cloudCredentialPath());
|
|
348
|
+
if (outcome.ingest.ok) paths.push(ingestPath());
|
|
349
|
+
// Deduplicated because layout 2 consolidated both credentials into
|
|
350
|
+
// `credentials.toml`, so the two capabilities now name the SAME file and the
|
|
351
|
+
// closing note read "stored in <path> and <path>" — the same path, twice.
|
|
352
|
+
// They stay two entries above because the JSON override can still split them.
|
|
353
|
+
return [...new Set(paths)];
|
|
354
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `failproofai config --connect | --disconnect`, and the connection half of
|
|
3
|
+
* `--status`.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
clearCloudCredentials,
|
|
7
|
+
maskToken,
|
|
8
|
+
readCloudCredentials,
|
|
9
|
+
resolveMachineId,
|
|
10
|
+
resolveMachineLabel,
|
|
11
|
+
validateCloudUrl,
|
|
12
|
+
verifyCloudCredentials,
|
|
13
|
+
writeCloudCredentials,
|
|
14
|
+
cloudCredentialPath,
|
|
15
|
+
} from "./cloud-enrollment";
|
|
16
|
+
import { daemonRestartCommand, daemonServiceStatus, daemonVersionSkew } from "./daemon-service";
|
|
17
|
+
import { clearActiveCloudManagedPolicies } from "./cloud-managed-policies";
|
|
18
|
+
import { readVersionFile, readCredentials } from "./fp-config";
|
|
19
|
+
import { version as cliVersion } from "../../package.json";
|
|
20
|
+
import { daemonSocketPresent } from "./daemon-client";
|
|
21
|
+
import {
|
|
22
|
+
cloudBaseFor,
|
|
23
|
+
configuredPaths,
|
|
24
|
+
connectToCloud,
|
|
25
|
+
describeOutcome,
|
|
26
|
+
ingestUrlFor,
|
|
27
|
+
} from "./cloud-connection";
|
|
28
|
+
import {
|
|
29
|
+
clearIngestCredential,
|
|
30
|
+
hasIngestCredential,
|
|
31
|
+
readIngestCredential,
|
|
32
|
+
validateIngestKey,
|
|
33
|
+
} from "./collector-config";
|
|
34
|
+
import { updateConfig } from "./fp-config";
|
|
35
|
+
import type { introspectKey } from "./cloud-introspect";
|
|
36
|
+
|
|
37
|
+
export interface ConnectOptions {
|
|
38
|
+
url?: string;
|
|
39
|
+
token?: string;
|
|
40
|
+
machineId?: string;
|
|
41
|
+
/** Human-facing display name for this machine. Defaults to the hostname. */
|
|
42
|
+
machineLabel?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The hostname, passed in by the caller (and injected by tests). Used as the
|
|
45
|
+
* default machine *label*, no longer as the machine id — the id now mints or
|
|
46
|
+
* reuses a stable key so two hosts with the same hostname never merge.
|
|
47
|
+
*/
|
|
48
|
+
defaultMachineId?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Send full session transcripts as well as hook decisions.
|
|
51
|
+
*
|
|
52
|
+
* DEFAULTS ON. A transcript carries prompts, file contents and whatever was
|
|
53
|
+
* pasted into a terminal — which is precisely what makes a dashboard worth
|
|
54
|
+
* connecting to, so hiding it behind an opt-in produced the empty-dashboard
|
|
55
|
+
* problem in a different costume. `false` is the explicit opt-out; the
|
|
56
|
+
* disclosure is printed either way.
|
|
57
|
+
*/
|
|
58
|
+
sessions?: boolean;
|
|
59
|
+
environment?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Injected for tests so nothing reaches the network.
|
|
62
|
+
*
|
|
63
|
+
* All THREE must be here, and a new network call added below has to gain its
|
|
64
|
+
* seam in the same commit: `introspect` was added without one, so every test
|
|
65
|
+
* in `cloud-enrollment-cli.test.ts` silently began making a real request to
|
|
66
|
+
* `be.failproof.ai`. It usually failed fast enough to pass, which is the worst
|
|
67
|
+
* available outcome — the suite went intermittently red on network timing
|
|
68
|
+
* rather than on anything a change had broken.
|
|
69
|
+
*/
|
|
70
|
+
verify?: typeof verifyCloudCredentials;
|
|
71
|
+
verifyIngest?: typeof validateIngestKey;
|
|
72
|
+
introspect?: typeof introspectKey;
|
|
73
|
+
daemonStatus?: () => ReturnType<typeof daemonServiceStatus>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CommandResult {
|
|
77
|
+
exitCode: number;
|
|
78
|
+
lines: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Warn rather than refuse when there is no daemon.
|
|
83
|
+
*
|
|
84
|
+
* Cloud policy is evaluated by failproofaid, so credentials alone pull
|
|
85
|
+
* nothing — but refusing would break baking a machine image where the daemon
|
|
86
|
+
* is installed later, and enrolment is genuinely independent of it. Say so
|
|
87
|
+
* loudly instead of failing.
|
|
88
|
+
*/
|
|
89
|
+
function daemonWarning(status: ReturnType<typeof daemonServiceStatus>): string[] {
|
|
90
|
+
if (status === "running") return [];
|
|
91
|
+
// A daemon started by hand is invisible to the service manager but is
|
|
92
|
+
// running and pulling. Telling someone whose machine is actively enforcing
|
|
93
|
+
// that "nothing will be pulled" is false, and it is exactly the state a
|
|
94
|
+
// developer testing locally is in.
|
|
95
|
+
if (daemonSocketPresent()) {
|
|
96
|
+
return [
|
|
97
|
+
"",
|
|
98
|
+
" A daemon is running outside the service manager, so policy is being pulled.",
|
|
99
|
+
" Install it as a service to survive reboot and logout: failproofai config",
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
if (status === "unsupported-platform") {
|
|
103
|
+
return [
|
|
104
|
+
"",
|
|
105
|
+
"! failproofaid does not run on this platform yet, so nothing will be pulled.",
|
|
106
|
+
" The credentials are stored and will work on a supported machine.",
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
if (status === "not-installed") {
|
|
110
|
+
// "as a service", not "at all": this reads the service manager, so a daemon
|
|
111
|
+
// someone is running by hand during development is reported as absent. The
|
|
112
|
+
// wording has to be true in both cases.
|
|
113
|
+
return [
|
|
114
|
+
"",
|
|
115
|
+
"! failproofaid is not installed as a service, so nothing will be pulled yet.",
|
|
116
|
+
" Install it with: failproofai config",
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
return [
|
|
120
|
+
"",
|
|
121
|
+
"! failproofaid is installed but not running, so nothing will be pulled yet.",
|
|
122
|
+
" Check it with: failproofai config --status",
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function runConnectCommand(opts: ConnectOptions): Promise<CommandResult> {
|
|
127
|
+
if (!opts.url) return { exitCode: 1, lines: ["--connect needs a URL, e.g. --connect https://be.failproof.ai"] };
|
|
128
|
+
if (!opts.token) {
|
|
129
|
+
return {
|
|
130
|
+
exitCode: 1,
|
|
131
|
+
lines: [
|
|
132
|
+
"--connect needs a machine token: --token <key>",
|
|
133
|
+
"",
|
|
134
|
+
"Create an API key carrying only the `policies:pull` permission for this",
|
|
135
|
+
"machine. Do not use an admin key.",
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Accepts the ingest endpoint too. People paste what the older setup asked
|
|
141
|
+
// for, or what is already in their `ingest.json`, and refusing a URL we can
|
|
142
|
+
// read perfectly well is pedantry.
|
|
143
|
+
const validated = validateCloudUrl(cloudBaseFor(opts.url));
|
|
144
|
+
if (!validated.ok) return { exitCode: 1, lines: [validated.reason] };
|
|
145
|
+
|
|
146
|
+
// The stable key: an explicit --machine-id wins, else the id already enrolled
|
|
147
|
+
// here is reused (idempotent re-connect), else a fresh one is minted. Never
|
|
148
|
+
// the hostname — that is the label, so same-hostname hosts do not merge.
|
|
149
|
+
const machineId = resolveMachineId(opts.machineId);
|
|
150
|
+
const machineLabel = resolveMachineLabel(opts.machineLabel ?? opts.defaultMachineId);
|
|
151
|
+
// Humans read the label; the id is shown in parentheses only when it differs,
|
|
152
|
+
// so an operator who set an explicit --machine-id still sees it.
|
|
153
|
+
const shownAs = machineLabel === machineId ? machineId : `${machineLabel} (${machineId})`;
|
|
154
|
+
|
|
155
|
+
// ONE connection, both capabilities. Each is verified before anything is
|
|
156
|
+
// written and reported on its own, because a key can carry `policies:pull`
|
|
157
|
+
// without `events:add` and the useful message names which one is missing.
|
|
158
|
+
const outcome = await connectToCloud({
|
|
159
|
+
url: validated.url,
|
|
160
|
+
token: opts.token,
|
|
161
|
+
machineId,
|
|
162
|
+
machineLabel,
|
|
163
|
+
sessions: opts.sessions,
|
|
164
|
+
environment: opts.environment,
|
|
165
|
+
verifyPolicy: opts.verify,
|
|
166
|
+
verifyIngest: opts.verifyIngest,
|
|
167
|
+
introspect: opts.introspect,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!outcome.anyConfigured) {
|
|
171
|
+
return { exitCode: 1, lines: describeOutcome(outcome, shownAs, validated.url) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The exit code tracks the PRIMARY purpose — enrolling this machine for
|
|
175
|
+
// cloud-managed policy. Configuring the dashboard is worth doing and worth
|
|
176
|
+
// reporting, but it must not turn a failed enrolment into a success: a fleet
|
|
177
|
+
// provisioning script running `--connect … && …` has to stop when the machine
|
|
178
|
+
// will not receive policy. The ingest credential still gets written, because
|
|
179
|
+
// it works and discarding it helps nobody.
|
|
180
|
+
const exitCode = outcome.policy.ok ? 0 : 1;
|
|
181
|
+
|
|
182
|
+
const status = (opts.daemonStatus ?? daemonServiceStatus)();
|
|
183
|
+
const paths = configuredPaths(outcome);
|
|
184
|
+
const lines = [
|
|
185
|
+
...describeOutcome(outcome, shownAs, validated.url),
|
|
186
|
+
` Token ${maskToken(opts.token)} stored in ${paths.join(" and ")} (owner-only).`,
|
|
187
|
+
];
|
|
188
|
+
if (outcome.ingest.ok) {
|
|
189
|
+
// Stated on BOTH branches, never only on the surprising one. Connecting
|
|
190
|
+
// sends transcripts by default — prompts, file contents, command output —
|
|
191
|
+
// and a default that carries that much is one the user has to be told
|
|
192
|
+
// about at the moment it takes effect, not left to find in --help.
|
|
193
|
+
lines.push(
|
|
194
|
+
opts.sessions === false
|
|
195
|
+
? " Session transcripts are NOT being sent (--no-transcripts). Decisions only."
|
|
196
|
+
: " Sending policy decisions AND full session transcripts (prompts, file\n" +
|
|
197
|
+
" contents, command output). Use --no-transcripts for decisions only.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
lines.push(...daemonWarning(status));
|
|
201
|
+
|
|
202
|
+
return { exitCode, lines };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function runDisconnectCommand(): CommandResult {
|
|
206
|
+
const existing = readCloudCredentials();
|
|
207
|
+
const hadIngest = hasIngestCredential();
|
|
208
|
+
const removed = clearCloudCredentials();
|
|
209
|
+
// Disconnect means disconnect. Clearing only the policy credential would
|
|
210
|
+
// leave the machine still shipping activity to a cloud the user believes
|
|
211
|
+
// they have left — the one outcome nobody expects from this command.
|
|
212
|
+
const removedIngest = clearIngestCredential();
|
|
213
|
+
// Stop ENFORCING them too, not merely refreshing them. Clearing the
|
|
214
|
+
// credential ends the daemon's polling; every artifact already on disk stayed
|
|
215
|
+
// referenced by `active.json` and kept being loaded on every tool call — so a
|
|
216
|
+
// machine that had deliberately left its organisation went on being governed
|
|
217
|
+
// by whatever generation was current when it left, indefinitely, while
|
|
218
|
+
// `--status` reported it as unconnected.
|
|
219
|
+
const stoppedManaged = clearActiveCloudManagedPolicies();
|
|
220
|
+
// Back to OSS. Leaving mode = "cloud" with no credentials would describe a
|
|
221
|
+
// machine that does not exist, and every cloud code path keys off this flag
|
|
222
|
+
// rather than off "is a token lying around" precisely so that a disconnected
|
|
223
|
+
// machine is provably silent instead of silent-by-happenstance.
|
|
224
|
+
updateConfig({ mode: "oss" });
|
|
225
|
+
|
|
226
|
+
if (!removed && !existing && !hadIngest && !stoppedManaged) {
|
|
227
|
+
return { exitCode: 0, lines: ["This machine is not connected to Failproof Cloud."] };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const lines = [
|
|
231
|
+
"Disconnected from Failproof Cloud.",
|
|
232
|
+
"",
|
|
233
|
+
stoppedManaged
|
|
234
|
+
? " Cloud-managed policies stop being enforced and stop being refreshed.\n" +
|
|
235
|
+
" Local builtin, custom and convention policies are unaffected."
|
|
236
|
+
: " Local builtin, custom and convention policies are unaffected.",
|
|
237
|
+
];
|
|
238
|
+
if (removedIngest) {
|
|
239
|
+
// Named honestly. The collector manager starts once for the daemon's
|
|
240
|
+
// lifetime (`main.rs`) and the uploader caches its bearer key at
|
|
241
|
+
// construction, so nothing already running notices this file disappear —
|
|
242
|
+
// "stop being sent" was true only of the NEXT daemon start. Telling the
|
|
243
|
+
// user the step that makes it true beats a claim that quietly is not.
|
|
244
|
+
lines.push(
|
|
245
|
+
" No new hook activity or transcripts will be queued. A running failproofaid",
|
|
246
|
+
" keeps the key it started with, so restart it to stop the current process",
|
|
247
|
+
` sending: ${daemonRestartCommand() ?? "restart failproofaid"}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return { exitCode: 0, lines };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The connection half of `--status`. Never prints the token. */
|
|
254
|
+
/**
|
|
255
|
+
* The version line for `--status`.
|
|
256
|
+
*
|
|
257
|
+
* First thing worth knowing in a bug report, and the only place a user can
|
|
258
|
+
* currently find out which daemon they are running without listing a directory.
|
|
259
|
+
*/
|
|
260
|
+
export function versionStatusLines(): string[] {
|
|
261
|
+
const skew = daemonVersionSkew();
|
|
262
|
+
const recorded = readVersionFile();
|
|
263
|
+
const daemon = recorded?.daemon ?? "not installed";
|
|
264
|
+
return [
|
|
265
|
+
`CLI ${cliVersion} · daemon ${daemon}${skew ? " (STALE)" : ""} · layout ${recorded?.layout ?? "-"}`,
|
|
266
|
+
...(skew ? [" Run `failproofai config` to update the daemon."] : []),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function connectionStatusLines(daemonStatus = daemonServiceStatus): string[] {
|
|
271
|
+
const envUrl = process.env.FAILPROOFAI_CLOUD_URL;
|
|
272
|
+
if (envUrl) {
|
|
273
|
+
// Env wins over the file in the daemon, so reporting the file here would
|
|
274
|
+
// describe a configuration that is not in effect.
|
|
275
|
+
return [
|
|
276
|
+
`Cloud: configured by environment (${envUrl}), which takes precedence over ${cloudCredentialPath()}.`,
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
const creds = readCloudCredentials();
|
|
280
|
+
const ingest = readIngestCredential();
|
|
281
|
+
|
|
282
|
+
// Reported as ONE connection with two capabilities. A machine sending
|
|
283
|
+
// activity but pulling no policy, or the reverse, is a state worth seeing at
|
|
284
|
+
// a glance — it is exactly the half-configured case this command exists to
|
|
285
|
+
// catch, and two unrelated lines made it easy to miss.
|
|
286
|
+
if (!creds && !ingest) return ["Cloud: not connected."];
|
|
287
|
+
|
|
288
|
+
// Recorded at connect time from the server's own answer, never guessed from
|
|
289
|
+
// the URL: one deployment hosts many orgs, so the host says nothing about
|
|
290
|
+
// which one this machine's data lands in. Absent against a server with no
|
|
291
|
+
// introspect endpoint, and on credentials written before this was recorded —
|
|
292
|
+
// in both cases the line is simply omitted rather than guessed at.
|
|
293
|
+
const org = readCredentials().org;
|
|
294
|
+
const orgLine = org
|
|
295
|
+
? org.name && org.slug
|
|
296
|
+
? `${org.name} (${org.slug})`
|
|
297
|
+
: (org.name ?? org.slug ?? org.id)
|
|
298
|
+
: undefined;
|
|
299
|
+
|
|
300
|
+
const lines: string[] = [];
|
|
301
|
+
if (creds) {
|
|
302
|
+
// Show the human label with the stable id in parentheses; fall back to the
|
|
303
|
+
// bare id for credentials written before labels existed.
|
|
304
|
+
const shownAs = creds.machineLabel
|
|
305
|
+
? `${creds.machineLabel} (${creds.machineId})`
|
|
306
|
+
: creds.machineId;
|
|
307
|
+
lines.push(`Cloud: connected to ${creds.url} as ${shownAs} (token ${maskToken(creds.token)}).`);
|
|
308
|
+
lines.push(` Policy pulling centrally-managed policies.`);
|
|
309
|
+
} else {
|
|
310
|
+
lines.push(`Cloud: connected to ${cloudBaseFor(ingest!.url)} for reporting only.`);
|
|
311
|
+
lines.push(` Policy NOT pulling — this machine enforces only its local policies.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (orgLine) lines.push(` Org ${orgLine}`);
|
|
315
|
+
|
|
316
|
+
if (ingest) {
|
|
317
|
+
lines.push(` Dashboard sending hook activity to ${ingest.url}.`);
|
|
318
|
+
} else {
|
|
319
|
+
lines.push(` Dashboard NOT sending — nothing from this machine appears in the dashboard.`);
|
|
320
|
+
lines.push(` Re-run: failproofai config --connect ${creds!.url} --token <key>`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
lines.push(...daemonWarning(daemonStatus()).map((l) => (l === "" ? "" : ` ${l.trim()}`)));
|
|
324
|
+
return lines;
|
|
325
|
+
}
|