failproofai 1.0.7 → 1.0.8

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 (268) 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 +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +9 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -10
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/pages/404.html +1 -1
  132. package/.next/standalone/.next/server/pages/500.html +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  135. package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
  136. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
  145. package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
  147. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  148. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  149. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  150. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  151. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  152. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  153. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  154. package/.next/standalone/app/settings/page.tsx +20 -1
  155. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  156. package/.next/standalone/app/settings/settings.css +79 -0
  157. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  158. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  159. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  163. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  164. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  165. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  166. package/.next/standalone/package.json +10 -10
  167. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  168. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  169. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  171. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  172. package/.next/standalone/server.js +1 -1
  173. package/README.md +3 -2
  174. package/bin/failproofai.mjs +148 -4
  175. package/dist/cli.mjs +18599 -9478
  176. package/dist/index.js +19 -1
  177. package/dist/worker.mjs +10500 -3773
  178. package/package.json +10 -10
  179. package/pi-extension/index.ts +11 -0
  180. package/scripts/build-policy-pack.mjs +53 -2
  181. package/src/audit/features.ts +3 -2
  182. package/src/hooks/builtin-policies.ts +195 -8
  183. package/src/hooks/cloud-connection.ts +190 -1
  184. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  185. package/src/hooks/cloud-introspect.ts +6 -0
  186. package/src/hooks/cloud-managed-policies.ts +22 -0
  187. package/src/hooks/configure-wizard.ts +26 -6
  188. package/src/hooks/custom-hooks-loader.ts +98 -10
  189. package/src/hooks/custom-hooks-registry.ts +45 -1
  190. package/src/hooks/effective-reviewers.ts +243 -0
  191. package/src/hooks/first-run-gate.ts +5 -0
  192. package/src/hooks/flush-cli.ts +35 -8
  193. package/src/hooks/fp-config.ts +266 -1
  194. package/src/hooks/fp-home.ts +23 -0
  195. package/src/hooks/fp-reset.ts +22 -4
  196. package/src/hooks/handler.ts +303 -7
  197. package/src/hooks/hook-activity-store.ts +118 -6
  198. package/src/hooks/hook-telemetry.ts +41 -0
  199. package/src/hooks/jev-activity.ts +385 -0
  200. package/src/hooks/jev-cli.ts +2172 -0
  201. package/src/hooks/jev-cloud-connection.ts +392 -0
  202. package/src/hooks/loader-utils.ts +6 -0
  203. package/src/hooks/manager.ts +45 -7
  204. package/src/hooks/pack-cli.ts +568 -37
  205. package/src/hooks/pack-failclosed.ts +3 -0
  206. package/src/hooks/pack-manifest.ts +489 -7
  207. package/src/hooks/pack-store.ts +194 -11
  208. package/src/hooks/policy-authority.ts +386 -0
  209. package/src/hooks/policy-catalog.ts +206 -0
  210. package/src/hooks/policy-evaluator.ts +963 -796
  211. package/src/hooks/policy-registry.ts +27 -1
  212. package/src/hooks/policy-reviewability.ts +258 -0
  213. package/src/hooks/policy-types.ts +128 -0
  214. package/src/hooks/semantic/combine.ts +581 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +513 -0
  217. package/src/hooks/semantic/envelope.ts +1255 -0
  218. package/src/hooks/semantic/evaluator.ts +556 -0
  219. package/src/hooks/semantic/facts.ts +387 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +1155 -0
  222. package/src/hooks/semantic/jev-config.ts +1142 -0
  223. package/src/hooks/semantic/jev-review.ts +389 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +427 -0
  226. package/src/hooks/semantic/pack-policies.ts +294 -0
  227. package/src/hooks/semantic/policies.ts +597 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2937 -0
  231. package/src/hooks/semantic/session-root.ts +116 -0
  232. package/src/hooks/semantic/types.ts +162 -0
  233. package/src/hooks/semver-precedence.ts +128 -0
  234. package/src/hooks/tui.ts +4 -0
  235. package/src/hooks/types.ts +1 -1
  236. package/src/hooks/worker-server.ts +121 -27
  237. package/src/index.ts +6 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  240. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  253. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
  255. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  258. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  259. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  260. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  261. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  263. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  265. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  266. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
  267. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
@@ -24,18 +24,32 @@ import {
24
24
  OPENCLAW_EVENT_MAP,
25
25
  ANTIGRAVITY_EVENT_MAP,
26
26
  } from "./types";
27
+ import { existsSync } from "node:fs";
27
28
  import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize";
28
29
  import { normalizeCliPayload } from "./normalize-cli-payload";
29
30
  import type { PolicyFunction, PolicyResult, HooksConfig } from "./policy-types";
