failproofai 1.0.7 → 1.0.8
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 +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/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.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/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.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/_not-found/__PAGE__.segment.rsc +14 -14
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
- 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.nft.json +1 -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.nft.json +1 -1
- 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.nft.json +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 +9 -7
- 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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- 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 +6 -6
- 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 +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -10
- 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 +8 -7
- 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 -6
- 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 +7 -6
- 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/server-reference-manifest.json +52 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -7
- 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]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- 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 +16 -16
- package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -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/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- 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 +67 -23
- package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
- package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
- package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
- package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
- package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
- package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
- package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
- package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
- package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
- package/.next/standalone/app/actions/get-jev-config.ts +604 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +569 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +14 -3
- package/.next/standalone/app/settings/jev-panel.tsx +630 -0
- package/.next/standalone/app/settings/page.tsx +20 -1
- package/.next/standalone/app/settings/settings-client.tsx +27 -1
- package/.next/standalone/app/settings/settings.css +79 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
- package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +3 -2
- package/bin/failproofai.mjs +148 -4
- package/dist/cli.mjs +18599 -9478
- package/dist/index.js +19 -1
- package/dist/worker.mjs +10500 -3773
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +53 -2
- package/src/audit/features.ts +3 -2
- package/src/hooks/builtin-policies.ts +195 -8
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +57 -8
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +98 -10
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +243 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +303 -7
- package/src/hooks/hook-activity-store.ts +118 -6
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +2172 -0
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/manager.ts +45 -7
- package/src/hooks/pack-cli.ts +568 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +489 -7
- package/src/hooks/pack-store.ts +194 -11
- package/src/hooks/policy-authority.ts +386 -0
- package/src/hooks/policy-catalog.ts +206 -0
- package/src/hooks/policy-evaluator.ts +963 -796
- package/src/hooks/policy-registry.ts +27 -1
- package/src/hooks/policy-reviewability.ts +258 -0
- package/src/hooks/policy-types.ts +128 -0
- package/src/hooks/semantic/combine.ts +581 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +513 -0
- package/src/hooks/semantic/envelope.ts +1255 -0
- package/src/hooks/semantic/evaluator.ts +556 -0
- package/src/hooks/semantic/facts.ts +387 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +1155 -0
- package/src/hooks/semantic/jev-config.ts +1142 -0
- package/src/hooks/semantic/jev-review.ts +389 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +427 -0
- package/src/hooks/semantic/pack-policies.ts +294 -0
- package/src/hooks/semantic/policies.ts +597 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2937 -0
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +162 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/types.ts +1 -1
- package/src/hooks/worker-server.ts +121 -27
- package/src/index.ts +6 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
- package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
- package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
|
@@ -17,7 +17,7 @@ import { readFile } from "node:fs/promises";
|
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { hookLogWarn, hookLogError, hookLogInfo } from "./hook-logger";
|
|
20
|
-
import { customPolicies, getCustomHooks, clearCustomHooks } from "./custom-hooks-registry";
|
|
20
|
+
import { customPolicies, getCustomHooks, getSemanticRegistrations, clearCustomHooks } from "./custom-hooks-registry";
|
|
21
21
|
import {
|
|
22
22
|
findDistIndex,
|
|
23
23
|
rewriteFileTree,
|
|
@@ -33,6 +33,8 @@ import type { CustomHook, PolicyCatalogEntry } from "./policy-types";
|
|
|
33
33
|
import type { CloudManagedPolicyArtifact } from "./cloud-managed-policies";
|
|
34
34
|
import type { ResolvedPack } from "./pack-manifest";
|
|
35
35
|
import { customPoliciesDir, shimsDir } from "./fp-home";
|
|
36
|
+
import { effectiveReviewerNames } from "./effective-reviewers";
|
|
37
|
+
import { refusedAuthorityWarning, warnAuthority, withMergedAuthority } from "./policy-authority";
|
|
36
38
|
|
|
37
39
|
const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
|
|
38
40
|
|
|
@@ -178,6 +180,8 @@ export interface PolicyLoadFailure {
|
|
|
178
180
|
reason: string;
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
const warnedSemanticOutsidePack = new Set<string>();
|
|
184
|
+
|
|
181
185
|
async function loadSingleFile(
|
|
182
186
|
absPath: string,
|
|
183
187
|
opts?: {
|
|
@@ -185,6 +189,12 @@ async function loadSingleFile(
|
|
|
185
189
|
conventionScope?: "project" | "user";
|
|
186
190
|
/** Cloud-managed policies pass their pinned digest for load-time re-verification. */
|
|
187
191
|
verifyEntrySha?: string;
|
|
192
|
+
/**
|
|
193
|
+
* Not a pack artifact, so a `semanticPolicies.add` in it is never asked: a
|
|
194
|
+
* Jev check reaches a machine only through a pack's manifest. Said once per
|
|
195
|
+
* file, whether or not Jev is configured — the declaration is dead either way.
|
|
196
|
+
*/
|
|
197
|
+
outsidePack?: boolean;
|
|
188
198
|
},
|
|
189
199
|
): Promise<PolicyLoadFailure | null> {
|
|
190
200
|
const g = globalThis as Record<string, unknown>;
|
|
@@ -220,7 +230,16 @@ async function loadSingleFile(
|
|
|
220
230
|
const entryTmp = absPath + tmpSuffix;
|
|
221
231
|
const fileUrl = pathToFileURL(entryTmp).href;
|
|
222
232
|
const hooksBefore = getCustomHooks().length;
|
|
233
|
+
const semanticBefore = getSemanticRegistrations().length;
|
|
223
234
|
await importWithDeadline(fileUrl);
|
|
235
|
+
const ignored = getSemanticRegistrations().slice(semanticBefore).map((d) => d?.name);
|
|
236
|
+
if (opts?.outsidePack && ignored.length > 0 && !warnedSemanticOutsidePack.has(absPath)) {
|
|
237
|
+
warnedSemanticOutsidePack.add(absPath);
|
|
238
|
+
hookLogWarn(
|
|
239
|
+
`${basename(absPath)}: semanticPolicies.add only takes effect in a pack published with \`failproofai publish\`; ` +
|
|
240
|
+
`${ignored.join(", ")} ${ignored.length === 1 ? "is" : "are"} never asked here`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
224
243
|
if (policyModuleCache.size >= POLICY_MODULE_CACHE_MAX_ENTRIES) policyModuleCache.clear();
|
|
225
244
|
policyModuleCache.set(absPath, {
|
|
226
245
|
fingerprint,
|
|
@@ -448,9 +467,33 @@ export async function loadAllCustomHooks(
|
|
|
448
467
|
`cloud-managed policies ${existing.id} and ${policy.id} have identical source, so they share ` +
|
|
449
468
|
`one artifact and load as one policy; enforcing it if either asks to enforce`,
|
|
450
469
|
);
|
|
451
|
-
|
|
452
|
-
|
|
470
|
+
// Authority resolves toward HARD, for the same reason: reviewable only if
|
|
471
|
+
// every assignment behind these bytes says so. Keeping one record's
|
|
472
|
+
// declaration let the order of active.json decide, so a team's reviewable
|
|
473
|
+
// assignment could clear what an org-wide hard one enforces.
|
|
474
|
+
//
|
|
475
|
+
// Judged against the checks THIS MACHINE can ask, which is what registration
|
|
476
|
+
// will judge these same declarations by. Against the builtin set instead, a
|
|
477
|
+
// `reviewedBy` naming a check an installed pack ships reads as unknown here,
|
|
478
|
+
// both assignments resolve hard, and the merge has no way to tell the
|
|
479
|
+
// reviewable one from the hard one. Read inside this branch, so a deployment
|
|
480
|
+
// with no duplicate artifact does not pay for the manifest read at all.
|
|
481
|
+
const knownReviewers = effectiveReviewerNames();
|
|
482
|
+
const winner = existing.effect !== "enforce" && policy.effect === "enforce" ? policy : existing;
|
|
483
|
+
const { merged, overruled, refused } = withMergedAuthority(winner, [existing, policy], knownReviewers);
|
|
484
|
+
if (overruled) {
|
|
485
|
+
warnAuthority(
|
|
486
|
+
`cloud-managed policies ${existing.id} and ${policy.id} share one artifact and do not all declare it ` +
|
|
487
|
+
`reviewable, so it stays hard`,
|
|
488
|
+
);
|
|
453
489
|
}
|
|
490
|
+
// The merge hardened the record, so registration will not see the refused
|
|
491
|
+
// declaration and cannot report it. Said here instead, naming both
|
|
492
|
+
// assignments — which is more than registration could have said.
|
|
493
|
+
if (refused) {
|
|
494
|
+
warnAuthority(refusedAuthorityWarning(`cloud-managed policies ${existing.id} and ${policy.id}`, refused));
|
|
495
|
+
}
|
|
496
|
+
cloudManagedByPath.set(key, merged);
|
|
454
497
|
}
|
|
455
498
|
|
|
456
499
|
// Installed packs, keyed by artifact path — the same content-addressing, and
|
|
@@ -482,14 +525,46 @@ export async function loadAllCustomHooks(
|
|
|
482
525
|
*
|
|
483
526
|
* Winner precedence on a name both declare, because the merged record carries
|
|
484
527
|
* the winner's id and version and there is no merging two different defaults.
|
|
528
|
+
*
|
|
529
|
+
* Except AUTHORITY, which resolves toward hard like the effect resolves toward
|
|
530
|
+
* enforce (`withMergedAuthority`): a name is reviewable only if every pack
|
|
531
|
+
* behind the artifact declares it reviewable. A pack whose manifest does not
|
|
532
|
+
* list the name at all counts as hard — its copy of the artifact registers
|
|
533
|
+
* the policy all the same, undeclared, and an undeclared pack policy is hard.
|
|
534
|
+
* Winner precedence here would let one pack's manifest make another pack's
|
|
535
|
+
* policy reviewable, which is the one thing a manifest may never do.
|
|
536
|
+
*
|
|
537
|
+
* `knownReviewers` is what makes that hold for a pack that ships BOTH tiers.
|
|
538
|
+
* Judged against the compiled-in set, a `reviewedBy` naming one of the pack's
|
|
539
|
+
* own checks is a name nothing here has, so the reviewable entry and its hard
|
|
540
|
+
* peer resolved alike and the merge could not tell them apart — while
|
|
541
|
+
* registration, which reads the manifest's own checks, honoured it. The set
|
|
542
|
+
* passed here is that same one.
|
|
485
543
|
*/
|
|
486
544
|
const unionCatalog = (
|
|
487
545
|
winner: PolicyCatalogEntry[],
|
|
488
546
|
other: PolicyCatalogEntry[],
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
547
|
+
knownReviewers: ReadonlySet<string>,
|
|
548
|
+
): { policies: PolicyCatalogEntry[]; overruled: string[]; refused: string[] } => {
|
|
549
|
+
/** Names a pack asked to be reviewable and did not get, for the warning. */
|
|
550
|
+
const overruled: string[] = [];
|
|
551
|
+
/** The refusals themselves, which registration will no longer see to report. */
|
|
552
|
+
const refused: string[] = [];
|
|
553
|
+
const harden = (entry: PolicyCatalogEntry, peer: PolicyCatalogEntry | undefined) => {
|
|
554
|
+
const r = withMergedAuthority(entry, [entry, peer ?? {}], knownReviewers);
|
|
555
|
+
if (r.overruled) overruled.push(entry.name);
|
|
556
|
+
if (r.refused) refused.push(refusedAuthorityWarning(entry.name, r.refused));
|
|
557
|
+
return r.merged;
|
|
558
|
+
};
|
|
559
|
+
return {
|
|
560
|
+
policies: [
|
|
561
|
+
...winner.map((w) => harden(w, other.find((p) => p.name === w.name))),
|
|
562
|
+
...other.filter((p) => !winner.some((w) => w.name === p.name)).map((p) => harden(p, undefined)),
|
|
563
|
+
],
|
|
564
|
+
overruled,
|
|
565
|
+
refused,
|
|
566
|
+
};
|
|
567
|
+
};
|
|
493
568
|
|
|
494
569
|
const packByPath = new Map<string, ResolvedPack>();
|
|
495
570
|
/**
|
|
@@ -535,12 +610,24 @@ export async function loadAllCustomHooks(
|
|
|
535
610
|
// being intersected away.
|
|
536
611
|
const winner = existing.effect !== "enforce" && pack.effect === "enforce" ? pack : existing;
|
|
537
612
|
const other = winner === existing ? pack : existing;
|
|
613
|
+
// Read here rather than at the top of the load: only a duplicate artifact
|
|
614
|
+
// needs it, and this is the branch that has one.
|
|
615
|
+
const catalog = unionCatalog(winner.policies, other.policies, effectiveReviewerNames());
|
|
538
616
|
packByPath.set(key, {
|
|
539
617
|
...winner,
|
|
540
|
-
policies:
|
|
618
|
+
policies: catalog.policies,
|
|
541
619
|
enabled: unionSelection(winner.enabled, other.enabled),
|
|
542
620
|
clis: unionSelection(winner.clis, other.clis),
|
|
543
621
|
});
|
|
622
|
+
if (catalog.overruled.length > 0) {
|
|
623
|
+
warnAuthority(
|
|
624
|
+
`packs ${existing.id} and ${pack.id} share one artifact and do not all declare ` +
|
|
625
|
+
`${catalog.overruled.join(", ")} reviewable, so ${catalog.overruled.length === 1 ? "it stays" : "they stay"} hard`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
// A refused `reviewable` is now hardened here rather than passed on to
|
|
629
|
+
// registration, so this is the only place left that can name the reason.
|
|
630
|
+
for (const message of catalog.refused) warnAuthority(message);
|
|
544
631
|
}
|
|
545
632
|
|
|
546
633
|
// 1. Explicit custom policy paths. Accept a string for callers/configs using
|
|
@@ -566,6 +653,7 @@ export async function loadAllCustomHooks(
|
|
|
566
653
|
const failure = await loadSingleFile(absPath, {
|
|
567
654
|
verifyEntrySha:
|
|
568
655
|
cloudManaged?.sha256 ?? pack?.sha256,
|
|
656
|
+
outsidePack: !pack,
|
|
569
657
|
});
|
|
570
658
|
// Every id behind these bytes. One artifact, one import, one failure —
|
|
571
659
|
// but as many fail-closed guards as there are packs depending on it.
|
|
@@ -646,7 +734,7 @@ export async function loadAllCustomHooks(
|
|
|
646
734
|
for (const file of projectFiles) {
|
|
647
735
|
loadedPaths.add(file);
|
|
648
736
|
const hooksBefore = getCustomHooks().length;
|
|
649
|
-
await loadSingleFile(file, { conventionScope: projectScope });
|
|
737
|
+
await loadSingleFile(file, { conventionScope: projectScope, outsidePack: true });
|
|
650
738
|
const newHooks = getCustomHooks().slice(hooksBefore);
|
|
651
739
|
for (const hook of newHooks) {
|
|
652
740
|
(hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId(projectScope, basename(file), hook.name);
|
|
@@ -686,7 +774,7 @@ export async function loadAllCustomHooks(
|
|
|
686
774
|
for (const file of userFiles) {
|
|
687
775
|
loadedPaths.add(file);
|
|
688
776
|
const hooksBefore = getCustomHooks().length;
|
|
689
|
-
await loadSingleFile(file, { conventionScope: "user" });
|
|
777
|
+
await loadSingleFile(file, { conventionScope: "user", outsidePack: true });
|
|
690
778
|
const newHooks = getCustomHooks().slice(hooksBefore);
|
|
691
779
|
for (const hook of newHooks) {
|
|
692
780
|
(hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId("user", basename(file), hook.name);
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* from 'failproofai') and the hook handler (which reads from this file) share the
|
|
5
5
|
* same in-process state when running within the same Node.js process.
|
|
6
6
|
*/
|
|
7
|
-
import type { CustomHook } from "./policy-types";
|
|
7
|
+
import type { CustomHook, SemanticPolicyDeclaration } from "./policy-types";
|
|
8
8
|
|
|
9
9
|
const REGISTRY_KEY = "__failproofai_custom_hooks__";
|
|
10
|
+
/**
|
|
11
|
+
* Beside the hook registry, on the same key namespace and cleared by the same
|
|
12
|
+
* call — the two are one registration pass and a build step reads both.
|
|
13
|
+
*/
|
|
14
|
+
const SEMANTIC_KEY = "__failproofai_semantic_policies__";
|
|
10
15
|
|
|
11
16
|
function getRegistry(): CustomHook[] {
|
|
12
17
|
const g = globalThis as Record<string, unknown>;
|
|
@@ -14,17 +19,56 @@ function getRegistry(): CustomHook[] {
|
|
|
14
19
|
return g[REGISTRY_KEY] as CustomHook[];
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
function getSemanticRegistry(): SemanticPolicyDeclaration[] {
|
|
23
|
+
const g = globalThis as Record<string, unknown>;
|
|
24
|
+
if (!Array.isArray(g[SEMANTIC_KEY])) g[SEMANTIC_KEY] = [];
|
|
25
|
+
return g[SEMANTIC_KEY] as SemanticPolicyDeclaration[];
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export const customPolicies = {
|
|
18
29
|
add(hook: CustomHook): void {
|
|
19
30
|
getRegistry().push(hook);
|
|
20
31
|
},
|
|
21
32
|
};
|
|
22
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Register a semantic (Jev) policy — a question set for the classifier, read by
|
|
36
|
+
* `failproofai publish` and emitted as the pack manifest's `semantic` array.
|
|
37
|
+
*
|
|
38
|
+
* A SEPARATE namespace from `customPolicies`, deliberately, rather than a
|
|
39
|
+
* `customPolicies.addSemantic`. A semantic policy has no `fn` and no `match`,
|
|
40
|
+
* and nothing about it ever executes on a user's machine: what installs is the
|
|
41
|
+
* declaration in the manifest, and the artifact's call here matters only at
|
|
42
|
+
* build time. Putting it behind the object whose entries return
|
|
43
|
+
* `allow()`/`deny()` invites exactly the shape confusion the manifest parser
|
|
44
|
+
* would then have to catch — a function on a semantic entry, a probe list on a
|
|
45
|
+
* regex one — and both mistakes would be silent until somebody read a manifest.
|
|
46
|
+
*
|
|
47
|
+
* Nothing is validated here. `parsePackSemanticPolicy` in `pack-manifest.ts`
|
|
48
|
+
* owns every rule, and it is applied at BUILD time (where the author can still
|
|
49
|
+
* fix it) and again at load time (where the bytes may have come from anywhere).
|
|
50
|
+
* Validating a third time in the setter would be a third copy of the rules.
|
|
51
|
+
*/
|
|
52
|
+
export const semanticPolicies = {
|
|
53
|
+
add(policy: SemanticPolicyDeclaration): void {
|
|
54
|
+
getSemanticRegistry().push(policy);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
23
58
|
export function getCustomHooks(): CustomHook[] {
|
|
24
59
|
return getRegistry();
|
|
25
60
|
}
|
|
26
61
|
|
|
62
|
+
export function getSemanticRegistrations(): SemanticPolicyDeclaration[] {
|
|
63
|
+
return getSemanticRegistry();
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
export function clearCustomHooks(): void {
|
|
28
67
|
const g = globalThis as Record<string, unknown>;
|
|
29
68
|
g[REGISTRY_KEY] = [];
|
|
69
|
+
// Both maps, on one call. A loader that cleared only the hooks would carry
|
|
70
|
+
// one entry file's semantic policies into the next file's build — and since
|
|
71
|
+
// the semantic half is read after the load rather than during it, the extras
|
|
72
|
+
// would be published under the wrong pack with nothing to notice.
|
|
73
|
+
g[SEMANTIC_KEY] = [];
|
|
30
74
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic checks a `reviewedBy` may name ON THIS MACHINE.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is not simply `SEMANTIC_REVIEWER_NAMES`
|
|
5
|
+
*
|
|
6
|
+
* A pack that declares its own `semantic` entries replaces the compiled-in
|
|
7
|
+
* `SEMANTIC_POLICIES` wholesale (`semantic/pack-policies.ts`). So on that
|
|
8
|
+
* machine the sixteen builtin names are checks that will never be asked, and the
|
|
9
|
+
* pack's own names are the only ones that will — while
|
|
10
|
+
* `resolvePolicyAuthority`, judging against the builtin set, would read the
|
|
11
|
+
* pack's `reviewedBy: ["its-own-check"]` as naming a check "this build does not
|
|
12
|
+
* have" and downgrade the policy to `hard` with nothing but a warning in the
|
|
13
|
+
* hook log. That is a rewritten policy set arriving with its reviewability
|
|
14
|
+
* quietly removed: the deny half of the two-tier evaluator working exactly as
|
|
15
|
+
* documented while the clearing half cannot fire once — the failure
|
|
16
|
+
* `policy-reviewability.ts` was written to make audible, reintroduced one layer
|
|
17
|
+
* down.
|
|
18
|
+
*
|
|
19
|
+
* The names come from the MANIFEST, which is already where `authority` and
|
|
20
|
+
* `reviewedBy` themselves are read from, so this adds no new trust and imports
|
|
21
|
+
* no semantic module.
|
|
22
|
+
*
|
|
23
|
+
* ## Why it is a module of its own, and cached the way it is
|
|
24
|
+
*
|
|
25
|
+
* `registerPolicy` asks this for every policy that declares an authority — forty
|
|
26
|
+
* times per hook event — and the honest answer costs a digest verification per
|
|
27
|
+
* installed pack. So it is cached, and the cache is dropped by `clearPolicies()`:
|
|
28
|
+
* one read per registration pass, and the answer's lifetime is exactly the
|
|
29
|
+
* registry's. That is the right coupling rather than a convenient one — this set
|
|
30
|
+
* describes the policies sitting in the registry, and the handler rebuilds both
|
|
31
|
+
* together, so they cannot come to describe different machines.
|
|
32
|
+
*
|
|
33
|
+
* It lives beside `policy-registry.ts` rather than inside `pack-manifest.ts`
|
|
34
|
+
* because the registry may not depend on the whole manifest reader: several
|
|
35
|
+
* tests isolate themselves from the running machine's real packs by mocking
|
|
36
|
+
* `pack-manifest` down to `readInstalledPacks` and `hasInstalledPacks`, and a
|
|
37
|
+
* registration path reaching for a third export of that module breaks them all
|
|
38
|
+
* — loudly here, but it is the shape of a change that would later break them
|
|
39
|
+
* silently.
|
|
40
|
+
*/
|
|
41
|
+
import { readInstalledPacks, type ResolvedPack } from "./pack-manifest";
|
|
42
|
+
import { SEMANTIC_REVIEWER_NAMES } from "./policy-authority";
|
|
43
|
+
|
|
44
|
+
let cached: ReadonlySet<string> | null = null;
|
|
45
|
+
let cachedContested: ReadonlyMap<string, string[]> = new Map();
|
|
46
|
+
/** The agent the current registration pass is for; see {@link forgetEffectiveReviewerNames}. */
|
|
47
|
+
let reviewerCli: string | undefined;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The packs whose Jev checks take part for `cli`, filtered the way the regex
|
|
51
|
+
* half already is: an `observe` pack blocks nothing, and a pack scoped to other
|
|
52
|
+
* agents guards none here. Its checks would otherwise replace the questions and
|
|
53
|
+
* supply reviewers that clear enforce packs' denies on every agent. Both the
|
|
54
|
+
* reviewer set and the question set start from this list, so they agree.
|
|
55
|
+
*/
|
|
56
|
+
export function jevPacks<T extends Pick<ResolvedPack, "effect" | "clis">>(packs: ReadonlyArray<T>, cli?: string): T[] {
|
|
57
|
+
return packs.filter(
|
|
58
|
+
(p) => p.effect !== "observe" && !(cli && Array.isArray(p.clis) && p.clis.length > 0 && !p.clis.includes(cli)),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A stable serialization, for comparing two declarations of the same name.
|
|
64
|
+
* Key order is a manifest's business, not a difference in what is asked.
|
|
65
|
+
*/
|
|
66
|
+
function canonical(value: unknown): string {
|
|
67
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
68
|
+
if (value !== null && typeof value === "object") {
|
|
69
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
70
|
+
.filter(([, v]) => v !== undefined)
|
|
71
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
72
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
73
|
+
}
|
|
74
|
+
return JSON.stringify(value) ?? "null";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The semantic check names that more than one installed pack claims, with
|
|
79
|
+
* declarations that are not the same check — and which this machine must
|
|
80
|
+
* therefore honour for nobody. Keyed by name, valued with the packs that
|
|
81
|
+
* claimed it, for the warning.
|
|
82
|
+
*
|
|
83
|
+
* ## Why a contested name has to be refused rather than resolved
|
|
84
|
+
*
|
|
85
|
+
* The reviewer set is the whole protection: a policy is `reviewable` only when
|
|
86
|
+
* every name its `reviewedBy` lists is a check this machine can actually ask.
|
|
87
|
+
* The union across packs is deliberate and must keep working — the shipped
|
|
88
|
+
* configuration is exactly that, `FailproofAI/policies` naming checks that live
|
|
89
|
+
* in `FailproofAI/jev-policies` — but the union was AMBIGUOUS. Two packs may be
|
|
90
|
+
* installed at once, `semanticPoliciesFromPacks` keeps the first declaration of
|
|
91
|
+
* a duplicated name and drops the later one, and nothing tied a name to the pack
|
|
92
|
+
* that meant it. So a second pack declaring `production-infra-change` with a
|
|
93
|
+
* question that answers "no concern" to everything supplies the reviewer for the
|
|
94
|
+
* FIRST pack's policies: install it, and every policy reviewable by that name is
|
|
95
|
+
* cleared on every call. The pack doing it never declares a regex policy, and
|
|
96
|
+
* scoping reviewers per pack is not the fix — that would break the pairing the
|
|
97
|
+
* product ships.
|
|
98
|
+
*
|
|
99
|
+
* So the name leaves the set. The policies naming it resolve to `hard`, the
|
|
100
|
+
* question is not asked, and the pack that tried it gains nothing: stricter than
|
|
101
|
+
* either author asked for, with the regex deny simply standing. Nothing is
|
|
102
|
+
* failed closed over it — one policy being hard is a cost; a pack that denies
|
|
103
|
+
* every tool call is a different order of failure (`pack-failclosed.ts`).
|
|
104
|
+
*
|
|
105
|
+
* ## Why identical declarations are not a contest
|
|
106
|
+
*
|
|
107
|
+
* Content-addressed artifacts mean a fork or a re-publish arrives as two packs
|
|
108
|
+
* carrying the same semantic entries, and `custom-hooks-loader.ts` treats that
|
|
109
|
+
* shape as expected rather than as a corner. Both declarations are then the same
|
|
110
|
+
* question, so whichever the resolver keeps asks exactly what the other would
|
|
111
|
+
* have: nothing is ambiguous, and refusing the name would switch off the
|
|
112
|
+
* clearing half on a machine whose two packs agree to the byte.
|
|
113
|
+
*
|
|
114
|
+
* Pure, and takes the packs rather than reading them, so the two callers that
|
|
115
|
+
* already have them do not read `installed.json` again — and so both answer from
|
|
116
|
+
* the same list. They MUST agree: a name in the reviewer set whose question is
|
|
117
|
+
* another pack's is the bug this function exists to remove.
|
|
118
|
+
*/
|
|
119
|
+
export function contestedSemanticNames(
|
|
120
|
+
packs: ReadonlyArray<Pick<ResolvedPack, "id" | "semantic"> & { source?: string }>,
|
|
121
|
+
): ReadonlyMap<string, string[]> {
|
|
122
|
+
const claims = new Map<string, { form: string; ids: string[] }>();
|
|
123
|
+
const contested = new Map<string, string[]>();
|
|
124
|
+
for (const pack of packs) {
|
|
125
|
+
for (const entry of pack.semantic ?? []) {
|
|
126
|
+
// Void, so not a claim either: see `isReservedClaim`.
|
|
127
|
+
if (isReservedClaim(pack, entry.name)) continue;
|
|
128
|
+
const form = canonical(entry);
|
|
129
|
+
const claim = claims.get(entry.name);
|
|
130
|
+
if (claim === undefined) {
|
|
131
|
+
claims.set(entry.name, { form, ids: [pack.id] });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
claim.ids.push(pack.id);
|
|
135
|
+
// The array is the live one, so a third claimant is named too.
|
|
136
|
+
if (claim.form !== form) contested.set(entry.name, claim.ids);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return contested;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Installed from a FailproofAI repository. Read from `source`, which this CLI
|
|
144
|
+
* wrote from the repository it fetched, never from the pack's self-declared `id`.
|
|
145
|
+
*/
|
|
146
|
+
export function isFirstPartyPack(pack: { source?: string }): boolean {
|
|
147
|
+
return /^github:FailproofAI\//i.test(pack.source ?? "");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A builtin check name declared by a pack that is not FailproofAI's. Core and
|
|
152
|
+
* user policies name those checks in `reviewedBy`, so the third party's
|
|
153
|
+
* question would become the reviewer that clears them — an `instruct`-mode
|
|
154
|
+
* `destructive-deletion` beside the core pack turned `block-rm-rf` into a
|
|
155
|
+
* warning. The claim is void: never asked, never a reviewer, and never a
|
|
156
|
+
* second declaration that contests FailproofAI's own, which would switch off a
|
|
157
|
+
* deny the regex tier does not have. The reviewer set, the question set and
|
|
158
|
+
* the contest all apply it.
|
|
159
|
+
*/
|
|
160
|
+
export function isReservedClaim(pack: { source?: string }, name: string): boolean {
|
|
161
|
+
return SEMANTIC_REVIEWER_NAMES.has(name) && !isFirstPartyPack(pack);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The installed packs' semantic policy names when any pack declares some, and
|
|
166
|
+
* this build's compiled-in set otherwise.
|
|
167
|
+
*
|
|
168
|
+
* A name two packs claim differently is left out — see
|
|
169
|
+
* {@link contestedSemanticNames}. When that leaves nothing, the compiled-in set
|
|
170
|
+
* stands, which is not a softening but the same rule
|
|
171
|
+
* `semanticPoliciesFromPacks` applies to the QUESTIONS: a declaring pack whose
|
|
172
|
+
* every entry was unusable leaves the compiled-in semantic set live, so the
|
|
173
|
+
* names honoured here are the names of the questions that will actually be
|
|
174
|
+
* asked, and each of those is one of ours.
|
|
175
|
+
*
|
|
176
|
+
* Never throws: an unreadable manifest declares nothing, and the builtin set
|
|
177
|
+
* stands. That is the same fail-open posture every other reader of the pack
|
|
178
|
+
* manifest takes, and here it also fails in the safe direction — a pack's
|
|
179
|
+
* `reviewedBy` will not match a builtin name, so its policies stay `hard`
|
|
180
|
+
* rather than being cleared by a question nobody could read.
|
|
181
|
+
*/
|
|
182
|
+
export function effectiveReviewerNames(): ReadonlySet<string> {
|
|
183
|
+
if (cached) return cached;
|
|
184
|
+
let names: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES;
|
|
185
|
+
cachedContested = new Map();
|
|
186
|
+
try {
|
|
187
|
+
const packs = jevPacks(readInstalledPacks().packs, reviewerCli);
|
|
188
|
+
names = reviewerNamesFor(packs);
|
|
189
|
+
cachedContested = contestedSemanticNames(packs);
|
|
190
|
+
} catch {
|
|
191
|
+
// See above: silence here is the builtin set, not an empty one.
|
|
192
|
+
}
|
|
193
|
+
cached = names;
|
|
194
|
+
return names;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The contest behind {@link effectiveReviewerNames}' answer, so a refusal can name it. */
|
|
198
|
+
export function contestedReviewerNames(): ReadonlyMap<string, string[]> {
|
|
199
|
+
effectiveReviewerNames();
|
|
200
|
+
return cachedContested;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A first-party pack declares Jev checks, so they REPLACE the compiled-in set.
|
|
205
|
+
* Anyone else's checks are ADDED to it: a stranger's one check must not switch
|
|
206
|
+
* off `credential-exfiltration` and turn every core reviewable policy hard.
|
|
207
|
+
* `semanticPoliciesFromPacks` applies the same rule to the questions.
|
|
208
|
+
*/
|
|
209
|
+
export function replacesBuiltinChecks(packs: ReadonlyArray<Pick<ResolvedPack, "semantic"> & { source?: string }>): boolean {
|
|
210
|
+
return packs.some((p) => isFirstPartyPack(p) && (p.semantic ?? []).length > 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The reviewer set for the packs taking part: see {@link replacesBuiltinChecks}. Pure.
|
|
215
|
+
*
|
|
216
|
+
* Manifest-only: it cannot see a check `semanticPoliciesFromPacks` drops for the
|
|
217
|
+
* question budget, because measuring questions means loading the semantic
|
|
218
|
+
* modules this hook-path file must not reach. That overcount is fail-safe — a
|
|
219
|
+
* dropped check is never answered, so it never clears — and the diagnostic
|
|
220
|
+
* (`surveyReviewableCoverage`) asks the resolver instead.
|
|
221
|
+
*/
|
|
222
|
+
export function reviewerNamesFor(
|
|
223
|
+
packs: ReadonlyArray<Pick<ResolvedPack, "id" | "semantic"> & { source?: string }>,
|
|
224
|
+
): ReadonlySet<string> {
|
|
225
|
+
const contested = contestedSemanticNames(packs);
|
|
226
|
+
const declared = packs
|
|
227
|
+
.flatMap((p) => (p.semantic ?? []).filter((s) => !isReservedClaim(p, s.name)).map((s) => s.name))
|
|
228
|
+
.filter((name) => !contested.has(name));
|
|
229
|
+
// Nothing usable declared: the compiled-in set is the one being asked.
|
|
230
|
+
if (declared.length === 0) return SEMANTIC_REVIEWER_NAMES;
|
|
231
|
+
return new Set(replacesBuiltinChecks(packs) ? declared : [...SEMANTIC_REVIEWER_NAMES, ...declared]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Drop the cached answer, and set the agent the next one is for. Called by
|
|
236
|
+
* `clearPolicies(cli)`, which every evaluation
|
|
237
|
+
* runs before it registers anything — so a pack installed under a long-lived
|
|
238
|
+
* warm worker is picked up on the next event rather than at the next restart.
|
|
239
|
+
*/
|
|
240
|
+
export function forgetEffectiveReviewerNames(cli?: string): void {
|
|
241
|
+
cached = null;
|
|
242
|
+
reviewerCli = cli;
|
|
243
|
+
}
|
|
@@ -35,6 +35,11 @@ export const FIRST_RUN_EXEMPT_SUBCOMMANDS: readonly string[] = [
|
|
|
35
35
|
// Same reason: a backfill is an explicit instruction about an already-set-up
|
|
36
36
|
// machine, and interrupting it to offer setup answers a question nobody asked.
|
|
37
37
|
"backfill",
|
|
38
|
+
// `jev setup` is configuration in its own right, and `jev status` / `test` /
|
|
39
|
+
// `remove` inspect or undo it. It is also the command most likely to be
|
|
40
|
+
// scripted with a key on stdin — a wizard in front of it would read that key
|
|
41
|
+
// as the answer to its first question.
|
|
42
|
+
"jev",
|
|
38
43
|
];
|
|
39
44
|
|
|
40
45
|
export function shouldOfferFirstRun(args: readonly string[]): boolean {
|
package/src/hooks/flush-cli.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
23
23
|
import { dirname, join } from "node:path";
|
|
24
|
-
import { failproofaiHome, spoolDir } from "./fp-home";
|
|
24
|
+
import { customAgentsEventsDir, failedDir, failproofaiHome, spoolDir } from "./fp-home";
|
|
25
25
|
import { readConfig } from "./fp-config";
|
|
26
26
|
import { readIngestCredential } from "./collector-config";
|
|
27
27
|
import { daemonServiceStatus, isDaemonSupportedPlatform } from "./daemon-service";
|
|
@@ -49,19 +49,41 @@ export function flushRequestPath(home?: string): string {
|
|
|
49
49
|
return join(failproofaiHome(home), "state", "flush-request.json");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Every directory the collector spools batches into.
|
|
52
|
+
/** Every directory the collector spools batches into. The daemon writes its
|
|
53
|
+
* own batches FLAT into state/spool and the SDKs into custom-agents/events
|
|
54
|
+
* (`spool_dirs` in crates/fpai-collect/src/config.rs). Counting only
|
|
55
|
+
* subdirectories of state/spool, as this once did, found nothing on every
|
|
56
|
+
* machine, so flush always answered "everything already delivered". */
|
|
53
57
|
function spoolDirs(home?: string): string[] {
|
|
54
58
|
const root = spoolDir(home);
|
|
55
|
-
|
|
59
|
+
const dirs = [root, customAgentsEventsDir(home)];
|
|
60
|
+
if (!existsSync(root)) return dirs;
|
|
56
61
|
try {
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
return dirs.concat(
|
|
63
|
+
readdirSync(root, { withFileTypes: true })
|
|
64
|
+
.filter((e) => e.isDirectory() && e.name !== "failed")
|
|
65
|
+
.map((e) => join(root, e.name)),
|
|
66
|
+
);
|
|
60
67
|
} catch {
|
|
61
|
-
return
|
|
68
|
+
return dirs;
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
/** Batches parked in state/failed after failed uploads, `.poison` excluded.
|
|
73
|
+
* Flush does not resend these: the daemon retries them about hourly and
|
|
74
|
+
* when it starts, so all this can do is not call them delivered. */
|
|
75
|
+
function parkedBatches(home?: string): number {
|
|
76
|
+
try {
|
|
77
|
+
return readdirSync(failedDir(home)).filter((f) => f.endsWith(".jsonl")).length;
|
|
78
|
+
} catch {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parkedLine(n: number, home?: string): string {
|
|
84
|
+
return `${n} batch${n === 1 ? "" : "es"} parked in ${failedDir(home)} after failed uploads; the daemon retries them about hourly and when it restarts.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/** Batches awaiting delivery. `.tmp` files are half-written and not counted. */
|
|
66
88
|
export function pendingBatches(home?: string): number {
|
|
67
89
|
let n = 0;
|
|
@@ -138,8 +160,12 @@ export async function runFlushCommand(opts: FlushOptions = {}): Promise<FlushRes
|
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
const before = pendingBatches(home);
|
|
163
|
+
const parked = parkedBatches(home);
|
|
141
164
|
if (before === 0) {
|
|
142
|
-
|
|
165
|
+
const nothing = parked
|
|
166
|
+
? ["Nothing spooled.", parkedLine(parked, home)]
|
|
167
|
+
: ["Nothing spooled — everything already delivered."];
|
|
168
|
+
return { exitCode: 0, pending: 0, lines: nothing };
|
|
143
169
|
}
|
|
144
170
|
|
|
145
171
|
const path = flushRequestPath(home);
|
|
@@ -149,6 +175,7 @@ export async function runFlushCommand(opts: FlushOptions = {}): Promise<FlushRes
|
|
|
149
175
|
});
|
|
150
176
|
|
|
151
177
|
lines.push(`${before} batch${before === 1 ? "" : "es"} spooled. Requested delivery.`);
|
|
178
|
+
if (parked) lines.push(parkedLine(parked, home));
|
|
152
179
|
|
|
153
180
|
if (!wait) {
|
|
154
181
|
lines.push("The daemon picks this up within a few seconds.");
|