failproofai 1.0.0-beta.4 → 1.0.0-beta.6
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 +3 -3
- 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/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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +3 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/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/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/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +4 -2
- 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/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/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/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]__1nl3dvw._.js → [externals]__0r-zamh._.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]__094p1f1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g0vcwt._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0h3lfmp._.js → [root-of-the-server]__0ky_n0r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__18d-9oi._.js → [root-of-the-server]__0lk_935._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p7s8-d._.js → [root-of-the-server]__0pbp9fe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__10oa-v9._.js → [root-of-the-server]__0v2fpxe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0sb_5m8._.js → [root-of-the-server]__131ebum._.js} +3 -3
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0-dh5jy._.js → [root-of-the-server]__1_ps0jw._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l4vuqm._.js → [root-of-the-server]__1a7i2q4._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +5 -0
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +5 -0
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +5 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- 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]__0spkm68._.js +10 -10
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_a_gwn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1a2hcb_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e7-pa7._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rtdfof._.js → [root-of-the-server]__1rdruw4._.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]__1tyf0tc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rag5dg._.js → [root-of-the-server]__1yw368c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0igtyj5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gmzkwj._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0dwo94s.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +5 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- 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 +54 -12
- package/.next/standalone/.next/static/chunks/{2c90h1g6x8z8y.js → 0_2n13i94rnw5.js} +1 -1
- package/.next/standalone/.next/static/chunks/1bm0q12hgs22l.css +2 -0
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +1 -0
- package/.next/standalone/.next/static/chunks/23557z4e6dtsr.js +1 -0
- package/.next/standalone/.next/static/chunks/28zkwzu50puki.js +1 -0
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +2 -0
- package/.next/standalone/.next/static/chunks/3081gykdubv8b.js +1 -0
- package/.next/standalone/.next/static/chunks/{06jakt_xhoolk.js → 34fa65k0cldkj.js} +1 -1
- package/.next/standalone/.next/static/chunks/35v07j9gc1pvr.js +6 -0
- package/.next/standalone/.next/static/chunks/{2zb3m1tzq6h98.js → 3f_939wi3kfnx.js} +22 -22
- package/.next/standalone/.next/static/chunks/{31cqiv_cpgj9m.js → 3p1vpm2_-tfwm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ue9ak_xexiqh.js → 3rbpw_waieafm.js} +1 -1
- package/.next/standalone/.next/static/chunks/40ffnfpkeyvb0.js +1 -0
- package/.next/standalone/app/actions/get-hooks-config.ts +2 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +70 -0
- package/.next/standalone/app/actions/update-scheduled-audit.ts +44 -0
- package/.next/standalone/app/api/audit/run/route.ts +28 -0
- package/.next/standalone/app/api/audit/status/route.ts +13 -1
- package/.next/standalone/app/api/auth/status/route.ts +1 -1
- package/.next/standalone/app/audit/_components/auth-dialog.css +159 -0
- package/.next/standalone/app/audit/_components/auth-dialog.tsx +3 -0
- package/.next/standalone/app/audit/audit-styles.css +6 -149
- package/.next/standalone/app/settings/page.tsx +31 -0
- package/.next/standalone/app/settings/settings-client.tsx +488 -0
- package/.next/standalone/components/navbar.tsx +2 -0
- package/.next/standalone/lib/auth/api-server-client.ts +2 -3
- package/.next/standalone/lib/auth/auth-store.ts +4 -4
- package/.next/standalone/lib/codex-sessions.ts +2 -1
- package/.next/standalone/lib/dashboard-host.ts +101 -0
- package/.next/standalone/lib/install-check.ts +22 -7
- package/.next/standalone/lib/pi-sessions.ts +99 -14
- package/.next/standalone/lib/telemetry-enabled.ts +54 -0
- package/.next/standalone/lib/telemetry-id.ts +52 -1
- package/.next/standalone/lib/telemetry.ts +8 -4
- package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
- package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
- package/.next/standalone/node_modules/react/package.json +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
- package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
- package/.next/standalone/node_modules/react-dom/package.json +4 -4
- package/.next/standalone/package.json +7 -6
- package/.next/standalone/proxy.ts +89 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +197 -80
- package/dist/cli.mjs +17699 -15345
- package/dist/worker.mjs +1620 -257
- package/lib/auth/api-server-client.ts +2 -3
- package/lib/auth/auth-store.ts +4 -4
- package/lib/codex-sessions.ts +2 -1
- package/lib/dashboard-host.ts +101 -0
- package/lib/install-check.ts +22 -7
- package/lib/pi-sessions.ts +99 -14
- package/lib/telemetry-enabled.ts +54 -0
- package/lib/telemetry-id.ts +52 -1
- package/lib/telemetry.ts +8 -4
- package/package.json +7 -6
- package/scripts/install-telemetry.mjs +8 -1
- package/scripts/launch.ts +25 -3
- package/scripts/parse-script-args.ts +17 -1
- package/scripts/prune-standalone.mjs +20 -0
- package/scripts/translate-docs/readme-translator.ts +163 -4
- package/scripts/validate-mdx.ts +153 -13
- package/src/audit/audit-lock.ts +285 -0
- package/src/audit/audit-schedule.ts +111 -0
- package/src/audit/cache.ts +18 -4
- package/src/audit/cli-adapters/goose.ts +2 -1
- package/src/audit/cli-adapters/hermes.ts +28 -10
- package/src/audit/cli.ts +191 -35
- package/src/audit/dashboard-cache.ts +2 -1
- package/src/hooks/builtin-policies.ts +100 -3
- package/src/hooks/cloud-connection.ts +163 -14
- package/src/hooks/cloud-enrollment-cli.ts +119 -21
- package/src/hooks/cloud-enrollment.ts +87 -17
- package/src/hooks/cloud-introspect.ts +147 -0
- package/src/hooks/cloud-managed-policies.ts +33 -2
- package/src/hooks/collector-config.ts +32 -40
- package/src/hooks/configure-wizard.ts +696 -318
- package/src/hooks/custom-hooks-loader.ts +125 -11
- package/src/hooks/daemon-client.ts +83 -17
- package/src/hooks/daemon-download.ts +54 -2
- package/src/hooks/daemon-service.ts +595 -27
- package/src/hooks/first-run-gate.ts +39 -0
- package/src/hooks/fp-config.ts +497 -0
- package/src/hooks/fp-home.ts +342 -0
- package/src/hooks/fp-reset.ts +332 -0
- package/src/hooks/handler.ts +42 -1
- package/src/hooks/hook-activity-store.ts +35 -20
- package/src/hooks/hook-logger.ts +2 -1
- package/src/hooks/hook-telemetry.ts +5 -1
- package/src/hooks/hooks-config.ts +4 -3
- package/src/hooks/loader-utils.ts +166 -5
- package/src/hooks/manager.ts +5 -4
- package/src/hooks/onboarding-lock.ts +119 -0
- package/src/hooks/policy-types.ts +0 -7
- package/src/hooks/session-pause-cli.ts +15 -13
- package/src/hooks/session-pause.ts +36 -4
- package/src/hooks/setup-state.ts +256 -0
- package/src/hooks/tui.ts +4 -4
- package/src/hooks/worker-server.ts +53 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0szqzl8._.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/[root-of-the-server]__02r5bgf._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cmdyn_._.js +0 -3
- 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]__0ls0gsr._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n_uvv0._.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]__0sdrz-j._.js +0 -3
- 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]__14dd6h8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1j4u3yd._.js +0 -4
- 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_1q9xxca.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/0oe2-hp0_2tzs.js +0 -6
- package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +0 -1
- package/.next/standalone/.next/static/chunks/3h5_3u46gu9jx.js +0 -1
- package/.next/standalone/.next/static/chunks/3i27c3hcriawq.css +0 -1
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +0 -2
- package/.next/standalone/.next/static/chunks/43a1ywqsg9bq9.js +0 -1
- package/.next/standalone/Cargo.lock +0 -2222
- package/.next/standalone/Cargo.toml +0 -9
- package/.next/standalone/crates/CLOUD_POLICIES.md +0 -136
- package/.next/standalone/crates/PROTOCOL.md +0 -122
- package/.next/standalone/crates/failproofaid/Cargo.toml +0 -25
- package/.next/standalone/crates/failproofaid/src/cloud_client.rs +0 -484
- package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +0 -921
- package/.next/standalone/crates/failproofaid/src/lock.rs +0 -81
- package/.next/standalone/crates/failproofaid/src/main.rs +0 -532
- package/.next/standalone/crates/failproofaid/src/paths.rs +0 -201
- 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-collect/Cargo.toml +0 -43
- package/.next/standalone/crates/fpai-collect/src/config.rs +0 -412
- package/.next/standalone/crates/fpai-collect/src/cursor.rs +0 -520
- package/.next/standalone/crates/fpai-collect/src/delivery.rs +0 -472
- package/.next/standalone/crates/fpai-collect/src/filetail.rs +0 -484
- package/.next/standalone/crates/fpai-collect/src/health.rs +0 -369
- package/.next/standalone/crates/fpai-collect/src/lib.rs +0 -34
- package/.next/standalone/crates/fpai-collect/src/redact.rs +0 -516
- package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +0 -279
- package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +0 -619
- package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +0 -156
- package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +0 -715
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +0 -158
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +0 -512
- package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +0 -511
- package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +0 -586
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +0 -318
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +0 -579
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +0 -310
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +0 -448
- package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +0 -23
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +0 -176
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +0 -500
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +0 -315
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +0 -493
- package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +0 -201
- package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +0 -533
- package/.next/standalone/crates/fpai-collect/src/spool.rs +0 -458
- package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +0 -387
- package/.next/standalone/crates/fpai-collect/src/supervisor.rs +0 -423
- package/.next/standalone/crates/fpai-collect/src/uploader.rs +0 -600
- package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +0 -738
- package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +0 -393
- package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +0 -975
- package/.next/standalone/crates/fpai-collect/tests/config.rs +0 -286
- package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +0 -1019
- package/.next/standalone/crates/fpai-collect/tests/delivery.rs +0 -281
- package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +0 -1008
- package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +0 -1129
- package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +0 -621
- package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +0 -854
- package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +0 -1478
- package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +0 -1191
- package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +0 -290
- package/.next/standalone/crates/fpai-collect/tests/uploader.rs +0 -506
- 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/{StV6Lay7Q1cU4ysYCFJmH → Ptn8aBQl_cnNgOd0ebyv0}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{StV6Lay7Q1cU4ysYCFJmH → Ptn8aBQl_cnNgOd0ebyv0}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{StV6Lay7Q1cU4ysYCFJmH → Ptn8aBQl_cnNgOd0ebyv0}/_ssgManifest.js +0 -0
package/src/hooks/handler.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
|
43
43
|
import { readStdinPayload } from "./read-stdin";
|
|
44
44
|
import { readActiveCloudManagedPolicies, type CloudManagedPolicyArtifact } from "./cloud-managed-policies";
|
|
45
45
|
import { readActivePause, type ActivePause } from "./session-pause";
|
|
46
|
+
import { layoutWarningForHook } from "./fp-reset";
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Canonicalize an event name to PascalCase. Codex sends snake_case event names
|
|
@@ -182,6 +183,13 @@ export async function evaluateHookEvent(
|
|
|
182
183
|
opts?: EvaluateHookEventOptions,
|
|
183
184
|
): Promise<HookEventOutcome> {
|
|
184
185
|
const startTime = performance.now();
|
|
186
|
+
// A home from another layout resolves to no global config, so every builtin
|
|
187
|
+
// silently stops firing — the machine looks protected and is not. Say so on
|
|
188
|
+
// every call rather than deleting anything (a hook is unattended) or denying
|
|
189
|
+
// (a blanket deny takes UserPromptSubmit with it and locks the user out of
|
|
190
|
+
// their agent entirely — demonstrated on a real machine during this work).
|
|
191
|
+
const layoutWarning = layoutWarningForHook();
|
|
192
|
+
if (layoutWarning) hookLogWarn(layoutWarning);
|
|
185
193
|
try {
|
|
186
194
|
let parsed: Record<string, unknown> = {};
|
|
187
195
|
if (stdinPayload) {
|
|
@@ -286,7 +294,40 @@ export async function evaluateHookEvent(
|
|
|
286
294
|
// Cloud-managed policies are daemon-reconciled artifacts, but they use
|
|
287
295
|
// the same public JS policy API as local custom policies. Verify and add
|
|
288
296
|
// only the paths referenced by the atomically active generation.
|
|
289
|
-
|
|
297
|
+
//
|
|
298
|
+
// Wrapped, like `readConfigAt` and `readActivePause` above it. This call
|
|
299
|
+
// has fourteen throw sites — a malformed manifest, an unsafe id, an
|
|
300
|
+
// integrity mismatch, an unknown effect — and it sat bare inside a `try`
|
|
301
|
+
// whose only handler is a `finally`, so any of them aborted the entire
|
|
302
|
+
// evaluation. What that cost depended on where the hook ran, and none of
|
|
303
|
+
// the outcomes were the intended one: on a daemon machine the client
|
|
304
|
+
// fail-closed denies every tool call, and off the daemon path the throw
|
|
305
|
+
// reaches `bin/failproofai.mjs`'s outer catch, which exits 2 with nothing
|
|
306
|
+
// on stdout — a deny on Claude and Factory, but a logged warning followed
|
|
307
|
+
// by an ALLOW on Copilot, Cursor, Goose, Pi and Hermes, which read a
|
|
308
|
+
// decision off stdout and ignore the exit code. One corrupt byte was
|
|
309
|
+
// either a permanent machine-wide lockout or silent non-enforcement,
|
|
310
|
+
// depending on the CLI.
|
|
311
|
+
//
|
|
312
|
+
// Failing open here degrades ONE layer: builtins and local custom
|
|
313
|
+
// policies were already registered above and keep enforcing. That is the
|
|
314
|
+
// same trade `2ad735e5` made on the Rust side, and it is strictly better
|
|
315
|
+
// than the status quo, which already failed open on five of the twelve —
|
|
316
|
+
// just accidentally, and without saying so.
|
|
317
|
+
let cloudManagedPolicies: CloudManagedPolicyArtifact[] = [];
|
|
318
|
+
try {
|
|
319
|
+
cloudManagedPolicies = readActiveCloudManagedPolicies();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
+
hookLogWarn(
|
|
323
|
+
`[failproofai] cloud-managed policies could NOT be loaded and are not being enforced: ${msg}. ` +
|
|
324
|
+
`Local policies are unaffected. Run \`failproofai config --status\` to check this machine's enrolment.`,
|
|
325
|
+
);
|
|
326
|
+
void trackHookEvent(getInstanceId(), "cloud_managed_load_error", {
|
|
327
|
+
event_type: eventType,
|
|
328
|
+
cli,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
290
331
|
// Recorded on every row once a machine is managed, whether or not a cloud
|
|
291
332
|
// policy decided this event: "what was deployed here at the time" is a
|
|
292
333
|
// separate question from "what decided", and only the former can tell a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Disk persistence for hook activity entries using page-sized JSONL files.
|
|
3
3
|
*
|
|
4
|
-
* Storage location: ~/.failproofai/
|
|
4
|
+
* Storage location: ~/.failproofai/hook-activity/ (see fp-home.ts)
|
|
5
5
|
*
|
|
6
6
|
* File structure:
|
|
7
7
|
* - current.jsonl — actively written to, 0–PAGE_SIZE entries
|
|
@@ -22,19 +22,32 @@ import {
|
|
|
22
22
|
unlinkSync,
|
|
23
23
|
} from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
-
import { homedir } from "node:os";
|
|
26
25
|
import type { IntegrationType } from "./types";
|
|
26
|
+
import { hookActivityDir } from "./fp-home";
|
|
27
27
|
|
|
28
28
|
export const PAGE_SIZE = 25;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Layout 2 promoted this out of `cache/`. It was never a cache — nothing
|
|
32
|
+
* regenerates it, and deleting it loses the decision history the dashboard's
|
|
33
|
+
* activity tab is built on. Resolved lazily (not at module load) so
|
|
34
|
+
* FAILPROOFAI_HOME is honoured by tests and containers that set it after
|
|
35
|
+
* import.
|
|
36
|
+
*/
|
|
37
|
+
const defaultStoreDir = () => hookActivityDir();
|
|
31
38
|
const CURRENT_FILE = "current.jsonl";
|
|
32
39
|
const COUNT_FILE = "current.count"; // tracks line count; O(1) read/write vs rereading current.jsonl
|
|
33
40
|
const STATS_FILE = "stats.json";
|
|
34
41
|
const LOCK_FILE = "current.lock"; // advisory lock for concurrent hook processes
|
|
35
42
|
const LOCK_STALE_MS = 2000; // steal lock if older than 2 s (covers crashed processes)
|
|
36
43
|
|
|
37
|
-
let
|
|
44
|
+
let storeDirOverride: string | null = null;
|
|
45
|
+
/**
|
|
46
|
+
* A getter, not a module-load constant: `FAILPROOFAI_HOME` is routinely set by
|
|
47
|
+
* a test or a container AFTER this module is imported, and a value captured at
|
|
48
|
+
* import time would silently point every write at the real home.
|
|
49
|
+
*/
|
|
50
|
+
const storeDirValue = () => storeDirOverride ?? defaultStoreDir();
|
|
38
51
|
let rotateSeq = 0;
|
|
39
52
|
|
|
40
53
|
// ── Types ──
|
|
@@ -134,8 +147,8 @@ export interface HookActivityStats {
|
|
|
134
147
|
// ── Directory setup ──
|
|
135
148
|
|
|
136
149
|
function ensureDir(): void {
|
|
137
|
-
if (!existsSync(
|
|
138
|
-
mkdirSync(
|
|
150
|
+
if (!existsSync(storeDirValue())) {
|
|
151
|
+
mkdirSync(storeDirValue(), { recursive: true });
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
|
|
@@ -143,7 +156,7 @@ function ensureDir(): void {
|
|
|
143
156
|
|
|
144
157
|
function acquireLock(): void {
|
|
145
158
|
ensureDir();
|
|
146
|
-
const lockPath = join(
|
|
159
|
+
const lockPath = join(storeDirValue(), LOCK_FILE);
|
|
147
160
|
const deadline = Date.now() + LOCK_STALE_MS;
|
|
148
161
|
while (Date.now() < deadline) {
|
|
149
162
|
try {
|
|
@@ -166,7 +179,7 @@ function acquireLock(): void {
|
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
function releaseLock(): void {
|
|
169
|
-
try { unlinkSync(join(
|
|
182
|
+
try { unlinkSync(join(storeDirValue(), LOCK_FILE)); } catch { /* ignore */ }
|
|
170
183
|
}
|
|
171
184
|
|
|
172
185
|
// ── Writing (synchronous — hook handler is short-lived) ──
|
|
@@ -175,8 +188,8 @@ export function persistHookActivity(entry: HookActivityEntry): void {
|
|
|
175
188
|
ensureDir();
|
|
176
189
|
acquireLock();
|
|
177
190
|
try {
|
|
178
|
-
const currentPath = join(
|
|
179
|
-
const countPath = join(
|
|
191
|
+
const currentPath = join(storeDirValue(), CURRENT_FILE);
|
|
192
|
+
const countPath = join(storeDirValue(), COUNT_FILE);
|
|
180
193
|
|
|
181
194
|
const lineCount = readCount(countPath);
|
|
182
195
|
if (lineCount >= PAGE_SIZE) {
|
|
@@ -198,7 +211,7 @@ export function persistHookActivity(entry: HookActivityEntry): void {
|
|
|
198
211
|
|
|
199
212
|
function rotate(currentPath: string, countPath: string): void {
|
|
200
213
|
const archiveName = `page-${Date.now()}-${rotateSeq++}.jsonl`;
|
|
201
|
-
const archivePath = join(
|
|
214
|
+
const archivePath = join(storeDirValue(), archiveName);
|
|
202
215
|
renameSync(currentPath, archivePath);
|
|
203
216
|
// Reset count for the fresh file (write 0 — next append will set it to 1)
|
|
204
217
|
writeCount(countPath, 0);
|
|
@@ -240,7 +253,7 @@ interface StoredStats {
|
|
|
240
253
|
|
|
241
254
|
function readStoredStats(): StoredStats {
|
|
242
255
|
try {
|
|
243
|
-
return JSON.parse(readFileSync(join(
|
|
256
|
+
return JSON.parse(readFileSync(join(storeDirValue(), STATS_FILE), "utf-8")) as StoredStats;
|
|
244
257
|
} catch {
|
|
245
258
|
return { totalEvents: 0, denyCount: 0, policyMap: {} };
|
|
246
259
|
}
|
|
@@ -258,10 +271,10 @@ function updateStats(entry: HookActivityEntry): void {
|
|
|
258
271
|
s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
|
|
259
272
|
}
|
|
260
273
|
// Write atomically: write to a PID-unique temp file then rename — prevents partial reads.
|
|
261
|
-
const tmpPath = join(
|
|
274
|
+
const tmpPath = join(storeDirValue(), `stats.json.${process.pid}.tmp`);
|
|
262
275
|
try {
|
|
263
276
|
writeFileSync(tmpPath, JSON.stringify(s), "utf-8");
|
|
264
|
-
renameSync(tmpPath, join(
|
|
277
|
+
renameSync(tmpPath, join(storeDirValue(), STATS_FILE));
|
|
265
278
|
} catch {
|
|
266
279
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
267
280
|
// Non-fatal: stats file write failure doesn't block the hook
|
|
@@ -288,7 +301,7 @@ export function getHookActivityPage(page: number): HookActivityEntry[] {
|
|
|
288
301
|
if (page < 1) return [];
|
|
289
302
|
|
|
290
303
|
if (page === 1) {
|
|
291
|
-
const currentPath = join(
|
|
304
|
+
const currentPath = join(storeDirValue(), CURRENT_FILE);
|
|
292
305
|
return readJsonlFile(currentPath).reverse();
|
|
293
306
|
}
|
|
294
307
|
|
|
@@ -296,7 +309,7 @@ export function getHookActivityPage(page: number): HookActivityEntry[] {
|
|
|
296
309
|
const archiveIndex = page - 2;
|
|
297
310
|
if (archiveIndex >= archives.length) return [];
|
|
298
311
|
|
|
299
|
-
return readJsonlFile(join(
|
|
312
|
+
return readJsonlFile(join(storeDirValue(), archives[archiveIndex])).reverse();
|
|
300
313
|
}
|
|
301
314
|
|
|
302
315
|
export function getHookActivityPageCount(): number {
|
|
@@ -307,13 +320,13 @@ export function getHookActivityPageCount(): number {
|
|
|
307
320
|
export function getAllHookActivityEntries(): HookActivityEntry[] {
|
|
308
321
|
ensureDir();
|
|
309
322
|
|
|
310
|
-
const currentPath = join(
|
|
323
|
+
const currentPath = join(storeDirValue(), CURRENT_FILE);
|
|
311
324
|
const currentEntries = readJsonlFile(currentPath).reverse();
|
|
312
325
|
|
|
313
326
|
const archives = getArchiveFiles();
|
|
314
327
|
const archiveEntries: HookActivityEntry[] = [];
|
|
315
328
|
for (const file of archives) {
|
|
316
|
-
const entries = readJsonlFile(join(
|
|
329
|
+
const entries = readJsonlFile(join(storeDirValue(), file));
|
|
317
330
|
archiveEntries.push(...entries.reverse());
|
|
318
331
|
}
|
|
319
332
|
|
|
@@ -392,7 +405,7 @@ function readJsonlFile(filePath: string): HookActivityEntry[] {
|
|
|
392
405
|
|
|
393
406
|
function getArchiveFiles(): string[] {
|
|
394
407
|
try {
|
|
395
|
-
const files = readdirSync(
|
|
408
|
+
const files = readdirSync(storeDirValue());
|
|
396
409
|
return files
|
|
397
410
|
.filter((f) => f.startsWith("page-") && f.endsWith(".jsonl"))
|
|
398
411
|
.sort((a, b) => {
|
|
@@ -414,5 +427,7 @@ function getArchiveFiles(): string[] {
|
|
|
414
427
|
|
|
415
428
|
export function _resetForTest(testDir?: string): void {
|
|
416
429
|
rotateSeq = 0;
|
|
417
|
-
|
|
430
|
+
// null, not the default path: clearing the override lets the getter re-read
|
|
431
|
+
// FAILPROOFAI_HOME, which a test may have changed since this module loaded.
|
|
432
|
+
storeDirOverride = testDir ?? null;
|
|
418
433
|
}
|
package/src/hooks/hook-logger.ts
CHANGED
|
@@ -22,13 +22,14 @@ import {
|
|
|
22
22
|
} from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
|
+
import { logsDir } from "./fp-home";
|
|
25
26
|
|
|
26
27
|
export type LogLevel = "info" | "warn" | "error";
|
|
27
28
|
|
|
28
29
|
const LEVEL_ORDER: Record<LogLevel, number> = { info: 0, warn: 1, error: 2 };
|
|
29
30
|
const MAX_FILE_SIZE = 512 * 1024; // 500 KB
|
|
30
31
|
const LOG_FILENAME = "hooks.log";
|
|
31
|
-
const DEFAULT_LOG_DIR =
|
|
32
|
+
const DEFAULT_LOG_DIR = logsDir();
|
|
32
33
|
|
|
33
34
|
// ── State (lazy-initialized) ──
|
|
34
35
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { version } from "../../package.json";
|
|
9
9
|
import { POSTHOG_API_KEY, POSTHOG_PRODUCT } from "../posthog-key";
|
|
10
|
+
import { isTelemetryEnabled } from "../../lib/telemetry-enabled";
|
|
10
11
|
|
|
11
12
|
const API_KEY = POSTHOG_API_KEY;
|
|
12
13
|
const CAPTURE_URL = "https://us.i.posthog.com/capture/";
|
|
@@ -57,7 +58,10 @@ export async function trackHookEvent(
|
|
|
57
58
|
event: string,
|
|
58
59
|
properties?: Record<string, unknown>,
|
|
59
60
|
): Promise<void> {
|
|
60
|
-
|
|
61
|
+
// Honours the config file as well as the env var — the env var alone cannot
|
|
62
|
+
// reach a system-scope daemon. Memoised, so this stays off the hook path's
|
|
63
|
+
// latency budget. See lib/telemetry-enabled.ts.
|
|
64
|
+
if (!isTelemetryEnabled()) return;
|
|
61
65
|
|
|
62
66
|
const p = sendEvent(distinctId, event, properties);
|
|
63
67
|
pending.add(p);
|
|
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
|
|
8
8
|
import type { HookScope } from "./types";
|
|
9
9
|
import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
10
|
+
import { globalPolicyConfigFile } from "./fp-home";
|
|
10
11
|
|
|
11
12
|
/** Normalize plural and legacy singular explicit policy paths for consumers. */
|
|
12
13
|
export function configuredCustomPolicyPaths(
|
|
@@ -69,7 +70,7 @@ export function readMergedHooksConfig(cwd?: string): HooksConfig {
|
|
|
69
70
|
const base = findProjectConfigDir(cwd ?? process.cwd());
|
|
70
71
|
const projectPath = resolve(base, ".failproofai", "policies-config.json");
|
|
71
72
|
const localPath = resolve(base, ".failproofai", "policies-config.local.json");
|
|
72
|
-
const globalPath =
|
|
73
|
+
const globalPath = globalPolicyConfigFile();
|
|
73
74
|
|
|
74
75
|
const project = readConfigAt(projectPath);
|
|
75
76
|
const local = readConfigAt(localPath);
|
|
@@ -128,7 +129,7 @@ export function readMergedHooksConfig(cwd?: string): HooksConfig {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
function getConfigPath(): string {
|
|
131
|
-
return
|
|
132
|
+
return globalPolicyConfigFile();
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
export function readHooksConfig(): HooksConfig {
|
|
@@ -160,7 +161,7 @@ export function writeHooksConfig(config: HooksConfig): void {
|
|
|
160
161
|
export function getConfigPathForScope(scope: HookScope, cwd?: string): string {
|
|
161
162
|
switch (scope) {
|
|
162
163
|
case "user":
|
|
163
|
-
return
|
|
164
|
+
return globalPolicyConfigFile();
|
|
164
165
|
case "project":
|
|
165
166
|
return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.json");
|
|
166
167
|
case "local":
|
|
@@ -11,12 +11,75 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The ESM shim includes hooks API exports.
|
|
13
13
|
*/
|
|
14
|
-
import { readFile, writeFile, unlink, access } from "fs/promises";
|
|
15
|
-
import {
|
|
14
|
+
import { readFile, writeFile, unlink, access, mkdir, lstat } from "fs/promises";
|
|
15
|
+
import { tmpdir } from "os";
|
|
16
|
+
import { resolve, dirname, relative, join } from "path";
|
|
16
17
|
import { pathToFileURL } from "url";
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
import { shimsDir } from "./fp-home";
|
|
20
|
+
import { hookLogWarn } from "./hook-logger";
|
|
17
21
|
|
|
18
22
|
export const TMP_SUFFIX = ".__failproofai_tmp__.mjs";
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Anything this loader generated, by name.
|
|
26
|
+
*
|
|
27
|
+
* The temporary tree is written BESIDE the user's sources, because that is the
|
|
28
|
+
* only place a rewritten relative import still resolves. That was harmless
|
|
29
|
+
* while the name was fixed — an abnormally terminated hook left at most one
|
|
30
|
+
* stale file per source, overwritten on the next load — but the name now
|
|
31
|
+
* carries a pid and a sequence number, so every kill (a CLI hook timeout, a
|
|
32
|
+
* Ctrl-C) leaves a uniquely-named file behind forever.
|
|
33
|
+
*
|
|
34
|
+
* Two consequences, and the second is the visible one: the directory fills up,
|
|
35
|
+
* and `findSkippedPolicyFiles` reports each leftover back to the user as a
|
|
36
|
+
* policy file that will not load — an accusation about a file failproofai
|
|
37
|
+
* wrote itself.
|
|
38
|
+
*/
|
|
39
|
+
const TMP_ARTIFACT_RE = /\.__failproofai_tmp__\./;
|
|
40
|
+
|
|
41
|
+
/** True for a file this loader generated rather than one the user wrote. */
|
|
42
|
+
export function isTmpArtifact(name: string): boolean {
|
|
43
|
+
return TMP_ARTIFACT_RE.test(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How stale a generated file must be before a sweep will remove it.
|
|
48
|
+
*
|
|
49
|
+
* Another process may be mid-load right now, and its temporary tree is live
|
|
50
|
+
* until its `import()` resolves. Well past the 10s a module load is allowed,
|
|
51
|
+
* so a sweep can never delete a file another loader is still using.
|
|
52
|
+
*/
|
|
53
|
+
const TMP_SWEEP_MIN_AGE_MS = 60_000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove generated files left behind by a load that was killed.
|
|
57
|
+
*
|
|
58
|
+
* Best-effort and never throws: this runs on the hook path, and failing to
|
|
59
|
+
* tidy up is not worth failing an evaluation over.
|
|
60
|
+
*/
|
|
61
|
+
export async function sweepStaleTmpArtifacts(dir: string, now = Date.now()): Promise<number> {
|
|
62
|
+
let removed = 0;
|
|
63
|
+
try {
|
|
64
|
+
const { readdir, stat } = await import("fs/promises");
|
|
65
|
+
for (const name of await readdir(dir)) {
|
|
66
|
+
if (!isTmpArtifact(name)) continue;
|
|
67
|
+
const path = resolve(dir, name);
|
|
68
|
+
try {
|
|
69
|
+
const info = await stat(path);
|
|
70
|
+
if (now - info.mtimeMs < TMP_SWEEP_MIN_AGE_MS) continue;
|
|
71
|
+
await unlink(path);
|
|
72
|
+
removed++;
|
|
73
|
+
} catch {
|
|
74
|
+
/* raced with another sweep, or not ours to remove */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
/* unreadable directory — nothing to sweep */
|
|
79
|
+
}
|
|
80
|
+
return removed;
|
|
81
|
+
}
|
|
82
|
+
|
|
20
83
|
/** Regex to find local relative import specifiers (ESM). */
|
|
21
84
|
const LOCAL_IMPORT_RE = /(?:import\s+(?:[\s\S]*?\s+from\s+)?|export\s+(?:[\s\S]*?\s+from\s+))(['"])(\.\.?\/[^'"]+)\1/g;
|
|
22
85
|
|
|
@@ -74,12 +137,57 @@ export async function resolveLocalImport(
|
|
|
74
137
|
* Exports the full public API of failproofai: customPolicies, allow, deny, instruct,
|
|
75
138
|
* getCustomHooks, clearCustomHooks.
|
|
76
139
|
*/
|
|
140
|
+
/**
|
|
141
|
+
* Create the ESM shim that lets a policy file's `import ... from 'failproofai'`
|
|
142
|
+
* resolve, and return where it landed.
|
|
143
|
+
*
|
|
144
|
+
* NOT beside `dist/index.js`, which is where this used to go. That directory
|
|
145
|
+
* belongs to whoever installed the package, and on a system-wide install
|
|
146
|
+
* (`sudo npm i -g`, a container image, a shared build host, a CI runner) it is
|
|
147
|
+
* root-owned — so every non-root user running a hook failed with EACCES, the
|
|
148
|
+
* policy never loaded, and the hook exited 0. Builtins kept firing while
|
|
149
|
+
* cloud-managed and custom policies silently stopped, so the machine looked
|
|
150
|
+
* protected. That is the worst available failure for a policy engine.
|
|
151
|
+
*
|
|
152
|
+
* Two locations, and the ORDER OF OPERATIONS matters more than the choice:
|
|
153
|
+
*
|
|
154
|
+
* 1. `<home>/state/shims` — created at 0700 and owned by the user running the
|
|
155
|
+
* hook. Unlike a shared `/tmp`, no other local user can pre-plant a file at
|
|
156
|
+
* a path we are about to `import`.
|
|
157
|
+
* 2. `os.tmpdir()` — only when the first is unusable.
|
|
158
|
+
*
|
|
159
|
+
* **The write lives inside the same `try` as the mkdir on purpose.** `mkdir`
|
|
160
|
+
* with `recursive` RESOLVES on a directory that already exists, whatever its
|
|
161
|
+
* mode — so a `state/shims` left behind unwritable (a container that ran the
|
|
162
|
+
* CLI as root and then dropped to a non-root user; this very function creates
|
|
163
|
+
* that chain) would sail past a mkdir-only guard and throw EACCES on the write,
|
|
164
|
+
* reproducing the exact fail-open this function exists to fix. Any failure of
|
|
165
|
+
* either step falls through to the fallback, and says so — a silent slide into
|
|
166
|
+
* the weaker path is the same class of bug again.
|
|
167
|
+
*/
|
|
168
|
+
async function writeShim(
|
|
169
|
+
dir: string,
|
|
170
|
+
name: string,
|
|
171
|
+
code: string,
|
|
172
|
+
exclusive: boolean,
|
|
173
|
+
): Promise<string> {
|
|
174
|
+
const shimPath = join(dir, name);
|
|
175
|
+
// 0600 explicitly: an unset mode is `0666 & ~umask`, and `umask 000` is
|
|
176
|
+
// routine in containers — which in the fallback means a world-writable file
|
|
177
|
+
// in shared /tmp that this process then imports.
|
|
178
|
+
await writeFile(shimPath, code, {
|
|
179
|
+
encoding: "utf-8",
|
|
180
|
+
mode: 0o600,
|
|
181
|
+
...(exclusive ? { flag: "wx" as const } : {}),
|
|
182
|
+
});
|
|
183
|
+
return shimPath;
|
|
184
|
+
}
|
|
185
|
+
|
|
77
186
|
export async function createEsmShim(
|
|
78
187
|
distIndex: string,
|
|
79
188
|
distUrl: string,
|
|
80
189
|
tmpSuffix = TMP_SUFFIX,
|
|
81
190
|
): Promise<{ shimPath: string; shimUrl: string }> {
|
|
82
|
-
const shimPath = `${distIndex}${tmpSuffix}.shim.mjs`;
|
|
83
191
|
const shimCode = [
|
|
84
192
|
`import _cjs from '${distUrl}';`,
|
|
85
193
|
`export const customPolicies = _cjs.customPolicies;`,
|
|
@@ -90,7 +198,35 @@ export async function createEsmShim(
|
|
|
90
198
|
`export const instruct = _cjs.instruct;`,
|
|
91
199
|
`export default _cjs;`,
|
|
92
200
|
].join("\n");
|
|
93
|
-
|
|
201
|
+
// The name still carries `tmpSuffix` (pid + load sequence + a random id).
|
|
202
|
+
// `fingerprintTemporaryTree` normalises exactly that substring away before
|
|
203
|
+
// hashing, so the policy module cache still hits; a name it could not
|
|
204
|
+
// normalise would force a cold module load on every single hook call.
|
|
205
|
+
const name = `failproofai-dist${tmpSuffix}.shim.mjs`;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const dir = shimsDir();
|
|
209
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
210
|
+
// `mkdir` accepts a pre-existing SYMLINK here and a plain write would
|
|
211
|
+
// follow it out of the home. Confirm we got a real directory we own,
|
|
212
|
+
// unreadable to anyone else, before writing something we are about to
|
|
213
|
+
// execute.
|
|
214
|
+
const st = await lstat(dir);
|
|
215
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
216
|
+
if (!st.isDirectory() || st.isSymbolicLink() || (uid !== null && st.uid !== uid) || st.mode & 0o077) {
|
|
217
|
+
throw new Error(`${dir} is not a private directory owned by this user`);
|
|
218
|
+
}
|
|
219
|
+
const shimPath = await writeShim(dir, name, shimCode, false);
|
|
220
|
+
return { shimPath, shimUrl: pathToFileURL(shimPath).href };
|
|
221
|
+
} catch (err) {
|
|
222
|
+
hookLogWarn(
|
|
223
|
+
`policy shim directory unusable (${err instanceof Error ? err.message : String(err)}); falling back to ${tmpdir()}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Predictable name in a shared directory, so O_EXCL: refuse rather than write
|
|
228
|
+
// through something another user planted.
|
|
229
|
+
const shimPath = await writeShim(tmpdir(), name, shimCode, true);
|
|
94
230
|
return { shimPath, shimUrl: pathToFileURL(shimPath).href };
|
|
95
231
|
}
|
|
96
232
|
|
|
@@ -103,6 +239,16 @@ export async function rewriteFileTree(
|
|
|
103
239
|
distUrl: string | null,
|
|
104
240
|
distIndex: string | null,
|
|
105
241
|
tmpSuffix = TMP_SUFFIX,
|
|
242
|
+
/**
|
|
243
|
+
* SHA-256 the ENTRY file's raw bytes must match, re-checked here at the moment
|
|
244
|
+
* they are read for rewriting and import. Cloud-managed policies pass their
|
|
245
|
+
* pinned digest so the bytes that get imported are the same bytes that were
|
|
246
|
+
* verified — closing a TOCTOU where an earlier caller hashed the file, then
|
|
247
|
+
* this function independently re-read it (a window in which a same-user
|
|
248
|
+
* attacker could swap the bytes so a verified file imports unverified code).
|
|
249
|
+
* Only the entry is content-addressed; transitive local imports are not.
|
|
250
|
+
*/
|
|
251
|
+
entrySha256?: string | null,
|
|
106
252
|
): Promise<string[]> {
|
|
107
253
|
const queue: string[] = [entryPath];
|
|
108
254
|
const visited = new Set<string>();
|
|
@@ -120,7 +266,22 @@ export async function rewriteFileTree(
|
|
|
120
266
|
if (visited.has(filePath)) continue;
|
|
121
267
|
visited.add(filePath);
|
|
122
268
|
|
|
123
|
-
let code
|
|
269
|
+
let code: string;
|
|
270
|
+
if (entrySha256 && filePath === entryPath) {
|
|
271
|
+
// Read the raw bytes, verify the pin against THEM, then decode and rewrite
|
|
272
|
+
// that same in-memory content — filePath is never read again, so the bytes
|
|
273
|
+
// imported below are exactly the bytes verified here.
|
|
274
|
+
const buf = await readFile(filePath);
|
|
275
|
+
const actual = createHash("sha256").update(buf).digest("hex");
|
|
276
|
+
if (actual !== entrySha256) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`cloud-managed policy failed integrity re-verification at load: expected ${entrySha256}, got ${actual}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
code = buf.toString("utf-8");
|
|
282
|
+
} else {
|
|
283
|
+
code = await readFile(filePath, "utf-8");
|
|
284
|
+
}
|
|
124
285
|
|
|
125
286
|
// Rewrite 'failproofai' or legacy 'claudeye' imports to the ESM shim (or direct CJS for require)
|
|
126
287
|
if (esmShimUrl) {
|
package/src/hooks/manager.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { trackHookEvent } from "./hook-telemetry";
|
|
|
24
24
|
import { getInstanceId, hashToId } from "../../lib/telemetry-id";
|
|
25
25
|
import { CliError } from "../cli-error";
|
|
26
26
|
import { hookLogWarn } from "./hook-logger";
|
|
27
|
+
import { customPoliciesDir, globalPolicyConfigFile } from "./fp-home";
|
|
27
28
|
|
|
28
29
|
const VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
|
|
29
30
|
|
|
@@ -645,7 +646,7 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
645
646
|
} else {
|
|
646
647
|
console.log("\n Run `failproofai policies --install` to get started.");
|
|
647
648
|
}
|
|
648
|
-
console.log(
|
|
649
|
+
console.log(` Config: ${globalPolicyConfigFile()}\n`);
|
|
649
650
|
} else if (installedScopes.length === 1) {
|
|
650
651
|
// State B: Single scope — table with header row
|
|
651
652
|
const scope = installedScopes[0];
|
|
@@ -657,7 +658,7 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
657
658
|
for (const policy of regularPolicies) printSimpleRow(policy);
|
|
658
659
|
printBetaSection(printSimpleRow);
|
|
659
660
|
|
|
660
|
-
console.log(
|
|
661
|
+
console.log(`\n Config: ${globalPolicyConfigFile()}\n`);
|
|
661
662
|
} else {
|
|
662
663
|
// State C: Multiple scopes — column table
|
|
663
664
|
const COL = 9;
|
|
@@ -701,7 +702,7 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
701
702
|
for (const policy of betaPolicies) printMultiScopeRow(policy);
|
|
702
703
|
}
|
|
703
704
|
|
|
704
|
-
console.log(
|
|
705
|
+
console.log(`\n Config: ${globalPolicyConfigFile()}`);
|
|
705
706
|
|
|
706
707
|
// Multi-scope warning
|
|
707
708
|
const scopeNames = installedScopes.join(", ");
|
|
@@ -765,7 +766,7 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
765
766
|
// hook path was loading them.
|
|
766
767
|
const base = findProjectConfigDir(cwd ?? process.cwd());
|
|
767
768
|
const projectDir = resolve(base, ".failproofai", "policies");
|
|
768
|
-
const userDir =
|
|
769
|
+
const userDir = customPoliciesDir();
|
|
769
770
|
const sameDir = userDir === projectDir;
|
|
770
771
|
const conventionDirs: { label: string; dir: string }[] = [
|
|
771
772
|
{ label: sameDir ? "Project + User" : "Project", dir: projectDir },
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single-holder guard around first-run onboarding.
|
|
3
|
+
*
|
|
4
|
+
* Onboarding used to fire only on a bare `failproofai`. It now fires on any
|
|
5
|
+
* command an unconfigured machine runs, which makes concurrent runs a real
|
|
6
|
+
* shape rather than a theoretical one: two terminals, `failproofai audit` in
|
|
7
|
+
* each, and both draw a wizard. They would then race on the same settings
|
|
8
|
+
* files and, worse, both try to install the system service — two `sudo`
|
|
9
|
+
* prompts and two `systemctl enable` calls against one unit.
|
|
10
|
+
*
|
|
11
|
+
* ## Why liveness, not a timeout
|
|
12
|
+
*
|
|
13
|
+
* The obvious lock carries a timestamp and is treated as stale after N
|
|
14
|
+
* seconds. That cannot work here: the holder is a human answering questions,
|
|
15
|
+
* so any N short enough to recover promptly from a crash is also short enough
|
|
16
|
+
* to evict someone who walked away mid-setup. So the lock records a PID and is
|
|
17
|
+
* considered held only while that process is actually alive — which recovers
|
|
18
|
+
* instantly from a crash or a Ctrl-C, and never evicts a live wizard no matter
|
|
19
|
+
* how long it sits.
|
|
20
|
+
*
|
|
21
|
+
* ## Failure direction
|
|
22
|
+
*
|
|
23
|
+
* Every failure here resolves to "do not onboard". A lock we cannot take is
|
|
24
|
+
* indistinguishable from one already held, and skipping onboarding costs a
|
|
25
|
+
* user one printed hint, where wrongly proceeding costs a duplicated install.
|
|
26
|
+
* The command the user typed always runs either way.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { resolve } from "node:path";
|
|
31
|
+
import { onboardingLockFile, stateDir } from "./fp-home";
|
|
32
|
+
|
|
33
|
+
interface LockBody {
|
|
34
|
+
pid: number;
|
|
35
|
+
startedAt: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function onboardingLockPath(home: string = homedir()): string {
|
|
39
|
+
return onboardingLockFile(home);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether `pid` is a live process this user can signal.
|
|
44
|
+
*
|
|
45
|
+
* `kill(pid, 0)` performs the permission and existence checks without
|
|
46
|
+
* delivering a signal. `EPERM` means the process exists but belongs to another
|
|
47
|
+
* user — still alive, so still holding the lock.
|
|
48
|
+
*/
|
|
49
|
+
function processAlive(pid: number): boolean {
|
|
50
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, 0);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return (err as NodeJS.ErrnoException)?.code === "EPERM";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readLock(path: string): LockBody | null {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<LockBody>;
|
|
62
|
+
if (typeof parsed.pid !== "number") return null;
|
|
63
|
+
return { pid: parsed.pid, startedAt: Number(parsed.startedAt) || 0 };
|
|
64
|
+
} catch {
|
|
65
|
+
// Absent, unreadable, or truncated by a crash mid-write. All three mean
|
|
66
|
+
// "no live holder we can prove", and the liveness check below is what
|
|
67
|
+
// actually decides.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface OnboardingLock {
|
|
73
|
+
/** Release the lock. Safe to call more than once. */
|
|
74
|
+
release(): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Take the onboarding lock, or return `null` if another live process holds it.
|
|
79
|
+
*
|
|
80
|
+
* There is a small window between the liveness check and the write in which
|
|
81
|
+
* two processes could both decide the lock is free. That is accepted
|
|
82
|
+
* deliberately: closing it needs `O_EXCL` plus stale-file recovery, which
|
|
83
|
+
* reintroduces exactly the timeout problem this design avoids — and the
|
|
84
|
+
* consequence of losing that race is two wizards, which is the situation
|
|
85
|
+
* before this existed, not something worse.
|
|
86
|
+
*/
|
|
87
|
+
export function acquireOnboardingLock(home: string = homedir()): OnboardingLock | null {
|
|
88
|
+
const path = onboardingLockPath(home);
|
|
89
|
+
try {
|
|
90
|
+
const held = readLock(path);
|
|
91
|
+
// Our own PID appearing here means a previous run in this same process
|
|
92
|
+
// already holds it — re-entering is fine and must not deadlock.
|
|
93
|
+
if (held && held.pid !== process.pid && processAlive(held.pid)) return null;
|
|
94
|
+
|
|
95
|
+
mkdirSync(stateDir(home), { recursive: true });
|
|
96
|
+
const body: LockBody = { pid: process.pid, startedAt: Date.now() };
|
|
97
|
+
writeFileSync(path, JSON.stringify(body), "utf8");
|
|
98
|
+
|
|
99
|
+
let released = false;
|
|
100
|
+
return {
|
|
101
|
+
release() {
|
|
102
|
+
if (released) return;
|
|
103
|
+
released = true;
|
|
104
|
+
try {
|
|
105
|
+
// Only remove a lock still recorded as ours: if this process was
|
|
106
|
+
// evicted as dead and another wizard took over, deleting the file
|
|
107
|
+
// would release someone else's lock.
|
|
108
|
+
const current = readLock(path);
|
|
109
|
+
if (!current || current.pid === process.pid) rmSync(path, { force: true });
|
|
110
|
+
} catch {
|
|
111
|
+
/* best effort — a stray lock is reclaimed by the liveness check */
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
// Unwritable home, read-only filesystem, whatever it is: treat as held.
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|