failproofai 1.0.12-beta.0 → 1.0.12-beta.2

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 (214) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +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 +22 -22
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  57. package/.next/standalone/.next/server/app/settings/page.js +7 -7
  58. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/{[externals]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
  70. package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ts0fg5yih1c1._.js → [root-of-the-server]__00l2u6j86cb8y._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _0ouu886szh6yj._.js} +10 -9
  91. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1ve8y151ua7qx._.js → _1fw0_nzkae5ao._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_00jxj8foxqz5t.js → node_modules_html-to-image_es_index_1fv03uigni8s5.js} +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  112. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  113. package/.next/standalone/.next/server/pages/404.html +1 -1
  114. package/.next/standalone/.next/server/pages/500.html +1 -1
  115. package/.next/standalone/.next/server/preview-props.json +3 -3
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  118. package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 0ay__-d2h1xb5.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 0tbm3vjm62h-2.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/1c_jf6g5wnslk.js +1 -0
  121. package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 1my3-5p47t7r-.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +1 -0
  123. package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
  124. package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
  125. package/.next/standalone/.next/static/chunks/2o6d37j425uci.js +1 -0
  126. package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 42xptz88mtq3x.js} +1 -1
  128. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  129. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  130. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +32 -0
  131. package/.next/standalone/fp-cloud-cli/README.md +40 -1
  132. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +91 -12
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +78 -16
  134. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
  135. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +124 -6
  136. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -20
  138. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +14 -7
  141. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +527 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
  144. package/.next/standalone/hermes-plugin/README.md +6 -4
  145. package/.next/standalone/hermes-plugin/__init__.py +27 -0
  146. package/.next/standalone/hermes-plugin/client.py +5 -1
  147. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  148. package/.next/standalone/package.json +11 -10
  149. package/.next/standalone/sdk/python/uv.lock +31 -8
  150. package/.next/standalone/server.js +1 -1
  151. package/bin/failproofai.mjs +17 -1
  152. package/dist/cli.mjs +3507 -1250
  153. package/dist/worker.mjs +1779 -169
  154. package/hermes-plugin/README.md +6 -4
  155. package/hermes-plugin/__init__.py +27 -0
  156. package/hermes-plugin/client.py +5 -1
  157. package/hermes-plugin/plugin.yaml +1 -1
  158. package/package.json +11 -10
  159. package/src/audit/open-browser.ts +6 -7
  160. package/src/hooks/agent-roster.ts +180 -0
  161. package/src/hooks/agent-skill.ts +203 -0
  162. package/src/hooks/agent-targets.ts +60 -0
  163. package/src/hooks/cloud-connection.ts +91 -45
  164. package/src/hooks/cloud-enrollment-cli.ts +7 -4
  165. package/src/hooks/cloud-login.ts +6 -3
  166. package/src/hooks/cloud-managed-policies.ts +430 -20
  167. package/src/hooks/cloud-policy-errors.ts +481 -0
  168. package/src/hooks/codex-system-hooks.ts +10 -4
  169. package/src/hooks/config-status.ts +10 -4
  170. package/src/hooks/configure-wizard.ts +54 -26
  171. package/src/hooks/custom-hooks-loader.ts +26 -3
  172. package/src/hooks/daemon-client.ts +9 -4
  173. package/src/hooks/daemon-service.ts +2 -1
  174. package/src/hooks/delivery-health.ts +47 -2
  175. package/src/hooks/effective-reviewers.ts +36 -1
  176. package/src/hooks/fp-config.ts +19 -0
  177. package/src/hooks/fp-home.ts +6 -0
  178. package/src/hooks/handler.ts +255 -19
  179. package/src/hooks/health-probe.ts +15 -0
  180. package/src/hooks/hook-activity-store.ts +7 -1
  181. package/src/hooks/integrations.ts +36 -14
  182. package/src/hooks/jev-activity.ts +117 -1
  183. package/src/hooks/jev-cli.ts +196 -1
  184. package/src/hooks/jev-cloud-connection.ts +92 -0
  185. package/src/hooks/manager.ts +174 -34
  186. package/src/hooks/pack-failclosed.ts +2 -0
  187. package/src/hooks/pack-manifest.ts +8 -1
  188. package/src/hooks/policy-evaluator.ts +22 -1
  189. package/src/hooks/policy-registry.ts +3 -2
  190. package/src/hooks/policy-reviewability.ts +23 -5
  191. package/src/hooks/semantic/cloud-jev-health.ts +227 -0
  192. package/src/hooks/semantic/cloud-jev.ts +868 -0
  193. package/src/hooks/semantic/combine.ts +26 -1
  194. package/src/hooks/semantic/jev-client.ts +19 -2
  195. package/src/hooks/semantic/jev-config.ts +179 -30
  196. package/src/hooks/semantic/jev-review.ts +217 -10
  197. package/src/hooks/semantic/pack-policies.ts +2 -1
  198. package/src/hooks/semantic/types.ts +8 -0
  199. package/src/hooks/session-pause-cli.ts +1 -1
  200. package/src/hooks/tui.ts +13 -4
  201. package/src/hooks/uninstall-cli.ts +12 -1
  202. package/src/hooks/worker-server.ts +5 -1
  203. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
  204. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
  206. package/.next/standalone/.next/static/chunks/0a-ccsp3xmypz.js +0 -1
  207. package/.next/standalone/.next/static/chunks/13jv-xvabeb62.js +0 -6
  208. package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
  209. package/.next/standalone/.next/static/chunks/2meqb9kygrncb.js +0 -1
  210. package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
  211. package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
  212. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
  213. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
  214. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
