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
|
@@ -151,6 +151,9 @@ export function missingGuards(input: {
|
|
|
151
151
|
// loaded, would have allowed.
|
|
152
152
|
if (err.effect === "observe") continue;
|
|
153
153
|
if (outOfScope(err.clis, input.cli)) continue;
|
|
154
|
+
// Jev checks only: nothing in the regex tier is missing. The refusal is
|
|
155
|
+
// still reported (hook log, `failproofai policies`); it just denies nothing.
|
|
156
|
+
if (err.semanticOnly) continue;
|
|
154
157
|
const declared = err.declared ?? [];
|
|
155
158
|
out.push({
|
|
156
159
|
packId: err.id ?? "(unnamed pack)",
|
|
@@ -41,9 +41,16 @@
|
|
|
41
41
|
import { createHash } from "node:crypto";
|
|
42
42
|
import { existsSync, readFileSync } from "node:fs";
|
|
43
43
|
import { resolve } from "node:path";
|
|
44
|
+
import { version as cliVersion } from "../../package.json";
|
|
44
45
|
import { packsDir, packsInstalledFile } from "./fp-home";
|
|
45
46
|
import { resolveManagedPath } from "./cloud-managed-policies";
|
|
46
|
-
import
|
|
47
|
+
import { authorityFieldsOf } from "./policy-authority";
|
|
48
|
+
import { compareVersions } from "./semver-precedence";
|
|
49
|
+
// Strings only, and the one semantic module this file may touch: it runs on
|
|
50
|
+
// every hook event, and the predicate bodies pull in all sixteen of Jev's
|
|
51
|
+
// prompts. See the header of `semantic/precondition-names.ts`.
|
|
52
|
+
import { isPackPreconditionName, PACK_PRECONDITION_NAMES } from "./semantic/precondition-names";
|
|
53
|
+
import type { SemanticPolicyDeclaration, SemanticProbeDeclaration, PolicyCatalogEntry } from "./policy-types";
|
|
47
54
|
import type { PolicyEffect } from "./cloud-managed-policies";
|
|
48
55
|
|
|
49
56
|
/** Manifest schemas this reader accepts. */
|
|
@@ -82,6 +89,52 @@ export const PACK_COMMIT_RE = /^[0-9a-f]{7,40}$/;
|
|
|
82
89
|
*/
|
|
83
90
|
const PACK_POLICY_NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
84
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A semantic probe's id. Lowercase and underscore only, because it is half of a
|
|
94
|
+
* Jev question id (`<policy>.<probe>`) that `decide.ts` reads answers back by.
|
|
95
|
+
*/
|
|
96
|
+
const PROBE_ID_RE = /^[a-z][a-z0-9_]{0,31}$/;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Probe ids `decide.ts` has already taken in the answer map: `<policy>.exempt`
|
|
100
|
+
* is the documented-exception question and `<policy>.user_asked` is the "did the
|
|
101
|
+
* human ask for this" one. A probe claiming either name would not collide
|
|
102
|
+
* loudly — it would OVERWRITE that question in the compiled request, so the
|
|
103
|
+
* exemption or the override silently becomes whatever the pack wrote.
|
|
104
|
+
*/
|
|
105
|
+
const RESERVED_PROBE_IDS: ReadonlySet<string> = new Set(["exempt", "user_asked"]);
|
|
106
|
+
|
|
107
|
+
const SEMANTIC_TOOL_CLASSES: ReadonlySet<string> = new Set(["shell", "write", "read", "network", "other"]);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Cheap count guards on the question set, applied where the manifest is read.
|
|
111
|
+
*
|
|
112
|
+
* They are NOT the real budget. Every probe is a question in one Jev request,
|
|
113
|
+
* and what that request has to fit is a character budget
|
|
114
|
+
* (`MAX_REQUEST_CHARS - MAX_STATE_CHARS`) — which these counts cannot express,
|
|
115
|
+
* because a probe is between a sentence and a paragraph long. Counting to 24
|
|
116
|
+
* and 6 here rejects the obviously abusive shapes for free, on the hook path,
|
|
117
|
+
* without reaching a semantic module; `semantic/pack-policies.ts` owns the
|
|
118
|
+
* budget that actually decides, measured the way `compile.ts` measures it.
|
|
119
|
+
*
|
|
120
|
+
* There is deliberately no total probe count across packs. An earlier draft had
|
|
121
|
+
* one (80), and at the real average probe length that ceiling permits a question
|
|
122
|
+
* set 23,032 characters over what one request can carry — a cap that reads as a
|
|
123
|
+
* guarantee and is not one.
|
|
124
|
+
*/
|
|
125
|
+
export const MAX_SEMANTIC_POLICIES_PER_PACK = 24;
|
|
126
|
+
/** Per-policy conjunction width. The builtin set's widest policy uses three probes. */
|
|
127
|
+
export const MAX_PROBES_PER_POLICY = 6;
|
|
128
|
+
const MAX_TITLE_CHARS = 120;
|
|
129
|
+
const MAX_INSTRUCTIONS_CHARS = 600;
|
|
130
|
+
const MAX_CRITERION_CHARS = 300;
|
|
131
|
+
const MAX_GUIDANCE_CHARS = 600;
|
|
132
|
+
|
|
133
|
+
/** A semantic policy as a manifest carries it: validated, normalized, code-free. */
|
|
134
|
+
export type SemanticManifestEntry = SemanticPolicyDeclaration & {
|
|
135
|
+
probes: SemanticProbeDeclaration[];
|
|
136
|
+
};
|
|
137
|
+
|
|
85
138
|
export interface InstalledPackRecord {
|
|
86
139
|
id: string;
|
|
87
140
|
version: string;
|
|
@@ -94,6 +147,25 @@ export interface InstalledPackRecord {
|
|
|
94
147
|
commit?: string;
|
|
95
148
|
effect?: string;
|
|
96
149
|
policies?: unknown;
|
|
150
|
+
/**
|
|
151
|
+
* The pack's semantic (Jev) policies, as the manifest declared them.
|
|
152
|
+
*
|
|
153
|
+
* `unknown` like `policies`, and optional like it is not: a pack published
|
|
154
|
+
* before this field existed carries none, and every one of them must keep
|
|
155
|
+
* parsing. A pack that declares at least one REPLACES the compiled-in
|
|
156
|
+
* semantic set wholesale — see `semantic/pack-policies.ts`.
|
|
157
|
+
*/
|
|
158
|
+
semantic?: unknown;
|
|
159
|
+
/**
|
|
160
|
+
* The oldest CLI that may run this pack, as a semver string.
|
|
161
|
+
*
|
|
162
|
+
* Optional, and the ordering of its release is the reason it has to exist at
|
|
163
|
+
* all: a CLI with no support for this field ignores it AND ignores `semantic`,
|
|
164
|
+
* so it installs a pack whose Jev half does nothing and says so nowhere. A
|
|
165
|
+
* publisher can only protect against that by naming a minimum that the old
|
|
166
|
+
* CLI cannot read — which is why this ships before any pack declares one.
|
|
167
|
+
*/
|
|
168
|
+
minCliVersion?: string;
|
|
97
169
|
/**
|
|
98
170
|
* Which of the pack's policies to register. Absent means ALL of them.
|
|
99
171
|
*
|
|
@@ -131,6 +203,22 @@ export interface ResolvedPack {
|
|
|
131
203
|
effect: PolicyEffect;
|
|
132
204
|
/** The pack's own catalog, in declared order. */
|
|
133
205
|
policies: PolicyCatalogEntry[];
|
|
206
|
+
/**
|
|
207
|
+
* The pack's semantic policies, in declared order — EMPTY when it declared
|
|
208
|
+
* none, which is what every pack published before this release says. Entries
|
|
209
|
+
* that failed validation are not here; they were dropped, and the reason is in
|
|
210
|
+
* `PackReadResult.warnings`.
|
|
211
|
+
*
|
|
212
|
+
* `readInstalledPacks` always sets it, so on a record that came from a
|
|
213
|
+
* manifest this is an array. It is optional only so that a hand-built record —
|
|
214
|
+
* the loader fixtures, `pack-failclosed`'s guards — does not have to name a
|
|
215
|
+
* field it has no opinion about. Read it through {@link packSemantic} rather
|
|
216
|
+
* than `?? []` at each site, so "absent" and "empty" cannot come to mean
|
|
217
|
+
* different things.
|
|
218
|
+
*/
|
|
219
|
+
semantic?: SemanticManifestEntry[];
|
|
220
|
+
/** The minimum CLI this pack declared, once it has been checked against ours. */
|
|
221
|
+
minCliVersion?: string;
|
|
134
222
|
/** Selected policy names, or null when the user took the whole pack. */
|
|
135
223
|
enabled: string[] | null;
|
|
136
224
|
/** Agent CLIs this pack guards, or null for all of them. */
|
|
@@ -165,12 +253,37 @@ export interface PackError {
|
|
|
165
253
|
* deny on their behalf locks an agent out over enforcement it never had.
|
|
166
254
|
*/
|
|
167
255
|
clis?: string[] | null;
|
|
256
|
+
/**
|
|
257
|
+
* The record's regex `policies` list was readable and empty: a Jev-checks-only
|
|
258
|
+
* pack. It guards nothing in the regex tier, so its refusal has nothing to
|
|
259
|
+
* fail closed on. Read from the RAW list, never from `declared`: a list whose
|
|
260
|
+
* entries are all malformed also filters to nothing, and that must widen.
|
|
261
|
+
*/
|
|
262
|
+
semanticOnly?: true;
|
|
168
263
|
}
|
|
169
264
|
|
|
170
265
|
export interface PackReadResult {
|
|
171
266
|
packs: ResolvedPack[];
|
|
172
267
|
/** Every pack that was declared and refused, and why. Never silently dropped. */
|
|
173
268
|
errors: PackError[];
|
|
269
|
+
/**
|
|
270
|
+
* What was dropped from a pack that LOADED anyway, and why: a malformed
|
|
271
|
+
* semantic policy, an unknown precondition name, one entry past the cap, a
|
|
272
|
+
* `minCliVersion` nobody can compare.
|
|
273
|
+
*
|
|
274
|
+
* A SEPARATE channel from `errors`, and it has to be. An entry in `errors`
|
|
275
|
+
* means a pack was refused, and `pack-failclosed.ts` turns that into a deny
|
|
276
|
+
* for every event the missing policies claimed. Everything here is the
|
|
277
|
+
* opposite situation: the pack's regex policies are registered and enforcing,
|
|
278
|
+
* and what was lost is a reviewer (so a deny can no longer be CLEARED) or an
|
|
279
|
+
* unreadable version claim. Filing either as a pack failure would deny a
|
|
280
|
+
* machine over a typo in the half of the system whose job is to let more real
|
|
281
|
+
* work through.
|
|
282
|
+
*
|
|
283
|
+
* Absent rather than an empty array when there is nothing to say, so the
|
|
284
|
+
* common result stays the exact shape it has always been.
|
|
285
|
+
*/
|
|
286
|
+
warnings?: string[];
|
|
174
287
|
}
|
|
175
288
|
|
|
176
289
|
export function packsRoot(): string {
|
|
@@ -226,14 +339,56 @@ export function hasInstalledPacks(): boolean {
|
|
|
226
339
|
}
|
|
227
340
|
}
|
|
228
341
|
|
|
229
|
-
function installedFilePath(): string {
|
|
342
|
+
export function installedFilePath(): string {
|
|
230
343
|
return process.env.FAILPROOFAI_PACK_DIR
|
|
231
344
|
? resolve(process.env.FAILPROOFAI_PACK_DIR, "installed.json")
|
|
232
345
|
: packsInstalledFile();
|
|
233
346
|
}
|
|
234
347
|
|
|
348
|
+
|
|
349
|
+
/** The param types a schema may declare. Mirrors {@link PolicyParamsSchema}. */
|
|
350
|
+
const PARAM_TYPES: ReadonlySet<string> = new Set(["string", "number", "boolean", "string[]", "pattern[]"]);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Why a `params` schema cannot be used, or `undefined` when it can.
|
|
354
|
+
*
|
|
355
|
+
* A pack's schema is what `registerPolicy` stores and `evaluatePolicies` merges
|
|
356
|
+
* the user's configured values ON TOP OF, so a malformed one is not cosmetic: it
|
|
357
|
+
* decides what `ctx.params` contains, and a policy reading a param that is not
|
|
358
|
+
* there falls back to whatever its own code says — usually the stricter thing.
|
|
359
|
+
*
|
|
360
|
+
* Validated as a whole rather than per param. A partial schema would leave one
|
|
361
|
+
* param live and another silently absent inside the same policy, which is the
|
|
362
|
+
* hardest version of this to diagnose from the outside.
|
|
363
|
+
*/
|
|
364
|
+
function paramsSchemaProblem(value: unknown): string | undefined {
|
|
365
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "params is not an object";
|
|
366
|
+
for (const [name, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
367
|
+
if (name.length === 0) return "params has an empty parameter name";
|
|
368
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return `params.${name} is not an object`;
|
|
369
|
+
const spec = raw as Record<string, unknown>;
|
|
370
|
+
if (typeof spec.type !== "string" || !PARAM_TYPES.has(spec.type)) {
|
|
371
|
+
return `params.${name} has type ${JSON.stringify(spec.type)}, which is not one of ${[...PARAM_TYPES].join(", ")}`;
|
|
372
|
+
}
|
|
373
|
+
if (typeof spec.description !== "string" || spec.description.length === 0) {
|
|
374
|
+
return `params.${name} is missing a description`;
|
|
375
|
+
}
|
|
376
|
+
// The KEY has to be there, whatever it holds: an absent default and a
|
|
377
|
+
// `default: undefined` merge the same way, but only one of them says the
|
|
378
|
+
// author thought about it, and the merge is what the user's own value lands on.
|
|
379
|
+
if (!("default" in spec)) return `params.${name} is missing a default`;
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
235
384
|
/** Validate one serialized catalog entry carried by a pack. */
|
|
236
|
-
export function parsePackPolicy(
|
|
385
|
+
export function parsePackPolicy(
|
|
386
|
+
packId: string,
|
|
387
|
+
value: unknown,
|
|
388
|
+
index: number,
|
|
389
|
+
/** Where a DROPPED field is reported. Optional: most callers only care that the entry parsed. */
|
|
390
|
+
warnings?: string[],
|
|
391
|
+
): PolicyCatalogEntry {
|
|
237
392
|
const where = `${packId} policy #${index}`;
|
|
238
393
|
if (!value || typeof value !== "object") throw new Error(`${where} is not an object`);
|
|
239
394
|
const raw = value as Record<string, unknown>;
|
|
@@ -274,10 +429,322 @@ export function parsePackPolicy(packId: string, value: unknown, index: number):
|
|
|
274
429
|
throw new Error(`${where} has a match.${key} that is not a list of names`);
|
|
275
430
|
}
|
|
276
431
|
}
|
|
432
|
+
// Authority: whether Jev may clear this policy's verdict. Unlike `match`, a
|
|
433
|
+
// malformed value is DROPPED rather than refused — refusing the pack fails it
|
|
434
|
+
// closed and denies every tool call it covers, while dropping the field makes
|
|
435
|
+
// this one policy `hard`, which is what an unreadable declaration means
|
|
436
|
+
// anyway. A pack can only describe its OWN policies this way: the name above
|
|
437
|
+
// cannot contain `/`, so no entry can reach a builtin or another pack's
|
|
438
|
+
// policy. Absent fields stay absent, valid ones stay where they were, and an
|
|
439
|
+
// entry with nothing to drop is returned as is.
|
|
440
|
+
const valid = authorityFieldsOf(raw);
|
|
441
|
+
const invalid = (["authority", "reviewedBy"] as const).filter((k) => k in raw && !(k in valid));
|
|
442
|
+
// `params` follows the same rule, and is dropped for the same reason: refusing
|
|
443
|
+
// the pack fails it closed and denies every tool call it covers, while dropping
|
|
444
|
+
// the schema leaves the policy running on its own internal fallbacks. Recorded
|
|
445
|
+
// rather than silent, because the drop is a change to what the policy DOES —
|
|
446
|
+
// `block-sudo` without its `allowPatterns` is stricter than the user asked for,
|
|
447
|
+
// and stricter is the direction nobody reports as a bug.
|
|
448
|
+
if ("params" in raw) {
|
|
449
|
+
const problem = paramsSchemaProblem(raw.params);
|
|
450
|
+
if (problem) {
|
|
451
|
+
warnings?.push(`${where} declares a params schema that was dropped: ${problem}`);
|
|
452
|
+
invalid.push("params" as never);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (invalid.length > 0) {
|
|
456
|
+
return Object.fromEntries(
|
|
457
|
+
Object.entries(raw).filter(([k]) => !(invalid as readonly string[]).includes(k)),
|
|
458
|
+
) as unknown as PolicyCatalogEntry;
|
|
459
|
+
}
|
|
277
460
|
return raw as unknown as PolicyCatalogEntry;
|
|
278
461
|
}
|
|
279
462
|
|
|
280
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Why a `params` schema cannot be PUBLISHED, or `undefined` when it can.
|
|
465
|
+
*
|
|
466
|
+
* The build-time half of the rule above: at build time nothing is installed, so
|
|
467
|
+
* refusing costs nobody anything, while shipping the schema means every machine
|
|
468
|
+
* that installs the pack runs the policy without its parameters and says so only
|
|
469
|
+
* in its own log.
|
|
470
|
+
*/
|
|
471
|
+
export function packParamsProblem(params: unknown): string | undefined {
|
|
472
|
+
return params === undefined ? undefined : paramsSchemaProblem(params);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* What this build makes of a pack's declared `minCliVersion`.
|
|
477
|
+
*
|
|
478
|
+
* Three outcomes, not two, because "the pack states no requirement" and "the
|
|
479
|
+
* pack states one nobody can read" are different facts and only one of them is
|
|
480
|
+
* worth saying out loud.
|
|
481
|
+
*/
|
|
482
|
+
export type MinCliVerdict =
|
|
483
|
+
/** Satisfied, or nothing was claimed. `declared` is present only when it was. */
|
|
484
|
+
| { kind: "satisfied"; declared?: string }
|
|
485
|
+
/** The field was there and unusable. It is DROPPED; the pack still loads. */
|
|
486
|
+
| { kind: "unreadable"; reason: string }
|
|
487
|
+
/** This CLI is genuinely older than the pack requires. The pack is refused. */
|
|
488
|
+
| { kind: "too-old"; reason: string };
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Judge one pack's `minCliVersion` against the running CLI — the ONE place that
|
|
492
|
+
* decision is written down.
|
|
493
|
+
*
|
|
494
|
+
* ## Absent satisfies
|
|
495
|
+
*
|
|
496
|
+
* Every pack published before this release declares nothing, and those packs
|
|
497
|
+
* work. Treating silence as an unmet requirement would refuse them all, and a
|
|
498
|
+
* refused `enforce` pack denies every tool call its policies cover
|
|
499
|
+
* (`pack-failclosed.ts`) — a machine-wide lockout produced by an upgrade.
|
|
500
|
+
*
|
|
501
|
+
* ## Unreadable also satisfies, and says so
|
|
502
|
+
*
|
|
503
|
+
* The rule `parsePackPolicy` already applies to a malformed `authority`: drop
|
|
504
|
+
* the field, record it, do not refuse the pack. A publisher's typo in a version
|
|
505
|
+
* string must not be able to fail somebody's machine closed. It is recorded
|
|
506
|
+
* rather than ignored, because a requirement that quietly evaporates is how a
|
|
507
|
+
* pack ends up enforcing less than its README says.
|
|
508
|
+
*
|
|
509
|
+
* This is the one place the two differ from each other, which is why neither is
|
|
510
|
+
* a boolean: a caller reading `false` to refuse would ignore the typo, and one
|
|
511
|
+
* reading `!== true` would refuse the world.
|
|
512
|
+
*
|
|
513
|
+
* ## Too old refuses, and that is the field's entire purpose
|
|
514
|
+
*
|
|
515
|
+
* A CLI too old to know this field ignores it AND ignores `semantic` with it,
|
|
516
|
+
* installing a pack whose Jev half does nothing on a machine that reports
|
|
517
|
+
* healthy. Only a build new enough to have this check can act on the
|
|
518
|
+
* requirement, so here it acts. It rides the ordinary `PackError` path, which
|
|
519
|
+
* carries `effect` and `clis`, so refusing an `observe` pack does not make the
|
|
520
|
+
* machine deny on its behalf.
|
|
521
|
+
*/
|
|
522
|
+
export function checkPackMinCliVersion(
|
|
523
|
+
packId: string,
|
|
524
|
+
declared: unknown,
|
|
525
|
+
current: string = cliVersion,
|
|
526
|
+
): MinCliVerdict {
|
|
527
|
+
if (declared === undefined) return { kind: "satisfied" };
|
|
528
|
+
const ordered = compareVersions(current, declared);
|
|
529
|
+
if (ordered === null) {
|
|
530
|
+
return {
|
|
531
|
+
kind: "unreadable",
|
|
532
|
+
reason:
|
|
533
|
+
`pack ${packId} declares minCliVersion ${JSON.stringify(declared)}, which is not a version this CLI can ` +
|
|
534
|
+
`compare against its own (${current}) — the requirement was ignored`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (ordered < 0) {
|
|
538
|
+
return {
|
|
539
|
+
kind: "too-old",
|
|
540
|
+
reason:
|
|
541
|
+
`pack ${packId} needs failproofai ${String(declared)} or newer and this is ${current} — ` +
|
|
542
|
+
// As a range: the bare name installs `latest`, which can sit below a
|
|
543
|
+
// prerelease minimum. `declared` compared above, so it is plain semver.
|
|
544
|
+
`run \`npm i -g "failproofai@>=${String(declared)}" && failproofai update\``,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
return { kind: "satisfied", declared: declared as string };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** A bounded, non-empty string field. The cap is part of the envelope budget, not taste. */
|
|
551
|
+
function boundedString(where: string, field: string, value: unknown, max: number): string {
|
|
552
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
553
|
+
throw new Error(`${where} is missing ${field}`);
|
|
554
|
+
}
|
|
555
|
+
if (value.length > max) {
|
|
556
|
+
throw new Error(`${where} has a ${field} of ${value.length} characters, over the ${max}-character cap`);
|
|
557
|
+
}
|
|
558
|
+
return value;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* One probe (or the `exempt` question), validated.
|
|
563
|
+
*
|
|
564
|
+
* `forcedId` is how `exempt` gets its id: the answer map keys it
|
|
565
|
+
* `<policy>.exempt` whatever the manifest wrote, so the field is overwritten
|
|
566
|
+
* rather than checked. Refusing a mismatch there would fail a pack over a
|
|
567
|
+
* value that has no effect.
|
|
568
|
+
*/
|
|
569
|
+
function parseSemanticProbe(where: string, value: unknown, forcedId: string | null): SemanticProbeDeclaration {
|
|
570
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where} is not an object`);
|
|
571
|
+
const raw = value as Record<string, unknown>;
|
|
572
|
+
|
|
573
|
+
let id: string;
|
|
574
|
+
if (forcedId !== null) {
|
|
575
|
+
id = forcedId;
|
|
576
|
+
} else {
|
|
577
|
+
if (typeof raw.id !== "string" || !PROBE_ID_RE.test(raw.id)) {
|
|
578
|
+
throw new Error(`${where} has an unsafe id ${JSON.stringify(raw.id)}`);
|
|
579
|
+
}
|
|
580
|
+
if (RESERVED_PROBE_IDS.has(raw.id)) {
|
|
581
|
+
throw new Error(`${where} uses the reserved probe id ${JSON.stringify(raw.id)}`);
|
|
582
|
+
}
|
|
583
|
+
id = raw.id;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const instructions = boundedString(where, "instructions", raw.instructions, MAX_INSTRUCTIONS_CHARS);
|
|
587
|
+
if (raw.criteria === undefined) return { id, instructions };
|
|
588
|
+
if (!raw.criteria || typeof raw.criteria !== "object" || Array.isArray(raw.criteria)) {
|
|
589
|
+
throw new Error(`${where} has criteria that is not an object`);
|
|
590
|
+
}
|
|
591
|
+
const criteria = raw.criteria as Record<string, unknown>;
|
|
592
|
+
// Both halves or neither. Jev answers the question as written, and a `true`
|
|
593
|
+
// description with no `false` one is a question with only one side explained —
|
|
594
|
+
// the authoring mistake the criteria exist to prevent.
|
|
595
|
+
return {
|
|
596
|
+
id,
|
|
597
|
+
instructions,
|
|
598
|
+
criteria: {
|
|
599
|
+
true: boundedString(where, "criteria.true", criteria.true, MAX_CRITERION_CHARS),
|
|
600
|
+
false: boundedString(where, "criteria.false", criteria.false, MAX_CRITERION_CHARS),
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Validate one serialized SEMANTIC policy carried by a pack. Sibling of
|
|
607
|
+
* {@link parsePackPolicy}, and the rules are its rules where they overlap.
|
|
608
|
+
*
|
|
609
|
+
* It THROWS on every violation and the CALLER decides what a throw costs.
|
|
610
|
+
* `readInstalledPacks` drops the one entry and records why, because a semantic
|
|
611
|
+
* policy is what CLEARS a reviewable regex verdict: losing one leaves the regex
|
|
612
|
+
* block standing, which is noisier and never weaker. `failproofai publish`
|
|
613
|
+
* fails the whole build on the same throw, because nothing is installed yet and
|
|
614
|
+
* refusing there costs nobody anything.
|
|
615
|
+
*
|
|
616
|
+
* The returned entry is NORMALIZED, unlike a regex entry — which is returned
|
|
617
|
+
* as-is so a field this build does not know about survives into `params`. There
|
|
618
|
+
* is no equivalent here: the entry's entire use is to be compiled into a
|
|
619
|
+
* question set, so a field nothing reads is a field that would be published,
|
|
620
|
+
* digest-pinned and then ignored.
|
|
621
|
+
*/
|
|
622
|
+
export function parsePackSemanticPolicy(packId: string, value: unknown, index: number): SemanticManifestEntry {
|
|
623
|
+
const where = `${packId} semantic policy #${index}`;
|
|
624
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where} is not an object`);
|
|
625
|
+
const raw = value as Record<string, unknown>;
|
|
626
|
+
|
|
627
|
+
const name = raw.name;
|
|
628
|
+
if (typeof name !== "string" || !PACK_POLICY_NAME_RE.test(name)) {
|
|
629
|
+
throw new Error(`${where} has an unsafe name ${JSON.stringify(name)}`);
|
|
630
|
+
}
|
|
631
|
+
if ("alwaysOn" in raw) {
|
|
632
|
+
// The same refusal `parsePackPolicy` makes, for the same reason, and it has
|
|
633
|
+
// to be restated rather than inherited: nothing else stops a semantic entry
|
|
634
|
+
// from carrying the field, and a reader that ever honoured it would be
|
|
635
|
+
// honouring it on a downloaded file no local command can switch off.
|
|
636
|
+
throw new Error(`${where} declares alwaysOn, which packs may not set`);
|
|
637
|
+
}
|
|
638
|
+
const title = boundedString(`${packId} semantic policy ${name}`, "title", raw.title, MAX_TITLE_CHARS);
|
|
639
|
+
const scope = `${packId} semantic policy ${name}`;
|
|
640
|
+
|
|
641
|
+
if (!Array.isArray(raw.appliesTo) || raw.appliesTo.length === 0) {
|
|
642
|
+
throw new Error(`${scope} has no appliesTo tool classes`);
|
|
643
|
+
}
|
|
644
|
+
const unknownClass = raw.appliesTo.find((c) => typeof c !== "string" || !SEMANTIC_TOOL_CLASSES.has(c));
|
|
645
|
+
if (unknownClass !== undefined) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`${scope} applies to ${JSON.stringify(unknownClass)}, which is not a tool class ` +
|
|
648
|
+
`(${[...SEMANTIC_TOOL_CLASSES].join(", ")})`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (raw.mode !== "deny" && raw.mode !== "instruct") {
|
|
653
|
+
throw new Error(`${scope} has mode ${JSON.stringify(raw.mode)}, which must be "deny" or "instruct"`);
|
|
654
|
+
}
|
|
655
|
+
// REQUIRED, with no default. It decides whether a prompt injection that talks
|
|
656
|
+
// the human's words back at us can clear the policy, so a default here would
|
|
657
|
+
// be a security decision made by absence — and the direction it fell would
|
|
658
|
+
// depend on which of the two engines read the entry.
|
|
659
|
+
if (typeof raw.userCanOverride !== "boolean") {
|
|
660
|
+
throw new Error(`${scope} is missing userCanOverride, which has no default`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!Array.isArray(raw.probes) || raw.probes.length === 0) {
|
|
664
|
+
throw new Error(`${scope} declares no probes`);
|
|
665
|
+
}
|
|
666
|
+
if (raw.probes.length > MAX_PROBES_PER_POLICY) {
|
|
667
|
+
throw new Error(`${scope} declares ${raw.probes.length} probes, over the cap of ${MAX_PROBES_PER_POLICY}`);
|
|
668
|
+
}
|
|
669
|
+
const probes = raw.probes.map((p, i) => parseSemanticProbe(`${scope} probe #${i}`, p, null));
|
|
670
|
+
const ids = new Set<string>();
|
|
671
|
+
for (const probe of probes) {
|
|
672
|
+
if (ids.has(probe.id)) throw new Error(`${scope} declares probe ${probe.id} twice`);
|
|
673
|
+
ids.add(probe.id);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const exempt = raw.exempt === undefined ? undefined : parseSemanticProbe(`${scope} exempt`, raw.exempt, "exempt");
|
|
677
|
+
|
|
678
|
+
if (raw.precondition !== undefined && !isPackPreconditionName(raw.precondition)) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
`${scope} names precondition ${JSON.stringify(raw.precondition)}, which this build does not have ` +
|
|
681
|
+
`(${PACK_PRECONDITION_NAMES.join(", ")})`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const guidance = boundedString(scope, "guidance", raw.guidance, MAX_GUIDANCE_CHARS);
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
name,
|
|
689
|
+
title,
|
|
690
|
+
appliesTo: raw.appliesTo as SemanticManifestEntry["appliesTo"],
|
|
691
|
+
mode: raw.mode,
|
|
692
|
+
userCanOverride: raw.userCanOverride,
|
|
693
|
+
probes,
|
|
694
|
+
...(exempt ? { exempt } : {}),
|
|
695
|
+
...(raw.precondition !== undefined ? { precondition: raw.precondition } : {}),
|
|
696
|
+
guidance,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** A pack's semantic entries. Absent and empty are the same answer: it declares none. */
|
|
701
|
+
export function packSemantic(pack: Pick<ResolvedPack, "semantic">): ReadonlyArray<SemanticManifestEntry> {
|
|
702
|
+
return pack.semantic ?? [];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** The questions one entry compiles to: one per probe, plus one for the exemption. */
|
|
706
|
+
export function semanticQuestions(
|
|
707
|
+
entry: Pick<SemanticManifestEntry, "probes" | "exempt">,
|
|
708
|
+
): ReadonlyArray<SemanticProbeDeclaration> {
|
|
709
|
+
return entry.exempt ? [...entry.probes, entry.exempt] : entry.probes;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* The semantic entries a pack declared, with the unusable ones dropped.
|
|
714
|
+
*
|
|
715
|
+
* Per ENTRY, not per pack: one malformed question set must not take the rest of
|
|
716
|
+
* a publisher's semantic policies with it, for the same reason one malformed
|
|
717
|
+
* pack does not take the other packs with it.
|
|
718
|
+
*/
|
|
719
|
+
function parsePackSemantic(packId: string, value: unknown, warnings: string[]): SemanticManifestEntry[] {
|
|
720
|
+
if (value === undefined) return [];
|
|
721
|
+
if (!Array.isArray(value)) {
|
|
722
|
+
warnings.push(`pack ${packId} semantic is not an array, so none of it was loaded`);
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const entries: SemanticManifestEntry[] = [];
|
|
726
|
+
const names = new Set<string>();
|
|
727
|
+
for (const [index, raw] of value.entries()) {
|
|
728
|
+
if (entries.length >= MAX_SEMANTIC_POLICIES_PER_PACK) {
|
|
729
|
+
warnings.push(
|
|
730
|
+
`pack ${packId} declares more than ${MAX_SEMANTIC_POLICIES_PER_PACK} semantic policies; ` +
|
|
731
|
+
`the ${value.length - MAX_SEMANTIC_POLICIES_PER_PACK} past that cap were dropped`,
|
|
732
|
+
);
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const entry = parsePackSemanticPolicy(packId, raw, index);
|
|
737
|
+
if (names.has(entry.name)) throw new Error(`pack ${packId} declares semantic policy ${entry.name} twice`);
|
|
738
|
+
names.add(entry.name);
|
|
739
|
+
entries.push(entry);
|
|
740
|
+
} catch (err) {
|
|
741
|
+
warnings.push(errText(err));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return entries;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function parsePack(root: string, value: unknown, warnings: string[]): ResolvedPack {
|
|
281
748
|
if (!value || typeof value !== "object") throw new Error("pack entry is not an object");
|
|
282
749
|
const raw = value as InstalledPackRecord;
|
|
283
750
|
|
|
@@ -292,6 +759,12 @@ function parsePack(root: string, value: unknown): ResolvedPack {
|
|
|
292
759
|
// this pack was for.
|
|
293
760
|
const effect = identity.effect;
|
|
294
761
|
|
|
762
|
+
// Checked before the artifact is read: there is no point hashing bytes this
|
|
763
|
+
// build may have just been told it must not run.
|
|
764
|
+
const minCli = checkPackMinCliVersion(identity.id, raw.minCliVersion);
|
|
765
|
+
if (minCli.kind === "too-old") throw new Error(minCli.reason);
|
|
766
|
+
if (minCli.kind === "unreadable") warnings.push(minCli.reason);
|
|
767
|
+
|
|
295
768
|
const path = resolveManagedPath(root, raw.entry);
|
|
296
769
|
const actual = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
297
770
|
if (actual !== raw.sha256) {
|
|
@@ -301,7 +774,7 @@ function parsePack(root: string, value: unknown): ResolvedPack {
|
|
|
301
774
|
}
|
|
302
775
|
|
|
303
776
|
if (!Array.isArray(raw.policies)) throw new Error(`pack ${raw.id} policies is not an array`);
|
|
304
|
-
const policies = raw.policies.map((p, i) => parsePackPolicy(raw.id, p, i));
|
|
777
|
+
const policies = raw.policies.map((p, i) => parsePackPolicy(raw.id, p, i, warnings));
|
|
305
778
|
const names = new Set<string>();
|
|
306
779
|
for (const p of policies) {
|
|
307
780
|
if (names.has(p.name)) throw new Error(`pack ${raw.id} declares ${p.name} twice`);
|
|
@@ -340,6 +813,8 @@ function parsePack(root: string, value: unknown): ResolvedPack {
|
|
|
340
813
|
sha256: raw.sha256,
|
|
341
814
|
effect,
|
|
342
815
|
policies,
|
|
816
|
+
semantic: parsePackSemantic(identity.id, raw.semantic, warnings),
|
|
817
|
+
...(minCli.kind === "satisfied" && minCli.declared ? { minCliVersion: minCli.declared } : {}),
|
|
343
818
|
enabled,
|
|
344
819
|
};
|
|
345
820
|
}
|
|
@@ -388,6 +863,7 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
388
863
|
const root = packsRoot();
|
|
389
864
|
const packs: ResolvedPack[] = [];
|
|
390
865
|
const errors: PackError[] = [];
|
|
866
|
+
const warnings: string[] = [];
|
|
391
867
|
const seen = new Set<string>();
|
|
392
868
|
|
|
393
869
|
for (const entry of manifest.packs) {
|
|
@@ -395,11 +871,16 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
395
871
|
entry && typeof entry === "object" && typeof (entry as InstalledPackRecord).id === "string"
|
|
396
872
|
? (entry as InstalledPackRecord).id
|
|
397
873
|
: null;
|
|
874
|
+
// Collected per pack and merged only on success. A pack that throws after
|
|
875
|
+
// some of its semantic entries were dropped is refused whole, and reporting
|
|
876
|
+
// the drops as well would describe a policy set that is not loaded at all.
|
|
877
|
+
const packWarnings: string[] = [];
|
|
398
878
|
try {
|
|
399
|
-
const pack = parsePack(root, entry);
|
|
879
|
+
const pack = parsePack(root, entry, packWarnings);
|
|
400
880
|
if (seen.has(pack.id)) throw new Error(`duplicate pack id ${pack.id}`);
|
|
401
881
|
seen.add(pack.id);
|
|
402
882
|
packs.push(pack);
|
|
883
|
+
warnings.push(...packWarnings);
|
|
403
884
|
} catch (err) {
|
|
404
885
|
const rec = entry as InstalledPackRecord | null;
|
|
405
886
|
const declaredEffect =
|
|
@@ -411,6 +892,7 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
411
892
|
// Best effort: an entry too malformed to list policies yields nothing
|
|
412
893
|
// here, and a deny built from it is unavoidably blanket.
|
|
413
894
|
...(Array.isArray(rec?.policies) ? { declared: safeDeclared(rec.policies) } : {}),
|
|
895
|
+
...(Array.isArray(rec?.policies) && rec.policies.length === 0 ? { semanticOnly: true as const } : {}),
|
|
414
896
|
// A list of names, or nothing. Anything else is unreadable scope, and
|
|
415
897
|
// unreadable scope has to mean "every agent" — the same reasoning that
|
|
416
898
|
// widens an unreadable `match`: a narrowing nobody can parse says
|
|
@@ -422,7 +904,7 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
422
904
|
});
|
|
423
905
|
}
|
|
424
906
|
}
|
|
425
|
-
return { packs, errors };
|
|
907
|
+
return { packs, errors, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
426
908
|
}
|
|
427
909
|
|
|
428
910
|
/** Policy entries that at least carry a name and a match, for narrowing a deny. */
|