failproofai 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) 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 +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +9 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -10
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/pages/404.html +1 -1
  132. package/.next/standalone/.next/server/pages/500.html +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  135. package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
  136. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
  145. package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
  147. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  148. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  149. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  150. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  151. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  152. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  153. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  154. package/.next/standalone/app/settings/page.tsx +20 -1
  155. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  156. package/.next/standalone/app/settings/settings.css +79 -0
  157. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  158. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  159. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  163. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  164. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  165. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  166. package/.next/standalone/package.json +10 -10
  167. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  168. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  169. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  171. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  172. package/.next/standalone/server.js +1 -1
  173. package/README.md +3 -2
  174. package/bin/failproofai.mjs +148 -4
  175. package/dist/cli.mjs +18599 -9478
  176. package/dist/index.js +19 -1
  177. package/dist/worker.mjs +10500 -3773
  178. package/package.json +10 -10
  179. package/pi-extension/index.ts +11 -0
  180. package/scripts/build-policy-pack.mjs +53 -2
  181. package/src/audit/features.ts +3 -2
  182. package/src/hooks/builtin-policies.ts +195 -8
  183. package/src/hooks/cloud-connection.ts +190 -1
  184. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  185. package/src/hooks/cloud-introspect.ts +6 -0
  186. package/src/hooks/cloud-managed-policies.ts +22 -0
  187. package/src/hooks/configure-wizard.ts +26 -6
  188. package/src/hooks/custom-hooks-loader.ts +98 -10
  189. package/src/hooks/custom-hooks-registry.ts +45 -1
  190. package/src/hooks/effective-reviewers.ts +243 -0
  191. package/src/hooks/first-run-gate.ts +5 -0
  192. package/src/hooks/flush-cli.ts +35 -8
  193. package/src/hooks/fp-config.ts +266 -1
  194. package/src/hooks/fp-home.ts +23 -0
  195. package/src/hooks/fp-reset.ts +22 -4
  196. package/src/hooks/handler.ts +303 -7
  197. package/src/hooks/hook-activity-store.ts +118 -6
  198. package/src/hooks/hook-telemetry.ts +41 -0
  199. package/src/hooks/jev-activity.ts +385 -0
  200. package/src/hooks/jev-cli.ts +2172 -0
  201. package/src/hooks/jev-cloud-connection.ts +392 -0
  202. package/src/hooks/loader-utils.ts +6 -0
  203. package/src/hooks/manager.ts +45 -7
  204. package/src/hooks/pack-cli.ts +568 -37
  205. package/src/hooks/pack-failclosed.ts +3 -0
  206. package/src/hooks/pack-manifest.ts +489 -7
  207. package/src/hooks/pack-store.ts +194 -11
  208. package/src/hooks/policy-authority.ts +386 -0
  209. package/src/hooks/policy-catalog.ts +206 -0
  210. package/src/hooks/policy-evaluator.ts +963 -796
  211. package/src/hooks/policy-registry.ts +27 -1
  212. package/src/hooks/policy-reviewability.ts +258 -0
  213. package/src/hooks/policy-types.ts +128 -0
  214. package/src/hooks/semantic/combine.ts +581 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +513 -0
  217. package/src/hooks/semantic/envelope.ts +1255 -0
  218. package/src/hooks/semantic/evaluator.ts +556 -0
  219. package/src/hooks/semantic/facts.ts +387 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +1155 -0
  222. package/src/hooks/semantic/jev-config.ts +1142 -0
  223. package/src/hooks/semantic/jev-review.ts +389 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +427 -0
  226. package/src/hooks/semantic/pack-policies.ts +294 -0
  227. package/src/hooks/semantic/policies.ts +597 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2937 -0
  231. package/src/hooks/semantic/session-root.ts +116 -0
  232. package/src/hooks/semantic/types.ts +162 -0
  233. package/src/hooks/semver-precedence.ts +128 -0
  234. package/src/hooks/tui.ts +4 -0
  235. package/src/hooks/types.ts +1 -1
  236. package/src/hooks/worker-server.ts +121 -27
  237. package/src/index.ts +6 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  240. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  253. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
  255. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  258. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  259. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  260. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  261. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  263. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  265. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  266. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
  267. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
@@ -206,7 +206,24 @@ const HOW_IT_WORKS: ReadonlyArray<{ label: string; body: string }> = [
206
206
  },
207
207
  ];
208
208
 
