failproofai 1.0.7-beta.2 → 1.0.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- 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.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.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 +5 -5
- 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 +4 -4
- 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.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 +6 -7
- 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.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 +2 -2
- 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.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 +18 -7
- package/.next/standalone/.next/server/app/settings/page.js +9 -7
- 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]__20kzpkf._.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]__1ttrwnd._.js +22 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
- 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/_1ek68ln._.js +9 -9
- package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
- 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 +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.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/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
- 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 +33 -22
- package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
- package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
- package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
- package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
- package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
- package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
- package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
- package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/get-jev-config.ts +194 -14
- package/.next/standalone/app/actions/update-jev-config.ts +160 -11
- package/.next/standalone/app/policies/hooks-client.tsx +5 -3
- package/.next/standalone/app/settings/jev-panel.tsx +169 -12
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- 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/sdk/typescript/scripts/release.mjs +30 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +1 -0
- package/bin/failproofai.mjs +37 -6
- package/dist/cli.mjs +2861 -1145
- package/dist/worker.mjs +815 -197
- package/package.json +9 -9
- package/src/hooks/builtin-policies.ts +24 -6
- package/src/hooks/cloud-connection.ts +190 -1
- package/src/hooks/cloud-enrollment-cli.ts +56 -7
- package/src/hooks/cloud-introspect.ts +6 -0
- package/src/hooks/configure-wizard.ts +26 -6
- package/src/hooks/custom-hooks-loader.ts +58 -8
- package/src/hooks/effective-reviewers.ts +172 -8
- package/src/hooks/flush-cli.ts +35 -8
- package/src/hooks/fp-config.ts +266 -1
- package/src/hooks/fp-reset.ts +22 -4
- package/src/hooks/handler.ts +44 -7
- package/src/hooks/hook-activity-store.ts +13 -5
- package/src/hooks/jev-cli.ts +1009 -86
- package/src/hooks/jev-cloud-connection.ts +392 -0
- package/src/hooks/manager.ts +44 -6
- package/src/hooks/pack-cli.ts +175 -37
- package/src/hooks/pack-failclosed.ts +3 -0
- package/src/hooks/pack-manifest.ts +11 -1
- package/src/hooks/pack-store.ts +47 -10
- package/src/hooks/policy-authority.ts +73 -20
- package/src/hooks/policy-catalog.ts +10 -8
- package/src/hooks/policy-evaluator.ts +31 -8
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +41 -12
- package/src/hooks/policy-types.ts +5 -3
- package/src/hooks/semantic/combine.ts +124 -84
- package/src/hooks/semantic/decide.ts +16 -0
- package/src/hooks/semantic/envelope.ts +18 -59
- package/src/hooks/semantic/evaluator.ts +11 -2
- package/src/hooks/semantic/facts.ts +7 -1
- package/src/hooks/semantic/intent.ts +8 -8
- package/src/hooks/semantic/jev-client.ts +540 -28
- package/src/hooks/semantic/jev-config.ts +586 -38
- package/src/hooks/semantic/jev-review.ts +26 -11
- package/src/hooks/semantic/jev-throttle.ts +8 -2
- package/src/hooks/semantic/pack-policies.ts +58 -15
- package/src/hooks/semantic/policies.ts +4 -3
- package/src/hooks/semantic/redact.ts +28 -1
- package/src/hooks/semantic/session-root.ts +116 -0
- package/src/hooks/semantic/types.ts +11 -0
- package/src/hooks/tui.ts +4 -0
- package/src/hooks/worker-server.ts +2 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
- 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]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
- 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/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
- package/.next/standalone/.next/static/chunks/1qd741hzlmjbo.js +0 -1
- package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
- package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
- package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
|
@@ -49,17 +49,64 @@
|
|
|
49
49
|
* microseconds, and it means `failproofai jev setup` / `remove` take effect on
|
|
50
50
|
* the very next tool call — including inside the long-lived daemon worker —
|
|
51
51
|
* with no restart and no stale state to reason about.
|
|
52
|
+
*
|
|
53
|
+
* # FailproofAI Cloud (`provider: "failproofai"`)
|
|
54
|
+
*
|
|
55
|
+
* The one provider whose key is NOT in this file. `failproofai config --token`
|
|
56
|
+
* with a key carrying `jev:evaluate` stores that key in the `jev` slot of
|
|
57
|
+
* `credentials.json` and, when there is no `jev.json` yet, writes one naming
|
|
58
|
+
* this provider, the Cloud origin + `/enforcement/v1/jev` and `mode: "shadow"`.
|
|
59
|
+
* So for this provider:
|
|
60
|
+
*
|
|
61
|
+
* - the key comes from `credentials.json` (`readJevCloudCredential`), read with
|
|
62
|
+
* the same owner-only file, directory and size checks this file gets — a
|
|
63
|
+
* loose credentials file is refused, not read, and Jev is off;
|
|
64
|
+
* - an `apiKey` in `jev.json` makes the file invalid, and
|
|
65
|
+
* `FAILPROOFAI_JEV_API_KEY` is ignored: the Cloud key has exactly one home,
|
|
66
|
+
* so disconnecting or rotating it can never leave a second copy steering Jev;
|
|
67
|
+
* - `baseUrl` is required, and its ORIGIN must equal the origin the credential
|
|
68
|
+
* was verified against (`jev.url`). The key is only ever sent where it was
|
|
69
|
+
* issued, and both halves of that decision live in global files.
|
|
70
|
+
* - the credential counts only while the connection it came with is still on
|
|
71
|
+
* this machine: a policy or reporting credential on the SAME origin holding
|
|
72
|
+
* the SAME key, in the same file (`readJevCloudCredential`). A slot an older
|
|
73
|
+
* build's disconnect left behind is not a connection, and neither is one an
|
|
74
|
+
* older build's reconnect with another key (on hosted FailproofAI Cloud,
|
|
75
|
+
* possibly another org's, on the same origin) left beside it.
|
|
76
|
+
* - no usable credential is `not-connected` when the machine has no FailproofAI
|
|
77
|
+
* Cloud connection at all, and `key-lacks-jev` when it has one whose key does
|
|
78
|
+
* not carry `jev:evaluate` (connected with such a key, or the slot was
|
|
79
|
+
* cleared on a reconnect). Jev is off either way; the two remedies differ.
|
|
80
|
+
*
|
|
81
|
+
* # `mode: "off"`
|
|
82
|
+
*
|
|
83
|
+
* Every provider accepts `off | shadow | enforce`. `off` keeps the file — the
|
|
84
|
+
* endpoint, and for BYOK the key — while Jev does not run at all:
|
|
85
|
+
* `loadJevConfig` returns null exactly as for an absent file. It exists so the
|
|
86
|
+
* dashboard can switch the Cloud route off without deleting the file that
|
|
87
|
+
* `config --token` will never rewrite. An older build reads `off` as an
|
|
88
|
+
* invalid mode, which is also off.
|
|
52
89
|
*/
|
|
53
90
|
import { closeSync, constants as fsConstants, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
54
91
|
import { dirname } from "node:path";
|
|
92
|
+
import { readJevCloudCredential, type JevCloudCredential } from "../fp-config";
|
|
55
93
|
import { jevConfigFile } from "../fp-home";
|
|
56
94
|
|
|
57
|
-
export type
|
|
95
|
+
export type { JevCloudCredential } from "../fp-config";
|
|
96
|
+
|
|
97
|
+
export type JevProviderKind = "typesafe" | "openrouter" | "vercel" | "cloudflare" | "custom" | "failproofai";
|
|
98
|
+
|
|
99
|
+
/** `off` does not run Jev at all; `shadow` logs Jev and enforces regex; `enforce` applies the combine rules. */
|
|
100
|
+
export type JevConfigMode = "off" | "shadow" | "enforce";
|
|
58
101
|
|
|
59
102
|
export interface JevConfig {
|
|
60
103
|
provider: JevProviderKind;
|
|
104
|
+
/**
|
|
105
|
+
* The bearer key. For `failproofai` it is never in the FILE: the loader fills
|
|
106
|
+
* it from the `jev` slot of `credentials.json` (see the header).
|
|
107
|
+
*/
|
|
61
108
|
apiKey: string;
|
|
62
|
-
/** Required for `custom`; an optional override otherwise. */
|
|
109
|
+
/** Required for `custom` and `failproofai`; an optional override otherwise. */
|
|
63
110
|
baseUrl?: string;
|
|
64
111
|
/** `cloudflare` only. */
|
|
65
112
|
accountId?: string;
|
|
@@ -67,13 +114,49 @@ export interface JevConfig {
|
|
|
67
114
|
model?: string;
|
|
68
115
|
/** Default 3000; the reasoning is on `DEFAULT_JEV_TIMEOUT_MS` in `evaluator.ts`. */
|
|
69
116
|
timeoutMs?: number;
|
|
70
|
-
/**
|
|
71
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Default `enforce`. `off` is accepted in a file but never reaches the hook
|
|
119
|
+
* path: `loadJevConfig` returns null for it.
|
|
120
|
+
*/
|
|
121
|
+
mode?: JevConfigMode;
|
|
122
|
+
/**
|
|
123
|
+
* `failproofai` only: the origin of the Cloud credential this config was
|
|
124
|
+
* validated against — the one place its key may be sent. Set by
|
|
125
|
+
* `validateJevConfig` from the credential it was handed, never read from a
|
|
126
|
+
* file (a `credentialOrigin` in `jev.json` is an unknown key, ignored), and
|
|
127
|
+
* checked again by `validateLoadedJevConfig` before any route or transport
|
|
128
|
+
* is built. A Cloud config assembled by hand without it is refused there.
|
|
129
|
+
*/
|
|
130
|
+
credentialOrigin?: string;
|
|
72
131
|
}
|
|
73
132
|
|
|
74
|
-
export const DEFAULT_JEV_MODE:
|
|
133
|
+
export const DEFAULT_JEV_MODE: "shadow" | "enforce" = "enforce";
|
|
134
|
+
|
|
135
|
+
export const JEV_PROVIDER_KINDS: readonly JevProviderKind[] = ["typesafe", "openrouter", "vercel", "cloudflare", "custom", "failproofai"];
|
|
136
|
+
|
|
137
|
+
/** The provider whose key comes from this machine's FailproofAI Cloud connection. */
|
|
138
|
+
export const JEV_CLOUD_PROVIDER = "failproofai" as const satisfies JevProviderKind;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Where FailproofAI Cloud serves Jev, under the Cloud origin. The transport
|
|
142
|
+
* appends `/systemone` (`nativeEndpoint`), so the route is
|
|
143
|
+
* `POST <origin>/enforcement/v1/jev/systemone`.
|
|
144
|
+
*/
|
|
145
|
+
export const JEV_CLOUD_BASE_PATH = "/enforcement/v1/jev";
|
|
75
146
|
|
|
76
|
-
|
|
147
|
+
/**
|
|
148
|
+
* The `baseUrl` `config --token` writes for the Cloud base it connected to.
|
|
149
|
+
*
|
|
150
|
+
* Built the way every other Cloud route is — `<base>/enforcement/v1/…`, as the
|
|
151
|
+
* desired-state pull does (`cloud-enrollment.ts`) — so a self-hosted Cloud
|
|
152
|
+
* served under a path prefix gets its Jev route under the same prefix. For the
|
|
153
|
+
* hosted product the base IS the origin. Query and fragment are dropped: a base
|
|
154
|
+
* never carries either (`validateCloudUrl`).
|
|
155
|
+
*/
|
|
156
|
+
export function jevCloudBaseUrl(cloudBase: string): string {
|
|
157
|
+
const url = new URL(cloudBase);
|
|
158
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}${JEV_CLOUD_BASE_PATH}`;
|
|
159
|
+
}
|
|
77
160
|
|
|
78
161
|
/** The env var that may supply the key (and nothing else) when the file carries none. */
|
|
79
162
|
export const JEV_API_KEY_ENV = "FAILPROOFAI_JEV_API_KEY";
|
|
@@ -160,8 +243,11 @@ export type ValidationResult<T> =
|
|
|
160
243
|
* carries no `apiKey` and `FAILPROOFAI_JEV_API_KEY` is unset here. Callers
|
|
161
244
|
* that can tell those apart (`inspectJevConfig`, and `jev status` through it)
|
|
162
245
|
* use the flag rather than matching on `problem`.
|
|
246
|
+
*
|
|
247
|
+
* `notConnected` is its FailproofAI Cloud twin: the file names the Cloud
|
|
248
|
+
* provider and this machine holds no Jev credential for it.
|
|
163
249
|
*/
|
|
164
|
-
| { ok: false; problem: string; missingKey?: true };
|
|
250
|
+
| { ok: false; problem: string; missingKey?: true; notConnected?: true };
|
|
165
251
|
|
|
166
252
|
/** A key is visible ASCII, one line, at most 4 KiB. The message never includes the key. */
|
|
167
253
|
export function validateApiKey(key: unknown): string | null {
|
|
@@ -170,11 +256,62 @@ export function validateApiKey(key: unknown): string | null {
|
|
|
170
256
|
return null;
|
|
171
257
|
}
|
|
172
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Query parameter names that carry a CREDENTIAL rather than a routing choice.
|
|
261
|
+
*
|
|
262
|
+
* Matched on the parameter's name with everything but letters and digits
|
|
263
|
+
* removed, so `api_key`, `api-key`, `X-Api-Key` and `apiKey` are one rule
|
|
264
|
+
* rather than four — and `?api-version=`, the parameter the query string is
|
|
265
|
+
* permitted for in the first place, matches none of them.
|
|
266
|
+
*
|
|
267
|
+
* Substrings, because the real names are compounds: `access_token`,
|
|
268
|
+
* `subscription-key`, `authToken`, `x-functions-key`. The short ones that have
|
|
269
|
+
* no word to be a substring of are matched whole instead.
|
|
270
|
+
*/
|
|
271
|
+
const CREDENTIAL_PARAM_PARTS = ["key", "token", "secret", "password", "passwd", "credential", "auth", "signature"] as const;
|
|
272
|
+
const CREDENTIAL_PARAM_NAMES = new Set(["pw", "pass", "sig", "sid", "jwt", "bearer", "session", "code", "access"]);
|
|
273
|
+
|
|
274
|
+
/** A parameter name safe to quote back: it goes into a message the CLI prints. */
|
|
275
|
+
const QUOTABLE_PARAM_RE = /^[A-Za-z0-9._-]{1,40}$/;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Why a base URL's query string may not be stored, or null when it may.
|
|
279
|
+
*
|
|
280
|
+
* A credential in a base URL is a mistake wherever it ends up, because the
|
|
281
|
+
* config file already has an `apiKey` field that is sent as a bearer and never
|
|
282
|
+
* printed — while the endpoint DERIVED from the base URL is logged, printed by
|
|
283
|
+
* `jev status`, put in error messages and returned to the dashboard. So this
|
|
284
|
+
* refuses the credential at the door rather than eliding it on the way out:
|
|
285
|
+
* eliding leaves the secret in the file and in everything the file feeds.
|
|
286
|
+
*
|
|
287
|
+
* It refuses on the parameter NAME, not on the presence of a query, because
|
|
288
|
+
* some proxies genuinely route on one (`?api-version=`) — see the comment on
|
|
289
|
+
* the normalization below. The VALUE is checked too, through the same
|
|
290
|
+
* `looksLikeCredential` that refuses a key pasted into `--model`: a parameter
|
|
291
|
+
* called `t` whose value is `sk-…` is the same mistake under a name no list
|
|
292
|
+
* can carry. A false refusal there turns Jev off and leaves the regex tier
|
|
293
|
+
* enforcing, which is the direction this whole module errs in.
|
|
294
|
+
*/
|
|
295
|
+
function credentialQueryProblem(url: URL): string | null {
|
|
296
|
+
for (const [name, value] of url.searchParams) {
|
|
297
|
+
const shown = QUOTABLE_PARAM_RE.test(name) ? `?${name}=` : "one of its query parameters";
|
|
298
|
+
const flat = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
299
|
+
if (CREDENTIAL_PARAM_PARTS.some((part) => flat.includes(part)) || CREDENTIAL_PARAM_NAMES.has(flat)) {
|
|
300
|
+
return `baseUrl must not carry a credential in its query string, and ${shown} is one; put the key in the key field`;
|
|
301
|
+
}
|
|
302
|
+
if (looksLikeCredential(value)) {
|
|
303
|
+
return `baseUrl's ${shown} value is shaped like a credential (not repeated here); put the key in the key field`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
173
309
|
/**
|
|
174
310
|
* An endpoint base URL: https, or http to a loopback host only (a local proxy).
|
|
175
|
-
* No credentials in the URL
|
|
176
|
-
* where it is sent as a bearer
|
|
177
|
-
* accepts the loopback http form
|
|
311
|
+
* No credentials in the URL — not as userinfo, not as a query parameter — and no
|
|
312
|
+
* fragment, because a key belongs in the key field, where it is sent as a bearer
|
|
313
|
+
* and never printed. `validateJevConfig` further accepts the loopback http form
|
|
314
|
+
* only in shadow mode.
|
|
178
315
|
*/
|
|
179
316
|
export function validateBaseUrl(raw: unknown): ValidationResult<string> {
|
|
180
317
|
if (typeof raw !== "string" || raw.trim() === "") return { ok: false, problem: "baseUrl must be a non-empty string" };
|
|
@@ -189,6 +326,8 @@ export function validateBaseUrl(raw: unknown): ValidationResult<string> {
|
|
|
189
326
|
}
|
|
190
327
|
if (url.username || url.password) return { ok: false, problem: "baseUrl must not carry credentials; put the key in the key field" };
|
|
191
328
|
if (url.hash) return { ok: false, problem: "baseUrl must not have a #fragment" };
|
|
329
|
+
const credentialQuery = credentialQueryProblem(url);
|
|
330
|
+
if (credentialQuery) return { ok: false, problem: credentialQuery };
|
|
192
331
|
// Trailing slashes come off the PATH, never the string: a query string is legal
|
|
193
332
|
// (some proxies want `?api-version=`), and the endpoint path is appended to
|
|
194
333
|
// the pathname, so it must never end up inside the query.
|
|
@@ -196,6 +335,137 @@ export function validateBaseUrl(raw: unknown): ValidationResult<string> {
|
|
|
196
335
|
return { ok: true, value: url.toString() };
|
|
197
336
|
}
|
|
198
337
|
|
|
338
|
+
export interface BaseUrlWithoutQuery {
|
|
339
|
+
/** The URL with any `?query` taken off. */
|
|
340
|
+
url: string;
|
|
341
|
+
/** Whether there was one to take off. */
|
|
342
|
+
hadQuery: boolean;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* A stored base URL with its query string (and any userinfo) removed, and
|
|
347
|
+
* whether it had a query.
|
|
348
|
+
*
|
|
349
|
+
* The refusal above is the source fix, and this is what makes the way OUT safe
|
|
350
|
+
* regardless: a file written by an older build can already carry `?token=`, and
|
|
351
|
+
* that file is exactly the one whose owner is being shown the URL so they can
|
|
352
|
+
* repair it. So nothing that crosses the wire to the dashboard, and nothing the
|
|
353
|
+
* CLI prints, carries a query string off a base URL — `displayEndpoint` does it
|
|
354
|
+
* for the derived endpoint, and this does it for the base URL itself.
|
|
355
|
+
*
|
|
356
|
+
* Takes a string rather than a `URL` because the callers that need it most are
|
|
357
|
+
* reading a REFUSED file, where the value may not parse at all. An unparseable
|
|
358
|
+
* one is cut at the first `?`, which is strictly more aggressive than parsing.
|
|
359
|
+
*/
|
|
360
|
+
export function baseUrlWithoutQuery(raw: string): BaseUrlWithoutQuery {
|
|
361
|
+
// Userinfo (`https://svc:<key>@host`) comes off as well: it is the other place
|
|
362
|
+
// a URL carries a credential, and `validateBaseUrl` refuses it for that reason.
|
|
363
|
+
try {
|
|
364
|
+
const url = new URL(raw);
|
|
365
|
+
if (!url.search && !url.username && !url.password) return { url: raw, hadQuery: false };
|
|
366
|
+
const hadQuery = url.search !== "";
|
|
367
|
+
url.search = "";
|
|
368
|
+
url.username = "";
|
|
369
|
+
url.password = "";
|
|
370
|
+
return { url: url.toString(), hadQuery };
|
|
371
|
+
} catch {
|
|
372
|
+
const cut = raw.indexOf("?");
|
|
373
|
+
const head = (cut < 0 ? raw : raw.slice(0, cut)).replace(/^([^:/?#]+:\/\/)[^/?#]*@/, "$1");
|
|
374
|
+
return { url: head, hadQuery: cut >= 0 };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Path suffixes that are an ENDPOINT, never a base. Longest first, so
|
|
380
|
+
* `/v1/chat/completions` strips back to `/v1` rather than to `/v1/chat`.
|
|
381
|
+
*
|
|
382
|
+
* `/systemone` is this product's own suffix; the other four are what an OpenAI-
|
|
383
|
+
* compatible base serves. Every one of them is the LAST segment of a request URL
|
|
384
|
+
* whose base is its parent, which is what makes the repair unambiguous.
|
|
385
|
+
*/
|
|
386
|
+
const ENDPOINT_SUFFIXES = ["/chat/completions", "/systemone", "/completions", "/embeddings", "/models"] as const;
|
|
387
|
+
|
|
388
|
+
export interface EndpointGivenAsBase {
|
|
389
|
+
/** The suffix that gave it away, e.g. `/models`. */
|
|
390
|
+
suffix: string;
|
|
391
|
+
/** The base that was probably meant: the same URL with the suffix taken off. */
|
|
392
|
+
base: string;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Whether a URL is obviously an endpoint rather than an API base, and the base it
|
|
397
|
+
* implies.
|
|
398
|
+
*
|
|
399
|
+
* This is checked where a URL is being saved (`jev setup`, the dashboard's save),
|
|
400
|
+
* and deliberately not in `validateBaseUrl`: a config already on disk that names
|
|
401
|
+
* an endpoint as its base keeps working
|
|
402
|
+
* (`nativeEndpoint` does not append a second `/systemone`), and turning Jev off on
|
|
403
|
+
* an upgrade for a file that was routing correctly would be a worse failure than
|
|
404
|
+
* the one this prevents.
|
|
405
|
+
*
|
|
406
|
+
* What it prevents: `--url https://…/typesafe/v1/models` was saved without
|
|
407
|
+
* complaint, requests then went to `…/v1/models/systemone`, and the only signal
|
|
408
|
+
* was `http-404: Not Found` from `jev test`. The CLI knows `/systemone` is its own
|
|
409
|
+
* suffix and that a provider's base ends at a version root, so it can say so
|
|
410
|
+
* before anything is written.
|
|
411
|
+
*/
|
|
412
|
+
export function endpointGivenAsBase(url: string): EndpointGivenAsBase | null {
|
|
413
|
+
let parsed: URL;
|
|
414
|
+
try {
|
|
415
|
+
parsed = new URL(url);
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const path = parsed.pathname.replace(/\/+$/, "");
|
|
420
|
+
const suffix = ENDPOINT_SUFFIXES.find((s) => path.toLowerCase().endsWith(s));
|
|
421
|
+
if (suffix === undefined) return null;
|
|
422
|
+
parsed.pathname = path.slice(0, path.length - suffix.length) || "/";
|
|
423
|
+
return { suffix, base: parsed.toString() };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The provider each known host IS, so `failproofai jev --url <url>` needs no
|
|
428
|
+
* `--provider`. Exact hosts only: a neighbouring name (`eu.api.typesafe.ai`, a
|
|
429
|
+
* corporate proxy) is `custom`, which reaches the same TypeSafe-compatible API
|
|
430
|
+
* at the URL given, and `--provider` is there to say otherwise.
|
|
431
|
+
*/
|
|
432
|
+
const PROVIDER_BY_HOST: Readonly<Record<string, JevProviderKind>> = {
|
|
433
|
+
"api.typesafe.ai": "typesafe",
|
|
434
|
+
"openrouter.ai": "openrouter",
|
|
435
|
+
"ai-gateway.vercel.sh": "vercel",
|
|
436
|
+
"api.cloudflare.com": "cloudflare",
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/** The provider a validated URL names, or `custom` for a host that is nobody's. */
|
|
440
|
+
export function providerForUrl(url: string): JevProviderKind {
|
|
441
|
+
try {
|
|
442
|
+
return PROVIDER_BY_HOST[new URL(url).hostname.toLowerCase()] ?? "custom";
|
|
443
|
+
} catch {
|
|
444
|
+
return "custom";
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Why `provider` cannot be saved with a base on `url`'s host, or null.
|
|
450
|
+
*
|
|
451
|
+
* Two pairs: a provider the host says it is not (`openrouter` on Vercel's
|
|
452
|
+
* gateway — the two disagree about where the key is sent), and `custom` on
|
|
453
|
+
* Cloudflare, whose per-account, wrapped endpoint a custom route cannot speak.
|
|
454
|
+
* `custom` on any other known host is "treat this URL as itself", not a
|
|
455
|
+
* disagreement. Checked where a URL is saved (`jev --url`, `jev setup
|
|
456
|
+
* --base-url`, the dashboard's save) and, like `endpointGivenAsBase`, not by
|
|
457
|
+
* the loader: a file already on disk keeps working.
|
|
458
|
+
*/
|
|
459
|
+
export function providerHostConflict(provider: string, url: string): string | null {
|
|
460
|
+
const known = providerForUrl(url);
|
|
461
|
+
if (known === "custom" || known === provider) return null;
|
|
462
|
+
const host = new URL(url).host;
|
|
463
|
+
if (provider !== "custom") return `${host} is ${known}'s endpoint, not ${provider}'s`;
|
|
464
|
+
return known === "cloudflare"
|
|
465
|
+
? `${host} is Cloudflare Workers AI, which provider custom cannot reach — its endpoint is per-account and its answers are wrapped, and a custom endpoint is asked in TypeSafe's own shape at <url>/systemone`
|
|
466
|
+
: null;
|
|
467
|
+
}
|
|
468
|
+
|
|
199
469
|
function isPlainHttp(url: string): boolean {
|
|
200
470
|
try {
|
|
201
471
|
return new URL(url).protocol === "http:";
|
|
@@ -204,6 +474,21 @@ function isPlainHttp(url: string): boolean {
|
|
|
204
474
|
}
|
|
205
475
|
}
|
|
206
476
|
|
|
477
|
+
/**
|
|
478
|
+
* The origin a bearer token would be sent to: https, or http to a loopback host
|
|
479
|
+
* — the same schemes `validateBaseUrl` admits. Null for anything else, so a
|
|
480
|
+
* credential naming some other scheme matches no base URL at all.
|
|
481
|
+
*/
|
|
482
|
+
function originOf(raw: string): string | null {
|
|
483
|
+
try {
|
|
484
|
+
const url = new URL(raw);
|
|
485
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))) return null;
|
|
486
|
+
return url.origin;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
207
492
|
/**
|
|
208
493
|
* Prefixes real credentials carry: OpenAI / OpenRouter / Anthropic / Stripe
|
|
209
494
|
* (`sk-`, `sk_`, `rk_`), GitHub, GitLab, Slack, AWS, Google, Hugging Face and
|
|
@@ -223,6 +508,16 @@ export function looksLikeCredential(s: string): boolean {
|
|
|
223
508
|
return s.length >= 32 && !s.includes("/") && !/jev/i.test(s) && /[A-Za-z]/.test(s) && /[0-9]/.test(s);
|
|
224
509
|
}
|
|
225
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Whether a string is shaped like a model id. Used on names that came back from
|
|
513
|
+
* a provider's `/models` endpoint and are about to be printed: those are remote
|
|
514
|
+
* strings, so anything carrying a control character, an ANSI escape or a
|
|
515
|
+
* kilobyte of padding is dropped rather than rendered.
|
|
516
|
+
*/
|
|
517
|
+
export function isModelIdShaped(s: string): boolean {
|
|
518
|
+
return MODEL_RE.test(s);
|
|
519
|
+
}
|
|
520
|
+
|
|
226
521
|
function validateModel(raw: unknown): ValidationResult<string> {
|
|
227
522
|
if (typeof raw !== "string" || !MODEL_RE.test(raw)) {
|
|
228
523
|
return { ok: false, problem: "model must be 1–200 characters of letters, digits and . _ : / @ ~ + -" };
|
|
@@ -254,8 +549,17 @@ function validateModel(raw: unknown): ValidationResult<string> {
|
|
|
254
549
|
* `envKey` is the value of `FAILPROOFAI_JEV_API_KEY`, used only when the object
|
|
255
550
|
* has no `apiKey`. Unknown top-level keys are ignored so a newer failproofai's
|
|
256
551
|
* file does not switch Jev off on an older one. Problems never quote the key.
|
|
552
|
+
*
|
|
553
|
+
* `cloud` is the `jev` slot of `credentials.json`, and is read ONLY for the
|
|
554
|
+
* FailproofAI Cloud provider, which takes its key from nowhere else: not the
|
|
555
|
+
* file (an `apiKey` there is refused) and not `envKey` (ignored). The file's
|
|
556
|
+
* `baseUrl` must sit on the origin the credential was verified against.
|
|
257
557
|
*/
|
|
258
|
-
export function validateJevConfig(
|
|
558
|
+
export function validateJevConfig(
|
|
559
|
+
raw: unknown,
|
|
560
|
+
envKey?: string | null,
|
|
561
|
+
cloud?: JevCloudCredential | null,
|
|
562
|
+
): ValidationResult<JevConfig> {
|
|
259
563
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ok: false, problem: "the file must hold a JSON object" };
|
|
260
564
|
const o = raw as Record<string, unknown>;
|
|
261
565
|
|
|
@@ -266,7 +570,31 @@ export function validateJevConfig(raw: unknown, envKey?: string | null): Validat
|
|
|
266
570
|
const kind = provider as JevProviderKind;
|
|
267
571
|
|
|
268
572
|
let apiKey: string;
|
|
269
|
-
if (
|
|
573
|
+
if (kind === JEV_CLOUD_PROVIDER) {
|
|
574
|
+
// One home for the Cloud key, so revoking it at disconnect cannot leave a
|
|
575
|
+
// copy behind that keeps Jev spending — and so nothing a session can set
|
|
576
|
+
// (the environment) or a second file can hold decides which key is sent.
|
|
577
|
+
if (o.apiKey !== undefined) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
problem:
|
|
581
|
+
"provider failproofai takes its key from this machine's FailproofAI Cloud connection (credentials.json), " +
|
|
582
|
+
"never from jev.json — remove apiKey from the file (not repeated here)",
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (!cloud) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
problem:
|
|
589
|
+
"this machine is not connected to FailproofAI Cloud with a key that carries jev:evaluate — " +
|
|
590
|
+
"connect it with: failproofai config --token <key>",
|
|
591
|
+
notConnected: true,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const bad = validateApiKey(cloud.key);
|
|
595
|
+
if (bad) return { ok: false, problem: `the FailproofAI Cloud key in credentials.json is unusable: ${bad}` };
|
|
596
|
+
apiKey = cloud.key;
|
|
597
|
+
} else if (o.apiKey !== undefined) {
|
|
270
598
|
const bad = validateApiKey(o.apiKey);
|
|
271
599
|
if (bad) return { ok: false, problem: bad };
|
|
272
600
|
apiKey = o.apiKey as string;
|
|
@@ -288,8 +616,30 @@ export function validateJevConfig(raw: unknown, envKey?: string | null): Validat
|
|
|
288
616
|
const r = validateBaseUrl(o.baseUrl);
|
|
289
617
|
if (!r.ok) return r;
|
|
290
618
|
cfg.baseUrl = r.value;
|
|
291
|
-
} else if (kind === "custom") {
|
|
292
|
-
return { ok: false, problem:
|
|
619
|
+
} else if (kind === "custom" || kind === JEV_CLOUD_PROVIDER) {
|
|
620
|
+
return { ok: false, problem: `provider ${kind} needs a baseUrl` };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (kind === JEV_CLOUD_PROVIDER && cloud) {
|
|
624
|
+
// The key goes only to the origin it was issued for. Compared as origins
|
|
625
|
+
// (scheme + host + port), which is what decides where a bearer token lands;
|
|
626
|
+
// the path under it is the Cloud's own routing.
|
|
627
|
+
const credentialOrigin = originOf(cloud.url);
|
|
628
|
+
const baseOrigin = originOf(cfg.baseUrl as string);
|
|
629
|
+
if (credentialOrigin === null) {
|
|
630
|
+
return { ok: false, problem: "the FailproofAI Cloud credential in credentials.json names no usable origin — reconnect: failproofai config --token <key>" };
|
|
631
|
+
}
|
|
632
|
+
if (baseOrigin !== credentialOrigin) {
|
|
633
|
+
return {
|
|
634
|
+
ok: false,
|
|
635
|
+
problem:
|
|
636
|
+
`baseUrl is on ${baseOrigin ?? "an unusable origin"}, but this machine's FailproofAI Cloud key was issued for ${credentialOrigin} — ` +
|
|
637
|
+
"the key is only ever sent to the origin it was issued for. Point jev.json back at it: failproofai jev setup --provider failproofai",
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
// Carried with the config, so the check can be made again — for real —
|
|
641
|
+
// wherever the config is used (`validateLoadedJevConfig`).
|
|
642
|
+
cfg.credentialOrigin = credentialOrigin;
|
|
293
643
|
}
|
|
294
644
|
|
|
295
645
|
if (kind === "cloudflare") {
|
|
@@ -317,7 +667,7 @@ export function validateJevConfig(raw: unknown, envKey?: string | null): Validat
|
|
|
317
667
|
}
|
|
318
668
|
|
|
319
669
|
if (o.mode !== undefined) {
|
|
320
|
-
if (o.mode !== "shadow" && o.mode !== "enforce") return { ok: false, problem: 'mode must be "shadow" or "enforce"' };
|
|
670
|
+
if (o.mode !== "off" && o.mode !== "shadow" && o.mode !== "enforce") return { ok: false, problem: 'mode must be "off", "shadow" or "enforce"' };
|
|
321
671
|
cfg.mode = o.mode;
|
|
322
672
|
}
|
|
323
673
|
|
|
@@ -325,25 +675,107 @@ export function validateJevConfig(raw: unknown, envKey?: string | null): Validat
|
|
|
325
675
|
// authenticates the server there: while the local proxy is down, any process
|
|
326
676
|
// of this user — the agent being judged included — can bind its port and
|
|
327
677
|
// answer "none" to every question. In enforce mode that answer clears
|
|
328
|
-
// reviewable denies; in shadow mode it changes nothing,
|
|
329
|
-
// mode it is
|
|
330
|
-
if (cfg.baseUrl !== undefined && isPlainHttp(cfg.baseUrl) && cfg.mode
|
|
678
|
+
// reviewable denies; in shadow mode it changes nothing, and `off` sends
|
|
679
|
+
// nothing at all, so enforce is the one mode it is refused in.
|
|
680
|
+
if (cfg.baseUrl !== undefined && isPlainHttp(cfg.baseUrl) && cfg.mode === "enforce") {
|
|
331
681
|
return {
|
|
332
682
|
ok: false,
|
|
333
683
|
problem:
|
|
334
684
|
"plain http (to localhost) is accepted only with mode shadow: in enforce mode Jev's answers can clear a deny, " +
|
|
335
|
-
"and while the local proxy is down any process on this machine could take its port and answer.
|
|
685
|
+
"and while the local proxy is down any process on this machine could take its port and answer. " +
|
|
686
|
+
(cfg.provider === JEV_CLOUD_PROVIDER
|
|
687
|
+
? "Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or keep mode shadow"
|
|
688
|
+
: "Use https, or mode shadow"),
|
|
336
689
|
};
|
|
337
690
|
}
|
|
338
691
|
|
|
339
692
|
return { ok: true, value: cfg };
|
|
340
693
|
}
|
|
341
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Re-validate a config that was already LOADED — by `loadJevConfig` or
|
|
697
|
+
* `inspectJevConfig` — before a route or a transport is built from it.
|
|
698
|
+
*
|
|
699
|
+
* For every BYOK provider that is `validateJevConfig` as it always was. The
|
|
700
|
+
* FailproofAI Cloud provider is the one whose in-memory `apiKey` is not a file
|
|
701
|
+
* field: the loader filled it from `credentials.json`, and `validateJevConfig`
|
|
702
|
+
* would refuse the very key it put there as a file's `apiKey`. So the key goes
|
|
703
|
+
* back in the credential slot — under `credentialOrigin`, the origin of the
|
|
704
|
+
* credential the loader validated against, which the loader recorded on the
|
|
705
|
+
* config. The origin check therefore runs again, for real: a config whose
|
|
706
|
+
* `baseUrl` has since moved to another origin is refused, and so is a Cloud
|
|
707
|
+
* config assembled by hand with no `credentialOrigin` at all, so a future
|
|
708
|
+
* caller cannot build one and skip the check. (Display-only callers that build
|
|
709
|
+
* a stand-in on purpose name the origin they mean explicitly.)
|
|
710
|
+
*/
|
|
711
|
+
export function validateLoadedJevConfig(cfg: JevConfig): ValidationResult<JevConfig> {
|
|
712
|
+
if (cfg.provider !== JEV_CLOUD_PROVIDER) return validateJevConfig(cfg);
|
|
713
|
+
const { apiKey, credentialOrigin, ...rest } = cfg;
|
|
714
|
+
if (typeof credentialOrigin !== "string" || credentialOrigin === "") {
|
|
715
|
+
return {
|
|
716
|
+
ok: false,
|
|
717
|
+
problem: "this FailproofAI Cloud config names no credential origin — it did not come from the loader, so its key is not sent anywhere",
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return validateJevConfig(rest, null, { url: credentialOrigin, key: apiKey });
|
|
721
|
+
}
|
|
722
|
+
|
|
342
723
|
// ── Loading ──────────────────────────────────────────────────────────────────
|
|
343
724
|
|
|
344
725
|
export type JevConfigInspection =
|
|
345
726
|
| { status: "absent"; path: string }
|
|
346
|
-
| {
|
|
727
|
+
| {
|
|
728
|
+
status: "ok";
|
|
729
|
+
path: string;
|
|
730
|
+
mode: number | null;
|
|
731
|
+
/** `cloud`: the `jev` slot of `credentials.json` (the FailproofAI Cloud provider). */
|
|
732
|
+
keySource: "file" | "env" | "cloud";
|
|
733
|
+
config: JevConfig;
|
|
734
|
+
}
|
|
735
|
+
| {
|
|
736
|
+
/**
|
|
737
|
+
* A sound file that says `mode: "off"`. Jev does not run — `loadJevConfig`
|
|
738
|
+
* returns null — and nothing is wrong: the owner switched it off and kept
|
|
739
|
+
* the endpoint (and, for BYOK, the key) for later. Reported apart from
|
|
740
|
+
* `absent` so `jev status` and the dashboard can say "switched off"
|
|
741
|
+
* rather than "not configured", and apart from `ok` so no reader that
|
|
742
|
+
* takes `ok` to mean "on" can be wrong about it.
|
|
743
|
+
*/
|
|
744
|
+
status: "off";
|
|
745
|
+
path: string;
|
|
746
|
+
mode: number | null;
|
|
747
|
+
/** Everything the file says except the key. */
|
|
748
|
+
routing: Omit<JevConfig, "apiKey">;
|
|
749
|
+
}
|
|
750
|
+
| {
|
|
751
|
+
/**
|
|
752
|
+
* The file names the FailproofAI Cloud provider and is sound, but this
|
|
753
|
+
* machine holds no Jev credential (never connected with a `jev:evaluate`
|
|
754
|
+
* key, or disconnected since). Jev is off. The FailproofAI Cloud twin of
|
|
755
|
+
* `key-missing`: nothing is wrong with the FILE, so no reader should tell
|
|
756
|
+
* its owner to rewrite it.
|
|
757
|
+
*/
|
|
758
|
+
status: "not-connected";
|
|
759
|
+
path: string;
|
|
760
|
+
mode: number | null;
|
|
761
|
+
routing: Omit<JevConfig, "apiKey">;
|
|
762
|
+
problem: string;
|
|
763
|
+
}
|
|
764
|
+
| {
|
|
765
|
+
/**
|
|
766
|
+
* The file names the FailproofAI Cloud provider and is sound, and this
|
|
767
|
+
* machine IS connected to FailproofAI Cloud — but with a key that does not
|
|
768
|
+
* carry `jev:evaluate` (connected with one, or reconnected with one since,
|
|
769
|
+
* which clears the slot). Jev is off. Apart from `not-connected` because
|
|
770
|
+
* telling a connected machine it is "not connected" sends its owner to
|
|
771
|
+
* reconnect with the same key, when the remedy is a key with Jev.
|
|
772
|
+
*/
|
|
773
|
+
status: "key-lacks-jev";
|
|
774
|
+
path: string;
|
|
775
|
+
mode: number | null;
|
|
776
|
+
routing: Omit<JevConfig, "apiKey">;
|
|
777
|
+
problem: string;
|
|
778
|
+
}
|
|
347
779
|
| {
|
|
348
780
|
/**
|
|
349
781
|
* The file is sound and names `FAILPROOFAI_JEV_API_KEY` as the key's
|
|
@@ -371,8 +803,22 @@ export type JevConfigInspection =
|
|
|
371
803
|
problem: string;
|
|
372
804
|
/** The command that fixes it, when one does. */
|
|
373
805
|
fix?: string;
|
|
806
|
+
/** When the refusal is about credentials.json: that file's permission bits (`mode` stays jev.json's). */
|
|
807
|
+
credentialsMode?: number | null;
|
|
808
|
+
/** A too-open file with no key in it: refused as firmly, but no key was exposed. */
|
|
809
|
+
keyless?: true;
|
|
374
810
|
};
|
|
375
811
|
|
|
812
|
+
/** Whether config text is an object with no `apiKey`; anything else may hold one. */
|
|
813
|
+
function namesNoKey(text: string): boolean {
|
|
814
|
+
try {
|
|
815
|
+
const o: unknown = JSON.parse(text);
|
|
816
|
+
return o !== null && typeof o === "object" && !Array.isArray(o) && (o as Record<string, unknown>).apiKey === undefined;
|
|
817
|
+
} catch {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
376
822
|
function readEnvKey(): string | null {
|
|
377
823
|
const v = process.env[JEV_API_KEY_ENV];
|
|
378
824
|
return v === undefined || v === "" ? null : v;
|
|
@@ -442,14 +888,40 @@ export function inspectJevConfig(): JevConfigInspection {
|
|
|
442
888
|
const st = fstatSync(fd);
|
|
443
889
|
if (!st.isFile()) return { status: "refused", path, mode: null, reason: "unreadable", problem: "it is not a regular file" };
|
|
444
890
|
mode = st.mode & 0o777;
|
|
891
|
+
const readText = (): string => {
|
|
892
|
+
const buf = Buffer.alloc(st.size);
|
|
893
|
+
let off = 0;
|
|
894
|
+
while (off < buf.length) {
|
|
895
|
+
const n = readSync(fd, buf, off, buf.length - off, off);
|
|
896
|
+
if (n === 0) break;
|
|
897
|
+
off += n;
|
|
898
|
+
}
|
|
899
|
+
return buf.subarray(0, off).toString("utf8");
|
|
900
|
+
};
|
|
445
901
|
if (modesAreMeaningful() && (mode & 0o077) !== 0) {
|
|
902
|
+
// Read only to word the refusal: a file with no key in it (the Cloud's,
|
|
903
|
+
// whose key is in credentials.json, or a key-from-env one) is refused as
|
|
904
|
+
// firmly, but telling its owner a key leaked sends them to rotate one
|
|
905
|
+
// that never did.
|
|
906
|
+
let keyless = false;
|
|
907
|
+
try {
|
|
908
|
+
keyless = st.size <= MAX_CONFIG_BYTES && namesNoKey(readText());
|
|
909
|
+
} catch {
|
|
910
|
+
// Unreadable: it may hold one.
|
|
911
|
+
}
|
|
912
|
+
const perms = mode.toString(8).padStart(4, "0");
|
|
446
913
|
return {
|
|
447
914
|
status: "refused",
|
|
448
915
|
path,
|
|
449
916
|
mode,
|
|
450
917
|
reason: "too-open",
|
|
451
|
-
problem:
|
|
918
|
+
problem: !keyless
|
|
919
|
+
? `its permissions are ${perms}; it holds a key, so it must be owner-only (chmod 600 ${path})`
|
|
920
|
+
: (mode & 0o022) !== 0
|
|
921
|
+
? `its permissions are ${perms}; other users could change it, and with it where Jev sends tool calls and prompts, so it must be owner-only (chmod 600 ${path})`
|
|
922
|
+
: `its permissions are ${perms}; it must be owner-only (chmod 600 ${path})`,
|
|
452
923
|
fix: `chmod 600 ${path}`,
|
|
924
|
+
...(keyless ? { keyless: true as const } : {}),
|
|
453
925
|
};
|
|
454
926
|
}
|
|
455
927
|
const dirMode = looseConfigDirMode(path);
|
|
@@ -467,14 +939,7 @@ export function inspectJevConfig(): JevConfigInspection {
|
|
|
467
939
|
};
|
|
468
940
|
}
|
|
469
941
|
if (st.size > MAX_CONFIG_BYTES) return { status: "refused", path, mode, reason: "too-large", problem: `it is larger than ${MAX_CONFIG_BYTES} bytes` };
|
|
470
|
-
|
|
471
|
-
let off = 0;
|
|
472
|
-
while (off < buf.length) {
|
|
473
|
-
const n = readSync(fd, buf, off, buf.length - off, off);
|
|
474
|
-
if (n === 0) break;
|
|
475
|
-
off += n;
|
|
476
|
-
}
|
|
477
|
-
text = buf.subarray(0, off).toString("utf8");
|
|
942
|
+
text = readText();
|
|
478
943
|
} catch (err) {
|
|
479
944
|
return { status: "refused", path, mode, reason: "unreadable", problem: `cannot read it (${(err as NodeJS.ErrnoException).code ?? "error"})` };
|
|
480
945
|
} finally {
|
|
@@ -491,6 +956,50 @@ export function inspectJevConfig(): JevConfigInspection {
|
|
|
491
956
|
} catch {
|
|
492
957
|
return { status: "refused", path, mode, reason: "not-json", problem: "it is not valid JSON" };
|
|
493
958
|
}
|
|
959
|
+
const isObject = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
|
960
|
+
const fields = isObject ? (parsed as Record<string, unknown>) : null;
|
|
961
|
+
|
|
962
|
+
// Switched off. Checked before any key is looked for, because nothing will be
|
|
963
|
+
// sent: a BYOK file whose key lives in the environment, and a Cloud file on a
|
|
964
|
+
// machine that has since disconnected, are both simply "off". The rest of the
|
|
965
|
+
// file is still validated — with stand-ins in the key slots, which are never
|
|
966
|
+
// sent anywhere — so a file that is off AND broken is reported as broken.
|
|
967
|
+
if (fields?.mode === "off") {
|
|
968
|
+
const r = validateJevConfig(parsed, KEY_STAND_IN, standInCloudCredential(fields));
|
|
969
|
+
if (!r.ok) return { status: "refused", path, mode, reason: "invalid", problem: r.problem };
|
|
970
|
+
return { status: "off", path, mode, routing: routingOf(r.value) };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (fields?.provider === JEV_CLOUD_PROVIDER) {
|
|
974
|
+
// The key lives in credentials.json, which is read with the same owner-only
|
|
975
|
+
// checks as this file: a credentials file someone else could have written
|
|
976
|
+
// is refused, not read, and Jev is off.
|
|
977
|
+
const credential = readJevCloudCredential();
|
|
978
|
+
if (credential.status === "refused") {
|
|
979
|
+
return {
|
|
980
|
+
status: "refused",
|
|
981
|
+
path,
|
|
982
|
+
mode,
|
|
983
|
+
reason: credential.reason === "too-open" ? "too-open" : "unreadable",
|
|
984
|
+
problem: `the FailproofAI Cloud credential was refused: ${credential.problem}`,
|
|
985
|
+
...(credential.fix ? { fix: credential.fix } : {}),
|
|
986
|
+
credentialsMode: credential.mode,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
// `FAILPROOFAI_JEV_API_KEY` is deliberately not passed: see the header.
|
|
990
|
+
const r = validateJevConfig(parsed, null, credential.status === "ok" ? credential.credential : null);
|
|
991
|
+
if (r.ok) return { status: "ok", path, mode, keySource: "cloud", config: r.value };
|
|
992
|
+
if (r.notConnected === true) {
|
|
993
|
+
const rest = validateJevConfig(parsed, null, standInCloudCredential(fields));
|
|
994
|
+
if (!rest.ok) return { status: "refused", path, mode, reason: "invalid", problem: rest.problem };
|
|
995
|
+
if (credential.status === "absent" && credential.connected) {
|
|
996
|
+
return { status: "key-lacks-jev", path, mode, routing: routingOf(rest.value), problem: KEY_LACKS_JEV_PROBLEM };
|
|
997
|
+
}
|
|
998
|
+
return { status: "not-connected", path, mode, routing: routingOf(rest.value), problem: r.problem };
|
|
999
|
+
}
|
|
1000
|
+
return { status: "refused", path, mode, reason: "invalid", problem: r.problem };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
494
1003
|
const envKey = readEnvKey();
|
|
495
1004
|
const r = validateJevConfig(parsed, envKey);
|
|
496
1005
|
if (!r.ok) {
|
|
@@ -499,15 +1008,11 @@ export function inspectJevConfig(): JevConfigInspection {
|
|
|
499
1008
|
// validation never got that far, so ask again with a stand-in key.
|
|
500
1009
|
const rest = validateJevConfig(parsed, KEY_STAND_IN);
|
|
501
1010
|
if (rest.ok) {
|
|
502
|
-
const routing: Omit<JevConfig, "apiKey"> = { provider: rest.value.provider, mode: rest.value.mode, timeoutMs: rest.value.timeoutMs };
|
|
503
|
-
if (rest.value.baseUrl !== undefined) routing.baseUrl = rest.value.baseUrl;
|
|
504
|
-
if (rest.value.accountId !== undefined) routing.accountId = rest.value.accountId;
|
|
505
|
-
if (rest.value.model !== undefined) routing.model = rest.value.model;
|
|
506
1011
|
return {
|
|
507
1012
|
status: "key-missing",
|
|
508
1013
|
path,
|
|
509
1014
|
mode,
|
|
510
|
-
routing,
|
|
1015
|
+
routing: routingOf(rest.value),
|
|
511
1016
|
problem: `it carries no apiKey, so the key comes from ${JEV_API_KEY_ENV} — which is not set in this environment`,
|
|
512
1017
|
};
|
|
513
1018
|
}
|
|
@@ -520,14 +1025,57 @@ export function inspectJevConfig(): JevConfigInspection {
|
|
|
520
1025
|
}
|
|
521
1026
|
|
|
522
1027
|
/**
|
|
523
|
-
*
|
|
524
|
-
* key-
|
|
525
|
-
*
|
|
1028
|
+
* Why a Cloud file on a connected machine is off when no Jev key is stored for
|
|
1029
|
+
* the connection (`key-lacks-jev`). Hedged: a connect whose introspect went
|
|
1030
|
+
* unanswered clears the slot too, so nothing on disk says what the key carries.
|
|
1031
|
+
*/
|
|
1032
|
+
export const KEY_LACKS_JEV_PROBLEM =
|
|
1033
|
+
"this machine is connected to FailproofAI Cloud, but no Jev key is stored for that connection (its key lacks jev:evaluate, or the connect could not confirm it)";
|
|
1034
|
+
|
|
1035
|
+
/** Everything a validated config says except its key — copied field by field, so the key cannot ride along. */
|
|
1036
|
+
function routingOf(cfg: JevConfig): Omit<JevConfig, "apiKey"> {
|
|
1037
|
+
const routing: Omit<JevConfig, "apiKey"> = { provider: cfg.provider, mode: cfg.mode, timeoutMs: cfg.timeoutMs };
|
|
1038
|
+
if (cfg.baseUrl !== undefined) routing.baseUrl = cfg.baseUrl;
|
|
1039
|
+
if (cfg.accountId !== undefined) routing.accountId = cfg.accountId;
|
|
1040
|
+
if (cfg.model !== undefined) routing.model = cfg.model;
|
|
1041
|
+
// Never `credentialOrigin`: every status that reports routing is one with no
|
|
1042
|
+
// usable credential behind it, so there is no origin to report — only the
|
|
1043
|
+
// stand-in it was validated with.
|
|
1044
|
+
return routing;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* A Cloud credential that stands in for a missing one, on the file's own origin,
|
|
1049
|
+
* so the rest of a Cloud file can be validated when there is no key to check it
|
|
1050
|
+
* with. Like `KEY_STAND_IN` it is never written and never sent; an unparseable
|
|
1051
|
+
* `baseUrl` gets a placeholder origin and is refused by `validateBaseUrl` first.
|
|
1052
|
+
*/
|
|
1053
|
+
function standInCloudCredential(fields: Record<string, unknown>): JevCloudCredential {
|
|
1054
|
+
let url = "https://stand-in.invalid";
|
|
1055
|
+
if (typeof fields.baseUrl === "string") {
|
|
1056
|
+
try {
|
|
1057
|
+
url = new URL(fields.baseUrl.trim()).origin;
|
|
1058
|
+
} catch {
|
|
1059
|
+
// Keep the placeholder.
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return { url, key: KEY_STAND_IN };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* The validated global config, or null — absent, refused, invalid, switched
|
|
1067
|
+
* off, a key-from-the-environment file in an environment that does not set it,
|
|
1068
|
+
* or a FailproofAI Cloud file on a machine with no Cloud credential all mean
|
|
1069
|
+
* Jev is off and the regex path runs unchanged. Never throws.
|
|
1070
|
+
*
|
|
1071
|
+
* `mode: "off"` never comes out of here: `inspectJevConfig` reports it as its
|
|
1072
|
+
* own status, and the check below is the belt to that brace — an off config
|
|
1073
|
+
* reaching the hook path would be read as the default mode, `enforce`.
|
|
526
1074
|
*/
|
|
527
1075
|
export function loadJevConfig(): JevConfig | null {
|
|
528
1076
|
try {
|
|
529
1077
|
const r = inspectJevConfig();
|
|
530
|
-
return r.status === "ok" ? r.config : null;
|
|
1078
|
+
return r.status === "ok" && r.config.mode !== "off" ? r.config : null;
|
|
531
1079
|
} catch {
|
|
532
1080
|
return null;
|
|
533
1081
|
}
|