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
|
@@ -45,10 +45,14 @@
|
|
|
45
45
|
* value `jev setup` would also have printed. See `JevModelView`.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
+
import { readCredentials, readJevCloudCredential } from "@/src/hooks/fp-config";
|
|
48
49
|
import {
|
|
50
|
+
CLOUDFLARE_ACCOUNT_ID_RE,
|
|
49
51
|
DEFAULT_JEV_MODE,
|
|
50
52
|
JEV_API_KEY_ENV,
|
|
53
|
+
JEV_CLOUD_PROVIDER,
|
|
51
54
|
JEV_PROVIDER_KINDS,
|
|
55
|
+
baseUrlWithoutQuery,
|
|
52
56
|
inspectJevConfig,
|
|
53
57
|
looksLikeCredential,
|
|
54
58
|
readJevConfigForUpdate,
|
|
@@ -63,8 +67,8 @@ import {
|
|
|
63
67
|
surveyReviewableCoverage,
|
|
64
68
|
} from "@/src/hooks/policy-reviewability";
|
|
65
69
|
|
|
66
|
-
/** The loader's
|
|
67
|
-
export type JevSettingsStatus = "absent" | "ok" | "key-missing" | "refused";
|
|
70
|
+
/** The loader's answers, unchanged — see `JevConfigInspection`. */
|
|
71
|
+
export type JevSettingsStatus = "absent" | "ok" | "key-missing" | "refused" | "off" | "not-connected" | "key-lacks-jev";
|
|
68
72
|
|
|
69
73
|
/** What Jev has been doing lately, from `jevStats()`. Not a new pipeline. */
|
|
70
74
|
export interface JevSettingsStats {
|
|
@@ -89,7 +93,8 @@ export interface JevSettingsStats {
|
|
|
89
93
|
* not computed and not sent.
|
|
90
94
|
*/
|
|
91
95
|
export interface JevTokenPresence {
|
|
92
|
-
|
|
96
|
+
/** `cloud`: the key is this machine's FailproofAI Cloud key, held in `credentials.json`. */
|
|
97
|
+
source: "file" | "env" | "cloud";
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
@@ -139,10 +144,35 @@ export interface JevReviewabilityView {
|
|
|
139
144
|
problem: string | null;
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
/**
|
|
148
|
+
* This machine's FailproofAI Cloud connection, as far as Jev is concerned —
|
|
149
|
+
* read from `credentials.json` on every render, with NO network call and
|
|
150
|
+
* nothing secret in it: the org as the server named it at connect time, the
|
|
151
|
+
* host, and whether the key carries Jev. Every token in that file is read and
|
|
152
|
+
* dropped inside `cloudConnectionView`; none of it, nor any piece of it, is in
|
|
153
|
+
* this object.
|
|
154
|
+
*/
|
|
155
|
+
export interface JevCloudConnectionView {
|
|
156
|
+
/** A Cloud credential (policy or reporting) is stored on this machine. */
|
|
157
|
+
connected: boolean;
|
|
158
|
+
/** "Acme Inc (acme)" — recorded at connect time; null when it was not. */
|
|
159
|
+
org: string | null;
|
|
160
|
+
/** The Cloud host, e.g. `app.befailproof.ai`. */
|
|
161
|
+
host: string | null;
|
|
162
|
+
/**
|
|
163
|
+
* Whether the key this machine connected with carries `jev:evaluate` — which
|
|
164
|
+
* is exactly whether connect stored a `jev` slot. `refused`: a slot may be
|
|
165
|
+
* there, but `credentials.json` is not owner-only, so Jev will not read it.
|
|
166
|
+
*/
|
|
167
|
+
jev: "yes" | "no" | "refused";
|
|
168
|
+
}
|
|
169
|
+
|
|
142
170
|
export interface JevSettingsView {
|
|
143
171
|
status: JevSettingsStatus;
|
|
144
172
|
/** True only when a hook running right now would consult Jev. */
|
|
145
173
|
on: boolean;
|
|
174
|
+
/** See `JevCloudConnectionView`. Present on every status. */
|
|
175
|
+
cloud: JevCloudConnectionView;
|
|
146
176
|
/**
|
|
147
177
|
* `~/.failproofai/jev.json`, whether or not it exists, and its permission
|
|
148
178
|
* bits as `0600` when it has some.
|
|
@@ -158,8 +188,24 @@ export interface JevSettingsView {
|
|
|
158
188
|
path: string;
|
|
159
189
|
permissions: string | null;
|
|
160
190
|
provider: JevProviderKind | null;
|
|
161
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* The stored base URL, or "" for the provider's own API. Form value, and
|
|
193
|
+
* NEVER its query string — see `baseUrlView` below for why, and
|
|
194
|
+
* `baseUrlQueryWithheld` for what the panel says about the part it did not
|
|
195
|
+
* get.
|
|
196
|
+
*/
|
|
162
197
|
baseUrl: string;
|
|
198
|
+
/**
|
|
199
|
+
* True when the stored base URL carries a query string that `baseUrl` above
|
|
200
|
+
* does not.
|
|
201
|
+
*
|
|
202
|
+
* The panel needs it to avoid lying twice over: the field would otherwise
|
|
203
|
+
* show a URL that is not the stored one, and an untouched save would look
|
|
204
|
+
* like it kept a query string the person never saw. `update-jev-config.ts`
|
|
205
|
+
* keeps a stored query that the loader accepts when the field comes back
|
|
206
|
+
* unchanged, and this is how the panel can say so.
|
|
207
|
+
*/
|
|
208
|
+
baseUrlQueryWithheld: boolean;
|
|
163
209
|
/** Cloudflare only; "" otherwise. Form value. */
|
|
164
210
|
accountId: string;
|
|
165
211
|
/**
|
|
@@ -213,6 +259,27 @@ function modelView(stored: string): JevModelView {
|
|
|
213
259
|
return { kind: "id", id: stored };
|
|
214
260
|
}
|
|
215
261
|
|
|
262
|
+
/**
|
|
263
|
+
* A stored base URL as the browser may see it: never its query string.
|
|
264
|
+
*
|
|
265
|
+
* `validateBaseUrl` now refuses a credential-bearing query parameter, so a file
|
|
266
|
+
* written by THIS build cannot carry one. A file written by an older one can,
|
|
267
|
+
* and that file is precisely the one this module hands back for repair — the
|
|
268
|
+
* refused branch below copies the raw routing fields out on purpose. Only the
|
|
269
|
+
* derived `endpoint` was elided (`displayEndpoint`), so the base URL carried
|
|
270
|
+
* the secret through a response whose whole documented rule is that the
|
|
271
|
+
* credential never comes back.
|
|
272
|
+
*
|
|
273
|
+
* Every branch goes through this one function rather than reading `baseUrl`
|
|
274
|
+
* itself, for the reason `routingFromRaw` copies field by field: the next field
|
|
275
|
+
* added should not be able to quietly reintroduce it.
|
|
276
|
+
*/
|
|
277
|
+
function baseUrlView(stored: string | undefined): { baseUrl: string; baseUrlQueryWithheld: boolean } {
|
|
278
|
+
if (!stored) return { baseUrl: "", baseUrlQueryWithheld: false };
|
|
279
|
+
const { url, hadQuery } = baseUrlWithoutQuery(stored);
|
|
280
|
+
return { baseUrl: url, baseUrlQueryWithheld: hadQuery };
|
|
281
|
+
}
|
|
282
|
+
|
|
216
283
|
function octal(mode: number | null): string | null {
|
|
217
284
|
return mode === null ? null : mode.toString(8).padStart(4, "0");
|
|
218
285
|
}
|
|
@@ -244,25 +311,65 @@ function routingFromRaw(raw: Record<string, unknown> | null): {
|
|
|
244
311
|
return {
|
|
245
312
|
provider,
|
|
246
313
|
baseUrl: asString(raw?.baseUrl),
|
|
247
|
-
|
|
314
|
+
// An account id is 32 hex characters; anything else in that slot of a
|
|
315
|
+
// refused file may be a pasted key, and is not shown.
|
|
316
|
+
accountId: CLOUDFLARE_ACCOUNT_ID_RE.test(asString(raw?.accountId)) ? asString(raw?.accountId) : "",
|
|
248
317
|
model: asString(raw?.model),
|
|
249
|
-
mode: modeRaw === "shadow" || modeRaw === "enforce" ? modeRaw : DEFAULT_JEV_MODE,
|
|
318
|
+
mode: modeRaw === "off" || modeRaw === "shadow" || modeRaw === "enforce" ? modeRaw : DEFAULT_JEV_MODE,
|
|
250
319
|
};
|
|
251
320
|
}
|
|
252
321
|
|
|
253
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Whether a switched-off file still has a key to switch back on with — as a
|
|
324
|
+
* SOURCE, like every other token field here. The routing the loader reports
|
|
325
|
+
* for an `off` file deliberately carries no key, so this is read for presence
|
|
326
|
+
* only: the raw file's `apiKey` is tested and dropped, and the Cloud
|
|
327
|
+
* credential's status is read and its key never touched.
|
|
328
|
+
*/
|
|
329
|
+
function offTokenPresence(provider: JevProviderKind): JevTokenPresence | null {
|
|
330
|
+
if (provider === JEV_CLOUD_PROVIDER) {
|
|
331
|
+
try {
|
|
332
|
+
return readJevCloudCredential().status === "ok" ? { source: "cloud" } : null;
|
|
333
|
+
} catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const raw = readJevConfigForUpdate();
|
|
338
|
+
return typeof raw?.apiKey === "string" && raw.apiKey !== "" ? { source: "file" } : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* `jevRoute` for display, swallowing the throw a file that names no usable
|
|
343
|
+
* route produces.
|
|
344
|
+
*
|
|
345
|
+
* A FailproofAI Cloud config is only routed against the origin of the
|
|
346
|
+
* credential it was validated with (`validateLoadedJevConfig`). The loaded
|
|
347
|
+
* config carries it; a display stand-in built here — for a file that is off,
|
|
348
|
+
* refused, or has no credential behind it — gets the file's own origin, like
|
|
349
|
+
* the stand-in key beside it. Nothing built here is ever sent.
|
|
350
|
+
*/
|
|
254
351
|
function endpointFor(cfg: JevConfig): string | null {
|
|
255
352
|
try {
|
|
256
|
-
return displayEndpoint(jevRoute(cfg).endpoint);
|
|
353
|
+
return displayEndpoint(jevRoute(displayOnlyCloudOrigin(cfg)).endpoint);
|
|
257
354
|
} catch {
|
|
258
355
|
return null;
|
|
259
356
|
}
|
|
260
357
|
}
|
|
261
358
|
|
|
359
|
+
function displayOnlyCloudOrigin(cfg: JevConfig): JevConfig {
|
|
360
|
+
if (cfg.provider !== JEV_CLOUD_PROVIDER || cfg.credentialOrigin || typeof cfg.baseUrl !== "string") return cfg;
|
|
361
|
+
try {
|
|
362
|
+
return { ...cfg, credentialOrigin: new URL(cfg.baseUrl).origin };
|
|
363
|
+
} catch {
|
|
364
|
+
return cfg;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
262
368
|
/**
|
|
263
369
|
* The fallback rate and the counts behind it, for the one line on the panel
|
|
264
|
-
* that says whether Jev is working. Read only when Jev is
|
|
265
|
-
*
|
|
370
|
+
* that says whether Jev is working. Read only when Jev is on, or may be for
|
|
371
|
+
* hooks whose environment has the key: on a machine with no config the numbers
|
|
372
|
+
* are all zero and the line would be noise.
|
|
266
373
|
*
|
|
267
374
|
* `jevStats()` never throws and derives from the activity store the activity
|
|
268
375
|
* tab already reads, so this adds no pipeline and no persisted state.
|
|
@@ -295,7 +402,10 @@ async function statsOrNull(on: boolean): Promise<JevSettingsStats | null> {
|
|
|
295
402
|
function reviewableOrNull(on: boolean): JevReviewabilityView | null {
|
|
296
403
|
if (!on) return null;
|
|
297
404
|
try {
|
|
298
|
-
|
|
405
|
+
// The launch directory, as every other dashboard read resolves project
|
|
406
|
+
// config: the standalone server chdirs into the package, and `jev status`
|
|
407
|
+
// counts the project it is run in.
|
|
408
|
+
const coverage = surveyReviewableCoverage(process.env.FAILPROOFAI_LAUNCH_CWD || process.cwd());
|
|
299
409
|
return {
|
|
300
410
|
enabled: coverage.enabled,
|
|
301
411
|
reviewable: coverage.reviewable,
|
|
@@ -307,6 +417,42 @@ function reviewableOrNull(on: boolean): JevReviewabilityView | null {
|
|
|
307
417
|
}
|
|
308
418
|
}
|
|
309
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The Cloud connection row, from the local files only. `readCredentials`
|
|
422
|
+
* returns every token on the machine; this function takes the three
|
|
423
|
+
* non-secret facts out and lets the rest go, so the object it returns — and
|
|
424
|
+
* therefore the page — never holds a key. Never throws.
|
|
425
|
+
*/
|
|
426
|
+
function cloudConnectionView(): JevCloudConnectionView {
|
|
427
|
+
let connected = false;
|
|
428
|
+
let org: string | null = null;
|
|
429
|
+
let host: string | null = null;
|
|
430
|
+
try {
|
|
431
|
+
const creds = readCredentials();
|
|
432
|
+
const url = creds.cloud?.url ?? creds.ingest?.url ?? null;
|
|
433
|
+
connected = url !== null;
|
|
434
|
+
if (url !== null) {
|
|
435
|
+
try {
|
|
436
|
+
host = new URL(url).host;
|
|
437
|
+
} catch {
|
|
438
|
+
host = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const o = creds.org;
|
|
442
|
+
org = o ? (o.name && o.slug ? `${o.name} (${o.slug})` : (o.name ?? o.slug ?? o.id ?? null)) : null;
|
|
443
|
+
} catch {
|
|
444
|
+
// An unreadable file is "not connected" here, as it is to `config --status`.
|
|
445
|
+
}
|
|
446
|
+
let jev: JevCloudConnectionView["jev"] = "no";
|
|
447
|
+
try {
|
|
448
|
+
const slot = readJevCloudCredential().status;
|
|
449
|
+
jev = slot === "ok" ? "yes" : slot === "refused" ? "refused" : "no";
|
|
450
|
+
} catch {
|
|
451
|
+
jev = "no";
|
|
452
|
+
}
|
|
453
|
+
return { connected, org, host, jev };
|
|
454
|
+
}
|
|
455
|
+
|
|
310
456
|
/**
|
|
311
457
|
* Everything /settings shows about Jev, and nothing it must not.
|
|
312
458
|
*
|
|
@@ -317,10 +463,12 @@ function reviewableOrNull(on: boolean): JevReviewabilityView | null {
|
|
|
317
463
|
export async function getJevSettingsAction(): Promise<JevSettingsView> {
|
|
318
464
|
const inspection = inspectJevConfig();
|
|
319
465
|
const base = {
|
|
466
|
+
cloud: cloudConnectionView(),
|
|
320
467
|
path: inspection.path,
|
|
321
468
|
permissions: null as string | null,
|
|
322
469
|
provider: null as JevProviderKind | null,
|
|
323
470
|
baseUrl: "",
|
|
471
|
+
baseUrlQueryWithheld: false,
|
|
324
472
|
accountId: "",
|
|
325
473
|
model: { kind: "default" } as JevModelView,
|
|
326
474
|
endpoint: null as string | null,
|
|
@@ -340,7 +488,7 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
|
|
|
340
488
|
on: true,
|
|
341
489
|
permissions: octal(inspection.mode),
|
|
342
490
|
provider: cfg.provider,
|
|
343
|
-
|
|
491
|
+
...baseUrlView(cfg.baseUrl),
|
|
344
492
|
accountId: cfg.accountId ?? "",
|
|
345
493
|
// A loadable config's model already passed `validateModel`, so this can
|
|
346
494
|
// only be an id — run through the same gate anyway, because the gate is
|
|
@@ -365,7 +513,7 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
|
|
|
365
513
|
on: false,
|
|
366
514
|
permissions: octal(inspection.mode),
|
|
367
515
|
provider: r.provider,
|
|
368
|
-
|
|
516
|
+
...baseUrlView(r.baseUrl),
|
|
369
517
|
accountId: r.accountId ?? "",
|
|
370
518
|
model: modelView(r.model ?? ""),
|
|
371
519
|
endpoint: endpointFor({ ...r, apiKey: KEY_STAND_IN }),
|
|
@@ -376,6 +524,38 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
|
|
|
376
524
|
token: null,
|
|
377
525
|
problem: inspection.problem,
|
|
378
526
|
fix: `set ${JEV_API_KEY_ENV}, or save a token here`,
|
|
527
|
+
// Off in THIS process's environment only; hooks that have the variable
|
|
528
|
+
// still consult Jev, and the activity store records what they did.
|
|
529
|
+
stats: await statsOrNull(true),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (inspection.status === "off" || inspection.status === "not-connected" || inspection.status === "key-lacks-jev") {
|
|
534
|
+
// Configured, and not running: switched off by its owner, or a FailproofAI
|
|
535
|
+
// Cloud file on a machine with no Cloud key — not connected at all, or
|
|
536
|
+
// connected with a key that has no Jev. Routing only, as for
|
|
537
|
+
// `key-missing` — the loader's routing object carries no key to leak.
|
|
538
|
+
const r = inspection.routing;
|
|
539
|
+
return {
|
|
540
|
+
...base,
|
|
541
|
+
status: inspection.status,
|
|
542
|
+
on: false,
|
|
543
|
+
permissions: octal(inspection.mode),
|
|
544
|
+
provider: r.provider,
|
|
545
|
+
...baseUrlView(r.baseUrl),
|
|
546
|
+
accountId: r.accountId ?? "",
|
|
547
|
+
model: modelView(r.model ?? ""),
|
|
548
|
+
endpoint: endpointFor({ ...r, apiKey: KEY_STAND_IN }),
|
|
549
|
+
mode: r.mode ?? DEFAULT_JEV_MODE,
|
|
550
|
+
timeoutMs: r.timeoutMs ?? null,
|
|
551
|
+
token: inspection.status === "off" ? offTokenPresence(r.provider) : null,
|
|
552
|
+
problem: inspection.status === "off" ? null : inspection.problem,
|
|
553
|
+
fix:
|
|
554
|
+
inspection.status === "not-connected"
|
|
555
|
+
? "connect this machine with a key that carries jev:evaluate: failproofai config --token <key>"
|
|
556
|
+
: inspection.status === "key-lacks-jev"
|
|
557
|
+
? "reconnect this machine with a key that carries jev:evaluate: failproofai config --token <key>"
|
|
558
|
+
: null,
|
|
379
559
|
stats: null,
|
|
380
560
|
};
|
|
381
561
|
}
|
|
@@ -390,7 +570,7 @@ export async function getJevSettingsAction(): Promise<JevSettingsView> {
|
|
|
390
570
|
on: false,
|
|
391
571
|
permissions: octal(inspection.mode),
|
|
392
572
|
provider: r.provider,
|
|
393
|
-
|
|
573
|
+
...baseUrlView(r.baseUrl),
|
|
394
574
|
accountId: r.accountId,
|
|
395
575
|
// This is the branch the rule exists for: a file whose `model` slot holds
|
|
396
576
|
// a pasted key IS a refused file, and these fields are copied raw.
|
|
@@ -107,13 +107,18 @@ import {
|
|
|
107
107
|
resolveDashboardHost,
|
|
108
108
|
} from "@/lib/dashboard-host";
|
|
109
109
|
import {
|
|
110
|
+
JEV_CLOUD_PROVIDER,
|
|
110
111
|
JEV_PROVIDER_KINDS,
|
|
112
|
+
baseUrlWithoutQuery,
|
|
113
|
+
endpointGivenAsBase,
|
|
111
114
|
jevConfigPath,
|
|
115
|
+
providerHostConflict,
|
|
112
116
|
readJevConfigFileForUpdate,
|
|
113
117
|
validateApiKey,
|
|
114
118
|
validateBaseUrl,
|
|
115
119
|
validateJevConfig,
|
|
116
120
|
} from "@/src/hooks/semantic/jev-config";
|
|
121
|
+
import { endpointAsBaseReason } from "@/src/hooks/semantic/jev-client";
|
|
117
122
|
import { getJevSettingsAction, type JevSettingsView } from "./get-jev-config";
|
|
118
123
|
|
|
119
124
|
/**
|
|
@@ -129,7 +134,7 @@ export interface JevConfigInput {
|
|
|
129
134
|
baseUrl: string;
|
|
130
135
|
/** Cloudflare only. */
|
|
131
136
|
accountId: string;
|
|
132
|
-
/** "shadow" | "enforce". */
|
|
137
|
+
/** "off" | "shadow" | "enforce". */
|
|
133
138
|
mode: string;
|
|
134
139
|
/** "" keeps the key where it is: the stored one, or the environment's. */
|
|
135
140
|
token: string;
|
|
@@ -225,6 +230,32 @@ function originOf(raw: string): string | null {
|
|
|
225
230
|
}
|
|
226
231
|
}
|
|
227
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The base URL to WRITE for a submitted one.
|
|
235
|
+
*
|
|
236
|
+
* The panel is never sent a stored query string (`baseUrlView` in
|
|
237
|
+
* `get-jev-config.ts`), so what comes back from an untouched field is the stored
|
|
238
|
+
* URL with its query removed. Writing that verbatim would delete a query the
|
|
239
|
+
* person never saw — `?api-version=`, which some proxies route on — on any save
|
|
240
|
+
* that only meant to change the mode. So a submission that is exactly the
|
|
241
|
+
* stored URL minus its query keeps the stored one, and anything else is a URL
|
|
242
|
+
* they actually typed and is written as typed.
|
|
243
|
+
*
|
|
244
|
+
* Only a stored query the LOADER accepts is carried back. One it refuses is a
|
|
245
|
+
* credential in a base URL (`validateBaseUrl`), which is the very thing the
|
|
246
|
+
* person is on this page to repair: dropping it is the repair, and re-saving the
|
|
247
|
+
* file is the only way the panel has to perform it.
|
|
248
|
+
*/
|
|
249
|
+
function baseUrlToStore(submitted: string, stored: unknown): string {
|
|
250
|
+
if (typeof stored !== "string" || stored === "") return submitted;
|
|
251
|
+
const keep = validateBaseUrl(stored);
|
|
252
|
+
if (!keep.ok) return submitted;
|
|
253
|
+
const shown = baseUrlWithoutQuery(keep.value);
|
|
254
|
+
if (!shown.hadQuery) return submitted;
|
|
255
|
+
const typed = validateBaseUrl(submitted);
|
|
256
|
+
return typed.ok && typed.value === shown.url ? keep.value : submitted;
|
|
257
|
+
}
|
|
258
|
+
|
|
228
259
|
/**
|
|
229
260
|
* `~/.failproofai` at the umask is group-writable on a umask-002 machine, and a
|
|
230
261
|
* directory others can write into defeats the file's 0600 — they unlink it and
|
|
@@ -259,21 +290,55 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
|
|
|
259
290
|
if (!(JEV_PROVIDER_KINDS as readonly string[]).includes(provider)) {
|
|
260
291
|
return { ok: false, problem: `provider must be one of ${JEV_PROVIDER_KINDS.join(", ")}` };
|
|
261
292
|
}
|
|
293
|
+
if (provider === JEV_CLOUD_PROVIDER) {
|
|
294
|
+
// Its endpoint and key come from this machine's FailproofAI Cloud
|
|
295
|
+
// connection, not from a form: `config --token` writes both, and the only
|
|
296
|
+
// thing this page may change about it is the mode (`setJevModeAction`).
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
problem:
|
|
300
|
+
"FailproofAI Cloud's Jev is turned on by connecting this machine (failproofai config --token <key>), and switched with the mode control — not saved from this form.",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
262
303
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// endpoint is not the one the token was given for" — which is what a URL
|
|
268
|
-
// that resolves to no origin would otherwise look like below.
|
|
269
|
-
const url = validateBaseUrl(baseUrl);
|
|
270
|
-
if (!url.ok) return { ok: false, problem: url.problem };
|
|
304
|
+
// Refused, as `setJevModeAction` and `jev setup --mode` refuse it — not
|
|
305
|
+
// skipped, which kept the old mode or wrote none (and none loads as enforce).
|
|
306
|
+
if (input.mode !== "off" && input.mode !== "shadow" && input.mode !== "enforce") {
|
|
307
|
+
return { ok: false, problem: 'mode must be "off", "shadow" or "enforce".' };
|
|
271
308
|
}
|
|
272
309
|
|
|
310
|
+
const baseUrl = input.baseUrl.trim();
|
|
311
|
+
// Checked here, before anything is decided about the token, so an
|
|
312
|
+
// unparseable URL is reported as an unparseable URL rather than as "that
|
|
313
|
+
// endpoint is not the one the token was given for" — which is what a URL
|
|
314
|
+
// that resolves to no origin would otherwise look like below.
|
|
315
|
+
const url = baseUrl ? validateBaseUrl(baseUrl) : null;
|
|
316
|
+
if (url && !url.ok) return { ok: false, problem: url.problem };
|
|
317
|
+
|
|
273
318
|
const existingFile = readJevConfigFileForUpdate();
|
|
274
319
|
const existing = existingFile?.raw ?? null;
|
|
275
320
|
const sameProvider = existing !== null && existing.provider === provider;
|
|
276
321
|
|
|
322
|
+
// `jev setup`'s refusal of an endpoint where a base belongs (`…/v1/models`):
|
|
323
|
+
// every call would go to `<that>/systemone` and 404 back to regex. Only for a
|
|
324
|
+
// URL typed here, as the CLI checks only a `--base-url` it was given — an
|
|
325
|
+
// older file whose base already ends in `/systemone` routes correctly and the
|
|
326
|
+
// loader takes it, so re-saving it untouched (a mode switch) must still work.
|
|
327
|
+
if (url?.ok) {
|
|
328
|
+
const stored = validateBaseUrl(existing?.baseUrl);
|
|
329
|
+
const untouched =
|
|
330
|
+
sameProvider && stored.ok && baseUrlWithoutQuery(stored.value).url === baseUrlWithoutQuery(url.value).url;
|
|
331
|
+
const conflict = untouched ? null : providerHostConflict(provider, url.value);
|
|
332
|
+
if (conflict) return { ok: false, problem: `${conflict}.` };
|
|
333
|
+
const asEndpoint = untouched ? null : endpointGivenAsBase(url.value);
|
|
334
|
+
if (asEndpoint) {
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
problem: `that url is an endpoint, not an api base — its path ends in ${asEndpoint.suffix}. ${endpointAsBaseReason(url.value)} Use ${baseUrlWithoutQuery(asEndpoint.base).url} instead.`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
277
342
|
// Same provider: update in place, keeping every field not named here —
|
|
278
343
|
// `model`, a `timeoutMs` set from the CLI, and ones a newer failproofai wrote,
|
|
279
344
|
// none of which this form knows about or may drop. A different provider starts
|
|
@@ -287,7 +352,7 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
|
|
|
287
352
|
}
|
|
288
353
|
next.provider = provider;
|
|
289
354
|
|
|
290
|
-
if (baseUrl) next.baseUrl = baseUrl;
|
|
355
|
+
if (baseUrl) next.baseUrl = baseUrlToStore(baseUrl, existing?.baseUrl);
|
|
291
356
|
else delete next.baseUrl;
|
|
292
357
|
|
|
293
358
|
// `model` is NOT touched here, and that is the point: `JevConfigInput` has no
|
|
@@ -305,7 +370,7 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
|
|
|
305
370
|
delete next.accountId;
|
|
306
371
|
}
|
|
307
372
|
|
|
308
|
-
|
|
373
|
+
next.mode = input.mode;
|
|
309
374
|
|
|
310
375
|
// Where the key may travel. Unchanged provider AND unchanged origin, from a
|
|
311
376
|
// file only its owner could have written — anything else asks again.
|
|
@@ -393,6 +458,86 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
|
|
|
393
458
|
return { ok: true, view: await getJevSettingsAction() };
|
|
394
459
|
}
|
|
395
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Switch Jev's mode — `off`, `shadow` or `enforce` — and change NOTHING else.
|
|
463
|
+
*
|
|
464
|
+
* This is the on/off switch for FailproofAI Cloud's Jev, whose `jev.json` the
|
|
465
|
+
* page must not delete: its endpoint and key are not this page's to re-enter,
|
|
466
|
+
* so "off" by deletion would be off until the owner re-ran `config --token`
|
|
467
|
+
* with the key in hand. `off` keeps the file and runs no Jev at all
|
|
468
|
+
* (`loadJevConfig` returns null for it), which is the switch this needs.
|
|
469
|
+
* It works on a BYOK file too, where it keeps the key.
|
|
470
|
+
*
|
|
471
|
+
* What "nothing else" means, precisely: the existing object is spread and only
|
|
472
|
+
* `mode` is set — no field is added, dropped or normalised, and the key, the
|
|
473
|
+
* endpoint and the provider are exactly what they were. It is written the way
|
|
474
|
+
* every other write here is (`writeJsonAtomically`, 0600, owner-only
|
|
475
|
+
* directory), after the loader's own validator has checked the result with
|
|
476
|
+
* stand-ins in the key slots, so `enforce` is refused where the loader would
|
|
477
|
+
* refuse it (plain http to localhost) rather than written and then ignored.
|
|
478
|
+
*
|
|
479
|
+
* Refused for a file the loader calls too open: someone else may have written
|
|
480
|
+
* the endpoint it names, and re-saving it at 0600 would start trusting that
|
|
481
|
+
* endpoint. Jev is already off for such a file; the page shows why and how.
|
|
482
|
+
*/
|
|
483
|
+
export async function setJevModeAction(mode: string): Promise<JevWriteResult> {
|
|
484
|
+
const refusal = await crossOriginRefusal();
|
|
485
|
+
if (refusal) return { ok: false, problem: refusal };
|
|
486
|
+
if (mode !== "off" && mode !== "shadow" && mode !== "enforce") {
|
|
487
|
+
return { ok: false, problem: 'mode must be "off", "shadow" or "enforce".' };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const existingFile = readJevConfigFileForUpdate();
|
|
491
|
+
if (existingFile === null) {
|
|
492
|
+
return {
|
|
493
|
+
ok: false,
|
|
494
|
+
problem: `there is no readable ${jevConfigPath()} to switch. connect this machine (failproofai config --token <key>) or set up your own endpoint below.`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (existingFile.tooOpen) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
problem: `${jevConfigPath()} is open to other users, so Jev is already off and the endpoint it names is not trusted. fix its permissions first (chmod 600, and chmod 700 on its directory).`,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const next: Record<string, unknown> = { ...existingFile.raw, mode };
|
|
505
|
+
|
|
506
|
+
// The file as the loader would judge it, keys aside: a stored key is checked
|
|
507
|
+
// for real, a missing one (key-from-env, or the Cloud's) gets a stand-in —
|
|
508
|
+
// whether this machine is connected right now is not what a mode switch is
|
|
509
|
+
// about, and the page says so separately.
|
|
510
|
+
const cloud = next.provider === JEV_CLOUD_PROVIDER;
|
|
511
|
+
const checked = validateJevConfig(
|
|
512
|
+
next,
|
|
513
|
+
cloud || typeof next.apiKey === "string" ? null : ENV_KEY_STAND_IN,
|
|
514
|
+
cloud ? { url: originOrEmpty(next.baseUrl), key: ENV_KEY_STAND_IN } : null,
|
|
515
|
+
);
|
|
516
|
+
if (!checked.ok) return { ok: false, problem: checked.problem };
|
|
517
|
+
|
|
518
|
+
const path = jevConfigPath();
|
|
519
|
+
try {
|
|
520
|
+
writeJsonAtomically(path, next, { mode: 0o600, dirMode: 0o700 });
|
|
521
|
+
} catch (err) {
|
|
522
|
+
return {
|
|
523
|
+
ok: false,
|
|
524
|
+
problem: `could not write ${path} (${(err as NodeJS.ErrnoException).code ?? "error"}).`,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
tightenConfigDir(path);
|
|
528
|
+
return { ok: true, view: await getJevSettingsAction() };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** A stored base URL's origin, for a stand-in credential; "" when it has none. */
|
|
532
|
+
function originOrEmpty(raw: unknown): string {
|
|
533
|
+
if (typeof raw !== "string") return "";
|
|
534
|
+
try {
|
|
535
|
+
return new URL(raw).origin;
|
|
536
|
+
} catch {
|
|
537
|
+
return "";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
396
541
|
/**
|
|
397
542
|
* Delete `~/.failproofai/jev.json`. Hooks go back to the regex engine on the
|
|
398
543
|
* very next tool call — the loader reads the file per event, so there is
|
|
@@ -400,6 +545,10 @@ export async function saveJevConfigAction(input: JevConfigInput): Promise<JevWri
|
|
|
400
545
|
*
|
|
401
546
|
* Removing a file that is not there succeeds: the user asked for Jev to be off,
|
|
402
547
|
* and it is.
|
|
548
|
+
*
|
|
549
|
+
* The BYOK "turn jev off". The FailproofAI Cloud route is switched off with
|
|
550
|
+
* `setJevModeAction("off")` instead — see there for why deleting its file is
|
|
551
|
+
* the wrong switch.
|
|
403
552
|
*/
|
|
404
553
|
export async function removeJevConfigAction(): Promise<JevWriteResult> {
|
|
405
554
|
const refusal = await crossOriginRefusal();
|
|
@@ -492,9 +492,9 @@ function ActivityTab({
|
|
|
492
492
|
const v = url.get("cli");
|
|
493
493
|
return isKnownCli(v) ? v : "";
|
|
494
494
|
});
|
|
495
|
-
const [filterSource, setFilterSource] = useState<"" | "custom" | "convention" | "cloud" | "pack">(() => {
|
|
495
|
+
const [filterSource, setFilterSource] = useState<"" | "custom" | "convention" | "cloud" | "pack" | "jev">(() => {
|
|
496
496
|
const v = url.get("source");
|
|
497
|
-
return v === "custom" || v === "convention" || v === "cloud" || v === "pack" ? v : "";
|
|
497
|
+
return v === "custom" || v === "convention" || v === "cloud" || v === "pack" || v === "jev" ? v : "";
|
|
498
498
|
});
|
|
499
499
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
500
500
|
const filterTelemetryFirstRunRef = useRef(true);
|
|
@@ -665,7 +665,7 @@ function ActivityTab({
|
|
|
665
665
|
onChange={(e) => {
|
|
666
666
|
const v = e.target.value;
|
|
667
667
|
setFilterSource(
|
|
668
|
-
v === "custom" || v === "convention" || v === "cloud" || v === "pack" ? v : "",
|
|
668
|
+
v === "custom" || v === "convention" || v === "cloud" || v === "pack" || v === "jev" ? v : "",
|
|
669
669
|
);
|
|
670
670
|
}}
|
|
671
671
|
className="filter-input"
|
|
@@ -676,6 +676,8 @@ function ActivityTab({
|
|
|
676
676
|
<option value="convention">convention</option>
|
|
677
677
|
<option value="cloud">cloud</option>
|
|
678
678
|
<option value="pack">pack</option>
|
|
679
|
+
{/* Jev's own verdict, in enforce mode: no registered policy decided. */}
|
|
680
|
+
<option value="jev">jev</option>
|
|
679
681
|
</select>
|
|
680
682
|
</div>
|
|
681
683
|
<div className="filter-group">
|