failproofai 1.0.12-beta.1 → 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.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.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 +2 -2
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.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/_1ek68lnh2daur._.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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00l2u6j86cb8y._.js +3 -0
- 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]__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]__0b78b0j8iyv6l._.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/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
- 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/_1fw0_nzkae5ao._.js +3 -0
- 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_1i8oid1gc7s1f.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_0agdmptetetdt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.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/{2-tf6_7u623kd.js → 0ay__-d2h1xb5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 0tbm3vjm62h-2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 1c_jf6g5wnslk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 1my3-5p47t7r-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0jutamwqz8pg5.js → 20py8k1gd_i1m.js} +1 -1
- 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/{42e8zab314654.js → 2o6d37j425uci.js} +1 -1
- package/.next/standalone/.next/static/chunks/40oworn_-x12p.js +1 -0
- package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.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 +9 -18
- package/.next/standalone/fp-cloud-cli/README.md +7 -9
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1 -27
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +2 -71
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +1 -1
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +6 -7
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
- package/.next/standalone/hermes-plugin/__init__.py +11 -0
- 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/dist/cli.mjs +1686 -612
- package/dist/worker.mjs +354 -49
- package/hermes-plugin/__init__.py +11 -0
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +11 -10
- package/src/audit/open-browser.ts +24 -7
- package/src/hooks/agent-roster.ts +9 -1
- package/src/hooks/agent-skill.ts +203 -0
- package/src/hooks/cloud-connection.ts +27 -51
- package/src/hooks/cloud-introspect.ts +5 -2
- package/src/hooks/cloud-login.ts +248 -0
- package/src/hooks/cloud-managed-policies.ts +54 -4
- package/src/hooks/codex-system-hooks.ts +10 -4
- package/src/hooks/config-status.ts +9 -3
- package/src/hooks/configure-wizard.ts +356 -32
- package/src/hooks/delivery-health.ts +47 -2
- package/src/hooks/effective-reviewers.ts +14 -0
- package/src/hooks/fp-config.ts +19 -0
- package/src/hooks/handler.ts +21 -2
- package/src/hooks/hook-activity-store.ts +7 -1
- package/src/hooks/integrations.ts +18 -3
- package/src/hooks/jev-activity.ts +117 -1
- package/src/hooks/jev-cli.ts +7 -9
- package/src/hooks/manager.ts +80 -46
- package/src/hooks/policy-evaluator.ts +17 -0
- package/src/hooks/policy-reviewability.ts +4 -2
- package/src/hooks/semantic/cloud-jev.ts +219 -28
- package/src/hooks/semantic/combine.ts +26 -1
- package/src/hooks/semantic/jev-client.ts +1 -1
- package/src/hooks/semantic/jev-config.ts +6 -27
- package/src/hooks/semantic/jev-review.ts +90 -2
- package/src/hooks/tui.ts +280 -0
- package/src/hooks/uninstall-cli.ts +12 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
- package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
- package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
- package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → LHCtFxVG0mdvd4lsXAOFC}/_ssgManifest.js +0 -0
package/src/hooks/fp-config.ts
CHANGED
|
@@ -940,6 +940,25 @@ export function readCredentials(): FpCredentials {
|
|
|
940
940
|
}
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Whether FailproofAI Cloud sets this machine's Jev mode: it is connected for
|
|
945
|
+
* policies (a machine id in `credentials.json`). Cloud then runs Jev in enforce
|
|
946
|
+
* while a Jev policy is deployed here and switches it off when none is; local
|
|
947
|
+
* mode settings do not apply. Never throws.
|
|
948
|
+
*/
|
|
949
|
+
export function cloudManagesJevMode(): boolean {
|
|
950
|
+
try {
|
|
951
|
+
const cloud = readCredentials().cloud;
|
|
952
|
+
return Boolean(cloud?.url && cloud.machineId);
|
|
953
|
+
} catch {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** The one line a local mode switch answers with on such a machine. */
|
|
959
|
+
export const CLOUD_MANAGED_JEV_MODE =
|
|
960
|
+
"Jev mode is managed by FailproofAI Cloud on this machine: enforce while a Jev policy is deployed here, off when none is.";
|
|
961
|
+
|
|
943
962
|
/**
|
|
944
963
|
* Project a parsed credentials object onto {@link FpCredentials}.
|
|
945
964
|
*
|
package/src/hooks/handler.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type { JevActivityFields } from "./semantic/combine";
|
|
|
38
38
|
import type { JevConfig } from "./semantic/jev-config";
|
|
39
39
|
import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
|
|
40
40
|
import { loadAllCustomHooks, type LoadAllResult } from "./custom-hooks-loader";
|
|
41
|
-
import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled } from "./effective-reviewers";
|
|
41
|
+
import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled, packJevChecksInstalled } from "./effective-reviewers";
|
|
42
42
|
import {
|
|
43
43
|
authorityDeclarationFor,
|
|
44
44
|
refusedAuthorityWarning,
|
|
@@ -58,6 +58,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
|
58
58
|
import { readStdinPayload } from "./read-stdin";
|
|
59
59
|
import {
|
|
60
60
|
cloudJevAsks,
|
|
61
|
+
cloudJevAssignmentsFor,
|
|
61
62
|
withCloudAuthority,
|
|
62
63
|
readActiveCloudManagedPolicies,
|
|
63
64
|
readCloudJevMode,
|
|
@@ -305,6 +306,8 @@ interface CloudJevRoute {
|
|
|
305
306
|
deployment: number | null;
|
|
306
307
|
mode: "observe" | "enforce";
|
|
307
308
|
agent: AgentIdentity | null;
|
|
309
|
+
/** Cloud policies whose Jev assignment here is `observe`: their answers are recorded, never applied. */
|
|
310
|
+
observedPolicies: string[];
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
/**
|
|
@@ -346,7 +349,13 @@ async function readJevConfig(agent?: AgentIdentity | null): Promise<{
|
|
|
346
349
|
return {
|
|
347
350
|
config: resolved.config,
|
|
348
351
|
defaultMode: DEFAULT_JEV_MODE,
|
|
349
|
-
cloud: {
|
|
352
|
+
cloud: {
|
|
353
|
+
machineId: resolved.machineId,
|
|
354
|
+
deployment: cloud.deployment,
|
|
355
|
+
mode: cloud.jevMode,
|
|
356
|
+
agent: agent ?? null,
|
|
357
|
+
observedPolicies: (cloudJevAssignmentsFor(agent ?? null) ?? []).filter((a) => a.effect === "observe").map((a) => a.id),
|
|
358
|
+
},
|
|
350
359
|
};
|
|
351
360
|
}
|
|
352
361
|
if (!existsSync(jevConfigFile())) return null;
|
|
@@ -475,6 +484,11 @@ async function startTwoTier(
|
|
|
475
484
|
// must not switch off what the org assigned centrally).
|
|
476
485
|
if (activePause && !cloudAsks) return null;
|
|
477
486
|
if (!cloudAsks && !jevChecksInstalled()) return null;
|
|
487
|
+
// No Cloud Jev check is assigned to this agent (the deployment says so) and
|
|
488
|
+
// no installed pack's check takes part: nothing to ask, so no Cloud call
|
|
489
|
+
// and no Jev fields on the row. An older deployment without the list
|
|
490
|
+
// (`null`) still sends every call, as before.
|
|
491
|
+
if (cloudAsks && cloudJevAssignmentsFor(agent)?.length === 0 && (activePause || !packJevChecksInstalled())) return null;
|
|
478
492
|
const loaded = await readJevConfig(agent);
|
|
479
493
|
if (!loaded) return null;
|
|
480
494
|
// Paused, and the config is not Cloud's after all (read in between): the
|
|
@@ -1158,6 +1172,11 @@ export async function evaluateHookEvent(
|
|
|
1158
1172
|
reason: observeVerdict.reason,
|
|
1159
1173
|
});
|
|
1160
1174
|
}
|
|
1175
|
+
// An `observe` Cloud Jev assignment's checks: filed under their Cloud
|
|
1176
|
+
// policy, like an observe Cloud JS policy's verdict.
|
|
1177
|
+
for (const o of result.twoTier?.observedCloud ?? []) {
|
|
1178
|
+
observedResults.push({ policyId: o.cloudPolicyId, version: o.cloudVersion, decision: o.decision, reason: o.reason });
|
|
1179
|
+
}
|
|
1161
1180
|
const durationMs = Math.round(performance.now() - startTime);
|
|
1162
1181
|
hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
|
|
1163
1182
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import type { IntegrationType } from "./types";
|
|
26
26
|
import { hookActivityDir } from "./fp-home";
|
|
27
|
-
import { sanitizeJevActivity } from "./jev-activity";
|
|
27
|
+
import { sanitizeJevActivity, type JevCheckRecord } from "./jev-activity";
|
|
28
28
|
|
|
29
29
|
export const PAGE_SIZE = 25;
|
|
30
30
|
|
|
@@ -107,6 +107,12 @@ export interface HookActivityEntry {
|
|
|
107
107
|
jevModel?: string;
|
|
108
108
|
/** `observe` logs Jev but enforces the regex result; `enforce` applies the combine rules. */
|
|
109
109
|
jevMode?: "observe" | "enforce";
|
|
110
|
+
/**
|
|
111
|
+
* Every Jev check asked on an answered call and what it concluded (verdict,
|
|
112
|
+
* why, evidence, exempt and probe answers) — names, codes and numbers only.
|
|
113
|
+
* `[]`: the request carried no check, so Jev decided nothing (`no-request`).
|
|
114
|
+
*/
|
|
115
|
+
jevChecks?: JevCheckRecord[];
|
|
110
116
|
durationMs: number;
|
|
111
117
|
sessionId?: string;
|
|
112
118
|
transcriptPath?: string;
|
|
@@ -26,7 +26,7 @@ import { fileURLToPath } from "node:url";
|
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { parseDocument, type Document } from "yaml";
|
|
28
28
|
import { writeConfigFileAtomic } from "./safe-config-write";
|
|
29
|
-
import { codexSystemHooksPresent, type CodexSystemHookEntry } from "./codex-system-hooks";
|
|
29
|
+
import { codexHome, codexSystemHooksPresent, type CodexSystemHookEntry } from "./codex-system-hooks";
|
|
30
30
|
import { listHermesProfiles, hermesRoot } from "../../lib/hermes-profiles";
|
|
31
31
|
import { listOpenClawProfiles } from "../../lib/openclaw-profiles";
|
|
32
32
|
import {
|
|
@@ -253,6 +253,21 @@ export function settingsPathsFor(
|
|
|
253
253
|
return paths && paths.length > 0 ? paths : [integration.getSettingsPath(scope, cwd)];
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
// ── Agent profile homes ─────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The user-scope config folder of the Claude Code profile this command runs
|
|
260
|
+
* for: `CLAUDE_CONFIG_DIR` when set (`~/.claude-work`), else `~/.claude`. The
|
|
261
|
+
* hook resolves the same variable (`runtimeAgentSettingsPath`), so install,
|
|
262
|
+
* uninstall and the roster all name one file per profile.
|
|
263
|
+
*/
|
|
264
|
+
export function claudeConfigDir(): string {
|
|
265
|
+
const dir = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
266
|
+
return dir ? resolve(dir) : resolve(homedir(), ".claude");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { codexHome };
|
|
270
|
+
|
|
256
271
|
// ── Claude Code integration ─────────────────────────────────────────────────
|
|
257
272
|
|
|
258
273
|
export const claudeCode: Integration = {
|
|
@@ -265,7 +280,7 @@ export const claudeCode: Integration = {
|
|
|
265
280
|
const base = cwd ? resolve(cwd) : process.cwd();
|
|
266
281
|
switch (scope) {
|
|
267
282
|
case "user":
|
|
268
|
-
return resolve(
|
|
283
|
+
return resolve(claudeConfigDir(), "settings.json");
|
|
269
284
|
case "project":
|
|
270
285
|
return resolve(base, ".claude", "settings.json");
|
|
271
286
|
case "local":
|
|
@@ -439,7 +454,7 @@ export const codex: Integration = {
|
|
|
439
454
|
const base = cwd ? resolve(cwd) : process.cwd();
|
|
440
455
|
switch (scope) {
|
|
441
456
|
case "user":
|
|
442
|
-
return resolve(
|
|
457
|
+
return resolve(codexHome(), "hooks.json");
|
|
443
458
|
case "project":
|
|
444
459
|
return resolve(base, ".codex", "hooks.json");
|
|
445
460
|
case "local":
|
|
@@ -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
|
@@ -147,7 +147,7 @@ import {
|
|
|
147
147
|
} from "./policy-reviewability";
|
|
148
148
|
import { jevStats, type JevStats } from "./semantic/jev-stats";
|
|
149
149
|
import { listActiveCloudManagedPolicies, readCloudJevState, type CloudJevState } from "./cloud-managed-policies";
|
|
150
|
-
import { readCredentials, readJevCloudCredential, type JevCloudCredential } from "./fp-config";
|
|
150
|
+
import { CLOUD_MANAGED_JEV_MODE, cloudManagesJevMode, readCredentials, readJevCloudCredential, type JevCloudCredential } from "./fp-config";
|
|
151
151
|
import type { JevRequest } from "./semantic/types";
|
|
152
152
|
import { TOKEN_ON_ARGV, optsFor, paint, screenKit, stack, wrap, type RenderOpts } from "./tui";
|
|
153
153
|
|
|
@@ -367,6 +367,9 @@ function cloudConnection(): { cloudConnected: boolean; keyCarriesJev: boolean }
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
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
|
+
|
|
370
373
|
/** The one line `status` titles a connected machine whose key has no Jev with. */
|
|
371
374
|
const KEY_LACKS_JEV_TITLE = "off — no Jev key is stored for this machine's FailproofAI Cloud connection";
|
|
372
375
|
|
|
@@ -689,6 +692,9 @@ async function setupRun(argv: string[], deps: JevCliDeps, opts: RenderOpts): Pro
|
|
|
689
692
|
if (typeof parsed === "string") return fail([parsed, "", ...JEV_USAGE]);
|
|
690
693
|
if (parsed.positionals.length > 0) return fail([STRAY_ARGUMENT, "", ...JEV_USAGE]);
|
|
691
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);
|
|
692
698
|
if (bools.has("--key-stdin") && bools.has("--key-from-env")) return fail(["Use one of --key-stdin and --key-from-env, not both."]);
|
|
693
699
|
// One source for the key, so no run has to decide which of two the person
|
|
694
700
|
// meant. The message names the recommended one first.
|
|
@@ -1479,14 +1485,6 @@ function cloudProblemAdvice(problem: string): string[] {
|
|
|
1479
1485
|
"To use FailproofAI Cloud's Jev checks, reconnect without --no-transcripts (failproofai config --token <key>).",
|
|
1480
1486
|
];
|
|
1481
1487
|
}
|
|
1482
|
-
// Over plain http the credential is fine; enforce is what http refuses.
|
|
1483
|
-
// Asking for another key there sent people after the wrong fix.
|
|
1484
|
-
if (/plain http/.test(problem)) {
|
|
1485
|
-
return [
|
|
1486
|
-
"Jev does not run here: FailproofAI Cloud set enforce, and this machine is connected over plain http, where Jev runs only in observe mode.",
|
|
1487
|
-
"Reconnect to an https FailproofAI Cloud URL (failproofai config --token <key> --url https://…), or set the machine's Jev mode to observe from FailproofAI Cloud.",
|
|
1488
|
-
];
|
|
1489
|
-
}
|
|
1490
1488
|
return [
|
|
1491
1489
|
`Jev cannot run here: ${problem}.`,
|
|
1492
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.",
|
package/src/hooks/manager.ts
CHANGED
|
@@ -32,8 +32,8 @@ 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 { listActiveCloudManagedPolicies, readCloudJevState, type CloudJevMode } from "./cloud-managed-policies";
|
|
36
|
-
import {
|
|
35
|
+
import { listActiveCloudManagedPolicies, readCloudJevAssignments, readCloudJevState, type CloudJevMode } from "./cloud-managed-policies";
|
|
36
|
+
import { readCloudPolicyErrors } from "./cloud-policy-errors";
|
|
37
37
|
import { readRosterLabels } from "./agent-roster";
|
|
38
38
|
import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
|
|
39
39
|
import type { ResolvedPack } from "./pack-manifest";
|
|
@@ -334,6 +334,35 @@ export function integrationsInstalledAt(scope: HookScope, cwd?: string): Integra
|
|
|
334
334
|
*/
|
|
335
335
|
export type NotEnforcingReason = "no-hooks" | "no-policies" | "observe-only";
|
|
336
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
|
+
|
|
337
366
|
export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
338
367
|
const wired = deduplicateScopes(HOOK_SCOPES, cwd).some((scope) => hooksInstalledInSettings(scope, cwd));
|
|
339
368
|
if (!wired) return "no-hooks";
|
|
@@ -363,15 +392,11 @@ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
|
363
392
|
// An unreadable manifest is reported by the listing itself.
|
|
364
393
|
}
|
|
365
394
|
|
|
366
|
-
// Cloud
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
} catch {
|
|
373
|
-
// No deployment, or an unreadable one: nothing enforces from there.
|
|
374
|
-
}
|
|
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;
|
|
375
400
|
|
|
376
401
|
// A configured custom policy file, or convention files on disk. Loading them
|
|
377
402
|
// to ask what they hold would import user code on every launch, so their
|
|
@@ -392,10 +417,11 @@ export function notEnforcingReason(cwd?: string): NotEnforcingReason | null {
|
|
|
392
417
|
* How many policies enforce on this machine, for the line that says so.
|
|
393
418
|
*
|
|
394
419
|
* Counted the same way `notEnforcingReason` decides, so the two never disagree:
|
|
395
|
-
* pack policies that are taken and not switched off,
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
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.
|
|
399
425
|
*/
|
|
400
426
|
export function enforcingPolicyCount(cwd?: string): { count: number; custom: boolean } {
|
|
401
427
|
const config = readMergedHooksConfig(cwd);
|
|
@@ -410,13 +436,7 @@ export function enforcingPolicyCount(cwd?: string): { count: number; custom: boo
|
|
|
410
436
|
} catch {
|
|
411
437
|
// An unreadable manifest counts nothing, as it enforces nothing.
|
|
412
438
|
}
|
|
413
|
-
|
|
414
|
-
for (const artifact of listActiveCloudManagedPolicies()) {
|
|
415
|
-
if (artifact.effect !== "observe") count++;
|
|
416
|
-
}
|
|
417
|
-
} catch {
|
|
418
|
-
// No deployment.
|
|
419
|
-
}
|
|
439
|
+
count += cloudPolicyStatus().count;
|
|
420
440
|
if (!hasInstalledRegexPacks()) count += config.enabledPolicies.length;
|
|
421
441
|
const projectDir = resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies");
|
|
422
442
|
const custom =
|
|
@@ -1254,6 +1274,8 @@ export interface PoliciesScreen {
|
|
|
1254
1274
|
jevMode?: CloudJevMode | null;
|
|
1255
1275
|
/** Problems reading the deployment, each one line. */
|
|
1256
1276
|
problems?: string[];
|
|
1277
|
+
/** The manifest names its Jev assignments (`jevPolicies`), so Jev-only policies are among `rows`. */
|
|
1278
|
+
jevListed?: boolean;
|
|
1257
1279
|
} | null;
|
|
1258
1280
|
/**
|
|
1259
1281
|
* Builtins this build enforces itself because no pack is installed: the
|
|
@@ -1392,9 +1414,12 @@ export function renderPoliciesScreen(
|
|
|
1392
1414
|
out.push(`${INDENT}Jev mode: off, set by FailproofAI Cloud (overrides jev.json).`);
|
|
1393
1415
|
} else if (jevMode) {
|
|
1394
1416
|
out.push(`${INDENT}Jev mode: ${jevMode}, set by FailproofAI Cloud. Jev checks run on FailproofAI Cloud; nothing of them is installed here.`);
|
|
1395
|
-
//
|
|
1396
|
-
// empty section reads as "Cloud
|
|
1397
|
-
|
|
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
|
+
}
|
|
1398
1423
|
}
|
|
1399
1424
|
for (const problem of problems ?? []) out.push(kit.caution(problem));
|
|
1400
1425
|
}
|
|
@@ -1616,8 +1641,9 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
|
|
|
1616
1641
|
// Each policy's KIND is shown — `regex` (JS) or `both` (JS whose verdict
|
|
1617
1642
|
// its own Jev checks may clear) — with the Jev check names a `both` policy
|
|
1618
1643
|
// is reviewed by. Those checks, and every `jev` policy, run on FailproofAI
|
|
1619
|
-
// Cloud (CONTRACT C10):
|
|
1620
|
-
//
|
|
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.
|
|
1621
1647
|
let cloud: PoliciesScreen["cloud"] = null;
|
|
1622
1648
|
try {
|
|
1623
1649
|
// Every assignment, agent-scoped ones included (B4): this lists the
|
|
@@ -1650,25 +1676,41 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
|
|
|
1650
1676
|
} catch {
|
|
1651
1677
|
// No report: every row reads as the deployment says.
|
|
1652
1678
|
}
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
// not
|
|
1656
|
-
|
|
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;
|
|
1657
1687
|
try {
|
|
1658
|
-
|
|
1659
|
-
loadChecked = new Set(record && record.deployment === active[0]?.deployment ? record.checked : []);
|
|
1688
|
+
jevAssignments = readCloudJevAssignments();
|
|
1660
1689
|
} catch {
|
|
1661
|
-
|
|
1690
|
+
// Never throws by contract.
|
|
1662
1691
|
}
|
|
1663
|
-
const
|
|
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>();
|
|
1664
1697
|
const targetText = (targets: NonNullable<(typeof active)[number]["agentTargets"]>): string =>
|
|
1665
1698
|
`for ${targets
|
|
1666
1699
|
.map((tg) => (tg.instanceId ? `${tg.integration} / ${labels.get(tg.instanceId) ?? tg.instanceId}` : tg.integration))
|
|
1667
1700
|
.join(", ")}`;
|
|
1668
|
-
if (active.length > 0 || jev.jevMode !== null) {
|
|
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
|
+
});
|
|
1669
1710
|
cloud = {
|
|
1670
1711
|
deployment: active[0]?.deployment ?? jev.deployment ?? null,
|
|
1671
|
-
|
|
1712
|
+
jevListed: jevAssignments !== null,
|
|
1713
|
+
rows: [...active.map((artifact) => {
|
|
1672
1714
|
// Only a `both` policy carries a server-derived reviewer list (C1).
|
|
1673
1715
|
const both = artifact.authority === "reviewable" && (artifact.reviewedBy?.length ?? 0) > 0;
|
|
1674
1716
|
const checks = both ? (artifact.reviewedBy ?? []).join(", ") : "";
|
|
@@ -1691,21 +1733,13 @@ export async function listHooks(cwd?: string, { all = false }: { all?: boolean }
|
|
|
1691
1733
|
tag: [...tags, `not loaded: ${failed}`].join(" · "),
|
|
1692
1734
|
};
|
|
1693
1735
|
}
|
|
1694
|
-
if (!loadChecked?.has(`${artifact.id}@${artifact.version}`)) {
|
|
1695
|
-
return {
|
|
1696
|
-
state: "off" as const,
|
|
1697
|
-
name: artifact.id,
|
|
1698
|
-
description: `v${artifact.version}`,
|
|
1699
|
-
tag: [...tags, "not loaded yet: it loads at the next agent tool call"].join(" · "),
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
1736
|
return {
|
|
1703
1737
|
state: "on" as const,
|
|
1704
1738
|
name: artifact.id,
|
|
1705
1739
|
description: `v${artifact.version}`,
|
|
1706
1740
|
...(tags.length > 0 ? { tag: tags.join(" · ") } : {}),
|
|
1707
1741
|
};
|
|
1708
|
-
}),
|
|
1742
|
+
}), ...jevRows],
|
|
1709
1743
|
jevMode: jev.jevMode,
|
|
1710
1744
|
problems: [
|
|
1711
1745
|
...(jsProblem !== null ? [`The Cloud JavaScript policies could not be read, so none of them is enforcing: ${jsProblem}`] : []),
|
|
@@ -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(", ")}`);
|
|
@@ -256,6 +272,7 @@ export async function evaluatePolicies(
|
|
|
256
272
|
? { jevOrigin: review.origin }
|
|
257
273
|
: {}),
|
|
258
274
|
...(combined.observeVerdict ? { observeVerdict: combined.observeVerdict } : {}),
|
|
275
|
+
...(review.kind === "answered" && review.observedCloud?.length ? { observedCloud: review.observedCloud } : {}),
|
|
259
276
|
},
|
|
260
277
|
};
|
|
261
278
|
}
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
import {
|
|
59
59
|
cloudAuthorityDeclaration,
|
|
60
60
|
cloudReviewerNames,
|
|
61
|
-
|
|
61
|
+
listActiveCloudManagedPolicies,
|
|
62
62
|
readCloudJevMode,
|
|
63
63
|
} from "./cloud-managed-policies";
|
|
64
64
|
import { jevPacks } from "./effective-reviewers";
|
|
@@ -212,7 +212,9 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
|
|
|
212
212
|
// Cloud checks (`cloud:<id>/<name>`), which this machine cannot list but
|
|
213
213
|
// Cloud answers for. No mode, `off`, or an `observe` assignment: hard.
|
|
214
214
|
const mode = readCloudJevMode();
|
|
215
|
-
|
|
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();
|
|
216
218
|
const cloudReviewers = cloudReviewerNames(assignments, mode);
|
|
217
219
|
if (cloudReviewers.length > 0) reviewers = new Set([...(reviewers ?? []), ...cloudReviewers]);
|
|
218
220
|
for (const assignment of assignments) records.push(cloudAuthorityDeclaration(assignment, mode));
|