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
package/src/hooks/jev-cli.ts
CHANGED
|
@@ -10,8 +10,28 @@
|
|
|
10
10
|
* enabled policy set Jev is allowed to clear, and how Jev has
|
|
11
11
|
* been doing (fallbacks, latency, clears) — never the key
|
|
12
12
|
* jev test one tiny live request: latency and the Jev version that answered
|
|
13
|
+
* jev models what `GET <base>/models` says this endpoint serves
|
|
13
14
|
* jev remove delete the file; hooks go back to the regex engine unchanged
|
|
14
15
|
*
|
|
16
|
+
* # `--url` is a BASE, and the model has to exist
|
|
17
|
+
*
|
|
18
|
+
* Two contract mistakes cost a customer on a LiteLLM proxy real time, and both
|
|
19
|
+
* were things this command already knew enough to say:
|
|
20
|
+
*
|
|
21
|
+
* - `--url https://…/typesafe/v1/models` was an ENDPOINT given where a BASE
|
|
22
|
+
* belongs. It saved without complaint, `/systemone` was appended to it, and the
|
|
23
|
+
* only signal was `http-404: Not Found` from `jev test`. Setup now refuses a URL
|
|
24
|
+
* whose path ends in `/models`, `/chat/completions`, `/completions`,
|
|
25
|
+
* `/embeddings` or `/systemone` and names the base it implies
|
|
26
|
+
* (`endpointGivenAsBase`).
|
|
27
|
+
* - the model then still did not exist: `custom`'s default is `jev-1.13.0` and
|
|
28
|
+
* that proxy serves `jev-latest` and `jev-preview`. Setup now reads
|
|
29
|
+
* `GET <base>/models` before it writes, and refuses a model the list does not
|
|
30
|
+
* carry, naming the ones it does. When the list cannot be read — no key yet, a
|
|
31
|
+
* proxy that serves no list, Cloudflare, an unreachable host — setup proceeds
|
|
32
|
+
* exactly as it did before: this is a check where one is possible, never a
|
|
33
|
+
* dependency on a model-list endpoint existing.
|
|
34
|
+
*
|
|
15
35
|
* # The provider is read off the URL
|
|
16
36
|
*
|
|
17
37
|
* `--url` names the endpoint, and its HOST says which provider that is
|
|
@@ -80,25 +100,42 @@ import { writeJsonAtomically } from "../../lib/atomic-write";
|
|
|
80
100
|
import {
|
|
81
101
|
DEFAULT_JEV_MODE,
|
|
82
102
|
JEV_API_KEY_ENV,
|
|
103
|
+
JEV_CLOUD_PROVIDER,
|
|
83
104
|
JEV_CONFIG_DEFAULT_TIMEOUT_MS,
|
|
84
105
|
JEV_PROVIDER_KINDS,
|
|
106
|
+
MAX_JEV_TIMEOUT_MS,
|
|
107
|
+
endpointGivenAsBase,
|
|
85
108
|
inspectJevConfig,
|
|
109
|
+
jevCloudBaseUrl,
|
|
86
110
|
jevConfigPath,
|
|
111
|
+
providerForUrl,
|
|
112
|
+
providerHostConflict,
|
|
87
113
|
readJevConfigFileForUpdate,
|
|
88
114
|
validateApiKey,
|
|
89
115
|
validateBaseUrl,
|
|
90
116
|
validateJevConfig,
|
|
117
|
+
type EndpointGivenAsBase,
|
|
91
118
|
type JevConfig,
|
|
119
|
+
type JevConfigInspection,
|
|
92
120
|
type JevProviderKind,
|
|
93
121
|
} from "./semantic/jev-config";
|
|
94
122
|
import {
|
|
123
|
+
JEV_MODEL_LIST_TIMEOUT_MS,
|
|
95
124
|
JEV_PROVIDER_DEFAULTS,
|
|
96
125
|
JevError,
|
|
97
126
|
displayEndpoint,
|
|
127
|
+
endpointAsBaseReason,
|
|
128
|
+
jevModelsUrl,
|
|
98
129
|
jevRoute,
|
|
130
|
+
listDescribesSystemOne,
|
|
131
|
+
modelListHasModel,
|
|
132
|
+
modelsUrlForBase,
|
|
99
133
|
readAnswers,
|
|
134
|
+
readJevModelList,
|
|
100
135
|
scrubSecret,
|
|
101
136
|
transportForConfig,
|
|
137
|
+
type JevModelListRead,
|
|
138
|
+
type JevModelListResult,
|
|
102
139
|
} from "./semantic/jev-client";
|
|
103
140
|
import {
|
|
104
141
|
reviewableProblem,
|
|
@@ -107,8 +144,9 @@ import {
|
|
|
107
144
|
type ReviewableCoverage,
|
|
108
145
|
} from "./policy-reviewability";
|
|
109
146
|
import { jevStats, type JevStats } from "./semantic/jev-stats";
|
|
147
|
+
import { readCredentials, readJevCloudCredential, type JevCloudCredential } from "./fp-config";
|
|
110
148
|
import type { JevRequest } from "./semantic/types";
|
|
111
|
-
import { emptyState, nextStep, note, optsFor, rows, rule, stack, title, warning, type RenderOpts } from "./tui";
|
|
149
|
+
import { TOKEN_ON_ARGV, emptyState, nextStep, note, optsFor, rows, rule, stack, title, warning, type RenderOpts } from "./tui";
|
|
112
150
|
|
|
113
151
|
export interface JevCliResult {
|
|
114
152
|
lines: string[];
|
|
@@ -117,6 +155,9 @@ export interface JevCliResult {
|
|
|
117
155
|
json?: string;
|
|
118
156
|
}
|
|
119
157
|
|
|
158
|
+
/** `GET <base>/models`, as `setup` and `models` reach it. */
|
|
159
|
+
export type JevModelListReader = (modelsUrl: string, apiKey: string | null) => Promise<JevModelListResult>;
|
|
160
|
+
|
|
120
161
|
export interface JevCliDeps {
|
|
121
162
|
/** Everything piped on stdin (for `--key-stdin` off a terminal). */
|
|
122
163
|
readStdin?: () => Promise<string>;
|
|
@@ -125,9 +166,19 @@ export interface JevCliDeps {
|
|
|
125
166
|
stdinIsTTY?: boolean;
|
|
126
167
|
/** Budget for `jev test`'s single request. */
|
|
127
168
|
testTimeoutMs?: number;
|
|
169
|
+
/**
|
|
170
|
+
* How `<base>/models` is read. The default reaches the network, which `setup`
|
|
171
|
+
* did not used to do at all — so it is injectable, and a unit test that is not
|
|
172
|
+
* about the list injects a reader that reads nothing rather than calling out to
|
|
173
|
+
* a provider from a test run.
|
|
174
|
+
*/
|
|
175
|
+
readModelList?: JevModelListReader;
|
|
128
176
|
render?: RenderOpts;
|
|
129
177
|
}
|
|
130
178
|
|
|
179
|
+
/** The real reader, on its own budget: see `JEV_MODEL_LIST_TIMEOUT_MS`. */
|
|
180
|
+
const liveModelListReader: JevModelListReader = (url, apiKey) => readJevModelList(url, apiKey, AbortSignal.timeout(JEV_MODEL_LIST_TIMEOUT_MS));
|
|
181
|
+
|
|
131
182
|
const ok = (lines: string[], json?: string): JevCliResult => ({ lines, exitCode: 0, ...(json !== undefined ? { json } : {}) });
|
|
132
183
|
const fail = (lines: string[], json?: string): JevCliResult => ({ lines, exitCode: 1, ...(json !== undefined ? { json } : {}) });
|
|
133
184
|
|
|
@@ -135,8 +186,10 @@ export const JEV_USAGE = [
|
|
|
135
186
|
"Usage:",
|
|
136
187
|
" failproofai jev --url <url> [--key-stdin | --token <token>] [options]",
|
|
137
188
|
" failproofai jev setup --provider <kind> [--key-stdin | --key-from-env] [options]",
|
|
189
|
+
" failproofai jev setup --provider failproofai [--mode off|shadow|enforce] (FailproofAI Cloud: no key, no URL)",
|
|
138
190
|
" failproofai jev status [--json]",
|
|
139
191
|
" failproofai jev test [--json]",
|
|
192
|
+
" failproofai jev models [--provider <kind>] [--url <base>] [--json]",
|
|
140
193
|
" failproofai jev remove",
|
|
141
194
|
];
|
|
142
195
|
|
|
@@ -213,11 +266,84 @@ function parseFlags(argv: string[], allowed: Set<string>): Parsed | string {
|
|
|
213
266
|
// ── Rendering helpers ────────────────────────────────────────────────────────
|
|
214
267
|
|
|
215
268
|
function modeLine(mode: NonNullable<JevConfig["mode"]>): string {
|
|
269
|
+
if (mode === "off") return "off — Jev is not asked at all; the regex policies decide alone";
|
|
216
270
|
return mode === "enforce"
|
|
217
271
|
? "enforce — Jev's verdicts apply: it may clear a reviewable policy's deny and add its own"
|
|
218
272
|
: "shadow — Jev is asked and logged; the regex result is what is enforced";
|
|
219
273
|
}
|
|
220
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The name a provider is SHOWN by. Only the Cloud route differs from its kind:
|
|
277
|
+
* `failproofai` is the value in the file, and "FailproofAI Cloud" is what the
|
|
278
|
+
* person connected to.
|
|
279
|
+
*/
|
|
280
|
+
export function providerLabel(provider: JevProviderKind): string {
|
|
281
|
+
return provider === JEV_CLOUD_PROVIDER ? "FailproofAI Cloud" : provider;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Where requests go, as `jev status` shows it. The Cloud route shows its HOST
|
|
286
|
+
* only: the path under it is the Cloud's own routing, the same on every
|
|
287
|
+
* deployment, and the host is the one fact that says which Cloud this machine
|
|
288
|
+
* is talking to.
|
|
289
|
+
*/
|
|
290
|
+
function shownEndpoint(provider: JevProviderKind, endpoint: string): string {
|
|
291
|
+
if (provider !== JEV_CLOUD_PROVIDER) return displayEndpoint(endpoint);
|
|
292
|
+
try {
|
|
293
|
+
return new URL(endpoint).host;
|
|
294
|
+
} catch {
|
|
295
|
+
return "(invalid URL)";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** What `jev status` says about where the key comes from. */
|
|
300
|
+
const CLOUD_KEY_SOURCE = "FailproofAI Cloud connection";
|
|
301
|
+
|
|
302
|
+
/** Whether this machine holds a usable Jev credential, read owner-only. Never the key. */
|
|
303
|
+
function cloudCredentialPresent(): boolean {
|
|
304
|
+
return cloudConnection().keyCarriesJev;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Whether a refused or missing jev.json is fixed from the Cloud connection: it
|
|
309
|
+
* names the Cloud, or it names no provider (absent, not JSON) on a machine whose
|
|
310
|
+
* key carries Jev — which is what `config --token` suggests for the same file.
|
|
311
|
+
*/
|
|
312
|
+
function cloudRoute(raw: Record<string, unknown> | null | undefined): boolean {
|
|
313
|
+
return raw?.provider === JEV_CLOUD_PROVIDER || (raw?.provider === undefined && cloudCredentialPresent());
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The two facts `status --json` reports about the FailproofAI Cloud connection,
|
|
318
|
+
* read owner-only: whether this machine is connected at all, and whether the
|
|
319
|
+
* key it connected with carries Jev. Never the key.
|
|
320
|
+
*/
|
|
321
|
+
function cloudConnection(): { cloudConnected: boolean; keyCarriesJev: boolean } {
|
|
322
|
+
try {
|
|
323
|
+
const read = readJevCloudCredential();
|
|
324
|
+
if (read.status === "ok") return { cloudConnected: true, keyCarriesJev: true };
|
|
325
|
+
if (read.status === "absent") return { cloudConnected: read.connected, keyCarriesJev: false };
|
|
326
|
+
// Refused (a loose or unreadable credentials.json): no key is usable, but a
|
|
327
|
+
// connection on record is still a connection — as the dashboard reads it.
|
|
328
|
+
const creds = readCredentials();
|
|
329
|
+
return { cloudConnected: Boolean(creds.cloud?.url ?? creds.ingest?.url), keyCarriesJev: false };
|
|
330
|
+
} catch {
|
|
331
|
+
return { cloudConnected: false, keyCarriesJev: false };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The one line `status` titles a connected machine whose key has no Jev with. */
|
|
336
|
+
const KEY_LACKS_JEV_TITLE = "off — no Jev key is stored for this machine's FailproofAI Cloud connection";
|
|
337
|
+
|
|
338
|
+
/** The `key` row for a Cloud file, saying which of the three states the connection is in. */
|
|
339
|
+
function cloudKeyRow(): string {
|
|
340
|
+
const c = cloudConnection();
|
|
341
|
+
if (c.keyCarriesJev) return CLOUD_KEY_SOURCE;
|
|
342
|
+
return c.cloudConnected
|
|
343
|
+
? `${CLOUD_KEY_SOURCE} — connected, no Jev key stored for it`
|
|
344
|
+
: `${CLOUD_KEY_SOURCE} — this machine is not connected`;
|
|
345
|
+
}
|
|
346
|
+
|
|
221
347
|
/** `0600`, or null when unknown. */
|
|
222
348
|
function octal(mode: number | null): string | null {
|
|
223
349
|
return mode === null ? null : mode.toString(8).padStart(4, "0");
|
|
@@ -238,10 +364,68 @@ function scrubbed(e: { code: string; message: string }, key: string): { code: st
|
|
|
238
364
|
return { code: e.code, message: scrubSecret(e.message, key) };
|
|
239
365
|
}
|
|
240
366
|
|
|
367
|
+
/**
|
|
368
|
+
* What the person should do about a failed FailproofAI Cloud request, by cause,
|
|
369
|
+
* or null where the generic advice already fits. The remedies differ from a
|
|
370
|
+
* BYOK provider's because the key and the budget are the Cloud connection's and
|
|
371
|
+
* the org's, not anything `jev setup` holds.
|
|
372
|
+
*/
|
|
373
|
+
function cloudRemedy(code: string, message = ""): string | null {
|
|
374
|
+
if (code === "http-401" || code === "http-403") {
|
|
375
|
+
return "FailproofAI Cloud refused this machine's key: it was revoked, or it does not carry jev:evaluate. Reconnect with a key that does (the \"machine\" preset on the Keys page): failproofai config --token <key>";
|
|
376
|
+
}
|
|
377
|
+
if (code === "out-of-credits") {
|
|
378
|
+
return "Your FailproofAI Cloud org has used its plan allowance (HTTP 402). Until it resets or the plan changes, hooks fall back to regex.";
|
|
379
|
+
}
|
|
380
|
+
if (code === "http-429") {
|
|
381
|
+
// Two limits answer 429 (contract decision 18): the per-minute ones, and
|
|
382
|
+
// the org's daily Jev cap, whose body says `daily_limit_reached` and whose
|
|
383
|
+
// Retry-After runs to the next UTC midnight. Only the words tell them
|
|
384
|
+
// apart, and "right now … at most 60 s" is wrong for the second: the
|
|
385
|
+
// machine still asks again after at most 60 s, and keeps being refused.
|
|
386
|
+
if (/\bdaily_limit_reached\b/.test(message)) {
|
|
387
|
+
return "Daily Jev limit for this org reached; resets at 00:00 UTC. Until then hooks fall back to regex; the machine asks FailproofAI Cloud again at most once a minute (Retry-After, capped at 60 s).";
|
|
388
|
+
}
|
|
389
|
+
return "FailproofAI Cloud is rate-limiting Jev for this org right now. Hooks fall back to regex, and send it nothing more until the wait it asked for (Retry-After, at most 60 s) is over.";
|
|
390
|
+
}
|
|
391
|
+
if (code === "http-404") return "This FailproofAI Cloud does not serve Jev (its server predates the Jev route). Hooks fall back to regex until it does.";
|
|
392
|
+
if (code === "http-503") {
|
|
393
|
+
return (
|
|
394
|
+
"This FailproofAI Cloud cannot serve Jev for your org: its deployment has no model gateway, the org is not " +
|
|
395
|
+
"provisioned yet, or the gateway is down. Ask your FailproofAI Cloud admin. Hooks fall back to regex and ask " +
|
|
396
|
+
"again at most once a minute."
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
// `request_rejected`: TypeSafe refused this request itself, typically dense
|
|
400
|
+
// text over Jev's token budget. The same call gets the same answer.
|
|
401
|
+
if (code === "http-422") return "Jev refused this call's request, usually because it held dense text (base64, hex, minified code) over Jev's token budget. That call falls back to regex every time; this is not an outage.";
|
|
402
|
+
if (code === "model-mismatch") return "FailproofAI Cloud answered with a model outside the Jev 1.13 family, so hooks would fall back to regex. This is the server's to fix.";
|
|
403
|
+
// The generic advice for these names `--base-url`, which this route refuses:
|
|
404
|
+
// its endpoint is the Cloud this machine connected to, changed only by
|
|
405
|
+
// reconnecting.
|
|
406
|
+
if (/^http-3(?:\d\d|xx)$/.test(code)) {
|
|
407
|
+
return "FailproofAI Cloud answered with a redirect, and Jev requests never follow one. The URL this machine connected to is probably not the one that serves the API (a sign-in page, or an old address). Reconnect with the right one: failproofai config --token <key> --url <url>";
|
|
408
|
+
}
|
|
409
|
+
if (code === "network") {
|
|
410
|
+
return "FailproofAI Cloud could not be reached. Check your network; if the Cloud's address has changed, reconnect: failproofai config --token <key> --url <url>";
|
|
411
|
+
}
|
|
412
|
+
if (code === "config") return "The config is not usable. Rebuild it from this machine's FailproofAI Cloud connection: failproofai jev setup --provider failproofai";
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
241
416
|
/** What the person should do about a failed request, by cause. */
|
|
242
|
-
function remedy(code: string): string {
|
|
417
|
+
function remedy(code: string, provider?: JevProviderKind, message?: string): string {
|
|
418
|
+
if (provider === JEV_CLOUD_PROVIDER) {
|
|
419
|
+
const cloud = cloudRemedy(code, message);
|
|
420
|
+
if (cloud) return cloud;
|
|
421
|
+
}
|
|
243
422
|
if (code === "http-401" || code === "http-403") return "The provider refused the key. Re-run `failproofai jev setup` with the right one.";
|
|
244
423
|
if (code === "out-of-credits") return "The account is out of credits (HTTP 402). Top it up with the provider; until then hooks fall back to regex.";
|
|
424
|
+
// The message from `httpFailureMessage` has already said what a 404 there
|
|
425
|
+
// means; this is the part that is an instruction rather than a diagnosis.
|
|
426
|
+
if (code === "http-404") {
|
|
427
|
+
return "Set the base URL to the provider's version root, without the endpoint path: `failproofai jev setup --base-url <base>`. `failproofai jev models` says what a base serves.";
|
|
428
|
+
}
|
|
245
429
|
if (code === "http-429") return "The provider rate-limited this key. Hooks fall back to regex whenever that happens.";
|
|
246
430
|
if (code.startsWith("http-5")) return "The provider had a server error. Hooks fall back to regex whenever that happens; try again shortly.";
|
|
247
431
|
if (code === "timeout") return "No answer in time. Check the endpoint and your network.";
|
|
@@ -323,28 +507,6 @@ function movedHost(provider: JevProviderKind, before: unknown, after: unknown):
|
|
|
323
507
|
return { from, to };
|
|
324
508
|
}
|
|
325
509
|
|
|
326
|
-
/**
|
|
327
|
-
* The provider each known host IS, so `failproofai jev --url <url>` needs no
|
|
328
|
-
* `--provider`. Exact hosts only: a neighbouring name (`eu.api.typesafe.ai`, a
|
|
329
|
-
* corporate proxy) is `custom`, which reaches the same TypeSafe-compatible API
|
|
330
|
-
* at the URL given, and `--provider` is there to say otherwise.
|
|
331
|
-
*/
|
|
332
|
-
const PROVIDER_BY_HOST: Readonly<Record<string, JevProviderKind>> = {
|
|
333
|
-
"api.typesafe.ai": "typesafe",
|
|
334
|
-
"openrouter.ai": "openrouter",
|
|
335
|
-
"ai-gateway.vercel.sh": "vercel",
|
|
336
|
-
"api.cloudflare.com": "cloudflare",
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
/** The provider a validated URL names, or `custom` for a host that is nobody's. */
|
|
340
|
-
function providerForUrl(url: string): JevProviderKind {
|
|
341
|
-
try {
|
|
342
|
-
return PROVIDER_BY_HOST[new URL(url).hostname.toLowerCase()] ?? "custom";
|
|
343
|
-
} catch {
|
|
344
|
-
return "custom";
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
510
|
/**
|
|
349
511
|
* The account id a Cloudflare URL already carries, or null.
|
|
350
512
|
*
|
|
@@ -360,7 +522,7 @@ function providerForUrl(url: string): JevProviderKind {
|
|
|
360
522
|
export function accountIdFromUrl(url: string): string | null {
|
|
361
523
|
try {
|
|
362
524
|
const parsed = new URL(url);
|
|
363
|
-
if (
|
|
525
|
+
if (providerForUrl(url) !== "cloudflare") return null;
|
|
364
526
|
const m = /\/accounts\/([0-9a-f]{32})(?:\/|$)/i.exec(parsed.pathname);
|
|
365
527
|
return m ? m[1].toLowerCase() : null;
|
|
366
528
|
} catch {
|
|
@@ -402,6 +564,70 @@ function offProviderBase(cfg: JevConfig, endpoint: string): { endpoint: string;
|
|
|
402
564
|
}
|
|
403
565
|
}
|
|
404
566
|
|
|
567
|
+
/**
|
|
568
|
+
* The refusal for a URL that is an endpoint where a base belongs.
|
|
569
|
+
*
|
|
570
|
+
* Both URLs go through `displayEndpoint`, which replaces a query string with
|
|
571
|
+
* `?…`: a proxy base may legitimately carry `?api-version=` — or a token — and a
|
|
572
|
+
* refusal is not a reason to print one. That does make the suggested command a
|
|
573
|
+
* shape rather than something to paste, which is the right trade: the person has
|
|
574
|
+
* their own URL, and the point of the line is which segment to drop.
|
|
575
|
+
*/
|
|
576
|
+
function endpointAsBaseRefusal(flag: string, given: string, found: EndpointGivenAsBase): string[] {
|
|
577
|
+
return [
|
|
578
|
+
`Not saved: ${flag} names an endpoint, not an API base — its path ends in ${found.suffix}.`,
|
|
579
|
+
endpointAsBaseReason(given),
|
|
580
|
+
"Give the base it sits under:",
|
|
581
|
+
` failproofai jev ${flag} ${displayEndpoint(found.base)}`,
|
|
582
|
+
"",
|
|
583
|
+
"Nothing was written.",
|
|
584
|
+
];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** At most this many names in one refusal or one rendered list; the rest are counted. */
|
|
588
|
+
const MAX_SHOWN_MODELS = 20;
|
|
589
|
+
|
|
590
|
+
/** Model names for one line, bounded — the list came from a provider and may be long. */
|
|
591
|
+
function shownModels(models: string[]): string {
|
|
592
|
+
if (models.length <= MAX_SHOWN_MODELS) return models.join(", ");
|
|
593
|
+
return `${models.slice(0, MAX_SHOWN_MODELS).join(", ")} … and ${models.length - MAX_SHOWN_MODELS} more`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* The list this config's endpoint serves, when it both could be read AND is
|
|
598
|
+
* authoritative about `<base>/systemone` AND does not carry the configured model
|
|
599
|
+
* — i.e. the one case that is worth refusing. Null in every other case, so a
|
|
600
|
+
* caller's "carry on unchanged" path is the default rather than the exception.
|
|
601
|
+
*
|
|
602
|
+
* `apiKey` is passed separately because a `--key-from-env` config in a shell with
|
|
603
|
+
* no variable set holds a stand-in, which must not be sent anywhere; the public
|
|
604
|
+
* lists (Vercel's, OpenRouter's) answer without a key anyway.
|
|
605
|
+
*/
|
|
606
|
+
async function modelAbsentFromList(
|
|
607
|
+
cfg: JevConfig,
|
|
608
|
+
model: string,
|
|
609
|
+
apiKey: string | null,
|
|
610
|
+
read: JevModelListReader,
|
|
611
|
+
): Promise<{ list: JevModelListRead; url: string } | null> {
|
|
612
|
+
let url: string | null;
|
|
613
|
+
try {
|
|
614
|
+
url = jevModelsUrl(cfg);
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
if (url === null) return null;
|
|
619
|
+
let list: JevModelListResult;
|
|
620
|
+
try {
|
|
621
|
+
list = await read(url, apiKey);
|
|
622
|
+
} catch {
|
|
623
|
+
// A reader that throws is a reader that read nothing, which is the case this
|
|
624
|
+
// whole check is required to survive.
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
if (!list.ok || !listDescribesSystemOne(list) || modelListHasModel(list, model)) return null;
|
|
628
|
+
return { list, url };
|
|
629
|
+
}
|
|
630
|
+
|
|
405
631
|
async function readAllStdin(): Promise<string> {
|
|
406
632
|
const chunks: Buffer[] = [];
|
|
407
633
|
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
@@ -413,7 +639,20 @@ async function maskedPrompt(): Promise<string | null> {
|
|
|
413
639
|
return promptText({ message: "Jev API key", mask: true, validate: (v) => validateApiKey(v) });
|
|
414
640
|
}
|
|
415
641
|
|
|
642
|
+
/** What a run that put the key in argv ends with, saved or refused: the key is in history either way. */
|
|
643
|
+
const TOKEN_HISTORY_WARNING = [
|
|
644
|
+
TOKEN_ON_ARGV,
|
|
645
|
+
"Prefer piping the key in — `failproofai jev --url <url> --key-stdin < key-file` — and rotate this one if it matters.",
|
|
646
|
+
];
|
|
647
|
+
|
|
416
648
|
async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise<JevCliResult> {
|
|
649
|
+
const r = await setupRun(argv, deps, opts);
|
|
650
|
+
// A save says it in its own place; every refusal says it here.
|
|
651
|
+
const tokenGiven = argv.some((a, i) => a.startsWith("--token=") || (a === "--token" && i + 1 < argv.length));
|
|
652
|
+
return r.exitCode === 0 || !tokenGiven ? r : { ...r, lines: [...r.lines, "", ...TOKEN_HISTORY_WARNING] };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function setupRun(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise<JevCliResult> {
|
|
417
656
|
const parsed = parseFlags(argv, new Set([...VALUE_FLAGS, "--key-stdin", "--key-from-env"]));
|
|
418
657
|
if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
|
|
419
658
|
if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
|
|
@@ -450,6 +689,22 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
450
689
|
]);
|
|
451
690
|
}
|
|
452
691
|
|
|
692
|
+
// FailproofAI Cloud has a setup of its own: its endpoint and key come from the
|
|
693
|
+
// machine's connection, so none of the flags below that choose them apply.
|
|
694
|
+
// Reached by naming it, or by re-running setup (a mode switch, say) over a
|
|
695
|
+
// Cloud file without naming another provider or a URL — never by a URL: a
|
|
696
|
+
// `--url` is a BYOK endpoint whatever host it names (`PROVIDER_BY_HOST`
|
|
697
|
+
// deliberately has no Cloud entry).
|
|
698
|
+
if (
|
|
699
|
+
namedProvider === JEV_CLOUD_PROVIDER ||
|
|
700
|
+
(namedProvider === undefined &&
|
|
701
|
+
!values.has("--url") &&
|
|
702
|
+
!values.has("--base-url") &&
|
|
703
|
+
readJevConfigFileForUpdate()?.raw.provider === JEV_CLOUD_PROVIDER)
|
|
704
|
+
) {
|
|
705
|
+
return cloudSetup(values, bools, opts);
|
|
706
|
+
}
|
|
707
|
+
|
|
453
708
|
// `--url` is `--base-url` that also picks the provider. It is validated here,
|
|
454
709
|
// by the loader's own `validateBaseUrl`, so a URL is refused in the same
|
|
455
710
|
// words whichever way it arrived — and so the host below is a parsed host,
|
|
@@ -467,24 +722,15 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
467
722
|
// An explicit provider wins — except where it disagrees with the host about
|
|
468
723
|
// which gateway this is, which is a disagreement about where the key goes.
|
|
469
724
|
// `custom` is not a disagreement: it is the "treat this URL as itself" ask.
|
|
470
|
-
|
|
725
|
+
// The rule is `providerHostConflict`'s, shared with `--base-url` below and
|
|
726
|
+
// the dashboard's save; only the advice here is about `--url`.
|
|
727
|
+
const conflict = explicit === null ? null : providerHostConflict(explicit, normalized);
|
|
728
|
+
if (conflict !== null) {
|
|
471
729
|
return fail([
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
]);
|
|
477
|
-
}
|
|
478
|
-
// The one host where "treat this URL as itself" cannot be honoured. A
|
|
479
|
-
// `custom` route POSTs the TypeSafe-native body to `<url>/systemone` and
|
|
480
|
-
// reads a bare answer back; Workers AI answers at `/accounts/<id>/ai/run`
|
|
481
|
-
// and wraps its answer in `result`, which is why it has a provider of its
|
|
482
|
-
// own. Saved as `custom` it would ask for the `--account-id` it then had
|
|
483
|
-
// nowhere to put, and every call would fall back to regex.
|
|
484
|
-
if (explicit === "custom" && urlProvider.kind === "cloudflare") {
|
|
485
|
-
return fail([
|
|
486
|
-
`Not saved: ${urlProvider.host} is Cloudflare Workers AI, which provider custom cannot reach — its endpoint is per-account and its answers are wrapped, and a custom endpoint is asked in TypeSafe's own shape at <url>/systemone.`,
|
|
487
|
-
"Use the provider that speaks it: --provider cloudflare --account-id <32 hex characters> — or drop --provider, since the URL already says cloudflare.",
|
|
730
|
+
explicit === "custom" ? `Not saved: ${conflict}.` : `--provider ${explicit} and --url disagree: ${conflict}.`,
|
|
731
|
+
explicit === "custom"
|
|
732
|
+
? "Use the provider that speaks it: --provider cloudflare --account-id <32 hex characters> — or drop --provider, since the URL already says cloudflare."
|
|
733
|
+
: "Drop --provider to take the provider from the URL, or give the URL that provider's own endpoint.",
|
|
488
734
|
"",
|
|
489
735
|
"Nothing was written.",
|
|
490
736
|
]);
|
|
@@ -526,6 +772,21 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
526
772
|
values.delete("--url");
|
|
527
773
|
}
|
|
528
774
|
|
|
775
|
+
// An endpoint given where a base belongs, whichever flag carried it — `--url`
|
|
776
|
+
// has already become `--base-url` above, so the flag NAMED here is the one the
|
|
777
|
+
// person typed. Before the key is asked for, and before anything is written:
|
|
778
|
+
// the fix is one segment, and nothing should be stored or prompted for first.
|
|
779
|
+
const baseArg = values.get("--base-url");
|
|
780
|
+
if (baseArg !== undefined && baseArg !== "default") {
|
|
781
|
+
const checkedBase = validateBaseUrl(baseArg);
|
|
782
|
+
// An unusable URL is refused by validation below, in its own words; this
|
|
783
|
+
// check only has something to say about one that parses.
|
|
784
|
+
if (checkedBase.ok) {
|
|
785
|
+
const asEndpoint = endpointGivenAsBase(checkedBase.value);
|
|
786
|
+
if (asEndpoint) return fail(endpointAsBaseRefusal(urlArg !== undefined ? "--url" : "--base-url", checkedBase.value, asEndpoint));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
529
790
|
const existingFile = readJevConfigFileForUpdate();
|
|
530
791
|
const existing = existingFile?.raw ?? null;
|
|
531
792
|
const providerArg = values.get("--provider");
|
|
@@ -542,6 +803,22 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
542
803
|
return fail(["Unknown provider (not repeated here, in case it is a key).", `Providers: ${JEV_PROVIDER_KINDS.join(", ")}`]);
|
|
543
804
|
}
|
|
544
805
|
const kind = provider as JevProviderKind;
|
|
806
|
+
// The same rule for a `--base-url` typed as such (a `--url` was checked above,
|
|
807
|
+
// with advice about `--url`), so the flag and the dashboard cannot save a
|
|
808
|
+
// pair `--url` refuses.
|
|
809
|
+
const typedBase = urlArg === undefined ? values.get("--base-url") : undefined;
|
|
810
|
+
const checkedTyped = typedBase === undefined || typedBase === "default" ? null : validateBaseUrl(typedBase);
|
|
811
|
+
const hostConflict = checkedTyped?.ok ? providerHostConflict(kind, checkedTyped.value) : null;
|
|
812
|
+
if (checkedTyped?.ok && hostConflict !== null) {
|
|
813
|
+
return fail([
|
|
814
|
+
`Not saved: ${hostConflict}.`,
|
|
815
|
+
kind === "custom"
|
|
816
|
+
? "Use the provider that speaks it: --provider cloudflare --account-id <32 hex characters>."
|
|
817
|
+
: `Give ${kind}'s own endpoint, or the provider this URL belongs to: --provider ${providerForUrl(checkedTyped.value)}.`,
|
|
818
|
+
"",
|
|
819
|
+
"Nothing was written.",
|
|
820
|
+
]);
|
|
821
|
+
}
|
|
545
822
|
const sameProvider = existing !== null && existing.provider === provider;
|
|
546
823
|
|
|
547
824
|
// Same provider: update in place, keeping the key and every field not named
|
|
@@ -718,6 +995,42 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
718
995
|
if (!checked.ok) return fail([`Not saved: ${checked.problem}.`, "", "Nothing was written."]);
|
|
719
996
|
const cfg = checked.value;
|
|
720
997
|
|
|
998
|
+
// The model, against what the endpoint says it serves. This is the second half
|
|
999
|
+
// of the LiteLLM-proxy failure: the URL was fixed and the default model still
|
|
1000
|
+
// did not exist there, which nothing said until `jev test` returned a 404 and
|
|
1001
|
+
// the person curled the endpoint by hand.
|
|
1002
|
+
//
|
|
1003
|
+
// Never a hard requirement: a list that cannot be read (no key yet, a proxy
|
|
1004
|
+
// that serves none, Cloudflare, an unreachable host, a shape nobody knows) and
|
|
1005
|
+
// a list that is not authoritative about `/systemone` both fall through to the
|
|
1006
|
+
// write, so this can only ever turn a silent misconfiguration into a refusal —
|
|
1007
|
+
// it can never turn a working setup into a failing one.
|
|
1008
|
+
const routeForCheck = ((): ReturnType<typeof jevRoute> | null => {
|
|
1009
|
+
try {
|
|
1010
|
+
return jevRoute(cfg);
|
|
1011
|
+
} catch {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
})();
|
|
1015
|
+
if (routeForCheck !== null) {
|
|
1016
|
+
// The stand-in is not a key and must not be sent; the public lists answer
|
|
1017
|
+
// without one, so the read is still worth making.
|
|
1018
|
+
const listKey = keyFromEnv && !envKey ? null : cfg.apiKey;
|
|
1019
|
+
const absent = await modelAbsentFromList(cfg, routeForCheck.model, listKey, deps.readModelList ?? liveModelListReader);
|
|
1020
|
+
if (absent) {
|
|
1021
|
+
return fail([
|
|
1022
|
+
`Not saved: ${displayEndpoint(absent.url)} does not list ${routeForCheck.model}, so every request would fail there.`,
|
|
1023
|
+
` it serves: ${shownModels(absent.list.models)}`,
|
|
1024
|
+
routeForCheck.modelIsDefault
|
|
1025
|
+
? `${routeForCheck.model} is this build's default for provider ${provider}, and this endpoint names Jev differently — so --model is not optional here. Add it to the command you just ran:`
|
|
1026
|
+
: "Pick one of the names above:",
|
|
1027
|
+
` --model ${absent.list.models[0]}`,
|
|
1028
|
+
"",
|
|
1029
|
+
"Nothing was written.",
|
|
1030
|
+
]);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
721
1034
|
const path = jevConfigPath();
|
|
722
1035
|
try {
|
|
723
1036
|
writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
|
|
@@ -793,17 +1106,223 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
793
1106
|
// command line it came on is not — it is in this shell's history file and
|
|
794
1107
|
// was readable from /proc by anything running as this user while the
|
|
795
1108
|
// process lived.
|
|
796
|
-
tokenOnCommandLine
|
|
797
|
-
? warning(
|
|
798
|
-
[
|
|
799
|
-
"--token was on the command line: your shell history has it, and while this command ran any process of yours could read it from the process list.",
|
|
800
|
-
"Prefer piping the key in — `failproofai jev --url <url> --key-stdin < key-file` — and rotate this one if it matters.",
|
|
801
|
-
],
|
|
802
|
-
opts,
|
|
803
|
-
)
|
|
804
|
-
: null,
|
|
1109
|
+
tokenOnCommandLine ? warning(TOKEN_HISTORY_WARNING, opts) : null,
|
|
805
1110
|
note("Hooks read this file on every tool call — no restart. Without it they run the regex policies exactly as before.", opts),
|
|
806
|
-
|
|
1111
|
+
// Switched off, `jev test` only answers "not run — switched off": a next
|
|
1112
|
+
// step that leads nowhere is worse than none.
|
|
1113
|
+
(cfg.mode ?? DEFAULT_JEV_MODE) === "off" ? null : nextStep("failproofai jev test", "Check it with one live request:", opts),
|
|
1114
|
+
),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ── setup: FailproofAI Cloud ─────────────────────────────────────────────────
|
|
1119
|
+
|
|
1120
|
+
/** How `inspectJevConfig` opens a refusal that is about credentials.json. */
|
|
1121
|
+
const CLOUD_CREDENTIAL_REFUSED = "the FailproofAI Cloud credential was refused";
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* The next step for a refused config: one ladder, so `status` and `test` never
|
|
1125
|
+
* disagree about the same inspection.
|
|
1126
|
+
*/
|
|
1127
|
+
function refusedNextStep(inspection: Extract<JevConfigInspection, { status: "refused" }>, cloudFile: boolean): { cmd: string; lead: string } {
|
|
1128
|
+
// The Cloud route's key file is credentials.json, and a refusal can be about
|
|
1129
|
+
// THAT file: the key it holds was put there by `config --token`, not by
|
|
1130
|
+
// anything `jev setup` could re-ask for.
|
|
1131
|
+
if (cloudFile && inspection.problem.startsWith(CLOUD_CREDENTIAL_REFUSED)) {
|
|
1132
|
+
if (inspection.reason !== "too-open") {
|
|
1133
|
+
return { cmd: "failproofai config --token <key>", lead: "credentials.json could not be read. Reconnect, which rewrites it at 0600:" };
|
|
1134
|
+
}
|
|
1135
|
+
const m = inspection.credentialsMode ?? 0;
|
|
1136
|
+
const risks = [
|
|
1137
|
+
...((m & 0o044) !== 0 ? ["other users can read it, and with it spend this machine's FailproofAI Cloud key"] : []),
|
|
1138
|
+
...((m & 0o022) !== 0 ? ["other users could change it"] : []),
|
|
1139
|
+
];
|
|
1140
|
+
const risk = risks.length > 0 ? risks.join(", and ") : "other users can write where it lives, so they could put their own in its place";
|
|
1141
|
+
return {
|
|
1142
|
+
cmd: inspection.fix ?? "failproofai config --token <key>",
|
|
1143
|
+
lead: `credentials.json holds the key Jev sends to FailproofAI Cloud, and ${risk}. Make it owner-only (or reconnect, which rewrites it at 0600):`,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (inspection.reason === "too-open") {
|
|
1147
|
+
// Read bits alone expose the key in it; only write bits (or a loose
|
|
1148
|
+
// directory, where the file's own mode is owner-only) let others change it.
|
|
1149
|
+
const m = inspection.mode ?? 0;
|
|
1150
|
+
const readOnly = (m & 0o044) !== 0 && (m & 0o022) === 0;
|
|
1151
|
+
return {
|
|
1152
|
+
// Either the file or the directory it sits in; `fix` says which.
|
|
1153
|
+
cmd: inspection.fix ?? `chmod 600 ${inspection.path}`,
|
|
1154
|
+
lead: readOnly
|
|
1155
|
+
? inspection.keyless
|
|
1156
|
+
? "Make it owner-only:"
|
|
1157
|
+
: "Other users can read this file, and with it any key it holds. Make it owner-only (and rotate the key if it matters):"
|
|
1158
|
+
: cloudFile
|
|
1159
|
+
? "Other users could change this file, so check that endpoint is one you chose. Then make it owner-only (or rebuild it from this machine's connection: `failproofai jev setup --provider failproofai`):"
|
|
1160
|
+
: "Other users could change this file, so check that endpoint is one you chose. Then make it owner-only (or re-run `failproofai jev setup`, which asks for the key again unless the endpoint is the provider's own):",
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (cloudFile) return { cmd: "failproofai jev setup --provider failproofai", lead: "Rewrite it from this machine's FailproofAI Cloud connection:" };
|
|
1164
|
+
return { cmd: "failproofai jev setup --provider <kind> --key-stdin", lead: "Write a valid one:" };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* `jev setup --provider failproofai`, and any setup run over a Cloud file.
|
|
1169
|
+
*
|
|
1170
|
+
* The Cloud route has nothing to choose but its mode and timeout: the endpoint
|
|
1171
|
+
* is `<Cloud base>/enforcement/v1/jev` on the origin the machine connected to,
|
|
1172
|
+
* the key is the connection's own (`credentials.json`, never this file) and the
|
|
1173
|
+
* model is pinned by the server. So every flag that would choose one of those is
|
|
1174
|
+
* refused — naming it would be asking for something this route cannot honour —
|
|
1175
|
+
* and the file is rebuilt from the connection. An `apiKey` a hand-edited file
|
|
1176
|
+
* carries is dropped, which is also what makes such a file valid again.
|
|
1177
|
+
*
|
|
1178
|
+
* A mode or timeout change on an existing Cloud file does not need the machine
|
|
1179
|
+
* to be connected: it changes nothing about where a key goes. Creating one,
|
|
1180
|
+
* or rebuilding its endpoint, does.
|
|
1181
|
+
*/
|
|
1182
|
+
async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts: RenderOpts): Promise<JevCliResult> {
|
|
1183
|
+
const refusedFlag = (["--token", "--url", "--base-url", "--account-id", "--model"] as const).find((f) => values.has(f)) ??
|
|
1184
|
+
(["--key-stdin", "--key-from-env"] as const).find((f) => bools.has(f));
|
|
1185
|
+
if (refusedFlag !== undefined) {
|
|
1186
|
+
const why =
|
|
1187
|
+
refusedFlag === "--model"
|
|
1188
|
+
? `FailproofAI Cloud runs ${JEV_PROVIDER_DEFAULTS.failproofai.model}, pinned on the server; there is no model to choose.`
|
|
1189
|
+
: refusedFlag === "--account-id"
|
|
1190
|
+
? "--account-id is Cloudflare's; FailproofAI Cloud has no use for one."
|
|
1191
|
+
: refusedFlag === "--url" || refusedFlag === "--base-url"
|
|
1192
|
+
? "FailproofAI Cloud's endpoint is the one this machine connected to; to change it, reconnect: failproofai config --token <key> --url <url>"
|
|
1193
|
+
: "FailproofAI Cloud's key is this machine's connection key, kept in credentials.json and never in jev.json; to change it, reconnect: failproofai config --token <key>";
|
|
1194
|
+
return fail([`Not saved: ${refusedFlag} does not apply to provider failproofai. ${why}`, "", "Nothing was written."]);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const existing = readJevConfigFileForUpdate();
|
|
1198
|
+
const sameProvider = existing?.raw.provider === JEV_CLOUD_PROVIDER;
|
|
1199
|
+
const credential = readJevCloudCredential();
|
|
1200
|
+
if (credential.status === "refused") {
|
|
1201
|
+
return fail([`Not saved: ${credential.problem}.`, "", "Nothing was written."]);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const next: Record<string, unknown> = sameProvider ? { ...existing!.raw } : { provider: JEV_CLOUD_PROVIDER };
|
|
1205
|
+
// Never a key in a Cloud file, and nothing only another provider reads.
|
|
1206
|
+
delete next.apiKey;
|
|
1207
|
+
delete next.accountId;
|
|
1208
|
+
if (!sameProvider && existing?.raw.timeoutMs !== undefined) next.timeoutMs = existing.raw.timeoutMs;
|
|
1209
|
+
// Shadow unless told otherwise, as `config --token` starts it: Jev on a new
|
|
1210
|
+
// route is logged before it is allowed to clear anything. A mode already in a
|
|
1211
|
+
// Cloud file is kept.
|
|
1212
|
+
const mode = values.get("--mode") ?? (sameProvider && typeof next.mode === "string" ? next.mode : "shadow");
|
|
1213
|
+
next.mode = mode;
|
|
1214
|
+
if (values.has("--timeout-ms")) {
|
|
1215
|
+
const rawTimeout = values.get("--timeout-ms") as string;
|
|
1216
|
+
const n = Number(rawTimeout);
|
|
1217
|
+
if (!/^\d+$/.test(rawTimeout) || !Number.isSafeInteger(n)) return fail(["Could not read --timeout-ms. Give a whole number of milliseconds."]);
|
|
1218
|
+
next.timeoutMs = n;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
let keySource: JevCloudCredential | null = null;
|
|
1222
|
+
if (credential.status === "ok") {
|
|
1223
|
+
keySource = credential.credential;
|
|
1224
|
+
// The endpoint, from the connection: kept when it is already on the
|
|
1225
|
+
// credential's origin, else rebuilt — under the policy connection's base
|
|
1226
|
+
// when that is on the same origin, so a path prefix survives.
|
|
1227
|
+
const onOrigin = (u: unknown) => {
|
|
1228
|
+
try {
|
|
1229
|
+
return typeof u === "string" && new URL(u).origin === new URL(credential.credential.url).origin;
|
|
1230
|
+
} catch {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
if (!onOrigin(next.baseUrl)) {
|
|
1235
|
+
const policyBase = readCredentials().cloud?.url;
|
|
1236
|
+
next.baseUrl = jevCloudBaseUrl(onOrigin(policyBase) ? (policyBase as string) : credential.credential.url);
|
|
1237
|
+
}
|
|
1238
|
+
} else if (!sameProvider || typeof next.baseUrl !== "string") {
|
|
1239
|
+
return fail([
|
|
1240
|
+
...(credential.connected
|
|
1241
|
+
? [
|
|
1242
|
+
// Its key may well carry jev:evaluate: an older failproofai connected
|
|
1243
|
+
// without storing a Jev key, and the same key fixes that.
|
|
1244
|
+
"Not saved: this machine is connected to FailproofAI Cloud, but no Jev key is stored for that connection.",
|
|
1245
|
+
"Reconnect with this machine's key to re-check what it carries (an older failproofai connected without storing its Jev key; " +
|
|
1246
|
+
"a key without jev:evaluate needs the \"machine\" preset on the dashboard's Keys page):",
|
|
1247
|
+
]
|
|
1248
|
+
: [
|
|
1249
|
+
"Not saved: this machine is not connected to FailproofAI Cloud with a key that carries jev:evaluate.",
|
|
1250
|
+
"Connect it with one that does (the \"machine\" preset) — that also turns Jev on, in shadow mode, when there is no jev.json yet:",
|
|
1251
|
+
]),
|
|
1252
|
+
" failproofai config --token <key>",
|
|
1253
|
+
"",
|
|
1254
|
+
"Nothing was written.",
|
|
1255
|
+
]);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// The loader's own rules, with the connection's key or — for a mode switch
|
|
1259
|
+
// on a machine that is not connected right now — a stand-in on the file's own
|
|
1260
|
+
// origin, which is never written or sent.
|
|
1261
|
+
let standInOrigin = "";
|
|
1262
|
+
try {
|
|
1263
|
+
standInOrigin = new URL(String(next.baseUrl)).origin;
|
|
1264
|
+
} catch {
|
|
1265
|
+
standInOrigin = "";
|
|
1266
|
+
}
|
|
1267
|
+
const checked = validateJevConfig(next, null, keySource ?? { url: standInOrigin, key: ENV_KEY_STAND_IN });
|
|
1268
|
+
if (!checked.ok) return fail([`Not saved: ${checked.problem}.`, "", "Nothing was written."]);
|
|
1269
|
+
|
|
1270
|
+
const path = jevConfigPath();
|
|
1271
|
+
try {
|
|
1272
|
+
writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
return fail([`Could not write ${path}: ${(err as NodeJS.ErrnoException).code ?? "error"}.`]);
|
|
1275
|
+
}
|
|
1276
|
+
if (process.platform !== "win32") {
|
|
1277
|
+
try {
|
|
1278
|
+
const dir = dirname(path);
|
|
1279
|
+
const before = statSync(dir).mode & 0o777;
|
|
1280
|
+
if ((before & 0o022) !== 0) chmodSync(dir, before & ~0o022);
|
|
1281
|
+
} catch {
|
|
1282
|
+
// Not fatal: the loader reports a directory it will not read from.
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
let fileMode: number | null = null;
|
|
1286
|
+
try {
|
|
1287
|
+
fileMode = statSync(path).mode & 0o777;
|
|
1288
|
+
} catch {
|
|
1289
|
+
// Reported as unknown below.
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const cfg = checked.value;
|
|
1293
|
+
const route = jevRoute(cfg);
|
|
1294
|
+
const shownMode = cfg.mode ?? DEFAULT_JEV_MODE;
|
|
1295
|
+
return ok(
|
|
1296
|
+
stack(
|
|
1297
|
+
title("failproofai jev setup", `saved · FailproofAI Cloud · ${shownMode}`, opts),
|
|
1298
|
+
rows(
|
|
1299
|
+
[
|
|
1300
|
+
["provider", providerLabel(JEV_CLOUD_PROVIDER)],
|
|
1301
|
+
["endpoint", shownEndpoint(JEV_CLOUD_PROVIDER, route.endpoint)],
|
|
1302
|
+
["model", `${route.model} (pinned by FailproofAI Cloud)`],
|
|
1303
|
+
["mode", modeLine(shownMode)],
|
|
1304
|
+
["timeout", `${cfg.timeoutMs} ms`],
|
|
1305
|
+
// Which of the three states the connection is in — the same row
|
|
1306
|
+
// `status` shows. "Not connected" on a machine that IS connected, with
|
|
1307
|
+
// a key that lacks Jev, sent its owner to reconnect with that same key.
|
|
1308
|
+
["key", keySource ? CLOUD_KEY_SOURCE : cloudKeyRow()],
|
|
1309
|
+
["config", path],
|
|
1310
|
+
["permissions", permissions(fileMode)],
|
|
1311
|
+
],
|
|
1312
|
+
opts,
|
|
1313
|
+
),
|
|
1314
|
+
note("Hooks read this file on every tool call — no restart. Calls are charged to your FailproofAI Cloud org's plan.", opts),
|
|
1315
|
+
// With no usable key, `jev test` only answers "not run": the step that
|
|
1316
|
+
// helps is the connection. Switched off, there is no step to take.
|
|
1317
|
+
shownMode === "off"
|
|
1318
|
+
? null
|
|
1319
|
+
: keySource
|
|
1320
|
+
? nextStep("failproofai jev test", "Check it with one live request:", opts)
|
|
1321
|
+
: nextStep(
|
|
1322
|
+
"failproofai config --token <key>",
|
|
1323
|
+
"Jev stays off until this machine is connected with a key that carries jev:evaluate (the \"machine\" preset on the dashboard's Keys page):",
|
|
1324
|
+
opts,
|
|
1325
|
+
),
|
|
807
1326
|
),
|
|
808
1327
|
);
|
|
809
1328
|
}
|
|
@@ -824,15 +1343,30 @@ async function setup(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promis
|
|
|
824
1343
|
*/
|
|
825
1344
|
function routeForRouting(routing: Omit<JevConfig, "apiKey">): ReturnType<typeof jevRoute> | null {
|
|
826
1345
|
try {
|
|
827
|
-
return jevRoute({ ...routing, apiKey: ENV_KEY_STAND_IN });
|
|
1346
|
+
return jevRoute({ ...routing, apiKey: ENV_KEY_STAND_IN, ...displayOnlyCloudOrigin(routing) });
|
|
828
1347
|
} catch {
|
|
829
1348
|
return null;
|
|
830
1349
|
}
|
|
831
1350
|
}
|
|
832
1351
|
|
|
1352
|
+
/**
|
|
1353
|
+
* For DISPLAY only, like the stand-in key beside it: a FailproofAI Cloud route
|
|
1354
|
+
* is built only against the origin of the credential it was validated with
|
|
1355
|
+
* (`validateLoadedJevConfig`), and a config shown here has no credential behind
|
|
1356
|
+
* it, so the file's own origin stands in. Nothing built with it is ever sent.
|
|
1357
|
+
*/
|
|
1358
|
+
function displayOnlyCloudOrigin(routing: { provider?: unknown; baseUrl?: unknown }): { credentialOrigin?: string } {
|
|
1359
|
+
if (routing.provider !== JEV_CLOUD_PROVIDER || typeof routing.baseUrl !== "string") return {};
|
|
1360
|
+
try {
|
|
1361
|
+
return { credentialOrigin: new URL(routing.baseUrl).origin };
|
|
1362
|
+
} catch {
|
|
1363
|
+
return {};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
833
1367
|
function namedEndpoint(raw: Record<string, unknown> | null): string | null {
|
|
834
1368
|
if (!raw || typeof raw.provider !== "string" || !(JEV_PROVIDER_KINDS as readonly string[]).includes(raw.provider)) return null;
|
|
835
|
-
const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "shadow" };
|
|
1369
|
+
const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "shadow", ...displayOnlyCloudOrigin(raw) };
|
|
836
1370
|
if (raw.baseUrl !== undefined) routing.baseUrl = raw.baseUrl;
|
|
837
1371
|
if (raw.accountId !== undefined) routing.accountId = raw.accountId;
|
|
838
1372
|
try {
|
|
@@ -879,8 +1413,23 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
879
1413
|
|
|
880
1414
|
if (asJson) {
|
|
881
1415
|
const base: Record<string, unknown> = { path: inspection.path, status: inspection.status, legacyOverride: legacy, stats };
|
|
1416
|
+
// The absent text says whether this machine's key already carries Jev.
|
|
1417
|
+
if (inspection.status === "absent") Object.assign(base, cloudConnection());
|
|
882
1418
|
if (inspection.status === "refused") {
|
|
883
1419
|
Object.assign(base, { reason: inspection.reason, problem: inspection.problem, permissions: octal(inspection.mode) });
|
|
1420
|
+
if (inspection.fix) base.fix = inspection.fix;
|
|
1421
|
+
// `permissions` is jev.json's; a refusal about credentials.json says its own.
|
|
1422
|
+
if (inspection.credentialsMode !== undefined) base.credentialsPermissions = octal(inspection.credentialsMode);
|
|
1423
|
+
// Known whatever the file says, or whether it parses at all.
|
|
1424
|
+
Object.assign(base, cloudConnection());
|
|
1425
|
+
if (readJevConfigFileForUpdate()?.raw?.provider === JEV_CLOUD_PROVIDER) {
|
|
1426
|
+
Object.assign(base, {
|
|
1427
|
+
provider: JEV_CLOUD_PROVIDER,
|
|
1428
|
+
providerLabel: providerLabel(JEV_CLOUD_PROVIDER),
|
|
1429
|
+
keySource: "cloud",
|
|
1430
|
+
keySourceLabel: CLOUD_KEY_SOURCE,
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
884
1433
|
if (inspection.reason === "too-open") {
|
|
885
1434
|
const named = namedEndpoint(readJevConfigFileForUpdate()?.raw ?? null);
|
|
886
1435
|
if (named) base.endpoint = named;
|
|
@@ -894,6 +1443,7 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
894
1443
|
Object.assign(base, {
|
|
895
1444
|
permissions: octal(inspection.mode),
|
|
896
1445
|
provider: inspection.routing.provider,
|
|
1446
|
+
providerLabel: providerLabel(inspection.routing.provider),
|
|
897
1447
|
...(route ? { endpoint: displayEndpoint(route.endpoint), model: route.model, modelIsDefault: route.modelIsDefault } : {}),
|
|
898
1448
|
mode: inspection.routing.mode,
|
|
899
1449
|
timeoutMs: inspection.routing.timeoutMs,
|
|
@@ -903,18 +1453,42 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
903
1453
|
problem: inspection.problem,
|
|
904
1454
|
});
|
|
905
1455
|
}
|
|
1456
|
+
if (inspection.status === "off" || inspection.status === "not-connected" || inspection.status === "key-lacks-jev") {
|
|
1457
|
+
// Configured and deliberately (off) or circumstantially (not connected,
|
|
1458
|
+
// or connected with a key that has no Jev) not running. The routing is
|
|
1459
|
+
// reported exactly as for `ok`, so a provisioning check can tell any of
|
|
1460
|
+
// them from a file it should rewrite.
|
|
1461
|
+
const r = inspection.routing;
|
|
1462
|
+
const route = routeForRouting(r);
|
|
1463
|
+
const cloud = r.provider === JEV_CLOUD_PROVIDER;
|
|
1464
|
+
Object.assign(base, {
|
|
1465
|
+
permissions: octal(inspection.mode),
|
|
1466
|
+
provider: r.provider,
|
|
1467
|
+
providerLabel: providerLabel(r.provider),
|
|
1468
|
+
...(route ? { endpoint: shownEndpoint(r.provider, route.endpoint), model: route.model, modelIsDefault: route.modelIsDefault } : {}),
|
|
1469
|
+
mode: r.mode,
|
|
1470
|
+
timeoutMs: r.timeoutMs,
|
|
1471
|
+
// Read from the credentials file rather than inferred from the status:
|
|
1472
|
+
// `off` is decided before any key is looked for, so it says nothing
|
|
1473
|
+
// about the connection either way.
|
|
1474
|
+
...(cloud ? { keySource: "cloud", keySourceLabel: CLOUD_KEY_SOURCE, ...cloudConnection() } : {}),
|
|
1475
|
+
...(inspection.status === "off" ? { reason: "switched-off" } : { reason: inspection.status, problem: inspection.problem }),
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
906
1478
|
if (inspection.status === "ok") {
|
|
907
1479
|
const { config: cfg } = inspection;
|
|
908
1480
|
const route = jevRoute(cfg);
|
|
909
1481
|
Object.assign(base, {
|
|
910
1482
|
permissions: octal(inspection.mode),
|
|
911
1483
|
provider: cfg.provider,
|
|
912
|
-
|
|
1484
|
+
providerLabel: providerLabel(cfg.provider),
|
|
1485
|
+
endpoint: shownEndpoint(cfg.provider, route.endpoint),
|
|
913
1486
|
model: route.model,
|
|
914
1487
|
modelIsDefault: route.modelIsDefault,
|
|
915
1488
|
mode: cfg.mode,
|
|
916
1489
|
timeoutMs: cfg.timeoutMs,
|
|
917
1490
|
keySource: inspection.keySource,
|
|
1491
|
+
...(inspection.keySource === "cloud" ? { keySourceLabel: CLOUD_KEY_SOURCE, cloudConnected: true, keyCarriesJev: true } : {}),
|
|
918
1492
|
// How much of this machine's policy set Jev is allowed to clear, and
|
|
919
1493
|
// why it is none when it is none. A provisioning check that turns Jev
|
|
920
1494
|
// on has no other way to find out that the half it turned on cannot
|
|
@@ -957,6 +1531,12 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
957
1531
|
},
|
|
958
1532
|
opts,
|
|
959
1533
|
),
|
|
1534
|
+
note(
|
|
1535
|
+
cloudCredentialPresent()
|
|
1536
|
+
? "Or through FailproofAI Cloud, on your org's plan — this machine's key already carries jev:evaluate: failproofai jev setup --provider failproofai"
|
|
1537
|
+
: "Or through FailproofAI Cloud, on your org's plan, with no key of your own: connect with a key that carries jev:evaluate (the \"machine\" preset) — failproofai config --token <key>",
|
|
1538
|
+
opts,
|
|
1539
|
+
),
|
|
960
1540
|
jevStatsLines(stats, opts),
|
|
961
1541
|
),
|
|
962
1542
|
);
|
|
@@ -999,24 +1579,116 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
999
1579
|
);
|
|
1000
1580
|
}
|
|
1001
1581
|
|
|
1582
|
+
if (inspection.status === "off") {
|
|
1583
|
+
// Switched off, not broken and not absent: the file is kept so switching
|
|
1584
|
+
// back on needs no key and no endpoint re-typed. Said as "off" first,
|
|
1585
|
+
// because that is the whole answer to "is Jev running?".
|
|
1586
|
+
const r = inspection.routing;
|
|
1587
|
+
const route = routeForRouting(r);
|
|
1588
|
+
return ok(
|
|
1589
|
+
stack(
|
|
1590
|
+
title("failproofai jev status", "off (switched off)", opts),
|
|
1591
|
+
note(`Jev is switched off in ${inspection.path}: it is not asked about any tool call, and hooks run the regex policies exactly as before.`, opts),
|
|
1592
|
+
rows(
|
|
1593
|
+
[
|
|
1594
|
+
["provider", providerLabel(r.provider)],
|
|
1595
|
+
...((route ? [["endpoint", shownEndpoint(r.provider, route.endpoint)]] : []) as Array<[string, string]>),
|
|
1596
|
+
["mode", modeLine("off")],
|
|
1597
|
+
["config", inspection.path],
|
|
1598
|
+
...((r.provider === JEV_CLOUD_PROVIDER ? [["key", cloudKeyRow()]] : []) as Array<[string, string]>),
|
|
1599
|
+
],
|
|
1600
|
+
opts,
|
|
1601
|
+
),
|
|
1602
|
+
nextStep("failproofai jev setup --mode shadow", "Switch it back on (shadow logs Jev and keeps enforcing regex; enforce lets it clear), here or from the dashboard:", opts),
|
|
1603
|
+
legacyNote,
|
|
1604
|
+
jevStatsLines(stats, opts),
|
|
1605
|
+
),
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (inspection.status === "not-connected") {
|
|
1610
|
+
// The Cloud route's `key-missing`: nothing is wrong with the file, and the
|
|
1611
|
+
// remedy is the connection, not a rewrite.
|
|
1612
|
+
const r = inspection.routing;
|
|
1613
|
+
const route = routeForRouting(r);
|
|
1614
|
+
return ok(
|
|
1615
|
+
stack(
|
|
1616
|
+
title("failproofai jev status", "off — this machine is not connected to FailproofAI Cloud", opts),
|
|
1617
|
+
note(
|
|
1618
|
+
`Jev is off: ${inspection.path} sends Jev requests through FailproofAI Cloud, and this machine holds no FailproofAI Cloud key that carries jev:evaluate. ` +
|
|
1619
|
+
"Hooks run the regex policies exactly as before.",
|
|
1620
|
+
opts,
|
|
1621
|
+
),
|
|
1622
|
+
rows(
|
|
1623
|
+
[
|
|
1624
|
+
["provider", providerLabel(r.provider)],
|
|
1625
|
+
...((route ? [["endpoint", shownEndpoint(r.provider, route.endpoint)]] : []) as Array<[string, string]>),
|
|
1626
|
+
["mode", modeLine(r.mode ?? DEFAULT_JEV_MODE)],
|
|
1627
|
+
["config", inspection.path],
|
|
1628
|
+
["key", `${CLOUD_KEY_SOURCE} — not connected`],
|
|
1629
|
+
],
|
|
1630
|
+
opts,
|
|
1631
|
+
),
|
|
1632
|
+
nextStep(
|
|
1633
|
+
"failproofai config --token <key>",
|
|
1634
|
+
"Connect with a key that carries jev:evaluate (the \"machine\" preset on the dashboard's Keys page):",
|
|
1635
|
+
opts,
|
|
1636
|
+
),
|
|
1637
|
+
nextStep("failproofai jev remove", "Or switch Jev off:", opts),
|
|
1638
|
+
legacyNote,
|
|
1639
|
+
jevStatsLines(stats, opts),
|
|
1640
|
+
),
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (inspection.status === "key-lacks-jev") {
|
|
1645
|
+
// Connected, so "not connected" would be false and send its owner to
|
|
1646
|
+
// reconnect with the same key. The remedy is a key WITH Jev.
|
|
1647
|
+
const r = inspection.routing;
|
|
1648
|
+
const route = routeForRouting(r);
|
|
1649
|
+
return ok(
|
|
1650
|
+
stack(
|
|
1651
|
+
title("failproofai jev status", KEY_LACKS_JEV_TITLE, opts),
|
|
1652
|
+
note(
|
|
1653
|
+
`Jev is off: ${inspection.path} sends Jev requests through FailproofAI Cloud, and no Jev key is stored for this machine's connection: its key lacks jev:evaluate, or the last connect could not confirm that. ` +
|
|
1654
|
+
"Hooks run the regex policies exactly as before.",
|
|
1655
|
+
opts,
|
|
1656
|
+
),
|
|
1657
|
+
rows(
|
|
1658
|
+
[
|
|
1659
|
+
["provider", providerLabel(r.provider)],
|
|
1660
|
+
...((route ? [["endpoint", shownEndpoint(r.provider, route.endpoint)]] : []) as Array<[string, string]>),
|
|
1661
|
+
["mode", modeLine(r.mode ?? DEFAULT_JEV_MODE)],
|
|
1662
|
+
["config", inspection.path],
|
|
1663
|
+
["key", `${CLOUD_KEY_SOURCE} — connected, no Jev key stored for it`],
|
|
1664
|
+
],
|
|
1665
|
+
opts,
|
|
1666
|
+
),
|
|
1667
|
+
nextStep(
|
|
1668
|
+
"failproofai config --token <key>",
|
|
1669
|
+
"Reconnect with this machine's key to re-check what it carries (a key without jev:evaluate needs the \"machine\" preset on the dashboard's Keys page):",
|
|
1670
|
+
opts,
|
|
1671
|
+
),
|
|
1672
|
+
nextStep("failproofai jev setup --mode off", "Or keep Jev off for good:", opts),
|
|
1673
|
+
legacyNote,
|
|
1674
|
+
jevStatsLines(stats, opts),
|
|
1675
|
+
),
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1002
1679
|
if (inspection.status === "refused") {
|
|
1003
1680
|
// A file other users could change may name an endpoint its owner never
|
|
1004
1681
|
// chose, and `chmod 600` would start trusting it with the key — so say
|
|
1005
1682
|
// where it points before suggesting that.
|
|
1006
|
-
const
|
|
1683
|
+
const raw = readJevConfigFileForUpdate()?.raw ?? null;
|
|
1684
|
+
const named = inspection.reason === "too-open" ? namedEndpoint(raw) : null;
|
|
1685
|
+
const next = refusedNextStep(inspection, cloudRoute(raw));
|
|
1007
1686
|
return fail(
|
|
1008
1687
|
stack(
|
|
1009
1688
|
title("failproofai jev status", "off (config refused)", opts),
|
|
1010
1689
|
warning([`Jev is off: ${inspection.path} was refused — ${inspection.problem}.`, "Hooks run the regex policies exactly as before."], opts),
|
|
1011
1690
|
named ? rows([["endpoint it names", named]], opts) : null,
|
|
1012
|
-
|
|
1013
|
-
? nextStep(
|
|
1014
|
-
// Either the file or the directory it sits in; `fix` says which.
|
|
1015
|
-
inspection.fix ?? `chmod 600 ${inspection.path}`,
|
|
1016
|
-
"Other users could change this file, so check that endpoint is one you chose. Then make it owner-only (or re-run `failproofai jev setup`, which asks for the key again unless the endpoint is the provider's own):",
|
|
1017
|
-
opts,
|
|
1018
|
-
)
|
|
1019
|
-
: nextStep("failproofai jev setup --provider <kind> --key-stdin", "Write a valid one:", opts),
|
|
1691
|
+
nextStep(next.cmd, next.lead, opts),
|
|
1020
1692
|
legacyNote,
|
|
1021
1693
|
jevStatsLines(stats, opts),
|
|
1022
1694
|
),
|
|
@@ -1031,14 +1703,21 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
|
1031
1703
|
title("failproofai jev status", legacy ? "on (legacy override in this shell)" : `on · ${mode}`, opts),
|
|
1032
1704
|
rows(
|
|
1033
1705
|
[
|
|
1034
|
-
["provider", cfg.provider],
|
|
1035
|
-
["endpoint",
|
|
1706
|
+
["provider", providerLabel(cfg.provider)],
|
|
1707
|
+
["endpoint", shownEndpoint(cfg.provider, route.endpoint)],
|
|
1036
1708
|
["model", route.modelIsDefault ? `${route.model} (provider default)` : route.model],
|
|
1037
1709
|
["mode", modeLine(mode)],
|
|
1038
1710
|
["timeout", `${cfg.timeoutMs} ms`],
|
|
1039
1711
|
["config", inspection.path],
|
|
1040
1712
|
["permissions", permissions(inspection.mode)],
|
|
1041
|
-
[
|
|
1713
|
+
[
|
|
1714
|
+
"key",
|
|
1715
|
+
inspection.keySource === "cloud"
|
|
1716
|
+
? CLOUD_KEY_SOURCE
|
|
1717
|
+
: inspection.keySource === "file"
|
|
1718
|
+
? "set in the config file"
|
|
1719
|
+
: `from ${JEV_API_KEY_ENV} (this shell only; the daemon does not see it)`,
|
|
1720
|
+
],
|
|
1042
1721
|
],
|
|
1043
1722
|
opts,
|
|
1044
1723
|
),
|
|
@@ -1079,19 +1758,51 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1079
1758
|
? `There is no ${inspection.path}; nothing to test.`
|
|
1080
1759
|
: inspection.status === "key-missing"
|
|
1081
1760
|
? `${inspection.path} takes its key from ${JEV_API_KEY_ENV}, which is not set in this shell, so there is no key to test with.`
|
|
1082
|
-
:
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1761
|
+
: inspection.status === "off"
|
|
1762
|
+
? `Jev is switched off in ${inspection.path} (mode off), so nothing is sent.`
|
|
1763
|
+
: inspection.status === "not-connected"
|
|
1764
|
+
? `${inspection.path} sends Jev requests through FailproofAI Cloud, and this machine is not connected to it with a key that carries jev:evaluate.`
|
|
1765
|
+
: inspection.status === "key-lacks-jev"
|
|
1766
|
+
? `${inspection.path} sends Jev requests through FailproofAI Cloud, and no Jev key is stored for this machine's connection.`
|
|
1767
|
+
: `${inspection.path} was refused — ${inspection.problem}.`;
|
|
1768
|
+
const code =
|
|
1769
|
+
inspection.status === "absent"
|
|
1770
|
+
? "not-configured"
|
|
1771
|
+
: inspection.status === "key-missing"
|
|
1772
|
+
? "no-env-key"
|
|
1773
|
+
: inspection.status === "off"
|
|
1774
|
+
? "switched-off"
|
|
1775
|
+
: inspection.status === "not-connected" || inspection.status === "key-lacks-jev"
|
|
1776
|
+
? inspection.status
|
|
1777
|
+
: "config";
|
|
1778
|
+
const refusedNext =
|
|
1779
|
+
inspection.status === "refused"
|
|
1780
|
+
? refusedNextStep(inspection, cloudRoute(readJevConfigFileForUpdate()?.raw))
|
|
1781
|
+
: inspection.status === "absent" && cloudCredentialPresent()
|
|
1782
|
+
? { cmd: "failproofai jev setup --provider failproofai", lead: "This machine's FailproofAI Cloud key already carries jev:evaluate. Turn it on:" }
|
|
1783
|
+
: null;
|
|
1784
|
+
const fixCmd = refusedNext
|
|
1785
|
+
? refusedNext.cmd
|
|
1786
|
+
: inspection.status === "key-missing"
|
|
1787
|
+
? "failproofai jev setup --key-stdin < key-file"
|
|
1788
|
+
: inspection.status === "off"
|
|
1789
|
+
? "failproofai jev setup --mode shadow"
|
|
1790
|
+
: inspection.status === "not-connected" || inspection.status === "key-lacks-jev"
|
|
1791
|
+
? "failproofai config --token <key>"
|
|
1792
|
+
: "failproofai jev setup --provider <kind> --key-stdin";
|
|
1793
|
+
const fixLead = refusedNext
|
|
1794
|
+
? refusedNext.lead
|
|
1795
|
+
: inspection.status === "key-missing"
|
|
1796
|
+
? `Set ${JEV_API_KEY_ENV} for this shell, or store the key in the file:`
|
|
1797
|
+
: inspection.status === "off"
|
|
1798
|
+
? "Switch it back on:"
|
|
1799
|
+
: inspection.status === "not-connected"
|
|
1800
|
+
? "Connect with a key that carries jev:evaluate:"
|
|
1801
|
+
: inspection.status === "key-lacks-jev"
|
|
1802
|
+
? "Reconnect with this machine's key to re-check what it carries (a key without jev:evaluate needs the \"machine\" preset on the dashboard's Keys page):"
|
|
1803
|
+
: undefined;
|
|
1086
1804
|
const json = asJson ? JSON.stringify({ ok: false, error: { code, message: why } }, null, 2) : undefined;
|
|
1087
|
-
return fail(
|
|
1088
|
-
stack(
|
|
1089
|
-
title("failproofai jev test", "not run", opts),
|
|
1090
|
-
note(why, opts),
|
|
1091
|
-
nextStep(fixCmd, inspection.status === "key-missing" ? `Set ${JEV_API_KEY_ENV} for this shell, or store the key in the file:` : undefined, opts),
|
|
1092
|
-
),
|
|
1093
|
-
json,
|
|
1094
|
-
);
|
|
1805
|
+
return fail(stack(title("failproofai jev test", "not run", opts), note(why, opts), nextStep(fixCmd, fixLead, opts)), json);
|
|
1095
1806
|
}
|
|
1096
1807
|
|
|
1097
1808
|
const cfg = inspection.config;
|
|
@@ -1119,12 +1830,16 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1119
1830
|
const within = latencyMs <= budget;
|
|
1120
1831
|
const p = answers[JEV_TEST_QUESTION_ID];
|
|
1121
1832
|
const inputTokens = typeof response.usage?.input_tokens === "number" ? response.usage.input_tokens : null;
|
|
1833
|
+
// "ok" means a hook would use this answer: in time, and right about "blue".
|
|
1834
|
+
const problem = !within ? "over-timeout" : p < 0.5 ? "unexpected-answer" : null;
|
|
1835
|
+
const done = problem ? fail : ok;
|
|
1122
1836
|
if (asJson) {
|
|
1123
|
-
return
|
|
1837
|
+
return done(
|
|
1124
1838
|
[],
|
|
1125
1839
|
JSON.stringify(
|
|
1126
1840
|
{
|
|
1127
|
-
ok:
|
|
1841
|
+
ok: problem === null,
|
|
1842
|
+
...(problem ? { problem } : {}),
|
|
1128
1843
|
provider: cfg.provider,
|
|
1129
1844
|
endpoint: displayEndpoint(route.endpoint),
|
|
1130
1845
|
model: built.model,
|
|
@@ -1141,9 +1856,11 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1141
1856
|
),
|
|
1142
1857
|
);
|
|
1143
1858
|
}
|
|
1144
|
-
|
|
1859
|
+
const status =
|
|
1860
|
+
problem === "over-timeout" ? `over timeout · ${latencyMs} ms` : problem ? `wrong answer · p = ${p.toFixed(3)}` : `ok · ${latencyMs} ms`;
|
|
1861
|
+
return done(
|
|
1145
1862
|
stack(
|
|
1146
|
-
title("failproofai jev test",
|
|
1863
|
+
title("failproofai jev test", status, opts),
|
|
1147
1864
|
rows(
|
|
1148
1865
|
[
|
|
1149
1866
|
["provider", cfg.provider],
|
|
@@ -1163,6 +1880,9 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1163
1880
|
"One request, sent directly: the hook path's cache and rate limit were not involved, and a fresh process pays DNS and TLS setup that the daemon's warm worker does not.",
|
|
1164
1881
|
opts,
|
|
1165
1882
|
),
|
|
1883
|
+
problem === "over-timeout"
|
|
1884
|
+
? nextStep("failproofai jev setup --timeout-ms <n>", `Give hooks longer (up to ${MAX_JEV_TIMEOUT_MS} ms), or use a faster route:`, opts)
|
|
1885
|
+
: null,
|
|
1166
1886
|
),
|
|
1167
1887
|
);
|
|
1168
1888
|
} catch (err) {
|
|
@@ -1170,7 +1890,10 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1170
1890
|
// jev-client scrubs provider text already; this is the last line of that
|
|
1171
1891
|
// defence, for any error text that reached here some other way.
|
|
1172
1892
|
const e = scrubbed(describeError(err), cfg.apiKey);
|
|
1173
|
-
|
|
1893
|
+
// An answer after the hook budget is a `timeout` to every hook, whatever it
|
|
1894
|
+
// finally said (a Cloud route maps its own upstream timeout to 502).
|
|
1895
|
+
const late = latencyMs > budget;
|
|
1896
|
+
if (asJson) return fail([], JSON.stringify({ ok: false, provider: cfg.provider, latencyMs, timeoutMs: budget, withinTimeout: !late, error: e }, null, 2));
|
|
1174
1897
|
return fail(
|
|
1175
1898
|
stack(
|
|
1176
1899
|
title("failproofai jev test", `failed · ${e.code}`, opts),
|
|
@@ -1180,14 +1903,212 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
|
|
|
1180
1903
|
["endpoint", displayEndpoint(route.endpoint)],
|
|
1181
1904
|
["model asked", built.model],
|
|
1182
1905
|
["error", `${e.code}: ${e.message}`],
|
|
1183
|
-
["after", `${latencyMs} ms`],
|
|
1906
|
+
["after", late ? `${latencyMs} ms — OVER the ${budget} ms timeout: hooks would fall back to regex (timeout)` : `${latencyMs} ms`],
|
|
1907
|
+
],
|
|
1908
|
+
opts,
|
|
1909
|
+
),
|
|
1910
|
+
note(
|
|
1911
|
+
late
|
|
1912
|
+
? `Hooks stop waiting after ${budget} ms, so they record this as \`timeout\`, not ${e.code}: the provider or FailproofAI Cloud is slow or failing upstream.`
|
|
1913
|
+
: remedy(e.code, cfg.provider, e.message),
|
|
1914
|
+
opts,
|
|
1915
|
+
),
|
|
1916
|
+
),
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// ── models ───────────────────────────────────────────────────────────────────
|
|
1922
|
+
|
|
1923
|
+
/** What a shape means, which decides what the names in it can be used for. */
|
|
1924
|
+
function shapeMeans(shape: JevModelListRead["shape"]): string {
|
|
1925
|
+
return shape === "typesafe"
|
|
1926
|
+
? "TypeSafe's own inventory — the aliases <base>/systemone answers to; versioned ids such as jev-1.13.0 are accepted without being listed"
|
|
1927
|
+
: "an OpenAI-shaped gateway catalog — it lists what the gateway serves, not what <base>/systemone answers to, so it is shown and never used to refuse a model";
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* `failproofai jev models` — what `GET <base>/models` says an endpoint serves.
|
|
1932
|
+
*
|
|
1933
|
+
* The question a person asks after `jev test` fails, and the one the LiteLLM-proxy
|
|
1934
|
+
* customer answered by curling the endpoint by hand. With no flags it asks the
|
|
1935
|
+
* configured route; `--provider` asks a provider's own API and `--url` any base.
|
|
1936
|
+
*
|
|
1937
|
+
* The stored key is sent only to the origin it was stored for. `--url` takes a URL
|
|
1938
|
+
* off the command line, and a key issued for one gateway must not go to another
|
|
1939
|
+
* because a diagnostic named it — the same rule `setup` applies when a new
|
|
1940
|
+
* `--base-url` moves the host. Vercel's and OpenRouter's lists are public, so an
|
|
1941
|
+
* unkeyed read is often still an answer, and the output says which it was.
|
|
1942
|
+
*/
|
|
1943
|
+
async function models(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise<JevCliResult> {
|
|
1944
|
+
// The same test the launcher uses to print `json` instead of the lines, so a
|
|
1945
|
+
// refusal made before the flags parse is JSON as well: every `--json` outcome
|
|
1946
|
+
// is an object, as `jev test --json`'s are.
|
|
1947
|
+
const asJson = argv.includes("--json");
|
|
1948
|
+
const refuse = (code: string, lines: string[]) =>
|
|
1949
|
+
fail(lines, asJson ? JSON.stringify({ ok: false, error: { code, message: lines.join("\n") } }, null, 2) : undefined);
|
|
1950
|
+
const parsed = parseFlags(argv, new Set(["--json", "--provider", "--url"]));
|
|
1951
|
+
if (typeof parsed === "string") return refuse("usage", [parsed, "", ...JEV_USAGE]);
|
|
1952
|
+
if (parsed.positionals.length > 0) return refuse("usage", [STRAY_ARGUMENT, "", ...JEV_USAGE]);
|
|
1953
|
+
const { values } = parsed;
|
|
1954
|
+
|
|
1955
|
+
// Checked before anything else uses it, and never echoed — the same rule
|
|
1956
|
+
// `setup` follows, for the same reason: the likeliest wrong value is a key.
|
|
1957
|
+
const named = values.get("--provider");
|
|
1958
|
+
if (named !== undefined && !(JEV_PROVIDER_KINDS as readonly string[]).includes(named)) {
|
|
1959
|
+
return refuse("unknown-provider", [
|
|
1960
|
+
"Unknown provider (not repeated here, in case it is a key).",
|
|
1961
|
+
`Providers: ${JEV_PROVIDER_KINDS.join(", ")} — exactly as spelled here, lower-case.`,
|
|
1962
|
+
]);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// The configured route, for the default base, for the key, and for the model to
|
|
1966
|
+
// mark. `key-missing` counts: the file's routing is sound, it just has no key
|
|
1967
|
+
// here, and a public list answers anyway.
|
|
1968
|
+
const inspection = inspectJevConfig();
|
|
1969
|
+
const routing: Omit<JevConfig, "apiKey"> | null =
|
|
1970
|
+
inspection.status === "ok"
|
|
1971
|
+
? inspection.config
|
|
1972
|
+
: inspection.status === "key-missing" ||
|
|
1973
|
+
inspection.status === "off" ||
|
|
1974
|
+
inspection.status === "not-connected" ||
|
|
1975
|
+
inspection.status === "key-lacks-jev"
|
|
1976
|
+
? inspection.routing
|
|
1977
|
+
: null;
|
|
1978
|
+
const storedKey = inspection.status === "ok" ? inspection.config.apiKey : null;
|
|
1979
|
+
|
|
1980
|
+
let provider: JevProviderKind;
|
|
1981
|
+
let base: string | null;
|
|
1982
|
+
const urlArg = values.get("--url");
|
|
1983
|
+
if (urlArg !== undefined) {
|
|
1984
|
+
const checked = validateBaseUrl(urlArg);
|
|
1985
|
+
if (!checked.ok) return refuse("bad-url", [`Cannot read a model list: ${checked.problem}.`]);
|
|
1986
|
+
const asEndpoint = endpointGivenAsBase(checked.value);
|
|
1987
|
+
if (asEndpoint) return refuse("endpoint-as-base", endpointAsBaseRefusal("--url", checked.value, asEndpoint));
|
|
1988
|
+
base = checked.value;
|
|
1989
|
+
provider = (named as JevProviderKind | undefined) ?? providerForUrl(base);
|
|
1990
|
+
} else if (named !== undefined) {
|
|
1991
|
+
provider = named as JevProviderKind;
|
|
1992
|
+
base = JEV_PROVIDER_DEFAULTS[provider].baseUrl;
|
|
1993
|
+
if (base === null && provider === JEV_CLOUD_PROVIDER) {
|
|
1994
|
+
return refuse("no-model-list", [
|
|
1995
|
+
`FailproofAI Cloud serves no model list: it runs ${JEV_PROVIDER_DEFAULTS.failproofai.model}, pinned on the server, and every answer is checked against the Jev 1.13 family.`,
|
|
1996
|
+
]);
|
|
1997
|
+
}
|
|
1998
|
+
if (base === null) {
|
|
1999
|
+
return refuse("no-api", ["Provider custom has no API of its own — its URL is the whole address.", " failproofai jev models --url <base>"]);
|
|
2000
|
+
}
|
|
2001
|
+
} else if (routing !== null) {
|
|
2002
|
+
provider = routing.provider;
|
|
2003
|
+
base = routing.baseUrl ?? JEV_PROVIDER_DEFAULTS[routing.provider].baseUrl;
|
|
2004
|
+
} else {
|
|
2005
|
+
return refuse("not-configured", [
|
|
2006
|
+
`Jev is not configured here (no ${inspection.path}), so there is no endpoint to ask.`,
|
|
2007
|
+
"Name one:",
|
|
2008
|
+
" failproofai jev models --provider vercel",
|
|
2009
|
+
" failproofai jev models --url https://your-proxy.example.com/typesafe/v1",
|
|
2010
|
+
]);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Cloudflare is the one route with no `<base>/models`: Workers AI runs models at
|
|
2014
|
+
// `/accounts/<id>/ai/run` and keeps its inventory behind a different API, so
|
|
2015
|
+
// there is nothing here to read rather than something that failed to read.
|
|
2016
|
+
// FailproofAI Cloud serves no list either: it pins the model server-side
|
|
2017
|
+
// (`jev-1.13.0`), so there is nothing to choose and nothing to read.
|
|
2018
|
+
const modelsUrl = provider === "cloudflare" || provider === JEV_CLOUD_PROVIDER || base === null ? null : modelsUrlForBase(base);
|
|
2019
|
+
if (modelsUrl === null) {
|
|
2020
|
+
return refuse("no-model-list", [
|
|
2021
|
+
provider === "cloudflare"
|
|
2022
|
+
? "Cloudflare Workers AI serves no <base>/models: its models are listed through the Cloudflare API, not through the Jev base URL."
|
|
2023
|
+
: provider === JEV_CLOUD_PROVIDER
|
|
2024
|
+
? `FailproofAI Cloud serves no model list: it runs ${JEV_PROVIDER_DEFAULTS.failproofai.model}, pinned on the server, and every answer is checked against the Jev 1.13 family.`
|
|
2025
|
+
: "That base URL names no endpoint a model list could be read from.",
|
|
2026
|
+
]);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const askOrigin = originOf(provider, base);
|
|
2030
|
+
const configuredOrigin = routing !== null ? originOf(routing.provider, routing.baseUrl) : null;
|
|
2031
|
+
const sameOrigin = askOrigin !== null && askOrigin === configuredOrigin;
|
|
2032
|
+
const apiKey = sameOrigin ? storedKey : null;
|
|
2033
|
+
// Only meaningful for the route that is actually configured: marking a name on
|
|
2034
|
+
// some other host as "configured" would be saying something untrue.
|
|
2035
|
+
const configuredModel = sameOrigin && routing !== null ? (routing.model ?? JEV_PROVIDER_DEFAULTS[routing.provider].model) : null;
|
|
2036
|
+
|
|
2037
|
+
let list: JevModelListResult;
|
|
2038
|
+
try {
|
|
2039
|
+
list = await (deps.readModelList ?? liveModelListReader)(modelsUrl, apiKey);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
list = { ok: false, reason: scrubSecret(err instanceof Error ? err.message : String(err), apiKey ?? "") };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const shown = displayEndpoint(modelsUrl);
|
|
2045
|
+
const keyNote = apiKey
|
|
2046
|
+
? "the configured key for this origin"
|
|
2047
|
+
: sameOrigin
|
|
2048
|
+
? `none stored here — set ${JEV_API_KEY_ENV}, or store one with \`failproofai jev setup --key-stdin\``
|
|
2049
|
+
: storedKey !== null
|
|
2050
|
+
? "none sent: a stored key belongs to the endpoint it was stored for"
|
|
2051
|
+
: "none: nothing is configured for this endpoint, and some lists are public";
|
|
2052
|
+
|
|
2053
|
+
if (!list.ok) {
|
|
2054
|
+
if (asJson) {
|
|
2055
|
+
return fail([], JSON.stringify({ ok: false, provider, endpoint: shown, error: { code: "model-list-unread", message: list.reason } }, null, 2));
|
|
2056
|
+
}
|
|
2057
|
+
return fail(
|
|
2058
|
+
stack(
|
|
2059
|
+
title("failproofai jev models", "not read", opts),
|
|
2060
|
+
rows(
|
|
2061
|
+
[
|
|
2062
|
+
["provider", provider],
|
|
2063
|
+
["endpoint", shown],
|
|
2064
|
+
["key", keyNote],
|
|
2065
|
+
["reason", list.reason],
|
|
1184
2066
|
],
|
|
1185
2067
|
opts,
|
|
1186
2068
|
),
|
|
1187
|
-
note(
|
|
2069
|
+
note(
|
|
2070
|
+
"Not every endpoint serves a model list — a proxy may expose only /systemone — and nothing depends on one: setup and hooks work without it.",
|
|
2071
|
+
opts,
|
|
2072
|
+
),
|
|
1188
2073
|
),
|
|
1189
2074
|
);
|
|
1190
2075
|
}
|
|
2076
|
+
|
|
2077
|
+
if (asJson) {
|
|
2078
|
+
return ok(
|
|
2079
|
+
[],
|
|
2080
|
+
JSON.stringify({ ok: true, provider, endpoint: shown, shape: list.shape, models: list.models, configuredModel, keySent: apiKey !== null }, null, 2),
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
const listed = list.models.slice(0, MAX_SHOWN_MODELS);
|
|
2084
|
+
return ok(
|
|
2085
|
+
stack(
|
|
2086
|
+
title("failproofai jev models", `${list.models.length} · ${provider}`, opts),
|
|
2087
|
+
rows(
|
|
2088
|
+
[
|
|
2089
|
+
["provider", provider],
|
|
2090
|
+
["endpoint", shown],
|
|
2091
|
+
["key", keyNote],
|
|
2092
|
+
["list", shapeMeans(list.shape)],
|
|
2093
|
+
],
|
|
2094
|
+
opts,
|
|
2095
|
+
),
|
|
2096
|
+
rows(
|
|
2097
|
+
listed.map((name) => [name, name === configuredModel ? "configured" : ""] as [string, string]),
|
|
2098
|
+
opts,
|
|
2099
|
+
),
|
|
2100
|
+
list.models.length > listed.length ? note(`… and ${list.models.length - listed.length} more.`, opts) : null,
|
|
2101
|
+
configuredModel !== null && !modelListHasModel(list, configuredModel) && listDescribesSystemOne(list)
|
|
2102
|
+
? warning(
|
|
2103
|
+
[
|
|
2104
|
+
`The configured model ${configuredModel} is not one of these, so every evaluation would fail at this endpoint and hooks would fall back to regex.`,
|
|
2105
|
+
`Point it at one of them: failproofai jev setup --model ${list.models[0]}`,
|
|
2106
|
+
],
|
|
2107
|
+
opts,
|
|
2108
|
+
)
|
|
2109
|
+
: null,
|
|
2110
|
+
),
|
|
2111
|
+
);
|
|
1191
2112
|
}
|
|
1192
2113
|
|
|
1193
2114
|
// ── remove ───────────────────────────────────────────────────────────────────
|
|
@@ -1232,6 +2153,8 @@ export async function runJevCommand(argv: string[], deps: JevCliDeps = {}): Prom
|
|
|
1232
2153
|
return status(rest, opts);
|
|
1233
2154
|
case "test":
|
|
1234
2155
|
return test(rest, deps, opts);
|
|
2156
|
+
case "models":
|
|
2157
|
+
return models(rest, deps, opts);
|
|
1235
2158
|
case "remove":
|
|
1236
2159
|
return remove(rest, opts);
|
|
1237
2160
|
default: {
|