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,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `VERSION`, `config.json` and `credentials.json` — the three files a human
|
|
3
|
+
* might actually open in `~/.failproofai`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the split
|
|
6
|
+
*
|
|
7
|
+
* `config.json` inherits the umask and lands at 0664 on a normal machine.
|
|
8
|
+
* `credentials.json` is written 0600 and holds every token. That boundary is
|
|
9
|
+
* not tidiness: it is why `ingest.json` and `cloud.json` were separate files
|
|
10
|
+
* before this, and it means CI, tooling and a screen-shared `cat config.json`
|
|
11
|
+
* can read a machine's configuration without ever seeing a live credential.
|
|
12
|
+
*
|
|
13
|
+
* ## One format, not two
|
|
14
|
+
*
|
|
15
|
+
* These were TOML through layout 2, for the comments — the file explained what
|
|
16
|
+
* `oss` meant and why `daemon.configured` must never be hand-set. Layout 3
|
|
17
|
+
* moved them to JSON so the home speaks one format end to end: one parser, one
|
|
18
|
+
* escaping rule, and no `toml` dependency in either the CLI or the daemon for
|
|
19
|
+
* files that were always flat key/value.
|
|
20
|
+
*
|
|
21
|
+
* The comments are the cost, and they were not decoration — this is the file
|
|
22
|
+
* someone opens when something is wrong. That guidance now lives in
|
|
23
|
+
* `failproofai config --status` and the docs, which is a worse place for it.
|
|
24
|
+
*/
|
|
25
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { dirname } from "node:path";
|
|
27
|
+
import { version as cliVersion } from "../../package.json";
|
|
28
|
+
import {
|
|
29
|
+
LAYOUT_VERSION,
|
|
30
|
+
configFile,
|
|
31
|
+
credentialsFile,
|
|
32
|
+
failproofaiHome,
|
|
33
|
+
legacy,
|
|
34
|
+
versionFile,
|
|
35
|
+
} from "./fp-home";
|
|
36
|
+
|
|
37
|
+
// ── VERSION ──────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface VersionFile {
|
|
40
|
+
/** The on-disk layout this home was written by. */
|
|
41
|
+
layout: number;
|
|
42
|
+
/** The CLI version that last wrote it — replaces the old `last-version`. */
|
|
43
|
+
cli: string;
|
|
44
|
+
/** The daemon binary version installed, if any. */
|
|
45
|
+
daemon?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type LayoutState =
|
|
49
|
+
/** No home yet, or an empty one. A fresh install. */
|
|
50
|
+
| { kind: "absent" }
|
|
51
|
+
/** A home written by this layout. `inferred` means the layout was recovered
|
|
52
|
+
* from a landmark because `VERSION` was missing or unreadable — the caller
|
|
53
|
+
* should write the marker back. */
|
|
54
|
+
| { kind: "current"; version: VersionFile; inferred?: boolean }
|
|
55
|
+
/** A home from an older layout — must be reset, never half-read. */
|
|
56
|
+
| { kind: "stale"; found: number }
|
|
57
|
+
/** A home from a NEWER layout: a downgrade. Refuse rather than corrupt it. */
|
|
58
|
+
| { kind: "future"; found: number };
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What layout the home on disk speaks.
|
|
62
|
+
*
|
|
63
|
+
* The `VERSION` file is the primary signal; its absence is only treated as
|
|
64
|
+
* "layout 1" when the home actually contains something, because an empty or
|
|
65
|
+
* missing directory is a fresh install and must not be reported as stale.
|
|
66
|
+
*
|
|
67
|
+
* A NEWER layout is called out separately from an older one. Both are
|
|
68
|
+
* unreadable, but only one of them is fixable by re-running setup — telling a
|
|
69
|
+
* user to reset a home written by a newer CLI would delete data that a simple
|
|
70
|
+
* upgrade would have read fine.
|
|
71
|
+
*/
|
|
72
|
+
export function detectLayout(): LayoutState {
|
|
73
|
+
const raw = readVersionFile();
|
|
74
|
+
if (raw) {
|
|
75
|
+
if (raw.layout === LAYOUT_VERSION) return { kind: "current", version: raw };
|
|
76
|
+
return raw.layout > LAYOUT_VERSION
|
|
77
|
+
? { kind: "future", found: raw.layout }
|
|
78
|
+
: { kind: "stale", found: raw.layout };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// No VERSION file — fall back to landmarks.
|
|
82
|
+
//
|
|
83
|
+
// ORDER MATTERS, because layout 3 collides with layout 1 on one of them.
|
|
84
|
+
// Layout 3 put `policies-config.json` back at the home root, which is exactly
|
|
85
|
+
// where layout 1 kept it. A layout-3 home that lost its VERSION would
|
|
86
|
+
// therefore match a layout-1 landmark and be reported as layout 1 — and
|
|
87
|
+
// `resetHome(1)` runs layout-1 MIGRATIONS, which move `policies/*.mjs` on the
|
|
88
|
+
// assumption they are in layout 1's positions. In layout 3 those are the
|
|
89
|
+
// user's own convention policies, sitting where they belong.
|
|
90
|
+
//
|
|
91
|
+
// So check for a marker only the newer layouts have first — and DISTINGUISH
|
|
92
|
+
// them, because the two answers are not equally safe.
|
|
93
|
+
//
|
|
94
|
+
// `config.json` is layout 3's OWN file. Layout 2 wrote `config.toml`, layout 1
|
|
95
|
+
// had neither, and nothing but setup writes either one. So its presence does
|
|
96
|
+
// not merely prove "newer than layout 1" — it identifies THIS layout, and the
|
|
97
|
+
// only thing actually missing is the marker. Reporting it as stale instead ran
|
|
98
|
+
// a destructive reset over a healthy machine: `resettablePaths()` lists
|
|
99
|
+
// `configFile()` and `credentialsFile()`, so the home lost its live cloud
|
|
100
|
+
// token AND `daemon.configured` — which is what makes a machine fail closed.
|
|
101
|
+
// A machine that dropped out of fail-closed enforcement because a marker file
|
|
102
|
+
// went missing is the exact failure this module exists to prevent, and it
|
|
103
|
+
// announced itself as a routine "reorganised your home" message.
|
|
104
|
+
if (existsSync(configFile())) {
|
|
105
|
+
// `inferred`: the layout is right but the MARKER is missing, and nothing
|
|
106
|
+
// else rewrites it — so every later command re-derives it from a landmark,
|
|
107
|
+
// and the daemon version recorded in that file is gone for good
|
|
108
|
+
// (`daemonVersionSkew()` reads it, and stops nudging about a stale daemon).
|
|
109
|
+
// The caller re-stamps, exactly as it already does for a fresh home.
|
|
110
|
+
return { kind: "current", version: { layout: LAYOUT_VERSION, cli: "" }, inferred: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// `config.toml` and no `config.json` is genuinely layout 2, and a reset is
|
|
114
|
+
// right: its files are the ones being replaced.
|
|
115
|
+
if (existsSync(legacy.configToml())) return { kind: "stale", found: LAYOUT_VERSION - 1 };
|
|
116
|
+
|
|
117
|
+
// Layout 1 if any of its landmarks are present, otherwise this is simply a
|
|
118
|
+
// home that has not been set up yet.
|
|
119
|
+
const layoutOneMarkers = [
|
|
120
|
+
legacy.policyConfig(),
|
|
121
|
+
legacy.cacheDir(),
|
|
122
|
+
legacy.launcherMarker(),
|
|
123
|
+
legacy.lastVersion(),
|
|
124
|
+
legacy.cloudCredentials(),
|
|
125
|
+
legacy.ingestCredentials(),
|
|
126
|
+
legacy.auditDashboard(),
|
|
127
|
+
];
|
|
128
|
+
return layoutOneMarkers.some((p) => existsSync(p)) ? { kind: "stale", found: 1 } : { kind: "absent" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read a layout-2 `VERSION`, which is TOML.
|
|
133
|
+
*
|
|
134
|
+
* Layout 3 made this file JSON, so `JSON.parse` throws on every layout-2 home —
|
|
135
|
+
* and "unreadable" is NOT a harmless answer here. It sends `detectLayout()` to
|
|
136
|
+
* the landmarks, and a layout-2 home whose owner never completed setup has no
|
|
137
|
+
* `config.toml` to match: it falls through every layout-1 marker too (layout 2
|
|
138
|
+
* nested `policies-config.json`, so the root file layout 1 is recognised by is
|
|
139
|
+
* absent) and reads as `absent` — a FRESH INSTALL. The CLI then stamps it
|
|
140
|
+
* layout 3 with no reset and no migration, and
|
|
141
|
+
* `policies/local-policies/policies-config.json` plus
|
|
142
|
+
* `policies/custom-policies/*.mjs` are orphaned permanently: the user's enabled
|
|
143
|
+
* policies revert to the defaults, their own policy files stop loading, and
|
|
144
|
+
* nothing says so. Reproduced exactly that way on a seeded home.
|
|
145
|
+
*
|
|
146
|
+
* A regex rather than a TOML parser because removing that dependency is half
|
|
147
|
+
* the point of layout 3, and this file is three flat `key = value` lines that
|
|
148
|
+
* this codebase itself wrote.
|
|
149
|
+
*
|
|
150
|
+
* ALL THREE fields are read, including `daemon`. It is tempting to take only the
|
|
151
|
+
* layout number on the grounds that the home is about to be reset — but
|
|
152
|
+
* `writeVersionFile()` carries the daemon version forward from `existing?.daemon`,
|
|
153
|
+
* so a field missing here is not re-derived, it is ERASED. The machine then
|
|
154
|
+
* reports "daemon not installed" with the binary sitting in `bin/`, and
|
|
155
|
+
* `daemonVersionSkew()` stops nudging about a stale daemon precisely when an
|
|
156
|
+
* upgrade has just happened.
|
|
157
|
+
*/
|
|
158
|
+
function readLegacyTomlVersion(text: string): VersionFile | null {
|
|
159
|
+
const layout = Number(/^\s*layout\s*=\s*(\d+)\s*$/m.exec(text)?.[1]);
|
|
160
|
+
if (!Number.isFinite(layout)) return null;
|
|
161
|
+
const str = (key: string) => new RegExp(`^\\s*${key}\\s*=\\s*"([^"]*)"`, "m").exec(text)?.[1];
|
|
162
|
+
return { layout, cli: str("cli") ?? "", daemon: str("daemon") };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function readVersionFile(): VersionFile | null {
|
|
166
|
+
let text: string;
|
|
167
|
+
try {
|
|
168
|
+
text = readFileSync(versionFile(), "utf8");
|
|
169
|
+
} catch {
|
|
170
|
+
// Absent or unreadable. The landmark check decides what that implies.
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
175
|
+
const layout = Number(parsed.layout);
|
|
176
|
+
if (!Number.isFinite(layout)) return null;
|
|
177
|
+
return {
|
|
178
|
+
layout,
|
|
179
|
+
cli: typeof parsed.cli === "string" ? parsed.cli : "",
|
|
180
|
+
daemon: typeof parsed.daemon === "string" ? parsed.daemon : undefined,
|
|
181
|
+
};
|
|
182
|
+
} catch {
|
|
183
|
+
// Not JSON. Before concluding "no layout", try the one other format this
|
|
184
|
+
// file has ever had — see `readLegacyTomlVersion` for what guessing wrong
|
|
185
|
+
// costs. Genuinely corrupt content still yields null from there.
|
|
186
|
+
return readLegacyTomlVersion(text);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function writeVersionFile(
|
|
191
|
+
v: Partial<VersionFile> & { /** Erase the daemon version rather than keeping it. */ clearDaemon?: boolean } = {},
|
|
192
|
+
): void {
|
|
193
|
+
const existing = readVersionFile();
|
|
194
|
+
const next: VersionFile = {
|
|
195
|
+
layout: LAYOUT_VERSION,
|
|
196
|
+
cli: v.cli ?? cliVersion,
|
|
197
|
+
// `undefined` means "leave whatever is there" — a CLI-only rewrite must not
|
|
198
|
+
// drop a daemon version it never touched. Erasing it is therefore an
|
|
199
|
+
// explicit act, used on uninstall.
|
|
200
|
+
daemon: v.clearDaemon ? undefined : (v.daemon ?? existing?.daemon),
|
|
201
|
+
};
|
|
202
|
+
writeFileAt(
|
|
203
|
+
versionFile(),
|
|
204
|
+
`${JSON.stringify(
|
|
205
|
+
{ layout: next.layout, cli: next.cli, ...(next.daemon ? { daemon: next.daemon } : {}) },
|
|
206
|
+
null,
|
|
207
|
+
2,
|
|
208
|
+
)}\n`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── config.json ──────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* How this machine relates to Failproof Cloud.
|
|
216
|
+
*
|
|
217
|
+
* `oss` is not merely "no credentials configured" — it is a hard gate. In OSS
|
|
218
|
+
* mode nothing constructs a cloud URL, polls for policy, or spools an event
|
|
219
|
+
* for upload. A machine that never opted in must be provably silent, not
|
|
220
|
+
* silent-by-happenstance because a token lookup returned undefined.
|
|
221
|
+
*/
|
|
222
|
+
export type Mode = "oss" | "cloud";
|
|
223
|
+
|
|
224
|
+
export interface FpConfig {
|
|
225
|
+
mode: Mode;
|
|
226
|
+
daemon: {
|
|
227
|
+
/**
|
|
228
|
+
* Route hooks through the daemon — and FAIL CLOSED when it is unreachable.
|
|
229
|
+
*
|
|
230
|
+
* The installed binary VERSION deliberately does not live here; it is in
|
|
231
|
+
* `VERSION`, which is the file about versions. Two copies could disagree,
|
|
232
|
+
* and a stale one would misreport which daemon a machine is running.
|
|
233
|
+
*/
|
|
234
|
+
configured: boolean;
|
|
235
|
+
};
|
|
236
|
+
collector: {
|
|
237
|
+
sessions: boolean;
|
|
238
|
+
hooks: boolean;
|
|
239
|
+
hooksVerbosity: "all" | "decisions" | "off";
|
|
240
|
+
redact: "minimal" | "off";
|
|
241
|
+
environment: string;
|
|
242
|
+
machineId?: string;
|
|
243
|
+
/**
|
|
244
|
+
* Extra locations to capture per harness, beyond the one each source ships
|
|
245
|
+
* with — `collector.sources.<harness>.extra_paths` in `config.json`.
|
|
246
|
+
*
|
|
247
|
+
* Each entry is `label=path` or a bare `path`; the label becomes an agent-id
|
|
248
|
+
* namespace (`<label>-<agentId>`), which is what keeps two copies of one
|
|
249
|
+
* project from merging into a single agent. The daemon is the authority on
|
|
250
|
+
* the grammar and on which harness names are real — see
|
|
251
|
+
* `crates/fpai-collect/src/extra_paths.rs` and `HARNESS_KEYS` in
|
|
252
|
+
* `crates/failproofaid/src/main.rs`. This side stores and edits the strings;
|
|
253
|
+
* it deliberately does not re-implement the parser, because two parsers is
|
|
254
|
+
* how the CLI comes to accept a path the daemon then silently drops.
|
|
255
|
+
*
|
|
256
|
+
* Absent for every machine that has not configured one, which is almost all
|
|
257
|
+
* of them — so `writeConfig` emits nothing at all in that case and a config
|
|
258
|
+
* file is byte-identical to what this version wrote before the field
|
|
259
|
+
* existed.
|
|
260
|
+
*/
|
|
261
|
+
sources?: Record<string, { extraPaths: string[] }>;
|
|
262
|
+
};
|
|
263
|
+
telemetry: {
|
|
264
|
+
/**
|
|
265
|
+
* Anonymous product telemetry. On unless switched off.
|
|
266
|
+
*
|
|
267
|
+
* This is the ONLY off-switch that can reach the daemon.
|
|
268
|
+
* `FAILPROOFAI_TELEMETRY_DISABLED=1` is read from `process.env`, and
|
|
269
|
+
* failproofaid runs as a system-scope service whose environment carries
|
|
270
|
+
* essentially nothing — so a shell export is structurally incapable of
|
|
271
|
+
* turning it off there. A file both sides read is the only thing that works.
|
|
272
|
+
*/
|
|
273
|
+
enabled: boolean;
|
|
274
|
+
};
|
|
275
|
+
audit: {
|
|
276
|
+
/**
|
|
277
|
+
* Scan this machine's agent history on a schedule.
|
|
278
|
+
*
|
|
279
|
+
* OFF by default, and the asymmetry with `telemetry.enabled` above is
|
|
280
|
+
* deliberate: the audit reads the CONTENTS of every session transcript on
|
|
281
|
+
* disk — prompts, file contents, command output — so nothing scans on a
|
|
282
|
+
* timer until somebody asks for it.
|
|
283
|
+
*/
|
|
284
|
+
auto: boolean;
|
|
285
|
+
/** Days between scheduled runs. Wall clock, so it survives suspend. */
|
|
286
|
+
intervalDays: number;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Days between scheduled audits when nothing says otherwise, and the bounds a
|
|
292
|
+
* hand-written value is held to.
|
|
293
|
+
*
|
|
294
|
+
* Both ends fail quietly, which is why they are clamped at all. A measured full
|
|
295
|
+
* scan is ~104 seconds over 3,277 transcripts and grows with history, so an
|
|
296
|
+
* interval under a day spends minutes of disk every day re-reading transcripts
|
|
297
|
+
* that have barely changed. At the other end a typo'd `interval_days = 3650`
|
|
298
|
+
* reads as "never", which on a status line is indistinguishable from the
|
|
299
|
+
* feature being broken — and "never" already has its own switch, `auto = false`.
|
|
300
|
+
*/
|
|
301
|
+
export const DEFAULT_AUDIT_INTERVAL_DAYS = 7;
|
|
302
|
+
const MIN_AUDIT_INTERVAL_DAYS = 1;
|
|
303
|
+
const MAX_AUDIT_INTERVAL_DAYS = 90;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Resolve `interval_days`, refusing anything that is not a whole number of days.
|
|
307
|
+
*
|
|
308
|
+
* 0, a negative, a fraction below a day, and any non-number all resolve to the
|
|
309
|
+
* DEFAULT rather than clamping up to the 1-day floor. A `0` almost certainly
|
|
310
|
+
* means "off", and reading it as a DAILY 104-second scan of every transcript on
|
|
311
|
+
* the machine is the loudest possible way to get that wrong. A too-large value
|
|
312
|
+
* is clamped DOWN to 90 instead, because that is the conservative direction of
|
|
313
|
+
* the two available: falling back to 7 there would scan an order of magnitude
|
|
314
|
+
* more often than was asked for.
|
|
315
|
+
*/
|
|
316
|
+
function readIntervalDays(raw: unknown): number {
|
|
317
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return DEFAULT_AUDIT_INTERVAL_DAYS;
|
|
318
|
+
const days = Math.floor(raw);
|
|
319
|
+
if (days < MIN_AUDIT_INTERVAL_DAYS) return DEFAULT_AUDIT_INTERVAL_DAYS;
|
|
320
|
+
return Math.min(days, MAX_AUDIT_INTERVAL_DAYS);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const DEFAULT_CONFIG: FpConfig = {
|
|
324
|
+
mode: "oss",
|
|
325
|
+
daemon: { configured: false },
|
|
326
|
+
collector: {
|
|
327
|
+
sessions: false,
|
|
328
|
+
hooks: true,
|
|
329
|
+
hooksVerbosity: "decisions",
|
|
330
|
+
redact: "minimal",
|
|
331
|
+
environment: "local",
|
|
332
|
+
},
|
|
333
|
+
telemetry: { enabled: true },
|
|
334
|
+
audit: { auto: false, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* `[collector.sources.*]` → the in-memory shape, dropping anything unusable.
|
|
339
|
+
*
|
|
340
|
+
* Entries are kept as written rather than normalised. The daemon parses them
|
|
341
|
+
* (label derivation, overlap rejection, `~` expansion) and reports what it
|
|
342
|
+
* rejected at startup; normalising here would give the two sides two answers
|
|
343
|
+
* and hide the daemon's rejection behind a value the CLI had already rewritten.
|
|
344
|
+
* The only things dropped are shapes that could not survive a round trip at
|
|
345
|
+
* all — a non-array, or a non-string element.
|
|
346
|
+
*
|
|
347
|
+
* `undefined` rather than `{}` when nothing is configured, so `writeConfig`
|
|
348
|
+
* emits no table and an untouched config file keeps its exact previous bytes.
|
|
349
|
+
*/
|
|
350
|
+
function readSources(raw: unknown): FpConfig["collector"]["sources"] {
|
|
351
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
352
|
+
const out: Record<string, { extraPaths: string[] }> = {};
|
|
353
|
+
for (const [name, table] of Object.entries(raw as Record<string, unknown>)) {
|
|
354
|
+
if (!table || typeof table !== "object" || Array.isArray(table)) continue;
|
|
355
|
+
const entries = (table as Record<string, unknown>).extra_paths;
|
|
356
|
+
if (!Array.isArray(entries)) continue;
|
|
357
|
+
const paths = entries.filter((e): e is string => typeof e === "string" && e.trim() !== "");
|
|
358
|
+
if (paths.length > 0) out[name] = { extraPaths: paths };
|
|
359
|
+
}
|
|
360
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* {@link readConfig}, plus the raw object it was projected from.
|
|
365
|
+
*
|
|
366
|
+
* `readConfig` is a WHITELIST PROJECTION, not a merge: it names every key it
|
|
367
|
+
* understands and builds a fresh `FpConfig`. Paired with a `writeConfig` that
|
|
368
|
+
* regenerates the file wholesale, that means **any key this build does not know
|
|
369
|
+
* is erased on the next write** — and that is a data-loss bug with no layout
|
|
370
|
+
* change involved at all. Two CLI versions on the same layout round-trip the
|
|
371
|
+
* file and silently delete each other's keys, and most releases do not bump the
|
|
372
|
+
* layout, so `detectLayout()`'s `future` refusal never fires to protect it.
|
|
373
|
+
*
|
|
374
|
+
* `collector.sources` is the live example: added inside layout 3, so a CLI that
|
|
375
|
+
* predates it drops every extra path a user typed on the first `updateConfig()`
|
|
376
|
+
* call — from `harness add-path`, which is the one command whose entire output is
|
|
377
|
+
* that key.
|
|
378
|
+
*
|
|
379
|
+
* So every mutation path reads the raw object and hands it back to `writeConfig`,
|
|
380
|
+
* which preserves what it does not own. `readConfig` stays as it was for the many
|
|
381
|
+
* read-only callers that genuinely only want the projection.
|
|
382
|
+
*/
|
|
383
|
+
export function readConfigRaw(): { raw: Record<string, unknown>; config: FpConfig } {
|
|
384
|
+
let raw: Record<string, unknown> = {};
|
|
385
|
+
try {
|
|
386
|
+
const parsed = JSON.parse(readFileSync(configFile(), "utf8")) as unknown;
|
|
387
|
+
// An array or a scalar is not a config object, and spreading one into the
|
|
388
|
+
// merge below would produce index keys. Anything unusable reads as empty,
|
|
389
|
+
// which is what the projection already falls back to.
|
|
390
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
391
|
+
raw = parsed as Record<string, unknown>;
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
// Absent or unparseable. `readConfig` returns defaults for the same input;
|
|
395
|
+
// an empty raw means the write below carries nothing forward, which is the
|
|
396
|
+
// only honest answer when the previous bytes could not be read.
|
|
397
|
+
}
|
|
398
|
+
return { raw, config: readConfig() };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function readConfig(): FpConfig {
|
|
402
|
+
try {
|
|
403
|
+
const parsed = JSON.parse(readFileSync(configFile(), "utf8")) as Record<string, unknown>;
|
|
404
|
+
return projectConfig(parsed);
|
|
405
|
+
} catch {
|
|
406
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Project a parsed config object onto {@link FpConfig}.
|
|
412
|
+
*
|
|
413
|
+
* Split out of `readConfig` so the layout-2 → 3 carry can run the SAME
|
|
414
|
+
* projection over a `config.toml` it parsed itself, rather than a second reader
|
|
415
|
+
* that has to be kept in step with this one. The two formats use identical key
|
|
416
|
+
* names (`hooks_verbosity`, `machine_id`, `interval_days`,
|
|
417
|
+
* `sources.<h>.extra_paths`), so the only difference between them is how the
|
|
418
|
+
* bytes become an object — and once that is a parameter there is nothing left to
|
|
419
|
+
* drift. Every failure-direction decision below is therefore made once, for both.
|
|
420
|
+
*/
|
|
421
|
+
export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
422
|
+
{
|
|
423
|
+
const mode = (parsed.mode as Record<string, unknown> | undefined)?.kind;
|
|
424
|
+
const daemon = (parsed.daemon ?? {}) as Record<string, unknown>;
|
|
425
|
+
const collector = (parsed.collector ?? {}) as Record<string, unknown>;
|
|
426
|
+
const telemetry = (parsed.telemetry ?? {}) as Record<string, unknown>;
|
|
427
|
+
const audit = (parsed.audit ?? {}) as Record<string, unknown>;
|
|
428
|
+
return {
|
|
429
|
+
// Anything unrecognised reads as `oss`. The failure direction matters: a
|
|
430
|
+
// corrupt config must not be able to turn cloud reporting ON.
|
|
431
|
+
//
|
|
432
|
+
// This is a real check rather than `?? "oss"`, which only caught a MISSING
|
|
433
|
+
// mode and passed every present-but-unknown value straight through — so
|
|
434
|
+
// `"enterprise"`, or a typo like `"clod"`, became the machine's mode. The
|
|
435
|
+
// test named for this branch was writing a TOML body into a `.json` file,
|
|
436
|
+
// so the parse failed, the whole function fell to its defaults, and the
|
|
437
|
+
// assertion passed without the branch ever running.
|
|
438
|
+
mode: mode === "cloud" ? "cloud" : "oss",
|
|
439
|
+
daemon: { configured: daemon.configured === true },
|
|
440
|
+
collector: {
|
|
441
|
+
sessions: collector.sessions === true,
|
|
442
|
+
hooks: collector.hooks !== false,
|
|
443
|
+
hooksVerbosity:
|
|
444
|
+
collector.hooks_verbosity === "all" || collector.hooks_verbosity === "off"
|
|
445
|
+
? collector.hooks_verbosity
|
|
446
|
+
: "decisions",
|
|
447
|
+
redact: collector.redact === "off" ? "off" : "minimal",
|
|
448
|
+
environment:
|
|
449
|
+
typeof collector.environment === "string" ? collector.environment : "local",
|
|
450
|
+
machineId: typeof collector.machine_id === "string" ? collector.machine_id : undefined,
|
|
451
|
+
sources: readSources(collector.sources),
|
|
452
|
+
},
|
|
453
|
+
// Only an explicit `false` switches it off. Absent, or any other value,
|
|
454
|
+
// reads as on — the shipped default, and what a config with no
|
|
455
|
+
// [telemetry] block at all means.
|
|
456
|
+
telemetry: { enabled: telemetry.enabled !== false },
|
|
457
|
+
// Mirror image of telemetry above: only an explicit `true` switches the
|
|
458
|
+
// scheduled scan on. Absent, misspelled, or `"yes"` all read as off,
|
|
459
|
+
// because the failure direction here is a machine that starts reading
|
|
460
|
+
// every transcript it can find on a timer nobody set.
|
|
461
|
+
audit: { auto: audit.auto === true, intervalDays: readIntervalDays(audit.interval_days) },
|
|
462
|
+
// Same shape as `audit.auto` above and for the same reason: only an
|
|
463
|
+
// explicit `true` opts in. Anything else — absent, misspelled, `"yes"` —
|
|
464
|
+
// reads as off, because the failure direction is a machine that starts
|
|
465
|
+
// mailing a report nobody asked for to an org address it never sees.
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Every key `writeConfig` owns, as a path into the emitted JSON.
|
|
472
|
+
*
|
|
473
|
+
* Ownership is what makes preserving unknown keys safe in BOTH directions, and
|
|
474
|
+
* getting the direction wrong is a bug either way:
|
|
475
|
+
*
|
|
476
|
+
* - An **owned** key absent from the projection is a DELIBERATE OMISSION and
|
|
477
|
+
* must be deleted. `writeConfig` uses absence to express state — `telemetry`
|
|
478
|
+
* is emitted only when switched off, `machine_id` only when set, `sources`
|
|
479
|
+
* only when non-empty. Merging blindly over the old bytes would resurrect
|
|
480
|
+
* them, so switching telemetry back on would leave `enabled: false` in the
|
|
481
|
+
* file and the opt-out would become un-revokable.
|
|
482
|
+
* - An **unowned** key belongs to a build that knows more than this one and
|
|
483
|
+
* must survive. That is the bug this list exists to fix; see
|
|
484
|
+
* {@link readConfigRaw}.
|
|
485
|
+
*
|
|
486
|
+
* A `"*"` segment matches every key at that level. `collector.sources` is keyed
|
|
487
|
+
* by harness name, so its paths cannot be enumerated — and owning the subtree
|
|
488
|
+
* wholesale would delete a future per-harness sibling of `extra_paths`, which is
|
|
489
|
+
* the same class of loss one level down.
|
|
490
|
+
*
|
|
491
|
+
* Held honest by `config-round-trip.test.ts`, which populates every optional
|
|
492
|
+
* field, runs `writeConfig`, and asserts the emitted key set is exactly this.
|
|
493
|
+
*/
|
|
494
|
+
const OWNED_CONFIG_KEYS: readonly (readonly string[])[] = [
|
|
495
|
+
["mode", "kind"],
|
|
496
|
+
["daemon", "configured"],
|
|
497
|
+
["collector", "sessions"],
|
|
498
|
+
["collector", "hooks"],
|
|
499
|
+
["collector", "hooks_verbosity"],
|
|
500
|
+
["collector", "redact"],
|
|
501
|
+
["collector", "environment"],
|
|
502
|
+
["collector", "machine_id"],
|
|
503
|
+
["collector", "sources", "*", "extra_paths"],
|
|
504
|
+
["telemetry", "enabled"],
|
|
505
|
+
["audit", "auto"],
|
|
506
|
+
["audit", "interval_days"],
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
510
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Remove one owned path, pruning any container the removal emptied.
|
|
514
|
+
*
|
|
515
|
+
* The pruning is not tidiness. Without it, deleting `telemetry.enabled` from a
|
|
516
|
+
* file that held nothing else under `[telemetry]` leaves `telemetry: {}` — so a
|
|
517
|
+
* default install would stop producing the byte-identical file that
|
|
518
|
+
* `readSources` and `writeConfig` both document, and every fixture asserting
|
|
519
|
+
* exact bytes would drift. A container that still holds an UNKNOWN key is never
|
|
520
|
+
* pruned, which is the whole point.
|
|
521
|
+
*/
|
|
522
|
+
function deleteOwnedPath(node: Record<string, unknown>, path: readonly string[]): void {
|
|
523
|
+
const [head, ...rest] = path;
|
|
524
|
+
if (head === undefined) return;
|
|
525
|
+
const keys = head === "*" ? Object.keys(node) : [head];
|
|
526
|
+
for (const key of keys) {
|
|
527
|
+
if (rest.length === 0) {
|
|
528
|
+
delete node[key];
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const child = node[key];
|
|
532
|
+
if (!isPlainObject(child)) continue;
|
|
533
|
+
deleteOwnedPath(child, rest);
|
|
534
|
+
if (Object.keys(child).length === 0) delete node[key];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Deep-merge `patch` onto `base`, recursing only where both sides are objects. */
|
|
539
|
+
function mergeOnto(base: Record<string, unknown>, patch: Record<string, unknown>): void {
|
|
540
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
541
|
+
const existing = base[key];
|
|
542
|
+
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
543
|
+
mergeOnto(existing, value);
|
|
544
|
+
} else {
|
|
545
|
+
base[key] = value;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Regenerates the keys `config.json` owns, preserving every key it does not.
|
|
552
|
+
*
|
|
553
|
+
* The layout-2 file was hand-written TOML carrying its own documentation — what
|
|
554
|
+
* `oss` meant, why `daemon.configured` must never be hand-set. JSON cannot hold
|
|
555
|
+
* a comment, so that guidance now lives in `failproofai config --status` and the
|
|
556
|
+
* docs. It is a real loss on the one file a user opens when something is wrong;
|
|
557
|
+
* the trade is one serialisation format across the whole home instead of two.
|
|
558
|
+
*
|
|
559
|
+
* `raw` is the object the projection came from — pass it whenever you have one
|
|
560
|
+
* ({@link readConfigRaw} returns both together). Omitting it re-reads the file,
|
|
561
|
+
* which is the safe default: a caller who forgets still preserves, where the
|
|
562
|
+
* alternative silently erases. Pass `{}` deliberately to write a clean file.
|
|
563
|
+
*/
|
|
564
|
+
export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): void {
|
|
565
|
+
const c = config.collector;
|
|
566
|
+
const out: Record<string, unknown> = {
|
|
567
|
+
mode: { kind: config.mode },
|
|
568
|
+
daemon: { configured: config.daemon.configured },
|
|
569
|
+
collector: {
|
|
570
|
+
sessions: c.sessions,
|
|
571
|
+
hooks: c.hooks,
|
|
572
|
+
hooks_verbosity: c.hooksVerbosity,
|
|
573
|
+
redact: c.redact,
|
|
574
|
+
environment: c.environment,
|
|
575
|
+
// Omitted when unset rather than written null: the Rust side treats an
|
|
576
|
+
// absent machine_id as "derive one", and an explicit null is not that.
|
|
577
|
+
...(c.machineId ? { machine_id: c.machineId } : {}),
|
|
578
|
+
// Extra capture paths per harness. Emitted ONLY when non-empty, so a
|
|
579
|
+
// machine that never configured one gets a file byte-identical to what
|
|
580
|
+
// this function produced before the field existed — the same reasoning as
|
|
581
|
+
// `telemetry` below. (In TOML this also had to come after every scalar of
|
|
582
|
+
// `[collector]`, or the scalars after it read as belonging to the
|
|
583
|
+
// sub-table. JSON has no such ordering hazard, which is one of the things
|
|
584
|
+
// the format change buys.)
|
|
585
|
+
...(Object.entries(c.sources ?? {}).some(([, s]) => s.extraPaths?.length)
|
|
586
|
+
? {
|
|
587
|
+
sources: Object.fromEntries(
|
|
588
|
+
Object.entries(c.sources ?? {})
|
|
589
|
+
.filter(([, s]) => s.extraPaths?.length)
|
|
590
|
+
.map(([name, s]) => [name, { extra_paths: s.extraPaths }]),
|
|
591
|
+
),
|
|
592
|
+
}
|
|
593
|
+
: {}),
|
|
594
|
+
},
|
|
595
|
+
// Written ONLY when switched off. A default install therefore carries no
|
|
596
|
+
// telemetry block at all, but an operator who switched it off keeps it:
|
|
597
|
+
// this function regenerates the file wholesale, so emitting the key only
|
|
598
|
+
// when it is set is what stops a later rewrite from silently switching
|
|
599
|
+
// telemetry back on underneath them.
|
|
600
|
+
...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
|
|
601
|
+
// Written ALWAYS, unlike telemetry directly above — the two are opposites
|
|
602
|
+
// on purpose. Telemetry ships on and is deliberately not advertised;
|
|
603
|
+
// the scheduled audit ships off and is meant to be FOUND, and a switch
|
|
604
|
+
// nobody can see is the same as a switch that does not exist. Emitting both
|
|
605
|
+
// keys unconditionally also makes "a user's setting survives a rewrite"
|
|
606
|
+
// total rather than conditional.
|
|
607
|
+
audit: { auto: config.audit.auto, interval_days: config.audit.intervalDays },
|
|
608
|
+
};
|
|
609
|
+
// Start from the previous bytes, strip the keys this build owns — so an
|
|
610
|
+
// omission above really removes — then lay the projection on top. What is left
|
|
611
|
+
// untouched in between is exactly the set of keys a newer build wrote and this
|
|
612
|
+
// one has never heard of.
|
|
613
|
+
const merged = structuredClone(raw ?? readConfigRaw().raw);
|
|
614
|
+
for (const path of OWNED_CONFIG_KEYS) deleteOwnedPath(merged, path);
|
|
615
|
+
mergeOnto(merged, out);
|
|
616
|
+
writeFileAt(configFile(), `${JSON.stringify(merged, null, 2)}\n`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Merge a partial update into the config on disk. */
|
|
620
|
+
export function updateConfig(patch: {
|
|
621
|
+
mode?: Mode;
|
|
622
|
+
daemon?: Partial<FpConfig["daemon"]>;
|
|
623
|
+
collector?: Partial<FpConfig["collector"]>;
|
|
624
|
+
telemetry?: Partial<FpConfig["telemetry"]>;
|
|
625
|
+
audit?: Partial<FpConfig["audit"]>;
|
|
626
|
+
}): FpConfig {
|
|
627
|
+
// `readConfigRaw`, not `readConfig` — this is the function EVERY mutation path
|
|
628
|
+
// goes through (the wizard, `harness add-path`, `healDaemonFlag`, the telemetry
|
|
629
|
+
// opt-out), so carrying the raw object here is what makes unknown-key
|
|
630
|
+
// preservation total rather than per-caller.
|
|
631
|
+
const { raw, config: current } = readConfigRaw();
|
|
632
|
+
const next: FpConfig = {
|
|
633
|
+
mode: patch.mode ?? current.mode,
|
|
634
|
+
daemon: { ...current.daemon, ...patch.daemon },
|
|
635
|
+
collector: { ...current.collector, ...patch.collector },
|
|
636
|
+
telemetry: { ...current.telemetry, ...patch.telemetry },
|
|
637
|
+
audit: { ...current.audit, ...patch.audit },
|
|
638
|
+
};
|
|
639
|
+
writeConfig(next, raw);
|
|
640
|
+
return next;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ── credentials.json ─────────────────────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
export interface FpCredentials {
|
|
646
|
+
cloud?: { url: string; machineId: string; token: string; machineLabel?: string };
|
|
647
|
+
/**
|
|
648
|
+
* Which organisation this machine's credentials report into, as the server
|
|
649
|
+
* named it at connect time (`/v1/auth/introspect`).
|
|
650
|
+
*
|
|
651
|
+
* ONE table, not a field on `[cloud]` and `[ingest]` both: it describes the
|
|
652
|
+
* TOKEN, and connect uses the same token for both capabilities, so two copies
|
|
653
|
+
* could only ever drift or disagree. It also has to survive the case that
|
|
654
|
+
* motivated recording it at all — an `events:add`-only key, which configures
|
|
655
|
+
* ingest and never writes `[cloud]`.
|
|
656
|
+
*
|
|
657
|
+
* Recorded so `--status` can answer "where does this machine's data go?"
|
|
658
|
+
* with no network call: the question asked when a dashboard looks empty, and
|
|
659
|
+
* the one a key pasted from the wrong org gets silently wrong. Absent against
|
|
660
|
+
* a server predating introspect, and in files written by an older CLI.
|
|
661
|
+
*/
|
|
662
|
+
org?: { id?: string; slug?: string; name?: string };
|
|
663
|
+
ingest?: { url: string; key: string };
|
|
664
|
+
auth?: { baseUrl?: string; sessionToken?: string; expiresAt?: number; email?: string };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Every key `writeCredentials` owns. Same contract as {@link OWNED_CONFIG_KEYS},
|
|
669
|
+
* same reason — `readCredentials` is a whitelist projection too, and
|
|
670
|
+
* `writeCredentials` regenerates the file wholesale.
|
|
671
|
+
*
|
|
672
|
+
* `org` is the live example on this side: its own doc records that it is "absent
|
|
673
|
+
* against a server predating introspect, and in files written by an older CLI" —
|
|
674
|
+
* which is precisely a key an older CLI would delete on write, taking with it the
|
|
675
|
+
* only local answer to "where does this machine's data go?".
|
|
676
|
+
*/
|
|
677
|
+
const OWNED_CREDENTIAL_KEYS: readonly (readonly string[])[] = [
|
|
678
|
+
["cloud", "url"],
|
|
679
|
+
["cloud", "machine_id"],
|
|
680
|
+
["cloud", "machine_label"],
|
|
681
|
+
["cloud", "token"],
|
|
682
|
+
["org", "id"],
|
|
683
|
+
["org", "slug"],
|
|
684
|
+
["org", "name"],
|
|
685
|
+
["ingest", "url"],
|
|
686
|
+
["ingest", "key"],
|
|
687
|
+
["auth", "base_url"],
|
|
688
|
+
["auth", "session_token"],
|
|
689
|
+
["auth", "expires_at"],
|
|
690
|
+
["auth", "email"],
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
/** {@link readCredentials}, plus the raw object it was projected from. */
|
|
694
|
+
export function readCredentialsRaw(): { raw: Record<string, unknown>; credentials: FpCredentials } {
|
|
695
|
+
let raw: Record<string, unknown> = {};
|
|
696
|
+
try {
|
|
697
|
+
const parsed = JSON.parse(readFileSync(credentialsFile(), "utf8")) as unknown;
|
|
698
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
699
|
+
raw = parsed as Record<string, unknown>;
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
// Absent or unparseable — nothing to carry forward.
|
|
703
|
+
}
|
|
704
|
+
return { raw, credentials: readCredentials() };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function readCredentials(): FpCredentials {
|
|
708
|
+
try {
|
|
709
|
+
return projectCredentials(
|
|
710
|
+
JSON.parse(readFileSync(credentialsFile(), "utf8")) as Record<string, unknown>,
|
|
711
|
+
);
|
|
712
|
+
} catch {
|
|
713
|
+
return {};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Project a parsed credentials object onto {@link FpCredentials}.
|
|
719
|
+
*
|
|
720
|
+
* Split out for the reason {@link projectConfig} is: the layout-2 → 3 carry
|
|
721
|
+
* parses `credentials.toml` itself and needs the SAME validation, not a copy of
|
|
722
|
+
* it. The validation is the part worth not duplicating — "a cloud block without
|
|
723
|
+
* a token is not a cloud block" is a security property, and two readers
|
|
724
|
+
* disagreeing about it is how a half-written credential gets treated as live.
|
|
725
|
+
*/
|
|
726
|
+
export function projectCredentials(parsed: Record<string, unknown>): FpCredentials {
|
|
727
|
+
{
|
|
728
|
+
const cloud = parsed.cloud as Record<string, unknown> | undefined;
|
|
729
|
+
const ingest = parsed.ingest as Record<string, unknown> | undefined;
|
|
730
|
+
const auth = parsed.auth as Record<string, unknown> | undefined;
|
|
731
|
+
const out: FpCredentials = {};
|
|
732
|
+
if (
|
|
733
|
+
cloud &&
|
|
734
|
+
typeof cloud.url === "string" &&
|
|
735
|
+
typeof cloud.machine_id === "string" &&
|
|
736
|
+
typeof cloud.token === "string" &&
|
|
737
|
+
cloud.url &&
|
|
738
|
+
cloud.token
|
|
739
|
+
) {
|
|
740
|
+
const machineLabel =
|
|
741
|
+
typeof cloud.machine_label === "string" && cloud.machine_label ? cloud.machine_label : undefined;
|
|
742
|
+
out.cloud = { url: cloud.url, machineId: cloud.machine_id, token: cloud.token, machineLabel };
|
|
743
|
+
}
|
|
744
|
+
const org = parsed.org as Record<string, unknown> | undefined;
|
|
745
|
+
if (org) {
|
|
746
|
+
// Every field independently optional: a server may send fewer than three,
|
|
747
|
+
// and a partial org record is still better than none for `--status`.
|
|
748
|
+
const pick = (v: unknown) => (typeof v === "string" && v ? v : undefined);
|
|
749
|
+
const built = { id: pick(org.id), slug: pick(org.slug), name: pick(org.name) };
|
|
750
|
+
if (built.id || built.slug || built.name) out.org = built;
|
|
751
|
+
}
|
|
752
|
+
if (ingest && typeof ingest.url === "string" && typeof ingest.key === "string" && ingest.url && ingest.key) {
|
|
753
|
+
out.ingest = { url: ingest.url, key: ingest.key };
|
|
754
|
+
}
|
|
755
|
+
if (auth) {
|
|
756
|
+
out.auth = {
|
|
757
|
+
baseUrl: typeof auth.base_url === "string" ? auth.base_url : undefined,
|
|
758
|
+
sessionToken: typeof auth.session_token === "string" ? auth.session_token : undefined,
|
|
759
|
+
expiresAt: typeof auth.expires_at === "number" ? auth.expires_at : undefined,
|
|
760
|
+
email: typeof auth.email === "string" ? auth.email : undefined,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Write credentials at 0600, tightening the home that holds them.
|
|
769
|
+
*
|
|
770
|
+
* A 0600 file inside a 0775 directory is still reachable by every local user,
|
|
771
|
+
* so the directory is tightened too. `mode` on `writeFileSync` applies only
|
|
772
|
+
* when the file is CREATED, so an existing over-permissive file keeps its mode
|
|
773
|
+
* without the explicit chmod.
|
|
774
|
+
*/
|
|
775
|
+
export function writeCredentials(creds: FpCredentials, raw?: Record<string, unknown>): void {
|
|
776
|
+
const out: Record<string, unknown> = {};
|
|
777
|
+
if (creds.cloud) {
|
|
778
|
+
out.cloud = {
|
|
779
|
+
url: creds.cloud.url,
|
|
780
|
+
machine_id: creds.cloud.machineId,
|
|
781
|
+
...(creds.cloud.machineLabel ? { machine_label: creds.cloud.machineLabel } : {}),
|
|
782
|
+
token: creds.cloud.token,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
if (creds.ingest) {
|
|
786
|
+
out.ingest = { url: creds.ingest.url, key: creds.ingest.key };
|
|
787
|
+
}
|
|
788
|
+
if (creds.org && (creds.org.id || creds.org.slug || creds.org.name)) {
|
|
789
|
+
out.org = {
|
|
790
|
+
...(creds.org.id ? { id: creds.org.id } : {}),
|
|
791
|
+
...(creds.org.slug ? { slug: creds.org.slug } : {}),
|
|
792
|
+
...(creds.org.name ? { name: creds.org.name } : {}),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
if (creds.auth && (creds.auth.sessionToken || creds.auth.email)) {
|
|
796
|
+
out.auth = {
|
|
797
|
+
...(creds.auth.baseUrl ? { base_url: creds.auth.baseUrl } : {}),
|
|
798
|
+
...(creds.auth.sessionToken ? { session_token: creds.auth.sessionToken } : {}),
|
|
799
|
+
...(creds.auth.expiresAt ? { expires_at: creds.auth.expiresAt } : {}),
|
|
800
|
+
...(creds.auth.email ? { email: creds.auth.email } : {}),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Same three-step as `writeConfig`: previous bytes, minus the keys this build
|
|
805
|
+
// owns, plus the projection. An unowned key here is a credential a newer build
|
|
806
|
+
// stored, and deleting one costs the machine an enrolment it cannot re-derive.
|
|
807
|
+
const merged = structuredClone(raw ?? readCredentialsRaw().raw);
|
|
808
|
+
for (const path of OWNED_CREDENTIAL_KEYS) deleteOwnedPath(merged, path);
|
|
809
|
+
mergeOnto(merged, out);
|
|
810
|
+
|
|
811
|
+
const home = failproofaiHome();
|
|
812
|
+
mkdirSync(home, { recursive: true });
|
|
813
|
+
try {
|
|
814
|
+
if (statSync(home).mode & 0o077) chmodSync(home, 0o700);
|
|
815
|
+
} catch {
|
|
816
|
+
// Not fatal: the file's own 0600 is the primary protection.
|
|
817
|
+
}
|
|
818
|
+
writeFileSync(credentialsFile(), `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
819
|
+
try {
|
|
820
|
+
chmodSync(credentialsFile(), 0o600);
|
|
821
|
+
} catch {
|
|
822
|
+
/* best effort on filesystems without POSIX modes */
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** True when this machine has any cloud credential at all. */
|
|
827
|
+
export function hasCloudCredentials(): boolean {
|
|
828
|
+
const c = readCredentials();
|
|
829
|
+
return Boolean(c.cloud || c.ingest);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function writeFileAt(path: string, contents: string): void {
|
|
833
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
834
|
+
writeFileSync(path, contents, "utf8");
|
|
835
|
+
}
|