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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +22 -22
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  57. package/.next/standalone/.next/server/app/settings/page.js +7 -7
  58. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/{[externals]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
  70. package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ts0fg5yih1c1._.js → [root-of-the-server]__00l2u6j86cb8y._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _0ouu886szh6yj._.js} +10 -9
  91. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1ve8y151ua7qx._.js → _1fw0_nzkae5ao._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_00jxj8foxqz5t.js → node_modules_html-to-image_es_index_1fv03uigni8s5.js} +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  112. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  113. package/.next/standalone/.next/server/pages/404.html +1 -1
  114. package/.next/standalone/.next/server/pages/500.html +1 -1
  115. package/.next/standalone/.next/server/preview-props.json +3 -3
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  118. package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 0ay__-d2h1xb5.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 0tbm3vjm62h-2.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/1c_jf6g5wnslk.js +1 -0
  121. package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 1my3-5p47t7r-.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +1 -0
  123. package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
  124. package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
  125. package/.next/standalone/.next/static/chunks/2o6d37j425uci.js +1 -0
  126. package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 42xptz88mtq3x.js} +1 -1
  128. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  129. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  130. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +32 -0
  131. package/.next/standalone/fp-cloud-cli/README.md +40 -1
  132. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +91 -12
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +78 -16
  134. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
  135. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +124 -6
  136. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -20
  138. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +14 -7
  141. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +527 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
  144. package/.next/standalone/hermes-plugin/README.md +6 -4
  145. package/.next/standalone/hermes-plugin/__init__.py +27 -0
  146. package/.next/standalone/hermes-plugin/client.py +5 -1
  147. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  148. package/.next/standalone/package.json +11 -10
  149. package/.next/standalone/sdk/python/uv.lock +31 -8
  150. package/.next/standalone/server.js +1 -1
  151. package/bin/failproofai.mjs +17 -1
  152. package/dist/cli.mjs +3507 -1250
  153. package/dist/worker.mjs +1779 -169
  154. package/hermes-plugin/README.md +6 -4
  155. package/hermes-plugin/__init__.py +27 -0
  156. package/hermes-plugin/client.py +5 -1
  157. package/hermes-plugin/plugin.yaml +1 -1
  158. package/package.json +11 -10
  159. package/src/audit/open-browser.ts +6 -7
  160. package/src/hooks/agent-roster.ts +180 -0
  161. package/src/hooks/agent-skill.ts +203 -0
  162. package/src/hooks/agent-targets.ts +60 -0
  163. package/src/hooks/cloud-connection.ts +91 -45
  164. package/src/hooks/cloud-enrollment-cli.ts +7 -4
  165. package/src/hooks/cloud-login.ts +6 -3
  166. package/src/hooks/cloud-managed-policies.ts +430 -20
  167. package/src/hooks/cloud-policy-errors.ts +481 -0
  168. package/src/hooks/codex-system-hooks.ts +10 -4
  169. package/src/hooks/config-status.ts +10 -4
  170. package/src/hooks/configure-wizard.ts +54 -26
  171. package/src/hooks/custom-hooks-loader.ts +26 -3
  172. package/src/hooks/daemon-client.ts +9 -4
  173. package/src/hooks/daemon-service.ts +2 -1
  174. package/src/hooks/delivery-health.ts +47 -2
  175. package/src/hooks/effective-reviewers.ts +36 -1
  176. package/src/hooks/fp-config.ts +19 -0
  177. package/src/hooks/fp-home.ts +6 -0
  178. package/src/hooks/handler.ts +255 -19
  179. package/src/hooks/health-probe.ts +15 -0
  180. package/src/hooks/hook-activity-store.ts +7 -1
  181. package/src/hooks/integrations.ts +36 -14
  182. package/src/hooks/jev-activity.ts +117 -1
  183. package/src/hooks/jev-cli.ts +196 -1
  184. package/src/hooks/jev-cloud-connection.ts +92 -0
  185. package/src/hooks/manager.ts +174 -34
  186. package/src/hooks/pack-failclosed.ts +2 -0
  187. package/src/hooks/pack-manifest.ts +8 -1
  188. package/src/hooks/policy-evaluator.ts +22 -1
  189. package/src/hooks/policy-registry.ts +3 -2
  190. package/src/hooks/policy-reviewability.ts +23 -5
  191. package/src/hooks/semantic/cloud-jev-health.ts +227 -0
  192. package/src/hooks/semantic/cloud-jev.ts +868 -0
  193. package/src/hooks/semantic/combine.ts +26 -1
  194. package/src/hooks/semantic/jev-client.ts +19 -2
  195. package/src/hooks/semantic/jev-config.ts +179 -30
  196. package/src/hooks/semantic/jev-review.ts +217 -10
  197. package/src/hooks/semantic/pack-policies.ts +2 -1
  198. package/src/hooks/semantic/types.ts +8 -0
  199. package/src/hooks/session-pause-cli.ts +1 -1
  200. package/src/hooks/tui.ts +13 -4
  201. package/src/hooks/uninstall-cli.ts +12 -1
  202. package/src/hooks/worker-server.ts +5 -1
  203. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
  204. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
  206. package/.next/standalone/.next/static/chunks/0a-ccsp3xmypz.js +0 -1
  207. package/.next/standalone/.next/static/chunks/13jv-xvabeb62.js +0 -6
  208. package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
  209. package/.next/standalone/.next/static/chunks/2meqb9kygrncb.js +0 -1
  210. package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
  211. package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
  212. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
  213. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
  214. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
