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
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cloud-policies/errors.json` — what this machine could not load from its
|
|
3
|
+
* FailproofAI Cloud deployment, for the daemon to report on its next poll
|
|
4
|
+
* (`policyErrors`, CONTRACT C4–C6).
|
|
5
|
+
*
|
|
6
|
+
* Nothing here changes a verdict. Cloud policies are read fail-open: a JS
|
|
7
|
+
* policy that fails to import costs that one policy on this machine and
|
|
8
|
+
* nothing else, which is the right trade on the hook path and the wrong thing
|
|
9
|
+
* to keep quiet about. Before this file the only trace was a line in the local
|
|
10
|
+
* hook log, so a fleet page read "applied" for a deployment that was not. The
|
|
11
|
+
* same goes for FailproofAI Cloud's Jev (CONTRACT C10): a Jev mode this machine
|
|
12
|
+
* cannot act on, and an installed pack's check Cloud dropped for the question
|
|
13
|
+
* budget, are said here rather than only in a local log.
|
|
14
|
+
*
|
|
15
|
+
* ## One writer per file
|
|
16
|
+
*
|
|
17
|
+
* The CLI writes `errors.json`; the daemon keeps its own reconcile errors in
|
|
18
|
+
* `daemon-errors.json` and merges the two onto the wire. Neither writes the
|
|
19
|
+
* other's file, so there is no read-modify-write race between processes.
|
|
20
|
+
*
|
|
21
|
+
* ## Written on change, atomically
|
|
22
|
+
*
|
|
23
|
+
* Computed on every hook evaluation of a Cloud-managed machine, so the write
|
|
24
|
+
* has to be almost always a no-op: the new content is compared with what is on
|
|
25
|
+
* disk (and with what this process last wrote, to skip even that read in the
|
|
26
|
+
* warm worker) and written only when it differs — tmp file + rename, owner-only,
|
|
27
|
+
* so the daemon never reads half a file.
|
|
28
|
+
*
|
|
29
|
+
* A machine that never had a problem never gets the file at all: an empty list
|
|
30
|
+
* is written only to REPLACE a non-empty one, which is what clears the report
|
|
31
|
+
* on the server. That keeps the poll parameter absent on a healthy fleet.
|
|
32
|
+
*/
|
|
33
|
+
import { randomUUID } from "node:crypto";
|
|
34
|
+
import { chmodSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
import { resolve } from "node:path";
|
|
37
|
+
import {
|
|
38
|
+
CLOUD_POLICY_ERRORS_FILE,
|
|
39
|
+
JEV_BUDGET_FILE,
|
|
40
|
+
cloudManagedPolicyRoot,
|
|
41
|
+
type CloudJevState,
|
|
42
|
+
type CloudManagedPolicyArtifact,
|
|
43
|
+
type CloudPolicyError,
|
|
44
|
+
type CloudPolicyErrorKind,
|
|
45
|
+
} from "./cloud-managed-policies";
|
|
46
|
+
import type { PolicyLoadFailure } from "./custom-hooks-loader";
|
|
47
|
+
import { packsRoot } from "./pack-manifest";
|
|
48
|
+
|
|
49
|
+
export type { CloudPolicyError, CloudPolicyErrorKind } from "./cloud-managed-policies";
|
|
50
|
+
|
|
51
|
+
/** The message the contract fixes for "Cloud set a Jev mode and this machine has no Cloud Jev credential". */
|
|
52
|
+
export const JEV_UNCONFIGURED = "jev_unconfigured";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The message the contract fixes for "Cloud set a Jev mode on a machine
|
|
56
|
+
* connected for decisions only" (`--no-transcripts`), which never runs Jev
|
|
57
|
+
* (CONTRACT C10.2).
|
|
58
|
+
*/
|
|
59
|
+
export const TRANSCRIPTS_DISABLED = "transcripts_disabled";
|
|
60
|
+
|
|
61
|
+
/** How an installed pack's check FailproofAI Cloud dropped for the question budget is reported (C10.5). */
|
|
62
|
+
export const JEV_BUDGET = "jev_budget";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How an installed pack's check is reported when FailproofAI Cloud dropped it
|
|
66
|
+
* because one of the org's Cloud checks has the same name (Cloud wins the name,
|
|
67
|
+
* D-S-2). It is not a budget problem, and its remedy differs.
|
|
68
|
+
*/
|
|
69
|
+
export const JEV_NAME_CLASH = "jev_name_clash";
|
|
70
|
+
|
|
71
|
+
/** Is `e` one of the pack-check drops `jev-budget.json` records? */
|
|
72
|
+
export function isPackDropEntry(e: Pick<CloudPolicyError, "id" | "message">): boolean {
|
|
73
|
+
return e.id.startsWith("pack:") && (e.message.startsWith(JEV_BUDGET) || e.message.startsWith(JEV_NAME_CLASH));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A message with no local path in it (CONTRACT C9.5): the home directory
|
|
78
|
+
* becomes `~`, and any other absolute path becomes its last segment.
|
|
79
|
+
*
|
|
80
|
+
* The report leaves the machine, and a path names the user — the home-relative
|
|
81
|
+
* artifact path in a missing-artifact message, a loader's temp path in an
|
|
82
|
+
* import error, the `chmod 600 <path>` hint of a refused `jev.json`. The daemon
|
|
83
|
+
* applies the same rule again on the way out (`redact_local_paths` in
|
|
84
|
+
* `cloud_client.rs`, whose tests pin the same cases), so a report never carries
|
|
85
|
+
* a username whichever side wrote it. The full text stays in the local hook
|
|
86
|
+
* log, where it is useful and goes nowhere.
|
|
87
|
+
*
|
|
88
|
+
* A path starts at a `/` that begins the text or follows whitespace, a quote,
|
|
89
|
+
* an opening bracket, `=`, `,` or `:` — but not a URL's `//host` after `:`,
|
|
90
|
+
* and never a `/` following any other character, so a URL (`https://host/path`)
|
|
91
|
+
* and a relative path (`a/b`) are left alone while `file:///tmp/x` and
|
|
92
|
+
* `open:/etc/x` are not — and runs to the next whitespace, quote, closing
|
|
93
|
+
* bracket, `,` or `;`.
|
|
94
|
+
*/
|
|
95
|
+
export function redactLocalPaths(message: string, home: string | null = safeHomedir()): string {
|
|
96
|
+
let text = message;
|
|
97
|
+
const base = home?.replace(/\/+$/, "") ?? "";
|
|
98
|
+
if (base.length > 1) {
|
|
99
|
+
let out = "";
|
|
100
|
+
let rest = text;
|
|
101
|
+
for (let at = rest.indexOf(base); at >= 0; at = rest.indexOf(base)) {
|
|
102
|
+
const next = rest.charAt(at + base.length);
|
|
103
|
+
// `/home/al` must not match inside `/home/alice`.
|
|
104
|
+
const whole = next === "" || next === "/" || next === ":" || PATH_END.test(next);
|
|
105
|
+
out += rest.slice(0, at) + (whole ? "~" : base);
|
|
106
|
+
rest = rest.slice(at + base.length);
|
|
107
|
+
}
|
|
108
|
+
text = out + rest;
|
|
109
|
+
}
|
|
110
|
+
// `:(?!\/\/(?!\/))`: a `:` not followed by a URL's `//host` — `scheme://host`
|
|
111
|
+
// stays, while `file:///path` and `x:/path` are paths.
|
|
112
|
+
return text.replace(/(^|[\s"'`(\[<{=,]|:(?!\/\/(?!\/)))(\/[^\s"'`)\]>},;]*)/g, (_m, lead: string, path: string) => {
|
|
113
|
+
const last = path.replace(/\/+$/, "").split("/").pop() ?? "";
|
|
114
|
+
return lead + (last === "" ? "/" : last);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const PATH_END = /[\s"'`)\]>},;]/;
|
|
119
|
+
|
|
120
|
+
function safeHomedir(): string | null {
|
|
121
|
+
try {
|
|
122
|
+
return homedir();
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function cloudPolicyErrorsPath(): string {
|
|
129
|
+
return resolve(cloudManagedPolicyRoot(), CLOUD_POLICY_ERRORS_FILE);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const KINDS: ReadonlySet<string> = new Set<CloudPolicyErrorKind>(["regex", "jev", "both", "daemon"]);
|
|
133
|
+
|
|
134
|
+
/** The file as the CLI last wrote it, or null when absent or unreadable. */
|
|
135
|
+
export function readCloudPolicyErrors(): CloudPolicyError[] | null {
|
|
136
|
+
try {
|
|
137
|
+
const raw = JSON.parse(readFileSync(cloudPolicyErrorsPath(), "utf8")) as { errors?: unknown };
|
|
138
|
+
if (!Array.isArray(raw.errors)) return null;
|
|
139
|
+
return raw.errors.filter(
|
|
140
|
+
(e): e is CloudPolicyError =>
|
|
141
|
+
!!e &&
|
|
142
|
+
typeof e === "object" &&
|
|
143
|
+
typeof (e as CloudPolicyError).id === "string" &&
|
|
144
|
+
typeof (e as CloudPolicyError).message === "string" &&
|
|
145
|
+
KINDS.has((e as CloudPolicyError).kind) &&
|
|
146
|
+
((e as CloudPolicyError).version === null || Number.isSafeInteger((e as CloudPolicyError).version)),
|
|
147
|
+
);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Text this process last wrote, so the warm worker's steady state reads nothing. */
|
|
154
|
+
let lastWritten: { path: string; text: string } | null = null;
|
|
155
|
+
|
|
156
|
+
/** Tests only. */
|
|
157
|
+
export function _resetCloudPolicyErrorsCacheForTest(): void {
|
|
158
|
+
lastWritten = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function serialize(errors: ReadonlyArray<CloudPolicyError>): string {
|
|
162
|
+
// Field order fixed, so equal lists are equal text. Redacted here as well as
|
|
163
|
+
// in `collectCloudPolicyErrors`: this is the one way into the file.
|
|
164
|
+
const list = errors.map((e) => ({ id: e.id, version: e.version, kind: e.kind, message: redactLocalPaths(e.message) }));
|
|
165
|
+
return `${JSON.stringify({ errors: list }, null, 2)}\n`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Replace `errors.json` with `errors` when that changes it. Never throws.
|
|
170
|
+
*
|
|
171
|
+
* - `"skipped"`: nothing to report and no file to clear, or no deployment here
|
|
172
|
+
* (no `active.json` — a disconnected or never-managed machine gets no file).
|
|
173
|
+
* - `"unchanged"`: the file already says exactly this.
|
|
174
|
+
* - `"written"`: it did not, and now does.
|
|
175
|
+
* - `"failed"`: the write itself failed (logged nowhere: the next event retries).
|
|
176
|
+
*/
|
|
177
|
+
export function writeCloudPolicyErrors(errors: ReadonlyArray<CloudPolicyError>): "written" | "unchanged" | "skipped" | "failed" {
|
|
178
|
+
try {
|
|
179
|
+
const root = cloudManagedPolicyRoot();
|
|
180
|
+
if (!existsSync(resolve(root, "active.json"))) return "skipped";
|
|
181
|
+
const path = cloudPolicyErrorsPath();
|
|
182
|
+
const text = serialize(errors);
|
|
183
|
+
if (lastWritten && lastWritten.path === path && lastWritten.text === text && existsSync(path)) return "unchanged";
|
|
184
|
+
let current: string | null = null;
|
|
185
|
+
try {
|
|
186
|
+
current = readFileSync(path, "utf8");
|
|
187
|
+
} catch {
|
|
188
|
+
current = null;
|
|
189
|
+
}
|
|
190
|
+
if (current === text) {
|
|
191
|
+
lastWritten = { path, text };
|
|
192
|
+
return "unchanged";
|
|
193
|
+
}
|
|
194
|
+
if (current === null && errors.length === 0) return "skipped";
|
|
195
|
+
writeAtomic(path, text);
|
|
196
|
+
lastWritten = { path, text };
|
|
197
|
+
return "written";
|
|
198
|
+
} catch {
|
|
199
|
+
// A report that cannot be written must not cost a hook its answer.
|
|
200
|
+
return "failed";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Which Cloud JS policies a hook has load-checked (N4) ────────────────────
|
|
205
|
+
//
|
|
206
|
+
// `failproofai policies` must not draw a Cloud policy as ● on before any hook
|
|
207
|
+
// has imported it: only the hook path loads Cloud JavaScript, and only the hook
|
|
208
|
+
// path writes `errors.json`, so right after a deployment a broken policy has
|
|
209
|
+
// no error yet. The hook path records here which `id@version`s of which
|
|
210
|
+
// deployment it has load-checked (loaded, or failed into `errors.json`); the
|
|
211
|
+
// listing shows the rest as not loaded yet. Union across hooks, since an
|
|
212
|
+
// agent-scoped policy is only ever loaded by its own agent's hooks.
|
|
213
|
+
|
|
214
|
+
export const CLOUD_LOAD_CHECKED_FILE = "load-checked.json";
|
|
215
|
+
|
|
216
|
+
export interface CloudLoadChecked {
|
|
217
|
+
deployment: number;
|
|
218
|
+
checked: string[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function cloudLoadCheckedPath(): string {
|
|
222
|
+
return resolve(cloudManagedPolicyRoot(), CLOUD_LOAD_CHECKED_FILE);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The record, or null when absent or unreadable. */
|
|
226
|
+
export function readCloudLoadChecked(): CloudLoadChecked | null {
|
|
227
|
+
try {
|
|
228
|
+
const raw = JSON.parse(readFileSync(cloudLoadCheckedPath(), "utf8")) as Record<string, unknown>;
|
|
229
|
+
if (!Number.isSafeInteger(raw.deployment) || !Array.isArray(raw.checked)) return null;
|
|
230
|
+
return { deployment: raw.deployment as number, checked: raw.checked.filter((c): c is string => typeof c === "string") };
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Add `policies` to the record for `deployment` (a new deployment starts afresh). Never throws; writes only on change. */
|
|
237
|
+
export function recordCloudLoadChecked(deployment: number, policies: ReadonlyArray<{ id: string; version: number }>): void {
|
|
238
|
+
try {
|
|
239
|
+
if (!existsSync(resolve(cloudManagedPolicyRoot(), "active.json"))) return;
|
|
240
|
+
const path = cloudLoadCheckedPath();
|
|
241
|
+
const prior = readCloudLoadChecked();
|
|
242
|
+
const base = prior && prior.deployment === deployment ? prior.checked : [];
|
|
243
|
+
const next = [...new Set([...base, ...policies.map((p) => `${p.id}@${p.version}`)])].sort();
|
|
244
|
+
if (prior && prior.deployment === deployment && next.length === base.length) return;
|
|
245
|
+
const text = `${JSON.stringify({ deployment, checked: next }, null, 2)}\n`;
|
|
246
|
+
writeAtomic(path, text);
|
|
247
|
+
} catch {
|
|
248
|
+
// A record that cannot be written costs only the listing's accuracy.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface CloudPolicyErrorInputs {
|
|
253
|
+
/** `readActiveCloudManagedPolicies` threw: the whole JS half is not enforcing. */
|
|
254
|
+
manifestError?: string | null;
|
|
255
|
+
/** What the JS reader returned. */
|
|
256
|
+
jsPolicies: ReadonlyArray<CloudManagedPolicyArtifact>;
|
|
257
|
+
/** Import failures by Cloud policy id (`LoadAllResult.cloudFailures`). */
|
|
258
|
+
jsFailures?: ReadonlyMap<string, PolicyLoadFailure>;
|
|
259
|
+
/** Cloud's Jev state (its own problems — an unreadable mode — are in `errors`). */
|
|
260
|
+
jev: Pick<CloudJevState, "errors">;
|
|
261
|
+
/**
|
|
262
|
+
* Why Cloud's `observe`/`enforce` mode cannot run here: `transcripts_disabled`,
|
|
263
|
+
* `jev_unconfigured`, or `jev_unconfigured: <detail>`
|
|
264
|
+
* (`loadJevConfigForCloudMode`). Null when it runs, or Cloud's mode does not ask.
|
|
265
|
+
*/
|
|
266
|
+
jevProblem?: string | null;
|
|
267
|
+
/** Installed packs' checks FailproofAI Cloud dropped for the budget ({@link readJevBudgetDrops}). */
|
|
268
|
+
budgetDrops?: ReadonlyArray<CloudPolicyError>;
|
|
269
|
+
/** FailproofAI Cloud Jev rate-limited or unavailable here (`semantic/cloud-jev-health.ts`). */
|
|
270
|
+
health?: ReadonlyArray<CloudPolicyError>;
|
|
271
|
+
/** Schema-3 deployment, but the running hook could not identify its agent. */
|
|
272
|
+
agentScopeUnresolved?: boolean;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A Cloud JS assignment's kind, as far as this machine can tell: `both` when
|
|
277
|
+
* the server derived a reviewer list for it (only a `both` policy gets one,
|
|
278
|
+
* CONTRACT C1), else `regex`.
|
|
279
|
+
*/
|
|
280
|
+
function kindOf(policy: Pick<CloudManagedPolicyArtifact, "authority" | "reviewedBy">): CloudPolicyErrorKind {
|
|
281
|
+
return policy.authority === "reviewable" && (policy.reviewedBy?.length ?? 0) > 0 ? "both" : "regex";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Everything wrong with this machine's Cloud deployment, as `errors.json`
|
|
286
|
+
* entries: deduplicated, in a stable order. Pure.
|
|
287
|
+
*
|
|
288
|
+
* What is NOT here any more (CONTRACT C10): Jev artifact and declaration
|
|
289
|
+
* failures and a `reviewedBy` naming a missing check — the machine holds no
|
|
290
|
+
* Cloud Jev check to fail, and cannot tell which names Cloud has. A `both`
|
|
291
|
+
* policy on a machine whose Cloud mode does not ask simply stays hard; the
|
|
292
|
+
* server knows the mode it set.
|
|
293
|
+
*/
|
|
294
|
+
export function collectCloudPolicyErrors(input: CloudPolicyErrorInputs): CloudPolicyError[] {
|
|
295
|
+
const out: CloudPolicyError[] = [];
|
|
296
|
+
if (input.manifestError) {
|
|
297
|
+
out.push({ id: "active.json", version: null, kind: "daemon", message: `Cloud policies could not be loaded: ${input.manifestError}` });
|
|
298
|
+
}
|
|
299
|
+
if (input.agentScopeUnresolved) {
|
|
300
|
+
out.push({
|
|
301
|
+
id: "agentScope", version: null, kind: "daemon",
|
|
302
|
+
message: "agent_scope_unresolved: this hook's agent profile is unknown; targeted Cloud policies did not match",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
for (const policy of input.jsPolicies) {
|
|
306
|
+
const failure = input.jsFailures?.get(policy.id);
|
|
307
|
+
if (failure) {
|
|
308
|
+
out.push({ id: policy.id, version: policy.version, kind: kindOf(policy), message: `policy did not load (${failure.type}): ${failure.reason}` });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
out.push(...input.jev.errors);
|
|
312
|
+
out.push(...(input.budgetDrops ?? []));
|
|
313
|
+
if (input.jevProblem) {
|
|
314
|
+
out.push({ id: "jevMode", version: null, kind: "daemon", message: input.jevProblem });
|
|
315
|
+
}
|
|
316
|
+
out.push(...(input.health ?? []));
|
|
317
|
+
|
|
318
|
+
const seen = new Set<string>();
|
|
319
|
+
return out
|
|
320
|
+
.map((e) => ({ ...e, message: redactLocalPaths(e.message) }))
|
|
321
|
+
.filter((e) => {
|
|
322
|
+
const key = `${e.id}\u0000${e.version}\u0000${e.kind}\u0000${e.message}`;
|
|
323
|
+
if (seen.has(key)) return false;
|
|
324
|
+
seen.add(key);
|
|
325
|
+
return true;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Installed packs' checks FailproofAI Cloud dropped for the budget ─────────
|
|
330
|
+
//
|
|
331
|
+
// FailproofAI Cloud puts its own checks first in the one question budget and
|
|
332
|
+
// drops whole installed-pack question groups, last first, when the two do not
|
|
333
|
+
// fit together (CONTRACT C10.4 step 5); its reply names them (`droppedLocal`).
|
|
334
|
+
// Which groups a call carries depends on the tool, so a report of only the
|
|
335
|
+
// latest reply's drops would flap from call to call. So the drops are kept,
|
|
336
|
+
// as a union, in `jev-budget.json` for the machine, deployment and Jev mode
|
|
337
|
+
// they happened under — a new deployment or mode starts afresh, which is how
|
|
338
|
+
// a drop that no longer happens stops being reported — and `errors.json`
|
|
339
|
+
// carries them as `{id: "pack:<packId>", kind: "daemon", message: "jev_budget:
|
|
340
|
+
// dropped <name>"}`. A check dropped because an org Cloud check has its name
|
|
341
|
+
// is recorded the same way, as `jev_name_clash: <name> (the FailproofAI Cloud
|
|
342
|
+
// check is used)`. A drop of a pack no longer installed is not reported, here
|
|
343
|
+
// or by the daemon's poll, which also drops the report once the deployment it
|
|
344
|
+
// was recorded under is no longer the active one.
|
|
345
|
+
|
|
346
|
+
/** What a set of budget drops is keyed by: they are true of this deployment only. */
|
|
347
|
+
export interface JevBudgetKey {
|
|
348
|
+
machineId: string;
|
|
349
|
+
deployment: number | null;
|
|
350
|
+
jevMode: "observe" | "enforce";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** One dropped pack check; `clash` when Cloud dropped it for a same-named Cloud check. */
|
|
354
|
+
export interface JevPackDrop {
|
|
355
|
+
packId: string;
|
|
356
|
+
name: string;
|
|
357
|
+
clash?: true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
interface JevBudgetRecord extends JevBudgetKey {
|
|
361
|
+
dropped: JevPackDrop[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function jevBudgetPath(): string {
|
|
365
|
+
return resolve(cloudManagedPolicyRoot(), JEV_BUDGET_FILE);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function readJevBudgetRecord(): JevBudgetRecord | null {
|
|
369
|
+
try {
|
|
370
|
+
const raw = JSON.parse(readFileSync(jevBudgetPath(), "utf8")) as Partial<JevBudgetRecord>;
|
|
371
|
+
if (
|
|
372
|
+
typeof raw.machineId !== "string" ||
|
|
373
|
+
!(raw.deployment === null || Number.isSafeInteger(raw.deployment)) ||
|
|
374
|
+
(raw.jevMode !== "observe" && raw.jevMode !== "enforce") ||
|
|
375
|
+
!Array.isArray(raw.dropped)
|
|
376
|
+
) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const dropped = raw.dropped
|
|
380
|
+
.filter((d): d is JevPackDrop => !!d && typeof d.packId === "string" && typeof d.name === "string")
|
|
381
|
+
.map((d) => ({ packId: d.packId, name: d.name, ...(d.clash === true ? { clash: true as const } : {}) }));
|
|
382
|
+
return { machineId: raw.machineId, deployment: raw.deployment as number | null, jevMode: raw.jevMode, dropped };
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const sameKey = (a: JevBudgetKey, b: JevBudgetKey): boolean =>
|
|
389
|
+
a.machineId === b.machineId && a.deployment === b.deployment && a.jevMode === b.jevMode;
|
|
390
|
+
|
|
391
|
+
const dropKey = (d: JevPackDrop): string => `${d.packId}\u0000${d.name}\u0000${d.clash === true}`;
|
|
392
|
+
|
|
393
|
+
function budgetEntries(dropped: ReadonlyArray<JevPackDrop>): CloudPolicyError[] {
|
|
394
|
+
return [...dropped]
|
|
395
|
+
.sort((a, b) => (dropKey(a) < dropKey(b) ? -1 : dropKey(a) > dropKey(b) ? 1 : 0))
|
|
396
|
+
.map((d) => ({
|
|
397
|
+
id: `pack:${d.packId}`,
|
|
398
|
+
version: null,
|
|
399
|
+
kind: "daemon" as const,
|
|
400
|
+
message: d.clash ? `${JEV_NAME_CLASH}: ${d.name} (the FailproofAI Cloud check is used)` : `${JEV_BUDGET}: dropped ${d.name}`,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The ids of the installed packs, read straight off `installed.json` (no
|
|
406
|
+
* artifact is verified: only the ids are wanted). An empty set when no pack is
|
|
407
|
+
* installed; null when the file exists and cannot be read, which filters
|
|
408
|
+
* nothing ("cannot tell" is not "not installed"). Never throws.
|
|
409
|
+
*/
|
|
410
|
+
export function installedPackIds(): ReadonlySet<string> | null {
|
|
411
|
+
try {
|
|
412
|
+
const path = resolve(packsRoot(), "installed.json");
|
|
413
|
+
if (!existsSync(path)) return new Set();
|
|
414
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as { packs?: unknown };
|
|
415
|
+
if (!Array.isArray(raw.packs)) return null;
|
|
416
|
+
return new Set(
|
|
417
|
+
raw.packs.map((p) => (p && typeof p === "object" ? (p as { id?: unknown }).id : null)).filter((id): id is string => typeof id === "string"),
|
|
418
|
+
);
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The drops recorded for exactly this machine, deployment and mode, as
|
|
426
|
+
* `errors.json` entries — none for any other key, and none of a pack that is
|
|
427
|
+
* no longer installed (`installed`, when known). Never throws.
|
|
428
|
+
*/
|
|
429
|
+
export function readJevBudgetDrops(key: JevBudgetKey, installed: ReadonlySet<string> | null = null): CloudPolicyError[] {
|
|
430
|
+
const record = readJevBudgetRecord();
|
|
431
|
+
if (!record || !sameKey(record, key)) return [];
|
|
432
|
+
return budgetEntries(installed ? record.dropped.filter((d) => installed.has(d.packId)) : record.dropped);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Add the pack checks FailproofAI Cloud just dropped to the record for `key`
|
|
437
|
+
* (starting afresh when the key changed), and to `errors.json` at once, so the
|
|
438
|
+
* report does not wait for the next hook. Both writes are atomic, owner-only,
|
|
439
|
+
* and happen only when something changed. Never throws: a report must not
|
|
440
|
+
* cost a hook its answer.
|
|
441
|
+
*/
|
|
442
|
+
export function recordJevBudgetDrops(key: JevBudgetKey, drops: ReadonlyArray<JevPackDrop>): void {
|
|
443
|
+
try {
|
|
444
|
+
if (drops.length === 0 || !existsSync(resolve(cloudManagedPolicyRoot(), "active.json"))) return;
|
|
445
|
+
const current = readJevBudgetRecord();
|
|
446
|
+
const base = current && sameKey(current, key) ? current.dropped : [];
|
|
447
|
+
const seen = new Set(base.map(dropKey));
|
|
448
|
+
const added: JevPackDrop[] = [];
|
|
449
|
+
for (const d of drops) {
|
|
450
|
+
const id = dropKey(d);
|
|
451
|
+
if (seen.has(id)) continue;
|
|
452
|
+
seen.add(id);
|
|
453
|
+
added.push({ packId: d.packId, name: d.name, ...(d.clash === true ? { clash: true as const } : {}) });
|
|
454
|
+
}
|
|
455
|
+
if (current && sameKey(current, key) && added.length === 0) return;
|
|
456
|
+
const record: JevBudgetRecord = { ...key, dropped: [...base, ...added] };
|
|
457
|
+
writeAtomic(jevBudgetPath(), `${JSON.stringify(record, null, 2)}\n`);
|
|
458
|
+
// Merged into the report as it stands: every other entry kept, the pack
|
|
459
|
+
// budget drops replaced by this deployment's.
|
|
460
|
+
const report = (readCloudPolicyErrors() ?? []).filter((e) => !isPackDropEntry(e));
|
|
461
|
+
writeCloudPolicyErrors([...report, ...budgetEntries(record.dropped)]);
|
|
462
|
+
} catch {
|
|
463
|
+
// The next event records it again.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function writeAtomic(path: string, text: string): void {
|
|
468
|
+
const tmp = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
469
|
+
try {
|
|
470
|
+
writeFileSync(tmp, text, { mode: 0o600, flag: "wx" });
|
|
471
|
+
chmodSync(tmp, 0o600);
|
|
472
|
+
renameSync(tmp, path);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
try {
|
|
475
|
+
rmSync(tmp, { force: true });
|
|
476
|
+
} catch {
|
|
477
|
+
// Nothing useful to do.
|
|
478
|
+
}
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
@@ -432,10 +432,16 @@ export type CodexHookTrust =
|
|
|
432
432
|
/** In hooks.json and not all trusted: Codex skips the untrusted ones. */
|
|
433
433
|
| { kind: "untrusted"; path: string; untrusted: number; total: number };
|
|
434
434
|
|
|
435
|
+
/** The Codex profile's home: `CODEX_HOME` when set, else `~/.codex`. */
|
|
436
|
+
export function codexHome(): string {
|
|
437
|
+
const dir = process.env.CODEX_HOME?.trim();
|
|
438
|
+
return dir ? resolve(dir) : resolve(homedir(), ".codex");
|
|
439
|
+
}
|
|
440
|
+
|
|
435
441
|
export interface CodexTrustDeps extends CodexSystemDeps {
|
|
436
|
-
/** `~/.codex/hooks.json
|
|
442
|
+
/** `$CODEX_HOME/hooks.json` (`~/.codex/hooks.json`). */
|
|
437
443
|
hooksJsonPath?: string;
|
|
438
|
-
/**
|
|
444
|
+
/** `$CODEX_HOME/config.toml`, where Codex keeps `[hooks.state]`. */
|
|
439
445
|
userConfigPath?: string;
|
|
440
446
|
}
|
|
441
447
|
|
|
@@ -452,7 +458,7 @@ export function codexHookTrust(deps: CodexTrustDeps = {}): CodexHookTrust {
|
|
|
452
458
|
const systemPath = resolvedPath(deps);
|
|
453
459
|
if (systemPath && codexSystemHooksPresent({ path: systemPath })) return { kind: "system", path: systemPath };
|
|
454
460
|
|
|
455
|
-
const hooksJsonPath = deps.hooksJsonPath ?? resolve(
|
|
461
|
+
const hooksJsonPath = deps.hooksJsonPath ?? resolve(codexHome(), "hooks.json");
|
|
456
462
|
const ours = failproofaiCodexHookIdentities(hooksJsonPath);
|
|
457
463
|
if (ours.length === 0) return { kind: "none" };
|
|
458
464
|
|
|
@@ -462,7 +468,7 @@ export function codexHookTrust(deps: CodexTrustDeps = {}): CodexHookTrust {
|
|
|
462
468
|
let trustedHashes = new Map<string, string>();
|
|
463
469
|
try {
|
|
464
470
|
trustedHashes = parseCodexTrustedHashes(
|
|
465
|
-
readFileSync(deps.userConfigPath ?? resolve(
|
|
471
|
+
readFileSync(deps.userConfigPath ?? resolve(codexHome(), "config.toml"), "utf8"),
|
|
466
472
|
);
|
|
467
473
|
} catch {
|
|
468
474
|
// No user config: nothing is trusted.
|
|
@@ -27,7 +27,7 @@ import { readCloudCredentials } from "./cloud-enrollment";
|
|
|
27
27
|
import { CODEX_UNTRUSTED_NOTICE, codexHookTrust, type CodexHookTrust } from "./codex-system-hooks";
|
|
28
28
|
import { describeMachine } from "./cloud-enrollment-cli";
|
|
29
29
|
import { cloudBaseFor } from "./cloud-connection";
|
|
30
|
-
import {
|
|
30
|
+
import { listActiveCloudManagedPolicies } from "./cloud-managed-policies";
|
|
31
31
|
import { readIngestCredential } from "./collector-config";
|
|
32
32
|
import { discoverPolicyFiles } from "./custom-hooks-loader";
|
|
33
33
|
import { daemonSocketPresent, isDaemonConfigured } from "./daemon-client";
|
|
@@ -259,7 +259,10 @@ export function summarizePolicySources(cwd?: string): StatusPolicies {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
try {
|
|
262
|
-
|
|
262
|
+
// Every assignment, whatever agent it targets: a hook's agent scope applied
|
|
263
|
+
// with no agent hid each targeted policy ("None installed" beside a
|
|
264
|
+
// deployment that enforces), the status twin of B4.
|
|
265
|
+
for (const artifact of listActiveCloudManagedPolicies()) {
|
|
263
266
|
if (artifact.effect === "observe") summary.observing++;
|
|
264
267
|
else summary.cloudOn++;
|
|
265
268
|
}
|
|
@@ -525,7 +528,10 @@ export function renderStatusScreen(facts: StatusFacts, opts: ScreenKitOpts & { c
|
|
|
525
528
|
return kit.caution(`No report from failproofaid for ${formatDuration(now - health.writtenAt)}`);
|
|
526
529
|
}
|
|
527
530
|
if (!health.delivering) return `${kit.off} Not delivering, failproofaid has no key`;
|
|
528
|
-
|
|
531
|
+
// Batches an earlier key had refused wait for the daemon to re-send them
|
|
532
|
+
// with the key that works now: queued, not refused.
|
|
533
|
+
const pending = queued + (refused.resending ?? 0);
|
|
534
|
+
const backlog = pending === 0 ? "nothing queued" : `${many(pending, "batch", "batches")} queued`;
|
|
529
535
|
const stuck =
|
|
530
536
|
queued > 0 &&
|
|
531
537
|
(oldestQueuedMs ?? 0) > STATUS_QUEUE_STUCK_MS &&
|
|
@@ -578,7 +584,7 @@ export function renderStatusScreen(facts: StatusFacts, opts: ScreenKitOpts & { c
|
|
|
578
584
|
`${pauseClockTime(pause.expiresAt, opts.clock)}${pause.cwd ? `${kit.sep}${pause.cwd}` : ""}`,
|
|
579
585
|
]),
|
|
580
586
|
// An enforcement fact, not fine print: a pause suspends local policy only.
|
|
581
|
-
...(facts.pauses.length > 0 ? [["", "Cloud-managed policies keep enforcing."] as [string, string]] : []),
|
|
587
|
+
...(facts.pauses.length > 0 ? [["", "Cloud-managed policies, and FailproofAI Cloud's Jev checks, keep enforcing."] as [string, string]] : []),
|
|
582
588
|
];
|
|
583
589
|
|
|
584
590
|
const cloudRows: Array<[string, string]> =
|