failproofai 1.0.0 → 1.0.1-beta.0
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/app-path-routes-manifest.json +0 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +0 -6
- 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 +4 -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 +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- 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 +4 -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/_not-found/__PAGE__.segment.rsc +14 -14
- 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 +3 -3
- 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 +2 -2
- 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 +4 -3
- 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 +3 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -3
- 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 +5 -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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- 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 +4 -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 +6 -5
- 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 +5 -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 +6 -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 +5 -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 +6 -4
- 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/app-paths-manifest.json +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0jx6cp6._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0f-x2b0._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1f290jp._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0hwvl7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1g2ji1t._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1ctjdzl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
- 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]__0v5_91z._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
- 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/00q6cfzuzpc85.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
- package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
- package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
- package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
- package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
- package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
- package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
- package/.next/standalone/app/api/auth/status/route.ts +7 -17
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
- package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/audit-styles.css +150 -26
- package/.next/standalone/app/globals.css +39 -1
- package/.next/standalone/app/settings/page.tsx +38 -21
- package/.next/standalone/app/settings/settings-client.tsx +583 -392
- package/.next/standalone/app/settings/settings.css +354 -0
- package/.next/standalone/components/navbar.tsx +33 -4
- package/.next/standalone/lib/auth/api-server-client.ts +51 -29
- package/.next/standalone/lib/auth/auth-store.ts +44 -59
- package/.next/standalone/package.json +12 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +6 -0
- package/bin/failproofaid-shim.mjs +0 -0
- package/dist/cli.mjs +1889 -426
- package/dist/worker.mjs +27 -5
- package/lib/auth/api-server-client.ts +51 -29
- package/lib/auth/auth-store.ts +44 -59
- package/package.json +12 -10
- package/scripts/docs-audit.ts +493 -0
- package/scripts/translate-docs/cli.ts +16 -2
- package/scripts/translate-docs/mdx-translator.ts +6 -1
- package/scripts/translate-docs/readme-translator.ts +4 -2
- package/src/audit/cli-login.ts +330 -0
- package/src/audit/cli.ts +124 -6
- package/src/audit/dashboard-cache.ts +44 -8
- package/src/audit/harm-report.ts +260 -0
- package/src/audit/machine-store.ts +120 -0
- package/src/audit/redact-example.ts +312 -0
- package/src/audit/report-harm.ts +167 -0
- package/src/audit/schedule-cli.ts +352 -0
- package/src/hooks/builtin-policies.ts +26 -0
- package/src/hooks/daemon-service.ts +76 -0
- package/src/hooks/fp-config.ts +96 -5
- package/src/hooks/fp-home.ts +118 -8
- package/src/hooks/fp-reset.ts +79 -9
- package/src/hooks/migrations.ts +241 -4
- package/src/hooks/tui.ts +61 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
- package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
- package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
- package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
- package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
- package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
- package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
- package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
- package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_ssgManifest.js +0 -0
package/src/hooks/fp-home.ts
CHANGED
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
* policies/ every policy: the user's *.mjs sit directly here
|
|
57
57
|
* cloud-policies/ the fleet's — flat: active.json, desired-state.json, artifacts/
|
|
58
58
|
* cursors/<source>/ per-source collector watermarks
|
|
59
|
-
* audit/
|
|
59
|
+
* audit/ MIXED — see the classification note below
|
|
60
|
+
* dashboard.json last result (derived)
|
|
61
|
+
* cache/ per-transcript cache (derived)
|
|
62
|
+
* schedule.json daemon's scan timer (derived)
|
|
63
|
+
* session.json 0600 the signed-in user (user-typed)
|
|
64
|
+
* machine.json this machine's report identity (identity)
|
|
60
65
|
* hook-activity/ decision log the dashboard reads
|
|
61
66
|
* custom-agents/ SDK spool (events/ + failed/)
|
|
62
67
|
* run/ sockets + flock — MUST stay shallow, see below
|
|
@@ -88,9 +93,17 @@ import { resolve } from "node:path";
|
|
|
88
93
|
* yields "no data" instead of an error.
|
|
89
94
|
*
|
|
90
95
|
* 1 — the original flat/`cache`-based layout, through 1.0.0-beta.5.
|
|
91
|
-
* 2 —
|
|
96
|
+
* 2 — `config.toml` / `credentials.toml`, policies nested two levels down.
|
|
97
|
+
* 3 — JSON config + credentials, policies flattened back up.
|
|
98
|
+
* 4 — everything the audit owns moved under `audit/`: the signed-in session
|
|
99
|
+
* (from `auth.json`), the daemon's scan timer (from
|
|
100
|
+
* `state/audit-schedule.json`), and the re-audit reminder (from
|
|
101
|
+
* `next-audit.json`, parked at `audit/reminder.json` and retired in the
|
|
102
|
+
* same release — see `legacy.auditReminder`). The point is that one
|
|
103
|
+
* directory now answers "what does the audit know about this machine", the
|
|
104
|
+
* way `policies/` answers it for enforcement.
|
|
92
105
|
*/
|
|
93
|
-
export const LAYOUT_VERSION =
|
|
106
|
+
export const LAYOUT_VERSION = 4;
|
|
94
107
|
|
|
95
108
|
/**
|
|
96
109
|
* `~/.failproofai`, or `FAILPROOFAI_HOME`.
|
|
@@ -212,10 +225,54 @@ export const customAgentsFailedDir = (home?: string) => resolve(customAgentsDir(
|
|
|
212
225
|
|
|
213
226
|
// ── Audit ────────────────────────────────────────────────────────────────────
|
|
214
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Everything the audit owns, and a MIXED directory as of layout 4.
|
|
230
|
+
*
|
|
231
|
+
* `auditDir` is deliberately NOT classified in `HOME_CLASSES`, for exactly the
|
|
232
|
+
* reason `stateDir` is not: it now holds a credential and a machine identity
|
|
233
|
+
* alongside two caches, so one class cannot be right for all of it. Before
|
|
234
|
+
* layout 4 the whole directory was `derived` — correct then, and the trap the
|
|
235
|
+
* moment `session.json` moved in, because `resettablePaths()` is a filter over
|
|
236
|
+
* that table and would have deleted the user's tokens on every reset and every
|
|
237
|
+
* future migration. Classify the CHILDREN; never the parent.
|
|
238
|
+
*/
|
|
215
239
|
export const auditDir = (home?: string) => atHome(home, "audit");
|
|
216
240
|
export const auditDashboardFile = (home?: string) => resolve(auditDir(home), "dashboard.json");
|
|
217
241
|
export const auditCacheDir = (home?: string) => resolve(auditDir(home), "cache");
|
|
218
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The signed-in user's tokens. `0600`, written only by the dashboard's auth
|
|
245
|
+
* routes and the audit child (`lib/auth/auth-store.ts`).
|
|
246
|
+
*
|
|
247
|
+
* Layout 3 kept this at the home root as `auth.json`, where it was invisible to
|
|
248
|
+
* `HOME_CLASSES` altogether — neither classified nor deleted, safe by accident
|
|
249
|
+
* rather than by decision. It is `user-typed`: nothing regenerates a session,
|
|
250
|
+
* and dropping it silently signs the machine out.
|
|
251
|
+
*
|
|
252
|
+
* TS-only, so it is absent from `paths.rs` by design: the daemon never opens
|
|
253
|
+
* it. The audit child does the reporting precisely so the daemon holds no human
|
|
254
|
+
* credential — see `audit_lane.rs`.
|
|
255
|
+
*/
|
|
256
|
+
export const auditSessionFile = (home?: string) => resolve(auditDir(home), "session.json");
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* This machine's report identity: the id the api-server keys reports on, and
|
|
260
|
+
* the watermark saying how far the last digest reached.
|
|
261
|
+
*
|
|
262
|
+
* SEPARATE from `auditSessionFile` on purpose, and the separation is the whole
|
|
263
|
+
* design. Both fields have to outlive a sign-out: regenerate the id and the
|
|
264
|
+
* server sees a brand-new machine and burns a slot off the account's cap on
|
|
265
|
+
* every logout; reset the watermark and the next digest re-reports months of
|
|
266
|
+
* history as though it just happened. So this is `identity` — never deleted,
|
|
267
|
+
* like `cursors/` and `telemetryIdFile` — while the tokens beside it come and
|
|
268
|
+
* go with the session.
|
|
269
|
+
*
|
|
270
|
+
* Minted fresh rather than reusing `telemetryIdFile`, so opting into emailed
|
|
271
|
+
* reports never links the anonymous telemetry person to a verified address.
|
|
272
|
+
*/
|
|
273
|
+
export const auditMachineFile = (home?: string) => resolve(auditDir(home), "machine.json");
|
|
274
|
+
|
|
275
|
+
|
|
219
276
|
// ── Hook activity ────────────────────────────────────────────────────────────
|
|
220
277
|
|
|
221
278
|
/** The decision log: page-sized JSONL the dashboard's activity tab reads. */
|
|
@@ -270,10 +327,17 @@ export const sessionPauseDir = () => resolve(stateDir(), "sessions");
|
|
|
270
327
|
* mirrors this path in `paths.rs`) — it owns the schedule, and a second writer
|
|
271
328
|
* racing it could hand a machine two full scans back to back. Everything on this
|
|
272
329
|
* side reads it: the interval itself lives in `config.json`'s `audit` object,
|
|
273
|
-
* which a human edits, while this file is derived state a human never opens
|
|
274
|
-
*
|
|
330
|
+
* which a human edits, while this file is derived state a human never opens.
|
|
331
|
+
*
|
|
332
|
+
* Layout 4 moved it out of `state/` and in beside the audit results. It stays
|
|
333
|
+
* `derived` — losing it costs one rescheduled scan, nothing more — but it now
|
|
334
|
+
* sits with the rest of what the audit owns rather than in the daemon's scratch
|
|
335
|
+
* drawer, which is what makes `audit/` answerable as one directory.
|
|
336
|
+
*
|
|
337
|
+
* Declared here, below `stateDir`, only because the section order of this file
|
|
338
|
+
* is historical; the path itself is under `auditDir`.
|
|
275
339
|
*/
|
|
276
|
-
export const auditScheduleFile = (home?: string) => resolve(
|
|
340
|
+
export const auditScheduleFile = (home?: string) => resolve(auditDir(home), "schedule.json");
|
|
277
341
|
/**
|
|
278
342
|
* The anonymous instance id this machine reports telemetry under.
|
|
279
343
|
*
|
|
@@ -427,6 +491,13 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
|
|
|
427
491
|
// source destroyed, while `isConfigured()` still read true so the wizard never
|
|
428
492
|
// re-asked and hooks kept firing against an empty policy set.
|
|
429
493
|
{ path: policiesDir, class: "user-typed" },
|
|
494
|
+
// The signed-in session. `auth.json` at the home root through layout 3, where
|
|
495
|
+
// it was in NEITHER this table nor the delete list — undeleted by oversight
|
|
496
|
+
// rather than by decision, which is the state this table exists to make
|
|
497
|
+
// impossible. Nothing regenerates a session; losing it signs the machine out
|
|
498
|
+
// with no notice, and the machine only finds out the next time it tries to
|
|
499
|
+
// report.
|
|
500
|
+
{ path: auditSessionFile, class: "user-typed" },
|
|
430
501
|
|
|
431
502
|
// ── Never deleted: recorded and not yet shipped ──
|
|
432
503
|
// Batches read out of transcripts and queued for upload. The reason losing
|
|
@@ -471,10 +542,23 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
|
|
|
471
542
|
// deleting the backup is deleting the undo for the step that just ran.
|
|
472
543
|
{ path: migrationsDir, class: "identity" },
|
|
473
544
|
|
|
545
|
+
// This machine's report identity + digest watermark. `identity` for the same
|
|
546
|
+
// reason `cursorsDir` is: a new id is a new machine to the api-server, which
|
|
547
|
+
// burns a slot off the account's machine cap, and a reset watermark re-reports
|
|
548
|
+
// history the user was already told about. Kept OUT of `auditSessionFile`
|
|
549
|
+
// precisely so both survive a sign-out.
|
|
550
|
+
{ path: auditMachineFile, class: "identity" },
|
|
551
|
+
|
|
474
552
|
// ── May be dropped: rebuilt on demand ──
|
|
475
|
-
|
|
476
|
-
|
|
553
|
+
// NOTE: `auditDir` itself is deliberately absent. Layout 4 made it MIXED — it
|
|
554
|
+
// holds the session and the machine identity above alongside these three — so
|
|
555
|
+
// it is classified per-file, exactly like `stateDir`. Listing the parent here
|
|
556
|
+
// (which layout 3 did, correctly for what it then held) would put the token on
|
|
557
|
+
// the delete list.
|
|
558
|
+
{ path: auditDashboardFile, class: "derived" },
|
|
559
|
+
{ path: auditCacheDir, class: "derived" },
|
|
477
560
|
{ path: auditScheduleFile, class: "derived" },
|
|
561
|
+
{ path: collectorHealthFile, class: "derived" },
|
|
478
562
|
{ path: codexSessionPathsFile, class: "derived" },
|
|
479
563
|
{ path: shimsDir, class: "derived" },
|
|
480
564
|
{ path: sessionPauseDir, class: "derived" },
|
|
@@ -555,6 +639,28 @@ export const legacy = {
|
|
|
555
639
|
launcherMarker: () => at(".launcher-configured"),
|
|
556
640
|
lastVersion: () => at("last-version"),
|
|
557
641
|
auditDashboard: () => at("audit-dashboard.json"),
|
|
642
|
+
/**
|
|
643
|
+
* Layout 3's audit-owned files, before layout 4 gathered them under `audit/`.
|
|
644
|
+
*
|
|
645
|
+
* The first two were never classified in `HOME_CLASSES`, so unlike every other
|
|
646
|
+
* entry in this map they were not on any delete list — the layout-4 step MOVES
|
|
647
|
+
* them and there is no older copy to prune. They are here so that step can
|
|
648
|
+
* find them, and so `filesToBackUp()` copies them aside first: a bug in the
|
|
649
|
+
* move would otherwise take a live session with it.
|
|
650
|
+
*/
|
|
651
|
+
authJson: () => at("auth.json"),
|
|
652
|
+
nextAudit: () => at("next-audit.json"),
|
|
653
|
+
/**
|
|
654
|
+
* Layout 4's `audit/reminder.json`, retired before it was ever written to.
|
|
655
|
+
*
|
|
656
|
+
* The layout-4 step MOVES `next-audit.json` here rather than deleting it,
|
|
657
|
+
* because the scheduled-audit work that replaces reminders had not landed yet
|
|
658
|
+
* and dropping a cadence someone chose would have been unrecoverable if it
|
|
659
|
+
* slipped. It has landed; the reminder concept is gone, and this is the
|
|
660
|
+
* position the file was parked in. Listed so a reset clears it.
|
|
661
|
+
*/
|
|
662
|
+
auditReminder: () => at("audit", "reminder.json"),
|
|
663
|
+
auditSchedule: () => at("state", "audit-schedule.json"),
|
|
558
664
|
cacheDir: () => at("cache"),
|
|
559
665
|
hookActivityDir: () => at("cache", "hook-activity"),
|
|
560
666
|
auditCacheDir: () => at("cache", "audit"),
|
|
@@ -640,6 +746,10 @@ function retiredLayoutPaths(): string[] {
|
|
|
640
746
|
// `migrateHookActivity()`, and everything else in `cache/` still goes —
|
|
641
747
|
// both remaining entries are re-derived on demand.
|
|
642
748
|
legacy.auditCacheDir(),
|
|
749
|
+
// The reminder, at the position layout 4 parked it in. It is on this list
|
|
750
|
+
// rather than in `HOME_CLASSES` because the path is RETIRED: nothing writes
|
|
751
|
+
// it any more, so it has no class to carry — only a location to clear.
|
|
752
|
+
legacy.auditReminder(),
|
|
643
753
|
legacy.codexSessionPaths(),
|
|
644
754
|
legacy.spoolDir(),
|
|
645
755
|
legacy.failedDir(),
|
package/src/hooks/fp-reset.ts
CHANGED
|
@@ -910,7 +910,12 @@ export function readCarriedLegacyCredentials(): FpCredentials | null {
|
|
|
910
910
|
return Object.keys(creds).length > 0 ? creds : null;
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
-
|
|
913
|
+
/**
|
|
914
|
+
* @param to The layout this step LANDS on, which is not always the current one.
|
|
915
|
+
* Defaults to `LAYOUT_VERSION` for a direct call, but the registry passes the
|
|
916
|
+
* step's own `to` — see the stamp at the end of this function.
|
|
917
|
+
*/
|
|
918
|
+
export function resetHome(from: number, to: number = LAYOUT_VERSION): ResetOutcome {
|
|
914
919
|
// BEFORE the deletions, so a file that is mid-move is never one the reset
|
|
915
920
|
// then walks over.
|
|
916
921
|
const migrated = migrateConventionPolicies();
|
|
@@ -959,7 +964,22 @@ export function resetHome(from: number): ResetOutcome {
|
|
|
959
964
|
// straight out of the same file. Both paths end at the same key, and only one
|
|
960
965
|
// of them represents something a person typed.
|
|
961
966
|
if (telemetryOptOut) updateConfig({ telemetry: { enabled: false } });
|
|
962
|
-
|
|
967
|
+
// The step's OWN target, not LAYOUT_VERSION.
|
|
968
|
+
//
|
|
969
|
+
// Every step used to end stamping the current layout, which was harmless
|
|
970
|
+
// while every chain was one hop. On `1 → 3 → 4` it means the first step
|
|
971
|
+
// marks the home layout 4 with its files still at layout 3, and the window
|
|
972
|
+
// between that stamp and the next step completing is a real one: a SIGKILL,
|
|
973
|
+
// an OOM or a power loss inside it leaves a home that reads as `current`
|
|
974
|
+
// forever. `detectLayout()` short-circuits on the marker, so no later
|
|
975
|
+
// command re-examines the landmarks, and `auth.json` sits at the root while
|
|
976
|
+
// layout 4 reads `audit/session.json` — a machine silently signed out with
|
|
977
|
+
// its session on disk and nothing that would ever move it.
|
|
978
|
+
//
|
|
979
|
+
// `runMigrations` also repairs an over-stamp in its `catch`, but a killed
|
|
980
|
+
// process runs no catch. Stamping the truth in the first place is what makes
|
|
981
|
+
// that repair a second line of defence rather than the only one.
|
|
982
|
+
writeVersionFile({ layout: to });
|
|
963
983
|
return { removed, migrated, activity, policyConfig, spooled, from };
|
|
964
984
|
}
|
|
965
985
|
|
|
@@ -1094,6 +1114,10 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
|
1094
1114
|
// After, not before — see the function's own note for why the intuitive
|
|
1095
1115
|
// order cannot work.
|
|
1096
1116
|
const pending = await drainSpoolAfterMigrating();
|
|
1117
|
+
// Read AFTER the migration: on a machine coming from layout 1 or 2 the
|
|
1118
|
+
// config this reads is the one the migration just carried across, so asking
|
|
1119
|
+
// any earlier would read a file that is about to move.
|
|
1120
|
+
const daemonHint = staleDaemonHint();
|
|
1097
1121
|
return {
|
|
1098
1122
|
state,
|
|
1099
1123
|
fatal: false,
|
|
@@ -1160,6 +1184,14 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
|
|
|
1160
1184
|
// so the machine enforces exactly as it did before this command ran. A
|
|
1161
1185
|
// home that genuinely never finished setup reaches the wizard through
|
|
1162
1186
|
// `shouldOfferFirstRun`, which reads `isConfigured()` — see `didReset`.
|
|
1187
|
+
//
|
|
1188
|
+
// The daemon hint belongs HERE above all, and was missing. This branch
|
|
1189
|
+
// is the one that moves the home and stamps the new layout marker — it
|
|
1190
|
+
// is the command that CREATES the incompatibility with an unrefreshed
|
|
1191
|
+
// daemon, and it was the one command saying nothing about it. Every
|
|
1192
|
+
// later command reached the non-stale return below and got the hint;
|
|
1193
|
+
// the one where the user is watching the reorganisation happen did not.
|
|
1194
|
+
...(daemonHint.length > 0 ? ["", ...daemonHint] : []),
|
|
1163
1195
|
],
|
|
1164
1196
|
};
|
|
1165
1197
|
}
|
|
@@ -1269,20 +1301,58 @@ async function healDaemonFlag(): Promise<string[]> {
|
|
|
1269
1301
|
}
|
|
1270
1302
|
|
|
1271
1303
|
/**
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
* Deliberately NOT on the hook path.
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1304
|
+
* What to say when the daemon's version does not match the CLI's.
|
|
1305
|
+
*
|
|
1306
|
+
* Deliberately NOT on the hook path. CLI commands are where a person is present
|
|
1307
|
+
* to act on it, and once per tool call would be noise.
|
|
1308
|
+
*
|
|
1309
|
+
* Two messages, because the stakes are not the same on both kinds of machine.
|
|
1310
|
+
*
|
|
1311
|
+
* On a machine that does NOT require the daemon, a stale one is what it looks
|
|
1312
|
+
* like: slower to notice an upgrade, still enforcing correctly.
|
|
1313
|
+
*
|
|
1314
|
+
* On a machine that DOES — `daemon.configured` — it is a scheduled outage.
|
|
1315
|
+
* failproofaid calls `refuse_foreign_layout()` before it binds its socket and
|
|
1316
|
+
* exits when the home's layout marker is not the one its binary was built
|
|
1317
|
+
* against, and a release that moves `~/.failproofai` therefore strands every
|
|
1318
|
+
* daemon that has not been refreshed. Nothing looks wrong in the meantime: the
|
|
1319
|
+
* running process read the marker once at startup and keeps serving from
|
|
1320
|
+
* memory. The failure lands at the next restart — a reboot, a crash,
|
|
1321
|
+
* `systemctl restart` — where the unit exits nonzero, `Restart=on-failure`
|
|
1322
|
+
* trips the start limit, and the service latches `failed`. From there the
|
|
1323
|
+
* machine fails closed and denies every tool call across all 11 CLIs, and
|
|
1324
|
+
* `healDaemonFlag()` will not rescue it because a layout-refusing unit reads as
|
|
1325
|
+
* `stopped`, which it deliberately excludes.
|
|
1326
|
+
*
|
|
1327
|
+
* This used to say a stale daemon "is slower to notice an upgrade, not broken"
|
|
1328
|
+
* to everybody, and pointed at `failproofai config`. Across a layout bump that
|
|
1329
|
+
* is the wrong sentence and the wrong command.
|
|
1278
1330
|
*/
|
|
1279
1331
|
function staleDaemonHint(): string[] {
|
|
1280
1332
|
try {
|
|
1281
1333
|
const skew = daemonVersionSkew();
|
|
1282
1334
|
if (!skew) return [];
|
|
1335
|
+
let requiresDaemon = false;
|
|
1336
|
+
try {
|
|
1337
|
+
requiresDaemon = readConfig().daemon.configured;
|
|
1338
|
+
} catch {
|
|
1339
|
+
// Unreadable config: fall through to the mild message rather than
|
|
1340
|
+
// frightening somebody whose machine may not require the daemon at all.
|
|
1341
|
+
}
|
|
1342
|
+
if (requiresDaemon) {
|
|
1343
|
+
return [
|
|
1344
|
+
`[failproofai] daemon is ${skew.installed}, CLI is ${skew.expected}.`,
|
|
1345
|
+
`This machine is configured to REQUIRE the daemon. A daemon built against a`,
|
|
1346
|
+
`different on-disk layout refuses to start, and this version moved it — so the`,
|
|
1347
|
+
`next reboot or restart can leave the service down, which denies every tool`,
|
|
1348
|
+
`call until it is fixed.`,
|
|
1349
|
+
`Run \`failproofai update\` now to bring the daemon in line.`,
|
|
1350
|
+
``,
|
|
1351
|
+
];
|
|
1352
|
+
}
|
|
1283
1353
|
return [
|
|
1284
1354
|
`[failproofai] daemon is ${skew.installed}, CLI is ${skew.expected} — ` +
|
|
1285
|
-
`run \`failproofai
|
|
1355
|
+
`run \`failproofai update\` to update it.`,
|
|
1286
1356
|
``,
|
|
1287
1357
|
];
|
|
1288
1358
|
} catch {
|
package/src/hooks/migrations.ts
CHANGED
|
@@ -37,11 +37,22 @@
|
|
|
37
37
|
* than counting. A chain from 1 today is one step; when layout 4 lands it becomes
|
|
38
38
|
* `1 → 3` then `3 → 4`, and only the second has to be written.
|
|
39
39
|
*/
|
|
40
|
-
import {
|
|
41
|
-
|
|
40
|
+
import {
|
|
41
|
+
chmodSync,
|
|
42
|
+
copyFileSync,
|
|
43
|
+
existsSync,
|
|
44
|
+
mkdirSync,
|
|
45
|
+
readFileSync,
|
|
46
|
+
renameSync,
|
|
47
|
+
rmSync,
|
|
48
|
+
writeFileSync,
|
|
49
|
+
} from "node:fs";
|
|
50
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
42
51
|
import { version as cliVersion } from "../../package.json";
|
|
43
52
|
import {
|
|
44
53
|
LAYOUT_VERSION,
|
|
54
|
+
auditScheduleFile,
|
|
55
|
+
auditSessionFile,
|
|
45
56
|
configFile,
|
|
46
57
|
credentialsFile,
|
|
47
58
|
failproofaiHome,
|
|
@@ -52,6 +63,7 @@ import {
|
|
|
52
63
|
migrationsDir,
|
|
53
64
|
versionFile,
|
|
54
65
|
} from "./fp-home";
|
|
66
|
+
import { readVersionFile, writeVersionFile } from "./fp-config";
|
|
55
67
|
import { resetHome, type ResetOutcome } from "./fp-reset";
|
|
56
68
|
|
|
57
69
|
export interface Migration {
|
|
@@ -78,17 +90,154 @@ export const MIGRATIONS: readonly Migration[] = [
|
|
|
78
90
|
to: 3,
|
|
79
91
|
describe:
|
|
80
92
|
"layout 1 → 3: carry the decision log out of cache/, keep the policy config in place, drop the layout-1 credential files",
|
|
81
|
-
run: () => resetHome(1),
|
|
93
|
+
run: () => resetHome(1, 3),
|
|
82
94
|
},
|
|
83
95
|
{
|
|
84
96
|
from: 2,
|
|
85
97
|
to: 3,
|
|
86
98
|
describe:
|
|
87
99
|
"layout 2 → 3: carry config.toml and credentials.toml into JSON, move custom-policies/ back up into policies/, nest the policy config at the root",
|
|
88
|
-
run: () => resetHome(2),
|
|
100
|
+
run: () => resetHome(2, 3),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
from: 3,
|
|
104
|
+
to: 4,
|
|
105
|
+
describe:
|
|
106
|
+
"layout 3 → 4: gather the audit's files under audit/ — auth.json becomes audit/session.json, next-audit.json becomes audit/reminder.json, state/audit-schedule.json becomes audit/schedule.json",
|
|
107
|
+
run: migrateToLayout4,
|
|
89
108
|
},
|
|
90
109
|
];
|
|
91
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Layout 3 → 4. The first step written against this registry rather than
|
|
113
|
+
* delegating to `resetHome`, which is what the header promised: additive.
|
|
114
|
+
*
|
|
115
|
+
* Three moves, no deletions. Each is a rename with a copy fallback, because
|
|
116
|
+
* `audit/` and the home root can sit on different filesystems once `$HOME` is a
|
|
117
|
+
* network mount or the home has been assembled by a container bind — `rename(2)`
|
|
118
|
+
* returns `EXDEV` there, and a step that threw on it would strand the machine at
|
|
119
|
+
* layout 3 forever.
|
|
120
|
+
*
|
|
121
|
+
* The reminder's destination is `legacy.auditReminder()`, a RETIRED path. The
|
|
122
|
+
* feature it belonged to is deleted in this same release, so nothing will ever
|
|
123
|
+
* read the file again — but a migration that DESTROYS something a person chose
|
|
124
|
+
* is a different act from one that moves it, and the difference matters even
|
|
125
|
+
* when the thing is obsolete. It is moved here and cleared by the next reset,
|
|
126
|
+
* via `retiredLayoutPaths()`.
|
|
127
|
+
*
|
|
128
|
+
* **A missing source is success, not failure.** Most homes have never signed in,
|
|
129
|
+
* so `auth.json` and `next-audit.json` are absent on the majority of machines,
|
|
130
|
+
* and a scheduled scan that has never run leaves no `audit-schedule.json`. Only
|
|
131
|
+
* a source that EXISTS and could not be moved is an error worth stopping for.
|
|
132
|
+
*
|
|
133
|
+
* **A destination that already exists wins.** Re-running the step — which is
|
|
134
|
+
* exactly what happens when a later step in the same chain throws and the user
|
|
135
|
+
* retries — must not copy a stale layout-3 file back over the layout-4 one that
|
|
136
|
+
* has since been written to.
|
|
137
|
+
*/
|
|
138
|
+
function migrateToLayout4(): ResetOutcome {
|
|
139
|
+
const moves: { from: string; to: string }[] = [
|
|
140
|
+
{ from: legacy.authJson(), to: auditSessionFile() },
|
|
141
|
+
{ from: legacy.nextAudit(), to: legacy.auditReminder() },
|
|
142
|
+
{ from: legacy.auditSchedule(), to: auditScheduleFile() },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
// `FAILPROOFAI_AUTH_DIR` names a directory OUTSIDE the managed home — a
|
|
146
|
+
// documented env var, not a test hook — and `auth-store` resolves the session
|
|
147
|
+
// relative to it. Every path above comes from `FAILPROOFAI_HOME`, so without
|
|
148
|
+
// this the override directory is never visited: the file stays `auth.json`,
|
|
149
|
+
// layout 4 reads `session.json`, and the upgrade signs the user out silently.
|
|
150
|
+
// Their scans keep running and their digests stop, which is the failure this
|
|
151
|
+
// whole area is built to avoid.
|
|
152
|
+
//
|
|
153
|
+
// The same two moves, in their directory, so one naming scheme holds
|
|
154
|
+
// everywhere rather than the file having a different name depending on how
|
|
155
|
+
// the process was configured.
|
|
156
|
+
const authDirOverride = process.env.FAILPROOFAI_AUTH_DIR;
|
|
157
|
+
if (authDirOverride) {
|
|
158
|
+
moves.push(
|
|
159
|
+
{ from: join(authDirOverride, "auth.json"), to: join(authDirOverride, "session.json") },
|
|
160
|
+
{
|
|
161
|
+
from: join(authDirOverride, "next-audit.json"),
|
|
162
|
+
to: join(authDirOverride, "reminder.json"),
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const migrated: string[] = [];
|
|
168
|
+
for (const { from, to } of moves) {
|
|
169
|
+
if (!existsSync(from)) continue;
|
|
170
|
+
if (existsSync(to)) {
|
|
171
|
+
// The layout-4 file is already authoritative. Drop the stale original
|
|
172
|
+
// rather than leaving a second copy of a credential lying at the root.
|
|
173
|
+
//
|
|
174
|
+
// A failure here PROPAGATES. Swallowing it continued to `writeVersionFile`
|
|
175
|
+
// and stamped the home as layout 4 with `auth.json` — a live bearer token
|
|
176
|
+
// — still sitting at the root, where nothing would ever look at it again
|
|
177
|
+
// and nothing would ever clean it up. Throwing leaves the home at layout 3
|
|
178
|
+
// and the next command retries, which is exactly what `runMigrations`
|
|
179
|
+
// documents a failed step to mean; the destination is already
|
|
180
|
+
// authoritative, so the retry is a no-op plus one more delete attempt.
|
|
181
|
+
rmSync(from, { force: true });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
185
|
+
try {
|
|
186
|
+
renameSync(from, to);
|
|
187
|
+
} catch {
|
|
188
|
+
// EXDEV, or a rename racing something holding the file open on Windows.
|
|
189
|
+
try {
|
|
190
|
+
copyFileSync(from, to);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Remove the PARTIAL destination before giving up.
|
|
193
|
+
//
|
|
194
|
+
// `copyFileSync` is not atomic: ENOSPC or a kill part-way through
|
|
195
|
+
// leaves a truncated `to` on disk. The source is still intact at this
|
|
196
|
+
// point, so nothing is lost yet — but the next attempt takes the
|
|
197
|
+
// `existsSync(to)` branch above, reads that fragment as the
|
|
198
|
+
// authoritative layout-4 file, and deletes the good original. A
|
|
199
|
+
// half-written session file is not a session, so the retry would have
|
|
200
|
+
// signed the machine out using the very branch that exists to protect
|
|
201
|
+
// the credential.
|
|
202
|
+
try {
|
|
203
|
+
rmSync(to, { force: true });
|
|
204
|
+
} catch {
|
|
205
|
+
// Nothing better to do. The throw below still leaves the home at
|
|
206
|
+
// layout 3, so `deleteAuth`'s sweep and the backup both still apply.
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
rmSync(from, { force: true });
|
|
211
|
+
}
|
|
212
|
+
migrated.push(`${basename(from)} → audit/${basename(to)}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// `session.json` carries tokens and `auth.json` was written 0600 by
|
|
216
|
+
// `writeJsonAtomically`. A rename preserves the mode, but a copy fallback
|
|
217
|
+
// inherits the process umask — so reassert it rather than assume which branch
|
|
218
|
+
// ran. Belt and braces on a file whose whole content is a bearer credential.
|
|
219
|
+
for (const secret of [auditSessionFile()]) {
|
|
220
|
+
if (!existsSync(secret)) continue;
|
|
221
|
+
try {
|
|
222
|
+
chmodSync(secret, 0o600);
|
|
223
|
+
} catch {
|
|
224
|
+
// Best effort, exactly as `writeJsonAtomically` treats it.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The same stamper every other write of this file goes through. Hand-rolling
|
|
229
|
+
// the JSON here would drop `daemon`, which nothing on this path touches and
|
|
230
|
+
// which `daemonVersionSkew()` reads on every CLI command.
|
|
231
|
+
//
|
|
232
|
+
// This step's own `to`, spelled out for the same reason `resetHome` takes
|
|
233
|
+
// one: a step must never mark the home as a layout it did not reach. Here
|
|
234
|
+
// they happen to be equal, and writing the literal is what keeps them equal
|
|
235
|
+
// by intent rather than by coincidence when layout 5 arrives.
|
|
236
|
+
writeVersionFile({ layout: 4 });
|
|
237
|
+
|
|
238
|
+
return { removed: [], migrated, activity: [], policyConfig: [], spooled: [], from: 3 };
|
|
239
|
+
}
|
|
240
|
+
|
|
92
241
|
/**
|
|
93
242
|
* The steps that take `from` to {@link LAYOUT_VERSION}.
|
|
94
243
|
*
|
|
@@ -256,6 +405,14 @@ const BACKED_UP_LEGACY: BackedUpFile[] = [
|
|
|
256
405
|
// most incomplete exactly where it mattered most.
|
|
257
406
|
{ at: legacy.cloudCredentials },
|
|
258
407
|
{ at: legacy.ingestCredentials },
|
|
408
|
+
// The three files the layout-4 step MOVES. `auth.json` is the one that
|
|
409
|
+
// matters: it is a live bearer credential, and unlike every other entry here
|
|
410
|
+
// it was never on a delete list — so it has never had a copy taken before a
|
|
411
|
+
// migration touched it. A move is not a deletion, but a move with a bug in it
|
|
412
|
+
// is, and this is the only insurance against that.
|
|
413
|
+
{ at: legacy.authJson },
|
|
414
|
+
{ at: legacy.nextAudit },
|
|
415
|
+
{ at: legacy.auditSchedule },
|
|
259
416
|
];
|
|
260
417
|
|
|
261
418
|
/** The name a file is saved under inside `backup-layout<n>/`. */
|
|
@@ -263,6 +420,57 @@ function backupNameOf(f: BackedUpFile): string {
|
|
|
263
420
|
return f.as ?? basename(f.at());
|
|
264
421
|
}
|
|
265
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Backup copies deleted once the whole chain has succeeded.
|
|
425
|
+
*
|
|
426
|
+
* The backup above is insurance against a migration that goes wrong. Insurance
|
|
427
|
+
* you keep forever on a live credential is not insurance, it is a second copy
|
|
428
|
+
* of the credential — and this one is worse than the original, because
|
|
429
|
+
* `migrationsDir` is classed `identity` in `HOME_CLASSES`, so no reset class
|
|
430
|
+
* ever removes it, and `deleteAuth()` only ever knew about the live path.
|
|
431
|
+
* Signing out of the dashboard, a 401 auto-delete and `failproofai reset` all
|
|
432
|
+
* left a working bearer and refresh token sitting at
|
|
433
|
+
* `migrations/backup-layout3/auth.json`, where every dotfile backup, container
|
|
434
|
+
* image, snapshot and handed-over machine would carry it. There is no CLI
|
|
435
|
+
* sign-out at all, so the headless boxes this feature targets had no supported
|
|
436
|
+
* way to remove it. A stale refresh token is also exactly the input
|
|
437
|
+
* `auth-store.ts` documents as triggering server-side replay revocation.
|
|
438
|
+
*
|
|
439
|
+
* Only entries whose source was MOVED are prunable, never ones that were
|
|
440
|
+
* DELETED: for `credentials.json` the backup is the only remaining copy, so
|
|
441
|
+
* removing it would be the data loss the backup exists to prevent. `landsAt` is
|
|
442
|
+
* checked rather than assumed, so a copy is dropped only once the file is
|
|
443
|
+
* provably readable at its new home.
|
|
444
|
+
*/
|
|
445
|
+
const PRUNED_AFTER_SUCCESS: ReadonlyArray<{ as: string; landsAt: () => string }> = [
|
|
446
|
+
{ as: basename(legacy.authJson()), landsAt: auditSessionFile },
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Remove the credential copies a completed chain no longer needs.
|
|
451
|
+
*
|
|
452
|
+
* Never throws: a chain that migrated correctly must not be reported as failed
|
|
453
|
+
* because a cleanup could not delete a file.
|
|
454
|
+
*/
|
|
455
|
+
export function pruneMigratedCredentials(from: number): string[] {
|
|
456
|
+
const pruned: string[] = [];
|
|
457
|
+
for (let layout = from; layout < LAYOUT_VERSION; layout++) {
|
|
458
|
+
for (const { as, landsAt } of PRUNED_AFTER_SUCCESS) {
|
|
459
|
+
try {
|
|
460
|
+
if (!existsSync(landsAt())) continue;
|
|
461
|
+
const copy = resolve(migrationBackupDir(layout), as);
|
|
462
|
+
if (!existsSync(copy)) continue;
|
|
463
|
+
rmSync(copy, { force: true });
|
|
464
|
+
pruned.push(as);
|
|
465
|
+
} catch {
|
|
466
|
+
// Best effort. `deleteAuth()` sweeps these too, so a copy that survives
|
|
467
|
+
// here is still removed the next time somebody signs out.
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return pruned;
|
|
472
|
+
}
|
|
473
|
+
|
|
266
474
|
/**
|
|
267
475
|
* The files that exist right now and would be backed up, each exactly once.
|
|
268
476
|
*
|
|
@@ -385,6 +593,31 @@ export function runMigrations(
|
|
|
385
593
|
});
|
|
386
594
|
} catch (err) {
|
|
387
595
|
steps.push({ from: step.from, to: step.to, ok: false });
|
|
596
|
+
// Undo an EARLIER step's over-stamp, if there was one.
|
|
597
|
+
//
|
|
598
|
+
// A step ends by stamping `VERSION`, and `writeVersionFile()` writes
|
|
599
|
+
// {@link LAYOUT_VERSION} rather than the step's own `to`. That was harmless
|
|
600
|
+
// while every chain was one hop and became a trap the moment one was two:
|
|
601
|
+
// on `2 → 3 → 4` the FIRST step stamps 4, so a `3 → 4` that then throws
|
|
602
|
+
// leaves a home marked CURRENT that was never migrated. `detectLayout()`
|
|
603
|
+
// reports `current`, no later command ever retries, and `auth.json` stays
|
|
604
|
+
// at the root while layout 4 reads `audit/session.json` — a machine
|
|
605
|
+
// silently signed out, with its session sitting on disk and nothing left
|
|
606
|
+
// that would ever move it.
|
|
607
|
+
//
|
|
608
|
+
// Only touched when the marker already claims the home is current, so a
|
|
609
|
+
// chain whose steps never got that far keeps whatever they left. `step.from`
|
|
610
|
+
// is where this one actually got to: every earlier step succeeded, and a
|
|
611
|
+
// failing step is documented not to roll back — which is exactly the state
|
|
612
|
+
// the next command should try to migrate again.
|
|
613
|
+
try {
|
|
614
|
+
const marker = readVersionFile();
|
|
615
|
+
if (marker && marker.layout >= LAYOUT_VERSION) writeVersionFile({ layout: step.from });
|
|
616
|
+
} catch {
|
|
617
|
+
// A marker we cannot rewrite leaves the home reading as whatever the last
|
|
618
|
+
// successful step claimed. Nothing further can be done about it here, and
|
|
619
|
+
// failing the run a second way would only hide the real error below.
|
|
620
|
+
}
|
|
388
621
|
failed = {
|
|
389
622
|
from: step.from,
|
|
390
623
|
to: step.to,
|
|
@@ -402,6 +635,10 @@ export function runMigrations(
|
|
|
402
635
|
}
|
|
403
636
|
}
|
|
404
637
|
|
|
638
|
+
// Only on a clean chain. A run that failed still needs its copies: the whole
|
|
639
|
+
// point of the backup is the state this branch is in.
|
|
640
|
+
if (!failed) pruneMigratedCredentials(from);
|
|
641
|
+
|
|
405
642
|
return { from, steps, backedUp, outcome, failed };
|
|
406
643
|
}
|
|
407
644
|
|