failproofai 1.0.4-beta.7 → 1.0.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 +5 -5
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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 +4 -4
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +4 -37
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -15
- package/.next/standalone/.next/server/app/settings/page.js +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]__0v1hz15._.js → [externals]__0-9ti7z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1nb206a._.js → [externals]__0u3wrvq._.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/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_0otft92._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +10 -10
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1fvfw59._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12hbpha._.js → [root-of-the-server]__09flfsx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kokc8z._.js → [root-of-the-server]__0n_lxhg._.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]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dxf928._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n2txhk._.js → [root-of-the-server]__1ctpynv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_02yoeej._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0a0zlpr._.js → _04rbnaq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0s-77v5._.js → _0h_douw._.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 +14 -51
- package/.next/standalone/.next/server/chunks/ssr/{_07t3jol._.js → _1u8-lu2._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +3 -0
- 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 +69 -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/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.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 +5 -5
- 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 +21 -65
- package/.next/standalone/.next/static/chunks/{0f1pznnc7zlub.js → 0-tyrpwuc3lla.js} +1 -1
- package/.next/standalone/.next/static/chunks/0ca7octhod0g2.js +69 -0
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{1csubx9gwhsf6.js → 1j6o9min_6qa9.js} +1 -1
- package/.next/standalone/.next/static/chunks/27cl8h-c2w7cd.js +1 -0
- package/.next/standalone/.next/static/chunks/{0jpd8dv2vk930.css → 29-iqnp9v_axt.css} +1 -1
- package/.next/standalone/.next/static/chunks/{3uyhmm01g64k4.css → 2k9f4tyv04809.css} +0 -1
- package/.next/standalone/.next/static/chunks/{0t6ea7d3bkmdv.js → 2lwipmv5etc8m.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ntdfr2552x-h.js → 2u8vh73tpw28_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{15uon66hpwbgg.js → 31u94yukw9fz-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2pwfrn6_mw-1u.js → 3p9_8x5ezce-9.js} +1 -1
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{083fk73jtv9a0.js → 3tjji451hrgc0.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-3s3c-u_u_l0u9.js → turbopack-3p73obe0xmrxm.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +27 -51
- package/.next/standalone/app/actions/get-scheduled-audit.ts +0 -5
- package/.next/standalone/app/actions/update-scheduled-audit.ts +57 -18
- package/.next/standalone/app/api/auth/status/route.ts +2 -3
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +34 -140
- package/.next/standalone/app/audit/_components/audit-poster.tsx +10 -29
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -6
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -4
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +5 -19
- package/.next/standalone/app/audit/_components/share-templates.ts +44 -110
- package/.next/standalone/app/audit/audit-styles.css +1 -132
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +45 -50
- package/.next/standalone/lib/auth/api-server-client.ts +5 -33
- package/.next/standalone/lib/claude-sessions.ts +27 -101
- package/.next/standalone/lib/codex-sessions.ts +4 -20
- package/.next/standalone/lib/copilot-sessions.ts +1 -32
- package/.next/standalone/lib/cursor-sessions.ts +5 -62
- package/.next/standalone/lib/sqlite-reader.ts +1 -78
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -1
- package/dist/cli.mjs +500 -1762
- package/dist/worker.mjs +16 -138
- package/lib/auth/api-server-client.ts +5 -33
- package/lib/claude-sessions.ts +27 -101
- package/lib/codex-sessions.ts +4 -20
- package/lib/copilot-sessions.ts +1 -32
- package/lib/cursor-sessions.ts +5 -62
- package/lib/sqlite-reader.ts +1 -78
- package/package.json +9 -9
- package/pi-extension/index.ts +5 -50
- package/src/audit/cache.ts +0 -6
- package/src/audit/cli-adapters/claude.ts +0 -13
- package/src/audit/cli-login.ts +1 -4
- package/src/audit/cli.ts +7 -157
- package/src/audit/harm-report.ts +4 -111
- package/src/audit/index.ts +15 -286
- package/src/audit/machine-store.ts +9 -8
- package/src/audit/redact-example.ts +15 -324
- package/src/audit/report-harm.ts +37 -33
- package/src/audit/report.ts +3 -6
- package/src/audit/schedule-cli.ts +33 -54
- package/src/audit/scoring.ts +0 -49
- package/src/audit/types.ts +0 -52
- package/src/hooks/builtin-policies.ts +2 -67
- package/src/hooks/configure-wizard.ts +0 -55
- package/src/hooks/fp-config.ts +48 -83
- package/src/hooks/fp-home.ts +0 -27
- package/src/hooks/integrations.ts +14 -51
- package/src/hooks/uninstall-cli.ts +2 -17
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-4ja6q._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cr3xu._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04px77e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egr5s1._.js +0 -6
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0i0l1di._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0r3c1_0._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17xnxlx._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ckpmkk._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +0 -3
- package/.next/standalone/.next/static/chunks/0divvxng208xo.js +0 -1
- package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +0 -1
- package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +0 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -104
- package/.next/standalone/app/audit/_components/leak-section.tsx +0 -192
- package/src/audit/desktop-notify.ts +0 -420
- package/src/audit/leak-fingerprint.ts +0 -198
- package/src/audit/leak-record.ts +0 -248
- package/src/audit/leak-scan.ts +0 -268
- package/src/audit/leak-store.ts +0 -220
- package/src/audit/macos-notifier.ts +0 -298
- package/src/hooks/notice.ts +0 -24
- /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{YiPsVpjrc-pEQoN7XvhZ0 → dg8VQ7Ejlo0Qsh_x92oIt}/_ssgManifest.js +0 -0
package/lib/cursor-sessions.ts
CHANGED
|
@@ -127,15 +127,7 @@ 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<{
|
|
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
|
-
}>;
|
|
130
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
139
131
|
};
|
|
140
132
|
}
|
|
141
133
|
|
|
@@ -191,36 +183,11 @@ export async function parseCursorLog(
|
|
|
191
183
|
if (!raw.type && raw.role && raw.message?.content) {
|
|
192
184
|
const synthDate = new Date(SYNTH_T0 + i);
|
|
193
185
|
const synthTs = synthDate.toISOString();
|
|
194
|
-
const
|
|
195
|
-
const textParts = messageContent
|
|
186
|
+
const textParts = raw.message.content
|
|
196
187
|
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
197
188
|
.map((c) => c.text!)
|
|
198
189
|
.join("");
|
|
199
190
|
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
|
-
}
|
|
224
191
|
// Strip the synthesized `<timestamp>...</timestamp>\n<user_query>...\n</user_query>`
|
|
225
192
|
// wrapper Cursor adds for context — keep just the user_query body.
|
|
226
193
|
const queryMatch = /<user_query>\s*([\s\S]*?)\s*<\/user_query>/.exec(textParts);
|
|
@@ -231,37 +198,13 @@ export async function parseCursorLog(
|
|
|
231
198
|
...baseEntry(rawCopy, synthTs, synthDate, source),
|
|
232
199
|
message: { role: "user", content: text },
|
|
233
200
|
} satisfies UserEntry);
|
|
234
|
-
} else if (!attachedResult) {
|
|
235
|
-
entries.push({
|
|
236
|
-
type: "system",
|
|
237
|
-
...baseEntry(rawCopy, synthTs, synthDate, source),
|
|
238
|
-
raw: rawCopy,
|
|
239
|
-
} satisfies GenericEntry);
|
|
240
201
|
}
|
|
241
202
|
continue;
|
|
242
203
|
}
|
|
243
204
|
if (raw.role === "assistant") {
|
|
244
|
-
const blocks: ContentBlock[] =
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
}
|
|
205
|
+
const blocks: ContentBlock[] = textParts
|
|
206
|
+
? [{ type: "text", text: textParts }]
|
|
207
|
+
: [];
|
|
265
208
|
if (blocks.length === 0) {
|
|
266
209
|
entries.push({
|
|
267
210
|
type: "system",
|
package/lib/sqlite-reader.ts
CHANGED
|
@@ -39,42 +39,8 @@ 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
|
-
|
|
73
42
|
async function tryNodeSqlite(dbPath: string): Promise<SqliteReader | null> {
|
|
74
43
|
try {
|
|
75
|
-
// Before the import, not after: the warning fires on first use of the
|
|
76
|
-
// module, which is this line.
|
|
77
|
-
silenceSqliteExperimentalWarning();
|
|
78
44
|
// Dynamic import: on Node < 22.5 this rejects and we fall through to sql.js.
|
|
79
45
|
const mod = (await import("node:sqlite")) as unknown as NodeSqliteModule;
|
|
80
46
|
const db = new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
@@ -100,53 +66,10 @@ function loadSqlJs(): Promise<SqlJsStatic> {
|
|
|
100
66
|
return sqlPromise;
|
|
101
67
|
}
|
|
102
68
|
|
|
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
|
-
|
|
144
69
|
async function trySqlJs(dbPath: string): Promise<SqliteReader | null> {
|
|
145
70
|
try {
|
|
146
71
|
const SQL = await loadSqlJs();
|
|
147
|
-
|
|
148
|
-
// bytes cannot touch the file on disk.
|
|
149
|
-
const db = new SQL.Database(clearWalFlag(readFileSync(dbPath)));
|
|
72
|
+
const db = new SQL.Database(readFileSync(dbPath));
|
|
150
73
|
return {
|
|
151
74
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
|
|
152
75
|
const stmt = db.prepare(sql);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.4
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -121,15 +121,15 @@
|
|
|
121
121
|
"browserslist": "4.28.8"
|
|
122
122
|
},
|
|
123
123
|
"optionalDependencies": {
|
|
124
|
-
"@failproofai/failproofaid-linux-x64": "1.0.4
|
|
125
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.4
|
|
126
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.4
|
|
127
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.4
|
|
124
|
+
"@failproofai/failproofaid-linux-x64": "1.0.4",
|
|
125
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.4",
|
|
126
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.4",
|
|
127
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.4"
|
|
128
128
|
},
|
|
129
129
|
"failproofaidBinaries": {
|
|
130
|
-
"linux-x64": "
|
|
131
|
-
"linux-arm64": "
|
|
132
|
-
"darwin-x64": "
|
|
133
|
-
"darwin-arm64": "
|
|
130
|
+
"linux-x64": "cca1d41ff951784ba084bf2d72cfad0fe8d988f8226e0e7a4538b1fb8d76a5bd",
|
|
131
|
+
"linux-arm64": "55e9c1e2864cbf6057fd0c27e4bc573ca8fba2cd35d480a3db3a55d398e7bec0",
|
|
132
|
+
"darwin-x64": "2a46ec77d2cac14a036f35e832f92f16895239814a5f10f02121f3c313e70574",
|
|
133
|
+
"darwin-arm64": "a0e7ccc6fe24e1017592637192ba8a26c9d05ca839c98bc72a983094c863b39b"
|
|
134
134
|
}
|
|
135
135
|
}
|
package/pi-extension/index.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* spawns extensions with an undefined cwd contract, so paths are resolved
|
|
26
26
|
* relative to this file via `import.meta.url`, NOT process.cwd().
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
28
|
+
import { spawnSync } from "node:child_process";
|
|
29
29
|
import { resolve, dirname, join } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
@@ -64,47 +64,6 @@ function debug(msg: string): void {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Forward an event whose VERDICT NOBODY READS, without blocking Pi.
|
|
69
|
-
*
|
|
70
|
-
* `callPolicy` is `spawnSync`, and Pi awaits its handlers serially on the
|
|
71
|
-
* startup path — so a `session_start` forward froze the whole TUI for as long
|
|
72
|
-
* as the subprocess took (measured floor 1.0-1.6s just to boot the binary,
|
|
73
|
-
* ceiling the full 60s timeout) and then threw the answer away. Three call
|
|
74
|
-
* sites are like that by design: `session_start`, `tool_result` and
|
|
75
|
-
* `session_shutdown` forward for the activity feed and for session-id
|
|
76
|
-
* continuity, and Pi cannot act on their return in any case
|
|
77
|
-
* (`ToolResultEventResult` has no `block`; the other two have no Result type
|
|
78
|
-
* at all).
|
|
79
|
-
*
|
|
80
|
-
* So they get a detached `spawn` instead. The event still reaches failproofai,
|
|
81
|
-
* the policies still run and still log, and Pi keeps rendering. Deliberately
|
|
82
|
-
* NOT used for `tool_call`, `user_bash`, `input` or `agent_end`: those four
|
|
83
|
-
* consume the decision, and blocking is what enforcement means.
|
|
84
|
-
*/
|
|
85
|
-
function forwardPolicy(
|
|
86
|
-
eventName: string,
|
|
87
|
-
payload: unknown,
|
|
88
|
-
): void {
|
|
89
|
-
const { cmd, args } = resolveSpawn();
|
|
90
|
-
debug(`forwardPolicy (detached) event=${eventName} cmd=${cmd}`);
|
|
91
|
-
try {
|
|
92
|
-
const child = spawn(cmd, [...args, "--hook", eventName, "--cli", "pi"], {
|
|
93
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
94
|
-
detached: false,
|
|
95
|
-
});
|
|
96
|
-
// A child that outlives its parent's interest must not keep the event loop
|
|
97
|
-
// alive, or Pi cannot exit until the subprocess does.
|
|
98
|
-
child.unref();
|
|
99
|
-
// EPIPE if the child died before stdin was consumed — fail-open, silently.
|
|
100
|
-
child.on("error", (err) => debug(`forward error ${err.message}`));
|
|
101
|
-
child.stdin?.on("error", () => {});
|
|
102
|
-
child.stdin?.end(JSON.stringify(payload));
|
|
103
|
-
} catch (err) {
|
|
104
|
-
debug(`FORWARD EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
67
|
function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string } {
|
|
109
68
|
const { cmd, args } = resolveSpawn();
|
|
110
69
|
debug(`callPolicy event=${eventName} cmd=${cmd}`);
|
|
@@ -124,12 +83,8 @@ function callPolicy(eventName: string, payload: unknown): { block: boolean; reas
|
|
|
124
83
|
const parsed = JSON.parse(stdout) as PolicyDecision;
|
|
125
84
|
if (parsed.permission === "deny") {
|
|
126
85
|
debug(`DENY reason=${parsed.reason}`);
|
|
127
|
-
return {
|
|
128
|
-
block: true,
|
|
129
|
-
reason: parsed.reason ?? "Blocked by failproofai",
|
|
130
|
-
};
|
|
86
|
+
return { block: true, reason: parsed.reason ?? "Blocked by failproofai" };
|
|
131
87
|
}
|
|
132
|
-
return { block: false, reason: "" };
|
|
133
88
|
} catch (err) {
|
|
134
89
|
debug(`EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
|
|
135
90
|
// Fail-open: never block tool execution because of an infra failure.
|
|
@@ -462,7 +417,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
462
417
|
// need session_id continuity see the metadata.
|
|
463
418
|
pi.on("session_start", (event: unknown): unknown => {
|
|
464
419
|
const e = event as PiSessionStartEvent;
|
|
465
|
-
|
|
420
|
+
callPolicy("session_start", {
|
|
466
421
|
session_id: resolveSessionId(e.sessionId, resolveCwd(e.cwd)),
|
|
467
422
|
cwd: resolveCwd(e.cwd),
|
|
468
423
|
reason: e.reason,
|
|
@@ -480,7 +435,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
480
435
|
pi.on("tool_result", (event: unknown): unknown => {
|
|
481
436
|
const e = event as PiToolResultEvent;
|
|
482
437
|
const canonicalTool = canonicalizeToolName(e.toolName);
|
|
483
|
-
|
|
438
|
+
callPolicy("tool_result", {
|
|
484
439
|
tool_name: canonicalTool,
|
|
485
440
|
tool_input: canonicalizeToolInput(canonicalTool, e.input) ?? {},
|
|
486
441
|
tool_response: { content: e.content, isError: e.isError },
|
|
@@ -545,7 +500,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
545
500
|
pi.on("session_shutdown", (event: unknown): unknown => {
|
|
546
501
|
const e = event as PiSessionShutdownEvent;
|
|
547
502
|
const cwd = resolveCwd(e.cwd);
|
|
548
|
-
|
|
503
|
+
callPolicy("session_shutdown", {
|
|
549
504
|
session_id: resolveSessionId(e.sessionId, cwd),
|
|
550
505
|
cwd,
|
|
551
506
|
reason: e.reason,
|
package/src/audit/cache.ts
CHANGED
|
@@ -14,7 +14,6 @@ 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";
|
|
18
17
|
import type { TranscriptAuditResult, DetectorSessionState } from "./types";
|
|
19
18
|
import { auditCacheDir } from "../hooks/fp-home";
|
|
20
19
|
|
|
@@ -163,9 +162,6 @@ interface CacheEntry {
|
|
|
163
162
|
sizeBytes: number;
|
|
164
163
|
engineVersion: string;
|
|
165
164
|
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;
|
|
169
165
|
result: TranscriptAuditResult;
|
|
170
166
|
/**
|
|
171
167
|
* How far into the file the recorded result actually accounts for, at a LINE
|
|
@@ -273,7 +269,6 @@ export function readCachedTranscript(
|
|
|
273
269
|
if (entry.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
274
270
|
if (entry.engineVersion !== getEngineVersion()) return null;
|
|
275
271
|
if (entry.detectorVersion !== getDetectorVersion()) return null;
|
|
276
|
-
if (entry.leakScanVersion !== LEAK_SCAN_VERSION) return null;
|
|
277
272
|
// Number.isFinite (not typeof) so a malformed JSON `Infinity` /
|
|
278
273
|
// `NaN` is rejected — those would otherwise bypass the TTL check
|
|
279
274
|
// and pin a stale entry as valid forever.
|
|
@@ -337,7 +332,6 @@ export function writeCachedTranscriptResult(
|
|
|
337
332
|
sizeBytes,
|
|
338
333
|
engineVersion: getEngineVersion(),
|
|
339
334
|
detectorVersion: getDetectorVersion(),
|
|
340
|
-
leakScanVersion: LEAK_SCAN_VERSION,
|
|
341
335
|
result,
|
|
342
336
|
};
|
|
343
337
|
if (resume && resume.bytesScanned > 0) {
|
|
@@ -6,7 +6,6 @@
|
|
|
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";
|
|
10
9
|
import {
|
|
11
10
|
listClaudeProjects,
|
|
12
11
|
listClaudeTranscripts,
|
|
@@ -41,17 +40,6 @@ export async function listClaudeTranscriptMetadata(
|
|
|
41
40
|
}
|
|
42
41
|
for (const t of transcripts) {
|
|
43
42
|
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
|
-
}
|
|
55
43
|
out.push({
|
|
56
44
|
cli: "claude",
|
|
57
45
|
projectName: project.name,
|
|
@@ -59,7 +47,6 @@ export async function listClaudeTranscriptMetadata(
|
|
|
59
47
|
transcriptPath: t.transcriptPath,
|
|
60
48
|
mtimeMs: t.mtimeMs,
|
|
61
49
|
sizeBytes: t.sizeBytes,
|
|
62
|
-
sessionDescription,
|
|
63
50
|
});
|
|
64
51
|
}
|
|
65
52
|
}
|
package/src/audit/cli-login.ts
CHANGED
|
@@ -173,10 +173,7 @@ 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
|
|
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.",
|
|
176
|
+
"or sign in from the dashboard — both write the same session file.",
|
|
180
177
|
);
|
|
181
178
|
}
|
|
182
179
|
|
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
|
|
6
|
+
* failproofai audit --schedule Put scans on a timer and mail the findings.
|
|
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.
|
|
@@ -35,11 +35,6 @@ import { getInstanceId } from "../../lib/telemetry-id";
|
|
|
35
35
|
import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
|
|
36
36
|
import { openWhenReady } from "./open-browser";
|
|
37
37
|
import { describeOutcome, reportHarm } from "./report-harm";
|
|
38
|
-
import { notifyDesktop } from "./desktop-notify";
|
|
39
|
-
import { macNotifierInstalled, pruneMacNotifyQueue, queueMacNotification } from "./macos-notifier";
|
|
40
|
-
import { desktopLeakNotice, type DesktopLeakEmailState } from "../hooks/notice";
|
|
41
|
-
import { readConfig } from "../hooks/fp-config";
|
|
42
|
-
import { readAuth } from "../../lib/auth/auth-store";
|
|
43
38
|
import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM, helpScreen, helpOptsFor } from "../hooks/tui";
|
|
44
39
|
import { version } from "../../package.json";
|
|
45
40
|
|
|
@@ -97,36 +92,26 @@ export function helpText(): string {
|
|
|
97
92
|
{
|
|
98
93
|
command: "audit",
|
|
99
94
|
version,
|
|
100
|
-
tagline: "
|
|
95
|
+
tagline: "review your agent CLIs for risky and wasteful patterns",
|
|
101
96
|
sections: [
|
|
102
97
|
{
|
|
103
98
|
label: "usage",
|
|
104
99
|
entries: [
|
|
105
|
-
["(bare)", `Scan your session history
|
|
106
|
-
["--schedule [days]", "Scan on a timer and
|
|
100
|
+
["(bare)", `Scan your session history, then open http://localhost:${DASHBOARD_PORT}/audit`],
|
|
101
|
+
["--schedule [days]", "Scan on a timer and email the findings. Default 7 days, range 1-90. Signs you in the first time."],
|
|
107
102
|
// Its own row now, rather than a clause inside --schedule's. It
|
|
108
103
|
// does not stand alone, which is why it used to be a clause — but
|
|
109
104
|
// a clause wraps, and `--email <address>` landing with the flag at
|
|
110
105
|
// the end of one line and its placeholder at the start of the next
|
|
111
106
|
// is not a flag anybody can read or copy.
|
|
112
|
-
["--email <address>", "With --schedule,
|
|
107
|
+
["--email <address>", "With --schedule, skips the sign-in prompt."],
|
|
113
108
|
["--no-schedule", "Stop the timer. Leaves you signed in."],
|
|
114
|
-
// Its own pair of flags rather than a clause on --schedule: whether
|
|
115
|
-
// this machine scans and whether it may interrupt you are separate
|
|
116
|
-
// decisions, and nobody silencing a banner should have to re-state
|
|
117
|
-
// their schedule to do it.
|
|
118
|
-
["--notify", "Show a desktop notification when a scan finds a credential. On by default."],
|
|
119
|
-
["--no-notify", "Stop desktop notifications. Scheduled scans and optional email alerts are unaffected."],
|
|
120
109
|
["--status", "Whether scheduling is on, where reports go, the daemon's state, and when the next scan is due."],
|
|
121
110
|
["-h, --help", "Show this help."],
|
|
122
111
|
],
|
|
123
112
|
},
|
|
124
113
|
],
|
|
125
|
-
footer: [
|
|
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.",
|
|
129
|
-
],
|
|
114
|
+
footer: ["Everything runs on this machine; only a scheduled digest ever leaves it."],
|
|
130
115
|
},
|
|
131
116
|
helpOptsFor(process.stdout),
|
|
132
117
|
);
|
|
@@ -214,18 +199,6 @@ function startProgress(): Progress {
|
|
|
214
199
|
const lines = Array.from({ length: n }, (_, i) => lineFor(i));
|
|
215
200
|
// Move the cursor back up over the previously-drawn block, then clear and
|
|
216
201
|
// rewrite each line in place.
|
|
217
|
-
//
|
|
218
|
-
// INVARIANT: nothing else may write to the terminal while this is running.
|
|
219
|
-
// The cursor-up is a fixed count, so any stray line pushes the cursor down
|
|
220
|
-
// and the next redraw repaints the block lower — stranding the top of the
|
|
221
|
-
// old frame above it, which reads as the audit having run twice. That is
|
|
222
|
-
// not hypothetical: Node's "SQLite is an experimental feature" warning did
|
|
223
|
-
// exactly this on every first run, two lines of it, and it is why
|
|
224
|
-
// `lib/sqlite-reader.ts` now filters that one warning at the source.
|
|
225
|
-
//
|
|
226
|
-
// There is no defensive fix from inside here — erasing to end of screen
|
|
227
|
-
// still leaves the stranded lines ABOVE the cursor. Keep the terminal
|
|
228
|
-
// quiet instead.
|
|
229
202
|
if (printed) process.stdout.write(`\x1b[${n}A`);
|
|
230
203
|
process.stdout.write(lines.map((l) => `\x1b[2K${l}`).join("\n") + "\n");
|
|
231
204
|
printed = true;
|
|
@@ -361,116 +334,6 @@ function heldByLine(held: AuditLockInfo | null): string {
|
|
|
361
334
|
return `another audit is already running (pid ${held.pid}, started by ${held.source} ${ageS}s ago)`;
|
|
362
335
|
}
|
|
363
336
|
|
|
364
|
-
/**
|
|
365
|
-
* Raise a desktop banner for credentials this scan found and nothing has
|
|
366
|
-
* announced yet.
|
|
367
|
-
*
|
|
368
|
-
* Scheduled runs only, and the reason is the same one that keeps `reportHarm`
|
|
369
|
-
* here: an interactive `failproofai audit` prints its findings to a person who
|
|
370
|
-
* is already looking at them, so a banner on top is noise. The scheduled run is
|
|
371
|
-
* the one with nobody watching, which is exactly when a notification is the
|
|
372
|
-
* only thing that reaches anyone.
|
|
373
|
-
*
|
|
374
|
-
* Every failure is swallowed on purpose. This is an announcement about a
|
|
375
|
-
* completed scan, not part of the scan: a machine with no desktop, a locked
|
|
376
|
-
* session, a bus that hung — none of those make the audit itself a failure, and
|
|
377
|
-
* turning a good scan into exit 1 would make the scheduler back off from
|
|
378
|
-
* running the thing that actually matters.
|
|
379
|
-
*/
|
|
380
|
-
async function announceLeaksOnDesktop(
|
|
381
|
-
result: AuditResult,
|
|
382
|
-
report: Awaited<ReturnType<typeof reportHarm>>,
|
|
383
|
-
): Promise<void> {
|
|
384
|
-
try {
|
|
385
|
-
await announceLeaksOrThrow(result, report);
|
|
386
|
-
} catch (err) {
|
|
387
|
-
// The catch is not decoration. `notifyDesktop` documents that it never
|
|
388
|
-
// throws, but it is doing socket I/O, and so are the config read and the
|
|
389
|
-
// marker write beside it — depending on three modules' promises to keep a
|
|
390
|
-
// completed scan from reporting failure is a dependency this does not need
|
|
391
|
-
// to have.
|
|
392
|
-
process.stderr.write(
|
|
393
|
-
`failproofai: could not announce a leak on the desktop: ` +
|
|
394
|
-
`${err instanceof Error ? err.message : String(err)}\n`,
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
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 ?? [];
|
|
409
|
-
if (ids.length === 0) return;
|
|
410
|
-
// Read at fire time, not at scan time: the switch answers "should this
|
|
411
|
-
// interrupt me", and it is the user's most recent answer that counts.
|
|
412
|
-
if (!readConfig().audit.notify) return;
|
|
413
|
-
|
|
414
|
-
const n = ids.length === 1 ? "a possible credential exposure" : `${ids.length} possible credential exposures`;
|
|
415
|
-
// Fixed text with no scanned content and no count. A finding's own text comes
|
|
416
|
-
// from a repository this machine cloned, and notification bodies render
|
|
417
|
-
// markup on several Linux desktops. The count is intentionally left to the
|
|
418
|
-
// report too: a banner must not turn detector candidates into a claim that
|
|
419
|
-
// the user leaked N real credentials.
|
|
420
|
-
//
|
|
421
|
-
// No action buttons, deliberately. `Notify` supports them, but a server
|
|
422
|
-
// delivers the click back as an `ActionInvoked` signal to the sender — and
|
|
423
|
-
// this process exits as soon as the scan finishes, so the button would be
|
|
424
|
-
// dead. A plain command the user can copy beats a button that does nothing.
|
|
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.
|
|
448
|
-
process.stderr.write(
|
|
449
|
-
`failproofai: found ${n}; the notifier is not installed, so nothing will show it ` +
|
|
450
|
-
`(run \`failproofai config\`)\n`,
|
|
451
|
-
);
|
|
452
|
-
}
|
|
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
|
-
);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
337
|
// ── Headless (scheduled) audit ───────────────────────────────────────────────
|
|
475
338
|
|
|
476
339
|
/**
|
|
@@ -541,9 +404,7 @@ export async function runScheduledAudit(): Promise<number> {
|
|
|
541
404
|
`${num(result.transcripts.scanned)} sessions, ${num(result.totals.hits)} hits\n`,
|
|
542
405
|
);
|
|
543
406
|
|
|
544
|
-
//
|
|
545
|
-
// the desktop notification can truthfully say whether this particular
|
|
546
|
-
// alert was also emailed; every failure remains a non-fatal outcome.
|
|
407
|
+
// Report harmful findings upstream, if the user switched emailed reports on.
|
|
547
408
|
//
|
|
548
409
|
// AFTER the dashboard cache is written and AFTER the success line, because
|
|
549
410
|
// the scan is the product and this is an optional extra on top of it.
|
|
@@ -557,7 +418,6 @@ export async function runScheduledAudit(): Promise<number> {
|
|
|
557
418
|
// would also make the manual command do a network call that
|
|
558
419
|
// `audit --help` promises it does not.
|
|
559
420
|
const outcome = await reportHarm(result);
|
|
560
|
-
await announceLeaksOnDesktop(result, outcome);
|
|
561
421
|
const line = describeOutcome(outcome);
|
|
562
422
|
if (line) {
|
|
563
423
|
// Anything other than a successful send goes to stderr: on a scheduled run
|
|
@@ -692,16 +552,6 @@ export async function runAuditCli(args: string[]): Promise<void> {
|
|
|
692
552
|
process.exit(0);
|
|
693
553
|
}
|
|
694
554
|
|
|
695
|
-
for (const [flag, enable] of [["--notify", true], ["--no-notify", false]] as const) {
|
|
696
|
-
if (args.includes(flag)) {
|
|
697
|
-
const extra = args.find((a) => a !== flag);
|
|
698
|
-
if (extra) die(`\`audit ${flag}\` takes no other arguments (got: ${extra}).`);
|
|
699
|
-
const { runNotifyToggle } = await import("./schedule-cli");
|
|
700
|
-
runNotifyToggle(enable);
|
|
701
|
-
process.exit(0);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
555
|
if (args.includes("--no-schedule")) {
|
|
706
556
|
const extra = args.find((a) => a !== "--no-schedule");
|
|
707
557
|
if (extra) die(`\`audit --no-schedule\` takes no other arguments (got: ${extra}).`);
|