failproofai 1.0.4-beta.7 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +4 -37
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -15
- package/.next/standalone/.next/server/app/settings/page.js +4 -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/chunks/{[externals]__0v1hz15._.js → [externals]__0-9ti7z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1nb206a._.js → [externals]__0u3wrvq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_0otft92._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +10 -10
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1fvfw59._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12hbpha._.js → [root-of-the-server]__09flfsx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kokc8z._.js → [root-of-the-server]__0n_lxhg._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dxf928._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n2txhk._.js → [root-of-the-server]__1ctpynv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_02yoeej._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0a0zlpr._.js → _04rbnaq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0s-77v5._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +14 -51
- package/.next/standalone/.next/server/chunks/ssr/{_07t3jol._.js → _1u8-lu2._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +69 -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/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +21 -65
- package/.next/standalone/.next/static/chunks/{0f1pznnc7zlub.js → 0-tyrpwuc3lla.js} +1 -1
- package/.next/standalone/.next/static/chunks/0ca7octhod0g2.js +69 -0
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{1csubx9gwhsf6.js → 1j6o9min_6qa9.js} +1 -1
- package/.next/standalone/.next/static/chunks/27cl8h-c2w7cd.js +1 -0
- package/.next/standalone/.next/static/chunks/{0jpd8dv2vk930.css → 29-iqnp9v_axt.css} +1 -1
- package/.next/standalone/.next/static/chunks/{3uyhmm01g64k4.css → 2k9f4tyv04809.css} +0 -1
- package/.next/standalone/.next/static/chunks/{0t6ea7d3bkmdv.js → 2lwipmv5etc8m.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ntdfr2552x-h.js → 2u8vh73tpw28_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{15uon66hpwbgg.js → 31u94yukw9fz-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2pwfrn6_mw-1u.js → 3p9_8x5ezce-9.js} +1 -1
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{083fk73jtv9a0.js → 3tjji451hrgc0.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-3s3c-u_u_l0u9.js → turbopack-3p73obe0xmrxm.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +27 -51
- package/.next/standalone/app/actions/get-scheduled-audit.ts +0 -5
- package/.next/standalone/app/actions/update-scheduled-audit.ts +57 -18
- package/.next/standalone/app/api/auth/status/route.ts +2 -3
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +34 -140
- package/.next/standalone/app/audit/_components/audit-poster.tsx +10 -29
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -6
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -4
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +5 -19
- package/.next/standalone/app/audit/_components/share-templates.ts +44 -110
- package/.next/standalone/app/audit/audit-styles.css +1 -132
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +45 -50
- package/.next/standalone/lib/auth/api-server-client.ts +5 -33
- package/.next/standalone/lib/claude-sessions.ts +27 -101
- package/.next/standalone/lib/codex-sessions.ts +4 -20
- package/.next/standalone/lib/copilot-sessions.ts +1 -32
- package/.next/standalone/lib/cursor-sessions.ts +5 -62
- package/.next/standalone/lib/sqlite-reader.ts +1 -78
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -1
- package/dist/cli.mjs +500 -1762
- package/dist/worker.mjs +16 -138
- package/lib/auth/api-server-client.ts +5 -33
- package/lib/claude-sessions.ts +27 -101
- package/lib/codex-sessions.ts +4 -20
- package/lib/copilot-sessions.ts +1 -32
- package/lib/cursor-sessions.ts +5 -62
- package/lib/sqlite-reader.ts +1 -78
- package/package.json +9 -9
- package/pi-extension/index.ts +5 -50
- package/src/audit/cache.ts +0 -6
- package/src/audit/cli-adapters/claude.ts +0 -13
- package/src/audit/cli-login.ts +1 -4
- package/src/audit/cli.ts +7 -157
- package/src/audit/harm-report.ts +4 -111
- package/src/audit/index.ts +15 -286
- package/src/audit/machine-store.ts +9 -8
- package/src/audit/redact-example.ts +15 -324
- package/src/audit/report-harm.ts +37 -33
- package/src/audit/report.ts +3 -6
- package/src/audit/schedule-cli.ts +33 -54
- package/src/audit/scoring.ts +0 -49
- package/src/audit/types.ts +0 -52
- package/src/hooks/builtin-policies.ts +2 -67
- package/src/hooks/configure-wizard.ts +0 -55
- package/src/hooks/fp-config.ts +48 -83
- package/src/hooks/fp-home.ts +0 -27
- package/src/hooks/integrations.ts +14 -51
- package/src/hooks/uninstall-cli.ts +2 -17
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-4ja6q._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cr3xu._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04px77e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egr5s1._.js +0 -6
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0i0l1di._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0r3c1_0._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17xnxlx._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ckpmkk._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +0 -3
- package/.next/standalone/.next/static/chunks/0divvxng208xo.js +0 -1
- package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +0 -1
- package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +0 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -104
- package/.next/standalone/app/audit/_components/leak-section.tsx +0 -192
- package/src/audit/desktop-notify.ts +0 -420
- package/src/audit/leak-fingerprint.ts +0 -198
- package/src/audit/leak-record.ts +0 -248
- package/src/audit/leak-scan.ts +0 -268
- package/src/audit/leak-store.ts +0 -220
- package/src/audit/macos-notifier.ts +0 -298
- package/src/hooks/notice.ts +0 -24
- /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_ssgManifest.js +0 -0
|
@@ -68,9 +68,13 @@ const dim = (s: string) => wrap(ANSI_DIM, s);
|
|
|
68
68
|
const bold = (s: string) => wrap(ANSI_BOLD, s);
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Turn scheduled audits on
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* Turn scheduled audits on, signing in first if needed.
|
|
72
|
+
*
|
|
73
|
+
* Scheduling and mailing are ONE decision — the reason to put a scan on a timer
|
|
74
|
+
* is to be told what it found — so this requires a session, exactly as the
|
|
75
|
+
* dashboard's `setAutoAuditAction` does. A timer set with nobody to tell is a
|
|
76
|
+
* switch that reads as on and produces nothing, discoverable only by noticing
|
|
77
|
+
* that no digest ever arrives.
|
|
74
78
|
*
|
|
75
79
|
* The interval is written and then RE-READ, so what is printed is what the
|
|
76
80
|
* config actually kept — `readIntervalDays` owns the 1..90 clamp and a second
|
|
@@ -104,13 +108,17 @@ export async function runScheduleOn(
|
|
|
104
108
|
if (bad) throw new ScheduleCliError(bad);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
const
|
|
108
|
-
? { user: readAuth()?.user ?? null, prompted: false }
|
|
109
|
-
: await ensureSignedIn(emailArg);
|
|
111
|
+
const { user, prompted } = await ensureSignedIn(emailArg);
|
|
110
112
|
|
|
111
113
|
const next = updateConfig({
|
|
114
|
+
// Stamped in the SAME call that sets `auto`, never separately: this records
|
|
115
|
+
// that a person completed a sign-in and read the disclosure printed below,
|
|
116
|
+
// and it is what `reportHarm` gates sending on. A machine that inherited
|
|
117
|
+
// `auto` from a release where it meant "scan locally" has no stamp and
|
|
118
|
+
// sends nothing until it comes through here.
|
|
112
119
|
audit: {
|
|
113
120
|
auto: true,
|
|
121
|
+
reportsConsentedAt: Date.now(),
|
|
114
122
|
...(days !== undefined ? { intervalDays: days } : {}),
|
|
115
123
|
},
|
|
116
124
|
});
|
|
@@ -126,14 +134,12 @@ export async function runScheduleOn(
|
|
|
126
134
|
// The list is the real payload from `report-harm.ts`: machine id, hostname,
|
|
127
135
|
// platform, the window bounds and the redacted examples.
|
|
128
136
|
const summary = [
|
|
129
|
-
`every ${interval} day${interval === 1 ? "" : "s"} ·
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
: "email is optional · add it with --email you@example.com",
|
|
133
|
-
"email contains only the most recent masked credential exposure",
|
|
137
|
+
`every ${interval} day${interval === 1 ? "" : "s"} · reports to ${user.email}`,
|
|
138
|
+
"you only hear from it when a scan finds something harmful",
|
|
139
|
+
"each report sends: finding counts, redacted example commands, this machine's name",
|
|
134
140
|
];
|
|
135
141
|
|
|
136
|
-
if (
|
|
142
|
+
if (prompted) {
|
|
137
143
|
// A sign-in just drew the frame, so the result continues it and the `└`
|
|
138
144
|
// closes both at once — rather than the frame ending and a loose line
|
|
139
145
|
// appearing underneath.
|
|
@@ -152,7 +158,7 @@ export async function runScheduleOn(
|
|
|
152
158
|
// exists to make visible.
|
|
153
159
|
warnIfDaemonWontRun();
|
|
154
160
|
|
|
155
|
-
if (
|
|
161
|
+
if (prompted) {
|
|
156
162
|
outro("failproofai audit --status · when the next scan is due");
|
|
157
163
|
} else {
|
|
158
164
|
process.stdout.write(dim(`\n failproofai audit --status when the next scan is due\n\n`));
|
|
@@ -211,22 +217,24 @@ export function runScheduleStatus(): void {
|
|
|
211
217
|
|
|
212
218
|
// A session whose refresh window has closed cannot mint another access token,
|
|
213
219
|
// so it is a destination in name only. Showing the address for one would tell
|
|
214
|
-
// somebody their
|
|
220
|
+
// somebody their digests are going somewhere they are not.
|
|
215
221
|
const live = auth && auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
|
|
216
|
-
// Beside the schedule, because they are the two halves of "what does this
|
|
217
|
-
// machine do on its own" — one decides whether it scans, the other whether it
|
|
218
|
-
// is allowed to interrupt you about what it found.
|
|
219
|
-
detail.push([
|
|
220
|
-
"leak notifications",
|
|
221
|
-
config.audit.notify ? green("on") : dim("off (failproofai audit --notify)"),
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
222
|
detail.push(["reports to", live ? live.user.email : dim("— signed out")]);
|
|
225
223
|
if (on && !live) {
|
|
226
224
|
// The state the reporter surfaces as "signed-out". Named here for the same
|
|
227
|
-
// reason the settings panel names it: the scans
|
|
228
|
-
//
|
|
229
|
-
detail.push(["", pink("scans
|
|
225
|
+
// reason the settings panel names it: the scans keep running, so silence
|
|
226
|
+
// about the digests would look like the feature failing.
|
|
227
|
+
detail.push(["", pink("scans continue; digests are paused until you sign in")]);
|
|
228
|
+
} else if (on && config.audit.reportsConsentedAt === undefined) {
|
|
229
|
+
// Signed in, scheduled, and still not sending: this machine set `audit.auto`
|
|
230
|
+
// when it only meant "scan locally", so nothing has consented to the digest
|
|
231
|
+
// leaving the box. Without this row the status screen would show a healthy
|
|
232
|
+
// schedule and a live address and still mail nothing, with no explanation
|
|
233
|
+
// anywhere the user can see.
|
|
234
|
+
detail.push([
|
|
235
|
+
"",
|
|
236
|
+
pink("scans continue; digests need a fresh opt-in — run `--schedule` to turn them on"),
|
|
237
|
+
]);
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
detail.push(["daemon", describeDaemon(daemon)]);
|
|
@@ -338,32 +346,3 @@ export function asScheduleError(err: unknown): never {
|
|
|
338
346
|
}
|
|
339
347
|
throw err;
|
|
340
348
|
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Turn the desktop banner on or off.
|
|
344
|
-
*
|
|
345
|
-
* A separate command from `--schedule` rather than a flag on it, because they
|
|
346
|
-
* are separate decisions with different lifetimes: somebody who wants the
|
|
347
|
-
* weekly scan and not the banner should not have to re-state their schedule to
|
|
348
|
-
* say so, and somebody silencing the banner in an annoyed moment should not
|
|
349
|
-
* discover they also turned off the scan.
|
|
350
|
-
*
|
|
351
|
-
* Writes the same `audit.notify` key the dashboard toggle writes and the audit
|
|
352
|
-
* child reads, so the two surfaces cannot disagree.
|
|
353
|
-
*/
|
|
354
|
-
export function runNotifyToggle(enable: boolean): void {
|
|
355
|
-
try {
|
|
356
|
-
updateConfig({ audit: { notify: enable } });
|
|
357
|
-
} catch (err) {
|
|
358
|
-
process.stderr.write(
|
|
359
|
-
`Could not update the setting: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
360
|
-
);
|
|
361
|
-
process.exit(1);
|
|
362
|
-
}
|
|
363
|
-
process.stdout.write(
|
|
364
|
-
enable
|
|
365
|
-
? `\n${green("on")} leak notifications — this machine will tell you when a scan finds a credential.\n\n`
|
|
366
|
-
: `\n${dim("off")} leak notifications — scans continue, and findings still appear in ` +
|
|
367
|
-
`\`failproofai audit\` and the emailed digest.\n\n`,
|
|
368
|
-
);
|
|
369
|
-
}
|
package/src/audit/scoring.ts
CHANGED
|
@@ -1,55 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Score derivation for the audit dashboard.
|
|
3
3
|
*
|
|
4
|
-
* ## SWITCHED OFF — nothing in the product calls any of this (2026-09-07)
|
|
5
|
-
*
|
|
6
|
-
* Every function below is intact and still covered by
|
|
7
|
-
* `__tests__/audit/scoring.test.ts`, deliberately: the score is commented out at
|
|
8
|
-
* its call sites, not deleted, so restoring it is un-commenting rather than
|
|
9
|
-
* rewriting. The call sites, all commented and cross-referenced back to here:
|
|
10
|
-
* `audit-dashboard.tsx` (the four `useMemo`s, the poster/how-to-improve/
|
|
11
|
-
* come-back-better props, and the `score`/`grade` PostHog properties),
|
|
12
|
-
* `audit-poster.tsx` (the `.poster-score` block, the PNG filename, the share
|
|
13
|
-
* payload), `how-to-improve-section.tsx` (the projected sub-heading),
|
|
14
|
-
* `share-templates.ts` (19 of 20 templates interpolated the number).
|
|
15
|
-
*
|
|
16
|
-
* ### Why
|
|
17
|
-
*
|
|
18
|
-
* Two reasons, one product and one correctness.
|
|
19
|
-
*
|
|
20
|
-
* The product reason is the owner's: the audit tried to do too many things
|
|
21
|
-
* shallowly, and grading a machine 0-100 with a letter tier was not the half
|
|
22
|
-
* worth keeping.
|
|
23
|
-
*
|
|
24
|
-
* The correctness reason is worse and is the one to read before switching it
|
|
25
|
-
* back on. **`deriveScore` never reads `enabledInConfig`** — only
|
|
26
|
-
* `projectedScore` does, at the filter below. So the number the dashboard
|
|
27
|
-
* rendered as "enable all N → projected {score}" was undeliverable by
|
|
28
|
-
* construction: a user who took the prescription, enabled the policies and
|
|
29
|
-
* re-ran got the SAME score with the projection gone. Advertised up to +22,
|
|
30
|
-
* delivered 0. There is also no time window, so a hit denied today is counted
|
|
31
|
-
* against the machine forever.
|
|
32
|
-
*
|
|
33
|
-
* Two further things measured at the same time, neither of them the score's
|
|
34
|
-
* fault but both worth knowing if this comes back:
|
|
35
|
-
*
|
|
36
|
-
* - `penaltyFor` consumes `row.hits`, i.e. OCCURRENCES. Occurrences amplify
|
|
37
|
-
* against distinct values by 10.7x-41x and non-uniformly by class, so the
|
|
38
|
-
* sum does not preserve rank order between classes. Fixing that needs a
|
|
39
|
-
* per-distinct-value carrier, which `AuditCount` does not have.
|
|
40
|
-
* - `syntheticRank`, `scoreToPercentile` and `COHORT_SIZE` had already lost
|
|
41
|
-
* their last caller before this change — the cohort they describe does not
|
|
42
|
-
* exist. They are dead either way.
|
|
43
|
-
*
|
|
44
|
-
* ### To restore
|
|
45
|
-
*
|
|
46
|
-
* Un-comment the call sites listed above (each carries its original inline) and
|
|
47
|
-
* put the score-bearing share copy back from the block at the bottom of
|
|
48
|
-
* `share-templates.ts`. Make `score`/`grade` required again in `ShareCtx` and in
|
|
49
|
-
* `audit-poster.tsx`'s `Props`, and the compiler will walk you through the rest.
|
|
50
|
-
* Fix the `enabledInConfig` defect first, or it will be advertising the same
|
|
51
|
-
* undeliverable number.
|
|
52
|
-
*
|
|
53
4
|
* Score is on 0-100, mapped to letter grades that anchor the leaderboard
|
|
54
5
|
* + tier prose. The thresholds match the reference design (assets/audit):
|
|
55
6
|
*
|
package/src/audit/types.ts
CHANGED
|
@@ -45,9 +45,6 @@ export interface TranscriptMetadata {
|
|
|
45
45
|
mtimeMs: number;
|
|
46
46
|
/** Byte size of the transcript file. Used for cache invalidation. */
|
|
47
47
|
sizeBytes: number;
|
|
48
|
-
/** Optional host-authored session purpose. Used only to distinguish
|
|
49
|
-
* deliberate credential-scanner research from evidence of a leak. */
|
|
50
|
-
sessionDescription?: string;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/** Per-session detector state. Detectors mutate this freely. */
|
|
@@ -122,27 +119,6 @@ export interface AuditCount {
|
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
/** Per-transcript scan result (also the per-transcript cache value). */
|
|
125
|
-
/** One credential sighting, already fingerprinted. */
|
|
126
|
-
export interface TranscriptLeak {
|
|
127
|
-
/** Salted, machine-local id for the DISTINCT value. */
|
|
128
|
-
id: string;
|
|
129
|
-
/** Masked rendering + class + length. Never the value. */
|
|
130
|
-
fingerprint: import("./leak-fingerprint").LeakFingerprint;
|
|
131
|
-
/** Identifier it was assigned to, when it came from an assignment. */
|
|
132
|
-
name: string | null;
|
|
133
|
-
/** Rule that found it. */
|
|
134
|
-
rule: string;
|
|
135
|
-
/** Whether a vendor shape matched — gates whether an alert may name a console. */
|
|
136
|
-
shaped: boolean;
|
|
137
|
-
timestamp: string;
|
|
138
|
-
cwd: string;
|
|
139
|
-
toolName: string;
|
|
140
|
-
/** Sent by the agent, or received by it. Different exposures, different fixes. */
|
|
141
|
-
direction: "input" | "result";
|
|
142
|
-
/** Home-shortened path the value was read from or written to, when there was one. */
|
|
143
|
-
path: string | null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
122
|
export interface TranscriptAuditResult {
|
|
147
123
|
/** Cache key: matches TranscriptMetadata.transcriptPath. */
|
|
148
124
|
transcriptPath: string;
|
|
@@ -164,20 +140,6 @@ export interface TranscriptAuditResult {
|
|
|
164
140
|
hitsByName: Record<string, number>;
|
|
165
141
|
/** Up to 3 example commands per policy/detector (later coalesced upstream). */
|
|
166
142
|
examplesByName: Record<string, { timestamp: string; cwd: string; example: string }[]>;
|
|
167
|
-
/**
|
|
168
|
-
* Credentials found in this transcript, one entry per SIGHTING.
|
|
169
|
-
*
|
|
170
|
-
* Deliberately separate from `hitsByName`, which counts by RULE and cannot
|
|
171
|
-
* enumerate distinct values. These carry the fingerprint rather than the
|
|
172
|
-
* credential — the raw value never leaves `scanOneTranscript`.
|
|
173
|
-
*
|
|
174
|
-
* Optional so a cached result written before this field existed still parses;
|
|
175
|
-
* absent reads as "this transcript predates leak scanning", not "clean".
|
|
176
|
-
*/
|
|
177
|
-
leaks?: TranscriptLeak[];
|
|
178
|
-
/** The session was explicitly generating/researching credential fixtures,
|
|
179
|
-
* so its matches are examples rather than evidence of user leakage. */
|
|
180
|
-
leakScanSuppressed?: "credential-research";
|
|
181
143
|
/** First/last timestamp per name. */
|
|
182
144
|
rangeByName: Record<string, { first: string; last: string }>;
|
|
183
145
|
}
|
|
@@ -217,20 +179,6 @@ export interface AuditResult {
|
|
|
217
179
|
* Lets the dashboard answer "is this policy already on?" for
|
|
218
180
|
* detector-mapped policies that may not have hit during this audit. */
|
|
219
181
|
enabledBuiltinNames: string[];
|
|
220
|
-
/** Ids of credentials encountered by this scan, including ones already in
|
|
221
|
-
* the persistent leak record. Scheduled desktop notifications key on this
|
|
222
|
-
* list so every completed scan that still sees a leak can notify; the
|
|
223
|
-
* narrower `newLeakIds` remains for first-seen-only surfaces. */
|
|
224
|
-
leakIds?: string[];
|
|
225
|
-
/**
|
|
226
|
-
* Ids of credentials seen for the FIRST time in this scan.
|
|
227
|
-
*
|
|
228
|
-
* What a notice keys on. Not "findings this scan saw" — a key already in the
|
|
229
|
-
* record must not re-alert however many fresh sightings it picks up, and one
|
|
230
|
-
* seen for the first time must alert even though its rule has fired before.
|
|
231
|
-
* Optional so a cached result predating leak scanning still parses.
|
|
232
|
-
*/
|
|
233
|
-
newLeakIds?: string[];
|
|
234
182
|
}
|
|
235
183
|
|
|
236
184
|
/** CLI-supplied options for `runAudit()`. Set by `bin/failproofai.mjs`. */
|
|
@@ -126,80 +126,15 @@ const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
|
|
|
126
126
|
|
|
127
127
|
// sanitizeApiKeys
|
|
128
128
|
const API_KEY_PATTERNS: Array<[RegExp, string]> = [
|
|
129
|
-
// ── Anthropic ── prefix doc-verified: platform.claude.com says a key
|
|
130
|
-
// "starts with `sk-ant-`" and its own example is `sk-ant-api03-...`.
|
|
131
|
-
[/sk-ant-api03-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
132
|
-
// Minted by the Claude CLI's OAuth flow and billed against a Pro/Max
|
|
133
|
-
// subscription rather than the API account — a different thing to revoke.
|
|
134
|
-
[/sk-ant-oat01-[A-Za-z0-9\-_]{20,}/, "Anthropic OAuth token"],
|
|
135
129
|
[/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
136
|
-
|
|
137
|
-
// ── OpenAI ── the body charset EXCLUDES `-` and `_` on purpose: with them,
|
|
138
|
-
// this rule's run would bridge the hyphen in `sk-proj-` and report every
|
|
139
|
-
// project key a second time under the wrong label.
|
|
140
130
|
[/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
|
|
141
|
-
[/
|
|
142
|
-
// Highest blast radius in the family: it authenticates the organisation
|
|
143
|
-
// admin surface, and lives at a DIFFERENT console from the other keys.
|
|
144
|
-
[/(?<!\w)sk-admin-[A-Za-z0-9\-_]{20,}/, "OpenAI admin key"],
|
|
145
|
-
|
|
146
|
-
// ── OpenAI-compatible gateways ──
|
|
147
|
-
[/sk-or-v1-[A-Za-z0-9\-_]{20,}/, "OpenRouter API key"],
|
|
148
|
-
// A BARE `sk-` CANNOT BE ATTRIBUTED, and calling it OpenAI is what this rule
|
|
149
|
-
// used to do. LiteLLM's own docs say its virtual keys "must start with sk-",
|
|
150
|
-
// and DeepSeek plus every other OpenAI-compatible proxy mints the same shape
|
|
151
|
-
// — so on a machine running a gateway (this one does) the old label sent the
|
|
152
|
-
// user to the wrong console to revoke a key that was never OpenAI's. The
|
|
153
|
-
// honest label names the family, not a vendor.
|
|
154
|
-
[/sk-[A-Za-z0-9]{20,}/, "OpenAI-compatible key (issuer unknown)"],
|
|
155
|
-
|
|
156
|
-
// ── GitHub ── all five documented types. github.blog's token-format post
|
|
157
|
-
// names ghp/gho/ghu/ghs/ghr; only two of them shipped before.
|
|
158
|
-
[/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
|
|
131
|
+
[/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
|
|
159
132
|
[/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
|
|
160
|
-
[/
|
|
161
|
-
[/ghu_[A-Za-z0-9]{36}/, "GitHub user-to-server token"],
|
|
162
|
-
[/ghs_[A-Za-z0-9]{36}/, "GitHub server-to-server token"],
|
|
163
|
-
[/ghr_[A-Za-z0-9]{36}/, "GitHub refresh token"],
|
|
164
|
-
[/glpat-[A-Za-z0-9\-_]{20,}/, "GitLab personal access token"],
|
|
165
|
-
|
|
166
|
-
// ── AWS ── the ID is the PUBLIC half; the paired 40-char secret is the
|
|
167
|
-
// credential. Reported anyway because an exposed ID names the account and is
|
|
168
|
-
// what a user searches their console for. `ASIA` (temporary STS) was missing.
|
|
133
|
+
[/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
|
|
169
134
|
[/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
|
|
170
|
-
[/ASIA[A-Z0-9]{16}/, "AWS temporary access key ID"],
|
|
171
|
-
|
|
172
|
-
// ── Payments ── Stripe documents `pk_*` as "Safe to expose: Yes", so only
|
|
173
|
-
// the secret and restricted halves appear here.
|
|
174
135
|
[/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
|
|
175
136
|
[/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
|
|
176
|
-
[/rk_live_[A-Za-z0-9]{24,}/, "Stripe restricted key"],
|
|
177
|
-
|
|
178
|
-
// ── Google ── `AIza` also covers Firebase Web and Maps browser keys, which
|
|
179
|
-
// are public by design, so the label stays non-committal about severity.
|
|
180
137
|
[/AIza[0-9A-Za-z\-_]{35}/, "Google API key"],
|
|
181
|
-
|
|
182
|
-
// ── Messaging ── the Telegram bot token was matched by ZERO of the shipped
|
|
183
|
-
// patterns and was found only because a phone-number regex tripped over it;
|
|
184
|
-
// two live-looking ones sit in this machine's corpus right now. Slack's
|
|
185
|
-
// prefixes were in the Rust collector but not here — one of nine such
|
|
186
|
-
// divergences between the two engines.
|
|
187
|
-
[/\d{8,10}:[A-Za-z0-9_-]{35}/, "Telegram bot token"],
|
|
188
|
-
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
|
|
189
|
-
[/xapp-[0-9]-[A-Za-z0-9-]{10,}/, "Slack app-level token"],
|
|
190
|
-
|
|
191
|
-
// ── Model hosts ──
|
|
192
|
-
[/hf_[A-Za-z0-9]{30,}/, "Hugging Face token"],
|
|
193
|
-
[/gsk_[A-Za-z0-9]{40,}/, "Groq API key"],
|
|
194
|
-
[/r8_[A-Za-z0-9]{30,}/, "Replicate token"],
|
|
195
|
-
[/xai-[A-Za-z0-9]{40,}/, "xAI API key"],
|
|
196
|
-
|
|
197
|
-
// ── Registries ──
|
|
198
|
-
[/npm_[A-Za-z0-9]{36}/, "npm access token"],
|
|
199
|
-
[/dop_v1_[a-f0-9]{64}/, "DigitalOcean token"],
|
|
200
|
-
[/shpat_[a-fA-F0-9]{32}/, "Shopify access token"],
|
|
201
|
-
[/sb_secret_[A-Za-z0-9\-_]{20,}/, "Supabase secret key"],
|
|
202
|
-
[/sbp_[a-f0-9]{40}/, "Supabase personal token"],
|
|
203
138
|
];
|
|
204
139
|
|
|
205
140
|
// sanitizeConnectionStrings
|
|
@@ -74,7 +74,6 @@ import {
|
|
|
74
74
|
probeDaemonEndToEnd,
|
|
75
75
|
uninstallDaemonService,
|
|
76
76
|
} from "./daemon-service";
|
|
77
|
-
import { installMacNotifier } from "../audit/macos-notifier";
|
|
78
77
|
import { hookLogWarn } from "./hook-logger";
|
|
79
78
|
import {
|
|
80
79
|
readCloudCredentials,
|
|
@@ -106,7 +105,6 @@ import {
|
|
|
106
105
|
type RetryProbe,
|
|
107
106
|
} from "./onboarding-attempt";
|
|
108
107
|
import { acquireOnboardingLock } from "./onboarding-lock";
|
|
109
|
-
import { readConfigRaw, updateConfig } from "./fp-config";
|
|
110
108
|
|
|
111
109
|
export interface WizardIO {
|
|
112
110
|
stdin?: TTYIn;
|
|
@@ -756,19 +754,6 @@ export async function runConfigureWizard(
|
|
|
756
754
|
// question, and no flag can supply it.
|
|
757
755
|
const preAnswered = Boolean(answers.token);
|
|
758
756
|
|
|
759
|
-
// Capture the preference BEFORE the daemon setup writes config.json. A fresh
|
|
760
|
-
// machine reaches `setDaemonConfigured(true)` with no config file; that helper
|
|
761
|
-
// necessarily materialises the conservative no-file default (`auto: false`).
|
|
762
|
-
// Looking after that write cannot distinguish "the user switched it off"
|
|
763
|
-
// from "setup just wrote its fallback", which is how new installs ended up
|
|
764
|
-
// with a seven-day scheduler that was explicitly disabled.
|
|
765
|
-
const initialAudit = readConfigRaw().raw.audit;
|
|
766
|
-
const hadExplicitAuditAuto =
|
|
767
|
-
!!initialAudit &&
|
|
768
|
-
typeof initialAudit === "object" &&
|
|
769
|
-
!Array.isArray(initialAudit) &&
|
|
770
|
-
typeof (initialAudit as Record<string, unknown>).auto === "boolean";
|
|
771
|
-
|
|
772
757
|
|
|
773
758
|
// Running the wizard itself under sudo configures the WRONG ACCOUNT, and
|
|
774
759
|
// does it silently: homedir() becomes /root, so the hooks land in root's
|
|
@@ -1453,50 +1438,10 @@ export async function runConfigureWizard(
|
|
|
1453
1438
|
// binary filename are both derived from it.
|
|
1454
1439
|
if (daemonInstalled) {
|
|
1455
1440
|
setDaemonConfigured(true, cliVersion);
|
|
1456
|
-
// A completed setup is the default opt-in to the LOCAL seven-day scan. It
|
|
1457
|
-
// does not imply an email address or make a network request: reporting is
|
|
1458
|
-
// independently gated by the signed-in audit identity. Reconfiguration
|
|
1459
|
-
// preserves an explicit `auto: false`, so the user's off switch remains an
|
|
1460
|
-
// off switch instead of being undone whenever they repair or upgrade the
|
|
1461
|
-
// daemon.
|
|
1462
|
-
if (!hadExplicitAuditAuto) {
|
|
1463
|
-
try {
|
|
1464
|
-
updateConfig({ audit: { auto: true } });
|
|
1465
|
-
} catch {
|
|
1466
|
-
// The daemon and hooks are already installed. Keep setup successful;
|
|
1467
|
-
// the status screen will expose that scheduling could not be persisted.
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
1441
|
// Only now — the unit points at the new binary, so older ones are no
|
|
1471
1442
|
// longer referenced by anything. Keeps the previous version for an
|
|
1472
1443
|
// offline rollback.
|
|
1473
1444
|
pruneOldDaemonBinaries();
|
|
1474
|
-
|
|
1475
|
-
// The delivery path for what the scheduled audit finds. macOS only, and a
|
|
1476
|
-
// no-op everywhere else: a Linux box's audit talks to the session bus
|
|
1477
|
-
// itself, while on a Mac the daemon runs outside the GUI session and cannot
|
|
1478
|
-
// reach Notification Center at all — see `audit/macos-notifier.ts`.
|
|
1479
|
-
//
|
|
1480
|
-
// Installed here, with the daemon, because it exists only to deliver what
|
|
1481
|
-
// the daemon's scheduled scan produces, and removed alongside it by
|
|
1482
|
-
// `uninstall`. Silent, and not a step in the wizard: it needs no password,
|
|
1483
|
-
// asks nothing, and narrating it would be an apology for the delivery half
|
|
1484
|
-
// of a feature the user just switched on. What they are actually asked —
|
|
1485
|
-
// does this machine scan, and may it interrupt — are real settings, and
|
|
1486
|
-
// both are theirs to change afterwards.
|
|
1487
|
-
//
|
|
1488
|
-
// Its failure is not the wizard's failure. A Mac where `osacompile` is
|
|
1489
|
-
// missing or launchd refuses the job must still finish installing its
|
|
1490
|
-
// daemon, its hooks and its policies, which are the parts that enforce
|
|
1491
|
-
// anything; the notice in-CLI still reaches them either way.
|
|
1492
|
-
const notifier = installMacNotifier();
|
|
1493
|
-
if (process.platform === "darwin" && !notifier.installed) {
|
|
1494
|
-
hookLogWarn(`macOS notification helper was not installed: ${notifier.reason ?? "unknown error"}`);
|
|
1495
|
-
stdout.write(
|
|
1496
|
-
"\nSystem notifications could not be installed. Scheduled scans and email alerts\n" +
|
|
1497
|
-
"still work; re-run `failproofai config` after checking the local LaunchAgent.\n\n",
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
1445
|
}
|
|
1501
1446
|
|
|
1502
1447
|
// Telemetry runs concurrently with the install (never rejects, 5s-bounded) so
|
package/src/hooks/fp-config.ts
CHANGED
|
@@ -308,65 +308,49 @@ export interface FpConfig {
|
|
|
308
308
|
};
|
|
309
309
|
audit: {
|
|
310
310
|
/**
|
|
311
|
-
* Scan this machine on a schedule
|
|
312
|
-
*
|
|
313
|
-
* email identity adds email alerts without changing the timer.
|
|
311
|
+
* Scan this machine on a schedule, and mail a digest when a scan finds
|
|
312
|
+
* something harmful.
|
|
314
313
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
314
|
+
* ONE switch, not two. An earlier revision split this into `auto` (scan
|
|
315
|
+
* locally, no account) and `email_enabled` (mail me, sign in), and the two
|
|
316
|
+
* could only ever disagree — a machine scanning on a timer with nothing to
|
|
317
|
+
* report it to is a feature that looks on and does nothing visible. The
|
|
318
|
+
* reason to put a scan on a timer is to be TOLD, so scheduling and mailing
|
|
319
|
+
* are the same decision and take the same switch.
|
|
320
320
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
321
|
+
* A signed-out machine with this on is therefore a real, expected state
|
|
322
|
+
* rather than a contradiction: it keeps scanning and keeps its local
|
|
323
|
+
* dashboard current, and the UI says "signed out" until somebody signs back
|
|
324
|
+
* in. Auth gates SETTING it up, never the machine's ongoing work — a
|
|
325
|
+
* refresh token expiring must not silently switch off a background feature
|
|
326
|
+
* somebody configured months ago.
|
|
324
327
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* feature that, for most users, never runs. Setup is where the machine's
|
|
330
|
-
* owner is told what it does, so completing setup is the opinion.
|
|
331
|
-
*
|
|
332
|
-
* Hence the split. Once `config.json` exists AND carries an `audit` table,
|
|
333
|
-
* this is on unless it says exactly `false`. Absent file, unparseable JSON,
|
|
334
|
-
* or no `audit` table at all still read as OFF, because those are the three
|
|
335
|
-
* ways of not knowing, and "we could not tell" must never turn a transcript
|
|
336
|
-
* scan on. `crates/failproofaid/src/audit_lane.rs` makes the identical
|
|
337
|
-
* distinction over the identical bytes — the daemon and this reader must
|
|
338
|
-
* agree, or the settings page describes a machine that is doing something
|
|
339
|
-
* else.
|
|
340
|
-
*
|
|
341
|
-
* Turning it on sends nothing remotely on its own. Email requires a valid
|
|
342
|
-
* identity in `audit/session.json`; without one, scans and notifications
|
|
343
|
-
* remain fully local.
|
|
328
|
+
* OFF by default, and the asymmetry with `telemetry.enabled` above is
|
|
329
|
+
* deliberate: the audit reads the CONTENTS of every session transcript on
|
|
330
|
+
* disk — prompts, file contents, command output — so nothing scans on a
|
|
331
|
+
* timer until somebody asks for it.
|
|
344
332
|
*/
|
|
345
333
|
auto: boolean;
|
|
346
334
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* Separate from `auto` because they answer different questions: `auto` is
|
|
350
|
-
* whether the machine scans, this is whether it interrupts. Somebody who
|
|
351
|
-
* wants the scan and not the banner is a coherent person, and without this
|
|
352
|
-
* their only way to stop the banner is to stop the scan.
|
|
335
|
+
* When this machine's owner agreed that a scheduled scan may send what it
|
|
336
|
+
* finds off the box, as epoch ms. Absent means they never did.
|
|
353
337
|
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
338
|
+
* This does NOT reintroduce the second switch the comment above rejects,
|
|
339
|
+
* and it is never drawn as one. `auto` is what a person sets; this is a
|
|
340
|
+
* record of the disclosure they were shown when they set it. The two cannot
|
|
341
|
+
* drift, because every path that turns `auto` on stamps this in the same
|
|
342
|
+
* call and nothing stamps it alone.
|
|
359
343
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
344
|
+
* It exists because `auto` CHANGED MEANING. Through 1.0.0 it meant "scan
|
|
345
|
+
* this machine on a timer" and nothing more — no account, no network, and
|
|
346
|
+
* the toggle that wrote it said as much in as many words. Harm digests gave
|
|
347
|
+
* the same stored bit a second job: uploading redacted transcript excerpts
|
|
348
|
+
* to the api-server and mailing them. Without a separate record, every
|
|
349
|
+
* machine that opted into the old meaning would have started sending on
|
|
350
|
+
* upgrade, having agreed to nothing of the kind, with the only notice a
|
|
351
|
+
* line in the journal. Gating on the stamp rather than the switch is what
|
|
352
|
+
* keeps that upgrade silent in the safe direction: those machines keep
|
|
353
|
+
* scanning locally and send nothing until somebody opts in again.
|
|
370
354
|
*/
|
|
371
355
|
reportsConsentedAt?: number;
|
|
372
356
|
/** Days between scheduled runs. Wall clock, so it survives suspend. */
|
|
@@ -418,9 +402,7 @@ export const DEFAULT_CONFIG: FpConfig = {
|
|
|
418
402
|
environment: "local",
|
|
419
403
|
},
|
|
420
404
|
telemetry: { enabled: true },
|
|
421
|
-
|
|
422
|
-
// why a machine that HAS a config reads the other way.
|
|
423
|
-
audit: { auto: false, notify: true, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
|
|
405
|
+
audit: { auto: false, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
|
|
424
406
|
};
|
|
425
407
|
|
|
426
408
|
/**
|
|
@@ -513,11 +495,7 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
|
513
495
|
const daemon = (parsed.daemon ?? {}) as Record<string, unknown>;
|
|
514
496
|
const collector = (parsed.collector ?? {}) as Record<string, unknown>;
|
|
515
497
|
const telemetry = (parsed.telemetry ?? {}) as Record<string, unknown>;
|
|
516
|
-
|
|
517
|
-
// table is absent than when it is present-but-silent. See the field's doc.
|
|
518
|
-
const auditPresent =
|
|
519
|
-
!!parsed.audit && typeof parsed.audit === "object" && !Array.isArray(parsed.audit);
|
|
520
|
-
const audit = (auditPresent ? parsed.audit : {}) as Record<string, unknown>;
|
|
498
|
+
const audit = (parsed.audit ?? {}) as Record<string, unknown>;
|
|
521
499
|
return {
|
|
522
500
|
// Anything unrecognised reads as `oss`. The failure direction matters: a
|
|
523
501
|
// corrupt config must not be able to turn cloud reporting ON.
|
|
@@ -547,23 +525,15 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
|
547
525
|
// reads as on — the shipped default, and what a config with no
|
|
548
526
|
// [telemetry] block at all means.
|
|
549
527
|
telemetry: { enabled: telemetry.enabled !== false },
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
// and an absence must not start reading every transcript on disk. Same
|
|
555
|
-
// three-way split as `audit_lane.rs`, over the same file.
|
|
528
|
+
// Mirror image of telemetry above: only an explicit `true` switches the
|
|
529
|
+
// scheduled scan on. Absent, misspelled, or `"yes"` all read as off,
|
|
530
|
+
// because the failure direction here is a machine that starts reading
|
|
531
|
+
// every transcript it can find on a timer nobody set.
|
|
556
532
|
audit: {
|
|
557
|
-
auto:
|
|
558
|
-
// Default ON, off only on an explicit `false` — a notification the user
|
|
559
|
-
// never sees is the same as no detection at all. Ungated on
|
|
560
|
-
// `auditPresent`, unlike `auto`: this is read at the moment a
|
|
561
|
-
// notification is about to fire, which means a scan already ran and the
|
|
562
|
-
// question is only whether to speak.
|
|
563
|
-
notify: audit.notify !== false,
|
|
533
|
+
auto: audit.auto === true,
|
|
564
534
|
intervalDays: readIntervalDays(audit.interval_days),
|
|
565
|
-
//
|
|
566
|
-
//
|
|
535
|
+
// A finite number or nothing. A garbage value reads as absent, which is
|
|
536
|
+
// the direction that sends nothing.
|
|
567
537
|
reportsConsentedAt:
|
|
568
538
|
typeof audit.reports_consented_at === "number" &&
|
|
569
539
|
Number.isFinite(audit.reports_consented_at)
|
|
@@ -614,7 +584,6 @@ const OWNED_CONFIG_KEYS: readonly (readonly string[])[] = [
|
|
|
614
584
|
["collector", "sources", "*", "extra_paths"],
|
|
615
585
|
["telemetry", "enabled"],
|
|
616
586
|
["audit", "auto"],
|
|
617
|
-
["audit", "notify"],
|
|
618
587
|
["audit", "interval_days"],
|
|
619
588
|
["audit", "reports_consented_at"],
|
|
620
589
|
];
|
|
@@ -713,20 +682,16 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
|
|
|
713
682
|
...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
|
|
714
683
|
// Written ALWAYS, unlike telemetry directly above — the two are opposites
|
|
715
684
|
// on purpose. Telemetry ships on and is deliberately not advertised;
|
|
716
|
-
// the scheduled audit
|
|
717
|
-
// and a switch
|
|
685
|
+
// the scheduled audit ships off and is meant to be FOUND, and a switch
|
|
718
686
|
// nobody can see is the same as a switch that does not exist. Emitting both
|
|
719
687
|
// keys unconditionally also makes "a user's setting survives a rewrite"
|
|
720
688
|
// total rather than conditional.
|
|
721
689
|
audit: {
|
|
722
690
|
auto: config.audit.auto,
|
|
723
|
-
// Emitted unconditionally, like `auto`. A switch that only appears in the
|
|
724
|
-
// file once you have used it is a switch nobody discovers, and this one
|
|
725
|
-
// exists to be found by somebody the banner annoyed.
|
|
726
|
-
notify: config.audit.notify,
|
|
727
691
|
interval_days: config.audit.intervalDays,
|
|
728
|
-
//
|
|
729
|
-
//
|
|
692
|
+
// Written only once there IS consent, so an untouched machine's config
|
|
693
|
+
// does not grow a key implying it was asked. It is in
|
|
694
|
+
// `OWNED_CONFIG_KEYS`, so omitting it here really removes it.
|
|
730
695
|
...(config.audit.reportsConsentedAt === undefined
|
|
731
696
|
? {}
|
|
732
697
|
: { reports_consented_at: config.audit.reportsConsentedAt }),
|