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.
Files changed (170) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
  32. package/.next/standalone/.next/server/app/audit/page.js +3 -3
  33. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/index.html +1 -1
  36. package/.next/standalone/.next/server/app/index.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  38. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  41. package/.next/standalone/.next/server/app/page.js +2 -2
  42. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  45. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  46. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  59. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +15 -4
  62. package/.next/standalone/.next/server/app/settings/page.js +4 -4
  63. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +11 -11
  76. package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
  81. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yl9wb5._.js → [root-of-the-server]__12hbpha._.js} +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__17xnxlx._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__1ckpmkk._.js} +3 -3
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00-s7h8._.js → [root-of-the-server]__1fvfw59._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/{_0guxnh-._.js → _0a0zlpr._.js} +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -14
  99. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  106. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  107. package/.next/standalone/.next/server/pages/404.html +1 -1
  108. package/.next/standalone/.next/server/pages/500.html +1 -1
  109. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  110. package/.next/standalone/.next/server/server-reference-manifest.json +33 -22
  111. package/.next/standalone/.next/static/chunks/{17tfki9x5rd_7.js → 083fk73jtv9a0.js} +1 -1
  112. package/.next/standalone/.next/static/chunks/{1i-i0pxg9q_8y.js → 0divvxng208xo.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{38dqur43qthrl.js → 0f1pznnc7zlub.js} +1 -1
  114. package/.next/standalone/.next/static/chunks/{323cyb3a2s06y.js → 0ntdfr2552x-h.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/{0po8n4tyotjf2.js → 0t6ea7d3bkmdv.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/{364cqbpeg1y5i.js → 15uon66hpwbgg.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{42gwu2bsz4ske.js → 1csubx9gwhsf6.js} +2 -2
  118. package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +1 -0
  119. package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +1 -0
  120. package/.next/standalone/.next/static/chunks/{2bxr2_2h4kaqq.js → 2pwfrn6_mw-1u.js} +1 -1
  121. package/.next/standalone/app/actions/get-leaks.ts +0 -5
  122. package/.next/standalone/app/actions/get-scheduled-audit.ts +5 -0
  123. package/.next/standalone/app/actions/update-scheduled-audit.ts +18 -57
  124. package/.next/standalone/app/api/auth/status/route.ts +3 -2
  125. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +1 -1
  126. package/.next/standalone/app/audit/_components/empty-state.tsx +1 -1
  127. package/.next/standalone/app/audit/_components/leak-section.tsx +1 -1
  128. package/.next/standalone/app/settings/page.tsx +1 -1
  129. package/.next/standalone/app/settings/settings-client.tsx +50 -45
  130. package/.next/standalone/lib/auth/api-server-client.ts +6 -6
  131. package/.next/standalone/lib/codex-sessions.ts +20 -4
  132. package/.next/standalone/lib/copilot-sessions.ts +32 -1
  133. package/.next/standalone/lib/cursor-sessions.ts +62 -5
  134. package/.next/standalone/package.json +9 -9
  135. package/.next/standalone/server.js +1 -1
  136. package/dist/cli.mjs +991 -1101
  137. package/dist/worker.mjs +175 -382
  138. package/lib/auth/api-server-client.ts +6 -6
  139. package/lib/codex-sessions.ts +20 -4
  140. package/lib/copilot-sessions.ts +32 -1
  141. package/lib/cursor-sessions.ts +62 -5
  142. package/package.json +9 -9
  143. package/pi-extension/index.ts +10 -18
  144. package/src/audit/cli.ts +72 -81
  145. package/src/audit/harm-report.ts +12 -6
  146. package/src/audit/index.ts +3 -1
  147. package/src/audit/leak-fingerprint.ts +2 -4
  148. package/src/audit/leak-record.ts +26 -10
  149. package/src/audit/leak-scan.ts +1 -1
  150. package/src/audit/leak-store.ts +1 -46
  151. package/src/audit/machine-store.ts +8 -9
  152. package/src/audit/macos-notifier.ts +56 -68
  153. package/src/audit/redact-example.ts +2 -0
  154. package/src/audit/report-harm.ts +31 -46
  155. package/src/audit/schedule-cli.ts +17 -33
  156. package/src/audit/types.ts +5 -0
  157. package/src/hooks/builtin-policies.ts +2 -2
  158. package/src/hooks/configure-wizard.ts +36 -1
  159. package/src/hooks/fp-config.ts +29 -54
  160. package/src/hooks/handler.ts +1 -79
  161. package/src/hooks/integrations.ts +4 -15
  162. package/src/hooks/notice.ts +12 -179
  163. package/src/hooks/uninstall-cli.ts +3 -3
  164. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +0 -3
  165. package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +0 -1
  166. package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +0 -1
  167. package/src/audit/leak-notice.ts +0 -259
  168. /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_buildManifest.js +0 -0
  169. /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_clientMiddlewareManifest.js +0 -0
  170. /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, 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.
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 { user, prompted } = await ensureSignedIn(emailArg);
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"} · 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",
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 digests are going somewhere they are not.
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 keep running, so silence
234
- // about the digests would look like the feature failing.
235
- detail.push(["", pink("scans continue; digests are paused until you sign in")]);
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)]);
@@ -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
@@ -308,22 +308,15 @@ export interface FpConfig {
308
308
  };
309
309
  audit: {
310
310
  /**
311
- * Scan this machine on a schedule, and mail a digest when a scan finds
312
- * something harmful.
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
- * 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
- *
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 NOTHING on its own: the digest is gated on
349
- * `reportsConsentedAt` below, not on this switch, so a machine that flips
350
- * on at upgrade scans locally and mails nobody.
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, and the direction is the point: this fires at most once
362
- * per distinct credential ever (`leak-notice.ts` claims each finding with
363
- * an O_EXCL marker), so the volume it can reach is bounded by how many
364
- * secrets are actually leaking. A silent default would make the common
365
- * case — a user who never opens the dashboard and never gave an email —
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
- * It does NOT silence the in-CLI notice. That one appears inside a session
369
- * the user is already driving, costs two lines, and is the only channel
370
- * left when there is no desktop to notify — a headless box, a container, an
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
- * When this machine's owner agreed that a scheduled scan may send what it
377
- * finds off the box, as epoch ms. Absent means they never did.
378
- *
379
- * This does NOT reintroduce the second switch the comment above rejects,
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
- // A finite number or nothing. A garbage value reads as absent, which is
591
- // the direction that sends nothing.
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 ships off and is meant to be FOUND, and a switch
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
- // Written only once there IS consent, so an untouched machine's config
753
- // does not grow a key implying it was asked. It is in
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 }),
@@ -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
- // Attach a pending leak notice, if this event is one that can carry it.
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 sessionID = props.sessionID || (props.session && props.session.id) || props.id;
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);