failproofai 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +9 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -10
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/pages/404.html +1 -1
  132. package/.next/standalone/.next/server/pages/500.html +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  135. package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
  136. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
  145. package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
  147. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  148. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  149. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  150. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  151. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  152. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  153. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  154. package/.next/standalone/app/settings/page.tsx +20 -1
  155. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  156. package/.next/standalone/app/settings/settings.css +79 -0
  157. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  158. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  159. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  163. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  164. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  165. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  166. package/.next/standalone/package.json +10 -10
  167. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  168. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  169. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  171. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  172. package/.next/standalone/server.js +1 -1
  173. package/README.md +3 -2
  174. package/bin/failproofai.mjs +148 -4
  175. package/dist/cli.mjs +18599 -9478
  176. package/dist/index.js +19 -1
  177. package/dist/worker.mjs +10500 -3773
  178. package/package.json +10 -10
  179. package/pi-extension/index.ts +11 -0
  180. package/scripts/build-policy-pack.mjs +53 -2
  181. package/src/audit/features.ts +3 -2
  182. package/src/hooks/builtin-policies.ts +195 -8
  183. package/src/hooks/cloud-connection.ts +190 -1
  184. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  185. package/src/hooks/cloud-introspect.ts +6 -0
  186. package/src/hooks/cloud-managed-policies.ts +22 -0
  187. package/src/hooks/configure-wizard.ts +26 -6
  188. package/src/hooks/custom-hooks-loader.ts +98 -10
  189. package/src/hooks/custom-hooks-registry.ts +45 -1
  190. package/src/hooks/effective-reviewers.ts +243 -0
  191. package/src/hooks/first-run-gate.ts +5 -0
  192. package/src/hooks/flush-cli.ts +35 -8
  193. package/src/hooks/fp-config.ts +266 -1
  194. package/src/hooks/fp-home.ts +23 -0
  195. package/src/hooks/fp-reset.ts +22 -4
  196. package/src/hooks/handler.ts +303 -7
  197. package/src/hooks/hook-activity-store.ts +118 -6
  198. package/src/hooks/hook-telemetry.ts +41 -0
  199. package/src/hooks/jev-activity.ts +385 -0
  200. package/src/hooks/jev-cli.ts +2172 -0
  201. package/src/hooks/jev-cloud-connection.ts +392 -0
  202. package/src/hooks/loader-utils.ts +6 -0
  203. package/src/hooks/manager.ts +45 -7
  204. package/src/hooks/pack-cli.ts +568 -37
  205. package/src/hooks/pack-failclosed.ts +3 -0
  206. package/src/hooks/pack-manifest.ts +489 -7
  207. package/src/hooks/pack-store.ts +194 -11
  208. package/src/hooks/policy-authority.ts +386 -0
  209. package/src/hooks/policy-catalog.ts +206 -0
  210. package/src/hooks/policy-evaluator.ts +963 -796
  211. package/src/hooks/policy-registry.ts +27 -1
  212. package/src/hooks/policy-reviewability.ts +258 -0
  213. package/src/hooks/policy-types.ts +128 -0
  214. package/src/hooks/semantic/combine.ts +581 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +513 -0
  217. package/src/hooks/semantic/envelope.ts +1255 -0
  218. package/src/hooks/semantic/evaluator.ts +556 -0
  219. package/src/hooks/semantic/facts.ts +387 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +1155 -0
  222. package/src/hooks/semantic/jev-config.ts +1142 -0
  223. package/src/hooks/semantic/jev-review.ts +389 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +427 -0
  226. package/src/hooks/semantic/pack-policies.ts +294 -0
  227. package/src/hooks/semantic/policies.ts +597 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2937 -0
  231. package/src/hooks/semantic/session-root.ts +116 -0
  232. package/src/hooks/semantic/types.ts +162 -0
  233. package/src/hooks/semver-precedence.ts +128 -0
  234. package/src/hooks/tui.ts +4 -0
  235. package/src/hooks/types.ts +1 -1
  236. package/src/hooks/worker-server.ts +121 -27
  237. package/src/index.ts +6 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  240. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  253. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
  255. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  258. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  259. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  260. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  261. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  263. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  265. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  266. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
  267. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
