failproofai 1.0.7-beta.2 → 1.0.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +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.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.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 +5 -5
- 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 +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 +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.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 +6 -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/server-reference-manifest.json +1 -1
- 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 +2 -2
- 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.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 +18 -7
- package/.next/standalone/.next/server/app/settings/page.js +9 -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]__1j-zsg5._.js → [externals]__20kzpkf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
- 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 +20 -20
- 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 +9 -9
- package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
- 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 +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
- 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 +12 -0
- 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 +33 -22
- package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
- package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
- package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
- package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
- package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
- package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
- package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
- package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/get-jev-config.ts +194 -14
- package/.next/standalone/app/actions/update-jev-config.ts +160 -11
- package/.next/standalone/app/policies/hooks-client.tsx +5 -3
- package/.next/standalone/app/settings/jev-panel.tsx +169 -12
- 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 +9 -9
- 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 +15 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
- package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +1 -0
- package/bin/failproofai.mjs +37 -6
- package/dist/cli.mjs +2861 -1145
- package/dist/worker.mjs +815 -197
- package/package.json +9 -9
- package/src/hooks/builtin-policies.ts +24 -6
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +56 -7
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +58 -8
- package/src/hooks/effective-reviewers.ts +172 -8
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +44 -7
- package/src/hooks/hook-activity-store.ts +13 -5
- package/src/hooks/jev-cli.ts +1009 -86
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/manager.ts +44 -6
- package/src/hooks/pack-cli.ts +175 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +11 -1
- package/src/hooks/pack-store.ts +47 -10
- package/src/hooks/policy-authority.ts +73 -20
- package/src/hooks/policy-catalog.ts +10 -8
- package/src/hooks/policy-evaluator.ts +31 -8
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +41 -12
- package/src/hooks/policy-types.ts +5 -3
- package/src/hooks/semantic/combine.ts +124 -84
- package/src/hooks/semantic/decide.ts +16 -0
- package/src/hooks/semantic/envelope.ts +18 -59
- package/src/hooks/semantic/evaluator.ts +11 -2
- package/src/hooks/semantic/facts.ts +7 -1
- package/src/hooks/semantic/intent.ts +8 -8
- package/src/hooks/semantic/jev-client.ts +540 -28
- package/src/hooks/semantic/jev-config.ts +586 -38
- package/src/hooks/semantic/jev-review.ts +26 -11
- package/src/hooks/semantic/jev-throttle.ts +8 -2
- package/src/hooks/semantic/pack-policies.ts +58 -15
- package/src/hooks/semantic/policies.ts +4 -3
- package/src/hooks/semantic/redact.ts +28 -1
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +11 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/worker-server.ts +2 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.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/1qd741hzlmjbo.js +0 -1
- package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
- package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
- package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
|
@@ -47,6 +47,21 @@
|
|
|
47
47
|
* Client-side validation here is a convenience only. Every rule — the URL
|
|
48
48
|
* scheme, plain http being refused outside shadow mode, cloudflare's account id
|
|
49
49
|
* — is enforced server-side by the same `validateJevConfig` the loader runs.
|
|
50
|
+
*
|
|
51
|
+
* ## FailproofAI Cloud
|
|
52
|
+
*
|
|
53
|
+
* When `jev.json` names the FailproofAI Cloud provider, the endpoint and the
|
|
54
|
+
* key are not this page's to edit: both come from the machine's connection
|
|
55
|
+
* (`config --token`). So the form is replaced by the two controls that are
|
|
56
|
+
* the owner's — an on/off switch and shadow/enforce — both through
|
|
57
|
+
* `setJevModeAction`, which rewrites `mode` and nothing else. "Off" there keeps
|
|
58
|
+
* the file (`mode: "off"`): deleting it would leave nothing on this page to
|
|
59
|
+
* switch back on — the only way to get the file back would be re-running
|
|
60
|
+
* `config --token` with the key.
|
|
61
|
+
*
|
|
62
|
+
* The connection itself — org, and whether the key carries Jev — is one row,
|
|
63
|
+
* read from `credentials.json` by the server with no network call; the key is
|
|
64
|
+
* never part of what this component receives.
|
|
50
65
|
*/
|
|
51
66
|
|
|
52
67
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
@@ -58,6 +73,7 @@ import {
|
|
|
58
73
|
import {
|
|
59
74
|
removeJevConfigAction,
|
|
60
75
|
saveJevConfigAction,
|
|
76
|
+
setJevModeAction,
|
|
61
77
|
} from "@/app/actions/update-jev-config";
|
|
62
78
|
import { toast } from "@/app/components/toast";
|
|
63
79
|
|
|
@@ -72,8 +88,32 @@ const PROVIDERS = [
|
|
|
72
88
|
const MODES = [
|
|
73
89
|
{ value: "enforce", label: "enforce — jev's verdict counts" },
|
|
74
90
|
{ value: "shadow", label: "shadow — log only, regex decides" },
|
|
91
|
+
{ value: "off", label: "off — keep this config, don't ask jev" },
|
|
75
92
|
] as const;
|
|
76
93
|
|
|
94
|
+
/** The provider whose endpoint and key come from the FailproofAI Cloud connection. */
|
|
95
|
+
const CLOUD_PROVIDER = "failproofai";
|
|
96
|
+
|
|
97
|
+
/** What a provider is called on this page. */
|
|
98
|
+
function providerName(provider: string | null): string {
|
|
99
|
+
return provider === CLOUD_PROVIDER ? "FailproofAI Cloud" : (provider ?? "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The connection row: org, and whether the key carries Jev. No key, ever. */
|
|
103
|
+
function fmtConnection(cloud: JevSettingsView["cloud"] | undefined): string {
|
|
104
|
+
if (!cloud || !cloud.connected) {
|
|
105
|
+
return cloud?.jev === "yes" ? "a jev key is stored, but no connection is" : "not connected";
|
|
106
|
+
}
|
|
107
|
+
const where = cloud.org ?? cloud.host ?? "connected";
|
|
108
|
+
const jev =
|
|
109
|
+
cloud.jev === "yes"
|
|
110
|
+
? "key carries jev"
|
|
111
|
+
: cloud.jev === "refused"
|
|
112
|
+
? "jev key refused — credentials.json is not owner-only"
|
|
113
|
+
: "no jev key stored";
|
|
114
|
+
return `connected to ${where} · ${jev}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
/** "24h" / "7d" / "30m", for the stats window. */
|
|
78
118
|
function fmtWindow(ms: number): string {
|
|
79
119
|
const h = ms / 3_600_000;
|
|
@@ -113,7 +153,8 @@ interface FormState {
|
|
|
113
153
|
|
|
114
154
|
function formFrom(view: JevSettingsView): FormState {
|
|
115
155
|
return {
|
|
116
|
-
|
|
156
|
+
// The form is the BYOK form: the Cloud provider is never one of its values.
|
|
157
|
+
provider: view.provider && view.provider !== CLOUD_PROVIDER ? view.provider : "typesafe",
|
|
117
158
|
baseUrl: view.baseUrl,
|
|
118
159
|
accountId: view.accountId,
|
|
119
160
|
mode: view.mode,
|
|
@@ -237,6 +278,30 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
237
278
|
}
|
|
238
279
|
}, [form, token]);
|
|
239
280
|
|
|
281
|
+
/**
|
|
282
|
+
* The FailproofAI Cloud switch: `off`, `shadow` or `enforce`, and nothing
|
|
283
|
+
* else about the file changes (see `setJevModeAction`).
|
|
284
|
+
*/
|
|
285
|
+
const onMode = useCallback(async (mode: "off" | "shadow" | "enforce") => {
|
|
286
|
+
setBusy(true);
|
|
287
|
+
setProblem(null);
|
|
288
|
+
try {
|
|
289
|
+
const res = await setJevModeAction(mode);
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
setProblem(res.problem);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
dirty.current = false;
|
|
295
|
+
setView(res.view);
|
|
296
|
+
setForm(formFrom(res.view));
|
|
297
|
+
toast(mode === "off" ? "jev is off. hooks run the regex policies." : `jev is on, in ${mode} mode.`);
|
|
298
|
+
} catch {
|
|
299
|
+
setProblem("could not switch that.");
|
|
300
|
+
} finally {
|
|
301
|
+
setBusy(false);
|
|
302
|
+
}
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
240
305
|
const onRemove = useCallback(async () => {
|
|
241
306
|
setBusy(true);
|
|
242
307
|
setProblem(null);
|
|
@@ -260,11 +325,14 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
260
325
|
}, []);
|
|
261
326
|
|
|
262
327
|
const configured = view !== null && view.status !== "absent";
|
|
328
|
+
const cloudRoute = view?.provider === CLOUD_PROVIDER;
|
|
263
329
|
const stored = view?.token;
|
|
264
330
|
const tokenHint = needsToken
|
|
265
331
|
? "needed for this save — type the token for this endpoint"
|
|
266
332
|
: stored
|
|
267
|
-
? stored.source === "
|
|
333
|
+
? stored.source === "cloud"
|
|
334
|
+
? "from this machine's FailproofAI Cloud connection"
|
|
335
|
+
: stored.source === "env"
|
|
268
336
|
? "configured, read from the environment — leave blank to keep it"
|
|
269
337
|
: // Presence and the keep rule, which is everything this field's reader
|
|
270
338
|
// has to decide. Naming the last four characters of the stored key
|
|
@@ -289,15 +357,27 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
289
357
|
{view === null
|
|
290
358
|
? "reading…"
|
|
291
359
|
: view.on
|
|
292
|
-
? `on. every judged command is also asked of ${view.provider}.`
|
|
293
|
-
:
|
|
360
|
+
? `on. every judged command is also asked of ${providerName(view.provider)}.`
|
|
361
|
+
: view.status === "off"
|
|
362
|
+
? "off — switched off. hooks run the regex policies exactly as before."
|
|
363
|
+
: view.status === "not-connected"
|
|
364
|
+
? "off — this machine is not connected to FailproofAI Cloud. hooks run the regex policies."
|
|
365
|
+
: view.status === "key-lacks-jev"
|
|
366
|
+
? "off — no jev key is stored for this machine's FailproofAI Cloud connection. hooks run the regex policies."
|
|
367
|
+
: view.status === "key-missing"
|
|
368
|
+
? // The CLI's "off in this shell": the key is read by whatever
|
|
369
|
+
// runs the hook, and this server's environment is not that.
|
|
370
|
+
"off for this dashboard — FAILPROOFAI_JEV_API_KEY is not set in its environment. hooks run the regex policies wherever it is unset, including under the daemon; a session that sets it still consults jev."
|
|
371
|
+
: "off. hooks run the regex policies exactly as before."}
|
|
294
372
|
</span>
|
|
295
373
|
<span className="set-dim">
|
|
296
374
|
{view?.on
|
|
297
375
|
? view.mode === "shadow"
|
|
298
376
|
? "shadow: jev's answers are logged, the regex result is what gets enforced."
|
|
299
377
|
: "enforce: jev's answers can clear a reviewable deny."
|
|
300
|
-
:
|
|
378
|
+
: cloudRoute
|
|
379
|
+
? "through FailproofAI Cloud, on your org's plan — no endpoint or token of your own."
|
|
380
|
+
: "a second opinion on the commands the regex policies flag. needs your own endpoint and token."}
|
|
301
381
|
</span>
|
|
302
382
|
</div>
|
|
303
383
|
</div>
|
|
@@ -308,8 +388,28 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
308
388
|
same per-account address, and the account id inside it, a second time
|
|
309
389
|
on one screen. The path of the config file and its mode were here too;
|
|
310
390
|
nobody acts on either from a browser. */}
|
|
391
|
+
{/* The FailproofAI Cloud connection, on every state: whether this
|
|
392
|
+
machine could run Jev through the Cloud is worth knowing before it
|
|
393
|
+
does. Org and a yes/no, never a key. */}
|
|
394
|
+
{view && (
|
|
395
|
+
<dl className="set-how-list set-jev-cloud">
|
|
396
|
+
<div className="set-how-row">
|
|
397
|
+
<dt className="set-how-label">FailproofAI Cloud connection</dt>
|
|
398
|
+
<dd className="set-how-body">{fmtConnection(view.cloud)}</dd>
|
|
399
|
+
</div>
|
|
400
|
+
</dl>
|
|
401
|
+
)}
|
|
402
|
+
|
|
311
403
|
{view && configured && (
|
|
312
404
|
<dl className="set-how-list">
|
|
405
|
+
{cloudRoute && (
|
|
406
|
+
<div className="set-how-row">
|
|
407
|
+
<dt className="set-how-label">provider</dt>
|
|
408
|
+
<dd className="set-how-body">
|
|
409
|
+
{view.cloud?.host ? `FailproofAI Cloud · ${view.cloud.host}` : "FailproofAI Cloud"}
|
|
410
|
+
</dd>
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
313
413
|
<div className="set-how-row">
|
|
314
414
|
<dt className="set-how-label">model</dt>
|
|
315
415
|
<dd className="set-how-body">{fmtModel(view.model)}</dd>
|
|
@@ -322,10 +422,16 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
322
422
|
<dt className="set-how-label">token</dt>
|
|
323
423
|
<dd className="set-how-body">
|
|
324
424
|
{stored
|
|
325
|
-
? stored.source === "
|
|
326
|
-
? "
|
|
327
|
-
: "
|
|
328
|
-
|
|
425
|
+
? stored.source === "cloud"
|
|
426
|
+
? "FailproofAI Cloud connection"
|
|
427
|
+
: stored.source === "env"
|
|
428
|
+
? "from FAILPROOFAI_JEV_API_KEY"
|
|
429
|
+
: "configured"
|
|
430
|
+
: cloudRoute
|
|
431
|
+
? view.cloud?.connected
|
|
432
|
+
? "none stored for this connection"
|
|
433
|
+
: "none — this machine is not connected"
|
|
434
|
+
: "none stored"}
|
|
329
435
|
</dd>
|
|
330
436
|
</div>
|
|
331
437
|
{view.reviewable && (
|
|
@@ -366,6 +472,45 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
366
472
|
|
|
367
473
|
<div className="set-rule" />
|
|
368
474
|
|
|
475
|
+
{cloudRoute ? (
|
|
476
|
+
<>
|
|
477
|
+
<div className="set-jev-fields">
|
|
478
|
+
<Field
|
|
479
|
+
id="jev-cloud-mode"
|
|
480
|
+
label="mode"
|
|
481
|
+
hint="the endpoint and key come from this machine's FailproofAI Cloud connection"
|
|
482
|
+
>
|
|
483
|
+
<select
|
|
484
|
+
id="jev-cloud-mode"
|
|
485
|
+
className="set-select"
|
|
486
|
+
value={view?.mode === "enforce" ? "enforce" : "shadow"}
|
|
487
|
+
disabled={busy || view?.mode === "off"}
|
|
488
|
+
onChange={(e) => void onMode(e.target.value === "enforce" ? "enforce" : "shadow")}
|
|
489
|
+
>
|
|
490
|
+
{MODES.filter((m) => m.value !== "off").map((m) => (
|
|
491
|
+
<option key={m.value} value={m.value}>
|
|
492
|
+
{m.label}
|
|
493
|
+
</option>
|
|
494
|
+
))}
|
|
495
|
+
</select>
|
|
496
|
+
</Field>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
{problem && <p className="set-warn">{problem}</p>}
|
|
500
|
+
|
|
501
|
+
<div className="set-actions set-jev-actions">
|
|
502
|
+
<button
|
|
503
|
+
type="button"
|
|
504
|
+
className="btn btn-press"
|
|
505
|
+
disabled={busy}
|
|
506
|
+
onClick={() => void onMode(view?.mode === "off" ? "shadow" : "off")}
|
|
507
|
+
>
|
|
508
|
+
{busy ? "[ switching… ]" : view?.mode === "off" ? "[ turn jev on ]" : "[ turn jev off ]"}
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
</>
|
|
512
|
+
) : (
|
|
513
|
+
<>
|
|
369
514
|
<div className="set-jev-fields">
|
|
370
515
|
<Field id="jev-provider" label="provider">
|
|
371
516
|
<select
|
|
@@ -386,11 +531,21 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
386
531
|
<Field
|
|
387
532
|
id="jev-url"
|
|
388
533
|
label="endpoint url"
|
|
389
|
-
hint={
|
|
534
|
+
hint={[
|
|
390
535
|
form.provider === "custom"
|
|
391
536
|
? "required — https, or http to localhost in shadow mode"
|
|
392
|
-
: "leave blank for the provider's own api"
|
|
393
|
-
|
|
537
|
+
: "leave blank for the provider's own api",
|
|
538
|
+
// The stored query string is not sent to this page (see
|
|
539
|
+
// `baseUrlView` in `get-jev-config.ts`), so the field holds the URL
|
|
540
|
+
// without it. Said out loud, because the alternative is a field that
|
|
541
|
+
// silently disagrees with the file and a save that looks like it
|
|
542
|
+
// kept something the person was never shown.
|
|
543
|
+
view?.baseUrlQueryWithheld
|
|
544
|
+
? "the stored url's query string is not shown here — leave the url as it is to keep it"
|
|
545
|
+
: null,
|
|
546
|
+
]
|
|
547
|
+
.filter(Boolean)
|
|
548
|
+
.join(" · ")}
|
|
394
549
|
>
|
|
395
550
|
<input
|
|
396
551
|
id="jev-url"
|
|
@@ -468,6 +623,8 @@ export default function JevPanel({ initial }: { initial: JevSettingsView | null
|
|
|
468
623
|
</button>
|
|
469
624
|
)}
|
|
470
625
|
</div>
|
|
626
|
+
</>
|
|
627
|
+
)}
|
|
471
628
|
</div>
|
|
472
629
|
);
|
|
473
630
|
}
|
|
@@ -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.
|