failproofai 1.0.4-beta.2 → 1.0.4-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) 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.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
  31. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/.next/server/app/index.html +1 -1
  34. package/.next/standalone/.next/server/app/index.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  36. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  39. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/{[externals]__1lh7m5d._.js → [externals]__0v1hz15._.js} +1 -1
  58. package/.next/standalone/.next/server/chunks/{[externals]__1rqkg_y._.js → [externals]__1nb206a._.js} +1 -1
  59. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/{_08w6xzm._.js → _0otft92._.js} +2 -2
  65. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +3 -3
  68. package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yl9wb5._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/_0guxnh-._.js +23 -0
  83. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +13 -0
  86. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  92. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  93. package/.next/standalone/.next/server/pages/404.html +1 -1
  94. package/.next/standalone/.next/server/pages/500.html +1 -1
  95. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  96. package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
  97. package/.next/standalone/.next/static/chunks/{04r6ch8uf_n8m.js → 04o5zfl7p5lhx.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{0o-hh5_turzlz.css → 0jpd8dv2vk930.css} +1 -1
  99. package/.next/standalone/.next/static/chunks/{2ej3b8gk5ittu.js → 0po8n4tyotjf2.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{2aquitk72k2op.js → 17tfki9x5rd_7.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{010bv1w6j171t.js → 1i-i0pxg9q_8y.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/{32spub4wqjem-.js → 2bxr2_2h4kaqq.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{0wz8yftk18ts2.js → 323cyb3a2s06y.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{1pb1oztsbwcss.js → 364cqbpeg1y5i.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{3m4upvybtrexd.js → 38dqur43qthrl.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/{2bi_1y0a_smt7.js → 42gwu2bsz4ske.js} +2 -2
  108. package/.next/standalone/app/actions/get-leaks.ts +10 -1
  109. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +29 -2
  110. package/.next/standalone/app/audit/_components/leak-section.tsx +15 -2
  111. package/.next/standalone/app/audit/audit-styles.css +18 -0
  112. package/.next/standalone/lib/sqlite-reader.ts +78 -1
  113. package/.next/standalone/package.json +10 -10
  114. package/.next/standalone/server.js +1 -1
  115. package/bin/failproofai.mjs +1 -1
  116. package/dist/cli.mjs +397 -225
  117. package/dist/worker.mjs +81 -35
  118. package/lib/sqlite-reader.ts +78 -1
  119. package/package.json +10 -10
  120. package/pi-extension/index.ts +19 -4
  121. package/src/audit/cache.ts +6 -0
  122. package/src/audit/cli-adapters/claude.ts +13 -0
  123. package/src/audit/cli-login.ts +4 -1
  124. package/src/audit/cli.ts +84 -59
  125. package/src/audit/index.ts +149 -8
  126. package/src/audit/leak-notice.ts +99 -1
  127. package/src/audit/leak-scan.ts +20 -44
  128. package/src/audit/leak-store.ts +49 -1
  129. package/src/audit/redact-example.ts +60 -5
  130. package/src/audit/types.ts +6 -0
  131. package/src/hooks/handler.ts +30 -10
  132. package/src/hooks/integrations.ts +14 -0
  133. package/src/hooks/notice.ts +45 -9
  134. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +0 -3
  135. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +0 -23
  136. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +0 -1
  137. /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → P6icI2V2PvVuePJrqjHCr}/_buildManifest.js +0 -0
  138. /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → P6icI2V2PvVuePJrqjHCr}/_clientMiddlewareManifest.js +0 -0
  139. /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → P6icI2V2PvVuePJrqjHCr}/_ssgManifest.js +0 -0
package/dist/worker.mjs CHANGED
@@ -3507,7 +3507,7 @@ function parsePackPolicyName(canonicalName) {
3507
3507
  }
3508
3508
 
3509
3509
  // package.json
3510
- var version = "1.0.4-beta.2";
3510
+ var version = "1.0.4-beta.4";
3511
3511
  var init_package = () => {};
3512
3512
 
3513
3513
  // src/posthog-key.ts
@@ -5241,6 +5241,20 @@ function shapeNotice(cli, notice, existingStdout = "") {
5241
5241
  return empty;
5242
5242
  return { stdout: text + `
5243
5243
  `, stderr: "" };
5244
+ case "shim": {
5245
+ if (existingStdout.trim().startsWith("{")) {
5246
+ try {
5247
+ const parsed = JSON.parse(existingStdout);
5248
+ parsed.failproofaiNotice = text;
5249
+ return { stdout: JSON.stringify(parsed), stderr: "" };
5250
+ } catch {
5251
+ return empty;
5252
+ }
5253
+ }
5254
+ if (existingStdout.trim())
5255
+ return empty;
5256
+ return { stdout: JSON.stringify({ failproofaiNotice: text }), stderr: "" };
5257
+ }
5244
5258
  default:
5245
5259
  return empty;
5246
5260
  }
@@ -5249,9 +5263,9 @@ function canDeliverNotice(cli) {
5249
5263
  return !!cli && (NOTICE_CHANNEL[cli] ?? "none") !== "none";
5250
5264
  }
5251
5265
  function leakNoticeText(newFindings) {
5252
- const n = newFindings === 1 ? "a credential" : `${newFindings} credentials`;
5266
+ const n = newFindings === 1 ? "a possible credential exposure" : `${newFindings} possible credential exposures`;
5253
5267
  return `failproofai found ${n} in this project's agent transcripts.
5254
- ` + "Run `failproofai audit` to see the details, " + "or `failproofai audit --schedule` to get these by email.";
5268
+ ` + "Run `failproofai audit` to review the matches and rotate anything real.";
5255
5269
  }
5256
5270
  var NOTICE_CHANNEL, MAX_NOTICE_CHARS = 900;
5257
5271
  var init_notice = __esm(() => {
@@ -5259,7 +5273,9 @@ var init_notice = __esm(() => {
5259
5273
  claude: "systemMessage",
5260
5274
  codex: "systemMessage",
5261
5275
  copilot: "stderr",
5262
- factory: "stdout"
5276
+ factory: "stdout",
5277
+ opencode: "shim",
5278
+ pi: "shim"
5263
5279
  };
5264
5280
  });
5265
5281
 
@@ -5335,7 +5351,11 @@ function readJson(path) {
5335
5351
  function readLeakIdentity(home) {
5336
5352
  const existing = readJson(auditLeakIdentityFile(home));
5337
5353
  if (existing && typeof existing.salt === "string" && existing.salt.length >= 32) {
5338
- return { salt: existing.salt, dismissed: existing.dismissed ?? [] };
5354
+ return {
5355
+ salt: existing.salt,
5356
+ dismissed: existing.dismissed ?? [],
5357
+ reportViewedAt: typeof existing.reportViewedAt === "number" && Number.isFinite(existing.reportViewedAt) ? existing.reportViewedAt : undefined
5358
+ };
5339
5359
  }
5340
5360
  const minted = { salt: randomBytes2(32).toString("hex"), dismissed: [] };
5341
5361
  writeLeakIdentity(minted, home);
@@ -5405,33 +5425,39 @@ var init_leak_store = __esm(() => {
5405
5425
  // src/audit/leak-notice.ts
5406
5426
  import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync3, statSync as statSync7, writeFileSync as writeFileSync5, rmSync as rmSync3 } from "node:fs";
5407
5427
  import { resolve as resolve8 } from "node:path";
5408
- function pendingLeakNotice(home, channel = "cli") {
5428
+ function sessionNoticePending(sessionId, home) {
5409
5429
  try {
5410
- const dir = NOTICE_DIR(home, channel);
5411
- const claimed = new Set(existsSync10(dir) ? readdirSync3(dir) : []);
5412
- const ids = activeFindings(readLeakRecord(home), home).map((f) => f.id).filter((id) => !claimed.has(id));
5413
- return { ids, count: ids.length };
5430
+ if (!sessionId || !/^[\w.:-]{1,200}$/.test(sessionId))
5431
+ return { ids: [], count: 0 };
5432
+ const dir = resolve8(auditDir(home), "notified-sessions");
5433
+ if (existsSync10(resolve8(dir, sessionId)))
5434
+ return { ids: [], count: 0 };
5435
+ const findings = activeFindings(readLeakRecord(home), home);
5436
+ if (findings.length === 0)
5437
+ return { ids: [], count: 0 };
5438
+ const viewedAt = readLeakIdentity(home).reportViewedAt ?? 0;
5439
+ const unviewed = findings.filter((f) => {
5440
+ const seen = Date.parse(f.firstSeen);
5441
+ return !Number.isFinite(seen) || seen > viewedAt;
5442
+ });
5443
+ return { ids: unviewed.map((f) => f.id), count: unviewed.length };
5414
5444
  } catch {
5415
5445
  return { ids: [], count: 0 };
5416
5446
  }
5417
5447
  }
5418
- function markLeakNoticeDelivered(ids, home, channel = "cli") {
5419
- const won = [];
5448
+ function markSessionNoticed(sessionId, home) {
5420
5449
  try {
5421
- const dir = NOTICE_DIR(home, channel);
5450
+ if (!sessionId || !/^[\w.:-]{1,200}$/.test(sessionId))
5451
+ return false;
5452
+ const dir = resolve8(auditDir(home), "notified-sessions");
5422
5453
  mkdirSync7(dir, { recursive: true });
5423
- for (const id of ids) {
5424
- if (!isFindingId(id))
5425
- continue;
5426
- try {
5427
- writeFileSync5(resolve8(dir, id), "", { flag: "wx", mode: 384 });
5428
- won.push(id);
5429
- } catch {}
5430
- }
5431
- } catch {}
5432
- return won;
5454
+ writeFileSync5(resolve8(dir, sessionId), "", { flag: "wx", mode: 384 });
5455
+ return true;
5456
+ } catch {
5457
+ return false;
5458
+ }
5433
5459
  }
5434
- var NOTICE_DIR = (home, channel = "cli") => resolve8(auditDir(home), channel === "cli" ? "notified" : `notified-${channel}`), MARKER_TTL_MS;
5460
+ var MARKER_TTL_MS;
5435
5461
  var init_leak_notice = __esm(() => {
5436
5462
  init_fp_home();
5437
5463
  init_leak_store();
@@ -6130,8 +6156,21 @@ var init_cursor_projects = __esm(() => {
6130
6156
  // lib/sqlite-reader.ts
6131
6157
  import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
6132
6158
  import initSqlJs from "sql.js/dist/sql-asm.js";
6159
+ function silenceSqliteExperimentalWarning() {
6160
+ if (warningPatched)
6161
+ return;
6162
+ warningPatched = true;
6163
+ const original = process.emitWarning.bind(process);
6164
+ process.emitWarning = (warning, ...rest) => {
6165
+ const text = typeof warning === "string" ? warning : String(warning?.message ?? "");
6166
+ if (text.includes("SQLite is an experimental feature"))
6167
+ return;
6168
+ return original(warning, ...rest);
6169
+ };
6170
+ }
6133
6171
  async function tryNodeSqlite(dbPath) {
6134
6172
  try {
6173
+ silenceSqliteExperimentalWarning();
6135
6174
  const mod = await import("node:sqlite");
6136
6175
  const db = new mod.DatabaseSync(dbPath, { readOnly: true });
6137
6176
  return {
@@ -6152,10 +6191,21 @@ function loadSqlJs() {
6152
6191
  sqlPromise = initSqlJs();
6153
6192
  return sqlPromise;
6154
6193
  }
6194
+ function clearWalFlag(bytes) {
6195
+ if (bytes.length < 20)
6196
+ return bytes;
6197
+ if (bytes.toString("latin1", 0, 16) !== SQLITE_MAGIC)
6198
+ return bytes;
6199
+ if (bytes[18] === 1 && bytes[19] === 1)
6200
+ return bytes;
6201
+ bytes[18] = 1;
6202
+ bytes[19] = 1;
6203
+ return bytes;
6204
+ }
6155
6205
  async function trySqlJs(dbPath) {
6156
6206
  try {
6157
6207
  const SQL = await loadSqlJs();
6158
- const db = new SQL.Database(readFileSync7(dbPath));
6208
+ const db = new SQL.Database(clearWalFlag(readFileSync7(dbPath)));
6159
6209
  return {
6160
6210
  query(sql, params) {
6161
6211
  const stmt = db.prepare(sql);
@@ -6183,7 +6233,7 @@ async function openSqliteReadonly(dbPath) {
6183
6233
  return null;
6184
6234
  return await tryNodeSqlite(dbPath) ?? await trySqlJs(dbPath);
6185
6235
  }
6186
- var sqlPromise = null;
6236
+ var warningPatched = false, sqlPromise = null, SQLITE_MAGIC = "SQLite format 3\x00";
6187
6237
  var init_sqlite_reader = () => {};
6188
6238
 
6189
6239
  // lib/opencode-db.ts
@@ -10231,22 +10281,22 @@ function canonicalizeEventType(raw, cli) {
10231
10281
  }
10232
10282
  return raw;
10233
10283
  }
10234
- function attachLeakNotice(eventType, cli, outcome) {
10284
+ function attachLeakNotice(eventType, cli, outcome, sessionId) {
10235
10285
  try {
10236
10286
  if (!canDeliverNotice(cli))
10237
10287
  return outcome;
10238
10288
  if (!cli)
10239
10289
  return outcome;
10240
10290
  const canonical = canonicalizeEventType(eventType, cli);
10241
- if (canonical !== "Stop" && canonical !== "SessionStart")
10291
+ if (canonical !== "Stop")
10242
10292
  return outcome;
10243
- const pending = pendingLeakNotice();
10293
+ const pending = sessionNoticePending(sessionId ?? "");
10244
10294
  if (pending.count === 0)
10245
10295
  return outcome;
10246
10296
  const shaped = shapeNotice(cli, leakNoticeText(pending.count), outcome.stdout);
10247
10297
  if (shaped.stdout === outcome.stdout && !shaped.stderr)
10248
10298
  return outcome;
10249
- markLeakNoticeDelivered(pending.ids);
10299
+ markSessionNoticed(sessionId ?? "");
10250
10300
  return {
10251
10301
  exitCode: outcome.exitCode,
10252
10302
  stdout: shaped.stdout,
@@ -10558,11 +10608,7 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
10558
10608
  }
10559
10609
  } catch {}
10560
10610
  }
10561
- const withNotice = attachLeakNotice(eventType, cli, {
10562
- exitCode: result.exitCode,
10563
- stdout: result.stdout,
10564
- stderr: result.stderr
10565
- });
10611
+ const withNotice = attachLeakNotice(eventType, cli, { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, sessionId);
10566
10612
  return withNotice;
10567
10613
  } finally {
10568
10614
  if (opts?.awaitTelemetryFlush ?? true) {
@@ -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.4",
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.4",
125
+ "@failproofai/failproofaid-linux-arm64": "1.0.4-beta.4",
126
+ "@failproofai/failproofaid-darwin-x64": "1.0.4-beta.4",
127
+ "@failproofai/failproofaid-darwin-arm64": "1.0.4-beta.4"
128
128
  },
129
129
  "failproofaidBinaries": {
130
- "linux-x64": "3039575242ff240c68a79c4ebd4e0e35e865da99c6d8a46d1a17b2d7962cb47b",
131
- "linux-arm64": "02fe993dca0faccb528c48472ca1d637274c562992ba7da1bc8d439103dbcc13",
132
- "darwin-x64": "2be5a422007a773253611f24f2ad9e2a301f075392ed7bc2dc0f6354d3186696",
133
- "darwin-arm64": "5882a4acd7317289346934290ac8277ca79edadb009e284c9fc0b23b74d321d7"
130
+ "linux-x64": "4487790ed9b881da2914b5985a23b5adca1f20e226d71c635d8170064e6b2340",
131
+ "linux-arm64": "c903c5e201198213ccd8295fdab741c7d08d22c8dd5c276e52aa766595bedcc2",
132
+ "darwin-x64": "f0933584abbf0f9ff95cd153f8c6c704e467af1d6ffc8f42f9c426bd44b1142a",
133
+ "darwin-arm64": "387a3acbf13f667a557c8ac632fe67dee350cda078e3266e71edfcfacb9ecae6"
134
134
  }
135
135
  }
@@ -49,6 +49,7 @@ function resolveSpawn(): { cmd: string; args: string[] } {
49
49
  interface PolicyDecision {
50
50
  permission?: "allow" | "deny";
51
51
  reason?: string;
52
+ failproofaiNotice?: string;
52
53
  }
53
54
 
54
55
  /**
@@ -102,7 +103,7 @@ function forwardPolicy(eventName: string, payload: unknown): void {
102
103
  }
103
104
  }
104
105
 
105
- function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string } {
106
+ function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string; notice?: string } {
106
107
  const { cmd, args } = resolveSpawn();
107
108
  debug(`callPolicy event=${eventName} cmd=${cmd}`);
108
109
  try {
@@ -121,8 +122,13 @@ function callPolicy(eventName: string, payload: unknown): { block: boolean; reas
121
122
  const parsed = JSON.parse(stdout) as PolicyDecision;
122
123
  if (parsed.permission === "deny") {
123
124
  debug(`DENY reason=${parsed.reason}`);
124
- return { block: true, reason: parsed.reason ?? "Blocked by failproofai" };
125
+ return {
126
+ block: true,
127
+ reason: parsed.reason ?? "Blocked by failproofai",
128
+ notice: parsed.failproofaiNotice,
129
+ };
125
130
  }
131
+ return { block: false, reason: "", notice: parsed.failproofaiNotice };
126
132
  } catch (err) {
127
133
  debug(`EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
128
134
  // Fail-open: never block tool execution because of an infra failure.
@@ -370,7 +376,13 @@ interface PiBeforeAgentStartEvent {
370
376
  }
371
377
 
372
378
  interface PiExtensionApi {
373
- on(event: string, handler: (event: unknown) => unknown): void;
379
+ on(event: string, handler: (event: unknown, ctx?: PiExtensionContext) => unknown): void;
380
+ }
381
+
382
+ interface PiExtensionContext {
383
+ ui?: {
384
+ notify(message: string, type?: "info" | "warning" | "error"): void;
385
+ };
374
386
  }
375
387
 
376
388
  export default function failproofaiBridge(pi: PiExtensionApi) {
@@ -491,7 +503,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
491
503
  // The 5 require-*-before-stop builtins thus enforce by gating the NEXT
492
504
  // user turn's system prompt rather than by retrying the same loop. If the
493
505
  // user kills Pi between turns, the gate is missed — same bound Claude has.
494
- pi.on("agent_end", (event: unknown): unknown => {
506
+ pi.on("agent_end", (event: unknown, ctx?: PiExtensionContext): unknown => {
495
507
  const e = event as PiAgentEndEvent;
496
508
  const cwd = resolveCwd(e.cwd);
497
509
  const sessionId = resolveSessionId(e.sessionId, cwd);
@@ -504,6 +516,9 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
504
516
  pendingStopBlockBySession.set(sessionId, decision.reason);
505
517
  debug(`agent_end deny stored for session=${sessionId}`);
506
518
  }
519
+ if (decision.notice) {
520
+ try { ctx?.ui?.notify(decision.notice, "warning"); } catch { /* fail-open */ }
521
+ }
507
522
  return undefined;
508
523
  });
509
524
 
@@ -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