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
@@ -37,8 +37,8 @@ import type { TwoTierReview } from "./semantic/combine";
37
37
  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
- import { loadAllCustomHooks } from "./custom-hooks-loader";
41
- import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled } from "./effective-reviewers";
40
+ import { loadAllCustomHooks, type LoadAllResult } from "./custom-hooks-loader";
41
+ import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled, packJevChecksInstalled } from "./effective-reviewers";
42
42
  import {
43
43
  authorityDeclarationFor,
44
44
  refusedAuthorityWarning,
@@ -56,13 +56,35 @@ import { resolveTranscriptPath } from "./resolve-transcript-path";
56
56
  import { getInstanceId } from "../../lib/telemetry-id";
57
57
  import { hookLogInfo, hookLogWarn } from "./hook-logger";
58
58
  import { readStdinPayload } from "./read-stdin";
59
- import { readActiveCloudManagedPolicies, type CloudManagedPolicyArtifact } from "./cloud-managed-policies";
59
+ import {
60
+ cloudJevAsks,
61
+ cloudJevAssignmentsFor,
62
+ withCloudAuthority,
63
+ readActiveCloudManagedPolicies,
64
+ readCloudJevMode,
65
+ readCloudJevState,
66
+ readCloudAgentScopeRequired,
67
+ type CloudManagedPolicyArtifact,
68
+ type CloudPolicyError,
69
+ } from "./cloud-managed-policies";
70
+ import {
71
+ cloudPolicyErrorsPath,
72
+ collectCloudPolicyErrors,
73
+ installedPackIds,
74
+ jevBudgetPath,
75
+ readJevBudgetDrops,
76
+ writeCloudPolicyErrors,
77
+ recordCloudLoadChecked,
78
+ } from "./cloud-policy-errors";
60
79
  import { hasInstalledRegexPacks, readInstalledPacks, type PackError, type ResolvedPack } from "./pack-manifest";
61
80
  import { missingGuards, packFailureReason, combinedGuardMatch, guardsCover } from "./pack-failclosed";
62
81
  import { readActivePause, type ActivePause } from "./session-pause";
63
82
  import { jevConfigFile } from "./fp-home";
64
83
  import { layoutWarningForHook } from "./fp-reset";
65
84
  import { noteUntracedAgent } from "./untraced-note";
85
+ import { isHealthProbe } from "./health-probe";
86
+ import { readRuntimeAgentIdentity, runtimeAgentSettingsPath } from "./agent-roster";
87
+ import type { AgentIdentity } from "./agent-targets";
66
88
 
67
89
  /**
68
90
  * Canonicalize an event name to PascalCase. Codex sends snake_case event names
@@ -169,6 +191,8 @@ export interface EvaluateHookEventOptions {
169
191
  * written for.
170
192
  */
171
193
  fallbackCwd?: string;
194
+ /** Config selected by the hook process (may differ from the worker's env). */
195
+ agentSettingsPath?: string;
172
196
  /**
173
197
  * The warm worker's hook into its request queue. Called at most once, and
174
198
  * only on the two-tier path, at the point this evaluation stops reading the
@@ -222,10 +246,20 @@ async function runObserved(
222
246
  // package ships none: until `failproofai policies add FailproofAI/jev-policies`
223
247
  // (or another pack declaring `semantic` checks) Jev is inert — no request, no
224
248
  // intent capture — whether the config is BYOK or FailproofAI Cloud's;
249
+ //
250
+ // …or, in place of the first two, FailproofAI Cloud sets this machine's Jev
251
+ // mode to `observe`/`enforce` (`active.json` `jevMode`, CONTRACT C10): then
252
+ // EVERY gated call goes to FailproofAI Cloud on the Cloud Jev credential —
253
+ // `jev.json` is not used — because the checks Cloud deployed to this machine
254
+ // live there and this machine cannot know which apply. Cloud's `off` switches
255
+ // Jev off whatever `jev.json` says. The remaining conditions hold either way:
225
256
  // - `FAILPROOFAI_EVALUATOR` is not `legacy` (see "Turning Jev off" below);
226
- // - this is not the fail-closed `forceDecision` path and no session pause is
257
+ // - this is not the fail-closed `forceDecision` path, and no session pause is
227
258
  // active — a pause suspends local policy, and Jev must not become a way to
228
- // evaluate what the pause switched off;
259
+ // evaluate what the pause switched off. FailproofAI Cloud's checks are the
260
+ // exception, as Cloud JS assignments are: under a Cloud mode that asks, a
261
+ // paused session's calls still go to Cloud, with no installed pack's check
262
+ // in them;
229
263
  // - the event is a gate (`PreToolUse` / `PermissionRequest`) for a named tool
230
264
  // that the AGENT requested (see `isHumanAuthoredGate`).
231
265
  //
@@ -266,6 +300,16 @@ function jevForcedOff(opts: EvaluateHookEventOptions | undefined): boolean {
266
300
  return process.env.FAILPROOFAI_EVALUATOR === "legacy" || !!opts?.forceDecision;
267
301
  }
268
302
 
303
+ /** How a Cloud-mode review reaches FailproofAI Cloud (`semantic/cloud-jev.ts`). */
304
+ interface CloudJevRoute {
305
+ machineId: string;
306
+ deployment: number | null;
307
+ mode: "observe" | "enforce";
308
+ agent: AgentIdentity | null;
309
+ /** Cloud policies whose Jev assignment here is `observe`: their answers are recorded, never applied. */
310
+ observedPolicies: string[];
311
+ }
312
+
269
313
  /**
270
314
  * The BYOK config (with the build's default mode, D2), or null (Jev off). A
271
315
  * config that cannot be read is off, never a failure. Logged at info, like
@@ -283,8 +327,37 @@ function jevForcedOff(opts: EvaluateHookEventOptions | undefined): boolean {
283
327
  * unreadable directory above it and a path that is not a file are all null
284
328
  * there too.
285
329
  */
286
- async function readJevConfig(): Promise<{ config: JevConfig; defaultMode: TwoTierReview["mode"] } | null> {
330
+ async function readJevConfig(agent?: AgentIdentity | null): Promise<{
331
+ config: JevConfig;
332
+ defaultMode: TwoTierReview["mode"];
333
+ /** Set when FailproofAI Cloud's mode asks: the review goes to Cloud. */
334
+ cloud?: CloudJevRoute;
335
+ } | null> {
287
336
  try {
337
+ // FailproofAI Cloud's mode, when its deployment sets one, decides (CONTRACT
338
+ // C10.2): `off` is no Jev at all, whatever `jev.json` says; `observe` or
339
+ // `enforce` runs Jev on FailproofAI Cloud with the Cloud Jev credential —
340
+ // never `jev.json`'s provider — or not at all, when this machine cannot
341
+ // (`loadJevConfigForCloudMode` says why, for `errors.json`). Read off
342
+ // `active.json`, which is small and read on this path already.
343
+ const cloud = readCloudJevState();
344
+ if (cloud.jevMode === "off") return null;
345
+ if (cloudJevAsks(cloud.jevMode)) {
346
+ const { loadJevConfigForCloudMode, DEFAULT_JEV_MODE } = await import("./semantic/jev-config");
347
+ const resolved = loadJevConfigForCloudMode(cloud.jevMode);
348
+ if (!resolved.config || !resolved.machineId) return null;
349
+ return {
350
+ config: resolved.config,
351
+ defaultMode: DEFAULT_JEV_MODE,
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
+ },
359
+ };
360
+ }
288
361
  if (!existsSync(jevConfigFile())) return null;
289
362
  const { loadJevConfig, DEFAULT_JEV_MODE } = await import("./semantic/jev-config");
290
363
  const config = loadJevConfig();
@@ -300,6 +373,90 @@ async function readJevConfig(): Promise<{ config: JevConfig; defaultMode: TwoTie
300
373
  }
301
374
  }
302
375
 
376
+ /**
377
+ * Where a Jev decision's check came from, as activity-row fields. A FailproofAI
378
+ * Cloud check's outcome arrives with `origin.cloudPolicyId`/`cloudVersion`,
379
+ * carried pack-shaped through the review (`cloud:<id>` / `<version>`, see
380
+ * `semantic/cloud-jev.ts`), and is filed here under the Cloud attribution pair
381
+ * a Cloud JS policy's decision uses — so the Cloud policy page counts it
382
+ * against the policy that was deployed.
383
+ */
384
+ function jevAttribution(
385
+ origin: { packId: string; packVersion?: string } | undefined,
386
+ ): { packId?: string; packVersion?: string; cloudPolicyId?: string; cloudVersion?: number } {
387
+ if (!origin) return {};
388
+ if (origin.packId.startsWith("cloud:")) {
389
+ const version = Number(origin.packVersion);
390
+ return {
391
+ cloudPolicyId: origin.packId.slice("cloud:".length),
392
+ ...(Number.isSafeInteger(version) ? { cloudVersion: version } : {}),
393
+ };
394
+ }
395
+ return origin;
396
+ }
397
+
398
+ /**
399
+ * Compute and write `cloud-policies/errors.json` for this machine's Cloud
400
+ * deployment. A no-op off a Cloud-managed machine (`writeCloudPolicyErrors`
401
+ * skips without an `active.json`) and when nothing changed. Never throws.
402
+ */
403
+ async function recordCloudPolicyErrors(input: {
404
+ manifestError: string | null;
405
+ jsPolicies: CloudManagedPolicyArtifact[];
406
+ jsFailures: LoadAllResult["cloudFailures"] | undefined;
407
+ agentScopeUnresolved?: boolean;
408
+ }): Promise<void> {
409
+ try {
410
+ const jev = readCloudJevState();
411
+ const managed = input.manifestError !== null || input.jsPolicies.length > 0 || jev.jevMode !== null ||
412
+ jev.errors.length > 0 || input.agentScopeUnresolved === true;
413
+ // An unmanaged machine never reads further. A managed machine whose
414
+ // deployment just emptied still writes, so a stale report is cleared.
415
+ if (!managed && !existsSync(cloudPolicyErrorsPath())) return;
416
+ // Only while Cloud's mode ASKS (CONTRACT C10.2): why this machine cannot
417
+ // send its calls (`transcripts_disabled`, `jev_unconfigured…`), and the
418
+ // installed packs' checks Cloud dropped for the question budget under this
419
+ // deployment. The config answer is kept by its module until an input
420
+ // changes, so the second ask of a gate event reads no file.
421
+ let jevProblem: string | null = null;
422
+ let budgetDrops: CloudPolicyError[] = [];
423
+ let health: CloudPolicyError[] = [];
424
+ if (cloudJevAsks(jev.jevMode)) {
425
+ const { loadJevConfigForCloudMode } = await import("./semantic/jev-config");
426
+ const resolved = loadJevConfigForCloudMode(jev.jevMode);
427
+ jevProblem = resolved.problem;
428
+ if (resolved.machineId) {
429
+ // A drop of a pack uninstalled since is not reported (review m2);
430
+ // `installed.json` is read only when there is a record to filter.
431
+ budgetDrops = existsSync(jevBudgetPath())
432
+ ? readJevBudgetDrops({ machineId: resolved.machineId, deployment: jev.deployment, jevMode: jev.jevMode }, installedPackIds())
433
+ : [];
434
+ }
435
+ // Rate-limited or unavailable FailproofAI Cloud Jev, as this process has seen it (review M3).
436
+ const { cloudJevHealthErrors } = await import("./semantic/cloud-jev-health");
437
+ health = cloudJevHealthErrors();
438
+ }
439
+ writeCloudPolicyErrors(
440
+ collectCloudPolicyErrors({
441
+ manifestError: input.manifestError,
442
+ jsPolicies: input.jsPolicies,
443
+ jsFailures: input.jsFailures,
444
+ jev,
445
+ jevProblem,
446
+ budgetDrops,
447
+ health,
448
+ agentScopeUnresolved: input.agentScopeUnresolved,
449
+ }),
450
+ );
451
+ // After the report: what this hook load-checked (N4). A failure is in
452
+ // errors.json first, so the listing never sees a checked policy without
453
+ // its error.
454
+ if (input.jsPolicies.length > 0) recordCloudLoadChecked(input.jsPolicies[0].deployment, input.jsPolicies);
455
+ } catch (err) {
456
+ hookLogInfo(`could not record Cloud policy errors: ${err instanceof Error ? err.message : String(err)}`);
457
+ }
458
+ }
459
+
303
460
  /**
304
461
  * Start Jev's review of this call — before the regex policies run, so the two
305
462
  * proceed in parallel — or return null for the regex-only path.
@@ -311,15 +468,32 @@ async function startTwoTier(
311
468
  cli: IntegrationType,
312
469
  opts: EvaluateHookEventOptions | undefined,
313
470
  activePause: ActivePause | null,
471
+ agent: AgentIdentity | null,
314
472
  ): Promise<TwoTierReview | null> {
315
473
  if (!JEV_GATE_EVENTS.has(canonicalEventType)) return null;
316
474
  if (isHumanAuthoredGate(session.rawHookEventName, cli)) return null;
317
475
  if (typeof parsed.tool_name !== "string" || parsed.tool_name.length === 0) return null;
318
- if (jevForcedOff(opts) || activePause) return null;
319
- // Read off the reviewer set registration just cached, so it is free.
320
- if (!jevChecksInstalled()) return null;
321
- const loaded = await readJevConfig();
476
+ if (jevForcedOff(opts)) return null;
477
+ // Read off the reviewer set registration just cached, so it is free. Under a
478
+ // Cloud mode that asks, every gated call goes to Cloud whether or not an
479
+ // installed pack declares a check: the Cloud checks are Cloud's to select.
480
+ const cloudAsks = cloudJevAsks(readCloudJevMode());
481
+ // A session pause suspends LOCAL policy only: installed packs' Jev checks
482
+ // with it, FailproofAI Cloud's Jev checks never — the same exemption Cloud
483
+ // JS assignments get below, for the same reason (a locally-issued command
484
+ // must not switch off what the org assigned centrally).
485
+ if (activePause && !cloudAsks) return null;
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;
492
+ const loaded = await readJevConfig(agent);
322
493
  if (!loaded) return null;
494
+ // Paused, and the config is not Cloud's after all (read in between): the
495
+ // local path has nothing left to ask.
496
+ if (activePause && !loaded.cloud) return null;
323
497
  const cfg = loaded.config;
324
498
  try {
325
499
  const { startJevReview } = await import("./semantic/jev-review");
@@ -331,6 +505,9 @@ async function startTwoTier(
331
505
  permissionMode: session.permissionMode,
332
506
  sessionId: session.sessionId,
333
507
  cli,
508
+ ...(loaded.cloud ? { cloud: loaded.cloud } : {}),
509
+ // Cloud's checks are still asked; the installed packs' are not.
510
+ ...(activePause ? { localPaused: true } : {}),
334
511
  });
335
512
  } catch (err) {
336
513
  // Configured but unable to start: that is a fallback, and it is recorded
@@ -405,7 +582,9 @@ async function captureJevIntent(
405
582
  ): Promise<void> {
406
583
  if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts)) return;
407
584
  try {
408
- if (!jevChecksInstalled()) return;
585
+ // Same gate as `startTwoTier`: a Cloud mode that asks needs what the human
586
+ // typed whatever the installed packs declare.
587
+ if (!cloudJevAsks(readCloudJevMode()) && !jevChecksInstalled()) return;
409
588
  if (!(await readJevConfig())) return;
410
589
  if (decision === "deny") {
411
590
  const { ENFORCEMENT_CAPABILITY } = await import("./enforcement-capability");
@@ -464,6 +643,12 @@ export async function evaluateHookEvent(
464
643
  }
465
644
  }
466
645
 
646
+ // The daemon health probe: proof the worker answers, not an agent event.
647
+ // Answered before anything records scope errors or activity (B3).
648
+ if (isHealthProbe(eventType, parsed)) {
649
+ return { exitCode: 0, stdout: "", stderr: "" };
650
+ }
651
+
467
652
  normalizeCliPayload(cli, parsed);
468
653
 
469
654
  // Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
@@ -508,6 +693,9 @@ export async function evaluateHookEvent(
508
693
  rawHookEventName: eventType,
509
694
  cli,
510
695
  };
696
+ const runtimeAgent = readRuntimeAgentIdentity(
697
+ cli, opts?.agentSettingsPath ?? runtimeAgentSettingsPath(cli, session.cwd),
698
+ );
511
699
 
512
700
  let config: HooksConfig;
513
701
  let customHooksList: CustomHook[] = [];
@@ -554,7 +742,7 @@ export async function evaluateHookEvent(
554
742
  } else {
555
743
  // Load enabled policies (merge across project/local/global scopes)
556
744
  config = readMergedHooksConfig(session.cwd);
557
- clearPolicies(cli);
745
+ clearPolicies(cli, runtimeAgent);
558
746
 
559
747
  // A session pause suspends LOCAL policy only, for a bounded time. Cloud
560
748
  // assignments are exempt below for the same reason `disabledCustomPolicies`
@@ -616,10 +804,13 @@ export async function evaluateHookEvent(
616
804
  // than the status quo, which already failed open on five of the twelve —
617
805
  // just accidentally, and without saying so.
618
806
  let cloudManagedPolicies: CloudManagedPolicyArtifact[] = [];
807
+ /** Why the Cloud JS half did not load at all, for `errors.json`. */
808
+ let cloudManifestError: string | null = null;
619
809
  try {
620
- cloudManagedPolicies = readActiveCloudManagedPolicies();
810
+ cloudManagedPolicies = readActiveCloudManagedPolicies(runtimeAgent);
621
811
  } catch (err) {
622
812
  const msg = err instanceof Error ? err.message : String(err);
813
+ cloudManifestError = msg;
623
814
  hookLogWarn(
624
815
  `[failproofai] cloud-managed policies could NOT be loaded and are not being enforced: ${msg}. ` +
625
816
  `Local policies are unaffected. Run \`failproofai config --status\` to check this machine's enrolment.`,
@@ -633,7 +824,9 @@ export async function evaluateHookEvent(
633
824
  // policy decided this event: "what was deployed here at the time" is a
634
825
  // separate question from "what decided", and only the former can tell a
635
826
  // rollout that changed nothing from one that never reached the machine.
636
- cloudDeployment = cloudManagedPolicies[0]?.deployment;
827
+ // A machine whose deployment carries only a Jev mode has no JS policy to
828
+ // read it off.
829
+ cloudDeployment = cloudManagedPolicies[0]?.deployment ?? readCloudJevState().deployment ?? undefined;
637
830
  // Installed packs. `readInstalledPacks` never throws: a bad manifest or a
638
831
  // tampered artifact yields zero packs and a recorded reason, which is
639
832
  // sound ONLY because the builtins still ship compiled in and keep
@@ -661,11 +854,22 @@ export async function evaluateHookEvent(
661
854
  ...installedPacks.map((pack) => pack.path),
662
855
  ];
663
856
 
857
+ // A FailproofAI Cloud policy's `reviewedBy` names ITS OWN Cloud Jev
858
+ // checks, which run on FailproofAI Cloud (CONTRACT C10.5). Each is
859
+ // translated here to `cloud:<id>/<name>` — a reviewer only that policy's
860
+ // own Cloud outcomes can satisfy, never an installed pack's check of the
861
+ // same name (C9.4) — and only while Cloud's Jev mode asks. An `observe`
862
+ // assignment, or no mode that asks, is hard, without a warning: that is
863
+ // the org's choice, not a broken policy. Before the loader, so two
864
+ // assignments sharing one artifact merge names that still say whose.
865
+ const cloudJevMode = readCloudJevMode();
866
+ const cloudForLoading = cloudManagedPolicies.map((policy) => withCloudAuthority(policy, cloudJevMode));
867
+
664
868
  // Load and register custom hooks (layer 2, after builtins)
665
869
  const loadResult = await loadAllCustomHooks(allExplicitPaths, {
666
870
  sessionCwd: session.cwd,
667
871
  customPoliciesEnabled: config.customPoliciesEnabled,
668
- ...(cloudManagedPolicies.length > 0 ? { cloudManagedPolicies } : {}),
872
+ ...(cloudForLoading.length > 0 ? { cloudManagedPolicies: cloudForLoading } : {}),
669
873
  ...(installedPacks.length > 0 ? { packs: installedPacks } : {}),
670
874
  });
671
875
  customHooksList = loadResult.hooks;
@@ -812,6 +1016,7 @@ export async function evaluateHookEvent(
812
1016
  // for the user's own files; anything unclear registers as hard. Said
813
1017
  // aloud when a `reviewable` claim is refused and Jev is configured, so
814
1018
  // an author is not left wondering why Jev never clears it.
1019
+ // A Cloud record's authority was translated before loading (above).
815
1020
  const authority = authorityDeclarationFor(hook, { cloudManaged, pack });
816
1021
  // Against the same set `registerPolicy` judges it by, or this warning
817
1022
  // describes a different machine than the registry does: a pack that
@@ -835,6 +1040,17 @@ export async function evaluateHookEvent(
835
1040
  );
836
1041
  }
837
1042
 
1043
+ // What this machine could not load from its FailproofAI Cloud deployment,
1044
+ // for the daemon to report on its next poll. Only on a managed machine,
1045
+ // written only when it changes, and never able to cost this event its
1046
+ // answer (see `cloud-policy-errors.ts`).
1047
+ await recordCloudPolicyErrors({
1048
+ manifestError: cloudManifestError,
1049
+ jsPolicies: cloudManagedPolicies,
1050
+ jsFailures: loadResult.cloudFailures,
1051
+ agentScopeUnresolved: runtimeAgent === null && readCloudAgentScopeRequired(),
1052
+ });
1053
+
838
1054
  // Fail closed on enforcement this machine was told it had and does not.
839
1055
  //
840
1056
  // Additive, and deliberately NOT a clearPolicies() the way the
@@ -924,7 +1140,7 @@ export async function evaluateHookEvent(
924
1140
  // starts HERE, before any regex policy runs, and evaluatePolicies combines
925
1141
  // the two (see semantic/combine.ts). Otherwise this is null and the call
926
1142
  // below is exactly the regex-only evaluation it always was.
927
- const twoTier = await startTwoTier(canonicalEventType, parsed, session, cli, opts, activePause);
1143
+ const twoTier = await startTwoTier(canonicalEventType, parsed, session, cli, opts, activePause, runtimeAgent);
928
1144
  // On the two-tier path the registry is read for the activity row BEFORE
929
1145
  // evaluating, because evaluatePolicies may hand the registry back to the
930
1146
  // warm worker's queue (`releaseRegistry`) while it waits on Jev, and the
@@ -956,6 +1172,11 @@ export async function evaluateHookEvent(
956
1172
  reason: observeVerdict.reason,
957
1173
  });
958
1174
  }
1175
+ // An `observe` Cloud Jev assignment's checks: filed under their Cloud
1176
+ // policy, like an observe Cloud JS policy's verdict.
1177
+ for (const o of result.twoTier?.observedCloud ?? []) {
1178
+ observedResults.push({ policyId: o.cloudPolicyId, version: o.cloudVersion, decision: o.decision, reason: o.reason });
1179
+ }
959
1180
  const durationMs = Math.round(performance.now() - startTime);
960
1181
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
961
1182
 
@@ -1001,7 +1222,7 @@ export async function evaluateHookEvent(
1001
1222
  // would be false — so it is attributed to Jev itself. Leaving it out
1002
1223
  // instead filed every Jev block under "unattributed" on FailproofAI
1003
1224
  // Cloud's policy page, beside rows written before attribution existed.
1004
- ...(result.policyName && result.twoTier?.decidedByJev ? { policySource: "jev" as const, ...jevOrigin } : {}),
1225
+ ...(result.policyName && result.twoTier?.decidedByJev ? { policySource: "jev" as const, ...jevAttribution(jevOrigin) } : {}),
1005
1226
  ...(result.policyName && !result.twoTier?.decidedByJev
1006
1227
  ? (() => {
1007
1228
  const attribution = policyAttribution.get(result.policyName);
@@ -1016,6 +1237,14 @@ export async function evaluateHookEvent(
1016
1237
  };
1017
1238
  })()
1018
1239
  : {}),
1240
+ // Observe mode, nothing else decided: the Jev check that would have is
1241
+ // named on the row, with its Cloud policy (B12). Before, the row read
1242
+ // policyName/cloudPolicyId null, so "would have" never reached a policy.
1243
+ // No `policySource`: nothing decided this call, and the policy page
1244
+ // counts a source as a decision; `observed` carries the "would have".
1245
+ ...(!result.policyName && observeVerdict
1246
+ ? { policyName: observeVerdict.policyName, ...jevAttribution(result.twoTier?.jevOrigin) }
1247
+ : {}),
1019
1248
  ...(cloudDeployment !== undefined ? { cloudDeployment } : {}),
1020
1249
  // The point of observe mode is this record. Without it the rollout is
1021
1250
  // unmeasurable and the row is indistinguishable from one where the policy
@@ -1116,7 +1345,11 @@ export async function evaluateHookEvent(
1116
1345
  * kept unchanged so nothing about the one-shot (non-daemon) path regresses.
1117
1346
  * Internally now just a thin wrapper around `evaluateHookEvent`.
1118
1347
  */
1119
- export async function handleHookEvent(eventType: string, cli: IntegrationType = "claude"): Promise<number> {
1348
+ export async function handleHookEvent(
1349
+ eventType: string,
1350
+ cli: IntegrationType = "claude",
1351
+ agentSettingsPath?: string,
1352
+ ): Promise<number> {
1120
1353
  const MAX_STDIN_BYTES = 1_048_576; // 1 MB
1121
1354
  const stdinRead = await readStdinPayload(MAX_STDIN_BYTES);
1122
1355
  if (stdinRead.readError) {
@@ -1148,7 +1381,10 @@ export async function handleHookEvent(eventType: string, cli: IntegrationType =
1148
1381
  });
1149
1382
  }
1150
1383
 
1151
- const result = await evaluateHookEvent(eventType, cli, stdinRead.payload);
1384
+ const result = await evaluateHookEvent(
1385
+ eventType, cli, stdinRead.payload,
1386
+ agentSettingsPath === undefined ? undefined : { agentSettingsPath },
1387
+ );
1152
1388
 
1153
1389
  // Say it out loud, once a session, when the collector is holding batches the
1154
1390
  // server definitively refused.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The daemon health probe's marker (`probeDaemon` in `daemon-service.ts`).
3
+ *
4
+ * The probe is a SessionStart sent through the daemon to the warm worker, so a
5
+ * reply proves the whole path answers. It is not an agent's session: the
6
+ * handler answers it before any scope, policy or activity logic, or every
7
+ * probe recorded `agent_scope_unresolved` in `errors.json` (it carries no cwd
8
+ * and no `--agent-scope`) and wrote a fake SessionStart activity row.
9
+ */
10
+ export const HEALTH_PROBE_SOURCE = "failproofai-health-probe";
11
+
12
+ /** Whether a parsed hook payload is the daemon health probe. */
13
+ export function isHealthProbe(eventType: string, parsed: Record<string, unknown>): boolean {
14
+ return eventType === "SessionStart" && parsed.source === HEALTH_PROBE_SOURCE && parsed.session_id === undefined;
15
+ }
@@ -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 {
@@ -154,6 +154,12 @@ function isMarkedHook(hook: unknown): boolean {
154
154
  return cmd.includes("failproofai") && cmd.includes("--hook");
155
155
  }
156
156
 
157
+ /** A hook names its installing settings scope, avoiding a cwd-based guess
158
+ * when user and project/local hooks both exist for the same integration. */
159
+ function agentScopeSuffix(scope?: HookScope): string {
160
+ return ` --agent-scope ${scope ?? "user"}`;
161
+ }
162
+
157
163
  function stripLegacyVersion(settings: Record<string, unknown>): boolean {
158
164
  if ("version" in settings) {
159
165
  delete settings.version;
@@ -247,6 +253,21 @@ export function settingsPathsFor(
247
253
  return paths && paths.length > 0 ? paths : [integration.getSettingsPath(scope, cwd)];
248
254
  }
249
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
+
250
271
  // ── Claude Code integration ─────────────────────────────────────────────────
251
272
 
252
273
  export const claudeCode: Integration = {
@@ -259,7 +280,7 @@ export const claudeCode: Integration = {
259
280
  const base = cwd ? resolve(cwd) : process.cwd();
260
281
  switch (scope) {
261
282
  case "user":
262
- return resolve(homedir(), ".claude", "settings.json");
283
+ return resolve(claudeConfigDir(), "settings.json");
263
284
  case "project":
264
285
  return resolve(base, ".claude", "settings.json");
265
286
  case "local":
@@ -285,7 +306,7 @@ export const claudeCode: Integration = {
285
306
  : `"${binaryPath}" --hook ${eventType}`;
286
307
  return {
287
308
  type: "command",
288
- command,
309
+ command: command + agentScopeSuffix(scope),
289
310
  // Claude reads `timeout` in SECONDS per https://code.claude.com/docs/en/hooks
290
311
  // ("Seconds before canceling. Defaults: 600 for command ...; 60 for agent"),
291
312
  // NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h. (#482-class unit fix)
@@ -433,7 +454,7 @@ export const codex: Integration = {
433
454
  const base = cwd ? resolve(cwd) : process.cwd();
434
455
  switch (scope) {
435
456
  case "user":
436
- return resolve(homedir(), ".codex", "hooks.json");
457
+ return resolve(codexHome(), "hooks.json");
437
458
  case "project":
438
459
  return resolve(base, ".codex", "hooks.json");
439
460
  case "local":
@@ -464,7 +485,7 @@ export const codex: Integration = {
464
485
  // Codex reads `timeout` in SECONDS (the field is literally `timeout`,
465
486
  // default 600 per https://developers.openai.com/codex/hooks) — same unit as
466
487
  // Claude/Cursor/Copilot. 60 = 60s.
467
- command,
488
+ command: command + agentScopeSuffix(scope),
468
489
  timeout: 60,
469
490
  [FAILPROOFAI_HOOK_MARKER]: true,
470
491
  };
@@ -644,8 +665,8 @@ export const copilot: Integration = {
644
665
  : `"${binaryPath}" --hook ${eventType} --cli copilot`;
645
666
  return {
646
667
  type: "command",
647
- bash: cmd,
648
- powershell: cmd,
668
+ bash: cmd + agentScopeSuffix(scope),
669
+ powershell: cmd + agentScopeSuffix(scope),
649
670
  timeoutSec: 60,
650
671
  [FAILPROOFAI_HOOK_MARKER]: true,
651
672
  };
@@ -788,7 +809,7 @@ export const cursor: Integration = {
788
809
  // use 30 and 10), NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h.
789
810
  return {
790
811
  type: "command",
791
- command,
812
+ command: command + agentScopeSuffix(scope),
792
813
  timeout: 60,
793
814
  [FAILPROOFAI_HOOK_MARKER]: true,
794
815
  };
@@ -981,12 +1002,13 @@ function canonicalizeToolInput(canonicalToolName, args) {
981
1002
 
982
1003
  const FAILPROOFAI_BIN = ${escapedBin};
983
1004
  const USE_NPX = ${useNpx};
1005
+ const AGENT_SCOPE = ${JSON.stringify(scope)};
984
1006
 
985
1007
  function runFailproofai(eventName, payload, directory) {
986
1008
  const cmd = USE_NPX ? "npx" : FAILPROOFAI_BIN;
987
1009
  const args = USE_NPX
988
- ? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode"]
989
- : ["--hook", eventName, "--cli", "opencode"];
1010
+ ? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode", "--agent-scope", AGENT_SCOPE]
1011
+ : ["--hook", eventName, "--cli", "opencode", "--agent-scope", AGENT_SCOPE];
990
1012
  const r = spawnSync(cmd, args, {
991
1013
  input: JSON.stringify(payload),
992
1014
  encoding: "utf8",
@@ -2460,7 +2482,7 @@ export const factory: Integration = {
2460
2482
  : `"${binaryPath}" --hook ${eventType} --cli factory`;
2461
2483
  return {
2462
2484
  type: "command",
2463
- command,
2485
+ command: command + agentScopeSuffix(scope),
2464
2486
  // droid reads `timeout` in SECONDS (verified against droid v0.171.0). 30s.
2465
2487
  timeout: 30,
2466
2488
  [FAILPROOFAI_HOOK_MARKER]: true,
@@ -2593,7 +2615,7 @@ export const devin: Integration = {
2593
2615
  : `"${binaryPath}" --hook ${eventType} --cli devin`;
2594
2616
  return {
2595
2617
  type: "command",
2596
- command,
2618
+ command: command + agentScopeSuffix(scope),
2597
2619
  // Devin reads `timeout` in SECONDS like Claude. 60 = 60s.
2598
2620
  timeout: 60,
2599
2621
  [FAILPROOFAI_HOOK_MARKER]: true,
@@ -2738,7 +2760,7 @@ export const antigravity: Integration = {
2738
2760
  : `"${binaryPath}" --hook ${eventType} --cli antigravity`;
2739
2761
  return {
2740
2762
  type: "command",
2741
- command,
2763
+ command: command + agentScopeSuffix(scope),
2742
2764
  // Antigravity reads `timeout` in SECONDS (verified agy v1.1.2). 30s.
2743
2765
  timeout: 30,
2744
2766
  [FAILPROOFAI_HOOK_MARKER]: true,
@@ -2926,7 +2948,7 @@ export const goose: Integration = {
2926
2948
  : `"${binaryPath}" --hook ${eventType} --cli goose`;
2927
2949
  // Open Plugins command entry: { type, command } only (Goose applies its own
2928
2950
  // timeout; no marker field — see isGooseFailproofaiHook).
2929
- return { type: "command", command };
2951
+ return { type: "command", command: command + agentScopeSuffix(scope) };
2930
2952
  },
2931
2953
 
2932
2954
  isFailproofaiHook: isGooseFailproofaiHook,