failproofai 1.0.6 → 1.0.7-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/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 +7 -5
- 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 +13 -11
- 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 +7 -7
- 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 +41 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -9
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__1j-zsg5._.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]__1_r2rbg._.js → [root-of-the-server]__1bf34x4._.js} +5 -7
- 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/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.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/[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]__056wjo4._.js +4 -0
- 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]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +4 -0
- 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]__0qxnccm._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +3 -0
- 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]__1svlq8e._.js → [root-of-the-server]__0yxwl6j._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +4 -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]__1b2dbet._.js +1 -1
- 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]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
- 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/{_0do6d26._.js → _0bn2oo8._.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/_1feuvhb._.js +5 -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/{_0h_douw._.js → _1w_5l7t._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_17o744m._.js → _214wgrp._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- 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/lib_utils_ts_0az0sfq._.js +1 -1
- 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/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.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/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 +56 -23
- package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +6 -0
- package/.next/standalone/.next/static/chunks/{40-wncjnguy4e.js → 0qrbdkv9qmvli.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3-6zhytp6adkk.js → 0uldbut9y2-e8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2y4ycwcwfjv4k.js → 0vmd180qfntfb.js} +1 -1
- package/.next/standalone/.next/static/chunks/{25m8mg7bosgip.js → 12tvm75t5ffui.js} +1 -1
- package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +1 -0
- package/.next/standalone/.next/static/chunks/258668t68du6b.js +1 -0
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +2 -0
- package/.next/standalone/.next/static/chunks/{35pw7zxhh76n7.js → 29fql3nbnfc9q.js} +1 -1
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/{0bepqeawy72n6.js → 2vkvu9-opa_1z.js} +1 -1
- package/.next/standalone/.next/static/chunks/34jpdb2xjm-x7.js +1 -0
- package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +1 -0
- package/.next/standalone/app/actions/get-jev-config.ts +409 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +420 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +9 -0
- package/.next/standalone/app/settings/jev-panel.tsx +469 -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 +51 -3
- package/.next/standalone/fp-cloud-cli/README.md +1 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +44 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +226 -5
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +11 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +68 -1
- package/.next/standalone/fp-cloud-cli/pyproject.toml +1 -1
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +17 -2
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +7 -4
- package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +184 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
- package/.next/standalone/package.json +11 -11
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +2 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +24 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +41 -6
- package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +88 -0
- package/.next/standalone/sdk/python/uv.lock +15 -15
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +115 -2
- package/dist/cli.mjs +9299 -2151
- package/dist/index.js +19 -1
- package/dist/worker.mjs +13761 -7834
- package/package.json +11 -11
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +39 -2
- package/src/hooks/builtin-policies.ts +21 -3
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/custom-hooks-loader.ts +45 -7
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +79 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/handler.ts +265 -6
- package/src/hooks/hook-activity-store.ts +105 -1
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +1193 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/pack-cli.ts +278 -17
- package/src/hooks/pack-manifest.ts +479 -7
- package/src/hooks/pack-store.ts +156 -10
- package/src/hooks/policy-authority.ts +333 -0
- package/src/hooks/policy-catalog.ts +65 -0
- package/src/hooks/policy-evaluator.ts +940 -796
- package/src/hooks/policy-registry.ts +25 -0
- package/src/hooks/policy-reviewability.ts +229 -0
- package/src/hooks/policy-types.ts +126 -0
- package/src/hooks/semantic/combine.ts +541 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +392 -0
- package/src/hooks/semantic/envelope.ts +1296 -0
- package/src/hooks/semantic/evaluator.ts +547 -0
- package/src/hooks/semantic/facts.ts +292 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +643 -0
- package/src/hooks/semantic/jev-config.ts +594 -0
- package/src/hooks/semantic/jev-review.ts +374 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +421 -0
- package/src/hooks/semantic/pack-policies.ts +251 -0
- package/src/hooks/semantic/policies.ts +596 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2910 -0
- package/src/hooks/semantic/types.ts +145 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/worker-server.ts +119 -26
- package/src/index.ts +6 -0
- 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]__013jr2b._.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]__02npjtd._.js +0 -4
- 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]__0da85px._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +0 -4
- 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]__1p2otjt._.js +0 -4
- 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/_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_1cv9_c4._.js +0 -10
- package/.next/standalone/.next/static/chunks/0h-gz8e00478q.js +0 -1
- package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +0 -2
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2lvr2pt70qn14.js +0 -1
- package/.next/standalone/.next/static/chunks/319c_91le7-h_.js +0 -1
- package/.next/standalone/.next/static/chunks/35m53kix7uaii.js +0 -6
- package/.next/standalone/.next/static/chunks/3xacd7qmuqlvs.js +0 -1
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → T8YAYM9h_64fKv705vug7}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → T8YAYM9h_64fKv705vug7}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → T8YAYM9h_64fKv705vug7}/_ssgManifest.js +0 -0
|
@@ -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. */
|
|
@@ -171,6 +259,24 @@ export interface PackReadResult {
|
|
|
171
259
|
packs: ResolvedPack[];
|
|
172
260
|
/** Every pack that was declared and refused, and why. Never silently dropped. */
|
|
173
261
|
errors: PackError[];
|
|
262
|
+
/**
|
|
263
|
+
* What was dropped from a pack that LOADED anyway, and why: a malformed
|
|
264
|
+
* semantic policy, an unknown precondition name, one entry past the cap, a
|
|
265
|
+
* `minCliVersion` nobody can compare.
|
|
266
|
+
*
|
|
267
|
+
* A SEPARATE channel from `errors`, and it has to be. An entry in `errors`
|
|
268
|
+
* means a pack was refused, and `pack-failclosed.ts` turns that into a deny
|
|
269
|
+
* for every event the missing policies claimed. Everything here is the
|
|
270
|
+
* opposite situation: the pack's regex policies are registered and enforcing,
|
|
271
|
+
* and what was lost is a reviewer (so a deny can no longer be CLEARED) or an
|
|
272
|
+
* unreadable version claim. Filing either as a pack failure would deny a
|
|
273
|
+
* machine over a typo in the half of the system whose job is to let more real
|
|
274
|
+
* work through.
|
|
275
|
+
*
|
|
276
|
+
* Absent rather than an empty array when there is nothing to say, so the
|
|
277
|
+
* common result stays the exact shape it has always been.
|
|
278
|
+
*/
|
|
279
|
+
warnings?: string[];
|
|
174
280
|
}
|
|
175
281
|
|
|
176
282
|
export function packsRoot(): string {
|
|
@@ -226,14 +332,56 @@ export function hasInstalledPacks(): boolean {
|
|
|
226
332
|
}
|
|
227
333
|
}
|
|
228
334
|
|
|
229
|
-
function installedFilePath(): string {
|
|
335
|
+
export function installedFilePath(): string {
|
|
230
336
|
return process.env.FAILPROOFAI_PACK_DIR
|
|
231
337
|
? resolve(process.env.FAILPROOFAI_PACK_DIR, "installed.json")
|
|
232
338
|
: packsInstalledFile();
|
|
233
339
|
}
|
|
234
340
|
|
|
341
|
+
|
|
342
|
+
/** The param types a schema may declare. Mirrors {@link PolicyParamsSchema}. */
|
|
343
|
+
const PARAM_TYPES: ReadonlySet<string> = new Set(["string", "number", "boolean", "string[]", "pattern[]"]);
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Why a `params` schema cannot be used, or `undefined` when it can.
|
|
347
|
+
*
|
|
348
|
+
* A pack's schema is what `registerPolicy` stores and `evaluatePolicies` merges
|
|
349
|
+
* the user's configured values ON TOP OF, so a malformed one is not cosmetic: it
|
|
350
|
+
* decides what `ctx.params` contains, and a policy reading a param that is not
|
|
351
|
+
* there falls back to whatever its own code says — usually the stricter thing.
|
|
352
|
+
*
|
|
353
|
+
* Validated as a whole rather than per param. A partial schema would leave one
|
|
354
|
+
* param live and another silently absent inside the same policy, which is the
|
|
355
|
+
* hardest version of this to diagnose from the outside.
|
|
356
|
+
*/
|
|
357
|
+
function paramsSchemaProblem(value: unknown): string | undefined {
|
|
358
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "params is not an object";
|
|
359
|
+
for (const [name, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
360
|
+
if (name.length === 0) return "params has an empty parameter name";
|
|
361
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return `params.${name} is not an object`;
|
|
362
|
+
const spec = raw as Record<string, unknown>;
|
|
363
|
+
if (typeof spec.type !== "string" || !PARAM_TYPES.has(spec.type)) {
|
|
364
|
+
return `params.${name} has type ${JSON.stringify(spec.type)}, which is not one of ${[...PARAM_TYPES].join(", ")}`;
|
|
365
|
+
}
|
|
366
|
+
if (typeof spec.description !== "string" || spec.description.length === 0) {
|
|
367
|
+
return `params.${name} is missing a description`;
|
|
368
|
+
}
|
|
369
|
+
// The KEY has to be there, whatever it holds: an absent default and a
|
|
370
|
+
// `default: undefined` merge the same way, but only one of them says the
|
|
371
|
+
// author thought about it, and the merge is what the user's own value lands on.
|
|
372
|
+
if (!("default" in spec)) return `params.${name} is missing a default`;
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
235
377
|
/** Validate one serialized catalog entry carried by a pack. */
|
|
236
|
-
export function parsePackPolicy(
|
|
378
|
+
export function parsePackPolicy(
|
|
379
|
+
packId: string,
|
|
380
|
+
value: unknown,
|
|
381
|
+
index: number,
|
|
382
|
+
/** Where a DROPPED field is reported. Optional: most callers only care that the entry parsed. */
|
|
383
|
+
warnings?: string[],
|
|
384
|
+
): PolicyCatalogEntry {
|
|
237
385
|
const where = `${packId} policy #${index}`;
|
|
238
386
|
if (!value || typeof value !== "object") throw new Error(`${where} is not an object`);
|
|
239
387
|
const raw = value as Record<string, unknown>;
|
|
@@ -274,10 +422,320 @@ export function parsePackPolicy(packId: string, value: unknown, index: number):
|
|
|
274
422
|
throw new Error(`${where} has a match.${key} that is not a list of names`);
|
|
275
423
|
}
|
|
276
424
|
}
|
|
425
|
+
// Authority: whether Jev may clear this policy's verdict. Unlike `match`, a
|
|
426
|
+
// malformed value is DROPPED rather than refused — refusing the pack fails it
|
|
427
|
+
// closed and denies every tool call it covers, while dropping the field makes
|
|
428
|
+
// this one policy `hard`, which is what an unreadable declaration means
|
|
429
|
+
// anyway. A pack can only describe its OWN policies this way: the name above
|
|
430
|
+
// cannot contain `/`, so no entry can reach a builtin or another pack's
|
|
431
|
+
// policy. Absent fields stay absent, valid ones stay where they were, and an
|
|
432
|
+
// entry with nothing to drop is returned as is.
|
|
433
|
+
const valid = authorityFieldsOf(raw);
|
|
434
|
+
const invalid = (["authority", "reviewedBy"] as const).filter((k) => k in raw && !(k in valid));
|
|
435
|
+
// `params` follows the same rule, and is dropped for the same reason: refusing
|
|
436
|
+
// the pack fails it closed and denies every tool call it covers, while dropping
|
|
437
|
+
// the schema leaves the policy running on its own internal fallbacks. Recorded
|
|
438
|
+
// rather than silent, because the drop is a change to what the policy DOES —
|
|
439
|
+
// `block-sudo` without its `allowPatterns` is stricter than the user asked for,
|
|
440
|
+
// and stricter is the direction nobody reports as a bug.
|
|
441
|
+
if ("params" in raw) {
|
|
442
|
+
const problem = paramsSchemaProblem(raw.params);
|
|
443
|
+
if (problem) {
|
|
444
|
+
warnings?.push(`${where} declares a params schema that was dropped: ${problem}`);
|
|
445
|
+
invalid.push("params" as never);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (invalid.length > 0) {
|
|
449
|
+
return Object.fromEntries(
|
|
450
|
+
Object.entries(raw).filter(([k]) => !(invalid as readonly string[]).includes(k)),
|
|
451
|
+
) as unknown as PolicyCatalogEntry;
|
|
452
|
+
}
|
|
277
453
|
return raw as unknown as PolicyCatalogEntry;
|
|
278
454
|
}
|
|
279
455
|
|
|
280
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Why a `params` schema cannot be PUBLISHED, or `undefined` when it can.
|
|
458
|
+
*
|
|
459
|
+
* The build-time half of the rule above: at build time nothing is installed, so
|
|
460
|
+
* refusing costs nobody anything, while shipping the schema means every machine
|
|
461
|
+
* that installs the pack runs the policy without its parameters and says so only
|
|
462
|
+
* in its own log.
|
|
463
|
+
*/
|
|
464
|
+
export function packParamsProblem(params: unknown): string | undefined {
|
|
465
|
+
return params === undefined ? undefined : paramsSchemaProblem(params);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* What this build makes of a pack's declared `minCliVersion`.
|
|
470
|
+
*
|
|
471
|
+
* Three outcomes, not two, because "the pack states no requirement" and "the
|
|
472
|
+
* pack states one nobody can read" are different facts and only one of them is
|
|
473
|
+
* worth saying out loud.
|
|
474
|
+
*/
|
|
475
|
+
export type MinCliVerdict =
|
|
476
|
+
/** Satisfied, or nothing was claimed. `declared` is present only when it was. */
|
|
477
|
+
| { kind: "satisfied"; declared?: string }
|
|
478
|
+
/** The field was there and unusable. It is DROPPED; the pack still loads. */
|
|
479
|
+
| { kind: "unreadable"; reason: string }
|
|
480
|
+
/** This CLI is genuinely older than the pack requires. The pack is refused. */
|
|
481
|
+
| { kind: "too-old"; reason: string };
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Judge one pack's `minCliVersion` against the running CLI — the ONE place that
|
|
485
|
+
* decision is written down.
|
|
486
|
+
*
|
|
487
|
+
* ## Absent satisfies
|
|
488
|
+
*
|
|
489
|
+
* Every pack published before this release declares nothing, and those packs
|
|
490
|
+
* work. Treating silence as an unmet requirement would refuse them all, and a
|
|
491
|
+
* refused `enforce` pack denies every tool call its policies cover
|
|
492
|
+
* (`pack-failclosed.ts`) — a machine-wide lockout produced by an upgrade.
|
|
493
|
+
*
|
|
494
|
+
* ## Unreadable also satisfies, and says so
|
|
495
|
+
*
|
|
496
|
+
* The rule `parsePackPolicy` already applies to a malformed `authority`: drop
|
|
497
|
+
* the field, record it, do not refuse the pack. A publisher's typo in a version
|
|
498
|
+
* string must not be able to fail somebody's machine closed. It is recorded
|
|
499
|
+
* rather than ignored, because a requirement that quietly evaporates is how a
|
|
500
|
+
* pack ends up enforcing less than its README says.
|
|
501
|
+
*
|
|
502
|
+
* This is the one place the two differ from each other, which is why neither is
|
|
503
|
+
* a boolean: a caller reading `false` to refuse would ignore the typo, and one
|
|
504
|
+
* reading `!== true` would refuse the world.
|
|
505
|
+
*
|
|
506
|
+
* ## Too old refuses, and that is the field's entire purpose
|
|
507
|
+
*
|
|
508
|
+
* A CLI too old to know this field ignores it AND ignores `semantic` with it,
|
|
509
|
+
* installing a pack whose Jev half does nothing on a machine that reports
|
|
510
|
+
* healthy. Only a build new enough to have this check can act on the
|
|
511
|
+
* requirement, so here it acts. It rides the ordinary `PackError` path, which
|
|
512
|
+
* carries `effect` and `clis`, so refusing an `observe` pack does not make the
|
|
513
|
+
* machine deny on its behalf.
|
|
514
|
+
*/
|
|
515
|
+
export function checkPackMinCliVersion(
|
|
516
|
+
packId: string,
|
|
517
|
+
declared: unknown,
|
|
518
|
+
current: string = cliVersion,
|
|
519
|
+
): MinCliVerdict {
|
|
520
|
+
if (declared === undefined) return { kind: "satisfied" };
|
|
521
|
+
const ordered = compareVersions(current, declared);
|
|
522
|
+
if (ordered === null) {
|
|
523
|
+
return {
|
|
524
|
+
kind: "unreadable",
|
|
525
|
+
reason:
|
|
526
|
+
`pack ${packId} declares minCliVersion ${JSON.stringify(declared)}, which is not a version this CLI can ` +
|
|
527
|
+
`compare against its own (${current}) — the requirement was ignored`,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (ordered < 0) {
|
|
531
|
+
return {
|
|
532
|
+
kind: "too-old",
|
|
533
|
+
reason:
|
|
534
|
+
`pack ${packId} needs failproofai ${String(declared)} or newer and this is ${current} — ` +
|
|
535
|
+
"run `npm i -g failproofai && failproofai update`",
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return { kind: "satisfied", declared: declared as string };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** A bounded, non-empty string field. The cap is part of the envelope budget, not taste. */
|
|
542
|
+
function boundedString(where: string, field: string, value: unknown, max: number): string {
|
|
543
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
544
|
+
throw new Error(`${where} is missing ${field}`);
|
|
545
|
+
}
|
|
546
|
+
if (value.length > max) {
|
|
547
|
+
throw new Error(`${where} has a ${field} of ${value.length} characters, over the ${max}-character cap`);
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* One probe (or the `exempt` question), validated.
|
|
554
|
+
*
|
|
555
|
+
* `forcedId` is how `exempt` gets its id: the answer map keys it
|
|
556
|
+
* `<policy>.exempt` whatever the manifest wrote, so the field is overwritten
|
|
557
|
+
* rather than checked. Refusing a mismatch there would fail a pack over a
|
|
558
|
+
* value that has no effect.
|
|
559
|
+
*/
|
|
560
|
+
function parseSemanticProbe(where: string, value: unknown, forcedId: string | null): SemanticProbeDeclaration {
|
|
561
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where} is not an object`);
|
|
562
|
+
const raw = value as Record<string, unknown>;
|
|
563
|
+
|
|
564
|
+
let id: string;
|
|
565
|
+
if (forcedId !== null) {
|
|
566
|
+
id = forcedId;
|
|
567
|
+
} else {
|
|
568
|
+
if (typeof raw.id !== "string" || !PROBE_ID_RE.test(raw.id)) {
|
|
569
|
+
throw new Error(`${where} has an unsafe id ${JSON.stringify(raw.id)}`);
|
|
570
|
+
}
|
|
571
|
+
if (RESERVED_PROBE_IDS.has(raw.id)) {
|
|
572
|
+
throw new Error(`${where} uses the reserved probe id ${JSON.stringify(raw.id)}`);
|
|
573
|
+
}
|
|
574
|
+
id = raw.id;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const instructions = boundedString(where, "instructions", raw.instructions, MAX_INSTRUCTIONS_CHARS);
|
|
578
|
+
if (raw.criteria === undefined) return { id, instructions };
|
|
579
|
+
if (!raw.criteria || typeof raw.criteria !== "object" || Array.isArray(raw.criteria)) {
|
|
580
|
+
throw new Error(`${where} has criteria that is not an object`);
|
|
581
|
+
}
|
|
582
|
+
const criteria = raw.criteria as Record<string, unknown>;
|
|
583
|
+
// Both halves or neither. Jev answers the question as written, and a `true`
|
|
584
|
+
// description with no `false` one is a question with only one side explained —
|
|
585
|
+
// the authoring mistake the criteria exist to prevent.
|
|
586
|
+
return {
|
|
587
|
+
id,
|
|
588
|
+
instructions,
|
|
589
|
+
criteria: {
|
|
590
|
+
true: boundedString(where, "criteria.true", criteria.true, MAX_CRITERION_CHARS),
|
|
591
|
+
false: boundedString(where, "criteria.false", criteria.false, MAX_CRITERION_CHARS),
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Validate one serialized SEMANTIC policy carried by a pack. Sibling of
|
|
598
|
+
* {@link parsePackPolicy}, and the rules are its rules where they overlap.
|
|
599
|
+
*
|
|
600
|
+
* It THROWS on every violation and the CALLER decides what a throw costs.
|
|
601
|
+
* `readInstalledPacks` drops the one entry and records why, because a semantic
|
|
602
|
+
* policy is what CLEARS a reviewable regex verdict: losing one leaves the regex
|
|
603
|
+
* block standing, which is noisier and never weaker. `failproofai publish`
|
|
604
|
+
* fails the whole build on the same throw, because nothing is installed yet and
|
|
605
|
+
* refusing there costs nobody anything.
|
|
606
|
+
*
|
|
607
|
+
* The returned entry is NORMALIZED, unlike a regex entry — which is returned
|
|
608
|
+
* as-is so a field this build does not know about survives into `params`. There
|
|
609
|
+
* is no equivalent here: the entry's entire use is to be compiled into a
|
|
610
|
+
* question set, so a field nothing reads is a field that would be published,
|
|
611
|
+
* digest-pinned and then ignored.
|
|
612
|
+
*/
|
|
613
|
+
export function parsePackSemanticPolicy(packId: string, value: unknown, index: number): SemanticManifestEntry {
|
|
614
|
+
const where = `${packId} semantic policy #${index}`;
|
|
615
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where} is not an object`);
|
|
616
|
+
const raw = value as Record<string, unknown>;
|
|
617
|
+
|
|
618
|
+
const name = raw.name;
|
|
619
|
+
if (typeof name !== "string" || !PACK_POLICY_NAME_RE.test(name)) {
|
|
620
|
+
throw new Error(`${where} has an unsafe name ${JSON.stringify(name)}`);
|
|
621
|
+
}
|
|
622
|
+
if ("alwaysOn" in raw) {
|
|
623
|
+
// The same refusal `parsePackPolicy` makes, for the same reason, and it has
|
|
624
|
+
// to be restated rather than inherited: nothing else stops a semantic entry
|
|
625
|
+
// from carrying the field, and a reader that ever honoured it would be
|
|
626
|
+
// honouring it on a downloaded file no local command can switch off.
|
|
627
|
+
throw new Error(`${where} declares alwaysOn, which packs may not set`);
|
|
628
|
+
}
|
|
629
|
+
const title = boundedString(`${packId} semantic policy ${name}`, "title", raw.title, MAX_TITLE_CHARS);
|
|
630
|
+
const scope = `${packId} semantic policy ${name}`;
|
|
631
|
+
|
|
632
|
+
if (!Array.isArray(raw.appliesTo) || raw.appliesTo.length === 0) {
|
|
633
|
+
throw new Error(`${scope} has no appliesTo tool classes`);
|
|
634
|
+
}
|
|
635
|
+
const unknownClass = raw.appliesTo.find((c) => typeof c !== "string" || !SEMANTIC_TOOL_CLASSES.has(c));
|
|
636
|
+
if (unknownClass !== undefined) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`${scope} applies to ${JSON.stringify(unknownClass)}, which is not a tool class ` +
|
|
639
|
+
`(${[...SEMANTIC_TOOL_CLASSES].join(", ")})`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (raw.mode !== "deny" && raw.mode !== "instruct") {
|
|
644
|
+
throw new Error(`${scope} has mode ${JSON.stringify(raw.mode)}, which must be "deny" or "instruct"`);
|
|
645
|
+
}
|
|
646
|
+
// REQUIRED, with no default. It decides whether a prompt injection that talks
|
|
647
|
+
// the human's words back at us can clear the policy, so a default here would
|
|
648
|
+
// be a security decision made by absence — and the direction it fell would
|
|
649
|
+
// depend on which of the two engines read the entry.
|
|
650
|
+
if (typeof raw.userCanOverride !== "boolean") {
|
|
651
|
+
throw new Error(`${scope} is missing userCanOverride, which has no default`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (!Array.isArray(raw.probes) || raw.probes.length === 0) {
|
|
655
|
+
throw new Error(`${scope} declares no probes`);
|
|
656
|
+
}
|
|
657
|
+
if (raw.probes.length > MAX_PROBES_PER_POLICY) {
|
|
658
|
+
throw new Error(`${scope} declares ${raw.probes.length} probes, over the cap of ${MAX_PROBES_PER_POLICY}`);
|
|
659
|
+
}
|
|
660
|
+
const probes = raw.probes.map((p, i) => parseSemanticProbe(`${scope} probe #${i}`, p, null));
|
|
661
|
+
const ids = new Set<string>();
|
|
662
|
+
for (const probe of probes) {
|
|
663
|
+
if (ids.has(probe.id)) throw new Error(`${scope} declares probe ${probe.id} twice`);
|
|
664
|
+
ids.add(probe.id);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const exempt = raw.exempt === undefined ? undefined : parseSemanticProbe(`${scope} exempt`, raw.exempt, "exempt");
|
|
668
|
+
|
|
669
|
+
if (raw.precondition !== undefined && !isPackPreconditionName(raw.precondition)) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`${scope} names precondition ${JSON.stringify(raw.precondition)}, which this build does not have ` +
|
|
672
|
+
`(${PACK_PRECONDITION_NAMES.join(", ")})`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const guidance = boundedString(scope, "guidance", raw.guidance, MAX_GUIDANCE_CHARS);
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
name,
|
|
680
|
+
title,
|
|
681
|
+
appliesTo: raw.appliesTo as SemanticManifestEntry["appliesTo"],
|
|
682
|
+
mode: raw.mode,
|
|
683
|
+
userCanOverride: raw.userCanOverride,
|
|
684
|
+
probes,
|
|
685
|
+
...(exempt ? { exempt } : {}),
|
|
686
|
+
...(raw.precondition !== undefined ? { precondition: raw.precondition } : {}),
|
|
687
|
+
guidance,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** A pack's semantic entries. Absent and empty are the same answer: it declares none. */
|
|
692
|
+
export function packSemantic(pack: Pick<ResolvedPack, "semantic">): ReadonlyArray<SemanticManifestEntry> {
|
|
693
|
+
return pack.semantic ?? [];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** The questions one entry compiles to: one per probe, plus one for the exemption. */
|
|
697
|
+
export function semanticQuestions(
|
|
698
|
+
entry: Pick<SemanticManifestEntry, "probes" | "exempt">,
|
|
699
|
+
): ReadonlyArray<SemanticProbeDeclaration> {
|
|
700
|
+
return entry.exempt ? [...entry.probes, entry.exempt] : entry.probes;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The semantic entries a pack declared, with the unusable ones dropped.
|
|
705
|
+
*
|
|
706
|
+
* Per ENTRY, not per pack: one malformed question set must not take the rest of
|
|
707
|
+
* a publisher's semantic policies with it, for the same reason one malformed
|
|
708
|
+
* pack does not take the other packs with it.
|
|
709
|
+
*/
|
|
710
|
+
function parsePackSemantic(packId: string, value: unknown, warnings: string[]): SemanticManifestEntry[] {
|
|
711
|
+
if (value === undefined) return [];
|
|
712
|
+
if (!Array.isArray(value)) {
|
|
713
|
+
warnings.push(`pack ${packId} semantic is not an array, so none of it was loaded`);
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
const entries: SemanticManifestEntry[] = [];
|
|
717
|
+
const names = new Set<string>();
|
|
718
|
+
for (const [index, raw] of value.entries()) {
|
|
719
|
+
if (entries.length >= MAX_SEMANTIC_POLICIES_PER_PACK) {
|
|
720
|
+
warnings.push(
|
|
721
|
+
`pack ${packId} declares more than ${MAX_SEMANTIC_POLICIES_PER_PACK} semantic policies; ` +
|
|
722
|
+
`the ${value.length - MAX_SEMANTIC_POLICIES_PER_PACK} past that cap were dropped`,
|
|
723
|
+
);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const entry = parsePackSemanticPolicy(packId, raw, index);
|
|
728
|
+
if (names.has(entry.name)) throw new Error(`pack ${packId} declares semantic policy ${entry.name} twice`);
|
|
729
|
+
names.add(entry.name);
|
|
730
|
+
entries.push(entry);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
warnings.push(errText(err));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return entries;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function parsePack(root: string, value: unknown, warnings: string[]): ResolvedPack {
|
|
281
739
|
if (!value || typeof value !== "object") throw new Error("pack entry is not an object");
|
|
282
740
|
const raw = value as InstalledPackRecord;
|
|
283
741
|
|
|
@@ -292,6 +750,12 @@ function parsePack(root: string, value: unknown): ResolvedPack {
|
|
|
292
750
|
// this pack was for.
|
|
293
751
|
const effect = identity.effect;
|
|
294
752
|
|
|
753
|
+
// Checked before the artifact is read: there is no point hashing bytes this
|
|
754
|
+
// build may have just been told it must not run.
|
|
755
|
+
const minCli = checkPackMinCliVersion(identity.id, raw.minCliVersion);
|
|
756
|
+
if (minCli.kind === "too-old") throw new Error(minCli.reason);
|
|
757
|
+
if (minCli.kind === "unreadable") warnings.push(minCli.reason);
|
|
758
|
+
|
|
295
759
|
const path = resolveManagedPath(root, raw.entry);
|
|
296
760
|
const actual = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
297
761
|
if (actual !== raw.sha256) {
|
|
@@ -301,7 +765,7 @@ function parsePack(root: string, value: unknown): ResolvedPack {
|
|
|
301
765
|
}
|
|
302
766
|
|
|
303
767
|
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));
|
|
768
|
+
const policies = raw.policies.map((p, i) => parsePackPolicy(raw.id, p, i, warnings));
|
|
305
769
|
const names = new Set<string>();
|
|
306
770
|
for (const p of policies) {
|
|
307
771
|
if (names.has(p.name)) throw new Error(`pack ${raw.id} declares ${p.name} twice`);
|
|
@@ -340,6 +804,8 @@ function parsePack(root: string, value: unknown): ResolvedPack {
|
|
|
340
804
|
sha256: raw.sha256,
|
|
341
805
|
effect,
|
|
342
806
|
policies,
|
|
807
|
+
semantic: parsePackSemantic(identity.id, raw.semantic, warnings),
|
|
808
|
+
...(minCli.kind === "satisfied" && minCli.declared ? { minCliVersion: minCli.declared } : {}),
|
|
343
809
|
enabled,
|
|
344
810
|
};
|
|
345
811
|
}
|
|
@@ -388,6 +854,7 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
388
854
|
const root = packsRoot();
|
|
389
855
|
const packs: ResolvedPack[] = [];
|
|
390
856
|
const errors: PackError[] = [];
|
|
857
|
+
const warnings: string[] = [];
|
|
391
858
|
const seen = new Set<string>();
|
|
392
859
|
|
|
393
860
|
for (const entry of manifest.packs) {
|
|
@@ -395,11 +862,16 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
395
862
|
entry && typeof entry === "object" && typeof (entry as InstalledPackRecord).id === "string"
|
|
396
863
|
? (entry as InstalledPackRecord).id
|
|
397
864
|
: null;
|
|
865
|
+
// Collected per pack and merged only on success. A pack that throws after
|
|
866
|
+
// some of its semantic entries were dropped is refused whole, and reporting
|
|
867
|
+
// the drops as well would describe a policy set that is not loaded at all.
|
|
868
|
+
const packWarnings: string[] = [];
|
|
398
869
|
try {
|
|
399
|
-
const pack = parsePack(root, entry);
|
|
870
|
+
const pack = parsePack(root, entry, packWarnings);
|
|
400
871
|
if (seen.has(pack.id)) throw new Error(`duplicate pack id ${pack.id}`);
|
|
401
872
|
seen.add(pack.id);
|
|
402
873
|
packs.push(pack);
|
|
874
|
+
warnings.push(...packWarnings);
|
|
403
875
|
} catch (err) {
|
|
404
876
|
const rec = entry as InstalledPackRecord | null;
|
|
405
877
|
const declaredEffect =
|
|
@@ -422,7 +894,7 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
422
894
|
});
|
|
423
895
|
}
|
|
424
896
|
}
|
|
425
|
-
return { packs, errors };
|
|
897
|
+
return { packs, errors, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
426
898
|
}
|
|
427
899
|
|
|
428
900
|
/** Policy entries that at least carry a name and a match, for narrowing a deny. */
|