failproofai 1.0.6 → 1.0.7-beta.0
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]__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]__1svlq8e._.js → [root-of-the-server]__1p28m0u._.js} +2 -2
- 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/_0bqoto4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_17o744m._.js → _0cf1olg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0do6d26._.js → _135-c3y._.js} +1 -1
- 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/_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/{25m8mg7bosgip.js → 00i9thnpht4l1.js} +1 -1
- package/.next/standalone/.next/static/chunks/0289oapk24zql.js +1 -0
- package/.next/standalone/.next/static/chunks/{35pw7zxhh76n7.js → 0pyy9m5cm8zku.js} +1 -1
- package/.next/standalone/.next/static/chunks/15heikmy3yjzc.js +1 -0
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +2 -0
- package/.next/standalone/.next/static/chunks/{40-wncjnguy4e.js → 2plg9qae1yejv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/2tqszh84mp_2l.js +1 -0
- package/.next/standalone/.next/static/chunks/34jpdb2xjm-x7.js +1 -0
- package/.next/standalone/.next/static/chunks/3cdkxrukvlvul.js +1 -0
- package/.next/standalone/.next/static/chunks/3dnd-_29uiaj3.js +6 -0
- package/.next/standalone/.next/static/chunks/3vp7c3knkh8mr.js +1 -0
- package/.next/standalone/.next/static/chunks/{2y4ycwcwfjv4k.js → 3vwnzfx9q80yl.js} +1 -1
- 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 +9283 -2149
- 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 +237 -11
- 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/0bepqeawy72n6.js +0 -1
- 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/3-6zhytp6adkk.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 → qbVI48kgBOf9CTOKcFIk2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → qbVI48kgBOf9CTOKcFIk2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LzR_etPlaZV9JZH09lEp3 → qbVI48kgBOf9CTOKcFIk2}/_ssgManifest.js +0 -0
package/src/hooks/pack-store.ts
CHANGED
|
@@ -35,8 +35,17 @@
|
|
|
35
35
|
import { createHash } from "node:crypto";
|
|
36
36
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
37
37
|
import { dirname, resolve } from "node:path";
|
|
38
|
-
import {
|
|
39
|
-
|
|
38
|
+
import {
|
|
39
|
+
MAX_SEMANTIC_POLICIES_PER_PACK,
|
|
40
|
+
PACK_COMMIT_RE,
|
|
41
|
+
checkPackMinCliVersion,
|
|
42
|
+
packsRoot,
|
|
43
|
+
parsePackIdentity,
|
|
44
|
+
parsePackPolicy,
|
|
45
|
+
parsePackSemanticPolicy,
|
|
46
|
+
readInstalledPacks,
|
|
47
|
+
} from "./pack-manifest";
|
|
48
|
+
import type { ResolvedPack, SemanticManifestEntry } from "./pack-manifest";
|
|
40
49
|
import type { InstalledPackRecord } from "./pack-manifest";
|
|
41
50
|
import type { PolicyCatalogEntry } from "./policy-types";
|
|
42
51
|
import type { PolicyEffect } from "./cloud-managed-policies";
|
|
@@ -86,6 +95,13 @@ export interface AddPackResult {
|
|
|
86
95
|
selection: SelectionReason;
|
|
87
96
|
/** Category slugs the pack offers, for `--category`. */
|
|
88
97
|
categories: string[];
|
|
98
|
+
/**
|
|
99
|
+
* How many Jev question sets came with it, and not a list of names: they are
|
|
100
|
+
* not selectable and not switchable, so a name here would invite a `--policy`
|
|
101
|
+
* that cannot work. A non-zero count is what stops a semantic-only install
|
|
102
|
+
* reporting "the pack is installed and enforcing nothing".
|
|
103
|
+
*/
|
|
104
|
+
semantic: number;
|
|
89
105
|
artifact: string;
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -454,6 +470,10 @@ interface FetchedPack {
|
|
|
454
470
|
id: string;
|
|
455
471
|
version: string;
|
|
456
472
|
policies: PolicyCatalogEntry[];
|
|
473
|
+
/** The pack's Jev question sets, validated. Empty when it declares none. */
|
|
474
|
+
semantic: SemanticManifestEntry[];
|
|
475
|
+
/** The minimum CLI it claims, once this one has been checked against it. */
|
|
476
|
+
minCliVersion?: string;
|
|
457
477
|
effect?: PolicyEffect;
|
|
458
478
|
/** The git commit the publisher built this from, when they had one. */
|
|
459
479
|
commit?: string;
|
|
@@ -461,6 +481,66 @@ interface FetchedPack {
|
|
|
461
481
|
artifactDigest: string;
|
|
462
482
|
}
|
|
463
483
|
|
|
484
|
+
/**
|
|
485
|
+
* The shape both fetch paths read a manifest's `semantic` array through.
|
|
486
|
+
*
|
|
487
|
+
* REFUSES what the loader would only drop, and the asymmetry is the same one
|
|
488
|
+
* `parsePackPolicy` already has on this path: nothing has been written yet, so
|
|
489
|
+
* refusing costs the user a clear error message and the publisher a fix —
|
|
490
|
+
* whereas dropping an entry here would install a pack that enforces less than
|
|
491
|
+
* the preview just showed, with the difference recorded only in a hook log. The
|
|
492
|
+
* loader drops instead because by then the pack IS installed and refusing it
|
|
493
|
+
* denies every tool call its regex policies cover.
|
|
494
|
+
*
|
|
495
|
+
* The per-pack cap is checked for the same reason. Over it, the loader keeps the
|
|
496
|
+
* first 24 in declared order and records the rest as dropped; letting that
|
|
497
|
+
* happen silently at install time means a machine quietly running a subset of
|
|
498
|
+
* what the user agreed to.
|
|
499
|
+
*
|
|
500
|
+
* The CHARACTER budget is deliberately not checked here. It lives in
|
|
501
|
+
* `semantic/pack-policies.ts`, and importing that module would pull Jev's
|
|
502
|
+
* sixteen prompts into this one — which is reachable from a Next.js server
|
|
503
|
+
* action (`app/actions/pack-actions.ts`), not just from the CLI. `failproofai
|
|
504
|
+
* publish` refuses an over-budget pack at build time, and a pack built by some
|
|
505
|
+
* other tool has its overflow dropped at resolve time with a recorded reason.
|
|
506
|
+
*/
|
|
507
|
+
function parseManifestSemantic(packId: string, value: unknown): SemanticManifestEntry[] {
|
|
508
|
+
if (value === undefined) return [];
|
|
509
|
+
if (!Array.isArray(value)) throw new Error(`pack ${packId} semantic is not an array`);
|
|
510
|
+
if (value.length > MAX_SEMANTIC_POLICIES_PER_PACK) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`pack ${packId} declares ${value.length} semantic policies, over the cap of ${MAX_SEMANTIC_POLICIES_PER_PACK}`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const entries = value.map((entry, i) => parsePackSemanticPolicy(packId, entry, i));
|
|
516
|
+
const names = new Set<string>();
|
|
517
|
+
for (const entry of entries) {
|
|
518
|
+
if (names.has(entry.name)) throw new Error(`pack ${packId} declares semantic policy ${entry.name} twice`);
|
|
519
|
+
names.add(entry.name);
|
|
520
|
+
}
|
|
521
|
+
return entries;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Refuse a pack this CLI is too old to run — HERE, where the message is
|
|
526
|
+
* something the person who typed the command can act on.
|
|
527
|
+
*
|
|
528
|
+
* The loader refuses it too, and that refusal is the fail-closed one: on a
|
|
529
|
+
* machine where it lands, every tool call the pack's policies cover is denied
|
|
530
|
+
* until somebody works out why. Catching it at install turns the same fact into
|
|
531
|
+
* one line at the moment of choosing.
|
|
532
|
+
*
|
|
533
|
+
* A minimum this build cannot compare is IGNORED, exactly as the loader ignores
|
|
534
|
+
* it: a publisher's typo in a version string must not be able to stop anyone
|
|
535
|
+
* installing a pack. `failproofai publish` refuses to write one, so this is
|
|
536
|
+
* only reachable from a pack built by some other tool.
|
|
537
|
+
*/
|
|
538
|
+
function assertMinCliVersion(packId: string, declared: unknown): string | undefined {
|
|
539
|
+
const verdict = checkPackMinCliVersion(packId, declared);
|
|
540
|
+
if (verdict.kind === "too-old") throw new Error(verdict.reason);
|
|
541
|
+
return verdict.kind === "satisfied" ? verdict.declared : undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
464
544
|
/** Fetch and fully validate a pack, without writing anything. */
|
|
465
545
|
/**
|
|
466
546
|
* The short names for the pack Failproof AI publishes.
|
|
@@ -543,6 +623,19 @@ export interface PackPreview {
|
|
|
543
623
|
/** The git commit the publisher built this from, when they had one. */
|
|
544
624
|
commit?: string;
|
|
545
625
|
policies: PolicyCatalogEntry[];
|
|
626
|
+
/**
|
|
627
|
+
* The pack's Jev question sets. Shown, not selectable: a pack's `enabled`
|
|
628
|
+
* narrowing picks which of its REGEX policies register, and its semantic set
|
|
629
|
+
* replaces this build's wholesale or not at all.
|
|
630
|
+
*
|
|
631
|
+
* Carried on the preview because the preview is what somebody consents to. A
|
|
632
|
+
* pack that quietly brings sixteen new questions to the classifier — and
|
|
633
|
+
* replaces the sixteen this build shipped — is not something to discover after
|
|
634
|
+
* installing it.
|
|
635
|
+
*/
|
|
636
|
+
semantic: SemanticManifestEntry[];
|
|
637
|
+
/** The minimum CLI this pack declares, when it declares one this build can read. */
|
|
638
|
+
minCliVersion?: string;
|
|
546
639
|
/** The exact source the preview was read from, tag resolved and pinned. */
|
|
547
640
|
source: string;
|
|
548
641
|
/** True when the tag was resolved rather than typed. */
|
|
@@ -589,22 +682,52 @@ export async function fetchPackPreview(source: string): Promise<PackPreview> {
|
|
|
589
682
|
if (!raw || typeof raw !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
|
|
590
683
|
const value = raw as {
|
|
591
684
|
id?: unknown; version?: unknown; policies?: unknown; effect?: unknown; commit?: unknown;
|
|
685
|
+
semantic?: unknown; minCliVersion?: unknown;
|
|
592
686
|
};
|
|
593
687
|
const identity = parsePackIdentity(value);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
688
|
+
const minCliVersion = assertMinCliVersion(identity.id, value.minCliVersion);
|
|
689
|
+
const semantic = parseManifestSemantic(identity.id, value.semantic);
|
|
690
|
+
const policies = parseManifestPolicies(identity.id, value.policies);
|
|
691
|
+
assertDeclaresSomething(policies, semantic);
|
|
597
692
|
return {
|
|
598
693
|
id: identity.id,
|
|
599
694
|
version: identity.version,
|
|
600
695
|
effect: identity.effect,
|
|
601
696
|
...(identity.commit ? { commit: identity.commit } : {}),
|
|
602
|
-
policies
|
|
697
|
+
policies,
|
|
698
|
+
semantic,
|
|
699
|
+
...(minCliVersion ? { minCliVersion } : {}),
|
|
603
700
|
source: formatPackSpec(spec),
|
|
604
701
|
resolvedFromLatest,
|
|
605
702
|
};
|
|
606
703
|
}
|
|
607
704
|
|
|
705
|
+
/**
|
|
706
|
+
* The regex half of a manifest. ABSENT is an empty list, not an error: a pack of
|
|
707
|
+
* Jev questions alone is a legitimate thing to publish now, and ours writes
|
|
708
|
+
* `policies: []` for one. Present and not an array is still malformed.
|
|
709
|
+
*/
|
|
710
|
+
function parseManifestPolicies(packId: string, value: unknown): PolicyCatalogEntry[] {
|
|
711
|
+
if (value === undefined) return [];
|
|
712
|
+
if (!Array.isArray(value)) throw new Error(`pack ${packId} policies is not an array`);
|
|
713
|
+
return value.map((policy, i) => parsePackPolicy(packId, policy, i));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* A pack has to bring SOMETHING, and either half counts.
|
|
718
|
+
*
|
|
719
|
+
* It used to be "at least one regex policy", which made a pack of Jev questions
|
|
720
|
+
* alone impossible to install — and that is a legitimate pack: the regex floor
|
|
721
|
+
* may already be somebody else's, and the two halves version independently. This
|
|
722
|
+
* is the same widening `build()` makes to its "registered no policies" refusal,
|
|
723
|
+
* so the two ends of the lane agree on what an empty pack is.
|
|
724
|
+
*/
|
|
725
|
+
function assertDeclaresSomething(policies: PolicyCatalogEntry[], semantic: SemanticManifestEntry[]): void {
|
|
726
|
+
if (policies.length === 0 && semantic.length === 0) {
|
|
727
|
+
throw new Error("pack manifest declares no policies");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
608
731
|
/**
|
|
609
732
|
* The record this install is really an update OF — matched by id, or failing
|
|
610
733
|
* that by artifact digest.
|
|
@@ -647,20 +770,26 @@ async function fetchPack(spec: PinnedPackSpec): Promise<FetchedPack> {
|
|
|
647
770
|
if (!parsed || typeof parsed !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
|
|
648
771
|
const raw = parsed as {
|
|
649
772
|
id?: unknown; version?: unknown; policies?: unknown; effect?: unknown; commit?: unknown;
|
|
773
|
+
semantic?: unknown; minCliVersion?: unknown;
|
|
650
774
|
};
|
|
651
775
|
const identity = parsePackIdentity(raw);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
776
|
+
// Before the shape checks below, because being too old for the pack is not a
|
|
777
|
+
// complaint about the pack: the message to give is "update the CLI", and a
|
|
778
|
+
// manifest field this build cannot read yet must not be reported as malformed.
|
|
779
|
+
const minCliVersion = assertMinCliVersion(identity.id, raw.minCliVersion);
|
|
655
780
|
// Validated with the SAME rules the loader applies, so a pack that could never
|
|
656
781
|
// load is refused here — while nothing has been written — rather than
|
|
657
782
|
// installing cleanly and failing silently on the next tool call.
|
|
658
|
-
const policies =
|
|
783
|
+
const policies = parseManifestPolicies(identity.id, raw.policies);
|
|
784
|
+
const semantic = parseManifestSemantic(identity.id, raw.semantic);
|
|
785
|
+
assertDeclaresSomething(policies, semantic);
|
|
659
786
|
|
|
660
787
|
return {
|
|
661
788
|
id: identity.id,
|
|
662
789
|
version: identity.version,
|
|
663
790
|
policies,
|
|
791
|
+
semantic,
|
|
792
|
+
...(minCliVersion ? { minCliVersion } : {}),
|
|
664
793
|
...(raw.effect !== undefined ? { effect: identity.effect } : {}),
|
|
665
794
|
...(identity.commit ? { commit: identity.commit } : {}),
|
|
666
795
|
artifact,
|
|
@@ -931,6 +1060,22 @@ export async function addPack(
|
|
|
931
1060
|
entry: artifactRel,
|
|
932
1061
|
sha256: fetched.artifactDigest,
|
|
933
1062
|
policies: fetched.policies,
|
|
1063
|
+
// Both halves of the manifest reach the record, because `installed.json` is
|
|
1064
|
+
// the only thing enforcement reads. Left off, a pack's Jev questions were
|
|
1065
|
+
// fetched, verified, shown in the preview and then dropped on the floor: the
|
|
1066
|
+
// resolver found no `semantic` on any installed pack, kept this build's
|
|
1067
|
+
// compiled set, and every pack policy naming one of the pack's own checks in
|
|
1068
|
+
// `reviewedBy` registered as hard. Nothing failed, and the half the user
|
|
1069
|
+
// installed did nothing.
|
|
1070
|
+
//
|
|
1071
|
+
// Omitted when empty for the same reason the manifest omits it: an empty
|
|
1072
|
+
// array reads as "this pack declares semantic entries", and the replacement
|
|
1073
|
+
// rule would then have it replace the compiled-in set with nothing.
|
|
1074
|
+
...(fetched.semantic.length > 0 ? { semantic: fetched.semantic } : {}),
|
|
1075
|
+
// Recorded so the READER re-checks it. This CLI has already satisfied it or
|
|
1076
|
+
// refused the install, but the record outlives this CLI: a downgrade, or a
|
|
1077
|
+
// machine restored from another's home directory, has to meet it again.
|
|
1078
|
+
...(fetched.minCliVersion ? { minCliVersion: fetched.minCliVersion } : {}),
|
|
934
1079
|
// Provenance, carried through from the manifest so `policies` and the
|
|
935
1080
|
// dashboard can say which source produced what is installed. NOT part of
|
|
936
1081
|
// verification — `sha256` above is the pin, and this is a label beside it.
|
|
@@ -978,6 +1123,7 @@ export async function addPack(
|
|
|
978
1123
|
available,
|
|
979
1124
|
selection: reason,
|
|
980
1125
|
categories: [...new Set(fetched.policies.map((p) => slugifyCategory(p.category)))],
|
|
1126
|
+
semantic: fetched.semantic.length,
|
|
981
1127
|
artifact: artifactAbs,
|
|
982
1128
|
};
|
|
983
1129
|
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy authority: who has the last word on a policy's deny or instruct once
|
|
3
|
+
* Jev is configured. See {@link PolicyAuthority} for the two values and
|
|
4
|
+
* `docs/policies/authority.mdx` for the builtin table.
|
|
5
|
+
*
|
|
6
|
+
* Every route a policy arrives by — a builtin, the user's own policy file, an
|
|
7
|
+
* installed pack, a cloud assignment — declares its authority somewhere
|
|
8
|
+
* different. This module is the one place those declarations are read and
|
|
9
|
+
* judged, so the rule cannot drift between them:
|
|
10
|
+
*
|
|
11
|
+
* - `hard` is the default and the answer to anything unclear. Absent, a value
|
|
12
|
+
* that is not one of the two, a `reviewedBy` that is empty or malformed or
|
|
13
|
+
* names a check this build does not have, or an `alwaysOn` policy: all hard.
|
|
14
|
+
* - `reviewable` means Jev may clear the policy's verdict — and only through the
|
|
15
|
+
* semantic policies it names, every one of which must have been asked.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here decides anything at evaluation time. It resolves a declaration
|
|
18
|
+
* once, at registration, and `registerPolicy` stores the result, so what sits in
|
|
19
|
+
* the registry is already the effective authority.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { jevConfigFile } from "./fp-home";
|
|
23
|
+
import { hookLogWarn } from "./hook-logger";
|
|
24
|
+
import { effectiveAuthority } from "./policy-types";
|
|
25
|
+
import type { CustomHook, PolicyAuthority, PolicyCatalogEntry } from "./policy-types";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A declaration as it arrives, before anything has vouched for it. Fields are
|
|
29
|
+
* `unknown` because three of the four sources are files on disk written by
|
|
30
|
+
* somebody else.
|
|
31
|
+
*/
|
|
32
|
+
export interface AuthorityDeclaration {
|
|
33
|
+
authority?: unknown;
|
|
34
|
+
reviewedBy?: unknown;
|
|
35
|
+
/** The self-protection guard. Always hard, whatever else it says. */
|
|
36
|
+
alwaysOn?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The shape-valid authority fields a parsed record (a pack entry, a cloud assignment) carries. */
|
|
40
|
+
export interface AuthorityFields {
|
|
41
|
+
authority?: PolicyAuthority;
|
|
42
|
+
reviewedBy?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What a registered policy carries: already judged, and honest to its type. */
|
|
46
|
+
export interface ResolvedAuthority {
|
|
47
|
+
authority: PolicyAuthority;
|
|
48
|
+
/** Present exactly when `authority` is `reviewable`. Deduplicated, in declared order. */
|
|
49
|
+
reviewedBy?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Why a `reviewable` declaration was not honored, for a warning. Absent when
|
|
52
|
+
* nothing asked for `reviewable` — a policy that is simply hard is not news.
|
|
53
|
+
*/
|
|
54
|
+
downgraded?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The names `reviewedBy` may use: the semantic policies in
|
|
59
|
+
* `src/hooks/semantic/policies.ts`. Not the probes (`INJECTION_PROBE`,
|
|
60
|
+
* `SCOPE_PROBE`, the task probes) — those are inputs to Jev's decision, not
|
|
61
|
+
* checks a regex verdict can be cleared by.
|
|
62
|
+
*
|
|
63
|
+
* Written out rather than read off `SEMANTIC_POLICIES`, and pinned to it by
|
|
64
|
+
* `policy-authority.test.ts`. The registry imports this module, so a runtime
|
|
65
|
+
* import of the semantic policies would load all sixteen of Jev's prompts on
|
|
66
|
+
* every hook event of a machine that never configured Jev, and bundle them into
|
|
67
|
+
* every pack artifact that registers a policy.
|
|
68
|
+
*/
|
|
69
|
+
export const SEMANTIC_POLICY_NAMES = [
|
|
70
|
+
"destructive-deletion",
|
|
71
|
+
"production-infra-change",
|
|
72
|
+
"git-history-rewrite",
|
|
73
|
+
"push-to-protected-branch",
|
|
74
|
+
"commit-on-protected-branch",
|
|
75
|
+
"secret-exposure",
|
|
76
|
+
"credential-exfiltration",
|
|
77
|
+
"remote-code-execution",
|
|
78
|
+
"privilege-escalation",
|
|
79
|
+
"database-destruction",
|
|
80
|
+
"read-outside-workspace",
|
|
81
|
+
"agent-config-tampering",
|
|
82
|
+
"system-modification",
|
|
83
|
+
"env-secrets-dump",
|
|
84
|
+
"external-destructive-action",
|
|
85
|
+
"external-data-egress",
|
|
86
|
+
] as const;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The compiled-in reviewer set, and the DEFAULT rather than the only one.
|
|
90
|
+
*
|
|
91
|
+
* A pack that declares its own `semantic` entries replaces
|
|
92
|
+
* `SEMANTIC_POLICIES` wholesale on the machine that installed it, so the names
|
|
93
|
+
* a `reviewedBy` may use there are the pack's. Callers that know which set is
|
|
94
|
+
* live pass it (`effectiveReviewerNames()` in `pack-manifest.ts`, which reads
|
|
95
|
+
* the manifest and imports nothing from `semantic/`); everyone else gets this
|
|
96
|
+
* one, which is what a machine with no pack runs.
|
|
97
|
+
*/
|
|
98
|
+
export const SEMANTIC_REVIEWER_NAMES: ReadonlySet<string> = new Set(SEMANTIC_POLICY_NAMES);
|
|
99
|
+
|
|
100
|
+
const isName = (n: unknown): n is string => typeof n === "string" && n.length > 0;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Judge one declaration.
|
|
104
|
+
*
|
|
105
|
+
* Stricter than `effectiveAuthority`, which is the §7 contract and needs only
|
|
106
|
+
* one usable name: here EVERY entry must be a semantic policy this build has,
|
|
107
|
+
* and anything else makes the whole declaration hard rather than being dropped.
|
|
108
|
+
* `reviewedBy` is a conjunction, so dropping a name the author wrote — a typo, a
|
|
109
|
+
* stray `7` — would let Jev clear the policy on fewer checks than they asked
|
|
110
|
+
* for. It is also the safe direction for version skew: a pack built against a
|
|
111
|
+
* newer semantic set, installed on an older build, enforces as hard rather than
|
|
112
|
+
* as reviewable-by-less.
|
|
113
|
+
*
|
|
114
|
+
* What this returns is what `registerPolicy` stores, so a registered policy's
|
|
115
|
+
* `reviewedBy` is always clean and the two functions agree on it.
|
|
116
|
+
*
|
|
117
|
+
* @param knownReviewers - the semantic policies that CAN be asked on this
|
|
118
|
+
* machine. Defaults to the compiled-in set, which is what runs until a pack
|
|
119
|
+
* ships its own; see {@link SEMANTIC_REVIEWER_NAMES}. It is a parameter rather
|
|
120
|
+
* than a lookup because this module is imported by the registry, and reading
|
|
121
|
+
* which set is live means reading a file — a cost registration is willing to
|
|
122
|
+
* pay once and this judgement must not pay per call.
|
|
123
|
+
*/
|
|
124
|
+
export function resolvePolicyAuthority(
|
|
125
|
+
decl: AuthorityDeclaration | undefined,
|
|
126
|
+
knownReviewers: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES,
|
|
127
|
+
): ResolvedAuthority {
|
|
128
|
+
const declaredReviewable = decl?.authority === "reviewable";
|
|
129
|
+
if (!decl || effectiveAuthority(decl) === "hard") {
|
|
130
|
+
if (!declaredReviewable) return { authority: "hard" };
|
|
131
|
+
return { authority: "hard", downgraded: whyHard(decl!) };
|
|
132
|
+
}
|
|
133
|
+
const names = decl.reviewedBy as unknown[];
|
|
134
|
+
if (!names.every(isName)) {
|
|
135
|
+
return { authority: "hard", downgraded: "reviewedBy is not a list of semantic policy names" };
|
|
136
|
+
}
|
|
137
|
+
const unknown = names.filter((n) => !knownReviewers.has(n));
|
|
138
|
+
if (unknown.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
authority: "hard",
|
|
141
|
+
downgraded:
|
|
142
|
+
`reviewedBy names ${unknown.map((n) => JSON.stringify(n)).join(", ")}, ` +
|
|
143
|
+
`which ${unknown.length === 1 ? "is not a semantic policy" : "are not semantic policies"} in this build`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { authority: "reviewable", reviewedBy: [...new Set(names)] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function whyHard(decl: AuthorityDeclaration): string {
|
|
150
|
+
if (decl.alwaysOn === true) return "it is alwaysOn, and the self-protection guard is never reviewable";
|
|
151
|
+
if (!Array.isArray(decl.reviewedBy) || decl.reviewedBy.length === 0) {
|
|
152
|
+
return "reviewedBy does not name any semantic policy";
|
|
153
|
+
}
|
|
154
|
+
return "reviewedBy is not a list of semantic policy names";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The shape-valid authority fields of a serialized policy entry, for parsers.
|
|
159
|
+
*
|
|
160
|
+
* Invalid fields are DROPPED, never thrown on. A pack or cloud deployment that
|
|
161
|
+
* is refused over a typo in an optional field fails closed and denies every
|
|
162
|
+
* tool call its policies cover; dropping the field makes that one policy hard,
|
|
163
|
+
* which is the default the typo would have produced at registration anyway.
|
|
164
|
+
* Names are not checked against the semantic set here — that is a property of
|
|
165
|
+
* the build reading the entry, not of the entry, and is judged at registration.
|
|
166
|
+
*/
|
|
167
|
+
export function authorityFieldsOf(raw: Record<string, unknown>): AuthorityFields {
|
|
168
|
+
const out: AuthorityFields = {};
|
|
169
|
+
if (raw.authority === "hard" || raw.authority === "reviewable") out.authority = raw.authority;
|
|
170
|
+
const list = raw.reviewedBy;
|
|
171
|
+
if (Array.isArray(list) && list.length > 0 && list.every(isName)) {
|
|
172
|
+
out.reviewedBy = [...(list as string[])];
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Why a declaration cannot be PUBLISHED as written, or `undefined` when it can.
|
|
179
|
+
*
|
|
180
|
+
* For build steps — `scripts/build-policy-pack.mjs` and `failproofai publish`.
|
|
181
|
+
* The loaders drop a malformed field and registration downgrades a refused
|
|
182
|
+
* `reviewable`, both toward hard and both only as a warning, because by then the
|
|
183
|
+
* pack is installed and refusing it would fail it closed. At build time nothing
|
|
184
|
+
* is installed yet, so refusing costs nobody anything, while shipping the
|
|
185
|
+
* declaration would make the policy silently hard on every machine that
|
|
186
|
+
* installs it — a decision its author made, quietly not taking effect.
|
|
187
|
+
*
|
|
188
|
+
* An empty `reviewedBy` on a hard policy is not a problem: it says nothing.
|
|
189
|
+
*/
|
|
190
|
+
export function authorityProblem(
|
|
191
|
+
decl: AuthorityDeclaration,
|
|
192
|
+
/**
|
|
193
|
+
* The reviewers the pack being built will SHIP WITH — its own `semantic`
|
|
194
|
+
* entries when it declares any, this build's set otherwise. A pack that
|
|
195
|
+
* carries both halves of the two-tier set names its own checks, and refusing
|
|
196
|
+
* that would make the feature unpublishable by the tool that implements it.
|
|
197
|
+
*/
|
|
198
|
+
knownReviewers: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES,
|
|
199
|
+
): string | undefined {
|
|
200
|
+
if (decl.authority !== undefined && decl.authority !== "hard" && decl.authority !== "reviewable") {
|
|
201
|
+
const t = typeof decl.authority;
|
|
202
|
+
return `authority must be "hard" or "reviewable", and is ${t === "string" ? JSON.stringify(decl.authority) : `${/^[aeiou]/.test(t) ? "an" : "a"} ${t}`}`;
|
|
203
|
+
}
|
|
204
|
+
if (decl.reviewedBy !== undefined && !(Array.isArray(decl.reviewedBy) && decl.reviewedBy.every(isName))) {
|
|
205
|
+
return "reviewedBy must be a list of semantic policy names";
|
|
206
|
+
}
|
|
207
|
+
const { downgraded } = resolvePolicyAuthority(decl, knownReviewers);
|
|
208
|
+
return downgraded ? `authority "reviewable" was refused — ${downgraded}` : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The authority fields a pack MANIFEST should carry for one entry: resolved, as
|
|
213
|
+
* the registry will store them, so the manifest says exactly what takes effect.
|
|
214
|
+
* Throws, naming the entry, on anything {@link authorityProblem} refuses.
|
|
215
|
+
*/
|
|
216
|
+
export function manifestAuthority(
|
|
217
|
+
entry: AuthorityDeclaration & { name: string },
|
|
218
|
+
knownReviewers: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES,
|
|
219
|
+
): {
|
|
220
|
+
authority: PolicyAuthority;
|
|
221
|
+
reviewedBy?: string[];
|
|
222
|
+
} {
|
|
223
|
+
const problem = authorityProblem(entry, knownReviewers);
|
|
224
|
+
if (problem) throw new Error(`${entry.name}: ${problem}`);
|
|
225
|
+
const { authority, reviewedBy } = resolvePolicyAuthority(entry, knownReviewers);
|
|
226
|
+
return reviewedBy ? { authority, reviewedBy } : { authority };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* One authority for several declarations that land on ONE registration.
|
|
231
|
+
*
|
|
232
|
+
* Artifacts are content-addressed, so two cloud assignments or two packs whose
|
|
233
|
+
* code is byte-identical load as one policy (the collapse in
|
|
234
|
+
* `custom-hooks-loader.ts`). Keeping one of their declarations would let the
|
|
235
|
+
* order of `active.json` or `installed.json` decide — and let one pack's
|
|
236
|
+
* manifest, or one team's assignment, make another's policy reviewable.
|
|
237
|
+
*
|
|
238
|
+
* So it resolves toward HARD, the way the collapse resolves the effect toward
|
|
239
|
+
* enforce: reviewable only when EVERY declaration resolves to reviewable, and
|
|
240
|
+
* then through the UNION of their checks (more checks that must all come back
|
|
241
|
+
* clear is stricter, never weaker). Otherwise it takes the first declaration
|
|
242
|
+
* that is not reviewable, so a refused one is still reported at registration.
|
|
243
|
+
*
|
|
244
|
+
* Returns `record` itself when its own fields already say that, and a copy with
|
|
245
|
+
* only the two authority fields replaced otherwise. `overruled` is true when
|
|
246
|
+
* some declaration asked for reviewable, validly, and did not get it.
|
|
247
|
+
*/
|
|
248
|
+
export function withMergedAuthority<T extends AuthorityFields>(
|
|
249
|
+
record: T,
|
|
250
|
+
declarations: ReadonlyArray<AuthorityFields>,
|
|
251
|
+
): { merged: T; overruled: boolean } {
|
|
252
|
+
const resolved = declarations.map((d) => resolvePolicyAuthority(d));
|
|
253
|
+
const allReviewable = resolved.length > 0 && resolved.every((r) => r.authority === "reviewable");
|
|
254
|
+
const fields: AuthorityFields = allReviewable
|
|
255
|
+
? { authority: "reviewable", reviewedBy: [...new Set(resolved.flatMap((r) => r.reviewedBy ?? []))] }
|
|
256
|
+
: fieldsOf(declarations[resolved.findIndex((r) => r.authority !== "reviewable")] ?? {});
|
|
257
|
+
const overruled = !allReviewable && resolved.some((r) => r.authority === "reviewable");
|
|
258
|
+
const same =
|
|
259
|
+
record.authority === fields.authority &&
|
|
260
|
+
JSON.stringify(record.reviewedBy) === JSON.stringify(fields.reviewedBy);
|
|
261
|
+
if (same) return { merged: record, overruled };
|
|
262
|
+
const { authority: _a, reviewedBy: _r, ...rest } = record;
|
|
263
|
+
return { merged: { ...rest, ...fields } as T, overruled };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The authority fields a record actually carries, and nothing else. */
|
|
267
|
+
function fieldsOf(d: AuthorityFields): AuthorityFields {
|
|
268
|
+
return {
|
|
269
|
+
...(d.authority !== undefined ? { authority: d.authority } : {}),
|
|
270
|
+
...(d.reviewedBy !== undefined ? { reviewedBy: d.reviewedBy } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const warnedAuthority = new Set<string>();
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Say that an authority declaration was not honored — once per process, and
|
|
278
|
+
* only once Jev is configured (a global `jev.json` exists).
|
|
279
|
+
*
|
|
280
|
+
* Without Jev, authority decides nothing, so there is nothing to tell anyone —
|
|
281
|
+
* and a warning is not free: it goes to the hook's stderr, which some CLIs show
|
|
282
|
+
* as the deny text itself, on every event, for every machine that installed a
|
|
283
|
+
* pack built against a newer semantic set. Once per process keeps the daemon's
|
|
284
|
+
* warm worker from repeating it for every tool call. Authors are told at build
|
|
285
|
+
* time as well: `failproofai publish` refuses what this would warn about.
|
|
286
|
+
*
|
|
287
|
+
* Presence of the file rather than a validated load: a machine where someone
|
|
288
|
+
* has started configuring Jev is where the answer to "why does Jev never clear
|
|
289
|
+
* this policy" is wanted, and a stat keeps the semantic modules unloaded.
|
|
290
|
+
*/
|
|
291
|
+
export function warnAuthority(message: string): void {
|
|
292
|
+
try {
|
|
293
|
+
if (!existsSync(jevConfigFile())) return;
|
|
294
|
+
} catch {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (warnedAuthority.has(message)) return;
|
|
298
|
+
warnedAuthority.add(message);
|
|
299
|
+
hookLogWarn(message);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Where a loaded hook's authority is declared, by the route it arrived on.
|
|
304
|
+
*
|
|
305
|
+
* - **Cloud-managed:** the artifact record in the active deployment. A cloud
|
|
306
|
+
* policy's authority is a central decision, so code inside the artifact
|
|
307
|
+
* cannot grant itself `reviewable`.
|
|
308
|
+
* - **Pack:** the pack's own manifest entry for that policy, matched by name —
|
|
309
|
+
* the same place its `params` schema is read from. A pack can only ever
|
|
310
|
+
* describe its own policies this way: its names cannot contain `/`, and they
|
|
311
|
+
* register under `pack/<id>@<version>/`, so no manifest can reach a builtin
|
|
312
|
+
* or another pack's policy. A policy the artifact registers but the manifest
|
|
313
|
+
* does not declare has no entry and is hard. When packs share one artifact,
|
|
314
|
+
* the loader has already merged their entries toward hard
|
|
315
|
+
* ({@link withMergedAuthority}), so no one of them decides for the others.
|
|
316
|
+
* - **The user's own file:** the hook itself.
|
|
317
|
+
*/
|
|
318
|
+
export function authorityDeclarationFor(
|
|
319
|
+
hook: Pick<CustomHook, "name" | "authority" | "reviewedBy">,
|
|
320
|
+
source: {
|
|
321
|
+
cloudManaged?: { authority?: unknown; reviewedBy?: unknown };
|
|
322
|
+
pack?: { policies: PolicyCatalogEntry[] };
|
|
323
|
+
},
|
|
324
|
+
): AuthorityDeclaration {
|
|
325
|
+
if (source.cloudManaged) {
|
|
326
|
+
return { authority: source.cloudManaged.authority, reviewedBy: source.cloudManaged.reviewedBy };
|
|
327
|
+
}
|
|
328
|
+
if (source.pack) {
|
|
329
|
+
const entry = source.pack.policies.find((p) => p.name === hook.name);
|
|
330
|
+
return { authority: entry?.authority, reviewedBy: entry?.reviewedBy };
|
|
331
|
+
}
|
|
332
|
+
return { authority: hook.authority, reviewedBy: hook.reviewedBy };
|
|
333
|
+
}
|