failproofai 1.0.0-beta.2 → 1.0.0-beta.21
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 +3 -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]__0l4vuqm._.js → [root-of-the-server]__00ni9za._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.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]__0-dh5jy._.js → [root-of-the-server]__0effm9m._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g0vcwt._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__18d-9oi._.js → [root-of-the-server]__0ix8gm7._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0h3lfmp._.js → [root-of-the-server]__0ji11a-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__10oa-v9._.js → [root-of-the-server]__0ojwjxl._.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]__0vb5fl2._.js +3 -0
- 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]__1ibjy5x._.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]__20d06rh._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +3 -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]__0-wfhrp._.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]__1apb7k_._.js → [root-of-the-server]__0_1tiya._.js} +2 -2
- 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]__0t281d4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17itmx-._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__1n2vcv4._.js} +2 -2
- 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]__1rv-sc9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tlvv9a._.js +3 -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/_0-yi74u._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0vo2qyt._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xej-0e._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_10399ge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_149k9oy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1_hwp06._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ctse6u._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1mnhi_r._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ror7k9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1sdgprl._.js +5 -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 +4 -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/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
- package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 113g8wtal6_c4.js} +22 -22
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/18wqn0_3mzylf.js +1 -0
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +1 -0
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 1xq1m8h14tnlj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 1y3vwogqb88dx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 1zmh-b0ulrqu7.js} +1 -1
- package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
- package/.next/standalone/.next/static/chunks/299lhw60e8gr6.js +1 -0
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 2adbpdaxp5ygq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 2kbqcw-6swmav.js} +1 -1
- package/.next/standalone/.next/static/chunks/2n9n5m0963pdg.js +6 -0
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +2 -0
- package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 30nrrpwymht5m.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/SECURITY.md +8 -2
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/actions/get-hooks-config.ts +2 -2
- 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/audit-dashboard.tsx +1 -3
- package/.next/standalone/app/audit/_components/audit-progress-strip.tsx +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/_components/come-back-better-section.tsx +1 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +0 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +1 -1
- package/.next/standalone/app/audit/_components/quirks-section.tsx +0 -1
- package/.next/standalone/app/audit/_components/report-footer.tsx +0 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/_components/strengths-section.tsx +0 -1
- package/.next/standalone/app/audit/audit-styles.css +6 -149
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/components/raw-log-viewer.tsx +1 -1
- package/.next/standalone/app/components/toast.tsx +1 -1
- package/.next/standalone/app/policies/hooks-client.tsx +84 -8
- package/.next/standalone/app/settings/page.tsx +31 -0
- package/.next/standalone/app/settings/settings-client.tsx +487 -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 -5
- 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 -12
- package/.next/standalone/proxy.ts +111 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai-worker.mjs +1 -1
- package/bin/failproofai.mjs +1041 -81
- package/bin/failproofaid-shim.mjs +4 -4
- package/dist/cli.mjs +18513 -10474
- package/dist/worker.mjs +1223 -299
- package/lib/auth/api-server-client.ts +2 -3
- package/lib/auth/auth-store.ts +4 -5
- 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 -12
- package/scripts/build-daemon-packages.mjs +247 -0
- package/scripts/daemon-platforms.mjs +42 -0
- package/scripts/dev-hook.mjs +46 -5
- 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/cache.ts +1 -1
- package/scripts/translate-docs/cli.ts +2 -2
- package/scripts/translate-docs/mintlify-nav.ts +1 -1
- 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 -5
- 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 -3
- package/src/audit/report.ts +0 -1
- package/src/hooks/backfill-cli.ts +247 -0
- package/src/hooks/builtin-policies.ts +391 -3
- package/src/hooks/cloud-connection.ts +354 -0
- package/src/hooks/cloud-enrollment-cli.ts +439 -0
- package/src/hooks/cloud-enrollment.ts +284 -0
- package/src/hooks/cloud-introspect.ts +147 -0
- package/src/hooks/cloud-managed-policies.ts +206 -0
- package/src/hooks/collector-config.ts +218 -0
- package/src/hooks/configure-wizard.ts +1211 -233
- package/src/hooks/custom-hooks-loader.ts +243 -37
- package/src/hooks/daemon-client.ts +136 -33
- package/src/hooks/daemon-download.ts +264 -31
- package/src/hooks/daemon-service.ts +1124 -76
- package/src/hooks/first-run-gate.ts +50 -0
- package/src/hooks/flush-cli.ts +180 -0
- package/src/hooks/fp-config.ts +835 -0
- package/src/hooks/fp-home.ts +714 -0
- package/src/hooks/fp-reset.ts +1363 -0
- package/src/hooks/handler.ts +185 -6
- package/src/hooks/harness-cli.ts +386 -0
- package/src/hooks/hook-activity-store.ts +171 -24
- package/src/hooks/hook-logger.ts +2 -2
- package/src/hooks/hook-telemetry.ts +5 -1
- package/src/hooks/hooks-config.ts +4 -3
- package/src/hooks/integrations.ts +0 -1
- package/src/hooks/loader-utils.ts +172 -9
- package/src/hooks/manager.ts +41 -5
- package/src/hooks/migrations.ts +450 -0
- package/src/hooks/onboarding-attempt.ts +195 -0
- package/src/hooks/onboarding-lock.ts +118 -0
- package/src/hooks/policy-types.ts +28 -0
- package/src/hooks/session-pause-cli.ts +159 -0
- package/src/hooks/session-pause.ts +267 -0
- package/src/hooks/setup-state.ts +256 -0
- package/src/hooks/tui.ts +209 -9
- package/src/hooks/uninstall-cli.ts +418 -0
- package/src/hooks/worker-server.ts +134 -15
- 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]__0nb0syg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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]__1psjybo._.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_0oy_-36.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +0 -5
- package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +0 -1
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.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/.next/static/chunks/3s7myk_co0gbh.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- 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/{NWAjs0tHFyzk97gRGG09r → VjEdIL6W5uBsvEzknXqV9}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → VjEdIL6W5uBsvEzknXqV9}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → VjEdIL6W5uBsvEzknXqV9}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connecting a machine to Failproof Cloud.
|
|
3
|
+
*
|
|
4
|
+
* The credential lives in a file the daemon reads, NOT in the service unit.
|
|
5
|
+
* `daemon-service.ts` installs that unit at mode 0644 into
|
|
6
|
+
* /etc/systemd/system (and the launchd plist likewise), both root-owned and
|
|
7
|
+
* world-readable — so an `Environment="FAILPROOFAI_CLOUD_TOKEN=…"` line would
|
|
8
|
+
* hand an organization-scoped bearer key to every local user, and
|
|
9
|
+
* `systemctl show` would print it back without any privilege at all.
|
|
10
|
+
*
|
|
11
|
+
* Keeping it out of the unit also means enrolment, re-enrolment, token
|
|
12
|
+
* rotation and disconnect need no root — none of them touch a privileged
|
|
13
|
+
* path — and enrolment stops being welded to service installation, so an
|
|
14
|
+
* already-running daemon can be connected without reinstalling anything.
|
|
15
|
+
*
|
|
16
|
+
* Environment variables still win over this file, so CI, containers and the
|
|
17
|
+
* existing tests are unaffected.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
21
|
+
import { hostname } from "node:os";
|
|
22
|
+
import { writeJsonAtomically } from "../../lib/atomic-write";
|
|
23
|
+
import { fetchWithTimeout, isAbortError } from "../../lib/fetch-with-timeout";
|
|
24
|
+
import { credentialsFile } from "./fp-home";
|
|
25
|
+
import { readConfig, readCredentials, writeCredentials } from "./fp-config";
|
|
26
|
+
|
|
27
|
+
const SCHEMA_VERSION = 1;
|
|
28
|
+
const VERIFY_TIMEOUT_MS = 10_000;
|
|
29
|
+
|
|
30
|
+
export interface CloudCredentials {
|
|
31
|
+
url: string;
|
|
32
|
+
machineId: string;
|
|
33
|
+
token: string;
|
|
34
|
+
/**
|
|
35
|
+
* A human-facing name for this machine (e.g. the hostname, or "Chetan's
|
|
36
|
+
* laptop"). Display only — the identity is `machineId`. Optional: absent on
|
|
37
|
+
* credentials written before labels existed, and never required to connect.
|
|
38
|
+
*/
|
|
39
|
+
machineLabel?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface StoredCredentials extends CloudCredentials {
|
|
43
|
+
schemaVersion: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where the cloud credential lives.
|
|
48
|
+
*
|
|
49
|
+
* Layout 2 moved it out of `cloud.json` and into the `[cloud]` table of
|
|
50
|
+
* `credentials.toml`, alongside every other token, so there is exactly one
|
|
51
|
+
* owner-only file to protect rather than three. The env override still names a
|
|
52
|
+
* standalone JSON file, because the daemon and CI both use it that way and it
|
|
53
|
+
* predates the consolidation.
|
|
54
|
+
*/
|
|
55
|
+
export function cloudCredentialPath(): string {
|
|
56
|
+
return process.env.FAILPROOFAI_CLOUD_CREDENTIALS ?? credentialsFile();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when the credential is coming from the JSON override, not the TOML. */
|
|
60
|
+
function usingJsonOverride(): boolean {
|
|
61
|
+
return Boolean(process.env.FAILPROOFAI_CLOUD_CREDENTIALS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rejects a URL that would put a bearer token on the wire in clear.
|
|
66
|
+
*
|
|
67
|
+
* Loopback stays allowed over http, because that is exactly what the
|
|
68
|
+
* enterprise doc's local walkthrough does (`http://localhost:8080`) and there
|
|
69
|
+
* is no network to intercept.
|
|
70
|
+
*/
|
|
71
|
+
export function validateCloudUrl(raw: string): { ok: true; url: string } | { ok: false; reason: string } {
|
|
72
|
+
let parsed: URL;
|
|
73
|
+
try {
|
|
74
|
+
parsed = new URL(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
return { ok: false, reason: `"${raw}" is not a valid URL. Expected something like https://be.failproof.ai` };
|
|
77
|
+
}
|
|
78
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
79
|
+
return { ok: false, reason: `Unsupported scheme "${parsed.protocol}" — use https (or http for localhost).` };
|
|
80
|
+
}
|
|
81
|
+
if (parsed.protocol === "http:") {
|
|
82
|
+
const host = parsed.hostname;
|
|
83
|
+
const loopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
84
|
+
if (!loopback) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason:
|
|
88
|
+
`Refusing to send the machine token to ${parsed.origin} over plain http. ` +
|
|
89
|
+
`Use https, or http only for localhost during development.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, url: parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "")) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Last four characters only — enough to tell two keys apart, useless if leaked. */
|
|
97
|
+
export function maskToken(token: string): string {
|
|
98
|
+
if (token.length <= 4) return "****";
|
|
99
|
+
return `****${token.slice(-4)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function readCloudCredentials(): CloudCredentials | null {
|
|
103
|
+
if (!usingJsonOverride()) return readCredentials().cloud ?? null;
|
|
104
|
+
const path = cloudCredentialPath();
|
|
105
|
+
if (!existsSync(path)) return null;
|
|
106
|
+
try {
|
|
107
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as Partial<StoredCredentials>;
|
|
108
|
+
if (raw.schemaVersion !== SCHEMA_VERSION) return null;
|
|
109
|
+
if (typeof raw.url !== "string" || typeof raw.machineId !== "string" || typeof raw.token !== "string") {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (!raw.url || !raw.machineId || !raw.token) return null;
|
|
113
|
+
const label = typeof raw.machineLabel === "string" && raw.machineLabel ? raw.machineLabel : undefined;
|
|
114
|
+
return { url: raw.url, machineId: raw.machineId, token: raw.token, machineLabel: label };
|
|
115
|
+
} catch {
|
|
116
|
+
// Unreadable or malformed reads as "not connected" rather than throwing:
|
|
117
|
+
// this is consulted by `--status`, and a corrupt file should be reported
|
|
118
|
+
// as disconnected, not crash the command.
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function writeCloudCredentials(creds: CloudCredentials): void {
|
|
124
|
+
if (usingJsonOverride()) {
|
|
125
|
+
// 0600 inside a 0700 dir — the default for writeJsonAtomically.
|
|
126
|
+
writeJsonAtomically(cloudCredentialPath(), { schemaVersion: SCHEMA_VERSION, ...creds });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Merge, never replace: credentials.toml also carries the ingest key and the
|
|
130
|
+
// auth session, and rewriting the file from one caller must not drop the
|
|
131
|
+
// others.
|
|
132
|
+
writeCredentials({ ...readCredentials(), cloud: creds });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The stable key that identifies this machine to the cloud.
|
|
137
|
+
*
|
|
138
|
+
* An explicit `--machine-id` always wins. Otherwise the id already recorded on
|
|
139
|
+
* this machine is reused, so re-running `--connect` is idempotent and never
|
|
140
|
+
* "moves" the machine. Only a machine that has none mints a fresh one — and it
|
|
141
|
+
* mints a random id, NOT the hostname, because two hosts sharing a hostname
|
|
142
|
+
* (fresh cloud VMs, cloned images) would otherwise silently merge into one
|
|
143
|
+
* machine on the server. The hostname becomes the human label instead.
|
|
144
|
+
*
|
|
145
|
+
* ## Why this looks in TWO places before minting
|
|
146
|
+
*
|
|
147
|
+
* `connectToCloud` writes the id to two files under two INDEPENDENT conditions:
|
|
148
|
+
* `credentials.json`'s `cloud` table only when `policies:pull` verified, and
|
|
149
|
+
* `config.json`'s `collector.machine_id` only when `events:add` did. A key
|
|
150
|
+
* carrying one grant and not the other is not a broken setup — it is a
|
|
151
|
+
* first-class state the key-creation drawer offers as two separate presets
|
|
152
|
+
* (`policies` and `collector`), so half-written is a shape this function must
|
|
153
|
+
* expect rather than an anomaly.
|
|
154
|
+
*
|
|
155
|
+
* Consulting only the cloud credential therefore missed the id whenever the
|
|
156
|
+
* FIRST connection was ingest-only, and the consequences all landed on the
|
|
157
|
+
* server, where nothing could undo them:
|
|
158
|
+
*
|
|
159
|
+
* 1. a telemetry-only connect stamps `collector.machine_id = A` on every
|
|
160
|
+
* event and writes no cloud credential;
|
|
161
|
+
* 2. a later connect with a policies-capable key finds no cloud credential,
|
|
162
|
+
* mints B, and overwrites the collector block with it;
|
|
163
|
+
* 3. the fleet list — a union of enrolment rows and event-derived ids — now
|
|
164
|
+
* shows ONE host as TWO machines: A reporting with no label and nothing
|
|
165
|
+
* deployed, B enrolled and empty. A's history is stranded, and A counts
|
|
166
|
+
* toward `unguarded` on the policy page, which is the exact false reading
|
|
167
|
+
* that page exists to surface.
|
|
168
|
+
*
|
|
169
|
+
* Order is most-explicit to least, and the cloud credential stays ABOVE the
|
|
170
|
+
* collector block deliberately: if the two ever disagree, the cloud one is what
|
|
171
|
+
* the server has keyed this machine's enrolment, deployment and history on, and
|
|
172
|
+
* that is the association that must not break. Minting drops to last, where it
|
|
173
|
+
* now means what it says — nothing on this machine has ever carried an id.
|
|
174
|
+
*
|
|
175
|
+
* The collector value is returned VERBATIM rather than trimmed, because the
|
|
176
|
+
* daemon stamps it on events verbatim too (`spool.rs`, which filters only the
|
|
177
|
+
* empty string); normalising it here would hand the cloud a different id than
|
|
178
|
+
* the one the events carry and reintroduce the split this exists to close. A
|
|
179
|
+
* blank or whitespace-only value is not adopted — it identifies nothing, and
|
|
180
|
+
* the server would refuse it.
|
|
181
|
+
*/
|
|
182
|
+
export function resolveMachineId(explicit?: string): string {
|
|
183
|
+
const trimmed = explicit?.trim();
|
|
184
|
+
if (trimmed) return trimmed;
|
|
185
|
+
const existing = readCloudCredentials();
|
|
186
|
+
if (existing?.machineId) return existing.machineId;
|
|
187
|
+
// `readConfig` swallows a missing or corrupt file and answers with defaults,
|
|
188
|
+
// so an unreadable config degrades to minting rather than throwing on a path
|
|
189
|
+
// whose whole job is to keep enrolment working.
|
|
190
|
+
const collectorId = readConfig().collector.machineId;
|
|
191
|
+
if (collectorId?.trim()) return collectorId;
|
|
192
|
+
return randomUUID();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The human-facing name for this machine. Defaults to the hostname as a
|
|
197
|
+
* *suggestion* — mutable, not the identity, and free to collide with another
|
|
198
|
+
* machine's label because `machineId` keeps them apart.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveMachineLabel(explicit?: string): string {
|
|
201
|
+
return explicit?.trim() || hostname();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** True if there was something to remove. */
|
|
205
|
+
export function clearCloudCredentials(): boolean {
|
|
206
|
+
if (usingJsonOverride()) {
|
|
207
|
+
const path = cloudCredentialPath();
|
|
208
|
+
if (!existsSync(path)) return false;
|
|
209
|
+
try {
|
|
210
|
+
rmSync(path);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const current = readCredentials();
|
|
217
|
+
if (!current.cloud) return false;
|
|
218
|
+
// Drop only the cloud table — disconnecting policy must not also revoke the
|
|
219
|
+
// ingest key or the dashboard session.
|
|
220
|
+
const { cloud: _dropped, ...rest } = current;
|
|
221
|
+
writeCredentials(rest);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type VerifyResult =
|
|
226
|
+
| { ok: true; policyCount: number; deployment: number }
|
|
227
|
+
| { ok: false; reason: string };
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Prove the credentials work before reporting success.
|
|
231
|
+
*
|
|
232
|
+
* Writing a credential file and saying nothing means a typo, a revoked key or
|
|
233
|
+
* a key missing `policies:pull` surfaces much later — as a policy that
|
|
234
|
+
* mysteriously never arrives, which is the worst moment to discover it. So
|
|
235
|
+
* enrolment makes exactly the request the daemon will make and reports what
|
|
236
|
+
* came back.
|
|
237
|
+
*/
|
|
238
|
+
export async function verifyCloudCredentials(creds: CloudCredentials): Promise<VerifyResult> {
|
|
239
|
+
// The label rides the enrolment request so the server can record a
|
|
240
|
+
// human-facing name alongside the id. An older server simply ignores the
|
|
241
|
+
// extra query param, so sending it is always safe.
|
|
242
|
+
const labelParam = creds.machineLabel
|
|
243
|
+
? `&label=${encodeURIComponent(creds.machineLabel)}`
|
|
244
|
+
: "";
|
|
245
|
+
const url = `${creds.url}/enforcement/v1/desired-state?machineId=${encodeURIComponent(creds.machineId)}${labelParam}`;
|
|
246
|
+
let response: Response;
|
|
247
|
+
try {
|
|
248
|
+
response = await fetchWithTimeout(
|
|
249
|
+
url,
|
|
250
|
+
{ headers: { Authorization: `Bearer ${creds.token}` } },
|
|
251
|
+
VERIFY_TIMEOUT_MS,
|
|
252
|
+
);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (isAbortError(err)) return { ok: false, reason: `No response from ${creds.url} within ${VERIFY_TIMEOUT_MS / 1000}s.` };
|
|
255
|
+
return { ok: false, reason: `Could not reach ${creds.url}: ${err instanceof Error ? err.message : String(err)}` };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (response.status === 401) {
|
|
259
|
+
return { ok: false, reason: "The server rejected this token (401). Check the key was copied whole." };
|
|
260
|
+
}
|
|
261
|
+
if (response.status === 403) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
reason: "This key is valid but lacks the `policies:pull` permission (403). Create a machine key with that permission.",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
return { ok: false, reason: `The server answered ${response.status} ${response.statusText}.` };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const body = (await response.json()) as { policies?: unknown[]; deployment?: number };
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
policyCount: Array.isArray(body.policies) ? body.policies.length : 0,
|
|
276
|
+
deployment: typeof body.deployment === "number" ? body.deployment : 0,
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
reason: `${creds.url} answered, but not with a desired-state document. Is this a Failproof Cloud URL?`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /v1/auth/introspect` — asking the server what a key actually is.
|
|
3
|
+
*
|
|
4
|
+
* Before this endpoint existed a machine could not describe its own
|
|
5
|
+
* credential: every other `/keys*` route needs a `keys:*` permission that a
|
|
6
|
+
* machine credential must never hold. So a revoked key, a valid key missing
|
|
7
|
+
* one permission, and a valid key pointed at the WRONG ORG all failed
|
|
8
|
+
* identically — at the point of use, later, as an empty dashboard.
|
|
9
|
+
*
|
|
10
|
+
* Introspect answers all three at connect time, in one request:
|
|
11
|
+
*
|
|
12
|
+
* is the key accepted at all? 401 vs 200
|
|
13
|
+
* which org will this machine report into? org_slug / org_name
|
|
14
|
+
* can it do the job it was provisioned for? permissions
|
|
15
|
+
*
|
|
16
|
+
* ## Effective, not granted — the subtle part
|
|
17
|
+
*
|
|
18
|
+
* Permissions imply other permissions, and the server widens a grant at
|
|
19
|
+
* authentication time (`expand_implied`): `alerts:read` also carries
|
|
20
|
+
* `issues:read`, and so on. Authorization is checked against the WIDENED set.
|
|
21
|
+
*
|
|
22
|
+
* The dashboard's key page and the CLI both display the *stored* grant, so both
|
|
23
|
+
* understate what a key can do. Introspect returns what the server actually
|
|
24
|
+
* enforces — which is precisely why a local permission check is only correct
|
|
25
|
+
* when it is built against this response and not against the displayed list.
|
|
26
|
+
*
|
|
27
|
+
* ## Do not poll this
|
|
28
|
+
*
|
|
29
|
+
* `/v1` has no rate limiting and failed auth is not negatively cached: one bad
|
|
30
|
+
* token is one database lookup per request. This runs ONCE, when a machine is
|
|
31
|
+
* provisioned. Revocation and expiry are cached server-side for up to a minute,
|
|
32
|
+
* so treat a positive answer as current to within a minute — never to the
|
|
33
|
+
* second.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The path introspect is served on, appended to the cloud origin. */
|
|
37
|
+
export const INTROSPECT_PATH = "/v1/auth/introspect";
|
|
38
|
+
|
|
39
|
+
export interface KeyIdentity {
|
|
40
|
+
keyId?: string;
|
|
41
|
+
orgId?: string;
|
|
42
|
+
orgSlug?: string;
|
|
43
|
+
orgName?: string;
|
|
44
|
+
/** The EFFECTIVE permission set — what the server enforces. */
|
|
45
|
+
permissions: string[];
|
|
46
|
+
expiresAt?: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type IntrospectResult =
|
|
50
|
+
| { kind: "ok"; identity: KeyIdentity }
|
|
51
|
+
/** The key was refused. Nothing more is knowable — the body carries no detail. */
|
|
52
|
+
| { kind: "rejected" }
|
|
53
|
+
/**
|
|
54
|
+
* This server has no introspect endpoint, i.e. it predates the endpoint.
|
|
55
|
+
* The caller must fall back to probing rather than treating it as a bad key:
|
|
56
|
+
* the CLI ships independently of whatever AgentEye a customer runs, so a
|
|
57
|
+
* perfectly good key on an older server must still connect.
|
|
58
|
+
*/
|
|
59
|
+
| { kind: "unsupported" }
|
|
60
|
+
/** Network/DNS/TLS — indistinguishable from a typo'd host, so reported as-is. */
|
|
61
|
+
| { kind: "unreachable"; reason: string };
|
|
62
|
+
|
|
63
|
+
export function introspectUrlFor(origin: string): string {
|
|
64
|
+
return `${origin.replace(/\/$/, "")}${INTROSPECT_PATH}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function introspectKey(
|
|
68
|
+
origin: string,
|
|
69
|
+
token: string,
|
|
70
|
+
fetchImpl: typeof fetch = fetch,
|
|
71
|
+
timeoutMs = 10_000,
|
|
72
|
+
): Promise<IntrospectResult> {
|
|
73
|
+
// AbortController rather than the shared fetchWithTimeout helper: this needs
|
|
74
|
+
// an injectable `fetch` so tests never reach the network, and that helper
|
|
75
|
+
// closes over the global one.
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
+
let res: Response;
|
|
79
|
+
try {
|
|
80
|
+
res = await fetchImpl(introspectUrlFor(origin), {
|
|
81
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
82
|
+
// Never follow a redirect. The dashboard app answers unrouted paths and
|
|
83
|
+
// will happily 307 to a login page that then returns 200 — which would
|
|
84
|
+
// read as a valid key against an endpoint that authenticated nothing.
|
|
85
|
+
redirect: "manual",
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
// Deliberately does not echo the URL: setup is routinely run while
|
|
91
|
+
// screen-sharing, and the host may be one the user would rather not show.
|
|
92
|
+
return { kind: "unreachable", reason: message };
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (res.status === 401 || res.status === 403) return { kind: "rejected" };
|
|
98
|
+
// 404 is "this server has no such route"; a redirect means we reached the web
|
|
99
|
+
// app rather than the API, which on an older deployment means the same thing.
|
|
100
|
+
if (res.status === 404 || (res.status >= 300 && res.status < 400)) {
|
|
101
|
+
return { kind: "unsupported" };
|
|
102
|
+
}
|
|
103
|
+
if (!res.ok) return { kind: "unreachable", reason: `the server answered ${res.status}` };
|
|
104
|
+
|
|
105
|
+
let body: unknown;
|
|
106
|
+
try {
|
|
107
|
+
body = await res.json();
|
|
108
|
+
} catch {
|
|
109
|
+
// A 200 that is not JSON is a proxy or a static host, not introspect.
|
|
110
|
+
return { kind: "unsupported" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const b = body as Record<string, unknown>;
|
|
114
|
+
// `valid: false` is not documented as a 200 shape, but treating a negative
|
|
115
|
+
// assertion as success would be the worst possible reading of it.
|
|
116
|
+
if (b.valid === false) return { kind: "rejected" };
|
|
117
|
+
if (!Array.isArray(b.permissions)) {
|
|
118
|
+
// Shaped like something else entirely — an unrelated 200 from a proxy.
|
|
119
|
+
return { kind: "unsupported" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
kind: "ok",
|
|
124
|
+
identity: {
|
|
125
|
+
keyId: typeof b.key_id === "string" ? b.key_id : undefined,
|
|
126
|
+
orgId: typeof b.org_id === "string" ? b.org_id : undefined,
|
|
127
|
+
orgSlug: typeof b.org_slug === "string" ? b.org_slug : undefined,
|
|
128
|
+
orgName: typeof b.org_name === "string" ? b.org_name : undefined,
|
|
129
|
+
permissions: b.permissions.filter((p): p is string => typeof p === "string"),
|
|
130
|
+
expiresAt: typeof b.expires_at === "string" ? b.expires_at : null,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The permission each capability needs. Named so the messages can quote them. */
|
|
136
|
+
export const PERMISSION_EVENTS = "events:add";
|
|
137
|
+
export const PERMISSION_POLICIES = "policies:pull";
|
|
138
|
+
|
|
139
|
+
export function hasPermission(identity: KeyIdentity, permission: string): boolean {
|
|
140
|
+
return identity.permissions.includes(permission);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** "Acme Inc (acme)", or the slug/id alone when the server sent less. */
|
|
144
|
+
export function describeOrg(identity: KeyIdentity): string {
|
|
145
|
+
if (identity.orgName && identity.orgSlug) return `${identity.orgName} (${identity.orgSlug})`;
|
|
146
|
+
return identity.orgName ?? identity.orgSlug ?? identity.orgId ?? "an unknown organisation";
|
|
147
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the daemon-owned active cloud policy deployment for the TypeScript
|
|
3
|
+
* evaluator. The Rust reconciler is responsible for downloading and repairing
|
|
4
|
+
* artifacts; this boundary independently verifies the digest immediately
|
|
5
|
+
* before import so the worker never knowingly executes modified bytes.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { existsSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
9
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
10
|
+
import { cloudPoliciesDir } from "./fp-home";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Active-manifest schema versions this reader accepts.
|
|
14
|
+
*
|
|
15
|
+
* MUST stay in step with `SUPPORTED_SCHEMA_VERSIONS` in `cloud_policies.rs`,
|
|
16
|
+
* which is what WRITES the file this parses. They are two hand-maintained
|
|
17
|
+
* copies of one contract in two languages, and the failure is silent in the
|
|
18
|
+
* worst direction: the daemon reconciles happily, `active.json` is correct on
|
|
19
|
+
* disk, and only the hook path refuses it — so cloud policy stops being
|
|
20
|
+
* enforced while every other signal says the machine is healthy. Reproduced
|
|
21
|
+
* exactly that way while syncing this with AgentEye#559.
|
|
22
|
+
*
|
|
23
|
+
* 1 is accepted for files a pre-rename beta daemon left behind; 2 is what is
|
|
24
|
+
* written now.
|
|
25
|
+
*/
|
|
26
|
+
const ACCEPTED_ACTIVE_SCHEMA_VERSIONS: readonly number[] = [1, 2];
|
|
27
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
28
|
+
const POLICY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
29
|
+
|
|
30
|
+
/** What an assignment may do. See PolicyEffect in cloud_policies.rs. */
|
|
31
|
+
export type PolicyEffect = "enforce" | "observe";
|
|
32
|
+
|
|
33
|
+
export interface CloudManagedPolicyArtifact {
|
|
34
|
+
id: string;
|
|
35
|
+
version: number;
|
|
36
|
+
/**
|
|
37
|
+
* `observe` policies are evaluated exactly like any other and then have their
|
|
38
|
+
* verdict discarded, so a rollout can be measured against real traffic before
|
|
39
|
+
* it can break anyone's work. Absent means `enforce`: the default has to be
|
|
40
|
+
* the one that keeps enforcing, or a manifest written before observe mode
|
|
41
|
+
* would silently downgrade a machine to observation.
|
|
42
|
+
*/
|
|
43
|
+
effect: PolicyEffect;
|
|
44
|
+
sha256: string;
|
|
45
|
+
path: string;
|
|
46
|
+
deployment: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ActiveManifest {
|
|
50
|
+
schemaVersion: number;
|
|
51
|
+
deployment: number;
|
|
52
|
+
policies: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
version: number;
|
|
55
|
+
effect?: string;
|
|
56
|
+
sha256: string;
|
|
57
|
+
path: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function cloudManagedPolicyRoot(): string {
|
|
62
|
+
return (
|
|
63
|
+
process.env.FAILPROOFAI_CLOUD_POLICY_DIR ??
|
|
64
|
+
cloudPoliciesDir()
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A version-1 manifest, in the spelling a pre-rename daemon actually wrote.
|
|
70
|
+
*
|
|
71
|
+
* Accepting schema 1 while reading only the version-2 FIELD NAMES made the
|
|
72
|
+
* acceptance unreachable: every genuine v1 file threw "active manifest
|
|
73
|
+
* deployment is invalid". The Rust reader of this same file handles it with
|
|
74
|
+
* `#[serde(alias = "generation")]` / `#[serde(alias = "revision")]`; this is the
|
|
75
|
+
* TypeScript half of that pair, and it was missing.
|
|
76
|
+
*/
|
|
77
|
+
interface LegacyActiveManifest {
|
|
78
|
+
generation?: number;
|
|
79
|
+
policies?: Array<{ revision?: number }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseManifest(value: unknown): ActiveManifest {
|
|
83
|
+
if (!value || typeof value !== "object") throw new Error("active manifest is not an object");
|
|
84
|
+
const raw = value as Partial<ActiveManifest> & LegacyActiveManifest;
|
|
85
|
+
if (!ACCEPTED_ACTIVE_SCHEMA_VERSIONS.includes(raw.schemaVersion as number)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`unsupported active manifest schema ${String(raw.schemaVersion)} ` +
|
|
88
|
+
`(supported: ${ACCEPTED_ACTIVE_SCHEMA_VERSIONS.join(", ")})`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// New name wins; the old one is a fallback, not an equal. A file carrying both
|
|
92
|
+
// (written by a mixed-version machine) must resolve to the current field.
|
|
93
|
+
const deployment = raw.deployment ?? raw.generation;
|
|
94
|
+
if (!Number.isSafeInteger(deployment) || (deployment ?? -1) < 0) {
|
|
95
|
+
throw new Error("active manifest deployment is invalid");
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(raw.policies)) throw new Error("active manifest policies is not an array");
|
|
98
|
+
const policies = raw.policies.map((p) => {
|
|
99
|
+
const entry = p as ActiveManifest["policies"][number] & { revision?: number };
|
|
100
|
+
return { ...entry, version: entry.version ?? entry.revision } as ActiveManifest["policies"][number];
|
|
101
|
+
});
|
|
102
|
+
return { schemaVersion: raw.schemaVersion as number, deployment: deployment as number, policies };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveManagedPath(root: string, candidate: string): string {
|
|
106
|
+
if (!candidate || isAbsolute(candidate)) throw new Error(`unsafe managed policy path ${JSON.stringify(candidate)}`);
|
|
107
|
+
const absolute = resolve(root, candidate);
|
|
108
|
+
const lexicalRelative = relative(root, absolute);
|
|
109
|
+
if (lexicalRelative.startsWith("..") || isAbsolute(lexicalRelative)) {
|
|
110
|
+
throw new Error(`managed policy path escapes its root: ${JSON.stringify(candidate)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Reject a symlinked artifact or deployment directory escaping the managed
|
|
114
|
+
// root. The current daemon is same-user, but following an arbitrary path is
|
|
115
|
+
// still unnecessary ambient authority and becomes dangerous if its service
|
|
116
|
+
// identity is hardened later.
|
|
117
|
+
const realRoot = realpathSync(root);
|
|
118
|
+
const realFile = realpathSync(absolute);
|
|
119
|
+
const physicalRelative = relative(realRoot, realFile);
|
|
120
|
+
if (physicalRelative.startsWith("..") || isAbsolute(physicalRelative)) {
|
|
121
|
+
throw new Error(`managed policy symlink escapes its root: ${JSON.stringify(candidate)}`);
|
|
122
|
+
}
|
|
123
|
+
return realFile;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Stop enforcing cloud-managed policies on this machine.
|
|
128
|
+
*
|
|
129
|
+
* Removes `active.json` only — the manifest that says which deployment is
|
|
130
|
+
* live. The deployment directories and content-addressed artifacts are left
|
|
131
|
+
* alone: they are large, hash-verified on use, and inert once nothing points
|
|
132
|
+
* at them, so keeping them makes a reconnect cheap and offline-safe.
|
|
133
|
+
*
|
|
134
|
+
* Called by `--disconnect`, which without it did not disconnect. Clearing the
|
|
135
|
+
* credential stops the daemon REFRESHING policy; every artifact already on disk
|
|
136
|
+
* kept being loaded and enforced on every tool call, so a user who had
|
|
137
|
+
* deliberately left their organisation's cloud went on being governed by
|
|
138
|
+
* whatever deployment happened to be current when they left — indefinitely,
|
|
139
|
+
* with `--status` reporting the machine as unconnected.
|
|
140
|
+
*
|
|
141
|
+
* Returns true when a manifest was actually removed.
|
|
142
|
+
*/
|
|
143
|
+
export function clearActiveCloudManagedPolicies(): boolean {
|
|
144
|
+
const activePath = resolve(cloudManagedPolicyRoot(), "active.json");
|
|
145
|
+
if (!existsSync(activePath)) return false;
|
|
146
|
+
try {
|
|
147
|
+
rmSync(activePath, { force: true });
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
// Best-effort: `--disconnect` still clears the credentials, and reporting
|
|
151
|
+
// a failure to remove one file would obscure that the rest succeeded.
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function readActiveCloudManagedPolicies(): CloudManagedPolicyArtifact[] {
|
|
157
|
+
const root = cloudManagedPolicyRoot();
|
|
158
|
+
const activePath = resolve(root, "active.json");
|
|
159
|
+
if (!existsSync(activePath)) return [];
|
|
160
|
+
|
|
161
|
+
let raw: unknown;
|
|
162
|
+
try {
|
|
163
|
+
raw = JSON.parse(readFileSync(activePath, "utf8"));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new Error(`failed to read cloud-managed active manifest: ${err instanceof Error ? err.message : String(err)}`);
|
|
166
|
+
}
|
|
167
|
+
const manifest = parseManifest(raw);
|
|
168
|
+
const seen = new Set<string>();
|
|
169
|
+
|
|
170
|
+
return manifest.policies.map((policy) => {
|
|
171
|
+
if (!POLICY_ID_RE.test(policy.id) || policy.id === "." || policy.id === "..") {
|
|
172
|
+
throw new Error(`unsafe cloud-managed policy id ${JSON.stringify(policy.id)}`);
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isSafeInteger(policy.version) || policy.version < 0) {
|
|
175
|
+
throw new Error(`invalid version for cloud-managed policy ${policy.id}`);
|
|
176
|
+
}
|
|
177
|
+
if (!SHA256_RE.test(policy.sha256)) {
|
|
178
|
+
throw new Error(`invalid SHA-256 for cloud-managed policy ${policy.id}`);
|
|
179
|
+
}
|
|
180
|
+
if (seen.has(policy.id)) throw new Error(`duplicate cloud-managed policy id ${policy.id}`);
|
|
181
|
+
seen.add(policy.id);
|
|
182
|
+
|
|
183
|
+
const path = resolveManagedPath(root, policy.path);
|
|
184
|
+
const bytes = readFileSync(path);
|
|
185
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
186
|
+
if (actual !== policy.sha256) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`cloud-managed policy ${policy.id} failed integrity verification: expected ${policy.sha256}, got ${actual}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (policy.effect !== undefined && policy.effect !== "enforce" && policy.effect !== "observe") {
|
|
192
|
+
// Guessing would mean either enforcing something cloud did not ask to
|
|
193
|
+
// enforce, or observing something it wanted enforced. Both are worse than
|
|
194
|
+
// refusing the deployment.
|
|
195
|
+
throw new Error(`unknown effect ${JSON.stringify(policy.effect)} for cloud-managed policy ${policy.id}`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
id: policy.id,
|
|
199
|
+
version: policy.version,
|
|
200
|
+
effect: (policy.effect as PolicyEffect | undefined) ?? "enforce",
|
|
201
|
+
sha256: policy.sha256,
|
|
202
|
+
path,
|
|
203
|
+
deployment: manifest.deployment,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|