@@ -0,0 +1,481 @@
1
+ /**
2
+ * `cloud-policies/errors.json` — what this machine could not load from its
3
+ * FailproofAI Cloud deployment, for the daemon to report on its next poll
4
+ * (`policyErrors`, CONTRACT C4–C6).
5
+ *
6
+ * Nothing here changes a verdict. Cloud policies are read fail-open: a JS
7
+ * policy that fails to import costs that one policy on this machine and
8
+ * nothing else, which is the right trade on the hook path and the wrong thing
9
+ * to keep quiet about. Before this file the only trace was a line in the local
10
+ * hook log, so a fleet page read "applied" for a deployment that was not. The
11
+ * same goes for FailproofAI Cloud's Jev (CONTRACT C10): a Jev mode this machine
12
+ * cannot act on, and an installed pack's check Cloud dropped for the question
13
+ * budget, are said here rather than only in a local log.
14
+ *
15
+ * ## One writer per file
16
+ *
17
+ * The CLI writes `errors.json`; the daemon keeps its own reconcile errors in
18
+ * `daemon-errors.json` and merges the two onto the wire. Neither writes the
19
+ * other's file, so there is no read-modify-write race between processes.
20
+ *
21
+ * ## Written on change, atomically
22
+ *
23
+ * Computed on every hook evaluation of a Cloud-managed machine, so the write
24
+ * has to be almost always a no-op: the new content is compared with what is on
25
+ * disk (and with what this process last wrote, to skip even that read in the
26
+ * warm worker) and written only when it differs — tmp file + rename, owner-only,
27
+ * so the daemon never reads half a file.
28
+ *
29
+ * A machine that never had a problem never gets the file at all: an empty list
30
+ * is written only to REPLACE a non-empty one, which is what clears the report
31
+ * on the server. That keeps the poll parameter absent on a healthy fleet.
32
+ */
33
+ import { randomUUID } from "node:crypto";
34
+ import { chmodSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
35
+ import { homedir } from "node:os";
36
+ import { resolve } from "node:path";
37
+ import {
38
+ CLOUD_POLICY_ERRORS_FILE,
39
+ JEV_BUDGET_FILE,
40
+ cloudManagedPolicyRoot,
41
+ type CloudJevState,
42
+ type CloudManagedPolicyArtifact,
43
+ type CloudPolicyError,
44
+ type CloudPolicyErrorKind,
45
+ } from "./cloud-managed-policies";
46
+ import type { PolicyLoadFailure } from "./custom-hooks-loader";
47
+ import { packsRoot } from "./pack-manifest";
48
+
49
+ export type { CloudPolicyError, CloudPolicyErrorKind } from "./cloud-managed-policies";
50
+
51
+ /** The message the contract fixes for "Cloud set a Jev mode and this machine has no Cloud Jev credential". */
52
+ export const JEV_UNCONFIGURED = "jev_unconfigured";
53
+
54
+ /**
55
+ * The message the contract fixes for "Cloud set a Jev mode on a machine
56
+ * connected for decisions only" (`--no-transcripts`), which never runs Jev
57
+ * (CONTRACT C10.2).
58
+ */
59
+ export const TRANSCRIPTS_DISABLED = "transcripts_disabled";
60
+
61
+ /** How an installed pack's check FailproofAI Cloud dropped for the question budget is reported (C10.5). */
62
+ export const JEV_BUDGET = "jev_budget";
63
+
64
+ /**
65
+ * How an installed pack's check is reported when FailproofAI Cloud dropped it
66
+ * because one of the org's Cloud checks has the same name (Cloud wins the name,
67
+ * D-S-2). It is not a budget problem, and its remedy differs.
68
+ */
69
+ export const JEV_NAME_CLASH = "jev_name_clash";
70
+
71
+ /** Is `e` one of the pack-check drops `jev-budget.json` records? */
72
+ export function isPackDropEntry(e: Pick<CloudPolicyError, "id" | "message">): boolean {
73
+ return e.id.startsWith("pack:") && (e.message.startsWith(JEV_BUDGET) || e.message.startsWith(JEV_NAME_CLASH));
74
+ }
75
+
76
+ /**
77
+ * A message with no local path in it (CONTRACT C9.5): the home directory
78
+ * becomes `~`, and any other absolute path becomes its last segment.
79
+ *
80
+ * The report leaves the machine, and a path names the user — the home-relative
81
+ * artifact path in a missing-artifact message, a loader's temp path in an
82
+ * import error, the `chmod 600 <path>` hint of a refused `jev.json`. The daemon
83
+ * applies the same rule again on the way out (`redact_local_paths` in
84
+ * `cloud_client.rs`, whose tests pin the same cases), so a report never carries
85
+ * a username whichever side wrote it. The full text stays in the local hook
86
+ * log, where it is useful and goes nowhere.
87
+ *
88
+ * A path starts at a `/` that begins the text or follows whitespace, a quote,
89
+ * an opening bracket, `=`, `,` or `:` — but not a URL's `//host` after `:`,
90
+ * and never a `/` following any other character, so a URL (`https://host/path`)
91
+ * and a relative path (`a/b`) are left alone while `file:///tmp/x` and
92
+ * `open:/etc/x` are not — and runs to the next whitespace, quote, closing
93
+ * bracket, `,` or `;`.
94
+ */
95
+ export function redactLocalPaths(message: string, home: string | null = safeHomedir()): string {
96
+ let text = message;
97
+ const base = home?.replace(/\/+$/, "") ?? "";
98
+ if (base.length > 1) {
99
+ let out = "";
100
+ let rest = text;
101
+ for (let at = rest.indexOf(base); at >= 0; at = rest.indexOf(base)) {
102
+ const next = rest.charAt(at + base.length);
103
+ // `/home/al` must not match inside `/home/alice`.
104
+ const whole = next === "" || next === "/" || next === ":" || PATH_END.test(next);
105
+ out += rest.slice(0, at) + (whole ? "~" : base);
106
+ rest = rest.slice(at + base.length);
107
+ }
108
+ text = out + rest;
109
+ }
110
+ // `:(?!\/\/(?!\/))`: a `:` not followed by a URL's `//host` — `scheme://host`
111
+ // stays, while `file:///path` and `x:/path` are paths.
112
+ return text.replace(/(^|[\s"'`(\[<{=,]|:(?!\/\/(?!\/)))(\/[^\s"'`)\]>},;]*)/g, (_m, lead: string, path: string) => {
113
+ const last = path.replace(/\/+$/, "").split("/").pop() ?? "";
114
+ return lead + (last === "" ? "/" : last);
115
+ });
116
+ }
117
+
118
+ const PATH_END = /[\s"'`)\]>},;]/;
119
+
120
+ function safeHomedir(): string | null {
121
+ try {
122
+ return homedir();
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function cloudPolicyErrorsPath(): string {
129
+ return resolve(cloudManagedPolicyRoot(), CLOUD_POLICY_ERRORS_FILE);
130
+ }
131
+
132
+ const KINDS: ReadonlySet<string> = new Set<CloudPolicyErrorKind>(["regex", "jev", "both", "daemon"]);
133
+
134
+ /** The file as the CLI last wrote it, or null when absent or unreadable. */
135
+ export function readCloudPolicyErrors(): CloudPolicyError[] | null {
136
+ try {
137
+ const raw = JSON.parse(readFileSync(cloudPolicyErrorsPath(), "utf8")) as { errors?: unknown };
138
+ if (!Array.isArray(raw.errors)) return null;
139
+ return raw.errors.filter(
140
+ (e): e is CloudPolicyError =>
141
+ !!e &&
142
+ typeof e === "object" &&
143
+ typeof (e as CloudPolicyError).id === "string" &&
144
+ typeof (e as CloudPolicyError).message === "string" &&
145
+ KINDS.has((e as CloudPolicyError).kind) &&
146
+ ((e as CloudPolicyError).version === null || Number.isSafeInteger((e as CloudPolicyError).version)),
147
+ );
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /** Text this process last wrote, so the warm worker's steady state reads nothing. */
154
+ let lastWritten: { path: string; text: string } | null = null;
155
+
156
+ /** Tests only. */
157
+ export function _resetCloudPolicyErrorsCacheForTest(): void {
158
+ lastWritten = null;
159
+ }
160
+
161
+ function serialize(errors: ReadonlyArray<CloudPolicyError>): string {
162
+ // Field order fixed, so equal lists are equal text. Redacted here as well as
163
+ // in `collectCloudPolicyErrors`: this is the one way into the file.
164
+ const list = errors.map((e) => ({ id: e.id, version: e.version, kind: e.kind, message: redactLocalPaths(e.message) }));
165
+ return `${JSON.stringify({ errors: list }, null, 2)}\n`;
166
+ }
167
+
168
+ /**
169
+ * Replace `errors.json` with `errors` when that changes it. Never throws.
170
+ *
171
+ * - `"skipped"`: nothing to report and no file to clear, or no deployment here
172
+ * (no `active.json` — a disconnected or never-managed machine gets no file).
173
+ * - `"unchanged"`: the file already says exactly this.
174
+ * - `"written"`: it did not, and now does.
175
+ * - `"failed"`: the write itself failed (logged nowhere: the next event retries).
176
+ */
177
+ export function writeCloudPolicyErrors(errors: ReadonlyArray<CloudPolicyError>): "written" | "unchanged" | "skipped" | "failed" {
178
+ try {
179
+ const root = cloudManagedPolicyRoot();
180
+ if (!existsSync(resolve(root, "active.json"))) return "skipped";
181
+ const path = cloudPolicyErrorsPath();
182
+ const text = serialize(errors);
183
+ if (lastWritten && lastWritten.path === path && lastWritten.text === text && existsSync(path)) return "unchanged";
184
+ let current: string | null = null;
185
+ try {
186
+ current = readFileSync(path, "utf8");
187
+ } catch {
188
+ current = null;
189
+ }
190
+ if (current === text) {
191
+ lastWritten = { path, text };
192
+ return "unchanged";
193
+ }
194
+ if (current === null && errors.length === 0) return "skipped";
195
+ writeAtomic(path, text);
196
+ lastWritten = { path, text };
197
+ return "written";
198
+ } catch {
199
+ // A report that cannot be written must not cost a hook its answer.
200
+ return "failed";
201
+ }
202
+ }
203
+
204
+ // ── Which Cloud JS policies a hook has load-checked (N4) ────────────────────
205
+ //
206
+ // `failproofai policies` must not draw a Cloud policy as ● on before any hook
207
+ // has imported it: only the hook path loads Cloud JavaScript, and only the hook
208
+ // path writes `errors.json`, so right after a deployment a broken policy has
209
+ // no error yet. The hook path records here which `id@version`s of which
210
+ // deployment it has load-checked (loaded, or failed into `errors.json`); the
211
+ // listing shows the rest as not loaded yet. Union across hooks, since an
212
+ // agent-scoped policy is only ever loaded by its own agent's hooks.
213
+
214
+ export const CLOUD_LOAD_CHECKED_FILE = "load-checked.json";
215
+
216
+ export interface CloudLoadChecked {
217
+ deployment: number;
218
+ checked: string[];
219
+ }
220
+
221
+ export function cloudLoadCheckedPath(): string {
222
+ return resolve(cloudManagedPolicyRoot(), CLOUD_LOAD_CHECKED_FILE);
223
+ }
224
+
225
+ /** The record, or null when absent or unreadable. */
226
+ export function readCloudLoadChecked(): CloudLoadChecked | null {
227
+ try {
228
+ const raw = JSON.parse(readFileSync(cloudLoadCheckedPath(), "utf8")) as Record<string, unknown>;
229
+ if (!Number.isSafeInteger(raw.deployment) || !Array.isArray(raw.checked)) return null;
230
+ return { deployment: raw.deployment as number, checked: raw.checked.filter((c): c is string => typeof c === "string") };
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /** Add `policies` to the record for `deployment` (a new deployment starts afresh). Never throws; writes only on change. */
237
+ export function recordCloudLoadChecked(deployment: number, policies: ReadonlyArray<{ id: string; version: number }>): void {
238
+ try {
239
+ if (!existsSync(resolve(cloudManagedPolicyRoot(), "active.json"))) return;
240
+ const path = cloudLoadCheckedPath();
241
+ const prior = readCloudLoadChecked();
242
+ const base = prior && prior.deployment === deployment ? prior.checked : [];
243
+ const next = [...new Set([...base, ...policies.map((p) => `${p.id}@${p.version}`)])].sort();
244
+ if (prior && prior.deployment === deployment && next.length === base.length) return;
245
+ const text = `${JSON.stringify({ deployment, checked: next }, null, 2)}\n`;
246
+ writeAtomic(path, text);
247
+ } catch {
248
+ // A record that cannot be written costs only the listing's accuracy.
249
+ }
250
+ }
251
+
252
+ export interface CloudPolicyErrorInputs {
253
+ /** `readActiveCloudManagedPolicies` threw: the whole JS half is not enforcing. */
254
+ manifestError?: string | null;
255
+ /** What the JS reader returned. */
256
+ jsPolicies: ReadonlyArray<CloudManagedPolicyArtifact>;
257
+ /** Import failures by Cloud policy id (`LoadAllResult.cloudFailures`). */
258
+ jsFailures?: ReadonlyMap<string, PolicyLoadFailure>;
259
+ /** Cloud's Jev state (its own problems — an unreadable mode — are in `errors`). */
260
+ jev: Pick<CloudJevState, "errors">;
261
+ /**
262
+ * Why Cloud's `observe`/`enforce` mode cannot run here: `transcripts_disabled`,
263
+ * `jev_unconfigured`, or `jev_unconfigured: <detail>`
264
+ * (`loadJevConfigForCloudMode`). Null when it runs, or Cloud's mode does not ask.
265
+ */
266
+ jevProblem?: string | null;
267
+ /** Installed packs' checks FailproofAI Cloud dropped for the budget ({@link readJevBudgetDrops}). */
268
+ budgetDrops?: ReadonlyArray<CloudPolicyError>;
269
+ /** FailproofAI Cloud Jev rate-limited or unavailable here (`semantic/cloud-jev-health.ts`). */
270
+ health?: ReadonlyArray<CloudPolicyError>;
271
+ /** Schema-3 deployment, but the running hook could not identify its agent. */
272
+ agentScopeUnresolved?: boolean;
273
+ }
274
+
275
+ /**
276
+ * A Cloud JS assignment's kind, as far as this machine can tell: `both` when
277
+ * the server derived a reviewer list for it (only a `both` policy gets one,
278
+ * CONTRACT C1), else `regex`.
279
+ */
280
+ function kindOf(policy: Pick<CloudManagedPolicyArtifact, "authority" | "reviewedBy">): CloudPolicyErrorKind {
281
+ return policy.authority === "reviewable" && (policy.reviewedBy?.length ?? 0) > 0 ? "both" : "regex";
282
+ }
283
+
284
+ /**
285
+ * Everything wrong with this machine's Cloud deployment, as `errors.json`
286
+ * entries: deduplicated, in a stable order. Pure.
287
+ *
288
+ * What is NOT here any more (CONTRACT C10): Jev artifact and declaration
289
+ * failures and a `reviewedBy` naming a missing check — the machine holds no
290
+ * Cloud Jev check to fail, and cannot tell which names Cloud has. A `both`
291
+ * policy on a machine whose Cloud mode does not ask simply stays hard; the
292
+ * server knows the mode it set.
293
+ */
294
+ export function collectCloudPolicyErrors(input: CloudPolicyErrorInputs): CloudPolicyError[] {
295
+ const out: CloudPolicyError[] = [];
296
+ if (input.manifestError) {
297
+ out.push({ id: "active.json", version: null, kind: "daemon", message: `Cloud policies could not be loaded: ${input.manifestError}` });
298
+ }
299
+ if (input.agentScopeUnresolved) {
300
+ out.push({
301
+ id: "agentScope", version: null, kind: "daemon",
302
+ message: "agent_scope_unresolved: this hook's agent profile is unknown; targeted Cloud policies did not match",
303
+ });
304
+ }
305
+ for (const policy of input.jsPolicies) {
306
+ const failure = input.jsFailures?.get(policy.id);
307
+ if (failure) {
308
+ out.push({ id: policy.id, version: policy.version, kind: kindOf(policy), message: `policy did not load (${failure.type}): ${failure.reason}` });
309
+ }
310
+ }
311
+ out.push(...input.jev.errors);
312
+ out.push(...(input.budgetDrops ?? []));
313
+ if (input.jevProblem) {
314
+ out.push({ id: "jevMode", version: null, kind: "daemon", message: input.jevProblem });
315
+ }
316
+ out.push(...(input.health ?? []));
317
+
318
+ const seen = new Set<string>();
319
+ return out
320
+ .map((e) => ({ ...e, message: redactLocalPaths(e.message) }))
321
+ .filter((e) => {
322
+ const key = `${e.id}\u0000${e.version}\u0000${e.kind}\u0000${e.message}`;
323
+ if (seen.has(key)) return false;
324
+ seen.add(key);
325
+ return true;
326
+ });
327
+ }
328
+
329
+ // ── Installed packs' checks FailproofAI Cloud dropped for the budget ─────────
330
+ //
331
+ // FailproofAI Cloud puts its own checks first in the one question budget and
332
+ // drops whole installed-pack question groups, last first, when the two do not
333
+ // fit together (CONTRACT C10.4 step 5); its reply names them (`droppedLocal`).
334
+ // Which groups a call carries depends on the tool, so a report of only the
335
+ // latest reply's drops would flap from call to call. So the drops are kept,
336
+ // as a union, in `jev-budget.json` for the machine, deployment and Jev mode
337
+ // they happened under — a new deployment or mode starts afresh, which is how
338
+ // a drop that no longer happens stops being reported — and `errors.json`
339
+ // carries them as `{id: "pack:<packId>", kind: "daemon", message: "jev_budget:
340
+ // dropped <name>"}`. A check dropped because an org Cloud check has its name
341
+ // is recorded the same way, as `jev_name_clash: <name> (the FailproofAI Cloud
342
+ // check is used)`. A drop of a pack no longer installed is not reported, here
343
+ // or by the daemon's poll, which also drops the report once the deployment it
344
+ // was recorded under is no longer the active one.
345
+
346
+ /** What a set of budget drops is keyed by: they are true of this deployment only. */
347
+ export interface JevBudgetKey {
348
+ machineId: string;
349
+ deployment: number | null;
350
+ jevMode: "observe" | "enforce";
351
+ }
352
+
353
+ /** One dropped pack check; `clash` when Cloud dropped it for a same-named Cloud check. */
354
+ export interface JevPackDrop {
355
+ packId: string;
356
+ name: string;
357
+ clash?: true;
358
+ }
359
+
360
+ interface JevBudgetRecord extends JevBudgetKey {
361
+ dropped: JevPackDrop[];
362
+ }
363
+
364
+ export function jevBudgetPath(): string {
365
+ return resolve(cloudManagedPolicyRoot(), JEV_BUDGET_FILE);
366
+ }
367
+
368
+ function readJevBudgetRecord(): JevBudgetRecord | null {
369
+ try {
370
+ const raw = JSON.parse(readFileSync(jevBudgetPath(), "utf8")) as Partial<JevBudgetRecord>;
371
+ if (
372
+ typeof raw.machineId !== "string" ||
373
+ !(raw.deployment === null || Number.isSafeInteger(raw.deployment)) ||
374
+ (raw.jevMode !== "observe" && raw.jevMode !== "enforce") ||
375
+ !Array.isArray(raw.dropped)
376
+ ) {
377
+ return null;
378
+ }
379
+ const dropped = raw.dropped
380
+ .filter((d): d is JevPackDrop => !!d && typeof d.packId === "string" && typeof d.name === "string")
381
+ .map((d) => ({ packId: d.packId, name: d.name, ...(d.clash === true ? { clash: true as const } : {}) }));
382
+ return { machineId: raw.machineId, deployment: raw.deployment as number | null, jevMode: raw.jevMode, dropped };
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+
388
+ const sameKey = (a: JevBudgetKey, b: JevBudgetKey): boolean =>
389
+ a.machineId === b.machineId && a.deployment === b.deployment && a.jevMode === b.jevMode;
390
+
391
+ const dropKey = (d: JevPackDrop): string => `${d.packId}\u0000${d.name}\u0000${d.clash === true}`;
392
+
393
+ function budgetEntries(dropped: ReadonlyArray<JevPackDrop>): CloudPolicyError[] {
394
+ return [...dropped]
395
+ .sort((a, b) => (dropKey(a) < dropKey(b) ? -1 : dropKey(a) > dropKey(b) ? 1 : 0))
396
+ .map((d) => ({
397
+ id: `pack:${d.packId}`,
398
+ version: null,
399
+ kind: "daemon" as const,
400
+ message: d.clash ? `${JEV_NAME_CLASH}: ${d.name} (the FailproofAI Cloud check is used)` : `${JEV_BUDGET}: dropped ${d.name}`,
401
+ }));
402
+ }
403
+
404
+ /**
405
+ * The ids of the installed packs, read straight off `installed.json` (no
406
+ * artifact is verified: only the ids are wanted). An empty set when no pack is
407
+ * installed; null when the file exists and cannot be read, which filters
408
+ * nothing ("cannot tell" is not "not installed"). Never throws.
409
+ */
410
+ export function installedPackIds(): ReadonlySet<string> | null {
411
+ try {
412
+ const path = resolve(packsRoot(), "installed.json");
413
+ if (!existsSync(path)) return new Set();
414
+ const raw = JSON.parse(readFileSync(path, "utf8")) as { packs?: unknown };
415
+ if (!Array.isArray(raw.packs)) return null;
416
+ return new Set(
417
+ raw.packs.map((p) => (p && typeof p === "object" ? (p as { id?: unknown }).id : null)).filter((id): id is string => typeof id === "string"),
418
+ );
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+
424
+ /**
425
+ * The drops recorded for exactly this machine, deployment and mode, as
426
+ * `errors.json` entries — none for any other key, and none of a pack that is
427
+ * no longer installed (`installed`, when known). Never throws.
428
+ */
429
+ export function readJevBudgetDrops(key: JevBudgetKey, installed: ReadonlySet<string> | null = null): CloudPolicyError[] {
430
+ const record = readJevBudgetRecord();
431
+ if (!record || !sameKey(record, key)) return [];
432
+ return budgetEntries(installed ? record.dropped.filter((d) => installed.has(d.packId)) : record.dropped);
433
+ }
434
+
435
+ /**
436
+ * Add the pack checks FailproofAI Cloud just dropped to the record for `key`
437
+ * (starting afresh when the key changed), and to `errors.json` at once, so the
438
+ * report does not wait for the next hook. Both writes are atomic, owner-only,
439
+ * and happen only when something changed. Never throws: a report must not
440
+ * cost a hook its answer.
441
+ */
442
+ export function recordJevBudgetDrops(key: JevBudgetKey, drops: ReadonlyArray<JevPackDrop>): void {
443
+ try {
444
+ if (drops.length === 0 || !existsSync(resolve(cloudManagedPolicyRoot(), "active.json"))) return;
445
+ const current = readJevBudgetRecord();
446
+ const base = current && sameKey(current, key) ? current.dropped : [];
447
+ const seen = new Set(base.map(dropKey));
448
+ const added: JevPackDrop[] = [];
449
+ for (const d of drops) {
450
+ const id = dropKey(d);
451
+ if (seen.has(id)) continue;
452
+ seen.add(id);
453
+ added.push({ packId: d.packId, name: d.name, ...(d.clash === true ? { clash: true as const } : {}) });
454
+ }
455
+ if (current && sameKey(current, key) && added.length === 0) return;
456
+ const record: JevBudgetRecord = { ...key, dropped: [...base, ...added] };
457
+ writeAtomic(jevBudgetPath(), `${JSON.stringify(record, null, 2)}\n`);
458
+ // Merged into the report as it stands: every other entry kept, the pack
459
+ // budget drops replaced by this deployment's.
460
+ const report = (readCloudPolicyErrors() ?? []).filter((e) => !isPackDropEntry(e));
461
+ writeCloudPolicyErrors([...report, ...budgetEntries(record.dropped)]);
462
+ } catch {
463
+ // The next event records it again.
464
+ }
465
+ }
466
+
467
+ function writeAtomic(path: string, text: string): void {
468
+ const tmp = `${path}.tmp-${process.pid}-${randomUUID()}`;
469
+ try {
470
+ writeFileSync(tmp, text, { mode: 0o600, flag: "wx" });
471
+ chmodSync(tmp, 0o600);
472
+ renameSync(tmp, path);
473
+ } catch (err) {
474
+ try {
475
+ rmSync(tmp, { force: true });
476
+ } catch {
477
+ // Nothing useful to do.
478
+ }
479
+ throw err;
480
+ }
481
+ }
@@ -432,10 +432,16 @@ export type CodexHookTrust =
432
432
  /** In hooks.json and not all trusted: Codex skips the untrusted ones. */
433
433
  | { kind: "untrusted"; path: string; untrusted: number; total: number };
434
434
 
435
+ /** The Codex profile's home: `CODEX_HOME` when set, else `~/.codex`. */
436
+ export function codexHome(): string {
437
+ const dir = process.env.CODEX_HOME?.trim();
438
+ return dir ? resolve(dir) : resolve(homedir(), ".codex");
439
+ }
440
+
435
441
  export interface CodexTrustDeps extends CodexSystemDeps {
436
- /** `~/.codex/hooks.json`. */
442
+ /** `$CODEX_HOME/hooks.json` (`~/.codex/hooks.json`). */
437
443
  hooksJsonPath?: string;
438
- /** `~/.codex/config.toml`, where Codex keeps `[hooks.state]`. */
444
+ /** `$CODEX_HOME/config.toml`, where Codex keeps `[hooks.state]`. */
439
445
  userConfigPath?: string;
440
446
  }
441
447
 
@@ -452,7 +458,7 @@ export function codexHookTrust(deps: CodexTrustDeps = {}): CodexHookTrust {
452
458
  const systemPath = resolvedPath(deps);
453
459
  if (systemPath && codexSystemHooksPresent({ path: systemPath })) return { kind: "system", path: systemPath };
454
460
 
455
- const hooksJsonPath = deps.hooksJsonPath ?? resolve(homedir(), ".codex", "hooks.json");
461
+ const hooksJsonPath = deps.hooksJsonPath ?? resolve(codexHome(), "hooks.json");
456
462
  const ours = failproofaiCodexHookIdentities(hooksJsonPath);
457
463
  if (ours.length === 0) return { kind: "none" };
458
464
 
@@ -462,7 +468,7 @@ export function codexHookTrust(deps: CodexTrustDeps = {}): CodexHookTrust {
462
468
  let trustedHashes = new Map<string, string>();
463
469
  try {
464
470
  trustedHashes = parseCodexTrustedHashes(
465
- readFileSync(deps.userConfigPath ?? resolve(homedir(), ".codex", "config.toml"), "utf8"),
471
+ readFileSync(deps.userConfigPath ?? resolve(codexHome(), "config.toml"), "utf8"),
466
472
  );
467
473
  } catch {
468
474
  // No user config: nothing is trusted.
@@ -27,7 +27,7 @@ import { readCloudCredentials } from "./cloud-enrollment";
27
27
  import { CODEX_UNTRUSTED_NOTICE, codexHookTrust, type CodexHookTrust } from "./codex-system-hooks";
28
28
  import { describeMachine } from "./cloud-enrollment-cli";
29
29
  import { cloudBaseFor } from "./cloud-connection";
30
- import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
30
+ import { listActiveCloudManagedPolicies } from "./cloud-managed-policies";
31
31
  import { readIngestCredential } from "./collector-config";
32
32
  import { discoverPolicyFiles } from "./custom-hooks-loader";
33
33
  import { daemonSocketPresent, isDaemonConfigured } from "./daemon-client";
@@ -259,7 +259,10 @@ export function summarizePolicySources(cwd?: string): StatusPolicies {
259
259
  }
260
260
 
261
261
  try {
262
- for (const artifact of readActiveCloudManagedPolicies()) {
262
+ // Every assignment, whatever agent it targets: a hook's agent scope applied
263
+ // with no agent hid each targeted policy ("None installed" beside a
264
+ // deployment that enforces), the status twin of B4.
265
+ for (const artifact of listActiveCloudManagedPolicies()) {
263
266
  if (artifact.effect === "observe") summary.observing++;
264
267
  else summary.cloudOn++;
265
268
  }
@@ -525,7 +528,10 @@ export function renderStatusScreen(facts: StatusFacts, opts: ScreenKitOpts & { c
525
528
  return kit.caution(`No report from failproofaid for ${formatDuration(now - health.writtenAt)}`);
526
529
  }
527
530
  if (!health.delivering) return `${kit.off} Not delivering, failproofaid has no key`;
528
- const backlog = queued === 0 ? "nothing queued" : `${many(queued, "batch", "batches")} queued`;
531
+ // Batches an earlier key had refused wait for the daemon to re-send them
532
+ // with the key that works now: queued, not refused.
533
+ const pending = queued + (refused.resending ?? 0);
534
+ const backlog = pending === 0 ? "nothing queued" : `${many(pending, "batch", "batches")} queued`;
529
535
  const stuck =
530
536
  queued > 0 &&
531
537
  (oldestQueuedMs ?? 0) > STATUS_QUEUE_STUCK_MS &&
@@ -578,7 +584,7 @@ export function renderStatusScreen(facts: StatusFacts, opts: ScreenKitOpts & { c
578
584
  `${pauseClockTime(pause.expiresAt, opts.clock)}${pause.cwd ? `${kit.sep}${pause.cwd}` : ""}`,
579
585
  ]),
580
586
  // An enforcement fact, not fine print: a pause suspends local policy only.
581
- ...(facts.pauses.length > 0 ? [["", "Cloud-managed policies keep enforcing."] as [string, string]] : []),
587
+ ...(facts.pauses.length > 0 ? [["", "Cloud-managed policies, and FailproofAI Cloud's Jev checks, keep enforcing."] as [string, string]] : []),
582
588
  ];
583
589
 
584
590
  const cloudRows: Array<[string, string]> =