failproofai 1.0.12-beta.1 → 1.0.12

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 (236) 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 +4 -4
  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 +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  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 +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  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.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +3 -3
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__167mipqmjprq5._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
  75. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_1d98fyfrcaqhy._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1i8oid1gc7s1f.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  90. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  91. package/.next/standalone/.next/server/pages/404.html +1 -1
  92. package/.next/standalone/.next/server/pages/500.html +1 -1
  93. package/.next/standalone/.next/server/preview-props.json +3 -3
  94. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  96. package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 0el9n3h9_zp5m.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
  98. package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 0umzct31duk5g.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 0wobqgosirs8p.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 1kduj74-715jh.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/2701amhrbsbot.js +1 -0
  103. package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 27cavoc8paelg.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 2ve23a7keupjw.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/3bv6xaoitjpnh.js +6 -0
  106. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  107. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  108. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +28 -18
  109. package/.next/standalone/fp-cloud-cli/README.md +127 -9
  110. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
  111. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +199 -34
  112. package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
  113. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
  114. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
  115. package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
  116. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
  118. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
  119. package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
  120. package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
  121. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
  122. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
  125. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
  126. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
  127. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
  128. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
  129. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
  131. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
  132. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
  134. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
  135. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
  136. package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -152
  138. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
  140. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
  141. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +173 -22
  142. package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
  144. package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
  145. package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
  146. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
  147. package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
  148. package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
  149. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
  150. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
  151. package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
  152. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
  153. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
  154. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
  155. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
  156. package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
  157. package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
  158. package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
  159. package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
  160. package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
  161. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
  162. package/.next/standalone/hermes-plugin/__init__.py +11 -0
  163. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  164. package/.next/standalone/package.json +11 -10
  165. package/.next/standalone/sdk/python/uv.lock +31 -8
  166. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
  167. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
  168. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
  169. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
  171. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
  172. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
  173. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
  174. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
  175. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
  176. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
  177. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
  178. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
  179. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
  180. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
  181. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
  182. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
  183. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
  184. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
  185. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
  186. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
  187. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
  188. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
  189. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
  190. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
  191. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
  192. package/.next/standalone/server.js +1 -1
  193. package/dist/cli.mjs +1760 -638
  194. package/dist/worker.mjs +426 -76
  195. package/hermes-plugin/__init__.py +11 -0
  196. package/hermes-plugin/plugin.yaml +1 -1
  197. package/package.json +11 -10
  198. package/src/audit/open-browser.ts +24 -7
  199. package/src/hooks/agent-roster.ts +9 -1
  200. package/src/hooks/agent-skill.ts +206 -0
  201. package/src/hooks/cloud-connection.ts +27 -51
  202. package/src/hooks/cloud-introspect.ts +5 -2
  203. package/src/hooks/cloud-login.ts +248 -0
  204. package/src/hooks/cloud-managed-policies.ts +54 -4
  205. package/src/hooks/codex-system-hooks.ts +10 -4
  206. package/src/hooks/config-status.ts +9 -3
  207. package/src/hooks/configure-wizard.ts +356 -32
  208. package/src/hooks/delivery-health.ts +47 -2
  209. package/src/hooks/effective-reviewers.ts +14 -0
  210. package/src/hooks/fp-config.ts +19 -0
  211. package/src/hooks/fp-home.ts +1 -1
  212. package/src/hooks/handler.ts +43 -5
  213. package/src/hooks/hook-activity-store.ts +7 -1
  214. package/src/hooks/integrations.ts +18 -3
  215. package/src/hooks/jev-activity.ts +117 -1
  216. package/src/hooks/jev-cli.ts +7 -9
  217. package/src/hooks/manager.ts +80 -46
  218. package/src/hooks/policy-evaluator.ts +17 -0
  219. package/src/hooks/policy-reviewability.ts +4 -2
  220. package/src/hooks/semantic/cloud-jev.ts +219 -28
  221. package/src/hooks/semantic/combine.ts +26 -1
  222. package/src/hooks/semantic/intent.ts +127 -24
  223. package/src/hooks/semantic/jev-client.ts +1 -1
  224. package/src/hooks/semantic/jev-config.ts +6 -27
  225. package/src/hooks/semantic/jev-review.ts +111 -4
  226. package/src/hooks/tui.ts +280 -0
  227. package/src/hooks/uninstall-cli.ts +12 -1
  228. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
  229. package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
  230. package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +0 -1
  231. package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
  232. package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
  233. package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
  234. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
  235. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
  236. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
