failproofai 1.0.4-beta.2 → 1.0.4-beta.6

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 (176) 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 +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
  33. package/.next/standalone/.next/server/app/audit/page.js +3 -3
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +14 -14
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +2 -2
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  63. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1lh7m5d._.js → [externals]__0v1hz15._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1rqkg_y._.js → [externals]__1nb206a._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.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/{_08w6xzm._.js → _0otft92._.js} +2 -2
  74. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
  77. package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
  81. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
  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/[root-of-the-server]__1ui5ur8._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/_02b3i98._.js +23 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{_0oq1dh6._.js → _04ng_6x._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -1
  101. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  107. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  108. package/.next/standalone/.next/server/pages/404.html +1 -1
  109. package/.next/standalone/.next/server/pages/500.html +1 -1
  110. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  111. package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
  112. package/.next/standalone/.next/static/chunks/{1pb1oztsbwcss.js → 07c6l1lq5_69b.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{0o-hh5_turzlz.css → 0jpd8dv2vk930.css} +1 -1
  114. package/.next/standalone/.next/static/chunks/{32spub4wqjem-.js → 12bsgz8czvcnn.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/{3m4upvybtrexd.js → 1gn08w2pgqh09.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/{010bv1w6j171t.js → 1xgq22tri86an.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{2aquitk72k2op.js → 20xkiqzvfkuvm.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{2bi_1y0a_smt7.js → 2a84ug32tzik2.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{2ej3b8gk5ittu.js → 32m2ryfankf_2.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/3ioub_lpen-sq.js +1 -0
  121. package/.next/standalone/.next/static/chunks/{0wz8yftk18ts2.js → 3jxd133cw2vm3.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/44ncu88eq6z71.js +1 -0
  123. package/.next/standalone/app/actions/get-leaks.ts +5 -1
  124. package/.next/standalone/app/actions/update-scheduled-audit.ts +4 -56
  125. package/.next/standalone/app/api/auth/status/route.ts +3 -2
  126. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +29 -2
  127. package/.next/standalone/app/audit/_components/leak-section.tsx +15 -2
  128. package/.next/standalone/app/audit/audit-styles.css +18 -0
  129. package/.next/standalone/app/settings/page.tsx +1 -1
  130. package/.next/standalone/app/settings/settings-client.tsx +13 -39
  131. package/.next/standalone/lib/auth/api-server-client.ts +6 -6
  132. package/.next/standalone/lib/codex-sessions.ts +20 -4
  133. package/.next/standalone/lib/copilot-sessions.ts +32 -1
  134. package/.next/standalone/lib/cursor-sessions.ts +62 -5
  135. package/.next/standalone/lib/sqlite-reader.ts +78 -1
  136. package/.next/standalone/package.json +10 -10
  137. package/.next/standalone/server.js +1 -1
  138. package/bin/failproofai.mjs +1 -1
  139. package/dist/cli.mjs +1191 -1130
  140. package/dist/worker.mjs +198 -359
  141. package/lib/auth/api-server-client.ts +6 -6
  142. package/lib/codex-sessions.ts +20 -4
  143. package/lib/copilot-sessions.ts +32 -1
  144. package/lib/cursor-sessions.ts +62 -5
  145. package/lib/sqlite-reader.ts +78 -1
  146. package/package.json +10 -10
  147. package/pi-extension/index.ts +11 -4
  148. package/src/audit/cache.ts +6 -0
  149. package/src/audit/cli-adapters/claude.ts +13 -0
  150. package/src/audit/cli-login.ts +4 -1
  151. package/src/audit/cli.ts +67 -51
  152. package/src/audit/harm-report.ts +12 -6
  153. package/src/audit/index.ts +151 -8
  154. package/src/audit/leak-fingerprint.ts +2 -4
  155. package/src/audit/leak-record.ts +13 -9
  156. package/src/audit/leak-scan.ts +20 -44
  157. package/src/audit/leak-store.ts +5 -2
  158. package/src/audit/machine-store.ts +8 -9
  159. package/src/audit/macos-notifier.ts +2 -2
  160. package/src/audit/redact-example.ts +62 -5
  161. package/src/audit/report-harm.ts +31 -46
  162. package/src/audit/schedule-cli.ts +17 -33
  163. package/src/audit/types.ts +11 -0
  164. package/src/hooks/configure-wizard.ts +28 -0
  165. package/src/hooks/fp-config.ts +29 -54
  166. package/src/hooks/handler.ts +1 -59
  167. package/src/hooks/integrations.ts +4 -1
  168. package/src/hooks/notice.ts +23 -154
  169. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +0 -3
  170. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +0 -23
  171. package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +0 -1
  172. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +0 -1
  173. package/src/audit/leak-notice.ts +0 -161
  174. /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_buildManifest.js +0 -0
  175. /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_clientMiddlewareManifest.js +0 -0
  176. /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_ssgManifest.js +0 -0
@@ -257,7 +257,7 @@ export interface AuditReportBody {
257
257
  * Credentials this machine found in its own transcripts.
258
258
  *
259
259
  * Optional on the wire, so a CLI newer than the api-server sends a field the
260
- * server ignores rather than failing the whole digest — the harmful counts
260
+ * server ignores rather than failing the whole report — the harmful counts
261
261
  * are the older half of the contract and must keep arriving either way.
262
262
  *
263
263
  * Every field here is a mask, a label, a length or a location. There is no
@@ -287,15 +287,15 @@ export interface AuditReportResult {
287
287
  report_id: string;
288
288
  /** Whether this report produced an email. */
289
289
  emailed: boolean;
290
- /** `below_threshold`, `cooldown`, `send_failed`, or null when mail went out. */
290
+ /** `no_leak`, `cooldown`, `send_failed`, or null when mail went out. */
291
291
  reason: string | null;
292
292
  /**
293
293
  * Where the next window starts, per the SERVER.
294
294
  *
295
295
  * Persisted verbatim rather than computed locally. The server anchors it on
296
- * the last DELIVERED digest, so a report held by the cooldown — or one whose
296
+ * the last DELIVERED alert, so a report held by the cooldown — or one whose
297
297
  * send failed — correctly leaves the watermark where it was, and its findings
298
- * turn up in the next digest instead of falling into a gap. A machine that
298
+ * turn up in the next alert instead of falling into a gap. A machine that
299
299
  * lost `machine.json` also resyncs here rather than re-reporting from the
300
300
  * beginning of time.
301
301
  */
@@ -303,11 +303,11 @@ export interface AuditReportResult {
303
303
  }
304
304
 
305
305
  /**
306
- * Submit one scheduled scan's harmful findings.
306
+ * Submit one scheduled scan's harmful findings and newest masked credential.
307
307
  *
308
308
  * Called only by the audit child, and only on `--scheduled`. The destination
309
309
  * address is never sent: the api-server takes it from the access-token claims,
310
- * so a report cannot name where its digest goes.
310
+ * so a report cannot name where its email alert goes.
311
311
  */
312
312
  export async function submitAuditReport(
313
313
  accessToken: string,
@@ -158,6 +158,21 @@ function joinTexts(blocks: CodexContentBlock[] | undefined, wantedType: "input_t
158
158
  .join("\n");
159
159
  }
160
160
 
161
+ /** Text carried by tool outputs. Newer Codex chronicles use an array of
162
+ * `input_text` blocks for `custom_tool_call_output`, while older releases use
163
+ * one string for `function_call_output`. */
164
+ function toolOutputText(output: unknown): string {
165
+ if (typeof output === "string") return output;
166
+ if (Array.isArray(output)) {
167
+ return output
168
+ .filter((block): block is CodexContentBlock => !!block && typeof block === "object")
169
+ .map((block) => typeof block.text === "string" ? block.text : "")
170
+ .filter(Boolean)
171
+ .join("\n");
172
+ }
173
+ return output === undefined ? "" : JSON.stringify(output);
174
+ }
175
+
161
176
  /**
162
177
  * Parse a Codex JSONL transcript into `LogEntry[]` plus the raw lines.
163
178
  * Yields to the event loop every 200 lines so big transcripts don't block.
@@ -250,10 +265,11 @@ export async function parseCodexLog(
250
265
  continue;
251
266
  }
252
267
 
253
- if (subType === "function_call") {
268
+ if (subType === "function_call" || subType === "custom_tool_call") {
254
269
  const callId = payload.call_id as string | undefined;
255
270
  const name = (payload.name as string | undefined) ?? "function_call";
256
- const input = safeJsonParse(payload.arguments as string | undefined);
271
+ const encodedInput = (payload.arguments ?? payload.input) as string | undefined;
272
+ const input = safeJsonParse(encodedInput);
257
273
  const toolUse: ToolUseBlock = {
258
274
  type: "tool_use",
259
275
  id: callId ?? `${timestamp}-${name}`,
@@ -273,7 +289,7 @@ export async function parseCodexLog(
273
289
  continue;
274
290
  }
275
291
 
276
- if (subType === "function_call_output") {
292
+ if (subType === "function_call_output" || subType === "custom_tool_call_output") {
277
293
  const callId = payload.call_id as string | undefined;
278
294
  const block = callId ? toolUseById.get(callId) : undefined;
279
295
  if (block) {
@@ -282,7 +298,7 @@ export async function parseCodexLog(
282
298
  block.result = {
283
299
  timestamp,
284
300
  timestampFormatted: formatTimestamp(date),
285
- content: typeof payload.output === "string" ? (payload.output as string) : JSON.stringify(payload.output),
301
+ content: toolOutputText(payload.output),
286
302
  durationMs: duration,
287
303
  durationFormatted: formatDuration(duration),
288
304
  };
@@ -151,6 +151,11 @@ interface CopilotToolResult {
151
151
  detailedContent?: string;
152
152
  }
153
153
 
154
+ interface CopilotHookToolResult {
155
+ textResultForLlm?: string;
156
+ sessionLog?: string;
157
+ }
158
+
154
159
  interface CopilotToolTelemetry {
155
160
  metrics?: { commandTimeMs?: number; durationMs?: number };
156
161
  properties?: Record<string, unknown>;
@@ -171,6 +176,8 @@ export async function parseCopilotLog(
171
176
  // toolCallId → tool_use block, so we can attach tool.execution_complete back.
172
177
  const toolUseById = new Map<string, ToolUseBlock>();
173
178
  const toolUseStartMs = new Map<string, number>();
179
+ let mostRecentToolUse: ToolUseBlock | undefined;
180
+ let mostRecentToolUseStartMs = 0;
174
181
  let cwd: string | undefined;
175
182
  let seenSessionStart = false;
176
183
 
@@ -273,9 +280,32 @@ export async function parseCopilotLog(
273
280
  toolUseById.set(callId, toolUse);
274
281
  toolUseStartMs.set(callId, date.getTime());
275
282
  }
283
+ mostRecentToolUse = toolUse;
284
+ mostRecentToolUseStartMs = date.getTime();
276
285
  continue;
277
286
  }
278
287
 
288
+ // Copilot CLI 1.0.39 can omit the command's real stdout from
289
+ // `tool.execution_complete` and expose it only to the PostToolUse hook as
290
+ // `input.toolResult.textResultForLlm`. That is the value the model saw and
291
+ // the value failproofai must audit. Keep it on the active tool block; the
292
+ // later completion record may still contribute timing, but must not replace
293
+ // this output with its shorter status-only summary.
294
+ if (recType === "hook.start" && data.hookType === "postToolUse" && mostRecentToolUse) {
295
+ const input = (data.input as Record<string, unknown> | undefined) ?? {};
296
+ const hookResult = (input.toolResult as CopilotHookToolResult | undefined) ?? {};
297
+ const hookContent = hookResult.textResultForLlm ?? hookResult.sessionLog;
298
+ if (typeof hookContent === "string" && hookContent.length > 0) {
299
+ mostRecentToolUse.result = {
300
+ timestamp,
301
+ timestampFormatted: formatTimestamp(date),
302
+ content: hookContent,
303
+ durationMs: Math.max(0, date.getTime() - mostRecentToolUseStartMs),
304
+ durationFormatted: formatDuration(Math.max(0, date.getTime() - mostRecentToolUseStartMs)),
305
+ };
306
+ }
307
+ }
308
+
279
309
  if (recType === "tool.execution_complete") {
280
310
  const callId = data.toolCallId as string | undefined;
281
311
  const block = callId ? toolUseById.get(callId) : undefined;
@@ -289,7 +319,8 @@ export async function parseCopilotLog(
289
319
  typeof reportedMs === "number" && reportedMs >= 0
290
320
  ? reportedMs
291
321
  : Math.max(0, date.getTime() - startMs);
292
- const content = result.detailedContent ?? result.content ?? "";
322
+ const completionContent = result.detailedContent ?? result.content ?? "";
323
+ const content = block.result?.content || completionContent;
293
324
  block.result = {
294
325
  timestamp,
295
326
  timestampFormatted: formatTimestamp(date),
@@ -127,7 +127,15 @@ interface CursorRecord {
127
127
  /** Cursor 2026-04+ transcript shape: `{role, message: {content: [...]}}`. */
128
128
  role?: "user" | "assistant" | "system" | string;
129
129
  message?: {
130
- content?: Array<{ type?: string; text?: string }>;
130
+ content?: Array<{
131
+ type?: string;
132
+ text?: string;
133
+ id?: string;
134
+ name?: string;
135
+ input?: Record<string, unknown>;
136
+ tool_use_id?: string;
137
+ content?: unknown;
138
+ }>;
131
139
  };
132
140
  }
133
141
 
@@ -183,11 +191,36 @@ export async function parseCursorLog(
183
191
  if (!raw.type && raw.role && raw.message?.content) {
184
192
  const synthDate = new Date(SYNTH_T0 + i);
185
193
  const synthTs = synthDate.toISOString();
186
- const textParts = raw.message.content
194
+ const messageContent = raw.message.content;
195
+ const textParts = messageContent
187
196
  .filter((c) => c?.type === "text" && typeof c.text === "string")
188
197
  .map((c) => c.text!)
189
198
  .join("");
190
199
  if (raw.role === "user") {
200
+ // Some Cursor builds emit Claude-style tool_result blocks in the next
201
+ // user record. Pair them before treating any remaining text as a user
202
+ // turn so command output is available to the credential scanner.
203
+ let attachedResult = false;
204
+ for (const block of messageContent) {
205
+ if (block?.type !== "tool_result") continue;
206
+ const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
207
+ const toolUse = callId ? toolUseById.get(callId) : undefined;
208
+ if (!toolUse) continue;
209
+ const durationMs = Math.max(
210
+ 0,
211
+ synthDate.getTime() - (toolUseStartMs.get(callId!) ?? synthDate.getTime()),
212
+ );
213
+ toolUse.result = {
214
+ timestamp: synthTs,
215
+ timestampFormatted: formatTimestamp(synthDate),
216
+ content: typeof block.content === "string"
217
+ ? block.content
218
+ : block.content == null ? "" : JSON.stringify(block.content),
219
+ durationMs,
220
+ durationFormatted: formatDuration(durationMs),
221
+ };
222
+ attachedResult = true;
223
+ }
191
224
  // Strip the synthesized `<timestamp>...</timestamp>\n<user_query>...\n</user_query>`
192
225
  // wrapper Cursor adds for context — keep just the user_query body.
193
226
  const queryMatch = /<user_query>\s*([\s\S]*?)\s*<\/user_query>/.exec(textParts);
@@ -198,13 +231,37 @@ export async function parseCursorLog(
198
231
  ...baseEntry(rawCopy, synthTs, synthDate, source),
199
232
  message: { role: "user", content: text },
200
233
  } satisfies UserEntry);
234
+ } else if (!attachedResult) {
235
+ entries.push({
236
+ type: "system",
237
+ ...baseEntry(rawCopy, synthTs, synthDate, source),
238
+ raw: rawCopy,
239
+ } satisfies GenericEntry);
201
240
  }
202
241
  continue;
203
242
  }
204
243
  if (raw.role === "assistant") {
205
- const blocks: ContentBlock[] = textParts
206
- ? [{ type: "text", text: textParts }]
207
- : [];
244
+ const blocks: ContentBlock[] = [];
245
+ if (textParts) blocks.push({ type: "text", text: textParts });
246
+ for (let j = 0; j < messageContent.length; j++) {
247
+ const contentBlock = messageContent[j];
248
+ if (contentBlock?.type !== "tool_use") continue;
249
+ const name = typeof contentBlock.name === "string" ? contentBlock.name : "tool";
250
+ const id = typeof contentBlock.id === "string" && contentBlock.id
251
+ ? contentBlock.id
252
+ : `cursor-${i}-${name}-${j}`;
253
+ const toolUse: ToolUseBlock = {
254
+ type: "tool_use",
255
+ id,
256
+ name,
257
+ input: contentBlock.input && typeof contentBlock.input === "object"
258
+ ? contentBlock.input
259
+ : {},
260
+ };
261
+ blocks.push(toolUse);
262
+ toolUseById.set(id, toolUse);
263
+ toolUseStartMs.set(id, synthDate.getTime());
264
+ }
208
265
  if (blocks.length === 0) {
209
266
  entries.push({
210
267
  type: "system",
@@ -39,8 +39,42 @@ interface NodeSqliteModule {
39
39
  DatabaseSync: new (path: string, options?: { readOnly?: boolean }) => NodeSqliteDb;
40
40
  }
41
41
 
42
+ /**
43
+ * Swallow Node's "SQLite is an experimental feature" warning, once.
44
+ *
45
+ * It is not information the user can act on — we chose `node:sqlite`
46
+ * deliberately, and the alternative tier is a WASM build that leaks. But it is
47
+ * emitted on first use, which lands it in the MIDDLE of whatever is on screen,
48
+ * and the audit's progress display redraws itself by moving the cursor up a
49
+ * fixed number of lines. Two unexpected lines of stderr push the cursor down
50
+ * two rows, so the next redraw repaints the block two rows lower and strands
51
+ * the top of the previous frame above it. The user sees every stage twice and
52
+ * reasonably concludes the audit ran twice.
53
+ *
54
+ * Filtered by message rather than silenced wholesale: `process.emitWarning` is
55
+ * how deprecations and real problems surface too, and a blanket
56
+ * `NODE_NO_WARNINGS` in a tool that installs into other people's machines would
57
+ * hide those as well.
58
+ */
59
+ let warningPatched = false;
60
+ function silenceSqliteExperimentalWarning(): void {
61
+ if (warningPatched) return;
62
+ warningPatched = true;
63
+ const original = process.emitWarning.bind(process);
64
+ // The overloads differ across Node versions; the shape we care about is the
65
+ // first argument, and everything else is passed straight through.
66
+ process.emitWarning = ((warning: unknown, ...rest: unknown[]) => {
67
+ const text = typeof warning === "string" ? warning : String((warning as Error)?.message ?? "");
68
+ if (text.includes("SQLite is an experimental feature")) return;
69
+ return (original as (...a: unknown[]) => void)(warning, ...rest);
70
+ }) as typeof process.emitWarning;
71
+ }
72
+
42
73
  async function tryNodeSqlite(dbPath: string): Promise<SqliteReader | null> {
43
74
  try {
75
+ // Before the import, not after: the warning fires on first use of the
76
+ // module, which is this line.
77
+ silenceSqliteExperimentalWarning();
44
78
  // Dynamic import: on Node < 22.5 this rejects and we fall through to sql.js.
45
79
  const mod = (await import("node:sqlite")) as unknown as NodeSqliteModule;
46
80
  const db = new mod.DatabaseSync(dbPath, { readOnly: true });
@@ -66,10 +100,53 @@ function loadSqlJs(): Promise<SqlJsStatic> {
66
100
  return sqlPromise;
67
101
  }
68
102
 
103
+ const SQLITE_MAGIC = "SQLite format 3\0";
104
+
105
+ /**
106
+ * Clear the WAL flag on OUR OWN in-memory copy of the database image.
107
+ *
108
+ * Header bytes 18 and 19 are the file-format write and read versions; `2`
109
+ * means WAL. sql.js never sees the `-wal` sidecar — it is handed the main
110
+ * file's bytes and nothing else, which is exactly the snapshot caveat this
111
+ * module already documents — so the flag buys us nothing, and it costs a great
112
+ * deal: opening a WAL-flagged image makes SQLite build a wal-index
113
+ * shared-memory region that sql.js's MEMFS-backed VFS never reclaims on
114
+ * `close()` (close unlinks only the db file it created).
115
+ *
116
+ * The asm.js build's heap is a FIXED 22,151,168-byte ArrayBuffer with
117
+ * ALLOW_MEMORY_GROWTH off, so `emscripten_resize_heap` is compiled to
118
+ * `abort("OOM")`. Measured: the 128th WAL-mode open exhausts it — the same
119
+ * threshold for a 319 KB database and a 10 MB one, because the leaked region
120
+ * is a fixed per-connection allocation rather than data.
121
+ *
122
+ * And the abort is not confined to that open. `initSqlJs` memoizes one
123
+ * process-wide module, so once it aborts every later `openSqliteReadonly`
124
+ * silently returns null. A full audit opens ~150 databases — devin, goose and
125
+ * opencode each open theirs once per SESSION — so it crossed 128 and then
126
+ * dropped whatever it had not yet read, with no error anywhere. The
127
+ * `Aborted(OOM)` lines on stderr were the only symptom, and they read as
128
+ * harmless because the run still completed.
129
+ *
130
+ * Only under bun and Node < 22.5: tier 1 is `node:sqlite`, which bun does not
131
+ * provide, and `engines.node` allows 20.9. Both fall through to here.
132
+ */
133
+ function clearWalFlag(bytes: Buffer): Buffer {
134
+ // Too short to carry a header, or not a SQLite image at all — hand it back
135
+ // untouched and let sql.js produce its own error.
136
+ if (bytes.length < 20) return bytes;
137
+ if (bytes.toString("latin1", 0, 16) !== SQLITE_MAGIC) return bytes;
138
+ if (bytes[18] === 1 && bytes[19] === 1) return bytes; // already rollback-journal
139
+ bytes[18] = 1;
140
+ bytes[19] = 1;
141
+ return bytes;
142
+ }
143
+
69
144
  async function trySqlJs(dbPath: string): Promise<SqliteReader | null> {
70
145
  try {
71
146
  const SQL = await loadSqlJs();
72
- const db = new SQL.Database(readFileSync(dbPath));
147
+ // `readFileSync` already returns a private copy, so mutating the two header
148
+ // bytes cannot touch the file on disk.
149
+ const db = new SQL.Database(clearWalFlag(readFileSync(dbPath)));
73
150
  return {
74
151
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
75
152
  const stmt = db.prepare(sql);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.4-beta.2",
3
+ "version": "1.0.4-beta.6",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -117,19 +117,19 @@
117
117
  "vite": "8.0.16",
118
118
  "undici": "7.29.0",
119
119
  "brace-expansion": "5.0.9",
120
- "sharp": "0.35.0",
120
+ "sharp": "0.35.4",
121
121
  "browserslist": "4.28.8"
122
122
  },
123
123
  "optionalDependencies": {
124
- "@failproofai/failproofaid-linux-x64": "1.0.4-beta.2",
125
- "@failproofai/failproofaid-linux-arm64": "1.0.4-beta.2",
126
- "@failproofai/failproofaid-darwin-x64": "1.0.4-beta.2",
127
- "@failproofai/failproofaid-darwin-arm64": "1.0.4-beta.2"
124
+ "@failproofai/failproofaid-linux-x64": "1.0.4-beta.6",
125
+ "@failproofai/failproofaid-linux-arm64": "1.0.4-beta.6",
126
+ "@failproofai/failproofaid-darwin-x64": "1.0.4-beta.6",
127
+ "@failproofai/failproofaid-darwin-arm64": "1.0.4-beta.6"
128
128
  },
129
129
  "failproofaidBinaries": {
130
- "linux-x64": "3039575242ff240c68a79c4ebd4e0e35e865da99c6d8a46d1a17b2d7962cb47b",
131
- "linux-arm64": "02fe993dca0faccb528c48472ca1d637274c562992ba7da1bc8d439103dbcc13",
132
- "darwin-x64": "2be5a422007a773253611f24f2ad9e2a301f075392ed7bc2dc0f6354d3186696",
133
- "darwin-arm64": "5882a4acd7317289346934290ac8277ca79edadb009e284c9fc0b23b74d321d7"
130
+ "linux-x64": "24118b8bd058cc5d8711053c4c77cdca9361c12dbf0f421a757d33f07596d428",
131
+ "linux-arm64": "b4278c34a288de2c747bcebcc623d8fd5bda3a1f1e55800926eb1a4b89f0a5e9",
132
+ "darwin-x64": "ecb66699f45f605b0d54ac9e4bac360ac11e4e136442dcccdd083514d77c7d42",
133
+ "darwin-arm64": "b3dbad58bda960ef9481f90bc5085ba8624b2472519cc0bf212a0e8451dd2db4"
134
134
  }
135
135
  }
@@ -82,7 +82,10 @@ function debug(msg: string): void {
82
82
  * NOT used for `tool_call`, `user_bash`, `input` or `agent_end`: those four
83
83
  * consume the decision, and blocking is what enforcement means.
84
84
  */
85
- function forwardPolicy(eventName: string, payload: unknown): void {
85
+ function forwardPolicy(
86
+ eventName: string,
87
+ payload: unknown,
88
+ ): void {
86
89
  const { cmd, args } = resolveSpawn();
87
90
  debug(`forwardPolicy (detached) event=${eventName} cmd=${cmd}`);
88
91
  try {
@@ -95,8 +98,8 @@ function forwardPolicy(eventName: string, payload: unknown): void {
95
98
  child.unref();
96
99
  // EPIPE if the child died before stdin was consumed — fail-open, silently.
97
100
  child.on("error", (err) => debug(`forward error ${err.message}`));
98
- child.stdin.on("error", () => {});
99
- child.stdin.end(JSON.stringify(payload));
101
+ child.stdin?.on("error", () => {});
102
+ child.stdin?.end(JSON.stringify(payload));
100
103
  } catch (err) {
101
104
  debug(`FORWARD EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
102
105
  }
@@ -121,8 +124,12 @@ function callPolicy(eventName: string, payload: unknown): { block: boolean; reas
121
124
  const parsed = JSON.parse(stdout) as PolicyDecision;
122
125
  if (parsed.permission === "deny") {
123
126
  debug(`DENY reason=${parsed.reason}`);
124
- return { block: true, reason: parsed.reason ?? "Blocked by failproofai" };
127
+ return {
128
+ block: true,
129
+ reason: parsed.reason ?? "Blocked by failproofai",
130
+ };
125
131
  }
132
+ return { block: false, reason: "" };
126
133
  } catch (err) {
127
134
  debug(`EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
128
135
  // Fail-open: never block tool execution because of an infra failure.
@@ -14,6 +14,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, openSync
14
14
  import { join } from "node:path";
15
15
  import { BUILTIN_POLICIES } from "../hooks/builtin-policies";
16
16
  import { AUDIT_DETECTORS } from "./detectors";
17
+ import { LEAK_SCAN_VERSION } from "./leak-scan";
17
18
  import type { TranscriptAuditResult, DetectorSessionState } from "./types";
18
19
  import { auditCacheDir } from "../hooks/fp-home";
19
20
 
@@ -162,6 +163,9 @@ interface CacheEntry {
162
163
  sizeBytes: number;
163
164
  engineVersion: string;
164
165
  detectorVersion: string;
166
+ /** Separate because leak detection is neither a builtin policy nor one of
167
+ * the behavioural AUDIT_DETECTORS hashed above. */
168
+ leakScanVersion: number;
165
169
  result: TranscriptAuditResult;
166
170
  /**
167
171
  * How far into the file the recorded result actually accounts for, at a LINE
@@ -269,6 +273,7 @@ export function readCachedTranscript(
269
273
  if (entry.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
270
274
  if (entry.engineVersion !== getEngineVersion()) return null;
271
275
  if (entry.detectorVersion !== getDetectorVersion()) return null;
276
+ if (entry.leakScanVersion !== LEAK_SCAN_VERSION) return null;
272
277
  // Number.isFinite (not typeof) so a malformed JSON `Infinity` /
273
278
  // `NaN` is rejected — those would otherwise bypass the TTL check
274
279
  // and pin a stale entry as valid forever.
@@ -332,6 +337,7 @@ export function writeCachedTranscriptResult(
332
337
  sizeBytes,
333
338
  engineVersion: getEngineVersion(),
334
339
  detectorVersion: getDetectorVersion(),
340
+ leakScanVersion: LEAK_SCAN_VERSION,
335
341
  result,
336
342
  };
337
343
  if (resume && resume.bytesScanned > 0) {
@@ -6,6 +6,7 @@
6
6
  * parses each via lib/log-entries.ts.
7
7
  */
8
8
  import { readFile, open, type FileHandle } from "node:fs/promises";
9
+ import { readFileSync } from "node:fs";
9
10
  import {
10
11
  listClaudeProjects,
11
12
  listClaudeTranscripts,
@@ -40,6 +41,17 @@ export async function listClaudeTranscriptMetadata(
40
41
  }
41
42
  for (const t of transcripts) {
42
43
  if (t.mtimeMs < sinceMs) continue;
44
+ let sessionDescription: string | undefined;
45
+ if (t.isSubagent) {
46
+ try {
47
+ const meta = JSON.parse(
48
+ readFileSync(t.transcriptPath.replace(/\.jsonl$/, ".meta.json"), "utf8"),
49
+ ) as { description?: unknown };
50
+ if (typeof meta.description === "string") sessionDescription = meta.description;
51
+ } catch {
52
+ // Sidecars are optional (journal transcripts do not have one).
53
+ }
54
+ }
43
55
  out.push({
44
56
  cli: "claude",
45
57
  projectName: project.name,
@@ -47,6 +59,7 @@ export async function listClaudeTranscriptMetadata(
47
59
  transcriptPath: t.transcriptPath,
48
60
  mtimeMs: t.mtimeMs,
49
61
  sizeBytes: t.sizeBytes,
62
+ sessionDescription,
50
63
  });
51
64
  }
52
65
  }
@@ -173,7 +173,10 @@ export async function ensureSignedIn(preset?: string): Promise<EnsureSignedIn> {
173
173
  throw new LoginError(
174
174
  "Signing in needs an interactive terminal, and this one is not.\n" +
175
175
  "Run `failproofai audit --schedule` from a shell you are sitting at,\n" +
176
- "or sign in from the dashboard — both write the same session file.",
176
+ "or sign in from the dashboard — both write the same session file.\n" +
177
+ "\n" +
178
+ "`--email` does not avoid this: it fills in the address so you are not\n" +
179
+ "asked for it, but a one-time code is still sent there for you to paste.",
177
180
  );
178
181
  }
179
182