30
31
  import { readMergedHooksConfig } from "./hooks-config";
31
32
  import { registerBuiltinPolicies } from "./builtin-policies";
32
33
  import { evaluatePolicies } from "./policy-evaluator";
34
+ // Types only: the semantic (Jev) modules are loaded with a dynamic import, and
35
+ // only once a Jev config exists, so an unconfigured machine never loads them.
36
+ import type { TwoTierReview } from "./semantic/combine";
37
+ import type { JevActivityFields } from "./semantic/combine";
38
+ import type { JevConfig } from "./semantic/jev-config";
33
39
  import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
34
40
  import { loadAllCustomHooks } from "./custom-hooks-loader";
41
+ import { contestedReviewerNames, effectiveReviewerNames } from "./effective-reviewers";
42
+ import {
43
+ authorityDeclarationFor,
44
+ refusedAuthorityWarning,
45
+ resolvePolicyAuthority,
46
+ warnAuthority,
47
+ } from "./policy-authority";
35
48
  import type { CustomHook } from "./policy-types";
36
49
  import { persistHookActivity } from "./hook-activity-store";
37
50
  import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
38
51
  import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
52
+ import * as hookTelemetry from "./hook-telemetry";
39
53
  import { resolveCwd } from "./resolve-cwd";
40
54
  import { resolvePermissionMode } from "./resolve-permission-mode";
41
55
  import { resolveTranscriptPath } from "./resolve-transcript-path";
@@ -46,6 +60,7 @@ import { readActiveCloudManagedPolicies, type CloudManagedPolicyArtifact } from
46
60
  import { hasInstalledPacks, readInstalledPacks, type PackError, type ResolvedPack } from "./pack-manifest";
47
61
  import { missingGuards, packFailureReason, combinedGuardMatch, guardsCover } from "./pack-failclosed";
48
62
  import { readActivePause, type ActivePause } from "./session-pause";
63
+ import { jevConfigFile } from "./fp-home";
49
64
  import { layoutWarningForHook } from "./fp-reset";
50
65
 
51
66
  /**
@@ -153,6 +168,16 @@ export interface EvaluateHookEventOptions {
153
168
  * written for.
154
169
  */
155
170
  fallbackCwd?: string;
171
+ /**
172
+ * The warm worker's hook into its request queue. Called at most once, and
173
+ * only on the two-tier path, at the point this evaluation stops reading the
174
+ * process-global policy registry and starts waiting on Jev's network answer
175
+ * — so the next queued hook can run instead of every hook on the machine
176
+ * queueing behind that round trip. After it is called nothing in this
177
+ * evaluation reads the registry again. The one-shot path has no queue and
178
+ * leaves it unset.
179
+ */
180
+ releaseRegistry?: () => void;
156
181
  }
157
182
 