@@ -22,7 +22,7 @@ from .. import _click_compat as click # the Click Typer is running; see _click_
22
22
  from ..enforcement import RefError, RefUsageError, parse_semantic, read_source, resolve_kind
23
23
  from ..policy_check import SyntaxResult, check_syntax, run_policy
24
24
  from ..errors import ApiError, NotFoundError
25
- from . import _write
25
+ from . import _cloud, _write
26
26
 
27
27
  #: Every command here is session-only. These endpoints are ROOT-ONLY on the
28
28
  #: server — deliberately absent from `/v1`, because `/v1` is internet-facing and
@@ -565,4 +565,146 @@ def register(app: typer.Typer) -> None:
565
565
  policies_app.command("delete", epilog=GLOBALS_EPILOG)(policies_delete)
566
566
  policies_app.command("test", epilog=GLOBALS_EPILOG)(policies_test)
567
567
  policies_app.command("compose", epilog=GLOBALS_EPILOG)(policies_compose)
568
+ policies_app.command("author", epilog=GLOBALS_EPILOG)(policies_author)
569
+ policies_app.command("backtest", epilog=GLOBALS_EPILOG)(policies_backtest)
570
+ policies_app.command("candidacy", epilog=GLOBALS_EPILOG)(policies_candidacy)
568
571
  app.add_typer(policies_app, name="policies")
