failproofai 1.0.0-beta.12 → 1.0.0-beta.13
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 +2 -3
- 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 +7 -7
- 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 +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- 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 +2 -3
- 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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- 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/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- 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 +2 -2
- 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 +3 -2
- 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 +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- 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 +2 -3
- 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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -3
- 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 +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +2 -3
- 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 +3 -4
- 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 +3 -4
- 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 +3 -4
- 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 +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -3
- 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/{[root-of-the-server]__1a7i2q4._.js → [root-of-the-server]__00ni9za._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_ps0jw._.js → [root-of-the-server]__0effm9m._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0lk_935._.js → [root-of-the-server]__0ix8gm7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0ky_n0r._.js → [root-of-the-server]__0ji11a-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0v2fpxe._.js → [root-of-the-server]__0ojwjxl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vb5fl2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uyc-z4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20d06rh._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -3
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1rdruw4._.js → [root-of-the-server]__0_1tiya._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ha3gul._.js → [root-of-the-server]__0eeipso._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tyf0tc._.js → [root-of-the-server]__17itmx-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1io8n-9._.js → [root-of-the-server]__1dx-a-c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1yw368c._.js → [root-of-the-server]__1n2vcv4._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1a2hcb_._.js → [root-of-the-server]__1tlvv9a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1fahg9e._.js → _0vo2qyt._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0uz7o9y._.js → _0xej-0e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_04mekdy._.js → _10399ge._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0se484m._.js → _149k9oy._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1vsgpqt._.js → _166jvu8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0r-dsre._.js → _1_hwp06._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w8sola._.js → _1mnhi_r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w_6p9g._.js → _1ror7k9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gmzkwj._.js → _1sdgprl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.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/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/{2_h--2ame3r83.js → 01s3vpzky_qew.js} +1 -1
- package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
- package/.next/standalone/.next/static/chunks/{23hp7hekegpe5.js → 0zbcd_n8ku2ov.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0cakk77ppmlo7.js → 1erji0ratofla.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40-zk44qsg5y-.js → 23hnlzqx44vbe.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1wb8cnmzx69rz.js → 2cwsiyhc7d2cw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_rdbv_k5j51p.js → 2y42ebzynyxdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1spk1bub0vd-h.js → 315lncj7dsp9c.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0-zh1umoq6ppc.js → 31_px0dfgdg9j.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2bie7ajv70p9b.js → 3l4-fu3xt75j8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{298v78ypf8edl.js → 3poscoxn69x9g.js} +1 -1
- package/.next/standalone/app/policies/hooks-client.tsx +4 -4
- package/.next/standalone/ino.ujh3/.failproofai/VERSION +4 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/last-version +1 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/telemetry-id +1 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +186 -5
- package/dist/cli.mjs +1130 -1352
- package/dist/worker.mjs +107 -1000
- package/package.json +10 -10
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/cloud-connection.ts +4 -4
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-enrollment.ts +3 -3
- package/src/hooks/cloud-managed-policies.ts +60 -22
- package/src/hooks/configure-wizard.ts +124 -53
- package/src/hooks/custom-hooks-loader.ts +57 -8
- package/src/hooks/daemon-service.ts +66 -11
- package/src/hooks/flush-cli.ts +180 -0
- package/src/hooks/fp-config.ts +242 -118
- package/src/hooks/fp-home.ts +138 -22
- package/src/hooks/fp-reset.ts +357 -43
- package/src/hooks/handler.ts +11 -11
- package/src/hooks/harness-cli.ts +309 -0
- package/src/hooks/hook-activity-store.ts +5 -5
- package/src/hooks/manager.ts +35 -0
- package/src/hooks/onboarding-attempt.ts +22 -6
- package/src/hooks/tui.ts +64 -7
- package/src/hooks/uninstall-cli.ts +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +0 -5
- package/.next/standalone/.next/static/chunks/13vrxdsdihqd_.css +0 -2
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_ssgManifest.js +0 -0
package/src/hooks/fp-reset.ts
CHANGED
|
@@ -29,12 +29,24 @@
|
|
|
29
29
|
* user out of their agent entirely, with no way back except hand-editing JSON.
|
|
30
30
|
* A loud warning that survives until setup runs is the proportionate answer.
|
|
31
31
|
*/
|
|
32
|
-
import {
|
|
33
|
-
|
|
32
|
+
import {
|
|
33
|
+
copyFileSync,
|
|
34
|
+
existsSync,
|
|
35
|
+
mkdirSync,
|
|
36
|
+
readFileSync,
|
|
37
|
+
readdirSync,
|
|
38
|
+
renameSync,
|
|
39
|
+
rmSync,
|
|
40
|
+
rmdirSync,
|
|
41
|
+
statSync,
|
|
42
|
+
writeFileSync,
|
|
43
|
+
} from "node:fs";
|
|
44
|
+
import { dirname, resolve } from "node:path";
|
|
34
45
|
import {
|
|
35
46
|
LAYOUT_VERSION,
|
|
36
47
|
customPoliciesDir,
|
|
37
48
|
failproofaiHome,
|
|
49
|
+
globalPolicyConfigFile,
|
|
38
50
|
hookActivityDir,
|
|
39
51
|
legacy,
|
|
40
52
|
policiesDir,
|
|
@@ -56,60 +68,123 @@ export interface ResetOutcome {
|
|
|
56
68
|
migrated: string[];
|
|
57
69
|
/** Basenames of decision-log pages carried into layout 2's `hook-activity/`. */
|
|
58
70
|
activity: string[];
|
|
71
|
+
/** Keys of the layout-1 policy config carried into layout 2's. */
|
|
72
|
+
policyConfig: string[];
|
|
59
73
|
/** The layout that was found before the reset. */
|
|
60
74
|
from: number;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
/** Script files a person could have written by hand. `.d.ts` is a type stub. */
|
|
64
|
-
const USER_SOURCE_RE = /\.(js|mjs|ts)$/;
|
|
65
|
-
|
|
66
78
|
/**
|
|
67
|
-
* Move
|
|
68
|
-
* `policies/custom-policies/`, where the loader
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* the
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
79
|
+
* Move the user's own policy directory back up: layout 2's
|
|
80
|
+
* `policies/custom-policies/` into layout 3's `policies/`, where the loader
|
|
81
|
+
* reads it.
|
|
82
|
+
*
|
|
83
|
+
* Left alone, nothing would ever load those files again — and worse, the reset
|
|
84
|
+
* DELETES `custom-policies/` (it is in `resettablePaths()`), so they would not
|
|
85
|
+
* merely stop loading, they would be gone.
|
|
86
|
+
*
|
|
87
|
+
* EVERY entry moves — every file regardless of extension, and every
|
|
88
|
+
* subdirectory — not just the loadable sources. Moving only `*.{js,mjs,ts}`
|
|
89
|
+
* deleted the rest along with the directory, and the two things it deleted are
|
|
90
|
+
* exactly the things a real policy depends on: a `lib/` of shared helpers the
|
|
91
|
+
* policy imports, and the `.json` data file it reads its rules from. The
|
|
92
|
+
* surviving policy then referenced a `./lib/rules.mjs` that no longer existed,
|
|
93
|
+
* so the migration turned a working policy into a broken one AND destroyed
|
|
94
|
+
* source nothing regenerates. Reproduced exactly that way on a seeded home.
|
|
95
|
+
*
|
|
96
|
+
* Moving the whole directory is also what keeps relative imports valid: every
|
|
97
|
+
* entry keeps its position relative to every other, so `./lib/rules.mjs`
|
|
98
|
+
* resolves after the move for the same reason it did before.
|
|
99
|
+
*
|
|
100
|
+
* A destination that already exists is never overwritten. Two things make that
|
|
101
|
+
* safe rather than lossy, and BOTH are required:
|
|
102
|
+
*
|
|
103
|
+
* - DIRECTORIES MERGE. A colliding directory is not a conflict — `lib/` on both
|
|
104
|
+
* sides usually holds different files, and the likeliest collision of all is
|
|
105
|
+
* exactly `lib/`, because layout 1 → 2 left one behind (it moved only
|
|
106
|
+
* `*.{js,mjs,ts}`, which is the bug this function now fixes). Skipping the
|
|
107
|
+
* whole directory on a name match discarded every file inside it. Only a
|
|
108
|
+
* genuine leaf collision — the same name, and at least one side a file — is
|
|
109
|
+
* left alone.
|
|
110
|
+
* - WHAT CANNOT MOVE IS NOT DELETED. `custom-policies/` is deliberately NOT in
|
|
111
|
+
* `resettablePaths()`; this function removes it itself, and only once it is
|
|
112
|
+
* empty. It used to be on that list, so every entry deliberately "left in
|
|
113
|
+
* place" was deleted seconds later by the same `resetHome()` call that had
|
|
114
|
+
* just decided to preserve it — the user lost hand-written source, and the
|
|
115
|
+
* policy that DID move was left importing a `./lib/rules.mjs` that no longer
|
|
116
|
+
* existed. Verified on a seeded home: the helper was gone from the machine
|
|
117
|
+
* entirely and the surviving policy failed to load.
|
|
87
118
|
*/
|
|
88
119
|
export function migrateConventionPolicies(): string[] {
|
|
89
|
-
|
|
120
|
+
// LAYOUT 3 REVERSED THIS. Layout 2 moved the user's `*.mjs` DOWN into
|
|
121
|
+
// `policies/custom-policies/`; layout 3 reads them straight out of
|
|
122
|
+
// `policies/`, which is where layout 1 kept them all along. So:
|
|
123
|
+
//
|
|
124
|
+
// • from layout 1 — nothing to do. The files are already in the directory
|
|
125
|
+
// that now loads them, and moving them anywhere would be a regression.
|
|
126
|
+
// • from layout 2 — move them back UP, out of `custom-policies/`, or the
|
|
127
|
+
// reset deletes that directory (it is in `resettablePaths()`) and takes
|
|
128
|
+
// the user's own policies with it.
|
|
129
|
+
//
|
|
130
|
+
// `resetHome` calls this BEFORE it deletes anything, which is what makes the
|
|
131
|
+
// second case safe rather than a race.
|
|
132
|
+
const from = legacy.customPoliciesDir();
|
|
90
133
|
if (!existsSync(from)) return [];
|
|
91
134
|
const moved: string[] = [];
|
|
92
|
-
try {
|
|
93
|
-
const entries = readdirSync(from, { withFileTypes: true }).filter(
|
|
94
|
-
(e) => e.isFile() && USER_SOURCE_RE.test(e.name) && !e.name.endsWith(".d.ts"),
|
|
95
|
-
);
|
|
96
|
-
if (entries.length === 0) return [];
|
|
97
135
|
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
// Returns the names it moved, relative to `dir`. Recurses only where both
|
|
137
|
+
// sides are directories; everything else either moves whole or stays put.
|
|
138
|
+
const mergeInto = (dir: string, dest: string, prefix: string): void => {
|
|
139
|
+
let entries;
|
|
140
|
+
try {
|
|
141
|
+
// No filter: this whole directory is the user's — policy sources, the
|
|
142
|
+
// helpers they import, and the data files they read.
|
|
143
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
144
|
+
} catch {
|
|
145
|
+
// A directory we cannot read is not worth aborting a reset over, and its
|
|
146
|
+
// contents stay where they are rather than being lost.
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (entries.length === 0) return;
|
|
150
|
+
try {
|
|
151
|
+
mkdirSync(dest, { recursive: true });
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
100
155
|
for (const entry of entries) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
const source = resolve(dir, entry.name);
|
|
157
|
+
const target = resolve(dest, entry.name);
|
|
158
|
+
const name = `${prefix}${entry.name}`;
|
|
159
|
+
if (!existsSync(target)) {
|
|
160
|
+
try {
|
|
161
|
+
renameSync(source, target);
|
|
162
|
+
moved.push(name);
|
|
163
|
+
} catch {
|
|
164
|
+
// One unmovable entry must not strand the rest — and because this
|
|
165
|
+
// directory is no longer on the reset list, "not moved" now really
|
|
166
|
+
// does mean "still on disk".
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// Both directories: merge their contents rather than discarding the
|
|
171
|
+
// source wholesale. Anything genuinely colliding inside is left alone by
|
|
172
|
+
// the same rule, one level down.
|
|
173
|
+
if (entry.isDirectory() && statSync(target).isDirectory()) {
|
|
174
|
+
mergeInto(source, target, `${name}/`);
|
|
109
175
|
}
|
|
110
176
|
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
mergeInto(from, policiesDir(), "");
|
|
180
|
+
|
|
181
|
+
// Remove the directory ONLY if the merge emptied it. A non-empty
|
|
182
|
+
// `custom-policies/` is the user's remaining files, and this is the one place
|
|
183
|
+
// that decides their fate — `resettablePaths()` no longer lists it.
|
|
184
|
+
try {
|
|
185
|
+
rmdirSync(from);
|
|
111
186
|
} catch {
|
|
112
|
-
//
|
|
187
|
+
// Not empty, or not removable. Either way it stays, which is the point.
|
|
113
188
|
}
|
|
114
189
|
return moved.sort((a, b) => a.localeCompare(b));
|
|
115
190
|
}
|
|
@@ -202,6 +277,126 @@ export function migrateHookActivity(): string[] {
|
|
|
202
277
|
return moved.sort((a, b) => a.localeCompare(b));
|
|
203
278
|
}
|
|
204
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Fields of layout 1's `policies-config.json` that layout 2's still means.
|
|
282
|
+
*
|
|
283
|
+
* An allowlist, not a copy, and the exclusion is the point: layout 1's file
|
|
284
|
+
* ALSO carried a `collector` block, and layout 2 moved collector settings to
|
|
285
|
+
* `[collector]` in `config.toml` — in snake_case, deliberately, because
|
|
286
|
+
* `fpai-collect`'s `Settings` deserializes them and camelCase keys would make
|
|
287
|
+
* every field silently fall back to its default (see the note on `Settings` in
|
|
288
|
+
* `crates/fpai-collect/src/config.rs`, which records that exact bug). Carrying
|
|
289
|
+
* `collector` forward would put a block into the new file that nothing reads,
|
|
290
|
+
* where it would look like a preserved setting and behave like an absent one.
|
|
291
|
+
*
|
|
292
|
+
* The `llm` block moves because the loader still reads it from here, and its
|
|
293
|
+
* value is an endpoint and a model name a person configured by hand.
|
|
294
|
+
*/
|
|
295
|
+
const CARRIED_POLICY_CONFIG_KEYS = [
|
|
296
|
+
"enabledPolicies",
|
|
297
|
+
"customPoliciesPaths",
|
|
298
|
+
"customPoliciesPath",
|
|
299
|
+
"disabledCustomPolicies",
|
|
300
|
+
"conventionPolicies",
|
|
301
|
+
"disabledConventionPolicies",
|
|
302
|
+
"policyParams",
|
|
303
|
+
"llm",
|
|
304
|
+
] as const;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Carry the user's policy SELECTION across the layout-1 → layout-2 move.
|
|
308
|
+
*
|
|
309
|
+
* Layout 1 kept it at `~/.failproofai/policies-config.json`; layout 2 keeps it
|
|
310
|
+
* at `policies/local-policies/policies-config.json`, and both were on the reset
|
|
311
|
+
* list. So an upgrade silently emptied `enabledPolicies` — every builtin the
|
|
312
|
+
* user had turned on, every explicit `customPoliciesPaths` entry, and every
|
|
313
|
+
* per-policy parameter. The machine still read as configured afterwards
|
|
314
|
+
* (`isConfigured()` is a union that sees the agent CLIs' untouched settings
|
|
315
|
+
* files), so hooks kept firing against a policy set that had quietly become
|
|
316
|
+
* the default one. That is the same silent enforcement gap
|
|
317
|
+
* `migrateConventionPolicies()` exists to close, by a different route.
|
|
318
|
+
*
|
|
319
|
+
* This is deliberately NARROW. The standing decision for layout 1 is
|
|
320
|
+
* wipe-and-re-setup rather than migrate, because a half-migrated home that
|
|
321
|
+
* reads "no data" is worse than one that says so — see `legacy` in
|
|
322
|
+
* `fp-home.ts`. Everything derived (cursors, spool, health, audit cache) still
|
|
323
|
+
* goes and is rebuilt. What is carried is only what a person typed and nothing
|
|
324
|
+
* regenerates, which is the same test `migrateHookActivity()` applies to the
|
|
325
|
+
* decision log.
|
|
326
|
+
*
|
|
327
|
+
* # Two phases, because the source and the destination are BOTH on the list
|
|
328
|
+
*
|
|
329
|
+
* Unlike the two migrations above, this one cannot run entirely before the
|
|
330
|
+
* deletions. Its source (`legacy.policyConfig()`) is removed by them, and so is
|
|
331
|
+
* its destination (`globalPolicyConfigFile()`) — rightly, since clearing a
|
|
332
|
+
* stale selection on a layout migration is the documented behaviour. Writing
|
|
333
|
+
* first would have the reset delete the carry moments after it happened, which
|
|
334
|
+
* is exactly what the note on `hookActivityDir()` in `resettablePaths()` records
|
|
335
|
+
* happening once already.
|
|
336
|
+
*
|
|
337
|
+
* So: READ before, WRITE after.
|
|
338
|
+
*/
|
|
339
|
+
export function readCarriedPolicyConfig(): Record<string, unknown> | null {
|
|
340
|
+
const to = globalPolicyConfigFile();
|
|
341
|
+
// NEWEST SOURCE WINS. Two older layouts kept this file in two places, and a
|
|
342
|
+
// home being upgraded may hold either:
|
|
343
|
+
//
|
|
344
|
+
// layout 2 — `policies/local-policies/policies-config.json`
|
|
345
|
+
// layout 1 — `policies-config.json` at the home root
|
|
346
|
+
//
|
|
347
|
+
// Layout 3 puts it back at the ROOT, so layout 1's path and the destination
|
|
348
|
+
// are now the same file. Reading layout 1's copy while a layout-2 one exists
|
|
349
|
+
// would carry the OLDER answer forward and silently undo whatever the user
|
|
350
|
+
// chose since — so layout 2's nested file is preferred whenever it is there.
|
|
351
|
+
const layoutTwo = resolve(legacy.localPoliciesDir(), "policies-config.json");
|
|
352
|
+
const from = existsSync(layoutTwo) ? layoutTwo : legacy.policyConfig();
|
|
353
|
+
if (!existsSync(from)) return null;
|
|
354
|
+
|
|
355
|
+
let parsed: Record<string, unknown>;
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(readFileSync(from, "utf8")) as Record<string, unknown>;
|
|
358
|
+
} catch {
|
|
359
|
+
// Unparseable is not worth aborting a reset over, and there is nothing to
|
|
360
|
+
// carry. The file is removed with the rest of layout 1, which is also what
|
|
361
|
+
// would have happened before this function existed.
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
365
|
+
|
|
366
|
+
const carried: Record<string, unknown> = {};
|
|
367
|
+
let found = false;
|
|
368
|
+
for (const key of CARRIED_POLICY_CONFIG_KEYS) {
|
|
369
|
+
if (parsed[key] !== undefined) {
|
|
370
|
+
carried[key] = parsed[key];
|
|
371
|
+
found = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Nothing worth carrying — a file holding only excluded fields must not
|
|
375
|
+
// produce an empty layout-2 config that looks like a real one.
|
|
376
|
+
if (!found) return null;
|
|
377
|
+
|
|
378
|
+
// `enabledPolicies` is required by the type and by every reader. A layout-1
|
|
379
|
+
// file that somehow lacked it would otherwise produce a layout-2 file that
|
|
380
|
+
// throws on read — worse than the empty default it replaces.
|
|
381
|
+
if (carried.enabledPolicies === undefined) carried.enabledPolicies = [];
|
|
382
|
+
return carried;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Second phase of {@link readCarriedPolicyConfig}; runs AFTER the deletions. */
|
|
386
|
+
export function writeCarriedPolicyConfig(carried: Record<string, unknown> | null): string[] {
|
|
387
|
+
if (!carried) return [];
|
|
388
|
+
const to = globalPolicyConfigFile();
|
|
389
|
+
try {
|
|
390
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
391
|
+
writeFileSync(to, JSON.stringify(carried, null, 2) + "\n", "utf8");
|
|
392
|
+
} catch {
|
|
393
|
+
// A destination we cannot write is not worth aborting the reset over; the
|
|
394
|
+
// user re-runs setup, which is the pre-existing behaviour.
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
return Object.keys(carried).sort((a, b) => a.localeCompare(b));
|
|
398
|
+
}
|
|
399
|
+
|
|
205
400
|
/**
|
|
206
401
|
* Delete every path layout 1 or layout 2 could have written, then stamp
|
|
207
402
|
* VERSION so the next run reads as current.
|
|
@@ -212,13 +407,73 @@ export function migrateHookActivity(): string[] {
|
|
|
212
407
|
* large, version-pinned and re-verified on use, and `run/` holds sockets
|
|
213
408
|
* belonging to a process that may be alive right now.
|
|
214
409
|
*/
|
|
410
|
+
/**
|
|
411
|
+
* Carry a telemetry OPT-OUT across a layout-2 upgrade.
|
|
412
|
+
*
|
|
413
|
+
* Layout 2 kept it in `config.toml`, which the reset deletes, and layout 3
|
|
414
|
+
* writes a fresh `config.json` whose default is `telemetry.enabled: true`. So an
|
|
415
|
+
* upgrade silently turned anonymous telemetry back ON for anyone who had turned
|
|
416
|
+
* it off — and that file is the ONLY off-switch that reaches the daemon, because
|
|
417
|
+
* a system-scope service unit does not inherit `FAILPROOFAI_TELEMETRY_DISABLED`
|
|
418
|
+
* from anyone's shell. An opt-out that a routine upgrade revokes is not an
|
|
419
|
+
* opt-out.
|
|
420
|
+
*
|
|
421
|
+
* Deliberately one-directional: only `false` is carried. Everything else in that
|
|
422
|
+
* file is either re-derived by setup or a thing the wizard re-asks, and carrying
|
|
423
|
+
* an ENABLED flag forward would be carrying the default, which is not a choice
|
|
424
|
+
* anyone made. This matches the narrow rule the policy-config carry follows —
|
|
425
|
+
* move only what a person typed and nothing regenerates.
|
|
426
|
+
*
|
|
427
|
+
* A regex rather than a TOML parser, for the reason `readLegacyTomlVersion`
|
|
428
|
+
* gives: the dependency is gone, and this is a flat `key = value` file this
|
|
429
|
+
* codebase wrote. Read BEFORE the deletions, applied after — same two-phase
|
|
430
|
+
* shape, same reason.
|
|
431
|
+
*/
|
|
432
|
+
export function readCarriedTelemetryOptOut(): boolean {
|
|
433
|
+
const from = legacy.configToml();
|
|
434
|
+
if (!existsSync(from)) return false;
|
|
435
|
+
try {
|
|
436
|
+
const text = readFileSync(from, "utf8");
|
|
437
|
+
// Scoped to the `[telemetry]` table, not the whole file — a stray
|
|
438
|
+
// `enabled = false` under `[collector]` is not a telemetry opt-out.
|
|
439
|
+
//
|
|
440
|
+
// Sliced rather than matched with a single expression: the obvious form ends
|
|
441
|
+
// the section with `(?=^\s*\[|\Z)`, and JavaScript HAS NO `\Z`. It parses as
|
|
442
|
+
// a literal "Z", so the lookahead only ever succeeded via its other branch —
|
|
443
|
+
// meaning the section was found when another table followed it and MISSED
|
|
444
|
+
// whenever `[telemetry]` was last in the file, which is exactly where a
|
|
445
|
+
// hand-added opt-out tends to be. Caught by the test for this function; the
|
|
446
|
+
// live fixture happened to have `[collector]` after it and passed.
|
|
447
|
+
const start = text.search(/^\s*\[telemetry\]\s*$/m);
|
|
448
|
+
if (start === -1) return false;
|
|
449
|
+
const body = text.slice(start).replace(/^\s*\[telemetry\]\s*$/m, "");
|
|
450
|
+
const nextTable = body.search(/^\s*\[/m);
|
|
451
|
+
const section = nextTable === -1 ? body : body.slice(0, nextTable);
|
|
452
|
+
return /^\s*enabled\s*=\s*false\s*$/m.test(section);
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
215
458
|
export function resetHome(from: number): ResetOutcome {
|
|
216
459
|
// BEFORE the deletions, so a file that is mid-move is never one the reset
|
|
217
460
|
// then walks over.
|
|
218
461
|
const migrated = migrateConventionPolicies();
|
|
219
462
|
const activity = migrateHookActivity();
|
|
463
|
+
const pendingPolicyConfig = readCarriedPolicyConfig();
|
|
464
|
+
// Read before the deletions for the same reason as the policy config: its
|
|
465
|
+
// source (`config.toml`) is on the list below.
|
|
466
|
+
const telemetryOptOut = readCarriedTelemetryOptOut();
|
|
220
467
|
const removed: string[] = [];
|
|
221
468
|
for (const path of resettablePaths()) {
|
|
469
|
+
// A reset FROM the current layout is not a layout migration, and the only
|
|
470
|
+
// thing under `local-policies/` is the user's enabled-policy selection.
|
|
471
|
+
// Clearing it is right when moving off an old layout (setup re-asks) and
|
|
472
|
+
// wrong when nothing is being migrated — there it would silently discard a
|
|
473
|
+
// current, valid selection, including one this function had just carried.
|
|
474
|
+
// The single production caller always passes a DETECTED stale layout, so
|
|
475
|
+
// this only ever changes the forced same-layout case.
|
|
476
|
+
if (from === LAYOUT_VERSION && path === globalPolicyConfigFile()) continue;
|
|
222
477
|
if (!existsSync(path)) continue;
|
|
223
478
|
try {
|
|
224
479
|
rmSync(path, { recursive: true, force: true });
|
|
@@ -229,8 +484,12 @@ export function resetHome(from: number): ResetOutcome {
|
|
|
229
484
|
// directory from an old layout is inert once nothing reads it.
|
|
230
485
|
}
|
|
231
486
|
}
|
|
487
|
+
const policyConfig = writeCarriedPolicyConfig(pendingPolicyConfig);
|
|
488
|
+
// After the deletions, onto the fresh config, so the upgrade cannot revoke a
|
|
489
|
+
// choice the user made.
|
|
490
|
+
if (telemetryOptOut) updateConfig({ telemetry: { enabled: false } });
|
|
232
491
|
writeVersionFile();
|
|
233
|
-
return { removed, migrated, activity, from };
|
|
492
|
+
return { removed, migrated, activity, policyConfig, from };
|
|
234
493
|
}
|
|
235
494
|
|
|
236
495
|
export interface LayoutCheck {
|
|
@@ -320,7 +579,15 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
|
320
579
|
};
|
|
321
580
|
}
|
|
322
581
|
|
|
323
|
-
|
|
582
|
+
// Write the marker back for a fresh home — and ALSO for one whose layout had
|
|
583
|
+
// to be recovered from a landmark. `inferred` means the home is genuinely on
|
|
584
|
+
// this layout and only `VERSION` is missing; leaving it missing means every
|
|
585
|
+
// later command re-infers it, and `writeVersionFile()` carries the daemon
|
|
586
|
+
// version forward from the file it just failed to read, so the recorded
|
|
587
|
+
// daemon version is erased and `daemonVersionSkew()` goes quiet about a stale
|
|
588
|
+
// daemon. Re-stamping is what "the only thing actually missing is the marker"
|
|
589
|
+
// was supposed to mean.
|
|
590
|
+
if (state.kind === "absent" || (state.kind === "current" && state.inferred)) writeVersionFile();
|
|
324
591
|
return {
|
|
325
592
|
state,
|
|
326
593
|
fatal: false,
|
|
@@ -447,6 +714,40 @@ function staleDaemonHint(): string[] {
|
|
|
447
714
|
* machine whose global policies have silently stopped firing) persists until
|
|
448
715
|
* somebody acts on it.
|
|
449
716
|
*/
|
|
717
|
+
/**
|
|
718
|
+
* Why an UNATTENDED run must not proceed — a different question from the hook's.
|
|
719
|
+
*
|
|
720
|
+
* The hook asks "are global policies unenforced?", and answers it about one
|
|
721
|
+
* file. This asks "is this home on a layout this build understands?", and the
|
|
722
|
+
* answer must not be softened by anything: the scheduled audit runs with nobody
|
|
723
|
+
* watching, and the ordinary CLI path RESETS a stale home — which deletes
|
|
724
|
+
* `config.toml` (revoking a telemetry opt-out and the `[audit] auto` flag that
|
|
725
|
+
* scheduled the run) and `credentials.toml` (dropping cloud enrolment). Doing
|
|
726
|
+
* that on a timer, with the explanation going only to the service journal, is
|
|
727
|
+
* the failure this gate exists to prevent.
|
|
728
|
+
*
|
|
729
|
+
* Sharing `layoutWarningForHook()` for both is what made this subtle: teaching
|
|
730
|
+
* that function to stay quiet when the global policy config IS readable — right
|
|
731
|
+
* for the hook, since layout 1 and layout 3 keep it at the same path — silently
|
|
732
|
+
* turned this gate off for exactly the homes most likely to hit it.
|
|
733
|
+
*/
|
|
734
|
+
export function layoutBlockerForScheduledRun(): string | null {
|
|
735
|
+
const state = detectLayout();
|
|
736
|
+
if (state.kind === "current" || state.kind === "absent") return null;
|
|
737
|
+
if (state.kind === "future") {
|
|
738
|
+
return (
|
|
739
|
+
`[failproofai] this directory was written by a newer version ` +
|
|
740
|
+
`(layout ${state.found} vs ${LAYOUT_VERSION}) — refusing to run unattended. ` +
|
|
741
|
+
`Upgrade failproofai.`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return (
|
|
745
|
+
`[failproofai] setup predates this version (layout ${state.found} vs ${LAYOUT_VERSION}) — ` +
|
|
746
|
+
`refusing to run unattended, because completing it would reset this home. ` +
|
|
747
|
+
`Run \`failproofai config\`.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
450
751
|
export function layoutWarningForHook(): string | null {
|
|
451
752
|
const state = detectLayout();
|
|
452
753
|
if (state.kind === "current" || state.kind === "absent") return null;
|
|
@@ -457,6 +758,19 @@ export function layoutWarningForHook(): string | null {
|
|
|
457
758
|
`Upgrade failproofai.`
|
|
458
759
|
);
|
|
459
760
|
}
|
|
761
|
+
// The claim is about ONE FILE — the global policy config — so ask about that
|
|
762
|
+
// file rather than about the layout number. Layout 1 and layout 3 keep it at
|
|
763
|
+
// the SAME path (`~/.failproofai/policies-config.json`), so a home carrying
|
|
764
|
+
// one is read as layout 1 by the landmark fallback while the hook path loads
|
|
765
|
+
// and enforces it perfectly well. Hand-writing that file is something
|
|
766
|
+
// `docs/configuration.mdx` explicitly tells people to do, and they were told
|
|
767
|
+
// on every single tool call that their policies were not being enforced
|
|
768
|
+
// WHILE THEY WERE — the deny and the warning printed together.
|
|
769
|
+
//
|
|
770
|
+
// A warning that contradicts the behaviour it describes is worse than no
|
|
771
|
+
// warning: it teaches people to ignore the one channel that will matter when
|
|
772
|
+
// enforcement really has stopped.
|
|
773
|
+
if (existsSync(globalPolicyConfigFile())) return null;
|
|
460
774
|
return (
|
|
461
775
|
`[failproofai] setup predates this version — global policies are NOT being enforced. ` +
|
|
462
776
|
`Run \`failproofai config\` to re-create it.`
|
package/src/hooks/handler.ts
CHANGED
|
@@ -254,13 +254,13 @@ export async function evaluateHookEvent(
|
|
|
254
254
|
/** Registered policy name → where it came from. See the set() below. */
|
|
255
255
|
const policyAttribution = new Map<
|
|
256
256
|
string,
|
|
257
|
-
{ source: "custom" | "convention" | "cloud"; cloudPolicyId?: string;
|
|
257
|
+
{ source: "custom" | "convention" | "cloud"; cloudPolicyId?: string; cloudVersion?: number }
|
|
258
258
|
>();
|
|
259
|
-
let
|
|
259
|
+
let cloudDeployment: number | undefined;
|
|
260
260
|
/** What observe-mode policies WOULD have done, had they been enforcing. */
|
|
261
261
|
const observedResults: Array<{
|
|
262
262
|
policyId: string;
|
|
263
|
-
|
|
263
|
+
version: number;
|
|
264
264
|
decision: "deny" | "instruct";
|
|
265
265
|
reason: string | null;
|
|
266
266
|
}> = [];
|
|
@@ -293,7 +293,7 @@ export async function evaluateHookEvent(
|
|
|
293
293
|
|
|
294
294
|
// Cloud-managed policies are daemon-reconciled artifacts, but they use
|
|
295
295
|
// the same public JS policy API as local custom policies. Verify and add
|
|
296
|
-
// only the paths referenced by the atomically active
|
|
296
|
+
// only the paths referenced by the atomically active deployment.
|
|
297
297
|
//
|
|
298
298
|
// Wrapped, like `readConfigAt` and `readActivePause` above it. This call
|
|
299
299
|
// has fourteen throw sites — a malformed manifest, an unsafe id, an
|
|
@@ -332,7 +332,7 @@ export async function evaluateHookEvent(
|
|
|
332
332
|
// policy decided this event: "what was deployed here at the time" is a
|
|
333
333
|
// separate question from "what decided", and only the former can tell a
|
|
334
334
|
// rollout that changed nothing from one that never reached the machine.
|
|
335
|
-
|
|
335
|
+
cloudDeployment = cloudManagedPolicies[0]?.deployment;
|
|
336
336
|
const configuredCustomPaths = config.customPoliciesPaths ?? config.customPoliciesPath;
|
|
337
337
|
const allExplicitPaths =
|
|
338
338
|
cloudManagedPolicies.length === 0
|
|
@@ -368,7 +368,7 @@ export async function evaluateHookEvent(
|
|
|
368
368
|
const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
|
|
369
369
|
const isConvention = !!conventionScope;
|
|
370
370
|
const prefix = cloudManaged
|
|
371
|
-
? `cloud/${cloudManaged.id}@${cloudManaged.
|
|
371
|
+
? `cloud/${cloudManaged.id}@${cloudManaged.version}`
|
|
372
372
|
: isConvention
|
|
373
373
|
? `.failproofai-${conventionScope}`
|
|
374
374
|
: "custom";
|
|
@@ -383,7 +383,7 @@ export async function evaluateHookEvent(
|
|
|
383
383
|
if (shadow.decision !== "allow") {
|
|
384
384
|
observedResults.push({
|
|
385
385
|
policyId: cloudManaged!.id,
|
|
386
|
-
|
|
386
|
+
version: cloudManaged!.version,
|
|
387
387
|
decision: shadow.decision,
|
|
388
388
|
reason: shadow.reason ?? null,
|
|
389
389
|
});
|
|
@@ -419,7 +419,7 @@ export async function evaluateHookEvent(
|
|
|
419
419
|
// this decision", could only be answered by re-parsing our own label.
|
|
420
420
|
policyAttribution.set(registeredName, {
|
|
421
421
|
source: cloudManaged ? "cloud" : isConvention ? "convention" : "custom",
|
|
422
|
-
...(cloudManaged ? { cloudPolicyId: cloudManaged.id,
|
|
422
|
+
...(cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudVersion: cloudManaged.version } : {}),
|
|
423
423
|
});
|
|
424
424
|
registerPolicy(
|
|
425
425
|
registeredName,
|
|
@@ -497,13 +497,13 @@ export async function evaluateHookEvent(
|
|
|
497
497
|
return {
|
|
498
498
|
policySource: attribution?.source ?? ("builtin" as const),
|
|
499
499
|
...(attribution?.cloudPolicyId ? { cloudPolicyId: attribution.cloudPolicyId } : {}),
|
|
500
|
-
...(attribution?.
|
|
501
|
-
? {
|
|
500
|
+
...(attribution?.cloudVersion !== undefined
|
|
501
|
+
? { cloudVersion: attribution.cloudVersion }
|
|
502
502
|
: {}),
|
|
503
503
|
};
|
|
504
504
|
})()
|
|
505
505
|
: {}),
|
|
506
|
-
...(
|
|
506
|
+
...(cloudDeployment !== undefined ? { cloudDeployment } : {}),
|
|
507
507
|
// The point of observe mode is this record. Without it the rollout is
|
|
508
508
|
// unmeasurable and the row is indistinguishable from one where the policy
|
|
509
509
|
// never matched at all.
|