failproofai 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +9 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -10
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +7 -6
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
- package/.next/standalone/.next/server/app/settings/page.js +12 -7
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
- package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
- package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
- package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
- package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
- package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
- package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
- package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
- package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
- package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
- package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
- package/.next/standalone/app/actions/get-jev-config.ts +604 -0
- package/.next/standalone/app/actions/pack-actions.ts +12 -0
- package/.next/standalone/app/actions/update-jev-config.ts +569 -0
- package/.next/standalone/app/components/jev-notices.tsx +96 -0
- package/.next/standalone/app/policies/hooks-client.tsx +14 -3
- package/.next/standalone/app/settings/jev-panel.tsx +630 -0
- package/.next/standalone/app/settings/page.tsx +20 -1
- package/.next/standalone/app/settings/settings-client.tsx +27 -1
- package/.next/standalone/app/settings/settings.css +79 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
- package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +3 -2
- package/bin/failproofai.mjs +148 -4
- package/dist/cli.mjs +18599 -9478
- package/dist/index.js +19 -1
- package/dist/worker.mjs +10500 -3773
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -0
- package/scripts/build-policy-pack.mjs +53 -2
- package/src/audit/features.ts +3 -2
- package/src/hooks/builtin-policies.ts +195 -8
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +57 -8
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/cloud-managed-policies.ts +22 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +98 -10
- package/src/hooks/custom-hooks-registry.ts +45 -1
- package/src/hooks/effective-reviewers.ts +243 -0
- package/src/hooks/first-run-gate.ts +5 -0
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-home.ts +23 -0
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +303 -7
- package/src/hooks/hook-activity-store.ts +118 -6
- package/src/hooks/hook-telemetry.ts +41 -0
- package/src/hooks/jev-activity.ts +385 -0
- package/src/hooks/jev-cli.ts +2172 -0
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/loader-utils.ts +6 -0
- package/src/hooks/manager.ts +45 -7
- package/src/hooks/pack-cli.ts +568 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +489 -7
- package/src/hooks/pack-store.ts +194 -11
- package/src/hooks/policy-authority.ts +386 -0
- package/src/hooks/policy-catalog.ts +206 -0
- package/src/hooks/policy-evaluator.ts +963 -796
- package/src/hooks/policy-registry.ts +27 -1
- package/src/hooks/policy-reviewability.ts +258 -0
- package/src/hooks/policy-types.ts +128 -0
- package/src/hooks/semantic/combine.ts +581 -0
- package/src/hooks/semantic/compile.ts +176 -0
- package/src/hooks/semantic/decide.ts +513 -0
- package/src/hooks/semantic/envelope.ts +1255 -0
- package/src/hooks/semantic/evaluator.ts +556 -0
- package/src/hooks/semantic/facts.ts +387 -0
- package/src/hooks/semantic/intent.ts +1190 -0
- package/src/hooks/semantic/jev-client.ts +1155 -0
- package/src/hooks/semantic/jev-config.ts +1142 -0
- package/src/hooks/semantic/jev-review.ts +389 -0
- package/src/hooks/semantic/jev-stats.ts +289 -0
- package/src/hooks/semantic/jev-throttle.ts +427 -0
- package/src/hooks/semantic/pack-policies.ts +294 -0
- package/src/hooks/semantic/policies.ts +597 -0
- package/src/hooks/semantic/precondition-names.ts +60 -0
- package/src/hooks/semantic/preconditions.ts +58 -0
- package/src/hooks/semantic/redact.ts +2937 -0
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +162 -0
- package/src/hooks/semver-precedence.ts +128 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/types.ts +1 -1
- package/src/hooks/worker-server.ts +121 -27
- package/src/index.ts +6 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
- package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
- package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
- package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
|
@@ -206,7 +206,24 @@ const HOW_IT_WORKS: ReadonlyArray<{ label: string; body: string }> = [
|
|
|
206
206
|
},
|
|
207
207
|
];
|
|
208
208
|
|
|
209
|
-
export default function SettingsClient({
|
|
209
|
+
export default function SettingsClient({
|
|
210
|
+
initial,
|
|
211
|
+
jevPanel,
|
|
212
|
+
}: {
|
|
213
|
+
initial: ScheduledAuditView | null;
|
|
214
|
+
/**
|
|
215
|
+
* The Jev panel, rendered by the server page and handed in as an element
|
|
216
|
+
* rather than imported here.
|
|
217
|
+
*
|
|
218
|
+
* It is its own feature with its own file, its own actions and its own
|
|
219
|
+
* refresh — nothing on this page reads or writes what it does. Composing it
|
|
220
|
+
* in keeps that true in the code as well as in the design: this component
|
|
221
|
+
* does not import the Jev actions, so a test of the scheduled-audit panel
|
|
222
|
+
* neither renders nor touches Jev, and vice versa. Optional so the existing
|
|
223
|
+
* callers that only care about the audit panel keep working unchanged.
|
|
224
|
+
*/
|
|
225
|
+
jevPanel?: ReactNode;
|
|
226
|
+
}) {
|
|
210
227
|
// Seeded from the server render, so the first paint already tells the truth
|
|
211
228
|
// about whether scheduled audits are on. See the note in `page.tsx`.
|
|
212
229
|
const [view, setView] = useState<ScheduledAuditView | null>(initial);
|
|
@@ -507,6 +524,10 @@ export default function SettingsClient({ initial }: { initial: ScheduledAuditVie
|
|
|
507
524
|
dashboard can reach <code>~/.failproofai/config.json</code>.
|
|
508
525
|
</p>
|
|
509
526
|
</div>
|
|
527
|
+
{/* A different file. An unreadable `config.json` says nothing about
|
|
528
|
+
`jev.json`, and hiding the panel that can turn a second
|
|
529
|
+
evaluator off would be the worst moment to hide it. */}
|
|
530
|
+
{jevPanel}
|
|
510
531
|
</div>
|
|
511
532
|
) : (
|
|
512
533
|
<div className="set-console">
|
|
@@ -653,6 +674,11 @@ export default function SettingsClient({ initial }: { initial: ScheduledAuditVie
|
|
|
653
674
|
</dl>
|
|
654
675
|
</div>
|
|
655
676
|
</div>
|
|
677
|
+
|
|
678
|
+
{/* Full width, below the two panels: Jev is a second evaluator for
|
|
679
|
+
the whole machine, not a property of the scheduled scan, and its
|
|
680
|
+
endpoint would not fit a half-width column anyway. */}
|
|
681
|
+
{jevPanel}
|
|
656
682
|
</div>
|
|
657
683
|
)}
|
|
658
684
|
|
|
@@ -151,6 +151,51 @@
|
|
|
151
151
|
margin: 0;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/* ── jev ──────────────────────────────────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
/* A full-width cell in the same console, below the two panels. The gap above it
|
|
157
|
+
* is the console's own 1px grid — this rule adds no border of its own. */
|
|
158
|
+
.set-jev {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Two columns at desktop: a provider or a mode needs about half the width an
|
|
165
|
+
* endpoint does, and stacking five controls in one column turns a short form
|
|
166
|
+
* into a scroll. */
|
|
167
|
+
.set-jev-fields {
|
|
168
|
+
display: grid;
|
|
169
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
170
|
+
gap: 16px 20px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.set-field {
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
}
|
|
179
|
+
.set-field-label {
|
|
180
|
+
font-family: var(--font-mono);
|
|
181
|
+
font-size: 9.5px;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
letter-spacing: 0.18em;
|
|
184
|
+
text-transform: uppercase;
|
|
185
|
+
color: var(--dim);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.set-jev-actions {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 16px;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@media (max-width: 640px) {
|
|
196
|
+
.set-jev-fields { grid-template-columns: 1fr; }
|
|
197
|
+
}
|
|
198
|
+
|
|
154
199
|
/* ── masthead ─────────────────────────────────────────────────────────────── */
|
|
155
200
|
|
|
156
201
|
.set-mast {
|
|
@@ -228,6 +273,40 @@
|
|
|
228
273
|
outline-offset: 1px;
|
|
229
274
|
}
|
|
230
275
|
|
|
276
|
+
/* `.set-num`'s box, stretched. Same background, same hairline, same square
|
|
277
|
+
* corners and the same pink focus ring — a second field style would read as a
|
|
278
|
+
* second form on one page. */
|
|
279
|
+
.set-text,
|
|
280
|
+
.set-select {
|
|
281
|
+
font-family: var(--font-mono);
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
width: 100%;
|
|
284
|
+
padding: 6px 10px;
|
|
285
|
+
background: var(--bg);
|
|
286
|
+
border: 1px solid var(--line-2);
|
|
287
|
+
color: var(--ink);
|
|
288
|
+
border-radius: 0;
|
|
289
|
+
}
|
|
290
|
+
.set-text:focus-visible,
|
|
291
|
+
.set-select:focus-visible {
|
|
292
|
+
outline: 2px solid var(--accent-pink);
|
|
293
|
+
outline-offset: 1px;
|
|
294
|
+
}
|
|
295
|
+
.set-text:disabled,
|
|
296
|
+
.set-select:disabled {
|
|
297
|
+
opacity: 0.5;
|
|
298
|
+
cursor: not-allowed;
|
|
299
|
+
}
|
|
300
|
+
.set-text::placeholder {
|
|
301
|
+
color: var(--dim);
|
|
302
|
+
}
|
|
303
|
+
/* The native arrow is drawn by the platform and cannot be tokenised; leaving
|
|
304
|
+
* the control's own appearance in place is the one honest option, so the box
|
|
305
|
+
* around it is what carries the page's style. */
|
|
306
|
+
.set-select {
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
}
|
|
309
|
+
|
|
231
310
|
.set-link {
|
|
232
311
|
font-family: var(--font-mono);
|
|
233
312
|
font-size: 12px;
|
|
@@ -2,10 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.0.2b0 — 2026-09-22
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
it
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `jev:evaluate` is a known permission — Jev through FailproofAI Cloud, charged to the
|
|
8
|
+
org's plan — so `fp keys` and `fp users` accept it instead of refusing it as unknown,
|
|
9
|
+
and the `admin` preset carries it, as the server's built-in admin set now does. The
|
|
10
|
+
server refuses a KEY carrying it without both `events:add` and `policies:pull` (422,
|
|
11
|
+
naming what to add in `missing_permissions`); the `admin` preset has both. (#833)
|
|
12
|
+
- `fp keys create/update --permission-set machine`: the dashboard's key-only preset
|
|
13
|
+
(`events:add` + `policies:pull` + `jev:evaluate`), for an enrolled machine using Jev through
|
|
14
|
+
FailproofAI Cloud. `fp keys` also refuses `jev:evaluate` without both prerequisites itself
|
|
15
|
+
(exit 2, naming what is missing) instead of sending the key for the server's 422. (#833)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `fp policies publish` (and `policies compose --publish`) refuses a source carrying Jev
|
|
20
|
+
fields a cloud policy never reads — a `semanticPolicies.add(...)` call, or a registration
|
|
21
|
+
declaring `authority: "reviewable"` — before anything is sent (exit 1, `--no-verify` does
|
|
22
|
+
not skip it). Both were published as versions that read Jev-aware and are not: semantic
|
|
23
|
+
checks load only from a pack, and a cloud policy's authority comes from its deployment, so
|
|
24
|
+
it stays hard. Ship Jev checks in a pack with `failproofai publish`. Same rule, and the
|
|
25
|
+
same comment-aware detection, as the dashboard's publish form; the server does not read
|
|
26
|
+
the source, so nothing else refuses it for `fp`. (#833)
|
|
9
27
|
|
|
10
28
|
## 0.0.1 — 2026-09-22
|
|
11
29
|
|
|
@@ -34,6 +34,7 @@ from .errors import (
|
|
|
34
34
|
NetworkError,
|
|
35
35
|
NotFoundError,
|
|
36
36
|
)
|
|
37
|
+
from .policy_check import cloud_publish_problem
|
|
37
38
|
from .models import (
|
|
38
39
|
AgentEvent,
|
|
39
40
|
Alert,
|
|
@@ -1601,7 +1602,19 @@ def list_policies(ctx: ClientContext) -> List[PolicyVersion]:
|
|
|
1601
1602
|
def publish_policy(
|
|
1602
1603
|
ctx: ClientContext, policy_id: str, source: str, description: str = ""
|
|
1603
1604
|
) -> PolicyVersion:
|
|
1604
|
-
"""POST /api/enforcement/policies — mints a NEW VERSION; never edits in place.
|
|
1605
|
+
"""POST /api/enforcement/policies — mints a NEW VERSION; never edits in place.
|
|
1606
|
+
|
|
1607
|
+
Refuses Jev fields a cloud policy never reads before anything is sent, here
|
|
1608
|
+
because `policies publish` and `policies compose --publish` both route through
|
|
1609
|
+
it — and regardless of `--no-verify`, which only skips the syntax check.
|
|
1610
|
+
"""
|
|
1611
|
+
problem = cloud_publish_problem(source)
|
|
1612
|
+
if problem:
|
|
1613
|
+
raise ApiError(
|
|
1614
|
+
f"{policy_id} cannot be published as a cloud policy: {problem}",
|
|
1615
|
+
hint=("Jev checks ship in a failproofai pack — `failproofai publish` — where "
|
|
1616
|
+
"semanticPolicies.add and authority: \"reviewable\" take effect"),
|
|
1617
|
+
)
|
|
1605
1618
|
body = {"id": policy_id, "source": source, "description": description}
|
|
1606
1619
|
return PolicyVersion.from_dict(_post_json(ctx, "/api/enforcement/policies", body) or {})
|
|
1607
1620
|
|
|
@@ -77,12 +77,24 @@ def _expand_key_set_or_exit(state: AppState, cctx, set_name: str) -> List[str]:
|
|
|
77
77
|
return permissions.key_assignable_only(sets[set_name])
|
|
78
78
|
if set_name in permissions.PRESETS:
|
|
79
79
|
return permissions.key_assignable_only(permissions.PRESETS[set_name])
|
|
80
|
-
|
|
80
|
+
if set_name in permissions.KEY_PRESETS:
|
|
81
|
+
return list(permissions.KEY_PRESETS[set_name])
|
|
82
|
+
available = sorted(set(sets) | (set(permissions.PRESETS) - {"clear"}) | set(permissions.KEY_PRESETS))
|
|
81
83
|
raise click.UsageError(
|
|
82
84
|
f'unknown permission set "{set_name}". available: {", ".join(available)}'
|
|
83
85
|
)
|
|
84
86
|
|
|
85
87
|
|
|
88
|
+
def _check_jev_or_exit(perms) -> None:
|
|
89
|
+
"""The server's 422 for jev:evaluate without its prerequisites, as a usage error (exit 2)
|
|
90
|
+
before anything is sent."""
|
|
91
|
+
missing = permissions.jev_requirements_missing(perms)
|
|
92
|
+
if missing:
|
|
93
|
+
raise click.UsageError(
|
|
94
|
+
f"jev:evaluate also requires events:add and policies:pull; missing: {', '.join(missing)}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
86
98
|
def keys_list(
|
|
87
99
|
ctx: typer.Context,
|
|
88
100
|
show_id: bool = typer.Option(False, "--show-id", help="Prepend a short key-id column (the full id is always in --json)."),
|
|
@@ -142,7 +154,7 @@ def keys_show(
|
|
|
142
154
|
def keys_create(
|
|
143
155
|
ctx: typer.Context,
|
|
144
156
|
name: str = typer.Argument(..., help="Human-readable key name (unique within the org)."),
|
|
145
|
-
permission_set: Optional[str] = typer.Option(None, "--permission-set", help="Role to seed the key from — a permission set: `read-only`, `standard`, `admin`, or a custom set your org defines in the dashboard. The set is expanded into the key's grants (human-only perms are dropped). Omit for no base role."),
|
|
157
|
+
permission_set: Optional[str] = typer.Option(None, "--permission-set", help="Role to seed the key from — a permission set: `read-only`, `standard`, `admin`, `machine` (events:add + policies:pull + jev:evaluate), or a custom set your org defines in the dashboard. The set is expanded into the key's grants (human-only perms are dropped). Omit for no base role."),
|
|
146
158
|
add: Optional[List[str]] = typer.Option(None, "--add", help="Grant extra permissions on top of the set, as `slug:action.action` tokens (dotted actions expand: `events:read.add` → `events:read`, `events:add`). Several via comma, repeated flag, or a quoted group: `--add events:read,keys:read` · `--add a --add b` · `--add \"a b\"`."),
|
|
147
159
|
remove: Optional[List[str]] = typer.Option(None, "--remove", help="Drop permissions from the set, same `slug:action.action` token format as --add (comma / repeated / quoted)."),
|
|
148
160
|
) -> None:
|
|
@@ -179,6 +191,7 @@ def keys_create(
|
|
|
179
191
|
raise click.UsageError(f'a key named "{name}" already exists')
|
|
180
192
|
base = _expand_key_set_or_exit(state, cctx, permission_set) if permission_set else []
|
|
181
193
|
flat = sorted((set(base) | set(parsed_add)) - set(parsed_remove))
|
|
194
|
+
_check_jev_or_exit(flat)
|
|
182
195
|
secret = secrets.token_hex(32) # 64 hex chars, mirrors the dashboard's generateToken()
|
|
183
196
|
result = api.create_key(cctx, name=name, key=secret, permissions=flat)
|
|
184
197
|
_write.record_action("api_key_created", resource="key", success=True, permission_count=len(flat))
|
|
@@ -198,7 +211,7 @@ def keys_create(
|
|
|
198
211
|
def keys_update(
|
|
199
212
|
ctx: typer.Context,
|
|
200
213
|
name: str = typer.Argument(..., help="Key name to update (unique within the org)."),
|
|
201
|
-
permission_set: Optional[str] = typer.Option(None, "--permission-set", help="Reseed the key from a permission set: `read-only`, `standard`, `admin`, or a custom org set. REPLACES the key's grants with the set (then applies any --add/--remove). Human-only perms are dropped."),
|
|
214
|
+
permission_set: Optional[str] = typer.Option(None, "--permission-set", help="Reseed the key from a permission set: `read-only`, `standard`, `admin`, `machine`, or a custom org set. REPLACES the key's grants with the set (then applies any --add/--remove). Human-only perms are dropped."),
|
|
202
215
|
add: Optional[List[str]] = typer.Option(None, "--add", help="Grant permissions, same `slug:action.action` token format as `keys create` (dotted actions expand; comma / repeated flag / quoted compose). Incremental — merged into the key's CURRENT grants, unless --permission-set is also given."),
|
|
203
216
|
remove: Optional[List[str]] = typer.Option(None, "--remove", help="Revoke permissions, same `slug:action.action` token format as --add. Incremental — applied to the key's CURRENT grants, unless --permission-set is also given."),
|
|
204
217
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
|
|
@@ -267,6 +280,7 @@ def keys_update(
|
|
|
267
280
|
else:
|
|
268
281
|
output.key_no_change()
|
|
269
282
|
return
|
|
283
|
+
_check_jev_or_exit(after_set)
|
|
270
284
|
|
|
271
285
|
proceed = (not _write.should_prompt(state, yes)) or output.confirm_key_update(
|
|
272
286
|
key.name, len(added), len(removed))
|
|
@@ -116,6 +116,9 @@ def policies_publish(
|
|
|
116
116
|
does this: the server validates the id and a size ceiling, and a broken
|
|
117
117
|
policy otherwise fails on the machine at enforcement time. `--no-verify`
|
|
118
118
|
skips it; a host without node publishes with a warning rather than a block.
|
|
119
|
+
Jev fields a cloud policy never reads (`semanticPolicies.add`, `authority:
|
|
120
|
+
"reviewable"`) are refused regardless — ship those in a pack with
|
|
121
|
+
`failproofai publish`.
|
|
119
122
|
|
|
120
123
|
Source can come from a path, `@path`, a pipe, `-`, or an interactive paste
|
|
121
124
|
when you give none and stdin is a terminal.
|
|
@@ -48,6 +48,12 @@ ALL_PERMISSIONS: List[str] = [
|
|
|
48
48
|
"policies:write",
|
|
49
49
|
"policies:pull",
|
|
50
50
|
"usage:read",
|
|
51
|
+
# Jev through FailproofAI Cloud, charged to the org's plan. The built-in
|
|
52
|
+
# admin set carries it, so PRESETS["admin"] below does too. The server
|
|
53
|
+
# refuses a KEY that carries it without both events:add and policies:pull
|
|
54
|
+
# (422, naming what to add in `missing_permissions`): a Jev key is always a
|
|
55
|
+
# machine's whole connection, never a Jev-only one.
|
|
56
|
+
"jev:evaluate",
|
|
51
57
|
"orgs:admin",
|
|
52
58
|
]
|
|
53
59
|
|
|
@@ -103,6 +109,22 @@ PRESETS = {
|
|
|
103
109
|
"clear": [],
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
# Key-only presets (dashboard/lib/keyPresets.ts). `machine` is an enrolled machine's whole
|
|
113
|
+
# connection: events out, policy in, and Jev through FailproofAI Cloud (charged per call).
|
|
114
|
+
KEY_PRESETS = {"machine": ["events:add", "policies:pull", "jev:evaluate"]}
|
|
115
|
+
|
|
116
|
+
# What a key carrying jev:evaluate must also carry: the server refuses it otherwise (422,
|
|
117
|
+
# `JEV_REQUIRES` in server/src/auth.rs), on create and on an update that drops one.
|
|
118
|
+
JEV_REQUIRES = ["events:add", "policies:pull"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def jev_requirements_missing(perms) -> List[str]:
|
|
122
|
+
"""The Jev prerequisites a key's grant list lacks (empty when it has no jev:evaluate)."""
|
|
123
|
+
if "jev:evaluate" not in perms:
|
|
124
|
+
return []
|
|
125
|
+
return [p for p in JEV_REQUIRES if p not in perms]
|
|
126
|
+
|
|
127
|
+
|
|
106
128
|
_ASSIGNABLE_SET = frozenset(ASSIGNABLE_PERMISSIONS)
|
|
107
129
|
|
|
108
130
|
# Permissions that parse + are user-assignable but must NEVER sit on an API key — the server's
|
|
@@ -288,3 +288,172 @@ def run_policy(
|
|
|
288
288
|
"at import time"),
|
|
289
289
|
)
|
|
290
290
|
return PolicyRun(ok=True, results=results)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ── Jev fields a cloud policy never reads ────────────────────────────────────
|
|
294
|
+
#
|
|
295
|
+
# A port of agenteye's `cloudPublishProblem` (dashboard/lib/policies/policyMeta.ts),
|
|
296
|
+
# kept to the same rule and the same words so the dashboard and this CLI refuse the
|
|
297
|
+
# same sources. The server never reads the source (its 422 is for body fields such
|
|
298
|
+
# as `authority`, which the CLI does not send), so for `fp` this is the only guard.
|
|
299
|
+
# Static by design: this reads source it must never run. Keep the two in step — a
|
|
300
|
+
# change to one is a change to both.
|
|
301
|
+
|
|
302
|
+
_MAX_SCAN = 200_000
|
|
303
|
+
_ADD = "customPolicies.add("
|
|
304
|
+
_ADD_CALL = re.compile(re.escape(_ADD))
|
|
305
|
+
#: The whitespace JavaScript allows around `.` and `(`.
|
|
306
|
+
_SEMANTIC_CALL = re.compile(r"semanticPolicies\s*\.\s*add\s*\(")
|
|
307
|
+
#: After one of these, `/` is division, not the start of a regex literal.
|
|
308
|
+
_VALUE_END = re.compile(r"[A-Za-z0-9_$)\]]")
|
|
309
|
+
_KEY = re.compile(r"([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _skip_trivia(s: str, i: int) -> int:
|
|
313
|
+
"""Index of the next character that is code: past whitespace and comments."""
|
|
314
|
+
while True:
|
|
315
|
+
while i < len(s) and s[i].isspace():
|
|
316
|
+
i += 1
|
|
317
|
+
if s.startswith("//", i):
|
|
318
|
+
nl = s.find("\n", i)
|
|
319
|
+
if nl == -1:
|
|
320
|
+
return len(s)
|
|
321
|
+
i = nl + 1
|
|
322
|
+
elif s.startswith("/*", i):
|
|
323
|
+
end = s.find("*/", i + 2)
|
|
324
|
+
if end == -1:
|
|
325
|
+
return len(s)
|
|
326
|
+
i = end + 2
|
|
327
|
+
else:
|
|
328
|
+
return i
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _end_of_regex(s: str, i: int, prev: str) -> int:
|
|
332
|
+
"""Index past the regex literal at `i`, or -1 if none starts (or closes) there."""
|
|
333
|
+
if s[i] != "/" or _VALUE_END.match(prev):
|
|
334
|
+
return -1
|
|
335
|
+
j, in_class = i + 1, False
|
|
336
|
+
while j < len(s):
|
|
337
|
+
c = s[j]
|
|
338
|
+
if c == "\\":
|
|
339
|
+
j += 2
|
|
340
|
+
continue
|
|
341
|
+
if c == "\n":
|
|
342
|
+
return -1
|
|
343
|
+
if c == "[":
|
|
344
|
+
in_class = True
|
|
345
|
+
elif c == "]":
|
|
346
|
+
in_class = False
|
|
347
|
+
elif c == "/" and not in_class:
|
|
348
|
+
j += 1
|
|
349
|
+
while j < len(s) and "a" <= s[j] <= "z":
|
|
350
|
+
j += 1
|
|
351
|
+
return j
|
|
352
|
+
j += 1
|
|
353
|
+
return -1
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _end_of_string(s: str, i: int) -> int:
|
|
357
|
+
"""Index past the string literal at `i`, or -1 if it never closes."""
|
|
358
|
+
q, j = s[i], i + 1
|
|
359
|
+
while j < len(s):
|
|
360
|
+
if s[j] == "\\":
|
|
361
|
+
j += 2
|
|
362
|
+
continue
|
|
363
|
+
if s[j] == q:
|
|
364
|
+
return j + 1
|
|
365
|
+
j += 1
|
|
366
|
+
return -1
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _find_call(s: str, call: "re.Pattern[str]", start: int = 0) -> int:
|
|
370
|
+
"""Index of the first `call` that is code — not in a comment, string or regex."""
|
|
371
|
+
i, prev = start, ("(" if start > 0 else "")
|
|
372
|
+
for _ in range(_MAX_SCAN):
|
|
373
|
+
i = _skip_trivia(s, i)
|
|
374
|
+
if i >= len(s):
|
|
375
|
+
return -1
|
|
376
|
+
if call.match(s, i):
|
|
377
|
+
return i
|
|
378
|
+
end = _end_of_regex(s, i, prev)
|
|
379
|
+
if end != -1:
|
|
380
|
+
i, prev = end, "/"
|
|
381
|
+
continue
|
|
382
|
+
if s[i] in "\"'`":
|
|
383
|
+
end = _end_of_string(s, i)
|
|
384
|
+
if end == -1:
|
|
385
|
+
return -1 # unterminated: nothing after it is readable
|
|
386
|
+
i, prev = end, '"'
|
|
387
|
+
continue
|
|
388
|
+
prev = s[i]
|
|
389
|
+
i += 1
|
|
390
|
+
return -1
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _direct_props(s: str, at: int) -> Dict[str, str]:
|
|
394
|
+
"""The static string properties of the object passed to the `customPolicies.add(`
|
|
395
|
+
at `at` — direct ones only, never a key in `match`, `params` or the fn body.
|
|
396
|
+
Fails closed: anything unbalanced or unterminated reads as nothing."""
|
|
397
|
+
out: Dict[str, str] = {}
|
|
398
|
+
i = _skip_trivia(s, at + len(_ADD))
|
|
399
|
+
if i >= len(s) or s[i] != "{":
|
|
400
|
+
return out
|
|
401
|
+
i, depth, prev = i + 1, 1, "{"
|
|
402
|
+
while i < len(s) and depth > 0:
|
|
403
|
+
i = _skip_trivia(s, i)
|
|
404
|
+
if i >= len(s):
|
|
405
|
+
break
|
|
406
|
+
c = s[i]
|
|
407
|
+
end = _end_of_regex(s, i, prev)
|
|
408
|
+
if end != -1:
|
|
409
|
+
i, prev = end, "/"
|
|
410
|
+
continue
|
|
411
|
+
if c in "\"'`":
|
|
412
|
+
end = _end_of_string(s, i)
|
|
413
|
+
if end == -1:
|
|
414
|
+
return {}
|
|
415
|
+
i, prev = end, '"'
|
|
416
|
+
continue
|
|
417
|
+
if c in "{[(" or c in "}])":
|
|
418
|
+
depth += 1 if c in "{[(" else -1
|
|
419
|
+
i, prev = i + 1, c
|
|
420
|
+
continue
|
|
421
|
+
if depth == 1 and prev in ("{", ","):
|
|
422
|
+
m = _KEY.match(s, i, i + 64)
|
|
423
|
+
if m:
|
|
424
|
+
i = _skip_trivia(s, m.end())
|
|
425
|
+
q = s[i] if i < len(s) else ""
|
|
426
|
+
end = _end_of_string(s, i) if q in ("\"", "'", "`") else -1
|
|
427
|
+
raw = s[i + 1:end - 1] if end != -1 else ""
|
|
428
|
+
# Escapes and interpolation are not knowable statically: absent.
|
|
429
|
+
if raw.strip() and "\\" not in raw and not (q == "`" and "${" in raw):
|
|
430
|
+
out.setdefault(m.group(1), raw.strip()) # first wins
|
|
431
|
+
prev = ":"
|
|
432
|
+
continue
|
|
433
|
+
prev = c
|
|
434
|
+
i += 1
|
|
435
|
+
return out if depth == 0 else {}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def cloud_publish_problem(source: str) -> Optional[str]:
|
|
439
|
+
"""Why this source cannot be published as a cloud policy as written, or None.
|
|
440
|
+
|
|
441
|
+
Both halves of Jev are inert in a cloud policy and neither says so on the
|
|
442
|
+
machine: `semanticPolicies.add` is read only from an installed pack, and a
|
|
443
|
+
cloud policy's authority comes from its deployment, which never sets it. A
|
|
444
|
+
commented-out example does not count.
|
|
445
|
+
"""
|
|
446
|
+
if not source or len(source) > _MAX_SCAN:
|
|
447
|
+
return None
|
|
448
|
+
if _find_call(source, _SEMANTIC_CALL) != -1:
|
|
449
|
+
return ("semanticPolicies.add does nothing in a cloud policy: Jev reads semantic checks only "
|
|
450
|
+
"from a failproofai pack. Remove it to publish the rest, or ship the file with "
|
|
451
|
+
"`failproofai publish`.")
|
|
452
|
+
# Every registration: any of them can carry a reviewable declaration.
|
|
453
|
+
at = _find_call(source, _ADD_CALL)
|
|
454
|
+
while at != -1:
|
|
455
|
+
if _direct_props(source, at).get("authority") == "reviewable":
|
|
456
|
+
return ("a cloud policy is always hard, so Jev never clears it and authority/reviewedBy "
|
|
457
|
+
"are ignored. Remove them to publish it as a hard policy.")
|
|
458
|
+
at = _find_call(source, _ADD_CALL, at + len(_ADD))
|
|
459
|
+
return None
|
|
@@ -222,7 +222,7 @@ Cloud-managed enforcement, split the way the dashboard splits it: `policies` wri
|
|
|
222
222
|
### policies
|
|
223
223
|
- `policies list` — one row per published VERSION (versions are immutable and all stay addressable), newest of each policy first; the title counts distinct policies and captions the version total. `state` is active / disabled / archived. JSON `{policies:[…]}` — also every version, so deduplicate on `id` for one row per policy.
|
|
224
224
|
- `policies show <id>` — the NEWEST version, including the full `source`.
|
|
225
|
-
- `policies publish <id> [SOURCE] [--description "…"] [--no-verify]` — mints a **new version**; never edits one. SOURCE is a path, `@path`, `-`, a pipe, or omitted to paste on a TTY (Ctrl-D ends). The source is **parse-checked with node** before it is sent; nothing downstream does this, and a broken policy otherwise fails on the machine at enforcement time. `--no-verify` skips it, and a host without node publishes with a warning rather than a block. **Publishing deploys nothing** — the version is unused until `fleet deploy` puts it on a machine.
|
|
225
|
+
- `policies publish <id> [SOURCE] [--description "…"] [--no-verify]` — mints a **new version**; never edits one. SOURCE is a path, `@path`, `-`, a pipe, or omitted to paste on a TTY (Ctrl-D ends). The source is **parse-checked with node** before it is sent; nothing downstream does this, and a broken policy otherwise fails on the machine at enforcement time. `--no-verify` skips it, and a host without node publishes with a warning rather than a block. A source with Jev fields a cloud policy never reads — `semanticPolicies.add(...)`, or `authority: "reviewable"` on a registration — is refused before sending (**exit 1**, not skipped by `--no-verify`; commented-out code does not count): Jev checks ship in a pack via `failproofai publish`. **Publishing deploys nothing** — the version is unused until `fleet deploy` puts it on a machine.
|
|
226
226
|
- `policies enable <id>` · `policies disable <id> [-y]` — **disable REMOVES the policy from every deployment carrying it**, reissuing each affected machine at a new generation (visible in `fleet history`). `enable` is the exact inverse — it puts the policy back into every deployment it was removed from, reissuing those machines again. Nothing needs redeploying by hand, and `machinesUpdated` in the JSON reports the count for both directions.
|
|
227
227
|
- `policies test [SOURCE] [--tool Bash] [--command "…"] [--file PATH] [--event PreToolUse] [--expect allow|deny|instruct]` — run a policy LOCALLY and print what it decides. Executes the real file (bare `import { deny } from "failproofai"` and all) against a synthetic context; nothing is published, nothing installed. Needs `node`. `--expect` asserts the decision and exits 1 when it differs — a correct `deny` is a PASSING test, so the decision alone never sets the exit code. JSON `{ok, decision, policies:[{name,decision,reason}], expected, met}`; `decision` is the strictest any registered policy returned.
|
|
228
228
|
- `policies compose "<description>" [--out FILE] [--publish ID]` — the assistant drafts policy source from plain English. Prints it and stops by default: a generated policy that deploys itself is one nobody read. `--publish` still syntax-checks first. Needs `policies:write` for BOTH the draft and the publish — `POST /api/agent/compose-policy` is `withAuth("policies:write")`; `agent:use` gates the assistant's other routes and is not checked on this one. The composer has a **30s server-side limit** — a long or vague description simply does not finish, and raising `--timeout` does not help because the cut is not client-side. Retry with something shorter and more specific.
|
|
@@ -34,6 +34,35 @@ def test_policy_permissions_are_assignable_and_in_presets():
|
|
|
34
34
|
assert "policies:read" in PRESETS["standard"]
|
|
35
35
|
assert "policies:pull" in PRESETS["admin"]
|
|
36
36
|
|
|
37
|
+
|
|
38
|
+
def test_jev_permission_is_assignable_and_in_the_admin_preset_only():
|
|
39
|
+
from fp_cli.permissions import (
|
|
40
|
+
ALL_PERMISSIONS,
|
|
41
|
+
ASSIGNABLE_PERMISSIONS,
|
|
42
|
+
KEY_ASSIGNABLE_PERMISSIONS,
|
|
43
|
+
PRESETS,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert "jev:evaluate" in ALL_PERMISSIONS
|
|
47
|
+
# In the server's declared order: after usage:read, and orgs:admin stays last.
|
|
48
|
+
assert ALL_PERMISSIONS.index("jev:evaluate") == ALL_PERMISSIONS.index("usage:read") + 1
|
|
49
|
+
assert ALL_PERMISSIONS[-1] == "orgs:admin"
|
|
50
|
+
# A machine key carries it, so it is grantable to a key as well as a member.
|
|
51
|
+
assert "jev:evaluate" in ASSIGNABLE_PERMISSIONS
|
|
52
|
+
assert "jev:evaluate" in KEY_ASSIGNABLE_PERMISSIONS
|
|
53
|
+
# The server's built-in admin set carries it; nothing narrower does.
|
|
54
|
+
assert "jev:evaluate" in PRESETS["admin"]
|
|
55
|
+
assert "jev:evaluate" not in PRESETS["read-only"]
|
|
56
|
+
assert "jev:evaluate" not in PRESETS["standard"]
|
|
57
|
+
# And the admin preset can be granted to a key: it carries both permissions
|
|
58
|
+
# the server requires beside jev:evaluate.
|
|
59
|
+
assert {"events:add", "policies:pull"} <= set(PRESETS["admin"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_parse_permissions_accepts_jev_evaluate():
|
|
63
|
+
assert _parse_permissions(["events:add policies:pull jev:evaluate"]) == [
|
|
64
|
+
"events:add", "policies:pull", "jev:evaluate"]
|
|
65
|
+
|
|
37
66
|
BASE = "http://dash.test"
|
|
38
67
|
|
|
39
68
|
|
|
@@ -111,6 +140,42 @@ def test_keys_create_rejects_malformed_token(logged_in, runner):
|
|
|
111
140
|
assert runner.invoke(app, ["keys", "create", "k", "--add", "events:"]).exit_code == 2
|
|
112
141
|
|
|
113
142
|
|
|
143
|
+
@respx.mock
|
|
144
|
+
def test_keys_create_machine_preset(logged_in, runner):
|
|
145
|
+
# The dashboard's key-only `machine` preset: collect, pull policy, Jev.
|
|
146
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[]))
|
|
147
|
+
respx.get(f"{BASE}/api/permission-sets").mock(return_value=httpx.Response(200, json=[]))
|
|
148
|
+
route = respx.post(f"{BASE}/api/keys").mock(
|
|
149
|
+
return_value=httpx.Response(201, json={"id": "k9", "name": "m", "permissions": [], "created_at": "t"})
|
|
150
|
+
)
|
|
151
|
+
result = runner.invoke(app, ["--json", "keys", "create", "m", "--permission-set", "machine"])
|
|
152
|
+
assert result.exit_code == 0, result.output
|
|
153
|
+
assert json.loads(route.calls.last.request.content)["permissions"] == ["events:add", "jev:evaluate", "policies:pull"]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@respx.mock
|
|
157
|
+
def test_keys_create_refuses_jev_without_its_prerequisites(logged_in, runner):
|
|
158
|
+
# The server's 422 (JEV_REQUIRES), said before anything is sent.
|
|
159
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[]))
|
|
160
|
+
route = respx.post(f"{BASE}/api/keys").mock(return_value=httpx.Response(201, json={}))
|
|
161
|
+
result = runner.invoke(app, ["--json", "keys", "create", "k", "--add", "jev:evaluate,events:add"])
|
|
162
|
+
assert result.exit_code == 2, result.output
|
|
163
|
+
assert "missing: policies:pull" in json.loads(result.stdout)["error"]
|
|
164
|
+
assert not route.called
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@respx.mock
|
|
168
|
+
def test_keys_update_refuses_dropping_a_jev_prerequisite(logged_in, runner):
|
|
169
|
+
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
170
|
+
{"id": "k1", "name": "m", "permissions": ["events:add", "jev:evaluate", "policies:pull"],
|
|
171
|
+
"created_at": "t", "revoked_at": None}]))
|
|
172
|
+
route = respx.patch(f"{BASE}/api/keys/k1").mock(return_value=httpx.Response(200, json={}))
|
|
173
|
+
result = runner.invoke(app, ["--json", "keys", "update", "m", "--remove", "events:add", "--yes"])
|
|
174
|
+
assert result.exit_code == 2, result.output
|
|
175
|
+
assert "missing: events:add" in json.loads(result.stdout)["error"]
|
|
176
|
+
assert not route.called
|
|
177
|
+
|
|
178
|
+
|
|
114
179
|
@respx.mock
|
|
115
180
|
def test_keys_create_name_collision(logged_in, runner):
|
|
116
181
|
respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
|
|
@@ -189,3 +189,64 @@ def test_parsing_and_executing_have_separate_budgets():
|
|
|
189
189
|
|
|
190
190
|
assert policy_check._TIMEOUT_SECS == 5
|
|
191
191
|
assert policy_check._SYNTAX_TIMEOUT_SECS > policy_check._TIMEOUT_SECS
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── Jev fields a cloud policy never reads ────────────────────────────────────
|
|
195
|
+
#
|
|
196
|
+
# `semanticPolicies.add` is read only from a pack built by `failproofai publish`,
|
|
197
|
+
# and a cloud policy's authority comes from its deployment record, which never
|
|
198
|
+
# sets it — so both publish as a version that reads Jev-aware and is not. The
|
|
199
|
+
# dashboard refuses them (agenteye `dashboard/lib/policies/policyMeta.ts`,
|
|
200
|
+
# `cloudPublishProblem`); the server never reads the source. These cases are the
|
|
201
|
+
# dashboard's own, so the two surfaces agree on what counts.
|
|
202
|
+
|
|
203
|
+
JEV_BASE = '''import { customPolicies, allow } from "failproofai";
|
|
204
|
+
|
|
205
|
+
customPolicies.add({
|
|
206
|
+
name: "block-wrong-jira-project",
|
|
207
|
+
description: "Deny Jira writes outside the configured project",
|
|
208
|
+
match: { events: ["PreToolUse"] },
|
|
209
|
+
fn: async (ctx) => allow(),
|
|
210
|
+
});
|
|
211
|
+
'''
|
|
212
|
+
_SECOND = JEV_BASE.split("\n", 1)[1].replace("match:", 'authority: "reviewable",\n match:')
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@pytest.mark.parametrize("src,needle", [
|
|
216
|
+
(JEV_BASE, None),
|
|
217
|
+
(JEV_BASE + '\nsemanticPolicies.add({ name: "db-migration-on-production" });\n', "failproofai pack"),
|
|
218
|
+
(JEV_BASE.replace("match:", 'authority: "reviewable", reviewedBy: ["destructive-deletion"],\n match:'),
|
|
219
|
+
"always hard"),
|
|
220
|
+
('// semanticPolicies.add({ name: "later" })\n' + JEV_BASE, None),
|
|
221
|
+
('/* customPolicies.add({ authority: "reviewable" }) */\n' + JEV_BASE, None),
|
|
222
|
+
('const s = "semanticPolicies.add(";\n' + JEV_BASE, None),
|
|
223
|
+
(JEV_BASE.replace("allow()", 'allow("authority: reviewable")'), None),
|
|
224
|
+
(JEV_BASE + '\nsemanticPolicies\n .add ({ name: "db-migration-on-production" });\n', "failproofai pack"),
|
|
225
|
+
(JEV_BASE + "\n" + _SECOND, "always hard"),
|
|
226
|
+
], ids=["plain", "semantic", "reviewable", "commented-semantic", "commented-reviewable",
|
|
227
|
+
"string-literal", "nested-text", "split-spelling", "later-registration"])
|
|
228
|
+
def test_jev_fields_are_refused_the_way_the_dashboard_refuses_them(src, needle):
|
|
229
|
+
from fp_cli.policy_check import cloud_publish_problem
|
|
230
|
+
|
|
231
|
+
problem = cloud_publish_problem(src)
|
|
232
|
+
if needle is None:
|
|
233
|
+
assert problem is None
|
|
234
|
+
else:
|
|
235
|
+
assert problem and needle in problem
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_publish_refuses_jev_fields_before_anything_is_sent(monkeypatch):
|
|
239
|
+
"""Both `policies publish` and `policies compose --publish` route through
|
|
240
|
+
`client.publish_policy`, so the refusal lives there — and must fire before
|
|
241
|
+
the POST, whatever `--no-verify` said about the syntax check."""
|
|
242
|
+
from fp_cli import client
|
|
243
|
+
from fp_cli.errors import ApiError
|
|
244
|
+
|
|
245
|
+
def _no_post(*a, **kw):
|
|
246
|
+
raise AssertionError("a refused source reached the server")
|
|
247
|
+
|
|
248
|
+
monkeypatch.setattr(client, "_post_json", _no_post)
|
|
249
|
+
src = JEV_BASE + '\nsemanticPolicies.add({ name: "db-migration-on-production" });\n'
|
|
250
|
+
with pytest.raises(ApiError) as exc:
|
|
251
|
+
client.publish_policy(None, "p", src)
|
|
252
|
+
assert "failproofai publish" in str(exc.value) + (exc.value.hint or "")
|