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.
Files changed (225) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -5
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +4 -4
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +6 -7
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +18 -7
  58. package/.next/standalone/.next/server/app/settings/page.js +9 -7
  59. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__20kzpkf._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  65. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +9 -9
  69. package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  97. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +12 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  113. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  114. package/.next/standalone/.next/server/pages/404.html +1 -1
  115. package/.next/standalone/.next/server/pages/500.html +1 -1
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +33 -22
  118. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
  119. package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
  121. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
  124. package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
  129. package/.next/standalone/SECURITY.md +53 -0
  130. package/.next/standalone/app/actions/get-jev-config.ts +194 -14
  131. package/.next/standalone/app/actions/update-jev-config.ts +160 -11
  132. package/.next/standalone/app/policies/hooks-client.tsx +5 -3
  133. package/.next/standalone/app/settings/jev-panel.tsx +169 -12
  134. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  135. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  136. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  137. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  138. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  139. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  141. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  143. package/.next/standalone/package.json +9 -9
  144. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  145. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  146. package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
  147. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
  148. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
  150. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
  151. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
  152. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
  153. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
  154. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
  156. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
  157. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  158. package/.next/standalone/server.js +1 -1
  159. package/README.md +1 -0
  160. package/bin/failproofai.mjs +37 -6
  161. package/dist/cli.mjs +2861 -1145
  162. package/dist/worker.mjs +815 -197
  163. package/package.json +9 -9
  164. package/src/hooks/builtin-policies.ts +24 -6
  165. package/src/hooks/cloud-connection.ts +190 -1
  166. package/src/hooks/cloud-enrollment-cli.ts +56 -7
  167. package/src/hooks/cloud-introspect.ts +6 -0
  168. package/src/hooks/configure-wizard.ts +26 -6
  169. package/src/hooks/custom-hooks-loader.ts +58 -8
  170. package/src/hooks/effective-reviewers.ts +172 -8
  171. package/src/hooks/flush-cli.ts +35 -8
  172. package/src/hooks/fp-config.ts +266 -1
  173. package/src/hooks/fp-reset.ts +22 -4
  174. package/src/hooks/handler.ts +44 -7
  175. package/src/hooks/hook-activity-store.ts +13 -5
  176. package/src/hooks/jev-cli.ts +1009 -86
  177. package/src/hooks/jev-cloud-connection.ts +392 -0
  178. package/src/hooks/manager.ts +44 -6
  179. package/src/hooks/pack-cli.ts +175 -37
  180. package/src/hooks/pack-failclosed.ts +3 -0
  181. package/src/hooks/pack-manifest.ts +11 -1
  182. package/src/hooks/pack-store.ts +47 -10
  183. package/src/hooks/policy-authority.ts +73 -20
  184. package/src/hooks/policy-catalog.ts +10 -8
  185. package/src/hooks/policy-evaluator.ts +31 -8
  186. package/src/hooks/policy-registry.ts +3 -2
  187. package/src/hooks/policy-reviewability.ts +41 -12
  188. package/src/hooks/policy-types.ts +5 -3
  189. package/src/hooks/semantic/combine.ts +124 -84
  190. package/src/hooks/semantic/decide.ts +16 -0
  191. package/src/hooks/semantic/envelope.ts +18 -59
  192. package/src/hooks/semantic/evaluator.ts +11 -2
  193. package/src/hooks/semantic/facts.ts +7 -1
  194. package/src/hooks/semantic/intent.ts +8 -8
  195. package/src/hooks/semantic/jev-client.ts +540 -28
  196. package/src/hooks/semantic/jev-config.ts +586 -38
  197. package/src/hooks/semantic/jev-review.ts +26 -11
  198. package/src/hooks/semantic/jev-throttle.ts +8 -2
  199. package/src/hooks/semantic/pack-policies.ts +58 -15
  200. package/src/hooks/semantic/policies.ts +4 -3
  201. package/src/hooks/semantic/redact.ts +28 -1
  202. package/src/hooks/semantic/session-root.ts +116 -0
  203. package/src/hooks/semantic/types.ts +11 -0
  204. package/src/hooks/tui.ts +4 -0
  205. package/src/hooks/worker-server.ts +2 -1
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
  208. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  209. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  211. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  212. package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  214. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  215. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  216. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  219. package/.next/standalone/.next/static/chunks/1qd741hzlmjbo.js +0 -1
  220. package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
  221. package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
  222. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
  223. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
  224. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
  225. /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
- provider: view.provider ?? "typesafe",
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 === "env"
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
- : "off. hooks run the regex policies exactly as before."}
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
- : "a second opinion on the commands the regex policies flag. needs your own endpoint and token."}
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 === "env"
326
- ? "from FAILPROOFAI_JEV_API_KEY"
327
- : "configured"
328
- : "none stored"}
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
- Open for the next release. `0.0.1` published on 2026-09-22 and the `bump` job
6
- moved the version here automatically; nothing has landed against `0.0.2b0` yet.
7
- Add entries as changes merge — this section becomes the GitHub Release body when
8
- it ships.
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
- available = sorted(set(sets) | (set(permissions.PRESETS) - {"clear"}))
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.