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/handler.ts
CHANGED
|
@@ -37,8 +37,8 @@ import type { TwoTierReview } from "./semantic/combine";
|
|
|
37
37
|
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
|
-
import { loadAllCustomHooks } from "./custom-hooks-loader";
|
|
41
|
-
import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled } from "./effective-reviewers";
|
|
40
|
+
import { loadAllCustomHooks, type LoadAllResult } from "./custom-hooks-loader";
|
|
41
|
+
import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled, packJevChecksInstalled } from "./effective-reviewers";
|
|
42
42
|
import {
|
|
43
43
|
authorityDeclarationFor,
|
|
44
44
|
refusedAuthorityWarning,
|
|
@@ -56,13 +56,35 @@ import { resolveTranscriptPath } from "./resolve-transcript-path";
|
|
|
56
56
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
57
57
|
import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
58
58
|
import { readStdinPayload } from "./read-stdin";
|
|
59
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
cloudJevAsks,
|
|
61
|
+
cloudJevAssignmentsFor,
|
|
62
|
+
withCloudAuthority,
|
|
63
|
+
readActiveCloudManagedPolicies,
|
|
64
|
+
readCloudJevMode,
|
|
65
|
+
readCloudJevState,
|
|
66
|
+
readCloudAgentScopeRequired,
|
|
67
|
+
type CloudManagedPolicyArtifact,
|
|
68
|
+
type CloudPolicyError,
|
|
69
|
+
} from "./cloud-managed-policies";
|
|
70
|
+
import {
|
|
71
|
+
cloudPolicyErrorsPath,
|
|
72
|
+
collectCloudPolicyErrors,
|
|
73
|
+
installedPackIds,
|
|
74
|
+
jevBudgetPath,
|
|
75
|
+
readJevBudgetDrops,
|
|
76
|
+
writeCloudPolicyErrors,
|
|
77
|
+
recordCloudLoadChecked,
|
|
78
|
+
} from "./cloud-policy-errors";
|
|
60
79
|
import { hasInstalledRegexPacks, readInstalledPacks, type PackError, type ResolvedPack } from "./pack-manifest";
|
|
61
80
|
import { missingGuards, packFailureReason, combinedGuardMatch, guardsCover } from "./pack-failclosed";
|
|
62
81
|
import { readActivePause, type ActivePause } from "./session-pause";
|
|
63
82
|
import { jevConfigFile } from "./fp-home";
|
|
64
83
|
import { layoutWarningForHook } from "./fp-reset";
|
|
65
84
|
import { noteUntracedAgent } from "./untraced-note";
|
|
85
|
+
import { isHealthProbe } from "./health-probe";
|
|
86
|
+
import { readRuntimeAgentIdentity, runtimeAgentSettingsPath } from "./agent-roster";
|
|
87
|
+
import type { AgentIdentity } from "./agent-targets";
|
|
66
88
|
|
|
67
89
|
/**
|
|
68
90
|
* Canonicalize an event name to PascalCase. Codex sends snake_case event names
|
|
@@ -169,6 +191,8 @@ export interface EvaluateHookEventOptions {
|
|
|
169
191
|
* written for.
|
|
170
192
|
*/
|
|
171
193
|
fallbackCwd?: string;
|
|
194
|
+
/** Config selected by the hook process (may differ from the worker's env). */
|
|
195
|
+
agentSettingsPath?: string;
|
|
172
196
|
/**
|
|
173
197
|
* The warm worker's hook into its request queue. Called at most once, and
|
|
174
198
|
* only on the two-tier path, at the point this evaluation stops reading the
|
|
@@ -222,10 +246,20 @@ async function runObserved(
|
|
|
222
246
|
// package ships none: until `failproofai policies add FailproofAI/jev-policies`
|
|
223
247
|
// (or another pack declaring `semantic` checks) Jev is inert — no request, no
|
|
224
248
|
// intent capture — whether the config is BYOK or FailproofAI Cloud's;
|
|
249
|
+
//
|
|
250
|
+
// …or, in place of the first two, FailproofAI Cloud sets this machine's Jev
|
|
251
|
+
// mode to `observe`/`enforce` (`active.json` `jevMode`, CONTRACT C10): then
|
|
252
|
+
// EVERY gated call goes to FailproofAI Cloud on the Cloud Jev credential —
|
|
253
|
+
// `jev.json` is not used — because the checks Cloud deployed to this machine
|
|
254
|
+
// live there and this machine cannot know which apply. Cloud's `off` switches
|
|
255
|
+
// Jev off whatever `jev.json` says. The remaining conditions hold either way:
|
|
225
256
|
// - `FAILPROOFAI_EVALUATOR` is not `legacy` (see "Turning Jev off" below);
|
|
226
|
-
// - this is not the fail-closed `forceDecision` path and no session pause is
|
|
257
|
+
// - this is not the fail-closed `forceDecision` path, and no session pause is
|
|
227
258
|
// active — a pause suspends local policy, and Jev must not become a way to
|
|
228
|
-
// evaluate what the pause switched off
|
|
259
|
+
// evaluate what the pause switched off. FailproofAI Cloud's checks are the
|
|
260
|
+
// exception, as Cloud JS assignments are: under a Cloud mode that asks, a
|
|
261
|
+
// paused session's calls still go to Cloud, with no installed pack's check
|
|
262
|
+
// in them;
|
|
229
263
|
// - the event is a gate (`PreToolUse` / `PermissionRequest`) for a named tool
|
|
230
264
|
// that the AGENT requested (see `isHumanAuthoredGate`).
|
|
231
265
|
//
|
|
@@ -266,6 +300,16 @@ function jevForcedOff(opts: EvaluateHookEventOptions | undefined): boolean {
|
|
|
266
300
|
return process.env.FAILPROOFAI_EVALUATOR === "legacy" || !!opts?.forceDecision;
|
|
267
301
|
}
|
|
268
302
|
|
|
303
|
+
/** How a Cloud-mode review reaches FailproofAI Cloud (`semantic/cloud-jev.ts`). */
|
|
304
|
+
interface CloudJevRoute {
|
|
305
|
+
machineId: string;
|
|
306
|
+
deployment: number | null;
|
|
307
|
+
mode: "observe" | "enforce";
|
|
308
|
+
agent: AgentIdentity | null;
|
|
309
|
+
/** Cloud policies whose Jev assignment here is `observe`: their answers are recorded, never applied. */
|
|
310
|
+
observedPolicies: string[];
|
|
311
|
+
}
|
|
312
|
+
|
|
269
313
|
/**
|
|
270
314
|
* The BYOK config (with the build's default mode, D2), or null (Jev off). A
|
|
271
315
|
* config that cannot be read is off, never a failure. Logged at info, like
|
|
@@ -283,8 +327,37 @@ function jevForcedOff(opts: EvaluateHookEventOptions | undefined): boolean {
|
|
|
283
327
|
* unreadable directory above it and a path that is not a file are all null
|
|
284
328
|
* there too.
|
|
285
329
|
*/
|
|
286
|
-
async function readJevConfig(
|
|
330
|
+
async function readJevConfig(agent?: AgentIdentity | null): Promise<{
|
|
331
|
+
config: JevConfig;
|
|
332
|
+
defaultMode: TwoTierReview["mode"];
|
|
333
|
+
/** Set when FailproofAI Cloud's mode asks: the review goes to Cloud. */
|
|
334
|
+
cloud?: CloudJevRoute;
|
|
335
|
+
} | null> {
|
|
287
336
|
try {
|
|
337
|
+
// FailproofAI Cloud's mode, when its deployment sets one, decides (CONTRACT
|
|
338
|
+
// C10.2): `off` is no Jev at all, whatever `jev.json` says; `observe` or
|
|
339
|
+
// `enforce` runs Jev on FailproofAI Cloud with the Cloud Jev credential —
|
|
340
|
+
// never `jev.json`'s provider — or not at all, when this machine cannot
|
|
341
|
+
// (`loadJevConfigForCloudMode` says why, for `errors.json`). Read off
|
|
342
|
+
// `active.json`, which is small and read on this path already.
|
|
343
|
+
const cloud = readCloudJevState();
|
|
344
|
+
if (cloud.jevMode === "off") return null;
|
|
345
|
+
if (cloudJevAsks(cloud.jevMode)) {
|
|
346
|
+
const { loadJevConfigForCloudMode, DEFAULT_JEV_MODE } = await import("./semantic/jev-config");
|
|
347
|
+
const resolved = loadJevConfigForCloudMode(cloud.jevMode);
|
|
348
|
+
if (!resolved.config || !resolved.machineId) return null;
|
|
349
|
+
return {
|
|
350
|
+
config: resolved.config,
|
|
351
|
+
defaultMode: DEFAULT_JEV_MODE,
|
|
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
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
288
361
|
if (!existsSync(jevConfigFile())) return null;
|
|
289
362
|
const { loadJevConfig, DEFAULT_JEV_MODE } = await import("./semantic/jev-config");
|
|
290
363
|
const config = loadJevConfig();
|
|
@@ -300,6 +373,90 @@ async function readJevConfig(): Promise<{ config: JevConfig; defaultMode: TwoTie
|
|
|
300
373
|
}
|
|
301
374
|
}
|
|
302
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Where a Jev decision's check came from, as activity-row fields. A FailproofAI
|
|
378
|
+
* Cloud check's outcome arrives with `origin.cloudPolicyId`/`cloudVersion`,
|
|
379
|
+
* carried pack-shaped through the review (`cloud:<id>` / `<version>`, see
|
|
380
|
+
* `semantic/cloud-jev.ts`), and is filed here under the Cloud attribution pair
|
|
381
|
+
* a Cloud JS policy's decision uses — so the Cloud policy page counts it
|
|
382
|
+
* against the policy that was deployed.
|
|
383
|
+
*/
|
|
384
|
+
function jevAttribution(
|
|
385
|
+
origin: { packId: string; packVersion?: string } | undefined,
|
|
386
|
+
): { packId?: string; packVersion?: string; cloudPolicyId?: string; cloudVersion?: number } {
|
|
387
|
+
if (!origin) return {};
|
|
388
|
+
if (origin.packId.startsWith("cloud:")) {
|
|
389
|
+
const version = Number(origin.packVersion);
|
|
390
|
+
return {
|
|
391
|
+
cloudPolicyId: origin.packId.slice("cloud:".length),
|
|
392
|
+
...(Number.isSafeInteger(version) ? { cloudVersion: version } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return origin;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Compute and write `cloud-policies/errors.json` for this machine's Cloud
|
|
400
|
+
* deployment. A no-op off a Cloud-managed machine (`writeCloudPolicyErrors`
|
|
401
|
+
* skips without an `active.json`) and when nothing changed. Never throws.
|
|
402
|
+
*/
|
|
403
|
+
async function recordCloudPolicyErrors(input: {
|
|
404
|
+
manifestError: string | null;
|
|
405
|
+
jsPolicies: CloudManagedPolicyArtifact[];
|
|
406
|
+
jsFailures: LoadAllResult["cloudFailures"] | undefined;
|
|
407
|
+
agentScopeUnresolved?: boolean;
|
|
408
|
+
}): Promise<void> {
|
|
409
|
+
try {
|
|
410
|
+
const jev = readCloudJevState();
|
|
411
|
+
const managed = input.manifestError !== null || input.jsPolicies.length > 0 || jev.jevMode !== null ||
|
|
412
|
+
jev.errors.length > 0 || input.agentScopeUnresolved === true;
|
|
413
|
+
// An unmanaged machine never reads further. A managed machine whose
|
|
414
|
+
// deployment just emptied still writes, so a stale report is cleared.
|
|
415
|
+
if (!managed && !existsSync(cloudPolicyErrorsPath())) return;
|
|
416
|
+
// Only while Cloud's mode ASKS (CONTRACT C10.2): why this machine cannot
|
|
417
|
+
// send its calls (`transcripts_disabled`, `jev_unconfigured…`), and the
|
|
418
|
+
// installed packs' checks Cloud dropped for the question budget under this
|
|
419
|
+
// deployment. The config answer is kept by its module until an input
|
|
420
|
+
// changes, so the second ask of a gate event reads no file.
|
|
421
|
+
let jevProblem: string | null = null;
|
|
422
|
+
let budgetDrops: CloudPolicyError[] = [];
|
|
423
|
+
let health: CloudPolicyError[] = [];
|
|
424
|
+
if (cloudJevAsks(jev.jevMode)) {
|
|
425
|
+
const { loadJevConfigForCloudMode } = await import("./semantic/jev-config");
|
|
426
|
+
const resolved = loadJevConfigForCloudMode(jev.jevMode);
|
|
427
|
+
jevProblem = resolved.problem;
|
|
428
|
+
if (resolved.machineId) {
|
|
429
|
+
// A drop of a pack uninstalled since is not reported (review m2);
|
|
430
|
+
// `installed.json` is read only when there is a record to filter.
|
|
431
|
+
budgetDrops = existsSync(jevBudgetPath())
|
|
432
|
+
? readJevBudgetDrops({ machineId: resolved.machineId, deployment: jev.deployment, jevMode: jev.jevMode }, installedPackIds())
|
|
433
|
+
: [];
|
|
434
|
+
}
|
|
435
|
+
// Rate-limited or unavailable FailproofAI Cloud Jev, as this process has seen it (review M3).
|
|
436
|
+
const { cloudJevHealthErrors } = await import("./semantic/cloud-jev-health");
|
|
437
|
+
health = cloudJevHealthErrors();
|
|
438
|
+
}
|
|
439
|
+
writeCloudPolicyErrors(
|
|
440
|
+
collectCloudPolicyErrors({
|
|
441
|
+
manifestError: input.manifestError,
|
|
442
|
+
jsPolicies: input.jsPolicies,
|
|
443
|
+
jsFailures: input.jsFailures,
|
|
444
|
+
jev,
|
|
445
|
+
jevProblem,
|
|
446
|
+
budgetDrops,
|
|
447
|
+
health,
|
|
448
|
+
agentScopeUnresolved: input.agentScopeUnresolved,
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
// After the report: what this hook load-checked (N4). A failure is in
|
|
452
|
+
// errors.json first, so the listing never sees a checked policy without
|
|
453
|
+
// its error.
|
|
454
|
+
if (input.jsPolicies.length > 0) recordCloudLoadChecked(input.jsPolicies[0].deployment, input.jsPolicies);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
hookLogInfo(`could not record Cloud policy errors: ${err instanceof Error ? err.message : String(err)}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
303
460
|
/**
|
|
304
461
|
* Start Jev's review of this call — before the regex policies run, so the two
|
|
305
462
|
* proceed in parallel — or return null for the regex-only path.
|
|
@@ -311,15 +468,32 @@ async function startTwoTier(
|
|
|
311
468
|
cli: IntegrationType,
|
|
312
469
|
opts: EvaluateHookEventOptions | undefined,
|
|
313
470
|
activePause: ActivePause | null,
|
|
471
|
+
agent: AgentIdentity | null,
|
|
314
472
|
): Promise<TwoTierReview | null> {
|
|
315
473
|
if (!JEV_GATE_EVENTS.has(canonicalEventType)) return null;
|
|
316
474
|
if (isHumanAuthoredGate(session.rawHookEventName, cli)) return null;
|
|
317
475
|
if (typeof parsed.tool_name !== "string" || parsed.tool_name.length === 0) return null;
|
|
318
|
-
if (jevForcedOff(opts)
|
|
319
|
-
// Read off the reviewer set registration just cached, so it is free.
|
|
320
|
-
|
|
321
|
-
|
|
476
|
+
if (jevForcedOff(opts)) return null;
|
|
477
|
+
// Read off the reviewer set registration just cached, so it is free. Under a
|
|
478
|
+
// Cloud mode that asks, every gated call goes to Cloud whether or not an
|
|
479
|
+
// installed pack declares a check: the Cloud checks are Cloud's to select.
|
|
480
|
+
const cloudAsks = cloudJevAsks(readCloudJevMode());
|
|
481
|
+
// A session pause suspends LOCAL policy only: installed packs' Jev checks
|
|
482
|
+
// with it, FailproofAI Cloud's Jev checks never — the same exemption Cloud
|
|
483
|
+
// JS assignments get below, for the same reason (a locally-issued command
|
|
484
|
+
// must not switch off what the org assigned centrally).
|
|
485
|
+
if (activePause && !cloudAsks) return null;
|
|
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;
|
|
492
|
+
const loaded = await readJevConfig(agent);
|
|
322
493
|
if (!loaded) return null;
|
|
494
|
+
// Paused, and the config is not Cloud's after all (read in between): the
|
|
495
|
+
// local path has nothing left to ask.
|
|
496
|
+
if (activePause && !loaded.cloud) return null;
|
|
323
497
|
const cfg = loaded.config;
|
|
324
498
|
try {
|
|
325
499
|
const { startJevReview } = await import("./semantic/jev-review");
|
|
@@ -331,6 +505,9 @@ async function startTwoTier(
|
|
|
331
505
|
permissionMode: session.permissionMode,
|
|
332
506
|
sessionId: session.sessionId,
|
|
333
507
|
cli,
|
|
508
|
+
...(loaded.cloud ? { cloud: loaded.cloud } : {}),
|
|
509
|
+
// Cloud's checks are still asked; the installed packs' are not.
|
|
510
|
+
...(activePause ? { localPaused: true } : {}),
|
|
334
511
|
});
|
|
335
512
|
} catch (err) {
|
|
336
513
|
// Configured but unable to start: that is a fallback, and it is recorded
|
|
@@ -405,7 +582,9 @@ async function captureJevIntent(
|
|
|
405
582
|
): Promise<void> {
|
|
406
583
|
if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts)) return;
|
|
407
584
|
try {
|
|
408
|
-
|
|
585
|
+
// Same gate as `startTwoTier`: a Cloud mode that asks needs what the human
|
|
586
|
+
// typed whatever the installed packs declare.
|
|
587
|
+
if (!cloudJevAsks(readCloudJevMode()) && !jevChecksInstalled()) return;
|
|
409
588
|
if (!(await readJevConfig())) return;
|
|
410
589
|
if (decision === "deny") {
|
|
411
590
|
const { ENFORCEMENT_CAPABILITY } = await import("./enforcement-capability");
|
|
@@ -464,6 +643,12 @@ export async function evaluateHookEvent(
|
|
|
464
643
|
}
|
|
465
644
|
}
|
|
466
645
|
|
|
646
|
+
// The daemon health probe: proof the worker answers, not an agent event.
|
|
647
|
+
// Answered before anything records scope errors or activity (B3).
|
|
648
|
+
if (isHealthProbe(eventType, parsed)) {
|
|
649
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
650
|
+
}
|
|
651
|
+
|
|
467
652
|
normalizeCliPayload(cli, parsed);
|
|
468
653
|
|
|
469
654
|
// Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
|
|
@@ -508,6 +693,9 @@ export async function evaluateHookEvent(
|
|
|
508
693
|
rawHookEventName: eventType,
|
|
509
694
|
cli,
|
|
510
695
|
};
|
|
696
|
+
const runtimeAgent = readRuntimeAgentIdentity(
|
|
697
|
+
cli, opts?.agentSettingsPath ?? runtimeAgentSettingsPath(cli, session.cwd),
|
|
698
|
+
);
|
|
511
699
|
|
|
512
700
|
let config: HooksConfig;
|
|
513
701
|
let customHooksList: CustomHook[] = [];
|
|
@@ -554,7 +742,7 @@ export async function evaluateHookEvent(
|
|
|
554
742
|
} else {
|
|
555
743
|
// Load enabled policies (merge across project/local/global scopes)
|
|
556
744
|
config = readMergedHooksConfig(session.cwd);
|
|
557
|
-
clearPolicies(cli);
|
|
745
|
+
clearPolicies(cli, runtimeAgent);
|
|
558
746
|
|
|
559
747
|
// A session pause suspends LOCAL policy only, for a bounded time. Cloud
|
|
560
748
|
// assignments are exempt below for the same reason `disabledCustomPolicies`
|
|
@@ -616,10 +804,13 @@ export async function evaluateHookEvent(
|
|
|
616
804
|
// than the status quo, which already failed open on five of the twelve —
|
|
617
805
|
// just accidentally, and without saying so.
|
|
618
806
|
let cloudManagedPolicies: CloudManagedPolicyArtifact[] = [];
|
|
807
|
+
/** Why the Cloud JS half did not load at all, for `errors.json`. */
|
|
808
|
+
let cloudManifestError: string | null = null;
|
|
619
809
|
try {
|
|
620
|
-
cloudManagedPolicies = readActiveCloudManagedPolicies();
|
|
810
|
+
cloudManagedPolicies = readActiveCloudManagedPolicies(runtimeAgent);
|
|
621
811
|
} catch (err) {
|
|
622
812
|
const msg = err instanceof Error ? err.message : String(err);
|
|
813
|
+
cloudManifestError = msg;
|
|
623
814
|
hookLogWarn(
|
|
624
815
|
`[failproofai] cloud-managed policies could NOT be loaded and are not being enforced: ${msg}. ` +
|
|
625
816
|
`Local policies are unaffected. Run \`failproofai config --status\` to check this machine's enrolment.`,
|
|
@@ -633,7 +824,9 @@ export async function evaluateHookEvent(
|
|
|
633
824
|
// policy decided this event: "what was deployed here at the time" is a
|
|
634
825
|
// separate question from "what decided", and only the former can tell a
|
|
635
826
|
// rollout that changed nothing from one that never reached the machine.
|
|
636
|
-
|
|
827
|
+
// A machine whose deployment carries only a Jev mode has no JS policy to
|
|
828
|
+
// read it off.
|
|
829
|
+
cloudDeployment = cloudManagedPolicies[0]?.deployment ?? readCloudJevState().deployment ?? undefined;
|
|
637
830
|
// Installed packs. `readInstalledPacks` never throws: a bad manifest or a
|
|
638
831
|
// tampered artifact yields zero packs and a recorded reason, which is
|
|
639
832
|
// sound ONLY because the builtins still ship compiled in and keep
|
|
@@ -661,11 +854,22 @@ export async function evaluateHookEvent(
|
|
|
661
854
|
...installedPacks.map((pack) => pack.path),
|
|
662
855
|
];
|
|
663
856
|
|
|
857
|
+
// A FailproofAI Cloud policy's `reviewedBy` names ITS OWN Cloud Jev
|
|
858
|
+
// checks, which run on FailproofAI Cloud (CONTRACT C10.5). Each is
|
|
859
|
+
// translated here to `cloud:<id>/<name>` — a reviewer only that policy's
|
|
860
|
+
// own Cloud outcomes can satisfy, never an installed pack's check of the
|
|
861
|
+
// same name (C9.4) — and only while Cloud's Jev mode asks. An `observe`
|
|
862
|
+
// assignment, or no mode that asks, is hard, without a warning: that is
|
|
863
|
+
// the org's choice, not a broken policy. Before the loader, so two
|
|
864
|
+
// assignments sharing one artifact merge names that still say whose.
|
|
865
|
+
const cloudJevMode = readCloudJevMode();
|
|
866
|
+
const cloudForLoading = cloudManagedPolicies.map((policy) => withCloudAuthority(policy, cloudJevMode));
|
|
867
|
+
|
|
664
868
|
// Load and register custom hooks (layer 2, after builtins)
|
|
665
869
|
const loadResult = await loadAllCustomHooks(allExplicitPaths, {
|
|
666
870
|
sessionCwd: session.cwd,
|
|
667
871
|
customPoliciesEnabled: config.customPoliciesEnabled,
|
|
668
|
-
...(
|
|
872
|
+
...(cloudForLoading.length > 0 ? { cloudManagedPolicies: cloudForLoading } : {}),
|
|
669
873
|
...(installedPacks.length > 0 ? { packs: installedPacks } : {}),
|
|
670
874
|
});
|
|
671
875
|
customHooksList = loadResult.hooks;
|
|
@@ -812,6 +1016,7 @@ export async function evaluateHookEvent(
|
|
|
812
1016
|
// for the user's own files; anything unclear registers as hard. Said
|
|
813
1017
|
// aloud when a `reviewable` claim is refused and Jev is configured, so
|
|
814
1018
|
// an author is not left wondering why Jev never clears it.
|
|
1019
|
+
// A Cloud record's authority was translated before loading (above).
|
|
815
1020
|
const authority = authorityDeclarationFor(hook, { cloudManaged, pack });
|
|
816
1021
|
// Against the same set `registerPolicy` judges it by, or this warning
|
|
817
1022
|
// describes a different machine than the registry does: a pack that
|
|
@@ -835,6 +1040,17 @@ export async function evaluateHookEvent(
|
|
|
835
1040
|
);
|
|
836
1041
|
}
|
|
837
1042
|
|
|
1043
|
+
// What this machine could not load from its FailproofAI Cloud deployment,
|
|
1044
|
+
// for the daemon to report on its next poll. Only on a managed machine,
|
|
1045
|
+
// written only when it changes, and never able to cost this event its
|
|
1046
|
+
// answer (see `cloud-policy-errors.ts`).
|
|
1047
|
+
await recordCloudPolicyErrors({
|
|
1048
|
+
manifestError: cloudManifestError,
|
|
1049
|
+
jsPolicies: cloudManagedPolicies,
|
|
1050
|
+
jsFailures: loadResult.cloudFailures,
|
|
1051
|
+
agentScopeUnresolved: runtimeAgent === null && readCloudAgentScopeRequired(),
|
|
1052
|
+
});
|
|
1053
|
+
|
|
838
1054
|
// Fail closed on enforcement this machine was told it had and does not.
|
|
839
1055
|
//
|
|
840
1056
|
// Additive, and deliberately NOT a clearPolicies() the way the
|
|
@@ -924,7 +1140,7 @@ export async function evaluateHookEvent(
|
|
|
924
1140
|
// starts HERE, before any regex policy runs, and evaluatePolicies combines
|
|
925
1141
|
// the two (see semantic/combine.ts). Otherwise this is null and the call
|
|
926
1142
|
// below is exactly the regex-only evaluation it always was.
|
|
927
|
-
const twoTier = await startTwoTier(canonicalEventType, parsed, session, cli, opts, activePause);
|
|
1143
|
+
const twoTier = await startTwoTier(canonicalEventType, parsed, session, cli, opts, activePause, runtimeAgent);
|
|
928
1144
|
// On the two-tier path the registry is read for the activity row BEFORE
|
|
929
1145
|
// evaluating, because evaluatePolicies may hand the registry back to the
|
|
930
1146
|
// warm worker's queue (`releaseRegistry`) while it waits on Jev, and the
|
|
@@ -956,6 +1172,11 @@ export async function evaluateHookEvent(
|
|
|
956
1172
|
reason: observeVerdict.reason,
|
|
957
1173
|
});
|
|
958
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
|
+
}
|
|
959
1180
|
const durationMs = Math.round(performance.now() - startTime);
|
|
960
1181
|
hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
|
|
961
1182
|
|
|
@@ -1001,7 +1222,7 @@ export async function evaluateHookEvent(
|
|
|
1001
1222
|
// would be false — so it is attributed to Jev itself. Leaving it out
|
|
1002
1223
|
// instead filed every Jev block under "unattributed" on FailproofAI
|
|
1003
1224
|
// Cloud's policy page, beside rows written before attribution existed.
|
|
1004
|
-
...(result.policyName && result.twoTier?.decidedByJev ? { policySource: "jev" as const, ...jevOrigin } : {}),
|
|
1225
|
+
...(result.policyName && result.twoTier?.decidedByJev ? { policySource: "jev" as const, ...jevAttribution(jevOrigin) } : {}),
|
|
1005
1226
|
...(result.policyName && !result.twoTier?.decidedByJev
|
|
1006
1227
|
? (() => {
|
|
1007
1228
|
const attribution = policyAttribution.get(result.policyName);
|
|
@@ -1016,6 +1237,14 @@ export async function evaluateHookEvent(
|
|
|
1016
1237
|
};
|
|
1017
1238
|
})()
|
|
1018
1239
|
: {}),
|
|
1240
|
+
// Observe mode, nothing else decided: the Jev check that would have is
|
|
1241
|
+
// named on the row, with its Cloud policy (B12). Before, the row read
|
|
1242
|
+
// policyName/cloudPolicyId null, so "would have" never reached a policy.
|
|
1243
|
+
// No `policySource`: nothing decided this call, and the policy page
|
|
1244
|
+
// counts a source as a decision; `observed` carries the "would have".
|
|
1245
|
+
...(!result.policyName && observeVerdict
|
|
1246
|
+
? { policyName: observeVerdict.policyName, ...jevAttribution(result.twoTier?.jevOrigin) }
|
|
1247
|
+
: {}),
|
|
1019
1248
|
...(cloudDeployment !== undefined ? { cloudDeployment } : {}),
|
|
1020
1249
|
// The point of observe mode is this record. Without it the rollout is
|
|
1021
1250
|
// unmeasurable and the row is indistinguishable from one where the policy
|
|
@@ -1116,7 +1345,11 @@ export async function evaluateHookEvent(
|
|
|
1116
1345
|
* kept unchanged so nothing about the one-shot (non-daemon) path regresses.
|
|
1117
1346
|
* Internally now just a thin wrapper around `evaluateHookEvent`.
|
|
1118
1347
|
*/
|
|
1119
|
-
export async function handleHookEvent(
|
|
1348
|
+
export async function handleHookEvent(
|
|
1349
|
+
eventType: string,
|
|
1350
|
+
cli: IntegrationType = "claude",
|
|
1351
|
+
agentSettingsPath?: string,
|
|
1352
|
+
): Promise<number> {
|
|
1120
1353
|
const MAX_STDIN_BYTES = 1_048_576; // 1 MB
|
|
1121
1354
|
const stdinRead = await readStdinPayload(MAX_STDIN_BYTES);
|
|
1122
1355
|
if (stdinRead.readError) {
|
|
@@ -1148,7 +1381,10 @@ export async function handleHookEvent(eventType: string, cli: IntegrationType =
|
|
|
1148
1381
|
});
|
|
1149
1382
|
}
|
|
1150
1383
|
|
|
1151
|
-
const result = await evaluateHookEvent(
|
|
1384
|
+
const result = await evaluateHookEvent(
|
|
1385
|
+
eventType, cli, stdinRead.payload,
|
|
1386
|
+
agentSettingsPath === undefined ? undefined : { agentSettingsPath },
|
|
1387
|
+
);
|
|
1152
1388
|
|
|
1153
1389
|
// Say it out loud, once a session, when the collector is holding batches the
|
|
1154
1390
|
// server definitively refused.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon health probe's marker (`probeDaemon` in `daemon-service.ts`).
|
|
3
|
+
*
|
|
4
|
+
* The probe is a SessionStart sent through the daemon to the warm worker, so a
|
|
5
|
+
* reply proves the whole path answers. It is not an agent's session: the
|
|
6
|
+
* handler answers it before any scope, policy or activity logic, or every
|
|
7
|
+
* probe recorded `agent_scope_unresolved` in `errors.json` (it carries no cwd
|
|
8
|
+
* and no `--agent-scope`) and wrote a fake SessionStart activity row.
|
|
9
|
+
*/
|
|
10
|
+
export const HEALTH_PROBE_SOURCE = "failproofai-health-probe";
|
|
11
|
+
|
|
12
|
+
/** Whether a parsed hook payload is the daemon health probe. */
|
|
13
|
+
export function isHealthProbe(eventType: string, parsed: Record<string, unknown>): boolean {
|
|
14
|
+
return eventType === "SessionStart" && parsed.source === HEALTH_PROBE_SOURCE && parsed.session_id === undefined;
|
|
15
|
+
}
|
|
@@ -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 {
|
|
@@ -154,6 +154,12 @@ function isMarkedHook(hook: unknown): boolean {
|
|
|
154
154
|
return cmd.includes("failproofai") && cmd.includes("--hook");
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/** A hook names its installing settings scope, avoiding a cwd-based guess
|
|
158
|
+
* when user and project/local hooks both exist for the same integration. */
|
|
159
|
+
function agentScopeSuffix(scope?: HookScope): string {
|
|
160
|
+
return ` --agent-scope ${scope ?? "user"}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
157
163
|
function stripLegacyVersion(settings: Record<string, unknown>): boolean {
|
|
158
164
|
if ("version" in settings) {
|
|
159
165
|
delete settings.version;
|
|
@@ -247,6 +253,21 @@ export function settingsPathsFor(
|
|
|
247
253
|
return paths && paths.length > 0 ? paths : [integration.getSettingsPath(scope, cwd)];
|
|
248
254
|
}
|
|
249
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
|
+
|
|
250
271
|
// ── Claude Code integration ─────────────────────────────────────────────────
|
|
251
272
|
|
|
252
273
|
export const claudeCode: Integration = {
|
|
@@ -259,7 +280,7 @@ export const claudeCode: Integration = {
|
|
|
259
280
|
const base = cwd ? resolve(cwd) : process.cwd();
|
|
260
281
|
switch (scope) {
|
|
261
282
|
case "user":
|
|
262
|
-
return resolve(
|
|
283
|
+
return resolve(claudeConfigDir(), "settings.json");
|
|
263
284
|
case "project":
|
|
264
285
|
return resolve(base, ".claude", "settings.json");
|
|
265
286
|
case "local":
|
|
@@ -285,7 +306,7 @@ export const claudeCode: Integration = {
|
|
|
285
306
|
: `"${binaryPath}" --hook ${eventType}`;
|
|
286
307
|
return {
|
|
287
308
|
type: "command",
|
|
288
|
-
command,
|
|
309
|
+
command: command + agentScopeSuffix(scope),
|
|
289
310
|
// Claude reads `timeout` in SECONDS per https://code.claude.com/docs/en/hooks
|
|
290
311
|
// ("Seconds before canceling. Defaults: 600 for command ...; 60 for agent"),
|
|
291
312
|
// NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h. (#482-class unit fix)
|
|
@@ -433,7 +454,7 @@ export const codex: Integration = {
|
|
|
433
454
|
const base = cwd ? resolve(cwd) : process.cwd();
|
|
434
455
|
switch (scope) {
|
|
435
456
|
case "user":
|
|
436
|
-
return resolve(
|
|
457
|
+
return resolve(codexHome(), "hooks.json");
|
|
437
458
|
case "project":
|
|
438
459
|
return resolve(base, ".codex", "hooks.json");
|
|
439
460
|
case "local":
|
|
@@ -464,7 +485,7 @@ export const codex: Integration = {
|
|
|
464
485
|
// Codex reads `timeout` in SECONDS (the field is literally `timeout`,
|
|
465
486
|
// default 600 per https://developers.openai.com/codex/hooks) — same unit as
|
|
466
487
|
// Claude/Cursor/Copilot. 60 = 60s.
|
|
467
|
-
command,
|
|
488
|
+
command: command + agentScopeSuffix(scope),
|
|
468
489
|
timeout: 60,
|
|
469
490
|
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
470
491
|
};
|
|
@@ -644,8 +665,8 @@ export const copilot: Integration = {
|
|
|
644
665
|
: `"${binaryPath}" --hook ${eventType} --cli copilot`;
|
|
645
666
|
return {
|
|
646
667
|
type: "command",
|
|
647
|
-
bash: cmd,
|
|
648
|
-
powershell: cmd,
|
|
668
|
+
bash: cmd + agentScopeSuffix(scope),
|
|
669
|
+
powershell: cmd + agentScopeSuffix(scope),
|
|
649
670
|
timeoutSec: 60,
|
|
650
671
|
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
651
672
|
};
|
|
@@ -788,7 +809,7 @@ export const cursor: Integration = {
|
|
|
788
809
|
// use 30 and 10), NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h.
|
|
789
810
|
return {
|
|
790
811
|
type: "command",
|
|
791
|
-
command,
|
|
812
|
+
command: command + agentScopeSuffix(scope),
|
|
792
813
|
timeout: 60,
|
|
793
814
|
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
794
815
|
};
|
|
@@ -981,12 +1002,13 @@ function canonicalizeToolInput(canonicalToolName, args) {
|
|
|
981
1002
|
|
|
982
1003
|
const FAILPROOFAI_BIN = ${escapedBin};
|
|
983
1004
|
const USE_NPX = ${useNpx};
|
|
1005
|
+
const AGENT_SCOPE = ${JSON.stringify(scope)};
|
|
984
1006
|
|
|
985
1007
|
function runFailproofai(eventName, payload, directory) {
|
|
986
1008
|
const cmd = USE_NPX ? "npx" : FAILPROOFAI_BIN;
|
|
987
1009
|
const args = USE_NPX
|
|
988
|
-
? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode"]
|
|
989
|
-
: ["--hook", eventName, "--cli", "opencode"];
|
|
1010
|
+
? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode", "--agent-scope", AGENT_SCOPE]
|
|
1011
|
+
: ["--hook", eventName, "--cli", "opencode", "--agent-scope", AGENT_SCOPE];
|
|
990
1012
|
const r = spawnSync(cmd, args, {
|
|
991
1013
|
input: JSON.stringify(payload),
|
|
992
1014
|
encoding: "utf8",
|
|
@@ -2460,7 +2482,7 @@ export const factory: Integration = {
|
|
|
2460
2482
|
: `"${binaryPath}" --hook ${eventType} --cli factory`;
|
|
2461
2483
|
return {
|
|
2462
2484
|
type: "command",
|
|
2463
|
-
command,
|
|
2485
|
+
command: command + agentScopeSuffix(scope),
|
|
2464
2486
|
// droid reads `timeout` in SECONDS (verified against droid v0.171.0). 30s.
|
|
2465
2487
|
timeout: 30,
|
|
2466
2488
|
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
@@ -2593,7 +2615,7 @@ export const devin: Integration = {
|
|
|
2593
2615
|
: `"${binaryPath}" --hook ${eventType} --cli devin`;
|
|
2594
2616
|
return {
|
|
2595
2617
|
type: "command",
|
|
2596
|
-
command,
|
|
2618
|
+
command: command + agentScopeSuffix(scope),
|
|
2597
2619
|
// Devin reads `timeout` in SECONDS like Claude. 60 = 60s.
|
|
2598
2620
|
timeout: 60,
|
|
2599
2621
|
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
@@ -2738,7 +2760,7 @@ export const antigravity: Integration = {
|
|
|
2738
2760
|
: `"${binaryPath}" --hook ${eventType} --cli antigravity`;
|
|
2739
2761
|
return {
|
|
2740
2762
|
type: "command",
|
|
2741
|
-
command,
|
|
2763
|
+
command: command + agentScopeSuffix(scope),
|
|
2742
2764
|
// Antigravity reads `timeout` in SECONDS (verified agy v1.1.2). 30s.
|
|
2743
2765
|
timeout: 30,
|
|
2744
2766
|
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
@@ -2926,7 +2948,7 @@ export const goose: Integration = {
|
|
|
2926
2948
|
: `"${binaryPath}" --hook ${eventType} --cli goose`;
|
|
2927
2949
|
// Open Plugins command entry: { type, command } only (Goose applies its own
|
|
2928
2950
|
// timeout; no marker field — see isGooseFailproofaiHook).
|
|
2929
|
-
return { type: "command", command };
|
|
2951
|
+
return { type: "command", command: command + agentScopeSuffix(scope) };
|
|
2930
2952
|
},
|
|
2931
2953
|
|
|
2932
2954
|
isFailproofaiHook: isGooseFailproofaiHook,
|