failproofai 1.0.10 → 1.0.11-beta.1
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/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- 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 +4 -4
- 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.meta +8 -0
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- 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 +4 -4
- 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.meta +8 -0
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +4 -4
- 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 -4
- 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 -2
- 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 +4 -4
- 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 -3
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -3
- 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 +4 -4
- 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.meta +8 -0
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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 +4 -4
- 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 +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +8 -8
- 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 +5 -5
- 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 +5 -5
- 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 +5 -5
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +9 -9
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0qxhecv._.js → [externals]__0d7i9ea._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[externals]__0sg9-3p._.js → [externals]__0hjaw70._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1_9ze9b._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1oenfmn._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ga9hs7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0moxohc._.js +22 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z2ll-a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +11 -11
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1autwno._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1v25n-6._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
- package/.next/standalone/.next/server/chunks/_1fz8m31._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1jrv3hg._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1nx8fim._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ogwigf._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1q27azx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0el6rg8._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0ol5pa3._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1rqmk27._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1w7tiq4._.js} +2 -2
- 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]__0oqrl71._.js → [root-of-the-server]__042u-jn._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-bm5zf._.js → [root-of-the-server]__0502lat._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_gn5iz._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f765xr._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01bmjsj._.js → [root-of-the-server]__0h38gzz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hoaz1g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ouobn4._.js → [root-of-the-server]__0mxmwtx._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__19evfi8._.js → [root-of-the-server]__0uescqv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1484zv2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__19dutr7._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u6sn69._.js → [root-of-the-server]__1ab4ifb._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fbrk56._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1um4yxn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wb4yt2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1y3n3cn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0yq5_3a._.js → _1ffxwxz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_042cgl1._.js → _1o95rf0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1zopuov._.js → _1thr5rf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_16pa9ht._.js → _1v1o2ya._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -14
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0nkkbfv._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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 +23 -23
- package/.next/standalone/.next/static/chunks/08tmc_7n-rba1.js +1 -0
- package/.next/standalone/.next/static/chunks/{3mjfvng11xnoo.js → 09tlrmucn6oq3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{078dofaxvjgpd.js → 0i69k917myh7b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03h3b-uomi7_d.js → 0u_nhsujkactp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j92xul6y24_v.js → 0zp51hzx62i-u.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3u1s8dsqiean5.js → 1fz7q0gtnmeod.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3ob2161ap37ul.js → 1j75le8wln-26.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2kj826h1w9r47.js → 1z4km7tw0_50h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1fqukzzowjflt.js → 239ay_80-xr8z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3nkc2jlgyygda.js → 2c9pc5xco6rnu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 3n46e45atic-m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0542cuk_va8g1.js → 3sg07wnn969xq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-11dn2pegmg9_9.js → turbopack-1opxka4dtjbc5.js} +1 -1
- package/.next/standalone/app/actions/install-hooks-web.ts +10 -2
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/256.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +17 -17
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +18 -18
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +3 -3
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +13 -13
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/base-server.js +1 -2
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +265 -53
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +177 -19
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +36 -17
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +14 -6
- package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/lib/route-cache-key.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +9 -11
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +4 -2
- package/.next/standalone/node_modules/next/dist/server/next-server.js +7 -3
- package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +32 -16
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +6 -1
- package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +9 -2
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +30 -29
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +18 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +14 -11
- package/.next/standalone/sdk/python/README.md +41 -0
- package/.next/standalone/sdk/python/docs/README.md +1 -0
- package/.next/standalone/sdk/python/docs/otel/README.md +36 -0
- package/.next/standalone/sdk/python/docs/otel/examples/agent_trace.py +30 -0
- package/.next/standalone/sdk/python/docs/otel/examples/quickstart.py +22 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +8 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +3 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/otel.py +189 -0
- package/.next/standalone/sdk/python/pyproject.toml +2 -1
- package/.next/standalone/sdk/python/tests/test_docs.py +16 -3
- package/.next/standalone/sdk/python/tests/test_integrations.py +1 -1
- package/.next/standalone/sdk/python/tests/test_otel.py +147 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +4 -1
- package/.next/standalone/sdk/python/uv.lock +258 -147
- package/.next/standalone/sdk/typescript/README.md +43 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +9 -2
- package/.next/standalone/sdk/typescript/package-lock.json +57 -5
- package/.next/standalone/sdk/typescript/package.json +2 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +9 -0
- package/.next/standalone/sdk/typescript/src/index.ts +2 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +4 -1
- package/.next/standalone/sdk/typescript/src/integrations/otel.ts +204 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +1 -1
- package/.next/standalone/sdk/typescript/test/otel.test.ts +125 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +653 -1077
- package/dist/cli.mjs +8174 -3930
- package/dist/worker.mjs +170 -62
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +14 -11
- package/scripts/launch-facts.ts +157 -0
- package/scripts/launch-ready.ts +228 -0
- package/scripts/launch-screen.ts +202 -0
- package/scripts/launch.ts +106 -49
- package/scripts/skew-log-filter.ts +42 -0
- package/src/audit/cli-login.ts +39 -59
- package/src/audit/cli.ts +308 -228
- package/src/audit/index.ts +45 -2
- package/src/audit/schedule-cli.ts +83 -120
- package/src/audit/types.ts +31 -0
- package/src/hooks/agent-selection.ts +155 -0
- package/src/hooks/backfill-cli.ts +151 -137
- package/src/hooks/backfill-request.ts +149 -0
- package/src/hooks/cloud-connection.ts +1 -1
- package/src/hooks/cloud-enrollment-cli.ts +78 -28
- package/src/hooks/cloud-introspect.ts +3 -0
- package/src/hooks/collector-config.ts +10 -1
- package/src/hooks/config-status.ts +606 -0
- package/src/hooks/configure-wizard.ts +915 -989
- package/src/hooks/daemon-service.ts +16 -6
- package/src/hooks/did-you-mean.ts +185 -0
- package/src/hooks/flush-cli.ts +33 -7
- package/src/hooks/fp-config.ts +96 -0
- package/src/hooks/fp-home.ts +11 -0
- package/src/hooks/fp-reset.ts +102 -95
- package/src/hooks/handler.ts +4 -0
- package/src/hooks/harness-cli.ts +87 -138
- package/src/hooks/install-prompt.ts +38 -477
- package/src/hooks/integrations.ts +1 -1
- package/src/hooks/jev-cli.ts +183 -196
- package/src/hooks/manager.ts +554 -358
- package/src/hooks/onboarding-attempt.ts +11 -9
- package/src/hooks/otel-cli.ts +344 -0
- package/src/hooks/otel-output.ts +24 -0
- package/src/hooks/otel-writers.ts +225 -0
- package/src/hooks/pack-cli.ts +219 -204
- package/src/hooks/report-body.ts +50 -0
- package/src/hooks/safe-config-write.ts +4 -2
- package/src/hooks/session-pause-cli.ts +93 -73
- package/src/hooks/tui.ts +849 -389
- package/src/hooks/uninstall-cli.ts +136 -149
- package/src/hooks/untraced-note.ts +52 -0
- package/.next/standalone/.next/server/chunks/[externals]__19_pzeq._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__20kzpkf._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0igxt32._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +0 -22
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +0 -6
- package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +0 -1
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → UZKCiP4Q2TeU8zOB-h9Xj}/_ssgManifest.js +0 -0
package/src/hooks/manager.ts
CHANGED
|
@@ -24,27 +24,15 @@ 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
|
|
27
|
+
import { customPoliciesDir } from "./fp-home";
|
|
28
28
|
import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
|
|
29
29
|
import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
|
|
30
30
|
import type { ResolvedPack } from "./pack-manifest";
|
|
31
31
|
import { hasInstalledRegexPacks, readInstalledPacks } from "./pack-manifest";
|
|
32
32
|
import { packPolicyParamKey } from "./policy-evaluator";
|
|
33
33
|
import { probeDaemonPolicyEvaluation } from "./daemon-service";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
note,
|
|
37
|
-
nextStep,
|
|
38
|
-
optsFor,
|
|
39
|
-
printBlock,
|
|
40
|
-
rule,
|
|
41
|
-
stack,
|
|
42
|
-
table,
|
|
43
|
-
title,
|
|
44
|
-
warning,
|
|
45
|
-
type ChipState,
|
|
46
|
-
type TableRow,
|
|
47
|
-
} from "./tui";
|
|
34
|
+
import { keepAgentSelectionTrue } from "./agent-selection";
|
|
35
|
+
import { INDENT, colorsEnabled, optsFor, printBlock, screenKit, type ScreenKitOpts } from "./tui";
|
|
48
36
|
|
|
49
37
|
const VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
|
|
50
38
|
|
|
@@ -77,7 +65,7 @@ function resolveFailproofaiBinary(): string {
|
|
|
77
65
|
} catch {
|
|
78
66
|
throw new CliError(
|
|
79
67
|
"failproofai binary not found in PATH.\n" +
|
|
80
|
-
"Install it globally first:
|
|
68
|
+
"Install it globally first: `npm install -g failproofai`"
|
|
81
69
|
);
|
|
82
70
|
}
|
|
83
71
|
}
|
|
@@ -145,7 +133,7 @@ function resolvePolicyNames(names: string[]): { builtins: string[]; packs: PackP
|
|
|
145
133
|
throw new CliError(
|
|
146
134
|
`"${raw}" is declared by ${direct.length} installed packs.\n` +
|
|
147
135
|
`Name the one you mean:\n` +
|
|
148
|
-
direct.map((m) => `
|
|
136
|
+
direct.map((m) => ` \`${m.packId}:${m.name}\``).join("\n"),
|
|
149
137
|
);
|
|
150
138
|
}
|
|
151
139
|
// No pack carries it. Falls back to the compiled name set, which is what a
|
|
@@ -172,7 +160,7 @@ function resolvePolicyNames(names: string[]): { builtins: string[]; packs: PackP
|
|
|
172
160
|
throw new CliError(
|
|
173
161
|
`"${raw}" is declared by ${matches.length} installed packs.\n` +
|
|
174
162
|
`Name the one you mean:\n` +
|
|
175
|
-
matches.map((m) => `
|
|
163
|
+
matches.map((m) => ` \`${m.packId}:${m.name}\``).join("\n"),
|
|
176
164
|
);
|
|
177
165
|
}
|
|
178
166
|
unknown.push(raw);
|
|
@@ -218,10 +206,9 @@ function applyPackPolicies(
|
|
|
218
206
|
writeScopedHooksConfig(next, scope, cwd);
|
|
219
207
|
}
|
|
220
208
|
}
|
|
209
|
+
const kit = screenKit(optsFor(process.stdout));
|
|
221
210
|
for (const ref of refs) {
|
|
222
|
-
console.log(
|
|
223
|
-
`${on ? "Enabled" : "Disabled"} ${ref.name} from pack ${ref.packId}@${ref.packVersion}.`,
|
|
224
|
-
);
|
|
211
|
+
console.log(kit.ok(`${on ? "Enabled" : "Disabled"} ${ref.name} from pack ${ref.packId}@${ref.packVersion}.`));
|
|
225
212
|
}
|
|
226
213
|
}
|
|
227
214
|
|
|
@@ -240,9 +227,7 @@ function rejectAlwaysOnPolicies(names: string[]): void {
|
|
|
240
227
|
if (refused.length > 0) {
|
|
241
228
|
throw new CliError(
|
|
242
229
|
`Cannot disable: ${refused.join(", ")}\n` +
|
|
243
|
-
`
|
|
244
|
-
`is always on and ships with the package. A guard the agent can disable ` +
|
|
245
|
-
`by the means it is meant to prevent is not a guard.`
|
|
230
|
+
`It stops an agent from switching failproofai off, so it is always on.`
|
|
246
231
|
);
|
|
247
232
|
}
|
|
248
233
|
}
|
|
@@ -319,6 +304,119 @@ export function integrationsInstalledAt(scope: HookScope, cwd?: string): Integra
|
|
|
319
304
|
});
|
|
320
305
|
}
|
|
321
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Why the policies on this machine are not enforcing, or `null` when they are.
|
|
309
|
+
*
|
|
310
|
+
* ONE answer for every screen that warns about it — `failproofai policies`, the
|
|
311
|
+
* dashboard launch screen — so two screens can never disagree about whether
|
|
312
|
+
* this machine is protected. The redesign shows one generic warning whatever
|
|
313
|
+
* the cause (decision D18); the cause is returned anyway, for tests and so a
|
|
314
|
+
* screen that wants the specific fix can have it.
|
|
315
|
+
*
|
|
316
|
+
* Cheap enough for the launch path on purpose: it reads files and imports no
|
|
317
|
+
* policy file and probes no daemon.
|
|
318
|
+
*
|
|
319
|
+
* Deliberately NOT causes, because each needs its own words:
|
|
320
|
+
* - A refused pack and a down daemon on a daemon-configured machine. Both
|
|
321
|
+
* FAIL CLOSED: they DENY tool calls rather than let them through, so "not
|
|
322
|
+
* enforcing" would tell the user the opposite of what is happening.
|
|
323
|
+
* - A session pause. It belongs to one session, not to the machine, and
|
|
324
|
+
* `config --status` lists it.
|
|
325
|
+
*/
|
|
326
|
+
export type NotEnforcingReason = "no-hooks" | "no-policies" | "observe-only";
|
|
327
|
+
|
|
328
|
+
export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
329
|
+
const wired = deduplicateScopes(HOOK_SCOPES, cwd).some((scope) => hooksInstalledInSettings(scope, cwd));
|
|
330
|
+
if (!wired) return "no-hooks";
|
|
331
|
+
|
|
332
|
+
const config = readMergedHooksConfig(cwd);
|
|
333
|
+
const disabled = new Set(config.disabledCustomPolicies ?? []);
|
|
334
|
+
let observing = false;
|
|
335
|
+
|
|
336
|
+
// Installed packs: a policy enforces when its pack is not observe-only, it
|
|
337
|
+
// was taken at install, and nobody switched it off afterwards.
|
|
338
|
+
try {
|
|
339
|
+
const installed = readInstalledPacks();
|
|
340
|
+
// A pack that failed to load FAILS CLOSED: the calls it covers are denied,
|
|
341
|
+
// which is the opposite of "not enforcing" (see the doc comment above). An
|
|
342
|
+
// observe pack that fails does not deny, so it does not count; neither does
|
|
343
|
+
// a Jev-checks-only pack, which has nothing in the regex tier to deny on
|
|
344
|
+
// (pack-failclosed.ts skips it, and so does the listing below).
|
|
345
|
+
if (installed.errors.some((error) => error.effect !== "observe" && !error.semanticOnly)) return null;
|
|
346
|
+
for (const pack of installed.packs) {
|
|
347
|
+
const taken = pack.enabled ?? pack.policies.map((policy) => policy.name);
|
|
348
|
+
const on = taken.filter((name) => !disabled.has(`pack:${pack.id}@${pack.version}:${name}`));
|
|
349
|
+
if (on.length === 0) continue;
|
|
350
|
+
if (pack.effect === "observe") observing = true;
|
|
351
|
+
else return null;
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
// An unreadable manifest is reported by the listing itself.
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Cloud-managed policies, pushed from a deployment.
|
|
358
|
+
try {
|
|
359
|
+
for (const artifact of readActiveCloudManagedPolicies()) {
|
|
360
|
+
if (artifact.effect === "observe") observing = true;
|
|
361
|
+
else return null;
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
// No deployment, or an unreadable one: nothing enforces from there.
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// A configured custom policy file, or convention files on disk. Loading them
|
|
368
|
+
// to ask what they hold would import user code on every launch, so their
|
|
369
|
+
// presence counts: the listing below reports a file that fails to load.
|
|
370
|
+
if (configuredCustomPolicyPaths(config).length > 0) return null;
|
|
371
|
+
const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
|
|
372
|
+
if (discoverPolicyFiles(projectDir).length > 0 || discoverPolicyFiles(customPoliciesDir()).length > 0) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// A machine still on the pre-pack migration shim runs its legacy builtins.
|
|
377
|
+
if (config.enabledPolicies.length > 0 && !hasInstalledRegexPacks()) return null;
|
|
378
|
+
|
|
379
|
+
return observing ? "observe-only" : "no-policies";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* How many policies enforce on this machine, for the line that says so.
|
|
384
|
+
*
|
|
385
|
+
* Counted the same way `notEnforcingReason` decides, so the two never disagree:
|
|
386
|
+
* pack policies that are taken and not switched off, cloud-managed policies, and
|
|
387
|
+
* the legacy builtins of a machine still on the migration shim — observe-only
|
|
388
|
+
* ones excluded. Custom policy files cannot be counted without importing them,
|
|
389
|
+
* so their presence is reported on its own.
|
|
390
|
+
*/
|
|
391
|
+
export function enforcingPolicyCount(cwd?: string): { count: number; custom: boolean } {
|
|
392
|
+
const config = readMergedHooksConfig(cwd);
|
|
393
|
+
const disabled = new Set(config.disabledCustomPolicies ?? []);
|
|
394
|
+
let count = 0;
|
|
395
|
+
try {
|
|
396
|
+
for (const pack of readInstalledPacks().packs) {
|
|
397
|
+
if (pack.effect === "observe") continue;
|
|
398
|
+
const taken = pack.enabled ?? pack.policies.map((policy) => policy.name);
|
|
399
|
+
count += taken.filter((name) => !disabled.has(`pack:${pack.id}@${pack.version}:${name}`)).length;
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
// An unreadable manifest counts nothing, as it enforces nothing.
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
for (const artifact of readActiveCloudManagedPolicies()) {
|
|
406
|
+
if (artifact.effect !== "observe") count++;
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
// No deployment.
|
|
410
|
+
}
|
|
411
|
+
if (!hasInstalledRegexPacks()) count += config.enabledPolicies.length;
|
|
412
|
+
const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
|
|
413
|
+
const custom =
|
|
414
|
+
configuredCustomPolicyPaths(config).length > 0 ||
|
|
415
|
+
discoverPolicyFiles(projectDir).length > 0 ||
|
|
416
|
+
discoverPolicyFiles(customPoliciesDir()).length > 0;
|
|
417
|
+
return { count, custom };
|
|
418
|
+
}
|
|
419
|
+
|
|
322
420
|
function safeSettingsPath(
|
|
323
421
|
integration: Integration,
|
|
324
422
|
scope: HookScope,
|
|
@@ -337,6 +435,16 @@ export interface InstallHooksOptions {
|
|
|
337
435
|
/** Suppress this module's installation logging (for callers that render their
|
|
338
436
|
* own UI, like the configure wizard). Errors still surface via console.error. */
|
|
339
437
|
quiet?: boolean;
|
|
438
|
+
/**
|
|
439
|
+
* Trace every agent this call hooks, at whatever scope, and say so in one line
|
|
440
|
+
* after everything else it prints (decision D2, `./agent-selection`).
|
|
441
|
+
* Otherwise the next `failproofai config` takes these hooks out again.
|
|
442
|
+
*
|
|
443
|
+
* Asked for by the commands a person runs — `policies --install`,
|
|
444
|
+
* `policies add`, the dashboard — and off by default, because the one other
|
|
445
|
+
* caller is `failproofai config`, which writes the selection itself.
|
|
446
|
+
*/
|
|
447
|
+
syncAgentSelection?: boolean;
|
|
340
448
|
}
|
|
341
449
|
|
|
342
450
|
/**
|
|
@@ -359,10 +467,11 @@ export async function installHooks(
|
|
|
359
467
|
cli?: IntegrationType[],
|
|
360
468
|
options: InstallHooksOptions = {},
|
|
361
469
|
): Promise<void> {
|
|
362
|
-
const { replace = false, quiet = false } = options;
|
|
470
|
+
const { replace = false, quiet = false, syncAgentSelection = false } = options;
|
|
363
471
|
if (!quiet) {
|
|
364
472
|
return installHooksImpl(
|
|
365
473
|
policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace,
|
|
474
|
+
syncAgentSelection,
|
|
366
475
|
);
|
|
367
476
|
}
|
|
368
477
|
// Quiet mode: this module logs exclusively via console.log, so muting it for
|
|
@@ -373,6 +482,7 @@ export async function installHooks(
|
|
|
373
482
|
try {
|
|
374
483
|
return await installHooksImpl(
|
|
375
484
|
policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace,
|
|
485
|
+
syncAgentSelection,
|
|
376
486
|
);
|
|
377
487
|
} finally {
|
|
378
488
|
console.log = origLog;
|
|
@@ -389,6 +499,7 @@ async function installHooksImpl(
|
|
|
389
499
|
removeCustomHooks = false,
|
|
390
500
|
cli?: IntegrationType[],
|
|
391
501
|
replace = false,
|
|
502
|
+
syncAgentSelection = false,
|
|
392
503
|
): Promise<void> {
|
|
393
504
|
// Validate user input first before any system checks
|
|
394
505
|
if (policyNames !== undefined && policyNames.length > 0) {
|
|
@@ -439,7 +550,7 @@ async function installHooksImpl(
|
|
|
439
550
|
// Then check if "all" is mixed with valid specific names
|
|
440
551
|
throw new CliError(
|
|
441
552
|
`"all" cannot be combined with specific policy names.\n` +
|
|
442
|
-
|
|
553
|
+
"Use either `failproofai policies --install all` or `failproofai policies --install block-sudo sanitize-jwt`"
|
|
443
554
|
);
|
|
444
555
|
}
|
|
445
556
|
}
|
|
@@ -464,8 +575,8 @@ async function installHooksImpl(
|
|
|
464
575
|
});
|
|
465
576
|
} catch {}
|
|
466
577
|
throw new CliError(
|
|
467
|
-
`Scope "${scope}" is not supported by ${integration.displayName}
|
|
468
|
-
`
|
|
578
|
+
`Scope "${scope}" is not supported by ${integration.displayName}.\n` +
|
|
579
|
+
`Use one of: ${integration.scopes.join(", ")}`
|
|
469
580
|
);
|
|
470
581
|
}
|
|
471
582
|
}
|
|
@@ -483,7 +594,7 @@ async function installHooksImpl(
|
|
|
483
594
|
const verb = daemonRequiredBy.length === 1 ? "requires" : "require";
|
|
484
595
|
throw new CliError(
|
|
485
596
|
`${names} ${verb} a compatible failproofaid daemon with native policy evaluation before FailproofAI enforcement can be enabled.\n` +
|
|
486
|
-
"
|
|
597
|
+
"Install or update the daemon with `failproofai config`, then run this again.",
|
|
487
598
|
);
|
|
488
599
|
}
|
|
489
600
|
|
|
@@ -587,7 +698,7 @@ async function installHooksImpl(
|
|
|
587
698
|
error_type: /not found/i.test(msg) ? "file_not_found" : "load_error",
|
|
588
699
|
});
|
|
589
700
|
} catch {}
|
|
590
|
-
|
|
701
|
+
process.stderr.write(`${screenKit({ color: colorsEnabled(process.stderr) }).notice("fail", msg).join("\n")}\n`);
|
|
591
702
|
process.exit(1);
|
|
592
703
|
}
|
|
593
704
|
const semanticCount = getSemanticRegistrations().length;
|
|
@@ -604,9 +715,14 @@ async function installHooksImpl(
|
|
|
604
715
|
error_type: "no_hooks_registered",
|
|
605
716
|
});
|
|
606
717
|
} catch {}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
718
|
+
process.stderr.write(
|
|
719
|
+
`${screenKit({ color: colorsEnabled(process.stderr) })
|
|
720
|
+
.notice(
|
|
721
|
+
"fail",
|
|
722
|
+
`No hooks are registered in ${path}.\n` +
|
|
723
|
+
"Make sure the file calls `customPolicies.add(...)` at least once.",
|
|
724
|
+
)
|
|
725
|
+
.join("\n")}\n`,
|
|
610
726
|
);
|
|
611
727
|
process.exit(1);
|
|
612
728
|
}
|
|
@@ -665,8 +781,10 @@ async function installHooksImpl(
|
|
|
665
781
|
await addPack(CORE_SOURCE, { only: fromPack });
|
|
666
782
|
} catch (err) {
|
|
667
783
|
console.log(
|
|
668
|
-
|
|
669
|
-
`
|
|
784
|
+
screenKit(optsFor(process.stdout)).caution(
|
|
785
|
+
`Couldn't fetch the policy pack (${err instanceof Error ? err.message : String(err)}). Once you are online:`,
|
|
786
|
+
"failproofai policies add FailproofAI/policies",
|
|
787
|
+
),
|
|
670
788
|
);
|
|
671
789
|
}
|
|
672
790
|
} else {
|
|
@@ -685,13 +803,14 @@ async function installHooksImpl(
|
|
|
685
803
|
// with an empty list described work it had not done — or worse, re-stated a
|
|
686
804
|
// stale key as though it were this run's decision.
|
|
687
805
|
if (policyNames !== undefined && selectedPolicies.length > 0) {
|
|
688
|
-
|
|
806
|
+
const n = selectedPolicies.length;
|
|
807
|
+
console.log(screenKit(optsFor(process.stdout)).ok(`Enabled ${n} ${n === 1 ? "policy" : "policies"}: ${selectedPolicies.join(", ")}`));
|
|
689
808
|
}
|
|
690
809
|
if (removeCustomHooks) {
|
|
691
|
-
console.log("Custom hooks path cleared.");
|
|
810
|
+
console.log(screenKit(optsFor(process.stdout)).ok("Custom hooks path cleared."));
|
|
692
811
|
} else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {
|
|
693
812
|
const paths = configToWrite.customPoliciesPaths ?? [configToWrite.customPoliciesPath!];
|
|
694
|
-
|
|
813
|
+
for (const line of screenKit(optsFor(process.stdout)).kv([["custom", paths.join(", ")]])) console.log(line);
|
|
695
814
|
}
|
|
696
815
|
|
|
697
816
|
// Write hooks for each selected CLI
|
|
@@ -768,38 +887,37 @@ async function installHooksImpl(
|
|
|
768
887
|
// Telemetry is best-effort — never block the operation
|
|
769
888
|
}
|
|
770
889
|
|
|
890
|
+
// One ✓ per agent, with where it was written under it; then how the hooks
|
|
891
|
+
// call failproofai. Drawn with the kit, so colour follows the terminal and a
|
|
892
|
+
// pipe gets plain text.
|
|
893
|
+
const kit = screenKit(optsFor(process.stdout));
|
|
771
894
|
for (const { cli: cliId, path } of writtenSettingsPaths) {
|
|
772
895
|
const integration = getIntegration(cliId);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
`(${integration.eventTypes.length} event types, scope: ${scope}).`
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
console.log(`Settings: ${path}`);
|
|
896
|
+
console.log(
|
|
897
|
+
kit.ok(
|
|
898
|
+
cliId === "hermes"
|
|
899
|
+
? `${integration.displayName} plugin installed (8 hooks, ${scope} scope).`
|
|
900
|
+
: `${integration.displayName} hooks installed (${integration.eventTypes.length} events, ${scope} scope).`,
|
|
901
|
+
),
|
|
902
|
+
);
|
|
903
|
+
for (const line of kit.kv([["settings", path]])) console.log(line);
|
|
785
904
|
}
|
|
786
|
-
|
|
787
|
-
console.log(
|
|
788
|
-
console.log(`\nThis file can be committed to git — no machine-specific paths.`);
|
|
789
|
-
} else {
|
|
790
|
-
console.log(`Binary: ${binaryPath}`);
|
|
905
|
+
for (const line of kit.kv(scope === "project" ? [["command", "npx -y failproofai"]] : [["binary", binaryPath]])) {
|
|
906
|
+
console.log(line);
|
|
791
907
|
}
|
|
908
|
+
if (scope === "project") console.log(`${INDENT}This file can be committed to git: it has no machine-specific paths.`);
|
|
792
909
|
|
|
793
910
|
// Warn about duplicate-scope installations (Claude Code only — uses HOOK_SCOPES)
|
|
794
911
|
const otherScopes = deduplicateScopes(HOOK_SCOPES, cwd).filter((s) => s !== scope);
|
|
795
912
|
const duplicates = otherScopes.filter((s) => hooksInstalledInSettings(s, cwd));
|
|
796
913
|
if (duplicates.length > 0) {
|
|
797
914
|
const scopeList = duplicates.map((s) => `${s} (${scopeLabel(s)})`).join(", ");
|
|
798
|
-
console.log(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
915
|
+
console.log(
|
|
916
|
+
kit.caution(
|
|
917
|
+
`Hooks are also installed at ${scopeList}, so each policy may run twice.`,
|
|
918
|
+
`failproofai policies --uninstall --scope ${duplicates[0]}`,
|
|
919
|
+
),
|
|
920
|
+
);
|
|
803
921
|
try {
|
|
804
922
|
await trackHookEvent(getInstanceId(), "multi_scope_warning_shown", {
|
|
805
923
|
new_scope: scope,
|
|
@@ -808,6 +926,14 @@ async function installHooksImpl(
|
|
|
808
926
|
});
|
|
809
927
|
} catch {}
|
|
810
928
|
}
|
|
929
|
+
|
|
930
|
+
// Last, after everything above: the agents whose hooks were actually written.
|
|
931
|
+
// A call that returned early (only a third-party pack's policies were named)
|
|
932
|
+
// or threw wrote none and changes nothing.
|
|
933
|
+
if (syncAgentSelection) {
|
|
934
|
+
const note = keepAgentSelectionTrue("installed", writtenSettingsPaths.map((written) => written.cli));
|
|
935
|
+
if (note) console.log(note);
|
|
936
|
+
}
|
|
811
937
|
}
|
|
812
938
|
|
|
813
939
|
/**
|
|
@@ -818,8 +944,18 @@ async function installHooksImpl(
|
|
|
818
944
|
* - `["block-sudo"]` → disable specific policies in config, keep hooks installed
|
|
819
945
|
* @param scope — settings scope to remove from (default: "user"), or "all" to remove from all scopes
|
|
820
946
|
* @param opts.betaOnly — set to true when removing only beta policies (adds beta_only flag to telemetry)
|
|
947
|
+
* @param opts.syncAgentSelection — when hooks are taken out at user scope or
|
|
948
|
+
* every scope, stop tracing those agents and say so in one line (decision D2,
|
|
949
|
+
* `./agent-selection`). Asked for by `policies --uninstall`, `policies remove`
|
|
950
|
+
* and the dashboard; `failproofai uninstall` leaves the selection alone.
|
|
951
|
+
* @param opts.silent — print nothing; for a caller that reports the outcome itself
|
|
952
|
+
* (`failproofai uninstall` says what it removed in its own lines, under its
|
|
953
|
+
* question, and these old-style rows landed between the two)
|
|
821
954
|
*/
|
|
822
|
-
export async function removeHooks(policyNames?: string[], scope: HookScope | "all" = "user", cwd?: string, opts?: { betaOnly?: boolean; source?: string; removeCustomHooks?: boolean; cli?: IntegrationType[] }): Promise<void> {
|
|
955
|
+
export async function removeHooks(policyNames?: string[], scope: HookScope | "all" = "user", cwd?: string, opts?: { betaOnly?: boolean; source?: string; removeCustomHooks?: boolean; cli?: IntegrationType[]; syncAgentSelection?: boolean; silent?: boolean }): Promise<void> {
|
|
956
|
+
const say = (line: string): void => {
|
|
957
|
+
if (!opts?.silent) console.log(line);
|
|
958
|
+
};
|
|
823
959
|
// Resolve the effective config scope ("all" falls back to "user" for config reads/writes)
|
|
824
960
|
const configScope: HookScope = scope === "all" ? "user" : scope;
|
|
825
961
|
// Back-compat default: ["claude"]. The bin layer prompts for CLI selection
|
|
@@ -833,7 +969,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
833
969
|
delete config.customPoliciesPath;
|
|
834
970
|
delete config.customPoliciesPaths;
|
|
835
971
|
writeScopedHooksConfig(config, configScope, cwd);
|
|
836
|
-
|
|
972
|
+
say("Custom hooks path cleared.");
|
|
837
973
|
}
|
|
838
974
|
|
|
839
975
|
// Remove specific policies from config (keep hooks installed)
|
|
@@ -855,7 +991,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
855
991
|
const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
|
|
856
992
|
const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
|
|
857
993
|
if (notEnabled.length > 0) {
|
|
858
|
-
|
|
994
|
+
say(`Warning: policy(ies) not currently enabled: ${notEnabled.join(", ")}`);
|
|
859
995
|
}
|
|
860
996
|
const { policyParams: existingParams, ...baseConfig } = config;
|
|
861
997
|
const filteredParams = existingParams
|
|
@@ -889,14 +1025,25 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
889
1025
|
// Telemetry is best-effort — never block the operation
|
|
890
1026
|
}
|
|
891
1027
|
|
|
892
|
-
|
|
893
|
-
|
|
1028
|
+
say(`Disabled ${policyNames.length - notEnabled.length} policy(ies).`);
|
|
1029
|
+
say(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
|
|
894
1030
|
return;
|
|
895
1031
|
}
|
|
896
1032
|
|
|
897
1033
|
// Capture enabled policies before clearing (used for accurate telemetry below)
|
|
898
1034
|
const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
|
|
899
1035
|
|
|
1036
|
+
// Every agent this removal acted on stops being traced, including one that
|
|
1037
|
+
// turned out to have nothing to remove: the command still said "not this
|
|
1038
|
+
// agent", and leaving it traced would let the next `failproofai config` hook
|
|
1039
|
+
// it. Project and local scope never count; the selection is per machine.
|
|
1040
|
+
const untraceRemoved = (): void => {
|
|
1041
|
+
if (!opts?.syncAgentSelection || (scope !== "user" && scope !== "all")) return;
|
|
1042
|
+
const acted = selectedClis.filter((id) => scope === "all" || getIntegration(id).scopes.includes("user"));
|
|
1043
|
+
const note = keepAgentSelectionTrue("removed", acted);
|
|
1044
|
+
if (note) say(note);
|
|
1045
|
+
};
|
|
1046
|
+
|
|
900
1047
|
// Remove failproofai hooks from each selected CLI's settings file(s)
|
|
901
1048
|
let totalRemoved = 0;
|
|
902
1049
|
let nothingToReport = false;
|
|
@@ -923,7 +1070,7 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
923
1070
|
|
|
924
1071
|
if (existing.length === 0) {
|
|
925
1072
|
if (scope !== "all" && selectedClis.length === 1) {
|
|
926
|
-
|
|
1073
|
+
say(screenKit(optsFor(process.stdout)).ok("No settings file found, so there is nothing to remove."));
|
|
927
1074
|
nothingToReport = true;
|
|
928
1075
|
}
|
|
929
1076
|
continue;
|
|
@@ -934,13 +1081,13 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
934
1081
|
const removed = integration.removeHooksFromFile(settingsPath);
|
|
935
1082
|
removedHere += removed;
|
|
936
1083
|
if (removed > 0 && scope !== "all") {
|
|
937
|
-
|
|
938
|
-
|
|
1084
|
+
say(screenKit(optsFor(process.stdout)).ok(`Removed ${removed} failproofai ${removed === 1 ? "hook" : "hooks"} from ${integration.displayName} settings.`));
|
|
1085
|
+
for (const line of screenKit(optsFor(process.stdout)).kv([["settings", settingsPath]])) say(line);
|
|
939
1086
|
}
|
|
940
1087
|
}
|
|
941
1088
|
|
|
942
1089
|
if (removedHere === 0 && scope !== "all" && selectedClis.length === 1) {
|
|
943
|
-
|
|
1090
|
+
say(screenKit(optsFor(process.stdout)).ok("No hooks found in settings, so there is nothing to remove."));
|
|
944
1091
|
nothingToReport = true;
|
|
945
1092
|
continue;
|
|
946
1093
|
}
|
|
@@ -948,15 +1095,18 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
948
1095
|
}
|
|
949
1096
|
}
|
|
950
1097
|
|
|
951
|
-
if (nothingToReport && totalRemoved === 0)
|
|
1098
|
+
if (nothingToReport && totalRemoved === 0) {
|
|
1099
|
+
untraceRemoved();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
952
1102
|
|
|
953
1103
|
if (scope === "all") {
|
|
954
|
-
|
|
1104
|
+
say(screenKit(optsFor(process.stdout)).ok(`Removed ${totalRemoved} failproofai ${totalRemoved === 1 ? "hook" : "hooks"} from every scope.`));
|
|
955
1105
|
for (const cliId of selectedClis) {
|
|
956
1106
|
const integration = getIntegration(cliId);
|
|
957
1107
|
for (const s of integration.scopes) {
|
|
958
1108
|
for (const p of settingsPathsFor(integration, s, cwd)) {
|
|
959
|
-
|
|
1109
|
+
say(`${INDENT}${integration.displayName}, ${s}: ${p}`);
|
|
960
1110
|
}
|
|
961
1111
|
}
|
|
962
1112
|
}
|
|
@@ -996,29 +1146,215 @@ export async function removeHooks(policyNames?: string[], scope: HookScope | "al
|
|
|
996
1146
|
const { customPoliciesPath: _drop, customPoliciesPaths: _dropMany, policyParams: _dropParams, ...rest } = existing;
|
|
997
1147
|
writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
|
|
998
1148
|
}
|
|
1149
|
+
|
|
1150
|
+
untraceRemoved();
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** A row in one of the listing's plain sections: custom, convention, cloud. */
|
|
1154
|
+
export interface PoliciesScreenRow {
|
|
1155
|
+
state: "on" | "off" | "failed";
|
|
1156
|
+
name: string;
|
|
1157
|
+
description: string;
|
|
1158
|
+
/** Grey qualifier after the description — `observe` on a cloud policy. */
|
|
1159
|
+
tag?: string;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Everything `failproofai policies` draws, gathered by {@link listHooks} and
|
|
1164
|
+
* drawn by {@link renderPoliciesScreen}.
|
|
1165
|
+
*
|
|
1166
|
+
* Split in two so the drawing is a pure function of this value: it can be
|
|
1167
|
+
* asserted with colour on and off, at any width and with any version, without
|
|
1168
|
+
* a terminal, a home directory or a pack on disk.
|
|
1169
|
+
*/
|
|
1170
|
+
export interface PoliciesScreen {
|
|
1171
|
+
/** Installed packs that loaded, in `installed.json` order. */
|
|
1172
|
+
packs: Array<{
|
|
1173
|
+
id: string;
|
|
1174
|
+
version: string;
|
|
1175
|
+
/** Where it was fetched from — shown only when it is not the repository the id names. */
|
|
1176
|
+
source: string;
|
|
1177
|
+
observe: boolean;
|
|
1178
|
+
/** Jev checks this pack brings that Jev asks. Zero for an observe pack, whose checks are never asked. */
|
|
1179
|
+
jevChecks: number;
|
|
1180
|
+
policies: Array<{ name: string; description: string; category: string; on: boolean }>;
|
|
1181
|
+
}>;
|
|
1182
|
+
/** Packs that are installed and refused to load. */
|
|
1183
|
+
refused: Array<{ id: string | null; reason: string }>;
|
|
1184
|
+
/** The one line that needs attention, already chosen by severity. */
|
|
1185
|
+
attention: { failed: boolean; text: string; fix?: string } | null;
|
|
1186
|
+
custom: PoliciesScreenRow[];
|
|
1187
|
+
convention: Array<{ scope: string; rows: PoliciesScreenRow[] }>;
|
|
1188
|
+
cloud: { deployment: number; rows: PoliciesScreenRow[] } | null;
|
|
1189
|
+
/**
|
|
1190
|
+
* Builtins this build enforces itself because no pack is installed: the
|
|
1191
|
+
* migration shim (`handler.ts` registers `enabledPolicies` when
|
|
1192
|
+
* `hasInstalledRegexPacks()` is false). Listed so the screen does not show
|
|
1193
|
+
* nothing for a machine enforcing all of them.
|
|
1194
|
+
*/
|
|
1195
|
+
legacy?: Array<{ name: string; description: string }>;
|
|
999
1196
|
}
|
|
1000
1197
|
|
|
1001
1198
|
/**
|
|
1002
|
-
*
|
|
1003
|
-
* Layout adapts to the number of installed scopes:
|
|
1004
|
-
* 0 scopes: compact "not installed" summary
|
|
1005
|
-
* 1 scope: table with header + checkmarks, beta policies in a separate section
|
|
1006
|
-
* 2+ scopes: column table with per-scope status, beta policies in a separate section
|
|
1199
|
+
* The policies screen (decision D14: "the policies list exactly as designed").
|
|
1007
1200
|
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1201
|
+
* Packs say where their policies come from once, as `pack` rows at the top,
|
|
1202
|
+
* and every policy is ● on or ○ off. Off policies fold into one "○ n more off"
|
|
1203
|
+
* line per category; `all` lists every one instead.
|
|
1204
|
+
*
|
|
1205
|
+
* Where the design is silent on a real machine, a pack row carries a grey tag
|
|
1206
|
+
* rather than a new state: `observe` for a pack that evaluates and blocks
|
|
1207
|
+
* nothing, so it can never read as enforcing; its Jev checks; the source it
|
|
1208
|
+
* came from when that is not the repository its self-declared id names; and
|
|
1209
|
+
* `failed to load` with the reason for a pack that is installed and refused.
|
|
1012
1210
|
*/
|
|
1013
|
-
export
|
|
1211
|
+
export function renderPoliciesScreen(
|
|
1212
|
+
screen: PoliciesScreen,
|
|
1213
|
+
opts: ScreenKitOpts & { all?: boolean } = {},
|
|
1214
|
+
): string[] {
|
|
1215
|
+
const kit = screenKit(opts);
|
|
1216
|
+
const count = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`;
|
|
1217
|
+
const glyph = (state: PoliciesScreenRow["state"]): string =>
|
|
1218
|
+
state === "on" ? kit.on : state === "off" ? kit.off : kit.failed;
|
|
1219
|
+
const out: string[] = [kit.header("Policies")];
|
|
1220
|
+
|
|
1221
|
+
const kvRows: Array<[string, string]> = [];
|
|
1222
|
+
for (const pack of screen.packs) {
|
|
1223
|
+
const tags: string[] = [];
|
|
1224
|
+
if (pack.observe) tags.push("observe");
|
|
1225
|
+
if (pack.jevChecks > 0) tags.push(count(pack.jevChecks, "Jev check", "Jev checks"));
|
|
1226
|
+
// The id is whatever the manifest claims; the source is the repository this
|
|
1227
|
+
// CLI actually fetched it from. Equal on every ordinary install, so the row
|
|
1228
|
+
// is exactly the design's — and when they differ, saying so is the point.
|
|
1229
|
+
const repo = pack.source.replace(/^github:/i, "").replace(/@[^@]*$/, "");
|
|
1230
|
+
if (repo.toLowerCase() !== pack.id.toLowerCase()) tags.push(`from ${pack.source}`);
|
|
1231
|
+
kvRows.push(["pack", `${pack.id}@${pack.version}${tags.map((t) => `${kit.sep}${kit.meta(t)}`).join("")}`]);
|
|
1232
|
+
}
|
|
1233
|
+
for (const pack of screen.refused) {
|
|
1234
|
+
kvRows.push(["pack", `${pack.id ?? "(unnamed)"}${kit.sep}${kit.meta(`failed to load: ${pack.reason}`)}`]);
|
|
1235
|
+
}
|
|
1236
|
+
const total = screen.packs.reduce((n, pack) => n + pack.policies.length, 0);
|
|
1237
|
+
const enabled = screen.packs.reduce((n, pack) => n + pack.policies.filter((p) => p.on).length, 0);
|
|
1238
|
+
if (total > 0) kvRows.push(["enabled", `${enabled} of ${total}`]);
|
|
1239
|
+
if (kvRows.length > 0) out.push("", ...kit.kv(kvRows));
|
|
1240
|
+
|
|
1241
|
+
const empty =
|
|
1242
|
+
screen.packs.length === 0 &&
|
|
1243
|
+
screen.refused.length === 0 &&
|
|
1244
|
+
screen.custom.length === 0 &&
|
|
1245
|
+
screen.convention.length === 0 &&
|
|
1246
|
+
!screen.cloud &&
|
|
1247
|
+
!screen.legacy?.length;
|
|
1248
|
+
if (screen.attention || empty) out.push("");
|
|
1249
|
+
if (screen.attention) {
|
|
1250
|
+
const { failed, text, fix } = screen.attention;
|
|
1251
|
+
out.push(failed ? kit.fail(text, fix) : kit.caution(text, fix));
|
|
1252
|
+
}
|
|
1253
|
+
if (empty) {
|
|
1254
|
+
// Ours first is a convenience, not a channel: a screen that named only ours
|
|
1255
|
+
// would read as the place policies come from.
|
|
1256
|
+
out.push(
|
|
1257
|
+
`${INDENT}Turn on ours: ${kit.cmd(`failproofai policies add ${CORE_SOURCE}`)}`,
|
|
1258
|
+
`${INDENT}Or anyone's: ${kit.cmd("failproofai policies add <owner>/<repo>")}`,
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Category headings name their pack only when two packs could each have a
|
|
1263
|
+
// category of the same name. A pack of Jev checks alone has no categories, so
|
|
1264
|
+
// the shipped FailproofAI/policies + FailproofAI/jev-policies pairing still
|
|
1265
|
+
// reads exactly as designed.
|
|
1266
|
+
const sectioned = screen.packs.filter((pack) => pack.policies.length > 0);
|
|
1267
|
+
const prefixed = sectioned.length > 1;
|
|
1268
|
+
let folded = 0;
|
|
1269
|
+
const sections: Array<{ heading: string; meta: string; items: Array<[string, string?]> }> = [];
|
|
1270
|
+
for (const pack of sectioned) {
|
|
1271
|
+
// First appearance orders the categories, and a category is gathered
|
|
1272
|
+
// wherever it appears: "Dangerous Commands" comes back after "Infra
|
|
1273
|
+
// Commands" in our own pack, and two headings for it would read as two.
|
|
1274
|
+
for (const category of [...new Set(pack.policies.map((p) => p.category))]) {
|
|
1275
|
+
const inCategory = pack.policies.filter((p) => p.category === category);
|
|
1276
|
+
const on = inCategory.filter((p) => p.on).length;
|
|
1277
|
+
const shown = opts.all ? inCategory : inCategory.filter((p) => p.on);
|
|
1278
|
+
const items: Array<[string, string?]> = shown.map((p) => [
|
|
1279
|
+
`${p.on ? kit.on : kit.off} ${p.name}`,
|
|
1280
|
+
p.description,
|
|
1281
|
+
]);
|
|
1282
|
+
const hidden = inCategory.length - shown.length;
|
|
1283
|
+
if (hidden > 0) items.push([`${kit.off} ${hidden} more off`]);
|
|
1284
|
+
folded += hidden;
|
|
1285
|
+
sections.push({ heading: prefixed ? `${pack.id} · ${category}` : category, meta: `${on} of ${inCategory.length}`, items });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
// One description column for every pack section, so they line up down the
|
|
1289
|
+
// screen: at least the design's 32 for the name, always two spaces past the
|
|
1290
|
+
// longest one shown.
|
|
1291
|
+
const columnFor = (names: string[]): number => Math.max(34, ...names.map((name) => name.length + 4));
|
|
1292
|
+
const packColumn = columnFor(
|
|
1293
|
+
sectioned.flatMap((pack) => pack.policies.filter((p) => opts.all || p.on).map((p) => p.name)),
|
|
1294
|
+
);
|
|
1295
|
+
for (const section of sections) {
|
|
1296
|
+
out.push("", kit.head(section.heading, section.meta), ...kit.rows(section.items, packColumn));
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const plain = (heading: string, meta: string | undefined, rows: PoliciesScreenRow[]): void => {
|
|
1300
|
+
const items = rows.map((row): [string, string] => [
|
|
1301
|
+
`${glyph(row.state)} ${row.name}`,
|
|
1302
|
+
row.tag ? `${row.description}${kit.sep}${kit.meta(row.tag)}` : row.description,
|
|
1303
|
+
]);
|
|
1304
|
+
const column = columnFor(rows.filter((row) => row.description).map((row) => row.name));
|
|
1305
|
+
out.push("", kit.head(heading, meta), ...kit.rows(items, column));
|
|
1306
|
+
};
|
|
1307
|
+
// A count, not the names: since packs carry the policies, this screen lists
|
|
1308
|
+
// no builtin by name (#738 pins that), but it must not draw a machine that is
|
|
1309
|
+
// enforcing thirty of them as if nothing were there.
|
|
1310
|
+
if (screen.legacy?.length) {
|
|
1311
|
+
out.push(
|
|
1312
|
+
"",
|
|
1313
|
+
kit.head("Built in", `${screen.legacy.length} on, from before packs`),
|
|
1314
|
+
`${INDENT}Move them into a pack: ${kit.cmd(`failproofai policies add ${CORE_SOURCE}`)}`,
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (screen.custom.length > 0) plain("Custom policies", undefined, screen.custom);
|
|
1318
|
+
for (const { scope, rows } of screen.convention) plain("Convention policies", scope, rows);
|
|
1319
|
+
if (screen.cloud) plain("Cloud-managed", `deployment ${screen.cloud.deployment}`, screen.cloud.rows);
|
|
1320
|
+
|
|
1321
|
+
if (folded > 0) out.push("", `See all ${total} with ${kit.cmd("failproofai policies --all")}`);
|
|
1322
|
+
return out;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* `failproofai policies`: every policy on this machine and whether it is on.
|
|
1327
|
+
*
|
|
1328
|
+
* Gathers the screen and draws it with {@link renderPoliciesScreen}. At most
|
|
1329
|
+
* one line asks for attention, the most severe first: a pack that will not load
|
|
1330
|
+
* and so DENIES what it covers, then the machine not enforcing at all
|
|
1331
|
+
* ({@link notEnforcingReason}, the same answer the launch screen gives), then a
|
|
1332
|
+
* custom policy file that is not running, hooks in more than one scope, and an
|
|
1333
|
+
* unknown `policyParams` key.
|
|
1334
|
+
*
|
|
1335
|
+
* `all` lists every policy instead of folding the off ones.
|
|
1336
|
+
*/
|
|
1337
|
+
export async function listHooks(cwd?: string, { all = false }: { all?: boolean } = {}): Promise<void> {
|
|
1014
1338
|
const config = readMergedHooksConfig(cwd);
|
|
1015
1339
|
const disabledCustomSet = new Set(config.disabledCustomPolicies ?? []);
|
|
1016
|
-
const opts = optsFor(process.stdout);
|
|
1017
1340
|
|
|
1018
1341
|
// Determine which scopes have hooks installed (deduplicate when paths overlap, e.g. cwd === home)
|
|
1019
1342
|
const uniqueScopes = deduplicateScopes(HOOK_SCOPES, cwd);
|
|
1020
1343
|
const installedScopes = uniqueScopes.filter((s) => hooksInstalledInSettings(s, cwd));
|
|
1021
1344
|
|
|
1345
|
+
// One read of the pack store for the whole listing. It never throws by
|
|
1346
|
+
// contract; the guard is for a listing never being the thing that turns an
|
|
1347
|
+
// unreadable manifest into a broken command.
|
|
1348
|
+
const packRead: Pick<ReturnType<typeof readInstalledPacks>, "packs" | "errors"> = (() => {
|
|
1349
|
+
try {
|
|
1350
|
+
const read = readInstalledPacks();
|
|
1351
|
+
return { packs: read?.packs ?? [], errors: read?.errors ?? [] };
|
|
1352
|
+
} catch {
|
|
1353
|
+
return { packs: [], errors: [] };
|
|
1354
|
+
}
|
|
1355
|
+
})();
|
|
1356
|
+
const packErrors = packRead.errors;
|
|
1357
|
+
|
|
1022
1358
|
// Names a `policyParams` key may legitimately use: every policy an installed
|
|
1023
1359
|
// pack carries. Previously the compiled catalog, which no longer describes
|
|
1024
1360
|
// what runs.
|
|
@@ -1032,7 +1368,7 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1032
1368
|
// helper rather than a third copy of the `pack/<id>/<name>` format.
|
|
1033
1369
|
const knownPolicyNames = new Set<string>();
|
|
1034
1370
|
try {
|
|
1035
|
-
for (const pack of
|
|
1371
|
+
for (const pack of packRead.packs) {
|
|
1036
1372
|
for (const policy of pack.policies) {
|
|
1037
1373
|
knownPolicyNames.add(policy.name);
|
|
1038
1374
|
knownPolicyNames.add(packPolicyParamKey(pack.id, policy.name));
|
|
@@ -1042,157 +1378,14 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1042
1378
|
// Unreadable manifest: skip the typo warning rather than invent one.
|
|
1043
1379
|
}
|
|
1044
1380
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
return readInstalledPacks().packs.reduce(
|
|
1049
|
-
(n, pack) => n + (pack.enabled ?? pack.policies.map((p) => p.name)).length,
|
|
1050
|
-
0,
|
|
1051
|
-
);
|
|
1052
|
-
} catch {
|
|
1053
|
-
return 0;
|
|
1054
|
-
}
|
|
1055
|
-
})();
|
|
1056
|
-
// `installedScopes` is about HOOKS — whether any agent CLI is wired to call
|
|
1057
|
-
// us — and it says nothing about whether policies exist. Reporting it as
|
|
1058
|
-
// "not installed" above a listing of an installed pack was a flat
|
|
1059
|
-
// contradiction, and it hid the state that actually matters: a machine can
|
|
1060
|
-
// hold thirty-eight policies and enforce none of them, because nothing is
|
|
1061
|
-
// calling failproofai at all. That is the worst of the three states and it
|
|
1062
|
-
// used to read like the emptiest.
|
|
1063
|
-
const packsPresent = (() => {
|
|
1064
|
-
try {
|
|
1065
|
-
return readInstalledPacks().packs.length > 0;
|
|
1066
|
-
} catch {
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
})();
|
|
1070
|
-
const subtitle =
|
|
1071
|
-
installedScopes.length > 0
|
|
1072
|
-
? `${installedScopes.join(" + ")} · ${packCount} on`
|
|
1073
|
-
: packsPresent
|
|
1074
|
-
? `${packCount} on · NOT ENFORCING`
|
|
1075
|
-
: "nothing installed";
|
|
1076
|
-
groups.push(title("failproofai policies", subtitle, opts));
|
|
1077
|
-
|
|
1078
|
-
if (installedScopes.length === 0) {
|
|
1079
|
-
groups.push(
|
|
1080
|
-
packsPresent
|
|
1081
|
-
? warning(
|
|
1082
|
-
[
|
|
1083
|
-
// ONE string, not four hand-broken ones. `warning` wraps each
|
|
1084
|
-
// element to the terminal, so pre-wrapped prose keeps its author's
|
|
1085
|
-
// line breaks as paragraph breaks and then wraps again inside
|
|
1086
|
-
// them — which at 60 columns left the word "them." alone on a
|
|
1087
|
-
// line. The command stays separate because it is not prose and
|
|
1088
|
-
// must never be split across a wrap.
|
|
1089
|
-
"These policies are installed but NOTHING is running them. No agent " +
|
|
1090
|
-
"CLI on this machine is wired to call failproofai, so every policy " +
|
|
1091
|
-
"below is inert. Wire it up with:",
|
|
1092
|
-
" failproofai config",
|
|
1093
|
-
],
|
|
1094
|
-
opts,
|
|
1095
|
-
)
|
|
1096
|
-
: nextStep(
|
|
1097
|
-
"failproofai config",
|
|
1098
|
-
config.enabledPolicies.length > 0
|
|
1099
|
-
? "These are configured but NOT installed — no hook is running them:"
|
|
1100
|
-
: "Nothing is set up yet. Get started with:",
|
|
1101
|
-
opts,
|
|
1102
|
-
),
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
// Held back to the end rather than printed here. Everything below this point
|
|
1106
|
-
// is another section of the same listing, and a footer in the middle of it
|
|
1107
|
-
// reads as the end of the output — while a warning at the very end is the one
|
|
1108
|
-
// a reader scrolling back from their prompt actually sees.
|
|
1109
|
-
const footer: Array<string[] | null> = [note(`Config: ${globalPolicyConfigFile()}`, opts)];
|
|
1110
|
-
|
|
1111
|
-
// A machine with hooks wired and no policies to run is the normal state right
|
|
1112
|
-
// after setup — setup installs the hooks and deliberately chooses nothing —
|
|
1113
|
-
// and this listing said NOTHING about how to leave it. A reader who had just
|
|
1114
|
-
// finished `failproofai config` saw a header, a config path, and no next
|
|
1115
|
-
// step. Ours is named in full, the same way anyone else's pack is typed,
|
|
1116
|
-
// because it IS one.
|
|
1117
|
-
// `packCount` counts ENABLED policies, so a pack installed with everything
|
|
1118
|
-
// switched off reaches zero the same way an empty machine does — and the
|
|
1119
|
-
// advice for the two could not be more different. Telling somebody who has
|
|
1120
|
-
// just unticked all 39 to `policies add FailproofAI/policies` sends them to
|
|
1121
|
-
// install what they already have, and doing it would change nothing: the
|
|
1122
|
-
// selection is what is empty, not the shelf.
|
|
1123
|
-
//
|
|
1124
|
-
// Nor is it "nothing" while a refused pack fails closed (the warning above
|
|
1125
|
-
// says what is denied), or while a pack's Jev checks are live with no regex
|
|
1126
|
-
// policy on.
|
|
1127
|
-
const { packsInstalled, jevChecks, failClosed } = (() => {
|
|
1128
|
-
try {
|
|
1129
|
-
const { packs, errors } = readInstalledPacks();
|
|
1130
|
-
return {
|
|
1131
|
-
packsInstalled: packs.length,
|
|
1132
|
-
jevChecks: packs.reduce((n, p) => n + (p.effect === "observe" ? 0 : (p.semantic?.length ?? 0)), 0),
|
|
1133
|
-
// The refusals `missingGuards` denies over (pack-failclosed.ts).
|
|
1134
|
-
failClosed: errors.some((e) => e.effect !== "observe" && !e.semanticOnly),
|
|
1135
|
-
};
|
|
1136
|
-
} catch {
|
|
1137
|
-
return { packsInstalled: 0, jevChecks: 0, failClosed: false };
|
|
1138
|
-
}
|
|
1139
|
-
})();
|
|
1140
|
-
if (failClosed) {
|
|
1141
|
-
// Said by the pack section's warning, where the refused pack is named.
|
|
1142
|
-
} else if (packCount === 0 && packsInstalled === 0) {
|
|
1143
|
-
footer.unshift(
|
|
1144
|
-
nextStep(
|
|
1145
|
-
`failproofai policies add ${CORE_SOURCE}`,
|
|
1146
|
-
"Nothing is enforcing yet. Take ours, or anyone's:",
|
|
1147
|
-
opts,
|
|
1148
|
-
),
|
|
1149
|
-
note("Someone else's: failproofai policies add <owner>/<repo>", opts),
|
|
1150
|
-
note("Look first: failproofai policies show <owner>/<repo>", opts),
|
|
1151
|
-
);
|
|
1152
|
-
} else if (packCount === 0 && jevChecks > 0) {
|
|
1153
|
-
footer.unshift(
|
|
1154
|
-
note(
|
|
1155
|
-
`No regex policy is on; ${jevChecks} Jev check${jevChecks === 1 ? "" : "s"} from ` +
|
|
1156
|
-
`${packsInstalled === 1 ? "the pack" : "the packs"} above apply where Jev is configured (failproofai jev status).`,
|
|
1157
|
-
opts,
|
|
1158
|
-
),
|
|
1159
|
-
);
|
|
1160
|
-
} else if (packCount === 0) {
|
|
1161
|
-
footer.unshift(
|
|
1162
|
-
nextStep(
|
|
1163
|
-
"failproofai policies add",
|
|
1164
|
-
`Nothing is enforcing: ${packsInstalled === 1 ? "the pack above is" : "the packs above are"} installed with everything switched off. Pick what should be on:`,
|
|
1165
|
-
opts,
|
|
1166
|
-
),
|
|
1167
|
-
note("Or by name: failproofai policies add <policy>", opts),
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
if (installedScopes.length > 1) {
|
|
1172
|
-
footer.push(
|
|
1173
|
-
warning(
|
|
1174
|
-
[
|
|
1175
|
-
`Hooks in multiple scopes (${installedScopes.join(", ")}).`,
|
|
1176
|
-
"Consider keeping one. Remove with: failproofai policies --uninstall --scope <scope>",
|
|
1177
|
-
],
|
|
1178
|
-
opts,
|
|
1179
|
-
),
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Warn about unknown policyParams keys
|
|
1381
|
+
// Unknown policyParams keys. The event fires whenever there is one, whether or
|
|
1382
|
+
// not a more severe line takes the screen's one attention slot.
|
|
1383
|
+
const unknownKeys: string[] = [];
|
|
1184
1384
|
if (config.policyParams) {
|
|
1185
|
-
const unknownKeys: string[] = [];
|
|
1186
1385
|
for (const key of Object.keys(config.policyParams)) {
|
|
1187
1386
|
if (knownPolicyNames.size > 0 && !knownPolicyNames.has(key)) unknownKeys.push(key);
|
|
1188
1387
|
}
|
|
1189
1388
|
if (unknownKeys.length > 0) {
|
|
1190
|
-
footer.push(
|
|
1191
|
-
warning(
|
|
1192
|
-
unknownKeys.map((key) => `unknown policyParams key "${key}" — possible typo`),
|
|
1193
|
-
opts,
|
|
1194
|
-
),
|
|
1195
|
-
);
|
|
1196
1389
|
try {
|
|
1197
1390
|
await trackHookEvent(getInstanceId(), "policy_params_validation_warning", {
|
|
1198
1391
|
unknown_keys_count: unknownKeys.length,
|
|
@@ -1202,40 +1395,31 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1202
1395
|
}
|
|
1203
1396
|
}
|
|
1204
1397
|
|
|
1205
|
-
// Explicit
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
chip(disabledCustomSet.has(`custom:${absPath}:${hook.name}`) ? "off" : "on", opts),
|
|
1231
|
-
hook.name,
|
|
1232
|
-
hook.description ?? "",
|
|
1233
|
-
]),
|
|
1234
|
-
},
|
|
1235
|
-
opts,
|
|
1236
|
-
),
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1398
|
+
// Explicit custom policy files.
|
|
1399
|
+
const custom: PoliciesScreenRow[] = [];
|
|
1400
|
+
let customProblem: string | null = null;
|
|
1401
|
+
for (const path of configuredCustomPolicyPaths(config)) {
|
|
1402
|
+
// Enforcement resolves configured paths from the project config root.
|
|
1403
|
+
// Use the same canonical path here so the ID checked by the CLI exactly
|
|
1404
|
+
// matches the ID written by the dashboard.
|
|
1405
|
+
const absPath = resolve(findProjectConfigDir(cwd ?? process.cwd()), path);
|
|
1406
|
+
if (!existsSync(absPath)) {
|
|
1407
|
+
custom.push({ state: "failed", name: absPath, description: "not found" });
|
|
1408
|
+
customProblem ??= "A custom policy file was not found, so its policies are not running.";
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
const hooks = await loadCustomHooks(absPath);
|
|
1412
|
+
if (hooks.length === 0) {
|
|
1413
|
+
custom.push({ state: "failed", name: absPath, description: "failed to load" });
|
|
1414
|
+
customProblem ??= "A custom policy file failed to load, so its policies are not running.";
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
for (const hook of hooks) {
|
|
1418
|
+
custom.push({
|
|
1419
|
+
state: disabledCustomSet.has(`custom:${absPath}:${hook.name}`) ? "off" : "on",
|
|
1420
|
+
name: hook.name,
|
|
1421
|
+
description: hook.description ?? "",
|
|
1422
|
+
});
|
|
1239
1423
|
}
|
|
1240
1424
|
}
|
|
1241
1425
|
|
|
@@ -1248,14 +1432,14 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1248
1432
|
const projectDir = resolve(base, ".failproofai", "policies");
|
|
1249
1433
|
const userDir = customPoliciesDir();
|
|
1250
1434
|
const sameDir = userDir === projectDir;
|
|
1251
|
-
const conventionDirs: {
|
|
1252
|
-
{
|
|
1435
|
+
const conventionDirs: { scope: string; dir: string }[] = [
|
|
1436
|
+
{ scope: sameDir ? "project + user" : "project", dir: projectDir },
|
|
1253
1437
|
// Running from $HOME makes both paths identical. Listing the directory
|
|
1254
1438
|
// twice printed every file a second time as "failed to load" — the file was
|
|
1255
1439
|
// already imported by the first pass, so the module cache short-circuits
|
|
1256
1440
|
// `customPolicies.add` and `loadCustomHooks` legitimately returns 0 hooks.
|
|
1257
1441
|
// Nothing was wrong with the policy; the second listing was.
|
|
1258
|
-
...(sameDir ? [] : [{
|
|
1442
|
+
...(sameDir ? [] : [{ scope: "user", dir: userDir }]),
|
|
1259
1443
|
];
|
|
1260
1444
|
|
|
1261
1445
|
// Record of what was found, mirrored into policies-config.json below so the
|
|
@@ -1265,7 +1449,8 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1265
1449
|
user: [],
|
|
1266
1450
|
};
|
|
1267
1451
|
|
|
1268
|
-
|
|
1452
|
+
const convention: PoliciesScreen["convention"] = [];
|
|
1453
|
+
for (const { scope, dir } of conventionDirs) {
|
|
1269
1454
|
const files = discoverPolicyFiles(dir);
|
|
1270
1455
|
if (files.length === 0) continue;
|
|
1271
1456
|
|
|
@@ -1280,127 +1465,87 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1280
1465
|
for (const t of targets) discovered[t].push({ file, hooks });
|
|
1281
1466
|
};
|
|
1282
1467
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const rows:
|
|
1468
|
+
// One row per FILE: ● while any of its hooks is on, ○ once all of them are
|
|
1469
|
+
// off, and a count of the ones switched off when it is some of them.
|
|
1470
|
+
const rows: PoliciesScreenRow[] = [];
|
|
1286
1471
|
for (const file of files) {
|
|
1287
1472
|
const filename = basename(file);
|
|
1288
1473
|
try {
|
|
1289
1474
|
const hooks = await loadCustomHooks(file);
|
|
1290
1475
|
record(filename, hooks.map((h) => h.name));
|
|
1291
1476
|
if (hooks.length === 0) {
|
|
1292
|
-
rows.push({
|
|
1477
|
+
rows.push({ state: "failed", name: filename, description: "failed to load" });
|
|
1293
1478
|
continue;
|
|
1294
1479
|
}
|
|
1295
|
-
const
|
|
1296
|
-
hook,
|
|
1297
|
-
|
|
1298
|
-
}));
|
|
1299
|
-
const disabledCount = hookStates.filter((entry) => entry.disabled).length;
|
|
1300
|
-
const state: ChipState =
|
|
1301
|
-
disabledCount === 0 ? "on" : disabledCount === hooks.length ? "off" : "mixed";
|
|
1302
|
-
const hookSummary = hookStates
|
|
1303
|
-
.map(({ hook, disabled }) => `${hook.name}${disabled ? " (OFF)" : ""}`)
|
|
1304
|
-
.join(", ");
|
|
1480
|
+
const off = hooks.filter((hook) =>
|
|
1481
|
+
disabledCustomSet.has(`convention:${policyScope}:${filename}:${hook.name}`),
|
|
1482
|
+
).length;
|
|
1305
1483
|
rows.push({
|
|
1306
|
-
|
|
1484
|
+
state: off === hooks.length ? "off" : "on",
|
|
1485
|
+
name: filename,
|
|
1486
|
+
description:
|
|
1487
|
+
`${hooks.length} ${hooks.length === 1 ? "hook" : "hooks"}` +
|
|
1488
|
+
(off > 0 && off < hooks.length ? ` (${off} off)` : ""),
|
|
1307
1489
|
});
|
|
1308
1490
|
} catch {
|
|
1309
1491
|
record(filename, []);
|
|
1310
|
-
rows.push({
|
|
1492
|
+
rows.push({ state: "failed", name: filename, description: "failed to load" });
|
|
1311
1493
|
}
|
|
1312
1494
|
}
|
|
1313
|
-
|
|
1495
|
+
convention.push({ scope, rows });
|
|
1314
1496
|
}
|
|
1315
1497
|
|
|
1316
1498
|
// Installed packs. They enforce on this machine exactly like every section
|
|
1317
1499
|
// above, and until now the only way to see one was `failproofai policies` —
|
|
1318
1500
|
// so the command that answers "what is enforcing here?" answered it with a
|
|
1319
1501
|
// subset, for the one source a person had to go out of their way to install.
|
|
1502
|
+
const packs: PoliciesScreen["packs"] = [];
|
|
1320
1503
|
try {
|
|
1321
|
-
const
|
|
1322
|
-
for (const pack of packs) {
|
|
1504
|
+
for (const pack of packRead.packs) {
|
|
1323
1505
|
const taken = pack.enabled ?? pack.policies.map((p) => p.name);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
? "observe"
|
|
1340
|
-
: !taken.includes(policy.name) || disabled
|
|
1341
|
-
? "off"
|
|
1342
|
-
: "pack";
|
|
1343
|
-
return [chip(state, opts), policy.name, policy.description];
|
|
1344
|
-
}),
|
|
1345
|
-
},
|
|
1346
|
-
opts,
|
|
1347
|
-
),
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
if (errors.length > 0) {
|
|
1351
|
-
groups.push(
|
|
1352
|
-
warning(
|
|
1353
|
-
errors.flatMap((err) => [
|
|
1354
|
-
`pack ${err.id ?? "(unnamed)"} will not load: ${err.reason}`,
|
|
1355
|
-
...(err.effect !== "observe" && !err.semanticOnly
|
|
1356
|
-
? [
|
|
1357
|
-
"Until it loads, the tool calls its policies cover are DENIED. Fix it, or remove it with: " +
|
|
1358
|
-
`failproofai policies remove ${err.id ?? "<id>"}`,
|
|
1359
|
-
]
|
|
1360
|
-
: []),
|
|
1361
|
-
]),
|
|
1362
|
-
opts,
|
|
1363
|
-
),
|
|
1364
|
-
);
|
|
1506
|
+
packs.push({
|
|
1507
|
+
id: pack.id,
|
|
1508
|
+
version: pack.version,
|
|
1509
|
+
source: pack.source,
|
|
1510
|
+
observe: pack.effect === "observe",
|
|
1511
|
+
jevChecks: pack.effect === "observe" ? 0 : (pack.semantic?.length ?? 0),
|
|
1512
|
+
policies: pack.policies.map((policy) => ({
|
|
1513
|
+
name: policy.name,
|
|
1514
|
+
description: policy.description,
|
|
1515
|
+
category: policy.category,
|
|
1516
|
+
on:
|
|
1517
|
+
taken.includes(policy.name) &&
|
|
1518
|
+
!disabledCustomSet.has(`pack:${pack.id}@${pack.version}:${policy.name}`),
|
|
1519
|
+
})),
|
|
1520
|
+
});
|
|
1365
1521
|
}
|
|
1366
1522
|
} catch {
|
|
1367
1523
|
// Same rule as the cloud section below: a listing must not be the thing
|
|
1368
1524
|
// that turns an unreadable manifest into a broken command.
|
|
1369
1525
|
}
|
|
1526
|
+
const refused = packErrors.map((err) => ({ id: err.id, reason: err.reason }));
|
|
1370
1527
|
|
|
1371
|
-
// Cloud-managed policies. These enforce on this machine exactly like the
|
|
1528
|
+
// Cloud-managed policies. These enforce on this machine exactly like the
|
|
1372
1529
|
// sections above, but nothing here listed them — so `failproofai policies`
|
|
1373
1530
|
// answered "what is enforcing?" with a subset, and the policies an operator
|
|
1374
1531
|
// pushed to a fleet were the ones invisible to the person running the
|
|
1375
|
-
// command on it.
|
|
1376
|
-
//
|
|
1377
|
-
|
|
1378
|
-
// config. `--uninstall <name>` cannot switch one off, and printing them
|
|
1379
|
-
// beside toggleable rows without saying so would imply it can.
|
|
1532
|
+
// command on it. Read-only: they belong to the deployment, which the heading
|
|
1533
|
+
// says, and `--uninstall <name>` cannot switch one off.
|
|
1534
|
+
let cloud: PoliciesScreen["cloud"] = null;
|
|
1380
1535
|
try {
|
|
1381
|
-
const
|
|
1382
|
-
if (
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
`v${artifact.version}`,
|
|
1395
|
-
]),
|
|
1396
|
-
flex: 1,
|
|
1397
|
-
},
|
|
1398
|
-
opts,
|
|
1399
|
-
),
|
|
1400
|
-
);
|
|
1401
|
-
groups.push(
|
|
1402
|
-
note("Managed from the dashboard — not switchable with `failproofai policies`.", opts),
|
|
1403
|
-
);
|
|
1536
|
+
const active = readActiveCloudManagedPolicies();
|
|
1537
|
+
if (active.length > 0) {
|
|
1538
|
+
cloud = {
|
|
1539
|
+
deployment: active[0].deployment,
|
|
1540
|
+
rows: active.map((artifact) => ({
|
|
1541
|
+
state: "on" as const,
|
|
1542
|
+
name: artifact.id,
|
|
1543
|
+
description: `v${artifact.version}`,
|
|
1544
|
+
// `observe` is evaluated and then has its verdict discarded, so the
|
|
1545
|
+
// row says so rather than reading as plain enforcement.
|
|
1546
|
+
...(artifact.effect === "observe" ? { tag: "observe" } : {}),
|
|
1547
|
+
})),
|
|
1548
|
+
};
|
|
1404
1549
|
}
|
|
1405
1550
|
} catch {
|
|
1406
1551
|
// A machine with no deployment, or an unreadable manifest, simply has no
|
|
@@ -1408,7 +1553,58 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1408
1553
|
// the thing that turns a bad manifest into a broken command.
|
|
1409
1554
|
}
|
|
1410
1555
|
|
|
1411
|
-
|
|
1556
|
+
// The one attention line, most severe first. A refused pack that fails closed
|
|
1557
|
+
// is a FAILURE, not "not enforcing": it denies the calls its policies cover
|
|
1558
|
+
// (pack-failclosed.ts), so it is named before anything else.
|
|
1559
|
+
const failClosed = packErrors.filter((err) => err.effect !== "observe" && !err.semanticOnly);
|
|
1560
|
+
let attention: PoliciesScreen["attention"] = null;
|
|
1561
|
+
if (failClosed.length > 0) {
|
|
1562
|
+
const [first] = failClosed;
|
|
1563
|
+
const many = failClosed.length > 1;
|
|
1564
|
+
attention = {
|
|
1565
|
+
failed: true,
|
|
1566
|
+
text:
|
|
1567
|
+
`${first.id ?? "A pack"}${many ? ` and ${failClosed.length - 1} more` : ""} failed to load, ` +
|
|
1568
|
+
`so the calls ${many ? "they cover" : "it covers"} are denied.`,
|
|
1569
|
+
fix: `failproofai policies remove ${first.id ?? "<id>"}`,
|
|
1570
|
+
};
|
|
1571
|
+
} else if (notEnforcingReason(cwd) !== null) {
|
|
1572
|
+
// One generic line whatever the cause (decision D18).
|
|
1573
|
+
attention = { failed: false, text: "Policies are not enforcing yet.", fix: "failproofai config" };
|
|
1574
|
+
} else if (customProblem) {
|
|
1575
|
+
attention = { failed: false, text: customProblem };
|
|
1576
|
+
} else if (installedScopes.length > 1) {
|
|
1577
|
+
attention = {
|
|
1578
|
+
failed: false,
|
|
1579
|
+
text: `Hooks are installed in multiple scopes: ${installedScopes.join(", ")}.`,
|
|
1580
|
+
fix: "failproofai policies --uninstall --scope <scope>",
|
|
1581
|
+
};
|
|
1582
|
+
} else if (unknownKeys.length > 0) {
|
|
1583
|
+
attention = {
|
|
1584
|
+
failed: false,
|
|
1585
|
+
text:
|
|
1586
|
+
unknownKeys.length === 1
|
|
1587
|
+
? `Unknown policyParams key "${unknownKeys[0]}", possibly a typo.`
|
|
1588
|
+
: `Unknown policyParams keys ${unknownKeys.map((key) => `"${key}"`).join(", ")}, possibly typos.`,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// The same test the hook path uses to decide it enforces these itself.
|
|
1593
|
+
const legacy = hasInstalledRegexPacks()
|
|
1594
|
+
? []
|
|
1595
|
+
: BUILTIN_POLICIES.filter((p) => config.enabledPolicies.includes(p.name)).map((p) => ({
|
|
1596
|
+
name: p.name,
|
|
1597
|
+
description: p.description,
|
|
1598
|
+
}));
|
|
1599
|
+
|
|
1600
|
+
printBlock(
|
|
1601
|
+
process.stdout,
|
|
1602
|
+
renderPoliciesScreen(
|
|
1603
|
+
{ packs, refused, attention, custom, convention, cloud, legacy },
|
|
1604
|
+
// Descriptions are shortened only for a terminal; a pipe keeps every word.
|
|
1605
|
+
{ ...optsFor(process.stdout), fit: Boolean(process.stdout.isTTY), all },
|
|
1606
|
+
),
|
|
1607
|
+
);
|
|
1412
1608
|
|
|
1413
1609
|
// Mirror what was just listed into the USER config. Safe here because
|
|
1414
1610
|
// `failproofai policies` is a one-shot command — never do this on the hook
|