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
package/src/hooks/manager.ts
CHANGED
|
@@ -32,7 +32,9 @@ import { getInstanceId, hashToId } from "../../lib/telemetry-id";
|
|
|
32
32
|
import { CliError } from "../cli-error";
|
|
33
33
|
import { hookLogWarn } from "./hook-logger";
|
|
34
34
|
import { customPoliciesDir } from "./fp-home";
|
|
35
|
-
import {
|
|
35
|
+
import { listActiveCloudManagedPolicies, readCloudJevAssignments, readCloudJevState, type CloudJevMode } from "./cloud-managed-policies";
|
|
36
|
+
import { readCloudPolicyErrors } from "./cloud-policy-errors";
|
|
37
|
+
import { readRosterLabels } from "./agent-roster";
|
|
36
38
|
import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
|
|
37
39
|
import type { ResolvedPack } from "./pack-manifest";
|
|
38
40
|
import { hasInstalledRegexPacks, readInstalledPacks } from "./pack-manifest";
|
|
@@ -332,6 +334,35 @@ export function integrationsInstalledAt(scope: HookScope, cwd?: string): Integra
|
|
|
332
334
|
*/
|
|
333
335
|
export type NotEnforcingReason = "no-hooks" | "no-policies" | "observe-only";
|
|
334
336
|
|
|
337
|
+
/** Machine-wide assignments, like the listing: a `both` policy counts once. */
|
|
338
|
+
function cloudPolicyStatus(): { count: number; observing: boolean } {
|
|
339
|
+
let count = 0;
|
|
340
|
+
let observing = false;
|
|
341
|
+
const jsIds = new Set<string>();
|
|
342
|
+
try {
|
|
343
|
+
for (const artifact of listActiveCloudManagedPolicies()) {
|
|
344
|
+
jsIds.add(artifact.id);
|
|
345
|
+
if (artifact.effect === "observe") observing = true;
|
|
346
|
+
else count++;
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// The listing reports an unreadable JS deployment; Jev is read independently.
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const { jevMode } = readCloudJevState();
|
|
353
|
+
if (jevMode === "enforce" || jevMode === "observe") {
|
|
354
|
+
for (const assignment of readCloudJevAssignments() ?? []) {
|
|
355
|
+
if (jsIds.has(assignment.id)) continue;
|
|
356
|
+
if (jevMode === "observe" || assignment.effect === "observe") observing = true;
|
|
357
|
+
else count++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// No readable Jev state: neither enforcement nor observation is known.
|
|
362
|
+
}
|
|
363
|
+
return { count, observing };
|
|
364
|
+
}
|
|
365
|
+
|
|
335
366
|
export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
336
367
|
const wired = deduplicateScopes(HOOK_SCOPES, cwd).some((scope) => hooksInstalledInSettings(scope, cwd));
|
|
337
368
|
if (!wired) return "no-hooks";
|
|
@@ -361,15 +392,11 @@ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
|
361
392
|
// An unreadable manifest is reported by the listing itself.
|
|
362
393
|
}
|
|
363
394
|
|
|
364
|
-
// Cloud
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
} catch {
|
|
371
|
-
// No deployment, or an unreadable one: nothing enforces from there.
|
|
372
|
-
}
|
|
395
|
+
// Cloud JS enforces independently of Jev mode. Jev-only assignments enforce
|
|
396
|
+
// only in Cloud's enforce mode, and watch only while Jev is not off.
|
|
397
|
+
const cloud = cloudPolicyStatus();
|
|
398
|
+
if (cloud.count > 0) return null;
|
|
399
|
+
observing ||= cloud.observing;
|
|
373
400
|
|
|
374
401
|
// A configured custom policy file, or convention files on disk. Loading them
|
|
375
402
|
// to ask what they hold would import user code on every launch, so their
|
|
@@ -390,10 +417,11 @@ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
|
390
417
|
* How many policies enforce on this machine, for the line that says so.
|
|
391
418
|
*
|
|
392
419
|
* Counted the same way `notEnforcingReason` decides, so the two never disagree:
|
|
393
|
-
* pack policies that are taken and not switched off,
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
420
|
+
* pack policies that are taken and not switched off, Cloud JS policies and
|
|
421
|
+
* Jev-only assignments in Cloud's enforce mode, and the legacy builtins of a
|
|
422
|
+
* machine still on the migration shim — observe-only ones excluded, `both`
|
|
423
|
+
* assignments counted once. Custom policy files cannot be counted without
|
|
424
|
+
* importing them, so their presence is reported on its own.
|
|
397
425
|
*/
|
|
398
426
|
export function enforcingPolicyCount(cwd?: string): { count: number; custom: boolean } {
|
|
399
427
|
const config = readMergedHooksConfig(cwd);
|
|
@@ -408,13 +436,7 @@ export function enforcingPolicyCount(cwd?: string): { count: number; custom: boo
|
|
|
408
436
|
} catch {
|
|
409
437
|
// An unreadable manifest counts nothing, as it enforces nothing.
|
|
410
438
|
}
|
|
411
|
-
|
|
412
|
-
for (const artifact of readActiveCloudManagedPolicies()) {
|
|
413
|
-
if (artifact.effect !== "observe") count++;
|
|
414
|
-
}
|
|
415
|
-
} catch {
|
|
416
|
-
// No deployment.
|
|
417
|
-
}
|
|
439
|
+
count += cloudPolicyStatus().count;
|
|
418
440
|
if (!hasInstalledRegexPacks()) count += config.enabledPolicies.length;
|
|
419
441
|
const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
|
|
420
442
|
const custom =
|
|
@@ -1245,7 +1267,16 @@ export interface PoliciesScreen {
|
|
|
1245
1267
|
attention: { failed: boolean; text: string; fix?: string } | null;
|
|
1246
1268
|
custom: PoliciesScreenRow[];
|
|
1247
1269
|
convention: Array<{ scope: string; rows: PoliciesScreenRow[] }>;
|
|
1248
|
-
cloud: {
|
|
1270
|
+
cloud: {
|
|
1271
|
+
deployment: number | null;
|
|
1272
|
+
rows: PoliciesScreenRow[];
|
|
1273
|
+
/** FailproofAI Cloud's Jev mode for this machine, when the deployment sets one. */
|
|
1274
|
+
jevMode?: CloudJevMode | null;
|
|
1275
|
+
/** Problems reading the deployment, each one line. */
|
|
1276
|
+
problems?: string[];
|
|
1277
|
+
/** The manifest names its Jev assignments (`jevPolicies`), so Jev-only policies are among `rows`. */
|
|
1278
|
+
jevListed?: boolean;
|
|
1279
|
+
} | null;
|
|
1249
1280
|
/**
|
|
1250
1281
|
* Builtins this build enforces itself because no pack is installed: the
|
|
1251
1282
|
* migration shim (`handler.ts` registers `enabledPolicies` when
|
|
@@ -1376,7 +1407,22 @@ export function renderPoliciesScreen(
|
|
|
1376
1407
|
}
|
|
1377
1408
|
if (screen.custom.length > 0) plain("Custom policies", undefined, screen.custom);
|
|
1378
1409
|
for (const { scope, rows } of screen.convention) plain("Convention policies", scope, rows);
|
|
1379
|
-
if (screen.cloud)
|
|
1410
|
+
if (screen.cloud) {
|
|
1411
|
+
const { deployment, rows, jevMode, problems } = screen.cloud;
|
|
1412
|
+
plain("Cloud-managed", deployment !== null ? `deployment ${deployment}` : undefined, rows);
|
|
1413
|
+
if (jevMode === "off") {
|
|
1414
|
+
out.push(`${INDENT}Jev mode: off, set by FailproofAI Cloud (overrides jev.json).`);
|
|
1415
|
+
} else if (jevMode) {
|
|
1416
|
+
out.push(`${INDENT}Jev mode: ${jevMode}, set by FailproofAI Cloud. Jev checks run on FailproofAI Cloud; nothing of them is installed here.`);
|
|
1417
|
+
// A manifest from a server that predates `jevPolicies` does not name its
|
|
1418
|
+
// Jev-only policies: say so, or an empty section reads as "Cloud
|
|
1419
|
+
// deployed nothing".
|
|
1420
|
+
if (!screen.cloud.jevListed) {
|
|
1421
|
+
out.push(`${INDENT}Jev-only Cloud policies are not listed here; see them with ${kit.cmd("fp fleet show")} or on the dashboard.`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
for (const problem of problems ?? []) out.push(kit.caution(problem));
|
|
1425
|
+
}
|
|
1380
1426
|
|
|
1381
1427
|
if (folded > 0) out.push("", `See all ${total} with ${kit.cmd("failproofai policies --all")}`);
|
|
1382
1428
|
return out;
|
|
@@ -1591,20 +1637,114 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
|
|
|
1591
1637
|
// pushed to a fleet were the ones invisible to the person running the
|
|
1592
1638
|
// command on it. Read-only: they belong to the deployment, which the heading
|
|
1593
1639
|
// says, and `--uninstall <name>` cannot switch one off.
|
|
1640
|
+
//
|
|
1641
|
+
// Each policy's KIND is shown — `regex` (JS) or `both` (JS whose verdict
|
|
1642
|
+
// its own Jev checks may clear) — with the Jev check names a `both` policy
|
|
1643
|
+
// is reviewed by. Those checks, and every `jev` policy, run on FailproofAI
|
|
1644
|
+
// Cloud (CONTRACT C10): no question is on this machine, but the manifest's
|
|
1645
|
+
// `jevPolicies` names each Jev assignment, so a Jev-only policy gets a row
|
|
1646
|
+
// (tagged `jev`) like the JS ones.
|
|
1594
1647
|
let cloud: PoliciesScreen["cloud"] = null;
|
|
1595
1648
|
try {
|
|
1596
|
-
|
|
1597
|
-
|
|
1649
|
+
// Every assignment, agent-scoped ones included (B4): this lists the
|
|
1650
|
+
// machine's deployment, not what one hook would load.
|
|
1651
|
+
let active: ReturnType<typeof listActiveCloudManagedPolicies> = [];
|
|
1652
|
+
let jsProblem: string | null = null;
|
|
1653
|
+
try {
|
|
1654
|
+
active = listActiveCloudManagedPolicies();
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
// The Jev mode is read independently below; a machine Cloud set one on
|
|
1657
|
+
// still gets its section, with this said.
|
|
1658
|
+
jsProblem = err instanceof Error ? err.message : String(err);
|
|
1659
|
+
}
|
|
1660
|
+
let jev: ReturnType<typeof readCloudJevState> = { jevMode: null, deployment: null, errors: [] };
|
|
1661
|
+
try {
|
|
1662
|
+
jev = readCloudJevState();
|
|
1663
|
+
} catch {
|
|
1664
|
+
// Never throws by contract; guarded anyway, for the same reason as above.
|
|
1665
|
+
}
|
|
1666
|
+
// A Cloud JS policy that did not load (B5): the hook path records it in
|
|
1667
|
+
// errors.json for the deployment in force, so the row says "not loaded"
|
|
1668
|
+
// rather than ● on.
|
|
1669
|
+
const loadErrors = new Map<string, string>();
|
|
1670
|
+
try {
|
|
1671
|
+
for (const e of readCloudPolicyErrors() ?? []) {
|
|
1672
|
+
if (e.kind !== "regex" && e.kind !== "both") continue;
|
|
1673
|
+
const artifact = active.find((a) => a.id === e.id);
|
|
1674
|
+
if (artifact && (e.version === null || e.version === artifact.version)) loadErrors.set(e.id, e.message);
|
|
1675
|
+
}
|
|
1676
|
+
} catch {
|
|
1677
|
+
// No report: every row reads as the deployment says.
|
|
1678
|
+
}
|
|
1679
|
+
// A policy deployed here is drawn ● on. Only the hook path imports Cloud
|
|
1680
|
+
// JavaScript, so one that fails to load is known only after a tool call,
|
|
1681
|
+
// and then shows as not loaded (errors.json, above) — never as a transient
|
|
1682
|
+
// "not loaded yet" state of its own.
|
|
1683
|
+
// Jev-only assignments (`jevPolicies`): ids, versions, effects and scopes,
|
|
1684
|
+
// never a question. A `both` policy is in both lists and is drawn once,
|
|
1685
|
+
// from its JS row. Null from a server that predates the list.
|
|
1686
|
+
let jevAssignments: ReturnType<typeof readCloudJevAssignments> = null;
|
|
1687
|
+
try {
|
|
1688
|
+
jevAssignments = readCloudJevAssignments();
|
|
1689
|
+
} catch {
|
|
1690
|
+
// Never throws by contract.
|
|
1691
|
+
}
|
|
1692
|
+
const jsIds = new Set(active.map((a) => a.id));
|
|
1693
|
+
const jevOnly = (jevAssignments ?? []).filter((a) => !jsIds.has(a.id));
|
|
1694
|
+
const labels = [...active, ...jevOnly].some((a) => a.agentTargets?.some((tg) => tg.instanceId))
|
|
1695
|
+
? readRosterLabels()
|
|
1696
|
+
: new Map<string, string>();
|
|
1697
|
+
const targetText = (targets: NonNullable<(typeof active)[number]["agentTargets"]>): string =>
|
|
1698
|
+
`for ${targets
|
|
1699
|
+
.map((tg) => (tg.instanceId ? `${tg.integration} / ${labels.get(tg.instanceId) ?? tg.instanceId}` : tg.integration))
|
|
1700
|
+
.join(", ")}`;
|
|
1701
|
+
if (active.length > 0 || jevOnly.length > 0 || jev.jevMode !== null) {
|
|
1702
|
+
const jevRows: PoliciesScreenRow[] = jevOnly.map((a) => {
|
|
1703
|
+
const tags = [
|
|
1704
|
+
...(a.effect === "observe" ? ["observe"] : []),
|
|
1705
|
+
"jev",
|
|
1706
|
+
...(a.agentTargets?.length ? [targetText(a.agentTargets)] : []),
|
|
1707
|
+
];
|
|
1708
|
+
return { state: "on" as const, name: a.id, description: `v${a.version}`, tag: tags.join(" · ") };
|
|
1709
|
+
});
|
|
1598
1710
|
cloud = {
|
|
1599
|
-
deployment: active[0].deployment,
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1711
|
+
deployment: active[0]?.deployment ?? jev.deployment ?? null,
|
|
1712
|
+
jevListed: jevAssignments !== null,
|
|
1713
|
+
rows: [...active.map((artifact) => {
|
|
1714
|
+
// Only a `both` policy carries a server-derived reviewer list (C1).
|
|
1715
|
+
const both = artifact.authority === "reviewable" && (artifact.reviewedBy?.length ?? 0) > 0;
|
|
1716
|
+
const checks = both ? (artifact.reviewedBy ?? []).join(", ") : "";
|
|
1717
|
+
const tags = [
|
|
1718
|
+
// `observe` is evaluated and then has its verdict discarded, so the
|
|
1719
|
+
// row says so rather than reading as plain enforcement.
|
|
1720
|
+
...(artifact.effect === "observe" ? ["observe"] : []),
|
|
1721
|
+
// A plain JS policy stays the plain row it always was.
|
|
1722
|
+
...(both ? ["both"] : []),
|
|
1723
|
+
// An observe `both` has its Jev half withheld by Cloud (C9.3).
|
|
1724
|
+
...(both ? [artifact.effect === "observe" ? `${checks} (not asked while observed)` : `Jev checks: ${checks}`] : []),
|
|
1725
|
+
...(artifact.agentTargets?.length ? [targetText(artifact.agentTargets)] : []),
|
|
1726
|
+
];
|
|
1727
|
+
const failed = loadErrors.get(artifact.id);
|
|
1728
|
+
if (failed !== undefined) {
|
|
1729
|
+
return {
|
|
1730
|
+
state: "failed" as const,
|
|
1731
|
+
name: artifact.id,
|
|
1732
|
+
description: `v${artifact.version}`,
|
|
1733
|
+
tag: [...tags, `not loaded: ${failed}`].join(" · "),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
return {
|
|
1737
|
+
state: "on" as const,
|
|
1738
|
+
name: artifact.id,
|
|
1739
|
+
description: `v${artifact.version}`,
|
|
1740
|
+
...(tags.length > 0 ? { tag: tags.join(" · ") } : {}),
|
|
1741
|
+
};
|
|
1742
|
+
}), ...jevRows],
|
|
1743
|
+
jevMode: jev.jevMode,
|
|
1744
|
+
problems: [
|
|
1745
|
+
...(jsProblem !== null ? [`The Cloud JavaScript policies could not be read, so none of them is enforcing: ${jsProblem}`] : []),
|
|
1746
|
+
...jev.errors.map((e) => `${e.id}${e.version === null ? "" : ` v${e.version}`}: ${e.message}`),
|
|
1747
|
+
],
|
|
1608
1748
|
};
|
|
1609
1749
|
}
|
|
1610
1750
|
} catch {
|
|
@@ -54,6 +54,8 @@ export interface MissingGuard {
|
|
|
54
54
|
*/
|
|
55
55
|
export const PERMANENT_LOAD_FAILURES: ReadonlySet<string> = new Set([
|
|
56
56
|
"module_not_found",
|
|
57
|
+
// A data file the policy reads at load is as missing on the next call.
|
|
58
|
+
"file_not_found",
|
|
57
59
|
"syntax_error",
|
|
58
60
|
"runtime_error",
|
|
59
61
|
"path_missing",
|
|
@@ -738,8 +738,15 @@ export function semanticQuestions(
|
|
|
738
738
|
* Per ENTRY, not per pack: one malformed question set must not take the rest of
|
|
739
739
|
* a publisher's semantic policies with it, for the same reason one malformed
|
|
740
740
|
* pack does not take the other packs with it.
|
|
741
|
+
*
|
|
742
|
+
* Exported for one reader outside this file: the shared FailproofAI Cloud
|
|
743
|
+
* fixtures test (`cloud-jev-policies.test.ts`). A Cloud Jev declaration is
|
|
744
|
+
* exactly a manifest semantic entry, and Cloud's publish validation mirrors
|
|
745
|
+
* this parser — so the test pins that both accept and refuse the same
|
|
746
|
+
* declarations, and measure their questions alike. The machine itself never
|
|
747
|
+
* parses a Cloud declaration: those checks run on FailproofAI Cloud (C10).
|
|
741
748
|
*/
|
|
742
|
-
function parsePackSemantic(packId: string, value: unknown, warnings: string[]): SemanticManifestEntry[] {
|
|
749
|
+
export function parsePackSemantic(packId: string, value: unknown, warnings: string[]): SemanticManifestEntry[] {
|
|
743
750
|
if (value === undefined) return [];
|
|
744
751
|
if (!Array.isArray(value)) {
|
|
745
752
|
warnings.push(`pack ${packId} semantic is not an array, so none of it was loaded`);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type JevReview,
|
|
26
26
|
type RegexVerdict,
|
|
27
27
|
type ObserveVerdict,
|
|
28
|
+
type ObservedCloudVerdict,
|
|
28
29
|
type TwoTierReview,
|
|
29
30
|
} from "./semantic/combine";
|
|
30
31
|
// The code, not a copy of the string: the log line below turns on it, and a
|
|
@@ -64,6 +65,8 @@ export interface EvaluationResult {
|
|
|
64
65
|
jevOrigin?: { packId: string; packVersion?: string };
|
|
65
66
|
/** Observe mode: Jev's own deny/instruct, recorded as a "would have" (see `ObserveVerdict`). */
|
|
66
67
|
observeVerdict?: ObserveVerdict;
|
|
68
|
+
/** An `observe` Cloud Jev assignment's would-haves (see `JevReview.observedCloud`). */
|
|
69
|
+
observedCloud?: ReadonlyArray<ObservedCloudVerdict>;
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -217,6 +220,19 @@ export async function evaluatePolicies(
|
|
|
217
220
|
twoTier.releaseRegistry?.();
|
|
218
221
|
review = await twoTier.review;
|
|
219
222
|
}
|
|
223
|
+
// Jev asked no check about this call (Cloud's short-circuit, or nothing
|
|
224
|
+
// selected): it took no part, so the row carries no Jev fields at all — the
|
|
225
|
+
// regex result, exactly as without a review.
|
|
226
|
+
if (review.kind === "answered" && review.asked.length === 0 && review.decision === "allow" && !review.observedAsked?.length) {
|
|
227
|
+
return formatVerdict(eventType, session, toolName, regexOnly(collected.verdicts));
|
|
228
|
+
}
|
|
229
|
+
// A hard regex deny decided before FailproofAI Cloud's Jev answered, and the
|
|
230
|
+
// request was dropped: Jev took no part, so the row is the regex (Cloud or
|
|
231
|
+
// local) policy's, with no Jev fields. A local `jev.json` keeps its
|
|
232
|
+
// `not-consulted` row, which `jev status` counts.
|
|
233
|
+
if (review.kind === "not-consulted" && twoTier.cloud) {
|
|
234
|
+
return formatVerdict(eventType, session, toolName, regexOnly(collected.verdicts));
|
|
235
|
+
}
|
|
220
236
|
const combined = combineTwoTier(collected.verdicts, review, twoTier.mode);
|
|
221
237
|
if (combined.cleared.length > 0) {
|
|
222
238
|
hookLogInfo(`jev ${twoTier.mode === "observe" ? "would clear" : "cleared"}: ${combined.cleared.join(", ")}`);
|
|
@@ -250,8 +266,13 @@ export async function evaluatePolicies(
|
|
|
250
266
|
twoTier: {
|
|
251
267
|
activity: combined.activity,
|
|
252
268
|
decidedByJev: combined.decidedByJev,
|
|
253
|
-
|
|
269
|
+
// The deciding check's origin — and, in observe mode, the origin of the
|
|
270
|
+
// check that WOULD have decided (B12), so that row names its policy too.
|
|
271
|
+
...((combined.decidedByJev || combined.observeVerdict) && review.kind === "answered" && review.origin
|
|
272
|
+
? { jevOrigin: review.origin }
|
|
273
|
+
: {}),
|
|
254
274
|
...(combined.observeVerdict ? { observeVerdict: combined.observeVerdict } : {}),
|
|
275
|
+
...(review.kind === "answered" && review.observedCloud?.length ? { observedCloud: review.observedCloud } : {}),
|
|
255
276
|
},
|
|
256
277
|
};
|
|
257
278
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { HookEventType } from "./types";
|
|
9
9
|
import type { PolicyFunction, PolicyMatcher, PolicyParamsSchema, RegisteredPolicy } from "./policy-types";
|
|
10
10
|
import { effectiveReviewerNames, forgetEffectiveReviewerNames } from "./effective-reviewers";
|
|
11
|
+
import type { AgentIdentity } from "./agent-targets";
|
|
11
12
|
import { resolvePolicyAuthority, type AuthorityDeclaration } from "./policy-authority";
|
|
12
13
|
|
|
13
14
|
const REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__";
|
|
@@ -126,14 +127,14 @@ export function getPoliciesForEvent(
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/** `cli`: the agent this pass registers for, which scopes the Jev reviewer set. */
|
|
129
|
-
export function clearPolicies(cli?: string): void {
|
|
130
|
+
export function clearPolicies(cli?: string, agent: AgentIdentity | null = null): void {
|
|
130
131
|
const g = globalThis as GlobalWithRegistry;
|
|
131
132
|
g[REGISTRY_KEY] = [];
|
|
132
133
|
setIndexCache(null);
|
|
133
134
|
// The reviewer set describes the policies that are about to be registered, so
|
|
134
135
|
// it is rebuilt with them. Dropping it here is also what keeps one read per
|
|
135
136
|
// evaluation instead of one per policy.
|
|
136
|
-
forgetEffectiveReviewerNames(cli);
|
|
137
|
+
forgetEffectiveReviewerNames(cli, agent);
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
@@ -55,7 +55,12 @@
|
|
|
55
55
|
* - **A session pause.** It suspends local policy for minutes, and a warning
|
|
56
56
|
* about a policy set that is coming back shortly would be noise.
|
|
57
57
|
*/
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
cloudAuthorityDeclaration,
|
|
60
|
+
cloudReviewerNames,
|
|
61
|
+
listActiveCloudManagedPolicies,
|
|
62
|
+
readCloudJevMode,
|
|
63
|
+
} from "./cloud-managed-policies";
|
|
59
64
|
import { jevPacks } from "./effective-reviewers";
|
|
60
65
|
import { resolve } from "node:path";
|
|
61
66
|
import { discoverPolicyFiles } from "./custom-hooks-loader";
|
|
@@ -202,9 +207,17 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
// Exactly as `handler.ts` registers them: a `both` policy is reviewable
|
|
211
|
+
// only while FailproofAI Cloud's Jev mode asks, and then only by its own
|
|
212
|
+
// Cloud checks (`cloud:<id>/<name>`), which this machine cannot list but
|
|
213
|
+
// Cloud answers for. No mode, `off`, or an `observe` assignment: hard.
|
|
214
|
+
const mode = readCloudJevMode();
|
|
215
|
+
// Listed for every agent: this survey has no agent, and the hook-scoped
|
|
216
|
+
// read with none drops each agent-targeted assignment.
|
|
217
|
+
const assignments = listActiveCloudManagedPolicies();
|
|
218
|
+
const cloudReviewers = cloudReviewerNames(assignments, mode);
|
|
219
|
+
if (cloudReviewers.length > 0) reviewers = new Set([...(reviewers ?? []), ...cloudReviewers]);
|
|
220
|
+
for (const assignment of assignments) records.push(cloudAuthorityDeclaration(assignment, mode));
|
|
208
221
|
} catch {
|
|
209
222
|
// Same fail-open as the handler's own read of this file.
|
|
210
223
|
}
|
|
@@ -225,7 +238,12 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
225
238
|
customFiles = 0;
|
|
226
239
|
}
|
|
227
240
|
|
|
228
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
...countReviewable(records, reviewers),
|
|
243
|
+
customFiles,
|
|
244
|
+
// The checks THIS machine asks; a Cloud policy's checks are FailproofAI Cloud's.
|
|
245
|
+
jevChecks: reviewers ? [...reviewers].filter((name) => !name.startsWith("cloud:")).length : 0,
|
|
246
|
+
};
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
/**
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FailproofAI Cloud Jev's health as this machine sees it (review M3).
|
|
3
|
+
*
|
|
4
|
+
* Under a Cloud Jev mode every gated tool call is a Cloud call, and every one
|
|
5
|
+
* that fails falls back to the regex alone. That trade is right for the hook
|
|
6
|
+
* path, and it used to be silent. This module adds two things:
|
|
7
|
+
*
|
|
8
|
+
* - **A circuit breaker.** After {@link BREAKER_FAILURES} Cloud Jev failures
|
|
9
|
+
* in a row (a timeout, no connection, a 5xx, a reply that cannot be used),
|
|
10
|
+
* Cloud Jev is not asked for {@link BREAKER_OPEN_MS}. The regex decides at
|
|
11
|
+
* once, instead of every tool call on the machine waiting out the Cloud
|
|
12
|
+
* timeout. Then ONE call goes through (half-open): an answer closes the
|
|
13
|
+
* breaker, and another failure opens it again. A 429 is not a failure here,
|
|
14
|
+
* because the Cloud transport already honours its Retry-After
|
|
15
|
+
* (`cloudRetryAfter` in `jev-client.ts`). Nor is a call this machine
|
|
16
|
+
* abandoned itself, or one its own settings stopped.
|
|
17
|
+
* - **A report** in `errors.json`, machine-level (`id: "jevMode"`, beside
|
|
18
|
+
* `jev_unconfigured`):
|
|
19
|
+
* - `jev_rate_limited: <n> calls fell back to regex in the last 10 min`,
|
|
20
|
+
* while FailproofAI Cloud refuses calls for the organization's Jev rate
|
|
21
|
+
* limit;
|
|
22
|
+
* - `jev_unavailable: …`, while the breaker is open.
|
|
23
|
+
*
|
|
24
|
+
* Both clear as soon as a call is answered again. The count is refreshed at
|
|
25
|
+
* most every {@link REPORT_REFRESH_MS}, so a burst of refusals does not
|
|
26
|
+
* become a burst of writes.
|
|
27
|
+
*
|
|
28
|
+
* The state is module-level, like the Retry-After cool-down and the throttle:
|
|
29
|
+
* the daemon's warm worker is the process that sees every call. A one-shot
|
|
30
|
+
* hook process, on a machine not set up yet, starts clean each time. The state
|
|
31
|
+
* is keyed by where the calls go (endpoint and machine id), so a reconnect
|
|
32
|
+
* starts afresh.
|
|
33
|
+
*/
|
|
34
|
+
import { readCloudPolicyErrors, writeCloudPolicyErrors, type CloudPolicyError } from "../cloud-policy-errors";
|
|
35
|
+
import type { JevReview } from "./combine";
|
|
36
|
+
|
|
37
|
+
/** Consecutive failures that open the breaker. */
|
|
38
|
+
export const BREAKER_FAILURES = 3;
|
|
39
|
+
/** How long an open breaker skips FailproofAI Cloud Jev before it lets one call through. */
|
|
40
|
+
export const BREAKER_OPEN_MS = 60_000;
|
|
41
|
+
/** The window `jev_rate_limited` counts over. */
|
|
42
|
+
export const RATE_LIMIT_WINDOW_MS = 10 * 60_000;
|
|
43
|
+
/** A changing count is re-reported at most this often. */
|
|
44
|
+
export const REPORT_REFRESH_MS = 30_000;
|
|
45
|
+
|
|
46
|
+
/** Reported while FailproofAI Cloud refuses this machine's calls for the org's Jev rate limit. */
|
|
47
|
+
export const JEV_RATE_LIMITED = "jev_rate_limited";
|
|
48
|
+
/** Reported while the breaker is open. */
|
|
49
|
+
export const JEV_UNAVAILABLE = "jev_unavailable";
|
|
50
|
+
|
|
51
|
+
/** The fallback reason of a call the open breaker did not send (a known activity code). */
|
|
52
|
+
export const BREAKER_SKIP_REASON = "unavailable";
|
|
53
|
+
|
|
54
|
+
interface State {
|
|
55
|
+
scope: string;
|
|
56
|
+
failures: number;
|
|
57
|
+
lastFailure: string | null;
|
|
58
|
+
/** When an open breaker lets its next call through; null = closed. */
|
|
59
|
+
openUntil: number | null;
|
|
60
|
+
/** The half-open trial call is in flight: the others still skip. */
|
|
61
|
+
probing: boolean;
|
|
62
|
+
/** When each rate-limited fallback happened, within the window. */
|
|
63
|
+
rateLimited: number[];
|
|
64
|
+
/** The count last reported, and when, so the report changes at most every REPORT_REFRESH_MS. */
|
|
65
|
+
reportedRate: { n: number; at: number } | null;
|
|
66
|
+
/** The entries last merged into `errors.json` by {@link reportCloudJevHealth}. */
|
|
67
|
+
lastReport: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fresh = (scope: string): State => ({
|
|
71
|
+
scope,
|
|
72
|
+
failures: 0,
|
|
73
|
+
lastFailure: null,
|
|
74
|
+
openUntil: null,
|
|
75
|
+
probing: false,
|
|
76
|
+
rateLimited: [],
|
|
77
|
+
reportedRate: null,
|
|
78
|
+
lastReport: "[]",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let state: State = fresh("");
|
|
82
|
+
const clock = (): number => performance.now();
|
|
83
|
+
|
|
84
|
+
/** Tests only. */
|
|
85
|
+
export function _resetCloudJevHealthForTest(): void {
|
|
86
|
+
state = fresh("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function forScope(scope: string): State {
|
|
90
|
+
if (state.scope !== scope) state = fresh(scope);
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether a call may go to FailproofAI Cloud now; `probe` marks the half-open trial. */
|
|
95
|
+
export type CloudJevGate = { ask: true; probe: boolean } | { ask: false };
|
|
96
|
+
|
|
97
|
+
/** Ask the breaker before sending a call. Taking the trial (`probe: true`) must be followed by {@link recordCloudJevResult}. */
|
|
98
|
+
export function cloudJevGate(scope: string, at: number = clock()): CloudJevGate {
|
|
99
|
+
const s = forScope(scope);
|
|
100
|
+
if (s.openUntil === null) return { ask: true, probe: false };
|
|
101
|
+
if (at < s.openUntil || s.probing) return { ask: false };
|
|
102
|
+
s.probing = true;
|
|
103
|
+
return { ask: true, probe: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What one Cloud call's review says about Cloud's health. */
|
|
107
|
+
export type CloudJevResult = "answered" | "rate-limited" | "failed" | "neutral";
|
|
108
|
+
|
|
109
|
+
/** Fallback codes that say FailproofAI Cloud Jev could not be used; `http-5xx` too. */
|
|
110
|
+
const FAILURES: ReadonlySet<string> = new Set(["timeout", "network", "malformed", "model-mismatch", "upstream-error", "error"]);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* - `answered`: Jev's verdict arrived.
|
|
114
|
+
* - `rate-limited`: FailproofAI Cloud refused the call for a rate limit, or its
|
|
115
|
+
* Retry-After held the call (`http-429`); or the machine's own request budget
|
|
116
|
+
* stopped it (`rate-limited`), which a Cloud 429 empties so the calls right
|
|
117
|
+
* behind it do not go out (`jev-throttle.ts`).
|
|
118
|
+
* - `failed`: timeout, no connection, a 5xx, or a reply that could not be used.
|
|
119
|
+
* - `neutral`: anything that says nothing about Cloud. That covers a call this
|
|
120
|
+
* machine aborted (the regex decided first), its own config, and Cloud
|
|
121
|
+
* refusing THIS request (4xx): Cloud answered, so it is up.
|
|
122
|
+
*/
|
|
123
|
+
export function classifyCloudJevReview(
|
|
124
|
+
review: JevReview,
|
|
125
|
+
opts: { reachedCloud: boolean } = { reachedCloud: true },
|
|
126
|
+
): CloudJevResult {
|
|
127
|
+
if (review.kind === "answered") return opts.reachedCloud ? "answered" : "neutral";
|
|
128
|
+
if (review.kind !== "fallback") return "neutral";
|
|
129
|
+
const code = review.reason;
|
|
130
|
+
if (code === "http-429" || code === "rate-limited") return "rate-limited";
|
|
131
|
+
if (FAILURES.has(code) || /^http-5\d\d$/.test(code)) return "failed";
|
|
132
|
+
return "neutral";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Record one call's result against the breaker and the report. */
|
|
136
|
+
export function recordCloudJevResult(
|
|
137
|
+
scope: string,
|
|
138
|
+
result: CloudJevResult,
|
|
139
|
+
code: string | null,
|
|
140
|
+
probe: boolean,
|
|
141
|
+
at: number = clock(),
|
|
142
|
+
): void {
|
|
143
|
+
if (state.scope !== scope) return; // A call to a connection this machine no longer has.
|
|
144
|
+
const s = state;
|
|
145
|
+
if (probe) s.probing = false;
|
|
146
|
+
switch (result) {
|
|
147
|
+
case "answered":
|
|
148
|
+
s.failures = 0;
|
|
149
|
+
s.lastFailure = null;
|
|
150
|
+
s.openUntil = null;
|
|
151
|
+
s.rateLimited = [];
|
|
152
|
+
break;
|
|
153
|
+
case "rate-limited":
|
|
154
|
+
s.rateLimited.push(at);
|
|
155
|
+
// A 429 is Cloud answering, so it is up: the breaker's streak ends, and
|
|
156
|
+
// its Retry-After, not the breaker, holds the calls behind this one. The
|
|
157
|
+
// machine's own budget says nothing about Cloud either way.
|
|
158
|
+
if (code === "http-429") {
|
|
159
|
+
s.failures = 0;
|
|
160
|
+
s.openUntil = null;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
case "failed":
|
|
164
|
+
s.failures += 1;
|
|
165
|
+
s.lastFailure = code;
|
|
166
|
+
if (probe || s.failures >= BREAKER_FAILURES) s.openUntil = at + BREAKER_OPEN_MS;
|
|
167
|
+
break;
|
|
168
|
+
case "neutral":
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Is `message` one of this module's reports? */
|
|
174
|
+
export function isCloudJevHealthMessage(message: string): boolean {
|
|
175
|
+
return message.startsWith(JEV_RATE_LIMITED) || message.startsWith(JEV_UNAVAILABLE);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The report as it stands: `errors.json` entries, none when all is well. */
|
|
179
|
+
export function cloudJevHealthErrors(at: number = clock()): CloudPolicyError[] {
|
|
180
|
+
const s = state;
|
|
181
|
+
const out: CloudPolicyError[] = [];
|
|
182
|
+
s.rateLimited = s.rateLimited.filter((t) => at - t < RATE_LIMIT_WINDOW_MS);
|
|
183
|
+
const n = s.rateLimited.length;
|
|
184
|
+
if (n === 0) {
|
|
185
|
+
s.reportedRate = null;
|
|
186
|
+
} else {
|
|
187
|
+
if (s.reportedRate === null || at - s.reportedRate.at >= REPORT_REFRESH_MS) s.reportedRate = { n, at };
|
|
188
|
+
const minutes = Math.round(RATE_LIMIT_WINDOW_MS / 60_000);
|
|
189
|
+
const count = s.reportedRate.n;
|
|
190
|
+
out.push({
|
|
191
|
+
id: "jevMode",
|
|
192
|
+
version: null,
|
|
193
|
+
kind: "daemon",
|
|
194
|
+
message: `${JEV_RATE_LIMITED}: ${count} call${count === 1 ? "" : "s"} fell back to regex in the last ${minutes} min`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (s.openUntil !== null) {
|
|
198
|
+
out.push({
|
|
199
|
+
id: "jevMode",
|
|
200
|
+
version: null,
|
|
201
|
+
kind: "daemon",
|
|
202
|
+
message:
|
|
203
|
+
`${JEV_UNAVAILABLE}: FailproofAI Cloud Jev failed ${BREAKER_FAILURES} or more calls in a row ` +
|
|
204
|
+
`(last: ${s.lastFailure ?? "error"}), so tool calls skip it for ${BREAKER_OPEN_MS / 1000} s at a time ` +
|
|
205
|
+
"and the regex decides alone",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Merge the report into `errors.json` now, when it changed since this process
|
|
213
|
+
* last merged it, so the daemon's next poll carries it without waiting for the
|
|
214
|
+
* next hook. Every other entry is kept. Never throws.
|
|
215
|
+
*/
|
|
216
|
+
export function reportCloudJevHealth(at: number = clock()): void {
|
|
217
|
+
try {
|
|
218
|
+
const entries = cloudJevHealthErrors(at);
|
|
219
|
+
const text = JSON.stringify(entries);
|
|
220
|
+
if (text === state.lastReport) return;
|
|
221
|
+
state.lastReport = text;
|
|
222
|
+
const report = (readCloudPolicyErrors() ?? []).filter((e) => !(e.id === "jevMode" && isCloudJevHealthMessage(e.message)));
|
|
223
|
+
writeCloudPolicyErrors([...report, ...entries]);
|
|
224
|
+
} catch {
|
|
225
|
+
// A report must not cost a hook its answer; the next call reports again.
|
|
226
|
+
}
|
|
227
|
+
}
|