failproofai 1.0.0-beta.14 → 1.0.0-beta.16
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 +5 -5
- 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/build-manifest.json +2 -2
- 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 -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 +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/build-manifest.json +2 -2
- 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 -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 +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- 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 +9 -9
- 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 +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +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 +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/reminder/route.js +1 -1
- 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 +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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +2 -1
- 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/build-manifest.json +2 -2
- 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 +2 -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/build-manifest.json +2 -2
- 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 +2 -1
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -1
- 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]__06hexd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +3 -0
- 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]__0hxmox-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12y_xo0._.js → [root-of-the-server]__0no2gzh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17itmx-._.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]__1u-wx42._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{_202d777._.js → _09tcu2b._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0vo2qyt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0xej-0e._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_10399ge._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_149k9oy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_hwp06._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1mnhi_r._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ror7k9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- 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/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- 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/{0mot4rr-baiiq.js → 0fx3tgb_n0vz5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1_0nr2fuuslbb.js → 16yzosmvpinbh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ynhunx0nt0n-.js → 1_as8528fwqxf.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1tyg866lrq4kh.js → 1nsy22eg5q-pi.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2zilodg05qgcf.js → 2f2h7g8sm0bhr.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pkz6r-cgdr3-.js → 2n6wkcj5q_0yd.js} +2 -2
- package/.next/standalone/.next/static/chunks/{30f--ye1ly8m6.js → 2okcei5v0-xrx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31fmsp1soxi07.js → 2rl5gq4xl8oa2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1wn2p7yzbcifa.js → 3vsiw3ayz3c7_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2hiai8kvhjcov.js → 3xp43uu8jj8t4.js} +1 -1
- package/.next/standalone/.next/static/chunks/43kqvfa-2jq0g.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-3diub4ctvn1n0.js → turbopack-0ud32mdjjcn6y.js} +1 -1
- package/.next/standalone/SECURITY.md +8 -2
- package/.next/standalone/app/actions/get-hooks-config.ts +0 -1
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +1 -3
- package/.next/standalone/app/audit/_components/audit-progress-strip.tsx +1 -1
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +0 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +1 -1
- package/.next/standalone/app/audit/_components/quirks-section.tsx +0 -1
- package/.next/standalone/app/audit/_components/report-footer.tsx +0 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/_components/strengths-section.tsx +0 -1
- package/.next/standalone/app/components/pause-notices.tsx +1 -1
- package/.next/standalone/app/components/raw-log-viewer.tsx +1 -1
- package/.next/standalone/app/components/toast.tsx +1 -1
- package/.next/standalone/app/policies/hooks-client.tsx +3 -2
- package/.next/standalone/app/settings/settings-client.tsx +1 -2
- package/.next/standalone/lib/auth/auth-store.ts +0 -1
- package/.next/standalone/package.json +10 -12
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +253 -33
- package/bin/failproofaid-shim.mjs +4 -4
- package/dist/cli.mjs +6337 -3869
- package/dist/worker.mjs +26 -21
- package/lib/auth/auth-store.ts +0 -1
- package/package.json +10 -12
- package/scripts/dev-hook.mjs +46 -5
- package/scripts/translate-docs/cache.ts +1 -1
- package/scripts/translate-docs/cli.ts +2 -2
- package/scripts/translate-docs/mintlify-nav.ts +1 -1
- package/src/audit/cache.ts +0 -1
- package/src/audit/dashboard-cache.ts +0 -2
- package/src/audit/report.ts +0 -1
- package/src/hooks/builtin-policies.ts +1 -1
- package/src/hooks/cloud-enrollment-cli.ts +0 -2
- package/src/hooks/cloud-enrollment.ts +1 -2
- package/src/hooks/cloud-managed-policies.ts +0 -1
- package/src/hooks/collector-config.ts +4 -20
- package/src/hooks/configure-wizard.ts +0 -4
- package/src/hooks/custom-hooks-loader.ts +0 -1
- package/src/hooks/daemon-client.ts +4 -28
- package/src/hooks/daemon-download.ts +0 -1
- package/src/hooks/daemon-service.ts +108 -16
- package/src/hooks/fp-config.ts +226 -12
- package/src/hooks/fp-home.ts +270 -56
- package/src/hooks/fp-reset.ts +482 -73
- package/src/hooks/hook-logger.ts +0 -1
- package/src/hooks/integrations.ts +0 -1
- package/src/hooks/loader-utils.ts +1 -1
- package/src/hooks/manager.ts +1 -1
- package/src/hooks/migrations.ts +381 -0
- package/src/hooks/onboarding-lock.ts +1 -2
- package/src/hooks/session-pause.ts +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uyc-z4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vlyx3j._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
- package/.next/standalone/.next/static/chunks/2yy1767yfda67.js +0 -1
- package/.next/standalone/ino.ujh3/.failproofai/VERSION +0 -4
- package/.next/standalone/ino.ujh3/.failproofai/state/last-version +0 -1
- package/.next/standalone/ino.ujh3/.failproofai/state/telemetry-id +0 -1
- /package/.next/standalone/.next/static/{ZbPDZe0dNBNdNmo31aq_e → kSTfP_Tzl760gYDnYr-JC}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZbPDZe0dNBNdNmo31aq_e → kSTfP_Tzl760gYDnYr-JC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZbPDZe0dNBNdNmo31aq_e → kSTfP_Tzl760gYDnYr-JC}/_ssgManifest.js +0 -0
package/src/hooks/fp-reset.ts
CHANGED
|
@@ -2,11 +2,33 @@
|
|
|
2
2
|
* What happens when this CLI meets a home directory written by a different
|
|
3
3
|
* layout.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* ## This was wipe-and-re-setup. It is a migration now.
|
|
6
|
+
*
|
|
7
|
+
* The original decision, and the reasoning for it, was: a migration has to be
|
|
8
|
+
* right on every path or it half-moves a home, and a half-moved home fails in the
|
|
9
|
+
* worst available way — the daemon writes where the dashboard does not read, and
|
|
10
|
+
* an absent directory is indistinguishable from an idle one. A reset is one
|
|
11
|
+
* destructive operation that is either done or not done.
|
|
12
|
+
*
|
|
13
|
+
* That was correct while there were no customers, no cloud tokens on real
|
|
14
|
+
* machines, no fleet enrolment and no undelivered event spools. All four now
|
|
15
|
+
* exist, and the cost landed on the other side of the ledger: a wipe deleted the
|
|
16
|
+
* cloud token (so the machine dropped off the fleet, silently, still reporting
|
|
17
|
+
* healthy), `daemon.configured` (so it stopped failing closed), every
|
|
18
|
+
* `extra_paths` a person had typed, and events already read out of transcripts
|
|
19
|
+
* and queued — the last of those PERMANENTLY, because `cursors/` survived and the
|
|
20
|
+
* watermark had already moved past them.
|
|
21
|
+
*
|
|
22
|
+
* So the shape inverted. `HOME_CLASSES` in `fp-home.ts` classifies every path by
|
|
23
|
+
* what it HOLDS and the delete list is derived from that, which means the default
|
|
24
|
+
* is now "carried" and deletion is the exception a class has to earn. The
|
|
25
|
+
* half-moved-home worry is answered by keeping the property that made a reset
|
|
26
|
+
* safe: `VERSION` is stamped only after a step completes, so a step that fails
|
|
27
|
+
* leaves the home marked with the OLD layout and the next command retries it. No
|
|
28
|
+
* home is ever marked current on the strength of a partial migration.
|
|
29
|
+
*
|
|
30
|
+
* `migrations.ts` owns the ORDER and the record — which steps exist, which ran,
|
|
31
|
+
* and what was copied aside first. This file owns the MOVES.
|
|
10
32
|
*
|
|
11
33
|
* ## Where the deletion happens, and where it deliberately does not
|
|
12
34
|
*
|
|
@@ -49,10 +71,23 @@ import {
|
|
|
49
71
|
globalPolicyConfigFile,
|
|
50
72
|
hookActivityDir,
|
|
51
73
|
legacy,
|
|
74
|
+
migrationBackupDir,
|
|
52
75
|
policiesDir,
|
|
53
76
|
resettablePaths,
|
|
54
77
|
} from "./fp-home";
|
|
55
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
detectLayout,
|
|
80
|
+
projectConfig,
|
|
81
|
+
projectCredentials,
|
|
82
|
+
readConfig,
|
|
83
|
+
updateConfig,
|
|
84
|
+
writeConfig,
|
|
85
|
+
writeCredentials,
|
|
86
|
+
writeVersionFile,
|
|
87
|
+
type FpConfig,
|
|
88
|
+
type FpCredentials,
|
|
89
|
+
type LayoutState,
|
|
90
|
+
} from "./fp-config";
|
|
56
91
|
import {
|
|
57
92
|
daemonServiceStatus,
|
|
58
93
|
daemonStatusCommand,
|
|
@@ -278,30 +313,23 @@ export function migrateHookActivity(): string[] {
|
|
|
278
313
|
}
|
|
279
314
|
|
|
280
315
|
/**
|
|
281
|
-
*
|
|
316
|
+
* The eight keys the layout-2 carry used to move, kept only as a record.
|
|
317
|
+
*
|
|
318
|
+
* It was an ALLOWLIST, and that was the bug: anything outside these eight names
|
|
319
|
+
* was dropped, including a key a NEWER build had written into a layout-2 file.
|
|
320
|
+
* The carry preserves every key now and deletes only the retired ones
|
|
321
|
+
* (`RETIRED_POLICY_CONFIG_KEYS`), which is the same rule `writeConfig` follows for
|
|
322
|
+
* `config.json` — unowned keys survive, dead ones go.
|
|
282
323
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
324
|
+
* The one exclusion this list existed for is still enforced, by that other
|
|
325
|
+
* constant: layout 1's file also carried a `collector` block, and layout 2 moved
|
|
326
|
+
* collector settings to `config.toml` in snake_case — deliberately, because
|
|
286
327
|
* `fpai-collect`'s `Settings` deserializes them and camelCase keys would make
|
|
287
328
|
* every field silently fall back to its default (see the note on `Settings` in
|
|
288
329
|
* `crates/fpai-collect/src/config.rs`, which records that exact bug). Carrying
|
|
289
330
|
* `collector` forward would put a block into the new file that nothing reads,
|
|
290
|
-
*
|
|
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.
|
|
331
|
+
* looking like a preserved setting and behaving like an absent one.
|
|
294
332
|
*/
|
|
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
333
|
|
|
306
334
|
/**
|
|
307
335
|
* Carry the user's policy SELECTION across the layout-1 → layout-2 move.
|
|
@@ -336,20 +364,75 @@ const CARRIED_POLICY_CONFIG_KEYS = [
|
|
|
336
364
|
*
|
|
337
365
|
* So: READ before, WRITE after.
|
|
338
366
|
*/
|
|
367
|
+
/**
|
|
368
|
+
* Keys the policy config used to hold that nothing reads any more.
|
|
369
|
+
*
|
|
370
|
+
* `collector` is the whole list, and it is why the layout-1 carry was an
|
|
371
|
+
* ALLOWLIST rather than a copy: layout 1 kept collector settings here in
|
|
372
|
+
* camelCase, and layout 2 moved them to `config.toml`/`config.json` in
|
|
373
|
+
* snake_case, where `fpai-collect`'s `Settings` deserializes them. A camelCase
|
|
374
|
+
* `collector` block sitting in the layout-3 file reads as a preserved setting and
|
|
375
|
+
* does nothing — the worst of both, because it looks answered.
|
|
376
|
+
*
|
|
377
|
+
* A NAMED list, not "everything outside the keep-list". The file survives the
|
|
378
|
+
* reset now (`HOME_CLASSES` classes it `user-typed`), so dropping by exclusion
|
|
379
|
+
* would delete every key a NEWER build had written — which is the bug Phase 1
|
|
380
|
+
* fixed for `config.json`, arriving here by a different door.
|
|
381
|
+
*/
|
|
382
|
+
const RETIRED_POLICY_CONFIG_KEYS = ["collector"] as const;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Remove the retired keys from the policy config, in place.
|
|
386
|
+
*
|
|
387
|
+
* Runs regardless of which layout we came from: a key is retired or it is not,
|
|
388
|
+
* and a layout-2 home carrying a stray layout-1 root file should be cleaned the
|
|
389
|
+
* same way. Returns what it removed.
|
|
390
|
+
*
|
|
391
|
+
* The file is DELETED if stripping empties it — a `{}` policy config is not a
|
|
392
|
+
* selection, and the old behaviour for a file holding nothing but `collector`
|
|
393
|
+
* was no file at all.
|
|
394
|
+
*/
|
|
395
|
+
export function retirePolicyConfigKeys(): string[] {
|
|
396
|
+
const at = globalPolicyConfigFile();
|
|
397
|
+
let parsed: Record<string, unknown>;
|
|
398
|
+
try {
|
|
399
|
+
const raw = JSON.parse(readFileSync(at, "utf8")) as unknown;
|
|
400
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
401
|
+
parsed = raw as Record<string, unknown>;
|
|
402
|
+
} catch {
|
|
403
|
+
// Absent or unparseable. Not worth aborting a reset over, and there is
|
|
404
|
+
// nothing to retire — the same answer the carry gives for the same input.
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
const removed = RETIRED_POLICY_CONFIG_KEYS.filter((k) => parsed[k] !== undefined);
|
|
408
|
+
if (removed.length === 0) return [];
|
|
409
|
+
for (const key of removed) delete parsed[key];
|
|
410
|
+
try {
|
|
411
|
+
if (Object.keys(parsed).length === 0) rmSync(at, { force: true });
|
|
412
|
+
else writeFileSync(at, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
413
|
+
} catch {
|
|
414
|
+
// A file we cannot rewrite keeps its dead key. Misleading, but not fatal,
|
|
415
|
+
// and not worth failing the migration over.
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
return [...removed];
|
|
419
|
+
}
|
|
420
|
+
|
|
339
421
|
export function readCarriedPolicyConfig(): Record<string, unknown> | null {
|
|
340
|
-
|
|
341
|
-
// NEWEST SOURCE WINS. Two older layouts kept this file in two places, and a
|
|
342
|
-
// home being upgraded may hold either:
|
|
422
|
+
// LAYOUT 2's NESTED COPY ONLY — `policies/local-policies/policies-config.json`.
|
|
343
423
|
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
424
|
+
// This used to prefer that path and fall back to `legacy.policyConfig()`, the
|
|
425
|
+
// layout-1 file at the home root. That fallback is gone because the file is:
|
|
426
|
+
// layout 3 puts the live config back at exactly that path, `HOME_CLASSES`
|
|
427
|
+
// classes it `user-typed`, and it is no longer deleted — so a layout-1 home
|
|
428
|
+
// keeps it untouched, with EVERY key rather than the eight this function knows
|
|
429
|
+
// to carry. Reading it here and writing it back would be the only thing that
|
|
430
|
+
// could still narrow it.
|
|
346
431
|
//
|
|
347
|
-
// Layout
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
const layoutTwo = resolve(legacy.localPoliciesDir(), "policies-config.json");
|
|
352
|
-
const from = existsSync(layoutTwo) ? layoutTwo : legacy.policyConfig();
|
|
432
|
+
// Layout 2's copy is different: `legacy.localPoliciesDir()` is on the retired
|
|
433
|
+
// list and really is deleted, so without this it is lost. A home cannot hold
|
|
434
|
+
// both — layout 2 never wrote the root file, and going 1 → 2 removed it.
|
|
435
|
+
const from = resolve(legacy.localPoliciesDir(), "policies-config.json");
|
|
353
436
|
if (!existsSync(from)) return null;
|
|
354
437
|
|
|
355
438
|
let parsed: Record<string, unknown>;
|
|
@@ -363,17 +446,23 @@ export function readCarriedPolicyConfig(): Record<string, unknown> | null {
|
|
|
363
446
|
}
|
|
364
447
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
365
448
|
|
|
449
|
+
// EVERY key except the retired ones. This was an eight-name ALLOWLIST, and it
|
|
450
|
+
// dropped anything outside it — including a key a NEWER build had written into a
|
|
451
|
+
// layout-2 file, which is the same loss Phase 1 fixed for `config.json` arriving
|
|
452
|
+
// by a third door. Caught by a smoke test on a seeded home: a `futureKey` in the
|
|
453
|
+
// nested config was gone after the migration, silently.
|
|
454
|
+
//
|
|
455
|
+
// Retired keys still go, because they are dead rather than unknown — see
|
|
456
|
+
// `RETIRED_POLICY_CONFIG_KEYS` for why leaving `collector` here is worse than
|
|
457
|
+
// deleting it.
|
|
366
458
|
const carried: Record<string, unknown> = {};
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
carried[key] = parsed[key];
|
|
371
|
-
found = true;
|
|
372
|
-
}
|
|
459
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
460
|
+
if ((RETIRED_POLICY_CONFIG_KEYS as readonly string[]).includes(key)) continue;
|
|
461
|
+
carried[key] = value;
|
|
373
462
|
}
|
|
374
|
-
// Nothing worth carrying — a file holding only
|
|
375
|
-
//
|
|
376
|
-
if (
|
|
463
|
+
// Nothing worth carrying — a file holding only retired fields must not produce
|
|
464
|
+
// an empty config that looks like a real one.
|
|
465
|
+
if (Object.keys(carried).length === 0) return null;
|
|
377
466
|
|
|
378
467
|
// `enabledPolicies` is required by the type and by every reader. A layout-1
|
|
379
468
|
// file that somehow lacked it would otherwise produce a layout-2 file that
|
|
@@ -382,13 +471,30 @@ export function readCarriedPolicyConfig(): Record<string, unknown> | null {
|
|
|
382
471
|
return carried;
|
|
383
472
|
}
|
|
384
473
|
|
|
385
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* Second phase of {@link readCarriedPolicyConfig}; runs AFTER the deletions.
|
|
476
|
+
*
|
|
477
|
+
* MERGES onto whatever is at the destination rather than replacing it. The
|
|
478
|
+
* destination is `user-typed` now and survives the reset, so a plain write would
|
|
479
|
+
* truncate a live file down to the keys this carry happens to know about — the
|
|
480
|
+
* same class of loss the carry exists to prevent, arriving by the other door.
|
|
481
|
+
* The carried values still win: they are the ones being migrated.
|
|
482
|
+
*/
|
|
386
483
|
export function writeCarriedPolicyConfig(carried: Record<string, unknown> | null): string[] {
|
|
387
484
|
if (!carried) return [];
|
|
388
485
|
const to = globalPolicyConfigFile();
|
|
486
|
+
let existing: Record<string, unknown> = {};
|
|
487
|
+
try {
|
|
488
|
+
const parsed = JSON.parse(readFileSync(to, "utf8")) as unknown;
|
|
489
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
490
|
+
existing = parsed as Record<string, unknown>;
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
// Absent or unparseable — the carry is the whole file, as before.
|
|
494
|
+
}
|
|
389
495
|
try {
|
|
390
496
|
mkdirSync(dirname(to), { recursive: true });
|
|
391
|
-
writeFileSync(to, JSON.stringify(carried, null, 2) + "\n", "utf8");
|
|
497
|
+
writeFileSync(to, JSON.stringify({ ...existing, ...carried }, null, 2) + "\n", "utf8");
|
|
392
498
|
} catch {
|
|
393
499
|
// A destination we cannot write is not worth aborting the reset over; the
|
|
394
500
|
// user re-runs setup, which is the pre-existing behaviour.
|
|
@@ -455,25 +561,201 @@ export function readCarriedTelemetryOptOut(): boolean {
|
|
|
455
561
|
}
|
|
456
562
|
}
|
|
457
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Parse the TOML subset layout 2 actually wrote, into the object shape layout 3
|
|
566
|
+
* parses out of JSON.
|
|
567
|
+
*
|
|
568
|
+
* NOT a TOML implementation, and it does not need to be: layout 2's `config.toml`
|
|
569
|
+
* and `credentials.toml` were written by this codebase, by two functions that
|
|
570
|
+
* emitted nothing but `[table]` / `[dotted.table]` headers and `key = <value>`
|
|
571
|
+
* lines where every value went through `JSON.stringify`. So `JSON.parse` on the
|
|
572
|
+
* right-hand side is EXACT rather than approximate — strings, booleans, numbers
|
|
573
|
+
* and the one array (`extra_paths`) all round-trip by construction. Removing the
|
|
574
|
+
* `toml` dependency was half the point of layout 3; adding it back to read two
|
|
575
|
+
* files we wrote ourselves would undo that.
|
|
576
|
+
*
|
|
577
|
+
* The output uses layout 2's own snake_case key names, unchanged, because layout
|
|
578
|
+
* 3's JSON uses exactly the same ones (`hooks_verbosity`, `machine_id`,
|
|
579
|
+
* `interval_days`, `sources.<h>.extra_paths`). That is what lets the carry run
|
|
580
|
+
* `projectConfig` / `projectCredentials` — the SAME projections the JSON readers
|
|
581
|
+
* use — instead of a second reader that would have to be kept in step. The only
|
|
582
|
+
* thing that differs between the two layouts is how bytes become an object.
|
|
583
|
+
*
|
|
584
|
+
* A dotted header nests: `[collector.sources.claude]` lands at
|
|
585
|
+
* `collector.sources.claude`, which is where `readSources` looks for it.
|
|
586
|
+
* Anything unparseable is skipped rather than throwing, because a single
|
|
587
|
+
* malformed line must not cost the user the whole file.
|
|
588
|
+
*/
|
|
589
|
+
export function parseLegacyToml(text: string): Record<string, unknown> {
|
|
590
|
+
const root: Record<string, unknown> = {};
|
|
591
|
+
let table = root;
|
|
592
|
+
for (const raw of text.split("\n")) {
|
|
593
|
+
const line = raw.trim();
|
|
594
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
595
|
+
const header = /^\[([^\]]+)\]$/.exec(line);
|
|
596
|
+
if (header) {
|
|
597
|
+
table = root;
|
|
598
|
+
for (const part of header[1].split(".")) {
|
|
599
|
+
const key = part.trim();
|
|
600
|
+
if (!key) break;
|
|
601
|
+
const existing = table[key];
|
|
602
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
603
|
+
table = existing as Record<string, unknown>;
|
|
604
|
+
} else {
|
|
605
|
+
const created: Record<string, unknown> = {};
|
|
606
|
+
table[key] = created;
|
|
607
|
+
table = created;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const eq = line.indexOf("=");
|
|
613
|
+
if (eq === -1) continue;
|
|
614
|
+
const key = line.slice(0, eq).trim();
|
|
615
|
+
if (!key) continue;
|
|
616
|
+
try {
|
|
617
|
+
table[key] = JSON.parse(line.slice(eq + 1).trim()) as unknown;
|
|
618
|
+
} catch {
|
|
619
|
+
// A value this codebase did not write, or a hand-edit that broke it. Skip
|
|
620
|
+
// the line; the projection then falls back to that field's default, which
|
|
621
|
+
// is the same answer an absent key gets.
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return root;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Carry the whole layout-2 `config.toml` across, not just the telemetry opt-out.
|
|
629
|
+
*
|
|
630
|
+
* `readCarriedTelemetryOptOut()` above rescued one field, and the reason it was
|
|
631
|
+
* only one was that the rest was described as "re-derived by setup or a thing the
|
|
632
|
+
* wizard re-asks". That is true of a machine whose owner is about to re-run
|
|
633
|
+
* setup, and false of every other machine — `config.toml` also holds:
|
|
634
|
+
*
|
|
635
|
+
* mode a machine reverting to `oss` stops reporting entirely
|
|
636
|
+
* daemon.configured the flag that makes the machine FAIL CLOSED
|
|
637
|
+
* collector.* sessions/hooks/verbosity/redact/environment/machine_id
|
|
638
|
+
* collector.sources.* every extra_paths a person typed
|
|
639
|
+
* audit.auto/interval the scheduled scan they switched on
|
|
640
|
+
*
|
|
641
|
+
* Losing `daemon.configured` silently downgrades a machine from fail-closed
|
|
642
|
+
* enforcement to the in-process path, and losing `mode` disconnects it from the
|
|
643
|
+
* fleet — neither with any message, and neither re-derivable without a human.
|
|
644
|
+
* Those are the same failures `HOME_CLASSES` stopped for `config.json`; this is
|
|
645
|
+
* the layout-2 leg of the same problem, in the other format.
|
|
646
|
+
*
|
|
647
|
+
* Read BEFORE the deletions (the source is on the retired list), applied after.
|
|
648
|
+
*/
|
|
649
|
+
export function readCarriedLegacyConfig(): FpConfig | null {
|
|
650
|
+
const from = legacy.configToml();
|
|
651
|
+
if (!existsSync(from)) return null;
|
|
652
|
+
try {
|
|
653
|
+
return projectConfig(parseLegacyToml(readFileSync(from, "utf8")));
|
|
654
|
+
} catch {
|
|
655
|
+
// Unreadable is not worth aborting a reset over, and the telemetry carry
|
|
656
|
+
// below still gets its own chance at the same bytes.
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Carry the layout-2 `credentials.toml` across.
|
|
663
|
+
*
|
|
664
|
+
* This is the one that takes a machine off the fleet. `credentials.toml` is on
|
|
665
|
+
* the retired list and NOTHING carried it, so a layout-2 → 3 upgrade deleted the
|
|
666
|
+
* cloud token and the ingest key outright: the machine stops reconciling
|
|
667
|
+
* cloud-managed policy, stops delivering anything it spools, and says nothing —
|
|
668
|
+
* it keeps enforcing whatever it last had and keeps reporting healthy. On a fleet
|
|
669
|
+
* that is every box going quiet at once, with no operator action that caused it.
|
|
670
|
+
*
|
|
671
|
+
* `HOME_CLASSES` classes `credentials.json` `user-typed` so this cannot happen
|
|
672
|
+
* again from layout 3 onwards, but 2 → 3 is the upgrade that actually exists to
|
|
673
|
+
* be run, and it needed this.
|
|
674
|
+
*
|
|
675
|
+
* Written through `writeCredentials`, so the file lands 0600 with the home
|
|
676
|
+
* tightened to 0700 — a token must not arrive here by a path that skips that.
|
|
677
|
+
*/
|
|
678
|
+
export function readCarriedLegacyCredentials(): FpCredentials | null {
|
|
679
|
+
// NEWEST SOURCE WINS, and both older layouts have to be handled here because
|
|
680
|
+
// BOTH of their credential files are on the retired list with nothing else
|
|
681
|
+
// carrying them:
|
|
682
|
+
//
|
|
683
|
+
// layout 2 — `credentials.toml`, one file, TOML
|
|
684
|
+
// layout 1 — `cloud.json` + `ingest.json`, two files, JSON, camelCase
|
|
685
|
+
//
|
|
686
|
+
// Layout 1 is the one that matters most in practice: the published `latest`
|
|
687
|
+
// npm tag is still a pre-daemon 0.0.x release, so "install the current stable,
|
|
688
|
+
// then upgrade" is a LAYOUT-1 → 3 migration, which makes this the upgrade real
|
|
689
|
+
// users actually perform. Losing the token there takes the machine off the
|
|
690
|
+
// fleet exactly as the layout-2 case does — silently, still enforcing whatever
|
|
691
|
+
// it last had, still reporting healthy.
|
|
692
|
+
const toml = legacy.credentialsToml();
|
|
693
|
+
if (existsSync(toml)) {
|
|
694
|
+
try {
|
|
695
|
+
const creds = projectCredentials(parseLegacyToml(readFileSync(toml, "utf8")));
|
|
696
|
+
// An empty object means the file held nothing that passed validation — a
|
|
697
|
+
// cloud table with no token, say. Writing that would create a credentials
|
|
698
|
+
// file that looks present and authenticates nothing.
|
|
699
|
+
if (Object.keys(creds).length > 0) return creds;
|
|
700
|
+
} catch {
|
|
701
|
+
// Fall through to layout 1's files rather than returning: an unreadable
|
|
702
|
+
// layout-2 file is not evidence that a layout-1 one is absent.
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Layout 1. `machineId` is CAMELCASE in `cloud.json` — layout 2 moved to
|
|
707
|
+
// snake_case — so the raw object is rebuilt in the shape `projectCredentials`
|
|
708
|
+
// reads rather than passed through. Reusing that projection is the point: "a
|
|
709
|
+
// cloud block without a token is not a cloud block" is a security property,
|
|
710
|
+
// and a second validator here is how a half-written credential comes to be
|
|
711
|
+
// treated as live.
|
|
712
|
+
const readJson = (path: string): Record<string, unknown> | null => {
|
|
713
|
+
try {
|
|
714
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
715
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
716
|
+
? (parsed as Record<string, unknown>)
|
|
717
|
+
: null;
|
|
718
|
+
} catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
const cloud = readJson(legacy.cloudCredentials());
|
|
723
|
+
const ingest = readJson(legacy.ingestCredentials());
|
|
724
|
+
if (!cloud && !ingest) return null;
|
|
725
|
+
const raw: Record<string, unknown> = {};
|
|
726
|
+
if (cloud) {
|
|
727
|
+
raw.cloud = {
|
|
728
|
+
url: cloud.url,
|
|
729
|
+
machine_id: cloud.machineId,
|
|
730
|
+
token: cloud.token,
|
|
731
|
+
// Layout 1 had no machine label; absent is correct rather than empty.
|
|
732
|
+
...(typeof cloud.machineLabel === "string" ? { machine_label: cloud.machineLabel } : {}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (ingest) raw.ingest = { url: ingest.url, key: ingest.key };
|
|
736
|
+
const creds = projectCredentials(raw);
|
|
737
|
+
return Object.keys(creds).length > 0 ? creds : null;
|
|
738
|
+
}
|
|
739
|
+
|
|
458
740
|
export function resetHome(from: number): ResetOutcome {
|
|
459
741
|
// BEFORE the deletions, so a file that is mid-move is never one the reset
|
|
460
742
|
// then walks over.
|
|
461
743
|
const migrated = migrateConventionPolicies();
|
|
462
744
|
const activity = migrateHookActivity();
|
|
463
745
|
const pendingPolicyConfig = readCarriedPolicyConfig();
|
|
464
|
-
// Read before the deletions for the same reason as the policy config:
|
|
465
|
-
//
|
|
746
|
+
// Read before the deletions for the same reason as the policy config: their
|
|
747
|
+
// sources (`config.toml`, `credentials.toml`) are both on the list below.
|
|
748
|
+
const pendingConfig = readCarriedLegacyConfig();
|
|
749
|
+
const pendingCredentials = readCarriedLegacyCredentials();
|
|
466
750
|
const telemetryOptOut = readCarriedTelemetryOptOut();
|
|
467
751
|
const removed: string[] = [];
|
|
468
752
|
for (const path of resettablePaths()) {
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// this only ever changes the forced same-layout case.
|
|
476
|
-
if (from === LAYOUT_VERSION && path === globalPolicyConfigFile()) continue;
|
|
753
|
+
// The guard that stood here skipped `globalPolicyConfigFile()` when the
|
|
754
|
+
// reset was not a layout migration, because clearing a current, valid
|
|
755
|
+
// policy selection is only right when setup is about to re-ask. That path is
|
|
756
|
+
// `user-typed` in `HOME_CLASSES` now and therefore never on this list at
|
|
757
|
+
// all, for either kind of reset — so the special case has nothing left to
|
|
758
|
+
// guard. See the `legacy.policyConfig()` note in `retiredLayoutPaths()`.
|
|
477
759
|
if (!existsSync(path)) continue;
|
|
478
760
|
try {
|
|
479
761
|
rmSync(path, { recursive: true, force: true });
|
|
@@ -485,8 +767,21 @@ export function resetHome(from: number): ResetOutcome {
|
|
|
485
767
|
}
|
|
486
768
|
}
|
|
487
769
|
const policyConfig = writeCarriedPolicyConfig(pendingPolicyConfig);
|
|
488
|
-
//
|
|
770
|
+
// AFTER the carry, so a layout-2 value that happens to be named `collector`
|
|
771
|
+
// is retired too rather than surviving because it arrived a moment later.
|
|
772
|
+
retirePolicyConfigKeys();
|
|
773
|
+
// After the deletions, onto the fresh files, so the upgrade cannot revoke a
|
|
489
774
|
// choice the user made.
|
|
775
|
+
//
|
|
776
|
+
// The credentials go first and unconditionally: `daemon.configured` below makes
|
|
777
|
+
// the machine fail closed, and a machine that fails closed against a fleet it
|
|
778
|
+
// can no longer authenticate to is worse than either half alone.
|
|
779
|
+
if (pendingCredentials) writeCredentials(pendingCredentials);
|
|
780
|
+
if (pendingConfig) writeConfig(pendingConfig);
|
|
781
|
+
// AFTER the config carry, or a carried `telemetry.enabled: true` — the default,
|
|
782
|
+
// which is not a choice anyone made — would overwrite the opt-out this reads
|
|
783
|
+
// straight out of the same file. Both paths end at the same key, and only one
|
|
784
|
+
// of them represents something a person typed.
|
|
490
785
|
if (telemetryOptOut) updateConfig({ telemetry: { enabled: false } });
|
|
491
786
|
writeVersionFile();
|
|
492
787
|
return { removed, migrated, activity, policyConfig, from };
|
|
@@ -499,15 +794,32 @@ export interface LayoutCheck {
|
|
|
499
794
|
/** True when the caller should stop rather than continue. */
|
|
500
795
|
fatal: boolean;
|
|
501
796
|
/**
|
|
502
|
-
* True when this call actually
|
|
797
|
+
* True when this call actually migrated the home.
|
|
798
|
+
*
|
|
799
|
+
* **Reporting only. The caller must NOT force setup on it.**
|
|
503
800
|
*
|
|
504
|
-
*
|
|
505
|
-
* policy config
|
|
506
|
-
* CLIs' settings files — which the reset deliberately leaves
|
|
507
|
-
* machine
|
|
508
|
-
* `markLauncherSeen()` back-
|
|
509
|
-
* The user
|
|
510
|
-
* at all, and nothing ever
|
|
801
|
+
* It used to mean "force the wizard", and the reason was a real gap: the reset
|
|
802
|
+
* removed the global policy config while `isConfigured()` is a union that also
|
|
803
|
+
* counts the agent CLIs' settings files — which the reset deliberately leaves
|
|
804
|
+
* alone — so the machine read as configured, the wizard was skipped, and
|
|
805
|
+
* `markLauncherSeen()` back-filled the marker so every later run skipped it
|
|
806
|
+
* too. The user was left with hooks firing on every tool call against no
|
|
807
|
+
* policies at all, and nothing ever said so again.
|
|
808
|
+
*
|
|
809
|
+
* That gap is closed at the source. `policies-config.json`, `config.json` and
|
|
810
|
+
* `credentials.json` are `user-typed` in `HOME_CLASSES` and are no longer
|
|
811
|
+
* removed, so after a migration `isConfigured()` is true because the machine
|
|
812
|
+
* genuinely IS configured — its policy selection, its `daemon.configured`
|
|
813
|
+
* flag and its cloud enrolment all survived. Everything the migration still
|
|
814
|
+
* drops (audit cache, cloud deployments, daemon scratch) is re-derived or
|
|
815
|
+
* re-fetched with nobody present.
|
|
816
|
+
*
|
|
817
|
+
* Forcing setup from here would now mean opening an interactive wizard for a
|
|
818
|
+
* machine with nothing to answer — and on the machines that matter most (a
|
|
819
|
+
* fleet box, a CI runner, a headless gateway) there is nobody to answer it. A
|
|
820
|
+
* home that genuinely never finished setup still reaches the wizard by the
|
|
821
|
+
* ordinary route: `isConfigured()` is false for it, so `shouldOfferFirstRun`
|
|
822
|
+
* fires on its own. That is why forcing was redundant even before it was wrong.
|
|
511
823
|
*/
|
|
512
824
|
didReset: boolean;
|
|
513
825
|
}
|
|
@@ -520,6 +832,61 @@ export interface LayoutCheck {
|
|
|
520
832
|
* written by a NEWER CLI holds data this build cannot read but a simple
|
|
521
833
|
* upgrade could, and deleting it would destroy something recoverable.
|
|
522
834
|
*/
|
|
835
|
+
/**
|
|
836
|
+
* Deliver what is already spooled, immediately AFTER the migration.
|
|
837
|
+
*
|
|
838
|
+
* The order is the whole subtlety here, and the obvious one is wrong. Flushing
|
|
839
|
+
* FIRST reads intuitive — get the events out before touching the disk — and it
|
|
840
|
+
* cannot work: everything a flush needs to run is in a file the stale layout
|
|
841
|
+
* hasn't got. `readConfig()` reads `config.json` and `readIngestCredential()`
|
|
842
|
+
* reads `credentials.json`, both of which are LAYOUT-3 files, and a stale home by
|
|
843
|
+
* definition has neither. Called before the migration, the flush would find no
|
|
844
|
+
* ingest credential on every machine it ever ran on, refuse, and report nothing
|
|
845
|
+
* pending — a step that looks like it protects data and is structurally incapable
|
|
846
|
+
* of doing anything at all.
|
|
847
|
+
*
|
|
848
|
+
* Afterwards, both files exist: `readCarriedLegacyCredentials()` has just put the
|
|
849
|
+
* token back and `readCarriedLegacyConfig()` the mode. So this runs where it can
|
|
850
|
+
* actually succeed.
|
|
851
|
+
*
|
|
852
|
+
* What makes that safe rather than a gamble is that this is NOT the thing
|
|
853
|
+
* protecting the events. `HOME_CLASSES` classes the spool `undelivered`, so it
|
|
854
|
+
* survives the migration whatever happens here — losing it would be permanent,
|
|
855
|
+
* because `cursors/` survives too and the watermark has already advanced past
|
|
856
|
+
* every batch in it. This is the difference between "delivered" and "delivered on
|
|
857
|
+
* the next collector pass", which matters only because the collector is unhurried
|
|
858
|
+
* on purpose (a batch is swept once it is older than two minutes, at most 64 per
|
|
859
|
+
* pass, on a 60-second cadence) and somebody standing at a dashboard cannot tell
|
|
860
|
+
* "not yet" from "not working".
|
|
861
|
+
*
|
|
862
|
+
* BEST-EFFORT BY CONSTRUCTION. `runFlushCommand` refuses, with its own message
|
|
863
|
+
* and a non-zero code, on every machine where a flush cannot work: collection
|
|
864
|
+
* off, no ingest credential, an unsupported platform, no daemon listening. All of
|
|
865
|
+
* those are ordinary here rather than errors, so the result is read for its COUNT
|
|
866
|
+
* and its exit code discarded. Bounded at 30s: this sits in front of a command
|
|
867
|
+
* the user typed, and a spool that will not drain is not a reason to hold their
|
|
868
|
+
* terminal.
|
|
869
|
+
*/
|
|
870
|
+
async function drainSpoolAfterMigrating(): Promise<number> {
|
|
871
|
+
try {
|
|
872
|
+
// Cheap gates first, so an OSS machine and one that never connected cost
|
|
873
|
+
// nothing at all — no dynamic import, no daemon probe. Read AFTER the
|
|
874
|
+
// migration, so these see the carried values rather than the defaults a
|
|
875
|
+
// missing layout-3 file would have produced.
|
|
876
|
+
const cfg = readConfig();
|
|
877
|
+
if (cfg.mode !== "cloud") return 0;
|
|
878
|
+
if (!cfg.collector.hooks && !cfg.collector.sessions) return 0;
|
|
879
|
+
const { runFlushCommand } = await import("./flush-cli");
|
|
880
|
+
const result = await runFlushCommand({ wait: true, timeoutSecs: 30 });
|
|
881
|
+
return result.pending;
|
|
882
|
+
} catch {
|
|
883
|
+
// A flush that throws is not a migration failure. Reported as "nothing
|
|
884
|
+
// pending" because the number only adds a line to a message, and inventing a
|
|
885
|
+
// count from a failed probe would be worse than saying nothing.
|
|
886
|
+
return 0;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
523
890
|
export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
524
891
|
const state = detectLayout();
|
|
525
892
|
|
|
@@ -539,7 +906,18 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
|
539
906
|
}
|
|
540
907
|
|
|
541
908
|
if (state.kind === "stale") {
|
|
542
|
-
|
|
909
|
+
// Through the registry, not `resetHome` directly. The two look identical for
|
|
910
|
+
// today's layouts — the chain from 1 or 2 is one step, and that step IS
|
|
911
|
+
// `resetHome` — and they stop being identical the moment a layout 4 exists,
|
|
912
|
+
// at which point this call site needs no change. It also means every
|
|
913
|
+
// migration this machine has ever run is recorded, and the irreplaceable
|
|
914
|
+
// files are copied aside first. See `migrations.ts`.
|
|
915
|
+
const { runMigrations } = await import("./migrations");
|
|
916
|
+
const run = runMigrations(state.found);
|
|
917
|
+
const { removed, migrated, activity } = run.outcome;
|
|
918
|
+
// After, not before — see the function's own note for why the intuitive
|
|
919
|
+
// order cannot work.
|
|
920
|
+
const pending = await drainSpoolAfterMigrating();
|
|
543
921
|
return {
|
|
544
922
|
state,
|
|
545
923
|
fatal: false,
|
|
@@ -547,11 +925,15 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
|
547
925
|
lines: [
|
|
548
926
|
`failproofai reorganised ${failproofaiHome()} in this version.`,
|
|
549
927
|
// "activity history" was in this sentence while the reset was deleting
|
|
550
|
-
// it
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
`
|
|
554
|
-
|
|
928
|
+
// it, and "policy config" stayed in it after that file stopped being
|
|
929
|
+
// removed. Both are the same failure: a message describing a delete list
|
|
930
|
+
// it is not derived from. It names the CLASSES now, which is what
|
|
931
|
+
// `HOME_CLASSES` actually decides — so it cannot drift again without the
|
|
932
|
+
// rule itself changing.
|
|
933
|
+
`Removed ${removed.length} item(s) that this version rebuilds — the audit`,
|
|
934
|
+
`cache, cloud deployments (re-fetched on the next poll) and daemon scratch.`,
|
|
935
|
+
`Your settings, cloud enrolment, policy selection, decision history,`,
|
|
936
|
+
`undelivered events and daemon binary were all kept.`,
|
|
555
937
|
// Named individually rather than counted. These are files a person
|
|
556
938
|
// wrote; "moved 3 items" is not something you can check at a glance,
|
|
557
939
|
// and the whole point of saying it is that they can.
|
|
@@ -573,8 +955,35 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
|
573
955
|
`Carried ${activity.length} page(s) of decision history into ${hookActivityDir()}.`,
|
|
574
956
|
]
|
|
575
957
|
: []),
|
|
576
|
-
|
|
577
|
-
|
|
958
|
+
// Said only when a backlog actually survived the flush. Silence here
|
|
959
|
+
// would be the wrong kind: the events are safe, but "safe" and
|
|
960
|
+
// "delivered" are different states and only one of them shows up on a
|
|
961
|
+
// dashboard. Naming the count is what stops a user reading an incomplete
|
|
962
|
+
// dashboard as data loss.
|
|
963
|
+
...(pending > 0
|
|
964
|
+
? [
|
|
965
|
+
``,
|
|
966
|
+
`${pending} batch(es) were still undelivered and were carried across.`,
|
|
967
|
+
`They ship on the next collector pass — \`failproofai flush --wait\` now if you`,
|
|
968
|
+
`are waiting on a dashboard.`,
|
|
969
|
+
]
|
|
970
|
+
: []),
|
|
971
|
+
// A step that threw leaves the home marked with the OLD layout, so the
|
|
972
|
+
// next command tries again — which is right, and is also why this must
|
|
973
|
+
// say so rather than let a partial migration pass for a finished one.
|
|
974
|
+
...(run.failed
|
|
975
|
+
? [
|
|
976
|
+
``,
|
|
977
|
+
`Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
|
|
978
|
+
`The home is still marked layout ${state.found} and will be retried. Copies of`,
|
|
979
|
+
`your settings and enrolment were saved first, in ${migrationBackupDir(state.found)}.`,
|
|
980
|
+
]
|
|
981
|
+
: []),
|
|
982
|
+
// No "run `failproofai config` to set up again". There is nothing to set
|
|
983
|
+
// up: the settings, the enrolment and the policy selection all survived,
|
|
984
|
+
// so the machine enforces exactly as it did before this command ran. A
|
|
985
|
+
// home that genuinely never finished setup reaches the wizard through
|
|
986
|
+
// `shouldOfferFirstRun`, which reads `isConfigured()` — see `didReset`.
|
|
578
987
|
],
|
|
579
988
|
};
|
|
580
989
|
}
|