failproofai 1.0.12-beta.1 → 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 (168) 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.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]__00l2u6j86cb8y._.js +3 -0
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
  78. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/_1fw0_nzkae5ao._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1i8oid1gc7s1f.js → node_modules_html-to-image_es_index_1fv03uigni8s5.js} +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  96. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  97. package/.next/standalone/.next/server/pages/404.html +1 -1
  98. package/.next/standalone/.next/server/pages/500.html +1 -1
  99. package/.next/standalone/.next/server/preview-props.json +3 -3
  100. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  102. package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 0ay__-d2h1xb5.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 0tbm3vjm62h-2.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 1c_jf6g5wnslk.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 1my3-5p47t7r-.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{0jutamwqz8pg5.js → 20py8k1gd_i1m.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
  108. package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
  109. package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 2o6d37j425uci.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
  111. package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 42xptz88mtq3x.js} +1 -1
  112. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  113. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  114. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +9 -18
  115. package/.next/standalone/fp-cloud-cli/README.md +7 -9
  116. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1 -27
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
  118. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
  119. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +2 -71
  120. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +1 -1
  121. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +6 -7
  122. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
  123. package/.next/standalone/hermes-plugin/__init__.py +11 -0
  124. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  125. package/.next/standalone/package.json +11 -10
  126. package/.next/standalone/sdk/python/uv.lock +31 -8
  127. package/.next/standalone/server.js +1 -1
  128. package/dist/cli.mjs +1686 -612
  129. package/dist/worker.mjs +354 -49
  130. package/hermes-plugin/__init__.py +11 -0
  131. package/hermes-plugin/plugin.yaml +1 -1
  132. package/package.json +11 -10
  133. package/src/audit/open-browser.ts +24 -7
  134. package/src/hooks/agent-roster.ts +9 -1
  135. package/src/hooks/agent-skill.ts +203 -0
  136. package/src/hooks/cloud-connection.ts +27 -51
  137. package/src/hooks/cloud-introspect.ts +5 -2
  138. package/src/hooks/cloud-login.ts +248 -0
  139. package/src/hooks/cloud-managed-policies.ts +54 -4
  140. package/src/hooks/codex-system-hooks.ts +10 -4
  141. package/src/hooks/config-status.ts +9 -3
  142. package/src/hooks/configure-wizard.ts +356 -32
  143. package/src/hooks/delivery-health.ts +47 -2
  144. package/src/hooks/effective-reviewers.ts +14 -0
  145. package/src/hooks/fp-config.ts +19 -0
  146. package/src/hooks/handler.ts +21 -2
  147. package/src/hooks/hook-activity-store.ts +7 -1
  148. package/src/hooks/integrations.ts +18 -3
  149. package/src/hooks/jev-activity.ts +117 -1
  150. package/src/hooks/jev-cli.ts +7 -9
  151. package/src/hooks/manager.ts +80 -46
  152. package/src/hooks/policy-evaluator.ts +17 -0
  153. package/src/hooks/policy-reviewability.ts +4 -2
  154. package/src/hooks/semantic/cloud-jev.ts +219 -28
  155. package/src/hooks/semantic/combine.ts +26 -1
  156. package/src/hooks/semantic/jev-client.ts +1 -1
  157. package/src/hooks/semantic/jev-config.ts +6 -27
  158. package/src/hooks/semantic/jev-review.ts +90 -2
  159. package/src/hooks/tui.ts +280 -0
  160. package/src/hooks/uninstall-cli.ts +12 -1
  161. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
  162. package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
  163. package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
  164. package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
  165. package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
  166. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
  167. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
  168. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
@@ -490,7 +490,7 @@ _TOP_LEVEL_GROUPS = [
490
490
  ]),
491
491
  ("ENFORCE", [
492
492
  ("policies", "Write cloud-managed policies.", "list show publish test compose enable disable delete"),
493
- ("fleet", "Deploy policies to machines.", "list show deploy jev-mode diff history rollback rename"),
493
+ ("fleet", "Deploy policies to machines.", "list show deploy diff history rollback rename"),
494
494
  ("guardrails", "What enforcement actually blocked.", "summary timeline"),
495
495
  ]),
