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
|
@@ -11,29 +11,57 @@
|
|
|
11
11
|
* and results in an empty hook list for that file. Builtins continue normally.
|
|
12
12
|
*/
|
|
13
13
|
import { resolve, isAbsolute, basename } from "node:path";
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
14
15
|
import { existsSync, readdirSync } from "node:fs";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
15
17
|
import { pathToFileURL } from "node:url";
|
|
16
|
-
import {
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
17
19
|
import { hookLogWarn, hookLogError, hookLogInfo } from "./hook-logger";
|
|
18
|
-
import { getCustomHooks, clearCustomHooks } from "./custom-hooks-registry";
|
|
19
|
-
import {
|
|
20
|
+
import { customPolicies, getCustomHooks, clearCustomHooks } from "./custom-hooks-registry";
|
|
21
|
+
import {
|
|
22
|
+
findDistIndex,
|
|
23
|
+
rewriteFileTree,
|
|
24
|
+
TMP_SUFFIX,
|
|
25
|
+
cleanupTmpFiles,
|
|
26
|
+
isTmpArtifact,
|
|
27
|
+
sweepStaleTmpArtifacts,
|
|
28
|
+
} from "./loader-utils";
|
|
20
29
|
import { findProjectConfigDir } from "./hooks-config";
|
|
21
30
|
import { trackHookEvent } from "./hook-telemetry";
|
|
22
31
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
23
32
|
import type { CustomHook } from "./policy-types";
|
|
33
|
+
import type { CloudManagedPolicyArtifact } from "./cloud-managed-policies";
|
|
34
|
+
import { customPoliciesDir, shimsDir } from "./fp-home";
|
|
24
35
|
|
|
25
36
|
const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
|
|
26
37
|
|
|
27
38
|
/** Regex matching convention policy filenames: *policies.{js,mjs,ts} */
|
|
28
39
|
const CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
|
|
29
40
|
|
|
41
|
+
/** Monotonic suffix for physically distinct temporary module trees. Bun
|
|
42
|
+
* ignores URL query strings when caching imports, so a query-only cache
|
|
43
|
+
* buster silently drops custom policies after the first request in the warm
|
|
44
|
+
* daemon worker. A unique filename is honored by both Bun and Node. */
|
|
45
|
+
let loadSequence = 0;
|
|
46
|
+
|
|
47
|
+
interface CachedPolicyModule {
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
hooks: CustomHook[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One retained module instance per source entry. Reusing its registered hook
|
|
53
|
+
* functions prevents an unbounded ESM module-cache leak in the resident worker.
|
|
54
|
+
* A source-tree change replaces this entry and imports one new module. */
|
|
55
|
+
const policyModuleCache = new Map<string, CachedPolicyModule>();
|
|
30
56
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
57
|
+
* Cap, for the reason `gitBranchCache` states and this cache did not carry
|
|
58
|
+
* over: a warm worker touching many projects over its lifetime must not grow
|
|
59
|
+
* this unboundedly. Keyed by absolute policy-file path and holding cloned hook
|
|
60
|
+
* closures, so the entries are not small. Cleared wholesale rather than evicted
|
|
61
|
+
* one at a time — the next load simply re-imports, which is the same cost the
|
|
62
|
+
* cache exists to avoid paying twice, not a correctness change.
|
|
35
63
|
*/
|
|
36
|
-
|
|
64
|
+
const POLICY_MODULE_CACHE_MAX_ENTRIES = 500;
|
|
37
65
|
|
|
38
66
|
/** Script extensions we could load, used to spot near-miss filenames. */
|
|
39
67
|
const LOADABLE_EXT_RE = /\.(js|mjs|ts)$/;
|
|
@@ -76,7 +104,11 @@ export function findSkippedPolicyFiles(dir: string): string[] {
|
|
|
76
104
|
e.isFile() &&
|
|
77
105
|
LOADABLE_EXT_RE.test(e.name) &&
|
|
78
106
|
!CONVENTION_FILE_RE.test(e.name) &&
|
|
79
|
-
!e.name.endsWith(".d.ts")
|
|
107
|
+
!e.name.endsWith(".d.ts") &&
|
|
108
|
+
// Never our own generated files. They are `.mjs` and never match the
|
|
109
|
+
// convention, so each one a killed load left behind was reported to
|
|
110
|
+
// the user as a policy file that would not load.
|
|
111
|
+
!isTmpArtifact(e.name),
|
|
80
112
|
)
|
|
81
113
|
.map((e) => e.name)
|
|
82
114
|
.sort((a, b) => a.localeCompare(b));
|
|
@@ -85,13 +117,69 @@ export function findSkippedPolicyFiles(dir: string): string[] {
|
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
|
|
120
|
+
/**
|
|
121
|
+
* How long a policy module gets to finish evaluating its top level.
|
|
122
|
+
*
|
|
123
|
+
* Matches the 10s `handler.ts` gives a policy's `fn` to run, for the same
|
|
124
|
+
* reason: a policy file is user code, and user code that hangs must cost one
|
|
125
|
+
* skipped policy rather than the process.
|
|
126
|
+
*
|
|
127
|
+
* `FAILPROOFAI_POLICY_LOAD_TIMEOUT_MS` shortens it for tests — a suite proving
|
|
128
|
+
* the deadline fires should not have to sit out the real one.
|
|
129
|
+
*/
|
|
130
|
+
function moduleLoadTimeoutMs(): number {
|
|
131
|
+
const override = Number(process.env.FAILPROOFAI_POLICY_LOAD_TIMEOUT_MS);
|
|
132
|
+
return Number.isFinite(override) && override > 0 ? override : 10_000;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `import()`, but it cannot hang forever.
|
|
137
|
+
*
|
|
138
|
+
* A bare `await import(...)` on a module whose top level contains an `await`
|
|
139
|
+
* that never resolves never settles — it does not reject, so no `catch` and no
|
|
140
|
+
* `finally` anywhere above it ever runs. In the one-shot hook path that costs
|
|
141
|
+
* one hook process, which the agent CLI's own timeout reaps. In the warm daemon
|
|
142
|
+
* worker it holds `worker-server.ts`'s serialization chain forever, and every
|
|
143
|
+
* subsequent hook on the machine queues behind it and fail-closed denies.
|
|
144
|
+
*
|
|
145
|
+
* Losing the race does NOT cancel the import — nothing can. It stops us
|
|
146
|
+
* *waiting* on it, which is the part that matters, and the caller's `catch`
|
|
147
|
+
* below reports the file as failed to load. The abandoned module keeps its
|
|
148
|
+
* half-initialized entry in the ESM cache, but each load writes a uniquely
|
|
149
|
+
* named temporary tree, so a retry imports a different specifier and is never
|
|
150
|
+
* poisoned by it.
|
|
151
|
+
*/
|
|
152
|
+
async function importWithDeadline(fileUrl: string): Promise<void> {
|
|
153
|
+
const budget = moduleLoadTimeoutMs();
|
|
154
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
155
|
+
try {
|
|
156
|
+
await Promise.race([
|
|
157
|
+
import(/* webpackIgnore: true */ fileUrl),
|
|
158
|
+
new Promise<never>((_, reject) => {
|
|
159
|
+
timer = setTimeout(
|
|
160
|
+
() => reject(new Error(`timed out loading module after ${budget}ms`)),
|
|
161
|
+
budget,
|
|
162
|
+
);
|
|
163
|
+
timer.unref?.();
|
|
164
|
+
}),
|
|
165
|
+
]);
|
|
166
|
+
} finally {
|
|
167
|
+
if (timer) clearTimeout(timer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
88
171
|
/**
|
|
89
172
|
* Load a single policy file into the globalThis custom hooks registry.
|
|
90
173
|
* Does NOT clear the registry — caller is responsible for that.
|
|
91
174
|
*/
|
|
92
175
|
async function loadSingleFile(
|
|
93
176
|
absPath: string,
|
|
94
|
-
opts?: {
|
|
177
|
+
opts?: {
|
|
178
|
+
strict?: boolean;
|
|
179
|
+
conventionScope?: "project" | "user";
|
|
180
|
+
/** Cloud-managed policies pass their pinned digest for load-time re-verification. */
|
|
181
|
+
verifyEntrySha?: string;
|
|
182
|
+
},
|
|
95
183
|
): Promise<void> {
|
|
96
184
|
const g = globalThis as Record<string, unknown>;
|
|
97
185
|
g[LOADING_KEY] = true;
|
|
@@ -101,30 +189,48 @@ async function loadSingleFile(
|
|
|
101
189
|
const distIndex = await findDistIndex();
|
|
102
190
|
const distUrl = distIndex ? pathToFileURL(distIndex).href : null;
|
|
103
191
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
192
|
+
const sequence = ++loadSequence;
|
|
193
|
+
// pid + sequence is not unique enough on its own: two containers sharing a
|
|
194
|
+
// mounted HOME, or two pid namespaces, can collide — and on the shim's
|
|
195
|
+
// fallback path a collision means EEXIST, which fails the load OPEN. A
|
|
196
|
+
// random id removes the class. It costs nothing: `fingerprintTemporaryTree`
|
|
197
|
+
// normalises the whole suffix away, so the module cache still hits.
|
|
198
|
+
const tmpSuffix = `${TMP_SUFFIX}.${process.pid}.${sequence}.${randomUUID()}.mjs`;
|
|
199
|
+
tmpFiles = await rewriteFileTree(
|
|
200
|
+
absPath,
|
|
201
|
+
distUrl,
|
|
202
|
+
distIndex,
|
|
203
|
+
tmpSuffix,
|
|
204
|
+
opts?.verifyEntrySha,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const fingerprint = await fingerprintTemporaryTree(tmpFiles, tmpSuffix);
|
|
208
|
+
const cached = policyModuleCache.get(absPath);
|
|
209
|
+
if (cached?.fingerprint === fingerprint) {
|
|
210
|
+
for (const hook of cached.hooks) customPolicies.add({ ...hook });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const entryTmp = absPath + tmpSuffix;
|
|
215
|
+
const fileUrl = pathToFileURL(entryTmp).href;
|
|
216
|
+
const hooksBefore = getCustomHooks().length;
|
|
217
|
+
await importWithDeadline(fileUrl);
|
|
218
|
+
if (policyModuleCache.size >= POLICY_MODULE_CACHE_MAX_ENTRIES) policyModuleCache.clear();
|
|
219
|
+
policyModuleCache.set(absPath, {
|
|
220
|
+
fingerprint,
|
|
221
|
+
hooks: getCustomHooks()
|
|
222
|
+
.slice(hooksBefore)
|
|
223
|
+
.map((hook) => ({ ...hook })),
|
|
224
|
+
});
|
|
121
225
|
} catch (err) {
|
|
122
226
|
const msg = err instanceof Error ? err.message : String(err);
|
|
123
227
|
const errorType = /Cannot find module|MODULE_NOT_FOUND|ENOENT/i.test(msg)
|
|
124
228
|
? "module_not_found"
|
|
125
229
|
: /SyntaxError|Unexpected token/i.test(msg)
|
|
126
230
|
? "syntax_error"
|
|
127
|
-
:
|
|
231
|
+
: /timed out loading module/i.test(msg)
|
|
232
|
+
? "load_timeout"
|
|
233
|
+
: "runtime_error";
|
|
128
234
|
void trackHookEvent(getInstanceId(), "custom_hooks_load_error", {
|
|
129
235
|
error_type: errorType,
|
|
130
236
|
is_convention: !!opts?.conventionScope,
|
|
@@ -139,6 +245,18 @@ async function loadSingleFile(
|
|
|
139
245
|
}
|
|
140
246
|
}
|
|
141
247
|
|
|
248
|
+
async function fingerprintTemporaryTree(tmpFiles: string[], tmpSuffix: string): Promise<string> {
|
|
249
|
+
const hash = createHash("sha256");
|
|
250
|
+
for (const tmpFile of [...tmpFiles].sort()) {
|
|
251
|
+
// The generated filenames change per load. Normalize both the logical path
|
|
252
|
+
// and rewritten import specifiers so identical source graphs hash equally.
|
|
253
|
+
hash.update(tmpFile.replaceAll(tmpSuffix, TMP_SUFFIX));
|
|
254
|
+
const contents = await readFile(tmpFile, "utf8");
|
|
255
|
+
hash.update(contents.replaceAll(tmpSuffix, TMP_SUFFIX));
|
|
256
|
+
}
|
|
257
|
+
return hash.digest("hex");
|
|
258
|
+
}
|
|
259
|
+
|
|
142
260
|
/**
|
|
143
261
|
* Load a single explicit custom hooks file (legacy API).
|
|
144
262
|
* Clears the registry, loads the file, returns registered hooks.
|
|
@@ -214,7 +332,11 @@ function warnSkippedPolicyFiles(dir: string, scope: "project" | "user"): void {
|
|
|
214
332
|
|
|
215
333
|
export async function loadAllCustomHooks(
|
|
216
334
|
customPoliciesPaths: string | string[] | undefined,
|
|
217
|
-
opts?: {
|
|
335
|
+
opts?: {
|
|
336
|
+
sessionCwd?: string;
|
|
337
|
+
customPoliciesEnabled?: boolean;
|
|
338
|
+
cloudManagedPolicies?: CloudManagedPolicyArtifact[];
|
|
339
|
+
},
|
|
218
340
|
): Promise<LoadAllResult> {
|
|
219
341
|
clearCustomHooks();
|
|
220
342
|
|
|
@@ -236,6 +358,43 @@ export async function loadAllCustomHooks(
|
|
|
236
358
|
// convention passes.
|
|
237
359
|
const loadedPaths = new Set<string>();
|
|
238
360
|
|
|
361
|
+
// Cloud policies keyed by the artifact they resolve to — and artifacts are
|
|
362
|
+
// CONTENT-ADDRESSED, so two policies whose source is byte-identical share one
|
|
363
|
+
// path. Building this map with a plain `new Map(entries)` let the last one win
|
|
364
|
+
// silently, and since `loadedPaths` then loads that file exactly once, the
|
|
365
|
+
// other policy vanished: its id never appeared in `failproofai policies` or in
|
|
366
|
+
// the decision log, and — the part that matters — ITS EFFECT DECIDED NOTHING.
|
|
367
|
+
// An `enforce` policy sharing bytes with an `observe` one was downgraded to
|
|
368
|
+
// observation, which is a silent enforcement gap, and the reverse turned a
|
|
369
|
+
// measurement-only rollout into live denials.
|
|
370
|
+
//
|
|
371
|
+
// It could not happen before the flattening: `deployments/<n>/<id>.mjs` gave
|
|
372
|
+
// every policy its own path even when the bytes matched.
|
|
373
|
+
//
|
|
374
|
+
// One module instance is right — `customPolicies.add` is an unconditional
|
|
375
|
+
// push, so importing the file twice would register every hook twice and run
|
|
376
|
+
// the policy twice per event. So the collision is resolved rather than
|
|
377
|
+
// avoided, and it resolves toward ENFORCEMENT: over-enforcing is visible to
|
|
378
|
+
// whoever hits it, while under-enforcing is exactly the silent failure this
|
|
379
|
+
// codebase exists to remove. It is also announced, so an operator can give the
|
|
380
|
+
// two policies distinguishable source instead of living with the merge.
|
|
381
|
+
const cloudManagedByPath = new Map<string, CloudManagedPolicyArtifact>();
|
|
382
|
+
for (const policy of opts?.cloudManagedPolicies ?? []) {
|
|
383
|
+
const key = resolve(policy.path);
|
|
384
|
+
const existing = cloudManagedByPath.get(key);
|
|
385
|
+
if (!existing) {
|
|
386
|
+
cloudManagedByPath.set(key, policy);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
hookLogWarn(
|
|
390
|
+
`cloud-managed policies ${existing.id} and ${policy.id} have identical source, so they share ` +
|
|
391
|
+
`one artifact and load as one policy; enforcing it if either asks to enforce`,
|
|
392
|
+
);
|
|
393
|
+
if (existing.effect !== "enforce" && policy.effect === "enforce") {
|
|
394
|
+
cloudManagedByPath.set(key, policy);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
239
398
|
// 1. Explicit custom policy paths. Accept a string for callers/configs using
|
|
240
399
|
// the legacy singular form.
|
|
241
400
|
for (const customPoliciesPath of typeof customPoliciesPaths === "string"
|
|
@@ -248,9 +407,23 @@ export async function loadAllCustomHooks(
|
|
|
248
407
|
if (!loadedPaths.has(absPath)) {
|
|
249
408
|
loadedPaths.add(absPath);
|
|
250
409
|
const hooksBefore = getCustomHooks().length;
|
|
251
|
-
|
|
410
|
+
// A cloud-managed policy re-verifies its pinned digest at load, binding
|
|
411
|
+
// the imported bytes to what desired-state promised.
|
|
412
|
+
await loadSingleFile(absPath, {
|
|
413
|
+
verifyEntrySha: cloudManagedByPath.get(absPath)?.sha256,
|
|
414
|
+
});
|
|
252
415
|
for (const hook of getCustomHooks().slice(hooksBefore)) {
|
|
253
|
-
|
|
416
|
+
const cloudManaged = cloudManagedByPath.get(absPath);
|
|
417
|
+
const tagged = hook as CustomHook & {
|
|
418
|
+
__policyId?: string;
|
|
419
|
+
__cloudManaged?: CloudManagedPolicyArtifact;
|
|
420
|
+
};
|
|
421
|
+
if (cloudManaged) {
|
|
422
|
+
tagged.__cloudManaged = cloudManaged;
|
|
423
|
+
tagged.__policyId = `cloud:${cloudManaged.id}@${cloudManaged.version}:${hook.name}`;
|
|
424
|
+
} else {
|
|
425
|
+
tagged.__policyId = customPolicyId(absPath, hook.name);
|
|
426
|
+
}
|
|
254
427
|
}
|
|
255
428
|
}
|
|
256
429
|
} else {
|
|
@@ -268,21 +441,51 @@ export async function loadAllCustomHooks(
|
|
|
268
441
|
// exactly that — and it is discovered here as well. Skip what step 1 loaded,
|
|
269
442
|
// or the file is imported twice and every hook in it fires twice per event.
|
|
270
443
|
const projectDir = resolve(projectRoot, ".failproofai", "policies");
|
|
271
|
-
|
|
444
|
+
// Clear out generated files a killed load left behind, before anything scans
|
|
445
|
+
// this directory. The temporary tree has to be written beside the sources for
|
|
446
|
+
// a rewritten relative import to resolve, and its name now carries a pid and
|
|
447
|
+
// sequence number — so unlike the old fixed name, each abnormal termination
|
|
448
|
+
// leaks one file permanently rather than leaving one that the next load
|
|
449
|
+
// overwrites. Best-effort and age-gated, so it can never remove a tree
|
|
450
|
+
// another process is still importing.
|
|
451
|
+
// WHICH SCOPE this directory is depends on whether it IS the user directory.
|
|
452
|
+
// Run an agent CLI with the cwd set to $HOME and `<cwd>/.failproofai/policies`
|
|
453
|
+
// resolves to `~/.failproofai/policies` — the user scope itself. Layout 3 made
|
|
454
|
+
// that reachable by collapsing `customPoliciesDir()` onto `policiesDir()`;
|
|
455
|
+
// layout 2 nested the user directory one level deeper, so the two could never
|
|
456
|
+
// be the same path.
|
|
457
|
+
//
|
|
458
|
+
// Tagging it `project` there would make a policy's id depend on where the
|
|
459
|
+
// agent happened to be launched from — `convention:project:x-policies.mjs:foo`
|
|
460
|
+
// from $HOME and `convention:user:x-policies.mjs:foo` from anywhere else — and
|
|
461
|
+
// a disable is RECORDED AGAINST THAT ID. So a user who switched a policy off
|
|
462
|
+
// would find it firing again whenever they worked out of their home
|
|
463
|
+
// directory, with nothing to explain it. The files are the user's global
|
|
464
|
+
// policies; that the cwd also points at them is incidental.
|
|
465
|
+
const projectScope: "project" | "user" = projectDir === customPoliciesDir() ? "user" : "project";
|
|
466
|
+
if (conventionEnabled) {
|
|
467
|
+
void sweepStaleTmpArtifacts(projectDir);
|
|
468
|
+
warnSkippedPolicyFiles(projectDir, projectScope);
|
|
469
|
+
}
|
|
470
|
+
// The shim directory leaks the same way and is swept on the same terms. It is
|
|
471
|
+
// ours alone and outside the policy tree, so nothing else would ever reap it;
|
|
472
|
+
// the age gate is what keeps this from removing a shim another process is
|
|
473
|
+
// still importing.
|
|
474
|
+
void sweepStaleTmpArtifacts(shimsDir());
|
|
272
475
|
const projectFiles = conventionEnabled
|
|
273
476
|
? discoverPolicyFiles(projectDir).filter((f) => !loadedPaths.has(f))
|
|
274
477
|
: [];
|
|
275
478
|
for (const file of projectFiles) {
|
|
276
479
|
loadedPaths.add(file);
|
|
277
480
|
const hooksBefore = getCustomHooks().length;
|
|
278
|
-
await loadSingleFile(file, { conventionScope:
|
|
481
|
+
await loadSingleFile(file, { conventionScope: projectScope });
|
|
279
482
|
const newHooks = getCustomHooks().slice(hooksBefore);
|
|
280
483
|
for (const hook of newHooks) {
|
|
281
|
-
(hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId(
|
|
484
|
+
(hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId(projectScope, basename(file), hook.name);
|
|
282
485
|
}
|
|
283
486
|
if (newHooks.length > 0) {
|
|
284
487
|
conventionSources.push({
|
|
285
|
-
scope:
|
|
488
|
+
scope: projectScope,
|
|
286
489
|
file: basename(file),
|
|
287
490
|
hookNames: newHooks.map((h) => h.name),
|
|
288
491
|
});
|
|
@@ -303,8 +506,11 @@ export async function loadAllCustomHooks(
|
|
|
303
506
|
// so the second import is a no-op — but Node honours the query and would
|
|
304
507
|
// double-register. The binary runs under Bun and the tests under Node, so the
|
|
305
508
|
// bug is invisible from both sides. Do not rely on that; dedupe the paths.
|
|
306
|
-
const userDir =
|
|
307
|
-
if (conventionEnabled && userDir !== projectDir)
|
|
509
|
+
const userDir = customPoliciesDir();
|
|
510
|
+
if (conventionEnabled && userDir !== projectDir) {
|
|
511
|
+
void sweepStaleTmpArtifacts(userDir);
|
|
512
|
+
warnSkippedPolicyFiles(userDir, "user");
|
|
513
|
+
}
|
|
308
514
|
const userFiles =
|
|
309
515
|
conventionEnabled && userDir !== projectDir
|
|
310
516
|
? discoverPolicyFiles(userDir).filter((f) => !loadedPaths.has(f))
|
|
@@ -6,17 +6,15 @@
|
|
|
6
6
|
* request, a 4-byte big-endian u32 length prefix followed by that many
|
|
7
7
|
* bytes of UTF-8 JSON, camelCase fields, `"type"` as the tag.
|
|
8
8
|
*
|
|
9
|
-
* This module
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* on a machine that was never daemon-configured, or fail closed on one that
|
|
13
|
-
* was. See `isDaemonConfigured`.
|
|
9
|
+
* This module reports whether a daemon request succeeded, was unreachable, or
|
|
10
|
+
* used an incompatible protocol. The caller (`bin/failproofai.mjs`) owns the
|
|
11
|
+
* fail-closed response for daemon-configured machines.
|
|
14
12
|
*/
|
|
15
13
|
import { createConnection } from "node:net";
|
|
16
|
-
import { resolve } from "node:path";
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
14
|
import type { IntegrationType } from "./types";
|
|
19
|
-
import {
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { daemonSocket as daemonSocketPath } from "./fp-home";
|
|
17
|
+
import { readConfig } from "./fp-config";
|
|
20
18
|
|
|
21
19
|
const PROTOCOL_VERSION = 1;
|
|
22
20
|
|
|
@@ -66,9 +64,90 @@ export interface DaemonHookResponse {
|
|
|
66
64
|
stderr: string;
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Why an attempt failed. BOTH cases deny; they differ only in what the user is
|
|
69
|
+
* told to do about it.
|
|
70
|
+
*
|
|
71
|
+
* `protocol-mismatch` means a daemon answered, so it is alive — we simply
|
|
72
|
+
* cannot speak its wire format. The cause is known and benign: the CLI upgraded
|
|
73
|
+
* via npm and the daemon has not been reinstalled yet, so the remedy is
|
|
74
|
+
* `failproofai config`, and naming it is the difference between a one-command
|
|
75
|
+
* fix and a support ticket.
|
|
76
|
+
*
|
|
77
|
+
* `unreachable` means nothing answered. A stopped service, a deleted socket and
|
|
78
|
+
* deliberate tampering are indistinguishable from here.
|
|
79
|
+
*
|
|
80
|
+
* This comment used to say a mismatch should FALL BACK to in-process
|
|
81
|
+
* evaluation, "because denying every tool call over it would take a working
|
|
82
|
+
* machine offline to protect nothing". `2926252` deliberately reversed that,
|
|
83
|
+
* and `bin/failproofai.mjs` now routes both failures to the same forced deny:
|
|
84
|
+
* on a daemon-configured machine the daemon is the ONLY evaluator, and a second
|
|
85
|
+
* policy engine reachable by breaking the first is not a guarantee. The
|
|
86
|
+
* distinction survives in the MESSAGE and nowhere else. Do not restore the
|
|
87
|
+
* fallback without revisiting that decision — the comment outliving the code is
|
|
88
|
+
* exactly how it would come back by accident.
|
|
89
|
+
*/
|
|
90
|
+
export type DaemonFailure = "unreachable" | "protocol-mismatch";
|
|
91
|
+
|
|
92
|
+
export type DaemonAttempt =
|
|
93
|
+
| { ok: true; response: DaemonHookResponse }
|
|
94
|
+
| { ok: false; failure: DaemonFailure };
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether a daemon is listening, regardless of how it was started.
|
|
98
|
+
*
|
|
99
|
+
* Distinct from `daemonServiceStatus()`, which asks the service manager. A
|
|
100
|
+
* daemon run by hand — during development, or from a container — is invisible
|
|
101
|
+
* to that check but is very much running and pulling policy, so reporting "not
|
|
102
|
+
* installed, so nothing will be pulled" at someone whose machine is actively
|
|
103
|
+
* enforcing is simply false.
|
|
104
|
+
*/
|
|
105
|
+
export function daemonSocketPresent(): boolean {
|
|
106
|
+
try {
|
|
107
|
+
return existsSync(socketPath());
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether the daemon socket accepts a connection *right now*.
|
|
115
|
+
*
|
|
116
|
+
* Narrower than `daemonSocketPresent()` (a stale socket file outlives the
|
|
117
|
+
* process that made it) and narrower than `attemptDaemonHook`, which reports
|
|
118
|
+
* `"unreachable"` for BOTH a refused connection and a request that was accepted
|
|
119
|
+
* and then never answered. Those two have different causes and different
|
|
120
|
+
* remedies — a socket that is not up yet versus a worker that cannot run — and
|
|
121
|
+
* telling them apart is the whole point of this function.
|
|
122
|
+
*
|
|
123
|
+
* Connect budget only; nothing is sent. Used by the health probe, never on the
|
|
124
|
+
* hook path.
|
|
125
|
+
*/
|
|
126
|
+
export async function daemonAcceptsConnections(
|
|
127
|
+
timeoutMs = DAEMON_CONNECT_TIMEOUT_MS,
|
|
128
|
+
): Promise<boolean> {
|
|
129
|
+
if (process.platform === "win32") return false;
|
|
130
|
+
return new Promise<boolean>((resolve) => {
|
|
131
|
+
let settled = false;
|
|
132
|
+
const done = (v: boolean) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
socket.removeAllListeners();
|
|
137
|
+
socket.destroy();
|
|
138
|
+
resolve(v);
|
|
139
|
+
};
|
|
140
|
+
const socket = createConnection({ path: socketPath() });
|
|
141
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
142
|
+
timer.unref?.();
|
|
143
|
+
socket.on("connect", () => done(true));
|
|
144
|
+
socket.on("error", () => done(false));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
69
148
|
function socketPath(): string {
|
|
70
149
|
if (process.env.FAILPROOFAI_DAEMON_SOCKET) return process.env.FAILPROOFAI_DAEMON_SOCKET;
|
|
71
|
-
return
|
|
150
|
+
return daemonSocketPath();
|
|
72
151
|
}
|
|
73
152
|
|
|
74
153
|
/**
|
|
@@ -81,8 +160,11 @@ function socketPath(): string {
|
|
|
81
160
|
*/
|
|
82
161
|
export function isDaemonConfigured(): boolean {
|
|
83
162
|
try {
|
|
84
|
-
return
|
|
163
|
+
return readConfig().daemon.configured === true;
|
|
85
164
|
} catch {
|
|
165
|
+
// Unreadable config reads as NOT daemon-configured. The failure direction
|
|
166
|
+
// is deliberate: the alternative is a machine that fails closed on every
|
|
167
|
+
// tool call because a config file got truncated.
|
|
86
168
|
return false;
|
|
87
169
|
}
|
|
88
170
|
}
|
|
@@ -94,32 +176,41 @@ function encodeFrame(value: unknown): Buffer {
|
|
|
94
176
|
return Buffer.concat([header, body]);
|
|
95
177
|
}
|
|
96
178
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
179
|
+
/** Attempts a daemon evaluation while preserving the failure category. */
|
|
180
|
+
export async function attemptDaemonHook(
|
|
181
|
+
req: DaemonHookRequest,
|
|
182
|
+
opts?: {
|
|
183
|
+
/**
|
|
184
|
+
* Override the RESPONSE budget only — the connect budget is never
|
|
185
|
+
* relaxed. Used by the health probe (`probeDaemonEndToEnd`), which runs
|
|
186
|
+
* from an interactive command rather than a hook and must not make a
|
|
187
|
+
* person wait out the full 30s hook budget to be told their daemon is
|
|
188
|
+
* broken. Never set this on the hook path: 30s is matched to the
|
|
189
|
+
* daemon's own read timeout so this side never gives up on a request the
|
|
190
|
+
* daemon is still honestly working on.
|
|
191
|
+
*/
|
|
192
|
+
responseTimeoutMs?: number;
|
|
193
|
+
},
|
|
194
|
+
): Promise<DaemonAttempt> {
|
|
105
195
|
// Windows never has a daemon in this phase (see the plan's platform
|
|
106
196
|
// scope) — skip the attempt outright rather than depending on however
|
|
107
197
|
// Node happens to behave when handed a POSIX socket path on Windows.
|
|
108
|
-
if (process.platform === "win32") return
|
|
198
|
+
if (process.platform === "win32") return { ok: false, failure: "unreachable" };
|
|
199
|
+
const responseBudget = opts?.responseTimeoutMs ?? DAEMON_RESPONSE_TIMEOUT_MS;
|
|
109
200
|
|
|
110
|
-
return new Promise<
|
|
201
|
+
return new Promise<DaemonAttempt>((resolvePromise) => {
|
|
111
202
|
let settled = false;
|
|
112
203
|
let timer: ReturnType<typeof setTimeout>;
|
|
113
204
|
const arm = (ms: number) => {
|
|
114
205
|
clearTimeout(timer);
|
|
115
|
-
timer = setTimeout(() =>
|
|
206
|
+
timer = setTimeout(() => fail("unreachable"), ms);
|
|
116
207
|
// Don't let this timer alone keep the process alive if everything else
|
|
117
208
|
// has already finished — it's always cleared on the success/failure
|
|
118
209
|
// paths below, this just avoids it being the sole reason a --hook
|
|
119
210
|
// process lingers if something upstream forgets to await us.
|
|
120
211
|
timer.unref?.();
|
|
121
212
|
};
|
|
122
|
-
const finish = (result:
|
|
213
|
+
const finish = (result: DaemonAttempt) => {
|
|
123
214
|
if (settled) return;
|
|
124
215
|
settled = true;
|
|
125
216
|
clearTimeout(timer);
|
|
@@ -127,6 +218,7 @@ export async function tryDaemonHook(req: DaemonHookRequest): Promise<DaemonHookR
|
|
|
127
218
|
socket.destroy();
|
|
128
219
|
resolvePromise(result);
|
|
129
220
|
};
|
|
221
|
+
const fail = (failure: DaemonFailure) => finish({ ok: false, failure });
|
|
130
222
|
|
|
131
223
|
const socket = createConnection({ path: socketPath() });
|
|
132
224
|
arm(DAEMON_CONNECT_TIMEOUT_MS);
|
|
@@ -137,7 +229,7 @@ export async function tryDaemonHook(req: DaemonHookRequest): Promise<DaemonHookR
|
|
|
137
229
|
socket.on("connect", () => {
|
|
138
230
|
// Connected: the daemon is demonstrably reachable, so the question is
|
|
139
231
|
// no longer "is it there" but "how long does this evaluation take".
|
|
140
|
-
arm(
|
|
232
|
+
arm(responseBudget);
|
|
141
233
|
socket.write(
|
|
142
234
|
encodeFrame({
|
|
143
235
|
type: "hook",
|
|
@@ -157,7 +249,7 @@ export async function tryDaemonHook(req: DaemonHookRequest): Promise<DaemonHookR
|
|
|
157
249
|
if (recvBuf.length < 4) return;
|
|
158
250
|
declaredLen = recvBuf.readUInt32BE(0);
|
|
159
251
|
if (declaredLen > MAX_FRAME_LEN) {
|
|
160
|
-
|
|
252
|
+
fail("unreachable");
|
|
161
253
|
return;
|
|
162
254
|
}
|
|
163
255
|
recvBuf = recvBuf.subarray(4);
|
|
@@ -170,12 +262,16 @@ export async function tryDaemonHook(req: DaemonHookRequest): Promise<DaemonHookR
|
|
|
170
262
|
try {
|
|
171
263
|
message = JSON.parse(body.toString("utf8")) as Record<string, unknown>;
|
|
172
264
|
} catch {
|
|
173
|
-
|
|
265
|
+
fail("unreachable");
|
|
174
266
|
return;
|
|
175
267
|
}
|
|
176
268
|
|
|
177
269
|
if (message.protocolVersion !== PROTOCOL_VERSION) {
|
|
178
|
-
|
|
270
|
+
// Catches BOTH directions. A newer CLI sends v2 and the daemon replies
|
|
271
|
+
// with an error stamped v1; an older CLI sends v1 and a newer daemon
|
|
272
|
+
// replies stamped v2. Either way the versions disagree here, and either
|
|
273
|
+
// way a daemon answered — so this is skew, not absence.
|
|
274
|
+
fail("protocol-mismatch");
|
|
179
275
|
return;
|
|
180
276
|
}
|
|
181
277
|
if (
|
|
@@ -184,16 +280,23 @@ export async function tryDaemonHook(req: DaemonHookRequest): Promise<DaemonHookR
|
|
|
184
280
|
typeof message.stdout === "string" &&
|
|
185
281
|
typeof message.stderr === "string"
|
|
186
282
|
) {
|
|
187
|
-
finish({
|
|
283
|
+
finish({
|
|
284
|
+
ok: true,
|
|
285
|
+
response: {
|
|
286
|
+
exitCode: message.exitCode,
|
|
287
|
+
stdout: message.stdout,
|
|
288
|
+
stderr: message.stderr,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
188
291
|
return;
|
|
189
292
|
}
|
|
190
|
-
// Anything else — an explicit `error` message
|
|
191
|
-
// confusion), or a well-formed-but-wrong-shape
|
|
192
|
-
// identically to a connection failure: no partial trust.
|
|
193
|
-
|
|
293
|
+
// Anything else — an explicit `error` message at a MATCHING protocol
|
|
294
|
+
// version, a `pong` (protocol confusion), or a well-formed-but-wrong-shape
|
|
295
|
+
// body — is treated identically to a connection failure: no partial trust.
|
|
296
|
+
fail("unreachable");
|
|
194
297
|
});
|
|
195
298
|
|
|
196
|
-
socket.on("error", () =>
|
|
197
|
-
socket.on("close", () =>
|
|
299
|
+
socket.on("error", () => fail("unreachable"));
|
|
300
|
+
socket.on("close", () => fail("unreachable"));
|
|
198
301
|
});
|
|
199
302
|
}
|