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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1lh7m5d._.js → [externals]__0v1hz15._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1rqkg_y._.js → [externals]__1nb206a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_08w6xzm._.js → _0otft92._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +3 -3
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yl9wb5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0guxnh-._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +13 -0
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
- package/.next/standalone/.next/static/chunks/{04r6ch8uf_n8m.js → 04o5zfl7p5lhx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0o-hh5_turzlz.css → 0jpd8dv2vk930.css} +1 -1
- package/.next/standalone/.next/static/chunks/{2ej3b8gk5ittu.js → 0po8n4tyotjf2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +1 -0
- package/.next/standalone/.next/static/chunks/{2aquitk72k2op.js → 17tfki9x5rd_7.js} +1 -1
- package/.next/standalone/.next/static/chunks/{010bv1w6j171t.js → 1i-i0pxg9q_8y.js} +1 -1
- package/.next/standalone/.next/static/chunks/{32spub4wqjem-.js → 2bxr2_2h4kaqq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0wz8yftk18ts2.js → 323cyb3a2s06y.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1pb1oztsbwcss.js → 364cqbpeg1y5i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3m4upvybtrexd.js → 38dqur43qthrl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bi_1y0a_smt7.js → 42gwu2bsz4ske.js} +2 -2
- package/.next/standalone/app/actions/get-leaks.ts +10 -1
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +29 -2
- package/.next/standalone/app/audit/_components/leak-section.tsx +15 -2
- package/.next/standalone/app/audit/audit-styles.css +18 -0
- package/.next/standalone/lib/sqlite-reader.ts +78 -1
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -1
- package/dist/cli.mjs +397 -225
- package/dist/worker.mjs +81 -35
- package/lib/sqlite-reader.ts +78 -1
- package/package.json +10 -10
- package/pi-extension/index.ts +19 -4
- package/src/audit/cache.ts +6 -0
- package/src/audit/cli-adapters/claude.ts +13 -0
- package/src/audit/cli-login.ts +4 -1
- package/src/audit/cli.ts +84 -59
- package/src/audit/index.ts +149 -8
- package/src/audit/leak-notice.ts +99 -1
- package/src/audit/leak-scan.ts +20 -44
- package/src/audit/leak-store.ts +49 -1
- package/src/audit/redact-example.ts +60 -5
- package/src/audit/types.ts +6 -0
- package/src/hooks/handler.ts +30 -10
- package/src/hooks/integrations.ts +14 -0
- package/src/hooks/notice.ts +45 -9
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +0 -23
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +0 -1
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → P6icI2V2PvVuePJrqjHCr}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → P6icI2V2PvVuePJrqjHCr}/_clientMiddlewareManifest.js +0 -0
- /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.
|
|
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}
|
|
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
|
|
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 {
|
|
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
|
|
5428
|
+
function sessionNoticePending(sessionId, home) {
|
|
5409
5429
|
try {
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
const
|
|
5413
|
-
|
|
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
|
|
5419
|
-
const won = [];
|
|
5448
|
+
function markSessionNoticed(sessionId, home) {
|
|
5420
5449
|
try {
|
|
5421
|
-
|
|
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
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
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
|
|
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"
|
|
10291
|
+
if (canonical !== "Stop")
|
|
10242
10292
|
return outcome;
|
|
10243
|
-
const pending =
|
|
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
|
-
|
|
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) {
|
package/lib/sqlite-reader.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
125
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.4-beta.
|
|
126
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.4-beta.
|
|
127
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.4-beta.
|
|
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": "
|
|
131
|
-
"linux-arm64": "
|
|
132
|
-
"darwin-x64": "
|
|
133
|
-
"darwin-arm64": "
|
|
130
|
+
"linux-x64": "4487790ed9b881da2914b5985a23b5adca1f20e226d71c635d8170064e6b2340",
|
|
131
|
+
"linux-arm64": "c903c5e201198213ccd8295fdab741c7d08d22c8dd5c276e52aa766595bedcc2",
|
|
132
|
+
"darwin-x64": "f0933584abbf0f9ff95cd153f8c6c704e467af1d6ffc8f42f9c426bd44b1142a",
|
|
133
|
+
"darwin-arm64": "387a3acbf13f667a557c8ac632fe67dee350cda078e3266e71edfcfacb9ecae6"
|
|
134
134
|
}
|
|
135
135
|
}
|
package/pi-extension/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/audit/cache.ts
CHANGED
|
@@ -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
|
}
|
package/src/audit/cli-login.ts
CHANGED
|
@@ -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
|
|