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
@@ -28,20 +28,34 @@ re-reads rather than reporting a success that erased somebody.
28
28
  """
29
29
  from __future__ import annotations
30
30
 
31
+ import json
31
32
  import re
32
33
  import sys
33
- from dataclasses import dataclass
34
- from typing import Dict, Iterable, List, Optional, Sequence, Tuple
34
+ from dataclasses import dataclass, replace as dataclass_replace
35
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
35
36
 
36
37
  from .errors import ApiError
37
38
  from .models import PolicyRef, PolicyVersion
38
39
 
39
40
  VALID_EFFECTS = ("enforce", "observe")
40
41
 
42
+ #: What a policy version carries: JavaScript (`regex`), Jev checks (`jev`), or
43
+ #: JavaScript reviewable by its own Jev checks (`both`).
44
+ VALID_KINDS = ("regex", "jev", "both")
45
+
46
+ #: The server's (and the machine's) cap on Jev declarations per policy version.
47
+ MAX_JEV_DECLARATIONS = 24
48
+ AGENT_INTEGRATIONS = frozenset({
49
+ "claude", "codex", "copilot", "cursor", "opencode", "pi",
50
+ "hermes", "openclaw", "factory", "devin", "antigravity", "goose",
51
+ })
52
+ AGENT_ID = re.compile(r"^agt_[0-9a-f]{16,32}$")
53
+
41
54
  #: `id`, `id@3`, `id:observe`, `id@3:observe`. The id charset mirrors the
42
55
  #: server's `safe_identifier`, so a ref this accepts is one the server will too
43
56
  #: — a rejection should come from the policy not existing, not from parsing.
44
57
  _REF = re.compile(r"^(?P<id>[A-Za-z0-9._-]{1,128})(?:@(?P<version>\d+))?(?:[:](?P<effect>[a-z]+))?$")
58
+ _POLICY_ID = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
45
59
 
46
60
 
47
61
  class RefError(ValueError):
@@ -103,12 +117,17 @@ class DeployPlan:
103
117
  changed: List[Tuple[PolicyRef, PolicyRef]]
104
118
  unchanged: List[PolicyRef]
105
119
 
120
+ @property
121
+ def set_changes(self) -> bool:
122
+ """Whether the POLICY SET changes — anything added, removed or moved."""
123
+ return bool(self.added or self.removed or self.changed)
124
+
106
125
  @property
107
126
  def is_noop(self) -> bool:
108
- return not (self.added or self.removed or self.changed)
127
+ return not self.set_changes
109
128
 
110
129
  def to_dict(self) -> Dict[str, object]:
111
- return {
130
+ out: Dict[str, object] = {
112
131
  "machineId": self.machine_id,
113
132
  "base": self.base,
114
133
  "result": [p.to_dict() for p in self.result],
@@ -118,6 +137,7 @@ class DeployPlan:
118
137
  "unchanged": [p.to_dict() for p in self.unchanged],
119
138
  "noop": self.is_noop,
120
139
  }
140
+ return out
121
141
 
122
142
 
123
143
  def latest_versions(policies: Iterable[PolicyVersion]) -> Dict[str, int]:
@@ -174,7 +194,10 @@ def resolve_ref(
174
194
  )
175
195
  if effect is None:
176
196
  effect = existing.effect if existing else "enforce"
177
- return PolicyRef(id=pid, version=version, effect=effect)
197
+ return PolicyRef(
198
+ id=pid, version=version, effect=effect,
199
+ agent_targets=existing.agent_targets if existing else None,
200
+ )
178
201
 
179
202
 
180
203
  def plan_deploy(
@@ -187,13 +210,23 @@ def plan_deploy(
187
210
  replace: Optional[Sequence[str]] = None,
188
211
  latest: Optional[Dict[str, int]] = None,
189
212
  disabled: Optional[set] = None,
213
+ kinds: Optional[Dict[Tuple[str, int], str]] = None,
214
+ targets: Sequence[str] = (),
215
+ all_agents: Sequence[str] = (),
190
216
  ) -> DeployPlan:
191
217
  """Compute the full resulting set, plus the diff to show before writing.
192
218
 
