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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -5
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +4 -4
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +6 -7
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +18 -7
  58. package/.next/standalone/.next/server/app/settings/page.js +9 -7
  59. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__20kzpkf._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  65. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +9 -9
  69. package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  97. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +12 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  113. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  114. package/.next/standalone/.next/server/pages/404.html +1 -1
  115. package/.next/standalone/.next/server/pages/500.html +1 -1
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +33 -22
  118. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
  119. package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
  121. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
  124. package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
  129. package/.next/standalone/SECURITY.md +53 -0
  130. package/.next/standalone/app/actions/get-jev-config.ts +194 -14
  131. package/.next/standalone/app/actions/update-jev-config.ts +160 -11
  132. package/.next/standalone/app/policies/hooks-client.tsx +5 -3
  133. package/.next/standalone/app/settings/jev-panel.tsx +169 -12
  134. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  135. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  136. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  137. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  138. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  139. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  141. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  143. package/.next/standalone/package.json +9 -9
  144. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  145. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  146. package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
  147. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
  148. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
  150. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
  151. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
  152. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
  153. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
  154. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
  156. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
  157. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  158. package/.next/standalone/server.js +1 -1
  159. package/README.md +1 -0
  160. package/bin/failproofai.mjs +37 -6
  161. package/dist/cli.mjs +2861 -1145
  162. package/dist/worker.mjs +815 -197
  163. package/package.json +9 -9
  164. package/src/hooks/builtin-policies.ts +24 -6
  165. package/src/hooks/cloud-connection.ts +190 -1
  166. package/src/hooks/cloud-enrollment-cli.ts +56 -7
  167. package/src/hooks/cloud-introspect.ts +6 -0
  168. package/src/hooks/configure-wizard.ts +26 -6
  169. package/src/hooks/custom-hooks-loader.ts +58 -8
  170. package/src/hooks/effective-reviewers.ts +172 -8
  171. package/src/hooks/flush-cli.ts +35 -8
  172. package/src/hooks/fp-config.ts +266 -1
  173. package/src/hooks/fp-reset.ts +22 -4
  174. package/src/hooks/handler.ts +44 -7
  175. package/src/hooks/hook-activity-store.ts +13 -5
  176. package/src/hooks/jev-cli.ts +1009 -86
  177. package/src/hooks/jev-cloud-connection.ts +392 -0
  178. package/src/hooks/manager.ts +44 -6
  179. package/src/hooks/pack-cli.ts +175 -37
  180. package/src/hooks/pack-failclosed.ts +3 -0
  181. package/src/hooks/pack-manifest.ts +11 -1
  182. package/src/hooks/pack-store.ts +47 -10
  183. package/src/hooks/policy-authority.ts +73 -20
  184. package/src/hooks/policy-catalog.ts +10 -8
  185. package/src/hooks/policy-evaluator.ts +31 -8
  186. package/src/hooks/policy-registry.ts +3 -2
  187. package/src/hooks/policy-reviewability.ts +41 -12
  188. package/src/hooks/policy-types.ts +5 -3
  189. package/src/hooks/semantic/combine.ts +124 -84
  190. package/src/hooks/semantic/decide.ts +16 -0
  191. package/src/hooks/semantic/envelope.ts +18 -59
  192. package/src/hooks/semantic/evaluator.ts +11 -2
  193. package/src/hooks/semantic/facts.ts +7 -1
  194. package/src/hooks/semantic/intent.ts +8 -8
  195. package/src/hooks/semantic/jev-client.ts +540 -28
  196. package/src/hooks/semantic/jev-config.ts +586 -38
  197. package/src/hooks/semantic/jev-review.ts +26 -11
  198. package/src/hooks/semantic/jev-throttle.ts +8 -2
  199. package/src/hooks/semantic/pack-policies.ts +58 -15
  200. package/src/hooks/semantic/policies.ts +4 -3
  201. package/src/hooks/semantic/redact.ts +28 -1
  202. package/src/hooks/semantic/session-root.ts +116 -0
  203. package/src/hooks/semantic/types.ts +11 -0
  204. package/src/hooks/tui.ts +4 -0
  205. package/src/hooks/worker-server.ts +2 -1
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
  208. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  209. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  211. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  212. package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  214. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  215. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  216. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  219. package/.next/standalone/.next/static/chunks/1qd741hzlmjbo.js +0 -1
  220. package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
  221. package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
  222. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
  223. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
  224. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
  225. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