572
+
573
+
574
+ def policies_author(
575
+ ctx: typer.Context,
576
+ intent: str = typer.Argument(...),
577
+ kind: Optional[str] = typer.Option(
578
+ None, "--kind",
579
+ help="regex | jev | both. Default: the assistant writes a regex (JavaScript) policy.",
580
+ ),
581
+ ) -> None:
582
+ """Author a vetted policy from an intent (session; does not publish).
583
+
584
+ `--kind jev` asks for Jev checks only, `--kind both` for JavaScript plus the
585
+ Jev checks that may clear it. Publish the result with `fp policies publish`.
586
+ """
587
+ _cloud.session_only(ctx.obj, "policies author")
588
+ if kind is not None and kind not in ("regex", "jev", "both"):
589
+ raise click.UsageError("--kind must be regex, jev or both")
590
+ body = {"issue": {"title": intent, "source": "manual"}}
591
+ if kind in ("jev", "both"):
592
+ # The dashboard proxy treats anything else as regex, exactly as before kinds.
593
+ body["kind"] = kind
594
+ events = list(api.cloud_stream(require_auth(ctx.obj), "/api/agent/policy-author", body))
595
+ for event in events:
596
+ if event.get("type") == "error":
597
+ raise ApiError(str(event.get("message") or event.get("error") or event.get("reason") or "Policy author failed"))
598
+ result = next((e.get("result", e) for e in reversed(events) if e.get("type") in ("result", "done")), None)
599
+ if result is None:
600
+ raise ApiError("Policy author returned no result; nothing was published")
601
+ _cloud.emit(ctx.obj, result, "Policy draft")
602
+
603
+
604
+ def policies_backtest(
605
+ ctx: typer.Context,
606
+ file_or_id: Optional[str] = typer.Argument(None, metavar="FILE_OR_ID"),
607
+ kind: Optional[str] = typer.Option(
608
+ None, "--kind",
609
+ help="regex | jev | both. Default: a published policy's own kind; for a file, "
610
+ "regex, or jev/both when --semantic is given.",
611
+ ),
612
+ semantic_path: Optional[str] = typer.Option(
613
+ None, "--semantic", help="Jev checks JSON (the same file `policies publish --semantic` takes).",
614
+ ),
615
+ hours: Optional[float] = typer.Option(None, "--hours", help="How far back to replay (default 168 for jev/both)."),
616
+ agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Replay only this agent's calls."),
617
+ env: Optional[str] = typer.Option(None, "--env", help="Replay only this environment's calls."),
618
+ run: bool = typer.Option(
619
+ False, "--run",
620
+ help="jev/both: actually ask Jev (spends the org's Jev allowance). Default is a free estimate.",
621
+ ),
622
+ sample: Optional[int] = typer.Option(None, "--sample", help="jev/both with --run: calls to ask Jev about (max 50)."),
623
+ ) -> None:
624
+ """Replay local or published policy source against stored events (session).
625
+
626
+ A regex policy (a .mjs file or a published regex id) is replayed module-only,
627
+ as before. A jev/both policy — a published one, or `--semantic checks.json`
628
+ with or without a source file — runs the every-kind backtest: an estimate
629
+ by default, `--run` to spend Jev allowance on up to `--sample` calls.
630
+ Needs policies:write and events:read.
631
+ """
632
+ from pathlib import Path
633
+ _cloud.session_only(ctx.obj, "policies backtest")
634
+ if kind is not None and kind not in ("regex", "jev", "both"):
635
+ raise click.UsageError("--kind must be regex, jev or both")
636
+ if file_or_id is None and semantic_path is None:
637
+ raise click.UsageError("give a policy file or published id, or --semantic checks.json")
638
+ if hours is not None and not hours > 0:
639
+ raise click.UsageError("--hours must be a positive number")
640
+ if sample is not None and not 1 <= sample <= 50:
641
+ raise click.UsageError("--sample must be between 1 and 50")
642
+ if file_or_id == "-" and semantic_path == "-":
643
+ raise click.UsageError("only one of the source and --semantic can come from stdin")
644
+ cctx = require_auth(ctx.obj)
645
+ source: Optional[str] = None
646
+ semantic = None
647
+ published_kind = None
648
+ if file_or_id is not None:
649
+ if file_or_id == "-" or file_or_id.startswith("@") or Path(file_or_id).is_file():
650
+ source = _write.read_text_arg(file_or_id.removeprefix("@"))
651
+ else:
652
+ data = api.cloud_request(cctx, "GET", "/api/enforcement/policies")
653
+ versions = [p for p in _cloud.rows(data, "policies") if p.get("id") == file_or_id]
654
+ if not versions:
655
+ raise NotFoundError(f"no policy named {file_or_id}")
656
+ newest = max(versions, key=lambda p: p.get("version", 0))
657
+ published_kind = newest.get("kind") or "regex"
658
+ source = newest.get("source") or None
659
+ if semantic_path is None and isinstance(newest.get("semantic"), list):
660
+ semantic = newest["semantic"]
661
+ if semantic_path is not None:
662
+ try:
663
+ semantic = parse_semantic(read_source(semantic_path, isatty=False))
664
+ except RefUsageError as exc:
665
+ raise click.UsageError(str(exc))
666
+ except RefError as exc:
667
+ raise ApiError(str(exc))
668
+ resolved = kind or published_kind or ("regex" if not semantic else ("both" if source else "jev"))
669
+ if resolved == "regex":
670
+ if semantic_path is not None:
671
+ raise click.UsageError("--semantic needs --kind jev or both")
672
+ if not isinstance(source, str) or not source.strip():
673
+ raise click.UsageError("Policy source is empty")
674
+ body = {"source": source}
675
+ if agent_id:
676
+ body["agentId"] = agent_id
677
+ if env:
678
+ body["environment"] = env
679
+ if hours is not None:
680
+ body["hours"] = hours
681
+ _cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-backtest",
682
+ body=body), "Policy backtest")
683
+ return
684
+ if resolved == "both" and not (source or "").strip():
685
+ raise click.UsageError("a both backtest needs the JavaScript source too")
686
+ if not semantic:
687
+ raise click.UsageError(f"a {resolved} backtest needs Jev checks: pass --semantic checks.json")
688
+ body = {"kind": resolved, "semantic": semantic, "hours": hours if hours is not None else 168,
689
+ "mode": "run" if run else "estimate"}
690
+ if resolved == "both":
691
+ body["source"] = source
692
+ if agent_id:
693
+ body["agentId"] = agent_id
694
+ if env:
695
+ body["environment"] = env
696
+ if sample is not None:
697
+ body["sample"] = sample
698
+ if run and not ctx.obj.json:
699
+ output.warn("--run asks Jev about stored calls and spends the org's Jev allowance.")
700
+ _cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-backtest-all",
701
+ body=body), "Policy backtest")
702
+
703
+
704
+ def policies_candidacy(ctx: typer.Context, issue: str = typer.Option(..., "--issue")) -> None:
705
+ """Ask whether a policy could address an issue (session; read-only)."""
706
+ _cloud.session_only(ctx.obj, "policies candidacy")
707
+ cctx = require_auth(ctx.obj)
708
+ row = api.cloud_request(cctx, "GET", f"/api/issues/{issue}")
709
+ _cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-candidacy",
710
+ body={"issue": row}), "Policy candidacy")
@@ -17,6 +17,7 @@ from .._context import GLOBALS_EPILOG, AppState, require_auth, resolve_fields, v
17
17
  from ..errors import NotFoundError