193
219
  `replace` (`--set`) is exclusive with `add`/`remove`: mixing "these exactly"
194
220
  with "these as well" has no single obvious reading, and guessing one would
195
221
  be guessing about somebody's fleet.
222
+
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.
196
228
  """
229
+ del kinds
197
230
  latest = latest or {}
198
231
  current_list = list(current or [])
199
232
  current_map = {p.id: p for p in current_list}
@@ -220,13 +253,41 @@ def plan_deploy(
220
253
  ref = resolve_ref(token, latest=latest, current=current_map, disabled=disabled)
221
254
  result_map[ref.id] = ref
222
255
 
256
+ grouped: Dict[str, List[Dict[str, str]]] = {}
257
+ for token in targets:
258
+ if "=" not in token:
259
+ raise RefUsageError(f"--target {token!r} must be POLICY=INTEGRATION[/agt_ID]")
260
+ pid, selector = token.split("=", 1)
261
+ if not _POLICY_ID.fullmatch(pid):
262
+ raise RefUsageError(f"--target policy id {pid!r} is invalid")
263
+ integration, _, instance_id = selector.partition("/")
264
+ if integration not in AGENT_INTEGRATIONS or ("/" in selector and not AGENT_ID.fullmatch(instance_id)):
265
+ raise RefUsageError(f"--target {token!r} has an unknown integration or invalid profile ID")
266
+ entry = {"integration": integration}
267
+ if instance_id:
268
+ entry["instanceId"] = instance_id
269
+ collection = grouped.setdefault(pid, [])
270
+ if entry in collection or len(collection) == 32:
271
+ raise RefUsageError("--target has a duplicate selector or more than 32 selectors for a policy")
272
+ collection.append(entry)
273
+ clears = set(all_agents)
274
+ if clears.intersection(grouped):
275
+ raise RefUsageError("--all-agents and --target cannot name the same policy")
276
+ for pid in clears:
277
+ if not _POLICY_ID.fullmatch(pid):
278
+ raise RefUsageError(f"--all-agents policy id {pid!r} is invalid")
279
+ for pid in grouped.keys() | clears:
280
+ if pid not in result_map:
281
+ raise RefUsageError(f"{pid!r} is not in the resulting set — add or keep it before targeting")
282
+ result_map[pid] = dataclass_replace(result_map[pid], agent_targets=grouped.get(pid))
283
+
223
284
  result = sorted(result_map.values(), key=lambda p: p.id)
224
285
  added, removed, changed, unchanged = [], [], [], []
225
286
  for pid, ref in sorted(result_map.items()):
226
287
  was = current_map.get(pid)
227
288
  if was is None:
228
289
  added.append(ref)
229
- elif (was.version, was.effect) != (ref.version, ref.effect):
290
+ elif (was.version, was.effect, was.agent_targets) != (ref.version, ref.effect, ref.agent_targets):
230
291
  changed.append((was, ref))
231
292
  else:
232
293
  unchanged.append(ref)
@@ -245,6 +306,63 @@ def plan_deploy(
245
306
  )
246
307
 
247
308
 
309
+ def version_kinds(policies: Iterable[PolicyVersion]) -> Dict[Tuple[str, int], str]:
310
+ """`{(id, version): kind}` for every published version."""
311
+ return {(p.id, p.version): p.kind for p in policies}
312
+
313
+
314
+ def resolve_kind(kind: Optional[str], *, has_source: bool, has_semantic: bool) -> str:
315
+ """The kind a publish is, checked against what it was given.
316
+
317
+ Explicit wins; otherwise it follows from the inputs — JavaScript alone is
318
+ `regex` (what `publish` has always done), declarations alone `jev`, both
319
+ together `both`. A kind that contradicts its inputs is a usage error: a
320
+ `jev` policy has no JavaScript, and `regex` has no declarations.
321
+ """
322
+ if kind is None:
323
+ kind = "both" if (has_source and has_semantic) else ("jev" if has_semantic else "regex")
324
+ if kind not in VALID_KINDS:
325
+ raise RefUsageError(f"--kind {kind!r} is not one of {', '.join(VALID_KINDS)}")
326
+ if kind == "regex" and has_semantic:
327
+ raise RefUsageError("--kind regex takes no --semantic; use --kind both for JavaScript plus Jev checks")
328
+ if kind == "jev" and has_source:
329
+ raise RefUsageError("--kind jev takes no JavaScript source; use --kind both for JavaScript plus Jev checks")
330
+ if kind in ("jev", "both") and not has_semantic:
331
+ raise RefUsageError(f"--kind {kind} needs --semantic <file.json> with its Jev declarations")
332
+ return kind
333
+
334
+
335
+ def parse_semantic(text: str, *, what: str = "--semantic") -> List[Any]:
336
+ """The Jev declarations in a `--semantic` file: a JSON array of declaration
337
+ objects (a `{"semantic": [...]}` wrapper is accepted too).
338
+
339
+ Deliberately a SHAPE check only — 1 to 24 objects, each with a name. What a
340
+ declaration may say (modes, probes, caps, reserved names) is checked by the
341
+ server, and authoritatively by each machine's own parser; a third copy of
342
+ those rules here is a third thing to drift. The server's refusal names the
343
+ declaration and the rule.
344
+ """
345
+ try:
346
+ value = json.loads(text)
347
+ except ValueError as exc:
348
+ raise RefUsageError(f"{what} is not valid JSON: {exc}")
349
+ if isinstance(value, dict) and isinstance(value.get("semantic"), list):
350
+ value = value["semantic"]
351
+ if not isinstance(value, list):
352
+ raise RefUsageError(f"{what} must be a JSON array of Jev declarations")
353
+ if not value:
354
+ raise RefUsageError(f"{what} holds no Jev declarations")
355
+ if len(value) > MAX_JEV_DECLARATIONS:
356
+ raise RefUsageError(
357
+ f"{what} holds {len(value)} Jev declarations; a policy version carries at most "
358
+ f"{MAX_JEV_DECLARATIONS}"
359
+ )
360
+ for i, entry in enumerate(value):
361
+ if not isinstance(entry, dict) or not isinstance(entry.get("name"), str) or not entry["name"]:
362
+ raise RefUsageError(f"{what} declaration #{i} is not an object with a name")
363
+ return value
364
+
365
+
248
366
  def check_race(base: Optional[int], returned: int) -> None:
249
367
  """Raise when a deploy landed on top of somebody else's.
