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,692 @@
|
|
|
1
|
+
"""FailproofAI Cloud Jev / regex / both policies in `fp` (CONTRACT C4, C6).
|
|
2
|
+
|
|
3
|
+
`fp policies publish --kind regex|jev|both [--source] [--semantic]`, list/show
|
|
4
|
+
carrying the kind and the Jev check names, `fp fleet deploy --jev-mode`, and
|
|
5
|
+
machines showing the policy errors they report.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import pytest
|
|
14
|
+
import respx
|
|
15
|
+
|
|
16
|
+
from fp_cli import client
|
|
17
|
+
from fp_cli.app import app
|
|
18
|
+
from fp_cli.enforcement import (
|
|
19
|
+
RefUsageError,
|
|
20
|
+
parse_semantic,
|
|
21
|
+
plan_deploy,
|
|
22
|
+
resolve_kind,
|
|
23
|
+
version_kinds,
|
|
24
|
+
)
|
|
25
|
+
from fp_cli.models import Deployment, Machine, PolicyRef, PolicyVersion
|
|
26
|
+
|
|
27
|
+
BASE = "http://dash.test"
|
|
28
|
+
FIXTURES = Path(__file__).resolve().parents[2] / "__tests__" / "fixtures" / "cloud-jev"
|
|
29
|
+
VALID = json.loads((FIXTURES / "semantic-valid.json").read_text())
|
|
30
|
+
|
|
31
|
+
DECL = {
|
|
32
|
+
"name": "acme-prod-db",
|
|
33
|
+
"title": "Tried to write to the production database",
|
|
34
|
+
"appliesTo": ["shell"],
|
|
35
|
+
"mode": "deny",
|
|
36
|
+
"userCanOverride": True,
|
|
37
|
+
"probes": [{"id": "writes", "instructions": "It writes.", "criteria": {"true": "yes", "false": "no"}}],
|
|
38
|
+
"guidance": "Ask first.",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
SERVER_BOTH = {
|
|
42
|
+
"id": "no-prod-db", "version": 3, "description": "prod db guard", "sha256": "a" * 64,
|
|
43
|
+
"source": "export default 1;", "createdAt": "2026-09-30T00:00:00Z", "createdBy": "me@test",
|
|
44
|
+
"disabled": False, "archived": False, "kind": "both", "semantic": [DECL],
|
|
45
|
+
"semanticSha256": "b" * 64, "authority": "reviewable", "reviewedBy": ["acme-prod-db"],
|
|
46
|
+
}
|
|
47
|
+
SERVER_JEV = {
|
|
48
|
+
"id": "secrets", "version": 1, "description": "", "sha256": "", "source": None,
|
|
49
|
+
"createdAt": "2026-09-30T00:00:00Z", "createdBy": "me@test", "disabled": False,
|
|
50
|
+
"archived": False, "kind": "jev", "semantic": [dict(DECL, name="acme-secrets")],
|
|
51
|
+
"semanticSha256": "c" * 64,
|
|
52
|
+
}
|
|
53
|
+
SERVER_REGEX_OLD = {
|
|
54
|
+
"id": "no-force-push", "version": 2, "description": "", "sha256": "d" * 64,
|
|
55
|
+
"source": "export default 2;", "createdAt": "2026-09-01T00:00:00Z", "createdBy": None,
|
|
56
|
+
"disabled": False, "archived": False,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── models ────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_a_policy_version_carries_its_kind_and_jev_checks():
|
|
64
|
+
both = PolicyVersion.from_dict(SERVER_BOTH)
|
|
65
|
+
assert (both.kind, both.jev_names, both.authority, both.reviewed_by) == (
|
|
66
|
+
"both", ["acme-prod-db"], "reviewable", ["acme-prod-db"])
|
|
67
|
+
out = both.to_dict()
|
|
68
|
+
assert out["kind"] == "both" and out["semantic"] == [DECL]
|
|
69
|
+
assert out["semanticSha256"] == "b" * 64 and out["reviewedBy"] == ["acme-prod-db"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_a_version_from_a_server_that_predates_kinds_is_regex():
|
|
73
|
+
old = PolicyVersion.from_dict(SERVER_REGEX_OLD)
|
|
74
|
+
assert old.kind == "regex" and old.jev_names == [] and old.semantic is None
|
|
75
|
+
out = old.to_dict()
|
|
76
|
+
assert out["kind"] == "regex"
|
|
77
|
+
assert "semantic" not in out and "reviewedBy" not in out and "authority" not in out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_machines_carry_policy_errors_and_jev_mode_only_when_reported():
|
|
81
|
+
reported = Machine.from_dict({
|
|
82
|
+
"machineId": "m1", "policyErrors": [{"id": "p", "version": 1, "kind": "jev", "message": "bad"}],
|
|
83
|
+
"policyErrorsAt": "2026-09-30T01:00:00Z", "jevMode": "observe",
|
|
84
|
+
})
|
|
85
|
+
assert reported.policy_errors == [{"id": "p", "version": 1, "kind": "jev", "message": "bad"}]
|
|
86
|
+
assert reported.jev_mode == "observe"
|
|
87
|
+
d = reported.to_dict()
|
|
88
|
+
assert d["policyErrors"][0]["message"] == "bad" and d["jevMode"] == "observe"
|
|
89
|
+
assert d["policyErrorsAt"] == "2026-09-30T01:00:00Z"
|
|
90
|
+
|
|
91
|
+
quiet = Machine.from_dict({"machineId": "m2"})
|
|
92
|
+
assert quiet.policy_errors is None and quiet.jev_mode is None
|
|
93
|
+
assert not {"policyErrors", "policyErrorsAt", "jevMode"} & set(quiet.to_dict())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_a_deployment_carries_its_jev_mode():
|
|
97
|
+
dep = Deployment.from_dict({"machineId": "m1", "deployment": 4, "policies": [], "jevMode": "enforce"})
|
|
98
|
+
assert dep.jev_mode == "enforce" and dep.to_dict()["jevMode"] == "enforce"
|
|
99
|
+
assert "jevMode" not in Deployment.from_dict({"machineId": "m1", "deployment": 4}).to_dict()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── kind + semantic (pure) ────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.parametrize("kind,src,sem,expected", [
|
|
106
|
+
(None, True, False, "regex"),
|
|
107
|
+
(None, False, True, "jev"),
|
|
108
|
+
(None, True, True, "both"),
|
|
109
|
+
("regex", True, False, "regex"),
|
|
110
|
+
("jev", False, True, "jev"),
|
|
111
|
+
("both", True, True, "both"),
|
|
112
|
+
])
|
|
113
|
+
def test_the_kind_follows_from_what_was_given(kind, src, sem, expected):
|
|
114
|
+
assert resolve_kind(kind, has_source=src, has_semantic=sem) == expected
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize("kind,src,sem,needle", [
|
|
118
|
+
("regex", True, True, "takes no --semantic"),
|
|
119
|
+
("jev", True, True, "takes no JavaScript"),
|
|
120
|
+
("jev", False, False, "needs --semantic"),
|
|
121
|
+
("both", True, False, "needs --semantic"),
|
|
122
|
+
("hybrid", True, False, "not one of"),
|
|
123
|
+
])
|
|
124
|
+
def test_a_kind_that_contradicts_its_inputs_is_a_usage_error(kind, src, sem, needle):
|
|
125
|
+
with pytest.raises(RefUsageError, match=needle):
|
|
126
|
+
resolve_kind(kind, has_source=src, has_semantic=sem)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_semantic_accepts_the_contract_fixtures_and_a_wrapper():
|
|
130
|
+
assert len(VALID) == 16
|
|
131
|
+
assert parse_semantic(json.dumps(VALID)) == VALID
|
|
132
|
+
assert parse_semantic(json.dumps({"semantic": VALID[:2]})) == VALID[:2]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.parametrize("text,needle", [
|
|
136
|
+
("{not json", "not valid JSON"),
|
|
137
|
+
(json.dumps({"name": "x"}), "JSON array"),
|
|
138
|
+
("[]", "no Jev declarations"),
|
|
139
|
+
(json.dumps([DECL] * 25), "at most 24"),
|
|
140
|
+
(json.dumps([{"title": "no name"}]), "#0 is not an object with a name"),
|
|
141
|
+
])
|
|
142
|
+
def test_semantic_refuses_what_is_not_a_declaration_list(text, needle):
|
|
143
|
+
with pytest.raises(RefUsageError, match=needle):
|
|
144
|
+
parse_semantic(text)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ── deploy plan: --jev-mode ───────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _plan(**kw):
|
|
151
|
+
base = dict(current=[PolicyRef("a", 1)], base=5, latest={"a": 1, "j": 2})
|
|
152
|
+
base.update(kw)
|
|
153
|
+
return plan_deploy("m1", **base)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_jev_mode_alone_is_a_change_and_is_carried_in_the_plan():
|
|
157
|
+
plan = _plan(jev_mode="observe")
|
|
158
|
+
assert not plan.is_noop and plan.jev_mode_changes
|
|
159
|
+
assert plan.to_dict()["jevMode"] == {"from": "local", "to": "observe", "changed": True}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_the_same_jev_mode_again_is_a_noop():
|
|
163
|
+
assert _plan(jev_mode="observe", current_jev_mode="observe").is_noop
|
|
164
|
+
assert _plan(jev_mode="local").is_noop # already local
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_local_hands_the_mode_back_to_the_machine():
|
|
168
|
+
plan = _plan(jev_mode="local", current_jev_mode="enforce")
|
|
169
|
+
assert plan.jev_mode_changes and plan.jev_mode_after is None
|
|
170
|
+
assert plan.to_dict()["jevMode"] == {"from": "enforce", "to": "local", "changed": True}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_no_jev_mode_says_nothing_and_changes_nothing():
|
|
174
|
+
plan = _plan(add=["a"], current_jev_mode="enforce")
|
|
175
|
+
assert plan.is_noop and "jevMode" not in plan.to_dict()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_an_unknown_jev_mode_is_a_usage_error():
|
|
179
|
+
with pytest.raises(RefUsageError, match="not one of off, observe, enforce, local"):
|
|
180
|
+
_plan(jev_mode="shadow")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_observe_on_a_jev_policy_is_refused_with_the_mode_that_does_it():
|
|
184
|
+
kinds = version_kinds([PolicyVersion.from_dict(dict(SERVER_JEV, id="j", version=2))])
|
|
185
|
+
with pytest.raises(RefUsageError, match="use --jev-mode observe"):
|
|
186
|
+
_plan(add=["j:observe"], kinds=kinds)
|
|
187
|
+
# enforce is fine, and so is observe on a `both` policy (it applies to the JS half).
|
|
188
|
+
assert _plan(add=["j"], kinds=kinds).added[0].effect == "enforce"
|
|
189
|
+
both = version_kinds([PolicyVersion.from_dict(dict(SERVER_BOTH, id="j", version=2))])
|
|
190
|
+
assert _plan(add=["j:observe"], kinds=both).added[0].effect == "observe"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ── the wire ──────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_publish_sends_kind_and_semantic_and_no_source_for_jev(monkeypatch):
|
|
197
|
+
sent = []
|
|
198
|
+
monkeypatch.setattr(client, "_post_json", lambda ctx, path, body=None: sent.append(body) or {})
|
|
199
|
+
client.publish_policy(None, "secrets", None, "d", kind="jev", semantic=[DECL])
|
|
200
|
+
assert sent[-1] == {"id": "secrets", "description": "d", "kind": "jev", "semantic": [DECL]}
|
|
201
|
+
|
|
202
|
+
client.publish_policy(None, "p", "export default 1;", "")
|
|
203
|
+
assert sent[-1] == {"id": "p", "description": "", "source": "export default 1;"}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_publish_still_refuses_jev_fields_in_the_javascript(monkeypatch):
|
|
207
|
+
from fp_cli.errors import ApiError
|
|
208
|
+
|
|
209
|
+
monkeypatch.setattr(client, "_post_json", lambda *a, **k: pytest.fail("reached the server"))
|
|
210
|
+
src = 'semanticPolicies.add({ name: "x" });'
|
|
211
|
+
with pytest.raises(ApiError) as exc:
|
|
212
|
+
client.publish_policy(None, "p", src, "", kind="both", semantic=[DECL])
|
|
213
|
+
assert "--kind both --semantic" in (exc.value.hint or "")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_deploy_sends_jev_mode_only_when_given(monkeypatch):
|
|
217
|
+
bodies = []
|
|
218
|
+
monkeypatch.setattr(client, "_request_json",
|
|
219
|
+
lambda ctx, method, path, json_body=None: bodies.append(json_body) or {})
|
|
220
|
+
client.deploy_policies(None, "m1", [PolicyRef("a", 1)])
|
|
221
|
+
client.deploy_policies(None, "m1", [PolicyRef("a", 1)], jev_mode="local")
|
|
222
|
+
assert "jevMode" not in bodies[0]
|
|
223
|
+
assert bodies[1]["jevMode"] == "local"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── the commands, over HTTP ───────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@respx.mock
|
|
230
|
+
def test_publish_a_jev_policy_from_a_semantic_file(logged_in, runner, tmp_path):
|
|
231
|
+
checks = tmp_path / "checks.json"
|
|
232
|
+
checks.write_text(json.dumps([DECL]))
|
|
233
|
+
route = respx.post(f"{BASE}/api/enforcement/policies").mock(
|
|
234
|
+
return_value=httpx.Response(200, json=dict(SERVER_JEV, id="prod-db-intent", semantic=[DECL])))
|
|
235
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
|
|
236
|
+
result = runner.invoke(app, ["--json", "policies", "publish", "prod-db-intent", "--semantic", str(checks)])
|
|
237
|
+
assert result.exit_code == 0, result.output
|
|
238
|
+
body = json.loads(route.calls.last.request.content)
|
|
239
|
+
assert body == {"id": "prod-db-intent", "description": "", "kind": "jev", "semantic": [DECL]}
|
|
240
|
+
out = json.loads(result.stdout)
|
|
241
|
+
assert out["kind"] == "jev" and out["syntax"]["checked"] is False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@respx.mock
|
|
245
|
+
def test_publish_both_sends_the_source_and_the_declarations(logged_in, runner, tmp_path):
|
|
246
|
+
rule = tmp_path / "rule.mjs"
|
|
247
|
+
rule.write_text('import { customPolicies, allow } from "failproofai";\n')
|
|
248
|
+
checks = tmp_path / "checks.json"
|
|
249
|
+
checks.write_text(json.dumps([DECL]))
|
|
250
|
+
route = respx.post(f"{BASE}/api/enforcement/policies").mock(
|
|
251
|
+
return_value=httpx.Response(200, json=SERVER_BOTH))
|
|
252
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
|
|
253
|
+
result = runner.invoke(app, ["policies", "publish", "no-prod-db", "--kind", "both",
|
|
254
|
+
"--source", str(rule), "--semantic", str(checks), "--no-verify"])
|
|
255
|
+
assert result.exit_code == 0, result.output
|
|
256
|
+
body = json.loads(route.calls.last.request.content)
|
|
257
|
+
assert body["kind"] == "both" and body["semantic"] == [DECL] and "customPolicies" in body["source"]
|
|
258
|
+
assert "acme-prod-db" in result.stdout and "both" in result.stdout
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@respx.mock
|
|
262
|
+
def test_publish_jev_with_source_is_a_usage_error_before_anything_is_sent(logged_in, runner, tmp_path):
|
|
263
|
+
rule = tmp_path / "rule.mjs"
|
|
264
|
+
rule.write_text("export default 1;\n")
|
|
265
|
+
checks = tmp_path / "checks.json"
|
|
266
|
+
checks.write_text(json.dumps([DECL]))
|
|
267
|
+
result = runner.invoke(app, ["policies", "publish", "x", "--kind", "jev",
|
|
268
|
+
"--source", str(rule), "--semantic", str(checks)])
|
|
269
|
+
assert result.exit_code == 2
|
|
270
|
+
assert not respx.calls
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@respx.mock
|
|
274
|
+
def test_list_and_show_print_the_kind_and_the_jev_names(logged_in, runner):
|
|
275
|
+
respx.get(f"{BASE}/api/enforcement/policies").mock(
|
|
276
|
+
return_value=httpx.Response(200, json=[SERVER_BOTH, SERVER_JEV, SERVER_REGEX_OLD]))
|
|
277
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
|
|
278
|
+
listed = runner.invoke(app, ["policies", "list"])
|
|
279
|
+
assert listed.exit_code == 0, listed.output
|
|
280
|
+
cells = [ln.strip().strip("│").strip() for ln in listed.stdout.splitlines()]
|
|
281
|
+
rows = {c.split()[0]: c for c in cells if c[:1].isalpha()}
|
|
282
|
+
assert "both" in rows["no-prod-db"] and "acme-prod-db" in rows["no-prod-db"]
|
|
283
|
+
assert "jev" in rows["secrets"] and "acme-secrets" in rows["secrets"]
|
|
284
|
+
assert "regex" in rows["no-force-push"]
|
|
285
|
+
|
|
286
|
+
shown = runner.invoke(app, ["policies", "show", "secrets"])
|
|
287
|
+
assert shown.exit_code == 0, shown.output
|
|
288
|
+
assert "jev" in shown.stdout and "acme-secrets" in shown.stdout
|
|
289
|
+
# The declarations themselves, printed where the JavaScript source is.
|
|
290
|
+
assert '"name": "acme-secrets"' in shown.stdout + (shown.stderr or "")
|
|
291
|
+
|
|
292
|
+
js = json.loads(runner.invoke(app, ["--json", "policies", "show", "no-prod-db"]).stdout)
|
|
293
|
+
assert js["kind"] == "both" and js["reviewedBy"] == ["acme-prod-db"] and js["semantic"] == [DECL]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_a_version_carries_its_jev_question_chars_and_regex_has_none():
|
|
297
|
+
jev = PolicyVersion.from_dict(dict(SERVER_JEV, jevChars=412))
|
|
298
|
+
assert jev.jev_chars == 412 and jev.to_dict()["jevChars"] == 412
|
|
299
|
+
regex = PolicyVersion.from_dict(dict(SERVER_REGEX_OLD, jevChars=None))
|
|
300
|
+
assert regex.jev_chars is None and "jevChars" not in regex.to_dict()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@respx.mock
|
|
304
|
+
def test_list_and_show_print_the_jev_question_chars(logged_in, runner):
|
|
305
|
+
respx.get(f"{BASE}/api/enforcement/policies").mock(return_value=httpx.Response(
|
|
306
|
+
200, json=[dict(SERVER_JEV, jevChars=18653), dict(SERVER_REGEX_OLD, jevChars=None)]))
|
|
307
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
|
|
308
|
+
listed = runner.invoke(app, ["policies", "list"], env={"COLUMNS": "200"})
|
|
309
|
+
assert listed.exit_code == 0, listed.output
|
|
310
|
+
rows = {c.split()[0]: c for c in (ln.strip().strip("│").strip() for ln in listed.stdout.splitlines())
|
|
311
|
+
if c[:1].isalpha()}
|
|
312
|
+
assert "18,653 chars" in rows["secrets"]
|
|
313
|
+
assert "chars" not in rows["no-force-push"]
|
|
314
|
+
|
|
315
|
+
shown = runner.invoke(app, ["policies", "show", "secrets"], env={"COLUMNS": "200"})
|
|
316
|
+
assert shown.exit_code == 0, shown.output
|
|
317
|
+
assert "18,653 question chars" in shown.stdout
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@respx.mock
|
|
321
|
+
def test_a_jev_budget_refusal_names_each_policys_share_largest_first(logged_in, runner):
|
|
322
|
+
# The server's deploy 422 (CONTRACT C9.2): the message has the total, the
|
|
323
|
+
# body lists every contributing version — which is what says what to drop.
|
|
324
|
+
_fleet_routes([])
|
|
325
|
+
refusal = {
|
|
326
|
+
"error": "the Jev checks of this deployment are over the machine's question budget "
|
|
327
|
+
"(37756 of 27591 characters)",
|
|
328
|
+
"code": "jev_budget_exceeded", "used": 37756, "budget": 27591,
|
|
329
|
+
"policies": [{"id": "e2e-prod-db", "version": 1, "chars": 415},
|
|
330
|
+
{"id": "e2e-big", "version": 1, "chars": 18653},
|
|
331
|
+
{"id": "e2e-big2", "version": 1, "chars": 18688}],
|
|
332
|
+
}
|
|
333
|
+
respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(422, json=refusal))
|
|
334
|
+
human = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets", "--yes"], env={"COLUMNS": "400"})
|
|
335
|
+
assert human.exit_code == 1
|
|
336
|
+
text = " ".join((human.output + (human.stderr or "")).split())
|
|
337
|
+
assert "37756 of 27591" in text
|
|
338
|
+
assert "e2e-big2@1 18,688 · e2e-big@1 18,653 · e2e-prod-db@1 415" in text
|
|
339
|
+
|
|
340
|
+
js = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--add", "secrets", "--yes"])
|
|
341
|
+
out = json.loads(js.stdout)
|
|
342
|
+
assert out["status"] == 422
|
|
343
|
+
assert out["hint"].startswith("Jev question characters per policy: e2e-big2@1 18,688")
|
|
344
|
+
|
|
345
|
+
# Any other refusal carries no such line.
|
|
346
|
+
respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
347
|
+
422, json={"error": "bad", "code": "jev_effect_unsupported"}))
|
|
348
|
+
other = json.loads(runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--add", "secrets", "--yes"]).stdout)
|
|
349
|
+
assert "hint" not in other
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _fleet_routes(current_policies, jev_mode=None, errors=None):
|
|
353
|
+
machine = {"machineId": "m1", "deployment": 5, "appliedDeployment": 5, "deployed": True}
|
|
354
|
+
if errors is not None:
|
|
355
|
+
machine["policyErrors"] = errors
|
|
356
|
+
if jev_mode:
|
|
357
|
+
machine["jevMode"] = jev_mode
|
|
358
|
+
dep = {"machineId": "m1", "deployment": 5, "policies": current_policies}
|
|
359
|
+
if jev_mode:
|
|
360
|
+
dep["jevMode"] = jev_mode
|
|
361
|
+
respx.get(f"{BASE}/api/enforcement/machines").mock(return_value=httpx.Response(200, json=[machine]))
|
|
362
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[dep]))
|
|
363
|
+
respx.get(f"{BASE}/api/enforcement/policies").mock(
|
|
364
|
+
return_value=httpx.Response(200, json=[SERVER_BOTH, SERVER_JEV, SERVER_REGEX_OLD]))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@respx.mock
|
|
368
|
+
def test_fleet_deploy_jev_mode_alone_changes_only_the_mode_through_its_own_route(logged_in, runner):
|
|
369
|
+
# CONTRACT C9.1: a mode change sent as a deploy rewrote the set the CLI had
|
|
370
|
+
# read — which leaves out disabled policies' assignments, so they were lost.
|
|
371
|
+
_fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
|
|
372
|
+
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
|
|
373
|
+
mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode").mock(return_value=httpx.Response(
|
|
374
|
+
200, json={"machineId": "m1", "deployment": 6, "jevMode": "observe", "changed": True,
|
|
375
|
+
"policies": [{"id": "no-force-push", "version": 2, "effect": "enforce"}]}))
|
|
376
|
+
result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
|
|
377
|
+
assert result.exit_code == 0, result.output
|
|
378
|
+
assert not deploy.called
|
|
379
|
+
assert json.loads(mode.calls.last.request.content) == {"jevMode": "observe"}
|
|
380
|
+
out = json.loads(result.stdout)
|
|
381
|
+
assert out["applied"] is True and out["jevModeOnly"] is True
|
|
382
|
+
assert out["plan"]["jevMode"]["to"] == "observe" and out["deployment"]["deployment"] == 6
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@respx.mock
|
|
386
|
+
def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_flag(logged_in, runner):
|
|
387
|
+
_fleet_routes([], jev_mode="enforce")
|
|
388
|
+
put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
389
|
+
200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce",
|
|
390
|
+
"policies": [{"id": "secrets", "version": 1, "effect": "enforce"}]}))
|
|
391
|
+
result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets", "--yes"])
|
|
392
|
+
assert result.exit_code == 0, result.output
|
|
393
|
+
assert json.loads(put.calls.last.request.content) == {
|
|
394
|
+
"policies": [{"id": "secrets", "version": 1, "effect": "enforce"}]}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@respx.mock
|
|
398
|
+
def test_fleet_deploy_refuses_observe_on_a_jev_policy_before_writing(logged_in, runner):
|
|
399
|
+
_fleet_routes([])
|
|
400
|
+
put = respx.put(f"{BASE}/api/enforcement/deployments/m1")
|
|
401
|
+
result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets:observe", "--yes"])
|
|
402
|
+
assert result.exit_code == 2
|
|
403
|
+
assert "--jev-mode observe" in result.output + (result.stderr or "")
|
|
404
|
+
assert not put.called
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@respx.mock
|
|
408
|
+
def test_fleet_show_and_list_show_the_jev_mode_and_the_reported_errors(logged_in, runner):
|
|
409
|
+
errors = [{"id": "secrets", "version": 1, "kind": "jev", "message": "not loaded: failed integrity verification"},
|
|
410
|
+
{"id": "jevMode", "version": None, "kind": "daemon", "message": "jev_unconfigured"}]
|
|
411
|
+
_fleet_routes([{"id": "secrets", "version": 1, "effect": "enforce"}], jev_mode="observe", errors=errors)
|
|
412
|
+
shown = runner.invoke(app, ["fleet", "show", "m1"])
|
|
413
|
+
assert shown.exit_code == 0, shown.output
|
|
414
|
+
assert "jev mode" in shown.stdout and "observe" in shown.stdout and "set by FailproofAI Cloud" in shown.stdout
|
|
415
|
+
assert "2 policy errors" in shown.stdout and "jev_unconfigured" in shown.stdout
|
|
416
|
+
assert "failed integrity verification" in shown.stdout
|
|
417
|
+
|
|
418
|
+
listed = runner.invoke(app, ["fleet", "list"])
|
|
419
|
+
assert listed.exit_code == 0, listed.output
|
|
420
|
+
assert "2 errors" in listed.stdout
|
|
421
|
+
|
|
422
|
+
js = json.loads(runner.invoke(app, ["--json", "fleet", "show", "m1"]).stdout)
|
|
423
|
+
assert js["machine"]["policyErrors"] == errors and js["machine"]["jevMode"] == "observe"
|
|
424
|
+
assert js["deployment"]["jevMode"] == "observe"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ── Review fixes: the Jev mode route (C9.1), rollback and history (m6), help (m7) ──
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _fleet_many(deployments, machines=None):
|
|
431
|
+
"""`deployments`: {machine_id: jev_mode or None}; machines default to the same ids."""
|
|
432
|
+
ids = machines if machines is not None else list(deployments)
|
|
433
|
+
respx.get(f"{BASE}/api/enforcement/machines").mock(return_value=httpx.Response(200, json=[
|
|
434
|
+
{"machineId": m, "deployment": 5, "appliedDeployment": 5, "deployed": m in deployments} for m in ids]))
|
|
435
|
+
deps = []
|
|
436
|
+
for m, mode in deployments.items():
|
|
437
|
+
d = {"machineId": m, "deployment": 5, "policies": [{"id": "no-force-push", "version": 2, "effect": "enforce"}]}
|
|
438
|
+
if mode:
|
|
439
|
+
d["jevMode"] = mode
|
|
440
|
+
deps.append(d)
|
|
441
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=deps))
|
|
442
|
+
respx.get(f"{BASE}/api/enforcement/policies").mock(
|
|
443
|
+
return_value=httpx.Response(200, json=[SERVER_BOTH, SERVER_JEV, SERVER_REGEX_OLD]))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _mode_route(machine_id, mode, deployment=6, status=200):
|
|
447
|
+
body = ({"machineId": machine_id, "deployment": deployment, "jevMode": None if mode == "local" else mode,
|
|
448
|
+
"changed": True, "policies": []} if status == 200
|
|
449
|
+
else {"error": f"machine {machine_id} has no deployment", "code": "no_deployment"})
|
|
450
|
+
return respx.put(f"{BASE}/api/enforcement/deployments/{machine_id}/jev-mode").mock(
|
|
451
|
+
return_value=httpx.Response(status, json=body))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_set_jev_mode_sends_only_the_mode(monkeypatch):
|
|
455
|
+
seen = {}
|
|
456
|
+
|
|
457
|
+
def fake(ctx, method, path, json_body=None, **kw):
|
|
458
|
+
seen.update(method=method, path=path, body=json_body)
|
|
459
|
+
return {"machineId": "m1", "deployment": 9, "jevMode": "enforce", "changed": False, "policies": []}
|
|
460
|
+
|
|
461
|
+
monkeypatch.setattr(client, "_request_json", fake)
|
|
462
|
+
dep, changed = client.set_jev_mode(object(), "m1", "enforce")
|
|
463
|
+
assert seen == {"method": "PUT", "path": "/api/enforcement/deployments/m1/jev-mode", "body": {"jevMode": "enforce"}}
|
|
464
|
+
assert (dep.deployment, dep.jev_mode, changed) == (9, "enforce", False)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@respx.mock
|
|
468
|
+
def test_fleet_deploy_jev_mode_on_a_machine_with_no_deployment_deploys_its_first(logged_in, runner):
|
|
469
|
+
respx.get(f"{BASE}/api/enforcement/machines").mock(return_value=httpx.Response(
|
|
470
|
+
200, json=[{"machineId": "m1", "deployment": None, "deployed": False}]))
|
|
471
|
+
respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
|
|
472
|
+
respx.get(f"{BASE}/api/enforcement/policies").mock(return_value=httpx.Response(200, json=[SERVER_JEV]))
|
|
473
|
+
mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
|
|
474
|
+
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
475
|
+
200, json={"machineId": "m1", "deployment": 1, "jevMode": "observe", "policies": []}))
|
|
476
|
+
result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
|
|
477
|
+
assert result.exit_code == 0, result.output
|
|
478
|
+
assert not mode.called
|
|
479
|
+
assert json.loads(deploy.calls.last.request.content) == {"policies": [], "jevMode": "observe"}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@respx.mock
|
|
483
|
+
def test_fleet_deploy_with_a_set_change_and_a_mode_still_deploys_both_together(logged_in, runner):
|
|
484
|
+
_fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
|
|
485
|
+
mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
|
|
486
|
+
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
|
|
487
|
+
200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce", "policies": []}))
|
|
488
|
+
result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets", "--jev-mode", "enforce", "--yes"])
|
|
489
|
+
assert result.exit_code == 0, result.output
|
|
490
|
+
assert not mode.called
|
|
491
|
+
assert json.loads(deploy.calls.last.request.content)["jevMode"] == "enforce"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@respx.mock
|
|
495
|
+
def test_fleet_jev_mode_sets_named_machines_and_skips_one_already_there(logged_in, runner):
|
|
496
|
+
_fleet_many({"m1": None, "m2": "enforce", "m3": "observe"})
|
|
497
|
+
r1, r2, r3 = _mode_route("m1", "enforce"), _mode_route("m2", "enforce"), _mode_route("m3", "enforce", 7)
|
|
498
|
+
deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
|
|
499
|
+
result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "m3", "enforce"])
|
|
500
|
+
assert result.exit_code == 0, result.output
|
|
501
|
+
assert r1.called and r3.called and not r2.called and not deploy.called
|
|
502
|
+
assert json.loads(r1.calls.last.request.content) == {"jevMode": "enforce"}
|
|
503
|
+
out = json.loads(result.stdout)
|
|
504
|
+
assert out["mode"] == "enforce" and out["applied"] is True
|
|
505
|
+
rows = {r["machineId"]: r for r in out["machines"]}
|
|
506
|
+
assert rows["m1"]["from"] == "local" and rows["m1"]["to"] == "enforce" and rows["m1"]["deployment"] == 6
|
|
507
|
+
assert rows["m2"]["changed"] is False
|
|
508
|
+
assert rows["m3"]["from"] == "observe" and rows["m3"]["deployment"] == 7
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@respx.mock
|
|
512
|
+
def test_fleet_jev_mode_all_covers_every_machine_with_a_deployment_only(logged_in, runner):
|
|
513
|
+
_fleet_many({"m1": "enforce", "m2": "off"}, machines=["m1", "m2", "m-new"])
|
|
514
|
+
r1, r2 = _mode_route("m1", "local"), _mode_route("m2", "local")
|
|
515
|
+
fresh = respx.put(f"{BASE}/api/enforcement/deployments/m-new/jev-mode")
|
|
516
|
+
result = runner.invoke(app, ["fleet", "jev-mode", "--all", "local"])
|
|
517
|
+
assert result.exit_code == 0, result.output
|
|
518
|
+
assert r1.called and r2.called and not fresh.called
|
|
519
|
+
assert json.loads(r1.calls.last.request.content) == {"jevMode": "local"}
|
|
520
|
+
assert "enforce → local" in result.stdout and "off → local" in result.stdout
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@respx.mock
|
|
524
|
+
def test_fleet_jev_mode_when_nothing_changes_writes_nothing(logged_in, runner):
|
|
525
|
+
_fleet_many({"m1": "observe"})
|
|
526
|
+
route = _mode_route("m1", "observe")
|
|
527
|
+
result = runner.invoke(app, ["fleet", "jev-mode", "m1", "observe"])
|
|
528
|
+
assert result.exit_code == 0, result.output
|
|
529
|
+
assert not route.called
|
|
530
|
+
assert "already has jev mode observe" in result.output + (result.stderr or "")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@pytest.mark.parametrize("argv, needle", [
|
|
534
|
+
(["fleet", "jev-mode"], "pass the mode"),
|
|
535
|
+
(["fleet", "jev-mode", "m1", "shadow"], "is not a Jev mode"),
|
|
536
|
+
(["fleet", "jev-mode", "observe"], "name at least one machine"),
|
|
537
|
+
(["fleet", "jev-mode", "--all", "m1", "observe"], "do not name machines"),
|
|
538
|
+
])
|
|
539
|
+
def test_fleet_jev_mode_usage_errors_exit_2_before_any_request(logged_in, runner, argv, needle):
|
|
540
|
+
with respx.mock(assert_all_called=False) as mock:
|
|
541
|
+
result = runner.invoke(app, argv)
|
|
542
|
+
assert result.exit_code == 2, result.output
|
|
543
|
+
assert needle in result.output + (result.stderr or "")
|
|
544
|
+
assert not mock.calls
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@respx.mock
|
|
548
|
+
def test_fleet_jev_mode_refuses_a_named_machine_with_no_deployment_before_writing(logged_in, runner):
|
|
549
|
+
_fleet_many({"m1": None}, machines=["m1", "m2"])
|
|
550
|
+
r1 = _mode_route("m1", "observe")
|
|
551
|
+
result = runner.invoke(app, ["fleet", "jev-mode", "m1", "m2", "observe"])
|
|
552
|
+
assert result.exit_code == 1, result.output
|
|
553
|
+
text = result.output + (result.stderr or "")
|
|
554
|
+
assert "m2 has no deployment" in text and "fp fleet deploy <machine> --jev-mode observe" in text
|
|
555
|
+
assert not r1.called
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@respx.mock
|
|
559
|
+
def test_fleet_jev_mode_goes_on_past_a_machine_that_fails_and_names_it(logged_in, runner):
|
|
560
|
+
_fleet_many({"m1": None, "m2": None})
|
|
561
|
+
_mode_route("m1", "observe", status=404)
|
|
562
|
+
r2 = _mode_route("m2", "observe")
|
|
563
|
+
result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "observe"])
|
|
564
|
+
assert result.exit_code == 1, result.output
|
|
565
|
+
assert r2.called
|
|
566
|
+
# The result document, then the error document the exit code comes with.
|
|
567
|
+
out, end = json.JSONDecoder().raw_decode(result.stdout)
|
|
568
|
+
rows = {r["machineId"]: r for r in out["machines"]}
|
|
569
|
+
assert "error" in rows["m1"] and rows["m2"]["deployment"] == 6 and out["applied"] is False
|
|
570
|
+
assert "could not be changed on m1" in result.stdout[end:] + (result.stderr or "")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@respx.mock
|
|
574
|
+
def test_fleet_rollback_says_which_jev_mode_it_restores(logged_in, runner):
|
|
575
|
+
_fleet_many({"m1": "off"})
|
|
576
|
+
respx.get(f"{BASE}/api/enforcement/deployments/m1/history").mock(return_value=httpx.Response(200, json=[
|
|
577
|
+
{"deployment": 5, "policies": [], "updatedAt": "2026-09-30T02:00:00Z", "jevMode": "off"},
|
|
578
|
+
{"deployment": 3, "policies": [], "updatedAt": "2026-09-29T02:00:00Z", "jevMode": "enforce"},
|
|
579
|
+
]))
|
|
580
|
+
respx.post(f"{BASE}/api/enforcement/deployments/m1/rollback").mock(return_value=httpx.Response(
|
|
581
|
+
200, json={"machineId": "m1", "deployment": 6, "rolledBackFrom": 3, "policies": [], "jevMode": "enforce"}))
|
|
582
|
+
human = runner.invoke(app, ["fleet", "rollback", "m1", "3", "--yes"])
|
|
583
|
+
assert human.exit_code == 0, human.output
|
|
584
|
+
text = human.output + (human.stderr or "")
|
|
585
|
+
assert "jev mode" in text and "off → enforce" in text
|
|
586
|
+
|
|
587
|
+
js = runner.invoke(app, ["--json", "fleet", "rollback", "m1", "3", "--yes"])
|
|
588
|
+
assert js.exit_code == 0, js.output
|
|
589
|
+
out = json.loads(js.stdout)
|
|
590
|
+
assert out["jevMode"] == "enforce" and out["jevModeBefore"] == "off"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@respx.mock
|
|
594
|
+
def test_fleet_history_shows_each_generations_jev_mode_and_a_mode_only_change(logged_in, runner):
|
|
595
|
+
_fleet_many({"m1": "observe"})
|
|
596
|
+
pols = [{"id": "no-force-push", "version": 2, "effect": "enforce"}]
|
|
597
|
+
respx.get(f"{BASE}/api/enforcement/deployments/m1/history").mock(return_value=httpx.Response(200, json=[
|
|
598
|
+
{"deployment": 6, "policies": pols, "updatedAt": "2026-09-30T02:00:00Z", "jevMode": "observe"},
|
|
599
|
+
{"deployment": 5, "policies": pols, "updatedAt": "2026-09-30T01:00:00Z"},
|
|
600
|
+
]))
|
|
601
|
+
result = runner.invoke(app, ["fleet", "history", "m1"])
|
|
602
|
+
assert result.exit_code == 0, result.output
|
|
603
|
+
assert "jev" in result.stdout
|
|
604
|
+
assert "local→observe" in result.stdout
|
|
605
|
+
lines = [ln for ln in result.stdout.splitlines() if "#6" in ln or "#5" in ln]
|
|
606
|
+
assert any("observe" in ln for ln in lines if "#6" in ln)
|
|
607
|
+
assert any("local" in ln for ln in lines if "#5" in ln)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
|
|
611
|
+
for argv in (["fleet", "deploy", "--help"], ["fleet", "jev-mode", "--help"]):
|
|
612
|
+
result = runner.invoke(app, argv)
|
|
613
|
+
assert result.exit_code == 0, result.output
|
|
614
|
+
flat = " ".join(result.stdout.split())
|
|
615
|
+
assert "BLOCK" in flat, argv
|
|
616
|
+
assert "installed" in flat and "pack" in flat, argv
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# ── CONTRACT C10.6: Jev checks run on FailproofAI Cloud (help text only) ──────
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@pytest.mark.parametrize(
|
|
623
|
+
"argv",
|
|
624
|
+
[
|
|
625
|
+
["fleet", "deploy", "--help"],
|
|
626
|
+
["fleet", "jev-mode", "--help"],
|
|
627
|
+
["policies", "publish", "--help"],
|
|
628
|
+
],
|
|
629
|
+
)
|
|
630
|
+
def test_the_help_says_jev_checks_run_on_cloud_and_nothing_is_installed(runner, argv):
|
|
631
|
+
result = runner.invoke(app, argv)
|
|
632
|
+
assert result.exit_code == 0, result.output
|
|
633
|
+
flat = " ".join(result.stdout.split())
|
|
634
|
+
assert "Jev checks run on FailproofAI Cloud; nothing is installed on the machine" in flat, argv
|
|
635
|
+
# The on-machine delivery it replaced is not described any more.
|
|
636
|
+
assert "puts its Jev checks on the machine" not in flat, argv
|
|
637
|
+
assert "AgentEye" not in flat, argv
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@pytest.mark.parametrize("argv", [["fleet", "show", "--help"], ["fleet", "jev-mode", "--help"]])
|
|
641
|
+
def test_the_help_names_the_reasons_a_machine_cannot_ask(runner, argv):
|
|
642
|
+
flat = " ".join(runner.invoke(app, argv).stdout.split())
|
|
643
|
+
assert "transcripts_disabled" in flat, argv
|
|
644
|
+
if argv[1] == "show":
|
|
645
|
+
assert "jev_unconfigured" in flat
|
|
646
|
+
assert "jev_budget" in flat
|
|
647
|
+
# Review M3 and the name-clash split: the machine's own reports.
|
|
648
|
+
for code in ("jev_rate_limited", "jev_unavailable", "jev_name_clash"):
|
|
649
|
+
assert code in flat
|
|
650
|
+
# A machine no longer parses Jev checks, so it cannot fail to.
|
|
651
|
+
assert "could not parse" not in flat
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
# ── e2e findings: exact-profile targets read as their label (B11) ─────────────
|
|
655
|
+
|
|
656
|
+
_AGT = "agt_" + "1f" * 16
|
|
657
|
+
_PROFILE_POLS = [{"id": "no-force-push", "version": 2, "effect": "enforce",
|
|
658
|
+
"agentTargets": [{"integration": "claude", "instanceId": _AGT}]}]
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _roster_route(agents, status=200):
|
|
662
|
+
return respx.get(f"{BASE}/api/enforcement/machines/m1/agents").mock(return_value=httpx.Response(
|
|
663
|
+
status, json={"machineId": "m1", "generation": 3, "agents": agents} if status == 200
|
|
664
|
+
else {"error": "not found"}))
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@respx.mock
|
|
668
|
+
def test_fleet_show_and_history_name_an_exact_profile_by_its_label(logged_in, runner):
|
|
669
|
+
_fleet_routes(_PROFILE_POLS)
|
|
670
|
+
_roster_route([{"instanceId": _AGT, "integration": "claude", "profileLabel": ".claude-work",
|
|
671
|
+
"scope": "user", "hookInstalled": True}])
|
|
672
|
+
respx.get(f"{BASE}/api/enforcement/deployments/m1/history").mock(return_value=httpx.Response(200, json=[
|
|
673
|
+
{"deployment": 5, "policies": _PROFILE_POLS, "updatedAt": "2026-09-30T01:00:00Z"}]))
|
|
674
|
+
|
|
675
|
+
shown = runner.invoke(app, ["fleet", "show", "m1"])
|
|
676
|
+
assert shown.exit_code == 0, shown.output
|
|
677
|
+
# The label a person recognises, with the id a selector needs beside it.
|
|
678
|
+
assert f"claude/.claude-work ({_AGT})" in shown.stdout
|
|
679
|
+
|
|
680
|
+
history = runner.invoke(app, ["fleet", "history", "m1"])
|
|
681
|
+
assert history.exit_code == 0, history.output
|
|
682
|
+
assert "claude/.claude-work" in history.stdout
|
|
683
|
+
assert _AGT not in history.stdout
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@respx.mock
|
|
687
|
+
def test_an_exact_profile_without_a_reported_label_keeps_the_bare_id(logged_in, runner):
|
|
688
|
+
_fleet_routes(_PROFILE_POLS)
|
|
689
|
+
_roster_route([], status=404)
|
|
690
|
+
shown = runner.invoke(app, ["fleet", "show", "m1"])
|
|
691
|
+
assert shown.exit_code == 0, shown.output
|
|
692
|
+
assert f"claude/{_AGT}" in shown.stdout
|