158
183
  /**
@@ -186,6 +211,214 @@ async function runObserved(
186
211
  }
187
212
  }
188
213
 
214
+ // ── Two-tier (Jev) opt-in ────────────────────────────────────────────────────
215
+ //
216
+ // Jev reviews a call only when ALL of these hold, and the regex engine runs
217
+ // alone — exactly as it did before two tiers existed — otherwise:
218
+ //
219
+ // - a valid BYOK config exists (`~/.failproofai/jev.json`, global only);
220
+ // - `FAILPROOFAI_EVALUATOR` is not `legacy` (see "Turning Jev off" below);
221
+ // - this is not the fail-closed `forceDecision` path and no session pause is
222
+ // active — a pause suspends local policy, and Jev must not become a way to
223
+ // evaluate what the pause switched off;
224
+ // - the event is a gate (`PreToolUse` / `PermissionRequest`) for a named tool
225
+ // that the AGENT requested (see `isHumanAuthoredGate`).
226
+ //
227
+ // There is no cloud-deployment exclusion: cloud policies default to `hard`, so
228
+ // Jev can only make a centrally managed machine stricter, never weaker.
229
+ //
230
+ // Turning Jev off. The switch that works everywhere is the config file itself:
231
+ // it is read on every event, here, in whichever process evaluates — the
232
+ // daemon's warm worker included — so `failproofai jev remove` (or a `mode:
233
+ // "shadow"` config, which keeps enforcing the regex result) applies from the
234
+ // next tool call, with no restart. `FAILPROOFAI_EVALUATOR=legacy` is the dev
235
+ // escape hatch, read from the EVALUATING process's environment: it covers a
236
+ // one-shot hook (no daemon) and a worker whose own environment sets it, but
237
+ // not a variable exported in a shell on a daemon machine — the daemon
238
+ // forwards a hook's event, cli, stdin and cwd to the worker, never the hook
239
+ // process's environment. `failproofai jev status` says so when it sees the
240
+ // variable. A per-session switch for daemon machines would be a new field in
241
+ // the daemon protocol; there is none today.
242
+
243
+ /** The two gate events Jev reviews. Everything else is regex-only. */
244
+ const JEV_GATE_EVENTS: ReadonlySet<string> = new Set(["PreToolUse", "PermissionRequest"]);
245
+
246
+ /**
247
+ * A gate event that carries a command the HUMAN typed, not one the agent
248
+ * requested: Pi's `user_bash` (`!cmd` at Pi's prompt) canonicalizes to
249
+ * `PreToolUse` like an agent's `tool_call`. Jev judges an agent's request
250
+ * against what the human asked for, and says so to the model ("A coding agent
251
+ * has REQUESTED the tool call"); the human's own command is the human asking,
252
+ * and never appears in `user_said`, so Jev would judge it as an unrequested
253
+ * agent action and could block the human's own command. The regex policies
254
+ * still run on it exactly as they always have.
255
+ */
256
+ function isHumanAuthoredGate(rawEventType: string | undefined, cli: IntegrationType): boolean {
257
+ return cli === "pi" && rawEventType === "user_bash";
258
+ }
259
+
260
+ function jevForcedOff(opts: EvaluateHookEventOptions | undefined): boolean {
261
+ return process.env.FAILPROOFAI_EVALUATOR === "legacy" || !!opts?.forceDecision;
262
+ }
263
+
264
+ /**
265
+ * The BYOK config (with the build's default mode, D2), or null (Jev off). A
266
+ * config that cannot be read is off, never a failure. Logged at info, like
267
+ * every configured-but-broken Jev path: warn-level lines reach the hook's
268
+ * stderr, which is the deny text itself on some CLIs (Factory's exit 2).
269
+ * `failproofai jev status` is where a broken config is made visible.
270
+ *
271
+ * The file is checked for BEFORE the import, and that order is the point. With
272
+ * no `jev.json` the answer is null however the module would have read it — so
273
+ * the stat decides it, and a machine that never configured Jev never evaluates
274
+ * a line of the semantic modules to be told what their absence already says.
275
+ * The same stat-rather-than-load trade `warnAuthority` makes, for the same
276
+ * reason. It short-circuits nothing but a null `loadJevConfig` would have
277
+ * returned itself: that function reads this path, and an absent file, an
278
+ * unreadable directory above it and a path that is not a file are all null
279
+ * there too.
280
+ */
281
+ async function readJevConfig(): Promise<{ config: JevConfig; defaultMode: TwoTierReview["mode"] } | null> {
282
+ try {
283
+ if (!existsSync(jevConfigFile())) return null;
284
+ const { loadJevConfig, DEFAULT_JEV_MODE } = await import("./semantic/jev-config");
285
+ const config = loadJevConfig();
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;
290
+ } catch (err) {
291
+ hookLogInfo(
292
+ `Jev config could not be read (${err instanceof Error ? err.message : String(err)}); the regex engine decides alone`,
293
+ );
294
+ return null;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Start Jev's review of this call — before the regex policies run, so the two
300
+ * proceed in parallel — or return null for the regex-only path.
301
+ */
302
+ async function startTwoTier(
303
+ canonicalEventType: string,
304
+ parsed: Record<string, unknown>,
305
+ session: SessionMetadata,
306
+ cli: IntegrationType,
307
+ opts: EvaluateHookEventOptions | undefined,
308
+ activePause: ActivePause | null,
309
+ ): Promise<TwoTierReview | null> {
310
+ if (!JEV_GATE_EVENTS.has(canonicalEventType)) return null;
311
+ if (isHumanAuthoredGate(session.rawHookEventName, cli)) return null;
312
+ if (typeof parsed.tool_name !== "string" || parsed.tool_name.length === 0) return null;
313
+ if (jevForcedOff(opts) || activePause) return null;
314
+ const loaded = await readJevConfig();
315
+ if (!loaded) return null;
316
+ const cfg = loaded.config;
317
+ try {
318
+ const { startJevReview } = await import("./semantic/jev-review");
319
+ return startJevReview(cfg, {
320
+ eventType: canonicalEventType,
321
+ toolName: parsed.tool_name,
322
+ toolInput: parsed.tool_input,
323
+ cwd: session.cwd,
324
+ permissionMode: session.permissionMode,
325
+ sessionId: session.sessionId,
326
+ cli,
327
+ });
328
+ } catch (err) {
329
+ // Configured but unable to start: that is a fallback, and it is recorded
330
+ // as one — every regex verdict hard, the regex result final. Recorded as
331
+ // `error`, one of the reason codes the activity store and the collector
332
+ // know (T8's closed list); an unknown code would ship as `other`. Logged
333
+ // at info, off the hook's stderr (see `readJevConfig`); the activity row
334
+ // carries the fallback.
335
+ hookLogInfo(`Jev review could not start (${err instanceof Error ? err.message : String(err)})`);
336
+ return {
337
+ mode: cfg.mode === "shadow" || cfg.mode === "enforce" ? cfg.mode : loaded.defaultMode,
338
+ review: Promise.resolve({ kind: "fallback", reason: "error", latencyMs: null, model: null }),
339
+ abort: () => {},
340
+ authorityOf: () => ({ authority: "hard", reviewedBy: [] }),
341
+ };
342
+ }
343
+ }
344
+
345
+ /**
346
+ * The Jev properties for `hook_policy_triggered`, from T8's
347
+ * `jevTelemetryProperties` (decisions, reason codes, policy and model names,
348
+ * a latency — never command or prompt text). `{}` when the two-tier path did
349
+ * not run, so an unconfigured machine's event is unchanged, and `{}` when the
350
+ * helper is not in this build (it is T8's, not a §7 contract) or throws:
351
+ * telemetry never costs a hook its answer.
352
+ *
353
+ * Only its `jev_`-prefixed keys are kept. These are spread into an event whose
354
+ * core properties (`decision`, `policy_name`, `event_type`, …) are what the
355
+ * rollout is read from, and a helper owned by another task must not be able to
356
+ * overwrite one of them — silently, and only on two-tier machines — by
357
+ * emitting a key of that name. The helper is treated as untrusted in every
358
+ * other respect (absent, non-object, throwing); this is the same guard for
359
+ * what it returns.
360
+ */
361
+ function jevTelemetry(activity: JevActivityFields | undefined): Record<string, unknown> {
362
+ if (!activity) return {};
363
+ try {
364
+ const build = (hookTelemetry as unknown as { jevTelemetryProperties?: (entry: JevActivityFields) => unknown })
365
+ .jevTelemetryProperties;
366
+ const props = typeof build === "function" ? build(activity) : null;
367
+ if (!props || typeof props !== "object" || Array.isArray(props)) return {};
368
+ return Object.fromEntries(Object.entries(props as Record<string, unknown>).filter(([key]) => key.startsWith("jev_")));
369
+ } catch {
370
+ return {};
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Record what the human just typed, for Jev to judge later calls against.
376
+ * Only when Jev is configured. Every canonical `UserPromptSubmit` is passed,
377
+ * except a prompt a policy denied on a CLI where that deny is known to hold
378
+ * (`ENFORCEMENT_CAPABILITY` says `block`): the agent never receives that
379
+ * prompt, so it cannot be what the agent is working on. Where a prompt deny
380
+ * is only observed — Goose, OpenCode, Antigravity — or not verified, the
381
+ * agent does get the prompt, and it is passed like any other. Which CLIs'
382
+ * prompt events count as human is decided inside `captureIntent` (T4).
383
+ * Never throws; a failure is logged at info (see `readJevConfig`).
384
+ *
385
+ * `captureIntent` gets the whole normalized payload (`payload`): the prompt
386
+ * text is in a different field on some harnesses (Goose: `message`), and the
387
+ * marks that tell a human's prompt from a subagent's or an extension's are
388
+ * elsewhere in it. `prompt` is §7's original field, still read by the
389
+ * contract stub; T4's implementation reads `payload` and ignores it.
390
+ */
391
+ async function captureJevIntent(
392
+ canonicalEventType: string,
393
+ decision: "allow" | "deny" | "instruct",
394
+ parsed: Record<string, unknown>,
395
+ session: SessionMetadata,
396
+ cli: IntegrationType,
397
+ opts: EvaluateHookEventOptions | undefined,
398
+ ): Promise<void> {
399
+ if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts)) return;
400
+ try {
401
+ if (!(await readJevConfig())) return;
402
+ if (decision === "deny") {
403
+ const { ENFORCEMENT_CAPABILITY } = await import("./enforcement-capability");
404
+ if (ENFORCEMENT_CAPABILITY[cli]?.UserPromptSubmit === "block") return;
405
+ }
406
+ const { captureIntent } = await import("./semantic/intent");
407
+ // Built first, then passed: the one object satisfies §7's shape and T4's.
408
+ const event = {
409
+ eventType: canonicalEventType,
410
+ sessionId: session.sessionId,
411
+ transcriptPath: session.transcriptPath,
412
+ cli,
413
+ payload: parsed,
414
+ prompt: parsed.prompt,
415
+ };
416
+ captureIntent(event);
417
+ } catch (err) {
418
+ hookLogInfo(`Jev intent capture failed: ${err instanceof Error ? err.message : String(err)}`);
419
+ }
420
+ }
421
+
189
422
  /**
190
423
  * The core hook-evaluation logic, decoupled from process stdin/stdout so it
191
424
  * can be called repeatedly inside a long-lived process (the daemon's warm
@@ -310,7 +543,7 @@ export async function evaluateHookEvent(
310
543
  } else {
311
544
  // Load enabled policies (merge across project/local/global scopes)
312
545
  config = readMergedHooksConfig(session.cwd);
313
- clearPolicies();
546
+ clearPolicies(cli);
314
547
 
315
548
  // A session pause suspends LOCAL policy only, for a bounded time. Cloud
316
549
  // assignments are exempt below for the same reason `disabledCustomPolicies`
@@ -561,6 +794,19 @@ export async function evaluateHookEvent(
561
794
  ...(cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudVersion: cloudManaged.version } : {}),
562
795
  ...(pack ? { packId: pack.id, packVersion: pack.version } : {}),
563
796
  });
797
+ // Whether Jev may clear this policy's verdict. The cloud assignment or
798
+ // the pack manifest declares it for those routes, the hook itself only
799
+ // for the user's own files; anything unclear registers as hard. Said
800
+ // aloud when a `reviewable` claim is refused and Jev is configured, so
801
+ // an author is not left wondering why Jev never clears it.
802
+ const authority = authorityDeclarationFor(hook, { cloudManaged, pack });
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));
564
810
  registerPolicy(
565
811
  registeredName,
566
812
  hook.description ?? "",
@@ -572,6 +818,7 @@ export async function evaluateHookEvent(
572
818
  // configured. Matched by the pack's own name for the policy, which is
573
819
  // the name before the `pack/<id>@<version>/` prefix is applied.
574
820
  pack?.policies.find((p) => p.name === hook.name)?.params,
821
+ authority,
575
822
  );
576
823
  }
577
824
 
@@ -660,19 +907,60 @@ export async function evaluateHookEvent(
660
907
  );
661
908
  }
662
909
 
910
+ // Two-tier: when a Jev config exists and this is a gate, Jev's request
911
+ // starts HERE, before any regex policy runs, and evaluatePolicies combines
912
+ // the two (see semantic/combine.ts). Otherwise this is null and the call
913
+ // below is exactly the regex-only evaluation it always was.
914
+ const twoTier = await startTwoTier(canonicalEventType, parsed, session, cli, opts, activePause);
915
+ // On the two-tier path the registry is read for the activity row BEFORE
916
+ // evaluating, because evaluatePolicies may hand the registry back to the
917
+ // warm worker's queue (`releaseRegistry`) while it waits on Jev, and the
918
+ // next request's clearPolicies() could then run under this one. Same
919
+ // cached lookup, same answer: nothing between here and there registers.
920
+ const matchedBeforeRelease = twoTier
921
+ ? getPoliciesForEvent(canonicalEventType, parsed.tool_name as string | undefined).map((p) => p.name)
922
+ : null;
923
+
663
924
  // Evaluate policies (use canonical PascalCase event type)
664
- const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
925
+ const result = twoTier
926
+ ? await evaluatePolicies(canonicalEventType, parsed, session, config, {
927
+ ...twoTier,
928
+ releaseRegistry: opts?.releaseRegistry,
929
+ })
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
+ }
665
946
  const durationMs = Math.round(performance.now() - startTime);
666
947
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
667
948
 
949
+ await captureJevIntent(canonicalEventType, result.decision, parsed, session, cli, opts);
950
+
668
951
  // Which policies actually ran for this event, regardless of how they
669
952
  // decided. `result.policyName` names only the decider — null on a plain
670
953
  // allow — so without this a row cannot tell "your policy ran and allowed"
671
954
  // from "no policy covers this event". The lookup is the same cached call
672
955
  // the evaluator already made, so it costs nothing.
673
- const matchedPolicies = getPoliciesForEvent(canonicalEventType, parsed.tool_name as string | undefined).map(
674
- (p) => p.name,
675
- );
956
+ const matchedPolicies =
957
+ matchedBeforeRelease ??
958
+ getPoliciesForEvent(canonicalEventType, parsed.tool_name as string | undefined).map((p) => p.name);
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;
676
964
 
677
965
  // Persist activity to disk (visible in /policies activity tab)
678
966
  const activityEntry = {
@@ -686,6 +974,8 @@ export async function evaluateHookEvent(
686
974
  decision: result.decision,
687
975
  reason: result.reason,
688
976
  durationMs,
977
+ // How Jev took part (§7 fields). Absent unless the two-tier path ran.
978
+ ...(result.twoTier ? result.twoTier.activity : {}),
689
979
  sessionId: session.sessionId,
690
980
  transcriptPath: session.transcriptPath,
691
981
  cwd: session.cwd,
@@ -693,8 +983,13 @@ export async function evaluateHookEvent(
693
983
  hookEventName: session.hookEventName,
694
984
  // Attribution. A builtin is anything registered that is not in the map,
695
985
  // so its absence is meaningful rather than missing — but only when a
696
- // policy actually decided; a plain allow names nobody.
697
- ...(result.policyName
986
+ // policy actually decided; a plain allow names nobody. When Jev's own
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 } : {}),
992
+ ...(result.policyName && !result.twoTier?.decidedByJev
698
993
  ? (() => {
699
994
  const attribution = policyAttribution.get(result.policyName);
700
995
  return {
@@ -751,6 +1046,7 @@ export async function evaluateHookEvent(
751
1046
  convention_scope: conventionScope,
752
1047
  has_custom_params: hasCustomParams,
753
1048
  param_keys_overridden: paramKeysOverridden,
1049
+ ...jevTelemetry(result.twoTier?.activity),
754
1050
  });
755
1051
  // Deny/instruct is exactly the response the daemon's warm worker
756
1052
  // needs to return fast — a live network POST to PostHog on this path
@@ -24,6 +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
28
 
28
29
  export const PAGE_SIZE = 25;
29
30
 
@@ -76,6 +77,36 @@ export interface HookActivityEntry {
76
77
  matchedPolicies?: string[];
77
78
  decision: "allow" | "deny" | "instruct";
78
79
  reason: string | null;
80
+ /**
81
+ * The Jev (two-tier) fields. Present only when a Jev config (BYOK) exists and
82
+ * the call was a PreToolUse / PermissionRequest gate. Absent means the regex
83
+ * engine decided alone, exactly as before these fields existed.
84
+ *
85
+ * `evaluator`: `jev` (the two-tier path ran) or `jev-fallback` (Jev was
86
+ * unavailable, truncated or mismatched, so the regex result stood;
87
+ * `jevFallbackReason` says why). `jev` alone does NOT mean Jev answered:
88
+ * when a hard policy denies, Jev is aborted and the row carries
89
+ * `{ evaluator: "jev", jevMode }` and no other Jev field; when no semantic
90
+ * policy applies to the call, no request is sent and the row carries
91
+ * `{ evaluator: "jev", jevDecision: "allow", jevMode }`. Read a row with
92
+ * `jevOutcome` (`jev-activity.ts`): answered, fallback, not-consulted or
93
+ * no-request.
94
+ *
95
+ * Validated on write by `persistHookActivity` (see `jev-activity.ts`): an
96
+ * invalid value is dropped field by field, and `jevFallbackReason` is stored
97
+ * as a known code (`timeout`, `http-429`, `error`, …) or `other`, never free
98
+ * text. The collector ships these fields to FailproofAI Cloud.
99
+ */
100
+ evaluator?: "jev" | "jev-fallback";
101
+ /** Jev's own verdict, before combining with the regex results. */
102
+ jevDecision?: "allow" | "instruct" | "deny";
103
+ /** Reviewable policies whose deny/instruct Jev cleared. */
104
+ jevCleared?: string[];
105
+ jevFallbackReason?: string;
106
+ jevLatencyMs?: number;
107
+ jevModel?: string;
108
+ /** `shadow` logs Jev but enforces the regex result; `enforce` applies the combine rules. */
109
+ jevMode?: "shadow" | "enforce";
79
110
  durationMs: number;
80
111
  sessionId?: string;
81
112
  transcriptPath?: string;
@@ -95,15 +126,19 @@ export interface HookActivityEntry {
95
126
  * Where the policy that DECIDED came from. Absent when nothing decided (a
96
127
  * plain allow) and on rows written before this existed — so, like
97
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.
98
133
  */
99
- policySource?: "builtin" | "custom" | "convention" | "cloud" | "pack";
134
+ policySource?: "builtin" | "custom" | "convention" | "cloud" | "pack" | "jev";
100
135
  /** Cloud policy id of the decider. Present only when `policySource` is "cloud". */
101
136
  cloudPolicyId?: string;
102
137
  /** Immutable version of that policy — the half of attribution that identifies WHICH version ran. */
103
138
  cloudVersion?: number;
104
139
  /**
105
- * Pack id and version of the decider. Present only when `policySource` is
106
- * "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
107
142
  * encodes both ("pack/acme/finance@1.2.0/…") but only as a string, so without
108
143
  * these the question "which pack, which version decided this" could only be
109
144
  * answered by re-parsing our own label.
@@ -123,10 +158,14 @@ export interface HookActivityEntry {
123
158
  * record is the entire point of observe mode: without it the row is
124
159
  * indistinguishable from one where the policy never matched, and the rollout
125
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.
126
165
  */
127
166
  observed?: Array<{
128
167
  policyId: string;
129
- /** 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). */
130
169
  version: string | number;
131
170
  decision: "deny" | "instruct";
132
171
  reason: string | null;
@@ -144,7 +183,7 @@ export interface HookActivityFilters {
144
183
  * organization's policies decided" is the question cloud rollout reporting
145
184
  * is built on, and it is unanswerable without this.
146
185
  */
147
- source?: "builtin" | "custom" | "convention" | "cloud" | "pack";
186
+ source?: "builtin" | "custom" | "convention" | "cloud" | "pack" | "jev";
148
187
  }
149
188
 
150
189
  export interface HookActivityStats {
@@ -194,7 +233,13 @@ function releaseLock(): void {
194
233
 
195
234
  // ── Writing (synchronous — hook handler is short-lived) ──
196
235
 
197
- export function persistHookActivity(entry: HookActivityEntry): void {
236
+ export function persistHookActivity(input: HookActivityEntry): void {
237
+ // The Jev fields are validated here, at the one door every row passes
238
+ // through, rather than trusted from the caller: they are shipped off the
239
+ // machine by the collector, and a fallback reason is the one field that can
240
+ // carry free text (see jev-activity.ts). A row with no Jev fields is written
241
+ // exactly as given.
242
+ const entry = sanitizeJevActivity(input);
198
243
  ensureDir();
199
244
  acquireLock();
200
245
  try {
@@ -397,6 +442,72 @@ export function getAllHookActivityEntries(): HookActivityEntry[] {
397
442
  return [...currentEntries, ...archiveEntries];
398
443
  }
399
444
 
445
+ /** See {@link getHookActivityEntriesSince}. */
446
+ export const ROTATION_CLOCK_SLACK_MS = 10 * 60 * 1000;
447
+
448
+ /**
449
+ * Every entry stamped at or after `sinceMs`, newest first — without reading the
450
+ * whole history.
451
+ *
452
+ * Pages are never pruned, so a long-lived machine has hundreds of them, and a
453
+ * windowed question ("how did Jev do in the last day") should not pay for all
454
+ * of them. It does not have to: an entry's timestamp is taken before it is
455
+ * appended, and a page is only renamed to `page-<rotatedAt>-<seq>` after its
456
+ * last append, so every entry in a page is at or before `rotatedAt`. Archives
457
+ * are listed newest first, so once one was rotated before the window opened,
458
+ * it and every older page lie wholly outside it and reading stops there.
459
+ *
460
+ * `current.jsonl` and the newest pages can still hold entries just outside the
461
+ * window (two hook processes race to append), hence the per-entry filter.
462
+ * The stop is taken with {@link ROTATION_CLOCK_SLACK_MS} to spare, so a clock
463
+ * stepped back a few minutes between two writes costs one extra page read
464
+ * rather than silently dropping rows from the window.
465
+ *
466
+ * No row is returned twice, although this reads without the writers' lock. A
467
+ * hook process can rotate `current.jsonl` into a new page between this reading
468
+ * `current.jsonl` and listing the pages, and that page then holds the rows just
469
+ * read. So the pages are listed once before `current.jsonl` is read; a page
470
+ * that appears only in the second listing was rotated during the read, and any
471
+ * of its rows that were also in `current.jsonl` are skipped. (A page rotated
472
+ * before the read holds none of them: `current.jsonl` was fresh by then.)
473
+ */
474
+ export function getHookActivityEntriesSince(sinceMs: number): HookActivityEntry[] {
475
+ ensureDir();
476
+ const dir = storeDirValue();
477
+ const inWindow = (e: HookActivityEntry) => typeof e.timestamp === "number" && e.timestamp >= sinceMs;
478
+ const listedBefore = new Set(getArchiveFiles());
479
+ sinceReadProbe?.("before-current");
480
+ const current = readJsonlFile(join(dir, CURRENT_FILE));
481
+ sinceReadProbe?.("after-current");
482
+ const readFromCurrent = new Map<string, number>();
483
+ for (const e of current) {
484
+ const key = JSON.stringify(e);
485
+ readFromCurrent.set(key, (readFromCurrent.get(key) ?? 0) + 1);
486
+ }
487
+ const notReadYet = (e: HookActivityEntry): boolean => {
488
+ const key = JSON.stringify(e);
489
+ const n = readFromCurrent.get(key) ?? 0;
490
+ if (n === 0) return true;
491
+ readFromCurrent.set(key, n - 1);
492
+ return false;
493
+ };
494
+ const out = current.reverse().filter(inWindow);
495
+ for (const file of getArchiveFiles()) {
496
+ const rotatedAt = parseInt(file.slice(5, -6).split("-")[0], 10);
497
+ if (Number.isFinite(rotatedAt) && rotatedAt < sinceMs - ROTATION_CLOCK_SLACK_MS) break;
498
+ let entries = readJsonlFile(join(dir, file));
499
+ if (!listedBefore.has(file)) entries = entries.filter(notReadYet);
500
+ out.push(...entries.reverse().filter(inWindow));
501
+ }
502
+ return out;
503
+ }
504
+
505
+ /** Test-only: runs between the reads of {@link getHookActivityEntriesSince}, to land a rotation exactly there. */
506
+ let sinceReadProbe: ((phase: "before-current" | "after-current") => void) | null = null;
507
+ export function _setSinceReadProbeForTest(probe: ((phase: "before-current" | "after-current") => void) | null): void {
508
+ sinceReadProbe = probe;
509
+ }
510
+
400
511
 
401
512
  export function searchHookActivity(
402
513
  filters: HookActivityFilters,
@@ -519,6 +630,7 @@ function getArchiveFiles(): string[] {
519
630
 
520
631
  export function _resetForTest(testDir?: string): void {
521
632
  rotateSeq = 0;
633
+ sinceReadProbe = null;
522
634
  // null, not the default path: clearing the override lets the getter re-read
523
635
  // FAILPROOFAI_HOME, which a test may have changed since this module loaded.
524
636
  storeDirOverride = testDir ?? null;
@@ -8,6 +8,7 @@
8
8
  import { version } from "../../package.json";
9
9
  import { POSTHOG_API_KEY, POSTHOG_PRODUCT } from "../posthog-key";
10
10
  import { isTelemetryEnabled } from "../../lib/telemetry-enabled";
11
+ import { jevOutcome, sanitizeJevActivity, type JevActivityFields } from "./jev-activity";
11
12
 
12
13
  const API_KEY = POSTHOG_API_KEY;
13
14
  const CAPTURE_URL = "https://us.i.posthog.com/capture/";
@@ -86,3 +87,43 @@ export async function flushHookTelemetry(): Promise<void> {
86
87
  await Promise.allSettled([...pending]);
87
88
  }
88
89
  }
90
+
91
+ /**
92
+ * The Jev (two-tier evaluator) properties for a hook telemetry event, built
93
+ * from the activity row's Jev fields — or `{}` when Jev was not involved, so
94
+ * spreading this into an event changes nothing on an unconfigured machine.
95
+ *
96
+ * Same rule as every other hook property: decisions, reason codes, policy and
97
+ * model names, and a latency — never command or prompt text. The row's fields
98
+ * go through the same normaliser the activity store applies on write, so a
99
+ * free-text fallback reason is reduced to its code here too.
100
+ *
101
+ * A call a hard policy denied before Jev's answer was read carries
102
+ * `jev_outcome: "not-consulted"`, and a call no semantic policy applied to (no
103
+ * request sent) `jev_outcome: "no-request"`; neither carries a Jev verdict,
104
+ * clears, latency or model: `jev_evaluator: "jev"` alone does not mean Jev
105
+ * answered (see `jevOutcome`).
106
+ *
107
+ * Meant to be spread into `hook_policy_triggered` by the handler:
108
+ * `{ ...existingProps, ...jevTelemetryProperties(activityEntry) }`.
109
+ */
110
+ export function jevTelemetryProperties(entry: JevActivityFields): Record<string, unknown> {
111
+ const e = sanitizeJevActivity(entry);
112
+ const outcome = jevOutcome(e);
113
+ if (outcome === null) return {};
114
+ const props: Record<string, unknown> = { jev_evaluator: e.evaluator };
115
+ if (e.jevMode) props.jev_mode = e.jevMode;
116
+ if (outcome === "not-consulted" || outcome === "no-request") {
117
+ props.jev_outcome = outcome;
118
+ return props;
119
+ }
120
+ if (e.jevDecision) props.jev_decision = e.jevDecision;
121
+ if (e.jevCleared) {
122
+ props.jev_cleared = e.jevCleared;
123
+ props.jev_cleared_count = e.jevCleared.length;
124
+ }
125
+ if (e.jevFallbackReason) props.jev_fallback_reason = e.jevFallbackReason;
126
+ if (e.jevLatencyMs !== undefined) props.jev_latency_ms = e.jevLatencyMs;
127
+ if (e.jevModel) props.jev_model = e.jevModel;
128
+ return props;
129
+ }