failproofai 1.0.12-beta.1 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +3 -3
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__167mipqmjprq5._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
  75. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_1d98fyfrcaqhy._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1i8oid1gc7s1f.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  90. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  91. package/.next/standalone/.next/server/pages/404.html +1 -1
  92. package/.next/standalone/.next/server/pages/500.html +1 -1
  93. package/.next/standalone/.next/server/preview-props.json +3 -3
  94. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  96. package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 0el9n3h9_zp5m.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
  98. package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 0umzct31duk5g.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 0wobqgosirs8p.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 1kduj74-715jh.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/2701amhrbsbot.js +1 -0
  103. package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 27cavoc8paelg.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 2ve23a7keupjw.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/3bv6xaoitjpnh.js +6 -0
  106. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  107. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  108. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +28 -18
  109. package/.next/standalone/fp-cloud-cli/README.md +127 -9
  110. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
  111. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +199 -34
  112. package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
  113. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
  114. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
  115. package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
  116. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
  118. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
  119. package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
  120. package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
  121. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
  122. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
  125. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
  126. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
  127. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
  128. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
  129. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
  131. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
  132. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
  134. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
  135. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
  136. package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -152
  138. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
  140. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
  141. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +173 -22
  142. package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
  144. package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
  145. package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
  146. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
  147. package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
  148. package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
  149. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
  150. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
  151. package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
  152. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
  153. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
  154. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
  155. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
  156. package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
  157. package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
  158. package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
  159. package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
  160. package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
  161. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
  162. package/.next/standalone/hermes-plugin/__init__.py +11 -0
  163. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  164. package/.next/standalone/package.json +11 -10
  165. package/.next/standalone/sdk/python/uv.lock +31 -8
  166. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
  167. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
  168. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
  169. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
  171. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
  172. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
  173. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
  174. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
  175. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
  176. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
  177. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
  178. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
  179. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
  180. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
  181. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
  182. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
  183. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
  184. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
  185. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
  186. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
  187. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
  188. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
  189. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
  190. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
  191. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
  192. package/.next/standalone/server.js +1 -1
  193. package/dist/cli.mjs +1760 -638
  194. package/dist/worker.mjs +426 -76
  195. package/hermes-plugin/__init__.py +11 -0
  196. package/hermes-plugin/plugin.yaml +1 -1
  197. package/package.json +11 -10
  198. package/src/audit/open-browser.ts +24 -7
  199. package/src/hooks/agent-roster.ts +9 -1
  200. package/src/hooks/agent-skill.ts +206 -0
  201. package/src/hooks/cloud-connection.ts +27 -51
  202. package/src/hooks/cloud-introspect.ts +5 -2
  203. package/src/hooks/cloud-login.ts +248 -0
  204. package/src/hooks/cloud-managed-policies.ts +54 -4
  205. package/src/hooks/codex-system-hooks.ts +10 -4
  206. package/src/hooks/config-status.ts +9 -3
  207. package/src/hooks/configure-wizard.ts +356 -32
  208. package/src/hooks/delivery-health.ts +47 -2
  209. package/src/hooks/effective-reviewers.ts +14 -0
  210. package/src/hooks/fp-config.ts +19 -0
  211. package/src/hooks/fp-home.ts +1 -1
  212. package/src/hooks/handler.ts +43 -5
  213. package/src/hooks/hook-activity-store.ts +7 -1
  214. package/src/hooks/integrations.ts +18 -3
  215. package/src/hooks/jev-activity.ts +117 -1
  216. package/src/hooks/jev-cli.ts +7 -9
  217. package/src/hooks/manager.ts +80 -46
  218. package/src/hooks/policy-evaluator.ts +17 -0
  219. package/src/hooks/policy-reviewability.ts +4 -2
  220. package/src/hooks/semantic/cloud-jev.ts +219 -28
  221. package/src/hooks/semantic/combine.ts +26 -1
  222. package/src/hooks/semantic/intent.ts +127 -24
  223. package/src/hooks/semantic/jev-client.ts +1 -1
  224. package/src/hooks/semantic/jev-config.ts +6 -27
  225. package/src/hooks/semantic/jev-review.ts +111 -4
  226. package/src/hooks/tui.ts +280 -0
  227. package/src/hooks/uninstall-cli.ts +12 -1
  228. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
  229. package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
  230. package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +0 -1
  231. package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
  232. package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
  233. package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
  234. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
  235. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
  236. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
@@ -940,6 +940,25 @@ export function readCredentials(): FpCredentials {
940
940
  }
941
941
  }
942
942
 
943
+ /**
944
+ * Whether FailproofAI Cloud sets this machine's Jev mode: it is connected for
945
+ * policies (a machine id in `credentials.json`). Cloud then runs Jev in enforce
946
+ * while a Jev policy is deployed here and switches it off when none is; local
947
+ * mode settings do not apply. Never throws.
948
+ */
949
+ export function cloudManagesJevMode(): boolean {
950
+ try {
951
+ const cloud = readCredentials().cloud;
952
+ return Boolean(cloud?.url && cloud.machineId);
953
+ } catch {
954
+ return false;
955
+ }
956
+ }
957
+
958
+ /** The one line a local mode switch answers with on such a machine. */
959
+ export const CLOUD_MANAGED_JEV_MODE =
960
+ "Jev mode is managed by FailproofAI Cloud on this machine: enforce while a Jev policy is deployed here, off when none is.";
961
+
943
962
  /**
944
963
  * Project a parsed credentials object onto {@link FpCredentials}.
945
964
  *
@@ -410,7 +410,7 @@ export const collectorHealthFile = (home?: string) => resolve(stateDir(home), "c
410
410
  /** Per-session enforcement pauses, keyed by a hash of the session id. */