209
- export default function SettingsClient({ initial }: { initial: ScheduledAuditView | null }) {
209
+ export default function SettingsClient({
210
+ initial,
211
+ jevPanel,
212
+ }: {
213
+ initial: ScheduledAuditView | null;
214
+ /**
215
+ * The Jev panel, rendered by the server page and handed in as an element
216
+ * rather than imported here.
217
+ *
218
+ * It is its own feature with its own file, its own actions and its own
219
+ * refresh — nothing on this page reads or writes what it does. Composing it
220
+ * in keeps that true in the code as well as in the design: this component
221
+ * does not import the Jev actions, so a test of the scheduled-audit panel
222
+ * neither renders nor touches Jev, and vice versa. Optional so the existing
223
+ * callers that only care about the audit panel keep working unchanged.
224
+ */
225
+ jevPanel?: ReactNode;
226
+ }) {
210
227
  // Seeded from the server render, so the first paint already tells the truth
211
228
  // about whether scheduled audits are on. See the note in `page.tsx`.
212
229
  const [view, setView] = useState<ScheduledAuditView | null>(initial);
@@ -507,6 +524,10 @@ export default function SettingsClient({ initial }: { initial: ScheduledAuditVie
507
524
  dashboard can reach <code>~/.failproofai/config.json</code>.
508
525
  </p>
509
526
  </div>
527
+ {/* A different file. An unreadable `config.json` says nothing about
528
+ `jev.json`, and hiding the panel that can turn a second
529
+ evaluator off would be the worst moment to hide it. */}
530
+ {jevPanel}
510
531
  </div>
511
532
  ) : (
512
533
  <div className="set-console">
@@ -653,6 +674,11 @@ export default function SettingsClient({ initial }: { initial: ScheduledAuditVie
653
674
  </dl>
654
675
  </div>
655
676
  </div>
677
+
678
+ {/* Full width, below the two panels: Jev is a second evaluator for
679
+ the whole machine, not a property of the scheduled scan, and its
680
+ endpoint would not fit a half-width column anyway. */}
681
+ {jevPanel}
656
682
  </div>
657
683
  )}
658
684
 
@@ -151,6 +151,51 @@
151
151
  margin: 0;
152
152
  }
153
153
 
154
+ /* ── jev ──────────────────────────────────────────────────────────────────── */
155
+
156
+ /* A full-width cell in the same console, below the two panels. The gap above it
157
+ * is the console's own 1px grid — this rule adds no border of its own. */
158
+ .set-jev {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 16px;
162
+ }
163
+
164
+ /* Two columns at desktop: a provider or a mode needs about half the width an
165
+ * endpoint does, and stacking five controls in one column turns a short form
166
+ * into a scroll. */
167
+ .set-jev-fields {
168
+ display: grid;
169
+ grid-template-columns: repeat(2, minmax(0, 1fr));
170
+ gap: 16px 20px;
171
+ }
172
+
173
+ .set-field {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 6px;
177
+ min-width: 0;
178
+ }
179
+ .set-field-label {
180
+ font-family: var(--font-mono);
181
+ font-size: 9.5px;
182
+ font-weight: 500;
183
+ letter-spacing: 0.18em;
184
+ text-transform: uppercase;
185
+ color: var(--dim);
186
+ }
187
+
188
+ .set-jev-actions {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 16px;
192
+ flex-wrap: wrap;
193
+ }
194
+
195
+ @media (max-width: 640px) {
196
+ .set-jev-fields { grid-template-columns: 1fr; }
197
+ }
198
+
154
199
  /* ── masthead ─────────────────────────────────────────────────────────────── */
155
200
 
156
201
  .set-mast {
@@ -228,6 +273,40 @@
228
273
  outline-offset: 1px;
229
274
  }
230
275
 
276
+ /* `.set-num`'s box, stretched. Same background, same hairline, same square
277
+ * corners and the same pink focus ring — a second field style would read as a
278
+ * second form on one page. */
279
+ .set-text,
280
+ .set-select {
281
+ font-family: var(--font-mono);
282
+ font-size: 13px;
283
+ width: 100%;
284
+ padding: 6px 10px;
285
+ background: var(--bg);
286
+ border: 1px solid var(--line-2);
287
+ color: var(--ink);
288
+ border-radius: 0;
289
+ }
290
+ .set-text:focus-visible,
291
+ .set-select:focus-visible {
292
+ outline: 2px solid var(--accent-pink);
293
+ outline-offset: 1px;
294
+ }
295
+ .set-text:disabled,
296
+ .set-select:disabled {
297
+ opacity: 0.5;
298
+ cursor: not-allowed;
299
+ }
300
+ .set-text::placeholder {
301
+ color: var(--dim);
302
+ }
303
+ /* The native arrow is drawn by the platform and cannot be tokenised; leaving
304
+ * the control's own appearance in place is the one honest option, so the box
305
+ * around it is what carries the page's style. */
306
+ .set-select {
307
+ cursor: pointer;
308
+ }
309
+
231
310
  .set-link {
232
311
  font-family: var(--font-mono);
233
312
  font-size: 12px;
@@ -2,10 +2,28 @@
2
2
 
3
3
  ## 0.0.2b0 — 2026-09-22
4
4
 
5
- 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.
@@ -34,6 +34,35 @@ def test_policy_permissions_are_assignable_and_in_presets():
34
34
  assert "policies:read" in PRESETS["standard"]
35
35
  assert "policies:pull" in PRESETS["admin"]
36
36
 
37
+
38
+ def test_jev_permission_is_assignable_and_in_the_admin_preset_only():
39
+ from fp_cli.permissions import (
40
+ ALL_PERMISSIONS,
41
+ ASSIGNABLE_PERMISSIONS,
42
+ KEY_ASSIGNABLE_PERMISSIONS,
43
+ PRESETS,
44
+ )
45
+
46
+ assert "jev:evaluate" in ALL_PERMISSIONS
47
+ # In the server's declared order: after usage:read, and orgs:admin stays last.
48
+ assert ALL_PERMISSIONS.index("jev:evaluate") == ALL_PERMISSIONS.index("usage:read") + 1
49
+ assert ALL_PERMISSIONS[-1] == "orgs:admin"
50
+ # A machine key carries it, so it is grantable to a key as well as a member.
51
+ assert "jev:evaluate" in ASSIGNABLE_PERMISSIONS
52
+ assert "jev:evaluate" in KEY_ASSIGNABLE_PERMISSIONS
53
+ # The server's built-in admin set carries it; nothing narrower does.
54
+ assert "jev:evaluate" in PRESETS["admin"]
55
+ assert "jev:evaluate" not in PRESETS["read-only"]
56
+ assert "jev:evaluate" not in PRESETS["standard"]
57
+ # And the admin preset can be granted to a key: it carries both permissions
58
+ # the server requires beside jev:evaluate.
59
+ assert {"events:add", "policies:pull"} <= set(PRESETS["admin"])
60
+
61
+
62
+ def test_parse_permissions_accepts_jev_evaluate():
63
+ assert _parse_permissions(["events:add policies:pull jev:evaluate"]) == [
64
+ "events:add", "policies:pull", "jev:evaluate"]
65
+
37
66
  BASE = "http://dash.test"
38
67
 
39
68
 
@@ -111,6 +140,42 @@ def test_keys_create_rejects_malformed_token(logged_in, runner):
111
140
  assert runner.invoke(app, ["keys", "create", "k", "--add", "events:"]).exit_code == 2
112
141
 
113
142
 
143
+ @respx.mock
144
+ def test_keys_create_machine_preset(logged_in, runner):
145
+ # The dashboard's key-only `machine` preset: collect, pull policy, Jev.
146
+ respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[]))
147
+ respx.get(f"{BASE}/api/permission-sets").mock(return_value=httpx.Response(200, json=[]))
148
+ route = respx.post(f"{BASE}/api/keys").mock(
149
+ return_value=httpx.Response(201, json={"id": "k9", "name": "m", "permissions": [], "created_at": "t"})
150
+ )
151
+ result = runner.invoke(app, ["--json", "keys", "create", "m", "--permission-set", "machine"])
152
+ assert result.exit_code == 0, result.output
153
+ assert json.loads(route.calls.last.request.content)["permissions"] == ["events:add", "jev:evaluate", "policies:pull"]
154
+
155
+
156
+ @respx.mock
157
+ def test_keys_create_refuses_jev_without_its_prerequisites(logged_in, runner):
158
+ # The server's 422 (JEV_REQUIRES), said before anything is sent.
159
+ respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[]))
160
+ route = respx.post(f"{BASE}/api/keys").mock(return_value=httpx.Response(201, json={}))
161
+ result = runner.invoke(app, ["--json", "keys", "create", "k", "--add", "jev:evaluate,events:add"])
162
+ assert result.exit_code == 2, result.output
163
+ assert "missing: policies:pull" in json.loads(result.stdout)["error"]
164
+ assert not route.called
165
+
166
+
167
+ @respx.mock
168
+ def test_keys_update_refuses_dropping_a_jev_prerequisite(logged_in, runner):
169
+ respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
170
+ {"id": "k1", "name": "m", "permissions": ["events:add", "jev:evaluate", "policies:pull"],
171
+ "created_at": "t", "revoked_at": None}]))
172
+ route = respx.patch(f"{BASE}/api/keys/k1").mock(return_value=httpx.Response(200, json={}))
173
+ result = runner.invoke(app, ["--json", "keys", "update", "m", "--remove", "events:add", "--yes"])
174
+ assert result.exit_code == 2, result.output
175
+ assert "missing: events:add" in json.loads(result.stdout)["error"]
176
+ assert not route.called
177
+
178
+
114
179
  @respx.mock