18
18
  from ..models import SavedQuery
19
19
  from . import _write
20
+ from . import _cloud
20
21
 
21
22
 
22
23
  def _humanize_list(items: List[str]) -> str:
@@ -375,4 +376,73 @@ def register(app: typer.Typer) -> None:
375
376
  query_app.command("delete", epilog=GLOBALS_EPILOG)(query_delete)
376
377
  query_app.command("run", epilog=GLOBALS_EPILOG)(query_run)
377
378
  query_app.command("schema", epilog=GLOBALS_EPILOG)(query_schema)
379
+ query_app.command("compose", epilog=GLOBALS_EPILOG)(query_compose)
380
+ query_app.command("export", epilog=GLOBALS_EPILOG)(query_export)
378
381
  app.add_typer(query_app, name="query")
382
+
383
+
384
+ def query_compose(ctx: typer.Context, question: str = typer.Argument(...),
385
+ run: bool = typer.Option(False, "--run")) -> None:
386
+ """AI-compose SQL (session only); optionally run it."""
387
+ _cloud.session_only(ctx.obj, "query compose")
388
+ cctx = require_auth(ctx.obj)
389
+ draft = None
390
+ for event in api.cloud_stream(cctx, "/api/agent/compose-sql", {"intent": question}):
391
+ if event.get("type") == "error":
392
+ from ..errors import ApiError
393
+ raise ApiError(str(event.get("error") or event.get("reason") or "SQL composer failed"))
394
+ if event.get("type") in ("result", "done"):
395
+ draft = event.get("result", event)
396
+ if not isinstance(draft, dict):
397
+ raise click.UsageError("Cloud did not return composed SQL")
398
+ sql = draft.get("sql") or draft.get("sql_text")
399
+ if not isinstance(sql, str) or not sql.strip():
400
+ raise click.UsageError("Cloud did not return composed SQL")
401
+ if run:
402
+ data = api.cloud_request(cctx, "POST", "/api/queries/run", body={"sql": sql, "params": []})
403
+ _cloud.emit(ctx.obj, {"sql": sql, "result": data}, "Composed query")
404
+ elif ctx.obj.json:
405
+ output.emit_json(draft)
406
+ else:
407
+ _cloud.emit(ctx.obj, {"sql": sql}, "Composed query")
408
+
409
+
410
+ def query_export(ctx: typer.Context, id: Optional[str] = typer.Argument(None),
411
+ sql: Optional[str] = typer.Option(None, "--sql"),
412
+ format: str = typer.Option("csv", "--format"),
413
+ out: Optional[str] = typer.Option(None, "--out")) -> None:
414
+ """Export a saved or ad-hoc query as CSV/JSON (all returned rows).
415
+
416
+ --json wraps CSV in {format,content} unless --out writes the file.
417
+ """
418
+ import csv
419
+ import io
420
+ import json
421
+ if (id is None) == (sql is None):
422
+ raise click.UsageError("Pass a saved query id or --sql, exactly one")
423
+ if format not in ("csv", "json"):
424
+ raise click.UsageError("--format must be csv or json")
425
+ data = api.cloud_request(require_auth(ctx.obj), "POST", "/api/queries/run",
426
+ body={"query_id": id, "sql": _read_sql(sql) if sql is not None else None, "params": []})
427
+ if format == "json":
428
+ content = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
429
+ else:
430
+ buf = io.StringIO()
431
+ writer = csv.writer(buf)
432
+ writer.writerow([c.get("name", "") if isinstance(c, dict) else c for c in data.get("columns", [])])
433
+ # Spreadsheet-safe export of untrusted event cells.
434
+ writer.writerows([
435
+ ["'" + v if isinstance(v, str) and v.startswith(("=", "+", "-", "@", "\t", "\r")) else v for v in row]
436
+ for row in data.get("rows", [])
437
+ ])
438
+ content = buf.getvalue()
439
+ if out:
440
+ _cloud.write_file(out, content)
441
+ _cloud.emit(ctx.obj, {"out": out, "format": format}, "Query exported")
442
+ elif ctx.obj.json:
443
+ output.emit_json(data if format == "json" else {"format": format, "content": content})
444
+ elif format == "json":
445
+ from .. import cloud_output
446
+ cloud_output.query_result(data, "Query export")
447
+ else:
448
+ print(content, end="")
@@ -0,0 +1,49 @@
1
+ """Conversation sentiment summary, messages and sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from .. import client as api
10
+ from .._context import GLOBALS_EPILOG, require_auth, resolve_dates, validate_limit
11
+ from . import _cloud
12
+
13
+
14
+ def read(ctx, suffix, since=None, limit=None, session_id=None):
15
+ validate_limit(limit)
16
+ frm, to = resolve_dates(since, None, None)
17
+ data = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/events/sentiment{suffix}",
18
+ params={"ts_from": frm, "ts_to": to, "limit": limit, "session_id": session_id})
19
+ _cloud.emit(ctx.obj, data, "Sentiment", view=suffix.strip("/") or "summary")
20
+
21
+
22
+ def summary(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since")) -> None:
23
+ """Summarize conversation sentiment."""
24
+ read(ctx, "", since)
25
+
26
+
27
+ def messages(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since"),
28
+ limit: int = typer.Option(50, "--limit"),
29
+ session_id: Optional[str] = typer.Option(None, "--session-id")) -> None:
30
+ """List sentiment-scored messages."""
31
+ read(ctx, "/messages", since, limit, session_id)
32
+
33
+
34
+ def sessions(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since"),
35
+ limit: int = typer.Option(50, "--limit")) -> None:
36
+ """List sentiment-scored sessions."""
37
+ read(ctx, "/sessions", since, limit)
38
+
39
+
40
+ def session(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
41
+ """Show one session's sentiment."""
42
+ read(ctx, f"/sessions/{id}")
43
+
44
+
45
+ def register(app: typer.Typer) -> None:
46
+ group = typer.Typer(no_args_is_help=True, help="Inspect conversation sentiment.", context_settings={"help_option_names": ["-h", "--help"]})
47
+ for name, fn in (("summary", summary), ("messages", messages), ("sessions", sessions), ("session", session)):
48
+ group.command(name, epilog=GLOBALS_EPILOG)(fn)
49
+ app.add_typer(group, name="sentiment")
@@ -18,7 +18,9 @@ from .._context import (
18
18
  validate_choice,
19
19
  validate_limit,
20
20
  )