411
411
  export const sessionPauseDir = () => resolve(stateDir(), "sessions");
412
412
  /**
413
- * The semantic (Jev) evaluator's local state: `sessions/<id>.json` holds the
413
+ * The semantic (Jev) evaluator's local state: `sessions/<profile>/<id>.json` holds the
414
414
  * human prompts recorded at `UserPromptSubmit`, and `verdicts.jsonl` one row
415
415
  * per semantic evaluation. Written only when a Jev config (BYOK) is present.
416
416
  */
@@ -38,7 +38,7 @@ import type { JevActivityFields } from "./semantic/combine";
38
38
  import type { JevConfig } from "./semantic/jev-config";
39
39
  import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
40
40
  import { loadAllCustomHooks, type LoadAllResult } from "./custom-hooks-loader";
41
- import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled } from "./effective-reviewers";
41
+ import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled, packJevChecksInstalled } from "./effective-reviewers";
42
42
  import {
43
43
  authorityDeclarationFor,
44
44
  refusedAuthorityWarning,
@@ -58,6 +58,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
58
58
  import { readStdinPayload } from "./read-stdin";
59
59
  import {
60
60
  cloudJevAsks,
61
+ cloudJevAssignmentsFor,
61
62
  withCloudAuthority,
62
63
  readActiveCloudManagedPolicies,
63
64
  readCloudJevMode,
@@ -305,6 +306,8 @@ interface CloudJevRoute {
305
306
  deployment: number | null;
306
307
  mode: "observe" | "enforce";
307
308
  agent: AgentIdentity | null;
309
+ /** Cloud policies whose Jev assignment here is `observe`: their answers are recorded, never applied. */
310
+ observedPolicies: string[];
308
311
  }
309
312
 
310
313
  /**
@@ -346,7 +349,13 @@ async function readJevConfig(agent?: AgentIdentity | null): Promise<{
346
349
  return {
347
350
  config: resolved.config,
348
351
  defaultMode: DEFAULT_JEV_MODE,
349
- cloud: { machineId: resolved.machineId, deployment: cloud.deployment, mode: cloud.jevMode, agent: agent ?? null },
352
+ cloud: {
353
+ machineId: resolved.machineId,
354
+ deployment: cloud.deployment,
355
+ mode: cloud.jevMode,
356
+ agent: agent ?? null,
357
+ observedPolicies: (cloudJevAssignmentsFor(agent ?? null) ?? []).filter((a) => a.effect === "observe").map((a) => a.id),
358
+ },
350
359
  };
351
360
  }
352
361
  if (!existsSync(jevConfigFile())) return null;
@@ -475,6 +484,11 @@ async function startTwoTier(
475
484
  // must not switch off what the org assigned centrally).
476
485
  if (activePause && !cloudAsks) return null;
477
486
  if (!cloudAsks && !jevChecksInstalled()) return null;
487
+ // No Cloud Jev check is assigned to this agent (the deployment says so) and
488
+ // no installed pack's check takes part: nothing to ask, so no Cloud call
489
+ // and no Jev fields on the row. An older deployment without the list
490
+ // (`null`) still sends every call, as before.
491
+ if (cloudAsks && cloudJevAssignmentsFor(agent)?.length === 0 && (activePause || !packJevChecksInstalled())) return null;
478
492
  const loaded = await readJevConfig(agent);
479
493
  if (!loaded) return null;
480
494
  // Paused, and the config is not Cloud's after all (read in between): the
@@ -491,6 +505,8 @@ async function startTwoTier(
491
505
  permissionMode: session.permissionMode,
492
506
  sessionId: session.sessionId,
493
507
  cli,
508
+ // The profile the intent store is read under: the one capture used.
509
+ agent,
494
510
  ...(loaded.cloud ? { cloud: loaded.cloud } : {}),
495
511
  // Cloud's checks are still asked; the installed packs' are not.
496
512
  ...(activePause ? { localPaused: true } : {}),
@@ -557,6 +573,15 @@ function jevTelemetry(activity: JevActivityFields | undefined): Record<string, u
557
573
  * marks that tell a human's prompt from a subagent's or an extension's are
558
574
  * elsewhere in it. `prompt` is §7's original field, still read by the
559
575
  * contract stub; T4's implementation reads `payload` and ignores it.
576
+ *
577
+ * The prompt is stored under the agent profile (`agent`, the runtime identity
578
+ * Cloud's targeting matches on) and read back by `startTwoTier` under the same
579
+ * one, so a prompt typed into one profile is never another's stated intent.
580
+ * Under a Cloud mode, a profile the deployment assigns no Cloud Jev check to,
581
+ * with no installed pack's check to review it either, has nothing that will
582
+ * ever read the prompt (`startTwoTier` returns before Jev for it), so nothing
583
+ * is kept: FailproofAI does not hold prompt text from a profile the
584
+ * deployment did not target.
560
585
  */
561
586
  async function captureJevIntent(
562
587
  canonicalEventType: string,
@@ -565,13 +590,20 @@ async function captureJevIntent(
565
590
  session: SessionMetadata,
566
591
  cli: IntegrationType,
567
592
  opts: EvaluateHookEventOptions | undefined,
593
+ agent: AgentIdentity | null,
568
594
  ): Promise<void> {
569
595
  if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts)) return;
570
596
  try {
571
597
  // Same gate as `startTwoTier`: a Cloud mode that asks needs what the human
572
598
  // typed whatever the installed packs declare.
573
- if (!cloudJevAsks(readCloudJevMode()) && !jevChecksInstalled()) return;
574
- if (!(await readJevConfig())) return;
599
+ const cloudAsks = cloudJevAsks(readCloudJevMode());
600
+ if (!cloudAsks && !jevChecksInstalled()) return;
601
+ // ...but not for a profile no Cloud Jev check targets and no installed
602
+ // pack's check reviews: `startTwoTier`'s own skip, without the pause
603
+ // (a pause can end while the session goes on). An older deployment with
604
+ // no assignment list (`null`) captures as before.
605
+ if (cloudAsks && cloudJevAssignmentsFor(agent)?.length === 0 && !packJevChecksInstalled()) return;
606
+ if (!(await readJevConfig(agent))) return;
575
607
  if (decision === "deny") {
576
608
  const { ENFORCEMENT_CAPABILITY } = await import("./enforcement-capability");
577
609
  if (ENFORCEMENT_CAPABILITY[cli]?.UserPromptSubmit === "block") return;
@@ -585,6 +617,7 @@ async function captureJevIntent(
585
617
  cli,
586
618
  payload: parsed,
587
619
  prompt: parsed.prompt,
620
+ profile: { cli, agent },
588
621
  };
589
622
  captureIntent(event);
590
623
  } catch (err) {
@@ -1158,10 +1191,15 @@ export async function evaluateHookEvent(
1158
1191
  reason: observeVerdict.reason,
1159
1192
  });
1160
1193
  }
1194
+ // An `observe` Cloud Jev assignment's checks: filed under their Cloud
1195
+ // policy, like an observe Cloud JS policy's verdict.
1196
+ for (const o of result.twoTier?.observedCloud ?? []) {
1197
+ observedResults.push({ policyId: o.cloudPolicyId, version: o.cloudVersion, decision: o.decision, reason: o.reason });
1198
+ }
1161
1199
  const durationMs = Math.round(performance.now() - startTime);
1162
1200
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
1163
1201
 
1164
- await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts);
1202
+ await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts, runtimeAgent);
1165
1203
 
