failproofai 1.0.7-beta.2 → 1.0.7

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 (252) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +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 +7 -8
  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 +5 -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 -13
  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 +7 -8
  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 +7 -7
  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 +6 -7
  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 +8 -41
  63. package/.next/standalone/.next/server/app/settings/page.js +9 -12
  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]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
  71. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  72. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
  98. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-i_gzc._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _166t73i._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
  104. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0w6mzq5._.js → node_modules_next_dist_0drixxt._.js} +4 -4
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  117. package/.next/standalone/.next/server/pages/404.html +1 -1
  118. package/.next/standalone/.next/server/pages/500.html +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/server-reference-manifest.json +23 -56
  121. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 0__8a7m868fvf.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 0o6qlkgubtoex.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 13i7-9is-vhys.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
  126. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
  127. package/.next/standalone/.next/static/chunks/{2_pltstd8-xgs.js → 2klitrtzpaoe0.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 2mdh397ghgnvv.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
  130. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
  131. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +69 -0
  132. package/.next/standalone/.next/static/chunks/{1qd741hzlmjbo.js → 43ufqrz8qo3h-.js} +1 -1
  133. package/.next/standalone/SECURITY.md +53 -0
  134. package/.next/standalone/app/actions/pack-actions.ts +0 -12
  135. package/.next/standalone/app/policies/hooks-client.tsx +0 -9
  136. package/.next/standalone/app/settings/page.tsx +1 -20
  137. package/.next/standalone/app/settings/settings-client.tsx +1 -27
  138. package/.next/standalone/app/settings/settings.css +0 -79
  139. package/.next/standalone/package.json +10 -10
  140. package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
  141. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
  142. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
  143. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
  144. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
  145. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
  146. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
  147. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
  148. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
  150. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
  151. package/.next/standalone/server.js +1 -1
  152. package/README.md +2 -2
  153. package/bin/failproofai.mjs +2 -115
  154. package/dist/cli.mjs +6543 -13872
  155. package/dist/index.js +1 -19
  156. package/dist/worker.mjs +2022 -8055
  157. package/package.json +10 -10
  158. package/pi-extension/index.ts +0 -11
  159. package/scripts/build-policy-pack.mjs +2 -53
  160. package/src/audit/features.ts +2 -3
  161. package/src/hooks/builtin-policies.ts +8 -177
  162. package/src/hooks/cloud-enrollment-cli.ts +1 -1
  163. package/src/hooks/cloud-managed-policies.ts +0 -22
  164. package/src/hooks/custom-hooks-loader.ts +7 -45
  165. package/src/hooks/custom-hooks-registry.ts +1 -45
  166. package/src/hooks/first-run-gate.ts +0 -5
  167. package/src/hooks/fp-home.ts +0 -23
  168. package/src/hooks/handler.ts +6 -265
  169. package/src/hooks/hook-activity-store.ts +1 -105
  170. package/src/hooks/hook-telemetry.ts +0 -41
  171. package/src/hooks/loader-utils.ts +0 -6
  172. package/src/hooks/manager.ts +1 -1
  173. package/src/hooks/pack-cli.ts +19 -412
  174. package/src/hooks/pack-manifest.ts +7 -479
  175. package/src/hooks/pack-store.ts +11 -157
  176. package/src/hooks/policy-catalog.ts +0 -204
  177. package/src/hooks/policy-evaluator.ts +796 -940
  178. package/src/hooks/policy-registry.ts +0 -25
  179. package/src/hooks/policy-types.ts +0 -126
  180. package/src/hooks/types.ts +1 -1
  181. package/src/hooks/worker-server.ts +26 -119
  182. package/src/index.ts +0 -6
  183. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  184. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
  191. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
  192. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
  193. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
  194. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  195. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
  196. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
  197. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
  198. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
  199. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
  200. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
  201. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
  202. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
  203. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
  204. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
  206. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  207. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
  208. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
  209. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +0 -151
  210. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  211. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  212. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  213. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  214. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  215. package/.next/standalone/.next/static/chunks/043j99m8ykg__.css +0 -2
  216. package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
  217. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
  218. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
  219. package/.next/standalone/.next/static/chunks/3otmypm6j_xfo.js +0 -6
  220. package/.next/standalone/.next/static/chunks/3yxro_r2_o9ad.js +0 -69
  221. package/.next/standalone/PROBE-FOLLOWUP.md +0 -186
  222. package/.next/standalone/app/actions/get-jev-config.ts +0 -424
  223. package/.next/standalone/app/actions/update-jev-config.ts +0 -420
  224. package/.next/standalone/app/components/jev-notices.tsx +0 -96
  225. package/.next/standalone/app/settings/jev-panel.tsx +0 -473
  226. package/src/hooks/effective-reviewers.ts +0 -79
  227. package/src/hooks/jev-activity.ts +0 -385
  228. package/src/hooks/jev-cli.ts +0 -1249
  229. package/src/hooks/policy-authority.ts +0 -333
  230. package/src/hooks/policy-reviewability.ts +0 -229
  231. package/src/hooks/semantic/combine.ts +0 -541
  232. package/src/hooks/semantic/compile.ts +0 -176
  233. package/src/hooks/semantic/decide.ts +0 -392
  234. package/src/hooks/semantic/envelope.ts +0 -1296
  235. package/src/hooks/semantic/evaluator.ts +0 -547
  236. package/src/hooks/semantic/facts.ts +0 -292
  237. package/src/hooks/semantic/intent.ts +0 -1190
  238. package/src/hooks/semantic/jev-client.ts +0 -643
  239. package/src/hooks/semantic/jev-config.ts +0 -594
  240. package/src/hooks/semantic/jev-review.ts +0 -374
  241. package/src/hooks/semantic/jev-stats.ts +0 -289
  242. package/src/hooks/semantic/jev-throttle.ts +0 -421
  243. package/src/hooks/semantic/pack-policies.ts +0 -251
  244. package/src/hooks/semantic/policies.ts +0 -596
  245. package/src/hooks/semantic/precondition-names.ts +0 -60
  246. package/src/hooks/semantic/preconditions.ts +0 -58
  247. package/src/hooks/semantic/redact.ts +0 -2910
  248. package/src/hooks/semantic/types.ts +0 -145
  249. package/src/hooks/semver-precedence.ts +0 -128
  250. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
  251. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
  252. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → PgeWCHmyVbjRznv2VO7KF}/_ssgManifest.js +0 -0