250
368
 
@@ -724,7 +724,17 @@ class AuditFinding:
724
724
 
725
725
  @dataclass
726
726
  class PolicyVersion:
727
- """One published version of a policy. Versions are minted, never edited."""
727
+ """One published version of a policy. Versions are minted, never edited.
728
+
729
+ ``kind`` is what the version carries: ``regex`` (JavaScript only — every
730
+ version published before kinds existed), ``jev`` (Jev checks only, no
731
+ JavaScript) or ``both`` (JavaScript whose verdict its own Jev checks may
732
+ clear). ``semantic`` is the list of Jev declarations for ``jev``/``both``.
733
+ ``authority``/``reviewed_by`` are DERIVED by the server from the kind and
734
+ never sent by a client. ``jev_chars`` is what its Jev checks take of a
735
+ machine's Jev question budget, measured by the server the way the machine
736
+ measures it; ``None`` for ``regex``.
737
+ """
728
738
 
729
739
  id: str
730
740
  version: int
@@ -735,9 +745,18 @@ class PolicyVersion:
735
745
  created_by: Optional[str]
736
746
  disabled: bool
737
747
  archived: bool
748
+ kind: str = "regex"
749
+ semantic: Optional[List[Any]] = None
750
+ semantic_sha256: Optional[str] = None
751
+ authority: Optional[str] = None
752
+ reviewed_by: Optional[List[str]] = None
753
+ jev_chars: Optional[int] = None
738
754
 
739
755
  @classmethod
740
756
  def from_dict(cls, d: Dict[str, Any]) -> "PolicyVersion":