1166
1204
  // Which policies actually ran for this event, regardless of how they
1167
1205
  // decided. `result.policyName` names only the decider — null on a plain
@@ -24,7 +24,7 @@ import {
24
24
  import { join } from "node:path";
25
25
  import type { IntegrationType } from "./types";
26
26
  import { hookActivityDir } from "./fp-home";
27
- import { sanitizeJevActivity } from "./jev-activity";
27
+ import { sanitizeJevActivity, type JevCheckRecord } from "./jev-activity";
28
28
 
29
29
  export const PAGE_SIZE = 25;
30
30
 
@@ -107,6 +107,12 @@ export interface HookActivityEntry {
107
107
  jevModel?: string;
108
108
  /** `observe` logs Jev but enforces the regex result; `enforce` applies the combine rules. */
109
109
  jevMode?: "observe" | "enforce";
110
+ /**
111
+ * Every Jev check asked on an answered call and what it concluded (verdict,
112
+ * why, evidence, exempt and probe answers) — names, codes and numbers only.
113
+ * `[]`: the request carried no check, so Jev decided nothing (`no-request`).
114
+ */
115
+ jevChecks?: JevCheckRecord[];
110
116
  durationMs: number;
111
117
  sessionId?: string;
112
118
  transcriptPath?: string;
@@ -26,7 +26,7 @@ import { fileURLToPath } from "node:url";
26
26
  import { homedir } from "node:os";
27
27
  import { parseDocument, type Document } from "yaml";
28
28
  import { writeConfigFileAtomic } from "./safe-config-write";
29
- import { codexSystemHooksPresent, type CodexSystemHookEntry } from "./codex-system-hooks";
29
+ import { codexHome, codexSystemHooksPresent, type CodexSystemHookEntry } from "./codex-system-hooks";
30
30
  import { listHermesProfiles, hermesRoot } from "../../lib/hermes-profiles";
31
31
  import { listOpenClawProfiles } from "../../lib/openclaw-profiles";
32
32
  import {
@@ -253,6 +253,21 @@ export function settingsPathsFor(
253
253
  return paths && paths.length > 0 ? paths : [integration.getSettingsPath(scope, cwd)];
254
254
  }
255
255
 
256
+ // ── Agent profile homes ─────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * The user-scope config folder of the Claude Code profile this command runs
260
+ * for: `CLAUDE_CONFIG_DIR` when set (`~/.claude-work`), else `~/.claude`. The
261
+ * hook resolves the same variable (`runtimeAgentSettingsPath`), so install,
262
+ * uninstall and the roster all name one file per profile.
263
+ */
264
+ export function claudeConfigDir(): string {
265
+ const dir = process.env.CLAUDE_CONFIG_DIR?.trim();
266
+ return dir ? resolve(dir) : resolve(homedir(), ".claude");
267
+ }
268
+
269
+ export { codexHome };
270
+
256
271
  // ── Claude Code integration ─────────────────────────────────────────────────
257
272
 
258
273
  export const claudeCode: Integration = {
@@ -265,7 +280,7 @@ export const claudeCode: Integration = {
265
280
  const base = cwd ? resolve(cwd) : process.cwd();
266
281
  switch (scope) {
267
282
  case "user":
268
- return resolve(homedir(), ".claude", "settings.json");
283
+ return resolve(claudeConfigDir(), "settings.json");
269
284
  case "project":
270
285
  return resolve(base, ".claude", "settings.json");
271
286
  case "local":
@@ -439,7 +454,7 @@ export const codex: Integration = {
439
454
  const base = cwd ? resolve(cwd) : process.cwd();
440
455
  switch (scope) {
441
456
  case "user":
442
- return resolve(homedir(), ".codex", "hooks.json");
457
+ return resolve(codexHome(), "hooks.json");
443
458
  case "project":
444
459
  return resolve(base, ".codex", "hooks.json");
445
460
  case "local":
@@ -41,7 +41,7 @@ import type { HookActivityEntry } from "./hook-activity-store";
41
41
 
42
42
  export type JevActivityFields = Pick<
43
43
  HookActivityEntry,
44
- "evaluator" | "jevDecision" | "jevCleared" | "jevFallbackReason" | "jevLatencyMs" | "jevModel" | "jevMode"
44
+ "evaluator" | "jevDecision" | "jevCleared" | "jevFallbackReason" | "jevLatencyMs" | "jevModel" | "jevMode" | "jevChecks"
45
45
  >;
46
46
 
47
47
  /** Every Jev key on an activity row, in the order they are documented. */
@@ -53,6 +53,7 @@ export const JEV_ACTIVITY_KEYS = [
53
53
  "jevLatencyMs",
54
54
  "jevModel",
55
55
  "jevMode",
56
+ "jevChecks",
56
57
  ] as const satisfies ReadonlyArray<keyof JevActivityFields>;
57
58
 
58
59
  const EVALUATORS = new Set(["jev", "jev-fallback"]);
@@ -219,6 +220,113 @@ export function normalizeJevFallbackReason(raw: unknown): string | undefined {
219
220
  return JEV_REASON_CODES.has(code) ? code : JEV_REASON_OTHER;
220
221
  }
221
222
 
223
+ // ── Per-check answers ────────────────────────────────────────────────────────
224
+
225
+ /** What one asked Jev check concluded. */
226
+ export const JEV_CHECK_VERDICTS = ["none", "deny", "instruct", "overridden"] as const;
227
+
228
+ /**
229
+ * Why a check ended where it did, as a closed code (read off its outcome,
230
+ * never text from the call):
231
+ * - `not-detected`: its probes did not hold (evidence under the fire threshold);
232
+ * - `exempt`: it fired, and its `exempt` question held;
233
+ * - `fired`: it fired and nothing softened it;
234
+ * - `injection`: it fired and the request also argued for its own approval;
235
+ * - `user-asked` (v0), `op-requested`, `task-step` (v1): the human's consent cleared it;
236
+ * - `downgraded`: a deny the human's task softened to a warning;
237
+ * - `scan-incomplete`: a clear withheld because the command scan may have missed a target.
238
+ */
239
+ export const JEV_CHECK_WHY = [
240
+ "not-detected",
241
+ "exempt",
242
+ "fired",
243
+ "injection",
244
+ "user-asked",
245
+ "op-requested",
246
+ "task-step",
247
+ "downgraded",
248
+ "scan-incomplete",
249
+ ] as const;
250
+
251
+ /**
252
+ * One Jev check that was asked on this call, and its answer: names, closed
253
+ * codes and probabilities only — never question text, the command or the
254
+ * human's words. Jev answers yes/no probabilities, not reasons, so `why` is
255
+ * the decider's own reading of them.
256
+ */
257
+ export interface JevCheckRecord {
258
+ /** The check's name (`semantic/<check>` without the prefix). */
259
+ check: string;
260
+ /** Who declared it: `cloud/<policyId>@<version>` or `pack/<packId>@<version>`. Absent for a caller-supplied set. */
261
+ policy?: string;
262
+ verdict: (typeof JEV_CHECK_VERDICTS)[number];
263
+ why: (typeof JEV_CHECK_WHY)[number];
264
+ /** Evidence: the minimum over its probes, 2 decimals. */
265
+ p: number;
266
+ /** Its `exempt` answer, when it declares one. */
267
+ exempt?: number;
268
+ /** Each probe's answer by probe id, when the answers came back. */
269
+ probes?: Record<string, number>;
270
+ /** A check of a watched (observe) Cloud policy: recorded, never enforced. */
271
+ watched?: true;
272
+ }
273
+
274
+ export const JEV_CHECKS_MAX = 32;
275
+ export const JEV_CHECK_PROBES_MAX = 8;
276
+ /** A check name: a slug, `.` allowed (a Cloud check name may carry one). */
277
+ export const JEV_CHECK_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
278
+ /** A probe id, as declarations allow it. */
279
+ export const JEV_PROBE_ID_RE = /^[a-z][a-z0-9_]{0,31}$/;
280
+ const CHECK_VERDICT_SET: ReadonlySet<string> = new Set(JEV_CHECK_VERDICTS);
281
+ const CHECK_WHY_SET: ReadonlySet<string> = new Set(JEV_CHECK_WHY);
282
+
283
+ const isP = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1;
284
+ /** A probability at 2 decimals — what the reason strings print. */
285
+ export const round2 = (v: number): number => Math.round(v * 100) / 100;
286
+
287
+ /** One record, rebuilt field by field, or null when it is not one. */
288
+ function sanitizeCheck(raw: unknown): JevCheckRecord | null {
289
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
290
+ const r = raw as Record<string, unknown>;
291
+ if (typeof r.check !== "string" || !JEV_CHECK_NAME_RE.test(r.check)) return null;
292
+ if (typeof r.verdict !== "string" || !CHECK_VERDICT_SET.has(r.verdict)) return null;
293
+ if (typeof r.why !== "string" || !CHECK_WHY_SET.has(r.why)) return null;
294
+ if (!isP(r.p)) return null;
295
+ const out: JevCheckRecord = {
296
+ check: r.check,
297
+ verdict: r.verdict as JevCheckRecord["verdict"],
298
+ why: r.why as JevCheckRecord["why"],
299
+ p: round2(r.p),
300
+ };
301
+ if (typeof r.policy === "string" && isJevPolicyName(r.policy) && !/\s/.test(r.policy) && /^(?:cloud|pack)\//.test(r.policy)) {
302
+ out.policy = r.policy;
303
+ }
304
+ if (isP(r.exempt)) out.exempt = round2(r.exempt);
305
+ if (r.watched === true) out.watched = true;
306
+ if (r.probes && typeof r.probes === "object" && !Array.isArray(r.probes)) {
307
+ const probes: Record<string, number> = {};
308
+ for (const [id, v] of Object.entries(r.probes as Record<string, unknown>)) {
309
+ if (Object.keys(probes).length >= JEV_CHECK_PROBES_MAX) break;
310
+ if (JEV_PROBE_ID_RE.test(id) && isP(v)) probes[id] = round2(v);
311
+ }
312
+ if (Object.keys(probes).length > 0) out.probes = probes;
313
+ }
314
+ return out;
315
+ }
316
+
317
+ /** A row's `jevChecks`, valid records only (at most {@link JEV_CHECKS_MAX}), or undefined when it is not a list. */
318
+ export function sanitizeJevChecks(raw: unknown): JevCheckRecord[] | undefined {
319
+ if (!Array.isArray(raw)) return undefined;
320
+ const out: JevCheckRecord[] = [];
321
+ for (const item of raw) {
322
+ if (out.length >= JEV_CHECKS_MAX) break;
323
+ const rec = sanitizeCheck(item);
324
+ if (rec) out.push(rec);
325
+ }
326
+ // Every entry invalid: not "nothing asked" — the list is unreadable, so it goes.
327
+ return raw.length > 0 && out.length === 0 ? undefined : out;
328
+ }
329
+
222
330
  /**
223
331
  * The Jev fields of `entry`, each one valid or absent. Non-Jev keys pass
224
332
  * through untouched, and an entry with no Jev keys comes back as the same
@@ -265,6 +373,11 @@ export function sanitizeJevActivity<T extends JevActivityFields>(entry: T): T {
265
373
  if (typeof out.jevModel === "string" && JEV_MODEL_RE.test(out.jevModel.trim())) out.jevModel = out.jevModel.trim();
266
374
  else drop("jevModel");
267
375
 
376
+ // Only on an answered call: a fallback or an unconsulted call asked nothing.
377
+ const checks = out.evaluator === "jev" ? sanitizeJevChecks(out.jevChecks) : undefined;
378
+ if (checks === undefined) drop("jevChecks");
379
+ else out.jevChecks = checks;
380
+
268
381
  return out;
269
382
  }
270
383
 
@@ -307,6 +420,9 @@ export function jevOutcome(raw: JevActivityFields): JevOutcome | null {
307
420
  function outcomeOf(e: JevActivityFields): JevOutcome | null {
308
421
  if (e.evaluator === "jev-fallback") return "fallback";
309
422
  if (e.evaluator !== "jev") return null;
423
+ // A request that carried no check (only the global intent questions, or a
424
+ // FailproofAI Cloud short-circuit) decided nothing: Jev had nothing to ask.
425
+ if (Array.isArray(e.jevChecks) && e.jevChecks.length === 0) return "no-request";
310
426
  if (e.jevCleared !== undefined || e.jevLatencyMs !== undefined || e.jevModel !== undefined) return "answered";
311
427
  if (e.jevDecision === "deny" || e.jevDecision === "instruct") return "answered";
312
428
  if (e.jevDecision === "allow") return "no-request";
@@ -147,7 +147,7 @@ import {
147
147
  } from "./policy-reviewability";
148
148
  import { jevStats, type JevStats } from "./semantic/jev-stats";
149
149
  import { listActiveCloudManagedPolicies, readCloudJevState, type CloudJevState } from "./cloud-managed-policies";
150
- import { readCredentials, readJevCloudCredential, type JevCloudCredential } from "./fp-config";
150
+ import { CLOUD_MANAGED_JEV_MODE, cloudManagesJevMode, readCredentials, readJevCloudCredential, type JevCloudCredential } from "./fp-config";
151
151
  import type { JevRequest } from "./semantic/types";
152
152
  import { TOKEN_ON_ARGV, optsFor, paint, screenKit, stack, wrap, type RenderOpts } from "./tui";
153
153
 
@@ -367,6 +367,9 @@ function cloudConnection(): { cloudConnected: boolean; keyCarriesJev: boolean }
367
367
  }
368
368
  }
369
369
 
370
+ /** What `jev setup --mode` says on a machine whose Jev mode FailproofAI Cloud sets. */
371
+ const CLOUD_MANAGED_MODE_LINES = [CLOUD_MANAGED_JEV_MODE, "Deploy or remove Jev policies in FailproofAI Cloud to change it. Nothing was written."];
372
+
370
373
  /** The one line `status` titles a connected machine whose key has no Jev with. */
371
374
  const KEY_LACKS_JEV_TITLE = "off — no Jev key is stored for this machine's FailproofAI Cloud connection";
372
375
 
@@ -689,6 +692,9 @@ async function setupRun(argv: string[], deps: JevCliDeps, opts: RenderOpts): Pro
689
692
  if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
690
693
  if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
691
694
  const { values, bools } = parsed;
695
+ // A machine connected for policies has its Jev mode set by FailproofAI Cloud
696
+ // alone: enforce while a Jev policy is deployed to it, off when none is.
697
+ if (values.has("--mode") && cloudManagesJevMode()) return fail(CLOUD_MANAGED_MODE_LINES);
692
698
  if (bools.has("--key-stdin") && bools.has("--key-from-env")) return fail(["Use one of --key-stdin and --key-from-env, not both."]);
693
699
  // One source for the key, so no run has to decide which of two the person
694
700
  // meant. The message names the recommended one first.
@@ -1479,14 +1485,6 @@ function cloudProblemAdvice(problem: string): string[] {
1479
1485
  "To use FailproofAI Cloud's Jev checks, reconnect without --no-transcripts (failproofai config --token <key>).",
1480
1486
  ];
1481
1487
  }
1482
- // Over plain http the credential is fine; enforce is what http refuses.
1483
- // Asking for another key there sent people after the wrong fix.
1484
- if (/plain http/.test(problem)) {
1485
- return [
1486
- "Jev does not run here: FailproofAI Cloud set enforce, and this machine is connected over plain http, where Jev runs only in observe mode.",
1487
- "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or set the machine's Jev mode to observe from FailproofAI Cloud.",
1488
- ];
1489
- }
1490
1488
  return [
1491
1489
  `Jev cannot run here: ${problem}.`,
1492
1490
  "Connect with a key that carries jev:evaluate (failproofai config --token <key>). A local jev.json is not used while FailproofAI Cloud sets the mode.",
@@ -32,8 +32,8 @@ import { getInstanceId, hashToId } from "../../lib/telemetry-id";
32
32
  import { CliError } from "../cli-error";
33
33
  import { hookLogWarn } from "./hook-logger";
34
34
  import { customPoliciesDir } from "./fp-home";
35
- import { listActiveCloudManagedPolicies, readCloudJevState, type CloudJevMode } from "./cloud-managed-policies";
36
- import { readCloudLoadChecked, readCloudPolicyErrors } from "./cloud-policy-errors";
35
+ import { listActiveCloudManagedPolicies, readCloudJevAssignments, readCloudJevState, type CloudJevMode } from "./cloud-managed-policies";
36
+ import { readCloudPolicyErrors } from "./cloud-policy-errors";
37
37
  import { readRosterLabels } from "./agent-roster";
38
38
  import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
39
39
  import type { ResolvedPack } from "./pack-manifest";
@@ -334,6 +334,35 @@ export function integrationsInstalledAt(scope: HookScope, cwd?: string): Integra
334
334
  */
335
335
  export type NotEnforcingReason = "no-hooks" | "no-policies" | "observe-only";
336
336
 
337
+ /** Machine-wide assignments, like the listing: a `both` policy counts once. */
338
+ function cloudPolicyStatus(): { count: number; observing: boolean } {
339
+ let count = 0;
340
+ let observing = false;
341
+ const jsIds = new Set<string>();
342
+ try {
343
+ for (const artifact of listActiveCloudManagedPolicies()) {
344
+ jsIds.add(artifact.id);
345
+ if (artifact.effect === "observe") observing = true;
346
+ else count++;
347
+ }
348
+ } catch {
349
+ // The listing reports an unreadable JS deployment; Jev is read independently.
350
+ }
351
+ try {
352
+ const { jevMode } = readCloudJevState();
353
+ if (jevMode === "enforce" || jevMode === "observe") {
354
+ for (const assignment of readCloudJevAssignments() ?? []) {
355
+ if (jsIds.has(assignment.id)) continue;
356
+ if (jevMode === "observe" || assignment.effect === "observe") observing = true;
357
+ else count++;
358
+ }
359
+ }
360
+ } catch {
361
+ // No readable Jev state: neither enforcement nor observation is known.
362
+ }
363
+ return { count, observing };
364
+ }
365
+
337
366
  export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
338
367
  const wired = deduplicateScopes(HOOK_SCOPES, cwd).some((scope) => hooksInstalledInSettings(scope, cwd));
339
368
  if (!wired) return "no-hooks";
@@ -363,15 +392,11 @@ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
363
392
  // An unreadable manifest is reported by the listing itself.
364
393
  }
365
394
 
366
- // Cloud-managed policies, pushed from a deployment.
367
- try {
368
- for (const artifact of listActiveCloudManagedPolicies()) {
369
- if (artifact.effect === "observe") observing = true;
370
- else return null;
371
- }
372
- } catch {
373
- // No deployment, or an unreadable one: nothing enforces from there.
374
- }
395
+ // Cloud JS enforces independently of Jev mode. Jev-only assignments enforce
396
+ // only in Cloud's enforce mode, and watch only while Jev is not off.
397
+ const cloud = cloudPolicyStatus();
398
+ if (cloud.count > 0) return null;
399
+ observing ||= cloud.observing;
375
400
 
376
401
  // A configured custom policy file, or convention files on disk. Loading them
377
402
  // to ask what they hold would import user code on every launch, so their
@@ -392,10 +417,11 @@ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
392
417
  * How many policies enforce on this machine, for the line that says so.
393
418
  *
394
419
  * Counted the same way `notEnforcingReason` decides, so the two never disagree:
395
- * pack policies that are taken and not switched off, cloud-managed policies, and
396
- * the legacy builtins of a machine still on the migration shim — observe-only
397
- * ones excluded. Custom policy files cannot be counted without importing them,
398
- * so their presence is reported on its own.
420
+ * pack policies that are taken and not switched off, Cloud JS policies and
421
+ * Jev-only assignments in Cloud's enforce mode, and the legacy builtins of a
422
+ * machine still on the migration shim — observe-only ones excluded, `both`
423
+ * assignments counted once. Custom policy files cannot be counted without
424
+ * importing them, so their presence is reported on its own.
399
425
  */
400
426
  export function enforcingPolicyCount(cwd?: string): { count: number; custom: boolean } {
401
427
  const config = readMergedHooksConfig(cwd);
@@ -410,13 +436,7 @@ export function enforcingPolicyCount(cwd?: string): { count: number; custom: boo
410
436
  } catch {
411
437
  // An unreadable manifest counts nothing, as it enforces nothing.
412
438
  }
413
- try {
414
- for (const artifact of listActiveCloudManagedPolicies()) {
415
- if (artifact.effect !== "observe") count++;
416
- }
417
- } catch {
418
- // No deployment.
419
- }
439
+ count += cloudPolicyStatus().count;
420
440
  if (!hasInstalledRegexPacks()) count += config.enabledPolicies.length;
421
441
  const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
422
442
  const custom =
@@ -1254,6 +1274,8 @@ export interface PoliciesScreen {
1254
1274
  jevMode?: CloudJevMode | null;
1255
1275
  /** Problems reading the deployment, each one line. */
1256
1276
  problems?: string[];
1277
+ /** The manifest names its Jev assignments (`jevPolicies`), so Jev-only policies are among `rows`. */
1278
+ jevListed?: boolean;
1257
1279
  } | null;
1258
1280
  /**
1259
1281
  * Builtins this build enforces itself because no pack is installed: the
@@ -1392,9 +1414,12 @@ export function renderPoliciesScreen(
1392
1414
  out.push(`${INDENT}Jev mode: off, set by FailproofAI Cloud (overrides jev.json).`);
1393
1415
  } else if (jevMode) {
1394
1416
  out.push(`${INDENT}Jev mode: ${jevMode}, set by FailproofAI Cloud. Jev checks run on FailproofAI Cloud; nothing of them is installed here.`);
1395
- // Jev-only Cloud policies never reach the machine (N6): say so, or an
1396
- // empty section reads as "Cloud deployed nothing".
1397
- out.push(`${INDENT}Jev-only Cloud policies are not listed here; see them with ${kit.cmd("fp fleet show")} or on the dashboard.`);
1417
+ // A manifest from a server that predates `jevPolicies` does not name its
1418
+ // Jev-only policies: say so, or an empty section reads as "Cloud
1419
+ // deployed nothing".
1420
+ if (!screen.cloud.jevListed) {
1421
+ out.push(`${INDENT}Jev-only Cloud policies are not listed here; see them with ${kit.cmd("fp fleet show")} or on the dashboard.`);
1422
+ }
1398
1423
  }
1399
1424
  for (const problem of problems ?? []) out.push(kit.caution(problem));
1400
1425
  }
@@ -1616,8 +1641,9 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
1616
1641
  // Each policy's KIND is shown — `regex` (JS) or `both` (JS whose verdict
1617
1642
  // its own Jev checks may clear) — with the Jev check names a `both` policy
1618
1643
  // is reviewed by. Those checks, and every `jev` policy, run on FailproofAI
1619
- // Cloud (CONTRACT C10): nothing of them is on this machine, so no Jev-only
1620
- // policy is listed here, and the machine cannot count them.
1644
+ // Cloud (CONTRACT C10): no question is on this machine, but the manifest's
1645
+ // `jevPolicies` names each Jev assignment, so a Jev-only policy gets a row
1646
+ // (tagged `jev`) like the JS ones.
1621
1647
  let cloud: PoliciesScreen["cloud"] = null;
1622
1648
  try {
1623
1649
  // Every assignment, agent-scoped ones included (B4): this lists the
@@ -1650,25 +1676,41 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
1650
1676
  } catch {
1651
1677
  // No report: every row reads as the deployment says.
1652
1678
  }
1653
- // Only the hook path imports Cloud JavaScript (N4): a policy no hook has
1654
- // load-checked under this deployment is not yet known to load, so it is
1655
- // not drawn as ● on.
1656
- let loadChecked: Set<string> | null = null;
1679
+ // A policy deployed here is drawn ● on. Only the hook path imports Cloud
1680
+ // JavaScript, so one that fails to load is known only after a tool call,
1681
+ // and then shows as not loaded (errors.json, above) — never as a transient
1682
+ // "not loaded yet" state of its own.
1683
+ // Jev-only assignments (`jevPolicies`): ids, versions, effects and scopes,
1684
+ // never a question. A `both` policy is in both lists and is drawn once,
1685
+ // from its JS row. Null from a server that predates the list.
1686
+ let jevAssignments: ReturnType<typeof readCloudJevAssignments> = null;
1657
1687
  try {
1658
- const record = readCloudLoadChecked();
1659
- loadChecked = new Set(record && record.deployment === active[0]?.deployment ? record.checked : []);
1688
+ jevAssignments = readCloudJevAssignments();
1660
1689
  } catch {
1661
- loadChecked = new Set();
1690
+ // Never throws by contract.
1662
1691
  }
1663
- const labels = active.some((a) => a.agentTargets?.some((tg) => tg.instanceId)) ? readRosterLabels() : new Map<string, string>();
1692
+ const jsIds = new Set(active.map((a) => a.id));
1693
+ const jevOnly = (jevAssignments ?? []).filter((a) => !jsIds.has(a.id));
1694
+ const labels = [...active, ...jevOnly].some((a) => a.agentTargets?.some((tg) => tg.instanceId))
1695
+ ? readRosterLabels()
1696
+ : new Map<string, string>();
1664
1697
  const targetText = (targets: NonNullable<(typeof active)[number]["agentTargets"]>): string =>
1665
1698
  `for ${targets
1666
1699
  .map((tg) => (tg.instanceId ? `${tg.integration} / ${labels.get(tg.instanceId) ?? tg.instanceId}` : tg.integration))
1667
1700
  .join(", ")}`;
1668
- if (active.length > 0 || jev.jevMode !== null) {
1701
+ if (active.length > 0 || jevOnly.length > 0 || jev.jevMode !== null) {
1702
+ const jevRows: PoliciesScreenRow[] = jevOnly.map((a) => {
1703
+ const tags = [
1704
+ ...(a.effect === "observe" ? ["observe"] : []),
1705
+ "jev",
1706
+ ...(a.agentTargets?.length ? [targetText(a.agentTargets)] : []),
1707
+ ];
1708
+ return { state: "on" as const, name: a.id, description: `v${a.version}`, tag: tags.join(" · ") };
1709
+ });
1669
1710
  cloud = {
1670
1711
  deployment: active[0]?.deployment ?? jev.deployment ?? null,
1671
- rows: active.map((artifact) => {
1712
+ jevListed: jevAssignments !== null,
1713
+ rows: [...active.map((artifact) => {
1672
1714
  // Only a `both` policy carries a server-derived reviewer list (C1).
1673
1715
  const both = artifact.authority === "reviewable" && (artifact.reviewedBy?.length ?? 0) > 0;
1674
1716
  const checks = both ? (artifact.reviewedBy ?? []).join(", ") : "";
@@ -1691,21 +1733,13 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
1691
1733
  tag: [...tags, `not loaded: ${failed}`].join(" · "),
1692
1734
  };
1693
1735
  }
1694
- if (!loadChecked?.has(`${artifact.id}@${artifact.version}`)) {
1695
- return {
1696
- state: "off" as const,
1697
- name: artifact.id,
1698
- description: `v${artifact.version}`,
1699
- tag: [...tags, "not loaded yet: it loads at the next agent tool call"].join(" · "),
1700
- };
1701
- }
1702
1736
  return {
1703
1737
  state: "on" as const,
1704
1738
  name: artifact.id,
1705
1739
  description: `v${artifact.version}`,
1706
1740
  ...(tags.length > 0 ? { tag: tags.join(" · ") } : {}),
1707
1741
  };
1708
- }),
1742
+ }), ...jevRows],
1709
1743
  jevMode: jev.jevMode,
1710
1744
  problems: [
1711
1745
  ...(jsProblem !== null ? [`The Cloud JavaScript policies could not be read, so none of them is enforcing: ${jsProblem}`] : []),