@@ -0,0 +1,569 @@
1
+ "use server";
2
+
3
+ /**
4
+ * Write side of the /settings "Jev" section — the dashboard's half of
5
+ * `failproofai jev setup` / `failproofai jev remove`.
6
+ *
7
+ * ## One file, one writer, one validator
8
+ *
9
+ * The file is `~/.failproofai/jev.json`, written through the same
10
+ * `writeJsonAtomically` the CLI's `jev setup` calls, with the same
11
+ * `{ mode: 0o600, dirMode: 0o700 }`, after the same `validateJevConfig` the
12
+ * loader itself runs. Nothing here re-states a rule that lives in
13
+ * `jev-config.ts`: not the URL scheme, not "plain http only in shadow mode",
14
+ * not "cloudflare needs an account id". A second copy of those rules is how the
15
+ * dashboard ends up writing a file the hooks then refuse — the exact failure
16
+ * mode this module exists to avoid.
17
+ *
18
+ * ## Cross-origin writes
19
+ *
20
+ * The dashboard is an unauthenticated local web server, so a page on any other
21
+ * site can POST to it from the victim's own browser. That matters more here
22
+ * than anywhere else on the surface: a cross-site write could point the
23
+ * evaluator at the attacker's endpoint — which then sees a redacted envelope
24
+ * for every command failproofai judges — or switch Jev on in enforce mode,
25
+ * where its answers can clear a deny.
26
+ *
27
+ * `proxy.ts` already refuses cross-origin mutating requests and non-loopback
28
+ * `Host` headers for the whole dashboard, and a Server Action is a POST, so it
29
+ * is covered. The check is repeated HERE, against the same
30
+ * `lib/dashboard-host` helpers so the two cannot diverge on what "loopback"
31
+ * or "same origin" means, because this is the one write on the dashboard whose
32
+ * payload is a credential and whose effect is the destination that credential
33
+ * is sent to. Two layers that read the same rule from one place cost a header
34
+ * lookup; one layer costs the whole feature if the middleware matcher is ever
35
+ * narrowed.
36
+ *
37
+ * ## The token
38
+ *
39
+ * It arrives, it is validated, it is written at 0600, and it is never returned.
40
+ * Both actions answer with a `JevSettingsView`, which carries a presence flag
41
+ * and no part of the key itself — see `get-jev-config.ts`.
42
+ *
43
+ * A stored token is KEPT only when the provider and the base URL are both
44
+ * unchanged and the file was owner-only. Anything else — a new provider, a
45
+ * moved endpoint, or a file other users could have written — asks for it
46
+ * again, because a key issued for one gateway must not be sent to another
47
+ * unasked. That is the CLI's rule, applied slightly more strictly: the CLI
48
+ * carries a key back to the provider's own API, and this surface does not,
49
+ * because a form with a blank field is a weaker statement of intent than a
50
+ * typed command.
51
+ *
52
+ * That rule is about a key MOVING, so it only applies where there is one to
53
+ * move. A config written by `jev setup --key-from-env` stores no key — a hook
54
+ * reads `FAILPROOFAI_JEV_API_KEY` at the moment it runs — and its endpoint,
55
+ * provider and mode are therefore editable here with the token field left
56
+ * blank: nothing travels anywhere it had not already been sent, and the key
57
+ * stays where its owner put it, outside the file. Asking for a token instead
58
+ * would be asking them to abandon that choice in order to change an endpoint,
59
+ * and the refusal did it while naming "the stored token" — a thing that
60
+ * deliberately does not exist in such a file.
61
+ *
62
+ * That includes a file the loader called too open. Re-saving it is the only
63
+ * remedy the panel has for those permissions — the write is 0600 and it
64
+ * tightens the directory — and there is no stored key to withhold, so refusing
65
+ * would leave the file broken and buy nothing: the endpoint that gets written is
66
+ * the one in the form, which the person can see and change, above the loader's
67
+ * own warning about the file. For a file that DOES store a key the refusal
68
+ * stands, because re-typing the token is what says "this key, for this
69
+ * endpoint".
70
+ *
71
+ * `--key-from-env` is inferred from the file being updated rather than asked
72
+ * for again, which is the one place this path is laxer than `jev setup`: the CLI
73
+ * makes you re-state `--key-from-env` when the provider changes. A form that
74
+ * offers no such flag would have no way to say it.
75
+ *
76
+ * ## What a save does NOT touch
77
+ *
78
+ * Only the fields the panel sends. Every other field in the file — `model`, a
79
+ * `timeoutMs` set from the CLI, anything a newer failproofai wrote — is carried
80
+ * over untouched. This is not a nicety. `model` decides which model id the
81
+ * request names, and a self-hosted or gateway endpoint that must be told its
82
+ * model stops answering anything at all once it is dropped — while the panel,
83
+ * whose write passed validation, reports success. A form must not be able to
84
+ * delete a field it does not show.
85
+ *
86
+ * The merge below is `jev setup`'s, field for field: spread the existing file
87
+ * when the provider is unchanged, carry `mode` and `timeoutMs` across when it
88
+ * is not, and set only what was named — `setOrClear` in `src/hooks/jev-cli.ts`,
89
+ * where a flag that was not given leaves its field alone. It is written out
90
+ * again here rather than called because that path is a terminal command
91
+ * (`argv` in, rendered lines out, its own key prompt) that lives in the hook
92
+ * bundle, and because this surface's key rule is deliberately stricter. What
93
+ * keeps the two from drifting is a test that runs both over the same file and
94
+ * compares the bytes: `__tests__/actions/update-jev-config.test.ts`, "agrees
95
+ * with `jev setup` about what an update keeps". Extracting one shared merge
96
+ * would be better still, and needs `src/hooks/semantic/jev-config.ts` to own
97
+ * it.
98
+ */
99
+
100
+ import { chmodSync, existsSync, statSync, unlinkSync } from "node:fs";
101
+ import { dirname } from "node:path";
102
+ import { headers } from "next/headers";
103
+ import { writeJsonAtomically } from "@/lib/atomic-write";
104
+ import {
105
+ hostnameFromHostHeader,
106
+ isLoopbackHostname,
107
+ resolveDashboardHost,
108
+ } from "@/lib/dashboard-host";
109
+ import {
110
+ JEV_CLOUD_PROVIDER,
111
+ JEV_PROVIDER_KINDS,
112
+ baseUrlWithoutQuery,
113
+ endpointGivenAsBase,
114
+ jevConfigPath,
115
+ providerHostConflict,
116
+ readJevConfigFileForUpdate,
117
+ validateApiKey,
118
+ validateBaseUrl,
119
+ validateJevConfig,
120
+ } from "@/src/hooks/semantic/jev-config";
121
+ import { endpointAsBaseReason } from "@/src/hooks/semantic/jev-client";
122
+ import { getJevSettingsAction, type JevSettingsView } from "./get-jev-config";
123
+
124
+ /**
125
+ * What the panel sends — the four fields the form owns, and the token.
126
+ *
127
+ * Every field is a string; "" means "not set". There is deliberately no `model`:
128
+ * the form does not show one, so it has nothing to say about it, and a field
129
+ * this type does not carry is a field `saveJevConfigAction` cannot clear.
130
+ */
131
+ export interface JevConfigInput {
132
+ provider: string;
133
+ /** "" routes to the provider's own API. */
134
+ baseUrl: string;
135
+ /** Cloudflare only. */
136
+ accountId: string;
137
+ /** "off" | "shadow" | "enforce". */
138
+ mode: string;
139
+ /** "" keeps the key where it is: the stored one, or the environment's. */
140
+ token: string;
141
+ }
142
+
143
+ export type JevWriteResult =
144
+ | { ok: true; view: JevSettingsView }
145
+ /**
146
+ * Returned, never thrown. Next masks a thrown server-action error before the
147
+ * browser sees it — the client gets an opaque digest and no message — so a
148
+ * validation problem the user could act on would arrive as "something went
149
+ * wrong". The same reasoning as `SetAutoAuditResult` next door.
150
+ */
151
+ | {
152
+ ok: false;
153
+ problem: string;
154
+ /** The one failure whose remedy is "type the token again", so the panel can say so on the field. */
155
+ needsToken?: true;
156
+ };
157
+
158
+ /**
159
+ * The refusal text for every cross-origin shape, deliberately identical.
160
+ *
161
+ * Naming which check failed is a probing aid on an unauthenticated surface, and
162
+ * the legitimate caller — the dashboard's own page — can never see this string.
163
+ */
164
+ const CROSS_ORIGIN_REFUSAL =
165
+ "this request did not come from the dashboard in your browser, so nothing was written.";
166
+
167
+ /** Validation-only placeholder for a config whose key comes from the environment. Never written. */
168
+ const ENV_KEY_STAND_IN = "env-key-stand-in";
169
+
170
+ /**
171
+ * Refuse anything that is not a same-machine, same-origin call.
172
+ *
173
+ * A transcription of `proxy.ts`'s rule, built from the same
174
+ * `lib/dashboard-host` helpers rather than its own idea of loopback:
175
+ *
176
+ * - on a loopback bind the `Host` must be loopback (this is what defeats DNS
177
+ * rebinding, whose whole trick is arriving with an attacker-controlled Host
178
+ * that its own Origin then matches);
179
+ * - a mutating call with an `Origin` must have one whose full authority
180
+ * equals that Host — another app on localhost:3000 is a different origin;
181
+ * - no `Origin` at all is a non-browser caller, allowed only on a loopback
182
+ * bind, where it is necessarily a local process that could edit
183
+ * `jev.json` directly anyway. On a deliberately non-loopback bind
184
+ * (`--host`, `FAILPROOFAI_DASHBOARD_HOST`) that describes every curl on the
185
+ * network segment, so it is refused.
186
+ */
187
+ async function crossOriginRefusal(): Promise<string | null> {
188
+ let h: Headers;
189
+ try {
190
+ h = await headers();
191
+ } catch {
192
+ // No request context at all. Nothing legitimate reaches these actions that
193
+ // way, and a write of a credential is not the place to assume otherwise.
194
+ return CROSS_ORIGIN_REFUSAL;
195
+ }
196
+ const bindHost = resolveDashboardHost(undefined, process.env.FAILPROOFAI_DASHBOARD_HOST);
197
+ const boundToLoopback = isLoopbackHostname(bindHost);
198
+ const hostHeader = h.get("host");
199
+
200
+ if (boundToLoopback) {
201
+ if (!hostHeader || !isLoopbackHostname(hostnameFromHostHeader(hostHeader))) {
202
+ return CROSS_ORIGIN_REFUSAL;
203
+ }
204
+ }
205
+
206
+ const origin = h.get("origin");
207
+ if (!origin) return boundToLoopback ? null : CROSS_ORIGIN_REFUSAL;
208
+
209
+ let originHost: string | null = null;
210
+ try {
211
+ // "null" (sandboxed iframes, some redirects) is a real Origin value; the
212
+ // URL constructor throws on it, which is the behaviour we want.
213
+ originHost = new URL(origin).host.toLowerCase();
214
+ } catch {
215
+ originHost = null;
216
+ }
217
+ return originHost && originHost === (hostHeader ?? "").toLowerCase() ? null : CROSS_ORIGIN_REFUSAL;
218
+ }
219
+
220
+ /** A base URL reduced to the thing that decides whether a key may travel: its origin. */
221
+ function originOf(raw: string): string | null {
222
+ if (!raw) return null;
223
+ const r = validateBaseUrl(raw);
224
+ if (!r.ok) return null;
225
+ try {
226
+ const o = new URL(r.value).origin;
227
+ return o === "null" ? null : o;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
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
+
259
+ /**
260
+ * `~/.failproofai` at the umask is group-writable on a umask-002 machine, and a
261
+ * directory others can write into defeats the file's 0600 — they unlink it and
262
+ * leave their own, which every check on the file then passes. The one command
263
+ * that puts a key there takes those bits off, so the one PAGE that does must
264
+ * too. Exactly the write bits the loader refuses and no more.
265
+ */
266
+ function tightenConfigDir(path: string): void {
267
+ if (process.platform === "win32") return;
268
+ const dir = dirname(path);
269
+ try {
270
+ const before = statSync(dir).mode & 0o777;
271
+ if ((before & 0o022) !== 0) chmodSync(dir, before & ~0o022);
272
+ } catch {
273
+ // Not fatal: the loader reports a directory it will not read from, and the
274
+ // panel shows that reason.
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Save the Jev configuration, exactly as `failproofai jev setup` would.
280
+ *
281
+ * Returns the re-read view on success rather than echoing the input, so the
282
+ * panel confirms against what is actually on disk — including the permissions,
283
+ * the resolved endpoint and the provider default model it did not type.
284
+ */
285
+ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWriteResult> {
286
+ const refusal = await crossOriginRefusal();
287
+ if (refusal) return { ok: false, problem: refusal };
288
+
289
+ const provider = input.provider.trim();
290
+ if (!(JEV_PROVIDER_KINDS as readonly string[]).includes(provider)) {
291
+ return { ok: false, problem: `provider must be one of ${JEV_PROVIDER_KINDS.join(", ")}` };
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
+ }
303
+
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".' };
308
+ }
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
+
318
+ const existingFile = readJevConfigFileForUpdate();
319
+ const existing = existingFile?.raw ?? null;
320
+ const sameProvider = existing !== null && existing.provider === provider;
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
+
342
+ // Same provider: update in place, keeping every field not named here —
343
+ // `model`, a `timeoutMs` set from the CLI, and ones a newer failproofai wrote,
344
+ // none of which this form knows about or may drop. A different provider starts
345
+ // over: a key, model or URL for one gateway means nothing to another. Mode and
346
+ // timeout are provider-neutral, so they carry across, exactly as `jev setup`
347
+ // carries them.
348
+ const next: Record<string, unknown> = sameProvider ? { ...existing } : {};
349
+ if (!sameProvider && existing) {
350
+ if (existing.mode !== undefined) next.mode = existing.mode;
351
+ if (existing.timeoutMs !== undefined) next.timeoutMs = existing.timeoutMs;
352
+ }
353
+ next.provider = provider;
354
+
355
+ if (baseUrl) next.baseUrl = baseUrlToStore(baseUrl, existing?.baseUrl);
356
+ else delete next.baseUrl;
357
+
358
+ // `model` is NOT touched here, and that is the point: `JevConfigInput` has no
359
+ // model, so the spread above is the whole story for it. The CLI's `setOrClear`
360
+ // clears a field only for `--model default`, which is a thing a person typed;
361
+ // an empty input from a form that never offered the field is not.
362
+
363
+ const accountId = input.accountId.trim();
364
+ if (provider === "cloudflare") {
365
+ if (accountId) next.accountId = accountId;
366
+ else delete next.accountId;
367
+ } else {
368
+ // An account id means nothing anywhere else, and leaving a stale one behind
369
+ // would show up in `jev status` as a field this provider ignores.
370
+ delete next.accountId;
371
+ }
372
+
373
+ next.mode = input.mode;
374
+
375
+ // Where the key may travel. Unchanged provider AND unchanged origin, from a
376
+ // file only its owner could have written — anything else asks again.
377
+ const routeHeld =
378
+ sameProvider &&
379
+ existingFile?.tooOpen !== true &&
380
+ originOf(baseUrl) === originOf(typeof existing?.baseUrl === "string" ? existing.baseUrl : "");
381
+
382
+ /** Whether the file has a key in it at all. */
383
+ const storesKey = typeof existing?.apiKey === "string" && existing.apiKey !== "";
384
+
385
+ /**
386
+ * Whether the file being updated is one whose owner chose to keep the key OUT
387
+ * of it — `jev setup --key-from-env`, which the loader reports as
388
+ * `key-missing`, or as `ok` with `keySource: "env"` where the variable is set.
389
+ *
390
+ * Every refusal below is about a STORED key being carried somewhere it was not
391
+ * issued for, and there is none here: a hook reads
392
+ * `FAILPROOFAI_JEV_API_KEY` at the moment it runs, so the endpoint, the
393
+ * provider and the mode can all be edited with the token field left blank and
394
+ * nothing moves.
395
+ *
396
+ * It is the loader's own validator that decides, with a stand-in in the one
397
+ * slot that is deliberately empty — so a file that is merely BROKEN, and a
398
+ * machine with no file at all, are not mistaken for a deliberate choice and
399
+ * silently saved without any key. Those still ask for the token.
400
+ */
401
+ const keyFromEnvConfig =
402
+ existing !== null && !storesKey && validateJevConfig(existing, ENV_KEY_STAND_IN).ok;
403
+
404
+ const token = input.token.trim();
405
+ if (token) {
406
+ const bad = validateApiKey(token);
407
+ if (bad) return { ok: false, problem: bad, needsToken: true };
408
+ next.apiKey = token;
409
+ } else if (!routeHeld && !keyFromEnvConfig) {
410
+ return {
411
+ ok: false,
412
+ needsToken: true,
413
+ problem: !storesKey
414
+ ? // Nothing stored to carry and no deliberate absence to preserve: the
415
+ // only thing missing is a key, and no sentence here may imply there
416
+ // is one on disk.
417
+ "enter the token for this provider."
418
+ : sameProvider && existingFile?.tooOpen === true
419
+ ? // The loader's "someone else may have chosen this endpoint" case. The
420
+ // stored token is not carried anywhere from a file other users could
421
+ // have written, whatever endpoint it names.
422
+ `${jevConfigPath()} was open to other users, so the endpoint it names may not be one you chose and its stored token is not reused. check the endpoint above, then enter the token for it.`
423
+ : sameProvider
424
+ ? "that endpoint is not the one the stored token was given for, so it is not sent there. enter the token for it."
425
+ : "enter the token for this provider.",
426
+ };
427
+ }
428
+ // Otherwise the key stays exactly where it is: a stored one is carried over by
429
+ // the spread above, and a config that takes it from the environment keeps
430
+ // taking it from the environment.
431
+
432
+ // A config with no stored key takes it from `FAILPROOFAI_JEV_API_KEY` when a
433
+ // hook runs. That is a shape `jev setup --key-from-env` writes and this
434
+ // surface preserves but never creates, so the rest of the file still has to
435
+ // be checked — hence a stand-in in the one slot that is deliberately empty.
436
+ // It is never written and never sent.
437
+ const keyFromEnv = next.apiKey === undefined;
438
+ const checked = validateJevConfig(next, keyFromEnv ? ENV_KEY_STAND_IN : null);
439
+ if (!checked.ok) {
440
+ return {
441
+ ok: false,
442
+ problem: checked.problem,
443
+ ...(checked.missingKey === true ? { needsToken: true as const } : {}),
444
+ };
445
+ }
446
+
447
+ const path = jevConfigPath();
448
+ try {
449
+ writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
450
+ } catch (err) {
451
+ return {
452
+ ok: false,
453
+ problem: `could not write ${path} (${(err as NodeJS.ErrnoException).code ?? "error"}).`,
454
+ };
455
+ }
456
+ tightenConfigDir(path);
457
+
458
+ return { ok: true, view: await getJevSettingsAction() };
459
+ }
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
+
541
+ /**
542
+ * Delete `~/.failproofai/jev.json`. Hooks go back to the regex engine on the
543
+ * very next tool call — the loader reads the file per event, so there is
544
+ * nothing to restart and nothing to un-cache.
545
+ *
546
+ * Removing a file that is not there succeeds: the user asked for Jev to be off,
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.
552
+ */
553
+ export async function removeJevConfigAction(): Promise<JevWriteResult> {
554
+ const refusal = await crossOriginRefusal();
555
+ if (refusal) return { ok: false, problem: refusal };
556
+
557
+ const path = jevConfigPath();
558
+ if (existsSync(path)) {
559
+ try {
560
+ unlinkSync(path);
561
+ } catch (err) {
562
+ return {
563
+ ok: false,
564
+ problem: `could not remove ${path} (${(err as NodeJS.ErrnoException).code ?? "error"}).`,
565
+ };
566
+ }
567
+ }
568
+ return { ok: true, view: await getJevSettingsAction() };
569
+ }
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The activity view's rendering of what Jev (the two-tier evaluator) did on a
5
+ * row.
6
+ *
7
+ * Only the rows where Jev changed or could have changed something get a pill:
8
+ * a clear (a regex deny Jev overruled), a fallback (Jev's answer was not used —
9
+ * unavailable, truncated or mismatched — and the regex policies decided alone),
10
+ * and a shadow-mode row where Jev disagreed with what was enforced. A fallback
11
+ * where Jev did answer (the call was truncated to fit the envelope) and its
12
+ * unapplied verdict was stricter than what was enforced gets a louder fallback
13
+ * pill; the collector ships that row on its own for the same reason.
14
+ *
15
+ * An ordinary Jev allow gets none — on a configured machine
16
+ * that is nearly every row, and a pill on all of them marks nothing. Nor does a
17
+ * hard deny Jev was never consulted on, or a call no semantic policy applied
18
+ * to: the detail panel says so, but the row is an ordinary regex result.
19
+ */
20
+ import { describeJevActivity, jevOutcome, sanitizeJevActivity, type JevActivityFields } from "@/src/hooks/jev-activity";
21
+
22
+ type JevRow = JevActivityFields & { decision?: string };
23
+
24
+ const SEVERITY: Record<string, number> = { allow: 0, instruct: 1, deny: 2 };
25
+
26
+ /** Which pill a row gets, if any. Exported for tests. */
27
+ export function jevPillKind(
28
+ item: JevRow,
29
+ ): "cleared" | "would-clear" | "fallback" | "fallback-stricter" | "shadow-stricter" | null {
30
+ const e = sanitizeJevActivity(item);
31
+ const outcome = jevOutcome(e);
32
+ if (outcome === null || outcome === "not-consulted" || outcome === "no-request") return null;
33
+ const jevWasStricter = () => (SEVERITY[e.jevDecision ?? "allow"] ?? 0) > (SEVERITY[item.decision ?? "allow"] ?? 0);
34
+ if (outcome === "fallback") return e.jevDecision !== undefined && jevWasStricter() ? "fallback-stricter" : "fallback";
35
+ const cleared = (e.jevCleared ?? []).length > 0;
36
+ if (e.jevMode === "shadow") {
37
+ if (cleared) return "would-clear";
38
+ return jevWasStricter() ? "shadow-stricter" : null;
39
+ }
40
+ return cleared ? "cleared" : null;
41
+ }
42
+
43
+ const PILLS = {
44
+ cleared: {
45
+ label: "jev cleared",
46
+ title: "Jev reviewed a reviewable policy's block and cleared it",
47
+ className: "border-sky-500/40 bg-sky-500/10 text-sky-600 dark:text-sky-400",
48
+ },
49
+ "would-clear": {
50
+ label: "jev shadow",
51
+ title: "Shadow mode: Jev would have cleared a block here; the regex result was enforced",
52
+ className: "border-sky-500/30 bg-sky-500/5 text-sky-600/80 dark:text-sky-400/80",
53
+ },
54
+ "shadow-stricter": {
55
+ label: "jev shadow",
56
+ title: "Shadow mode: Jev would have been stricter here; the regex result was enforced",
57
+ className: "border-sky-500/30 bg-sky-500/5 text-sky-600/80 dark:text-sky-400/80",
58
+ },
59
+ "fallback-stricter": {
60
+ label: "jev fallback",
61
+ title: "Jev answered but its answer was not applied (e.g. the call was truncated); it would have been stricter than the regex result",
62
+ className: "border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400",
63
+ },
64
+ fallback: {
65
+ label: "jev fallback",
66
+ title: "Jev's answer was not used for this call (unavailable, truncated or mismatched); the regex policies decided alone",
67
+ className: "border-muted-foreground/30 bg-muted/40 text-muted-foreground",
68
+ },
69
+ } as const;
70
+
71
+ /** Marks a row where Jev cleared, fell back, or (in shadow mode) disagreed. */
72
+ export function JevPill({ item }: { item: JevRow }) {
73
+ const kind = jevPillKind(item);
74
+ if (!kind) return null;
75
+ const pill = PILLS[kind];
76
+ return (
77
+ <span
78
+ title={pill.title}
79
+ className={`ml-1.5 inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium uppercase tracking-wide border ${pill.className}`}
80
+ >
81
+ {pill.label}
82
+ </span>
83
+ );
84
+ }
85
+
86
+ /** The detail-panel line: what Jev said, cleared, or why it fell back. */
87
+ export function JevNote({ item }: { item: JevRow }) {
88
+ const facts = describeJevActivity(item);
89
+ if (!facts) return null;
90
+ return (
91
+ <div className="sm:col-span-2 lg:col-span-3">
92
+ <span className="text-muted-foreground">Semantic review: </span>
93
+ <span className="font-mono text-foreground">{facts.join(" · ")}</span>
94
+ </div>
95
+ );
96
+ }