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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
- package/.next/standalone/.next/server/app/audit/page.js +3 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -2
- 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]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_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 +4 -4
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__17xnxlx._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__1ckpmkk._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00-s7h8._.js → [root-of-the-server]__1fvfw59._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ui5ur8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_02b3i98._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0oq1dh6._.js → _04ng_6x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -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/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
- package/.next/standalone/.next/static/chunks/{1pb1oztsbwcss.js → 07c6l1lq5_69b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0o-hh5_turzlz.css → 0jpd8dv2vk930.css} +1 -1
- package/.next/standalone/.next/static/chunks/{32spub4wqjem-.js → 12bsgz8czvcnn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3m4upvybtrexd.js → 1gn08w2pgqh09.js} +1 -1
- package/.next/standalone/.next/static/chunks/{010bv1w6j171t.js → 1xgq22tri86an.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2aquitk72k2op.js → 20xkiqzvfkuvm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bi_1y0a_smt7.js → 2a84ug32tzik2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2ej3b8gk5ittu.js → 32m2ryfankf_2.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ioub_lpen-sq.js +1 -0
- package/.next/standalone/.next/static/chunks/{0wz8yftk18ts2.js → 3jxd133cw2vm3.js} +1 -1
- package/.next/standalone/.next/static/chunks/44ncu88eq6z71.js +1 -0
- package/.next/standalone/app/actions/get-leaks.ts +5 -1
- package/.next/standalone/app/actions/update-scheduled-audit.ts +4 -56
- package/.next/standalone/app/api/auth/status/route.ts +3 -2
- 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/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +13 -39
- package/.next/standalone/lib/auth/api-server-client.ts +6 -6
- package/.next/standalone/lib/codex-sessions.ts +20 -4
- package/.next/standalone/lib/copilot-sessions.ts +32 -1
- package/.next/standalone/lib/cursor-sessions.ts +62 -5
- package/.next/standalone/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 +1191 -1130
- package/dist/worker.mjs +198 -359
- package/lib/auth/api-server-client.ts +6 -6
- package/lib/codex-sessions.ts +20 -4
- package/lib/copilot-sessions.ts +32 -1
- package/lib/cursor-sessions.ts +62 -5
- package/lib/sqlite-reader.ts +78 -1
- package/package.json +10 -10
- package/pi-extension/index.ts +11 -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 +67 -51
- package/src/audit/harm-report.ts +12 -6
- package/src/audit/index.ts +151 -8
- package/src/audit/leak-fingerprint.ts +2 -4
- package/src/audit/leak-record.ts +13 -9
- package/src/audit/leak-scan.ts +20 -44
- package/src/audit/leak-store.ts +5 -2
- package/src/audit/machine-store.ts +8 -9
- package/src/audit/macos-notifier.ts +2 -2
- package/src/audit/redact-example.ts +62 -5
- package/src/audit/report-harm.ts +31 -46
- package/src/audit/schedule-cli.ts +17 -33
- package/src/audit/types.ts +11 -0
- package/src/hooks/configure-wizard.ts +28 -0
- package/src/hooks/fp-config.ts +29 -54
- package/src/hooks/handler.ts +1 -59
- package/src/hooks/integrations.ts +4 -1
- package/src/hooks/notice.ts +23 -154
- 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/04r6ch8uf_n8m.js +0 -1
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +0 -1
- package/src/audit/leak-notice.ts +0 -161
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{aKNv4Kmv98Xpwns0Xa-C- → aeM365dNTtUZOZvNkX8ba}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
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
|
-
/** `
|
|
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
|
|
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
|
|
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
|
|
310
|
+
* so a report cannot name where its email alert goes.
|
|
311
311
|
*/
|
|
312
312
|
export async function submitAuditReport(
|
|
313
313
|
accessToken: string,
|
package/lib/codex-sessions.ts
CHANGED
|
@@ -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
|
|
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:
|
|
301
|
+
content: toolOutputText(payload.output),
|
|
286
302
|
durationMs: duration,
|
|
287
303
|
durationFormatted: formatDuration(duration),
|
|
288
304
|
};
|
package/lib/copilot-sessions.ts
CHANGED
|
@@ -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
|
|
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),
|
package/lib/cursor-sessions.ts
CHANGED
|
@@ -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<{
|
|
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
|
|
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[] =
|
|
206
|
-
|
|
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",
|
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.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.
|
|
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.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": "
|
|
131
|
-
"linux-arm64": "
|
|
132
|
-
"darwin-x64": "
|
|
133
|
-
"darwin-arm64": "
|
|
130
|
+
"linux-x64": "24118b8bd058cc5d8711053c4c77cdca9361c12dbf0f421a757d33f07596d428",
|
|
131
|
+
"linux-arm64": "b4278c34a288de2c747bcebcc623d8fd5bda3a1f1e55800926eb1a4b89f0a5e9",
|
|
132
|
+
"darwin-x64": "ecb66699f45f605b0d54ac9e4bac360ac11e4e136442dcccdd083514d77c7d42",
|
|
133
|
+
"darwin-arm64": "b3dbad58bda960ef9481f90bc5085ba8624b2472519cc0bf212a0e8451dd2db4"
|
|
134
134
|
}
|
|
135
135
|
}
|
package/pi-extension/index.ts
CHANGED
|
@@ -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(
|
|
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
|
|
99
|
-
child.stdin
|
|
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 {
|
|
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.
|
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
|
|