failproofai 1.0.4-beta.4 → 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.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/[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]__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]__1le9jqc._.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/_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/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]__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]__0yl9wb5._.js → [root-of-the-server]__1ui5ur8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0guxnh-._.js → _02b3i98._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +3 -0
- 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/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -14
- 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/{364cqbpeg1y5i.js → 07c6l1lq5_69b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bxr2_2h4kaqq.js → 12bsgz8czvcnn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{38dqur43qthrl.js → 1gn08w2pgqh09.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1i-i0pxg9q_8y.js → 1xgq22tri86an.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17tfki9x5rd_7.js → 20xkiqzvfkuvm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42gwu2bsz4ske.js → 2a84ug32tzik2.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0po8n4tyotjf2.js → 32m2ryfankf_2.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ioub_lpen-sq.js +1 -0
- package/.next/standalone/.next/static/chunks/{323cyb3a2s06y.js → 3jxd133cw2vm3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0qx7gav1c2_vu.js → 44ncu88eq6z71.js} +1 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -5
- 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/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/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +952 -1063
- package/dist/worker.mjs +173 -380
- 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/package.json +9 -9
- package/pi-extension/index.ts +10 -18
- package/src/audit/cli.ts +72 -81
- package/src/audit/harm-report.ts +12 -6
- package/src/audit/index.ts +2 -0
- package/src/audit/leak-fingerprint.ts +2 -4
- package/src/audit/leak-record.ts +13 -9
- package/src/audit/leak-scan.ts +1 -1
- package/src/audit/leak-store.ts +1 -46
- package/src/audit/machine-store.ts +8 -9
- package/src/audit/macos-notifier.ts +2 -2
- package/src/audit/redact-example.ts +2 -0
- package/src/audit/report-harm.ts +31 -46
- package/src/audit/schedule-cli.ts +17 -33
- package/src/audit/types.ts +5 -0
- package/src/hooks/configure-wizard.ts +28 -0
- package/src/hooks/fp-config.ts +29 -54
- package/src/hooks/handler.ts +1 -79
- package/src/hooks/integrations.ts +4 -15
- package/src/hooks/notice.ts +12 -179
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +0 -3
- package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +0 -1
- package/src/audit/leak-notice.ts +0 -259
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → 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/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",
|
|
@@ -121,15 +121,15 @@
|
|
|
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
|
@@ -49,7 +49,6 @@ function resolveSpawn(): { cmd: string; args: string[] } {
|
|
|
49
49
|
interface PolicyDecision {
|
|
50
50
|
permission?: "allow" | "deny";
|
|
51
51
|
reason?: string;
|
|
52
|
-
failproofaiNotice?: string;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
/**
|
|
@@ -83,7 +82,10 @@ function debug(msg: string): void {
|
|
|
83
82
|
* NOT used for `tool_call`, `user_bash`, `input` or `agent_end`: those four
|
|
84
83
|
* consume the decision, and blocking is what enforcement means.
|
|
85
84
|
*/
|
|
86
|
-
function forwardPolicy(
|
|
85
|
+
function forwardPolicy(
|
|
86
|
+
eventName: string,
|
|
87
|
+
payload: unknown,
|
|
88
|
+
): void {
|
|
87
89
|
const { cmd, args } = resolveSpawn();
|
|
88
90
|
debug(`forwardPolicy (detached) event=${eventName} cmd=${cmd}`);
|
|
89
91
|
try {
|
|
@@ -96,14 +98,14 @@ function forwardPolicy(eventName: string, payload: unknown): void {
|
|
|
96
98
|
child.unref();
|
|
97
99
|
// EPIPE if the child died before stdin was consumed — fail-open, silently.
|
|
98
100
|
child.on("error", (err) => debug(`forward error ${err.message}`));
|
|
99
|
-
child.stdin
|
|
100
|
-
child.stdin
|
|
101
|
+
child.stdin?.on("error", () => {});
|
|
102
|
+
child.stdin?.end(JSON.stringify(payload));
|
|
101
103
|
} catch (err) {
|
|
102
104
|
debug(`FORWARD EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string
|
|
108
|
+
function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string } {
|
|
107
109
|
const { cmd, args } = resolveSpawn();
|
|
108
110
|
debug(`callPolicy event=${eventName} cmd=${cmd}`);
|
|
109
111
|
try {
|
|
@@ -125,10 +127,9 @@ function callPolicy(eventName: string, payload: unknown): { block: boolean; reas
|
|
|
125
127
|
return {
|
|
126
128
|
block: true,
|
|
127
129
|
reason: parsed.reason ?? "Blocked by failproofai",
|
|
128
|
-
notice: parsed.failproofaiNotice,
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
|
-
return { block: false, reason: ""
|
|
132
|
+
return { block: false, reason: "" };
|
|
132
133
|
} catch (err) {
|
|
133
134
|
debug(`EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
|
|
134
135
|
// Fail-open: never block tool execution because of an infra failure.
|
|
@@ -376,13 +377,7 @@ interface PiBeforeAgentStartEvent {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
interface PiExtensionApi {
|
|
379
|
-
on(event: string, handler: (event: unknown
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
interface PiExtensionContext {
|
|
383
|
-
ui?: {
|
|
384
|
-
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
385
|
-
};
|
|
380
|
+
on(event: string, handler: (event: unknown) => unknown): void;
|
|
386
381
|
}
|
|
387
382
|
|
|
388
383
|
export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
@@ -503,7 +498,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
503
498
|
// The 5 require-*-before-stop builtins thus enforce by gating the NEXT
|
|
504
499
|
// user turn's system prompt rather than by retrying the same loop. If the
|
|
505
500
|
// user kills Pi between turns, the gate is missed — same bound Claude has.
|
|
506
|
-
pi.on("agent_end", (event: unknown
|
|
501
|
+
pi.on("agent_end", (event: unknown): unknown => {
|
|
507
502
|
const e = event as PiAgentEndEvent;
|
|
508
503
|
const cwd = resolveCwd(e.cwd);
|
|
509
504
|
const sessionId = resolveSessionId(e.sessionId, cwd);
|
|
@@ -516,9 +511,6 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
516
511
|
pendingStopBlockBySession.set(sessionId, decision.reason);
|
|
517
512
|
debug(`agent_end deny stored for session=${sessionId}`);
|
|
518
513
|
}
|
|
519
|
-
if (decision.notice) {
|
|
520
|
-
try { ctx?.ui?.notify(decision.notice, "warning"); } catch { /* fail-open */ }
|
|
521
|
-
}
|
|
522
514
|
return undefined;
|
|
523
515
|
});
|
|
524
516
|
|
package/src/audit/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the dashboard to view it.
|
|
4
4
|
*
|
|
5
5
|
* failproofai audit Scan, then launch the dashboard at /audit.
|
|
6
|
-
* failproofai audit --schedule Put scans on a timer
|
|
6
|
+
* failproofai audit --schedule Put local scans and notifications on a timer.
|
|
7
7
|
* failproofai audit --no-schedule Stop the timer.
|
|
8
8
|
* failproofai audit --status What this machine is scheduled to do.
|
|
9
9
|
* failproofai audit -h, --help Show usage.
|
|
@@ -37,10 +37,9 @@ import { openWhenReady } from "./open-browser";
|
|
|
37
37
|
import { describeOutcome, reportHarm } from "./report-harm";
|
|
38
38
|
import { notifyDesktop } from "./desktop-notify";
|
|
39
39
|
import { macNotifierInstalled, pruneMacNotifyQueue, queueMacNotification } from "./macos-notifier";
|
|
40
|
-
import {
|
|
41
|
-
import { desktopLeakNotice } from "../hooks/notice";
|
|
42
|
-
import { activeFindings, readLeakRecord } from "./leak-store";
|
|
40
|
+
import { desktopLeakNotice, type DesktopLeakEmailState } from "../hooks/notice";
|
|
43
41
|
import { readConfig } from "../hooks/fp-config";
|
|
42
|
+
import { readAuth } from "../../lib/auth/auth-store";
|
|
44
43
|
import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM, helpScreen, helpOptsFor } from "../hooks/tui";
|
|
45
44
|
import { version } from "../../package.json";
|
|
46
45
|
|
|
@@ -104,29 +103,29 @@ export function helpText(): string {
|
|
|
104
103
|
label: "usage",
|
|
105
104
|
entries: [
|
|
106
105
|
["(bare)", `Scan your session history for leaked keys, then open http://localhost:${DASHBOARD_PORT}/audit`],
|
|
107
|
-
["--schedule [days]", "Scan on a timer
|
|
106
|
+
["--schedule [days]", "Scan on a timer and notify you on this machine. Default 7 days, range 1-90. No account required."],
|
|
108
107
|
// Its own row now, rather than a clause inside --schedule's. It
|
|
109
108
|
// does not stand alone, which is why it used to be a clause — but
|
|
110
109
|
// a clause wraps, and `--email <address>` landing with the flag at
|
|
111
110
|
// the end of one line and its placeholder at the start of the next
|
|
112
111
|
// is not a flag anybody can read or copy.
|
|
113
|
-
["--email <address>", "With --schedule,
|
|
112
|
+
["--email <address>", "With --schedule, add email alerts for the most recent masked credential leak. A sign-in code is emailed to you."],
|
|
114
113
|
["--no-schedule", "Stop the timer. Leaves you signed in."],
|
|
115
114
|
// Its own pair of flags rather than a clause on --schedule: whether
|
|
116
115
|
// this machine scans and whether it may interrupt you are separate
|
|
117
116
|
// decisions, and nobody silencing a banner should have to re-state
|
|
118
117
|
// their schedule to do it.
|
|
119
118
|
["--notify", "Show a desktop notification when a scan finds a credential. On by default."],
|
|
120
|
-
["--no-notify", "Stop
|
|
119
|
+
["--no-notify", "Stop desktop notifications. Scheduled scans and optional email alerts are unaffected."],
|
|
121
120
|
["--status", "Whether scheduling is on, where reports go, the daemon's state, and when the next scan is due."],
|
|
122
121
|
["-h, --help", "Show this help."],
|
|
123
122
|
],
|
|
124
123
|
},
|
|
125
124
|
],
|
|
126
125
|
footer: [
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"and
|
|
126
|
+
"Every scan runs on this machine. Leak alerts appear as desktop system",
|
|
127
|
+
"notifications, never inside agent sessions. If you add email, only the",
|
|
128
|
+
"newest masked credential exposure and its metadata leave the machine.",
|
|
130
129
|
],
|
|
131
130
|
},
|
|
132
131
|
helpOptsFor(process.stdout),
|
|
@@ -378,9 +377,12 @@ function heldByLine(held: AuditLockInfo | null): string {
|
|
|
378
377
|
* turning a good scan into exit 1 would make the scheduler back off from
|
|
379
378
|
* running the thing that actually matters.
|
|
380
379
|
*/
|
|
381
|
-
async function announceLeaksOnDesktop(
|
|
380
|
+
async function announceLeaksOnDesktop(
|
|
381
|
+
result: AuditResult,
|
|
382
|
+
report: Awaited<ReturnType<typeof reportHarm>>,
|
|
383
|
+
): Promise<void> {
|
|
382
384
|
try {
|
|
383
|
-
await announceLeaksOrThrow(result);
|
|
385
|
+
await announceLeaksOrThrow(result, report);
|
|
384
386
|
} catch (err) {
|
|
385
387
|
// The catch is not decoration. `notifyDesktop` documents that it never
|
|
386
388
|
// throws, but it is doing socket I/O, and so are the config read and the
|
|
@@ -394,29 +396,22 @@ async function announceLeaksOnDesktop(result: AuditResult): Promise<void> {
|
|
|
394
396
|
}
|
|
395
397
|
}
|
|
396
398
|
|
|
397
|
-
async function announceLeaksOrThrow(
|
|
398
|
-
|
|
399
|
+
async function announceLeaksOrThrow(
|
|
400
|
+
result: AuditResult,
|
|
401
|
+
report: Awaited<ReturnType<typeof reportHarm>>,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
// `leakIds` includes credentials this scan encountered before. A scheduled
|
|
404
|
+
// run is a fresh safety check, so it must raise a fresh system notification
|
|
405
|
+
// when the leak is still present; the old per-finding marker is why only the
|
|
406
|
+
// first test run ever produced a banner. Older cached results fall back to
|
|
407
|
+
// first-seen ids.
|
|
408
|
+
const ids = result.leakIds ?? result.newLeakIds ?? [];
|
|
399
409
|
if (ids.length === 0) return;
|
|
400
410
|
// Read at fire time, not at scan time: the switch answers "should this
|
|
401
411
|
// interrupt me", and it is the user's most recent answer that counts.
|
|
402
412
|
if (!readConfig().audit.notify) return;
|
|
403
413
|
|
|
404
|
-
|
|
405
|
-
// because a process that dies mid-notify would otherwise re-announce the same
|
|
406
|
-
// finding on every scheduled run forever; desktop-only, because a banner the
|
|
407
|
-
// user may never have seen must not also silence the in-session notice.
|
|
408
|
-
const claimed = markLeakNoticeDelivered(ids, undefined, "desktop");
|
|
409
|
-
if (claimed.length === 0) return;
|
|
410
|
-
|
|
411
|
-
// Report what SURVIVED into the record, not how many ids the scan minted.
|
|
412
|
-
// `writeLeakRecord` prunes to MAX_FINDINGS, so on a large history the two
|
|
413
|
-
// numbers diverge wildly — a real run announced "5703 credentials" while the
|
|
414
|
-
// dashboard it points at showed 500. A banner that disagrees with the page it
|
|
415
|
-
// sends you to is worse than no banner: it teaches you the number is noise.
|
|
416
|
-
const live = new Set(activeFindings(readLeakRecord()).map((f) => f.id));
|
|
417
|
-
const shown = claimed.filter((id) => live.has(id)).length || claimed.length;
|
|
418
|
-
|
|
419
|
-
const n = shown === 1 ? "a possible credential exposure" : `${shown} possible credential exposures`;
|
|
414
|
+
const n = ids.length === 1 ? "a possible credential exposure" : `${ids.length} possible credential exposures`;
|
|
420
415
|
// Fixed text with no scanned content and no count. A finding's own text comes
|
|
421
416
|
// from a repository this machine cloned, and notification bodies render
|
|
422
417
|
// markup on several Linux desktops. The count is intentionally left to the
|
|
@@ -427,54 +422,52 @@ async function announceLeaksOrThrow(result: AuditResult): Promise<void> {
|
|
|
427
422
|
// delivers the click back as an `ActionInvoked` signal to the sender — and
|
|
428
423
|
// this process exits as soon as the scan finishes, so the button would be
|
|
429
424
|
// dead. A plain command the user can copy beats a button that does nothing.
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const outcome = await notifyDesktop(
|
|
459
|
-
title,
|
|
460
|
-
body,
|
|
461
|
-
// A stable id would let a repeat scan replace the previous bubble instead
|
|
462
|
-
// of stacking. We do not have one yet — the server assigns it and we would
|
|
463
|
-
// have to persist it — so a fresh notification is correct here, and it is
|
|
464
|
-
// bounded: at most one per distinct credential, ever.
|
|
465
|
-
0,
|
|
466
|
-
);
|
|
467
|
-
delivered = outcome.ok;
|
|
468
|
-
if (!outcome.ok) {
|
|
469
|
-
// stderr, so it lands in the journal beside the run it belongs to. Not a
|
|
470
|
-
// failure of the audit — but "we found a key and could not tell you" is
|
|
471
|
-
// exactly the sentence someone debugging this needs to find.
|
|
425
|
+
const emailState: DesktopLeakEmailState =
|
|
426
|
+
report.kind === "sent"
|
|
427
|
+
? "sent"
|
|
428
|
+
: report.kind === "signed-out" || !readAuth()
|
|
429
|
+
? "missing"
|
|
430
|
+
: report.kind === "failed"
|
|
431
|
+
? "failed"
|
|
432
|
+
: "held";
|
|
433
|
+
const { title, body } = desktopLeakNotice(emailState);
|
|
434
|
+
|
|
435
|
+
// The two platforms need opposite things, and the split is not cosmetic. On
|
|
436
|
+
// Linux this process can reach the session bus itself. On macOS it cannot
|
|
437
|
+
// reach Notification Center at all — it is a child of a LaunchDaemon, outside
|
|
438
|
+
// the GUI session — so it hands the message to an agent that lives inside
|
|
439
|
+
// one. See `macos-notifier.ts` for why that agent exists.
|
|
440
|
+
if (process.platform === "darwin") {
|
|
441
|
+
pruneMacNotifyQueue();
|
|
442
|
+
if (!queueMacNotification(ids[0], title, body)) {
|
|
443
|
+
process.stderr.write(`failproofai: found ${n} but could not queue a notification\n`);
|
|
444
|
+
} else if (!macNotifierInstalled()) {
|
|
445
|
+
// Queued, but nothing currently collects it. The payload is durable and
|
|
446
|
+
// will be consumed when the LaunchAgent returns.
|
|
447
|
+
// Worth a line, because the immediate symptom is otherwise silence.
|
|
472
448
|
process.stderr.write(
|
|
473
|
-
`failproofai: found ${n}
|
|
449
|
+
`failproofai: found ${n}; the notifier is not installed, so nothing will show it ` +
|
|
450
|
+
`(run \`failproofai config\`)\n`,
|
|
474
451
|
);
|
|
475
452
|
}
|
|
476
|
-
|
|
477
|
-
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const outcome = await notifyDesktop(
|
|
457
|
+
title,
|
|
458
|
+
body,
|
|
459
|
+
// `0` asks the notification server for a fresh id. Each scheduled scan that
|
|
460
|
+
// still sees a leak is a new reminder, rather than replacing or suppressing
|
|
461
|
+
// the first banner forever.
|
|
462
|
+
0,
|
|
463
|
+
);
|
|
464
|
+
if (!outcome.ok) {
|
|
465
|
+
// stderr, so it lands in the journal beside the run it belongs to. Not a
|
|
466
|
+
// failure of the audit — but "we found a key and could not tell you" is
|
|
467
|
+
// exactly the sentence someone debugging this needs to find.
|
|
468
|
+
process.stderr.write(
|
|
469
|
+
`failproofai: found ${n} but could not raise a desktop notification (${outcome.reason})\n`,
|
|
470
|
+
);
|
|
478
471
|
}
|
|
479
472
|
}
|
|
480
473
|
|
|
@@ -548,12 +541,9 @@ export async function runScheduledAudit(): Promise<number> {
|
|
|
548
541
|
`${num(result.transcripts.scanned)} sessions, ${num(result.totals.hits)} hits\n`,
|
|
549
542
|
);
|
|
550
543
|
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
await announceLeaksOnDesktop(result);
|
|
555
|
-
|
|
556
|
-
// Report harmful findings upstream, if the user switched emailed reports on.
|
|
544
|
+
// Email is optional and never controls the local scan. Run it first only so
|
|
545
|
+
// the desktop notification can truthfully say whether this particular
|
|
546
|
+
// alert was also emailed; every failure remains a non-fatal outcome.
|
|
557
547
|
//
|
|
558
548
|
// AFTER the dashboard cache is written and AFTER the success line, because
|
|
559
549
|
// the scan is the product and this is an optional extra on top of it.
|
|
@@ -567,6 +557,7 @@ export async function runScheduledAudit(): Promise<number> {
|
|
|
567
557
|
// would also make the manual command do a network call that
|
|
568
558
|
// `audit --help` promises it does not.
|
|
569
559
|
const outcome = await reportHarm(result);
|
|
560
|
+
await announceLeaksOnDesktop(result, outcome);
|
|
570
561
|
const line = describeOutcome(outcome);
|
|
571
562
|
if (line) {
|
|
572
563
|
// Anything other than a successful send goes to stderr: on a scheduled run
|
package/src/audit/harm-report.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turning an audit result into a harm report the api-server can act on.
|
|
3
3
|
*
|
|
4
|
-
* Runs only after a SCHEDULED scan (`failproofai audit --scheduled`)
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Runs only after a SCHEDULED scan (`failproofai audit --scheduled`) and only
|
|
5
|
+
* ever from the audit child — never the daemon, which holds no human credential
|
|
6
|
+
* precisely so that refresh rotation stays inside the audit lock. Without an
|
|
7
|
+
* email identity the scan and desktop notification still work; this module
|
|
8
|
+
* simply reports `signed-out` and sends nothing.
|
|
8
9
|
*
|
|
9
10
|
* ## What counts as harm
|
|
10
11
|
*
|
|
@@ -302,7 +303,10 @@ export function selectLeaks(findings: LeakFinding[], from: Date, to: Date): Repo
|
|
|
302
303
|
// is now, not where it was first noticed.
|
|
303
304
|
// Defensive even though `readLeakRecord` already sanitises: this is
|
|
304
305
|
// exported and takes whatever a caller hands it.
|
|
305
|
-
const seen = f.sightings?.
|
|
306
|
+
const seen = f.sightings?.reduce((latest, candidate) => {
|
|
307
|
+
if (!latest) return candidate;
|
|
308
|
+
return (ts(candidate.at) ?? 0) >= (ts(latest.at) ?? 0) ? candidate : latest;
|
|
309
|
+
}, undefined as LeakFinding["sightings"][number] | undefined);
|
|
306
310
|
out.push({
|
|
307
311
|
id: f.id,
|
|
308
312
|
display: f.fingerprint?.display ?? "[credential]",
|
|
@@ -356,6 +360,8 @@ export function buildHarmReport(
|
|
|
356
360
|
window_from: from.toISOString(),
|
|
357
361
|
window_to: windowTo.toISOString(),
|
|
358
362
|
harmful: selectHarmful(result, from, windowTo, { includeUnplaceable: isFirstReport }),
|
|
359
|
-
|
|
363
|
+
// One email, one actionable credential. Sending every row turns a leak
|
|
364
|
+
// alert into a backlog digest and obscures the event that just happened.
|
|
365
|
+
leaks: selectLeaks(leakFindings, from, windowTo).slice(0, 1),
|
|
360
366
|
};
|
|
361
367
|
}
|