failproofai 1.0.0-beta.12 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -3
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -3
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -3
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +2 -3
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -4
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -4
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -3
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1a7i2q4._.js → [root-of-the-server]__00ni9za._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_ps0jw._.js → [root-of-the-server]__0effm9m._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0lk_935._.js → [root-of-the-server]__0ix8gm7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0ky_n0r._.js → [root-of-the-server]__0ji11a-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0v2fpxe._.js → [root-of-the-server]__0ojwjxl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vb5fl2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uyc-z4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20d06rh._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -3
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1rdruw4._.js → [root-of-the-server]__0_1tiya._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ha3gul._.js → [root-of-the-server]__0eeipso._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tyf0tc._.js → [root-of-the-server]__17itmx-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1io8n-9._.js → [root-of-the-server]__1dx-a-c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1yw368c._.js → [root-of-the-server]__1n2vcv4._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1a2hcb_._.js → [root-of-the-server]__1tlvv9a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1fahg9e._.js → _0vo2qyt._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0uz7o9y._.js → _0xej-0e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_04mekdy._.js → _10399ge._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0se484m._.js → _149k9oy._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1vsgpqt._.js → _166jvu8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0r-dsre._.js → _1_hwp06._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w8sola._.js → _1mnhi_r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w_6p9g._.js → _1ror7k9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gmzkwj._.js → _1sdgprl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/{2_h--2ame3r83.js → 01s3vpzky_qew.js} +1 -1
- package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
- package/.next/standalone/.next/static/chunks/{23hp7hekegpe5.js → 0zbcd_n8ku2ov.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0cakk77ppmlo7.js → 1erji0ratofla.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40-zk44qsg5y-.js → 23hnlzqx44vbe.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1wb8cnmzx69rz.js → 2cwsiyhc7d2cw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_rdbv_k5j51p.js → 2y42ebzynyxdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1spk1bub0vd-h.js → 315lncj7dsp9c.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0-zh1umoq6ppc.js → 31_px0dfgdg9j.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2bie7ajv70p9b.js → 3l4-fu3xt75j8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{298v78ypf8edl.js → 3poscoxn69x9g.js} +1 -1
- package/.next/standalone/app/policies/hooks-client.tsx +4 -4
- package/.next/standalone/ino.ujh3/.failproofai/VERSION +4 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/last-version +1 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/telemetry-id +1 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +186 -5
- package/dist/cli.mjs +1130 -1352
- package/dist/worker.mjs +107 -1000
- package/package.json +10 -10
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/cloud-connection.ts +4 -4
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-enrollment.ts +3 -3
- package/src/hooks/cloud-managed-policies.ts +60 -22
- package/src/hooks/configure-wizard.ts +124 -53
- package/src/hooks/custom-hooks-loader.ts +57 -8
- package/src/hooks/daemon-service.ts +66 -11
- package/src/hooks/flush-cli.ts +180 -0
- package/src/hooks/fp-config.ts +242 -118
- package/src/hooks/fp-home.ts +138 -22
- package/src/hooks/fp-reset.ts +357 -43
- package/src/hooks/handler.ts +11 -11
- package/src/hooks/harness-cli.ts +309 -0
- package/src/hooks/hook-activity-store.ts +5 -5
- package/src/hooks/manager.ts +35 -0
- package/src/hooks/onboarding-attempt.ts +22 -6
- package/src/hooks/tui.ts +64 -7
- package/src/hooks/uninstall-cli.ts +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +0 -5
- package/.next/standalone/.next/static/chunks/13vrxdsdihqd_.css +0 -2
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_ssgManifest.js +0 -0
package/src/hooks/fp-config.ts
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `VERSION`, `config.
|
|
2
|
+
* `VERSION`, `config.json` and `credentials.json` — the three files a human
|
|
3
3
|
* might actually open in `~/.failproofai`.
|
|
4
4
|
*
|
|
5
5
|
* ## Why the split
|
|
6
6
|
*
|
|
7
|
-
* `config.
|
|
8
|
-
* `credentials.
|
|
7
|
+
* `config.json` inherits the umask and lands at 0664 on a normal machine.
|
|
8
|
+
* `credentials.json` is written 0600 and holds every token. That boundary is
|
|
9
9
|
* not tidiness: it is why `ingest.json` and `cloud.json` were separate files
|
|
10
|
-
* before this, and it means CI, tooling and a screen-shared `cat config.
|
|
10
|
+
* before this, and it means CI, tooling and a screen-shared `cat config.json`
|
|
11
11
|
* can read a machine's configuration without ever seeing a live credential.
|
|
12
12
|
*
|
|
13
|
-
* ##
|
|
13
|
+
* ## One format, not two
|
|
14
14
|
*
|
|
15
|
-
* These
|
|
16
|
-
* and
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* These were TOML through layout 2, for the comments — the file explained what
|
|
16
|
+
* `oss` meant and why `daemon.configured` must never be hand-set. Layout 3
|
|
17
|
+
* moved them to JSON so the home speaks one format end to end: one parser, one
|
|
18
|
+
* escaping rule, and no `toml` dependency in either the CLI or the daemon for
|
|
19
|
+
* files that were always flat key/value.
|
|
20
|
+
*
|
|
21
|
+
* The comments are the cost, and they were not decoration — this is the file
|
|
22
|
+
* someone opens when something is wrong. That guidance now lives in
|
|
23
|
+
* `failproofai config --status` and the docs, which is a worse place for it.
|
|
22
24
|
*/
|
|
23
25
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
24
26
|
import { dirname } from "node:path";
|
|
25
|
-
import { parse as parseToml } from "smol-toml";
|
|
26
27
|
import { version as cliVersion } from "../../package.json";
|
|
27
28
|
import {
|
|
28
29
|
LAYOUT_VERSION,
|
|
@@ -47,8 +48,10 @@ export interface VersionFile {
|
|
|
47
48
|
export type LayoutState =
|
|
48
49
|
/** No home yet, or an empty one. A fresh install. */
|
|
49
50
|
| { kind: "absent" }
|
|
50
|
-
/** A home written by this layout.
|
|
51
|
-
|
|
51
|
+
/** A home written by this layout. `inferred` means the layout was recovered
|
|
52
|
+
* from a landmark because `VERSION` was missing or unreadable — the caller
|
|
53
|
+
* should write the marker back. */
|
|
54
|
+
| { kind: "current"; version: VersionFile; inferred?: boolean }
|
|
52
55
|
/** A home from an older layout — must be reset, never half-read. */
|
|
53
56
|
| { kind: "stale"; found: number }
|
|
54
57
|
/** A home from a NEWER layout: a downgrade. Refuse rather than corrupt it. */
|
|
@@ -75,8 +78,44 @@ export function detectLayout(): LayoutState {
|
|
|
75
78
|
: { kind: "stale", found: raw.layout };
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
// No VERSION file
|
|
79
|
-
//
|
|
81
|
+
// No VERSION file — fall back to landmarks.
|
|
82
|
+
//
|
|
83
|
+
// ORDER MATTERS, because layout 3 collides with layout 1 on one of them.
|
|
84
|
+
// Layout 3 put `policies-config.json` back at the home root, which is exactly
|
|
85
|
+
// where layout 1 kept it. A layout-3 home that lost its VERSION would
|
|
86
|
+
// therefore match a layout-1 landmark and be reported as layout 1 — and
|
|
87
|
+
// `resetHome(1)` runs layout-1 MIGRATIONS, which move `policies/*.mjs` on the
|
|
88
|
+
// assumption they are in layout 1's positions. In layout 3 those are the
|
|
89
|
+
// user's own convention policies, sitting where they belong.
|
|
90
|
+
//
|
|
91
|
+
// So check for a marker only the newer layouts have first — and DISTINGUISH
|
|
92
|
+
// them, because the two answers are not equally safe.
|
|
93
|
+
//
|
|
94
|
+
// `config.json` is layout 3's OWN file. Layout 2 wrote `config.toml`, layout 1
|
|
95
|
+
// had neither, and nothing but setup writes either one. So its presence does
|
|
96
|
+
// not merely prove "newer than layout 1" — it identifies THIS layout, and the
|
|
97
|
+
// only thing actually missing is the marker. Reporting it as stale instead ran
|
|
98
|
+
// a destructive reset over a healthy machine: `resettablePaths()` lists
|
|
99
|
+
// `configFile()` and `credentialsFile()`, so the home lost its live cloud
|
|
100
|
+
// token AND `daemon.configured` — which is what makes a machine fail closed.
|
|
101
|
+
// A machine that dropped out of fail-closed enforcement because a marker file
|
|
102
|
+
// went missing is the exact failure this module exists to prevent, and it
|
|
103
|
+
// announced itself as a routine "reorganised your home" message.
|
|
104
|
+
if (existsSync(configFile())) {
|
|
105
|
+
// `inferred`: the layout is right but the MARKER is missing, and nothing
|
|
106
|
+
// else rewrites it — so every later command re-derives it from a landmark,
|
|
107
|
+
// and the daemon version recorded in that file is gone for good
|
|
108
|
+
// (`daemonVersionSkew()` reads it, and stops nudging about a stale daemon).
|
|
109
|
+
// The caller re-stamps, exactly as it already does for a fresh home.
|
|
110
|
+
return { kind: "current", version: { layout: LAYOUT_VERSION, cli: "" }, inferred: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// `config.toml` and no `config.json` is genuinely layout 2, and a reset is
|
|
114
|
+
// right: its files are the ones being replaced.
|
|
115
|
+
if (existsSync(legacy.configToml())) return { kind: "stale", found: LAYOUT_VERSION - 1 };
|
|
116
|
+
|
|
117
|
+
// Layout 1 if any of its landmarks are present, otherwise this is simply a
|
|
118
|
+
// home that has not been set up yet.
|
|
80
119
|
const layoutOneMarkers = [
|
|
81
120
|
legacy.policyConfig(),
|
|
82
121
|
legacy.cacheDir(),
|
|
@@ -89,9 +128,50 @@ export function detectLayout(): LayoutState {
|
|
|
89
128
|
return layoutOneMarkers.some((p) => existsSync(p)) ? { kind: "stale", found: 1 } : { kind: "absent" };
|
|
90
129
|
}
|
|
91
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Read a layout-2 `VERSION`, which is TOML.
|
|
133
|
+
*
|
|
134
|
+
* Layout 3 made this file JSON, so `JSON.parse` throws on every layout-2 home —
|
|
135
|
+
* and "unreadable" is NOT a harmless answer here. It sends `detectLayout()` to
|
|
136
|
+
* the landmarks, and a layout-2 home whose owner never completed setup has no
|
|
137
|
+
* `config.toml` to match: it falls through every layout-1 marker too (layout 2
|
|
138
|
+
* nested `policies-config.json`, so the root file layout 1 is recognised by is
|
|
139
|
+
* absent) and reads as `absent` — a FRESH INSTALL. The CLI then stamps it
|
|
140
|
+
* layout 3 with no reset and no migration, and
|
|
141
|
+
* `policies/local-policies/policies-config.json` plus
|
|
142
|
+
* `policies/custom-policies/*.mjs` are orphaned permanently: the user's enabled
|
|
143
|
+
* policies revert to the defaults, their own policy files stop loading, and
|
|
144
|
+
* nothing says so. Reproduced exactly that way on a seeded home.
|
|
145
|
+
*
|
|
146
|
+
* A regex rather than a TOML parser because removing that dependency is half
|
|
147
|
+
* the point of layout 3, and this file is three flat `key = value` lines that
|
|
148
|
+
* this codebase itself wrote.
|
|
149
|
+
*
|
|
150
|
+
* ALL THREE fields are read, including `daemon`. It is tempting to take only the
|
|
151
|
+
* layout number on the grounds that the home is about to be reset — but
|
|
152
|
+
* `writeVersionFile()` carries the daemon version forward from `existing?.daemon`,
|
|
153
|
+
* so a field missing here is not re-derived, it is ERASED. The machine then
|
|
154
|
+
* reports "daemon not installed" with the binary sitting in `bin/`, and
|
|
155
|
+
* `daemonVersionSkew()` stops nudging about a stale daemon precisely when an
|
|
156
|
+
* upgrade has just happened.
|
|
157
|
+
*/
|
|
158
|
+
function readLegacyTomlVersion(text: string): VersionFile | null {
|
|
159
|
+
const layout = Number(/^\s*layout\s*=\s*(\d+)\s*$/m.exec(text)?.[1]);
|
|
160
|
+
if (!Number.isFinite(layout)) return null;
|
|
161
|
+
const str = (key: string) => new RegExp(`^\\s*${key}\\s*=\\s*"([^"]*)"`, "m").exec(text)?.[1];
|
|
162
|
+
return { layout, cli: str("cli") ?? "", daemon: str("daemon") };
|
|
163
|
+
}
|
|
164
|
+
|
|
92
165
|
export function readVersionFile(): VersionFile | null {
|
|
166
|
+
let text: string;
|
|
93
167
|
try {
|
|
94
|
-
|
|
168
|
+
text = readFileSync(versionFile(), "utf8");
|
|
169
|
+
} catch {
|
|
170
|
+
// Absent or unreadable. The landmark check decides what that implies.
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
95
175
|
const layout = Number(parsed.layout);
|
|
96
176
|
if (!Number.isFinite(layout)) return null;
|
|
97
177
|
return {
|
|
@@ -100,9 +180,10 @@ export function readVersionFile(): VersionFile | null {
|
|
|
100
180
|
daemon: typeof parsed.daemon === "string" ? parsed.daemon : undefined,
|
|
101
181
|
};
|
|
102
182
|
} catch {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
183
|
+
// Not JSON. Before concluding "no layout", try the one other format this
|
|
184
|
+
// file has ever had — see `readLegacyTomlVersion` for what guessing wrong
|
|
185
|
+
// costs. Genuinely corrupt content still yields null from there.
|
|
186
|
+
return readLegacyTomlVersion(text);
|
|
106
187
|
}
|
|
107
188
|
}
|
|
108
189
|
|
|
@@ -118,17 +199,17 @@ export function writeVersionFile(
|
|
|
118
199
|
// explicit act, used on uninstall.
|
|
119
200
|
daemon: v.clearDaemon ? undefined : (v.daemon ?? existing?.daemon),
|
|
120
201
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
writeFileAt(
|
|
203
|
+
versionFile(),
|
|
204
|
+
`${JSON.stringify(
|
|
205
|
+
{ layout: next.layout, cli: next.cli, ...(next.daemon ? { daemon: next.daemon } : {}) },
|
|
206
|
+
null,
|
|
207
|
+
2,
|
|
208
|
+
)}\n`,
|
|
209
|
+
);
|
|
129
210
|
}
|
|
130
211
|
|
|
131
|
-
// ── config.
|
|
212
|
+
// ── config.json ──────────────────────────────────────────────────────────────
|
|
132
213
|
|
|
133
214
|
/**
|
|
134
215
|
* How this machine relates to Failproof Cloud.
|
|
@@ -159,6 +240,25 @@ export interface FpConfig {
|
|
|
159
240
|
redact: "minimal" | "off";
|
|
160
241
|
environment: string;
|
|
161
242
|
machineId?: string;
|
|
243
|
+
/**
|
|
244
|
+
* Extra locations to capture per harness, beyond the one each source ships
|
|
245
|
+
* with — `[collector.sources.<harness>] extra_paths` in `config.toml`.
|
|
246
|
+
*
|
|
247
|
+
* Each entry is `label=path` or a bare `path`; the label becomes an agent-id
|
|
248
|
+
* namespace (`<label>-<agentId>`), which is what keeps two copies of one
|
|
249
|
+
* project from merging into a single agent. The daemon is the authority on
|
|
250
|
+
* the grammar and on which harness names are real — see
|
|
251
|
+
* `crates/fpai-collect/src/extra_paths.rs` and `HARNESS_KEYS` in
|
|
252
|
+
* `crates/failproofaid/src/main.rs`. This side stores and edits the strings;
|
|
253
|
+
* it deliberately does not re-implement the parser, because two parsers is
|
|
254
|
+
* how the CLI comes to accept a path the daemon then silently drops.
|
|
255
|
+
*
|
|
256
|
+
* Absent for every machine that has not configured one, which is almost all
|
|
257
|
+
* of them — so `writeConfig` emits nothing at all in that case and a config
|
|
258
|
+
* file is byte-identical to what this version wrote before the field
|
|
259
|
+
* existed.
|
|
260
|
+
*/
|
|
261
|
+
sources?: Record<string, { extraPaths: string[] }>;
|
|
162
262
|
};
|
|
163
263
|
telemetry: {
|
|
164
264
|
/**
|
|
@@ -234,17 +334,50 @@ export const DEFAULT_CONFIG: FpConfig = {
|
|
|
234
334
|
audit: { auto: false, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
|
|
235
335
|
};
|
|
236
336
|
|
|
337
|
+
/**
|
|
338
|
+
* `[collector.sources.*]` → the in-memory shape, dropping anything unusable.
|
|
339
|
+
*
|
|
340
|
+
* Entries are kept as written rather than normalised. The daemon parses them
|
|
341
|
+
* (label derivation, overlap rejection, `~` expansion) and reports what it
|
|
342
|
+
* rejected at startup; normalising here would give the two sides two answers
|
|
343
|
+
* and hide the daemon's rejection behind a value the CLI had already rewritten.
|
|
344
|
+
* The only things dropped are shapes that could not survive a round trip at
|
|
345
|
+
* all — a non-array, or a non-string element.
|
|
346
|
+
*
|
|
347
|
+
* `undefined` rather than `{}` when nothing is configured, so `writeConfig`
|
|
348
|
+
* emits no table and an untouched config file keeps its exact previous bytes.
|
|
349
|
+
*/
|
|
350
|
+
function readSources(raw: unknown): FpConfig["collector"]["sources"] {
|
|
351
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
352
|
+
const out: Record<string, { extraPaths: string[] }> = {};
|
|
353
|
+
for (const [name, table] of Object.entries(raw as Record<string, unknown>)) {
|
|
354
|
+
if (!table || typeof table !== "object" || Array.isArray(table)) continue;
|
|
355
|
+
const entries = (table as Record<string, unknown>).extra_paths;
|
|
356
|
+
if (!Array.isArray(entries)) continue;
|
|
357
|
+
const paths = entries.filter((e): e is string => typeof e === "string" && e.trim() !== "");
|
|
358
|
+
if (paths.length > 0) out[name] = { extraPaths: paths };
|
|
359
|
+
}
|
|
360
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
237
363
|
export function readConfig(): FpConfig {
|
|
238
364
|
try {
|
|
239
|
-
const parsed =
|
|
365
|
+
const parsed = JSON.parse(readFileSync(configFile(), "utf8")) as Record<string, unknown>;
|
|
240
366
|
const mode = (parsed.mode as Record<string, unknown> | undefined)?.kind;
|
|
241
367
|
const daemon = (parsed.daemon ?? {}) as Record<string, unknown>;
|
|
242
368
|
const collector = (parsed.collector ?? {}) as Record<string, unknown>;
|
|
243
369
|
const telemetry = (parsed.telemetry ?? {}) as Record<string, unknown>;
|
|
244
370
|
const audit = (parsed.audit ?? {}) as Record<string, unknown>;
|
|
245
371
|
return {
|
|
246
|
-
// Anything unrecognised reads as `oss`. The failure direction matters:
|
|
247
|
-
//
|
|
372
|
+
// Anything unrecognised reads as `oss`. The failure direction matters: a
|
|
373
|
+
// corrupt config must not be able to turn cloud reporting ON.
|
|
374
|
+
//
|
|
375
|
+
// This is a real check rather than `?? "oss"`, which only caught a MISSING
|
|
376
|
+
// mode and passed every present-but-unknown value straight through — so
|
|
377
|
+
// `"enterprise"`, or a typo like `"clod"`, became the machine's mode. The
|
|
378
|
+
// test named for this branch was writing a TOML body into a `.json` file,
|
|
379
|
+
// so the parse failed, the whole function fell to its defaults, and the
|
|
380
|
+
// assertion passed without the branch ever running.
|
|
248
381
|
mode: mode === "cloud" ? "cloud" : "oss",
|
|
249
382
|
daemon: { configured: daemon.configured === true },
|
|
250
383
|
collector: {
|
|
@@ -258,6 +391,7 @@ export function readConfig(): FpConfig {
|
|
|
258
391
|
environment:
|
|
259
392
|
typeof collector.environment === "string" ? collector.environment : "local",
|
|
260
393
|
machineId: typeof collector.machine_id === "string" ? collector.machine_id : undefined,
|
|
394
|
+
sources: readSources(collector.sources),
|
|
261
395
|
},
|
|
262
396
|
// Only an explicit `false` switches it off. Absent, or any other value,
|
|
263
397
|
// reads as on — the shipped default, and what a config with no
|
|
@@ -278,62 +412,61 @@ export function readConfig(): FpConfig {
|
|
|
278
412
|
}
|
|
279
413
|
}
|
|
280
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Regenerates `config.json` wholesale.
|
|
417
|
+
*
|
|
418
|
+
* The layout-2 file was hand-written TOML carrying its own documentation — what
|
|
419
|
+
* `oss` meant, why `daemon.configured` must never be hand-set. JSON cannot hold
|
|
420
|
+
* a comment, so that guidance now lives in `failproofai config --status` and the
|
|
421
|
+
* docs. It is a real loss on the one file a user opens when something is wrong;
|
|
422
|
+
* the trade is one serialisation format across the whole home instead of two.
|
|
423
|
+
*/
|
|
281
424
|
export function writeConfig(config: FpConfig): void {
|
|
282
425
|
const c = config.collector;
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
"",
|
|
328
|
-
"[audit]",
|
|
329
|
-
"# Scan this machine's agent history on a schedule and refresh the audit",
|
|
330
|
-
"# dashboard. OFF by default: the scan reads the CONTENTS of every session",
|
|
331
|
-
"# transcript on disk, so it waits to be asked. It runs as a separate",
|
|
332
|
-
"# short-lived process — never on the hook path.",
|
|
333
|
-
`auto = ${config.audit.auto}`,
|
|
334
|
-
`interval_days = ${config.audit.intervalDays}`,
|
|
335
|
-
);
|
|
336
|
-
writeFileAt(configFile(), lines.join("\n") + "\n");
|
|
426
|
+
const out: Record<string, unknown> = {
|
|
427
|
+
mode: { kind: config.mode },
|
|
428
|
+
daemon: { configured: config.daemon.configured },
|
|
429
|
+
collector: {
|
|
430
|
+
sessions: c.sessions,
|
|
431
|
+
hooks: c.hooks,
|
|
432
|
+
hooks_verbosity: c.hooksVerbosity,
|
|
433
|
+
redact: c.redact,
|
|
434
|
+
environment: c.environment,
|
|
435
|
+
// Omitted when unset rather than written null: the Rust side treats an
|
|
436
|
+
// absent machine_id as "derive one", and an explicit null is not that.
|
|
437
|
+
...(c.machineId ? { machine_id: c.machineId } : {}),
|
|
438
|
+
// Extra capture paths per harness. Emitted ONLY when non-empty, so a
|
|
439
|
+
// machine that never configured one gets a file byte-identical to what
|
|
440
|
+
// this function produced before the field existed — the same reasoning as
|
|
441
|
+
// `telemetry` below. (In TOML this also had to come after every scalar of
|
|
442
|
+
// `[collector]`, or the scalars after it read as belonging to the
|
|
443
|
+
// sub-table. JSON has no such ordering hazard, which is one of the things
|
|
444
|
+
// the format change buys.)
|
|
445
|
+
...(Object.entries(c.sources ?? {}).some(([, s]) => s.extraPaths?.length)
|
|
446
|
+
? {
|
|
447
|
+
sources: Object.fromEntries(
|
|
448
|
+
Object.entries(c.sources ?? {})
|
|
449
|
+
.filter(([, s]) => s.extraPaths?.length)
|
|
450
|
+
.map(([name, s]) => [name, { extra_paths: s.extraPaths }]),
|
|
451
|
+
),
|
|
452
|
+
}
|
|
453
|
+
: {}),
|
|
454
|
+
},
|
|
455
|
+
// Written ONLY when switched off. A default install therefore carries no
|
|
456
|
+
// telemetry block at all, but an operator who switched it off keeps it:
|
|
457
|
+
// this function regenerates the file wholesale, so emitting the key only
|
|
458
|
+
// when it is set is what stops a later rewrite from silently switching
|
|
459
|
+
// telemetry back on underneath them.
|
|
460
|
+
...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
|
|
461
|
+
// Written ALWAYS, unlike telemetry directly above — the two are opposites
|
|
462
|
+
// on purpose. Telemetry ships on and is deliberately not advertised;
|
|
463
|
+
// the scheduled audit ships off and is meant to be FOUND, and a switch
|
|
464
|
+
// nobody can see is the same as a switch that does not exist. Emitting both
|
|
465
|
+
// keys unconditionally also makes "a user's setting survives a rewrite"
|
|
466
|
+
// total rather than conditional.
|
|
467
|
+
audit: { auto: config.audit.auto, interval_days: config.audit.intervalDays },
|
|
468
|
+
};
|
|
469
|
+
writeFileAt(configFile(), `${JSON.stringify(out, null, 2)}\n`);
|
|
337
470
|
}
|
|
338
471
|
|
|
339
472
|
/** Merge a partial update into the config on disk. */
|
|
@@ -356,7 +489,7 @@ export function updateConfig(patch: {
|
|
|
356
489
|
return next;
|
|
357
490
|
}
|
|
358
491
|
|
|
359
|
-
// ── credentials.
|
|
492
|
+
// ── credentials.json ─────────────────────────────────────────────────────────
|
|
360
493
|
|
|
361
494
|
export interface FpCredentials {
|
|
362
495
|
cloud?: { url: string; machineId: string; token: string; machineLabel?: string };
|
|
@@ -382,7 +515,7 @@ export interface FpCredentials {
|
|
|
382
515
|
|
|
383
516
|
export function readCredentials(): FpCredentials {
|
|
384
517
|
try {
|
|
385
|
-
const parsed =
|
|
518
|
+
const parsed = JSON.parse(readFileSync(credentialsFile(), "utf8")) as Record<string, unknown>;
|
|
386
519
|
const cloud = parsed.cloud as Record<string, unknown> | undefined;
|
|
387
520
|
const ingest = parsed.ingest as Record<string, unknown> | undefined;
|
|
388
521
|
const auth = parsed.auth as Record<string, unknown> | undefined;
|
|
@@ -433,41 +566,32 @@ export function readCredentials(): FpCredentials {
|
|
|
433
566
|
* without the explicit chmod.
|
|
434
567
|
*/
|
|
435
568
|
export function writeCredentials(creds: FpCredentials): void {
|
|
436
|
-
const
|
|
437
|
-
"# failproofai credentials — owner-only (0600). Do not commit.",
|
|
438
|
-
];
|
|
569
|
+
const out: Record<string, unknown> = {};
|
|
439
570
|
if (creds.cloud) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
? [`machine_label = ${JSON.stringify(creds.cloud.machineLabel)}`]
|
|
447
|
-
: []),
|
|
448
|
-
`token = ${JSON.stringify(creds.cloud.token)}`,
|
|
449
|
-
);
|
|
571
|
+
out.cloud = {
|
|
572
|
+
url: creds.cloud.url,
|
|
573
|
+
machine_id: creds.cloud.machineId,
|
|
574
|
+
...(creds.cloud.machineLabel ? { machine_label: creds.cloud.machineLabel } : {}),
|
|
575
|
+
token: creds.cloud.token,
|
|
576
|
+
};
|
|
450
577
|
}
|
|
451
578
|
if (creds.ingest) {
|
|
452
|
-
|
|
453
|
-
"",
|
|
454
|
-
"[ingest]",
|
|
455
|
-
`url = ${JSON.stringify(creds.ingest.url)}`,
|
|
456
|
-
`key = ${JSON.stringify(creds.ingest.key)}`,
|
|
457
|
-
);
|
|
579
|
+
out.ingest = { url: creds.ingest.url, key: creds.ingest.key };
|
|
458
580
|
}
|
|
459
581
|
if (creds.org && (creds.org.id || creds.org.slug || creds.org.name)) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
582
|
+
out.org = {
|
|
583
|
+
...(creds.org.id ? { id: creds.org.id } : {}),
|
|
584
|
+
...(creds.org.slug ? { slug: creds.org.slug } : {}),
|
|
585
|
+
...(creds.org.name ? { name: creds.org.name } : {}),
|
|
586
|
+
};
|
|
464
587
|
}
|
|
465
588
|
if (creds.auth && (creds.auth.sessionToken || creds.auth.email)) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
589
|
+
out.auth = {
|
|
590
|
+
...(creds.auth.baseUrl ? { base_url: creds.auth.baseUrl } : {}),
|
|
591
|
+
...(creds.auth.sessionToken ? { session_token: creds.auth.sessionToken } : {}),
|
|
592
|
+
...(creds.auth.expiresAt ? { expires_at: creds.auth.expiresAt } : {}),
|
|
593
|
+
...(creds.auth.email ? { email: creds.auth.email } : {}),
|
|
594
|
+
};
|
|
471
595
|
}
|
|
472
596
|
|
|
473
597
|
const home = failproofaiHome();
|
|
@@ -477,7 +601,7 @@ export function writeCredentials(creds: FpCredentials): void {
|
|
|
477
601
|
} catch {
|
|
478
602
|
// Not fatal: the file's own 0600 is the primary protection.
|
|
479
603
|
}
|
|
480
|
-
writeFileSync(credentialsFile(),
|
|
604
|
+
writeFileSync(credentialsFile(), `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
|
|
481
605
|
try {
|
|
482
606
|
chmodSync(credentialsFile(), 0o600);
|
|
483
607
|
} catch {
|