failproofai 1.0.4-beta.4 → 1.0.4-beta.7
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 -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/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.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/server-reference-manifest.json +5 -5
- package/.next/standalone/.next/server/app/audit/page.js +3 -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 +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/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/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/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/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/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/server-reference-manifest.json +15 -4
- 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/[root-of-the-server]__0--lkk6._.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]__0q0qzx1._.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]__1le9jqc._.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/_1ek68ln._.js +11 -11
- package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
- 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]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.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]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yl9wb5._.js → [root-of-the-server]__12hbpha._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__17xnxlx._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__1ckpmkk._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00-s7h8._.js → [root-of-the-server]__1fvfw59._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0guxnh-._.js → _0a0zlpr._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -14
- 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 +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/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.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-home_ts_0je3xkv._.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 +33 -22
- package/.next/standalone/.next/static/chunks/{17tfki9x5rd_7.js → 083fk73jtv9a0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1i-i0pxg9q_8y.js → 0divvxng208xo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{38dqur43qthrl.js → 0f1pznnc7zlub.js} +1 -1
- package/.next/standalone/.next/static/chunks/{323cyb3a2s06y.js → 0ntdfr2552x-h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0po8n4tyotjf2.js → 0t6ea7d3bkmdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{364cqbpeg1y5i.js → 15uon66hpwbgg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42gwu2bsz4ske.js → 1csubx9gwhsf6.js} +2 -2
- package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +1 -0
- package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +1 -0
- package/.next/standalone/.next/static/chunks/{2bxr2_2h4kaqq.js → 2pwfrn6_mw-1u.js} +1 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -5
- package/.next/standalone/app/actions/get-scheduled-audit.ts +5 -0
- package/.next/standalone/app/actions/update-scheduled-audit.ts +18 -57
- package/.next/standalone/app/api/auth/status/route.ts +3 -2
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +1 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -1
- package/.next/standalone/app/audit/_components/leak-section.tsx +1 -1
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +50 -45
- package/.next/standalone/lib/auth/api-server-client.ts +6 -6
- package/.next/standalone/lib/codex-sessions.ts +20 -4
- package/.next/standalone/lib/copilot-sessions.ts +32 -1
- package/.next/standalone/lib/cursor-sessions.ts +62 -5
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +991 -1101
- package/dist/worker.mjs +175 -382
- package/lib/auth/api-server-client.ts +6 -6
- package/lib/codex-sessions.ts +20 -4
- package/lib/copilot-sessions.ts +32 -1
- package/lib/cursor-sessions.ts +62 -5
- package/package.json +9 -9
- package/pi-extension/index.ts +10 -18
- package/src/audit/cli.ts +72 -81
- package/src/audit/harm-report.ts +12 -6
- package/src/audit/index.ts +3 -1
- package/src/audit/leak-fingerprint.ts +2 -4
- package/src/audit/leak-record.ts +26 -10
- package/src/audit/leak-scan.ts +1 -1
- package/src/audit/leak-store.ts +1 -46
- package/src/audit/machine-store.ts +8 -9
- package/src/audit/macos-notifier.ts +56 -68
- package/src/audit/redact-example.ts +2 -0
- package/src/audit/report-harm.ts +31 -46
- package/src/audit/schedule-cli.ts +17 -33
- package/src/audit/types.ts +5 -0
- package/src/hooks/builtin-policies.ts +2 -2
- package/src/hooks/configure-wizard.ts +36 -1
- package/src/hooks/fp-config.ts +29 -54
- package/src/hooks/handler.ts +1 -79
- package/src/hooks/integrations.ts +4 -15
- package/src/hooks/notice.ts +12 -179
- package/src/hooks/uninstall-cli.ts +3 -3
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +0 -3
- package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +0 -1
- package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +0 -1
- package/src/audit/leak-notice.ts +0 -259
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_ssgManifest.js +0 -0
|
@@ -68,13 +68,9 @@ 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
|
-
*
|
|
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.
|
|
71
|
+
* Turn scheduled audits on. Email is optional: `--email` signs in, while a
|
|
72
|
+
* plain `--schedule` enables the local scan and desktop notification without
|
|
73
|
+
* prompting for an account.
|
|
78
74
|
*
|
|
79
75
|
* The interval is written and then RE-READ, so what is printed is what the
|
|
80
76
|
* config actually kept — `readIntervalDays` owns the 1..90 clamp and a second
|
|
@@ -108,17 +104,13 @@ export async function runScheduleOn(
|
|
|
108
104
|
if (bad) throw new ScheduleCliError(bad);
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
const
|
|
107
|
+
const signedIn = emailArg === undefined
|
|
108
|
+
? { user: readAuth()?.user ?? null, prompted: false }
|
|
109
|
+
: await ensureSignedIn(emailArg);
|
|
112
110
|
|
|
113
111
|
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.
|
|
119
112
|
audit: {
|
|
120
113
|
auto: true,
|
|
121
|
-
reportsConsentedAt: Date.now(),
|
|
122
114
|
...(days !== undefined ? { intervalDays: days } : {}),
|
|
123
115
|
},
|
|
124
116
|
});
|
|
@@ -134,12 +126,14 @@ export async function runScheduleOn(
|
|
|
134
126
|
// The list is the real payload from `report-harm.ts`: machine id, hostname,
|
|
135
127
|
// platform, the window bounds and the redacted examples.
|
|
136
128
|
const summary = [
|
|
137
|
-
`every ${interval} day${interval === 1 ? "" : "s"} ·
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
`every ${interval} day${interval === 1 ? "" : "s"} · desktop notification on leaks`,
|
|
130
|
+
signedIn.user
|
|
131
|
+
? `masked leak alerts also go to ${signedIn.user.email}`
|
|
132
|
+
: "email is optional · add it with --email you@example.com",
|
|
133
|
+
"email contains only the most recent masked credential exposure",
|
|
140
134
|
];
|
|
141
135
|
|
|
142
|
-
if (prompted) {
|
|
136
|
+
if (signedIn.prompted) {
|
|
143
137
|
// A sign-in just drew the frame, so the result continues it and the `└`
|
|
144
138
|
// closes both at once — rather than the frame ending and a loose line
|
|
145
139
|
// appearing underneath.
|
|
@@ -158,7 +152,7 @@ export async function runScheduleOn(
|
|
|
158
152
|
// exists to make visible.
|
|
159
153
|
warnIfDaemonWontRun();
|
|
160
154
|
|
|
161
|
-
if (prompted) {
|
|
155
|
+
if (signedIn.prompted) {
|
|
162
156
|
outro("failproofai audit --status · when the next scan is due");
|
|
163
157
|
} else {
|
|
164
158
|
process.stdout.write(dim(`\n failproofai audit --status when the next scan is due\n\n`));
|
|
@@ -217,7 +211,7 @@ export function runScheduleStatus(): void {
|
|
|
217
211
|
|
|
218
212
|
// A session whose refresh window has closed cannot mint another access token,
|
|
219
213
|
// so it is a destination in name only. Showing the address for one would tell
|
|
220
|
-
// somebody their
|
|
214
|
+
// somebody their alerts are going somewhere they are not.
|
|
221
215
|
const live = auth && auth.refresh_expires_at * 1000 > Date.now() ? auth : null;
|
|
222
216
|
// Beside the schedule, because they are the two halves of "what does this
|
|
223
217
|
// machine do on its own" — one decides whether it scans, the other whether it
|
|
@@ -230,19 +224,9 @@ export function runScheduleStatus(): void {
|
|
|
230
224
|
detail.push(["reports to", live ? live.user.email : dim("— signed out")]);
|
|
231
225
|
if (on && !live) {
|
|
232
226
|
// The state the reporter surfaces as "signed-out". Named here for the same
|
|
233
|
-
// reason the settings panel names it: the scans
|
|
234
|
-
// about
|
|
235
|
-
detail.push(["", pink("scans continue;
|
|
236
|
-
} else if (on && config.audit.reportsConsentedAt === undefined) {
|
|
237
|
-
// Signed in, scheduled, and still not sending: this machine set `audit.auto`
|
|
238
|
-
// when it only meant "scan locally", so nothing has consented to the digest
|
|
239
|
-
// leaving the box. Without this row the status screen would show a healthy
|
|
240
|
-
// schedule and a live address and still mail nothing, with no explanation
|
|
241
|
-
// anywhere the user can see.
|
|
242
|
-
detail.push([
|
|
243
|
-
"",
|
|
244
|
-
pink("scans continue; digests need a fresh opt-in — run `--schedule` to turn them on"),
|
|
245
|
-
]);
|
|
227
|
+
// reason the settings panel names it: the scans and desktop notifications
|
|
228
|
+
// keep running, so silence about email would look like the feature failing.
|
|
229
|
+
detail.push(["", pink("scans and desktop alerts continue; email alerts are paused")]);
|
|
246
230
|
}
|
|
247
231
|
|
|
248
232
|
detail.push(["daemon", describeDaemon(daemon)]);
|
package/src/audit/types.ts
CHANGED
|
@@ -217,6 +217,11 @@ export interface AuditResult {
|
|
|
217
217
|
* Lets the dashboard answer "is this policy already on?" for
|
|
218
218
|
* detector-mapped policies that may not have hit during this audit. */
|
|
219
219
|
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[];
|
|
220
225
|
/**
|
|
221
226
|
* Ids of credentials seen for the FIRST time in this scan.
|
|
222
227
|
*
|
|
@@ -138,10 +138,10 @@ const API_KEY_PATTERNS: Array<[RegExp, string]> = [
|
|
|
138
138
|
// this rule's run would bridge the hyphen in `sk-proj-` and report every
|
|
139
139
|
// project key a second time under the wrong label.
|
|
140
140
|
[/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
|
|
141
|
-
[/sk-svcacct-[A-Za-z0-9\-_]{20,}/, "OpenAI service-account key"],
|
|
141
|
+
[/(?<!\w)sk-svcacct-[A-Za-z0-9\-_]{20,}/, "OpenAI service-account key"],
|
|
142
142
|
// Highest blast radius in the family: it authenticates the organisation
|
|
143
143
|
// admin surface, and lives at a DIFFERENT console from the other keys.
|
|
144
|
-
[/sk-admin-[A-Za-z0-9\-_]{20,}/, "OpenAI admin key"],
|
|
144
|
+
[/(?<!\w)sk-admin-[A-Za-z0-9\-_]{20,}/, "OpenAI admin key"],
|
|
145
145
|
|
|
146
146
|
// ── OpenAI-compatible gateways ──
|
|
147
147
|
[/sk-or-v1-[A-Za-z0-9\-_]{20,}/, "OpenRouter API key"],
|
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
type RetryProbe,
|
|
107
107
|
} from "./onboarding-attempt";
|
|
108
108
|
import { acquireOnboardingLock } from "./onboarding-lock";
|
|
109
|
+
import { readConfigRaw, updateConfig } from "./fp-config";
|
|
109
110
|
|
|
110
111
|
export interface WizardIO {
|
|
111
112
|
stdin?: TTYIn;
|
|
@@ -755,6 +756,19 @@ export async function runConfigureWizard(
|
|
|
755
756
|
// question, and no flag can supply it.
|
|
756
757
|
const preAnswered = Boolean(answers.token);
|
|
757
758
|
|
|
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
|
+
|
|
758
772
|
|
|
759
773
|
// Running the wizard itself under sudo configures the WRONG ACCOUNT, and
|
|
760
774
|
// does it silently: homedir() becomes /root, so the hooks land in root's
|
|
@@ -1439,6 +1453,20 @@ export async function runConfigureWizard(
|
|
|
1439
1453
|
// binary filename are both derived from it.
|
|
1440
1454
|
if (daemonInstalled) {
|
|
1441
1455
|
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
|
+
}
|
|
1442
1470
|
// Only now — the unit points at the new binary, so older ones are no
|
|
1443
1471
|
// longer referenced by anything. Keeps the previous version for an
|
|
1444
1472
|
// offline rollback.
|
|
@@ -1461,7 +1489,14 @@ export async function runConfigureWizard(
|
|
|
1461
1489
|
// missing or launchd refuses the job must still finish installing its
|
|
1462
1490
|
// daemon, its hooks and its policies, which are the parts that enforce
|
|
1463
1491
|
// anything; the notice in-CLI still reaches them either way.
|
|
1464
|
-
installMacNotifier();
|
|
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
|
+
}
|
|
1465
1500
|
}
|
|
1466
1501
|
|
|
1467
1502
|
// Telemetry runs concurrently with the install (never rejects, 5s-bounded) so
|
package/src/hooks/fp-config.ts
CHANGED
|
@@ -308,22 +308,15 @@ export interface FpConfig {
|
|
|
308
308
|
};
|
|
309
309
|
audit: {
|
|
310
310
|
/**
|
|
311
|
-
* Scan this machine on a schedule
|
|
312
|
-
*
|
|
311
|
+
* Scan this machine on a schedule. Delivery is deliberately separate:
|
|
312
|
+
* desktop notifications are controlled by `notify`, while a verified local
|
|
313
|
+
* email identity adds email alerts without changing the timer.
|
|
313
314
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* are the same decision and take the same switch.
|
|
320
|
-
*
|
|
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.
|
|
315
|
+
* A signed-out machine with this on is therefore the normal local-only
|
|
316
|
+
* state rather than a contradiction: it keeps scanning, keeps its local
|
|
317
|
+
* dashboard current, and raises desktop notifications for credential leaks.
|
|
318
|
+
* A refresh token expiring must not silently switch off a background
|
|
319
|
+
* security feature.
|
|
327
320
|
*
|
|
328
321
|
* ON for a configured machine that has never stated an opinion, OFF for
|
|
329
322
|
* every machine we could not read an opinion from — which is not the same
|
|
@@ -345,9 +338,9 @@ export interface FpConfig {
|
|
|
345
338
|
* agree, or the settings page describes a machine that is doing something
|
|
346
339
|
* else.
|
|
347
340
|
*
|
|
348
|
-
* Turning it on sends
|
|
349
|
-
*
|
|
350
|
-
*
|
|
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.
|
|
351
344
|
*/
|
|
352
345
|
auto: boolean;
|
|
353
346
|
/**
|
|
@@ -358,40 +351,22 @@ export interface FpConfig {
|
|
|
358
351
|
* wants the scan and not the banner is a coherent person, and without this
|
|
359
352
|
* their only way to stop the banner is to stop the scan.
|
|
360
353
|
*
|
|
361
|
-
* ON by default
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* a machine that finds the key and tells nobody.
|
|
354
|
+
* ON by default. Every scheduled scan that still sees a leak may notify,
|
|
355
|
+
* which keeps a credential that remains exposed from disappearing after
|
|
356
|
+
* its first alert. A silent default would make the common case — a user who
|
|
357
|
+
* never opens the dashboard and never gave an email — a machine that finds
|
|
358
|
+
* the key and tells nobody.
|
|
367
359
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* SSH session. Turning off every channel at once is not an option this
|
|
372
|
-
* offers, because the resulting state is indistinguishable from broken.
|
|
360
|
+
* On a headless box, container or SSH-only session there may be no desktop
|
|
361
|
+
* server to receive the banner. The scheduled scan still completes and
|
|
362
|
+
* records its result; notification failure never changes the scan outcome.
|
|
373
363
|
*/
|
|
374
364
|
notify: boolean;
|
|
375
365
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* and it is never drawn as one. `auto` is what a person sets; this is a
|
|
381
|
-
* record of the disclosure they were shown when they set it. The two cannot
|
|
382
|
-
* drift, because every path that turns `auto` on stamps this in the same
|
|
383
|
-
* call and nothing stamps it alone.
|
|
384
|
-
*
|
|
385
|
-
* It exists because `auto` CHANGED MEANING. Through 1.0.0 it meant "scan
|
|
386
|
-
* this machine on a timer" and nothing more — no account, no network, and
|
|
387
|
-
* the toggle that wrote it said as much in as many words. Harm digests gave
|
|
388
|
-
* the same stored bit a second job: uploading redacted transcript excerpts
|
|
389
|
-
* to the api-server and mailing them. Without a separate record, every
|
|
390
|
-
* machine that opted into the old meaning would have started sending on
|
|
391
|
-
* upgrade, having agreed to nothing of the kind, with the only notice a
|
|
392
|
-
* line in the journal. Gating on the stamp rather than the switch is what
|
|
393
|
-
* keeps that upgrade silent in the safe direction: those machines keep
|
|
394
|
-
* scanning locally and send nothing until somebody opts in again.
|
|
366
|
+
* Legacy migration marker from the earlier harm-digest consent model.
|
|
367
|
+
* Retained when reading and writing old configs so upgrades are lossless,
|
|
368
|
+
* but it no longer gates scheduling or credential-alert email. A verified
|
|
369
|
+
* email identity is now the delivery opt-in.
|
|
395
370
|
*/
|
|
396
371
|
reportsConsentedAt?: number;
|
|
397
372
|
/** Days between scheduled runs. Wall clock, so it survives suspend. */
|
|
@@ -587,8 +562,8 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
|
587
562
|
// question is only whether to speak.
|
|
588
563
|
notify: audit.notify !== false,
|
|
589
564
|
intervalDays: readIntervalDays(audit.interval_days),
|
|
590
|
-
//
|
|
591
|
-
//
|
|
565
|
+
// Preserve the legacy marker when it is valid; current behavior does
|
|
566
|
+
// not consult it.
|
|
592
567
|
reportsConsentedAt:
|
|
593
568
|
typeof audit.reports_consented_at === "number" &&
|
|
594
569
|
Number.isFinite(audit.reports_consented_at)
|
|
@@ -738,7 +713,8 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
|
|
|
738
713
|
...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
|
|
739
714
|
// Written ALWAYS, unlike telemetry directly above — the two are opposites
|
|
740
715
|
// on purpose. Telemetry ships on and is deliberately not advertised;
|
|
741
|
-
// the scheduled audit
|
|
716
|
+
// the scheduled audit is configured during setup and is meant to be FOUND,
|
|
717
|
+
// and a switch
|
|
742
718
|
// nobody can see is the same as a switch that does not exist. Emitting both
|
|
743
719
|
// keys unconditionally also makes "a user's setting survives a rewrite"
|
|
744
720
|
// total rather than conditional.
|
|
@@ -749,9 +725,8 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
|
|
|
749
725
|
// exists to be found by somebody the banner annoyed.
|
|
750
726
|
notify: config.audit.notify,
|
|
751
727
|
interval_days: config.audit.intervalDays,
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
// `OWNED_CONFIG_KEYS`, so omitting it here really removes it.
|
|
728
|
+
// Preserve the legacy consent marker when present. It is not used by the
|
|
729
|
+
// current local-schedule + optional-email model.
|
|
755
730
|
...(config.audit.reportsConsentedAt === undefined
|
|
756
731
|
? {}
|
|
757
732
|
: { reports_consented_at: config.audit.reportsConsentedAt }),
|
package/src/hooks/handler.ts
CHANGED
|
@@ -35,8 +35,6 @@ import { loadAllCustomHooks } from "./custom-hooks-loader";
|
|
|
35
35
|
import type { CustomHook } from "./policy-types";
|
|
36
36
|
import { persistHookActivity } from "./hook-activity-store";
|
|
37
37
|
import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
|
|
38
|
-
import { shapeNotice, canDeliverNotice, leakNoticeText } from "./notice";
|
|
39
|
-
import { sessionNoticePending, markSessionNoticed } from "../audit/leak-notice";
|
|
40
38
|
import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
|
|
41
39
|
import { resolveCwd } from "./resolve-cwd";
|
|
42
40
|
import { resolvePermissionMode } from "./resolve-permission-mode";
|
|
@@ -106,67 +104,6 @@ export interface HookEventOutcome {
|
|
|
106
104
|
stderr: string;
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
/**
|
|
110
|
-
* Merge a pending leak notice into an outcome, once.
|
|
111
|
-
*
|
|
112
|
-
* Anchored on Stop wherever the host renders there, and SessionStart only where
|
|
113
|
-
* it does not: Stop fires once per turn, AFTER the answer, so it never
|
|
114
|
-
* interrupts what the user was reading — and at session open the background
|
|
115
|
-
* scan may not have produced the finding yet.
|
|
116
|
-
*
|
|
117
|
-
* Every failure is swallowed. A courtesy message must never be the reason a
|
|
118
|
-
* hook misbehaves, and the finding is on disk either way.
|
|
119
|
-
*/
|
|
120
|
-
function attachLeakNotice(
|
|
121
|
-
eventType: string,
|
|
122
|
-
cli: IntegrationType | undefined,
|
|
123
|
-
outcome: HookEventOutcome,
|
|
124
|
-
sessionId: string | undefined,
|
|
125
|
-
): HookEventOutcome {
|
|
126
|
-
try {
|
|
127
|
-
if (!canDeliverNotice(cli)) return outcome;
|
|
128
|
-
// canDeliverNotice() above already proved cli is set; narrow for the call.
|
|
129
|
-
if (!cli) return outcome;
|
|
130
|
-
const canonical = canonicalizeEventType(eventType, cli);
|
|
131
|
-
// STOP ONLY, and the narrowing is the fix for a measured failure rather
|
|
132
|
-
// than caution. `notice.ts` derived each host's channel from a live probe
|
|
133
|
-
// on **Stop**; SessionStart was allowed here on the assumption that a
|
|
134
|
-
// channel proven on one event works on another. It does not. On a real
|
|
135
|
-
// machine 499 findings were CLAIMED and the user saw nothing: SessionStart
|
|
136
|
-
// fires first in a session, consumed every claim, and the host dropped the
|
|
137
|
-
// field — Claude Code documents `hookSpecificOutput.additionalContext` for
|
|
138
|
-
// SessionStart, not `systemMessage`.
|
|
139
|
-
//
|
|
140
|
-
// That is the worst outcome this design has: a finding recorded as
|
|
141
|
-
// delivered that reached nobody, and never retried. Stop fires at the end
|
|
142
|
-
// of every assistant turn, so nothing is lost by waiting for it — the user
|
|
143
|
-
// is reading output at that moment anyway. Add SessionStart back only with
|
|
144
|
-
// a probe showing the notice actually rendered there.
|
|
145
|
-
if (canonical !== "Stop") return outcome;
|
|
146
|
-
|
|
147
|
-
// Keyed on the SESSION and on whether the user has opened the report, not
|
|
148
|
-
// on a per-finding "we emitted this once" marker — see
|
|
149
|
-
// `sessionNoticePending` for the two ways that marker lost the alert
|
|
150
|
-
// permanently. A dropped notice now costs one session's silence instead.
|
|
151
|
-
const pending = sessionNoticePending(sessionId ?? "");
|
|
152
|
-
if (pending.count === 0) return outcome;
|
|
153
|
-
|
|
154
|
-
const shaped = shapeNotice(cli, leakNoticeText(pending.count), outcome.stdout);
|
|
155
|
-
if (shaped.stdout === outcome.stdout && !shaped.stderr) return outcome;
|
|
156
|
-
|
|
157
|
-
// Claimed only once the notice is actually on the stream, so a crash
|
|
158
|
-
// between the two re-notifies rather than silently swallowing the alert.
|
|
159
|
-
markSessionNoticed(sessionId ?? "");
|
|
160
|
-
return {
|
|
161
|
-
exitCode: outcome.exitCode,
|
|
162
|
-
stdout: shaped.stdout,
|
|
163
|
-
stderr: outcome.stderr + shaped.stderr,
|
|
164
|
-
};
|
|
165
|
-
} catch {
|
|
166
|
-
return outcome;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
107
|
export interface EvaluateHookEventOptions {
|
|
171
108
|
/**
|
|
172
109
|
* Await pending telemetry POSTs before returning. Defaults to `true`,
|
|
@@ -820,22 +757,7 @@ export async function evaluateHookEvent(
|
|
|
820
757
|
}
|
|
821
758
|
}
|
|
822
759
|
|
|
823
|
-
|
|
824
|
-
//
|
|
825
|
-
// Emitted HERE rather than in `handleHookEvent`, which is what fixes the
|
|
826
|
-
// bug this replaced: `bin/failproofai.mjs`'s `isDaemonConfigured()` branch
|
|
827
|
-
// writes `result.stdout`/`result.stderr` and EXITS INSIDE THAT BRANCH — it
|
|
828
|
-
// never falls through to `handleHookEvent`. Setup requires the daemon, so
|
|
829
|
-
// the previous notice was dead code on every configured machine, and had
|
|
830
|
-
// been since it shipped. Both paths return this object, so producing the
|
|
831
|
-
// notice here reaches both with no change to either caller.
|
|
832
|
-
const withNotice = attachLeakNotice(
|
|
833
|
-
eventType,
|
|
834
|
-
cli,
|
|
835
|
-
{ exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
836
|
-
sessionId,
|
|
837
|
-
);
|
|
838
|
-
return withNotice;
|
|
760
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
839
761
|
} finally {
|
|
840
762
|
if (opts?.awaitTelemetryFlush ?? true) {
|
|
841
763
|
// Await any un-awaited (`void trackHookEvent(...)`) events fired during
|
|
@@ -784,7 +784,6 @@ export const cursor: Integration = {
|
|
|
784
784
|
// • exit 2 OR `permissionDecision: "deny"` → `throw new Error(reason)`
|
|
785
785
|
// (which OpenCode surfaces as a tool-call failure to the agent)
|
|
786
786
|
// • `additionalContext` → `client.session.prompt(...)` (fire-and-forget)
|
|
787
|
-
// • `failproofaiNotice` → `client.tui.showToast(...)` (user-visible)
|
|
788
787
|
// • everything else → no-op (allow)
|
|
789
788
|
//
|
|
790
789
|
// Settings paths:
|
|
@@ -957,19 +956,6 @@ async function applyDecision(result, ctx, eventName) {
|
|
|
957
956
|
if (out && out.decision && out.decision.behavior === "deny") {
|
|
958
957
|
throw new Error((out.decision.message) || "Blocked by failproofai");
|
|
959
958
|
}
|
|
960
|
-
const notice = parsed.failproofaiNotice;
|
|
961
|
-
if (typeof notice === "string" && notice && ctx && ctx.client && ctx.client.tui) {
|
|
962
|
-
try {
|
|
963
|
-
await ctx.client.tui.showToast({
|
|
964
|
-
body: {
|
|
965
|
-
title: "failproofai audit",
|
|
966
|
-
message: notice,
|
|
967
|
-
variant: "warning",
|
|
968
|
-
duration: 12_000,
|
|
969
|
-
},
|
|
970
|
-
});
|
|
971
|
-
} catch { /* a courtesy notice must never break the host hook */ }
|
|
972
|
-
}
|
|
973
959
|
// Forward additional context as a prompt to the session. For Stop /
|
|
974
960
|
// SubagentStop the prompt is the only force-retry channel (session.idle
|
|
975
961
|
// already fired), so AWAIT to ensure the SDK round-trip completes before
|
|
@@ -1025,7 +1011,10 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
1025
1011
|
const claudeEvent = BUS_EVENT_MAP[event.type];
|
|
1026
1012
|
if (!claudeEvent) return;
|
|
1027
1013
|
const props = event.properties || {};
|
|
1028
|
-
const
|
|
1014
|
+
const info = props.info || props.session || {};
|
|
1015
|
+
|
|
1016
|
+
const sessionID = info.id || info.sessionID || info.sessionId || info.session_id ||
|
|
1017
|
+
props.sessionID || props.sessionId || props.session_id || props.id;
|
|
1029
1018
|
const r = await runFailproofai(claudeEvent, {
|
|
1030
1019
|
session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
|
|
1031
1020
|
}, directory);
|