496
496
  ("MANAGE", [
@@ -6312,16 +6312,6 @@ def render_deploy_plan(plan: Any, *, applied: bool = False) -> None:
6312
6312
  footer.append(f"polic{'y' if n == 1 else 'ies'} after this change", style=theme.LABEL)
6313
6313
  lines.append(Text())
6314
6314
  lines.append(footer)
6315
- if getattr(plan, "jev_mode", None) is not None:
6316
- before = plan.jev_mode_before or "local"
6317
- after = plan.jev_mode_after or "local"
6318
- jev = Text(" jev mode ", style=theme.LABEL)
6319
- if plan.jev_mode_changes:
6320
- jev.append(f"{before} → ", style=theme.FAINT)
6321
- jev.append(after, style=f"bold {theme.ACCENT}")
6322
- else:
6323
- jev.append(f"{after} (unchanged)", style=theme.TEXT_DIM)
6324
- lines.append(jev)
6325
6315
 
6326
6316
  head = Text(plan.machine_id, style=f"bold {theme.TEXT}")
6327
6317
  if plan.base is not None:
@@ -6482,65 +6472,6 @@ def deployment_rolled_back(
6482
6472
  _notice_box(body, color=theme.SUCCESS, title="rolled back")
6483
6473
 
6484
6474
 
6485
- def render_jev_mode_plan(rows: Sequence[dict]) -> None:
6486
- """The mode-only change, per machine: ``from → to``, the set untouched."""
6487
- out = []
6488
- for r in rows:
6489
- changed = bool(r.get("changed"))
6490
- mode = Text()
6491
- if changed:
6492
- mode.append(f"{r.get('from')} → ", style=theme.FAINT)
6493
- mode.append(str(r.get("to")), style=f"bold {theme.ACCENT}")
6494
- else:
6495
- mode.append(f"{r.get('to')} (unchanged)", style=theme.TEXT_DIM)
6496
- out.append([Text(str(r.get("machineId")), style=theme.TEXT), mode])
6497
- n = sum(1 for r in rows if r.get("changed"))
6498
- title = Text()
6499
- title.append("jev mode", style=f"bold {theme.ACCENT}")
6500
- title.append(" · ", style=theme.FAINT)
6501
- title.append(f"{n} of {len(rows)} to change", style=theme.LABEL)
6502
- title.append(" · policy sets untouched", style=theme.FAINT)
6503
- render_list_panel("jev mode", header=["machine", "jev mode"], rows=out, days=set(), order=None,
6504
- empty_message="no machines", title=title)
6505
-
6506
-
6507
- def jev_mode_applied(rows: Sequence[dict]) -> None:
6508
- """``✓ jev mode observe → enforce on N machines``, and any that failed."""
6509
- done = [r for r in rows if r.get("changed") and not r.get("error")]
6510
- failed = [r for r in rows if r.get("error")]
6511
- for r in done:
6512
- body = Text()
6513
- body.append("✓ ", style=theme.SUCCESS)
6514
- body.append("jev mode ", style=theme.TEXT)
6515
- body.append(f"{r.get('from')} → ", style=theme.FAINT)
6516
- body.append(str(r.get("to")), style=f"bold {theme.ACCENT}")
6517
- body.append(" on ", style=theme.TEXT)
6518
- body.append(str(r.get("machineId")), style=theme.ACCENT)
6519
- if r.get("deployment") is not None:
6520
- body.append(" · ", style=theme.FAINT)
6521
- body.append(f"now on deployment #{r.get('deployment')}", style=theme.LABEL)
6522
- _notice_box(body, color=theme.SUCCESS, title="jev mode")
6523
- for r in failed:
6524
- body = Text()
6525
- body.append("✗ ", style=theme.ERROR)
6526
- body.append(str(r.get("machineId")), style=theme.ACCENT)
6527
- body.append(" · ", style=theme.FAINT)
6528
- body.append(str(r.get("error")), style=theme.TEXT)
6529
- _notice_box(body, color=theme.ERROR, title="jev mode not changed")
6530
-
6531
-
6532
- def jev_mode_unchanged(machine_ids: Sequence[str], mode: str) -> None:
6533
- """Every machine already has the mode. Calm, like a no-op deploy."""
6534
- body = Text()
6535
- body.append("= ", style=theme.FAINT)
6536
- body.append(", ".join(machine_ids) if len(machine_ids) <= 3 else f"{len(machine_ids)} machines",
6537
- style=theme.ACCENT)
6538
- body.append(f" already {'has' if len(machine_ids) == 1 else 'have'} jev mode {mode}", style=theme.TEXT)
6539
- body.append(" · ", style=theme.FAINT)
6540
- body.append("nothing changed", style=theme.LABEL)
6541
- _notice_box(body, color=theme.ACCENT, title="no change")
6542
-
6543
-
6544
6475
  def machine_renamed(machine_id: str, label: str) -> None:
6545
6476
  body = Text()
6546
6477
  body.append("✓ ", style=theme.SUCCESS)
@@ -6819,7 +6750,7 @@ def render_deployment_history(machine_id: str, entries: Sequence[dict],
6819
6750
  [("+", f"{i} → {scope[i]}") for i in sorted(set(cur) - set(prev))]
6820
6751
  + [("-", i) for i in sorted(set(prev) - set(cur))]
6821
6752
  + [("~", f"{i} → {scope[i]}") for i in sorted(set(cur) & set(prev)) if cur[i] != prev[i]]
6822
- # A mode-only generation (`fp fleet jev-mode`) read as "no
6753
+ # A mode-only generation (a legacy jev-mode change) read as "no
6823
6754
  # change", which is exactly the change a rollback brings back.
6824
6755
  + ([("~", f"jev {prev_mode}→{mode}")] if mode != prev_mode else [])
6825
6756
  )
@@ -197,7 +197,7 @@ you need a flag you don't already know.
197
197
 
198
198
  **Enforce (cloud-managed policy, session-only — see §2):**
199
199
  - `policies list|show|publish|enable|disable|delete|test|compose` — policy versions. `publish` mints a version of a kind — `regex` (a local `.mjs`), `jev` (`--semantic checks.json`: Jev declarations, no JavaScript) or `both` (the two, the JavaScript reviewable by its own checks); list/show print the kind and the Jev check names; `test` runs one against a synthetic context locally (it applies each policy's `match` filter, so a policy that does not cover the `--event`/`--tool` you pass is reported `skipped`, not run). `enable`/`disable`/`delete` take `--yes`.
200
- - `fleet list|show|deploy|jev-mode|diff|history|rollback|rename` — which machines run which policies, and the policy errors each machine reports. **`deploy` REPLACES a machine's whole set** (`--add`/`--remove` amend it, `--set` replaces, `--create` mints a deployment, `--jev-mode off|observe|enforce|local` sets the machine's Jev mode from FailproofAI Cloud — overriding its own, machine-wide, and left alone when omitted; in `enforce` Jev's checks BLOCK calls as well as clear). `jev-mode <machine…|--all> <mode>` changes only the mode, never the set; `rollback` restores the generation's Jev mode too; it prints the plan and asks **only on an interactive terminal without `--json`** — under `--json` or with stdin redirected it applies immediately, so read `fleet show` first if you want review.
200
+ - `fleet list|show|deploy|diff|history|rollback|rename` — which machines run which policies, and the policy errors each machine reports. **`deploy` REPLACES a machine's whole set** (`--add`/`--remove` amend it, `--set` replaces, `--create` mints a deployment). There is no Jev mode to set: FailproofAI Cloud turns Jev on for a machine while a `jev`/`both` policy is deployed to it (off otherwise); trial one with `:observe`. `deploy` prints the plan and asks **only on an interactive terminal without `--json`** — under `--json` or with stdin redirected it applies immediately, so read `fleet show` first if you want review.
201
201
  - `guardrails summary|timeline` — what enforcement actually did; `--since 1h|6h|24h|7d`, `--machine`.
202
202
 
203
203
  **Analytics & assistant:**
@@ -235,14 +235,14 @@ Cloud-managed enforcement, split the way the dashboard splits it: `policies` wri
235
235
 
236
236
  ### fleet
237
237
  - `fleet list` — `machine · label · pol · intended · applied · seen · events · state`. `intended` is the generation deployed, `applied` what the machine last collected (they differ until it polls), and `seen` how long since it last reported anything — a machine can be in sync and dead, or alive and behind, which are different problems. `state` reads `N errors` when the machine reported policies it could not apply. JSON `{machines, deployments}` with raw epoch-ms timestamps plus the computed `drifted`; a machine carries `policyErrors`/`policyErrorsAt` and `jevMode` when the server has them.
238
- - `fleet show <machine>` — the set the machine is told to run, **and whether it has collected it**. Reads both the deployment and the machine record, so it reports `not yet collected` / `machine is on #N` / `collected` alongside who deployed it, when, and last-seen. A machine can be told to run a policy it has never picked up; the policy list alone cannot tell you which. Also the **Jev mode** FailproofAI Cloud sets on it (`local` when none) and the **policy errors** the machine last reported — `[{id, version, kind: regex|jev|both|daemon, message}]`: a policy that did not load, `jev_unconfigured` (a Jev mode, and the machine's key does not carry Jev), `transcripts_disabled` (a Jev mode on a machine connected with `--no-transcripts`, which never asks), `jev_budget: dropped <name>` (an installed pack's check FailproofAI Cloud left out of a request for the question budget). Whatever is listed there is not enforcing. JSON `{machine, deployment}` with raw timestamps and both label fields; `deployment: null` when nothing is deployed.
239
- - `fleet deploy <machine> [--add REF]… [--remove ID]… [--set REF]… [--jev-mode off|observe|enforce|local] [--create] [-y]`
238
+ - `fleet show <machine>` — the set the machine is told to run, **and whether it has collected it**. Reads both the deployment and the machine record, so it reports `not yet collected` / `machine is on #N` / `collected` alongside who deployed it, when, and last-seen. A machine can be told to run a policy it has never picked up; the policy list alone cannot tell you which. Also the **Jev mode** FailproofAI Cloud derived for it from its policies (`local` when none) and the **policy errors** the machine last reported — `[{id, version, kind: regex|jev|both|daemon, message}]`: a policy that did not load, `jev_unconfigured` (a Jev mode, and the machine's key does not carry Jev), `transcripts_disabled` (a Jev mode on a machine connected with `--no-transcripts`, which never asks), `jev_budget: dropped <name>` (an installed pack's check FailproofAI Cloud left out of a request for the question budget). Whatever is listed there is not enforcing. JSON `{machine, deployment}` with raw timestamps and both label fields; `deployment: null` when nothing is deployed.
239
+ - `fleet deploy <machine> [--add REF]… [--remove ID]… [--set REF]… [--create] [-y]`
240
240
 
241
241
  **A deploy REPLACES the whole set.** The endpoint takes the full list and does not merge. `--add`/`--remove` are a read-modify-write: the CLI reads the current set, applies the delta, prints the complete result, writes that. `--set` replaces everything and is refused alongside `--add`/`--remove`.
242
242
 
243
- REF is `id`, `id@version`, `id:effect`, or `id@version:effect`. Effect is `enforce` (default) or `observe`. A bare `--add` of an already-deployed policy **keeps its pinned version** — pass `id@version` to move it. Every kind deploys the same way (a `both` policy's JavaScript and its checks go together); the effect applies to JavaScript only, so `observe` on a `jev` policy is refused (**exit 2**) — watch Jev through the mode instead — and an `observe` `both` policy sends only its (observed) JavaScript: its Jev checks are not asked while it is observed.
243
+ REF is `id`, `id@version`, `id:effect`, or `id@version:effect`. Effect is `enforce` (default) or `observe`. A bare `--add` of an already-deployed policy **keeps its pinned version** — pass `id@version` to move it. Every kind deploys the same way (a `both` policy's JavaScript and its checks go together), and `observe` applies to every kind: a `jev` policy's checks, or both halves of a `both` policy, are asked and recorded and decide nothing.
244
244
 
245
- `--jev-mode` sets the machine's Jev mode from FailproofAI Cloud and **overrides the machine's own, a local `off` included**: `observe` asks Jev and logs it while the regex policies decide; `enforce` lets Jev's checks **block** calls (every `jev` policy's deny checks) as well as clear the regex verdicts they review; `off` stops Jev there; `local` hands the choice back to the machine. The mode is the **machine's**: it also governs the checks of every pack installed there. Omitted, it is left as it is (the request carries no `jevMode`). `--jev-mode` with no change to the set changes **only the mode**, through its own route (like `fleet jev-mode`), so disabled policies' assignments and any concurrent edit are untouched; the plan shows `jev mode <from> → <to>` and the JSON adds `jevModeOnly: true`. On a machine with no deployment yet, it deploys the (empty) set with the mode. Jev checks run on FailproofAI Cloud; nothing is installed on the machine. Under `observe`/`enforce` the machine sends each checked tool call to FailproofAI Cloud on its Cloud Jev credential (its `jev.json` is not used); without one it reports `jev_unconfigured`, and connected with `--no-transcripts` it never asks and reports `transcripts_disabled`.
245
+ There is no Jev mode to set: FailproofAI Cloud turns Jev on for a machine while a `jev` or `both` policy is deployed to it, and off when none is. Jev checks run on FailproofAI Cloud; nothing is installed on the machine. The machine sends each checked tool call of an agent such a policy targets to FailproofAI Cloud on its Cloud Jev credential; without one it reports `jev_unconfigured`, and connected with `--no-transcripts` it never asks and reports `transcripts_disabled`.
246
246
 
247
247
  Deploying to an id that has never checked in is refused (a typo would mint a machine); `--create` allows it for pre-staging.
248
248
 
@@ -252,10 +252,9 @@ Cloud-managed enforcement, split the way the dashboard splits it: `policies` wri
252
252
 
253
253
  **Exit codes.** A malformed ref (`bad ref!!`, `id:banana`, empty), or `--set` combined with `--add`/`--remove`, is a usage error → **exit 2**, like every other bad flag value. A ref that parses but names something that does not exist (`--add ghost-policy`, an unpublished `@version`) is **exit 1**; an unknown machine is **exit 6**. Branch on these rather than on the message.
254
254
 
255
- JSON `{plan:{result,added,removed,changed,unchanged,noop[,jevMode:{from,to,changed}]}, deployment, applied}` — the plan is included so a harness does not recompute the diff.
256
- - `fleet jev-mode <machine>… <mode>` · `fleet jev-mode --all <mode> [-y]` — set the Jev mode FailproofAI Cloud gives machines (`off|observe|enforce|local`) and **nothing else**: the policy set is never read back or rewritten. A machine already on the mode is skipped; `--all` covers every machine with a deployment and skips the rest; a named machine with no deployment is refused before anything is written (`fleet deploy <machine> --jev-mode <mode>` gives it its first). Each change is a new generation in `fleet history`. One machine's failure does not stop the others; the exit code is 1 and the JSON row carries `error`. A bad mode, or `--all` with machine ids, is **exit 2**. JSON `{mode, applied, machines:[{machineId, from, to, changed, deployment?, error?}]}`.
255
+ JSON `{plan:{result,added,removed,changed,unchanged,noop}, deployment, applied}` — the plan is included so a harness does not recompute the diff.
257
256
  - `fleet diff [machine]` — intent vs delivery per machine, with a `drifted` flag. A machine id nobody has reported under is refused (exit 6), not rendered as an empty fleet.
258
- - `fleet history <machine>` · `fleet rollback <machine> <generation> [-y]` — rollback mints a NEW generation carrying the old set; history stays append-only. The `change` column uses the deploy plan's vocabulary: `+` added, `-` removed, `~` same policy at a different version or effect (an enforce → observe flip is a policy that stopped blocking, so it is never "no change"), and `~ jev <from>→<to>` for a Jev mode change. The `jev` column is the mode FailproofAI Cloud set with each generation (`local` = none). **A rollback restores that generation's Jev mode too**, `local` included: the prompt and the result say `jev mode <now> → <then>`, and the JSON adds `jevModeBefore`.
257
+ - `fleet history <machine>` · `fleet rollback <machine> <generation> [-y]` — rollback mints a NEW generation carrying the old set; history stays append-only. The `change` column uses the deploy plan's vocabulary: `+` added, `-` removed, `~` same policy at a different version or effect (an enforce → observe flip is a policy that stopped blocking, so it is never "no change"), and `~ jev <from>→<to>` for a Jev mode change. The `jev` column is the mode FailproofAI Cloud had with each generation (`local` = none); a rollback's prompt and result say `jev mode <now> → <then>` when it differs, and the JSON adds `jevModeBefore`.
259
258
  - `fleet rename <machine> "<label>"` — a human label; the id never changes. The server stores it as an override beside the machine's self-asserted label. An empty label **clears** the override (the machine falls back to its own label, else its id) and the CLI says so rather than reporting a rename to nothing. A machine that has never checked in cannot be renamed → exit 6.
260
259
 
261
260
  ### guardrails
@@ -1,8 +1,8 @@
1
1
  """FailproofAI Cloud Jev / regex / both policies in `fp` (CONTRACT C4, C6).
2
2
 
3
3
  `fp policies publish --kind regex|jev|both [--source] [--semantic]`, list/show
4
- carrying the kind and the Jev check names, `fp fleet deploy --jev-mode`, and
5
- machines showing the policy errors they report.
4
+ carrying the kind and the Jev check names, deploys (no Jev mode: Cloud derives
5
+ it), and machines showing the policy errors they report.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
@@ -144,7 +144,7 @@ def test_semantic_refuses_what_is_not_a_declaration_list(text, needle):
144
144
  parse_semantic(text)
145
145
 
146
146
 
147
- # ── deploy plan: --jev-mode ───────────────────────────────────────────────────
147
+ # ── deploy plan: no Jev mode ──────────────────────────────────────────────────
148
148
 
149
149
 
150
150
  def _plan(**kw):
@@ -153,38 +153,16 @@ def _plan(**kw):
153
153
  return plan_deploy("m1", **base)
154
154
 
155
155
 
156
- def test_jev_mode_alone_is_a_change_and_is_carried_in_the_plan():
157
- plan = _plan(jev_mode="observe")
158
- assert not plan.is_noop and plan.jev_mode_changes
159
- assert plan.to_dict()["jevMode"] == {"from": "local", "to": "observe", "changed": True}
160
-
161
-
162
- def test_the_same_jev_mode_again_is_a_noop():
163
- assert _plan(jev_mode="observe", current_jev_mode="observe").is_noop
164
- assert _plan(jev_mode="local").is_noop # already local
165
-
166
-
167
- def test_local_hands_the_mode_back_to_the_machine():
168
- plan = _plan(jev_mode="local", current_jev_mode="enforce")
169
- assert plan.jev_mode_changes and plan.jev_mode_after is None
170
- assert plan.to_dict()["jevMode"] == {"from": "enforce", "to": "local", "changed": True}
171
-
172
-
173
- def test_no_jev_mode_says_nothing_and_changes_nothing():
174
- plan = _plan(add=["a"], current_jev_mode="enforce")
156
+ def test_a_plan_never_carries_a_jev_mode():
157
+ with pytest.raises(TypeError):
158
+ _plan(jev_mode="observe")
159
+ plan = _plan(add=["a"])
175
160
  assert plan.is_noop and "jevMode" not in plan.to_dict()
176
161
 
177
162
 
178
- def test_an_unknown_jev_mode_is_a_usage_error():
179
- with pytest.raises(RefUsageError, match="not one of off, observe, enforce, local"):
180
- _plan(jev_mode="shadow")
181
-
182
-
183
- def test_observe_on_a_jev_policy_is_refused_with_the_mode_that_does_it():
163
+ def test_observe_applies_to_a_jev_policy_like_any_other():
184
164
  kinds = version_kinds([PolicyVersion.from_dict(dict(SERVER_JEV, id="j", version=2))])
185
- with pytest.raises(RefUsageError, match="use --jev-mode observe"):
186
- _plan(add=["j:observe"], kinds=kinds)
187
- # enforce is fine, and so is observe on a `both` policy (it applies to the JS half).
165
+ assert _plan(add=["j:observe"], kinds=kinds).added[0].effect == "observe"
188
166
  assert _plan(add=["j"], kinds=kinds).added[0].effect == "enforce"
189
167
  both = version_kinds([PolicyVersion.from_dict(dict(SERVER_BOTH, id="j", version=2))])
190
168
  assert _plan(add=["j:observe"], kinds=both).added[0].effect == "observe"
@@ -213,14 +191,13 @@ def test_publish_still_refuses_jev_fields_in_the_javascript(monkeypatch):
213
191
  assert "--kind both --semantic" in (exc.value.hint or "")
214
192
 
215
193
 
216
- def test_deploy_sends_jev_mode_only_when_given(monkeypatch):
194
+ def test_deploy_never_sends_a_jev_mode(monkeypatch):
217
195
  bodies = []
218
196
  monkeypatch.setattr(client, "_request_json",
219
197
  lambda ctx, method, path, json_body=None: bodies.append(json_body) or {})
220
198
  client.deploy_policies(None, "m1", [PolicyRef("a", 1)])
221
- client.deploy_policies(None, "m1", [PolicyRef("a", 1)], jev_mode="local")
222
199
  assert "jevMode" not in bodies[0]
223
- assert bodies[1]["jevMode"] == "local"
200
+ assert not hasattr(client, "set_jev_mode")
224
201
 
225
202
 
226
203
  # ── the commands, over HTTP ───────────────────────────────────────────────────
@@ -365,25 +342,7 @@ def _fleet_routes(current_policies, jev_mode=None, errors=None):
365
342
 
366
343
 
367
344
  @respx.mock
368
- def test_fleet_deploy_jev_mode_alone_changes_only_the_mode_through_its_own_route(logged_in, runner):
369
- # CONTRACT C9.1: a mode change sent as a deploy rewrote the set the CLI had
370
- # read — which leaves out disabled policies' assignments, so they were lost.
371
- _fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
372
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
373
- mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode").mock(return_value=httpx.Response(
374
- 200, json={"machineId": "m1", "deployment": 6, "jevMode": "observe", "changed": True,
375
- "policies": [{"id": "no-force-push", "version": 2, "effect": "enforce"}]}))
376
- result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
377
- assert result.exit_code == 0, result.output
378
- assert not deploy.called
379
- assert json.loads(mode.calls.last.request.content) == {"jevMode": "observe"}
380
- out = json.loads(result.stdout)
381
- assert out["applied"] is True and out["jevModeOnly"] is True
382
- assert out["plan"]["jevMode"]["to"] == "observe" and out["deployment"]["deployment"] == 6
383
-
384
-
385
- @respx.mock
386
- def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_flag(logged_in, runner):
345
+ def test_fleet_deploy_adds_a_jev_policy_and_sends_no_mode(logged_in, runner):
387
346
  _fleet_routes([], jev_mode="enforce")
388
347
  put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
389
348
  200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce",
@@ -395,13 +354,26 @@ def test_fleet_deploy_adds_a_jev_policy_and_leaves_the_mode_alone_without_the_fl
395
354
 
396
355
 
397
356
  @respx.mock
398
- def test_fleet_deploy_refuses_observe_on_a_jev_policy_before_writing(logged_in, runner):
357
+ def test_fleet_deploy_observes_a_jev_policy(logged_in, runner):
399
358
  _fleet_routes([])
400
- put = respx.put(f"{BASE}/api/enforcement/deployments/m1")
359
+ put = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
360
+ 200, json={"machineId": "m1", "deployment": 6,
361
+ "policies": [{"id": "secrets", "version": 1, "effect": "observe"}]}))
401
362
  result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets:observe", "--yes"])
402
- assert result.exit_code == 2
403
- assert "--jev-mode observe" in result.output + (result.stderr or "")
404
- assert not put.called
363
+ assert result.exit_code == 0, result.output
364
+ assert json.loads(put.calls.last.request.content) == {
365
+ "policies": [{"id": "secrets", "version": 1, "effect": "observe"}]}
366
+
367
+
368
+ @pytest.mark.parametrize("argv", [
369
+ ["fleet", "jev-mode", "m1", "observe"],
370
+ ["fleet", "deploy", "m1", "--jev-mode", "observe"],
371
+ ])
372
+ def test_the_jev_mode_cannot_be_set_by_hand(logged_in, runner, argv):
373
+ with respx.mock(assert_all_called=False) as mock:
374
+ result = runner.invoke(app, argv)
375
+ assert result.exit_code == 2, result.output
376
+ assert not mock.calls
405
377
 
406
378
 
407
379
  @respx.mock
@@ -424,7 +396,7 @@ def test_fleet_show_and_list_show_the_jev_mode_and_the_reported_errors(logged_in
424
396
  assert js["deployment"]["jevMode"] == "observe"
425
397
 
426
398
 
427
- # ── Review fixes: the Jev mode route (C9.1), rollback and history (m6), help (m7) ──
399
+ # ── Rollback and history show the mode Cloud derived (m6) ────────────────────
428
400
 
429
401
 
430
402
  def _fleet_many(deployments, machines=None):
@@ -443,133 +415,6 @@ def _fleet_many(deployments, machines=None):
443
415
  return_value=httpx.Response(200, json=[SERVER_BOTH, SERVER_JEV, SERVER_REGEX_OLD]))
444
416
 
445
417
 
446
- def _mode_route(machine_id, mode, deployment=6, status=200):
447
- body = ({"machineId": machine_id, "deployment": deployment, "jevMode": None if mode == "local" else mode,
448
- "changed": True, "policies": []} if status == 200
449
- else {"error": f"machine {machine_id} has no deployment", "code": "no_deployment"})
450
- return respx.put(f"{BASE}/api/enforcement/deployments/{machine_id}/jev-mode").mock(
451
- return_value=httpx.Response(status, json=body))
452
-
453
-
454
- def test_set_jev_mode_sends_only_the_mode(monkeypatch):
455
- seen = {}
456
-
457
- def fake(ctx, method, path, json_body=None, **kw):
458
- seen.update(method=method, path=path, body=json_body)
459
- return {"machineId": "m1", "deployment": 9, "jevMode": "enforce", "changed": False, "policies": []}
460
-
461
- monkeypatch.setattr(client, "_request_json", fake)
462
- dep, changed = client.set_jev_mode(object(), "m1", "enforce")
463
- assert seen == {"method": "PUT", "path": "/api/enforcement/deployments/m1/jev-mode", "body": {"jevMode": "enforce"}}
464
- assert (dep.deployment, dep.jev_mode, changed) == (9, "enforce", False)
465
-
466
-
467
- @respx.mock
468
- def test_fleet_deploy_jev_mode_on_a_machine_with_no_deployment_deploys_its_first(logged_in, runner):
469
- respx.get(f"{BASE}/api/enforcement/machines").mock(return_value=httpx.Response(
470
- 200, json=[{"machineId": "m1", "deployment": None, "deployed": False}]))
471
- respx.get(f"{BASE}/api/enforcement/deployments").mock(return_value=httpx.Response(200, json=[]))
472
- respx.get(f"{BASE}/api/enforcement/policies").mock(return_value=httpx.Response(200, json=[SERVER_JEV]))
473
- mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
474
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
475
- 200, json={"machineId": "m1", "deployment": 1, "jevMode": "observe", "policies": []}))
476
- result = runner.invoke(app, ["--json", "fleet", "deploy", "m1", "--jev-mode", "observe", "--yes"])
477
- assert result.exit_code == 0, result.output
478
- assert not mode.called
479
- assert json.loads(deploy.calls.last.request.content) == {"policies": [], "jevMode": "observe"}
480
-
481
-
482
- @respx.mock
483
- def test_fleet_deploy_with_a_set_change_and_a_mode_still_deploys_both_together(logged_in, runner):
484
- _fleet_routes([{"id": "no-force-push", "version": 2, "effect": "enforce"}])
485
- mode = respx.put(f"{BASE}/api/enforcement/deployments/m1/jev-mode")
486
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1").mock(return_value=httpx.Response(
487
- 200, json={"machineId": "m1", "deployment": 6, "jevMode": "enforce", "policies": []}))
488
- result = runner.invoke(app, ["fleet", "deploy", "m1", "--add", "secrets", "--jev-mode", "enforce", "--yes"])
489
- assert result.exit_code == 0, result.output
490
- assert not mode.called
491
- assert json.loads(deploy.calls.last.request.content)["jevMode"] == "enforce"
492
-
493
-
494
- @respx.mock
495
- def test_fleet_jev_mode_sets_named_machines_and_skips_one_already_there(logged_in, runner):
496
- _fleet_many({"m1": None, "m2": "enforce", "m3": "observe"})
497
- r1, r2, r3 = _mode_route("m1", "enforce"), _mode_route("m2", "enforce"), _mode_route("m3", "enforce", 7)
498
- deploy = respx.put(f"{BASE}/api/enforcement/deployments/m1")
499
- result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "m3", "enforce"])
500
- assert result.exit_code == 0, result.output
501
- assert r1.called and r3.called and not r2.called and not deploy.called
502
- assert json.loads(r1.calls.last.request.content) == {"jevMode": "enforce"}
503
- out = json.loads(result.stdout)
504
- assert out["mode"] == "enforce" and out["applied"] is True
505
- rows = {r["machineId"]: r for r in out["machines"]}
506
- assert rows["m1"]["from"] == "local" and rows["m1"]["to"] == "enforce" and rows["m1"]["deployment"] == 6
507
- assert rows["m2"]["changed"] is False
508
- assert rows["m3"]["from"] == "observe" and rows["m3"]["deployment"] == 7
509
-
510
-
511
- @respx.mock
512
- def test_fleet_jev_mode_all_covers_every_machine_with_a_deployment_only(logged_in, runner):
513
- _fleet_many({"m1": "enforce", "m2": "off"}, machines=["m1", "m2", "m-new"])
514
- r1, r2 = _mode_route("m1", "local"), _mode_route("m2", "local")
515
- fresh = respx.put(f"{BASE}/api/enforcement/deployments/m-new/jev-mode")
516
- result = runner.invoke(app, ["fleet", "jev-mode", "--all", "local"])
517
- assert result.exit_code == 0, result.output
518
- assert r1.called and r2.called and not fresh.called
519
- assert json.loads(r1.calls.last.request.content) == {"jevMode": "local"}
520
- assert "enforce → local" in result.stdout and "off → local" in result.stdout
521
-
522
-
523
- @respx.mock
524
- def test_fleet_jev_mode_when_nothing_changes_writes_nothing(logged_in, runner):
525
- _fleet_many({"m1": "observe"})
526
- route = _mode_route("m1", "observe")
527
- result = runner.invoke(app, ["fleet", "jev-mode", "m1", "observe"])
528
- assert result.exit_code == 0, result.output
529
- assert not route.called
530
- assert "already has jev mode observe" in result.output + (result.stderr or "")
531
-
532
-
533
- @pytest.mark.parametrize("argv, needle", [
534
- (["fleet", "jev-mode"], "pass the mode"),
535
- (["fleet", "jev-mode", "m1", "shadow"], "is not a Jev mode"),
536
- (["fleet", "jev-mode", "observe"], "name at least one machine"),
537
- (["fleet", "jev-mode", "--all", "m1", "observe"], "do not name machines"),
538
- ])
539
- def test_fleet_jev_mode_usage_errors_exit_2_before_any_request(logged_in, runner, argv, needle):
540
- with respx.mock(assert_all_called=False) as mock:
541
- result = runner.invoke(app, argv)
542
- assert result.exit_code == 2, result.output
543
- assert needle in result.output + (result.stderr or "")
544
- assert not mock.calls
545
-
546
-
547
- @respx.mock
548
- def test_fleet_jev_mode_refuses_a_named_machine_with_no_deployment_before_writing(logged_in, runner):
549
- _fleet_many({"m1": None}, machines=["m1", "m2"])
550
- r1 = _mode_route("m1", "observe")
551
- result = runner.invoke(app, ["fleet", "jev-mode", "m1", "m2", "observe"])
552
- assert result.exit_code == 1, result.output
553
- text = result.output + (result.stderr or "")
554
- assert "m2 has no deployment" in text and "fp fleet deploy <machine> --jev-mode observe" in text
555
- assert not r1.called
556
-
557
-
558
- @respx.mock
559
- def test_fleet_jev_mode_goes_on_past_a_machine_that_fails_and_names_it(logged_in, runner):
560
- _fleet_many({"m1": None, "m2": None})
561
- _mode_route("m1", "observe", status=404)
562
- r2 = _mode_route("m2", "observe")
563
- result = runner.invoke(app, ["--json", "fleet", "jev-mode", "m1", "m2", "observe"])
564
- assert result.exit_code == 1, result.output
565
- assert r2.called
566
- # The result document, then the error document the exit code comes with.
567
- out, end = json.JSONDecoder().raw_decode(result.stdout)
568
- rows = {r["machineId"]: r for r in out["machines"]}
569
- assert "error" in rows["m1"] and rows["m2"]["deployment"] == 6 and out["applied"] is False
570
- assert "could not be changed on m1" in result.stdout[end:] + (result.stderr or "")
571
-
572
-
573
418
  @respx.mock
574
419
  def test_fleet_rollback_says_which_jev_mode_it_restores(logged_in, runner):
575
420
  _fleet_many({"m1": "off"})
@@ -607,15 +452,6 @@ def test_fleet_history_shows_each_generations_jev_mode_and_a_mode_only_change(lo
607
452
  assert any("local" in ln for ln in lines if "#5" in ln)
608
453
 
609
454
 
610
- def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
611
- for argv in (["fleet", "deploy", "--help"], ["fleet", "jev-mode", "--help"]):
612
- result = runner.invoke(app, argv)
613
- assert result.exit_code == 0, result.output
614
- flat = " ".join(result.stdout.split())
615
- assert "BLOCK" in flat, argv
616
- assert "installed" in flat and "pack" in flat, argv
617
-
618
-
619
455
  # ── CONTRACT C10.6: Jev checks run on FailproofAI Cloud (help text only) ──────
620
456
 
621
457
 
@@ -623,7 +459,6 @@ def test_the_help_says_enforce_blocks_and_that_the_mode_is_machine_wide(runner):
623
459
  "argv",
624
460
  [
625
461
  ["fleet", "deploy", "--help"],
626
- ["fleet", "jev-mode", "--help"],
627
462
  ["policies", "publish", "--help"],
628
463
  ],
629
464
  )
@@ -637,7 +472,7 @@ def test_the_help_says_jev_checks_run_on_cloud_and_nothing_is_installed(runner,
637
472
  assert "AgentEye" not in flat, argv
638
473
 
639
474
 
640
- @pytest.mark.parametrize("argv", [["fleet", "show", "--help"], ["fleet", "jev-mode", "--help"]])
475
+ @pytest.mark.parametrize("argv", [["fleet", "show", "--help"]])
641
476
  def test_the_help_names_the_reasons_a_machine_cannot_ask(runner, argv):
642
477
  flat = " ".join(runner.invoke(app, argv).stdout.split())
643
478
  assert "transcripts_disabled" in flat, argv
@@ -104,6 +104,17 @@ def _profile_name() -> str:
104
104
 
105
105
  def _profile_settings_path(profile: str) -> str | None:
106
106
  """Evidence of the profile running this plugin, not a transcript guess."""
107
+ try:
108
+ # The host's own resolver first: it honours a per-task profile
109
+ # override as well as HERMES_HOME, so each profile of a multi-profile
110
+ # gateway reports its own config (and roster identity), not the
111
+ # process-wide one.
112
+ from hermes_constants import get_hermes_home
113
+
114
+ hosted = Path(get_hermes_home()).expanduser() / "config.yaml"
115
+ return str(hosted.absolute()) if hosted.is_file() else None
116
+ except Exception:
117
+ pass
107
118
  configured = os.environ.get("HERMES_HOME", "").strip()
108
119
  if configured:
109
120
  selected = Path(configured).expanduser() / "config.yaml"
@@ -1,5 +1,5 @@
1
1
  name: failproofai
2
- version: 1.0.12-beta.1
2
+ version: 1.0.12-beta.2
3
3
  description: Real-time FailproofAI policy enforcement for Hermes
4
4
  manifest_version: 2
5
5
  api_version: 1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.12-beta.1",
3
+ "version": "1.0.12-beta.2",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -118,22 +118,23 @@
118
118
  "postcss": "8.5.26",
119
119
  "eslint-plugin-react-hooks": "7.0.1",
120
120
  "vite": "8.1.6",
121
- "undici": "7.29.1",
121
+ "undici": "7.30.1",
122
+ "zod": "4.6.6",
122
123
  "brace-expansion": "5.0.12",
123
124
  "sharp": "0.35.5",
124
125
  "source-map-js": "1.2.2",
125
126
  "browserslist": "4.28.8"
126
127
  },
127
128
  "optionalDependencies": {
128
- "@failproofai/failproofaid-linux-x64": "1.0.12-beta.1",
129
- "@failproofai/failproofaid-linux-arm64": "1.0.12-beta.1",
130
- "@failproofai/failproofaid-darwin-x64": "1.0.12-beta.1",
131
- "@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.1"
129
+ "@failproofai/failproofaid-linux-x64": "1.0.12-beta.2",
130
+ "@failproofai/failproofaid-linux-arm64": "1.0.12-beta.2",
131
+ "@failproofai/failproofaid-darwin-x64": "1.0.12-beta.2",
132
+ "@failproofai/failproofaid-darwin-arm64": "1.0.12-beta.2"
132
133
  },
133
134
  "failproofaidBinaries": {
134
- "linux-x64": "df15e65b60066248038c8ac6d423bffcb0e38637e8a42fa22250afa111fd46ac",
135
- "linux-arm64": "59d7ae758c8981c5e0a89eb64be18ebd278213525dd4447891a51be6bb018ed9",
136
- "darwin-x64": "b0d3d39abaa02ed0a14dddc68277975a7a23e200830b7edbffdedde3af81ce73",
137
- "darwin-arm64": "a6d343b5926e84c0eb437af307f85b69b3128f94c6e34acbf9d0b13a7c7e9d63"
135
+ "linux-x64": "4c09117fdb80d87792d1d2b77d9359cdbd0c522b5a6222ab7b927659130ddf54",
136
+ "linux-arm64": "dd390d76561267fc519ceb96fb90be340044e2f564bef36a44e284f227d93f29",
137
+ "darwin-x64": "62381f7ffe21d02289c36c4b205166b2867dbde986444d1ea185241b94fd04ef",
138
+ "darwin-arm64": "9ad798817d3048a5126e80dc71883538d37c1b6b6443e9c19389dc2583c8f675"
138
139
  }
139
140
  }
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 5
3
3
  requires-python = ">=3.10"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.14' and sys_platform == 'win32'",
@@ -1091,11 +1091,33 @@ provides-extras = ["dev", "langchain", "langgraph", "crewai", "llamaindex", "pyd
1091
1091
 
1092
1092
  [[package]]
1093
1093
  name = "filelock"
1094
- version = "3.32.3"
1094
+ version = "4.1.0"
1095
+ source = { registry = "https://pypi.org/simple" }
1096
+ resolution-markers = [
1097
+ "python_full_version < '3.11' and sys_platform == 'win32'",
1098
+ "python_full_version < '3.11' and sys_platform != 'win32'",
1099
+ ]
1100
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/58/6fd434bec86eff7c38a3168454cb132b762b2bea9b3ac094101a2f7bc32a/filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d", size = 561277, upload-time = "2026-10-09T19:57:20.34Z" }
1101
+ wheels = [
1102
+ { url = "https://files.pythonhosted.org/packages/ee/86/032133892a5de43b5a98200b01aadcad68cc255e274a762f08b8a76d2912/filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1", size = 133003, upload-time = "2026-10-09T19:57:18.716Z" },
1103
+ ]
1104
+
1105
+ [[package]]
1106
+ name = "filelock"
1107
+ version = "4.2.4"
1095
1108
  source = { registry = "https://pypi.org/simple" }
1096
- sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" }
1109
+ resolution-markers = [
1110
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
1111
+ "python_full_version >= '3.14' and sys_platform != 'win32'",
1112
+ "python_full_version == '3.13.*'",
1113
+ "python_full_version == '3.12.*' and sys_platform == 'win32'",
1114
+ "python_full_version == '3.12.*' and sys_platform != 'win32'",
1115
+ "python_full_version == '3.11.*' and sys_platform == 'win32'",
1116
+ "python_full_version == '3.11.*' and sys_platform != 'win32'",
1117
+ ]
1118
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" }
1097
1119
  wheels = [
1098
- { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" },
1120
+ { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" },
1099
1121
  ]
1100
1122
 
1101
1123
  [[package]]
@@ -1679,7 +1701,8 @@ version = "1.28.0"
1679
1701
  source = { registry = "https://pypi.org/simple" }
1680
1702
  dependencies = [
1681
1703
  { name = "click" },
1682
- { name = "filelock" },
1704
+ { name = "filelock", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
1705
+ { name = "filelock", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
1683
1706
  { name = "fsspec" },
1684
1707
  { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
1685
1708
  { name = "httpx" },
@@ -4967,11 +4990,11 @@ wheels = [
4967
4990
 
4968
4991
  [[package]]
4969
4992
  name = "setuptools"
4970
- version = "84.0.0"
4993
+ version = "84.2.1"
4971
4994
  source = { registry = "https://pypi.org/simple" }
4972
- sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
4995
+ sdist = { url = "https://files.pythonhosted.org/packages/70/b3/c19426f783614d625699a4b247faf628ea9ab1db228674491767b417f9e1/setuptools-84.2.1.tar.gz", hash = "sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc", size = 1230289, upload-time = "2026-10-20T19:01:37.742Z" }
4973
4996
  wheels = [
4974
- { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
4997
+ { url = "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", hash = "sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2", size = 876263, upload-time = "2026-10-20T19:01:35.678Z" },
4975
4998
  ]
4976
4999
 
4977
5000
  [[package]]