@@ -23,7 +23,7 @@ import re
23
23
  import uuid
24
24
  from dataclasses import dataclass
25
25
  from enum import Enum
26
- from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Union
26
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple, Union
27
27
 
28
28
  import httpx
29
29
 
@@ -487,9 +487,39 @@ def _raise_for_status(response: httpx.Response, ctx: ClientContext) -> None:
487
487
  message or f"Request failed with status {response.status_code}.",
488
488
  status=response.status_code,
489
489
  request_id=request_id,
490
+ hint=_jev_budget_breakdown(response),
490
491
  )
491
492
 
492
493
 
494
+ def _jev_budget_breakdown(response: httpx.Response) -> Optional[str]:
495
+ """The per-policy breakdown of a deploy's ``jev_budget_exceeded`` refusal.
496
+
497
+ The message carries only the total (``used``/``budget``); the body also lists
498
+ every Jev policy version the machine would carry with its question
499
+ characters (``policies: [{id, version, chars}]``), which is what tells an
500
+ operator WHICH policy to leave off. Largest first. ``None`` for any other
501
+ error, and for a publish refusal, which names no other policies.
502
+ """
503
+ try:
504
+ data = response.json()
505
+ except Exception:
506
+ return None
507
+ if not isinstance(data, dict) or data.get("code") != "jev_budget_exceeded":
508
+ return None
509
+ rows = [
510
+ (str(p.get("id", "")), p.get("version"), p["chars"])
511
+ for p in data.get("policies") or []
512
+ if isinstance(p, dict) and isinstance(p.get("chars"), int)
513
+ ]
514
+ if not rows:
515
+ return None
516
+ rows.sort(key=lambda row: -row[2])
517
+ parts = [f"{pid}@{version} {chars:,}" if version is not None else f"{pid} {chars:,}"
518
+ for pid, version, chars in rows]
519
+ return ("Jev question characters per policy: " + " · ".join(parts)
520
+ + " — deploy fewer of these Jev policies to this machine")
521
+
522
+
493
523
  def _get_json(ctx: ClientContext, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
494
524
  clean = {k: v for k, v in (params or {}).items() if v is not None}
495
525
  url = _path(ctx, path) # chokepoint 1 of 4 for the /api -> /v1 rewrite
@@ -1643,22 +1673,43 @@ def list_policies(ctx: ClientContext) -> List[PolicyVersion]:
1643
1673
 
1644
1674
 
1645
1675
  def publish_policy(
1646
- ctx: ClientContext, policy_id: str, source: str, description: str = ""
1676
+ ctx: ClientContext,
1677
+ policy_id: str,
1678
+ source: Optional[str],
1679
+ description: str = "",
1680
+ *,
1681
+ kind: Optional[str] = None,
1682
+ semantic: Optional[List[Any]] = None,
1647
1683
  ) -> PolicyVersion:
1648
1684
  """POST /api/enforcement/policies — mints a NEW VERSION; never edits in place.
1649
1685
 
1650
- Refuses Jev fields a cloud policy never reads before anything is sent, here
1651
- because `policies publish` and `policies compose --publish` both route through
1652
- it — and regardless of `--no-verify`, which only skips the syntax check.
1686
+ ``kind`` is ``regex`` (JavaScript ``source`` only — what an omitted kind
1687
+ means to the server), ``jev`` (Jev declarations in ``semantic``, no
1688
+ ``source``) or ``both`` (the two together: the JavaScript is reviewable by
1689
+ exactly its own checks). ``authority``/``reviewedBy`` are derived by the
1690
+ server from the kind and are never sent.
1691
+
1692
+ Refuses Jev fields in the JAVASCRIPT before anything is sent, here because
1693
+ `policies publish` and `policies compose --publish` both route through it —
1694
+ and regardless of `--no-verify`, which only skips the syntax check. Jev
1695
+ checks belong in ``semantic``, where they take effect.
1653
1696
  """
1654
- problem = cloud_publish_problem(source)
1697
+ problem = cloud_publish_problem(source or "")
1655
1698
  if problem:
1656
1699
  raise ApiError(
1657
1700
  f"{policy_id} cannot be published as a cloud policy: {problem}",
1658
- hint=("Jev checks ship in a failproofai pack — `failproofai publish` — where "
1659
- "semanticPolicies.add and authority: \"reviewable\" take effect"),
1701
+ hint=("put Jev checks in the policy's Jev declarations — `--kind both --semantic "
1702
+ "checks.json` — or ship them in a failproofai pack with `failproofai publish`"),
1660
1703
  )
1661
- body = {"id": policy_id, "source": source, "description": description}
1704
+ body: Dict[str, Any] = {"id": policy_id, "description": description}
1705
+ # `source` is omitted for a Jev-only policy: the server takes absent (or
1706
+ # empty) as "no JavaScript", which is what a `jev` version is.
1707
+ if source:
1708
+ body["source"] = source
1709
+ if kind is not None:
1710
+ body["kind"] = kind
1711
+ if semantic is not None:
1712
+ body["semantic"] = semantic
1662
1713
  return PolicyVersion.from_dict(_post_json(ctx, "/api/enforcement/policies", body) or {})
1663
1714
 
1664
1715
 
@@ -1681,6 +1732,29 @@ def list_machines(ctx: ClientContext) -> List[Machine]:
1681
1732
  return [Machine.from_dict(m) for m in items]
1682
1733
 
1683
1734
 
1735
+ def machine_agents(ctx: ClientContext, machine_id: str) -> List[Dict[str, Any]]:
1736
+ """GET /api/enforcement/machines/{id}/agents — the agent profiles the daemon
1737
+ last reported: `[{instanceId, integration, profileLabel, scope, ...}]`."""
1738
+ data = _get_json(ctx, f"/api/enforcement/machines/{machine_id}/agents")
1739
+ items = data if isinstance(data, list) else (data or {}).get("agents", [])
1740
+ return [a for a in items if isinstance(a, dict)]
1741
+
1742
+
1743
+ def agent_labels(ctx: ClientContext, machine_id: str) -> Dict[str, str]:
1744
+ """`{instanceId: profileLabel}` for one machine, or `{}` when unavailable.
1745
+
1746
+ Best effort on purpose: it only names the `agt_…` ids an exact-profile
1747
+ assignment carries. A server without the roster route, or a machine that
1748
+ never reported one, falls back to the bare id rather than failing the view.
1749
+ """
1750
+ try:
1751
+ agents = machine_agents(ctx, machine_id)
1752
+ except Exception: # noqa: BLE001 — a missing label must never fail the read
1753
+ return {}
1754
+ return {str(a["instanceId"]): str(a["profileLabel"])
1755
+ for a in agents if a.get("instanceId") and a.get("profileLabel")}
1756
+
1757
+
1684
1758
  def rename_machine(ctx: ClientContext, machine_id: str, label: str) -> Dict[str, Any]:
1685
1759
  """PATCH /api/enforcement/machines/{id} — a human label, not the id."""
1686
1760
  path = f"/api/enforcement/machines/{machine_id}"
@@ -1708,11 +1782,16 @@ def get_deployment(ctx: ClientContext, machine_id: str) -> Optional[Deployment]:
1708
1782
 
1709
1783
 
1710
1784
  def deploy_policies(
1711
- ctx: ClientContext, machine_id: str, policies: Sequence[PolicyRef]
1785
+ ctx: ClientContext,
1786
+ machine_id: str,
1787
+ policies: Sequence[PolicyRef],
1712
1788
  ) -> Deployment:
1713
- """PUT /api/enforcement/deployments/{id} — REPLACES the machine's whole set."""
1789
+ """PUT /api/enforcement/deployments/{id} — REPLACES the machine's whole set.
1790
+
1791
+ Never carries a Jev mode: FailproofAI Cloud derives it from the set.
1792
+ """
1714
1793
  path = f"/api/enforcement/deployments/{machine_id}"
1715
- body = {"policies": [p.to_dict() for p in policies]}
1794
+ body: Dict[str, Any] = {"policies": [p.to_dict() for p in policies]}
1716
1795
  return Deployment.from_dict(_request_json(ctx, "PUT", path, json_body=body) or {})
1717
1796
 
1718
1797
 
@@ -32,8 +32,10 @@ from ..enforcement import (
32
32
  disabled_ids,
33
33
  latest_versions,
34
34
  plan_deploy,
35
+ version_kinds,
35
36
  )
36
- from ..errors import ApiError, NotFoundError
37
+ from ..errors import ApiError, AuthError, FpCliError, NotFoundError
38
+ from ..models import PolicyRef
37
39
  from . import _write
38
40
 
39
41
  _KEY_MODE_REASON = (
@@ -62,6 +64,8 @@ def fleet_list(ctx: typer.Context) -> None:
62
64
  `intended` is the generation deployed, `applied` is the one the machine last
63
65
  collected, and `seen` is when it last reported anything — a machine can be
64
66
  in sync and dead, or alive and behind, and those are different problems.
67
+ `state` reads `N errors` when the machine reported policies it could not
68
+ apply (`fp fleet show <machine>` lists them).
65
69
 
66
70
  A machine appears from its very first check-in, including the poll that
67
71
  finds nothing deployed — that is exactly the machine you are usually looking
@@ -101,10 +105,22 @@ def fleet_show(
101
105
  machine can be told to run a policy and not yet have it; the policy list
102
106
  alone cannot tell you which, and that is usually the question.
103
107
 
108
+ Shows the Jev mode FailproofAI Cloud sets on the machine (`local` when it
109
+ sets none) and the policy errors the machine last reported: a policy it
110
+ could not load, a Jev mode it cannot act on (`jev_unconfigured`: its key
111
+ does not carry Jev; `transcripts_disabled`: it was connected with
112
+ `--no-transcripts`), FailproofAI Cloud Jev refusing or failing its calls
113
+ (`jev_rate_limited`: over the organization's Jev rate limit;
114
+ `jev_unavailable`: it skips a failing Cloud Jev for a minute at a time), an
115
+ installed pack's check left out for the question budget (`jev_budget`) or
116
+ because an organization Cloud check has its name (`jev_name_clash`).
117
+ Anything listed there is not enforcing, whatever the deployment says.
118
+
104
119
  Needs `policies:read`. With `--json`: `{machine, deployment}` — the machine
105
- record (including `appliedDeployment`, `drifted`, `lastSeen` and both label
106
- fields, with raw timestamps) and the deployment, or `deployment: null` when
107
- nothing is deployed.
120
+ record (including `appliedDeployment`, `drifted`, `lastSeen`, both label
121
+ fields, `jevMode` and `policyErrors`/`policyErrorsAt` when reported, with raw
122
+ timestamps) and the deployment, or `deployment: null` when nothing is
123
+ deployed.
108
124
 
109
125
  Example:
110
126
 
@@ -129,7 +145,8 @@ def fleet_show(
129
145
  "deployment": dep.to_dict() if dep else None,
130
146
  })
131
147
  return
132
- output.render_machine_policies(machine_id, dep, machine)
148
+ output.render_machine_policies(machine_id, dep, machine,
149
+ agent_labels=api.agent_labels(cctx, machine_id))
133
150
 
134
151
 
135
152
  def fleet_deploy(
@@ -137,13 +154,22 @@ def fleet_deploy(
137
154
  machine_id: str = typer.Argument(..., help="Machine id."),
138
155
  add: Optional[List[str]] = typer.Option(
139
156
  None, "--add",
140
- help="Add or update a policy: `id`, `id@version`, `id:effect` or `id@version:effect`.",
157
+ help="Add or update a policy: `id`, `id@version`, `id:effect` or `id@version:effect`. "
158
+ "`observe` records what the policy (its JavaScript and its Jev checks) would decide, and decides nothing.",
141
159
  ),
142
160
  remove: Optional[List[str]] = typer.Option(None, "--remove", help="Remove a policy by id."),
143
161
  replace: Optional[List[str]] = typer.Option(
144
162
  None, "--set",
145
163
  help="REPLACE the whole set with exactly these. Cannot be combined with --add/--remove.",
146
164
  ),
165
+ targets: Optional[List[str]] = typer.Option(
166
+ None, "--target",
167
+ help="Scope one assigned policy: POLICY=INTEGRATION or POLICY=INTEGRATION/agt_ID. Repeat to OR selectors.",
168
+ ),
169
+ all_agents: Optional[List[str]] = typer.Option(
170
+ None, "--all-agents",
171
+ help="Clear one policy's targeting back to every agent on this machine.",
172
+ ),
147
173
  create: bool = typer.Option(
148
174
  False, "--create",
149
175
  help="Allow deploying to a machine id that has not checked in yet (pre-staging).",
@@ -159,6 +185,14 @@ def fleet_deploy(
159
185
 
160
186
  `--set` replaces everything — the only way to drop policies you do not name.
161
187
 
188
+ Any kind deploys the same way. Jev checks run on FailproofAI Cloud; nothing
189
+ is installed on the machine: a `both` policy sends the machine its
190
+ JavaScript, and the machine asks FailproofAI Cloud about each checked tool
191
+ call. An `observe` effect applies to every kind: the policy's JavaScript
192
+ and its Jev checks are evaluated and recorded, and decide nothing. There is
193
+ no Jev mode to set: FailproofAI Cloud turns Jev on for a machine while any
194
+ `jev` or `both` policy is deployed to it, and off when none is.
195
+
162
196
  **Concurrency.** The write is a full replace with no server-side lock, so the
163
197
  CLI records the generation it read and refuses if the result is not exactly
164
198
  one higher: that means somebody else deployed in between and a replace does
@@ -170,17 +204,18 @@ def fleet_deploy(
170
204
  * `fp fleet deploy ci-runner-01 --add no-force-push`
171
205
  * `fp fleet deploy ci-runner-01 --add prod-guard@1:observe --remove old-rule`
172
206
  * `fp fleet deploy ci-runner-01 --set no-force-push --set no-secret-echo`
207
+ * `fp fleet deploy ci-runner-01 --add prod-db-intent:observe`
173
208
  """
174
209
  state: AppState = ctx.obj
175
210
  deny_in_key_mode(state, "fleet deploy", _KEY_MODE_REASON)
176
211
  cctx = require_auth(state)
177
212
 
178
- if not add and not remove and replace is None:
213
+ if not add and not remove and replace is None and not targets and not all_agents:
179
214
  # Exit 2 for the same reason `--set` with `--add` is: no flag
180
215
  # combination was given that this command can act on. Both are the
181
216
  # caller's command line, not the server's answer.
182
217
  raise click.UsageError(
183
- "nothing to do — pass --add, --remove, or --set. "
218
+ "nothing to do — pass --add, --remove, --set, --target, or --all-agents. "
184
219
  "`fp fleet show <machine>` prints the current set."
185
220
  )
186
221
 
@@ -212,6 +247,9 @@ def fleet_deploy(
212
247
  replace=replace,
213
248
  latest=latest,
214
249
  disabled=disabled_ids(published),
250
+ kinds=version_kinds(published),
251
+ targets=targets or (),
252
+ all_agents=all_agents or (),
215
253
  )
216
254
  except RefUsageError as exc:
217
255
  # Exit 2, like every other bad flag value in this CLI (`--since`,
@@ -314,8 +352,11 @@ def fleet_history(
314
352
  """List a machine's deployment generations, newest first.
315
353
 
316
354
  A reissue — the server rewriting a deployment because a policy was disabled
317
- — appears as an ordinary entry. Needs `policies:read`. With `--json`:
318
- `{machineId, history:[{deployment, policies, updatedAt}]}`.
355
+ — appears as an ordinary entry, and so does a Jev mode change. Each
356
+ generation shows the Jev mode FailproofAI Cloud set with it (`local` when it
357
+ set none), which is the mode a rollback to it restores. Needs
358
+ `policies:read`. With `--json`: `{machineId, history:[{deployment, policies,
359
+ updatedAt, jevMode}]}`.
319
360
 
320
361
  Example:
321
362
 
@@ -329,7 +370,8 @@ def fleet_history(
329
370
  if output.is_json():
330
371
  output.emit_json({"machineId": machine_id, "history": entries})
331
372
  return
332
- output.render_deployment_history(machine_id, entries)
373
+ output.render_deployment_history(machine_id, entries,
374
+ agent_labels=api.agent_labels(cctx, machine_id))
333
375
 
334
376
 
335
377
  def fleet_rollback(
@@ -344,8 +386,12 @@ def fleet_rollback(
344
386
  counter, so history stays append-only. A generation containing a policy that
345
387
  has since been disabled or deleted cannot be reinstated; the server says so.
346
388
 
347
- Needs `policies:write`. With `--json`: the resulting deployment, or
348
- `{"cancelled": true}` if you decline.
389
+ It restores that generation's Jev mode too, `local` included: the prompt
390
+ and the result say `jev mode <now> → <then>` when that changes it.
391
+
392
+ Needs `policies:write`. With `--json`: the resulting deployment plus
393
+ `jevModeBefore` (the mode it replaced), or `{"cancelled": true}` if you
394
+ decline.
349
395
 
350
396
  Example:
351
397
 
@@ -356,9 +402,22 @@ def fleet_rollback(
356
402
  cctx = require_auth(state)
357
403
  _require_machine(cctx, machine_id)
358
404
  current = api.get_deployment(cctx, machine_id)
405
+ # The mode that generation carried comes back with it, NULL (`local`)
406
+ # included, as the server reports it.
407
+ # Read it first, so the prompt says so rather than the fleet finding out.
408
+ target = next(
409
+ (e for e in api.deployment_history(cctx, machine_id) if e.get("deployment") == deployment), None)
410
+ mode_before = (current.jev_mode if current else None) or "local"
411
+ mode_target = (target.get("jevMode") or "local") if target is not None else None
412
+ consequence = "this REPLACES the machine's current set with the one from that generation"
413
+ if target is not None:
414
+ refs = [PolicyRef.from_dict(ref).label for ref in (target.get("policies") or [])]
415
+ consequence += "; assignments: " + ("; ".join(refs) if refs else "no policies")
416
+ if mode_target is not None and mode_target != mode_before:
417
+ consequence += f", and its Jev mode: jev mode {mode_before} → {mode_target}"
359
418
  if not _write.confirm_destructive(
360
419
  state, f"reinstate deployment #{deployment} on", machine_id,
361
- consequence="this REPLACES the machine's current set with the one from that generation",
420
+ consequence=consequence,
362
421
  assume_yes=yes,
363
422
  ):
364
423
  if output.is_json():
@@ -369,9 +428,12 @@ def fleet_rollback(
369
428
  result = api.rollback_deployment(cctx, machine_id, deployment)
370
429
  check_race(current.deployment if current else None, result.deployment)
371
430
  if output.is_json():
372
- output.emit_json(result.to_dict())
431
+ output.emit_json({**result.to_dict(), "jevModeBefore": mode_before})
373
432
  return
374
- output.deployment_rolled_back(machine_id, deployment, result.deployment)
433
+ output.deployment_rolled_back(
434
+ machine_id, deployment, result.deployment,
435
+ jev_from=mode_before, jev_to=result.jev_mode or "local",
436
+ )
375
437
 
376
438
 
377
439
  def fleet_rename(
@@ -10,6 +10,7 @@ single most surprising thing here, so every success path says so.
10
10
  """
11
11
  from __future__ import annotations
12
12
 
13
+ import json
13
14
  from typing import Optional
14
15
 
15
16
  import typer
@@ -18,8 +19,8 @@ from .. import client as api
18
19
  from .. import output, theme
19
20
  from .._context import GLOBALS_EPILOG, AppState, deny_in_key_mode, require_auth
20
21
  from .. import _click_compat as click # the Click Typer is running; see _click_compat
21
- from ..enforcement import RefError, RefUsageError, read_source
22
- from ..policy_check import check_syntax, run_policy
22
+ from ..enforcement import RefError, RefUsageError, parse_semantic, read_source, resolve_kind
23
+ from ..policy_check import SyntaxResult, check_syntax, run_policy
23
24
  from ..errors import ApiError, NotFoundError
24
25
  from . import _write
25
26
 
@@ -36,11 +37,15 @@ _KEY_MODE_REASON = (
36
37
  def policies_list(ctx: typer.Context) -> None:
37
38
  """List every published policy version, newest of each policy first.
38
39
 
39
- Shows `policy · version · state · description`, one row per VERSION —
40
+ Shows `policy · version · kind · state · description`, one row per VERSION —
40
41
  versions are immutable and every one stays addressable, so a policy
41
42
  published three times is three rows. The title counts distinct policies and
42
43
  captions the version total, the way the dashboard's library does.
43
44
 
45
+ `kind` is `regex` (JavaScript), `jev` (Jev checks only) or `both`
46
+ (JavaScript its own Jev checks may clear); a `jev`/`both` row names its
47
+ checks and the characters they take of a machine's Jev question budget.
48
+
44
49
  `state` is active, disabled (kept but not enforced) or archived (deleted;
45
50
  machines already carrying it keep it until redeployed). Needs
46
51
  `policies:read`. With `--json`: the server's policy list verbatim — also
@@ -64,9 +69,11 @@ def policies_show(
64
69
  ctx: typer.Context,
65
70
  policy_id: str = typer.Argument(..., help="Policy id."),
66
71
  ) -> None:
67
- """Show one policy, including its full source.
72
+ """Show one policy, including its full source and its Jev checks.
68
73
 
69
- Needs `policies:read`. With `--json`: the policy object with `source`.
74
+ Needs `policies:read`. With `--json`: the policy object with `source`,
75
+ `kind`, and for `jev`/`both` its `semantic` declarations, `semanticSha256`
76
+ and the server-derived `authority`/`reviewedBy`.
70
77
 
71
78
  Example:
72
79
 
@@ -96,6 +103,8 @@ def policies_show(
96
103
  source_bytes=len((match.source or "").encode("utf-8")))
97
104
  if match.source:
98
105
  output.info(match.source)
106
+ if match.semantic:
107
+ output.info(json.dumps(match.semantic, indent=2))
99
108
 
100
109
 
101
110
  def policies_publish(
@@ -103,7 +112,19 @@ def policies_publish(
103
112
  policy_id: str = typer.Argument(..., help="Policy id (letters, numbers, '.', '_', '-')."),
104
113
  source: Optional[str] = typer.Argument(
105
114
  None,
106
- help="Path to the policy source, @path, or - for stdin. Omit to paste it.",
115
+ help="Path to the JavaScript source, @path, or - for stdin. Omit to paste it. Same as --source.",
116
+ ),
117
+ kind: Optional[str] = typer.Option(
118
+ None, "--kind",
119
+ help="regex (JavaScript), jev (Jev checks only) or both (JavaScript its own Jev checks may "
120
+ "clear). Default: follows from what you give — --semantic alone is jev, with source both.",
121
+ ),
122
+ source_opt: Optional[str] = typer.Option(
123
+ None, "--source", help="The JavaScript source: a path, @path, or - for stdin.",
124
+ ),
125
+ semantic_path: Optional[str] = typer.Option(
126
+ None, "--semantic",
127
+ help="A JSON file holding the Jev declarations (a JSON array): a path, @path, or - for stdin.",
107
128
  ),
108
129
  description: str = typer.Option("", "--description", help="One-line description."),
109
130
  no_verify: bool = typer.Option(
@@ -112,13 +133,29 @@ def policies_publish(
112
133
  ) -> None:
113
134
  """Publish a policy — mints a NEW VERSION; it never edits one in place.
114
135
 
115
- The source is parse-checked with node before it is sent. Nothing downstream
116
- does this: the server validates the id and a size ceiling, and a broken
117
- policy otherwise fails on the machine at enforcement time. `--no-verify`
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`.
136
+ A policy has a **kind**:
137
+
138
+ * `regex` — JavaScript (`--source`, or the positional source): what publish
139
+ has always done, and the default.
140
+ * `jev` — Jev checks only (`--semantic checks.json`): a JSON array of Jev
141
+ declarations, each `{name, title, appliesTo, mode, userCanOverride,
142
+ probes:[{id, instructions, criteria:{true,false}}], guidance, ...}` —
143
+ exactly a failproofai pack manifest's semantic entry. No JavaScript.
144
+ * `both` — the two together. FailproofAI Cloud makes the JavaScript's
145
+ verdict reviewable by exactly its own checks, and deploys them as one.
146
+
147
+ Jev checks run on FailproofAI Cloud; nothing is installed on the machine.
148
+ A machine gets a `both` policy's JavaScript, and asks FailproofAI Cloud
149
+ about each checked tool call while its Jev mode is `observe` or `enforce`.
150
+
151
+ The JavaScript is parse-checked with node before it is sent. Nothing
152
+ downstream does this: the server validates the id and a size ceiling, and a
153
+ broken policy otherwise fails on the machine at enforcement time.
154
+ `--no-verify` skips it; a host without node publishes with a warning rather
155
+ than a block. Jev fields in the JavaScript (`semanticPolicies.add`,
156
+ `authority: "reviewable"`) are refused regardless — put Jev checks in
157
+ `--semantic`. The server checks each declaration (limits, reserved
158
+ FailproofAI names, names unique in the org) and says which one it refused.
122
159
 
123
160
  Source can come from a path, `@path`, a pipe, `-`, or an interactive paste
124
161
  when you give none and stdin is a terminal.
@@ -134,21 +171,42 @@ def policies_publish(
134
171
  * `fp policies publish no-force-push ./rule.mjs`
135
172
  * `cat rule.mjs | fp policies publish no-force-push`
136
173
  * `fp policies publish no-force-push -` — read stdin explicitly
174
+ * `fp policies publish prod-db-intent --kind jev --semantic ./checks.json`
175
+ * `fp policies publish no-prod-db --kind both --source ./rule.mjs --semantic ./checks.json`
137
176
  """
138
177
  state: AppState = ctx.obj
139
178
  deny_in_key_mode(state, "policies publish", _KEY_MODE_REASON)
140
179
  cctx = require_auth(state)
141
180
 
181
+ if source is not None and source_opt is not None:
182
+ raise click.UsageError("give the JavaScript source once — positionally or with --source, not both")
183
+ source = source if source is not None else source_opt
184
+ if source == "-" and semantic_path == "-":
185
+ raise click.UsageError("only one of the JavaScript source and --semantic can come from stdin")
186
+
187
+ # Whether JavaScript is coming is decided WITHOUT reading stdin: a Jev-only
188
+ # publish must not sit waiting for a paste nobody is going to give.
189
+ wants_source = source is not None or kind in ("regex", "both") or (kind is None and semantic_path is None)
190
+ try:
191
+ resolved_kind = resolve_kind(kind, has_source=wants_source, has_semantic=semantic_path is not None)
192
+ except RefUsageError as exc:
193
+ raise click.UsageError(str(exc))
194
+
142
195
  def _paste_prompt() -> None:
143
196
  output.hint("paste the policy source, then press Ctrl-D")
144
197
 
198
+ text: Optional[str] = None
199
+ semantic = None
145
200
  try:
146
- text = read_source(source, prompt=_paste_prompt)
201
+ if semantic_path is not None:
202
+ semantic = parse_semantic(read_source(semantic_path, isatty=False))
203
+ if wants_source:
204
+ text = read_source(source, prompt=_paste_prompt)
147
205
  except RefUsageError as exc:
148
206
  raise click.UsageError(str(exc))
149
207
  except RefError as exc:
150
208
  raise ApiError(str(exc))
151
- if not text.strip():
209
+ if wants_source and not (text or "").strip():
152
210
  raise ApiError("policy source is empty — nothing to publish")
153
211
 
154
212
  # Nothing downstream parses this. The server checks the id and a size
@@ -156,7 +214,9 @@ def policies_publish(
156
214
  # place for a syntax error to surface. `--no-verify` exists because a
157
215
  # machine without node should still be able to publish, not because
158
216
  # skipping is ever a good idea.
159
- if not no_verify:
217
+ syn = SyntaxResult(ok=True, checked=False,
218
+ message="not checked" if text else "no JavaScript source")
219
+ if text and not no_verify:
160
220
  syn = check_syntax(text)
161
221
  if not syn.ok:
162
222
  raise ApiError(
@@ -167,7 +227,13 @@ def policies_publish(
167
227
  if not syn.checked and not output.is_json():
168
228
  output.warn(syn.message)
169
229
 
170
- created = api.publish_policy(cctx, policy_id, text, description)
230
+ created = api.publish_policy(
231
+ cctx, policy_id, text, description,
232
+ # Sent only when it says something the server's default does not, so a
233
+ # plain JavaScript publish is the same request it has always been.
234
+ kind=None if resolved_kind == "regex" and kind is None else resolved_kind,
235
+ semantic=semantic,
236
+ )
171
237
 
172
238
  # Which machines already carry this policy, and at which version. Publishing
173
239
  # deploys nothing, so this is the one thing the card must not guess at: it
@@ -191,7 +257,7 @@ def policies_publish(
191
257
  output.emit_json({**created.to_dict(), "carriers": carriers, "syntax": syn.to_dict()})
192
258
  return
193
259
  output.render_policy_published(created, carriers=carriers,
194
- source_bytes=len(text.encode("utf-8")))
260
+ source_bytes=len((text or "").encode("utf-8")))
195
261
 
196
262
 
197
263
  def policies_enable(