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,1249 +0,0 @@
1
- /**
2
- * `failproofai jev` — configure the customer's own Jev endpoint and key (BYOK),
3
- * the single opt-in to the two-tier hook evaluator.
4
- *
5
- * jev --url <url> [--token <token>]
6
- * the one-shot form: the same thing as `setup`, with the provider
7
- * read off the URL's host
8
- * jev setup write ~/.failproofai/jev.json at 0600 from flags + a key
9
- * jev status what is configured, whether it is being used, how much of the
10
- * enabled policy set Jev is allowed to clear, and how Jev has
11
- * been doing (fallbacks, latency, clears) — never the key
12
- * jev test one tiny live request: latency and the Jev version that answered
13
- * jev remove delete the file; hooks go back to the regex engine unchanged
14
- *
15
- * # The provider is read off the URL
16
- *
17
- * `--url` names the endpoint, and its HOST says which provider that is
18
- * (`PROVIDER_BY_HOST`); anything else is `custom` with that URL as its base.
19
- * `--provider` still overrides the inference, but a provider that contradicts
20
- * the URL's host — `--provider openrouter` against `api.typesafe.ai` — is
21
- * refused rather than guessed at, because the two spellings disagree about
22
- * where the key is about to be sent. `--url` is otherwise exactly
23
- * `--base-url`: it is validated by the same `validateBaseUrl` the loader uses
24
- * and refused in the same words, and when it names the provider's own API it
25
- * clears the override rather than writing it back.
26
- *
27
- * The provider NAME is validated before any of that, whatever else is on the
28
- * command line: a spelling that is not a provider — `OpenRouter`, a typo, a
29
- * pasted key — is refused, never replaced by the one the host implies. Two
30
- * combinations the URL settles rather than saves: `--provider custom` against
31
- * `api.cloudflare.com`, which `custom` cannot speak (Workers AI is per-account
32
- * and wraps its answers), and `--account-id` for any provider but cloudflare,
33
- * which is the only one whose route reads it. And a URL that keeps the provider
34
- * but leaves its API base — `https://api.typesafe.ai/v2` — is saved with a
35
- * warning naming the endpoint requests will actually go to, because nothing
36
- * else would say so until the fallback counts did.
37
- *
38
- * # The key is never echoed
39
- *
40
- * `--token <value>` exists because one command that configures Jev is worth
41
- * having, but it is not the recommended spelling and says so at the end of
42
- * every run that uses it: a command-line argument lands in shell history and
43
- * is readable from `ps` — from `/proc` — by anything running as this user.
44
- * The key otherwise arrives on stdin (`--key-stdin`), at a masked prompt when
45
- * stdin is a terminal, or — for people who keep keys off disk — from
46
- * `FAILPROOFAI_JEV_API_KEY` at run time (`--key-from-env`). `--key-stdin` on a
47
- * terminal uses the masked prompt too, because a cooked-mode read would echo
48
- * each character as it is typed. No output of this module, human or `--json`,
49
- * contains the key; provider error text is scrubbed of it in `jev-client.ts`,
50
- * and `jev test` scrubs whatever error it prints once more. Nothing here
51
- * repeats a value it could not use (a stray argument, an unknown provider, an
52
- * unreadable timeout, a model id shaped like a key, an option that is not one,
53
- * or an unknown subcommand that is not shaped like one): a key pasted into the
54
- * wrong place on the command line is already in shell history, and does not
55
- * also need to be on the screen.
56
- *
57
- * # A stored key stays with its host
58
- *
59
- * Re-running `setup` for the same provider keeps the stored key. When the new
60
- * `--base-url` moves requests to a different origin — other than the
61
- * provider's own API — the key is asked for again, exactly as on a provider
62
- * switch: for `custom` (and any override) the URL is what picks the gateway,
63
- * and a key issued for one gateway must not be sent to another unasked.
64
- *
65
- * A file that was open to other users is the loader's "someone else may have
66
- * chosen the endpoint" case, and re-saving it at 0600 must not undo that: its
67
- * stored key is carried only to the provider's own API. Any other endpoint it
68
- * names — kept or passed again with `--base-url` — needs the key again, given
69
- * explicitly (not at a bare prompt that would not say where it goes). `jev
70
- * status` shows such a file's endpoint next to its `chmod 600` hint.
71
- *
72
- * # No restart
73
- *
74
- * Hooks read the file on every event (see `loadJevConfig`), so a setup, a mode
75
- * switch or a remove applies on the next tool call, daemon or not.
76
- */
77
- import { chmodSync, existsSync, statSync, unlinkSync } from "node:fs";
78
- import { dirname } from "node:path";
79
- import { writeJsonAtomically } from "../../lib/atomic-write";
80
- import {
81
- DEFAULT_JEV_MODE,
82
- JEV_API_KEY_ENV,
83
- JEV_CONFIG_DEFAULT_TIMEOUT_MS,
84
- JEV_PROVIDER_KINDS,
85
- inspectJevConfig,
86
- jevConfigPath,
87
- readJevConfigFileForUpdate,
88
- validateApiKey,
89
- validateBaseUrl,
90
- validateJevConfig,
91
- type JevConfig,
92
- type JevProviderKind,
93
- } from "./semantic/jev-config";
94
- import {
95
- JEV_PROVIDER_DEFAULTS,
96
- JevError,
97
- displayEndpoint,
98
- jevRoute,
99
- readAnswers,
100
- scrubSecret,
101
- transportForConfig,
102
- } from "./semantic/jev-client";
103
- import {
104
- reviewableProblem,
105
- reviewableSummary,
106
- surveyReviewableCoverage,
107
- type ReviewableCoverage,
108
- } from "./policy-reviewability";
109
- import { jevStats, type JevStats } from "./semantic/jev-stats";
110
- import type { JevRequest } from "./semantic/types";
111
- import { emptyState, nextStep, note, optsFor, rows, rule, stack, title, warning, type RenderOpts } from "./tui";
112
-
113
- export interface JevCliResult {
114
- lines: string[];
115
- exitCode: number;
116
- /** `--json`: printed verbatim instead of `lines`. */
117
- json?: string;
118
- }
119
-
120
- export interface JevCliDeps {
121
- /** Everything piped on stdin (for `--key-stdin` off a terminal). */
122
- readStdin?: () => Promise<string>;
123
- /** A masked one-line prompt; null when cancelled. */
124
- promptKey?: () => Promise<string | null>;
125
- stdinIsTTY?: boolean;
126
- /** Budget for `jev test`'s single request. */
127
- testTimeoutMs?: number;
128
- render?: RenderOpts;
129
- }
130
-
131
- const ok = (lines: string[], json?: string): JevCliResult => ({ lines, exitCode: 0, ...(json !== undefined ? { json } : {}) });
132
- const fail = (lines: string[], json?: string): JevCliResult => ({ lines, exitCode: 1, ...(json !== undefined ? { json } : {}) });
133
-
134
- export const JEV_USAGE = [
135
- "Usage:",
136
- " failproofai jev --url <url> [--key-stdin | --token <token>] [options]",
137
- " failproofai jev setup --provider <kind> [--key-stdin | --key-from-env] [options]",
138
- " failproofai jev status [--json]",
139
- " failproofai jev test [--json]",
140
- " failproofai jev remove",
141
- ];
142
-
143
- /** One live `jev test` request: small, harmless, and with an answer that is obviously right. */
144
- export const JEV_TEST_QUESTION_ID = "jev_test";
145
- const DEFAULT_TEST_TIMEOUT_MS = 15_000;
146
- /** Validation-only placeholder for a config whose key comes from the environment. Never written. */
147
- const ENV_KEY_STAND_IN = "env-key-stand-in";
148
-
149
- // ── Argument parsing ─────────────────────────────────────────────────────────
150
-
151
- /**
152
- * Every subcommand's answer to a stray argument. Never the argument itself: the
153
- * likeliest one is a key pasted in the wrong place, which is already in shell
154
- * history and does not also need to be on the screen.
155
- */
156
- const STRAY_ARGUMENT = "Unexpected argument (not repeated here, in case it is a key). The key goes on stdin: --key-stdin.";
157
-
158
- /** What a mistyped subcommand looks like, and no key does: short, lower-case letters and dashes. */
159
- const SUBCOMMAND_SHAPE = /^[a-z][a-z-]{0,20}$/;
160
-
161
- const VALUE_FLAGS = new Set(["--provider", "--url", "--token", "--base-url", "--account-id", "--model", "--timeout-ms", "--mode"]);
162
- const BOOL_FLAGS = new Set(["--key-stdin", "--key-from-env", "--json"]);
163
-
164
- /** What an option name looks like, and no key does — the same idea as `SUBCOMMAND_SHAPE`. */
165
- const OPTION_SHAPE = /^--?[a-z][a-z0-9-]{0,30}$/;
166
-
167
- /**
168
- * An unknown option, named only when it is shaped like an option name. A
169
- * mistyped `--token` is the one place a key is most likely to arrive glued to
170
- * an option (`-token=<key>`, `--tokn=<key>`), so what is echoed is the name
171
- * that failed to parse and never the value behind it.
172
- */
173
- function unknownOption(arg: string): string {
174
- return OPTION_SHAPE.test(arg) ? `Unknown option: ${arg}` : "Unknown option (not repeated here, in case it carries a key).";
175
- }
176
-
177
- interface Parsed {
178
- values: Map<string, string>;
179
- bools: Set<string>;
180
- positionals: string[];
181
- }
182
-
183
- function parseFlags(argv: string[], allowed: Set<string>): Parsed | string {
184
- const values = new Map<string, string>();
185
- const bools = new Set<string>();
186
- const positionals: string[] = [];
187
- for (let i = 0; i < argv.length; i++) {
188
- let arg = argv[i];
189
- let inline: string | undefined;
190
- // Split on `=` for a single dash too (`-token=<key>`): a value must never
191
- // survive into the "unknown option" message below.
192
- if (arg.startsWith("-") && arg.includes("=")) {
193
- inline = arg.slice(arg.indexOf("=") + 1);
194
- arg = arg.slice(0, arg.indexOf("="));
195
- }
196
- if (!arg.startsWith("-")) {
197
- positionals.push(arg);
198
- continue;
199
- }
200
- if (!allowed.has(arg)) return unknownOption(arg);
201
- if (VALUE_FLAGS.has(arg)) {
202
- const v = inline ?? argv[++i];
203
- if (v === undefined || (inline === undefined && v.startsWith("--"))) return `Missing value after ${arg}`;
204
- values.set(arg, v);
205
- } else if (BOOL_FLAGS.has(arg)) {
206
- if (inline !== undefined) return `${arg} takes no value`;
207
- bools.add(arg);
208
- }
209
- }
210
- return { values, bools, positionals };
211
- }
212
-
213
- // ── Rendering helpers ────────────────────────────────────────────────────────
214
-
215
- function modeLine(mode: NonNullable<JevConfig["mode"]>): string {
216
- return mode === "enforce"
217
- ? "enforce — Jev's verdicts apply: it may clear a reviewable policy's deny and add its own"
218
- : "shadow — Jev is asked and logged; the regex result is what is enforced";
219
- }
220
-
221
- /** `0600`, or null when unknown. */
222
- function octal(mode: number | null): string | null {
223
- return mode === null ? null : mode.toString(8).padStart(4, "0");
224
- }
225
-
226
- function permissions(mode: number | null): string {
227
- if (mode === null) return "unknown";
228
- return (mode & 0o077) === 0 ? `${octal(mode)} (owner-only)` : `${octal(mode)} (too open)`;
229
- }
230
-
231
- function describeError(err: unknown): { code: string; message: string } {
232
- if (err instanceof JevError) return { code: err.code, message: err.message };
233
- if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) return { code: "timeout", message: "no answer in time" };
234
- return { code: "error", message: err instanceof Error ? err.message : String(err) };
235
- }
236
-
237
- function scrubbed(e: { code: string; message: string }, key: string): { code: string; message: string } {
238
- return { code: e.code, message: scrubSecret(e.message, key) };
239
- }
240
-
241
- /** What the person should do about a failed request, by cause. */
242
- function remedy(code: string): string {
243
- if (code === "http-401" || code === "http-403") return "The provider refused the key. Re-run `failproofai jev setup` with the right one.";
244
- if (code === "out-of-credits") return "The account is out of credits (HTTP 402). Top it up with the provider; until then hooks fall back to regex.";
245
- if (code === "http-429") return "The provider rate-limited this key. Hooks fall back to regex whenever that happens.";
246
- if (code.startsWith("http-5")) return "The provider had a server error. Hooks fall back to regex whenever that happens; try again shortly.";
247
- if (code === "timeout") return "No answer in time. Check the endpoint and your network.";
248
- if (code === "network") return "The endpoint could not be reached. Check the base URL and your network.";
249
- if (/^http-3(?:\d\d|xx)$/.test(code)) {
250
- return "The endpoint answered with a redirect, and Jev requests never follow one (the answer must come from the URL you configured). Set --base-url to the final URL.";
251
- }
252
- if (code === "model-mismatch") {
253
- return "A Jev version the thresholds were not calibrated for answered (or a custom endpoint did not say which model answered), so hooks would fall back to regex. Pin a jev-1.13 model with --model.";
254
- }
255
- if (code === "config") return "The config is not usable. Re-run `failproofai jev setup`.";
256
- return "Hooks would fall back to regex for this reason.";
257
- }
258
-
259
- /** The `jev status` activity block, from `jevStats()` (null when it could not be read). */
260
- export function jevStatsLines(stats: JevStats | null, opts: RenderOpts = {}): string[] {
261
- const hours = stats ? Math.round(stats.windowMs / 3_600_000) : 24;
262
- const heading = rule(`last ${hours} hours`, opts);
263
- if (!stats) return stack(heading, note("Activity could not be read.", opts));
264
- if (stats.total === 0) return stack(heading, note(`No Jev evaluations recorded in the last ${hours} hours.`, opts));
265
- const pct = (x: number) => `${(x * 100).toFixed(1)}%`;
266
- const reasons = Object.entries(stats.fallbackReasons)
267
- .sort((a, b) => b[1] - a[1])
268
- .map(([r, n]) => `${r} ×${n}`)
269
- .join(", ");
270
- const byPolicy = (counts: Record<string, number> | undefined) =>
271
- Object.entries(counts ?? {})
272
- .sort((a, b) => b[1] - a[1])
273
- .map(([p, n]) => `${p} ×${n}`)
274
- .join(", ");
275
- const clears = byPolicy(stats.clearsByPolicy);
276
- // `clearsByPolicy` counts clears that CHANGED an outcome, which only enforce
277
- // mode can do; in shadow mode every clear Jev would have made is in
278
- // `shadowClearsByPolicy` instead and this one is empty. Printing the first
279
- // alone would tell a shadow user "cleared nothing" — the one number they
280
- // turned shadow mode on to watch. The field is optional because the stats
281
- // module T1 builds against does not have it yet (T8 adds it).
282
- const shadowClears = byPolicy((stats as { shadowClearsByPolicy?: Record<string, number> }).shadowClearsByPolicy);
283
- const ms = (v: number | null) => (v === null ? "—" : `${Math.round(v)} ms`);
284
- return stack(
285
- heading,
286
- rows(
287
- [
288
- ["evaluations", String(stats.total)],
289
- ["fell back to regex", `${pct(stats.fallbackRate)}${reasons ? ` (${reasons})` : ""}`],
290
- ["latency", `p50 ${ms(stats.latencyP50Ms)} · p95 ${ms(stats.latencyP95Ms)}`],
291
- ["cleared", clears || "nothing"],
292
- ...(shadowClears ? ([["would have cleared (shadow)", shadowClears]] as Array<[string, string]>) : []),
293
- ],
294
- opts,
295
- ),
296
- );
297
- }
298
-
299
- // ── setup ────────────────────────────────────────────────────────────────────
300
-
301
- /** The origin requests go to for this provider and base URL (the provider's own API when none), or null. */
302
- function originOf(provider: JevProviderKind, baseUrl: unknown): string | null {
303
- const base = typeof baseUrl === "string" ? baseUrl : JEV_PROVIDER_DEFAULTS[provider].baseUrl;
304
- if (!base) return null;
305
- try {
306
- const origin = new URL(base.trim()).origin;
307
- return origin === "null" ? null : origin;
308
- } catch {
309
- return null;
310
- }
311
- }
312
-
313
- /**
314
- * Whether a same-provider re-run's base URL sends requests to a new host, and
315
- * which. Not a move: the same origin, an unusable URL (validation reports it),
316
- * or the provider's own API, which is where its key belongs anyway.
317
- */
318
- function movedHost(provider: JevProviderKind, before: unknown, after: unknown): { from: string | null; to: string } | null {
319
- const to = originOf(provider, after);
320
- if (to === null) return null;
321
- const from = originOf(provider, before);
322
- if (to === from || to === originOf(provider, undefined)) return null;
323
- return { from, to };
324
- }
325
-
326
- /**
327
- * The provider each known host IS, so `failproofai jev --url <url>` needs no
328
- * `--provider`. Exact hosts only: a neighbouring name (`eu.api.typesafe.ai`, a
329
- * corporate proxy) is `custom`, which reaches the same TypeSafe-compatible API
330
- * at the URL given, and `--provider` is there to say otherwise.
331
- */
332
- const PROVIDER_BY_HOST: Readonly<Record<string, JevProviderKind>> = {
333
- "api.typesafe.ai": "typesafe",
334
- "openrouter.ai": "openrouter",
335
- "ai-gateway.vercel.sh": "vercel",
336
- "api.cloudflare.com": "cloudflare",
337
- };
338
-
339
- /** The provider a validated URL names, or `custom` for a host that is nobody's. */
340
- function providerForUrl(url: string): JevProviderKind {
341
- try {
342
- return PROVIDER_BY_HOST[new URL(url).hostname.toLowerCase()] ?? "custom";
343
- } catch {
344
- return "custom";
345
- }
346
- }
347
-
348
- /**
349
- * The account id a Cloudflare URL already carries, or null.
350
- *
351
- * Cloudflare's run endpoint IS per-account — `…/client/v4/accounts/<id>/ai/run`
352
- * — so the id is in the canonical URL, and asking for it again as a flag asks
353
- * for something the person has already typed. The first real attempt to
354
- * configure Cloudflare pasted exactly that URL and was refused with "pass the
355
- * account id as well", with the id visible in the string being rejected.
356
- *
357
- * Read from the path only, never from a query or fragment, and only on a host
358
- * this build already recognises as Cloudflare's, so nothing else can inject one.
359
- */
360
- export function accountIdFromUrl(url: string): string | null {
361
- try {
362
- const parsed = new URL(url);
363
- if (PROVIDER_BY_HOST[parsed.hostname.toLowerCase()] !== "cloudflare") return null;
364
- const m = /\/accounts\/([0-9a-f]{32})(?:\/|$)/i.exec(parsed.pathname);
365
- return m ? m[1].toLowerCase() : null;
366
- } catch {
367
- return null;
368
- }
369
- }
370
-
371
- /** Whether a validated URL is exactly where this provider's requests already go. */
372
- function isProviderDefaultUrl(kind: JevProviderKind, url: string): boolean {
373
- const base = JEV_PROVIDER_DEFAULTS[kind].baseUrl;
374
- if (!base) return false;
375
- const normalized = validateBaseUrl(base);
376
- return normalized.ok && normalized.value === url;
377
- }
378
-
379
- /**
380
- * A saved `baseUrl` that keeps the provider but leaves its API base — `--url
381
- * https://api.typesafe.ai/v2`, or a v1 path this provider does not serve.
382
- * Nothing refuses it: it is a valid https URL, on the right host, for a
383
- * provider that exists. But a provider serves its API at one base, so requests
384
- * would go to a path it does not answer and every evaluation would fall back to
385
- * regex — which is worth saying while the person is still looking at the
386
- * screen. Null when there is nothing to say: no override at all; `custom`,
387
- * whose URL IS the address; another host, which may well be the customer's own
388
- * proxy, whose layout this cannot know; or an override that lands on the
389
- * provider's own endpoint anyway (the full endpoint path given as the base,
390
- * which works).
391
- */
392
- function offProviderBase(cfg: JevConfig, endpoint: string): { endpoint: string; api: string } | null {
393
- if (cfg.baseUrl === undefined || cfg.provider === "custom") return null;
394
- const apiBase = JEV_PROVIDER_DEFAULTS[cfg.provider].baseUrl;
395
- if (!apiBase) return null;
396
- try {
397
- if (new URL(cfg.baseUrl).host !== new URL(apiBase).host) return null;
398
- const api = jevRoute({ ...cfg, baseUrl: undefined }).endpoint;
399
- return api === endpoint ? null : { endpoint: displayEndpoint(endpoint), api: displayEndpoint(api) };
400
- } catch {
401
- return null;
402
- }
403
- }
404
-
405
- async function readAllStdin(): Promise<string> {
406
- const chunks: Buffer[] = [];
407
- for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
408
- return Buffer.concat(chunks).toString("utf8");
409
- }
410
-
411
- async function maskedPrompt(): Promise<string | null> {
412
- const { promptText } = await import("./tui");
413
- return promptText({ message: "Jev API key", mask: true, validate: (v) => validateApiKey(v) });
414
- }
415
-
416
- async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise<JevCliResult> {
417
- const parsed = parseFlags(argv, new Set([...VALUE_FLAGS, "--key-stdin", "--key-from-env"]));
418
- if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
419
- if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
420
- const { values, bools } = parsed;
421
- if (bools.has("--key-stdin") && bools.has("--key-from-env")) return fail(["Use one of --key-stdin and --key-from-env, not both."]);
422
- // One source for the key, so no run has to decide which of two the person
423
- // meant. The message names the recommended one first.
424
- if (values.has("--token") && (bools.has("--key-stdin") || bools.has("--key-from-env"))) {
425
- return fail([
426
- `Use one of --key-stdin, --key-from-env and --token, not more than one.`,
427
- "",
428
- " failproofai jev --url <url> --key-stdin < key-file",
429
- "Nothing was written.",
430
- ]);
431
- }
432
- if (values.has("--url") && values.has("--base-url")) {
433
- return fail(["--url and --base-url are the same thing; give one of them.", "", "Nothing was written."]);
434
- }
435
-
436
- // The provider NAME is checked before anything else on the command line is
437
- // used. With `--url` present, an unknown or miscapitalized spelling used to
438
- // be dropped on the floor and replaced by the provider the URL's host
439
- // implies: the run wrote a config for a provider nobody had named and exited
440
- // 0, and neither this refusal nor the contradiction refusal below could be
441
- // reached that way. The value is still never echoed — the likeliest wrong
442
- // thing after `--provider` is a pasted key.
443
- const namedProvider = values.get("--provider");
444
- if (namedProvider !== undefined && !(JEV_PROVIDER_KINDS as readonly string[]).includes(namedProvider)) {
445
- return fail([
446
- "Unknown provider (not repeated here, in case it is a key).",
447
- `Providers: ${JEV_PROVIDER_KINDS.join(", ")} — exactly as spelled here, lower-case.`,
448
- "",
449
- "Nothing was written.",
450
- ]);
451
- }
452
-
453
- // `--url` is `--base-url` that also picks the provider. It is validated here,
454
- // by the loader's own `validateBaseUrl`, so a URL is refused in the same
455
- // words whichever way it arrived — and so the host below is a parsed host,
456
- // never a value that was echoed before it was known to be a URL.
457
- let urlProvider: { kind: JevProviderKind; host: string } | null = null;
458
- const urlArg = values.get("--url");
459
- if (urlArg !== undefined) {
460
- const checkedUrl = validateBaseUrl(urlArg);
461
- if (!checkedUrl.ok) return fail([`Not saved: ${checkedUrl.problem}.`, "", "Nothing was written."]);
462
- const normalized = checkedUrl.value;
463
- urlProvider = { kind: providerForUrl(normalized), host: new URL(normalized).host };
464
- // Checked above, so this is a provider kind or nothing: a spelling that is
465
- // not one is refused there rather than quietly becoming the host's.
466
- const explicit = (values.get("--provider") as JevProviderKind | undefined) ?? null;
467
- // An explicit provider wins — except where it disagrees with the host about
468
- // which gateway this is, which is a disagreement about where the key goes.
469
- // `custom` is not a disagreement: it is the "treat this URL as itself" ask.
470
- if (explicit !== null && explicit !== "custom" && urlProvider.kind !== "custom" && explicit !== urlProvider.kind) {
471
- return fail([
472
- `--provider ${explicit} and --url disagree: ${urlProvider.host} is ${urlProvider.kind}'s endpoint, not ${explicit}'s.`,
473
- "Drop --provider to take the provider from the URL, or give the URL that provider's own endpoint.",
474
- "",
475
- "Nothing was written.",
476
- ]);
477
- }
478
- // The one host where "treat this URL as itself" cannot be honoured. A
479
- // `custom` route POSTs the TypeSafe-native body to `<url>/systemone` and
480
- // reads a bare answer back; Workers AI answers at `/accounts/<id>/ai/run`
481
- // and wraps its answer in `result`, which is why it has a provider of its
482
- // own. Saved as `custom` it would ask for the `--account-id` it then had
483
- // nowhere to put, and every call would fall back to regex.
484
- if (explicit === "custom" && urlProvider.kind === "cloudflare") {
485
- return fail([
486
- `Not saved: ${urlProvider.host} is Cloudflare Workers AI, which provider custom cannot reach — its endpoint is per-account and its answers are wrapped, and a custom endpoint is asked in TypeSafe's own shape at <url>/systemone.`,
487
- "Use the provider that speaks it: --provider cloudflare --account-id <32 hex characters> — or drop --provider, since the URL already says cloudflare.",
488
- "",
489
- "Nothing was written.",
490
- ]);
491
- }
492
- const kind = explicit ?? urlProvider.kind;
493
- if (explicit === null) values.set("--provider", kind);
494
- // Cloudflare's endpoint IS per-account, so the run URL copied out of the
495
- // dashboard already names the account. Read it from there rather than
496
- // demanding a value that is sitting in the string being read — the first
497
- // person to configure Cloudflare pasted exactly that URL and was told to
498
- // "pass the account id as well", with the id visible in the refusal.
499
- //
500
- // Set into `values` here, while the URL is still in scope, because the
501
- // block below deletes `--url` once it has moved the address to
502
- // `--base-url`. Everything downstream then treats it as a flag that was
503
- // given, including the 32-hex validation.
504
- if (kind === "cloudflare") {
505
- const fromUrl = accountIdFromUrl(normalized);
506
- const given = values.get("--account-id");
507
- if (fromUrl !== null && given === undefined) {
508
- values.set("--account-id", fromUrl);
509
- } else if (fromUrl !== null && given !== undefined && given.toLowerCase() !== fromUrl) {
510
- // Preferring either one silently would send every request to an account
511
- // the person did not name on the line they are looking at.
512
- return fail([
513
- "Not saved: --account-id and the account id in --url are different.",
514
- ` --url names ${fromUrl}`,
515
- ` --account-id names ${given.toLowerCase()}`,
516
- "Drop one of them.",
517
- "",
518
- "Nothing was written.",
519
- ]);
520
- }
521
- }
522
- // The provider's own API is where its key belongs anyway, so a URL naming
523
- // it clears the override instead of writing the same address into the file
524
- // (`custom` has no API of its own — its URL is the whole address).
525
- values.set("--base-url", kind !== "custom" && isProviderDefaultUrl(kind, normalized) ? "default" : normalized);
526
- values.delete("--url");
527
- }
528
-
529
- const existingFile = readJevConfigFileForUpdate();
530
- const existing = existingFile?.raw ?? null;
531
- const providerArg = values.get("--provider");
532
- const provider = providerArg ?? (typeof existing?.provider === "string" ? existing.provider : undefined);
533
- if (!provider) {
534
- return fail([
535
- "--provider is required the first time.",
536
- `Providers: ${JEV_PROVIDER_KINDS.join(", ")}`,
537
- "",
538
- " failproofai jev setup --provider cloudflare --account-id <id> --key-stdin < key-file",
539
- ]);
540
- }
541
- if (!(JEV_PROVIDER_KINDS as readonly string[]).includes(provider)) {
542
- return fail(["Unknown provider (not repeated here, in case it is a key).", `Providers: ${JEV_PROVIDER_KINDS.join(", ")}`]);
543
- }
544
- const kind = provider as JevProviderKind;
545
- const sameProvider = existing !== null && existing.provider === provider;
546
-
547
- // Same provider: update in place, keeping the key and every field not named
548
- // (including ones a newer failproofai wrote). A different provider starts
549
- // over: a key, model or URL for one gateway means nothing to another. Mode
550
- // and timeout are provider-neutral, so they carry across.
551
- const next: Record<string, unknown> = sameProvider ? { ...existing } : { provider };
552
- if (!sameProvider && existing) {
553
- if (existing.mode !== undefined) next.mode = existing.mode;
554
- if (existing.timeoutMs !== undefined) next.timeoutMs = existing.timeoutMs;
555
- }
556
- next.provider = provider;
557
-
558
- const setOrClear = (flag: string, field: string) => {
559
- const v = values.get(flag);
560
- if (v === undefined) return;
561
- if (v === "default") delete next[field];
562
- else next[field] = v;
563
- };
564
- setOrClear("--base-url", "baseUrl");
565
- setOrClear("--model", "model");
566
- if (values.has("--account-id")) {
567
- // Only Cloudflare's endpoint is per-account, and `validateJevConfig` keeps
568
- // `accountId` for no other provider: writing it for one of those would put
569
- // a field in the file that nothing reads, while `jev status` showed an
570
- // endpoint that does not contain it. Refused rather than written and
571
- // ignored. (The value is never echoed: it is 32 hex characters, or a
572
- // pasted key.)
573
- if (kind !== "cloudflare") {
574
- return fail([
575
- `Not saved: --account-id is Cloudflare's, and provider ${kind} has no use for one — nothing would read it.`,
576
- kind === "custom"
577
- ? "A custom endpoint's URL is the whole address: if the account belongs in it, it goes in --url."
578
- : "Drop --account-id, or ask for Cloudflare: --provider cloudflare with its own API URL.",
579
- "",
580
- "Nothing was written.",
581
- ]);
582
- }
583
- next.accountId = values.get("--account-id");
584
- }
585
- if (values.has("--mode")) next.mode = values.get("--mode");
586
- if (values.has("--timeout-ms")) {
587
- const raw = values.get("--timeout-ms") as string;
588
- const n = Number(raw);
589
- if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n)) return fail(["Could not read --timeout-ms. Give a whole number of milliseconds."]);
590
- next.timeoutMs = n;
591
- }
592
-
593
- // Cloudflare's endpoint is per-account, so a URL that turns out to be
594
- // Cloudflare's needs one. Named here rather than left to validation, which
595
- // knows the field and not the flag — and this run never said "cloudflare",
596
- // the URL did. Only when Cloudflare is what is being saved: a URL on that
597
- // host with another provider asked for is settled above, and nothing here
598
- // demands an id the file would not keep.
599
- if (kind === "cloudflare" && urlProvider?.kind === "cloudflare" && typeof next.accountId !== "string") {
600
- // Reached only when the URL did not carry one either — a bare
601
- // `/client/v4`, say. The URL form is named as well as the flag, because it
602
- // is the address the dashboard hands out.
603
- return fail([
604
- `Not saved: ${urlProvider.host} is Cloudflare Workers AI, whose endpoint is per-account.`,
605
- "Pass the account id as well: --account-id <32 hex characters>,",
606
- "or give the full run URL, which already contains it:",
607
- " https://api.cloudflare.com/client/v4/accounts/<id>/ai/run",
608
- "",
609
- "Nothing was written.",
610
- ]);
611
- }
612
-
613
- // A key stored for one host is not carried to another (see the header). A
614
- // file that was open to other users is the loader's "someone else may have
615
- // chosen the endpoint" case, so the URL it names is measured against the
616
- // provider's own API, not trusted as the host the key already went to —
617
- // whether it is carried over or passed again with --base-url.
618
- const untrustedFile = sameProvider && existingFile?.tooOpen === true;
619
- const hostMove =
620
- sameProvider && (values.has("--base-url") || untrustedFile)
621
- ? movedHost(kind, untrustedFile ? undefined : existing?.baseUrl, next.baseUrl)
622
- : null;
623
- if (hostMove) delete next.apiKey;
624
-
625
- // The key. A config that takes it from the environment stores none, and
626
- // stays that way across a re-run for the same provider.
627
- let keyNote: string;
628
- let keyFromEnv = false;
629
- let tokenOnCommandLine = false;
630
- const envKey = process.env[JEV_API_KEY_ENV];
631
- const envKeyNote = () =>
632
- envKey
633
- ? `not stored — read from ${JEV_API_KEY_ENV} when a hook runs`
634
- : `not stored — ${JEV_API_KEY_ENV} is not set in this shell, and Jev stays off wherever it is not`;
635
- if (bools.has("--key-from-env")) {
636
- delete next.apiKey;
637
- keyFromEnv = true;
638
- keyNote = envKeyNote();
639
- } else if (bools.has("--key-stdin")) {
640
- const tty = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
641
- // On a terminal a cooked-mode read echoes each keystroke, so a person
642
- // typing or pasting there gets the masked prompt instead.
643
- const raw = tty ? await (deps.promptKey ?? maskedPrompt)() : await (deps.readStdin ?? readAllStdin)();
644
- if (raw === null) return fail(["Cancelled; nothing was written."]);
645
- const key = raw.trim();
646
- const bad = validateApiKey(key);
647
- if (bad) return fail([`Not saved: ${bad}.`]);
648
- next.apiKey = key;
649
- keyNote = "set from stdin";
650
- } else if (values.has("--token")) {
651
- // Trimmed like the stdin key, and validated by the same rule, so the same
652
- // paste is accepted or refused identically whichever way it arrived. The
653
- // refusal quotes the rule, never the value.
654
- const key = (values.get("--token") as string).trim();
655
- const bad = validateApiKey(key);
656
- if (bad) return fail([`Not saved: ${bad}.`]);
657
- next.apiKey = key;
658
- tokenOnCommandLine = true;
659
- keyNote = "set from --token — this shell's history has it too";
660
- } else if (typeof next.apiKey === "string") {
661
- keyNote = "kept from the existing config";
662
- } else if (sameProvider && !hostMove) {
663
- keyFromEnv = true;
664
- keyNote = envKeyNote();
665
- } else {
666
- const tty = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
667
- if (hostMove && untrustedFile) {
668
- // Not even at a terminal: a bare "Jev API key" prompt would not say where
669
- // the key is going, and here that is exactly the question.
670
- const perms = octal(existingFile?.mode ?? null) ?? "unknown";
671
- const named = values.has("--base-url");
672
- return fail([
673
- named
674
- ? `${jevConfigPath()} was open to other users (${perms}), so its stored key is not carried to ${hostMove.to}.`
675
- : `${jevConfigPath()} was open to other users (${perms}), so the endpoint it names — ${hostMove.to} — may not be one you chose, and its stored key is not carried there.`,
676
- "If that endpoint is yours, give the key for it again:",
677
- ` failproofai jev setup${named ? " --base-url <url>" : ""} --key-stdin < key-file`,
678
- "",
679
- kind === "custom"
680
- ? "If it is not, give your own --base-url along with its key."
681
- : "If it is not, send requests back to the provider's own API, which keeps the stored key: failproofai jev setup --base-url default",
682
- "Nothing was written.",
683
- ]);
684
- }
685
- if (!tty && hostMove) {
686
- return fail([
687
- `The new base URL sends requests to ${hostMove.to}${hostMove.from ? `, not ${hostMove.from}` : ""}.`,
688
- "A stored key is not carried to a different host. Give the key for that endpoint again:",
689
- " failproofai jev setup --base-url <url> --key-stdin < key-file",
690
- "",
691
- `Or store no key and supply ${JEV_API_KEY_ENV} per session (--key-from-env).`,
692
- "Nothing was written.",
693
- ]);
694
- }
695
- if (!tty) {
696
- return fail([
697
- "No key given, and there is no terminal to ask on.",
698
- "",
699
- "Pass it on stdin, which keeps it out of `ps` and shell history:",
700
- ` failproofai jev setup --provider ${provider} --key-stdin < key-file`,
701
- "",
702
- `Or store no key and supply ${JEV_API_KEY_ENV} per session (--key-from-env).`,
703
- ]);
704
- }
705
- const raw = await (deps.promptKey ?? maskedPrompt)();
706
- if (raw === null) return fail(["Cancelled; nothing was written."]);
707
- const key = raw.trim();
708
- const bad = validateApiKey(key);
709
- if (bad) return fail([`Not saved: ${bad}.`]);
710
- next.apiKey = key;
711
- keyNote = hostMove ? `set at the prompt, for ${hostMove.to}` : "set at the prompt";
712
- }
713
-
714
- // With the key in the environment the FILE is what is being checked, so a
715
- // stand-in fills the key when the variable is unset here. A variable that is
716
- // set but malformed is checked for real, and refused.
717
- const checked = validateJevConfig(next, keyFromEnv ? envKey || ENV_KEY_STAND_IN : null);
718
- if (!checked.ok) return fail([`Not saved: ${checked.problem}.`, "", "Nothing was written."]);
719
- const cfg = checked.value;
720
-
721
- const path = jevConfigPath();
722
- try {
723
- writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
724
- } catch (err) {
725
- return fail([`Could not write ${path}: ${(err as NodeJS.ErrnoException).code ?? "error"}.`]);
726
- }
727
- let fileMode: number | null = null;
728
- try {
729
- fileMode = statSync(path).mode & 0o777;
730
- } catch {
731
- // Reported as unknown below.
732
- }
733
- // `writeJsonAtomically` creates the directory at 0700, but leaves a
734
- // pre-existing one alone — and older code paths create ~/.failproofai at the
735
- // umask, which on a umask-002 machine is group-writable. A directory others
736
- // can write into defeats the file's 0600 (they replace the file and the key
737
- // goes to their endpoint), so the one command that puts a key there takes
738
- // that away. Exactly the write bits the loader refuses, and no more: read
739
- // bits give nobody that power, and config.json in the same directory is
740
- // world-readable by design.
741
- const dir = dirname(path);
742
- let tightenedDir: { path: string; to: string } | null = null;
743
- if (process.platform !== "win32") {
744
- try {
745
- const before = statSync(dir).mode & 0o777;
746
- if ((before & 0o022) !== 0) {
747
- const after = before & ~0o022;
748
- chmodSync(dir, after);
749
- tightenedDir = { path: dir, to: after.toString(8).padStart(4, "0") };
750
- }
751
- } catch {
752
- // Not fatal: the loader reports a directory it will not read from.
753
- }
754
- }
755
-
756
- const route = jevRoute(cfg);
757
- const offBase = offProviderBase(cfg, route.endpoint);
758
- return ok(
759
- stack(
760
- title("failproofai jev setup", `saved · ${provider} · ${cfg.mode ?? DEFAULT_JEV_MODE}`, opts),
761
- rows(
762
- [
763
- ["provider", provider],
764
- ["endpoint", displayEndpoint(route.endpoint)],
765
- ["model", route.modelIsDefault ? `${route.model} (provider default)` : route.model],
766
- ["mode", modeLine(cfg.mode ?? DEFAULT_JEV_MODE)],
767
- ["timeout", `${cfg.timeoutMs} ms`],
768
- ["key", keyNote],
769
- ["config", path],
770
- ["permissions", permissions(fileMode)],
771
- ],
772
- opts,
773
- ),
774
- // Saved, and saved as asked — but saying only "saved" would leave someone
775
- // to discover from the fallback counts that the URL they gave is not an
776
- // API base. So the file is written and what it will do is named.
777
- offBase
778
- ? warning(
779
- [
780
- `Saved as given, but ${provider}'s API is at ${offBase.api}, and this URL sends requests to ${offBase.endpoint} instead.`,
781
- `Unless something of yours answers Jev requests at that path, every evaluation will fail there and hooks will fall back to regex — \`failproofai jev test\` says which it is in one request. To send requests back to ${provider}'s own API: failproofai jev setup --base-url default`,
782
- ],
783
- opts,
784
- )
785
- : null,
786
- tightenedDir
787
- ? note(
788
- `${tightenedDir.path} was writable by other users, who could have replaced this file whatever its own permissions were; it is now ${tightenedDir.to}.`,
789
- opts,
790
- )
791
- : null,
792
- // Said once, after the key is safely stored: the file is 0600, but the
793
- // command line it came on is not — it is in this shell's history file and
794
- // was readable from /proc by anything running as this user while the
795
- // process lived.
796
- tokenOnCommandLine
797
- ? warning(
798
- [
799
- "--token was on the command line: your shell history has it, and while this command ran any process of yours could read it from the process list.",
800
- "Prefer piping the key in — `failproofai jev --url <url> --key-stdin < key-file` — and rotate this one if it matters.",
801
- ],
802
- opts,
803
- )
804
- : null,
805
- note("Hooks read this file on every tool call — no restart. Without it they run the regex policies exactly as before.", opts),
806
- nextStep("failproofai jev test", "Check it with one live request:", opts),
807
- ),
808
- );
809
- }
810
-
811
- // ── status ───────────────────────────────────────────────────────────────────
812
-
813
- /**
814
- * Where a file would send requests, for display, whether or not it loads. Only
815
- * the routing fields are used — the key and model do not decide where requests
816
- * go — and shadow mode lets a loopback http URL through validation. Null when
817
- * it names nothing recognisable.
818
- */
819
- /**
820
- * The route a config with no stored key would take, for display. The key does
821
- * not decide where requests go, so a stand-in fills it; null when the routing
822
- * fields do not make a usable route (`inspectJevConfig` has already validated
823
- * them, so that is a belt-and-braces null).
824
- */
825
- function routeForRouting(routing: Omit<JevConfig, "apiKey">): ReturnType<typeof jevRoute> | null {
826
- try {
827
- return jevRoute({ ...routing, apiKey: ENV_KEY_STAND_IN });
828
- } catch {
829
- return null;
830
- }
831
- }
832
-
833
- function namedEndpoint(raw: Record<string, unknown> | null): string | null {
834
- if (!raw || typeof raw.provider !== "string" || !(JEV_PROVIDER_KINDS as readonly string[]).includes(raw.provider)) return null;
835
- const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "shadow" };
836
- if (raw.baseUrl !== undefined) routing.baseUrl = raw.baseUrl;
837
- if (raw.accountId !== undefined) routing.accountId = raw.accountId;
838
- try {
839
- return displayEndpoint(jevRoute(routing as unknown as JevConfig).endpoint);
840
- } catch {
841
- return typeof raw.baseUrl === "string" ? displayEndpoint(raw.baseUrl) : null;
842
- }
843
- }
844
-
845
- /**
846
- * What Jev may clear here, or null when it could not be worked out. `status`
847
- * answers "is this thing working", and a settings read that threw would replace
848
- * that answer with a stack trace — the survey reads three files it does not
849
- * own, and none of them is worth the whole command.
850
- */
851
- function safeCoverage(): ReviewableCoverage | null {
852
- try {
853
- return surveyReviewableCoverage();
854
- } catch {
855
- return null;
856
- }
857
- }
858
-
859
- async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
860
- const parsed = parseFlags(argv, new Set(["--json"]));
861
- if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
862
- if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
863
- const asJson = parsed.bools.has("--json");
864
-
865
- const inspection = inspectJevConfig();
866
- let stats: JevStats | null = null;
867
- try {
868
- stats = await jevStats();
869
- } catch {
870
- stats = null;
871
- }
872
- const legacy = process.env.FAILPROOFAI_EVALUATOR === "legacy";
873
- // Only where a clear could actually happen. A file that is absent, refused or
874
- // keyless means hooks run the regex policies whatever any policy's authority
875
- // says, and an authority count there would answer a question nobody is in a
876
- // position to ask yet.
877
- const coverage = inspection.status === "ok" ? safeCoverage() : null;
878
- const coverageProblem = coverage ? reviewableProblem(coverage) : null;
879
-
880
- if (asJson) {
881
- const base: Record<string, unknown> = { path: inspection.path, status: inspection.status, legacyOverride: legacy, stats };
882
- if (inspection.status === "refused") {
883
- Object.assign(base, { reason: inspection.reason, problem: inspection.problem, permissions: octal(inspection.mode) });
884
- if (inspection.reason === "too-open") {
885
- const named = namedEndpoint(readJevConfigFileForUpdate()?.raw ?? null);
886
- if (named) base.endpoint = named;
887
- }
888
- }
889
- if (inspection.status === "key-missing") {
890
- // Configured, just not usable in THIS environment: a provisioning check
891
- // must be able to tell that from a file it should rewrite, so the routing
892
- // is reported exactly as for `ok`, with `keySource: "env"` and a reason.
893
- const route = routeForRouting(inspection.routing);
894
- Object.assign(base, {
895
- permissions: octal(inspection.mode),
896
- provider: inspection.routing.provider,
897
- ...(route ? { endpoint: displayEndpoint(route.endpoint), model: route.model, modelIsDefault: route.modelIsDefault } : {}),
898
- mode: inspection.routing.mode,
899
- timeoutMs: inspection.routing.timeoutMs,
900
- keySource: "env",
901
- keyEnvVar: JEV_API_KEY_ENV,
902
- reason: "no-env-key",
903
- problem: inspection.problem,
904
- });
905
- }
906
- if (inspection.status === "ok") {
907
- const { config: cfg } = inspection;
908
- const route = jevRoute(cfg);
909
- Object.assign(base, {
910
- permissions: octal(inspection.mode),
911
- provider: cfg.provider,
912
- endpoint: displayEndpoint(route.endpoint),
913
- model: route.model,
914
- modelIsDefault: route.modelIsDefault,
915
- mode: cfg.mode,
916
- timeoutMs: cfg.timeoutMs,
917
- keySource: inspection.keySource,
918
- // How much of this machine's policy set Jev is allowed to clear, and
919
- // why it is none when it is none. A provisioning check that turns Jev
920
- // on has no other way to find out that the half it turned on cannot
921
- // fire — the call counts look identical either way.
922
- ...(coverage
923
- ? {
924
- reviewablePolicies: {
925
- enabled: coverage.enabled,
926
- reviewable: coverage.reviewable,
927
- customPolicyFiles: coverage.customFiles,
928
- problem: coverageProblem,
929
- },
930
- }
931
- : {}),
932
- });
933
- }
934
- const json = JSON.stringify(base, null, 2);
935
- return inspection.status === "refused" ? fail([], json) : ok([], json);
936
- }
937
-
938
- const legacyNote = legacy
939
- ? warning(
940
- [
941
- "FAILPROOFAI_EVALUATOR=legacy is set in this shell: sessions started from it skip Jev when their hooks evaluate in-process.",
942
- "The daemon does not see this shell's environment.",
943
- ],
944
- opts,
945
- )
946
- : null;
947
-
948
- if (inspection.status === "absent") {
949
- return ok(
950
- stack(
951
- title("failproofai jev status", "off", opts),
952
- emptyState(
953
- {
954
- what: `Jev is off: there is no ${inspection.path}. Hooks run the regex policies exactly as before.`,
955
- hint: "Turn it on with your own Jev endpoint and key:",
956
- cmd: "failproofai jev setup --provider <typesafe|openrouter|vercel|cloudflare|custom> --key-stdin",
957
- },
958
- opts,
959
- ),
960
- jevStatsLines(stats, opts),
961
- ),
962
- );
963
- }
964
-
965
- if (inspection.status === "key-missing") {
966
- // Not a refusal: this is the file `setup --key-from-env` writes, doing what
967
- // it was asked to. Saying "write a valid one" here would tell its owner to
968
- // undo the one choice they made, so it reads like `absent` — off HERE —
969
- // and the next step is the variable, not a rewrite.
970
- const route = routeForRouting(inspection.routing);
971
- return ok(
972
- stack(
973
- title("failproofai jev status", "off in this shell", opts),
974
- note(
975
- `Jev is off in this shell: ${inspection.path} stores no key and takes it from ${JEV_API_KEY_ENV}, which is not set here. ` +
976
- "The config is fine; hooks run the regex policies wherever the variable is unset — including under the daemon, which does not see a shell's environment.",
977
- opts,
978
- ),
979
- rows(
980
- [
981
- ["provider", inspection.routing.provider],
982
- ...((route
983
- ? [
984
- ["endpoint", displayEndpoint(route.endpoint)],
985
- ["model", route.modelIsDefault ? `${route.model} (provider default)` : route.model],
986
- ]
987
- : []) as Array<[string, string]>),
988
- ["mode", modeLine(inspection.routing.mode ?? DEFAULT_JEV_MODE)],
989
- ["config", inspection.path],
990
- ["permissions", permissions(inspection.mode)],
991
- ["key", `from ${JEV_API_KEY_ENV} — not set in this shell`],
992
- ],
993
- opts,
994
- ),
995
- nextStep(`failproofai jev setup --key-stdin < key-file`, `Set ${JEV_API_KEY_ENV} for this shell, or store the key in the file instead:`, opts),
996
- legacyNote,
997
- jevStatsLines(stats, opts),
998
- ),
999
- );
1000
- }
1001
-
1002
- if (inspection.status === "refused") {
1003
- // A file other users could change may name an endpoint its owner never
1004
- // chose, and `chmod 600` would start trusting it with the key — so say
1005
- // where it points before suggesting that.
1006
- const named = inspection.reason === "too-open" ? namedEndpoint(readJevConfigFileForUpdate()?.raw ?? null) : null;
1007
- return fail(
1008
- stack(
1009
- title("failproofai jev status", "off (config refused)", opts),
1010
- warning([`Jev is off: ${inspection.path} was refused — ${inspection.problem}.`, "Hooks run the regex policies exactly as before."], opts),
1011
- named ? rows([["endpoint it names", named]], opts) : null,
1012
- inspection.reason === "too-open"
1013
- ? nextStep(
1014
- // Either the file or the directory it sits in; `fix` says which.
1015
- inspection.fix ?? `chmod 600 ${inspection.path}`,
1016
- "Other users could change this file, so check that endpoint is one you chose. Then make it owner-only (or re-run `failproofai jev setup`, which asks for the key again unless the endpoint is the provider's own):",
1017
- opts,
1018
- )
1019
- : nextStep("failproofai jev setup --provider <kind> --key-stdin", "Write a valid one:", opts),
1020
- legacyNote,
1021
- jevStatsLines(stats, opts),
1022
- ),
1023
- );
1024
- }
1025
-
1026
- const { config: cfg } = inspection;
1027
- const route = jevRoute(cfg);
1028
- const mode = cfg.mode ?? DEFAULT_JEV_MODE;
1029
- return ok(
1030
- stack(
1031
- title("failproofai jev status", legacy ? "on (legacy override in this shell)" : `on · ${mode}`, opts),
1032
- rows(
1033
- [
1034
- ["provider", cfg.provider],
1035
- ["endpoint", displayEndpoint(route.endpoint)],
1036
- ["model", route.modelIsDefault ? `${route.model} (provider default)` : route.model],
1037
- ["mode", modeLine(mode)],
1038
- ["timeout", `${cfg.timeoutMs} ms`],
1039
- ["config", inspection.path],
1040
- ["permissions", permissions(inspection.mode)],
1041
- ["key", inspection.keySource === "file" ? "set in the config file" : `from ${JEV_API_KEY_ENV} (this shell only; the daemon does not see it)`],
1042
- ],
1043
- opts,
1044
- ),
1045
- coverage ? note(reviewableSummary(coverage), opts) : null,
1046
- coverageProblem ? warning([coverageProblem], opts) : null,
1047
- legacyNote,
1048
- jevStatsLines(stats, opts),
1049
- ),
1050
- );
1051
- }
1052
-
1053
- // ── test ─────────────────────────────────────────────────────────────────────
1054
-
1055
- export function jevTestRequest(model: string): JevRequest {
1056
- return {
1057
- model,
1058
- state: { word: "blue" },
1059
- questions: {
1060
- [JEV_TEST_QUESTION_ID]: {
1061
- type: "noul",
1062
- instructions: "Is state.word the name of a colour?",
1063
- criteria: { true: "It names a colour", false: "It does not name a colour" },
1064
- },
1065
- },
1066
- };
1067
- }
1068
-
1069
- async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise<JevCliResult> {
1070
- const parsed = parseFlags(argv, new Set(["--json"]));
1071
- if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
1072
- if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
1073
- const asJson = parsed.bools.has("--json");
1074
-
1075
- const inspection = inspectJevConfig();
1076
- if (inspection.status !== "ok") {
1077
- const why =
1078
- inspection.status === "absent"
1079
- ? `There is no ${inspection.path}; nothing to test.`
1080
- : inspection.status === "key-missing"
1081
- ? `${inspection.path} takes its key from ${JEV_API_KEY_ENV}, which is not set in this shell, so there is no key to test with.`
1082
- : `${inspection.path} was refused — ${inspection.problem}.`;
1083
- const code = inspection.status === "absent" ? "not-configured" : inspection.status === "key-missing" ? "no-env-key" : "config";
1084
- const fixCmd =
1085
- inspection.status === "key-missing" ? "failproofai jev setup --key-stdin < key-file" : "failproofai jev setup --provider <kind> --key-stdin";
1086
- const json = asJson ? JSON.stringify({ ok: false, error: { code, message: why } }, null, 2) : undefined;
1087
- return fail(
1088
- stack(
1089
- title("failproofai jev test", "not run", opts),
1090
- note(why, opts),
1091
- nextStep(fixCmd, inspection.status === "key-missing" ? `Set ${JEV_API_KEY_ENV} for this shell, or store the key in the file:` : undefined, opts),
1092
- ),
1093
- json,
1094
- );
1095
- }
1096
-
1097
- const cfg = inspection.config;
1098
- let route: ReturnType<typeof jevRoute>;
1099
- let built: ReturnType<typeof transportForConfig>;
1100
- try {
1101
- route = jevRoute(cfg);
1102
- built = transportForConfig(cfg);
1103
- } catch (err) {
1104
- const e = scrubbed(describeError(err), cfg.apiKey);
1105
- return fail([`Not run: ${e.message}`], asJson ? JSON.stringify({ ok: false, error: e }, null, 2) : undefined);
1106
- }
1107
-
1108
- const request = jevTestRequest(built.model);
1109
- // `validateJevConfig` always fills this in; the fallback is the same constant
1110
- // it fills it with, imported rather than copied — a copy of it here was left
1111
- // behind at 1500 ms when the default became 3000.
1112
- const budget = cfg.timeoutMs ?? JEV_CONFIG_DEFAULT_TIMEOUT_MS;
1113
- const started = performance.now();
1114
- try {
1115
- const response = await built.transport(request, AbortSignal.timeout(deps.testTimeoutMs ?? DEFAULT_TEST_TIMEOUT_MS));
1116
- const answers = readAnswers(request, response);
1117
- const latencyMs = Math.round(performance.now() - started);
1118
- const verified = response.modelUnverified !== true;
1119
- const within = latencyMs <= budget;
1120
- const p = answers[JEV_TEST_QUESTION_ID];
1121
- const inputTokens = typeof response.usage?.input_tokens === "number" ? response.usage.input_tokens : null;
1122
- if (asJson) {
1123
- return ok(
1124
- [],
1125
- JSON.stringify(
1126
- {
1127
- ok: true,
1128
- provider: cfg.provider,
1129
- endpoint: displayEndpoint(route.endpoint),
1130
- model: built.model,
1131
- reportedModel: verified ? response.model : null,
1132
- modelVerified: verified,
1133
- latencyMs,
1134
- timeoutMs: budget,
1135
- withinTimeout: within,
1136
- answer: p,
1137
- inputTokens,
1138
- },
1139
- null,
1140
- 2,
1141
- ),
1142
- );
1143
- }
1144
- return ok(
1145
- stack(
1146
- title("failproofai jev test", `ok · ${latencyMs} ms`, opts),
1147
- rows(
1148
- [
1149
- ["provider", cfg.provider],
1150
- ["endpoint", displayEndpoint(route.endpoint)],
1151
- ["model asked", built.model],
1152
- [
1153
- "answered by",
1154
- verified ? `${response.model} (Jev 1.13 family — verified)` : `not reported — ${cfg.provider} names Jev only by an alias (modelVerified: false)`,
1155
- ],
1156
- ["latency", `${latencyMs} ms — ${within ? `within the ${budget} ms timeout` : `OVER the ${budget} ms timeout: hooks would fall back to regex`}`],
1157
- ["answer", `p = ${p.toFixed(3)} that "blue" names a colour${p >= 0.5 ? "" : " (expected high)"}`],
1158
- ...(inputTokens !== null ? ([["input tokens", String(inputTokens)]] as Array<[string, string]>) : []),
1159
- ],
1160
- opts,
1161
- ),
1162
- note(
1163
- "One request, sent directly: the hook path's cache and rate limit were not involved, and a fresh process pays DNS and TLS setup that the daemon's warm worker does not.",
1164
- opts,
1165
- ),
1166
- ),
1167
- );
1168
- } catch (err) {
1169
- const latencyMs = Math.round(performance.now() - started);
1170
- // jev-client scrubs provider text already; this is the last line of that
1171
- // defence, for any error text that reached here some other way.
1172
- const e = scrubbed(describeError(err), cfg.apiKey);
1173
- if (asJson) return fail([], JSON.stringify({ ok: false, provider: cfg.provider, latencyMs, error: e }, null, 2));
1174
- return fail(
1175
- stack(
1176
- title("failproofai jev test", `failed · ${e.code}`, opts),
1177
- rows(
1178
- [
1179
- ["provider", cfg.provider],
1180
- ["endpoint", displayEndpoint(route.endpoint)],
1181
- ["model asked", built.model],
1182
- ["error", `${e.code}: ${e.message}`],
1183
- ["after", `${latencyMs} ms`],
1184
- ],
1185
- opts,
1186
- ),
1187
- note(remedy(e.code), opts),
1188
- ),
1189
- );
1190
- }
1191
- }
1192
-
1193
- // ── remove ───────────────────────────────────────────────────────────────────
1194
-
1195
- function remove(argv: string[], opts: RenderOpts): JevCliResult {
1196
- const parsed = parseFlags(argv, new Set());
1197
- if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
1198
- if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
1199
- const path = jevConfigPath();
1200
- if (!existsSync(path)) {
1201
- return ok(stack(title("failproofai jev remove", "nothing to do", opts), note(`There is no ${path}; Jev is already off.`, opts)));
1202
- }
1203
- try {
1204
- unlinkSync(path);
1205
- } catch (err) {
1206
- return fail([`Could not remove ${path}: ${(err as NodeJS.ErrnoException).code ?? "error"}.`]);
1207
- }
1208
- return ok(
1209
- stack(
1210
- title("failproofai jev remove", "off", opts),
1211
- note(`Removed ${path}. Jev is off; hooks run the regex policies exactly as before, from the next tool call.`, opts),
1212
- process.env[JEV_API_KEY_ENV] ? note(`${JEV_API_KEY_ENV} is still set in this shell. Without the file it does nothing.`, opts) : null,
1213
- ),
1214
- );
1215
- }
1216
-
1217
- // ── dispatch ─────────────────────────────────────────────────────────────────
1218
-
1219
- /** Dispatch for `failproofai jev <sub> ...`. Never prints; the caller does. */
1220
- export async function runJevCommand(argv: string[], deps: JevCliDeps = {}): Promise<JevCliResult> {
1221
- const opts = deps.render ?? optsFor(process.stdout);
1222
- const [sub, ...rest] = argv;
1223
- // `failproofai jev --url <url> --token <token>` — the one-shot form. An argv
1224
- // that opens with an option is `setup` with that option: nothing else here
1225
- // takes one in the subcommand slot, and the two spellings then parse through
1226
- // exactly the same code.
1227
- if (sub !== undefined && sub.startsWith("-")) return setup(argv, deps, opts);
1228
- switch (sub) {
1229
- case "setup":
1230
- return setup(rest, deps, opts);
1231
- case "status":
1232
- return status(rest, opts);
1233
- case "test":
1234
- return test(rest, deps, opts);
1235
- case "remove":
1236
- return remove(rest, opts);
1237
- default: {
1238
- // The subcommand slot takes a pasted key as readily as any flag does
1239
- // (`failproofai jev <key>`, having forgotten `setup --key-stdin`), so it
1240
- // is repeated only when it is shaped like a subcommand — which no key is.
1241
- const nameLike = sub !== undefined && SUBCOMMAND_SHAPE.test(sub);
1242
- return fail([
1243
- !sub ? "A subcommand is required." : nameLike ? `Unknown subcommand: ${sub}` : "Unknown subcommand (not repeated here, in case it is a key).",
1244
- "",
1245
- ...JEV_USAGE,
1246
- ]);
1247
- }
1248
- }
1249
- }