@@ -1,421 +0,0 @@
1
- /**
2
- * Cache and rate limit in front of a Jev transport, so a burst of tool calls
3
- * stays under the provider's limit instead of degrading silently.
4
- *
5
- * `throttleTransport(t, opts?)` returns a transport with the same contract as
6
- * `t`. It adds two things and nothing else:
7
- *
8
- * - **An LRU cache** keyed by the SHA-256 of the full request (model, state
9
- * and questions, byte for byte as serialized, plus an optional `scope`).
10
- * The request is everything Jev sees — no clock, no randomness — so an
11
- * identical request is an identical question, and its answer is reused for
12
- * a short TTL. The TTL runs from when the answer was fetched; a hit never
13
- * extends it. Only answers the evaluator accepts are stored (`readAnswers`:
14
- * a probability in [0, 1] for every question asked, from a model it
15
- * accepts), bounded by entry count and by size, and
16
- * every hit hands back a fresh copy, so a caller mutating its response can
17
- * never change what the next caller gets. A side effect worth having: within
18
- * the TTL, retrying a denied call gets the same answer rather than a fresh
19
- * draw from a model that is ~98% run-to-run stable.
20
- * - **A token bucket** (default 5 req/s, burst 5; see `ThrottleOptions` for
21
- * what those bound) on the calls that actually go upstream; cache hits are
22
- * free. Over budget, it throws
23
- * `JevError("rate-limited")` at once, so the evaluator degrades and the
24
- * caller falls back to the regex result with a recorded reason — visibly,
25
- * not silently. `maxWaitMs` lets a call wait briefly for the next token
26
- * instead. A provider 429 empties the bucket as of when the 429 arrived
27
- * (however long that round-trip took), so the calls right behind it fall
28
- * back locally instead of spending more requests on a full window.
29
- *
30
- * It never throws on its own except for those two cases: an over-budget call,
31
- * and a signal that is already aborted (or aborts while waiting for a token).
32
- * Every error from the wrapped transport is rethrown unchanged — the same
33
- * object — and never cached. Internal failures (a request that cannot be
34
- * serialized, say) skip the cache; they never fail the call.
35
- *
36
- * **State is module-level.** The daemon's warm worker (`worker-server.ts`) is
37
- * the only evaluator on a configured machine and lives for hours, so the
38
- * process is the cache and the bucket is the process's upstream budget: a
39
- * caller may build a new wrapper per hook event and every wrapper shares both.
40
- * The worker also serializes `evaluateHookEvent`, so there is never more than
41
- * one Jev call in flight and no in-flight de-duplication is needed.
42
- *
43
- * **No on-disk cache**, deliberately. It would only help the one-shot
44
- * in-process path, which on a configured machine does not exist (CLAUDE.md,
45
- * "Enforcement routes through the daemon": only this repo's dogfood configs
46
- * and not-yet-set-up machines evaluate in-process). And it would be a
47
- * poisoning surface: every file under `~/.failproofai` is writable by the same
48
- * user the agent runs as, and the request format is open source, so the agent
49
- * being judged could compute the digest of the call it is about to make and
50
- * pre-write an all-clear answer — clearing reviewable denies without Jev ever
51
- * being asked. The worker's memory has no such surface. Signing the entries
52
- * would not help: any key the hook can read, the agent can read too.
53
- *
54
- * **What a caller wiring this in must do** (T3), because the module cannot see
55
- * any of it from here:
56
- *
57
- * - **Pass `scope`** naming where answers come from, e.g.
58
- * `` `${route.via}|${route.endpoint}|${route.model}` `` from T1's route. The
59
- * request alone does not say which provider, endpoint or model answered — a
60
- * transport may send a model other than `request.model` — and the cache is
61
- * process-wide, so without a scope a `jev.json` change in a long-lived
62
- * worker serves the old provider's answers under the new one until they
63
- * expire. The scope is only ever hashed into the key, never stored, so an
64
- * endpoint carrying a credential in its query string is safe to pass. It
65
- * cannot default to anything better: the transport's identity changes on
66
- * every hook event (a new wrapper each time), which would turn the cache off.
67
- * - **A hit is the original answer, `usage` included**, returned in ~0 ms.
68
- * Anything that sums tokens or cost, or takes latency percentiles, must tell
69
- * hits apart. `evaluateSemantic` never exposes the response, so wrap the
70
- * throttled transport and ask {@link isCachedJevResponse}:
71
- * `async (r, s) => { const res = await throttled(r, s); cached = isCachedJevResponse(res); return res; }`
72
- * then record `inputTokens: null` (and flag the latency) when `cached`.
73
- * - **Tests** that route through a throttled transport share this module's
74
- * state across every file bun runs in one process: call
75
- * `resetJevThrottle()` in `beforeEach`, or pass
76
- * `{ cacheTtlMs: 0, ratePerSec: 1e6 }` where caching and limiting are not
77
- * what is under test.
78
- */
79
- import { createHash } from "node:crypto";
80
- import { JevError, readAnswers, type JevTransport } from "./jev-client";
81
- import type { JevRequest, JevResponse } from "./types";
82
-
83
- export interface ThrottleOptions {
84
- /**
85
- * Sustained upstream requests per second. Default 5, from the one Cloudflare
86
- * figure there is: a replay against one key sustained ~6 calls/s before
87
- * HTTP 429s took over (measured over the replay as a whole, not over any
88
- * one-second window). Cloudflare publishes its Workers AI limits per
89
- * minute (300/min for text generation by default, i.e. 5/s); whether
90
- * `typesafe/jev` is under that default, and so which window a burst runs
91
- * into, is unconfirmed (R4 or a live check). TypeSafe documents 1,200/min
92
- * (20/s). Must be a positive finite number, else the default.
93
- */
94
- ratePerSec?: number;
95
- /**
96
- * Bucket capacity — how many calls may go out back to back. Default: one
97
- * second's worth (`ratePerSec`, at least 1), so a handful of parallel tool
98
- * calls after a pause are all judged. A bucket admits at most
99
- * `burst + ratePerSec` calls in any one-second window (10 with the
100
- * defaults) and `burst + 60 × ratePerSec` in any minute (305). Against a
101
- * per-minute limit that is the sustained rate plus one burst; against a
102
- * strict per-second one it could spend a few calls a provider refuses, and
103
- * the first 429 then empties the bucket, so the loss stays at a few calls
104
- * per burst. Pass `burst: 1` to cap any one second at `1 + ratePerSec`.
105
- */
106
- burst?: number;
107
- /** How long a call may wait for the next token before it is rate-limited. Default 0: over budget fails at once. */
108
- maxWaitMs?: number;
109
- /** How long an answer is reused, from when it was fetched. Default 5 minutes. 0 disables the cache. */
110
- cacheTtlMs?: number;
111
- /** Most answers kept. Default 512. 0 disables the cache. */
112
- cacheMaxEntries?: number;
113
- /** Most characters of serialized answers kept (≈ bytes; answers are ASCII JSON). Default 4 MiB. 0 disables the cache. */
114
- cacheMaxBytes?: number;
115
- /**
116
- * Mixed into the cache key (hashed, never stored): where the answers came
117
- * from — provider, endpoint and the model actually sent, e.g.
118
- * `` `${via}|${endpoint}|${model}` ``. Answers cached under one scope are
119
- * never served under another. Default `""`, which every unscoped wrapper in
120
- * the process shares: a real caller should always pass one (see the module
121
- * doc).
122
- */
123
- scope?: string;
124
- /** Monotonic clock in milliseconds. For tests; defaults to `performance.now()`. */
125
- now?: () => number;
126
- }
127
-
128
- export const DEFAULT_THROTTLE = Object.freeze({
129
- ratePerSec: 5,
130
- maxWaitMs: 0,
131
- cacheTtlMs: 5 * 60_000,
132
- cacheMaxEntries: 512,
133
- cacheMaxBytes: 4 * 1024 * 1024,
134
- });
135
-
136
- /** One answer bigger than this is never cached. A full request's answers are a few KB. */
137
- export const MAX_CACHED_ENTRY_CHARS = 64 * 1024;
138
-
139
- interface ResolvedOptions {
140
- ratePerSec: number;
141
- burst: number;
142
- maxWaitMs: number;
143
- cacheEnabled: boolean;
144
- cacheTtlMs: number;
145
- cacheMaxEntries: number;
146
- cacheMaxBytes: number;
147
- scope: string;
148
- now: () => number;
149
- }
150
-
151
- const positive = (v: unknown, fallback: number): number =>
152
- typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
153
- const nonNegative = (v: unknown, fallback: number): number =>
154
- typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : fallback;
155
-
156
- function resolveOptions(opts: ThrottleOptions): ResolvedOptions {
157
- const ratePerSec = positive(opts.ratePerSec, DEFAULT_THROTTLE.ratePerSec);
158
- const cacheTtlMs = nonNegative(opts.cacheTtlMs, DEFAULT_THROTTLE.cacheTtlMs);
159
- const cacheMaxEntries = Math.floor(nonNegative(opts.cacheMaxEntries, DEFAULT_THROTTLE.cacheMaxEntries));
160
- const cacheMaxBytes = nonNegative(opts.cacheMaxBytes, DEFAULT_THROTTLE.cacheMaxBytes);
161
- return {
162
- ratePerSec,
163
- burst: Math.max(1, positive(opts.burst, ratePerSec)),
164
- maxWaitMs: nonNegative(opts.maxWaitMs, DEFAULT_THROTTLE.maxWaitMs),
165
- cacheEnabled: cacheTtlMs > 0 && cacheMaxEntries > 0 && cacheMaxBytes > 0,
166
- cacheTtlMs,
167
- cacheMaxEntries,
168
- cacheMaxBytes,
169
- scope: typeof opts.scope === "string" ? opts.scope : "",
170
- now: typeof opts.now === "function" ? opts.now : () => performance.now(),
171
- };
172
- }
173
-
174
- // ── Module state (shared by every wrapper in the process) ────────────────────
175
-
176
- interface Entry {
177
- body: string;
178
- expiresAt: number;
179
- size: number;
180
- }
181
-
182
- const cache = new Map<string, Entry>();
183
- let cacheChars = 0;
184
- /** Tokens may go negative: a waiting call reserves the next one. NaN = not yet used. */
185
- const bucket = { tokens: Number.NaN, last: Number.NaN };
186
- const counters = { hits: 0, misses: 0, rateLimited: 0, upstreamErrors: 0, evictions: 0 };
187
- /** Responses this module handed out from the cache; see {@link isCachedJevResponse}. */
188
- let servedFromCache = new WeakSet<object>();
189
-
190
- export interface JevThrottleStats {
191
- /** Answers served from the cache. */
192
- hits: number;
193
- /** Lookups that found nothing (cache enabled only). */
194
- misses: number;
195
- /** Calls refused locally because the bucket was empty. */
196
- rateLimited: number;
197
- /** Errors the wrapped transport threw (passed through). */
198
- upstreamErrors: number;
199
- /** Entries dropped to stay within the size bounds (not counting expiry). */
200
- evictions: number;
201
- /** Answers currently cached. */
202
- entries: number;
203
- /** Characters currently cached. */
204
- chars: number;
205
- }
206
-
207
- export function jevThrottleStats(): JevThrottleStats {
208
- return { ...counters, entries: cache.size, chars: cacheChars };
209
- }
210
-
211
- /** Empties the cache and the bucket and zeroes the counters. */
212
- export function resetJevThrottle(): void {
213
- cache.clear();
214
- cacheChars = 0;
215
- bucket.tokens = Number.NaN;
216
- bucket.last = Number.NaN;
217
- counters.hits = 0;
218
- counters.misses = 0;
219
- counters.rateLimited = 0;
220
- counters.upstreamErrors = 0;
221
- counters.evictions = 0;
222
- servedFromCache = new WeakSet<object>();
223
- }
224
-
225
- /**
226
- * True when `response` came from the cache rather than the network — for a
227
- * caller that wants to record it. A hit costs nothing and takes ~0 ms, but it
228
- * still carries the original answer's `usage`: token and cost totals must
229
- * skip it. Through `evaluateSemantic`, wrap the throttled transport to see
230
- * the response (module doc).
231
- */
232
- export function isCachedJevResponse(response: unknown): boolean {
233
- return typeof response === "object" && response !== null && servedFromCache.has(response);
234
- }
235
-
236
- // ── Cache ────────────────────────────────────────────────────────────────────
237
-
238
- /**
239
- * The cache key: SHA-256 over the scope and the full request exactly as
240
- * serialized. Exported so a replay (R4) can measure hit rates with the same
241
- * key the worker uses. Throws if the request cannot be serialized.
242
- */
243
- export function jevRequestDigest(request: JevRequest, scope = ""): string {
244
- return createHash("sha256").update(scope).update("\0").update(JSON.stringify(request)).digest("hex");
245
- }
246
-
247
- function keyFor(cfg: ResolvedOptions, request: JevRequest): string | null {
248
- if (!cfg.cacheEnabled) return null;
249
- try {
250
- return jevRequestDigest(request, cfg.scope);
251
- } catch {
252
- return null; // Unserializable: no cache for this call, never an error.
253
- }
254
- }
255
-
256
- function dropEntry(key: string, entry: Entry): void {
257
- cache.delete(key);
258
- cacheChars -= entry.size;
259
- }
260
-
261
- function cacheGet(key: string, now: number): JevResponse | null {
262
- const entry = cache.get(key);
263
- if (!entry) return null;
264
- if (now >= entry.expiresAt) {
265
- dropEntry(key, entry);
266
- return null;
267
- }
268
- // Most recently used goes last; eviction takes from the front.
269
- cache.delete(key);
270
- cache.set(key, entry);
271
- try {
272
- return JSON.parse(entry.body) as JevResponse;
273
- } catch {
274
- dropEntry(key, entry);
275
- return null;
276
- }
277
- }
278
-
279
- /**
280
- * Only an answer the evaluator could use is worth keeping. The rule is the
281
- * evaluator's own (`readAnswers`: a probability in [0, 1] for every question,
282
- * and a model it accepts), not a copy of it, so the two cannot drift apart: a
283
- * wrong-model answer the evaluator rejects is never pinned for the TTL.
284
- */
285
- function isUsableAnswer(request: JevRequest, response: JevResponse): boolean {
286
- try {
287
- readAnswers(request, response);
288
- return true;
289
- } catch {
290
- return false;
291
- }
292
- }
293
-
294
- function cachePut(cfg: ResolvedOptions, key: string, request: JevRequest, response: JevResponse, now: number): void {
295
- if (!isUsableAnswer(request, response)) return;
296
- let body: string;
297
- try {
298
- body = JSON.stringify(response);
299
- } catch {
300
- return;
301
- }
302
- const size = key.length + body.length;
303
- if (size > MAX_CACHED_ENTRY_CHARS || size > cfg.cacheMaxBytes) return;
304
- const prev = cache.get(key);
305
- if (prev) dropEntry(key, prev);
306
- cache.set(key, { body, expiresAt: now + cfg.cacheTtlMs, size });
307
- cacheChars += size;
308
- for (const [k, e] of cache) {
309
- const expired = now >= e.expiresAt;
310
- if (!expired && cache.size <= cfg.cacheMaxEntries && cacheChars <= cfg.cacheMaxBytes) break;
311
- dropEntry(k, e);
312
- if (!expired) counters.evictions++;
313
- }
314
- }
315
-
316
- // ── Token bucket ─────────────────────────────────────────────────────────────
317
-
318
- /** Error codes meaning the provider itself refused for rate: `http-429` from the HTTP layer, or a transport's own `rate-limited`. */
319
- const PROVIDER_WINDOW_FULL = new Set(["http-429", "rate-limited"]);
320
-
321
- function refill(cfg: ResolvedOptions, now: number): void {
322
- if (!Number.isFinite(bucket.last) || !Number.isFinite(bucket.tokens)) {
323
- bucket.tokens = cfg.burst;
324
- bucket.last = now;
325
- return;
326
- }
327
- const elapsed = Math.max(0, now - bucket.last);
328
- bucket.last = now;
329
- bucket.tokens = Math.min(cfg.burst, bucket.tokens + (elapsed * cfg.ratePerSec) / 1000);
330
- }
331
-
332
- function abortedError(signal: AbortSignal): JevError {
333
- const name = (signal.reason as { name?: unknown } | undefined)?.name;
334
- return name === "TimeoutError"
335
- ? new JevError("timeout", "Jev did not answer in time")
336
- : new JevError("aborted", "the Jev request was aborted");
337
- }
338
-
339
- function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
340
- return new Promise<void>((resolve, reject) => {
341
- const onAbort = () => {
342
- clearTimeout(timer);
343
- reject(abortedError(signal as AbortSignal));
344
- };
345
- const timer = setTimeout(() => {
346
- signal?.removeEventListener("abort", onAbort);
347
- resolve();
348
- }, ms);
349
- signal?.addEventListener("abort", onAbort, { once: true });
350
- });
351
- }
352
-
353
- /** Takes one token, waiting up to `maxWaitMs` for it, or throws `rate-limited`. */
354
- async function acquire(cfg: ResolvedOptions, signal: AbortSignal | undefined): Promise<void> {
355
- refill(cfg, cfg.now());
356
- bucket.tokens -= 1;
357
- if (bucket.tokens >= 0) return;
358
- const waitMs = (-bucket.tokens / cfg.ratePerSec) * 1000;
359
- if (waitMs > cfg.maxWaitMs) {
360
- bucket.tokens += 1;
361
- counters.rateLimited++;
362
- throw new JevError(
363
- "rate-limited",
364
- `local Jev budget of ${cfg.ratePerSec} request(s)/s is spent; next slot in ${Math.ceil(waitMs)} ms`,
365
- );
366
- }
367
- try {
368
- await sleep(waitMs, signal);
369
- } catch (err) {
370
- bucket.tokens += 1; // The reserved slot was never used.
371
- throw err;
372
- }
373
- }
374
-
375
- // ── The wrapper ──────────────────────────────────────────────────────────────
376
-
377
- /**
378
- * `t` behind the shared cache and token bucket. `opts` is read once, here;
379
- * every call through the returned transport uses it.
380
- */
381
- export function throttleTransport(t: JevTransport, opts?: ThrottleOptions | null): JevTransport {
382
- const cfg = resolveOptions(opts ?? {});
383
- return async (request, signal) => {
384
- // The type says a signal is always passed; a caller that omits one still gets no throw from here.
385
- if (signal?.aborted) throw abortedError(signal);
386
-
387
- const key = keyFor(cfg, request);
388
- if (key !== null) {
389
- const hit = cacheGet(key, cfg.now());
390
- if (hit) {
391
- counters.hits++;
392
- servedFromCache.add(hit);
393
- return hit;
394
- }
395
- counters.misses++;
396
- }
397
-
398
- await acquire(cfg, signal);
399
-
400
- let response: JevResponse;
401
- try {
402
- response = await t(request, signal);
403
- } catch (err) {
404
- counters.upstreamErrors++;
405
- // The provider's window is full: the calls right behind this one fall
406
- // back locally until the bucket refills, rather than each spending a
407
- // request to learn the same thing. Refill to now first: the bucket was
408
- // last stamped when this call was admitted, so without it the next
409
- // refill would credit the whole round-trip (hundreds of ms against a
410
- // real provider, i.e. one or more tokens) and undo the emptying.
411
- if (err instanceof JevError && PROVIDER_WINDOW_FULL.has(err.code)) {
412
- refill(cfg, cfg.now());
413
- bucket.tokens = Math.min(bucket.tokens, 0);
414
- }
415
- throw err;
416
- }
417
-
418
- if (key !== null) cachePut(cfg, key, request, response, cfg.now());
419
- return response;
420
- };
421
- }
@@ -1,251 +0,0 @@
1
- /**
2
- * Which semantic policy set this machine runs: a pack's, or the compiled-in one.
3
- *
4
- * ## The replacement rule
5
- *
6
- * A pack that declares at least ONE `semantic` entry replaces
7
- * `SEMANTIC_POLICIES` wholesale. It mirrors the rule already in force for the
8
- * regex builtins — installing a pack stops the compiled builtins registering —
9
- * and it exists for the same reason: one source of truth, so a name collision
10
- * between a pack's `destructive-deletion` and the builtin of that name cannot
11
- * arise, and `reviewedBy: ["destructive-deletion"]` in a pack manifest cannot
12
- * silently mean the builtin's question set instead of the pack's.
13
- *
14
- * Two packs that both declare entries CONCATENATE; a name declared by two packs
15
- * keeps the first and drops the later one, because the answer map is keyed by
16
- * policy name and the second would overwrite the first's questions in the
17
- * compiled request.
18
- *
19
- * ## Where the manifest ends and this begins
20
- *
21
- * `pack-manifest.ts` validates the shape — names, modes, probe ids, caps it can
22
- * check by counting — because it runs on every hook event and may not touch a
23
- * semantic module. Everything that needs the semantic side is here: binding a
24
- * precondition NAME to its predicate, and the request budget, which is a
25
- * character count and not a probe count.
26
- *
27
- * ## Dropping, not failing
28
- *
29
- * Every failure here drops ONE policy and records why. A semantic policy is what
30
- * CLEARS a reviewable regex verdict, so losing one leaves the regex deny
31
- * standing: noisier, never weaker. Dropping the whole set would be the same
32
- * direction but much louder, and failing the pack would deny every event its
33
- * regex policies cover — a machine locked out over a typo in the half of the
34
- * system whose job is to let more real work through.
35
- */
36
- import { hookLogWarn } from "../hook-logger";
37
- import {
38
- packSemantic,
39
- readInstalledPacks,
40
- semanticQuestions,
41
- type ResolvedPack,
42
- type SemanticManifestEntry,
43
- } from "../pack-manifest";
44
- import { MAX_REQUEST_CHARS, userAskedQuestion } from "./compile";
45
- import { MAX_STATE_CHARS } from "./envelope";
46
- import { INJECTION_PROBE, SCOPE_PROBE, SEMANTIC_POLICIES, TASK_PROBES } from "./policies";
47
- import { preconditionFor } from "./preconditions";
48
- import { isPackPreconditionName } from "./precondition-names";
49
- import type { NoulQuestion, SemanticPolicy } from "./types";
50
-
51
- /** The question object `compile.ts` builds for one probe. Shared so the measurement matches. */
52
- function noulOf(probe: { instructions: string; criteria?: { true: string; false: string } }): NoulQuestion {
53
- return { type: "noul", instructions: probe.instructions, ...(probe.criteria ? { criteria: { ...probe.criteria } } : {}) };
54
- }
55
-
56
- /**
57
- * What the questions a call does NOT get to choose already cost: the injection
58
- * probe, the v0 scope probe and the v1 task probes, whichever set is larger.
59
- *
60
- * Reserved out of the budget below because they are in every request that asks
61
- * anything, so a pack budget that ignored them would be over by their size on
62
- * every call.
63
- */
64
- const GLOBAL_QUESTION_CHARS = Math.max(
65
- JSON.stringify({ [INJECTION_PROBE.id]: noulOf(INJECTION_PROBE), [SCOPE_PROBE.id]: noulOf(SCOPE_PROBE) }).length,
66
- JSON.stringify(
67
- Object.fromEntries([INJECTION_PROBE, ...TASK_PROBES].map((p) => [p.id, noulOf(p)])),
68
- ).length,
69
- );
70
-
71
- /**
72
- * How many characters of questions every installed pack may contribute,
73
- * together.
74
- *
75
- * DERIVED, not chosen. `prepareSemantic` checks one number — the serialized
76
- * request against `MAX_REQUEST_CHARS` — and the state side of that is already
77
- * spent to the last character by `buildEnvelope` (`MAX_STATE_CHARS`), so what is
78
- * left for questions is the difference, minus the globals above. A pack set that
79
- * fits this fits at FULL state size, which is the only size worth budgeting for:
80
- * the state is a function of the caps and not of what the agent sent, so a
81
- * request that only fits on small inputs is a request that intermittently
82
- * doesn't.
83
- *
84
- * Deriving it is the point. The count caps it replaced (24 policies, 80 probes)
85
- * were set without reference to the envelope and permitted a question set 23,032
86
- * characters past what a request can carry — at which point `PreparedCall
87
- * .oversized` stops meaning "our own question set overran" and starts meaning
88
- * "somebody installed a big pack", and the flag that was supposed to be
89
- * unreachable from ordinary work becomes routine.
90
- *
91
- * The real sixteen use 19,787 characters at their worst, so this is not a
92
- * constraint on the set we ship. It is the ceiling on what a stranger's pack may
93
- * ask for.
94
- */
95
- export const MAX_PACK_QUESTION_CHARS = MAX_REQUEST_CHARS - MAX_STATE_CHARS - GLOBAL_QUESTION_CHARS;
96
-
97
- /**
98
- * The compiled cost of one entry's questions, measured the way
99
- * `prepareSemantic` measures the request: the serialized question map.
100
- *
101
- * `user_asked` is counted for an overridable policy even though the shipped
102
- * path is intent v1, which does not ask it. A budget is worst-case over the
103
- * modes a machine can run, and the alternative — a set that fits under v1 and
104
- * overruns the moment anything asks in v0 — is a size cliff that depends on a
105
- * flag rather than on the pack.
106
- */
107
- export function questionChars(entry: SemanticManifestEntry): number {
108
- const questions: Record<string, NoulQuestion> = {};
109
- for (const probe of semanticQuestions(entry)) questions[`${entry.name}.${probe.id}`] = noulOf(probe);
110
- if (entry.userCanOverride) {
111
- // It reads `title` and nothing else (`actionPhrase` in `compile.ts`), so a
112
- // stub is the honest argument here — building a whole `SemanticPolicy` to
113
- // measure one string would invite it to drift from the real conversion.
114
- questions[`${entry.name}.user_asked`] = userAskedQuestion({ title: entry.title } as SemanticPolicy);
115
- }
116
- return JSON.stringify(questions).length;
117
- }
118
-
119
- export interface ResolvedSemanticPolicies {
120
- policies: ReadonlyArray<SemanticPolicy>;
121
- /** True when a pack supplied the set, so the compiled-in one is not in play. */
122
- fromPack: boolean;
123
- /** One line per dropped policy. Diagnostics; nothing here changes a verdict. */
124
- errors: string[];
125
- }
126
-
127
- /** Turn one validated manifest entry into a policy the compiler can use. */
128
- function toSemanticPolicy(entry: SemanticManifestEntry): SemanticPolicy {
129
- // An unknown precondition name DROPS the policy (the caller catches this),
130
- // rather than compiling it with no gate at all. Ungating would be the wider
131
- // direction, not the weaker one, but it is not what the author asked for: a
132
- // question written for the one case its gate selects would then be put to Jev
133
- // on every call, and nothing would say so. Unreachable through
134
- // `parsePackSemanticPolicy`, which refuses the name; reachable if the two
135
- // precondition files ever drift, which is what this is here for.
136
- const name = entry.precondition;
137
- if (name !== undefined && !isPackPreconditionName(name)) {
138
- throw new Error(`precondition ${JSON.stringify(name)} is not a name this build has`);
139
- }
140
- const precondition = name === undefined ? null : preconditionFor(name);
141
- return {
142
- name: entry.name,
143
- title: entry.title,
144
- appliesTo: entry.appliesTo,
145
- mode: entry.mode,
146
- userCanOverride: entry.userCanOverride,
147
- probes: entry.probes,
148
- ...(entry.exempt ? { exempt: entry.exempt } : {}),
149
- // Absent, never `() => true`: `selectPolicies` treats an absent precondition
150
- // as "ask", so `precondition: "always"` and an omitted field have to compile
151
- // to the identical policy.
152
- ...(precondition ? { precondition } : {}),
153
- guidance: entry.guidance,
154
- };
155
- }
156
-
157
- /**
158
- * The semantic set these packs declare, or the compiled-in set when none of them
159
- * declares any.
160
- *
161
- * Pure: it takes the packs rather than reading them, so the caller that already
162
- * has them does not read `installed.json` a second time and a test does not need
163
- * a filesystem.
164
- */
165
- export function semanticPoliciesFromPacks(
166
- packs: ReadonlyArray<Pick<ResolvedPack, "id" | "semantic">>,
167
- ): ResolvedSemanticPolicies {
168
- const declared = packs.filter((p) => packSemantic(p).length > 0);
169
- if (declared.length === 0) return { policies: SEMANTIC_POLICIES, fromPack: false, errors: [] };
170
-
171
- const policies: SemanticPolicy[] = [];
172
- const errors: string[] = [];
173
- const seen = new Set<string>();
174
- let spent = 0;
175
- for (const pack of declared) {
176
- for (const entry of packSemantic(pack)) {
177
- if (seen.has(entry.name)) {
178
- errors.push(`pack ${pack.id} declares semantic policy ${entry.name}, which another pack already declared`);
179
- continue;
180
- }
181
- const cost = questionChars(entry);
182
- if (spent + cost > MAX_PACK_QUESTION_CHARS) {
183
- errors.push(
184
- `pack ${pack.id} semantic policy ${entry.name} was dropped: its questions need ${cost} characters and ` +
185
- `only ${Math.max(0, MAX_PACK_QUESTION_CHARS - spent)} of the ${MAX_PACK_QUESTION_CHARS}-character ` +
186
- `question budget one Jev request has are left`,
187
- );
188
- continue;
189
- }
190
- try {
191
- policies.push(toSemanticPolicy(entry));
192
- } catch (err) {
193
- errors.push(`pack ${pack.id} semantic policy ${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
194
- continue;
195
- }
196
- seen.add(entry.name);
197
- spent += cost;
198
- }
199
- }
200
-
201
- // Every entry a declaring pack shipped was unusable. The compiled-in set is
202
- // the honest answer — it is what the machine ran yesterday — and it is also
203
- // the safe one: what a pack's regex half names in `reviewedBy` will not match
204
- // it, so those policies stay hard rather than being cleared by questions
205
- // nobody validated.
206
- if (policies.length === 0) return { policies: SEMANTIC_POLICIES, fromPack: false, errors };
207
- return { policies, fromPack: true, errors };
208
- }
209
-
210
- const warned = new Set<string>();
211
-
212
- /**
213
- * The live set, read from the installed packs.
214
- *
215
- * Called from `prepareSemantic`, so only on a machine that has a Jev config and
216
- * is preparing a request. It re-reads the manifest rather than caching: the
217
- * daemon's warm worker lives for hours, a pack can be installed or upgraded
218
- * under it, and `readInstalledPacks` re-verifies each artifact digest for
219
- * exactly that reason. The read costs one digest per pack against a call that is
220
- * about to spend up to three seconds on the network.
221
- *
222
- * Never throws. An unreadable manifest yields the compiled-in set, which is the
223
- * same fail-open posture every other reader of that file takes, and here it also
224
- * fails in the safe direction: the set a pack's `reviewedBy` names is not the
225
- * builtin set, so nothing gets cleared by a policy nobody could read.
226
- */
227
- export function resolveSemanticPolicies(): ReadonlyArray<SemanticPolicy> {
228
- let packs: ReadonlyArray<ResolvedPack> = [];
229
- let manifestErrors: string[] = [];
230
- try {
231
- const read = readInstalledPacks();
232
- packs = read.packs;
233
- manifestErrors = read.warnings ?? [];
234
- } catch {
235
- return SEMANTIC_POLICIES;
236
- }
237
- const resolved = semanticPoliciesFromPacks(packs);
238
- // Once per process per message, like `warnAuthority`: this runs on every gate
239
- // event, and in the warm worker that is every tool call of every session.
240
- for (const message of [...manifestErrors, ...resolved.errors]) {
241
- if (warned.has(message)) continue;
242
- warned.add(message);
243
- hookLogWarn(message);
244
- }
245
- return resolved.policies;
246
- }
247
-
248
- /** Forget which warnings were already said. Tests only. */
249
- export function _resetSemanticWarningsForTest(): void {
250
- warned.clear();
251
- }