757
+ semantic = d.get("semantic")
758
+ reviewed = d.get("reviewedBy", d.get("reviewed_by"))
759
+ chars = d.get("jevChars", d.get("jev_chars"))
741
760
  return cls(
742
761
  id=str(d.get("id", "")),
743
762
  version=_as_int(d.get("version"), 0),
@@ -748,18 +767,46 @@ class PolicyVersion:
748
767
  created_by=d.get("createdBy", d.get("created_by")),
749
768
  disabled=bool(d.get("disabled", False)),
750
769
  archived=bool(d.get("archived", False)),
770
+ # A server that predates kinds sends none, and everything it has is
771
+ # JavaScript — `regex` is not a guess, it is the only thing it had.
772
+ kind=str(d.get("kind") or "regex"),
773
+ semantic=semantic if isinstance(semantic, list) else None,
774
+ semantic_sha256=d.get("semanticSha256", d.get("semantic_sha256")),
775
+ authority=d.get("authority"),
776
+ reviewed_by=[str(n) for n in reviewed] if isinstance(reviewed, list) else None,
777
+ jev_chars=chars if isinstance(chars, int) and not isinstance(chars, bool) else None,
751
778
  )
752
779
 
780
+ @property
781
+ def jev_names(self) -> List[str]:
782
+ """The names of this version's Jev checks, in declared order."""
783
+ return [
784
+ str(entry["name"])
785
+ for entry in (self.semantic or [])
786
+ if isinstance(entry, dict) and isinstance(entry.get("name"), str)
787
+ ]
788
+
753
789
  def to_dict(self) -> Dict[str, Any]:
754
790
  """The server's own shape. `vars()` would leak Python snake_case into a
755
791
  contract that is camelCase everywhere else, which is a difference a
756
792
  harness discovers at runtime rather than in review."""
757
- return {
793
+ out: Dict[str, Any] = {
758
794
  "id": self.id, "version": self.version, "description": self.description,
759
795
  "sha256": self.sha256, "source": self.source, "createdAt": self.created_at,
760
796
  "createdBy": self.created_by, "disabled": self.disabled,
761
- "archived": self.archived,
797
+ "archived": self.archived, "kind": self.kind,
762
798
  }
799
+ if self.semantic is not None:
800
+ out["semantic"] = self.semantic
801
+ if self.semantic_sha256:
802
+ out["semanticSha256"] = self.semantic_sha256
803
+ if self.authority:
804
+ out["authority"] = self.authority
805
+ if self.reviewed_by is not None:
806
+ out["reviewedBy"] = self.reviewed_by
807
+ if self.jev_chars is not None:
808
+ out["jevChars"] = self.jev_chars
809
+ return out
763
810
 
764
811
 
765
812
  @dataclass
@@ -774,6 +821,9 @@ class PolicyRef:
774
821
  id: str
775
822
  version: int
776
823
  effect: str = "enforce"
824
+ #: None means every agent; a nonempty selector list narrows this
825
+ #: machine's assignment, never the immutable published policy version.
826
+ agent_targets: Optional[List[Dict[str, str]]] = None
777
827
 
778
828
  @classmethod
779
829
  def from_dict(cls, d: Dict[str, Any]) -> "PolicyRef":
@@ -781,14 +831,23 @@ class PolicyRef:
781
831
  id=str(d.get("id", "")),
782
832
  version=_as_int(d.get("version"), 0),
783
833
  effect=str(d.get("effect") or "enforce"),
834
+ agent_targets=[dict(target) for target in d["agentTargets"]]
835
+ if isinstance(d.get("agentTargets"), list) else None,
784
836
  )
785
837
 
786
838
  def to_dict(self) -> Dict[str, Any]:
787
- return {"id": self.id, "version": self.version, "effect": self.effect}
839
+ out: Dict[str, Any] = {"id": self.id, "version": self.version, "effect": self.effect}
840
+ if self.agent_targets is not None:
841
+ out["agentTargets"] = self.agent_targets
842
+ return out
788
843
 
789
844
  @property
790
845
  def label(self) -> str:
791
- return f"{self.id}@{self.version}:{self.effect}"
846
+ scope = "" if not self.agent_targets else " → " + ", ".join(
847
+ f"{t['integration']}/{t['instanceId']}" if t.get("instanceId") else f"all {t['integration']}"
848
+ for t in self.agent_targets
849
+ )
850
+ return f"{self.id}@{self.version}:{self.effect}{scope}"
792
851
 
