failproofai 1.0.7-beta.2 → 1.0.8-beta.0

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 (225) 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 +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  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 +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  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 +5 -5
  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 +4 -4
  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 +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  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 +6 -7
  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 +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +18 -7
  58. package/.next/standalone/.next/server/app/settings/page.js +9 -7
  59. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__20kzpkf._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  65. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +9 -9
  69. package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  97. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +12 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  113. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  114. package/.next/standalone/.next/server/pages/404.html +1 -1
  115. package/.next/standalone/.next/server/pages/500.html +1 -1
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +33 -22
  118. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
  119. package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
  121. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
  124. package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
  129. package/.next/standalone/SECURITY.md +53 -0
  130. package/.next/standalone/app/actions/get-jev-config.ts +194 -14
  131. package/.next/standalone/app/actions/update-jev-config.ts +160 -11
  132. package/.next/standalone/app/policies/hooks-client.tsx +5 -3
  133. package/.next/standalone/app/settings/jev-panel.tsx +169 -12
  134. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  135. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  136. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  137. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  138. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  139. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  141. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  143. package/.next/standalone/package.json +9 -9
  144. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  145. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  146. package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
  147. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
  148. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
  150. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
  151. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
  152. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
  153. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
  154. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
  156. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
  157. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  158. package/.next/standalone/server.js +1 -1
  159. package/README.md +1 -0
  160. package/bin/failproofai.mjs +37 -6
  161. package/dist/cli.mjs +2861 -1145
  162. package/dist/worker.mjs +815 -197
  163. package/package.json +9 -9
  164. package/src/hooks/builtin-policies.ts +24 -6
  165. package/src/hooks/cloud-connection.ts +190 -1
  166. package/src/hooks/cloud-enrollment-cli.ts +56 -7
  167. package/src/hooks/cloud-introspect.ts +6 -0
  168. package/src/hooks/configure-wizard.ts +26 -6
  169. package/src/hooks/custom-hooks-loader.ts +58 -8
  170. package/src/hooks/effective-reviewers.ts +172 -8
  171. package/src/hooks/flush-cli.ts +35 -8
  172. package/src/hooks/fp-config.ts +266 -1
  173. package/src/hooks/fp-reset.ts +22 -4
  174. package/src/hooks/handler.ts +44 -7
  175. package/src/hooks/hook-activity-store.ts +13 -5
  176. package/src/hooks/jev-cli.ts +1009 -86
  177. package/src/hooks/jev-cloud-connection.ts +392 -0
  178. package/src/hooks/manager.ts +44 -6
  179. package/src/hooks/pack-cli.ts +175 -37
  180. package/src/hooks/pack-failclosed.ts +3 -0
  181. package/src/hooks/pack-manifest.ts +11 -1
  182. package/src/hooks/pack-store.ts +47 -10
  183. package/src/hooks/policy-authority.ts +73 -20
  184. package/src/hooks/policy-catalog.ts +10 -8
  185. package/src/hooks/policy-evaluator.ts +31 -8
  186. package/src/hooks/policy-registry.ts +3 -2
  187. package/src/hooks/policy-reviewability.ts +41 -12
  188. package/src/hooks/policy-types.ts +5 -3
  189. package/src/hooks/semantic/combine.ts +124 -84
  190. package/src/hooks/semantic/decide.ts +16 -0
  191. package/src/hooks/semantic/envelope.ts +18 -59
  192. package/src/hooks/semantic/evaluator.ts +11 -2
  193. package/src/hooks/semantic/facts.ts +7 -1
  194. package/src/hooks/semantic/intent.ts +8 -8
  195. package/src/hooks/semantic/jev-client.ts +540 -28
  196. package/src/hooks/semantic/jev-config.ts +586 -38
  197. package/src/hooks/semantic/jev-review.ts +26 -11
  198. package/src/hooks/semantic/jev-throttle.ts +8 -2
  199. package/src/hooks/semantic/pack-policies.ts +58 -15
  200. package/src/hooks/semantic/policies.ts +4 -3
  201. package/src/hooks/semantic/redact.ts +28 -1
  202. package/src/hooks/semantic/session-root.ts +116 -0
  203. package/src/hooks/semantic/types.ts +11 -0
  204. package/src/hooks/tui.ts +4 -0
  205. package/src/hooks/worker-server.ts +2 -1
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
  208. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  209. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  211. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  212. package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  214. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  215. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  216. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  219. package/.next/standalone/.next/static/chunks/1qd741hzlmjbo.js +0 -1
  220. package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
  221. package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
  222. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
  223. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
  224. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
  225. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
