failproofai 1.0.12-beta.0 → 1.0.12-beta.2
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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- 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 +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- 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 +22 -22
- 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 +1 -1
- 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 +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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 +4 -4
- 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.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 +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +7 -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]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
- package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.js} +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
- package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ts0fg5yih1c1._.js → [root-of-the-server]__00l2u6j86cb8y._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _0ouu886szh6yj._.js} +10 -9
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1ve8y151ua7qx._.js → _1fw0_nzkae5ao._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_00jxj8foxqz5t.js → node_modules_html-to-image_es_index_1fv03uigni8s5.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 0ay__-d2h1xb5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 0tbm3vjm62h-2.js} +1 -1
- package/.next/standalone/.next/static/chunks/1c_jf6g5wnslk.js +1 -0
- package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 1my3-5p47t7r-.js} +1 -1
- package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +1 -0
- package/.next/standalone/.next/static/chunks/21hckpmsc4g70.js +6 -0
- package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +1 -0
- package/.next/standalone/.next/static/chunks/2o6d37j425uci.js +1 -0
- package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
- package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 42xptz88mtq3x.js} +1 -1
- package/.next/standalone/app/actions/update-jev-config.ts +5 -3
- package/.next/standalone/app/settings/jev-panel.tsx +1 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +32 -0
- package/.next/standalone/fp-cloud-cli/README.md +40 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +91 -12
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +78 -16
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +124 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -20
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +14 -7
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +527 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
- package/.next/standalone/hermes-plugin/README.md +6 -4
- package/.next/standalone/hermes-plugin/__init__.py +27 -0
- package/.next/standalone/hermes-plugin/client.py +5 -1
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +11 -10
- package/.next/standalone/sdk/python/uv.lock +31 -8
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +17 -1
- package/dist/cli.mjs +3507 -1250
- package/dist/worker.mjs +1779 -169
- package/hermes-plugin/README.md +6 -4
- package/hermes-plugin/__init__.py +27 -0
- package/hermes-plugin/client.py +5 -1
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +6 -7
- package/src/hooks/agent-roster.ts +180 -0
- package/src/hooks/agent-skill.ts +203 -0
- package/src/hooks/agent-targets.ts +60 -0
- package/src/hooks/cloud-connection.ts +91 -45
- package/src/hooks/cloud-enrollment-cli.ts +7 -4
- package/src/hooks/cloud-login.ts +6 -3
- package/src/hooks/cloud-managed-policies.ts +430 -20
- package/src/hooks/cloud-policy-errors.ts +481 -0
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +10 -4
- package/src/hooks/configure-wizard.ts +54 -26
- package/src/hooks/custom-hooks-loader.ts +26 -3
- package/src/hooks/daemon-client.ts +9 -4
- package/src/hooks/daemon-service.ts +2 -1
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +36 -1
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/fp-home.ts +6 -0
- package/src/hooks/handler.ts +255 -19
- package/src/hooks/health-probe.ts +15 -0
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +36 -14
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +196 -1
- package/src/hooks/jev-cloud-connection.ts +92 -0
- package/src/hooks/manager.ts +174 -34
- package/src/hooks/pack-failclosed.ts +2 -0
- package/src/hooks/pack-manifest.ts +8 -1
- package/src/hooks/policy-evaluator.ts +22 -1
- package/src/hooks/policy-registry.ts +3 -2
- package/src/hooks/policy-reviewability.ts +23 -5
- package/src/hooks/semantic/cloud-jev-health.ts +227 -0
- package/src/hooks/semantic/cloud-jev.ts +868 -0
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/jev-client.ts +19 -2
- package/src/hooks/semantic/jev-config.ts +179 -30
- package/src/hooks/semantic/jev-review.ts +217 -10
- package/src/hooks/semantic/pack-policies.ts +2 -1
- package/src/hooks/semantic/types.ts +8 -0
- package/src/hooks/session-pause-cli.ts +1 -1
- package/src/hooks/tui.ts +13 -4
- package/src/hooks/uninstall-cli.ts +12 -1
- package/src/hooks/worker-server.ts +5 -1
- package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
- package/.next/standalone/.next/static/chunks/0a-ccsp3xmypz.js +0 -1
- package/.next/standalone/.next/static/chunks/13jv-xvabeb62.js +0 -6
- package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
- package/.next/standalone/.next/static/chunks/2meqb9kygrncb.js +0 -1
- package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
- package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
|
@@ -41,7 +41,7 @@ import type { HookActivityEntry } from "./hook-activity-store";
|
|
|
41
41
|
|
|
42
42
|
export type JevActivityFields = Pick<
|
|
43
43
|
HookActivityEntry,
|
|
44
|
-
"evaluator" | "jevDecision" | "jevCleared" | "jevFallbackReason" | "jevLatencyMs" | "jevModel" | "jevMode"
|
|
44
|
+
"evaluator" | "jevDecision" | "jevCleared" | "jevFallbackReason" | "jevLatencyMs" | "jevModel" | "jevMode" | "jevChecks"
|
|
45
45
|
>;
|
|
46
46
|
|
|
47
47
|
/** Every Jev key on an activity row, in the order they are documented. */
|
|
@@ -53,6 +53,7 @@ export const JEV_ACTIVITY_KEYS = [
|
|
|
53
53
|
"jevLatencyMs",
|
|
54
54
|
"jevModel",
|
|
55
55
|
"jevMode",
|
|
56
|
+
"jevChecks",
|
|
56
57
|
] as const satisfies ReadonlyArray<keyof JevActivityFields>;
|
|
57
58
|
|
|
58
59
|
const EVALUATORS = new Set(["jev", "jev-fallback"]);
|
|
@@ -219,6 +220,113 @@ export function normalizeJevFallbackReason(raw: unknown): string | undefined {
|
|
|
219
220
|
return JEV_REASON_CODES.has(code) ? code : JEV_REASON_OTHER;
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
// ── Per-check answers ────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/** What one asked Jev check concluded. */
|
|
226
|
+
export const JEV_CHECK_VERDICTS = ["none", "deny", "instruct", "overridden"] as const;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Why a check ended where it did, as a closed code (read off its outcome,
|
|
230
|
+
* never text from the call):
|
|
231
|
+
* - `not-detected`: its probes did not hold (evidence under the fire threshold);
|
|
232
|
+
* - `exempt`: it fired, and its `exempt` question held;
|
|
233
|
+
* - `fired`: it fired and nothing softened it;
|
|
234
|
+
* - `injection`: it fired and the request also argued for its own approval;
|
|
235
|
+
* - `user-asked` (v0), `op-requested`, `task-step` (v1): the human's consent cleared it;
|
|
236
|
+
* - `downgraded`: a deny the human's task softened to a warning;
|
|
237
|
+
* - `scan-incomplete`: a clear withheld because the command scan may have missed a target.
|
|
238
|
+
*/
|
|
239
|
+
export const JEV_CHECK_WHY = [
|
|
240
|
+
"not-detected",
|
|
241
|
+
"exempt",
|
|
242
|
+
"fired",
|
|
243
|
+
"injection",
|
|
244
|
+
"user-asked",
|
|
245
|
+
"op-requested",
|
|
246
|
+
"task-step",
|
|
247
|
+
"downgraded",
|
|
248
|
+
"scan-incomplete",
|
|
249
|
+
] as const;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* One Jev check that was asked on this call, and its answer: names, closed
|
|
253
|
+
* codes and probabilities only — never question text, the command or the
|
|
254
|
+
* human's words. Jev answers yes/no probabilities, not reasons, so `why` is
|
|
255
|
+
* the decider's own reading of them.
|
|
256
|
+
*/
|
|
257
|
+
export interface JevCheckRecord {
|
|
258
|
+
/** The check's name (`semantic/<check>` without the prefix). */
|
|
259
|
+
check: string;
|
|
260
|
+
/** Who declared it: `cloud/<policyId>@<version>` or `pack/<packId>@<version>`. Absent for a caller-supplied set. */
|
|
261
|
+
policy?: string;
|
|
262
|
+
verdict: (typeof JEV_CHECK_VERDICTS)[number];
|
|
263
|
+
why: (typeof JEV_CHECK_WHY)[number];
|
|
264
|
+
/** Evidence: the minimum over its probes, 2 decimals. */
|
|
265
|
+
p: number;
|
|
266
|
+
/** Its `exempt` answer, when it declares one. */
|
|
267
|
+
exempt?: number;
|
|
268
|
+
/** Each probe's answer by probe id, when the answers came back. */
|
|
269
|
+
probes?: Record<string, number>;
|
|
270
|
+
/** A check of a watched (observe) Cloud policy: recorded, never enforced. */
|
|
271
|
+
watched?: true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const JEV_CHECKS_MAX = 32;
|
|
275
|
+
export const JEV_CHECK_PROBES_MAX = 8;
|
|
276
|
+
/** A check name: a slug, `.` allowed (a Cloud check name may carry one). */
|
|
277
|
+
export const JEV_CHECK_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
278
|
+
/** A probe id, as declarations allow it. */
|
|
279
|
+
export const JEV_PROBE_ID_RE = /^[a-z][a-z0-9_]{0,31}$/;
|
|
280
|
+
const CHECK_VERDICT_SET: ReadonlySet<string> = new Set(JEV_CHECK_VERDICTS);
|
|
281
|
+
const CHECK_WHY_SET: ReadonlySet<string> = new Set(JEV_CHECK_WHY);
|
|
282
|
+
|
|
283
|
+
const isP = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1;
|
|
284
|
+
/** A probability at 2 decimals — what the reason strings print. */
|
|
285
|
+
export const round2 = (v: number): number => Math.round(v * 100) / 100;
|
|
286
|
+
|
|
287
|
+
/** One record, rebuilt field by field, or null when it is not one. */
|
|
288
|
+
function sanitizeCheck(raw: unknown): JevCheckRecord | null {
|
|
289
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
290
|
+
const r = raw as Record<string, unknown>;
|
|
291
|
+
if (typeof r.check !== "string" || !JEV_CHECK_NAME_RE.test(r.check)) return null;
|
|
292
|
+
if (typeof r.verdict !== "string" || !CHECK_VERDICT_SET.has(r.verdict)) return null;
|
|
293
|
+
if (typeof r.why !== "string" || !CHECK_WHY_SET.has(r.why)) return null;
|
|
294
|
+
if (!isP(r.p)) return null;
|
|
295
|
+
const out: JevCheckRecord = {
|
|
296
|
+
check: r.check,
|
|
297
|
+
verdict: r.verdict as JevCheckRecord["verdict"],
|
|
298
|
+
why: r.why as JevCheckRecord["why"],
|
|
299
|
+
p: round2(r.p),
|
|
300
|
+
};
|
|
301
|
+
if (typeof r.policy === "string" && isJevPolicyName(r.policy) && !/\s/.test(r.policy) && /^(?:cloud|pack)\//.test(r.policy)) {
|
|
302
|
+
out.policy = r.policy;
|
|
303
|
+
}
|
|
304
|
+
if (isP(r.exempt)) out.exempt = round2(r.exempt);
|
|
305
|
+
if (r.watched === true) out.watched = true;
|
|
306
|
+
if (r.probes && typeof r.probes === "object" && !Array.isArray(r.probes)) {
|
|
307
|
+
const probes: Record<string, number> = {};
|
|
308
|
+
for (const [id, v] of Object.entries(r.probes as Record<string, unknown>)) {
|
|
309
|
+
if (Object.keys(probes).length >= JEV_CHECK_PROBES_MAX) break;
|
|
310
|
+
if (JEV_PROBE_ID_RE.test(id) && isP(v)) probes[id] = round2(v);
|
|
311
|
+
}
|
|
312
|
+
if (Object.keys(probes).length > 0) out.probes = probes;
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** A row's `jevChecks`, valid records only (at most {@link JEV_CHECKS_MAX}), or undefined when it is not a list. */
|
|
318
|
+
export function sanitizeJevChecks(raw: unknown): JevCheckRecord[] | undefined {
|
|
319
|
+
if (!Array.isArray(raw)) return undefined;
|
|
320
|
+
const out: JevCheckRecord[] = [];
|
|
321
|
+
for (const item of raw) {
|
|
322
|
+
if (out.length >= JEV_CHECKS_MAX) break;
|
|
323
|
+
const rec = sanitizeCheck(item);
|
|
324
|
+
if (rec) out.push(rec);
|
|
325
|
+
}
|
|
326
|
+
// Every entry invalid: not "nothing asked" — the list is unreadable, so it goes.
|
|
327
|
+
return raw.length > 0 && out.length === 0 ? undefined : out;
|
|
328
|
+
}
|
|
329
|
+
|
|
222
330
|
/**
|
|
223
331
|
* The Jev fields of `entry`, each one valid or absent. Non-Jev keys pass
|
|
224
332
|
* through untouched, and an entry with no Jev keys comes back as the same
|
|
@@ -265,6 +373,11 @@ export function sanitizeJevActivity<T extends JevActivityFields>(entry: T): T {
|
|
|
265
373
|
if (typeof out.jevModel === "string" && JEV_MODEL_RE.test(out.jevModel.trim())) out.jevModel = out.jevModel.trim();
|
|
266
374
|
else drop("jevModel");
|
|
267
375
|
|
|
376
|
+
// Only on an answered call: a fallback or an unconsulted call asked nothing.
|
|
377
|
+
const checks = out.evaluator === "jev" ? sanitizeJevChecks(out.jevChecks) : undefined;
|
|
378
|
+
if (checks === undefined) drop("jevChecks");
|
|
379
|
+
else out.jevChecks = checks;
|
|
380
|
+
|
|
268
381
|
return out;
|
|
269
382
|
}
|
|
270
383
|
|
|
@@ -307,6 +420,9 @@ export function jevOutcome(raw: JevActivityFields): JevOutcome | null {
|
|
|
307
420
|
function outcomeOf(e: JevActivityFields): JevOutcome | null {
|
|
308
421
|
if (e.evaluator === "jev-fallback") return "fallback";
|
|
309
422
|
if (e.evaluator !== "jev") return null;
|
|
423
|
+
// A request that carried no check (only the global intent questions, or a
|
|
424
|
+
// FailproofAI Cloud short-circuit) decided nothing: Jev had nothing to ask.
|
|
425
|
+
if (Array.isArray(e.jevChecks) && e.jevChecks.length === 0) return "no-request";
|
|
310
426
|
if (e.jevCleared !== undefined || e.jevLatencyMs !== undefined || e.jevModel !== undefined) return "answered";
|
|
311
427
|
if (e.jevDecision === "deny" || e.jevDecision === "instruct") return "answered";
|
|
312
428
|
if (e.jevDecision === "allow") return "no-request";
|
package/src/hooks/jev-cli.ts
CHANGED
|
@@ -108,6 +108,8 @@ import {
|
|
|
108
108
|
inspectJevConfig,
|
|
109
109
|
jevCloudBaseUrl,
|
|
110
110
|
jevConfigPath,
|
|
111
|
+
loadJevConfigForCloudMode,
|
|
112
|
+
TRANSCRIPTS_DISABLED_PROBLEM,
|
|
111
113
|
providerForUrl,
|
|
112
114
|
providerHostConflict,
|
|
113
115
|
readJevConfigFileForUpdate,
|
|
@@ -144,7 +146,8 @@ import {
|
|
|
144
146
|
type ReviewableCoverage,
|
|
145
147
|
} from "./policy-reviewability";
|
|
146
148
|
import { jevStats, type JevStats } from "./semantic/jev-stats";
|
|
147
|
-
import {
|
|
149
|
+
import { listActiveCloudManagedPolicies, readCloudJevState, type CloudJevState } from "./cloud-managed-policies";
|
|
150
|
+
import { CLOUD_MANAGED_JEV_MODE, cloudManagesJevMode, readCredentials, readJevCloudCredential, type JevCloudCredential } from "./fp-config";
|
|
148
151
|
import type { JevRequest } from "./semantic/types";
|
|
149
152
|
import { TOKEN_ON_ARGV, optsFor, paint, screenKit, stack, wrap, type RenderOpts } from "./tui";
|
|
150
153
|
|
|
@@ -364,6 +367,9 @@ function cloudConnection(): { cloudConnected: boolean; keyCarriesJev: boolean }
|
|
|
364
367
|
}
|
|
365
368
|
}
|
|
366
369
|
|
|
370
|
+
/** What `jev setup --mode` says on a machine whose Jev mode FailproofAI Cloud sets. */
|
|
371
|
+
const CLOUD_MANAGED_MODE_LINES = [CLOUD_MANAGED_JEV_MODE, "Deploy or remove Jev policies in FailproofAI Cloud to change it. Nothing was written."];
|
|
372
|
+
|
|
367
373
|
/** The one line `status` titles a connected machine whose key has no Jev with. */
|
|
368
374
|
const KEY_LACKS_JEV_TITLE = "off — no Jev key is stored for this machine's FailproofAI Cloud connection";
|
|
369
375
|
|
|
@@ -686,6 +692,9 @@ async function setupRun(argv: string[], deps: JevCliDeps, opts: RenderOpts): Pro
|
|
|
686
692
|
if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
|
|
687
693
|
if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
|
|
688
694
|
const { values, bools } = parsed;
|
|
695
|
+
// A machine connected for policies has its Jev mode set by FailproofAI Cloud
|
|
696
|
+
// alone: enforce while a Jev policy is deployed to it, off when none is.
|
|
697
|
+
if (values.has("--mode") && cloudManagesJevMode()) return fail(CLOUD_MANAGED_MODE_LINES);
|
|
689
698
|
if (bools.has("--key-stdin") && bools.has("--key-from-env")) return fail(["Use one of --key-stdin and --key-from-env, not both."]);
|
|
690
699
|
// One source for the key, so no run has to decide which of two the person
|
|
691
700
|
// meant. The message names the recommended one first.
|
|
@@ -1418,7 +1427,193 @@ function safeCoverage(): ReviewableCoverage | null {
|
|
|
1418
1427
|
}
|
|
1419
1428
|
}
|
|
1420
1429
|
|
|
1430
|
+
// ── status: FailproofAI Cloud ────────────────────────────────────────────────
|
|
1431
|
+
//
|
|
1432
|
+
// FailproofAI Cloud can SET this machine's Jev mode (`active.json` `jevMode`).
|
|
1433
|
+
// With `observe`/`enforce` the Jev checks Cloud deployed here run ON Cloud —
|
|
1434
|
+
// nothing of them is on this machine (CONTRACT C10) — every gated tool call
|
|
1435
|
+
// goes there on the Cloud Jev credential, and `jev.json` is not used. With
|
|
1436
|
+
// `off`, Jev is off whatever `jev.json` says. So `status` answers from Cloud
|
|
1437
|
+
// first; without a Cloud mode the local answer stands.
|
|
1438
|
+
//
|
|
1439
|
+
// What it can show about Cloud's checks is only what the machine knows: the
|
|
1440
|
+
// mode, and the `reviewedBy` names of the deployment's `both` policies — the
|
|
1441
|
+
// Cloud checks that may clear their regex half. Never a check's contents: the
|
|
1442
|
+
// machine does not have them.
|
|
1443
|
+
|
|
1444
|
+
/** Label for everything Cloud decided here. Customer-facing: never "AgentEye". */
|
|
1445
|
+
const MODE_SET_BY_CLOUD = "mode set by FailproofAI Cloud";
|
|
1446
|
+
|
|
1447
|
+
/** The one sentence the contract fixes for `jev status` under a Cloud mode (C10.5). */
|
|
1448
|
+
export function jevRunsOnCloudLine(mode: string): string {
|
|
1449
|
+
return `Jev checks run on FailproofAI Cloud (mode: ${mode})`;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function safeCloudJev(): CloudJevState {
|
|
1453
|
+
try {
|
|
1454
|
+
return readCloudJevState();
|
|
1455
|
+
} catch {
|
|
1456
|
+
return { jevMode: null, deployment: null, errors: [] };
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/** A `both` policy's Cloud reviewers, as the deployment names them. */
|
|
1461
|
+
interface CloudReviewers {
|
|
1462
|
+
policy: string;
|
|
1463
|
+
version: number;
|
|
1464
|
+
effect: "enforce" | "observe";
|
|
1465
|
+
reviewedBy: string[];
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** Every Cloud JS policy that names Cloud checks in `reviewedBy` — a `both` policy. Never throws. */
|
|
1469
|
+
function cloudReviewers(): CloudReviewers[] {
|
|
1470
|
+
try {
|
|
1471
|
+
// Every assignment, agent-scoped ones included: this describes the machine.
|
|
1472
|
+
return listActiveCloudManagedPolicies()
|
|
1473
|
+
.filter((p) => p.authority === "reviewable" && (p.reviewedBy?.length ?? 0) > 0)
|
|
1474
|
+
.map((p) => ({ policy: p.id, version: p.version, effect: p.effect, reviewedBy: [...(p.reviewedBy ?? [])] }));
|
|
1475
|
+
} catch {
|
|
1476
|
+
return [];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/** What to do about a Cloud mode this machine cannot act on. */
|
|
1481
|
+
function cloudProblemAdvice(problem: string): string[] {
|
|
1482
|
+
if (problem === TRANSCRIPTS_DISABLED_PROBLEM) {
|
|
1483
|
+
return [
|
|
1484
|
+
"Jev does not run here: this machine was connected for decisions only (--no-transcripts), and a Jev call sends each checked tool call and the recent prompt to FailproofAI Cloud.",
|
|
1485
|
+
"To use FailproofAI Cloud's Jev checks, reconnect without --no-transcripts (failproofai config --token <key>).",
|
|
1486
|
+
];
|
|
1487
|
+
}
|
|
1488
|
+
return [
|
|
1489
|
+
`Jev cannot run here: ${problem}.`,
|
|
1490
|
+
"Connect with a key that carries jev:evaluate (failproofai config --token <key>). A local jev.json is not used while FailproofAI Cloud sets the mode.",
|
|
1491
|
+
];
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1421
1494
|
async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
1495
|
+
const cloud = safeCloudJev();
|
|
1496
|
+
if (cloud.jevMode === null && cloud.errors.length === 0) return localStatus(argv, opts);
|
|
1497
|
+
const parsed = parseFlags(argv, new Set(["--json"]));
|
|
1498
|
+
if (typeof parsed === "string" || parsed.positionals.length > 0) return localStatus(argv, opts);
|
|
1499
|
+
const asJson = parsed.bools.has("--json");
|
|
1500
|
+
|
|
1501
|
+
if (cloud.jevMode === null) {
|
|
1502
|
+
// A mode this build cannot read is no mode: jev.json decides. Said beside it.
|
|
1503
|
+
const local = await localStatus(argv, opts);
|
|
1504
|
+
if (asJson && local.json !== undefined) {
|
|
1505
|
+
const body = JSON.parse(local.json) as Record<string, unknown>;
|
|
1506
|
+
body.cloud = { jevMode: null, modeSetBy: null, errors: cloud.errors };
|
|
1507
|
+
return { ...local, json: JSON.stringify(body, null, 2) };
|
|
1508
|
+
}
|
|
1509
|
+
return { ...local, lines: [...local.lines, ...jevWarn(cloud.errors.map((e) => `${e.id}: ${e.message}`), opts)] };
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const mode = cloud.jevMode;
|
|
1513
|
+
const resolved = loadJevConfigForCloudMode(mode);
|
|
1514
|
+
const reviewers = cloudReviewers();
|
|
1515
|
+
let stats: JevStats | null = null;
|
|
1516
|
+
try {
|
|
1517
|
+
stats = await jevStats();
|
|
1518
|
+
} catch {
|
|
1519
|
+
stats = null;
|
|
1520
|
+
}
|
|
1521
|
+
const cfg = resolved.config;
|
|
1522
|
+
const coverage = cfg ? safeCoverage() : null;
|
|
1523
|
+
const route = cfg ? jevRoute(cfg) : null;
|
|
1524
|
+
const running = cfg !== null;
|
|
1525
|
+
|
|
1526
|
+
if (asJson) {
|
|
1527
|
+
const body: Record<string, unknown> = {
|
|
1528
|
+
path: jevConfigPath(),
|
|
1529
|
+
status: running ? "ok" : mode === "off" ? "off" : "unconfigured",
|
|
1530
|
+
mode,
|
|
1531
|
+
modeSetBy: "FailproofAI Cloud",
|
|
1532
|
+
...(mode !== "off" ? { runsOn: "FailproofAI Cloud" } : {}),
|
|
1533
|
+
...(cfg && route
|
|
1534
|
+
? {
|
|
1535
|
+
provider: cfg.provider,
|
|
1536
|
+
providerLabel: providerLabel(cfg.provider),
|
|
1537
|
+
endpoint: shownEndpoint(cfg.provider, route.endpoint),
|
|
1538
|
+
model: route.model,
|
|
1539
|
+
modelIsDefault: route.modelIsDefault,
|
|
1540
|
+
timeoutMs: cfg.timeoutMs,
|
|
1541
|
+
}
|
|
1542
|
+
: {}),
|
|
1543
|
+
...(resolved.problem ? { problem: resolved.problem } : {}),
|
|
1544
|
+
cloud: { jevMode: mode, modeSetBy: "FailproofAI Cloud", reviewers, errors: cloud.errors },
|
|
1545
|
+
...(coverage
|
|
1546
|
+
? {
|
|
1547
|
+
reviewablePolicies: {
|
|
1548
|
+
enabled: coverage.enabled,
|
|
1549
|
+
reviewable: coverage.reviewable,
|
|
1550
|
+
customPolicyFiles: coverage.customFiles,
|
|
1551
|
+
// The checks THIS machine's packs give; Cloud's are asked on Cloud.
|
|
1552
|
+
jevChecks: coverage.jevChecks,
|
|
1553
|
+
// "No checks installed" is not a problem here: FailproofAI Cloud's
|
|
1554
|
+
// checks are asked whatever this machine has installed.
|
|
1555
|
+
problem: coverage.jevChecks === 0 ? null : reviewableProblem(coverage),
|
|
1556
|
+
},
|
|
1557
|
+
}
|
|
1558
|
+
: {}),
|
|
1559
|
+
stats,
|
|
1560
|
+
};
|
|
1561
|
+
return ok([], JSON.stringify(body, null, 2));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const head =
|
|
1565
|
+
mode === "off"
|
|
1566
|
+
? jevHeader("Jev status", `off — ${MODE_SET_BY_CLOUD}`, opts)
|
|
1567
|
+
: running
|
|
1568
|
+
? jevHeader("Jev status", `on · ${mode} — ${jevRunsOnCloudLine(mode)}`, opts)
|
|
1569
|
+
: jevHeader("Jev status", `off — FailproofAI Cloud set ${mode}, and this machine cannot run it`, opts);
|
|
1570
|
+
const reviewerRows = reviewers.map((r): [string, string] => [
|
|
1571
|
+
`${r.policy} v${r.version}`,
|
|
1572
|
+
`${r.reviewedBy.join(", ")}${r.effect === "observe" ? " (not asked while observed)" : ""}`,
|
|
1573
|
+
]);
|
|
1574
|
+
return ok(
|
|
1575
|
+
stack(
|
|
1576
|
+
head,
|
|
1577
|
+
mode === "off"
|
|
1578
|
+
? jevNote(
|
|
1579
|
+
`FailproofAI Cloud switched Jev off on this machine. That overrides ${jevConfigPath()} for as long as this machine is connected.`,
|
|
1580
|
+
opts,
|
|
1581
|
+
)
|
|
1582
|
+
: jevNote(
|
|
1583
|
+
`${jevRunsOnCloudLine(mode)}. Every checked tool call goes to FailproofAI Cloud, which asks the Jev checks it ` +
|
|
1584
|
+
"deployed to this machine and this machine's installed packs' checks in one request; nothing of Cloud's checks is " +
|
|
1585
|
+
`installed here. ${jevConfigPath()} is not used while FailproofAI Cloud sets the mode. In enforce, Jev's own deny ` +
|
|
1586
|
+
"checks block calls as well as clearing the regex policies they review. Jev-only Cloud policies are not on this machine, so they are " +
|
|
1587
|
+
"not listed here; fp fleet show and the dashboard list them.",
|
|
1588
|
+
opts,
|
|
1589
|
+
),
|
|
1590
|
+
cfg && route
|
|
1591
|
+
? jevRows(
|
|
1592
|
+
[
|
|
1593
|
+
["runs on", "FailproofAI Cloud"],
|
|
1594
|
+
["endpoint", shownEndpoint(cfg.provider, route.endpoint)],
|
|
1595
|
+
["model", route.modelIsDefault ? `${route.model} (provider default)` : route.model],
|
|
1596
|
+
["mode", `${modeLine(mode)} (${MODE_SET_BY_CLOUD})`],
|
|
1597
|
+
["timeout", `${cfg.timeoutMs} ms`],
|
|
1598
|
+
],
|
|
1599
|
+
opts,
|
|
1600
|
+
)
|
|
1601
|
+
: jevRows([["mode", mode === "off" ? `${modeLine("off")} (${MODE_SET_BY_CLOUD})` : `${mode} (${MODE_SET_BY_CLOUD}) — not running`]], opts),
|
|
1602
|
+
resolved.problem ? jevWarn(cloudProblemAdvice(resolved.problem), opts) : null,
|
|
1603
|
+
reviewerRows.length > 0
|
|
1604
|
+
? stack(
|
|
1605
|
+
[screenKit(opts).head("Cloud reviewers", "FailproofAI Cloud policies reviewed by their Cloud Jev checks")],
|
|
1606
|
+
jevRows(reviewerRows, opts),
|
|
1607
|
+
)
|
|
1608
|
+
: [],
|
|
1609
|
+
cloud.errors.length > 0 ? jevWarn(cloud.errors.map((e) => `${e.id}: ${e.message}`), opts) : null,
|
|
1610
|
+
coverage ? jevNote(reviewableSummary(coverage), opts) : null,
|
|
1611
|
+
jevStatsLines(stats, opts),
|
|
1612
|
+
),
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function localStatus(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
|
|
1422
1617
|
const parsed = parseFlags(argv, new Set(["--json"]));
|
|
1423
1618
|
if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
|
|
1424
1619
|
if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
|
|
@@ -86,8 +86,100 @@ export type CloudJevConfigWrite =
|
|
|
86
86
|
/** Set when the kept file leaves Jev off here: refused, or switched off. */
|
|
87
87
|
jevOff?: { why: "refused"; problem: string; fix?: string } | { why: "off" };
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The Cloud's own `jev.json` (provider `failproofai`, as `config` writes it)
|
|
91
|
+
* named another FailproofAI Cloud origin, and was pointed at this one: same
|
|
92
|
+
* mode, same everything else. A BYOK file is never touched.
|
|
93
|
+
*/
|
|
94
|
+
| { status: "repointed"; path: string; from: string; baseUrl: string; mode: string }
|
|
89
95
|
| { status: "error"; path: string; problem: string };
|
|
90
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Create `jev.json` for the Cloud provider unless one exists. Never throws and
|
|
99
|
+
* never replaces a file — except the Cloud's own file pointing at another
|
|
100
|
+
* FailproofAI Cloud origin, which is re-pointed here (`repointCloudJevConfig`):
|
|
101
|
+
* a reconnect on a new URL (http → https) otherwise left Jev off, refused for
|
|
102
|
+
* naming an origin the key was not issued for.
|
|
103
|
+
*/
|
|
104
|
+
export function writeCloudJevConfig(cloudBase: string): CloudJevConfigWrite {
|
|
105
|
+
const first = writeCloudJevConfigIfAbsent(cloudBase);
|
|
106
|
+
if (first.status === "kept" && first.provider === JEV_CLOUD_PROVIDER && first.otherOrigin) {
|
|
107
|
+
return repointCloudJevConfig(cloudBase, first);
|
|
108
|
+
}
|
|
109
|
+
return first;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Point the Cloud's `jev.json` at `cloudBase`, keeping every other field. The
|
|
114
|
+
* file is moved aside first and judged there (as `removeCloudJevConfig` does),
|
|
115
|
+
* so a BYOK file renamed into place meanwhile is never the one rewritten, and
|
|
116
|
+
* the new file is linked in no-clobber.
|
|
117
|
+
*/
|
|
118
|
+
function repointCloudJevConfig(cloudBase: string, found: Extract<CloudJevConfigWrite, { status: "kept" }>): CloudJevConfigWrite {
|
|
119
|
+
const path = jevConfigPath();
|
|
120
|
+
let baseUrl: string;
|
|
121
|
+
try {
|
|
122
|
+
baseUrl = jevCloudBaseUrl(cloudBase);
|
|
123
|
+
} catch {
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
let st: Stats;
|
|
127
|
+
try {
|
|
128
|
+
st = lstatSync(path);
|
|
129
|
+
} catch {
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
if (!st.isFile()) return found;
|
|
133
|
+
const aside = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.repointing`;
|
|
134
|
+
try {
|
|
135
|
+
renameSync(path, aside);
|
|
136
|
+
} catch {
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
139
|
+
let raw: Record<string, unknown> | null = null;
|
|
140
|
+
try {
|
|
141
|
+
const parsed: unknown = JSON.parse(readFileSync(aside, "utf8"));
|
|
142
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) raw = parsed as Record<string, unknown>;
|
|
143
|
+
} catch {
|
|
144
|
+
raw = null;
|
|
145
|
+
}
|
|
146
|
+
if (!raw || raw.provider !== JEV_CLOUD_PROVIDER) {
|
|
147
|
+
// Not the Cloud's after all: back exactly as it was.
|
|
148
|
+
putBack(aside, path, st.mode & 0o777);
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
const body = `${JSON.stringify({ ...raw, baseUrl }, null, 2)}\n`;
|
|
152
|
+
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(tmp, body, { mode: 0o600, flag: "wx" });
|
|
155
|
+
chmodSync(tmp, 0o600);
|
|
156
|
+
linkSync(tmp, path);
|
|
157
|
+
} catch {
|
|
158
|
+
// Something landed at the path meanwhile, or the write failed: the old
|
|
159
|
+
// file goes back (never over the new one) and nothing was re-pointed.
|
|
160
|
+
putBack(aside, path, st.mode & 0o777);
|
|
161
|
+
return found;
|
|
162
|
+
} finally {
|
|
163
|
+
try {
|
|
164
|
+
rmSync(tmp, { force: true });
|
|
165
|
+
} catch {
|
|
166
|
+
// A stray temp file holds no key.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
unlinkSync(aside);
|
|
171
|
+
} catch {
|
|
172
|
+
// The aside copy is the old URL only; harmless if it lingers.
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
status: "repointed",
|
|
176
|
+
path,
|
|
177
|
+
from: found.otherOrigin ?? "",
|
|
178
|
+
baseUrl,
|
|
179
|
+
mode: typeof raw.mode === "string" ? raw.mode : DEFAULT_JEV_MODE,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
91
183
|
/**
|
|
92
184
|
* Create `jev.json` for the Cloud provider unless one exists. Never throws and
|
|
93
185
|
* never replaces a file.
|