failproofai 1.0.7-beta.2 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +7 -8
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -13
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +7 -8
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +6 -7
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -41
- package/.next/standalone/.next/server/app/settings/page.js +9 -12
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-i_gzc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _166t73i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0w6mzq5._.js → node_modules_next_dist_0drixxt._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -56
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
- package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 0__8a7m868fvf.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 0o6qlkgubtoex.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 13i7-9is-vhys.js} +1 -1
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/{2_pltstd8-xgs.js → 2klitrtzpaoe0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 2mdh397ghgnvv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
- package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +69 -0
- package/.next/standalone/.next/static/chunks/{1qd741hzlmjbo.js → 43ufqrz8qo3h-.js} +1 -1
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/pack-actions.ts +0 -12
- package/.next/standalone/app/policies/hooks-client.tsx +0 -9
- package/.next/standalone/app/settings/page.tsx +1 -20
- package/.next/standalone/app/settings/settings-client.tsx +1 -27
- package/.next/standalone/app/settings/settings.css +0 -79
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +2 -2
- package/bin/failproofai.mjs +2 -115
- package/dist/cli.mjs +6543 -13872
- package/dist/index.js +1 -19
- package/dist/worker.mjs +2022 -8055
- package/package.json +10 -10
- package/pi-extension/index.ts +0 -11
- package/scripts/build-policy-pack.mjs +2 -53
- package/src/audit/features.ts +2 -3
- package/src/hooks/builtin-policies.ts +8 -177
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +0 -22
- package/src/hooks/custom-hooks-loader.ts +7 -45
- package/src/hooks/custom-hooks-registry.ts +1 -45
- package/src/hooks/first-run-gate.ts +0 -5
- package/src/hooks/fp-home.ts +0 -23
- package/src/hooks/handler.ts +6 -265
- package/src/hooks/hook-activity-store.ts +1 -105
- package/src/hooks/hook-telemetry.ts +0 -41
- package/src/hooks/loader-utils.ts +0 -6
- package/src/hooks/manager.ts +1 -1
- package/src/hooks/pack-cli.ts +19 -412
- package/src/hooks/pack-manifest.ts +7 -479
- package/src/hooks/pack-store.ts +11 -157
- package/src/hooks/policy-catalog.ts +0 -204
- package/src/hooks/policy-evaluator.ts +796 -940
- package/src/hooks/policy-registry.ts +0 -25
- package/src/hooks/policy-types.ts +0 -126
- package/src/hooks/types.ts +1 -1
- package/src/hooks/worker-server.ts +26 -119
- package/src/index.ts +0 -6
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +0 -151
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
- package/.next/standalone/.next/static/chunks/043j99m8ykg__.css +0 -2
- package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
- package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
- package/.next/standalone/.next/static/chunks/3otmypm6j_xfo.js +0 -6
- package/.next/standalone/.next/static/chunks/3yxro_r2_o9ad.js +0 -69
- package/.next/standalone/PROBE-FOLLOWUP.md +0 -186
- package/.next/standalone/app/actions/get-jev-config.ts +0 -424
- package/.next/standalone/app/actions/update-jev-config.ts +0 -420
- package/.next/standalone/app/components/jev-notices.tsx +0 -96
- package/.next/standalone/app/settings/jev-panel.tsx +0 -473
- package/src/hooks/effective-reviewers.ts +0 -79
- package/src/hooks/jev-activity.ts +0 -385
- package/src/hooks/jev-cli.ts +0 -1249
- package/src/hooks/policy-authority.ts +0 -333
- package/src/hooks/policy-reviewability.ts +0 -229
- package/src/hooks/semantic/combine.ts +0 -541
- package/src/hooks/semantic/compile.ts +0 -176
- package/src/hooks/semantic/decide.ts +0 -392
- package/src/hooks/semantic/envelope.ts +0 -1296
- package/src/hooks/semantic/evaluator.ts +0 -547
- package/src/hooks/semantic/facts.ts +0 -292
- package/src/hooks/semantic/intent.ts +0 -1190
- package/src/hooks/semantic/jev-client.ts +0 -643
- package/src/hooks/semantic/jev-config.ts +0 -594
- package/src/hooks/semantic/jev-review.ts +0 -374
- package/src/hooks/semantic/jev-stats.ts +0 -289
- package/src/hooks/semantic/jev-throttle.ts +0 -421
- package/src/hooks/semantic/pack-policies.ts +0 -251
- package/src/hooks/semantic/policies.ts +0 -596
- package/src/hooks/semantic/precondition-names.ts +0 -60
- package/src/hooks/semantic/preconditions.ts +0 -58
- package/src/hooks/semantic/redact.ts +0 -2910
- package/src/hooks/semantic/types.ts +0 -145
- package/src/hooks/semver-precedence.ts +0 -128
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → PgeWCHmyVbjRznv2VO7KF}/_ssgManifest.js +0 -0
|
@@ -1,420 +0,0 @@
|
|
|
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_PROVIDER_KINDS,
|
|
111
|
-
jevConfigPath,
|
|
112
|
-
readJevConfigFileForUpdate,
|
|
113
|
-
validateApiKey,
|
|
114
|
-
validateBaseUrl,
|
|
115
|
-
validateJevConfig,
|
|
116
|
-
} from "@/src/hooks/semantic/jev-config";
|
|
117
|
-
import { getJevSettingsAction, type JevSettingsView } from "./get-jev-config";
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* What the panel sends — the four fields the form owns, and the token.
|
|
121
|
-
*
|
|
122
|
-
* Every field is a string; "" means "not set". There is deliberately no `model`:
|
|
123
|
-
* the form does not show one, so it has nothing to say about it, and a field
|
|
124
|
-
* this type does not carry is a field `saveJevConfigAction` cannot clear.
|
|
125
|
-
*/
|
|
126
|
-
export interface JevConfigInput {
|
|
127
|
-
provider: string;
|
|
128
|
-
/** "" routes to the provider's own API. */
|
|
129
|
-
baseUrl: string;
|
|
130
|
-
/** Cloudflare only. */
|
|
131
|
-
accountId: string;
|
|
132
|
-
/** "shadow" | "enforce". */
|
|
133
|
-
mode: string;
|
|
134
|
-
/** "" keeps the key where it is: the stored one, or the environment's. */
|
|
135
|
-
token: string;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export type JevWriteResult =
|
|
139
|
-
| { ok: true; view: JevSettingsView }
|
|
140
|
-
/**
|
|
141
|
-
* Returned, never thrown. Next masks a thrown server-action error before the
|
|
142
|
-
* browser sees it — the client gets an opaque digest and no message — so a
|
|
143
|
-
* validation problem the user could act on would arrive as "something went
|
|
144
|
-
* wrong". The same reasoning as `SetAutoAuditResult` next door.
|
|
145
|
-
*/
|
|
146
|
-
| {
|
|
147
|
-
ok: false;
|
|
148
|
-
problem: string;
|
|
149
|
-
/** The one failure whose remedy is "type the token again", so the panel can say so on the field. */
|
|
150
|
-
needsToken?: true;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The refusal text for every cross-origin shape, deliberately identical.
|
|
155
|
-
*
|
|
156
|
-
* Naming which check failed is a probing aid on an unauthenticated surface, and
|
|
157
|
-
* the legitimate caller — the dashboard's own page — can never see this string.
|
|
158
|
-
*/
|
|
159
|
-
const CROSS_ORIGIN_REFUSAL =
|
|
160
|
-
"this request did not come from the dashboard in your browser, so nothing was written.";
|
|
161
|
-
|
|
162
|
-
/** Validation-only placeholder for a config whose key comes from the environment. Never written. */
|
|
163
|
-
const ENV_KEY_STAND_IN = "env-key-stand-in";
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Refuse anything that is not a same-machine, same-origin call.
|
|
167
|
-
*
|
|
168
|
-
* A transcription of `proxy.ts`'s rule, built from the same
|
|
169
|
-
* `lib/dashboard-host` helpers rather than its own idea of loopback:
|
|
170
|
-
*
|
|
171
|
-
* - on a loopback bind the `Host` must be loopback (this is what defeats DNS
|
|
172
|
-
* rebinding, whose whole trick is arriving with an attacker-controlled Host
|
|
173
|
-
* that its own Origin then matches);
|
|
174
|
-
* - a mutating call with an `Origin` must have one whose full authority
|
|
175
|
-
* equals that Host — another app on localhost:3000 is a different origin;
|
|
176
|
-
* - no `Origin` at all is a non-browser caller, allowed only on a loopback
|
|
177
|
-
* bind, where it is necessarily a local process that could edit
|
|
178
|
-
* `jev.json` directly anyway. On a deliberately non-loopback bind
|
|
179
|
-
* (`--host`, `FAILPROOFAI_DASHBOARD_HOST`) that describes every curl on the
|
|
180
|
-
* network segment, so it is refused.
|
|
181
|
-
*/
|
|
182
|
-
async function crossOriginRefusal(): Promise<string | null> {
|
|
183
|
-
let h: Headers;
|
|
184
|
-
try {
|
|
185
|
-
h = await headers();
|
|
186
|
-
} catch {
|
|
187
|
-
// No request context at all. Nothing legitimate reaches these actions that
|
|
188
|
-
// way, and a write of a credential is not the place to assume otherwise.
|
|
189
|
-
return CROSS_ORIGIN_REFUSAL;
|
|
190
|
-
}
|
|
191
|
-
const bindHost = resolveDashboardHost(undefined, process.env.FAILPROOFAI_DASHBOARD_HOST);
|
|
192
|
-
const boundToLoopback = isLoopbackHostname(bindHost);
|
|
193
|
-
const hostHeader = h.get("host");
|
|
194
|
-
|
|
195
|
-
if (boundToLoopback) {
|
|
196
|
-
if (!hostHeader || !isLoopbackHostname(hostnameFromHostHeader(hostHeader))) {
|
|
197
|
-
return CROSS_ORIGIN_REFUSAL;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const origin = h.get("origin");
|
|
202
|
-
if (!origin) return boundToLoopback ? null : CROSS_ORIGIN_REFUSAL;
|
|
203
|
-
|
|
204
|
-
let originHost: string | null = null;
|
|
205
|
-
try {
|
|
206
|
-
// "null" (sandboxed iframes, some redirects) is a real Origin value; the
|
|
207
|
-
// URL constructor throws on it, which is the behaviour we want.
|
|
208
|
-
originHost = new URL(origin).host.toLowerCase();
|
|
209
|
-
} catch {
|
|
210
|
-
originHost = null;
|
|
211
|
-
}
|
|
212
|
-
return originHost && originHost === (hostHeader ?? "").toLowerCase() ? null : CROSS_ORIGIN_REFUSAL;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** A base URL reduced to the thing that decides whether a key may travel: its origin. */
|
|
216
|
-
function originOf(raw: string): string | null {
|
|
217
|
-
if (!raw) return null;
|
|
218
|
-
const r = validateBaseUrl(raw);
|
|
219
|
-
if (!r.ok) return null;
|
|
220
|
-
try {
|
|
221
|
-
const o = new URL(r.value).origin;
|
|
222
|
-
return o === "null" ? null : o;
|
|
223
|
-
} catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* `~/.failproofai` at the umask is group-writable on a umask-002 machine, and a
|
|
230
|
-
* directory others can write into defeats the file's 0600 — they unlink it and
|
|
231
|
-
* leave their own, which every check on the file then passes. The one command
|
|
232
|
-
* that puts a key there takes those bits off, so the one PAGE that does must
|
|
233
|
-
* too. Exactly the write bits the loader refuses and no more.
|
|
234
|
-
*/
|
|
235
|
-
function tightenConfigDir(path: string): void {
|
|
236
|
-
if (process.platform === "win32") return;
|
|
237
|
-
const dir = dirname(path);
|
|
238
|
-
try {
|
|
239
|
-
const before = statSync(dir).mode & 0o777;
|
|
240
|
-
if ((before & 0o022) !== 0) chmodSync(dir, before & ~0o022);
|
|
241
|
-
} catch {
|
|
242
|
-
// Not fatal: the loader reports a directory it will not read from, and the
|
|
243
|
-
// panel shows that reason.
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Save the Jev configuration, exactly as `failproofai jev setup` would.
|
|
249
|
-
*
|
|
250
|
-
* Returns the re-read view on success rather than echoing the input, so the
|
|
251
|
-
* panel confirms against what is actually on disk — including the permissions,
|
|
252
|
-
* the resolved endpoint and the provider default model it did not type.
|
|
253
|
-
*/
|
|
254
|
-
export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWriteResult> {
|
|
255
|
-
const refusal = await crossOriginRefusal();
|
|
256
|
-
if (refusal) return { ok: false, problem: refusal };
|
|
257
|
-
|
|
258
|
-
const provider = input.provider.trim();
|
|
259
|
-
if (!(JEV_PROVIDER_KINDS as readonly string[]).includes(provider)) {
|
|
260
|
-
return { ok: false, problem: `provider must be one of ${JEV_PROVIDER_KINDS.join(", ")}` };
|
|
261
|
-
}
|
|
262
|
-
|
|
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 };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const existingFile = readJevConfigFileForUpdate();
|
|
274
|
-
const existing = existingFile?.raw ?? null;
|
|
275
|
-
const sameProvider = existing !== null && existing.provider === provider;
|
|
276
|
-
|
|
277
|
-
// Same provider: update in place, keeping every field not named here —
|
|
278
|
-
// `model`, a `timeoutMs` set from the CLI, and ones a newer failproofai wrote,
|
|
279
|
-
// none of which this form knows about or may drop. A different provider starts
|
|
280
|
-
// over: a key, model or URL for one gateway means nothing to another. Mode and
|
|
281
|
-
// timeout are provider-neutral, so they carry across, exactly as `jev setup`
|
|
282
|
-
// carries them.
|
|
283
|
-
const next: Record<string, unknown> = sameProvider ? { ...existing } : {};
|
|
284
|
-
if (!sameProvider && existing) {
|
|
285
|
-
if (existing.mode !== undefined) next.mode = existing.mode;
|
|
286
|
-
if (existing.timeoutMs !== undefined) next.timeoutMs = existing.timeoutMs;
|
|
287
|
-
}
|
|
288
|
-
next.provider = provider;
|
|
289
|
-
|
|
290
|
-
if (baseUrl) next.baseUrl = baseUrl;
|
|
291
|
-
else delete next.baseUrl;
|
|
292
|
-
|
|
293
|
-
// `model` is NOT touched here, and that is the point: `JevConfigInput` has no
|
|
294
|
-
// model, so the spread above is the whole story for it. The CLI's `setOrClear`
|
|
295
|
-
// clears a field only for `--model default`, which is a thing a person typed;
|
|
296
|
-
// an empty input from a form that never offered the field is not.
|
|
297
|
-
|
|
298
|
-
const accountId = input.accountId.trim();
|
|
299
|
-
if (provider === "cloudflare") {
|
|
300
|
-
if (accountId) next.accountId = accountId;
|
|
301
|
-
else delete next.accountId;
|
|
302
|
-
} else {
|
|
303
|
-
// An account id means nothing anywhere else, and leaving a stale one behind
|
|
304
|
-
// would show up in `jev status` as a field this provider ignores.
|
|
305
|
-
delete next.accountId;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (input.mode === "shadow" || input.mode === "enforce") next.mode = input.mode;
|
|
309
|
-
|
|
310
|
-
// Where the key may travel. Unchanged provider AND unchanged origin, from a
|
|
311
|
-
// file only its owner could have written — anything else asks again.
|
|
312
|
-
const routeHeld =
|
|
313
|
-
sameProvider &&
|
|
314
|
-
existingFile?.tooOpen !== true &&
|
|
315
|
-
originOf(baseUrl) === originOf(typeof existing?.baseUrl === "string" ? existing.baseUrl : "");
|
|
316
|
-
|
|
317
|
-
/** Whether the file has a key in it at all. */
|
|
318
|
-
const storesKey = typeof existing?.apiKey === "string" && existing.apiKey !== "";
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Whether the file being updated is one whose owner chose to keep the key OUT
|
|
322
|
-
* of it — `jev setup --key-from-env`, which the loader reports as
|
|
323
|
-
* `key-missing`, or as `ok` with `keySource: "env"` where the variable is set.
|
|
324
|
-
*
|
|
325
|
-
* Every refusal below is about a STORED key being carried somewhere it was not
|
|
326
|
-
* issued for, and there is none here: a hook reads
|
|
327
|
-
* `FAILPROOFAI_JEV_API_KEY` at the moment it runs, so the endpoint, the
|
|
328
|
-
* provider and the mode can all be edited with the token field left blank and
|
|
329
|
-
* nothing moves.
|
|
330
|
-
*
|
|
331
|
-
* It is the loader's own validator that decides, with a stand-in in the one
|
|
332
|
-
* slot that is deliberately empty — so a file that is merely BROKEN, and a
|
|
333
|
-
* machine with no file at all, are not mistaken for a deliberate choice and
|
|
334
|
-
* silently saved without any key. Those still ask for the token.
|
|
335
|
-
*/
|
|
336
|
-
const keyFromEnvConfig =
|
|
337
|
-
existing !== null && !storesKey && validateJevConfig(existing, ENV_KEY_STAND_IN).ok;
|
|
338
|
-
|
|
339
|
-
const token = input.token.trim();
|
|
340
|
-
if (token) {
|
|
341
|
-
const bad = validateApiKey(token);
|
|
342
|
-
if (bad) return { ok: false, problem: bad, needsToken: true };
|
|
343
|
-
next.apiKey = token;
|
|
344
|
-
} else if (!routeHeld && !keyFromEnvConfig) {
|
|
345
|
-
return {
|
|
346
|
-
ok: false,
|
|
347
|
-
needsToken: true,
|
|
348
|
-
problem: !storesKey
|
|
349
|
-
? // Nothing stored to carry and no deliberate absence to preserve: the
|
|
350
|
-
// only thing missing is a key, and no sentence here may imply there
|
|
351
|
-
// is one on disk.
|
|
352
|
-
"enter the token for this provider."
|
|
353
|
-
: sameProvider && existingFile?.tooOpen === true
|
|
354
|
-
? // The loader's "someone else may have chosen this endpoint" case. The
|
|
355
|
-
// stored token is not carried anywhere from a file other users could
|
|
356
|
-
// have written, whatever endpoint it names.
|
|
357
|
-
`${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.`
|
|
358
|
-
: sameProvider
|
|
359
|
-
? "that endpoint is not the one the stored token was given for, so it is not sent there. enter the token for it."
|
|
360
|
-
: "enter the token for this provider.",
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
// Otherwise the key stays exactly where it is: a stored one is carried over by
|
|
364
|
-
// the spread above, and a config that takes it from the environment keeps
|
|
365
|
-
// taking it from the environment.
|
|
366
|
-
|
|
367
|
-
// A config with no stored key takes it from `FAILPROOFAI_JEV_API_KEY` when a
|
|
368
|
-
// hook runs. That is a shape `jev setup --key-from-env` writes and this
|
|
369
|
-
// surface preserves but never creates, so the rest of the file still has to
|
|
370
|
-
// be checked — hence a stand-in in the one slot that is deliberately empty.
|
|
371
|
-
// It is never written and never sent.
|
|
372
|
-
const keyFromEnv = next.apiKey === undefined;
|
|
373
|
-
const checked = validateJevConfig(next, keyFromEnv ? ENV_KEY_STAND_IN : null);
|
|
374
|
-
if (!checked.ok) {
|
|
375
|
-
return {
|
|
376
|
-
ok: false,
|
|
377
|
-
problem: checked.problem,
|
|
378
|
-
...(checked.missingKey === true ? { needsToken: true as const } : {}),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const path = jevConfigPath();
|
|
383
|
-
try {
|
|
384
|
-
writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
|
|
385
|
-
} catch (err) {
|
|
386
|
-
return {
|
|
387
|
-
ok: false,
|
|
388
|
-
problem: `could not write ${path} (${(err as NodeJS.ErrnoException).code ?? "error"}).`,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
tightenConfigDir(path);
|
|
392
|
-
|
|
393
|
-
return { ok: true, view: await getJevSettingsAction() };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Delete `~/.failproofai/jev.json`. Hooks go back to the regex engine on the
|
|
398
|
-
* very next tool call — the loader reads the file per event, so there is
|
|
399
|
-
* nothing to restart and nothing to un-cache.
|
|
400
|
-
*
|
|
401
|
-
* Removing a file that is not there succeeds: the user asked for Jev to be off,
|
|
402
|
-
* and it is.
|
|
403
|
-
*/
|
|
404
|
-
export async function removeJevConfigAction(): Promise<JevWriteResult> {
|
|
405
|
-
const refusal = await crossOriginRefusal();
|
|
406
|
-
if (refusal) return { ok: false, problem: refusal };
|
|
407
|
-
|
|
408
|
-
const path = jevConfigPath();
|
|
409
|
-
if (existsSync(path)) {
|
|
410
|
-
try {
|
|
411
|
-
unlinkSync(path);
|
|
412
|
-
} catch (err) {
|
|
413
|
-
return {
|
|
414
|
-
ok: false,
|
|
415
|
-
problem: `could not remove ${path} (${(err as NodeJS.ErrnoException).code ?? "error"}).`,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
return { ok: true, view: await getJevSettingsAction() };
|
|
420
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
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
|
-
}
|