@@ -22,7 +22,19 @@
22
22
  * someone opens when something is wrong. That guidance now lives in
23
23
  * `failproofai config --status` and the docs, which is a worse place for it.
24
24
  */
25
- import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
25
+ import {
26
+ chmodSync,
27
+ closeSync,
28
+ constants as fsConstants,
29
+ existsSync,
30
+ fstatSync,
31
+ mkdirSync,
32
+ openSync,
33
+ readFileSync,
34
+ readSync,
35
+ statSync,
36
+ writeFileSync,
37
+ } from "node:fs";
26
38
  import { dirname } from "node:path";
27
39
  import { version as cliVersion } from "../../package.json";
28
40
  import {
@@ -753,6 +765,26 @@ export interface FpCredentials {
753
765
  org?: { id?: string; slug?: string; name?: string };
754
766
  ingest?: { url: string; key: string };
755
767
  auth?: { baseUrl?: string; sessionToken?: string; expiresAt?: number; email?: string };
768
+ /**
769
+ * The key Jev's FailproofAI Cloud route sends, and the Cloud ORIGIN it was
770
+ * verified against. Written by connect only when introspect reports
771
+ * `jev:evaluate`; cleared by disconnect. See {@link readJevCloudCredential}.
772
+ *
773
+ * A slot of its own, not a reuse of `[cloud]` or `[ingest]`, although it holds
774
+ * the same key today: those two are written under their own permission checks
775
+ * and can hold DIFFERENT keys (see `cloud-connection.ts`), and "which of the
776
+ * two backs Jev" is a question nobody reading the file should have to answer.
777
+ * One slot per capability keeps revoking one from ever silently re-routing
778
+ * another.
779
+ */
780
+ jev?: JevCloudCredential;
781
+ }
782
+
783
+ /** The `jev` slot of `credentials.json`. */
784
+ export interface JevCloudCredential {
785
+ /** The Cloud origin the key was verified against, e.g. `https://app.befailproof.ai`. */
786
+ url: string;
787
+ key: string;
756
788
  }
757
789
 
758
790
  /**
@@ -775,6 +807,8 @@ const OWNED_CREDENTIAL_KEYS: readonly (readonly string[])[] = [
775
807
  ["org", "name"],
776
808
  ["ingest", "url"],
777
809
  ["ingest", "key"],
810
+ ["jev", "url"],
811
+ ["jev", "key"],
778
812
  ["auth", "base_url"],
779
813
  ["auth", "session_token"],
780
814
  ["auth", "expires_at"],
@@ -843,6 +877,8 @@ export function projectCredentials(parsed: Record<string, unknown>): FpCredentia
843
877
  if (ingest && typeof ingest.url === "string" && typeof ingest.key === "string" && ingest.url && ingest.key) {
844
878
  out.ingest = { url: ingest.url, key: ingest.key };
845
879
  }
880
+ const jev = projectJevSlot(parsed.jev);
881
+ if (jev) out.jev = jev;
846
882
  if (auth) {
847
883
  out.auth = {
848
884
  baseUrl: typeof auth.base_url === "string" ? auth.base_url : undefined,
@@ -876,6 +912,9 @@ export function writeCredentials(creds: FpCredentials, raw?: Record<string, unkn
876
912
  if (creds.ingest) {
877
913
  out.ingest = { url: creds.ingest.url, key: creds.ingest.key };
878
914
  }
915
+ if (creds.jev) {
916
+ out.jev = { url: creds.jev.url, key: creds.jev.key };
917
+ }
879
918
  if (creds.org && (creds.org.id || creds.org.slug || creds.org.name)) {
880
919
  out.org = {
881
920
  ...(creds.org.id ? { id: creds.org.id } : {}),
@@ -920,6 +959,232 @@ export function hasCloudCredentials(): boolean {
920
959
  return Boolean(c.cloud || c.ingest);
921
960
  }
922
961
 
962
+ // ── The Jev slot ─────────────────────────────────────────────────────────────
963
+ //
964
+ // The one credential in this file that a HOOK reads. Everything else here is
965
+ // read by the daemon or a CLI command; this is read by `inspectJevConfig` on
966
+ // every gated tool call of a machine whose `jev.json` names the FailproofAI
967
+ // Cloud provider — and the key it yields is what lets Jev's answers clear a
968
+ // deny. So it is read the way `jev.json` itself is (`semantic/jev-config.ts`),
969
+ // not the way `readCredentials` reads: a file or directory other users could
970
+ // have written is REFUSED rather than read, because a routing credential for a
971
+ // tier that can clear denies must not be one somebody else chose.
972
+ //
973
+ // And only from `credentialsFile()`, never from `FAILPROOFAI_CLOUD_CREDENTIALS`.
974
+ // That override relocates the policy credential for the daemon and CI; honoured
975
+ // here it would be an environment variable choosing the key Jev spends, which a
976
+ // repository's `.claude/settings.json` can set for a session. `FAILPROOFAI_HOME`
977
+ // is not an exception to that rule for the reason `jev-config.ts` gives: it
978
+ // moves the whole layout, `jev.json` included.
979
+ //
980
+ // # A slot counts only while the connection it came with is still there
981
+ //
982
+ // The slot is written by `config --token` and cleared by `config --disconnect`
983
+ // — by THIS build. An older one (1.0.7-beta.2 and before) knows nothing about
984
+ // it: its disconnect clears `[cloud]` and `[ingest]` and carries the `jev`
985
+ // table over as a key it does not own. So downgrade, disconnect, upgrade, and
986
+ // a machine its owner disconnected would find a Cloud key and a Cloud
987
+ // `jev.json` both still in place, and Jev would start spending on an org the
988
+ // machine had left. So the slot is read as usable only when a FailproofAI Cloud
989
+ // connection on the SAME origin — a policy (`[cloud]`) or reporting
990
+ // (`[ingest]`) credential — is in the same file. A key carrying `jev:evaluate`
991
+ // always carries `events:add` and `policies:pull` too (the server refuses it
992
+ // otherwise), so a live Jev connection always has one of them.
993
+ //
994
+ // And that connection must hold the SAME KEY. The origin alone does not say
995
+ // whose connection it is: on hosted FailproofAI Cloud every organisation shares
996
+ // one origin. An older build's `config --token` with another org's key
997
+ // rewrites `[cloud]` and `[ingest]` to that key and carries the `jev` table
998
+ // over untouched, so the origin still matches while the machine now reports
999
+ // into the other org — and Jev would go on spending the first org's budget.
1000
+ // This build's connect writes one key to all three (a partial connect may
1001
+ // leave one of `[cloud]`/`[ingest]` holding an earlier key, never both), so a
1002
+ // live slot always has a same-origin credential holding its key.
1003
+ //
1004
+ // Only from this file, for the reason above: under `FAILPROOFAI_CLOUD_CREDENTIALS`
1005
+ // the policy credential lives elsewhere, and an environment variable must not
1006
+ // be able to re-arm a slot either. The reporting credential is always here,
1007
+ // and connecting a Jev key writes it.
1008
+
1009
+ /** Same bound `jev.json` gets; no legitimate `credentials.json` is near it. */
1010
+ const MAX_CREDENTIALS_BYTES = 64 * 1024;
1011
+ /**
1012
+ * Non-blocking, so a FIFO in the file's place cannot hang a hook in `open()`.
1013
+ * A function, not a module-level constant: this module is imported everywhere,
1014
+ * and reading `fs.constants` at import time would make every importer depend on
1015
+ * it — including test doubles of `node:fs` that never needed it before.
1016
+ */
1017
+ const credentialsOpenFlags = (): number => fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0);
1018
+ /** Group- or world-WRITE bits on the directory: whoever has them can replace the file. */
1019
+ const DIR_WRITABLE_BY_OTHERS = 0o022;
1020
+
1021
+ export type JevCloudCredentialRead =
1022
+ | { status: "ok"; path: string; credential: JevCloudCredential }
1023
+ /**
1024
+ * No usable Jev credential: no file, no `jev` slot, or a slot no live
1025
+ * connection backs (see "A slot counts only while…" above).
1026
+ */
1027
+ | {
1028
+ status: "absent";
1029
+ path: string;
1030
+ /**
1031
+ * This machine IS connected to FailproofAI Cloud — a policy or reporting
1032
+ * credential is stored — just not with a Jev key. Tells "connected, but
1033
+ * the key does not carry Jev" apart from "not connected at all".
1034
+ */
1035
+ connected: boolean;
1036
+ /** A `jev` slot is there, but no connection on its origin holding its key is: it is ignored. */
1037
+ orphaned?: true;
1038
+ }
1039
+ | {
1040
+ status: "refused";
1041
+ path: string;
1042
+ /** The file's permission bits, when they are the reason. */
1043
+ mode: number | null;
1044
+ reason: "too-open" | "unreadable" | "too-large" | "not-json";
1045
+ problem: string;
1046
+ /** The command that fixes it, when one does. */
1047
+ fix?: string;
1048
+ };
1049
+
1050
+ /** A `jev` table with two non-empty strings, or null. Shape only; `jev-config.ts` validates the values. */
1051
+ function projectJevSlot(raw: unknown): JevCloudCredential | null {
1052
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
1053
+ const { url, key } = raw as Record<string, unknown>;
1054
+ return typeof url === "string" && url && typeof key === "string" && key ? { url, key } : null;
1055
+ }
1056
+
1057
+ /**
1058
+ * The `jev` slot, read owner-only.
1059
+ *
1060
+ * The file is opened once and every check runs against that descriptor, so the
1061
+ * permissions checked are the permissions of the bytes read — the same shape
1062
+ * as `inspectJevConfig`, for the same reason. Never throws.
1063
+ */
1064
+ export function readJevCloudCredential(): JevCloudCredentialRead {
1065
+ const path = credentialsFile();
1066
+ let fd: number;
1067
+ try {
1068
+ fd = openSync(path, credentialsOpenFlags());
1069
+ } catch (err) {
1070
+ const code = (err as NodeJS.ErrnoException).code;
1071
+ if (code === "ENOENT" || code === "ENOTDIR") return { status: "absent", path, connected: false };
1072
+ return { status: "refused", path, mode: null, reason: "unreadable", problem: `cannot open ${path} (${code ?? "error"})` };
1073
+ }
1074
+ let text: string;
1075
+ try {
1076
+ const st = fstatSync(fd);
1077
+ if (!st.isFile()) return { status: "refused", path, mode: null, reason: "unreadable", problem: `${path} is not a regular file` };
1078
+ // On Windows `mode` is synthesized and would refuse every file while
1079
+ // protecting nothing; Jev setup is refused there anyway.
1080
+ const meaningful = process.platform !== "win32";
1081
+ const mode = st.mode & 0o777;
1082
+ if (meaningful && (mode & 0o077) !== 0) {
1083
+ return {
1084
+ status: "refused",
1085
+ path,
1086
+ mode,
1087
+ reason: "too-open",
1088
+ problem:
1089
+ `${path} is ${mode.toString(8).padStart(4, "0")}; it holds the key Jev sends to FailproofAI Cloud, ` +
1090
+ `so it must be owner-only (chmod 600 ${path})`,
1091
+ fix: `chmod 600 ${path}`,
1092
+ };
1093
+ }
1094
+ if (meaningful) {
1095
+ let dirMode: number | null = null;
1096
+ try {
1097
+ const m = statSync(dirname(path)).mode & 0o777;
1098
+ if ((m & DIR_WRITABLE_BY_OTHERS) !== 0) dirMode = m;
1099
+ } catch {
1100
+ // No permission to stat it: the read below reports what it can.
1101
+ }
1102
+ if (dirMode !== null) {
1103
+ return {
1104
+ status: "refused",
1105
+ path,
1106
+ mode,
1107
+ reason: "too-open",
1108
+ problem:
1109
+ `its directory ${dirname(path)} is ${dirMode.toString(8).padStart(4, "0")} — other users can write there, ` +
1110
+ `so they can put their own credentials in its place; it must be owner-only (chmod 700 ${dirname(path)})`,
1111
+ fix: `chmod 700 ${dirname(path)}`,
1112
+ };
1113
+ }
1114
+ }
1115
+ if (st.size > MAX_CREDENTIALS_BYTES) {
1116
+ return { status: "refused", path, mode, reason: "too-large", problem: `${path} is larger than ${MAX_CREDENTIALS_BYTES} bytes` };
1117
+ }
1118
+ const buf = Buffer.alloc(st.size);
1119
+ let off = 0;
1120
+ while (off < buf.length) {
1121
+ const n = readSync(fd, buf, off, buf.length - off, off);
1122
+ if (n === 0) break;
1123
+ off += n;
1124
+ }
1125
+ text = buf.subarray(0, off).toString("utf8");
1126
+ } catch (err) {
1127
+ return { status: "refused", path, mode: null, reason: "unreadable", problem: `cannot read ${path} (${(err as NodeJS.ErrnoException).code ?? "error"})` };
1128
+ } finally {
1129
+ try {
1130
+ closeSync(fd);
1131
+ } catch {
1132
+ // Nothing useful to do.
1133
+ }
1134
+ }
1135
+ let parsed: unknown;
1136
+ try {
1137
+ parsed = JSON.parse(text);
1138
+ } catch {
1139
+ return { status: "refused", path, mode: null, reason: "not-json", problem: `${path} is not valid JSON` };
1140
+ }
1141
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1142
+ return { status: "refused", path, mode: null, reason: "not-json", problem: `${path} does not hold a JSON object` };
1143
+ }
1144
+ const fields = parsed as Record<string, unknown>;
1145
+ // The same projection `readCredentials` uses, so "a cloud block without a
1146
+ // token is not a cloud block" means the same thing here as everywhere else.
1147
+ const others = projectCredentials(fields);
1148
+ const connections = [
1149
+ others.cloud ? { origin: originOrNull(others.cloud.url), key: others.cloud.token } : null,
1150
+ others.ingest ? { origin: originOrNull(others.ingest.url), key: others.ingest.key } : null,
1151
+ ].filter((c): c is { origin: string; key: string } => c !== null && c.origin !== null);
1152
+ const connected = connections.length > 0;
1153
+ const credential = projectJevSlot(fields.jev);
1154
+ if (!credential) return { status: "absent", path, connected };
1155
+ const slotOrigin = originOrNull(credential.url);
1156
+ // The same origin AND the same key: see "A slot counts only while…" above.
1157
+ const backed = slotOrigin !== null && connections.some((c) => c.origin === slotOrigin && c.key === credential.key);
1158
+ if (!backed) return { status: "absent", path, connected, orphaned: true };
1159
+ return { status: "ok", path, credential };
1160
+ }
1161
+
1162
+ /** A URL's origin, or null for a missing or unparseable one. */
1163
+ function originOrNull(url: string | undefined): string | null {
1164
+ if (!url) return null;
1165
+ try {
1166
+ const origin = new URL(url).origin;
1167
+ return origin === "null" ? null : origin;
1168
+ } catch {
1169
+ return null;
1170
+ }
1171
+ }
1172
+
1173
+ /** Store the `jev` slot (0600, owner-only home), keeping every other credential. */
1174
+ export function writeJevCloudCredential(credential: JevCloudCredential): string {
1175
+ writeCredentials({ ...readCredentials(), jev: { url: credential.url, key: credential.key } });
1176
+ return credentialsFile();
1177
+ }
1178
+
1179
+ /** Remove the `jev` slot. True if there was one. Every other credential stays. */
1180
+ export function clearJevCloudCredential(): boolean {
1181
+ const current = readCredentials();
1182
+ if (!current.jev) return false;
1183
+ const { jev: _dropped, ...rest } = current;
1184
+ writeCredentials(rest);
1185
+ return true;
1186
+ }
1187
+
923
1188
  function writeFileAt(path: string, contents: string): void {
924
1189
  mkdirSync(dirname(path), { recursive: true });
925
1190
  writeFileSync(path, contents, "utf8");
@@ -97,6 +97,7 @@ import {
97
97
  daemonVersionSkew,
98
98
  isDaemonSupportedPlatform,
99
99
  probeDaemonEndToEnd,
100
+ type DaemonServiceStatus,
100
101
  } from "./daemon-service";
101
102
 
102
103
  export interface ResetOutcome {
@@ -1094,7 +1095,24 @@ async function drainSpoolAfterMigrating(): Promise<number> {
1094
1095
  }
1095
1096
  }
1096
1097
 
1097
- export async function checkLayoutForCli(): Promise<LayoutCheck> {
1098
+ /**
1099
+ * Injectable for tests, matching `refreshDaemonToCliVersion`'s shape in
1100
+ * `daemon-service.ts`.
1101
+ *
1102
+ * `healDaemonFlag` asks systemd whether the unit is installed, and on a machine
1103
+ * with no unit that call blocks for seconds before answering. Two tests here
1104
+ * set `daemon.configured` and so reach it, and they were timing out at vitest's
1105
+ * 5 s default against the real service manager — a check about what this command
1106
+ * PRINTS, failing on how long the host takes to say "not-installed". A
1107
+ * `vi.spyOn` cannot intercept it either: this module calls its own local
1108
+ * binding, so without a seam a stub is ignored and the test passes or fails for
1109
+ * the wrong reason.
1110
+ */
1111
+ export interface LayoutCheckDeps {
1112
+ daemonStatus?: () => DaemonServiceStatus;
1113
+ }
1114
+
1115
+ export async function checkLayoutForCli(deps: LayoutCheckDeps = {}): Promise<LayoutCheck> {
1098
1116
  const state = detectLayout();
1099
1117
 
1100
1118
  if (state.kind === "future") {
@@ -1220,7 +1238,7 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
1220
1238
  state,
1221
1239
  fatal: false,
1222
1240
  didReset: false,
1223
- lines: [...(await healDaemonFlag()), ...staleDaemonHint()],
1241
+ lines: [...(await healDaemonFlag(deps)), ...staleDaemonHint()],
1224
1242
  };
1225
1243
  }
1226
1244
 
@@ -1239,13 +1257,13 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
1239
1257
  * downgrade a healthy machine to the in-process path — trading a loud, correct
1240
1258
  * failure for a quiet, wrong one.
1241
1259
  */
1242
- async function healDaemonFlag(): Promise<string[]> {
1260
+ async function healDaemonFlag(deps: LayoutCheckDeps = {}): Promise<string[]> {
1243
1261
  try {
1244
1262
  const cfg = readConfig();
1245
1263
  if (!cfg.daemon.configured) return [];
1246
1264
  if (!isDaemonSupportedPlatform()) return [];
1247
1265
 
1248
- const status = daemonServiceStatus();
1266
+ const status = (deps.daemonStatus ?? daemonServiceStatus)();
1249
1267
  if (status === "not-installed") {
1250
1268
  updateConfig({ daemon: { configured: false } });
1251
1269
  return [
@@ -38,7 +38,13 @@ import type { JevActivityFields } from "./semantic/combine";
38
38
  import type { JevConfig } from "./semantic/jev-config";
39
39
  import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
40
40
  import { loadAllCustomHooks } from "./custom-hooks-loader";
41
- import { authorityDeclarationFor, resolvePolicyAuthority, warnAuthority } from "./policy-authority";
41
+ import { contestedReviewerNames, effectiveReviewerNames } from "./effective-reviewers";
42
+ import {
43
+ authorityDeclarationFor,
44
+ refusedAuthorityWarning,
45
+ resolvePolicyAuthority,
46
+ warnAuthority,
47
+ } from "./policy-authority";
42
48
  import type { CustomHook } from "./policy-types";
43
49
  import { persistHookActivity } from "./hook-activity-store";
44
50
  import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
@@ -277,7 +283,10 @@ async function readJevConfig(): Promise<{ config: JevConfig; defaultMode: TwoTie
277
283
  if (!existsSync(jevConfigFile())) return null;
278
284
  const { loadJevConfig, DEFAULT_JEV_MODE } = await import("./semantic/jev-config");
279
285
  const config = loadJevConfig();
280
- return config ? { config, defaultMode: DEFAULT_JEV_MODE } : null;
286
+ // `loadJevConfig` never returns a switched-off config; this says so again at
287
+ // the one place it matters, because an `off` that got through would fall to
288
+ // the default mode below — which is `enforce`.
289
+ return config && config.mode !== "off" ? { config, defaultMode: DEFAULT_JEV_MODE } : null;
281
290
  } catch (err) {
282
291
  hookLogInfo(
283
292
  `Jev config could not be read (${err instanceof Error ? err.message : String(err)}); the regex engine decides alone`,
@@ -534,7 +543,7 @@ export async function evaluateHookEvent(
534
543
  } else {
535
544
  // Load enabled policies (merge across project/local/global scopes)
536
545
  config = readMergedHooksConfig(session.cwd);
537
- clearPolicies();
546
+ clearPolicies(cli);
538
547
 
539
548
  // A session pause suspends LOCAL policy only, for a bounded time. Cloud
540
549
  // assignments are exempt below for the same reason `disabledCustomPolicies`
@@ -791,8 +800,13 @@ export async function evaluateHookEvent(
791
800
  // aloud when a `reviewable` claim is refused and Jev is configured, so
792
801
  // an author is not left wondering why Jev never clears it.
793
802
  const authority = authorityDeclarationFor(hook, { cloudManaged, pack });
794
- const refused = resolvePolicyAuthority(authority).downgraded;
795
- if (refused) warnAuthority(`${registeredName} asks to be reviewable, but ${refused} — it stays hard`);
803
+ // Against the same set `registerPolicy` judges it by, or this warning
804
+ // describes a different machine than the registry does: a pack that
805
+ // ships its own semantic checks registers reviewable and was told, on
806
+ // every one of those policies, that it stays hard. `effectiveReviewerNames`
807
+ // is cached for the registration pass, so this costs nothing extra.
808
+ const refused = resolvePolicyAuthority(authority, effectiveReviewerNames(), contestedReviewerNames()).downgraded;
809
+ if (refused) warnAuthority(refusedAuthorityWarning(registeredName, refused));
796
810
  registerPolicy(
797
811
  registeredName,
798
812
  hook.description ?? "",
@@ -914,6 +928,21 @@ export async function evaluateHookEvent(
914
928
  releaseRegistry: opts?.releaseRegistry,
915
929
  })
916
930
  : await evaluatePolicies(canonicalEventType, parsed, session, config);
931
+ // Shadow mode: what Jev WOULD have done, filed where observe-mode policies
932
+ // file theirs, so the "would have" view counts it with no second channel.
933
+ // `semantic/<check>` cannot be mistaken for a cloud policy — cloud ids
934
+ // carry no `/` (`POLICY_ID_RE` in cloud-managed-policies.ts) — and the
935
+ // version is a Jev model id or `jev`, never a deployment number. Only on
936
+ // the two-tier path, so an unconfigured row is untouched.
937
+ const shadowVerdict = result.twoTier?.shadowVerdict;
938
+ if (shadowVerdict) {
939
+ observedResults.push({
940
+ policyId: shadowVerdict.policyName,
941
+ version: shadowVerdict.version,
942
+ decision: shadowVerdict.decision,
943
+ reason: shadowVerdict.reason,
944
+ });
945
+ }
917
946
  const durationMs = Math.round(performance.now() - startTime);
918
947
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
919
948
 
@@ -928,6 +957,11 @@ export async function evaluateHookEvent(
928
957
  matchedBeforeRelease ??
929
958
  getPoliciesForEvent(canonicalEventType, parsed.tool_name as string | undefined).map((p) => p.name);
930
959
 
960
+ // The pack a deciding Jev check came from, recorded as a pack's regex
961
+ // verdict records its own. The evaluator already resolved it, so the hook
962
+ // path never loads the Jev resolver (pack-semantic-import-boundary).
963
+ const jevOrigin = result.policyName && result.twoTier?.decidedByJev ? result.twoTier.jevOrigin : undefined;
964
+
931
965
  // Persist activity to disk (visible in /policies activity tab)
932
966
  const activityEntry = {
933
967
  timestamp: Date.now(),
@@ -950,8 +984,11 @@ export async function evaluateHookEvent(
950
984
  // Attribution. A builtin is anything registered that is not in the map,
951
985
  // so its absence is meaningful rather than missing — but only when a
952
986
  // policy actually decided; a plain allow names nobody. When Jev's own
953
- // verdict decided, no registered policy did: `evaluator` + `jevDecision`
954
- // say so, and claiming "builtin" here would be false.
987
+ // verdict decided, no registered policy did, and claiming "builtin" here
988
+ // would be false — so it is attributed to Jev itself. Leaving it out
989
+ // instead filed every Jev block under "unattributed" on FailproofAI
990
+ // Cloud's policy page, beside rows written before attribution existed.
991
+ ...(result.policyName && result.twoTier?.decidedByJev ? { policySource: "jev" as const, ...jevOrigin } : {}),
955
992
  ...(result.policyName && !result.twoTier?.decidedByJev
956
993
  ? (() => {
957
994
  const attribution = policyAttribution.get(result.policyName);
@@ -126,15 +126,19 @@ export interface HookActivityEntry {
126
126
  * Where the policy that DECIDED came from. Absent when nothing decided (a
127
127
  * plain allow) and on rows written before this existed — so, like
128
128
  * `matchedPolicies`, `undefined` means "unknown", not "builtin".
129
+ *
130
+ * `jev`: no registered policy decided — Jev's own verdict did (enforce mode;
131
+ * `policyName` is then `semantic/<check>`). Rows written before this value
132
+ * existed leave it out on such calls.
129
133
  */
130
- policySource?: "builtin" | "custom" | "convention" | "cloud" | "pack";
134
+ policySource?: "builtin" | "custom" | "convention" | "cloud" | "pack" | "jev";
131
135
  /** Cloud policy id of the decider. Present only when `policySource` is "cloud". */
132
136
  cloudPolicyId?: string;
133
137
  /** Immutable version of that policy — the half of attribution that identifies WHICH version ran. */
134
138
  cloudVersion?: number;
135
139
  /**
136
- * Pack id and version of the decider. Present only when `policySource` is
137
- * "pack", and here for the same reason the cloud pair is: the display name
140
+ * Pack id and version of the decider. Present when `policySource` is "pack",
141
+ * or "jev" when the deciding check came from a pack; and here for the same reason the cloud pair is: the display name
138
142
  * encodes both ("pack/acme/finance@1.2.0/…") but only as a string, so without
139
143
  * these the question "which pack, which version decided this" could only be
140
144
  * answered by re-parsing our own label.
@@ -154,10 +158,14 @@ export interface HookActivityEntry {
154
158
  * record is the entire point of observe mode: without it the row is
155
159
  * indistinguishable from one where the policy never matched, and the rollout
156
160
  * being trialled is unmeasurable.
161
+ *
162
+ * Jev in shadow mode files its own deny/instruct here too, as
163
+ * `{policyId: "semantic/<check>", version: <Jev model id or "jev">}`: the
164
+ * same question — what would this have done — for the same reader.
157
165
  */
158
166
  observed?: Array<{
159
167
  policyId: string;
160
- /** A cloud deployment number, or a pack's version string. */
168
+ /** A cloud deployment number, a pack's version string, or a Jev model id (`jev` when unknown). */
161
169
  version: string | number;
162
170
  decision: "deny" | "instruct";
163
171
  reason: string | null;
@@ -175,7 +183,7 @@ export interface HookActivityFilters {
175
183
  * organization's policies decided" is the question cloud rollout reporting
176
184
  * is built on, and it is unanswerable without this.
177
185
  */
178
- source?: "builtin" | "custom" | "convention" | "cloud" | "pack";
186
+ source?: "builtin" | "custom" | "convention" | "cloud" | "pack" | "jev";
179
187
  }
180
188
 
181
189
  export interface HookActivityStats {