failproofai 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +9 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -10
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +7 -6
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -7
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
- package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
- package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
- package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
- package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
- package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
- package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
- package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
- package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
- package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
- package/.next/standalone/app/actions/get-jev-config.ts +604 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +569 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +14 -3
- package/.next/standalone/app/settings/jev-panel.tsx +630 -0
- package/.next/standalone/app/settings/page.tsx +20 -1
- package/.next/standalone/app/settings/settings-client.tsx +27 -1
- package/.next/standalone/app/settings/settings.css +79 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
- package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +3 -2
- package/bin/failproofai.mjs +148 -4
- package/dist/cli.mjs +18599 -9478
- package/dist/index.js +19 -1
- package/dist/worker.mjs +10500 -3773
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +53 -2
- package/src/audit/features.ts +3 -2
- package/src/hooks/builtin-policies.ts +195 -8
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +57 -8
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +98 -10
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +243 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +303 -7
- package/src/hooks/hook-activity-store.ts +118 -6
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +2172 -0
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/manager.ts +45 -7
- package/src/hooks/pack-cli.ts +568 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +489 -7
- package/src/hooks/pack-store.ts +194 -11
- package/src/hooks/policy-authority.ts +386 -0
- package/src/hooks/policy-catalog.ts +206 -0
- package/src/hooks/policy-evaluator.ts +963 -796
- package/src/hooks/policy-registry.ts +27 -1
- package/src/hooks/policy-reviewability.ts +258 -0
- package/src/hooks/policy-types.ts +128 -0
- package/src/hooks/semantic/combine.ts +581 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +513 -0
- package/src/hooks/semantic/envelope.ts +1255 -0
- package/src/hooks/semantic/evaluator.ts +556 -0
- package/src/hooks/semantic/facts.ts +387 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +1155 -0
- package/src/hooks/semantic/jev-config.ts +1142 -0
- package/src/hooks/semantic/jev-review.ts +389 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +427 -0
- package/src/hooks/semantic/pack-policies.ts +294 -0
- package/src/hooks/semantic/policies.ts +597 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2937 -0
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +162 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/types.ts +1 -1
- package/src/hooks/worker-server.ts +121 -27
- package/src/index.ts +6 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
- package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
- package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev answers → one allow / deny / instruct.
|
|
3
|
+
*
|
|
4
|
+
* Pure and deterministic: given the recorded probabilities, the same verdict
|
|
5
|
+
* comes back forever, offline. That is what makes a semantic decision
|
|
6
|
+
* replayable and testable even though the model behind it is not.
|
|
7
|
+
*
|
|
8
|
+
* The rules, in order of how much they matter:
|
|
9
|
+
*
|
|
10
|
+
* - A policy fires only when EVERY probe holds; its evidence is the minimum
|
|
11
|
+
* over probes (TypeSafe's own function-calling cookbook takes the minimum,
|
|
12
|
+
* not the product — one wrong argument spoils the call).
|
|
13
|
+
* - A `deny` policy blocks only on strong evidence; moderate evidence warns.
|
|
14
|
+
* - The user may clear a policy only if (a) they explicitly asked for this
|
|
15
|
+
* action, (b) everything the call affects stays inside what they asked for
|
|
16
|
+
* (the `scope` probe), (c) when the call names identifiable targets, EVERY
|
|
17
|
+
* one of them appears in what they typed — checked here, in code, not by the
|
|
18
|
+
* model — and (d) the request does not look like it is talking to the
|
|
19
|
+
* reviewer. When the local shell scan may have missed part of the command
|
|
20
|
+
* (`$'…'`, a heredoc, `$(…)`, …), (c) cannot be checked and nothing is
|
|
21
|
+
* cleared: the scan can see an innocent first target and stop before the
|
|
22
|
+
* destructive one.
|
|
23
|
+
* A call that names no target is never cleared by default: the scope answer
|
|
24
|
+
* has to carry it.
|
|
25
|
+
* - The injection probe withdraws any override, and turns a policy that has
|
|
26
|
+
* INDEPENDENTLY fired into a block. On its own it can never block: a
|
|
27
|
+
* detector that could would let anyone block any command by writing the
|
|
28
|
+
* right words into it.
|
|
29
|
+
*/
|
|
30
|
+
import { scanCommand } from "./facts";
|
|
31
|
+
import type { PolicyOutcome, SemanticPolicy, SemanticVerdict } from "./types";
|
|
32
|
+
|
|
33
|
+
export interface Thresholds {
|
|
34
|
+
/** Minimum evidence (min over probes) for a policy to fire at all. */
|
|
35
|
+
fire: number;
|
|
36
|
+
/** Minimum evidence for a `deny`-mode policy to block rather than warn. */
|
|
37
|
+
deny: number;
|
|
38
|
+
/** At or above this, the policy's documented exception applies. */
|
|
39
|
+
exempt: number;
|
|
40
|
+
/** At or above this, the user is taken to have explicitly asked. */
|
|
41
|
+
userAsked: number;
|
|
42
|
+
/** At or above this, the request is treated as addressing the reviewer. */
|
|
43
|
+
injection: number;
|
|
44
|
+
/** At or above this, the request is taken to stay within what the user asked. */
|
|
45
|
+
scope: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_THRESHOLDS: Thresholds = {
|
|
49
|
+
fire: 0.7,
|
|
50
|
+
deny: 0.85,
|
|
51
|
+
exempt: 0.5,
|
|
52
|
+
userAsked: 0.8,
|
|
53
|
+
injection: 0.7,
|
|
54
|
+
scope: 0.8,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Did the user name the target? ────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Words that name the operation or plumbing, never the thing acted on. */
|
|
60
|
+
const GENERIC_TOKENS = new Set([
|
|
61
|
+
"sudo", "env", "time", "nohup", "xargs", "bash", "sh", "zsh", "git", "push", "pull", "commit", "origin",
|
|
62
|
+
"head", "upstream", "force", "rm", "mv", "cp", "cat", "echo", "printf", "grep", "find", "sed", "awk",
|
|
63
|
+
"run", "npm", "npx", "bun", "bunx", "node", "python", "python3", "pip", "install", "uninstall",
|
|
64
|
+
"delete", "remove", "apply", "get", "set", "the", "and", "for", "with", "true", "false", "null", "dev",
|
|
65
|
+
"src", "tmp", "usr", "bin", "local", "home", "etc", "var", "lib", "json", "yaml", "yml", "txt", "log",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
function tokensOf(text: string): string[] {
|
|
69
|
+
return text
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.split(/[^a-z0-9._@-]+/)
|
|
72
|
+
.filter((t) => t.length >= 3 && !GENERIC_TOKENS.has(t) && !/^\d+$/.test(t));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What a tool call acts on, as the local target check sees it.
|
|
77
|
+
*
|
|
78
|
+
* `groups` holds one entry per identifiable target — a non-flag argument of a
|
|
79
|
+
* shell command (path components and all), or, for any other tool, the whole
|
|
80
|
+
* of its argument values taken together — and each entry is the set of words
|
|
81
|
+
* that name it. `targets` is every word of every group.
|
|
82
|
+
*
|
|
83
|
+
* `complete` is false when the shell scan may have missed a word bash would
|
|
84
|
+
* run (`ScannedCommand.complete`). The groups are then a lower bound, and no
|
|
85
|
+
* clear may rest on them: see {@link everyTargetNamed}'s callers.
|
|
86
|
+
*/
|
|
87
|
+
export interface TargetScan {
|
|
88
|
+
targets: Set<string>;
|
|
89
|
+
groups: Set<string>[];
|
|
90
|
+
complete: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The words that identify WHAT a tool call acts on: its non-flag arguments
|
|
95
|
+
* (path components included), file paths, URL hosts, MCP argument values.
|
|
96
|
+
* The verb is left to Jev's `user_asked` question; this only checks the noun.
|
|
97
|
+
*/
|
|
98
|
+
export function scanTargets(toolInput: Record<string, unknown>): TargetScan {
|
|
99
|
+
const groups: Set<string>[] = [];
|
|
100
|
+
const addGroup = (tok: string) => {
|
|
101
|
+
const words = tokensOf(tok);
|
|
102
|
+
if (words.length > 0) groups.push(new Set(words));
|
|
103
|
+
};
|
|
104
|
+
const command = typeof toolInput.command === "string" ? toolInput.command : null;
|
|
105
|
+
if (command) {
|
|
106
|
+
const scanned = scanCommand(command);
|
|
107
|
+
for (const seg of scanned.segments) {
|
|
108
|
+
for (const tok of seg.slice(1)) {
|
|
109
|
+
if (!tok.startsWith("-")) addGroup(tok);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Empty reads as "names no target", and lets consent rest on Jev's answers
|
|
113
|
+
// alone. But the scanner is not bash — a `#` inside `$'…'`, `${x:- # }` or
|
|
114
|
+
// backticks ends its view early, and it reads only MAX_SCAN_CHARS — so an
|
|
115
|
+
// empty scan may just have stopped before the target. Then judge the words
|
|
116
|
+
// as written. Only ever stricter: an empty set already passed. (Such a scan
|
|
117
|
+
// is also reported incomplete, which withholds the clear on its own; this
|
|
118
|
+
// keeps the recorded targets honest.)
|
|
119
|
+
if (groups.length === 0) {
|
|
120
|
+
for (const piece of command.split(/[;&|\n]+/)) {
|
|
121
|
+
for (const tok of piece.trim().split(/\s+/).slice(1)) {
|
|
122
|
+
if (!tok.startsWith("-")) addGroup(tok);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { targets: new Set(groups.flatMap((g) => [...g])), groups, complete: scanned.complete };
|
|
127
|
+
}
|
|
128
|
+
const all = new Set<string>();
|
|
129
|
+
for (const [key, value] of Object.entries(toolInput)) {
|
|
130
|
+
if (typeof value !== "string" || value.length > 300) continue;
|
|
131
|
+
if (/content|old_string|new_string|body|text|prompt/i.test(key)) continue;
|
|
132
|
+
for (const t of tokensOf(value)) all.add(t);
|
|
133
|
+
}
|
|
134
|
+
// A non-shell tool's fields qualify one another (`owner`, `repo`, `branch`)
|
|
135
|
+
// rather than naming separate targets, so they are ONE target: naming any of
|
|
136
|
+
// them names it.
|
|
137
|
+
return { targets: all, groups: all.size > 0 ? [all] : [], complete: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every word of {@link scanTargets}, flattened. */
|
|
141
|
+
export function targetTokens(toolInput: Record<string, unknown>): Set<string> {
|
|
142
|
+
return scanTargets(toolInput).targets;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* True when the user's own words name EVERY target the call acts on — each
|
|
147
|
+
* group through at least one of its words.
|
|
148
|
+
*
|
|
149
|
+
* Not "any one": `rm -rf build/ ~/important` after "clean the build" names
|
|
150
|
+
* `build` and not `important`, and a clear on the first would carry the
|
|
151
|
+
* second, which nobody asked for.
|
|
152
|
+
*/
|
|
153
|
+
export function everyTargetNamed(scan: TargetScan, userSaid: ReadonlyArray<string>): boolean {
|
|
154
|
+
if (userSaid.length === 0 || scan.groups.length === 0) return false;
|
|
155
|
+
const said = userSaid.join("\n").toLowerCase();
|
|
156
|
+
return scan.groups.every((g) => {
|
|
157
|
+
for (const t of g) if (said.includes(t)) return true;
|
|
158
|
+
return false;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* True when the user's words name SOME of the call's targets but not all:
|
|
164
|
+
* they drew a line and the call reaches past it. Naming none is not this — a
|
|
165
|
+
* goal ("fix the failing tests") names no path at all.
|
|
166
|
+
*/
|
|
167
|
+
export function partlyNamed(scan: TargetScan, userSaid: ReadonlyArray<string>): boolean {
|
|
168
|
+
if (userSaid.length === 0 || scan.groups.length < 2) return false;
|
|
169
|
+
const said = userSaid.join("\n").toLowerCase();
|
|
170
|
+
let named = 0;
|
|
171
|
+
for (const g of scan.groups) {
|
|
172
|
+
for (const t of g) {
|
|
173
|
+
if (said.includes(t)) {
|
|
174
|
+
named++;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return named > 0 && named < scan.groups.length;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* True when the user's own words name at least one of `targets`. The deciders
|
|
184
|
+
* do not use this any-one check — see {@link everyTargetNamed}.
|
|
185
|
+
*/
|
|
186
|
+
export function targetNamedByUser(targets: Set<string>, userSaid: ReadonlyArray<string>): boolean {
|
|
187
|
+
if (userSaid.length === 0) return false;
|
|
188
|
+
// Nothing identifiable to check is not a match. This used to return true,
|
|
189
|
+
// and `git push --force --all` — scope widened by a flag, naming nothing —
|
|
190
|
+
// passed by default. `decide` covers that case with the scope answer instead.
|
|
191
|
+
if (targets.size === 0) return false;
|
|
192
|
+
const said = userSaid.join("\n").toLowerCase();
|
|
193
|
+
for (const t of targets) if (said.includes(t)) return true;
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Verdict ──────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function formatOutcome(p: SemanticPolicy, o: PolicyOutcome): string {
|
|
200
|
+
return `${p.title} (semantic/${p.name}, p=${o.evidence.toFixed(2)}). ${p.guidance}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function decide(
|
|
204
|
+
selected: ReadonlyArray<SemanticPolicy>,
|
|
205
|
+
answers: Readonly<Record<string, number>>,
|
|
206
|
+
toolInput: Record<string, unknown>,
|
|
207
|
+
userSaid: ReadonlyArray<string>,
|
|
208
|
+
thresholds: Thresholds = DEFAULT_THRESHOLDS,
|
|
209
|
+
/** See {@link DecideV1Options.userSaidCut}. */
|
|
210
|
+
userSaidCut = false,
|
|
211
|
+
): SemanticVerdict {
|
|
212
|
+
const injection = typeof answers.injection === "number" ? answers.injection : null;
|
|
213
|
+
const injected = injection !== null && injection >= thresholds.injection;
|
|
214
|
+
const scope = typeof answers.scope === "number" ? answers.scope : null;
|
|
215
|
+
const withinScope = scope !== null && scope >= thresholds.scope;
|
|
216
|
+
let scan: TargetScan | null = null;
|
|
217
|
+
|
|
218
|
+
const outcomes: PolicyOutcome[] = selected.map((p) => {
|
|
219
|
+
const evidence = Math.min(...p.probes.map((probe) => answers[`${p.name}.${probe.id}`] ?? 0));
|
|
220
|
+
const exempt = p.exempt ? answers[`${p.name}.exempt`] ?? 0 : null;
|
|
221
|
+
const userAsked = answers[`${p.name}.user_asked`] ?? null;
|
|
222
|
+
const base: PolicyOutcome = {
|
|
223
|
+
policy: p.name,
|
|
224
|
+
mode: p.mode,
|
|
225
|
+
userCanOverride: p.userCanOverride,
|
|
226
|
+
...(p.origin ? { origin: p.origin } : {}),
|
|
227
|
+
evidence,
|
|
228
|
+
exempt,
|
|
229
|
+
userAsked,
|
|
230
|
+
targetNamedByUser: false,
|
|
231
|
+
escalatedByInjection: false,
|
|
232
|
+
verdict: "none",
|
|
233
|
+
};
|
|
234
|
+
if (evidence < thresholds.fire || (exempt !== null && exempt >= thresholds.exempt)) return base;
|
|
235
|
+
|
|
236
|
+
// An independently flagged call that also argues for its own approval.
|
|
237
|
+
if (injected) return { ...base, escalatedByInjection: true, verdict: "deny" };
|
|
238
|
+
|
|
239
|
+
const fired: PolicyOutcome["verdict"] = p.mode === "deny" && evidence >= thresholds.deny ? "deny" : "instruct";
|
|
240
|
+
if (p.userCanOverride && userAsked !== null && userAsked >= thresholds.userAsked && withinScope) {
|
|
241
|
+
scan ??= scanTargets(toolInput);
|
|
242
|
+
// A shell scan that may have missed a word cannot say what the call
|
|
243
|
+
// touches, so it cannot say the user named it: no clear. Not rescued by
|
|
244
|
+
// `userSaidCut` — that gap is in what the human typed, this one is in
|
|
245
|
+
// the call.
|
|
246
|
+
if (!scan.complete) return { ...base, verdict: fired, targetScanIncomplete: true };
|
|
247
|
+
// No identifiable target: the scope answer (already required) carries it.
|
|
248
|
+
// Otherwise EVERY target must also appear in the user's own words —
|
|
249
|
+
// unless the words we hold were cut, when "absent" is not something this
|
|
250
|
+
// check knows (see {@link DecideV1Options.userSaidCut}).
|
|
251
|
+
const named = scan.groups.length > 0 && everyTargetNamed(scan, userSaid);
|
|
252
|
+
if (scan.groups.length === 0 || named || userSaidCut) return { ...base, targetNamedByUser: named, verdict: "overridden" };
|
|
253
|
+
}
|
|
254
|
+
return { ...base, verdict: fired };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const byName = new Map(selected.map((p) => [p.name, p]));
|
|
258
|
+
const denies = outcomes.filter((o) => o.verdict === "deny");
|
|
259
|
+
const instructs = outcomes.filter((o) => o.verdict === "instruct");
|
|
260
|
+
const overridden = outcomes.filter((o) => o.verdict === "overridden");
|
|
261
|
+
const injectionNote = denies.some((o) => o.escalatedByInjection)
|
|
262
|
+
? " Blocked because the request also contains text addressed to the reviewer (claiming approval, safety or consent)."
|
|
263
|
+
: "";
|
|
264
|
+
|
|
265
|
+
if (denies.length > 0) {
|
|
266
|
+
const [first, ...rest] = denies;
|
|
267
|
+
const also = rest.length > 0 ? ` Also flagged: ${rest.map((o) => `semantic/${o.policy}`).join(", ")}.` : "";
|
|
268
|
+
return {
|
|
269
|
+
decision: "deny",
|
|
270
|
+
reason: formatOutcome(byName.get(first.policy)!, first) + also + injectionNote,
|
|
271
|
+
outcomes,
|
|
272
|
+
injectionSuspected: injection,
|
|
273
|
+
scopeWithinRequest: scope,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (instructs.length > 0) {
|
|
277
|
+
return {
|
|
278
|
+
decision: "instruct",
|
|
279
|
+
reason: instructs.map((o) => formatOutcome(byName.get(o.policy)!, o)).join("\n") + injectionNote,
|
|
280
|
+
outcomes,
|
|
281
|
+
injectionSuspected: injection,
|
|
282
|
+
scopeWithinRequest: scope,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
decision: "allow",
|
|
287
|
+
reason:
|
|
288
|
+
overridden.length > 0
|
|
289
|
+
? `Allowed because the user explicitly asked: ${overridden.map((o) => `semantic/${o.policy}`).join(", ")}.`
|
|
290
|
+
: null,
|
|
291
|
+
outcomes,
|
|
292
|
+
injectionSuspected: injection,
|
|
293
|
+
scopeWithinRequest: scope,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Intent mode v1 ───────────────────────────────────────────────────────────
|
|
298
|
+
//
|
|
299
|
+
// Pre-registered 2026-09-22 before the v1 blind set existed; see
|
|
300
|
+
// scripts/jev-eval/intent-v2/PREREG.md. Do not tune these on that set.
|
|
301
|
+
|
|
302
|
+
export interface ThresholdsV1 {
|
|
303
|
+
fire: number;
|
|
304
|
+
deny: number;
|
|
305
|
+
exempt: number;
|
|
306
|
+
injection: number;
|
|
307
|
+
/** `op_requested` at or above this: the human asked for this exact operation. */
|
|
308
|
+
opRequested: number;
|
|
309
|
+
/** An op-requested clear also needs `beyond_task` below this. */
|
|
310
|
+
opBeyondMax: number;
|
|
311
|
+
/** `task_step` at or above this: the call serves the human's task. */
|
|
312
|
+
taskStep: number;
|
|
313
|
+
/** A task-step clear or softening also needs `beyond_task` below this. */
|
|
314
|
+
taskBeyondMax: number;
|
|
315
|
+
/** Flag an unflagged call when `beyond_task` is at or above this… */
|
|
316
|
+
beyondFlag: number;
|
|
317
|
+
/** …and `task_step` is below this… */
|
|
318
|
+
beyondTaskStepMax: number;
|
|
319
|
+
/** …and at least one policy's first ("does it do X") probe is at or above this. */
|
|
320
|
+
beyondActionMin: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const DEFAULT_THRESHOLDS_V1: ThresholdsV1 = {
|
|
324
|
+
fire: 0.7,
|
|
325
|
+
deny: 0.85,
|
|
326
|
+
exempt: 0.5,
|
|
327
|
+
injection: 0.7,
|
|
328
|
+
opRequested: 0.8,
|
|
329
|
+
opBeyondMax: 0.5,
|
|
330
|
+
taskStep: 0.8,
|
|
331
|
+
taskBeyondMax: 0.3,
|
|
332
|
+
beyondFlag: 0.8,
|
|
333
|
+
beyondTaskStepMax: 0.5,
|
|
334
|
+
beyondActionMin: 0.5,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* v1 with the beyond-task flag's `task_step` gate removed: an otherwise-allowed
|
|
339
|
+
* call is flagged when it reaches beyond the human's task (`beyond_task` ≥ 0.8)
|
|
340
|
+
* and some "does it do X" probe is at least half-raised, whether or not Jev
|
|
341
|
+
* also calls it a step toward the task. Round 1 found the gate stopped 4 of 6
|
|
342
|
+
* missed overreaches; pre-registered for round 2 (PREREG-round2.md).
|
|
343
|
+
*/
|
|
344
|
+
export const THRESHOLDS_V1_NO_TASK_GATE: ThresholdsV1 = {
|
|
345
|
+
...DEFAULT_THRESHOLDS_V1,
|
|
346
|
+
beyondTaskStepMax: Number.POSITIVE_INFINITY,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export interface DecideV1Options {
|
|
350
|
+
thresholds?: ThresholdsV1;
|
|
351
|
+
/** Turn the beyond-the-task flag off (an ablation). */
|
|
352
|
+
flagBeyondTask?: boolean;
|
|
353
|
+
/** Turn the task-step clear/soften off, leaving only op-requested (an ablation). */
|
|
354
|
+
taskStepClears?: boolean;
|
|
355
|
+
/**
|
|
356
|
+
* A human turn we are reading arrived already cut, so `targetNamedByUser`
|
|
357
|
+
* cannot tell "the user did not name it" from "the part naming it was cut".
|
|
358
|
+
*
|
|
359
|
+
* The local target check is then INCONCLUSIVE rather than negative: the
|
|
360
|
+
* override falls back to what it rests on when a call names no identifiable
|
|
361
|
+
* target at all — the model's own `op_requested` and `beyond_task` answers.
|
|
362
|
+
* Without this, the LENGTH of the human's paste decided the verdict: the
|
|
363
|
+
* same `rm` of the same file came out `allow` after "delete cache.sqlite"
|
|
364
|
+
* and `instruct` + every clear withdrawn after the same sentence inside a
|
|
365
|
+
* 12,000-character prompt. Set only from the HUMAN channel, which the agent
|
|
366
|
+
* cannot write (`evaluator.ts`), so padding is not a way to buy one: it can
|
|
367
|
+
* restore a clear a short prompt would have had, never create one.
|
|
368
|
+
*/
|
|
369
|
+
userSaidCut?: boolean;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* v1: the human's task decides, not the policy's wording.
|
|
374
|
+
*
|
|
375
|
+
* - A policy fires exactly as in v0 (every probe holds, no exemption).
|
|
376
|
+
* - Injection withdraws every clear and turns a fired policy into a block.
|
|
377
|
+
* - The human asked for THIS operation on THIS target (`op_requested`), the
|
|
378
|
+
* call reaches no further (`beyond_task`), and — when the call names
|
|
379
|
+
* targets — every one of them appears in what the human typed or in the
|
|
380
|
+
* agent proposal they replied to: the policy is cleared.
|
|
381
|
+
* - A shell command the local scan could not read whole is cleared and
|
|
382
|
+
* softened by neither route: see `ScannedCommand.complete` in `facts.ts`.
|
|
383
|
+
* - Otherwise, the call is a step toward the human's task (`task_step`) and
|
|
384
|
+
* reaches no further: a warn-level outcome is cleared and a block is
|
|
385
|
+
* softened to a warning. A goal never licenses a block on its own. On a
|
|
386
|
+
* shell command whose targets the human named only in part, this route
|
|
387
|
+
* does not apply either.
|
|
388
|
+
* - Policies with `userCanOverride: false` are never cleared or softened.
|
|
389
|
+
* - Nothing fired, but the call reaches beyond the task, is not a step toward
|
|
390
|
+
* it, and some "does it do X" probe is at least half-raised: warn.
|
|
391
|
+
*/
|
|
392
|
+
export function decideV1(
|
|
393
|
+
selected: ReadonlyArray<SemanticPolicy>,
|
|
394
|
+
answers: Readonly<Record<string, number>>,
|
|
395
|
+
toolInput: Record<string, unknown>,
|
|
396
|
+
userSaid: ReadonlyArray<string>,
|
|
397
|
+
agentLastMessage: string | null,
|
|
398
|
+
opts: DecideV1Options = {},
|
|
399
|
+
): SemanticVerdict {
|
|
400
|
+
const t = opts.thresholds ?? DEFAULT_THRESHOLDS_V1;
|
|
401
|
+
const flagBeyond = opts.flagBeyondTask ?? true;
|
|
402
|
+
const taskClears = opts.taskStepClears ?? true;
|
|
403
|
+
const userSaidCut = opts.userSaidCut ?? false;
|
|
404
|
+
const num = (id: string): number | null => (typeof answers[id] === "number" ? answers[id] : null);
|
|
405
|
+
const injection = num("injection");
|
|
406
|
+
const injected = injection !== null && injection >= t.injection;
|
|
407
|
+
const task = num("task_step");
|
|
408
|
+
const op = num("op_requested");
|
|
409
|
+
const beyond = num("beyond_task");
|
|
410
|
+
let scan: TargetScan | null = null;
|
|
411
|
+
const targetScan = (): TargetScan => (scan ??= scanTargets(toolInput));
|
|
412
|
+
const evidenceSaid = agentLastMessage ? [...userSaid, agentLastMessage] : userSaid;
|
|
413
|
+
const targetOk = (): { ok: boolean; named: boolean } => {
|
|
414
|
+
const s = targetScan();
|
|
415
|
+
if (s.groups.length === 0) return { ok: true, named: false };
|
|
416
|
+
const named = everyTargetNamed(s, evidenceSaid);
|
|
417
|
+
return { ok: named || userSaidCut, named };
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const outcomes: PolicyOutcome[] = selected.map((p) => {
|
|
421
|
+
const evidence = Math.min(...p.probes.map((probe) => answers[`${p.name}.${probe.id}`] ?? 0));
|
|
422
|
+
const exempt = p.exempt ? answers[`${p.name}.exempt`] ?? 0 : null;
|
|
423
|
+
const base: PolicyOutcome = {
|
|
424
|
+
policy: p.name,
|
|
425
|
+
mode: p.mode,
|
|
426
|
+
userCanOverride: p.userCanOverride,
|
|
427
|
+
...(p.origin ? { origin: p.origin } : {}),
|
|
428
|
+
evidence,
|
|
429
|
+
exempt,
|
|
430
|
+
userAsked: op,
|
|
431
|
+
targetNamedByUser: false,
|
|
432
|
+
escalatedByInjection: false,
|
|
433
|
+
verdict: "none",
|
|
434
|
+
};
|
|
435
|
+
if (evidence < t.fire || (exempt !== null && exempt >= t.exempt)) return base;
|
|
436
|
+
if (injected) return { ...base, escalatedByInjection: true, verdict: "deny" };
|
|
437
|
+
|
|
438
|
+
const fired: PolicyOutcome["verdict"] = p.mode === "deny" && evidence >= t.deny ? "deny" : "instruct";
|
|
439
|
+
if (!p.userCanOverride) return { ...base, verdict: fired };
|
|
440
|
+
// The shell scan may have missed a word bash would run (`$'…'`, a heredoc,
|
|
441
|
+
// `$(…)`, …): what the call touches is not known here, so neither intent
|
|
442
|
+
// route may clear or soften it. Jev's own deny or instruct stands.
|
|
443
|
+
if (!targetScan().complete) return { ...base, verdict: fired, targetScanIncomplete: true };
|
|
444
|
+
|
|
445
|
+
if (op !== null && op >= t.opRequested && beyond !== null && beyond < t.opBeyondMax) {
|
|
446
|
+
const target = targetOk();
|
|
447
|
+
if (target.ok) return { ...base, targetNamedByUser: target.named, verdict: "overridden", intent: "op-requested" };
|
|
448
|
+
}
|
|
449
|
+
// The task-step route reaches `combine.ts` as a clear too — a warning it
|
|
450
|
+
// leaves behind clears a reviewable regex deny. It is consent by GOAL, so
|
|
451
|
+
// a shell command whose targets the human never named still rides on it
|
|
452
|
+
// ("fix the failing tests" → `rm -rf node_modules`). But once the human
|
|
453
|
+
// HAS named targets, they drew the line: a call reaching past it is not
|
|
454
|
+
// softened. After "clean the build", `rm -rf build/ ~/important` is not.
|
|
455
|
+
if (
|
|
456
|
+
taskClears &&
|
|
457
|
+
task !== null &&
|
|
458
|
+
task >= t.taskStep &&
|
|
459
|
+
beyond !== null &&
|
|
460
|
+
beyond < t.taskBeyondMax &&
|
|
461
|
+
!(typeof toolInput.command === "string" && !userSaidCut && partlyNamed(targetScan(), evidenceSaid))
|
|
462
|
+
) {
|
|
463
|
+
if (fired === "instruct") return { ...base, verdict: "overridden", intent: "task-step" };
|
|
464
|
+
return { ...base, verdict: "instruct", intent: "downgraded-task-step" };
|
|
465
|
+
}
|
|
466
|
+
return { ...base, verdict: fired };
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const byName = new Map(selected.map((p) => [p.name, p]));
|
|
470
|
+
const denies = outcomes.filter((o) => o.verdict === "deny");
|
|
471
|
+
const instructs = outcomes.filter((o) => o.verdict === "instruct");
|
|
472
|
+
const overridden = outcomes.filter((o) => o.verdict === "overridden");
|
|
473
|
+
const common = { outcomes, injectionSuspected: injection, scopeWithinRequest: null };
|
|
474
|
+
const injectionNote = denies.some((o) => o.escalatedByInjection)
|
|
475
|
+
? " Blocked because the request also contains text addressed to the reviewer (claiming approval, safety or consent)."
|
|
476
|
+
: "";
|
|
477
|
+
|
|
478
|
+
if (denies.length > 0) {
|
|
479
|
+
const [first, ...rest] = denies;
|
|
480
|
+
const also = rest.length > 0 ? ` Also flagged: ${rest.map((o) => `semantic/${o.policy}`).join(", ")}.` : "";
|
|
481
|
+
return { decision: "deny", reason: formatOutcome(byName.get(first.policy)!, first) + also + injectionNote, ...common };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const maxAction = Math.max(0, ...selected.map((p) => answers[`${p.name}.${p.probes[0].id}`] ?? 0));
|
|
485
|
+
const beyondFlag =
|
|
486
|
+
flagBeyond &&
|
|
487
|
+
userSaid.length > 0 &&
|
|
488
|
+
beyond !== null &&
|
|
489
|
+
beyond >= t.beyondFlag &&
|
|
490
|
+
task !== null &&
|
|
491
|
+
task < t.beyondTaskStepMax &&
|
|
492
|
+
maxAction >= t.beyondActionMin;
|
|
493
|
+
|
|
494
|
+
if (instructs.length > 0 || beyondFlag) {
|
|
495
|
+
const lines = instructs.map((o) => formatOutcome(byName.get(o.policy)!, o));
|
|
496
|
+
if (beyondFlag) {
|
|
497
|
+
lines.push(
|
|
498
|
+
`Goes beyond what the user asked for (semantic/beyond-task, p=${beyond!.toFixed(2)}). ` +
|
|
499
|
+
"Do only what they asked, or confirm the wider change with them first.",
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return { decision: "instruct", reason: lines.join("\n") + injectionNote, ...common, beyondTask: beyondFlag };
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
decision: "allow",
|
|
506
|
+
reason:
|
|
507
|
+
overridden.length > 0
|
|
508
|
+
? `Allowed because it is what the user asked for: ${overridden.map((o) => `semantic/${o.policy}`).join(", ")}.`
|
|
509
|
+
: null,
|
|
510
|
+
...common,
|
|
511
|
+
beyondTask: false,
|
|
512
|
+
};
|
|
513
|
+
}
|