793
852
 
794
853
  @dataclass
@@ -806,6 +865,9 @@ class Deployment:
806
865
  policies: List[PolicyRef]
807
866
  updated_at: str
808
867
  updated_by: Optional[str]
868
+ #: The Jev mode FailproofAI Cloud sets on this machine (`off|observe|enforce`),
869
+ #: or None when Cloud does not override the machine's own (`local`).
870
+ jev_mode: Optional[str] = None
809
871
 
810
872
  @classmethod
811
873
  def from_dict(cls, d: Dict[str, Any]) -> "Deployment":
@@ -815,14 +877,18 @@ class Deployment:
815
877
  policies=[PolicyRef.from_dict(p) for p in (d.get("policies") or [])],
816
878
  updated_at=str(d.get("updatedAt", d.get("updated_at", "")) or ""),
817
879
  updated_by=d.get("updatedBy", d.get("updated_by")),
880
+ jev_mode=d.get("jevMode", d.get("jev_mode")) or None,
818
881
  )
819
882
 
820
883
  def to_dict(self) -> Dict[str, Any]:
821
- return {
884
+ out: Dict[str, Any] = {
822
885
  "machineId": self.machine_id, "deployment": self.deployment,
823
886
  "policies": [p.to_dict() for p in self.policies],
824
887
  "updatedAt": self.updated_at, "updatedBy": self.updated_by,
825
888
  }
889
+ if self.jev_mode:
890
+ out["jevMode"] = self.jev_mode
891
+ return out
826
892
 
827
893
 
828
894
  @dataclass
@@ -851,6 +917,13 @@ class Machine:
851
917
  deployed: bool
852
918
  policy_count: int
853
919
  event_count: int
920
+ #: What the machine last reported it could not apply — `[{id, version, kind,
921
+ #: message}]` — or None when it has never reported (an empty list means it
922
+ #: reported and all is well).
923
+ policy_errors: Optional[List[Dict[str, Any]]] = None
924
+ policy_errors_at: Optional[Any] = None
925
+ #: The Jev mode FailproofAI Cloud sets on this machine, or None (`local`).
926
+ jev_mode: Optional[str] = None
854
927
 
855
928
  @classmethod
856
929
  def from_dict(cls, d: Dict[str, Any]) -> "Machine":
@@ -858,6 +931,8 @@ class Machine:
858
931
  v = d.get(key)
859
932
  return int(v) if isinstance(v, (int, float)) else None
860
933
 
934
+ errors = d.get("policyErrors", d.get("policy_errors"))
935
+
861
936
  return cls(
862
937
  machine_id=str(d.get("machineId", d.get("machine_id", "")) or ""),
863
938
  label=d.get("label"),
@@ -870,6 +945,9 @@ class Machine:
870
945
  deployed=bool(d.get("deployed", False)),
871
946
  policy_count=_as_int(d.get("policyCount"), 0),
872
947
  event_count=_as_int(d.get("eventCount"), 0),
948
+ policy_errors=[e for e in errors if isinstance(e, dict)] if isinstance(errors, list) else None,
949
+ policy_errors_at=d.get("policyErrorsAt", d.get("policy_errors_at")),
950
+ jev_mode=d.get("jevMode", d.get("jev_mode")) or None,
873
951
  )
874
952
 
875
953
  @property
@@ -892,6 +970,11 @@ class Machine:
892
970
  "appliedAt": self.applied_at, "deployed": self.deployed,
893
971
  "policyCount": self.policy_count, "eventCount": self.event_count,
894
972
  "drifted": self.drifted,
973
+ # Emitted only when the server sent them, so a server that predates
974
+ # them reads exactly as it always did.
975
+ **({"policyErrors": self.policy_errors} if self.policy_errors is not None else {}),
976
+ **({"policyErrorsAt": self.policy_errors_at} if self.policy_errors_at is not None else {}),
977
+ **({"jevMode": self.jev_mode} if self.jev_mode else {}),
895
978
  }
896
979
 
897
980
  @property