115
180
  def test_keys_create_name_collision(logged_in, runner):
116
181
  respx.get(f"{BASE}/api/keys").mock(return_value=httpx.Response(200, json=[
@@ -189,3 +189,64 @@ def test_parsing_and_executing_have_separate_budgets():
189
189
 
190
190
  assert policy_check._TIMEOUT_SECS == 5
191
191
  assert policy_check._SYNTAX_TIMEOUT_SECS > policy_check._TIMEOUT_SECS
192
+
193
+
194
+ # ── Jev fields a cloud policy never reads ────────────────────────────────────
195
+ #
196
+ # `semanticPolicies.add` is read only from a pack built by `failproofai publish`,
197
+ # and a cloud policy's authority comes from its deployment record, which never
198
+ # sets it — so both publish as a version that reads Jev-aware and is not. The
199
+ # dashboard refuses them (agenteye `dashboard/lib/policies/policyMeta.ts`,
200
+ # `cloudPublishProblem`); the server never reads the source. These cases are the
201
+ # dashboard's own, so the two surfaces agree on what counts.
202
+
203
+ JEV_BASE = '''import { customPolicies, allow } from "failproofai";
204
+
205
+ customPolicies.add({
206
+ name: "block-wrong-jira-project",
207
+ description: "Deny Jira writes outside the configured project",
208
+ match: { events: ["PreToolUse"] },
209
+ fn: async (ctx) => allow(),
210
+ });
211
+ '''
212
+ _SECOND = JEV_BASE.split("\n", 1)[1].replace("match:", 'authority: "reviewable",\n match:')
213
+
214
+
215
+ @pytest.mark.parametrize("src,needle", [
216
+ (JEV_BASE, None),
217
+ (JEV_BASE + '\nsemanticPolicies.add({ name: "db-migration-on-production" });\n', "failproofai pack"),
218
+ (JEV_BASE.replace("match:", 'authority: "reviewable", reviewedBy: ["destructive-deletion"],\n match:'),
219
+ "always hard"),
220
+ ('// semanticPolicies.add({ name: "later" })\n' + JEV_BASE, None),
221
+ ('/* customPolicies.add({ authority: "reviewable" }) */\n' + JEV_BASE, None),
222
+ ('const s = "semanticPolicies.add(";\n' + JEV_BASE, None),
223
+ (JEV_BASE.replace("allow()", 'allow("authority: reviewable")'), None),
224
+ (JEV_BASE + '\nsemanticPolicies\n .add ({ name: "db-migration-on-production" });\n', "failproofai pack"),
225
+ (JEV_BASE + "\n" + _SECOND, "always hard"),
226
+ ], ids=["plain", "semantic", "reviewable", "commented-semantic", "commented-reviewable",
227
+ "string-literal", "nested-text", "split-spelling", "later-registration"])
228
+ def test_jev_fields_are_refused_the_way_the_dashboard_refuses_them(src, needle):
229
+ from fp_cli.policy_check import cloud_publish_problem
230
+
231
+ problem = cloud_publish_problem(src)
232
+ if needle is None:
233
+ assert problem is None
234
+ else:
235
+ assert problem and needle in problem
236
+
237
+
238
+ def test_publish_refuses_jev_fields_before_anything_is_sent(monkeypatch):
239
+ """Both `policies publish` and `policies compose --publish` route through
240
+ `client.publish_policy`, so the refusal lives there — and must fire before
241
+ the POST, whatever `--no-verify` said about the syntax check."""
242
+ from fp_cli import client
243
+ from fp_cli.errors import ApiError
244
+
245
+ def _no_post(*a, **kw):
246
+ raise AssertionError("a refused source reached the server")
247
+
248
+ monkeypatch.setattr(client, "_post_json", _no_post)
249
+ src = JEV_BASE + '\nsemanticPolicies.add({ name: "db-migration-on-production" });\n'
250
+ with pytest.raises(ApiError) as exc:
251
+ client.publish_policy(None, "p", src)
252
+ assert "failproofai publish" in str(exc.value) + (exc.value.hint or "")