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
package/bin/failproofai.mjs
CHANGED
|
@@ -124,13 +124,13 @@ if (hookIdx >= 0) {
|
|
|
124
124
|
// machine until `failproofai config` has installed failproofaid AND
|
|
125
125
|
// written the daemonConfigured marker (Stage 4). Until then this is
|
|
126
126
|
// byte-for-byte the same handleHookEvent(...) call below.
|
|
127
|
-
const { isDaemonConfigured,
|
|
127
|
+
const { isDaemonConfigured, attemptDaemonHook } = await import("../src/hooks/daemon-client");
|
|
128
128
|
if (isDaemonConfigured()) {
|
|
129
129
|
const { readStdinPayload } = await import("../src/hooks/read-stdin");
|
|
130
130
|
const { evaluateHookEvent } = await import("../src/hooks/handler");
|
|
131
131
|
const stdinRead = await readStdinPayload();
|
|
132
132
|
|
|
133
|
-
const
|
|
133
|
+
const attempt = await attemptDaemonHook({
|
|
134
134
|
hookEvent: eventType,
|
|
135
135
|
cli,
|
|
136
136
|
stdin: stdinRead.payload,
|
|
@@ -140,21 +140,40 @@ if (hookIdx >= 0) {
|
|
|
140
140
|
cwd: process.cwd(),
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
// On a daemon-configured machine the daemon is the ONLY evaluator. Every
|
|
144
|
+
// way of not getting an answer from it denies, and in-process evaluation
|
|
145
|
+
// is never reached from this branch — that is what "all enforcement is
|
|
146
|
+
// routed through the daemon" means, and a fallback here would be a second
|
|
147
|
+
// policy engine reachable by breaking the first.
|
|
148
|
+
//
|
|
149
|
+
// The two failures still differ in what the USER has to do, so they are
|
|
150
|
+
// told apart in the message and nowhere else:
|
|
151
|
+
//
|
|
152
|
+
// protocol-mismatch: a daemon answered, so it is alive; the CLI and the
|
|
153
|
+
// daemon are different versions, which is what an `npm update` that
|
|
154
|
+
// has not been followed by `failproofai config` looks like. The remedy
|
|
155
|
+
// is an upgrade, and naming it is the difference between a one-command
|
|
156
|
+
// fix and a support ticket. `daemonVersionSkew()` has already been
|
|
157
|
+
// hinting this on every CLI command.
|
|
158
|
+
//
|
|
159
|
+
// unreachable: nothing answered. A stopped service, a deleted socket and
|
|
160
|
+
// deliberate tampering are indistinguishable from here — and a machine
|
|
161
|
+
// where stopping one service silently disables every guardrail is not
|
|
162
|
+
// a guarded machine.
|
|
163
|
+
let result;
|
|
164
|
+
if (attempt.ok) {
|
|
165
|
+
result = attempt.response;
|
|
166
|
+
} else {
|
|
167
|
+
const reason =
|
|
168
|
+
attempt.failure === "protocol-mismatch"
|
|
169
|
+
? "failproofaid is running a different protocol version than this CLI, so it " +
|
|
170
|
+
"cannot evaluate this call. Run `failproofai config` to update the daemon."
|
|
171
|
+
: "failproofaid could not be reached. This machine is configured to run hooks through it " +
|
|
172
|
+
"— check the daemon (see `failproofai config`) rather than retrying blindly.";
|
|
173
|
+
result = await evaluateHookEvent(eventType, cli, stdinRead.payload, {
|
|
174
|
+
forceDecision: { decision: "deny", reason },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
158
177
|
|
|
159
178
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
160
179
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
@@ -168,6 +187,64 @@ if (hookIdx >= 0) {
|
|
|
168
187
|
await exitAfterFlush(exitCode);
|
|
169
188
|
} catch (err) {
|
|
170
189
|
const msg = err instanceof Error ? err.message : String(err);
|
|
190
|
+
|
|
191
|
+
// The outer fail-closed boundary. This wrote NOTHING to stdout and exited 2,
|
|
192
|
+
// which is a deny for Claude and Factory's non-Stop events and a silent
|
|
193
|
+
// ALLOW for the eight CLIs that read their verdict from stdout JSON —
|
|
194
|
+
// Cursor, Pi, Hermes, OpenClaw, Devin, Antigravity, Goose, and Factory's
|
|
195
|
+
// Stop. Everything above can land here, INCLUDING the forced-deny call that
|
|
196
|
+
// handles an unreachable daemon, so the one path whose entire job is to fail
|
|
197
|
+
// closed failed open instead.
|
|
198
|
+
//
|
|
199
|
+
// Emitted BEFORE any telemetry: `flushHookTelemetry` loops until its queue
|
|
200
|
+
// drains and is not bounded, so draining first meant a stuck send could hold
|
|
201
|
+
// the verdict back indefinitely — and a verdict that arrives after the agent
|
|
202
|
+
// has moved on is the same as no verdict.
|
|
203
|
+
const reason =
|
|
204
|
+
"failproofai could not evaluate this call and is failing closed. " +
|
|
205
|
+
`Check the failproofai installation (\`failproofai config\`). Underlying error: ${msg}`;
|
|
206
|
+
let emitted = false;
|
|
207
|
+
let denyExitCode = 2;
|
|
208
|
+
try {
|
|
209
|
+
// The real evaluator does the shaping, exactly as the unreachable-daemon
|
|
210
|
+
// path above does — that is what keeps this deny from being inert on some
|
|
211
|
+
// CLI whose contract nobody remembered here. It can only work if the
|
|
212
|
+
// handler module is loadable, which is why the fallback below exists.
|
|
213
|
+
const { evaluateHookEvent } = await import("../src/hooks/handler");
|
|
214
|
+
const forced = await evaluateHookEvent(eventType, cli, "", {
|
|
215
|
+
forceDecision: { decision: "deny", reason },
|
|
216
|
+
});
|
|
217
|
+
if (forced.stdout) process.stdout.write(forced.stdout);
|
|
218
|
+
if (forced.stderr) process.stderr.write(forced.stderr);
|
|
219
|
+
emitted = true;
|
|
220
|
+
denyExitCode = forced.exitCode;
|
|
221
|
+
} catch {
|
|
222
|
+
// Last ditch: the module that knows each CLI's exact deny shape is itself
|
|
223
|
+
// the thing that just failed, so this emits the union of them — every CLI
|
|
224
|
+
// reads only the keys it knows and ignores the rest. Deliberately NOT a
|
|
225
|
+
// per-CLI table: a second copy of those twelve contracts would drift from
|
|
226
|
+
// the real one, and this runs only when the install is already broken.
|
|
227
|
+
// Imperfect enforcement beats the zero bytes that were written before.
|
|
228
|
+
try {
|
|
229
|
+
process.stdout.write(
|
|
230
|
+
JSON.stringify({
|
|
231
|
+
decision: "block",
|
|
232
|
+
reason,
|
|
233
|
+
permission: "deny",
|
|
234
|
+
followup_message: reason,
|
|
235
|
+
hookSpecificOutput: {
|
|
236
|
+
hookEventName: eventType,
|
|
237
|
+
permissionDecision: "deny",
|
|
238
|
+
permissionDecisionReason: reason,
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
emitted = true;
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
if (!emitted) console.error(`Unexpected error: ${msg}`);
|
|
246
|
+
else console.error(`[failproofai] failing closed: ${msg}`);
|
|
247
|
+
|
|
171
248
|
await track("hook_dispatch_error", {
|
|
172
249
|
event_type: eventType,
|
|
173
250
|
cli,
|
|
@@ -180,8 +257,10 @@ if (hookIdx >= 0) {
|
|
|
180
257
|
const { flushHookTelemetry } = await import("../src/hooks/hook-telemetry");
|
|
181
258
|
await flushHookTelemetry();
|
|
182
259
|
} catch {}
|
|
183
|
-
|
|
184
|
-
process.exit
|
|
260
|
+
// `exitAfterFlush`, not a bare `process.exit`: this was the one exit in
|
|
261
|
+
// `--hook` handling that skipped it, so under load `process.exit` could
|
|
262
|
+
// truncate the very bytes carrying the deny.
|
|
263
|
+
await exitAfterFlush(denyExitCode);
|
|
185
264
|
}
|
|
186
265
|
}
|
|
187
266
|
|
|
@@ -191,18 +270,8 @@ if (hookIdx >= 0) {
|
|
|
191
270
|
* Error → unexpected; shows message only, exits 2
|
|
192
271
|
*/
|
|
193
272
|
async function runCli() {
|
|
194
|
-
// Report a fresh install / upgrade. Deliberately here rather than at module
|
|
195
|
-
// scope: everything above this point is the --hook fast path, which runs on
|
|
196
|
-
// every tool call. No-ops after the first run on a given version.
|
|
197
|
-
try {
|
|
198
|
-
const { maybeReportInstall } = await import("../lib/install-check");
|
|
199
|
-
await maybeReportInstall(version);
|
|
200
|
-
} catch {
|
|
201
|
-
// never block a command on reporting
|
|
202
|
-
}
|
|
203
|
-
|
|
204
273
|
// --help / -h (only when not inside a subcommand that handles its own --help)
|
|
205
|
-
const SUBCOMMANDS = ["policies", "policy", "
|
|
274
|
+
const SUBCOMMANDS = ["policies", "policy", "audit", "config", "uninstall", "backfill", "flush", "harness"];
|
|
206
275
|
if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
|
|
207
276
|
const extraArgs = args.filter((a) => a !== "--help" && a !== "-h");
|
|
208
277
|
if (extraArgs.length > 0) {
|
|
@@ -217,6 +286,13 @@ USAGE
|
|
|
217
286
|
COMMANDS
|
|
218
287
|
(no args) Launch the policy dashboard
|
|
219
288
|
config Interactive setup — pick scope, agents & policies
|
|
289
|
+
--connect <url> --token <key> Connect to Failproof Cloud non-interactively
|
|
290
|
+
--machine-id <id> Stable id for this machine
|
|
291
|
+
--machine-label <name> Human-readable name in the dashboard
|
|
292
|
+
--no-transcripts Report decisions only, never transcripts
|
|
293
|
+
--disconnect Stop pulling policy and sending activity
|
|
294
|
+
--status Show connection, daemon and pause state
|
|
295
|
+
--pause / --resume Pause or resume enforcement
|
|
220
296
|
|
|
221
297
|
policy add <name> Enable a single policy (see \`policy --help\`)
|
|
222
298
|
policy remove <name> Disable a single policy
|
|
@@ -243,16 +319,52 @@ COMMANDS
|
|
|
243
319
|
|
|
244
320
|
policies --help, -h Show this help for the policies command
|
|
245
321
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
322
|
+
harness list Show extra capture paths per agent CLI
|
|
323
|
+
harness add-path <h> <path> Also capture sessions from <path> for harness
|
|
324
|
+
<h>. Accepts \`<label>=<path>\`; the label
|
|
325
|
+
namespaces agent ids so two copies of one
|
|
326
|
+
project stay distinct.
|
|
327
|
+
harness remove-path <h> <p> Stop capturing that path
|
|
328
|
+
harness --help, -h Show this help for the harness command
|
|
251
329
|
|
|
252
330
|
audit Audit your agent's behavior, then open the
|
|
253
331
|
dashboard at http://localhost:8020/audit
|
|
254
332
|
audit --help, -h Show this help for the audit command
|
|
255
333
|
|
|
334
|
+
backfill Re-send history the collector already read past
|
|
335
|
+
— after clearing the dashboard, re-enrolling a
|
|
336
|
+
machine, or connecting later than the work
|
|
337
|
+
--since <when> How far back: 30d, 6m, or YYYY-MM-DD
|
|
338
|
+
(default: 30 days)
|
|
339
|
+
--dry-run Report what would be re-read, change nothing
|
|
340
|
+
|
|
341
|
+
flush Deliver everything already spooled, now,
|
|
342
|
+
instead of waiting for the next sweep
|
|
343
|
+
--wait Block until the spool drains (or --timeout)
|
|
344
|
+
--timeout <secs> How long to wait with --wait (default: 60)
|
|
345
|
+
|
|
346
|
+
update Finish an upgrade npm cannot: migrate
|
|
347
|
+
~/.failproofai to this version's layout, put the
|
|
348
|
+
matching daemon binary in place, restart it.
|
|
349
|
+
Run after \`npm install -g failproofai@latest\`.
|
|
350
|
+
--no-daemon Migrate the home only
|
|
351
|
+
|
|
352
|
+
migrate Run pending layout migrations on their own,
|
|
353
|
+
keyed on the layout in ~/.failproofai/VERSION
|
|
354
|
+
(not the npm version, so skipping releases with
|
|
355
|
+
no layout change runs nothing)
|
|
356
|
+
--dry-run Print the steps and change nothing
|
|
357
|
+
|
|
358
|
+
uninstall Remove failproofai from this machine: hook
|
|
359
|
+
entries from every agent CLI, and the daemon
|
|
360
|
+
service. Run this BEFORE \`npm rm -g failproofai\`
|
|
361
|
+
— npm runs no uninstall script, so removing the
|
|
362
|
+
package alone leaves both behind.
|
|
363
|
+
--purge Also delete ~/.failproofai (settings,
|
|
364
|
+
credentials, audit history, daemon binary)
|
|
365
|
+
--dry-run Show what would be removed, change nothing
|
|
366
|
+
--yes, -y Skip the confirmation prompt
|
|
367
|
+
|
|
256
368
|
--version, -v Print version and exit
|
|
257
369
|
--help, -h Show this help message
|
|
258
370
|
|
|
@@ -282,6 +394,10 @@ EXAMPLES
|
|
|
282
394
|
failproofai policies --uninstall --cli opencode
|
|
283
395
|
failproofai policies --uninstall --cli pi
|
|
284
396
|
failproofai policies --uninstall --custom
|
|
397
|
+
failproofai backfill --since 6m
|
|
398
|
+
failproofai backfill --dry-run
|
|
399
|
+
failproofai flush --wait
|
|
400
|
+
failproofai config --status
|
|
285
401
|
|
|
286
402
|
LINKS
|
|
287
403
|
⭐ Star us: https://github.com/failproofai/failproofai
|
|
@@ -301,6 +417,698 @@ LINKS
|
|
|
301
417
|
process.exit(0);
|
|
302
418
|
}
|
|
303
419
|
|
|
420
|
+
// First-run onboarding — before any subcommand runs its own work.
|
|
421
|
+
//
|
|
422
|
+
// On a machine that has never been set up, the first thing the user typed is
|
|
423
|
+
// almost never the thing they need first, so we run the wizard and then let
|
|
424
|
+
// their original command proceed. Exemptions matter more than the rule:
|
|
425
|
+
//
|
|
426
|
+
// --hook never reaches here (it exits above) — it runs on every
|
|
427
|
+
// tool call, and a wizard on that path would hang an agent
|
|
428
|
+
// --version/--help answering "what is this" must not require setup
|
|
429
|
+
// config IS the wizard
|
|
430
|
+
// policies/policy explicit configuration actions. Intercepting these would
|
|
431
|
+
// fight the intent the user just stated, and would break
|
|
432
|
+
// non-interactive scripts that call them to do setup.
|
|
433
|
+
//
|
|
434
|
+
// `maybeFirstRunConfigure` is itself a no-op on a configured machine, on a
|
|
435
|
+
// non-TTY, and under sudo — so this is a cheap check, not a second gate.
|
|
436
|
+
// Layout check, before onboarding decides anything. A home written by an
|
|
437
|
+
// older layout is reset here — visibly, in a real command the user typed —
|
|
438
|
+
// rather than from a hook, which runs unattended once per tool call. A home
|
|
439
|
+
// written by a NEWER layout stops the command instead: that data is fine and
|
|
440
|
+
// an upgrade would read it, so deleting it would destroy something
|
|
441
|
+
// recoverable.
|
|
442
|
+
//
|
|
443
|
+
// `audit --scheduled` is the exception, and it takes the HOOK's branch: it is
|
|
444
|
+
// spawned by failproofaid on a timer, so "visibly, in a real command the user
|
|
445
|
+
// typed" is exactly what it is not. `checkLayoutForCli()` deletes
|
|
446
|
+
// config.toml and credentials.toml (see `resettablePaths`), so letting a
|
|
447
|
+
// background process reach it would silently revoke a user's
|
|
448
|
+
// `[telemetry] enabled = false`, erase their cloud enrolment, and switch off
|
|
449
|
+
// `[audit] auto` — the setting that scheduled the run — with the explanation
|
|
450
|
+
// going only to the service journal. Reachable on every machine at the next
|
|
451
|
+
// LAYOUT_VERSION bump, when a home carrying `auto = true` is by definition
|
|
452
|
+
// stale. Verified live: one scheduled tick took a home's whole config.
|
|
453
|
+
//
|
|
454
|
+
// `--help` / `--version` take the same exemption `first-run-gate.ts` gives
|
|
455
|
+
// them, and for a stronger reason. Those two answer "what is this / how do I
|
|
456
|
+
// use it"; the subcommands that parse their own help (`policies --help`) fall
|
|
457
|
+
// past the help block above and reach here, so without this a user typing
|
|
458
|
+
// `failproofai policies --help` had their home reset by a question. The
|
|
459
|
+
// adjacent, far less destructive first-run gate exempted help from the start.
|
|
460
|
+
{
|
|
461
|
+
const isHelpOrVersion =
|
|
462
|
+
args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v");
|
|
463
|
+
if (args[0] === "audit" && args.includes("--scheduled")) {
|
|
464
|
+
// NOT `layoutWarningForHook` — that one answers the hook's question
|
|
465
|
+
// ("are policies unenforced?") and deliberately stays quiet when the
|
|
466
|
+
// global config is readable. This gate is about the LAYOUT, and must
|
|
467
|
+
// fire whenever completing the run would reset the home.
|
|
468
|
+
const { layoutBlockerForScheduledRun } = await import("../src/hooks/fp-reset");
|
|
469
|
+
const warning = layoutBlockerForScheduledRun();
|
|
470
|
+
if (warning) {
|
|
471
|
+
// Exit 1, not 75: 75 means "another audit holds the lock" and is retried
|
|
472
|
+
// in fifteen minutes, which here would just re-warn four times an hour
|
|
473
|
+
// forever. A stale home is a real failure that `failproofai config`
|
|
474
|
+
// fixes, so it is reported and retried at the ordinary cadence.
|
|
475
|
+
console.error(warning);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
} else if (!isHelpOrVersion && args[0] !== "migrate" && args[0] !== "update") {
|
|
479
|
+
// `migrate` and `update` are exempt because they ARE this step, and letting
|
|
480
|
+
// it run first breaks them both. `migrate --dry-run` promises to change
|
|
481
|
+
// nothing and print what would happen — with the check ahead of it, the home
|
|
482
|
+
// was already migrated by the time the subcommand looked, so it reported
|
|
483
|
+
// "nothing to migrate" on every stale machine it was ever pointed at, which
|
|
484
|
+
// is the one answer a dry run must never give wrongly. Caught by a smoke
|
|
485
|
+
// test on a seeded layout-2 home, not by a unit test: nothing below the CLI
|
|
486
|
+
// entry point can see this ordering.
|
|
487
|
+
const { checkLayoutForCli } = await import("../src/hooks/fp-reset");
|
|
488
|
+
const check = await checkLayoutForCli();
|
|
489
|
+
for (const line of check.lines) console.error(line);
|
|
490
|
+
if (check.fatal) process.exit(1);
|
|
491
|
+
// `check.didReset` is deliberately not read. It used to force the wizard
|
|
492
|
+
// below; see the note at `shouldOfferFirstRun` for why a migrated machine
|
|
493
|
+
// no longer needs setup re-run, and `didReset` in `fp-reset.ts` for what
|
|
494
|
+
// the field means now.
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Report a fresh install / upgrade. AFTER the layout check above, never
|
|
499
|
+
// before: this writes the `last-version` marker, and while that file lived at
|
|
500
|
+
// the root of the home it was one of the landmarks `detectLayout()` reads as
|
|
501
|
+
// "layout 1". Running first meant the CLI created the file and then read it
|
|
502
|
+
// back as evidence of an old layout, so every genuinely fresh machine had its
|
|
503
|
+
// very first command open with "failproofai reorganised … Removed 1 item(s)
|
|
504
|
+
// from the old layout." Nothing was lost — there was nothing there — but
|
|
505
|
+
// training every new user to ignore that banner is expensive given what it
|
|
506
|
+
// says on a real layout-1 home. The file has also moved under `state/`, so
|
|
507
|
+
// the two are independent now; the order is kept because it is the correct
|
|
508
|
+
// one regardless.
|
|
509
|
+
//
|
|
510
|
+
// Deliberately not at module scope: everything above the CLI entry is the
|
|
511
|
+
// --hook fast path, which runs on every tool call. No-ops after the first run
|
|
512
|
+
// on a given version.
|
|
513
|
+
try {
|
|
514
|
+
const { maybeReportInstall } = await import("../lib/install-check");
|
|
515
|
+
await maybeReportInstall(version);
|
|
516
|
+
} catch {
|
|
517
|
+
// never block a command on reporting
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const { shouldOfferFirstRun } = await import("../src/hooks/first-run-gate");
|
|
521
|
+
// `|| layoutWasReset` stood here, with `force: layoutWasReset` below, because a
|
|
522
|
+
// migration used to delete the home's policy config while leaving the agent
|
|
523
|
+
// CLIs' settings files alone — so `isConfigured()` read true off
|
|
524
|
+
// `hasGlobalHooks` and setup was skipped, leaving hooks firing against no
|
|
525
|
+
// policies, silently and permanently.
|
|
526
|
+
//
|
|
527
|
+
// The migration keeps that file now (`HOME_CLASSES` classes it `user-typed`),
|
|
528
|
+
// along with `config.json` and `credentials.json`, so a migrated machine is
|
|
529
|
+
// configured in fact and not merely in appearance. Forcing the wizard here
|
|
530
|
+
// would open an interactive prompt with nothing to answer — and on a fleet box,
|
|
531
|
+
// a CI runner or a headless gateway, nobody to answer it. A home that never
|
|
532
|
+
// finished setup still gets here on its own: `isConfigured()` is false for it.
|
|
533
|
+
// See `didReset` in `fp-reset.ts`.
|
|
534
|
+
if (shouldOfferFirstRun(args)) {
|
|
535
|
+
try {
|
|
536
|
+
const { maybeFirstRunConfigure } = await import("../src/hooks/configure-wizard");
|
|
537
|
+
// `audit` runs its own scan immediately after this returns; firing the
|
|
538
|
+
// post-setup audit too would scan the whole history twice in a row.
|
|
539
|
+
await maybeFirstRunConfigure({}, { postSetupAudit: args[0] !== "audit" });
|
|
540
|
+
} catch {
|
|
541
|
+
// Onboarding is never allowed to block the command the user actually typed.
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// backfill [--since <YYYY-MM-DD|Nd>] [--dry-run]
|
|
546
|
+
//
|
|
547
|
+
// Hands off to the daemon rather than doing the work: the cursors it rewinds
|
|
548
|
+
// are held in memory by the RUNNING collector, which would write them back
|
|
549
|
+
// over. Every precondition a person can get wrong is still checked HERE,
|
|
550
|
+
// synchronously, because reporting success and leaving the real failure in the
|
|
551
|
+
// journal is what already cost twenty minutes on a live machine.
|
|
552
|
+
if (args[0] === "flush") {
|
|
553
|
+
const subArgs = args.slice(1);
|
|
554
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
555
|
+
console.log(`
|
|
556
|
+
failproofai flush — deliver what is already spooled, now
|
|
557
|
+
|
|
558
|
+
USAGE
|
|
559
|
+
failproofai flush [--wait] [--timeout <secs>]
|
|
560
|
+
|
|
561
|
+
WHY
|
|
562
|
+
The collector is unhurried on purpose: a batch is swept once it is older than
|
|
563
|
+
two minutes, at most 64 per pass, on a 60-second cadence. That pacing keeps a
|
|
564
|
+
backlog from stampeding the server, and it is exactly wrong when you are
|
|
565
|
+
standing at a dashboard waiting to see your own events — "not delivered yet"
|
|
566
|
+
and "not working" look identical from there.
|
|
567
|
+
|
|
568
|
+
This asks the daemon to make a pass right now, with no minimum age and no
|
|
569
|
+
per-pass cap. It re-sends nothing: only batches already spooled and not yet
|
|
570
|
+
delivered. For history the collector has already read past, use \`backfill\`.
|
|
571
|
+
|
|
572
|
+
OPTIONS
|
|
573
|
+
--wait Block until the spool drains, or --timeout elapses.
|
|
574
|
+
--timeout <secs> How long --wait waits. Default: 60.
|
|
575
|
+
`);
|
|
576
|
+
process.exit(0);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const KNOWN = new Set(["--wait", "--timeout"]);
|
|
580
|
+
const unknown = subArgs.find(
|
|
581
|
+
(a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--timeout",
|
|
582
|
+
);
|
|
583
|
+
if (unknown) {
|
|
584
|
+
throw new CliError(`Unexpected argument: ${unknown}\nRun \`failproofai flush --help\` for usage.`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let timeoutSecs;
|
|
588
|
+
const tIdx = subArgs.indexOf("--timeout");
|
|
589
|
+
if (tIdx >= 0) {
|
|
590
|
+
const raw = subArgs[tIdx + 1];
|
|
591
|
+
if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --timeout.");
|
|
592
|
+
const n = Number(raw);
|
|
593
|
+
// Rejected rather than coerced: NaN would silently become "wait forever
|
|
594
|
+
// or not at all" depending on the comparison, and neither is what was asked.
|
|
595
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
596
|
+
throw new CliError(`Could not read --timeout ${raw}. Give a number of seconds.`);
|
|
597
|
+
}
|
|
598
|
+
timeoutSecs = n;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
lastSubcommand = "flush";
|
|
602
|
+
const { runFlushCommand } = await import("../src/hooks/flush-cli");
|
|
603
|
+
const result = await runFlushCommand({ wait: subArgs.includes("--wait"), timeoutSecs });
|
|
604
|
+
for (const line of result.lines) {
|
|
605
|
+
if (result.exitCode === 0) console.log(line);
|
|
606
|
+
else console.error(line);
|
|
607
|
+
}
|
|
608
|
+
await track("cli_flush", {
|
|
609
|
+
ok: result.exitCode === 0,
|
|
610
|
+
waited: subArgs.includes("--wait"),
|
|
611
|
+
pending: result.pending,
|
|
612
|
+
});
|
|
613
|
+
lastSubcommand = null;
|
|
614
|
+
await exitAfterFlush(result.exitCode);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (args[0] === "backfill") {
|
|
619
|
+
const subArgs = args.slice(1);
|
|
620
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
621
|
+
console.log(`
|
|
622
|
+
failproofai backfill — re-send history the collector has already read past
|
|
623
|
+
|
|
624
|
+
USAGE
|
|
625
|
+
failproofai backfill [--since <when>] [--dry-run]
|
|
626
|
+
|
|
627
|
+
WHY
|
|
628
|
+
The collector never re-reads a file it has a cursor for, which is right until
|
|
629
|
+
the dashboard's data is cleared, a machine is re-enrolled, or cursors advanced
|
|
630
|
+
before there was anywhere to send. Then the history exists on disk and nowhere
|
|
631
|
+
else, with no way to ask for it again.
|
|
632
|
+
|
|
633
|
+
Re-sending is safe: redaction is deterministic, so a re-sent event hashes
|
|
634
|
+
identically to its first send and collapses into the row already there.
|
|
635
|
+
|
|
636
|
+
OPTIONS
|
|
637
|
+
--since <when> How far back. \`30d\`, \`6m\`, or \`YYYY-MM-DD\`.
|
|
638
|
+
Default: 30 days.
|
|
639
|
+
--dry-run Report what would be re-read and change nothing.
|
|
640
|
+
|
|
641
|
+
Which streams are sent follows [collector] in ~/.failproofai/config.toml —
|
|
642
|
+
a backfill never sends something your config says you do not want.
|
|
643
|
+
`);
|
|
644
|
+
process.exit(0);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const KNOWN = new Set(["--since", "--dry-run"]);
|
|
648
|
+
const unknown = subArgs.find((a, i) => a.startsWith("-") && !KNOWN.has(a) && subArgs[i - 1] !== "--since");
|
|
649
|
+
if (unknown) {
|
|
650
|
+
throw new CliError(`Unexpected argument: ${unknown}\nRun \`failproofai backfill --help\` for usage.`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let sinceMs;
|
|
654
|
+
const sinceIdx = subArgs.indexOf("--since");
|
|
655
|
+
if (sinceIdx >= 0) {
|
|
656
|
+
const raw = subArgs[sinceIdx + 1];
|
|
657
|
+
if (!raw || raw.startsWith("-")) throw new CliError("Missing value after --since.");
|
|
658
|
+
// `30d` / `6m` / an ISO date. Rejected rather than guessed at: silently
|
|
659
|
+
// reading an unparseable window as "the default" would send a different
|
|
660
|
+
// amount of history than was asked for, and nothing would say so.
|
|
661
|
+
const rel = /^(\d+)([dmy])$/.exec(raw);
|
|
662
|
+
if (rel) {
|
|
663
|
+
const n = Number(rel[1]);
|
|
664
|
+
const days = rel[2] === "d" ? n : rel[2] === "m" ? n * 30 : n * 365;
|
|
665
|
+
sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
666
|
+
} else {
|
|
667
|
+
const t = Date.parse(raw);
|
|
668
|
+
if (Number.isNaN(t)) {
|
|
669
|
+
throw new CliError(`Could not read --since ${raw}. Use 30d, 6m, or YYYY-MM-DD.`);
|
|
670
|
+
}
|
|
671
|
+
sinceMs = t;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
lastSubcommand = "backfill";
|
|
676
|
+
const { runBackfillCommand } = await import("../src/hooks/backfill-cli");
|
|
677
|
+
const result = runBackfillCommand({ sinceMs, dryRun: subArgs.includes("--dry-run") });
|
|
678
|
+
for (const line of result.lines) {
|
|
679
|
+
if (result.exitCode === 0) console.log(line);
|
|
680
|
+
else console.error(line);
|
|
681
|
+
}
|
|
682
|
+
await track("cli_backfill", { ok: result.exitCode === 0, dry_run: subArgs.includes("--dry-run"), explicit_since: sinceIdx >= 0 });
|
|
683
|
+
lastSubcommand = null;
|
|
684
|
+
await exitAfterFlush(result.exitCode);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// harness list | add-path | remove-path
|
|
689
|
+
//
|
|
690
|
+
// Extra capture paths per harness. Writes ~/.failproofai/config.toml and
|
|
691
|
+
// nothing else — no root, no daemon call — so it works on a machine whose
|
|
692
|
+
// daemon is stopped, which is when someone is most likely to be fixing what
|
|
693
|
+
// it captures.
|
|
694
|
+
if (args[0] === "harness") {
|
|
695
|
+
const subArgs = args.slice(1);
|
|
696
|
+
if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
697
|
+
console.log(`
|
|
698
|
+
failproofai harness — capture sessions from more than one location per agent CLI
|
|
699
|
+
|
|
700
|
+
USAGE
|
|
701
|
+
failproofai harness list [<harness>]
|
|
702
|
+
failproofai harness add-path <harness> [<label>=]<path>
|
|
703
|
+
failproofai harness remove-path <harness> <path|label>
|
|
704
|
+
|
|
705
|
+
WHY
|
|
706
|
+
Each agent CLI is watched wherever its own installer put it — ~/.claude/projects,
|
|
707
|
+
~/.hermes/state.db, and so on. That misses every other arrangement: a second
|
|
708
|
+
profile, a mounted team share, a container's home beside the host's, an agent
|
|
709
|
+
an operator moved. Those hold real sessions and nothing collects them.
|
|
710
|
+
|
|
711
|
+
THE LABEL
|
|
712
|
+
An entry is \`<path>\` or \`<label>=<path>\`. The label namespaces agent ids as
|
|
713
|
+
<label>-<agentId>, and it matters: two locations holding the same project
|
|
714
|
+
derive the SAME id (it comes from the cwd inside the transcript, identical in
|
|
715
|
+
both copies), so without a label they merge into one agent whose sessions
|
|
716
|
+
interleave from two machines' worth of history. Omit it and one is derived
|
|
717
|
+
from the folder name.
|
|
718
|
+
|
|
719
|
+
HARNESSES
|
|
720
|
+
claude, codex, copilot, openclaw, pi, factory, antigravity, cursor,
|
|
721
|
+
goose, opencode, devin, hermes
|
|
722
|
+
|
|
723
|
+
\`claude\` covers subagent transcripts too — they live under the same root.
|
|
724
|
+
|
|
725
|
+
NOTES
|
|
726
|
+
• Session collection must be on for any of this to be read; extra paths live
|
|
727
|
+
under "collector" in ~/.failproofai/config.json like the default ones do.
|
|
728
|
+
• A path overlapping one already captured is REFUSED by the daemon at startup
|
|
729
|
+
rather than collected twice under two ids. It says so in the journal.
|
|
730
|
+
• Two entries sharing a LABEL are refused too: they would share one cursor
|
|
731
|
+
directory, whose whole map is written at once, so each would clobber the
|
|
732
|
+
other's watermark and re-read from zero after every restart.
|
|
733
|
+
• Takes effect within seconds — the daemon re-reads config.json on an interval
|
|
734
|
+
and cycles its collector. No restart, no sudo.
|
|
735
|
+
|
|
736
|
+
EXAMPLES
|
|
737
|
+
failproofai harness add-path claude work=/srv/team/.claude/projects
|
|
738
|
+
failproofai harness add-path hermes prod=/srv/hermes-prod/state.db
|
|
739
|
+
failproofai harness add-path codex /mnt/other-home/.codex/sessions
|
|
740
|
+
failproofai harness list
|
|
741
|
+
failproofai harness remove-path claude work
|
|
742
|
+
|
|
743
|
+
One home per person — label them, or both derive the same folder-name label
|
|
744
|
+
and the second is refused:
|
|
745
|
+
failproofai harness add-path openclaw user1=/srv/.openclaw-user1
|
|
746
|
+
failproofai harness add-path openclaw user2=/srv/.openclaw-user2
|
|
747
|
+
|
|
748
|
+
Containers: FAILPROOFAI_<HARNESS>_EXTRA_PATHS replaces the file's entries.
|
|
749
|
+
FAILPROOFAI_OPENCLAW_EXTRA_PATHS="user1=/srv/.openclaw-user1,user2=/srv/.openclaw-user2"
|
|
750
|
+
|
|
751
|
+
`.trimStart());
|
|
752
|
+
process.exit(0);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
lastSubcommand = "harness";
|
|
756
|
+
const { runHarnessCommand } = await import("../src/hooks/harness-cli");
|
|
757
|
+
const result = runHarnessCommand(subArgs);
|
|
758
|
+
for (const line of result.lines) {
|
|
759
|
+
if (result.exitCode === 0) console.log(line);
|
|
760
|
+
else console.error(line);
|
|
761
|
+
}
|
|
762
|
+
await track("cli_harness", {
|
|
763
|
+
ok: result.exitCode === 0,
|
|
764
|
+
// The subcommand only — never the harness name, the label or the path.
|
|
765
|
+
// A path is a value the user typed, and `enterprise-docs/product-analytics.md`
|
|
766
|
+
// promises we send shape, never value.
|
|
767
|
+
sub: ["list", "add-path", "remove-path"].includes(subArgs[0]) ? subArgs[0] : "unknown",
|
|
768
|
+
});
|
|
769
|
+
lastSubcommand = null;
|
|
770
|
+
await exitAfterFlush(result.exitCode);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// uninstall [--purge] [--dry-run] [--yes|-y]
|
|
775
|
+
//
|
|
776
|
+
// Top-level, and deliberately NOT a flag on `policies`. `policies --uninstall`
|
|
777
|
+
// disables policies; this removes the product — hook entries across every
|
|
778
|
+
// agent CLI plus the root-owned daemon service — and the two must not be one
|
|
779
|
+
// keystroke apart.
|
|
780
|
+
if (args[0] === "migrate") {
|
|
781
|
+
const subArgs = args.slice(1);
|
|
782
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
783
|
+
console.log(`
|
|
784
|
+
failproofai migrate — bring ~/.failproofai up to the layout this version speaks
|
|
785
|
+
|
|
786
|
+
USAGE
|
|
787
|
+
failproofai migrate [--dry-run]
|
|
788
|
+
|
|
789
|
+
WHY
|
|
790
|
+
npm cannot update an installed package on its own, so a machine can sit on an
|
|
791
|
+
old version for months and then jump several layouts at once. This runs the
|
|
792
|
+
steps for that jump in order, keyed on the LAYOUT recorded in
|
|
793
|
+
~/.failproofai/VERSION rather than on the npm version — so skipping thirty
|
|
794
|
+
releases with no layout change runs nothing at all.
|
|
795
|
+
|
|
796
|
+
It normally happens by itself, on the first command after an upgrade. This is
|
|
797
|
+
for running it deliberately, and for seeing what it would do first.
|
|
798
|
+
|
|
799
|
+
Your settings, cloud enrolment, policy selection, your own policy files,
|
|
800
|
+
decision history and undelivered events are carried across, not removed. The
|
|
801
|
+
irreplaceable files are copied to migrations/backup-layout<n>/ before anything
|
|
802
|
+
runs, and every step is recorded in migrations/applied.json.
|
|
803
|
+
|
|
804
|
+
OPTIONS
|
|
805
|
+
--dry-run Print the steps and the files that would be saved. Change nothing.
|
|
806
|
+
`);
|
|
807
|
+
process.exit(0);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const KNOWN = new Set(["--dry-run"]);
|
|
811
|
+
const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
|
|
812
|
+
if (unknown) {
|
|
813
|
+
throw new CliError(
|
|
814
|
+
`Unexpected argument: ${unknown}\nRun \`failproofai migrate --help\` for usage.`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
lastSubcommand = "migrate";
|
|
819
|
+
const { detectLayout } = await import("../src/hooks/fp-config");
|
|
820
|
+
const { LAYOUT_VERSION } = await import("../src/hooks/fp-home");
|
|
821
|
+
const { describePlan, runMigrations } = await import("../src/hooks/migrations");
|
|
822
|
+
const state = detectLayout();
|
|
823
|
+
|
|
824
|
+
// A NEWER home is refused here exactly as `checkLayoutForCli` refuses it: the
|
|
825
|
+
// data is fine and an upgrade would read it, so migrating "forward" from it
|
|
826
|
+
// is not a thing that exists.
|
|
827
|
+
if (state.kind === "future") {
|
|
828
|
+
console.error(
|
|
829
|
+
`This machine's failproofai directory was written by a newer version (layout ${state.found};`,
|
|
830
|
+
);
|
|
831
|
+
console.error(`this build speaks ${LAYOUT_VERSION}). Upgrade rather than migrate:`);
|
|
832
|
+
console.error(` npm install -g failproofai@latest`);
|
|
833
|
+
await track("cli_migrate", { ok: false, reason: "future_layout" });
|
|
834
|
+
lastSubcommand = null;
|
|
835
|
+
await exitAfterFlush(1);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const from = state.kind === "stale" ? state.found : LAYOUT_VERSION;
|
|
840
|
+
if (subArgs.includes("--dry-run")) {
|
|
841
|
+
for (const line of describePlan(from)) console.log(line);
|
|
842
|
+
await track("cli_migrate", { ok: true, dry_run: true, from });
|
|
843
|
+
lastSubcommand = null;
|
|
844
|
+
await exitAfterFlush(0);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (state.kind !== "stale") {
|
|
849
|
+
console.log(`Already at layout ${LAYOUT_VERSION}. Nothing to migrate.`);
|
|
850
|
+
await track("cli_migrate", { ok: true, from, steps: 0 });
|
|
851
|
+
lastSubcommand = null;
|
|
852
|
+
await exitAfterFlush(0);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const run = runMigrations(from);
|
|
857
|
+
for (const step of run.steps) {
|
|
858
|
+
console.log(`${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
|
|
859
|
+
}
|
|
860
|
+
if (run.backedUp.length > 0) {
|
|
861
|
+
console.log(`Saved first: ${run.backedUp.join(", ")}`);
|
|
862
|
+
}
|
|
863
|
+
if (run.failed) {
|
|
864
|
+
console.error(`Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`);
|
|
865
|
+
console.error(`The home is still marked layout ${from} and will be retried.`);
|
|
866
|
+
}
|
|
867
|
+
await track("cli_migrate", {
|
|
868
|
+
ok: !run.failed,
|
|
869
|
+
from,
|
|
870
|
+
steps: run.steps.length,
|
|
871
|
+
removed: run.outcome.removed.length,
|
|
872
|
+
});
|
|
873
|
+
lastSubcommand = null;
|
|
874
|
+
await exitAfterFlush(run.failed ? 1 : 0);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (args[0] === "update") {
|
|
879
|
+
const subArgs = args.slice(1);
|
|
880
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
881
|
+
console.log(`
|
|
882
|
+
failproofai update — finish an upgrade: migrate the home, match the daemon
|
|
883
|
+
|
|
884
|
+
USAGE
|
|
885
|
+
npm install -g failproofai@latest && failproofai update [--no-daemon]
|
|
886
|
+
|
|
887
|
+
WHY
|
|
888
|
+
npm replaces the CLI and nothing else. The daemon binary lives at
|
|
889
|
+
~/.failproofai/bin/failproofaid-<version> and stays exactly where it was, so
|
|
890
|
+
after an npm upgrade the two halves are different versions — and failproofaid
|
|
891
|
+
refuses to start against a layout it does not speak, which is the loud version
|
|
892
|
+
of that problem rather than the silent one.
|
|
893
|
+
|
|
894
|
+
This does the rest of the upgrade: runs any pending layout migrations, puts the
|
|
895
|
+
matching daemon binary in place, and restarts the service.
|
|
896
|
+
|
|
897
|
+
OPTIONS
|
|
898
|
+
--no-daemon Migrate the home only. Leaves a version-skewed daemon in place,
|
|
899
|
+
so prefer letting it run.
|
|
900
|
+
`);
|
|
901
|
+
process.exit(0);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const KNOWN = new Set(["--no-daemon"]);
|
|
905
|
+
const unknown = subArgs.find((a) => a.startsWith("-") && !KNOWN.has(a));
|
|
906
|
+
if (unknown) {
|
|
907
|
+
throw new CliError(
|
|
908
|
+
`Unexpected argument: ${unknown}\nRun \`failproofai update --help\` for usage.`,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
lastSubcommand = "update";
|
|
913
|
+
// Runs the migration itself rather than leaning on the check at the top of
|
|
914
|
+
// this file, which this command is exempt from. Reading the ledger and
|
|
915
|
+
// inferring "the top-level check must have done it" was the first version, and
|
|
916
|
+
// it is the kind of indirection that reads fine and reports the wrong thing
|
|
917
|
+
// the moment either half moves.
|
|
918
|
+
const { detectLayout } = await import("../src/hooks/fp-config");
|
|
919
|
+
const { LAYOUT_VERSION } = await import("../src/hooks/fp-home");
|
|
920
|
+
const { runMigrations } = await import("../src/hooks/migrations");
|
|
921
|
+
const state = detectLayout();
|
|
922
|
+
|
|
923
|
+
if (state.kind === "future") {
|
|
924
|
+
console.error(
|
|
925
|
+
`This machine's failproofai directory was written by a NEWER version (layout ${state.found};`,
|
|
926
|
+
);
|
|
927
|
+
console.error(`this build speaks ${LAYOUT_VERSION}). This CLI is the stale half:`);
|
|
928
|
+
console.error(` npm install -g failproofai@latest`);
|
|
929
|
+
await track("cli_update", { ok: false, reason: "future_layout" });
|
|
930
|
+
lastSubcommand = null;
|
|
931
|
+
await exitAfterFlush(1);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
let migrationsRan = 0;
|
|
936
|
+
let migrationFailed = false;
|
|
937
|
+
if (state.kind === "stale") {
|
|
938
|
+
const run = runMigrations(state.found);
|
|
939
|
+
migrationsRan = run.steps.length;
|
|
940
|
+
migrationFailed = Boolean(run.failed);
|
|
941
|
+
for (const step of run.steps) {
|
|
942
|
+
console.log(`${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
|
|
943
|
+
}
|
|
944
|
+
if (run.backedUp.length > 0) console.log(`Saved first: ${run.backedUp.join(", ")}`);
|
|
945
|
+
if (run.failed) {
|
|
946
|
+
console.error(
|
|
947
|
+
`Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
|
|
948
|
+
);
|
|
949
|
+
console.error(`The home is still marked layout ${state.found} and will be retried.`);
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
console.log(`Home is at layout ${LAYOUT_VERSION}; no migration was needed.`);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// The daemon is refreshed even when a step failed, and deliberately: the
|
|
956
|
+
// binary and the layout are independent halves, and leaving a version-skewed
|
|
957
|
+
// daemon behind on top of a failed migration is strictly worse than fixing
|
|
958
|
+
// the half that can be fixed. The exit code still reports the failure.
|
|
959
|
+
let daemonOk = true;
|
|
960
|
+
if (subArgs.includes("--no-daemon")) {
|
|
961
|
+
console.log("Skipped the daemon (--no-daemon). Its version may not match this CLI.");
|
|
962
|
+
} else {
|
|
963
|
+
const svc = await import("../src/hooks/daemon-service");
|
|
964
|
+
if (!svc.isDaemonSupportedPlatform()) {
|
|
965
|
+
console.log(`failproofaid does not run on ${process.platform}; nothing to update.`);
|
|
966
|
+
} else {
|
|
967
|
+
// `refreshDaemonToCliVersion` answers "is there a service at all" itself,
|
|
968
|
+
// so there is no separate installed check to get out of step with it.
|
|
969
|
+
const result = await svc.refreshDaemonToCliVersion();
|
|
970
|
+
for (const line of result.lines) console.log(line);
|
|
971
|
+
daemonOk = result.ok;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
await track("cli_update", {
|
|
976
|
+
ok: daemonOk && !migrationFailed,
|
|
977
|
+
migrations: migrationsRan,
|
|
978
|
+
migration_failed: migrationFailed,
|
|
979
|
+
});
|
|
980
|
+
lastSubcommand = null;
|
|
981
|
+
await exitAfterFlush(daemonOk && !migrationFailed ? 0 : 1);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (args[0] === "uninstall") {
|
|
986
|
+
const subArgs = args.slice(1);
|
|
987
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
988
|
+
console.log(`
|
|
989
|
+
failproofai uninstall — remove failproofai from this machine
|
|
990
|
+
|
|
991
|
+
USAGE
|
|
992
|
+
failproofai uninstall [--purge] [--dry-run] [--yes]
|
|
993
|
+
|
|
994
|
+
WHAT IT REMOVES
|
|
995
|
+
• failproofai hook entries from every agent CLI that has them
|
|
996
|
+
• the failproofaid daemon service — ASKED on a plain uninstall (kept unless
|
|
997
|
+
you say yes); always removed with --purge, which prompts for your password
|
|
998
|
+
rather than printing commands to paste
|
|
999
|
+
• the "require the daemon" flag — cleared FIRST, so a partial uninstall can
|
|
1000
|
+
never leave this machine denying every tool call
|
|
1001
|
+
|
|
1002
|
+
OPTIONS
|
|
1003
|
+
--purge Also delete ~/.failproofai — settings, credentials, audit
|
|
1004
|
+
history and the downloaded daemon binary. Off by default so a
|
|
1005
|
+
reinstall keeps your history.
|
|
1006
|
+
--dry-run Print what would be removed and change nothing.
|
|
1007
|
+
--yes, -y Skip the confirmation prompt. Required when there is no TTY.
|
|
1008
|
+
|
|
1009
|
+
WHY THIS EXISTS
|
|
1010
|
+
npm runs no uninstall script, so \`npm rm -g failproofai\` removes the package
|
|
1011
|
+
and leaves the hook entries and the service behind. Run this first, then:
|
|
1012
|
+
npm rm -g failproofai
|
|
1013
|
+
`);
|
|
1014
|
+
process.exit(0);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const KNOWN = new Set(["--purge", "--dry-run", "--yes", "-y"]);
|
|
1018
|
+
const unknown = subArgs.find((a) => !KNOWN.has(a));
|
|
1019
|
+
if (unknown) {
|
|
1020
|
+
throw new CliError(
|
|
1021
|
+
`Unexpected argument: ${unknown}\nRun \`failproofai uninstall --help\` for usage.`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
lastSubcommand = "uninstall_command";
|
|
1026
|
+
const { runUninstallCommand } = await import("../src/hooks/uninstall-cli");
|
|
1027
|
+
const purge = subArgs.includes("--purge");
|
|
1028
|
+
const dryRun = subArgs.includes("--dry-run");
|
|
1029
|
+
const yes = subArgs.includes("--yes") || subArgs.includes("-y");
|
|
1030
|
+
|
|
1031
|
+
// Set only when the prompt actually rendered the plan, which is the one
|
|
1032
|
+
// case where printing it again would duplicate it.
|
|
1033
|
+
let planWasShown = false;
|
|
1034
|
+
const result = await runUninstallCommand({
|
|
1035
|
+
purge,
|
|
1036
|
+
dryRun,
|
|
1037
|
+
yes,
|
|
1038
|
+
cwd: process.cwd(),
|
|
1039
|
+
// Only offered when a person can actually answer. Without a TTY the
|
|
1040
|
+
// command requires --yes rather than assuming consent — see the module.
|
|
1041
|
+
confirm: process.stdin.isTTY
|
|
1042
|
+
? async (planLines) => {
|
|
1043
|
+
planWasShown = true;
|
|
1044
|
+
const { selectOne } = await import("../src/hooks/tui");
|
|
1045
|
+
// "No" first, so the default landing position on Enter is the
|
|
1046
|
+
// non-destructive one.
|
|
1047
|
+
const answer = await selectOne({
|
|
1048
|
+
message: purge
|
|
1049
|
+
? "Remove failproofai and DELETE ~/.failproofai?"
|
|
1050
|
+
: "Remove failproofai from this machine?",
|
|
1051
|
+
body: planLines,
|
|
1052
|
+
choices: [
|
|
1053
|
+
{ label: "No, cancel", value: false },
|
|
1054
|
+
{ label: purge ? "Yes, remove and purge" : "Yes, remove it", value: true },
|
|
1055
|
+
],
|
|
1056
|
+
});
|
|
1057
|
+
return answer === true;
|
|
1058
|
+
}
|
|
1059
|
+
: undefined,
|
|
1060
|
+
// Only on a plain uninstall: `--purge` removes the service unconditionally
|
|
1061
|
+
// because it deletes the binary the service points at. Absent without a TTY,
|
|
1062
|
+
// which the module reads as "keep it".
|
|
1063
|
+
confirmDaemon:
|
|
1064
|
+
process.stdin.isTTY && !purge
|
|
1065
|
+
? async () => {
|
|
1066
|
+
const { selectOne } = await import("../src/hooks/tui");
|
|
1067
|
+
const answer = await selectOne({
|
|
1068
|
+
message: "Remove the failproofaid background service too?",
|
|
1069
|
+
body: [
|
|
1070
|
+
"It runs as a system service and needs sudo to remove, so you",
|
|
1071
|
+
"will be asked for your password.",
|
|
1072
|
+
"",
|
|
1073
|
+
"Keeping it is fine if you plan to reinstall — the hooks are",
|
|
1074
|
+
"already gone either way, so nothing is being enforced.",
|
|
1075
|
+
],
|
|
1076
|
+
choices: [
|
|
1077
|
+
{ label: "Yes, remove the service", value: true },
|
|
1078
|
+
{ label: "No, leave it installed", value: false },
|
|
1079
|
+
],
|
|
1080
|
+
});
|
|
1081
|
+
return answer === true;
|
|
1082
|
+
}
|
|
1083
|
+
: undefined,
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// The prompt already rendered the plan as its body; re-printing it would
|
|
1087
|
+
// show the same block twice. `planLines` is reported by the command rather
|
|
1088
|
+
// than guessed from the text — see UninstallResult.
|
|
1089
|
+
const skip = planWasShown ? result.planLines : 0;
|
|
1090
|
+
for (const line of result.lines.slice(skip)) {
|
|
1091
|
+
if (result.exitCode === 0) console.log(line);
|
|
1092
|
+
else console.error(line);
|
|
1093
|
+
}
|
|
1094
|
+
// NOT after a purge. `track` resolves the instance id, and `getInstanceId()`
|
|
1095
|
+
// lazily WRITES ~/.failproofai/state/telemetry-id — which re-created the
|
|
1096
|
+
// whole directory seconds after the purge deleted it, leaving a machine the
|
|
1097
|
+
// user had just wiped holding a brand-new tracking identifier and making
|
|
1098
|
+
// the command's own "✓ deleted" line false. A purge means gone; nothing
|
|
1099
|
+
// gets to touch the home afterwards, least of all telemetry.
|
|
1100
|
+
if (!result.purged) {
|
|
1101
|
+
await track("cli_uninstall_command", {
|
|
1102
|
+
ok: result.exitCode === 0,
|
|
1103
|
+
purge,
|
|
1104
|
+
dry_run: dryRun,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
lastSubcommand = null;
|
|
1108
|
+
await exitAfterFlush(result.exitCode);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
304
1112
|
// policies [--install|-i|--uninstall|-u|--help|-h] [names...] [--scope] [--beta] [--custom|-c <path>]
|
|
305
1113
|
if (args[0] === "policies") {
|
|
306
1114
|
const subArgs = args.slice(1);
|
|
@@ -566,19 +1374,6 @@ EXAMPLES
|
|
|
566
1374
|
process.exit(0);
|
|
567
1375
|
}
|
|
568
1376
|
|
|
569
|
-
// auth — email-OTP login flow against the FailproofAI api-server.
|
|
570
|
-
if (args[0] === "auth") {
|
|
571
|
-
lastSubcommand = "auth";
|
|
572
|
-
const { runAuthCli } = await import("../src/auth/cli");
|
|
573
|
-
await runAuthCli(args.slice(1));
|
|
574
|
-
await track("cli_auth_invoked", {
|
|
575
|
-
args_count: args.length - 1,
|
|
576
|
-
subcommand: args[1] ?? "help",
|
|
577
|
-
exit_code: process.exitCode ?? 0,
|
|
578
|
-
});
|
|
579
|
-
process.exit(process.exitCode ?? 0);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
1377
|
// audit — scan local agent-CLI history, then launch the dashboard at /audit.
|
|
583
1378
|
if (args[0] === "audit") {
|
|
584
1379
|
lastSubcommand = "audit";
|
|
@@ -765,23 +1560,203 @@ USAGE
|
|
|
765
1560
|
WHAT IT DOES
|
|
766
1561
|
Walks you through 4 quick steps and writes everything for you:
|
|
767
1562
|
1. Where — global (all projects) or just this project
|
|
768
|
-
2.
|
|
1563
|
+
2. Harnesses — which agent CLIs to protect (Claude, Codex, ...)
|
|
769
1564
|
3. Policies — presets (combine any), Everything, or a custom pick
|
|
770
1565
|
4. Review — confirms the exact files it will change, then applies
|
|
771
1566
|
|
|
1567
|
+
FAILPROOF CLOUD
|
|
1568
|
+
failproofai config --connect <url> --token <key> [--machine-id <id>]
|
|
1569
|
+
Connect this machine to Failproof Cloud
|
|
1570
|
+
[--no-transcripts] decisions only, no transcripts
|
|
1571
|
+
failproofai config --disconnect Stop pulling policy and sending activity
|
|
1572
|
+
failproofai config --status Show connection, daemon and pause state
|
|
1573
|
+
failproofai config --pause [--session <id>]
|
|
1574
|
+
Pause enforcement, time-boxed
|
|
1575
|
+
failproofai config --resume [--all]
|
|
1576
|
+
Resume enforcement
|
|
1577
|
+
|
|
1578
|
+
--machine-label <name> Human-readable name in the dashboard
|
|
1579
|
+
(use alone to rename an already-connected machine)
|
|
1580
|
+
|
|
1581
|
+
One connection, two capabilities: this machine PULLS centrally-managed
|
|
1582
|
+
policies and SENDS what its hooks decided, so the dashboard shows the fleet
|
|
1583
|
+
it is enforcing on. Both are checked against the server before anything is
|
|
1584
|
+
written, and reported separately — a key carrying policies:pull but not
|
|
1585
|
+
events:add connects for policy and says exactly why the dashboard is empty.
|
|
1586
|
+
|
|
1587
|
+
Tokens are stored owner-only in ~/.failproofai/, never in the service unit —
|
|
1588
|
+
that file is world-readable. Connecting needs no sudo, and the machine id
|
|
1589
|
+
defaults to this host's name.
|
|
1590
|
+
|
|
1591
|
+
Connecting sends BOTH policy decisions and full session transcripts. A
|
|
1592
|
+
transcript carries prompts, file contents and whatever was pasted into a
|
|
1593
|
+
terminal — that is the point of connecting, and it is stated here rather than
|
|
1594
|
+
buried behind a flag nobody finds. Use --no-transcripts for decisions only.
|
|
1595
|
+
|
|
1596
|
+
PAUSING ENFORCEMENT (one session, always time-boxed)
|
|
1597
|
+
failproofai config --pause Pause this directory's newest agent session (30m)
|
|
1598
|
+
failproofai config --pause 10m Pause for a given time (max 8h; s/m/h, bare = minutes)
|
|
1599
|
+
failproofai config --resume End the pause early
|
|
1600
|
+
failproofai config --status Show what is paused and when it lifts
|
|
1601
|
+
--session <id> Target a specific session
|
|
1602
|
+
--all With --resume, end every active pause
|
|
1603
|
+
|
|
1604
|
+
A pause suspends builtin, custom and convention policies for that session
|
|
1605
|
+
only, and always expires on its own. Cloud-managed policies keep enforcing.
|
|
1606
|
+
|
|
772
1607
|
Prefer flags? See \`failproofai policies --help\`.
|
|
773
1608
|
`.trimStart());
|
|
774
1609
|
process.exit(0);
|
|
775
1610
|
}
|
|
776
1611
|
lastSubcommand = "config";
|
|
1612
|
+
|
|
1613
|
+
// --pause / --resume / --status are non-interactive session actions that
|
|
1614
|
+
// share `config`'s surface but not the wizard. They write session state,
|
|
1615
|
+
// never the config file — a pause that reached policies-config.json would
|
|
1616
|
+
// be committed and outlive the session that asked for it.
|
|
1617
|
+
// Cloud enrolment. Deliberately writes a credential file the daemon reads
|
|
1618
|
+
// rather than an Environment= line in the service unit: that unit is
|
|
1619
|
+
// installed world-readable (0644, /etc/systemd/system), so a token there
|
|
1620
|
+
// would be readable by every local user. Keeping it out also means no
|
|
1621
|
+
// sudo, and lets an already-installed daemon be connected.
|
|
1622
|
+
const connectIdx = args.indexOf("--connect");
|
|
1623
|
+
const wantsDisconnect = args.includes("--disconnect");
|
|
1624
|
+
// `--machine-label` ALONE is a rename, not an enrolment. Alongside --connect it
|
|
1625
|
+
// keeps its old meaning (the name to enrol under); on its own it changes the
|
|
1626
|
+
// name of a machine that is already connected, which previously required
|
|
1627
|
+
// re-running enrolment with the url and token again just to fix a display name.
|
|
1628
|
+
const wantsRename =
|
|
1629
|
+
connectIdx < 0 && !wantsDisconnect && args.includes("--machine-label");
|
|
1630
|
+
if (connectIdx >= 0 || wantsDisconnect || wantsRename) {
|
|
1631
|
+
if (connectIdx >= 0 && wantsDisconnect) {
|
|
1632
|
+
throw new CliError("--connect and --disconnect cannot be combined.");
|
|
1633
|
+
}
|
|
1634
|
+
const valueAfter = (flag) => {
|
|
1635
|
+
const i = args.indexOf(flag);
|
|
1636
|
+
if (i < 0) return undefined;
|
|
1637
|
+
const v = args[i + 1];
|
|
1638
|
+
if (!v || v.startsWith("-")) throw new CliError(`Missing value after ${flag}.`);
|
|
1639
|
+
return v;
|
|
1640
|
+
};
|
|
1641
|
+
let result;
|
|
1642
|
+
if (wantsRename) {
|
|
1643
|
+
const { runRenameCommand } = await import("../src/hooks/cloud-enrollment-cli");
|
|
1644
|
+
result = await runRenameCommand(valueAfter("--machine-label"));
|
|
1645
|
+
} else if (wantsDisconnect) {
|
|
1646
|
+
const { runDisconnectCommand } = await import("../src/hooks/cloud-enrollment-cli");
|
|
1647
|
+
result = runDisconnectCommand();
|
|
1648
|
+
} else {
|
|
1649
|
+
const { hostname } = await import("node:os");
|
|
1650
|
+
const { runConnectCommand } = await import("../src/hooks/cloud-enrollment-cli");
|
|
1651
|
+
result = await runConnectCommand({
|
|
1652
|
+
url: valueAfter("--connect"),
|
|
1653
|
+
token: valueAfter("--token"),
|
|
1654
|
+
machineId: valueAfter("--machine-id"),
|
|
1655
|
+
machineLabel: valueAfter("--machine-label"),
|
|
1656
|
+
defaultMachineId: hostname(),
|
|
1657
|
+
// Transcripts are what connecting is FOR, so they default on and the
|
|
1658
|
+
// disclosure is made at the point of connection rather than hidden
|
|
1659
|
+
// behind an opt-in flag most people never discover — a dashboard
|
|
1660
|
+
// showing only decisions is the empty-dashboard problem in a
|
|
1661
|
+
// different costume. --no-transcripts is the explicit way out, and
|
|
1662
|
+
// `failproofai config --status` always says which is in effect.
|
|
1663
|
+
sessions: !args.includes("--no-transcripts"),
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
for (const line of result.lines) {
|
|
1667
|
+
if (result.exitCode === 0) console.log(line);
|
|
1668
|
+
else console.error(line);
|
|
1669
|
+
}
|
|
1670
|
+
await track("cli_cloud_enrollment", {
|
|
1671
|
+
action: wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect",
|
|
1672
|
+
ok: result.exitCode === 0,
|
|
1673
|
+
});
|
|
1674
|
+
await exitAfterFlush(result.exitCode);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const pauseIdx = args.indexOf("--pause");
|
|
1679
|
+
const wantsResume = args.includes("--resume");
|
|
1680
|
+
const wantsStatus = args.includes("--status");
|
|
1681
|
+
if (pauseIdx >= 0 || wantsResume || wantsStatus) {
|
|
1682
|
+
const chosen = [pauseIdx >= 0 && "--pause", wantsResume && "--resume", wantsStatus && "--status"].filter(Boolean);
|
|
1683
|
+
if (chosen.length > 1) {
|
|
1684
|
+
throw new CliError(`${chosen.join(" and ")} cannot be combined.`);
|
|
1685
|
+
}
|
|
1686
|
+
const sessionIdx = args.indexOf("--session");
|
|
1687
|
+
if (sessionIdx >= 0 && !args[sessionIdx + 1]) {
|
|
1688
|
+
throw new CliError("Missing session id after --session.");
|
|
1689
|
+
}
|
|
1690
|
+
// A bare `--pause` takes the default duration, so only treat the next
|
|
1691
|
+
// token as a duration when it isn't another flag.
|
|
1692
|
+
const next = pauseIdx >= 0 ? args[pauseIdx + 1] : undefined;
|
|
1693
|
+
const duration = next && !next.startsWith("-") ? next : undefined;
|
|
1694
|
+
|
|
1695
|
+
const { runPauseCommand } = await import("../src/hooks/session-pause-cli");
|
|
1696
|
+
const result = runPauseCommand({
|
|
1697
|
+
action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
|
|
1698
|
+
duration,
|
|
1699
|
+
sessionId: sessionIdx >= 0 ? args[sessionIdx + 1] : undefined,
|
|
1700
|
+
all: args.includes("--all"),
|
|
1701
|
+
cwd: process.cwd(),
|
|
1702
|
+
});
|
|
1703
|
+
// `--status` answers "what is this machine's state?", which is both
|
|
1704
|
+
// halves: whether enforcement is paused AND whether cloud is connected.
|
|
1705
|
+
if (wantsStatus) {
|
|
1706
|
+
const { connectionStatusLines } = await import("../src/hooks/cloud-enrollment-cli");
|
|
1707
|
+
for (const line of connectionStatusLines()) console.log(line);
|
|
1708
|
+
// Always printed, including where reports can never work: "why am I
|
|
1709
|
+
// not getting them?" is the question --status exists to answer, and an
|
|
1710
|
+
// omitted line answers it with silence.
|
|
1711
|
+
console.log("");
|
|
1712
|
+
}
|
|
1713
|
+
for (const line of result.lines) {
|
|
1714
|
+
if (result.exitCode === 0) console.log(line);
|
|
1715
|
+
else console.error(line);
|
|
1716
|
+
}
|
|
1717
|
+
await track("cli_pause_invoked", {
|
|
1718
|
+
action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
|
|
1719
|
+
ok: result.exitCode === 0,
|
|
1720
|
+
affected: result.affected,
|
|
1721
|
+
});
|
|
1722
|
+
await exitAfterFlush(result.exitCode);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
777
1726
|
const { runConfigureWizard } = await import("../src/hooks/configure-wizard");
|
|
778
1727
|
const result = await runConfigureWizard();
|
|
779
1728
|
await track("cli_configure_invoked", {
|
|
780
1729
|
applied: result.applied,
|
|
781
|
-
|
|
1730
|
+
// `target` and `scopes`, not `scope`: the wizard rework replaced that
|
|
1731
|
+
// field and nothing caught it, because `.mjs` is outside the tsconfig
|
|
1732
|
+
// include so `tsc --noEmit` never type-checks this file. Every
|
|
1733
|
+
// `cli_configure_invoked` since has reported `scope: null`.
|
|
1734
|
+
target: result.target ?? null,
|
|
1735
|
+
scopes: result.scopes ?? [],
|
|
782
1736
|
cli_count: result.clis?.length ?? 0,
|
|
1737
|
+
abort: result.abort ?? null,
|
|
783
1738
|
});
|
|
784
|
-
|
|
1739
|
+
// `abort` is the field `WizardAbort` exists to expose, and exiting 0
|
|
1740
|
+
// regardless discarded it: a fleet script could not tell "the user pressed
|
|
1741
|
+
// Esc" from "this machine could not install the required daemon and is
|
|
1742
|
+
// unconfigured". Cancelling is not a failure; the other two are.
|
|
1743
|
+
await exitAfterFlush(!result.applied && result.abort && result.abort !== "cancelled" ? 1 : 0);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Shared by both "unknown thing" guards below, so a mistyped SUBCOMMAND gets
|
|
1748
|
+
// the same nearest-match treatment a mistyped flag already got.
|
|
1749
|
+
function levenshtein(a, b) {
|
|
1750
|
+
const m = a.length, n = b.length;
|
|
1751
|
+
const dp = Array.from({ length: m + 1 }, (_, i) =>
|
|
1752
|
+
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
1753
|
+
);
|
|
1754
|
+
for (let i = 1; i <= m; i++)
|
|
1755
|
+
for (let j = 1; j <= n; j++)
|
|
1756
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
1757
|
+
? dp[i - 1][j - 1]
|
|
1758
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
1759
|
+
return dp[m][n];
|
|
785
1760
|
}
|
|
786
1761
|
|
|
787
1762
|
// Unknown flag guard — must appear after all known-flag branches
|
|
@@ -789,20 +1764,7 @@ WHAT IT DOES
|
|
|
789
1764
|
const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
|
|
790
1765
|
|
|
791
1766
|
if (unknownFlag) {
|
|
792
|
-
|
|
793
|
-
const m = a.length, n = b.length;
|
|
794
|
-
const dp = Array.from({ length: m + 1 }, (_, i) =>
|
|
795
|
-
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
796
|
-
);
|
|
797
|
-
for (let i = 1; i <= m; i++)
|
|
798
|
-
for (let j = 1; j <= n; j++)
|
|
799
|
-
dp[i][j] = a[i - 1] === b[j - 1]
|
|
800
|
-
? dp[i - 1][j - 1]
|
|
801
|
-
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
802
|
-
return dp[m][n];
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const primary = ["--version", "--help", "--hook", "policies", "policy", "auth", "audit"];
|
|
1767
|
+
const primary = ["--version", "--help", "--hook", "policies", "policy", "audit"];
|
|
806
1768
|
const closest = primary.reduce((best, flag) => {
|
|
807
1769
|
const dist = levenshtein(unknownFlag, flag);
|
|
808
1770
|
return dist < best.dist ? { flag, dist } : best;
|
|
@@ -818,29 +1780,27 @@ WHAT IT DOES
|
|
|
818
1780
|
// Unknown subcommand guard (non-flag args that aren't a known subcommand)
|
|
819
1781
|
const unknownSubcommand = args.find(a => !a.startsWith("-") && !SUBCOMMANDS.includes(a));
|
|
820
1782
|
if (unknownSubcommand) {
|
|
1783
|
+
// Nearest match rather than a hardcoded "policies", which was wrong for
|
|
1784
|
+
// every input that was not a typo of it. `auth` made that concrete: it was
|
|
1785
|
+
// a real subcommand until this release, so an old script or plain muscle
|
|
1786
|
+
// memory lands here, and answering "did you mean policies?" sends someone
|
|
1787
|
+
// to the one command that has nothing to do with what they typed.
|
|
1788
|
+
const nearest = SUBCOMMANDS.reduce(
|
|
1789
|
+
(best, name) => {
|
|
1790
|
+
const dist = levenshtein(unknownSubcommand, name);
|
|
1791
|
+
return dist < best.dist ? { name, dist } : best;
|
|
1792
|
+
},
|
|
1793
|
+
{ name: SUBCOMMANDS[0], dist: Infinity },
|
|
1794
|
+
);
|
|
821
1795
|
throw new CliError(
|
|
822
1796
|
`Unknown command: ${unknownSubcommand}\n` +
|
|
823
|
-
`Did you mean: failproofai
|
|
1797
|
+
`Did you mean: failproofai ${nearest.name}?\n` +
|
|
824
1798
|
`Run \`failproofai --help\` for usage details.`
|
|
825
1799
|
);
|
|
826
1800
|
}
|
|
827
1801
|
|
|
828
|
-
// First-run onboarding — on the first bare `failproofai` invocation, run the
|
|
829
|
-
// configure wizard (which also fires the post-setup audit) BEFORE the
|
|
830
|
-
// dashboard. Unlike before, we then fall through to launch the dashboard, so a
|
|
831
|
-
// fresh user gets: setup → audit → dashboard, and every later `failproofai`
|
|
832
|
-
// goes straight to the dashboard. Best-effort: any error must not block launch.
|
|
833
|
-
if (args.length === 0) {
|
|
834
|
-
try {
|
|
835
|
-
const { maybeFirstRunConfigure } = await import("../src/hooks/configure-wizard");
|
|
836
|
-
await maybeFirstRunConfigure();
|
|
837
|
-
} catch {
|
|
838
|
-
// First-run onboarding is non-critical; fall through to the dashboard.
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
1802
|
// Dashboard launch — always production mode. Runs on every bare `failproofai`
|
|
843
|
-
// (
|
|
1803
|
+
// (first-run onboarding, if any, already ran above).
|
|
844
1804
|
const { launch } = await import("../scripts/launch");
|
|
845
1805
|
launch("start");
|
|
846
1806
|
}
|