@@ -45,10 +45,14 @@
45
45
  * value `jev setup` would also have printed. See `JevModelView`.
46
46
  */
47
47
 
48
+ import { readCredentials, readJevCloudCredential } from "@/src/hooks/fp-config";
48
49
  import {
50
+ CLOUDFLARE_ACCOUNT_ID_RE,
49
51
  DEFAULT_JEV_MODE,
50
52
  JEV_API_KEY_ENV,
53
+ JEV_CLOUD_PROVIDER,
51
54
  JEV_PROVIDER_KINDS,
55
+ baseUrlWithoutQuery,
52
56
  inspectJevConfig,
53
57
  looksLikeCredential,
54
58
  readJevConfigForUpdate,
@@ -63,8 +67,8 @@ import {
63
67
  surveyReviewableCoverage,
64
68
  } from "@/src/hooks/policy-reviewability";
65
69
 
66
- /** The loader's four answers, unchanged — see `JevConfigInspection`. */
67
- export type JevSettingsStatus = "absent" | "ok" | "key-missing" | "refused";
70
+ /** The loader's answers, unchanged — see `JevConfigInspection`. */
71
+ export type JevSettingsStatus = "absent" | "ok" | "key-missing" | "refused" | "off" | "not-connected" | "key-lacks-jev";
68
72
 
69
73
  /** What Jev has been doing lately, from `jevStats()`. Not a new pipeline. */
70
74
  export interface JevSettingsStats {
@@ -89,7 +93,8 @@ export interface JevSettingsStats {
89
93
  * not computed and not sent.
90
94
  */
91
95
  export interface JevTokenPresence {
92
- source: "file" | "env";
96
+ /** `cloud`: the key is this machine's FailproofAI Cloud key, held in `credentials.json`. */
97
+ source: "file" | "env" | "cloud";
93
98
  }
94
99
 
95
100
  /**
@@ -139,10 +144,35 @@ export interface JevReviewabilityView {
139
144
  problem: string | null;
140
145
  }
141
146
 
147
+ /**
148
+ * This machine's FailproofAI Cloud connection, as far as Jev is concerned —
149
+ * read from `credentials.json` on every render, with NO network call and
150
+ * nothing secret in it: the org as the server named it at connect time, the
151
+ * host, and whether the key carries Jev. Every token in that file is read and
152
+ * dropped inside `cloudConnectionView`; none of it, nor any piece of it, is in
153
+ * this object.
154
+ */
155
+ export interface JevCloudConnectionView {
156
+ /** A Cloud credential (policy or reporting) is stored on this machine. */
157
+ connected: boolean;
158
+ /** "Acme Inc (acme)" — recorded at connect time; null when it was not. */
159
+ org: string | null;
160
+ /** The Cloud host, e.g. `app.befailproof.ai`. */
161
+ host: string | null;
162
+ /**
163
+ * Whether the key this machine connected with carries `jev:evaluate` — which
164
+ * is exactly whether connect stored a `jev` slot. `refused`: a slot may be
165
+ * there, but `credentials.json` is not owner-only, so Jev will not read it.
166
+ */
167
+ jev: "yes" | "no" | "refused";
168
+ }
169
+
142
170
  export interface JevSettingsView {
143
171
  status: JevSettingsStatus;
144
172
  /** True only when a hook running right now would consult Jev. */
145
173
  on: boolean;
174
+ /** See `JevCloudConnectionView`. Present on every status. */
175
+ cloud: JevCloudConnectionView;
146
176
  /**
147
177
  * `~/.failproofai/jev.json`, whether or not it exists, and its permission
148
178
  * bits as `0600` when it has some.
@@ -158,8 +188,24 @@ export interface JevSettingsView {
158
188
  path: string;
159
189
  permissions: string | null;
160
190
  provider: JevProviderKind | null;
161
- /** The stored base URL, or "" for the provider's own API. Form value. */
191
+ /**
192
+ * The stored base URL, or "" for the provider's own API. Form value, and
193
+ * NEVER its query string — see `baseUrlView` below for why, and
194
+ * `baseUrlQueryWithheld` for what the panel says about the part it did not
195
+ * get.
196
+ */
162
197
  baseUrl: string;
198
+ /**
199
+ * True when the stored base URL carries a query string that `baseUrl` above
200
+ * does not.
201
+ *
202
+ * The panel needs it to avoid lying twice over: the field would otherwise
203
+ * show a URL that is not the stored one, and an untouched save would look
204
+ * like it kept a query string the person never saw. `update-jev-config.ts`
205
+ * keeps a stored query that the loader accepts when the field comes back
206
+ * unchanged, and this is how the panel can say so.
207
+ */
208
+ baseUrlQueryWithheld: boolean;
163
209
  /** Cloudflare only; "" otherwise. Form value. */
164
210
  accountId: string;
165
211
  /**
@@ -213,6 +259,27 @@ function modelView(stored: string): JevModelView {
213
259
  return { kind: "id", id: stored };
214
260
  }
215
261
 
262
+ /**
263
+ * A stored base URL as the browser may see it: never its query string.
264
+ *
265
+ * `validateBaseUrl` now refuses a credential-bearing query parameter, so a file
266
+ * written by THIS build cannot carry one. A file written by an older one can,
267
+ * and that file is precisely the one this module hands back for repair — the
268
+ * refused branch below copies the raw routing fields out on purpose. Only the
269
+ * derived `endpoint` was elided (`displayEndpoint`), so the base URL carried
270
+ * the secret through a response whose whole documented rule is that the
271
+ * credential never comes back.
272
+ *
273
+ * Every branch goes through this one function rather than reading `baseUrl`
274
+ * itself, for the reason `routingFromRaw` copies field by field: the next field
275
+ * added should not be able to quietly reintroduce it.
276
+ */
277
+ function baseUrlView(stored: string | undefined): { baseUrl: string; baseUrlQueryWithheld: boolean } {
278
+ if (!stored) return { baseUrl: "", baseUrlQueryWithheld: false };
279
+ const { url, hadQuery } = baseUrlWithoutQuery(stored);
280
+ return { baseUrl: url, baseUrlQueryWithheld: hadQuery };
281
+ }
282
+
216
283
  function octal(mode: number | null): string | null {
217
284
  return mode === null ? null : mode.toString(8).padStart(4, "0");
218
285
  }
@@ -244,25 +311,65 @@ function routingFromRaw(raw: Record<string, unknown> | null): {
244
311
  return {
245
312
  provider,
246
313
  baseUrl: asString(raw?.baseUrl),
247
- accountId: asString(raw?.accountId),
314
+ // An account id is 32 hex characters; anything else in that slot of a
315
+ // refused file may be a pasted key, and is not shown.
316
+ accountId: CLOUDFLARE_ACCOUNT_ID_RE.test(asString(raw?.accountId)) ? asString(raw?.accountId) : "",
248
317
  model: asString(raw?.model),
249
- mode: modeRaw === "shadow" || modeRaw === "enforce" ? modeRaw : DEFAULT_JEV_MODE,
318
+ mode: modeRaw === "off" || modeRaw === "shadow" || modeRaw === "enforce" ? modeRaw : DEFAULT_JEV_MODE,
250
319
  };
251
320
  }
252
321
 
253
- /** `jevRoute` for display, swallowing the throw a file that names no usable route produces. */
322
+ /**
323
+ * Whether a switched-off file still has a key to switch back on with — as a
324
+ * SOURCE, like every other token field here. The routing the loader reports
325
+ * for an `off` file deliberately carries no key, so this is read for presence
326
+ * only: the raw file's `apiKey` is tested and dropped, and the Cloud
327
+ * credential's status is read and its key never touched.
328
+ */
329
+ function offTokenPresence(provider: JevProviderKind): JevTokenPresence | null {
330
+ if (provider === JEV_CLOUD_PROVIDER) {
331
+ try {
332
+ return readJevCloudCredential().status === "ok" ? { source: "cloud" } : null;
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ const raw = readJevConfigForUpdate();
338
+ return typeof raw?.apiKey === "string" && raw.apiKey !== "" ? { source: "file" } : null;
339
+ }
340
+
341
+ /**
342
+ * `jevRoute` for display, swallowing the throw a file that names no usable
343
+ * route produces.
344
+ *
345
+ * A FailproofAI Cloud config is only routed against the origin of the
346
+ * credential it was validated with (`validateLoadedJevConfig`). The loaded
347
+ * config carries it; a display stand-in built here — for a file that is off,
348
+ * refused, or has no credential behind it — gets the file's own origin, like
349
+ * the stand-in key beside it. Nothing built here is ever sent.
350
+ */
254
351
  function endpointFor(cfg: JevConfig): string | null {
255
352
  try {
256
- return displayEndpoint(jevRoute(cfg).endpoint);
353
+ return displayEndpoint(jevRoute(displayOnlyCloudOrigin(cfg)).endpoint);
257
354
  } catch {
258
355
  return null;
259
356
  }
260
357
  }
261
358
 
359
+ function displayOnlyCloudOrigin(cfg: JevConfig): JevConfig {
360
+ if (cfg.provider !== JEV_CLOUD_PROVIDER || cfg.credentialOrigin || typeof cfg.baseUrl !== "string") return cfg;
361
+ try {
362
+ return { ...cfg, credentialOrigin: new URL(cfg.baseUrl).origin };
363
+ } catch {
364
+ return cfg;
365
+ }
366
+ }
367
+
262
368
  /**
263
369
  * The fallback rate and the counts behind it, for the one line on the panel
264
- * that says whether Jev is working. Read only when Jev is actually on: on a
265
- * machine with no config the numbers are all zero and the line would be noise.
370
+ * that says whether Jev is working. Read only when Jev is on, or may be for
371
+ * hooks whose environment has the key: on a machine with no config the numbers
372
+ * are all zero and the line would be noise.
266
373
  *
267
374
  * `jevStats()` never throws and derives from the activity store the activity
268
375
  * tab already reads, so this adds no pipeline and no persisted state.
@@ -295,7 +402,10 @@ async function statsOrNull(on: boolean): Promise<JevSettingsStats | null> {
295
402
  function reviewableOrNull(on: boolean): JevReviewabilityView | null {
296
403
  if (!on) return null;
297
404
  try {
298
- const coverage = surveyReviewableCoverage();
405
+ // The launch directory, as every other dashboard read resolves project
406
+ // config: the standalone server chdirs into the package, and `jev status`
407
+ // counts the project it is run in.
408
+ const coverage = surveyReviewableCoverage(process.env.FAILPROOFAI_LAUNCH_CWD || process.cwd());
299
409
  return {
300
410
  enabled: coverage.enabled,
301
411
  reviewable: coverage.reviewable,
@@ -307,6 +417,42 @@ function reviewableOrNull(on: boolean): JevReviewabilityView | null {
307
417
  }
308
418
  }
309
419
 
420
+ /**
421
+ * The Cloud connection row, from the local files only. `readCredentials`
422
+ * returns every token on the machine; this function takes the three
423
+ * non-secret facts out and lets the rest go, so the object it returns — and
424
+ * therefore the page — never holds a key. Never throws.
425
+ */
426
+ function cloudConnectionView(): JevCloudConnectionView {
427
+ let connected = false;
428
+ let org: string | null = null;
429
+ let host: string | null = null;
430
+ try {
431
+ const creds = readCredentials();
432
+ const url = creds.cloud?.url ?? creds.ingest?.url ?? null;
433
+ connected = url !== null;
434
+ if (url !== null) {
435
+ try {
436
+ host = new URL(url).host;
437
+ } catch {
438
+ host = null;
439
+ }
440
+ }
441
+ const o = creds.org;
442
+ org = o ? (o.name && o.slug ? `${o.name} (${o.slug})` : (o.name ?? o.slug ?? o.id ?? null)) : null;
443
+ } catch {
444
+ // An unreadable file is "not connected" here, as it is to `config --status`.
445
+ }
446
+ let jev: JevCloudConnectionView["jev"] = "no";
447
+ try {
448
+ const slot = readJevCloudCredential().status;
449
+ jev = slot === "ok" ? "yes" : slot === "refused" ? "refused" : "no";
450
+ } catch {
451
+ jev = "no";
452
+ }
453
+ return { connected, org, host, jev };
454
+ }
455
+
310
456
  /**
311
457
  * Everything /settings shows about Jev, and nothing it must not.
312
458
  *
@@ -317,10 +463,12 @@ function reviewableOrNull(on: boolean): JevReviewabilityView | null {
317
463
  export async function getJevSettingsAction(): Promise<JevSettingsView> {
318
464
  const inspection = inspectJevConfig();
319
465
  const base = {
466
+ cloud: cloudConnectionView(),
320
467
  path: inspection.path,
321
468
  permissions: null as string | null,
322
469
  provider: null as JevProviderKind | null,
323
470
  baseUrl: "",
471
+ baseUrlQueryWithheld: false,
324
472
  accountId: "",
325
473
  model: { kind: "default" } as JevModelView,
326
474
  endpoint: null as string | null,
@@ -340,7 +488,7 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
340
488
  on: true,
341
489
  permissions: octal(inspection.mode),
342
490
  provider: cfg.provider,
343
- baseUrl: cfg.baseUrl ?? "",
491
+ ...baseUrlView(cfg.baseUrl),
344
492
  accountId: cfg.accountId ?? "",
345
493
  // A loadable config's model already passed `validateModel`, so this can
346
494
  // only be an id — run through the same gate anyway, because the gate is
@@ -365,7 +513,7 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
365
513
  on: false,
366
514
  permissions: octal(inspection.mode),
367
515
  provider: r.provider,
368
- baseUrl: r.baseUrl ?? "",
516
+ ...baseUrlView(r.baseUrl),
369
517
  accountId: r.accountId ?? "",
370
518
  model: modelView(r.model ?? ""),
371
519
  endpoint: endpointFor({ ...r, apiKey: KEY_STAND_IN }),
@@ -376,6 +524,38 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
376
524
  token: null,
377
525
  problem: inspection.problem,
378
526
  fix: `set ${JEV_API_KEY_ENV}, or save a token here`,
527
+ // Off in THIS process's environment only; hooks that have the variable
528
+ // still consult Jev, and the activity store records what they did.
529
+ stats: await statsOrNull(true),
530
+ };
531
+ }
532
+
533
+ if (inspection.status === "off" || inspection.status === "not-connected" || inspection.status === "key-lacks-jev") {
534
+ // Configured, and not running: switched off by its owner, or a FailproofAI
535
+ // Cloud file on a machine with no Cloud key — not connected at all, or
536
+ // connected with a key that has no Jev. Routing only, as for
537
+ // `key-missing` — the loader's routing object carries no key to leak.
538
+ const r = inspection.routing;
539
+ return {
540
+ ...base,
541
+ status: inspection.status,
542
+ on: false,
543
+ permissions: octal(inspection.mode),
544
+ provider: r.provider,
545
+ ...baseUrlView(r.baseUrl),
546
+ accountId: r.accountId ?? "",
547
+ model: modelView(r.model ?? ""),
548
+ endpoint: endpointFor({ ...r, apiKey: KEY_STAND_IN }),
549
+ mode: r.mode ?? DEFAULT_JEV_MODE,
550
+ timeoutMs: r.timeoutMs ?? null,
551
+ token: inspection.status === "off" ? offTokenPresence(r.provider) : null,
552
+ problem: inspection.status === "off" ? null : inspection.problem,
553
+ fix:
554
+ inspection.status === "not-connected"
555
+ ? "connect this machine with a key that carries jev:evaluate: failproofai config --token <key>"
556
+ : inspection.status === "key-lacks-jev"
557
+ ? "reconnect this machine with a key that carries jev:evaluate: failproofai config --token <key>"
558
+ : null,
379
559
  stats: null,
380
560
  };
381
561
  }
@@ -390,7 +570,7 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
390
570
  on: false,
391
571
  permissions: octal(inspection.mode),
392
572
  provider: r.provider,
393
- baseUrl: r.baseUrl,
573
+ ...baseUrlView(r.baseUrl),
394
574
  accountId: r.accountId,
395
575
  // This is the branch the rule exists for: a file whose `model` slot holds
396
576
  // a pasted key IS a refused file, and these fields are copied raw.
@@ -107,13 +107,18 @@ import {
107
107
  resolveDashboardHost,
108
108
  } from "@/lib/dashboard-host";
109
109
  import {
110
+ JEV_CLOUD_PROVIDER,
110
111
  JEV_PROVIDER_KINDS,
112
+ baseUrlWithoutQuery,
113
+ endpointGivenAsBase,
111
114
  jevConfigPath,
115
+ providerHostConflict,
112
116
  readJevConfigFileForUpdate,
113
117
  validateApiKey,
114
118
  validateBaseUrl,
115
119
  validateJevConfig,
116
120
  } from "@/src/hooks/semantic/jev-config";
121
+ import { endpointAsBaseReason } from "@/src/hooks/semantic/jev-client";
117
122
  import { getJevSettingsAction, type JevSettingsView } from "./get-jev-config";
118
123
 
119
124
  /**
@@ -129,7 +134,7 @@ export interface JevConfigInput {
129
134
  baseUrl: string;
130
135
  /** Cloudflare only. */
131
136
  accountId: string;
132
- /** "shadow" | "enforce". */
137
+ /** "off" | "shadow" | "enforce". */
133
138
  mode: string;
134
139
  /** "" keeps the key where it is: the stored one, or the environment's. */
135
140
  token: string;
@@ -225,6 +230,32 @@ function originOf(raw: string): string | null {
225
230
  }
226
231
  }
227
232
 
233
+ /**
234
+ * The base URL to WRITE for a submitted one.
235
+ *
236
+ * The panel is never sent a stored query string (`baseUrlView` in
237
+ * `get-jev-config.ts`), so what comes back from an untouched field is the stored
238
+ * URL with its query removed. Writing that verbatim would delete a query the
239
+ * person never saw — `?api-version=`, which some proxies route on — on any save
240
+ * that only meant to change the mode. So a submission that is exactly the
241
+ * stored URL minus its query keeps the stored one, and anything else is a URL
242
+ * they actually typed and is written as typed.
243
+ *
244
+ * Only a stored query the LOADER accepts is carried back. One it refuses is a
245
+ * credential in a base URL (`validateBaseUrl`), which is the very thing the
246
+ * person is on this page to repair: dropping it is the repair, and re-saving the
247
+ * file is the only way the panel has to perform it.
248
+ */
249
+ function baseUrlToStore(submitted: string, stored: unknown): string {
250
+ if (typeof stored !== "string" || stored === "") return submitted;
251
+ const keep = validateBaseUrl(stored);
252
+ if (!keep.ok) return submitted;
253
+ const shown = baseUrlWithoutQuery(keep.value);
254
+ if (!shown.hadQuery) return submitted;
255
+ const typed = validateBaseUrl(submitted);
256
+ return typed.ok && typed.value === shown.url ? keep.value : submitted;
257
+ }
258
+
228
259
  /**
229
260
  * `~/.failproofai` at the umask is group-writable on a umask-002 machine, and a
230
261
  * directory others can write into defeats the file's 0600 — they unlink it and
@@ -259,21 +290,55 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
259
290
  if (!(JEV_PROVIDER_KINDS as readonly string[]).includes(provider)) {
260
291
  return { ok: false, problem: `provider must be one of ${JEV_PROVIDER_KINDS.join(", ")}` };
261
292
  }
293
+ if (provider === JEV_CLOUD_PROVIDER) {
294
+ // Its endpoint and key come from this machine's FailproofAI Cloud
295
+ // connection, not from a form: `config --token` writes both, and the only
296
+ // thing this page may change about it is the mode (`setJevModeAction`).
297
+ return {
298
+ ok: false,
299
+ problem:
300
+ "FailproofAI Cloud's Jev is turned on by connecting this machine (failproofai config --token <key>), and switched with the mode control — not saved from this form.",
301
+ };
302
+ }
262
303
 
263
- const baseUrl = input.baseUrl.trim();
264
- if (baseUrl) {
265
- // Checked here, before anything is decided about the token, so an
266
- // unparseable URL is reported as an unparseable URL rather than as "that
267
- // endpoint is not the one the token was given for" — which is what a URL
268
- // that resolves to no origin would otherwise look like below.
269
- const url = validateBaseUrl(baseUrl);
270
- if (!url.ok) return { ok: false, problem: url.problem };
304
+ // Refused, as `setJevModeAction` and `jev setup --mode` refuse it — not
305
+ // skipped, which kept the old mode or wrote none (and none loads as enforce).
306
+ if (input.mode !== "off" && input.mode !== "shadow" && input.mode !== "enforce") {
307
+ return { ok: false, problem: 'mode must be "off", "shadow" or "enforce".' };
271
308
  }
272
309
 
310
+ const baseUrl = input.baseUrl.trim();
311
+ // Checked here, before anything is decided about the token, so an
312
+ // unparseable URL is reported as an unparseable URL rather than as "that
313
+ // endpoint is not the one the token was given for" — which is what a URL
314
+ // that resolves to no origin would otherwise look like below.
315
+ const url = baseUrl ? validateBaseUrl(baseUrl) : null;
316
+ if (url && !url.ok) return { ok: false, problem: url.problem };
317
+
273
318
  const existingFile = readJevConfigFileForUpdate();
274
319
  const existing = existingFile?.raw ?? null;
275
320
  const sameProvider = existing !== null && existing.provider === provider;
276
321
 
322
+ // `jev setup`'s refusal of an endpoint where a base belongs (`…/v1/models`):
323
+ // every call would go to `<that>/systemone` and 404 back to regex. Only for a
324
+ // URL typed here, as the CLI checks only a `--base-url` it was given — an
325
+ // older file whose base already ends in `/systemone` routes correctly and the
326
+ // loader takes it, so re-saving it untouched (a mode switch) must still work.
327
+ if (url?.ok) {
328
+ const stored = validateBaseUrl(existing?.baseUrl);
329
+ const untouched =
330
+ sameProvider && stored.ok && baseUrlWithoutQuery(stored.value).url === baseUrlWithoutQuery(url.value).url;
331
+ const conflict = untouched ? null : providerHostConflict(provider, url.value);
332
+ if (conflict) return { ok: false, problem: `${conflict}.` };
333
+ const asEndpoint = untouched ? null : endpointGivenAsBase(url.value);
334
+ if (asEndpoint) {
335
+ return {
336
+ ok: false,
337
+ problem: `that url is an endpoint, not an api base — its path ends in ${asEndpoint.suffix}. ${endpointAsBaseReason(url.value)} Use ${baseUrlWithoutQuery(asEndpoint.base).url} instead.`,
338
+ };
339
+ }
340
+ }
341
+
277
342
  // Same provider: update in place, keeping every field not named here —
278
343
  // `model`, a `timeoutMs` set from the CLI, and ones a newer failproofai wrote,
279
344
  // none of which this form knows about or may drop. A different provider starts
@@ -287,7 +352,7 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
287
352
  }
288
353
  next.provider = provider;
289
354
 
290
- if (baseUrl) next.baseUrl = baseUrl;
355
+ if (baseUrl) next.baseUrl = baseUrlToStore(baseUrl, existing?.baseUrl);
291
356
  else delete next.baseUrl;
292
357
 
293
358
  // `model` is NOT touched here, and that is the point: `JevConfigInput` has no
@@ -305,7 +370,7 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
305
370
  delete next.accountId;
306
371
  }
307
372
 
308
- if (input.mode === "shadow" || input.mode === "enforce") next.mode = input.mode;
373
+ next.mode = input.mode;
309
374
 
310
375
  // Where the key may travel. Unchanged provider AND unchanged origin, from a
311
376
  // file only its owner could have written — anything else asks again.
@@ -393,6 +458,86 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
393
458
  return { ok: true, view: await getJevSettingsAction() };
394
459
  }
395
460
 
461
+ /**
462
+ * Switch Jev's mode — `off`, `shadow` or `enforce` — and change NOTHING else.
463
+ *
464
+ * This is the on/off switch for FailproofAI Cloud's Jev, whose `jev.json` the
465
+ * page must not delete: its endpoint and key are not this page's to re-enter,
466
+ * so "off" by deletion would be off until the owner re-ran `config --token`
467
+ * with the key in hand. `off` keeps the file and runs no Jev at all
468
+ * (`loadJevConfig` returns null for it), which is the switch this needs.
469
+ * It works on a BYOK file too, where it keeps the key.
470
+ *
471
+ * What "nothing else" means, precisely: the existing object is spread and only
472
+ * `mode` is set — no field is added, dropped or normalised, and the key, the
473
+ * endpoint and the provider are exactly what they were. It is written the way
474
+ * every other write here is (`writeJsonAtomically`, 0600, owner-only
475
+ * directory), after the loader's own validator has checked the result with
476
+ * stand-ins in the key slots, so `enforce` is refused where the loader would
477
+ * refuse it (plain http to localhost) rather than written and then ignored.
478
+ *
479
+ * Refused for a file the loader calls too open: someone else may have written
480
+ * the endpoint it names, and re-saving it at 0600 would start trusting that
481
+ * endpoint. Jev is already off for such a file; the page shows why and how.
482
+ */
483
+ export async function setJevModeAction(mode: string): Promise<JevWriteResult> {
484
+ const refusal = await crossOriginRefusal();
485
+ if (refusal) return { ok: false, problem: refusal };
486
+ if (mode !== "off" && mode !== "shadow" && mode !== "enforce") {
487
+ return { ok: false, problem: 'mode must be "off", "shadow" or "enforce".' };
488
+ }
489
+
490
+ const existingFile = readJevConfigFileForUpdate();
491
+ if (existingFile === null) {
492
+ return {
493
+ ok: false,
494
+ problem: `there is no readable ${jevConfigPath()} to switch. connect this machine (failproofai config --token <key>) or set up your own endpoint below.`,
495
+ };
496
+ }
497
+ if (existingFile.tooOpen) {
498
+ return {
499
+ ok: false,
500
+ problem: `${jevConfigPath()} is open to other users, so Jev is already off and the endpoint it names is not trusted. fix its permissions first (chmod 600, and chmod 700 on its directory).`,
501
+ };
502
+ }
503
+
504
+ const next: Record<string, unknown> = { ...existingFile.raw, mode };
505
+
506
+ // The file as the loader would judge it, keys aside: a stored key is checked
507
+ // for real, a missing one (key-from-env, or the Cloud's) gets a stand-in —
508
+ // whether this machine is connected right now is not what a mode switch is
509
+ // about, and the page says so separately.
510
+ const cloud = next.provider === JEV_CLOUD_PROVIDER;
511
+ const checked = validateJevConfig(
512
+ next,
513
+ cloud || typeof next.apiKey === "string" ? null : ENV_KEY_STAND_IN,
514
+ cloud ? { url: originOrEmpty(next.baseUrl), key: ENV_KEY_STAND_IN } : null,
515
+ );
516
+ if (!checked.ok) return { ok: false, problem: checked.problem };
517
+
518
+ const path = jevConfigPath();
519
+ try {
520
+ writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
521
+ } catch (err) {
522
+ return {
523
+ ok: false,
524
+ problem: `could not write ${path} (${(err as NodeJS.ErrnoException).code ?? "error"}).`,
525
+ };
526
+ }
527
+ tightenConfigDir(path);
528
+ return { ok: true, view: await getJevSettingsAction() };
529
+ }
530
+
531
+ /** A stored base URL's origin, for a stand-in credential; "" when it has none. */
532
+ function originOrEmpty(raw: unknown): string {
533
+ if (typeof raw !== "string") return "";
534
+ try {
535
+ return new URL(raw).origin;
536
+ } catch {
537
+ return "";
538
+ }
539
+ }
540
+
396
541
  /**
397
542
  * Delete `~/.failproofai/jev.json`. Hooks go back to the regex engine on the
398
543
  * very next tool call — the loader reads the file per event, so there is
@@ -400,6 +545,10 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
400
545
  *
401
546
  * Removing a file that is not there succeeds: the user asked for Jev to be off,
402
547
  * and it is.
548
+ *
549
+ * The BYOK "turn jev off". The FailproofAI Cloud route is switched off with
550
+ * `setJevModeAction("off")` instead — see there for why deleting its file is
551
+ * the wrong switch.
403
552
  */
404
553
  export async function removeJevConfigAction(): Promise<JevWriteResult> {
405
554
  const refusal = await crossOriginRefusal();
@@ -492,9 +492,9 @@ function ActivityTab({
492
492
  const v = url.get("cli");
493
493
  return isKnownCli(v) ? v : "";
494
494
  });
495
- const [filterSource, setFilterSource] = useState<"" | "custom" | "convention" | "cloud" | "pack">(() => {
495
+ const [filterSource, setFilterSource] = useState<"" | "custom" | "convention" | "cloud" | "pack" | "jev">(() => {
496
496
  const v = url.get("source");
497
- return v === "custom" || v === "convention" || v === "cloud" || v === "pack" ? v : "";
497
+ return v === "custom" || v === "convention" || v === "cloud" || v === "pack" || v === "jev" ? v : "";
498
498
  });
499
499
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
500
500
  const filterTelemetryFirstRunRef = useRef(true);
@@ -665,7 +665,7 @@ function ActivityTab({
665
665
  onChange={(e) => {
666
666
  const v = e.target.value;
667
667
  setFilterSource(
668
- v === "custom" || v === "convention" || v === "cloud" || v === "pack" ? v : "",
668
+ v === "custom" || v === "convention" || v === "cloud" || v === "pack" || v === "jev" ? v : "",
669
669
  );
670
670
  }}
671
671
  className="filter-input"
@@ -676,6 +676,8 @@ function ActivityTab({
676
676
  <option value="convention">convention</option>
677
677
  <option value="cloud">cloud</option>
678
678
  <option value="pack">pack</option>
679
+ {/* Jev's own verdict, in enforce mode: no registered policy decided. */}
680
+ <option value="jev">jev</option>
679
681
  </select>
680
682
  </div>
681
683
  <div className="filter-group">