21
+ from .. import _click_compat as click
21
22
  from ..models import Session
23
+ from . import _cloud, _write
22
24
 
23
25
 
24
26
  def sessions(
@@ -70,6 +72,8 @@ def sessions(
70
72
  * `fp sessions --agents` — expand each multi-agent session to its full agent roster
71
73
  * `fp --json sessions --all --fields session_id,agents | jq '.sessions'` — every session's full agent roster as JSON
72
74
  """
75
+ if ctx.invoked_subcommand:
76
+ return
73
77
  state: AppState = ctx.obj
74
78
  cctx = require_auth(state)
75
79
  validate_limit(limit)
@@ -147,5 +151,95 @@ def sessions(
147
151
  output.recheck_filters_hint(active_filters)
148
152
 
149
153
 
154
+ def re_evaluate(ctx: typer.Context, session_id: str = typer.Argument(...)) -> None:
155
+ """Re-run evaluators on one session. Needs evaluations:trigger."""
156
+ _write.confirm(ctx.obj, f"Re-evaluate session {session_id}", assume_yes=False)
157
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "POST",
158
+ f"/api/sessions/{session_id}/re-evaluate"), "Session re-evaluation", body=session_id)
159
+
160
+
161
+ def import_sessions(
162
+ ctx: typer.Context,
163
+ files: List[str] = typer.Argument(..., metavar="FILE...", help="Event JSONL, Claude Code transcripts or Codex rollouts (.gz ok)."),
164
+ format: str = typer.Option("auto", "--format", help="auto | failproofai (FailproofAI event JSONL) | claude | codex."),
165
+ env: Optional[str] = typer.Option(None, "--env", help="Environment for events that name none (server default: upload)."),
166
+ dry_run: bool = typer.Option(False, "--dry-run", help="Convert and report; store nothing."),
167
+ ) -> None:
168
+ """Import sessions from files, as the dashboard's Import sessions dialog does.
169
+
170
+ Each file is gzipped and sent on its own. Sessions already in the org are skipped
171
+ whole; secrets are redacted the way the collector redacts them live. Claude Code
172
+ and Codex files take the session id from the file name, so pass the original files.
173
+ Needs events:add. With `--json`: `{"files": [{file, result}]}`.
174
+ """
175
+ import gzip
176
+ import time
177
+ from pathlib import Path
178
+ validate_choice(format, ("auto", "failproofai", "agenteye", "claude", "codex"), flag="--format")
179
+ # The server names FailproofAI's own event JSONL `agenteye`; `failproofai` is its public name.
180
+ wire_format = "agenteye" if format == "failproofai" else format
181
+ payloads = []
182
+ for name in files:
183
+ path = Path(name)
184
+ try:
185
+ raw = path.read_bytes()
186
+ except OSError as exc:
187
+ raise click.BadParameter(f"cannot read '{name}': {exc.strerror or exc}", param_hint="FILE")
188
+ rel = "/".join(path.resolve().parts[-2:])
189
+ payloads.append((name, rel, raw if raw[:2] == b"\x1f\x8b" else gzip.compress(raw)))
190
+ if not dry_run:
191
+ _write.confirm(ctx.obj, f"Import {len(files)} file{'s' if len(files) != 1 else ''}", assume_yes=False)
192
+ cctx = require_auth(ctx.obj)
193
+ # One cutoff for the whole run, so a later file of a session this run created
194
+ # is not skipped as "already present".
195
+ before = int(time.time() * 1000)
196
+ results = []
197
+ for name, rel, data in payloads:
198
+ result = api.import_events_file(cctx, data, {
199
+ "format": wire_format, "path": rel, "environment": env,
200
+ "dry_run": "true" if dry_run else None, "existing_before": before,
201
+ })
202
+ results.append({"file": name, "result": result})
203
+ if not dry_run:
204
+ _write.record_action("sessions_imported", resource="events", success=True)
205
+ _cloud.emit(ctx.obj, {"files": results}, "Session import")
206
+
207
+
208
+ def show(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
209
+ """Show a session overview and its event timeline."""
210
+ cctx = require_auth(ctx.obj)
211
+ overview = api.cloud_request(cctx, "GET", "/api/sessions", params={"session_id": id, "limit": 1})
212
+ items = _cloud.rows(overview, "sessions")
213
+ if not items:
214
+ from ..errors import NotFoundError
215
+ raise NotFoundError(f"no session {id}")
216
+ timeline = api.cloud_request(cctx, "GET", "/api/events/summary", params={"session_id": id, "limit": 200, "order": "asc"})
217
+ _cloud.emit(ctx.obj, {"overview": items[0], "timeline": timeline}, "Session")
218
+
219
+
220
+ def export(ctx: typer.Context, id: str = typer.Argument(...),
221
+ out: Optional[str] = typer.Option(None, "--out")) -> None:
222
+ """Export a full session as JSON."""
223
+ import json
224
+ data = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/sessions/{id}/export")
225
+ if out:
226
+ _cloud.write_file(out, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
227
+ _cloud.emit(ctx.obj, {"out": out}, "Session exported")
228
+ else:
229
+ _cloud.emit(ctx.obj, data, "Session")
230
+
231
+
232
+ def history(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
233
+ """Show a session's evaluation history."""
234
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", f"/api/sessions/{id}/evaluation-history"), "Evaluation history")
235
+
236
+
150
237
  def register(app: typer.Typer) -> None:
151
- app.command("sessions", epilog=GLOBALS_EPILOG)(sessions)
238
+ group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
239
+ group.callback(epilog=GLOBALS_EPILOG)(sessions)
240
+ group.command("re-evaluate", epilog=GLOBALS_EPILOG)(re_evaluate)
241
+ group.command("show", epilog=GLOBALS_EPILOG)(show)
242
+ group.command("export", epilog=GLOBALS_EPILOG)(export)
243
+ group.command("history", epilog=GLOBALS_EPILOG)(history)
244
+ group.command("import", epilog=GLOBALS_EPILOG)(import_sessions)
245
+ app.add_typer(group, name="sessions")
@@ -147,4 +147,6 @@ def register(app: typer.Typer) -> None:
147
147
  settings_app.command("list", epilog=GLOBALS_EPILOG)(settings_list)
148
148
  settings_app.command("schema", epilog=GLOBALS_EPILOG)(settings_schema)
149
149
  settings_app.command("set", epilog=GLOBALS_EPILOG)(settings_set)
150
+ from .account_cmds import register_context_windows
151
+ register_context_windows(settings_app)
150
152
  app.add_typer(settings_app, name="settings")
@@ -23,6 +23,8 @@ def usage(ctx: typer.Context) -> None:
23
23
  * `fp usage`
24
24
  * `fp --json usage | jq '.usage.events_ingested'`
25
25
  """
26
+ if ctx.invoked_subcommand:
27
+ return
26
28
  state: AppState = ctx.obj
27
29
  data = api.get_usage(require_auth(state))
28
30
  if state.json:
@@ -32,4 +34,22 @@ def usage(ctx: typer.Context) -> None:
32
34
 
33
35
 
34
36
  def register(app: typer.Typer) -> None:
35
- app.command("usage", epilog=GLOBALS_EPILOG)(usage)
37
+ group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
38
+ group.callback(epilog=GLOBALS_EPILOG)(usage)
39
+ group.command("windows", epilog=GLOBALS_EPILOG)(usage_windows)
40
+ app.add_typer(group, name="usage")
41
+
42
+
43
+ def usage_windows(ctx: typer.Context) -> None:
44
+ """List usage metering windows. API key only (the dashboard has no session route)."""
45
+ from .account_cmds import request
46
+ from ..client import AuthMode
47
+ from ..errors import KeyModeUnsupportedError
48
+ if ctx.obj.auth_mode is not AuthMode.API_KEY:
49
+ # GET /usage/windows is served on the versioned API only; a signed-in
50
+ # session would reach a dashboard 404 and print a misleading message.
51
+ raise KeyModeUnsupportedError(
52
+ "`fp usage windows` needs an API key — the dashboard has no session route for it.",
53
+ hint="pass --api-key (or FP_API_KEY) with a usage:read key",
54
+ )
55
+ request(ctx, "GET", "/api/usage/windows")
@@ -401,4 +401,18 @@ def register(app: typer.Typer) -> None:
401
401
  users_app.command("update", epilog=GLOBALS_EPILOG)(users_update)
402
402
  users_app.command("disable", epilog=GLOBALS_EPILOG)(users_disable)
403
403
  users_app.command("enable", epilog=GLOBALS_EPILOG)(users_enable)
404
+ users_app.command("delete", epilog=GLOBALS_EPILOG)(users_delete)
405
+ users_app.command("defaults", epilog=GLOBALS_EPILOG)(users_defaults)
404
406
  app.add_typer(users_app, name="users")
407
+
408
+
409
+ def users_delete(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
410
+ """Delete a member by id (not email)."""
411
+ from .account_cmds import request
412
+ request(ctx, "DELETE", f"/api/users/{id}")
413
+
414
+
415
+ def users_defaults(ctx: typer.Context) -> None:
416
+ """Show defaults for new members."""
417
+ from .account_cmds import request
418
+ request(ctx, "GET", "/api/users/defaults")
@@ -43,10 +43,6 @@ VALID_EFFECTS = ("enforce", "observe")
43
43
  #: JavaScript reviewable by its own Jev checks (`both`).
44
44
  VALID_KINDS = ("regex", "jev", "both")
45
45
 
46
- #: The Jev modes a deployment may set. `local` stops FailproofAI Cloud
47
- #: overriding the machine's own mode (the server stores it as no value).
48
- JEV_MODES = ("off", "observe", "enforce", "local")
49
-
50
46
  #: The server's (and the machine's) cap on Jev declarations per policy version.
51
47
  MAX_JEV_DECLARATIONS = 24
52
48
  AGENT_INTEGRATIONS = frozenset({
@@ -120,22 +116,6 @@ class DeployPlan:
120
116
  removed: List[PolicyRef]
121
117
  changed: List[Tuple[PolicyRef, PolicyRef]]
122
118
  unchanged: List[PolicyRef]
123
- #: `--jev-mode` as given (`off|observe|enforce|local`), or None when the
124
- #: command said nothing about Jev — which the server reads as "keep it".
125
- jev_mode: Optional[str] = None
126
- #: The mode FailproofAI Cloud sets on the machine now (None = local).
127
- jev_mode_before: Optional[str] = None
128
-
129
- @property
130
- def jev_mode_after(self) -> Optional[str]:
131
- """What the machine's Cloud-set mode will be (None = local)."""
132
- if self.jev_mode is None:
133
- return self.jev_mode_before
134
- return None if self.jev_mode == "local" else self.jev_mode
135
-
136
- @property
137
- def jev_mode_changes(self) -> bool:
138
- return self.jev_mode is not None and self.jev_mode_after != self.jev_mode_before
139
119
 
140
120
  @property
141
121
  def set_changes(self) -> bool:
@@ -144,7 +124,7 @@ class DeployPlan:
144
124
 
145
125
  @property
146
126
  def is_noop(self) -> bool:
147
- return not (self.set_changes or self.jev_mode_changes)
127
+ return not self.set_changes
148
128
 
149
129
  def to_dict(self) -> Dict[str, object]:
150
130
  out: Dict[str, object] = {
@@ -157,13 +137,6 @@ class DeployPlan:
157
137
  "unchanged": [p.to_dict() for p in self.unchanged],
158
138
  "noop": self.is_noop,
159
139
  }
160
- if self.jev_mode is not None:
161
- # `local` on both sides: the wire word, not the stored NULL.
162
- out["jevMode"] = {
163
- "from": self.jev_mode_before or "local",
164
- "to": self.jev_mode_after or "local",
165
- "changed": self.jev_mode_changes,
166
- }
167
140
  return out
168
141
 
169
142
 
@@ -237,8 +210,6 @@ def plan_deploy(
237
210
  replace: Optional[Sequence[str]] = None,
238
211
  latest: Optional[Dict[str, int]] = None,
239
212
  disabled: Optional[set] = None,
240
- jev_mode: Optional[str] = None,
241
- current_jev_mode: Optional[str] = None,
242
213
  kinds: Optional[Dict[Tuple[str, int], str]] = None,
243
214
  targets: Sequence[str] = (),
244
215
  all_agents: Sequence[str] = (),
@@ -249,16 +220,13 @@ def plan_deploy(
249
220
  with "these as well" has no single obvious reading, and guessing one would
250
221
  be guessing about somebody's fleet.
251
222
 
252
- `jev_mode` is `--jev-mode`; `current_jev_mode` what Cloud sets now (None =
253
- local). `kinds` maps `(id, version)` to the version's kind, so an `observe`
254
- effect on a Jev-only version — which the server refuses, because a Jev
255
- rollout is observed through the Jev MODE — is refused here, before a plan is
256
- drawn and confirmed for a write that cannot happen.
223
+ There is no Jev mode to set: FailproofAI Cloud derives a machine's Jev mode
224
+ from what is deployed to it (enforce while any `jev`/`both` policy is,
225
+ off otherwise), and a Jev rollout is observed per policy with an `observe`
226
+ effect like any other. `kinds` (`(id, version)` → kind) is accepted for
227
+ callers that already have it.
257
228
  """
258
- if jev_mode is not None and jev_mode not in JEV_MODES:
259
- raise RefUsageError(
260
- f"--jev-mode {jev_mode!r} is not one of {', '.join(JEV_MODES)}"
261
- )
229
+ del kinds
262
230
  latest = latest or {}
263
231
  current_list = list(current or [])
264
232
  current_map = {p.id: p for p in current_list}
@@ -314,13 +282,6 @@ def plan_deploy(
314
282
  result_map[pid] = dataclass_replace(result_map[pid], agent_targets=grouped.get(pid))
315
283
 
316
284
  result = sorted(result_map.values(), key=lambda p: p.id)
317
- for ref in result:
318
- if ref.effect == "observe" and (kinds or {}).get((ref.id, ref.version)) == "jev":
319
- raise RefUsageError(
320
- f"{ref.id}@{ref.version} is a Jev policy, and an effect does not apply to Jev "
321
- "checks — deploy it as enforce and use --jev-mode observe to watch Jev without "
322
- "it deciding anything"
323
- )
324
285
  added, removed, changed, unchanged = [], [], [], []
325
286
  for pid, ref in sorted(result_map.items()):
326
287
  was = current_map.get(pid)
@@ -342,16 +303,9 @@ def plan_deploy(
342
303
  removed=removed,
343
304
  changed=changed,
344
305
  unchanged=unchanged,
345
- jev_mode=jev_mode,
346
- jev_mode_before=current_jev_mode,
347
306
  )
348
307
 
349
308
 
350
- def jev_mode_after(mode: str) -> Optional[str]:
351
- """What a `--jev-mode`/`jev-mode` word leaves stored: `local` is None (Cloud stops overriding)."""
352
- return None if mode == "local" else mode
353
-
354
-
355
309
  def version_kinds(policies: Iterable[PolicyVersion]) -> Dict[Tuple[str, int], str]:
356
310
  """`{(id, version): kind}` for every published version."""
357
311
  return {(p.id, p.version): p.kind for p in policies}
@@ -85,6 +85,9 @@ class NotFoundError(FpCliError):
85
85
  """The requested resource does not exist."""
86
86
 
87
87
  exit_code = 6
88
+ # The HTTP response may explicitly distinguish an absent endpoint from a
89
+ # missing record. None means callers must fall back to the request shape.
90
+ route_unknown: Optional[bool] = None
88
91
 
89
92
 
90
93
  class ApiError(FpCliError):