failproofai 1.0.11 → 1.0.12-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +23 -23
- 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/build-manifest.json +3 -3
- 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 +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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 +3 -3
- 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 +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +5 -6
- 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 +24 -24
- 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 +2 -2
- 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 +4 -5
- 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 +6 -7
- 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 +6 -7
- 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 +5 -6
- 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 +2 -2
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +4 -4
- 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 +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +7 -7
- 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/build-manifest.json +3 -3
- 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 +7 -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/build-manifest.json +3 -3
- 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 +8 -8
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +10 -10
- 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]__0qkp92tjfvw8r._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1x24dzcbbq6-g._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__02vous7mpi2xm._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__03i5o4a840guz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0__a_je8fsn65._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_hsnj_a2mvl_._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1izwqj_ij_9x_._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l-oi4gdtmlo5._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0swiyzbmvja2d._.js → [root-of-the-server]__0t9ufnv5hi2hq._.js} +5 -5
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wg9dk8x8bnau._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12kq3x-h7t7i1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1a5zkndjwlc13._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1r7kbfkiw2xmu._.js → [root-of-the-server]__1aaw59g-5uty7._.js} +12 -12
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hqpg6vgmfexv._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xva4ce_2._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1vslkoe35wok6._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1ixl-kxq_ctsa._.js → _1y590mjzpkbos._.js} +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01j58mdcdw0vn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_a8z2ldp2v3n._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__0b78b0j8iyv6l._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ffu2-38y03ic._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g8b0kyorya-t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__075uro8t92s0_._.js → [root-of-the-server]__0pwj2h2efe5f-._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zn1d4blhd3bu._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17r4zrwanhp6r._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1lnfv02ucky7m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mc9-zlnh656_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1s2-ck3k3rsoo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1l1qgu5mfapkh._.js → [root-of-the-server]__1vx7o8ukefudh._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgifqv1ssi._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _1a3tubds7s7ow._.js} +10 -9
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1f_ymmaonkksc.js → node_modules_html-to-image_es_index_1i8oid1gc7s1f.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +23 -23
- package/.next/standalone/.next/server/middleware.js +2 -2
- 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/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{0frxv4_m-m2u5.js → 063al_q720fkj.js} +1 -1
- package/.next/standalone/.next/static/chunks/0_zfad4co-t2z.js +1 -0
- package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +1 -0
- package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +1 -0
- package/.next/standalone/.next/static/chunks/{37stoe-_yd852.js → 18433qz-8njv3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1kwj8uqxuv85t.js → 1b32-afqacaen.js} +1 -1
- package/.next/standalone/.next/static/chunks/{44a7k2_62qc-9.js → 1p-hsi58i-6h0.js} +2 -2
- package/.next/standalone/.next/static/chunks/{302llgh97ce6w.js → 2-tf6_7u623kd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1_c0dc7zqj1n7.js → 24w3x2da9sk49.js} +1 -1
- package/.next/standalone/.next/static/chunks/2tjfp9dirxnku.js +1 -0
- package/.next/standalone/.next/static/chunks/{16z6_d7sp61jm.js → 3-pku9qwv1yfu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{15rm15u5a6dua.js → 42e8zab314654.js} +1 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +41 -0
- package/.next/standalone/fp-cloud-cli/README.md +42 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +117 -12
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +285 -19
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +170 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +259 -21
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +17 -9
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +692 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +6 -6
- package/.next/standalone/hermes-plugin/README.md +6 -4
- package/.next/standalone/hermes-plugin/__init__.py +16 -0
- package/.next/standalone/hermes-plugin/client.py +5 -1
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/api/context.js +72 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/api/diag.js +84 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/api/metrics.js +50 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/api/propagation.js +80 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/api/trace.js +70 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/baggage/context-helpers.js +52 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/baggage/internal/baggage-impl.js +44 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/baggage/internal/symbol.js +12 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/baggage/utils.js +41 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/context/NoopContextManager.js +27 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/context/context.js +52 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/context-api.js +16 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/diag/ComponentLogger.js +47 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/diag/consoleLogger.js +73 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/diag/internal/logLevelLogger.js +34 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/diag/types.js +33 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/diag-api.js +20 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/index.js +71 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/internal/global-utils.js +60 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/internal/semver.js +111 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/metrics/Metric.js +18 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/metrics/NoopMeter.js +118 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/metrics/NoopMeterProvider.js +20 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/metrics-api.js +17 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/propagation/NoopTextMapPropagator.js +23 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/propagation/TextMapPropagator.js +36 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/propagation-api.js +17 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/NonRecordingSpan.js +58 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/NoopTracer.js +68 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/NoopTracerProvider.js +21 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/ProxyTracer.js +46 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/ProxyTracerProvider.js +46 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/SamplingResult.js +33 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/context-utils.js +71 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/internal/tracestate-impl.js +94 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/internal/tracestate-validators.js +35 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/internal/utils.js +16 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/invalid-span-constants.js +25 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/span_kind.js +38 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/spancontext-utils.js +66 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/status.js +29 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace/trace_flags.js +18 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/trace-api.js +17 -0
- package/.next/standalone/node_modules/@opentelemetry/api/build/src/version.js +10 -0
- package/.next/standalone/node_modules/@opentelemetry/api/package.json +96 -0
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +2 -2
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +2 -2
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/app-render/async-local-storage.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/generate-agent-files.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/lib/router-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/trace/tracer.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/config.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +11 -11
- package/.next/standalone/package.json +11 -11
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/pyproject.toml +19 -0
- package/.next/standalone/sdk/python/uv.lock +520 -301
- package/.next/standalone/sdk/typescript/CHANGELOG.md +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +4 -13
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +2 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4 -13
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +3 -0
- package/.next/standalone/sdk/typescript/package-lock.json +178 -162
- package/.next/standalone/sdk/typescript/package.json +1 -1
- package/.next/standalone/sdk/typescript/src/version.ts +1 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +17 -1
- package/dist/cli.mjs +2866 -1124
- package/dist/worker.mjs +1450 -145
- package/hermes-plugin/README.md +6 -4
- package/hermes-plugin/__init__.py +16 -0
- package/hermes-plugin/client.py +5 -1
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -11
- package/src/hooks/agent-roster.ts +172 -0
- package/src/hooks/agent-targets.ts +60 -0
- package/src/hooks/cloud-connection.ts +97 -27
- package/src/hooks/cloud-enrollment-cli.ts +7 -4
- package/src/hooks/cloud-managed-policies.ts +380 -20
- package/src/hooks/cloud-policy-errors.ts +481 -0
- package/src/hooks/config-status.ts +1 -1
- package/src/hooks/custom-hooks-loader.ts +26 -3
- package/src/hooks/daemon-client.ts +9 -4
- package/src/hooks/daemon-service.ts +2 -1
- package/src/hooks/effective-reviewers.ts +22 -1
- package/src/hooks/fp-home.ts +6 -0
- package/src/hooks/handler.ts +235 -18
- package/src/hooks/health-probe.ts +15 -0
- package/src/hooks/integrations.ts +18 -11
- package/src/hooks/jev-cli.ts +197 -0
- package/src/hooks/jev-cloud-connection.ts +92 -0
- package/src/hooks/manager.ts +122 -16
- package/src/hooks/pack-failclosed.ts +2 -0
- package/src/hooks/pack-manifest.ts +8 -1
- package/src/hooks/policy-evaluator.ts +5 -1
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +21 -5
- package/src/hooks/semantic/cloud-jev-health.ts +227 -0
- package/src/hooks/semantic/cloud-jev.ts +677 -0
- package/src/hooks/semantic/jev-client.ts +18 -1
- package/src/hooks/semantic/jev-config.ts +173 -3
- package/src/hooks/semantic/jev-review.ts +129 -10
- package/src/hooks/semantic/pack-policies.ts +2 -1
- package/src/hooks/semantic/types.ts +8 -0
- package/src/hooks/session-pause-cli.ts +1 -1
- package/src/hooks/worker-server.ts +5 -1
- package/.next/standalone/.next/server/chunks/[externals]__0hjaw70fchx66._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07l68zv0hqdiu._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0car70pylmvor._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0u3i025_setk_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10a2f6s636yc7._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qt5u58xrcuk6._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_077n_4ip5__fu._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mqvvxcn6v0op._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0pb7ds5g0vzgn._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1gasm5xkeqmm3._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1q1heqmkhh8l4._.js +0 -3
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0888k4s6spfmh._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_bflu046ep5j._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_lbstb5x2t15._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0fdwgktjfhedo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s4deza787asx._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__18qe-hu0gwqm4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_p_9xe520fex._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fmhm8_s9i4re._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1g76awtnmimd1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pczevlpk5lxh._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0dc-of_bn0cim._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
- package/.next/standalone/.next/static/chunks/0m-tv1y6ghhtk.js +0 -1
- package/.next/standalone/.next/static/chunks/0ol2gj4ntfn56.js +0 -1
- package/.next/standalone/.next/static/chunks/3hj5_7i3nw4zj.js +0 -1
- package/.next/standalone/.next/static/chunks/3iyoapeoq_hup.js +0 -1
- /package/.next/standalone/.next/static/{kEFP3ybXwZdriNEQmWwlH → xq8UgF5yFxty1VWxY2hyn}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kEFP3ybXwZdriNEQmWwlH → xq8UgF5yFxty1VWxY2hyn}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{kEFP3ybXwZdriNEQmWwlH → xq8UgF5yFxty1VWxY2hyn}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FailproofAI Cloud Jev (CONTRACT C10): one tool call in, one verdict out, with
|
|
3
|
+
* the checks FailproofAI Cloud deployed to this machine decided ON CLOUD.
|
|
4
|
+
*
|
|
5
|
+
* Jev policies, and the Jev half of `both` policies, never reach the machine.
|
|
6
|
+
* When Cloud sets this machine's Jev mode to `observe` or `enforce`, every
|
|
7
|
+
* gated tool call is sent to `POST /enforcement/v1/jev/systemone` on the Cloud
|
|
8
|
+
* Jev credential as today's Jev request — the machine's own questions, which
|
|
9
|
+
* are the global intent questions (ALWAYS, even with no installed pack's check
|
|
10
|
+
* applying) plus its installed packs' per-policy questions — and a `cloud`
|
|
11
|
+
* block of tool-call metadata (`buildCloudBlock`). Cloud strips the block,
|
|
12
|
+
* adds this machine's deployed Cloud checks, asks TypeSafe once, decides its
|
|
13
|
+
* own checks with a port of `decide` / `decideV1`, and replies with every
|
|
14
|
+
* answer plus `cloud.verdict`.
|
|
15
|
+
*
|
|
16
|
+
* Here the machine decides its pack checks from the answers exactly as the
|
|
17
|
+
* local path does (minus any group Cloud dropped, `droppedLocal`) and merges
|
|
18
|
+
* the two verdicts (`mergeCloudVerdict`). The merged verdict then goes to
|
|
19
|
+
* `combine.ts` unchanged, via `toReview` in `jev-review.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Never throws, and never guesses: anything short of a complete, well-formed
|
|
22
|
+
* reply — no transport, timeout, HTTP error, a reply without a valid `cloud`
|
|
23
|
+
* block, a missing answer — is `degraded`, and the combine keeps the regex
|
|
24
|
+
* result for that call (a `both` policy's regex half stays hard).
|
|
25
|
+
*/
|
|
26
|
+
import { DEFAULT_JEV_MODEL, MAX_REQUEST_CHARS } from "./compile";
|
|
27
|
+
import { DEFAULT_THRESHOLDS, decide, decideV1, everyTargetNamed, partlyNamed, scanTargets, type TargetScan } from "./decide";
|
|
28
|
+
import { redactSecrets } from "./envelope";
|
|
29
|
+
import { classifyTool } from "./facts";
|
|
30
|
+
import { DEFAULT_JEV_TIMEOUT_MS, prepareSemantic, type PreparedCall, type SemanticOptions, type SemanticOutcome } from "./evaluator";
|
|
31
|
+
import { JevError, readAnswers, type JevTransport } from "./jev-client";
|
|
32
|
+
import type { JevProviderKind } from "./jev-config";
|
|
33
|
+
import { INJECTION_PROBE, SCOPE_PROBE, TASK_PROBES } from "./policies";
|
|
34
|
+
import { redactSecretsDetailed } from "./redact";
|
|
35
|
+
import type {
|
|
36
|
+
Facts,
|
|
37
|
+
IntentMode,
|
|
38
|
+
JevRequest,
|
|
39
|
+
JevResponse,
|
|
40
|
+
NoulQuestion,
|
|
41
|
+
PolicyOutcome,
|
|
42
|
+
SemanticInput,
|
|
43
|
+
SemanticPolicy,
|
|
44
|
+
SemanticVerdict,
|
|
45
|
+
} from "./types";
|
|
46
|
+
import type { AgentIdentity } from "../agent-targets";
|
|
47
|
+
|
|
48
|
+
/** The `cloud` block's version (CONTRACT C10.3). */
|
|
49
|
+
export const CLOUD_BLOCK_VERSION = 1;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How many characters of `targetScan.groups` (serialized) are sent. The one
|
|
53
|
+
* part of the block that grows with the tool input: an MCP call with thousands
|
|
54
|
+
* of string fields, or a huge command whose scan fell back to its raw words.
|
|
55
|
+
* Past it a prefix of the groups goes with `complete: false`, which is strictly
|
|
56
|
+
* safer — an incomplete scan clears nothing (`decide.ts`).
|
|
57
|
+
*/
|
|
58
|
+
export const MAX_TARGET_SCAN_CHARS = 64_000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The block's serialized UTF-8 bytes may not exceed this: Cloud answers a
|
|
62
|
+
* larger one with a 400 (256 KiB there), and a refused call is a fallback.
|
|
63
|
+
* Kept a little under Cloud's cap. The rest of the block is bounded by the
|
|
64
|
+
* envelope's own caps (three human turns of at most `MAX_USER_MESSAGE_CHARS`,
|
|
65
|
+
* one capped agent message, the capped facts); if it still does not fit, the
|
|
66
|
+
* human's turns are left out — the stricter direction, since no consent can
|
|
67
|
+
* then be read.
|
|
68
|
+
*/
|
|
69
|
+
export const MAX_CLOUD_BLOCK_BYTES = 240_000;
|
|
70
|
+
|
|
71
|
+
const blockBytes = (block: CloudJevBlock): number => Buffer.byteLength(JSON.stringify(block), "utf8");
|
|
72
|
+
|
|
73
|
+
/** What a secret word in a target group becomes: no human's words can contain it. */
|
|
74
|
+
export const REDACTED_TARGET_WORD = "\u0000redacted";
|
|
75
|
+
|
|
76
|
+
/** The `cloud` block of a request (CONTRACT C10.3). */
|
|
77
|
+
export interface CloudJevBlock {
|
|
78
|
+
v: 1 | 2;
|
|
79
|
+
machineId: string;
|
|
80
|
+
agent?: AgentIdentity;
|
|
81
|
+
intentMode: IntentMode;
|
|
82
|
+
facts: Facts;
|
|
83
|
+
targetScan: { groups: string[][]; complete: boolean };
|
|
84
|
+
userSaid: string[];
|
|
85
|
+
agentLastMessage: string | null;
|
|
86
|
+
userSaidCut: boolean;
|
|
87
|
+
/** The machine's own per-policy question groups, in the order Cloud drops them from (last first). */
|
|
88
|
+
localPolicies: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** FailproofAI Cloud's reply block, validated. Its outcomes carry a pack-shaped `cloud:<id>` origin. */
|
|
92
|
+
export interface CloudJevReply {
|
|
93
|
+
verdict: SemanticVerdict;
|
|
94
|
+
asked: string[];
|
|
95
|
+
droppedLocal: string[];
|
|
96
|
+
droppedCloud: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What the Cloud review adds to a `SemanticOutcome`, for the verdict log and the budget report. */
|
|
100
|
+
export interface CloudReplySummary {
|
|
101
|
+
asked: string[];
|
|
102
|
+
/** The machine's own groups Cloud dropped, with the pack that declared each (null when none did). */
|
|
103
|
+
droppedLocal: Array<{ name: string; packId: string | null }>;
|
|
104
|
+
droppedCloud: string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type CloudSemanticOutcome =
|
|
108
|
+
| (Extract<SemanticOutcome, { status: "ok" }> & { cloud: CloudReplySummary })
|
|
109
|
+
| Extract<SemanticOutcome, { status: "degraded" }>;
|
|
110
|
+
|
|
111
|
+
export interface CloudSemanticOptions extends SemanticOptions {
|
|
112
|
+
/** This machine's FailproofAI Cloud machine id, from `credentials.json`. */
|
|
113
|
+
machineId: string;
|
|
114
|
+
/** Explicit null = v2 request with unresolved identity (no scoped checks). */
|
|
115
|
+
agent?: AgentIdentity | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── The request ──────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function noul(probe: { instructions: string; criteria?: { true: string; false: string } }): NoulQuestion {
|
|
121
|
+
return {
|
|
122
|
+
type: "noul",
|
|
123
|
+
instructions: probe.instructions,
|
|
124
|
+
...(probe.criteria ? { criteria: { ...probe.criteria } } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The global intent questions a Cloud call ALWAYS carries, whatever the
|
|
130
|
+
* machine's own checks are — Cloud's checks are decided on them:
|
|
131
|
+
*
|
|
132
|
+
* - `injection`, always (`compile.ts` asks it on every call it sends);
|
|
133
|
+
* - v1: the three task probes whenever a human message was recorded, as
|
|
134
|
+
* `compileRequest` asks them;
|
|
135
|
+
* - v0: `scope` whenever a human message was recorded. `compileRequest` asks
|
|
136
|
+
* it only when a SELECTED policy may be overridden, and the machine cannot
|
|
137
|
+
* know whether a Cloud check may be — without it no Cloud check could ever
|
|
138
|
+
* be cleared in v0. `decide` reads `scope` only inside the override branch,
|
|
139
|
+
* so the extra question changes no local verdict.
|
|
140
|
+
*/
|
|
141
|
+
export function cloudGlobalQuestions(intent: IntentMode, userSaidCount: number): Record<string, NoulQuestion> {
|
|
142
|
+
const questions: Record<string, NoulQuestion> = { [INJECTION_PROBE.id]: noul(INJECTION_PROBE) };
|
|
143
|
+
if (userSaidCount > 0) {
|
|
144
|
+
if (intent === "v1") for (const probe of TASK_PROBES) questions[probe.id] = noul(probe);
|
|
145
|
+
else questions[SCOPE_PROBE.id] = noul(SCOPE_PROBE);
|
|
146
|
+
}
|
|
147
|
+
return questions;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* How much of the tool input's text is searched for secrets. The groups sent
|
|
152
|
+
* are cut at {@link MAX_TARGET_SCAN_CHARS} and come from the text in order
|
|
153
|
+
* (the shell scan itself reads far less), so this covers every word that can
|
|
154
|
+
* be sent, while a 1 MB heredoc does not cost the hook a second full pass.
|
|
155
|
+
*/
|
|
156
|
+
const MAX_SECRET_SCAN_CHARS = 4 * MAX_TARGET_SCAN_CHARS;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Secret-shaped strings in the text `scanTargets` reads, lowercased like its
|
|
160
|
+
* words: the command, or — for any other tool — the string values it takes a
|
|
161
|
+
* target from (the same ≤300-character, non-content fields).
|
|
162
|
+
*/
|
|
163
|
+
function secretWords(toolInput: Record<string, unknown>): string[] {
|
|
164
|
+
const text = (
|
|
165
|
+
typeof toolInput.command === "string"
|
|
166
|
+
? toolInput.command
|
|
167
|
+
: Object.entries(toolInput)
|
|
168
|
+
.filter(([key, v]) => typeof v === "string" && v.length <= 300 && !/content|old_string|new_string|body|text|prompt/i.test(key))
|
|
169
|
+
.map(([, v]) => v as string)
|
|
170
|
+
.join("\n")
|
|
171
|
+
).slice(0, MAX_SECRET_SCAN_CHARS);
|
|
172
|
+
try {
|
|
173
|
+
return redactSecretsDetailed(text, { blunt: false })
|
|
174
|
+
.found.map((s) => s.toLowerCase())
|
|
175
|
+
.filter((s) => s.length > 0);
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `scanTargets(toolInput)` as the `cloud` block carries it: each group's Set as
|
|
183
|
+
* a sorted array, `targets` left out (it is the groups' union).
|
|
184
|
+
*
|
|
185
|
+
* Two changes, both in the stricter direction only:
|
|
186
|
+
*
|
|
187
|
+
* - A word that is (part of) a secret the narrow redactor finds in the tool
|
|
188
|
+
* input is replaced by {@link REDACTED_TARGET_WORD}, which no human's words
|
|
189
|
+
* can contain. The envelope already keeps that secret out of what Jev reads;
|
|
190
|
+
* this keeps it out of the metadata too. The group is kept, so the call still
|
|
191
|
+
* names as many targets — one the human can no longer be matched against.
|
|
192
|
+
* - Past {@link MAX_TARGET_SCAN_CHARS}, a prefix of the groups with
|
|
193
|
+
* `complete: false`: an incomplete scan clears nothing.
|
|
194
|
+
*/
|
|
195
|
+
export function cloudTargetScan(toolInput: Record<string, unknown>, scan: TargetScan = scanTargets(toolInput)): CloudJevBlock["targetScan"] {
|
|
196
|
+
const secrets = scan.groups.length > 0 ? secretWords(toolInput) : [];
|
|
197
|
+
const hidden = (word: string): boolean =>
|
|
198
|
+
secrets.some((s) => s === word || (word.length >= 8 && s.includes(word)) || (s.length >= 8 && word.includes(s)));
|
|
199
|
+
const groups: string[][] = [];
|
|
200
|
+
let chars = 2;
|
|
201
|
+
let complete = scan.complete;
|
|
202
|
+
for (const group of scan.groups) {
|
|
203
|
+
const words = [...new Set([...group].map((w) => (hidden(w) ? REDACTED_TARGET_WORD : w)))].sort();
|
|
204
|
+
const cost = JSON.stringify(words).length + 1;
|
|
205
|
+
if (chars + cost > MAX_TARGET_SCAN_CHARS) {
|
|
206
|
+
complete = false;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
chars += cost;
|
|
210
|
+
groups.push(words);
|
|
211
|
+
}
|
|
212
|
+
return { groups, complete };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The machine's facts with their strings passed through the narrow redactor, as the envelope sends them. */
|
|
216
|
+
function cloudFacts(facts: Facts): Facts {
|
|
217
|
+
const scrub = (s: string | null): string | null => (s === null ? null : redactSecrets(s, { blunt: false }).text);
|
|
218
|
+
return {
|
|
219
|
+
toolName: facts.toolName,
|
|
220
|
+
toolClass: facts.toolClass,
|
|
221
|
+
toolIsKnown: facts.toolIsKnown,
|
|
222
|
+
cwd: scrub(facts.cwd),
|
|
223
|
+
projectRoot: scrub(facts.projectRoot),
|
|
224
|
+
currentGitBranch: scrub(facts.currentGitBranch),
|
|
225
|
+
paths: facts.paths.map((p) => ({ asWritten: scrub(p.asWritten) ?? "", resolved: scrub(p.resolved) ?? "", relation: p.relation })),
|
|
226
|
+
permissionMode: facts.permissionMode,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The `cloud` block for a prepared call (CONTRACT C10.3): what the local
|
|
232
|
+
* decider reads, so Cloud decides its checks on the same evidence.
|
|
233
|
+
*
|
|
234
|
+
* - `facts`: the machine's `Facts`, strings redacted as the envelope's are.
|
|
235
|
+
* - `targetScan`: {@link cloudTargetScan}.
|
|
236
|
+
* - `userSaid`: the human turns the envelope CARRIES, uncut (`prepared.userSaid`,
|
|
237
|
+
* cleaned in v1), each with secrets redacted by the rules the envelope uses
|
|
238
|
+
* for everything that leaves the machine (`blunt`), and each redaction marker
|
|
239
|
+
* then replaced by {@link REDACTED_SAID}, which has no letters: a marker's
|
|
240
|
+
* label ("GitHub personal access token", "assigned secret") would otherwise
|
|
241
|
+
* NAME a target the human never named.
|
|
242
|
+
* - `agentLastMessage`: the string the envelope SENT (v1), null in v0.
|
|
243
|
+
* - `userSaidCut`, `intentMode`: as the local decider takes them.
|
|
244
|
+
* - `localPolicies`: the names of this call's own per-policy question groups.
|
|
245
|
+
*
|
|
246
|
+
* Redaction changes the words Cloud's target checks compare, and a substring
|
|
247
|
+
* check can flip either way (a hidden target word no longer matches; a hidden
|
|
248
|
+
* human word no longer names one). So the checks Cloud runs on the block are
|
|
249
|
+
* run here on both the block and the unredacted data ({@link namingReadings});
|
|
250
|
+
* where they differ, the scan is sent `complete: false`, which withholds every
|
|
251
|
+
* clear and every softening of an overridable check — Cloud is then stricter
|
|
252
|
+
* than the local decider, never laxer.
|
|
253
|
+
*/
|
|
254
|
+
export function buildCloudBlock(prepared: PreparedCall, input: SemanticInput, machineId: string, agent?: AgentIdentity | null): CloudJevBlock {
|
|
255
|
+
const scan = scanTargets(input.toolInput);
|
|
256
|
+
const block: CloudJevBlock = {
|
|
257
|
+
v: agent === undefined ? CLOUD_BLOCK_VERSION : 2,
|
|
258
|
+
machineId,
|
|
259
|
+
...(agent ? { agent } : {}),
|
|
260
|
+
intentMode: prepared.intent,
|
|
261
|
+
facts: cloudFacts(prepared.facts),
|
|
262
|
+
targetScan: cloudTargetScan(input.toolInput, scan),
|
|
263
|
+
userSaid: prepared.userSaid.map((turn) => redactSecrets(turn, { blunt: true }).text.replace(REDACTION_MARKER, REDACTED_SAID)),
|
|
264
|
+
agentLastMessage: prepared.agentLastMessage,
|
|
265
|
+
userSaidCut: prepared.userSaidCut,
|
|
266
|
+
localPolicies: prepared.selected.map((p) => p.name),
|
|
267
|
+
};
|
|
268
|
+
// Bounded by the caps above in practice; this is the floor under them. The
|
|
269
|
+
// human's turns go first, then the target scan; each time the scan is marked
|
|
270
|
+
// incomplete, which clears nothing. `[""]`, not `[]`: it names nothing, but
|
|
271
|
+
// it still says a human spoke, so Cloud's beyond-the-task flag (which needs
|
|
272
|
+
// one) is not switched off by the size of the call.
|
|
273
|
+
if (blockBytes(block) > MAX_CLOUD_BLOCK_BYTES) {
|
|
274
|
+
block.userSaid = prepared.userSaid.length > 0 ? [""] : [];
|
|
275
|
+
block.userSaidCut = false;
|
|
276
|
+
block.targetScan = { ...block.targetScan, complete: false };
|
|
277
|
+
}
|
|
278
|
+
if (blockBytes(block) > MAX_CLOUD_BLOCK_BYTES) {
|
|
279
|
+
block.targetScan = { groups: [], complete: false };
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
block.targetScan.complete &&
|
|
283
|
+
namingReadings(block.intentMode, scan.groups, prepared.userSaid, prepared.agentLastMessage) !==
|
|
284
|
+
namingReadings(
|
|
285
|
+
block.intentMode,
|
|
286
|
+
block.targetScan.groups.map((g) => new Set(g)),
|
|
287
|
+
block.userSaid,
|
|
288
|
+
block.agentLastMessage,
|
|
289
|
+
)
|
|
290
|
+
) {
|
|
291
|
+
block.targetScan = { ...block.targetScan, complete: false };
|
|
292
|
+
}
|
|
293
|
+
return block;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A redaction marker (`<redacted:label>`, `redact.ts`) in the human's words. */
|
|
297
|
+
const REDACTION_MARKER = /<redacted:[^>]*>/g;
|
|
298
|
+
|
|
299
|
+
/** What a redaction marker in `userSaid` becomes: no letters, so it names no target. */
|
|
300
|
+
export const REDACTED_SAID = "\u0000";
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Everything Cloud's port of `decide` / `decideV1` reads from the target groups
|
|
304
|
+
* and the human's words, as one comparable string: the group count, whether a
|
|
305
|
+
* human spoke, `everyTargetNamed`, and (v1) `partlyNamed` — over the human's
|
|
306
|
+
* words plus, in v1, the agent message they replied to, exactly as the deciders
|
|
307
|
+
* build their evidence.
|
|
308
|
+
*/
|
|
309
|
+
function namingReadings(
|
|
310
|
+
intent: IntentMode,
|
|
311
|
+
groups: Set<string>[],
|
|
312
|
+
userSaid: ReadonlyArray<string>,
|
|
313
|
+
agentLastMessage: string | null,
|
|
314
|
+
): string {
|
|
315
|
+
const scan: TargetScan = { targets: new Set(), groups, complete: true };
|
|
316
|
+
const said = intent === "v1" && agentLastMessage ? [...userSaid, agentLastMessage] : userSaid;
|
|
317
|
+
return [groups.length, userSaid.length > 0, everyTargetNamed(scan, said), intent === "v1" && partlyNamed(scan, said)].join();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The request sent to FailproofAI Cloud: today's, the global questions always, and the block. */
|
|
321
|
+
export function buildCloudRequest(prepared: PreparedCall, input: SemanticInput, machineId: string, agent?: AgentIdentity | null): JevRequest {
|
|
322
|
+
const own = prepared.compiled.request;
|
|
323
|
+
return {
|
|
324
|
+
model: own.model,
|
|
325
|
+
state: own.state,
|
|
326
|
+
questions: { ...own.questions, ...cloudGlobalQuestions(prepared.intent, prepared.userSaid.length) },
|
|
327
|
+
cloud: buildCloudBlock(prepared, input, machineId, agent) as unknown as Record<string, unknown>,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── The reply ────────────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
/** A Cloud policy id, as `cloud-managed-policies.ts` accepts one. */
|
|
334
|
+
const POLICY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
335
|
+
/** Longest check name accepted back; declarations are far shorter. */
|
|
336
|
+
const MAX_NAME_CHARS = 256;
|
|
337
|
+
/** Most outcomes / names accepted back: 24 declarations per policy leaves this far out of reach. */
|
|
338
|
+
const MAX_LIST = 4096;
|
|
339
|
+
/**
|
|
340
|
+
* Longest Cloud reason kept (review n8). Cloud builds it from bounded titles
|
|
341
|
+
* and guidance, so this is never reached by a working server; past it the
|
|
342
|
+
* reason is cut rather than refused, since refusing would drop Cloud's verdict.
|
|
343
|
+
*/
|
|
344
|
+
export const MAX_CLOUD_REASON_CHARS = 8_192;
|
|
345
|
+
|
|
346
|
+
function malformed(what: string): never {
|
|
347
|
+
throw new JevError("malformed", `FailproofAI Cloud's reply: ${what}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const isProbability = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1;
|
|
351
|
+
const isName = (v: unknown): v is string => typeof v === "string" && v.length > 0 && v.length <= MAX_NAME_CHARS;
|
|
352
|
+
|
|
353
|
+
function nullableProbability(v: unknown, what: string): number | null {
|
|
354
|
+
if (v === null || v === undefined) return null;
|
|
355
|
+
if (!isProbability(v)) malformed(`${what} is not a probability`);
|
|
356
|
+
return v;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function names(v: unknown, what: string): string[] {
|
|
360
|
+
if (v === undefined || v === null) return [];
|
|
361
|
+
if (!Array.isArray(v) || v.length > MAX_LIST || !v.every(isName)) malformed(`${what} is not a list of names`);
|
|
362
|
+
return [...v];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const DECISIONS = new Set(["allow", "deny", "instruct"]);
|
|
366
|
+
const VERDICTS = new Set(["deny", "instruct", "overridden", "none"]);
|
|
367
|
+
const INTENTS = new Set(["op-requested", "task-step", "downgraded-task-step"]);
|
|
368
|
+
|
|
369
|
+
/** One Cloud outcome, rebuilt field by field so nothing but these fields crosses over. */
|
|
370
|
+
function parseOutcome(raw: unknown, i: number): PolicyOutcome {
|
|
371
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) malformed(`outcome ${i} is not an object`);
|
|
372
|
+
const o = raw as Record<string, unknown>;
|
|
373
|
+
if (!isName(o.policy)) malformed(`outcome ${i} has no policy name`);
|
|
374
|
+
if (o.mode !== "deny" && o.mode !== "instruct") malformed(`outcome ${i} has no mode`);
|
|
375
|
+
if (!isProbability(o.evidence)) malformed(`outcome ${i} has no evidence`);
|
|
376
|
+
if (typeof o.targetNamedByUser !== "boolean" || typeof o.escalatedByInjection !== "boolean") {
|
|
377
|
+
malformed(`outcome ${i} is missing a flag`);
|
|
378
|
+
}
|
|
379
|
+
if (typeof o.verdict !== "string" || !VERDICTS.has(o.verdict)) malformed(`outcome ${i} has no verdict`);
|
|
380
|
+
if (o.intent !== undefined && (typeof o.intent !== "string" || !INTENTS.has(o.intent))) malformed(`outcome ${i} has an unknown intent`);
|
|
381
|
+
if (o.targetScanIncomplete !== undefined && o.targetScanIncomplete !== true) malformed(`outcome ${i} has a bad targetScanIncomplete`);
|
|
382
|
+
if (o.userCanOverride !== undefined && typeof o.userCanOverride !== "boolean") malformed(`outcome ${i} has a bad userCanOverride`);
|
|
383
|
+
const origin = o.origin as Record<string, unknown> | undefined;
|
|
384
|
+
if (
|
|
385
|
+
!origin ||
|
|
386
|
+
typeof origin !== "object" ||
|
|
387
|
+
typeof origin.cloudPolicyId !== "string" ||
|
|
388
|
+
!POLICY_ID_RE.test(origin.cloudPolicyId) ||
|
|
389
|
+
!Number.isSafeInteger(origin.cloudVersion) ||
|
|
390
|
+
(origin.cloudVersion as number) < 0
|
|
391
|
+
) {
|
|
392
|
+
malformed(`outcome ${i} has no Cloud origin`);
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
policy: o.policy,
|
|
396
|
+
mode: o.mode,
|
|
397
|
+
...(typeof o.userCanOverride === "boolean" ? { userCanOverride: o.userCanOverride } : {}),
|
|
398
|
+
// Pack-shaped, so the attribution the hook path already has files it as
|
|
399
|
+
// `cloudPolicyId`/`cloudVersion` (`jevAttribution` in `handler.ts`), and the
|
|
400
|
+
// review keys it as that policy's own reviewer (`cloudReviewerName`).
|
|
401
|
+
origin: { packId: `cloud:${origin.cloudPolicyId}`, packVersion: String(origin.cloudVersion) },
|
|
402
|
+
evidence: o.evidence,
|
|
403
|
+
exempt: nullableProbability(o.exempt, `outcome ${i} exempt`),
|
|
404
|
+
userAsked: nullableProbability(o.userAsked, `outcome ${i} userAsked`),
|
|
405
|
+
targetNamedByUser: o.targetNamedByUser as boolean,
|
|
406
|
+
escalatedByInjection: o.escalatedByInjection as boolean,
|
|
407
|
+
verdict: o.verdict as PolicyOutcome["verdict"],
|
|
408
|
+
...(o.intent !== undefined ? { intent: o.intent as PolicyOutcome["intent"] } : {}),
|
|
409
|
+
...(o.targetScanIncomplete === true ? { targetScanIncomplete: true as const } : {}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The reply's `cloud` block, validated and rebuilt, or a `malformed` JevError.
|
|
415
|
+
* A reply without one (a server that does not implement C10) is malformed:
|
|
416
|
+
* nothing it says can be told apart from "Cloud has no checks here".
|
|
417
|
+
*/
|
|
418
|
+
export function parseCloudReply(raw: unknown): CloudJevReply {
|
|
419
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) malformed("no cloud block");
|
|
420
|
+
const block = raw as Record<string, unknown>;
|
|
421
|
+
const v = block.verdict as Record<string, unknown> | undefined;
|
|
422
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) malformed("no verdict");
|
|
423
|
+
if (typeof v.decision !== "string" || !DECISIONS.has(v.decision)) malformed("verdict has no decision");
|
|
424
|
+
if (v.reason !== null && v.reason !== undefined && typeof v.reason !== "string") malformed("verdict reason is not text");
|
|
425
|
+
if (!Array.isArray(v.outcomes) || v.outcomes.length > MAX_LIST) malformed("verdict has no outcomes list");
|
|
426
|
+
if (v.beyondTask !== undefined && typeof v.beyondTask !== "boolean") malformed("verdict beyondTask is not a flag");
|
|
427
|
+
const outcomes = v.outcomes.map(parseOutcome);
|
|
428
|
+
const asked = names(block.asked, "asked");
|
|
429
|
+
// Each outcome must answer a check Cloud says it ASKED, once. The review
|
|
430
|
+
// clears a `both` policy's regex half from these outcomes, so one that
|
|
431
|
+
// `asked` does not vouch for — `asked: []` beside a fabricated `none` — is a
|
|
432
|
+
// contradiction, not a clear: the whole reply is refused, and the call is
|
|
433
|
+
// decided by the regex alone (security review of #873).
|
|
434
|
+
const askedSet = new Set(asked);
|
|
435
|
+
const seen = new Set<string>();
|
|
436
|
+
outcomes.forEach((o, i) => {
|
|
437
|
+
if (!askedSet.has(o.policy)) malformed(`outcome ${i} answers ${JSON.stringify(o.policy)}, which Cloud did not ask`);
|
|
438
|
+
const key = `${o.origin?.packId ?? ""}\u0000${o.policy}`;
|
|
439
|
+
if (seen.has(key)) malformed(`outcome ${i} answers ${JSON.stringify(o.policy)} twice`);
|
|
440
|
+
seen.add(key);
|
|
441
|
+
});
|
|
442
|
+
// A check that fired deny makes the verdict a deny; a reply that says
|
|
443
|
+
// otherwise contradicts itself.
|
|
444
|
+
if (outcomes.some((o) => o.verdict === "deny") && v.decision !== "deny") malformed("a check denied but the verdict does not");
|
|
445
|
+
return {
|
|
446
|
+
verdict: {
|
|
447
|
+
decision: v.decision as SemanticVerdict["decision"],
|
|
448
|
+
reason:
|
|
449
|
+
typeof v.reason === "string"
|
|
450
|
+
? v.reason.length > MAX_CLOUD_REASON_CHARS
|
|
451
|
+
? `${v.reason.slice(0, MAX_CLOUD_REASON_CHARS - 1)}…`
|
|
452
|
+
: v.reason
|
|
453
|
+
: null,
|
|
454
|
+
outcomes,
|
|
455
|
+
injectionSuspected: nullableProbability(v.injectionSuspected, "injectionSuspected"),
|
|
456
|
+
scopeWithinRequest: nullableProbability(v.scopeWithinRequest, "scopeWithinRequest"),
|
|
457
|
+
beyondTask: v.beyondTask === true,
|
|
458
|
+
},
|
|
459
|
+
asked,
|
|
460
|
+
droppedLocal: names(block.droppedLocal, "droppedLocal"),
|
|
461
|
+
droppedCloud: names(block.droppedCloud, "droppedCloud"),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── The merge (CONTRACT C10.5) ───────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
const RANK: Record<SemanticVerdict["decision"], number> = { allow: 0, instruct: 1, deny: 2 };
|
|
468
|
+
|
|
469
|
+
/** Lines of the given reasons, first seen first, each once. */
|
|
470
|
+
function joinLines(reasons: ReadonlyArray<string | null>): string | null {
|
|
471
|
+
const lines: string[] = [];
|
|
472
|
+
for (const reason of reasons) {
|
|
473
|
+
if (!reason) continue;
|
|
474
|
+
for (const line of reason.split("\n")) if (!lines.includes(line)) lines.push(line);
|
|
475
|
+
}
|
|
476
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Cloud's verdict and the machine's own, as ONE verdict for `combine.ts`:
|
|
481
|
+
*
|
|
482
|
+
* - outcomes: Cloud's first, then the machine's;
|
|
483
|
+
* - decision: the most severe (deny > instruct > allow);
|
|
484
|
+
* - reason: the deny's (Cloud's first) for a deny; every warning line (Cloud's
|
|
485
|
+
* first, each once — a beyond-the-task line both sides raised is one line)
|
|
486
|
+
* for an instruct; the allow notes (Cloud's first) for an allow;
|
|
487
|
+
* - `beyondTask`: either side's; `injectionSuspected` / `scopeWithinRequest`:
|
|
488
|
+
* the machine's own reading of the shared answer, else Cloud's.
|
|
489
|
+
*
|
|
490
|
+
* Pure.
|
|
491
|
+
*/
|
|
492
|
+
export function mergeCloudVerdict(cloud: SemanticVerdict, local: SemanticVerdict): SemanticVerdict {
|
|
493
|
+
const decision = RANK[cloud.decision] >= RANK[local.decision] ? cloud.decision : local.decision;
|
|
494
|
+
let reason: string | null;
|
|
495
|
+
if (decision === "deny") {
|
|
496
|
+
reason = cloud.decision === "deny" ? cloud.reason ?? local.reason : local.reason;
|
|
497
|
+
} else if (decision === "instruct") {
|
|
498
|
+
reason = joinLines([cloud, local].filter((v) => v.decision === "instruct").map((v) => v.reason));
|
|
499
|
+
} else {
|
|
500
|
+
reason = joinLines([cloud.reason, local.reason]);
|
|
501
|
+
}
|
|
502
|
+
const beyond = cloud.beyondTask === undefined && local.beyondTask === undefined ? undefined : cloud.beyondTask === true || local.beyondTask === true;
|
|
503
|
+
return {
|
|
504
|
+
decision,
|
|
505
|
+
reason,
|
|
506
|
+
outcomes: [...cloud.outcomes, ...local.outcomes],
|
|
507
|
+
injectionSuspected: local.injectionSuspected ?? cloud.injectionSuspected,
|
|
508
|
+
scopeWithinRequest: local.scopeWithinRequest ?? cloud.scopeWithinRequest,
|
|
509
|
+
...(beyond !== undefined ? { beyondTask: beyond } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── The call ─────────────────────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
/** An allow with nothing decided: Cloud's short-circuit verdict. */
|
|
516
|
+
const EMPTY_ALLOW: SemanticVerdict = {
|
|
517
|
+
decision: "allow",
|
|
518
|
+
reason: null,
|
|
519
|
+
outcomes: [],
|
|
520
|
+
injectionSuspected: null,
|
|
521
|
+
scopeWithinRequest: null,
|
|
522
|
+
beyondTask: false,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
function envNumber(name: string, fallback: number): number {
|
|
526
|
+
const n = Number(process.env[name]);
|
|
527
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** The answers the machine needs back: its own questions, minus the groups Cloud dropped. */
|
|
531
|
+
function expectedQuestions(request: JevRequest, prepared: PreparedCall, dropped: ReadonlySet<string>): JevRequest {
|
|
532
|
+
if (dropped.size === 0) return request;
|
|
533
|
+
const questions = Object.fromEntries(
|
|
534
|
+
Object.entries(request.questions).filter(([id]) => {
|
|
535
|
+
const owner = prepared.compiled.owners.get(id);
|
|
536
|
+
return !(typeof owner === "string" && dropped.has(owner));
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
return { ...request, questions };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Whatever answers a short-circuit reply carries, read leniently: none is required. */
|
|
543
|
+
function presentAnswers(response: JevResponse): Record<string, number> {
|
|
544
|
+
const out: Record<string, number> = {};
|
|
545
|
+
const answers = response.answers && typeof response.answers === "object" ? response.answers : {};
|
|
546
|
+
for (const [id, a] of Object.entries(answers)) {
|
|
547
|
+
const p = (a as { noul?: unknown } | null)?.noul;
|
|
548
|
+
if (isProbability(p)) out[id] = p;
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Ask FailproofAI Cloud about one call and return the merged verdict. Mirrors
|
|
555
|
+
* `evaluateSemantic` in everything but where the verdict comes from.
|
|
556
|
+
*/
|
|
557
|
+
export async function evaluateCloudSemantic(input: SemanticInput, opts: CloudSemanticOptions): Promise<CloudSemanticOutcome> {
|
|
558
|
+
const started = performance.now();
|
|
559
|
+
const elapsed = () => Math.round(performance.now() - started);
|
|
560
|
+
// A known tool with no side effects (TodoWrite, Task, …) selects no check
|
|
561
|
+
// anywhere — `selectPolicies` answers `[]` for it before reading a single
|
|
562
|
+
// declaration, and Cloud selects its checks with an exact port of that
|
|
563
|
+
// function — so Cloud's reply is its short-circuit, an empty allow, every
|
|
564
|
+
// time. Not asked for: a round trip on every such call would buy nothing.
|
|
565
|
+
// Decided from the tool's name alone, before the envelope and its redaction
|
|
566
|
+
// passes are built for nothing (review n10).
|
|
567
|
+
const tool = classifyTool(input.toolName);
|
|
568
|
+
if (tool.toolIsKnown && tool.toolClass === "other") {
|
|
569
|
+
return {
|
|
570
|
+
status: "ok",
|
|
571
|
+
verdict: { ...EMPTY_ALLOW, outcomes: [] },
|
|
572
|
+
answers: {},
|
|
573
|
+
latencyMs: elapsed(),
|
|
574
|
+
inputTokens: null,
|
|
575
|
+
questionCount: 0,
|
|
576
|
+
truncated: false,
|
|
577
|
+
requestCut: false,
|
|
578
|
+
redactions: 0,
|
|
579
|
+
model: opts.model ?? DEFAULT_JEV_MODEL,
|
|
580
|
+
modelVerified: true,
|
|
581
|
+
via: "none",
|
|
582
|
+
cloud: { asked: [], droppedLocal: [], droppedCloud: [] },
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
let prepared: PreparedCall;
|
|
586
|
+
let request: JevRequest;
|
|
587
|
+
try {
|
|
588
|
+
prepared = prepareSemantic(input, opts);
|
|
589
|
+
request = buildCloudRequest(prepared, input, opts.machineId, opts.agent);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
return {
|
|
592
|
+
status: "degraded",
|
|
593
|
+
reason: `prepare: ${err instanceof Error ? err.message : String(err)}`,
|
|
594
|
+
latencyMs: elapsed(),
|
|
595
|
+
questionCount: 0,
|
|
596
|
+
truncated: false,
|
|
597
|
+
requestCut: false,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const questionCount = Object.keys(request.questions).length;
|
|
601
|
+
// What Cloud forwards is `{model, state, questions}` plus its own checks; the
|
|
602
|
+
// machine's share is checked here the way `prepareSemantic` checks it.
|
|
603
|
+
const { cloud: _block, ...forwarded } = request;
|
|
604
|
+
void _block;
|
|
605
|
+
const oversized = prepared.oversized || JSON.stringify(forwarded).length > MAX_REQUEST_CHARS;
|
|
606
|
+
const truncated = prepared.truncated || oversized;
|
|
607
|
+
const requestCut = prepared.requestCut || oversized;
|
|
608
|
+
const degraded = (reason: string): CloudSemanticOutcome => ({
|
|
609
|
+
status: "degraded",
|
|
610
|
+
reason,
|
|
611
|
+
latencyMs: elapsed(),
|
|
612
|
+
questionCount,
|
|
613
|
+
truncated,
|
|
614
|
+
requestCut,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const transport: JevTransport | undefined = opts.transport;
|
|
618
|
+
if (!transport) return degraded("no-transport");
|
|
619
|
+
const via: JevProviderKind = opts.via ?? "failproofai";
|
|
620
|
+
if (opts.signal?.aborted) return degraded("aborted");
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const timeout = AbortSignal.timeout(opts.timeoutMs ?? envNumber("FAILPROOFAI_JEV_TIMEOUT_MS", DEFAULT_JEV_TIMEOUT_MS));
|
|
624
|
+
const signal = opts.signal ? AbortSignal.any([timeout, opts.signal]) : timeout;
|
|
625
|
+
const response = await transport(request, signal);
|
|
626
|
+
const reply = parseCloudReply(response.cloud);
|
|
627
|
+
|
|
628
|
+
// Only the machine's own groups can be dropped from its side; a name it
|
|
629
|
+
// did not send is not its to act on.
|
|
630
|
+
const own = new Map(prepared.selected.map((p) => [p.name, p] as const));
|
|
631
|
+
const dropped = new Set(reply.droppedLocal.filter((name) => own.has(name)));
|
|
632
|
+
const kept: SemanticPolicy[] = prepared.selected.filter((p) => !dropped.has(p.name));
|
|
633
|
+
|
|
634
|
+
// Short-circuit (C10.4 step 6): nothing of the machine's left to decide and
|
|
635
|
+
// no Cloud check asked, so Cloud made no TypeSafe call and there are no
|
|
636
|
+
// answers to demand. The local verdict is then an empty allow whatever the
|
|
637
|
+
// answers — `decide` with nothing selected reads no per-policy answer, and
|
|
638
|
+
// no warning can fire without one.
|
|
639
|
+
const shortCircuit = kept.length === 0 && reply.asked.length === 0 && reply.verdict.outcomes.length === 0;
|
|
640
|
+
const answers = shortCircuit ? presentAnswers(response) : readAnswers(expectedQuestions(request, prepared, dropped), response);
|
|
641
|
+
|
|
642
|
+
const local =
|
|
643
|
+
prepared.intent === "v1"
|
|
644
|
+
? decideV1(kept, answers, input.toolInput, prepared.userSaid, prepared.agentLastMessage, {
|
|
645
|
+
...opts.v1,
|
|
646
|
+
userSaidCut: prepared.userSaidCut,
|
|
647
|
+
})
|
|
648
|
+
: decide(kept, answers, input.toolInput, prepared.userSaid, opts.thresholds ?? DEFAULT_THRESHOLDS, prepared.userSaidCut);
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
status: "ok",
|
|
652
|
+
verdict: mergeCloudVerdict(reply.verdict, local),
|
|
653
|
+
answers,
|
|
654
|
+
latencyMs: elapsed(),
|
|
655
|
+
inputTokens: typeof response.usage?.input_tokens === "number" ? response.usage.input_tokens : null,
|
|
656
|
+
questionCount,
|
|
657
|
+
truncated,
|
|
658
|
+
requestCut,
|
|
659
|
+
redactions: prepared.envelope.redactions,
|
|
660
|
+
model: response.model || request.model,
|
|
661
|
+
modelVerified: response.modelUnverified !== true,
|
|
662
|
+
via,
|
|
663
|
+
cloud: {
|
|
664
|
+
asked: reply.asked,
|
|
665
|
+
droppedLocal: [...dropped].map((name) => ({ name, packId: own.get(name)?.origin?.packId ?? null })),
|
|
666
|
+
droppedCloud: reply.droppedCloud,
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
} catch (err) {
|
|
670
|
+
// Checked first: a transport that surfaces the abort as its own timeout
|
|
671
|
+
// error must still read as the caller's abort, not as Jev being slow.
|
|
672
|
+
if (opts.signal?.aborted) return degraded("aborted");
|
|
673
|
+
if (err instanceof JevError) return degraded(err.code);
|
|
674
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) return degraded("timeout");
|
|
675
|
+
return degraded(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
676
|
+
}
|
|
677
|
+
}
|