failproofai 1.0.4-beta.4 → 1.0.4-beta.7
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 +15 -4
- 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/[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 +11 -11
- 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]__0yl9wb5._.js → [root-of-the-server]__12hbpha._.js} +2 -2
- 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/_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/{_0guxnh-._.js → _0a0zlpr._.js} +2 -2
- 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_builtin-policies_ts_09j2ndl._.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 +33 -22
- package/.next/standalone/.next/static/chunks/{17tfki9x5rd_7.js → 083fk73jtv9a0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1i-i0pxg9q_8y.js → 0divvxng208xo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{38dqur43qthrl.js → 0f1pznnc7zlub.js} +1 -1
- package/.next/standalone/.next/static/chunks/{323cyb3a2s06y.js → 0ntdfr2552x-h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0po8n4tyotjf2.js → 0t6ea7d3bkmdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{364cqbpeg1y5i.js → 15uon66hpwbgg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42gwu2bsz4ske.js → 1csubx9gwhsf6.js} +2 -2
- package/.next/standalone/.next/static/chunks/27du9n7ssja7l.js +1 -0
- package/.next/standalone/.next/static/chunks/2h_pcb1y8_jde.js +1 -0
- package/.next/standalone/.next/static/chunks/{2bxr2_2h4kaqq.js → 2pwfrn6_mw-1u.js} +1 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -5
- package/.next/standalone/app/actions/get-scheduled-audit.ts +5 -0
- package/.next/standalone/app/actions/update-scheduled-audit.ts +18 -57
- package/.next/standalone/app/api/auth/status/route.ts +3 -2
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +1 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -1
- package/.next/standalone/app/audit/_components/leak-section.tsx +1 -1
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +50 -45
- 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 +991 -1101
- package/dist/worker.mjs +175 -382
- 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 +3 -1
- package/src/audit/leak-fingerprint.ts +2 -4
- package/src/audit/leak-record.ts +26 -10
- 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 +56 -68
- 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/builtin-policies.ts +2 -2
- package/src/hooks/configure-wizard.ts +36 -1
- 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/src/hooks/uninstall-cli.ts +3 -3
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +0 -3
- package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +0 -1
- package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +0 -1
- package/src/audit/leak-notice.ts +0 -259
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → YiPsVpjrc-pEQoN7XvhZ0}/_ssgManifest.js +0 -0
package/src/hooks/notice.ts
CHANGED
|
@@ -1,191 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Putting a short message in front of a HUMAN, inside the agent they are using.
|
|
3
|
-
*
|
|
4
|
-
* A notice is not a verdict. `allow` / `deny` / `instruct` all answer "what
|
|
5
|
-
* happens to this tool call"; a notice answers nothing — it is one line of text
|
|
6
|
-
* for the person, delivered alongside whatever the verdict turned out to be.
|
|
7
|
-
* That is why it is a separate field rather than a fourth decision: shaping it
|
|
8
|
-
* into stdout beside a verdict ad hoc is how the copilot nested-vs-flat
|
|
9
|
-
* `additionalContext` bug happened.
|
|
10
|
-
*
|
|
11
|
-
* ## Why the channel differs per CLI, and why it is not `additionalContext`
|
|
12
|
-
*
|
|
13
|
-
* Measured across all 12 CLIs by live probe. The obvious channel —
|
|
14
|
-
* `hookSpecificOutput.additionalContext` — reaches the MODEL, not the user, and
|
|
15
|
-
* a model is a paraphraser: over eight real Claude turns it mentioned an
|
|
16
|
-
* informational notice 8/8, but an earlier notice that asked the agent to *act*
|
|
17
|
-
* was disclaimed as a suspected prompt injection 6/6 ("instructions arriving
|
|
18
|
-
* that way aren't something I'll act on unasked"). A security notice routed
|
|
19
|
-
* through something that may decline to repeat it is not a delivered notice.
|
|
20
|
-
*
|
|
21
|
-
* So each CLI gets the channel that was proven to paint text on the user's
|
|
22
|
-
* terminal, and the ones with no such channel get nothing rather than a
|
|
23
|
-
* pretend delivery:
|
|
24
|
-
*
|
|
25
|
-
* claude `{"systemMessage":…}` renders `Stop says: …`; model-invisible
|
|
26
|
-
* (a neutral probe answered `NONE`)
|
|
27
|
-
* codex `{"systemMessage":…}` renders `• Stop (completed) says: …`
|
|
28
|
-
* copilot exit 0 + stderr renders as a `!` session warning
|
|
29
|
-
* factory plain stdout renders under a `Hooks Stop` block
|
|
30
|
-
*
|
|
31
|
-
* opencode and pi have real channels too, but they live inside the plugin shims
|
|
32
|
-
* we ship rather than in a hook's stdout, so a private JSON field is consumed
|
|
33
|
-
* there and handed to `showToast` / `ui.notify`. OpenClaw, cursor, devin, goose,
|
|
34
|
-
* antigravity and hermes have no verified channel that reaches a human without
|
|
35
|
-
* an unsupported escape hatch — they get nothing here, and their users get the
|
|
36
|
-
* finding from `failproofai audit` and the scheduled desktop notification.
|
|
37
|
-
*/
|
|
38
|
-
import type { IntegrationType } from "./types";
|
|
39
|
-
|
|
40
|
-
/** How a notice is delivered on one CLI. */
|
|
41
|
-
type NoticeChannel = "systemMessage" | "stderr" | "stdout" | "shim" | "none";
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Per-CLI channel, from live probes rather than documentation.
|
|
45
|
-
*
|
|
46
|
-
* A CLI absent from this map delivers nothing — deliberately. Guessing a
|
|
47
|
-
* channel produces the worst outcome available: output that looks delivered
|
|
48
|
-
* from our side and reaches nobody, so we would report a leak as "notified"
|
|
49
|
-
* that the user never saw.
|
|
50
|
-
*/
|
|
51
|
-
const NOTICE_CHANNEL: Partial<Record<IntegrationType, NoticeChannel>> = {
|
|
52
|
-
claude: "systemMessage",
|
|
53
|
-
codex: "systemMessage",
|
|
54
|
-
copilot: "stderr",
|
|
55
|
-
factory: "stdout",
|
|
56
|
-
opencode: "shim",
|
|
57
|
-
pi: "shim",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Claude truncates `systemMessage` at 4000 characters on the cloud-forwarding
|
|
62
|
-
* path, and a notice that gets cut mid-sentence reads as a broken tool. Ours is
|
|
63
|
-
* two lines by design; this is a backstop, not a budget.
|
|
64
|
-
*/
|
|
65
|
-
const MAX_NOTICE_CHARS = 900;
|
|
66
|
-
|
|
67
|
-
export interface ShapedNotice {
|
|
68
|
-
/** Merged into the outcome's stdout, or empty. */
|
|
69
|
-
stdout: string;
|
|
70
|
-
/** Merged into the outcome's stderr, or empty. */
|
|
71
|
-
stderr: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Render a notice for one CLI, merging it into any verdict JSON already bound
|
|
76
|
-
* for stdout.
|
|
77
|
-
*
|
|
78
|
-
* `existingStdout` matters: a Stop-gate deny already emits a JSON object on
|
|
79
|
-
* several CLIs, and emitting a second object would produce two JSON documents
|
|
80
|
-
* on one stream — which every host parses as a syntax error and drops, taking
|
|
81
|
-
* the deny with it. So the notice is merged INTO that object when there is one.
|
|
82
|
-
*/
|
|
83
|
-
export function shapeNotice(
|
|
84
|
-
cli: IntegrationType | undefined,
|
|
85
|
-
notice: string,
|
|
86
|
-
existingStdout = "",
|
|
87
|
-
): ShapedNotice {
|
|
88
|
-
const empty: ShapedNotice = { stdout: existingStdout, stderr: "" };
|
|
89
|
-
if (!notice.trim() || !cli) return empty;
|
|
90
|
-
|
|
91
|
-
const channel = NOTICE_CHANNEL[cli] ?? "none";
|
|
92
|
-
const text = notice.length > MAX_NOTICE_CHARS
|
|
93
|
-
? notice.slice(0, MAX_NOTICE_CHARS - 1) + "…"
|
|
94
|
-
: notice;
|
|
95
|
-
|
|
96
|
-
switch (channel) {
|
|
97
|
-
case "systemMessage": {
|
|
98
|
-
// Merge into the verdict object when one exists, so the stream stays a
|
|
99
|
-
// single JSON document.
|
|
100
|
-
if (existingStdout.trim().startsWith("{")) {
|
|
101
|
-
try {
|
|
102
|
-
const parsed = JSON.parse(existingStdout) as Record<string, unknown>;
|
|
103
|
-
parsed.systemMessage = text;
|
|
104
|
-
return { stdout: JSON.stringify(parsed), stderr: "" };
|
|
105
|
-
} catch {
|
|
106
|
-
// Not the JSON we thought it was — leave the verdict untouched
|
|
107
|
-
// rather than risk destroying it to add a courtesy message.
|
|
108
|
-
return empty;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// Anything already on stdout that we could not merge into is left
|
|
112
|
-
// exactly as it is, and the notice is dropped instead. The alternative —
|
|
113
|
-
// emitting our own object over the top — DESTROYS it: measured with a
|
|
114
|
-
// JSON array on stdout, which does not start with `{`, so it fell past
|
|
115
|
-
// the merge and was replaced wholesale. A courtesy message is never worth
|
|
116
|
-
// more than whatever the host was already being told.
|
|
117
|
-
if (existingStdout.trim()) return empty;
|
|
118
|
-
return { stdout: JSON.stringify({ systemMessage: text }), stderr: "" };
|
|
119
|
-
}
|
|
120
|
-
case "stderr":
|
|
121
|
-
// Exit code is NOT touched: on copilot a non-zero exit from a tool event
|
|
122
|
-
// is a block, and a notice must never block anything.
|
|
123
|
-
return { stdout: existingStdout, stderr: text + "\n" };
|
|
124
|
-
case "stdout":
|
|
125
|
-
// factory renders plain text under its Hooks block. Only safe when no
|
|
126
|
-
// verdict JSON is already on the stream.
|
|
127
|
-
if (existingStdout.trim()) return empty;
|
|
128
|
-
return { stdout: text + "\n", stderr: "" };
|
|
129
|
-
case "shim": {
|
|
130
|
-
// OpenCode and Pi are in-process plugin integrations. Their generated
|
|
131
|
-
// shims consume this private field and call the host's real user-facing
|
|
132
|
-
// notification API; putting the text in additionalContext would send it
|
|
133
|
-
// to the model instead.
|
|
134
|
-
if (existingStdout.trim().startsWith("{")) {
|
|
135
|
-
try {
|
|
136
|
-
const parsed = JSON.parse(existingStdout) as Record<string, unknown>;
|
|
137
|
-
parsed.failproofaiNotice = text;
|
|
138
|
-
return { stdout: JSON.stringify(parsed), stderr: "" };
|
|
139
|
-
} catch {
|
|
140
|
-
return empty;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (existingStdout.trim()) return empty;
|
|
144
|
-
return { stdout: JSON.stringify({ failproofaiNotice: text }), stderr: "" };
|
|
145
|
-
}
|
|
146
|
-
default:
|
|
147
|
-
return empty;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** True when this CLI has a channel that was proven to reach a human. */
|
|
152
|
-
export function canDeliverNotice(cli: IntegrationType | undefined): boolean {
|
|
153
|
-
return !!cli && (NOTICE_CHANNEL[cli] ?? "none") !== "none";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* The notice itself. A FIXED template with no interpolation of scanned content.
|
|
158
|
-
*
|
|
159
|
-
* The count is a number we computed; everything else is a constant. That is not
|
|
160
|
-
* stylistic: the alternative is interpolating a finding's `example`, which is
|
|
161
|
-
* the verbatim text of a command from a repository — attacker-controlled
|
|
162
|
-
* through a README, a Makefile target or an npm script. Tested against the
|
|
163
|
-
* shipped redactor, `echo "IGNORE ALL PRIOR INSTRUCTIONS…"` passes through
|
|
164
|
-
* byte-identical, because that redactor masks secrets, not instructions. On the
|
|
165
|
-
* CLIs where the only channel is the model's context, that string would land
|
|
166
|
-
* directly in the prompt. So nothing scanned ever reaches this template.
|
|
167
|
-
*/
|
|
168
|
-
export function leakNoticeText(newFindings: number): string {
|
|
169
|
-
const n = newFindings === 1
|
|
170
|
-
? "a possible credential exposure"
|
|
171
|
-
: `${newFindings} possible credential exposures`;
|
|
172
|
-
return (
|
|
173
|
-
`failproofai found ${n} in this project's agent transcripts.\n` +
|
|
174
|
-
"Run `failproofai audit` to review the matches and rotate anything real."
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
1
|
/** Copy for the OS notification raised by an unattended scheduled scan.
|
|
179
2
|
*
|
|
180
3
|
* Deliberately omits the count. A burst of generated fixtures can hit the
|
|
181
4
|
* record cap, and a desktop banner saying "500 leaked credentials" states a
|
|
182
5
|
* conclusion the detector cannot support. The dashboard carries the evidence;
|
|
183
6
|
* the banner's job is only to ask for review. */
|
|
184
|
-
export
|
|
7
|
+
export type DesktopLeakEmailState = "sent" | "missing" | "failed" | "held";
|
|
8
|
+
|
|
9
|
+
export function desktopLeakNotice(email: DesktopLeakEmailState = "held"): { title: string; body: string } {
|
|
10
|
+
const followUp =
|
|
11
|
+
email === "sent"
|
|
12
|
+
? "A masked alert was also sent to your email."
|
|
13
|
+
: email === "missing"
|
|
14
|
+
? "Add email alerts with: failproofai audit --schedule --email you@example.com"
|
|
15
|
+
: email === "failed"
|
|
16
|
+
? "Email delivery failed this time; the local result is still available."
|
|
17
|
+
: "Email was not sent for this run.";
|
|
185
18
|
return {
|
|
186
19
|
title: "failproofai audit needs review",
|
|
187
20
|
body:
|
|
188
21
|
"Possible credential exposure was found in your agent transcripts. " +
|
|
189
|
-
|
|
22
|
+
`Run failproofai audit to verify the matches and rotate anything real. ${followUp}`,
|
|
190
23
|
};
|
|
191
24
|
}
|
|
@@ -193,6 +193,7 @@ export async function runUninstallCommand(opts: UninstallOptions = {}): Promise<
|
|
|
193
193
|
if (found.daemonConfigured) {
|
|
194
194
|
plan.push(` • stop requiring the daemon (policies go back to evaluating in-process)`);
|
|
195
195
|
}
|
|
196
|
+
let removeDaemon = opts.purge === true;
|
|
196
197
|
if (found.serviceInstalled) {
|
|
197
198
|
plan.push(
|
|
198
199
|
opts.purge || opts.yes
|
|
@@ -305,8 +306,7 @@ export async function runUninstallCommand(opts: UninstallOptions = {}): Promise<
|
|
|
305
306
|
//
|
|
306
307
|
// So the only case that keeps it is an INTERACTIVE run where the person said no
|
|
307
308
|
// (or could not be asked).
|
|
308
|
-
|
|
309
|
-
opts.purge || opts.yes ? true : ((await opts.confirmDaemon?.()) ?? false);
|
|
309
|
+
removeDaemon = opts.purge || opts.yes ? true : ((await opts.confirmDaemon?.()) ?? false);
|
|
310
310
|
|
|
311
311
|
if (!removeDaemon) {
|
|
312
312
|
lines.push(
|
|
@@ -357,7 +357,7 @@ export async function runUninstallCommand(opts: UninstallOptions = {}): Promise<
|
|
|
357
357
|
// No-ops on every other platform, and needs no elevation on macOS: a per-user
|
|
358
358
|
// agent is the user's own to load and unload. Tied to the daemon's removal
|
|
359
359
|
// because it exists only to deliver what the daemon's scheduled audit finds.
|
|
360
|
-
if (opts.purge ||
|
|
360
|
+
if (opts.purge || removeDaemon) {
|
|
361
361
|
uninstallMacNotifier();
|
|
362
362
|
}
|
|
363
363
|
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
module.exports=[74863,a=>{"use strict";var b=a.i(36359),c=a.i(37936),d=a.i(15501),e=a.i(13095);async function f(){let a=(0,d.readDashboardCache)();if(!a){let a=(0,d.readDashboardCacheMeta)();return{status:"empty",expired:null!==a,expiredAt:a?.cachedAt??null}}return{status:"cached",cachedAt:a.cachedAt,params:a.params,result:a.result}}(0,e.ensureServerEntryExports)([f]),(0,c.registerServerReference)(f,"0048814305f7563284b0e6413ac4ea842889a1d6eb",null);var g=a.i(66680),h=a.i(2157),i=a.i(50227),j=a.i(75818),k=a.i(8674);function l(a){try{if(!(0,h.existsSync)(a))return null;return JSON.parse((0,h.readFileSync)(a,"utf8"))}catch{return null}}function m(a){let b=l((0,j.auditLeakIdentityFile)(a));if(b&&"string"==typeof b.salt&&b.salt.length>=32)return{salt:b.salt,dismissed:b.dismissed??[],reportViewedAt:"number"==typeof b.reportViewedAt&&Number.isFinite(b.reportViewedAt)?b.reportViewedAt:void 0};let c={salt:(0,g.randomBytes)(32).toString("hex"),dismissed:[]};return n(c,a),c}function n(a,b){let c=(0,j.auditLeakIdentityFile)(b);try{return(0,h.mkdirSync)((0,i.dirname)(c),{recursive:!0}),(0,k.writeJsonAtomically)(c,a,{mode:384}),!0}catch{return!1}}function o(a){var b;if(!a||"object"!=typeof a||Array.isArray(a)||"string"!=typeof a.id||(b=a.id,!/^[0-9a-f]{16}$/.test(b)))return null;let c=a.fingerprint;if(!c||"object"!=typeof c||"string"!=typeof c.display||"string"!=typeof c.label)return null;let d=Array.isArray(a.sightings)?a.sightings.filter(a=>{if(!a||"object"!=typeof a)return!1;let b=a.mechanism;return!!b&&"object"==typeof b&&"string"==typeof b.summary}):[];return{...a,fingerprint:{display:c.display,label:c.label,length:"number"==typeof c.length&&Number.isFinite(c.length)?c.length:0,attributed:!0===c.attributed},name:"string"==typeof a.name?a.name:null,firstSeen:"string"==typeof a.firstSeen?a.firstSeen:"",lastSeen:"string"==typeof a.lastSeen?a.lastSeen:"",occurrences:"number"==typeof a.occurrences&&Number.isFinite(a.occurrences)?a.occurrences:d.length,sightings:d}}var p=a.i(21572);async function q(){var a;let b,c=(a=function(){let a=m(void 0),b=l((0,j.auditLeaksFile)(void 0));if(!b||1!==b.schemaVersion||!Array.isArray(b.findings))return{schemaVersion:1,salt:a.salt,updatedAt:new Date().toISOString(),findings:[]};let c=b.findings.map(o).filter(a=>null!==a);return{...b,findings:c,salt:a.salt}}(),b=new Set(m(void 0).dismissed),a.findings.filter(a=>!b.has(a.id)).sort((a,b)=>a.lastSeen<b.lastSeen?1:a.lastSeen>b.lastSeen?-1:0));!function(a=Date.now()){try{let b=m(void 0);(0,k.writeJsonAtomically)((0,j.auditLeakIdentityFile)(void 0),{...b,reportViewedAt:a},{mode:384})}catch{}}();let d=c.map(a=>{let b=a.sightings?.[a.sightings.length-1];return{id:a.id,display:a.fingerprint?.display??"[credential]",label:a.fingerprint?.label??"secret",length:a.fingerprint?.length??0,attributed:a.fingerprint?.attributed===!0,name:a.name,cli:b?.cli??"unknown",project:b?.cwd??"unknown",lastSeen:a.lastSeen,firstSeen:a.firstSeen,mechanism:b?.mechanism?.summary??"seen in a transcript",direction:b?.mechanism?.direction??"result",occurrences:a.occurrences,sessions:new Set((a.sightings??[]).map(a=>a.sessionId)).size}});return d.sort((a,b)=>Date.parse(b.lastSeen)-Date.parse(a.lastSeen)),{rows:d,notify:(0,p.readConfig)().audit.notify}}async function r(a){let b;return!!(b=m(void 0)).dismissed.includes(a)||(b.dismissed.push(a),n(b,void 0))}async function s(a){try{return(0,p.updateConfig)({audit:{notify:a}}),!0}catch{return!1}}(0,e.ensureServerEntryExports)([q,r,s]),(0,c.registerServerReference)(q,"00fd330814bcefc936e301ec49d66099ed614ecf67",null),(0,c.registerServerReference)(r,"4097c062c6b1d13b1d6f2a64af27881249bbf9d989",null),(0,c.registerServerReference)(s,"409ed106c4be852cd917c107309fc8fd09d79a0beb",null),a.s([],37550),a.i(37550),a.s(["000c85ddbcc0c76485115f8fdbd1792f16d26c97bb",()=>b.getTelemetryConfig,"0048814305f7563284b0e6413ac4ea842889a1d6eb",0,f,"00fd330814bcefc936e301ec49d66099ed614ecf67",0,q,"4097c062c6b1d13b1d6f2a64af27881249bbf9d989",0,r,"409ed106c4be852cd917c107309fc8fd09d79a0beb",0,s],74863)},2726,a=>{a.v(b=>Promise.all(["server/chunks/ssr/[externals]__0vmspq5._.js","server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js"].map(b=>a.l(b))).then(()=>b(74493)))}];
|
|
2
|
-
|
|
3
|
-
//# sourceMappingURL=_0oq1dh6._.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,86616,29054,e=>{"use strict";function t(e,a={},s=15e3){let n=AbortSignal.timeout(s),i=a.signal?AbortSignal.any([a.signal,n]):n;return fetch(e,{...a,signal:i})}function a(e){if(null===e||"object"!=typeof e)return!1;let t=e.name;return"AbortError"===t||"TimeoutError"===t}e.s(["fetchWithTimeout",0,t,"isAbortError",0,a],29054);class s extends Error{kind;constructor(e,t){super(t),this.kind=e,this.name="RerunError"}}async function n(e){try{let a=await t("/api/audit/run",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({cli:e.cli.length>0?e.cli:void 0,since:"all"===e.since?void 0:e.since,noCache:!!e.noCache||void 0})});if(!a.ok&&409!==a.status){let e=await a.text().catch(()=>"");throw console.error("audit run failed:",a.status,e),new s("post_failed",`audit run failed (${a.status})`)}}catch(e){if(e instanceof s)throw e;throw console.error("audit run request failed:",e),new s(a(e)?"timeout":"network","audit run request failed")}let n=0;for(;;){await new Promise(e=>setTimeout(e,1e3));let e=null;try{let a=await t("/api/audit/status",{cache:"no-store"});a.ok&&(e=await a.json())}catch{}if(null===e){if(++n>=10)throw new s("network","audit status poll lost the server");continue}if(n=0,!e.running){if(e.error)throw new s("post_failed",e.error);return}}}e.s(["RerunError",0,s,"triggerRun",0,n],86616)},93237,e=>{"use strict";var t=e.i(43476),a=e.i(71645),s=e.i(95187);let n=(0,s.createServerReference)("00d97410f220c9506cdadd420812b2c7ab41b6dee8",s.callServer,void 0,s.findSourceMapURL,"getScheduledAuditAction"),i=(0,s.createServerReference)("4070638273d8ce919cc1cb3a9c56f9581269fad56d",s.callServer,void 0,s.findSourceMapURL,"setAutoAuditAction"),r=(0,s.createServerReference)("40b2805baae56152c00aa41d8f14da989078e7227d",s.callServer,void 0,s.findSourceMapURL,"setAuditIntervalAction");var l=e.i(86616),o=e.i(12344),c=e.i(29054);let d=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;function u(e){if((0,c.isAbortError)(e))return"request timed out. check your network and try again.";let t=e instanceof Error?e.message:String(e);return`network error: ${t}`}function h({open:e,headline:s="where to route the reminder?",subhead:n="we'll send a one-time code to confirm.",onClose:i,onAuthed:r,source:l="unknown"}){let{capture:m}=(0,o.usePostHog)(),[f,b]=(0,a.useState)({kind:"email",error:null}),[p,x]=(0,a.useState)(!1),v=(0,a.useRef)(!1),g=(0,a.useRef)(null),y=(0,a.useRef)(null);(0,a.useEffect)(()=>{e&&(b({kind:"email",error:null}),x(!1),v.current=!1,m("audit_auth_dialog_opened",{source:l}))},[m,e,l]);let w=(0,a.useRef)(!1);(0,a.useEffect)(()=>{if(e){w.current=!0;return}w.current&&!v.current&&m("audit_auth_dialog_dismissed",{source:l,step:f.kind}),w.current=!1},[m,e,l,f.kind]),(0,a.useEffect)(()=>{if(!e)return;let t=setTimeout(()=>{"email"===f.kind?g.current?.focus():"code"===f.kind&&y.current?.focus()},50);return()=>clearTimeout(t)},[e,f.kind]),(0,a.useEffect)(()=>{if(!e)return;let t=e=>{"Escape"!==e.key||p||i()};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[e,p,i]);let j="code"===f.kind&&f.resendIn>0;(0,a.useEffect)(()=>{if(!j)return;let e=setInterval(()=>{b(e=>"code"===e.kind?{...e,resendIn:Math.max(0,e.resendIn-1)}:e)},1e3);return()=>clearInterval(e)},[j]);let k=(0,a.useCallback)(async(e,t={})=>{let{isResend:a=!1}=t,s=e=>{a?b(t=>"code"===t.kind?{...t,error:e}:t):b({kind:"email",error:e})};x(!0);try{let t=await (0,c.fetchWithTimeout)("/api/auth/login-request",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({email:e})}),a=await t.json().catch(()=>({}));if(!t.ok){let e=a.message??"could not send code.";"rate_limited"===a.code&&void 0!==a.retry_after_secs?e=`too many tries. wait ${a.retry_after_secs}s and try again.`:"upstream_unreachable"===a.code&&(e="api-server unreachable. check your network."),s(e);return}b({kind:"code",email:e,error:null,expiresIn:a.expires_in??600,resendIn:a.resend_available_in??30})}catch(e){s(u(e))}finally{x(!1)}},[]),N=(0,a.useCallback)(async(e,t)=>{x(!0);try{let a=await (0,c.fetchWithTimeout)("/api/auth/login-verify",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({email:e,code:t})}),s=await a.json().catch(()=>({}));if(!a.ok||!s.authenticated||!s.user){let e=s.message??"invalid code.";"invalid_code"===s.code&&(e="wrong code, or it expired. try again."),b(t=>"code"===t.kind?{...t,error:e}:t);return}v.current=!0,m("audit_auth_dialog_succeeded",{source:l}),b({kind:"done",user:s.user}),r(s.user)}catch(t){let e=u(t);b(t=>"code"===t.kind?{...t,error:e}:t)}finally{x(!1)}},[m,r,l]),S=(0,a.useCallback)(async e=>{if(e.preventDefault(),p||"email"!==f.kind)return;let t=String(new FormData(e.currentTarget).get("email")??"").trim().toLowerCase();d.test(t)?await k(t):b({kind:"email",error:"that doesn't look like an email."})},[p,f,k]),C=(0,a.useCallback)(async e=>{if(e.preventDefault(),p||"code"!==f.kind)return;let t=String(new FormData(e.currentTarget).get("code")??"").trim();t.length<4||t.length>12?b(e=>"code"===e.kind?{...e,error:"code is 4–12 characters."}:e):await N(f.email,t)},[p,f,N]),M=(0,a.useCallback)(async()=>{"code"!==f.kind||f.resendIn>0||p||await k(f.email,{isResend:!0})},[f,p,k]);return e?(0,t.jsx)("div",{className:"auth-dialog-backdrop",role:"dialog","aria-modal":"true","aria-labelledby":"auth-dialog-title",onClick:e=>{p||e.target!==e.currentTarget||i()},children:(0,t.jsxs)("div",{className:"auth-dialog",children:[(0,t.jsx)("button",{type:"button",className:"auth-close",onClick:i,disabled:p,"aria-label":"close",children:"×"}),(0,t.jsx)("h2",{id:"auth-dialog-title",className:"auth-headline",children:s}),"email"===f.kind&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("p",{className:"auth-sub",children:n}),(0,t.jsxs)("form",{onSubmit:S,className:"auth-form",children:[(0,t.jsx)("input",{ref:g,id:"auth-dialog-email",name:"email",type:"email",autoComplete:"email",inputMode:"email",spellCheck:!1,placeholder:"you@yourdomain.com",disabled:p,className:"auth-input",required:!0}),f.error&&(0,t.jsx)("div",{className:"auth-error",children:f.error}),(0,t.jsxs)("div",{className:"auth-actions",children:[(0,t.jsx)("button",{type:"submit",className:"auth-btn primary",disabled:p,children:p?"sending…":"send code"}),(0,t.jsx)("button",{type:"button",className:"auth-btn",onClick:i,disabled:p,children:"cancel"})]})]})]}),"code"===f.kind&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"auth-sub",children:["code sent to ",(0,t.jsx)("span",{className:"auth-email",children:f.email}),". expires in ",Math.ceil(f.expiresIn/60)," min."]}),(0,t.jsxs)("form",{onSubmit:C,className:"auth-form",children:[(0,t.jsx)("input",{ref:y,id:"auth-dialog-code",name:"code",type:"text",inputMode:"numeric",autoComplete:"one-time-code",spellCheck:!1,placeholder:"123456",disabled:p,className:"auth-input auth-input-code",maxLength:12,required:!0}),f.error&&(0,t.jsx)("div",{className:"auth-error",children:f.error}),(0,t.jsxs)("div",{className:"auth-actions",children:[(0,t.jsx)("button",{type:"submit",className:"auth-btn primary",disabled:p,children:p?"verifying…":"verify"}),(0,t.jsx)("button",{type:"button",className:"auth-btn",onClick:M,disabled:p||f.resendIn>0,children:f.resendIn>0?`resend in ${f.resendIn}s`:"resend code"})]}),(0,t.jsx)("button",{type:"button",className:"auth-back",onClick:()=>b({kind:"email",error:null}),disabled:p,children:"← use a different email"})]})]}),"done"===f.kind&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("p",{className:"auth-sub",children:[(0,t.jsx)("span",{className:"auth-ok",children:"✓"})," signed in as"," ",(0,t.jsx)("span",{className:"auth-email",children:f.user.email}),"."]}),(0,t.jsx)("div",{className:"auth-actions",children:(0,t.jsx)("button",{type:"button",className:"auth-btn primary",onClick:i,children:"continue"})})]})]})}):null}var m=e.i(55574),f=e.i(34713);function b(e){return new Date(e).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function p(e,t){let a=e-t;if(a<=0)return"now";let s=Math.floor(a/864e5),n=Math.floor(a%864e5/36e5);return s>0?n>0?`${s}d ${n}h`:`${s}d`:n>0?`${n}h`:`${Math.max(1,Math.floor(a/6e4))}m`}function x(e){return e.toLocaleString("en-US")}function v({label:e,value:a,sub:s,tone:n}){return(0,t.jsxs)("div",{className:"set-stat",children:[(0,t.jsx)("div",{className:"set-stat-label",children:e}),(0,t.jsx)("div",{className:`set-stat-value${n?` ${n}`:""}`,children:a}),(0,t.jsx)("div",{className:"set-stat-sub",children:s??" "})]})}function g({lastRunAtMs:e,nextDueAtMs:a,now:s}){if(null==e||null==a||a<=e)return null;let n=Math.min(100,Math.max(0,(s-e)/(a-e)*100));return(0,t.jsxs)("div",{className:"tape","aria-hidden":"true",children:[(0,t.jsxs)("div",{className:"tape-rail",children:[(0,t.jsx)("div",{className:"tape-fill",style:{width:`${n}%`}}),(0,t.jsx)("div",{className:"tape-now",style:{left:`${n}%`}})]}),(0,t.jsxs)("div",{className:"tape-ends",children:[(0,t.jsxs)("span",{children:["last scan · ",b(e)]}),(0,t.jsxs)("span",{className:"tape-next",children:["next · ",p(a,s)]})]})]})}function y({enabled:e,onChange:a,disabled:s,label:n}){return(0,t.jsx)("button",{type:"button",role:"switch","aria-checked":e,"aria-label":n,disabled:s,onClick:a,className:"sw","data-on":e?"true":"false",children:(0,t.jsx)("span",{className:"sw-knob"})})}let w=[{label:"reads",body:"every session transcript on disk — your prompts, the files your agents read and wrote, and command output."},{label:"runs",body:"entirely on this machine. the transcripts never leave it."},{label:"sends",body:"counts, redacted examples, and this machine's name — and only when a scan finds something harmful."}];e.s(["default",0,function({initial:e}){let[s,o]=(0,a.useState)(e),[c,d]=(0,a.useState)(e?.auto??!1),[u,j]=(0,a.useState)(e?.intervalDays??7),[k,N]=(0,a.useState)(!1),[S,C]=(0,a.useState)(!1),[M,$]=(0,a.useState)(!1),[E,A]=(0,a.useState)(null===e),[D,R]=(0,a.useState)(0),_=(0,a.useRef)(!0),T=(0,a.useRef)(null!==e),I=(0,a.useCallback)(async()=>{try{let e=await n();if(!_.current)return;o(e),T.current=!0,d(e.auto),j(e.intervalDays),R(Date.now()),A(!1)}catch{_.current&&!T.current&&A(!0)}},[]);(0,a.useEffect)(()=>(_.current=!0,I(),()=>{_.current=!1}),[I]),(0,a.useEffect)(()=>{let e=()=>void I();return window.addEventListener("focus",e),document.addEventListener("visibilitychange",e),()=>{window.removeEventListener("focus",e),document.removeEventListener("visibilitychange",e)}},[I]);let L=s?.signedInAs??null,F=s?.daemon==="running",O=s?.daemon==="unsupported-platform",P=s?.schedule??null,q=s?.lastScan??null,U=P?.lastExitCode!=null&&0!==P.lastExitCode&&75!==P.lastExitCode,B=(0,a.useCallback)(async()=>{N(!0);try{let e=await i(!0);if(!e.ok){if(d(!1),"unreachable"===e.reason)return void(0,m.toast)("could not reach the server. check your connection and try again.");await I(),$(!0),(0,m.toast)("that sign-in expired. one more code and it's on.");return}d(e.auto),(0,m.toast)("scheduled audits on."),await I()}catch{d(!1),(0,m.toast)("could not turn that on.")}finally{N(!1)}},[I]),J=(0,a.useCallback)(async()=>{if(c){N(!0);try{let e=await i(!1);e.ok&&d(e.auto),(0,m.toast)("scheduled audits off."),await I()}catch{(0,m.toast)("could not turn that off.")}finally{N(!1)}return}L?await B():$(!0)},[c,B,I,L]),W=(0,a.useCallback)(async e=>{N(!0);try{let t=await r(e);j(t.intervalDays),o(e=>e?{...e,intervalDays:t.intervalDays}:e),(0,m.toast)(`scanning every ${t.intervalDays} day${1===t.intervalDays?"":"s"}.`)}catch{j(s?.intervalDays??7),(0,m.toast)("could not save that.")}finally{N(!1)}},[s?.intervalDays]),K=(0,a.useCallback)(async()=>{N(!0);try{await fetch("/api/auth/logout",{method:"POST"}),(0,m.toast)("signed out. scans continue; digests pause."),await I()}catch{(0,m.toast)("could not sign out.")}finally{N(!1)}},[I]),H=(0,a.useCallback)(async()=>{if(!S){C(!0);try{await (0,l.triggerRun)({cli:[],since:"all",noCache:!1}),(0,m.toast)("scan complete."),await I()}catch(e){(0,m.toast)(e instanceof l.RerunError&&"timeout"===e.kind?"the scan is taking a while — it will finish in the background.":"the scan could not be completed.")}finally{C(!1)}}},[I,S]),z=(0,a.useCallback)(async e=>{$(!1),await I(),await B()},[B,I]),G=s?F?{value:"running",sub:null!=s.daemonStartedAtMs&&D>0?`up ${function(e){let t=Math.floor(e/864e5);if(t>0)return`${t}d`;let a=Math.floor(e/36e5);return a>0?`${a}h`:`${Math.max(1,Math.floor(e/6e4))}m`}(Math.max(0,D-s.daemonStartedAtMs))}`:"",tone:"ok"}:O?{value:"unavailable",sub:"not on this platform"}:"not-installed"===s.daemon?{value:"not installed",sub:"run failproofai config",tone:"warn"}:"unknown"===s.daemon?{value:"unknown",sub:"could not read it"}:{value:"stopped",sub:"run failproofai config",tone:"warn"}:{value:"…",sub:"reading"},Q=c?P?.nextDueAtMs==null?{value:"pending",sub:"after the next scan"}:{value:D>0?p(P.nextDueAtMs,D):"—",sub:b(P.nextDueAtMs)}:{value:"off",sub:"nothing scheduled"},V=q?{value:(0,f.formatRelativeTime)(Date.parse(q.finishedAt)),sub:null!=q.sessionsScanned?`${x(q.sessionsScanned)} sessions`:"scanned"}:{value:"none yet",sub:"run one below"},X=q?null==q.findings?{value:"—",sub:"unreadable"}:{value:x(q.findings),sub:"this scan",...q.findings>0?{tone:"warn"}:{}}:{value:"—",sub:"no scan yet"};return(0,t.jsxs)("main",{className:"report",children:[(0,t.jsxs)("section",{className:"section set-sec",children:[(0,t.jsxs)("div",{className:"set-mast",children:[(0,t.jsx)("h1",{className:"set-title",children:"settings"}),(0,t.jsx)("p",{className:"set-lede",children:"keeping watch, so you don't have to."})]}),E?(0,t.jsx)("div",{className:"set-console",children:(0,t.jsx)("div",{className:"set-cell",children:(0,t.jsxs)("p",{className:"set-warn",children:["could not read this machine's settings. check that the dashboard can reach ",(0,t.jsx)("code",{children:"~/.failproofai/config.json"}),"."]})})}):(0,t.jsxs)("div",{className:"set-console",children:[(0,t.jsxs)("div",{className:"set-stats",children:[(0,t.jsx)(v,{label:"daemon",...G}),(0,t.jsx)(v,{label:"next scan",...Q}),(0,t.jsx)(v,{label:"last scan",...V}),(0,t.jsx)(v,{label:"findings",...X})]}),(0,t.jsxs)("div",{className:"set-cols",children:[(0,t.jsxs)("div",{className:"set-cell set-config",children:[(0,t.jsx)("h2",{className:"set-cell-title",children:"scheduled audit"}),(0,t.jsxs)("div",{className:"set-row set-row-main",children:[(0,t.jsx)(y,{enabled:c,disabled:k||null===s,onChange:()=>void J(),label:c?"turn off scheduled audits":"turn on scheduled audits"}),(0,t.jsxs)("div",{className:"set-row-copy",children:[(0,t.jsx)("span",{className:"set-strong",children:c?"scanning on a schedule.":"scan on a schedule."}),(0,t.jsx)("span",{className:"set-dim",children:c?"you'll get an email only when a scan finds something.":"off. nothing runs and nothing is sent."})]})]}),(0,t.jsxs)("div",{className:"set-row set-row-interval",children:[(0,t.jsx)("span",{className:"set-dim",children:"scan every"}),(0,t.jsx)("input",{type:"number",className:"set-num",min:1,max:90,value:u,disabled:k||null===s,"aria-label":"days between scheduled scans",onChange:e=>j(Number(e.target.value)),onBlur:e=>{let t=Number(e.target.value);Number.isFinite(t)?t!==s?.intervalDays&&W(t):j(s?.intervalDays??7)}}),(0,t.jsx)("span",{className:"set-dim",children:"days"}),(0,t.jsxs)("span",{className:"set-hint",children:[1,"–",90]})]}),(0,t.jsx)("div",{className:"set-rule"}),(0,t.jsx)("div",{className:"set-row set-row-identity",children:L?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"set-dim",children:"reports go to"}),(0,t.jsx)("span",{className:"set-email",children:L.email}),(0,t.jsx)("button",{type:"button",className:"set-link",disabled:k,onClick:()=>void K(),children:"sign out"})]}):c?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"set-warn",children:"signed out — scans continue, digests are paused."}),(0,t.jsx)("button",{type:"button",className:"set-link",disabled:k,onClick:()=>$(!0),children:"sign in to resume"})]}):(0,t.jsx)("span",{className:"set-dim",children:"turning this on asks for an email, so there is somewhere to send the report."})}),c&&s&&!F&&(0,t.jsx)("p",{className:"set-warn",children:O?"the background service isn't available on this platform, so scheduled scans can't run here. you can still run one now.":"not-installed"===s.daemon?"the background service isn't installed, so nothing will run on the timer. install it with `failproofai config`.":"the background service is stopped, so nothing will run until it starts. repair it with `failproofai config`."}),U&&(0,t.jsxs)("p",{className:"set-warn",children:["the last scheduled scan exited ",P?.lastExitCode,". the next one will still run."]}),(0,t.jsx)("div",{className:"set-actions",children:(0,t.jsx)("button",{type:"button",className:"btn btn-press",disabled:S,onClick:()=>void H(),children:S?"[ scanning… ]":"[ run a scan now ]"})})]}),(0,t.jsxs)("div",{className:"set-cell set-how",children:[(0,t.jsx)("h2",{className:"set-cell-title",children:"how it works"}),(0,t.jsx)("dl",{className:"set-how-list",children:w.map(e=>(0,t.jsxs)("div",{className:"set-how-row",children:[(0,t.jsx)("dt",{className:"set-how-label",children:e.label}),(0,t.jsx)("dd",{className:"set-how-body",children:e.body})]},e.label))})]})]})]}),c&&D>0&&(0,t.jsx)(g,{lastRunAtMs:P?.lastRunAtMs??null,nextDueAtMs:P?.nextDueAtMs??null,now:D})]}),(0,t.jsx)(h,{open:M,source:"settings_scheduled_audit",headline:"where should the report go?",subhead:"we'll send a one-time code to confirm.",onClose:()=>$(!1),onAuthed:e=>void z(e)})]})}],93237)},34713,e=>{"use strict";e.s(["formatDuration",0,function(e){if(e<1e3)return`${e}ms`;let t=Math.round(e/100);if(t<600)return`${(t/10).toFixed(1)}s`;let a=Math.round(e/1e3);if(a<3600){let e=Math.floor(a/60);return`${e}m ${a%60}s`}let s=Math.round(e/6e4),n=Math.floor(s/60);return`${n}h ${s%60}m`},"formatRelativeTime",0,function(e){let t=Date.now()-e;return t<6e4?`${Math.max(1,Math.floor(t/1e3))}s ago`:t<36e5?`${Math.floor(t/6e4)}m ago`:t<864e5?`${Math.floor(t/36e5)}h ago`:`${Math.floor(t/864e5)}d ago`}])}]);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,24358,e=>{"use strict";var s=e.i(43476),a=e.i(71645),n=e.i(95187);let t=(0,n.createServerReference)("0048814305f7563284b0e6413ac4ea842889a1d6eb",n.callServer,void 0,n.findSourceMapURL,"getAuditResultAction");var r=e.i(12344);function i({cachedAt:e,fixed:a=!1}){return(0,s.jsxs)("footer",{className:a?"report-footer report-footer--fixed":"report-footer",children:[(0,s.jsx)("img",{src:"/logo.svg",alt:"failproof_ai",style:{height:18,display:"inline-block",verticalAlign:"middle"}}),(0,s.jsx)("span",{style:{margin:"0 12px",color:"var(--line-2)"},children:"·"}),"audit v1.0",(0,s.jsx)("span",{style:{margin:"0 12px",color:"var(--line-2)"},children:"·"}),"generated ",function(e){if(!e)return"—";let s=new Date(e);if(Number.isNaN(s.getTime()))return"—";let a=s.getUTCDate().toString().padStart(2,"0"),n=["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"][s.getUTCMonth()],t=s.getUTCFullYear(),r=s.getUTCHours().toString().padStart(2,"0"),i=s.getUTCMinutes().toString().padStart(2,"0");return`${a} ${n} ${t}, ${r}:${i} utc`}(e),(0,s.jsx)("span",{style:{margin:"0 12px",color:"var(--line-2)"},children:"·"}),(0,s.jsx)("span",{style:{color:"var(--ink-2)"},children:"auto-healing for your agents."})]})}let l=(0,n.createServerReference)("4097c062c6b1d13b1d6f2a64af27881249bbf9d989",n.callServer,void 0,n.findSourceMapURL,"dismissLeakAction"),c=(0,n.createServerReference)("00fd330814bcefc936e301ec49d66099ed614ecf67",n.callServer,void 0,n.findSourceMapURL,"getLeaksAction"),d=(0,n.createServerReference)("409ed106c4be852cd917c107309fc8fd09d79a0beb",n.callServer,void 0,n.findSourceMapURL,"setLeakNotifyAction");function o(e){let s=Date.parse(e);if(!Number.isFinite(s))return"unknown";let a=Math.floor((Date.now()-s)/864e5);return a<=0?"today":1===a?"yesterday":a<30?`${a}d ago`:new Date(s).toLocaleDateString(void 0,{month:"short",day:"numeric"})}function u(){let[e,n]=(0,a.useState)(null),[t,r]=(0,a.useState)(!0),[i,u]=(0,a.useState)(null),h=(0,a.useCallback)(async()=>{let e=await c();n(e.rows),r(e.notify)},[]);(0,a.useEffect)(()=>{h()},[h]);let p=(0,a.useCallback)(async e=>{u(e);try{await l(e)&&n(s=>s?.filter(s=>s.id!==e)??null)}finally{u(null)}},[]),m=(0,a.useCallback)(async()=>{let e=!t;r(e),await d(e)||r(!e)},[t]);return null===e?null:(0,s.jsxs)("section",{className:"audit-sec","data-screen-label":"02 Leaks",children:[(0,s.jsxs)("div",{className:"audit-sec-head",children:[(0,s.jsxs)("span",{className:"audit-sec-eyebrow",children:[(0,s.jsx)("span",{className:"ix",children:"02"}),"// credentials"]}),(0,s.jsx)("button",{type:"button",className:"cbb-toggle leak-notify","data-on":t,onClick:m,"aria-label":t?"turn desktop notifications off":"turn desktop notifications on",title:"desktop notification when a new credential is found",children:(0,s.jsx)("span",{className:"cbb-toggle-knob"})})]}),0===e.length?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("h2",{className:"audit-sec-title",children:"no credentials found"}),(0,s.jsxs)("div",{className:"audit-sec-sub",children:["// nothing in this machine's transcripts matched a known key format",(0,s.jsx)("br",{}),"// or a secret-named assignment."]})]}):(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)("h2",{className:"audit-sec-title",children:[e.length," credential",1===e.length?"":"s"," in your transcripts"]}),(0,s.jsx)("div",{className:"audit-sec-sub",children:"// one row per distinct key — an exposure count is not a key count."}),(0,s.jsx)("ul",{className:"leak-list",children:e.map(e=>(0,s.jsxs)("li",{className:"leak-row","data-attributed":e.attributed,children:[(0,s.jsxs)("div",{className:"leak-row-top",children:[(0,s.jsx)("code",{className:"leak-key",children:e.display}),(0,s.jsx)("span",{className:"leak-label",children:e.label}),e.name?(0,s.jsx)("span",{className:"leak-name",children:e.name}):null,(0,s.jsx)("button",{type:"button",className:"leak-dismiss",onClick:()=>void p(e.id),disabled:i===e.id,children:"not a secret"})]}),(0,s.jsxs)("div",{className:"leak-how",children:[(0,s.jsx)("span",{className:"leak-verb",children:e.mechanism})," in ",(0,s.jsx)("span",{className:"leak-where",children:e.project})," via ",(0,s.jsx)("span",{className:"leak-who",children:e.cli})," · leaked ",(0,s.jsx)("span",{className:"leak-when",children:o(e.firstSeen)}),o(e.firstSeen)!==o(e.lastSeen)?(0,s.jsxs)(s.Fragment,{children:[" · last seen ",(0,s.jsx)("span",{className:"leak-when",children:o(e.lastSeen)})]}):null]}),(0,s.jsxs)("div",{className:"leak-foot",children:[(0,s.jsxs)("span",{className:"leak-meta",children:[e.occurrences," exposure",1===e.occurrences?"":"s",e.sessions>1?` \xb7 ${e.sessions} sessions`:""," · ",e.length," chars"]}),(0,s.jsx)("span",{className:"leak-dir",children:"input"===e.direction?"sent by the agent — blockable at PreToolUse":"returned to the agent — already in the transcript"}),(0,s.jsx)("span",{className:"leak-fix",children:e.attributed?`rotate it in the ${e.label.replace(/ (API )?(key|token).*$/i,"")} console`:e.name?`find what reads ${e.name}, then rotate it there`:"trace it to its owner, then rotate it"})]})]},e.id))})]})]})}var h=e.i(86616);function p({mode:e,running:a,onStarted:n,onCompleted:t}){let{capture:i}=(0,r.usePostHog)(),l=async()=>{i("audit_first_run_clicked",{source:"empty_state",mode:e}),n();try{await (0,h.triggerRun)({cli:[],since:"all"})}catch(e){console.error("audit first run failed:",e)}finally{await t()}};return"no-cache"===e?(0,s.jsxs)("section",{className:"section empty-section","data-screen-label":"00 Empty",children:[(0,s.jsxs)("div",{className:"section-mast",children:[(0,s.jsxs)("div",{className:"section-label",children:[(0,s.jsx)("span",{className:"glyph",children:"━━"})," audit"," ",(0,s.jsx)("span",{className:"sep",children:"·"})," first run"]}),(0,s.jsxs)("div",{className:"section-meta",children:[(0,s.jsx)("span",{style:{color:"var(--dim)"},children:"○"})," no cache yet"]})]}),(0,s.jsx)("h2",{className:"section-h",children:"scan and see."}),(0,s.jsxs)("div",{className:"panel empty-panel",children:[(0,s.jsxs)("div",{className:"empty-glyph","aria-hidden":"true",children:[(0,s.jsx)("div",{className:"empty-glyph-grid",children:Array.from({length:36}).map((e,a)=>{let n=["1","5","8","10","13","17","20","23","25","30","32"].includes(String(a));return(0,s.jsx)("span",{className:"px"+(n?" on":"")},a)})}),(0,s.jsx)("div",{className:"empty-glyph-label",children:"▮▮ no audit data yet"})]}),(0,s.jsx)("h3",{className:"empty-headline",children:"run your first audit."}),(0,s.jsxs)("p",{className:"empty-sub",children:["we'll walk every transcript across your installed CLIs — Claude Code, Codex, Copilot, Cursor, OpenCode, Pi — and count every wasteful","or risky action. you'll get your agent's archetype and a punch-list."]}),(0,s.jsxs)("div",{className:"empty-actions",children:[(0,s.jsx)("button",{type:"button",className:"btn btn-primary btn-press empty-cta",onClick:l,disabled:a,children:a?"[ scanning… ]":"[ run audit ]"}),(0,s.jsx)("span",{className:"empty-meta",children:"scans all sessions · all installed CLIs · may take a while"})]})]})]}):(0,s.jsxs)("section",{className:"section empty-section","data-screen-label":"00 Empty",children:[(0,s.jsxs)("div",{className:"section-mast",children:[(0,s.jsxs)("div",{className:"section-label",children:[(0,s.jsx)("span",{className:"glyph",children:"━━"})," audit"," ",(0,s.jsx)("span",{className:"sep",children:"·"})," zero transcripts"]}),(0,s.jsxs)("div",{className:"section-meta",children:[(0,s.jsx)("span",{style:{color:"var(--amber)"},children:"●"})," hooks not installed"]})]}),(0,s.jsx)("h2",{className:"section-h",children:"nothing to read."}),(0,s.jsxs)("div",{className:"panel empty-panel",children:[(0,s.jsxs)("div",{className:"empty-glyph","aria-hidden":"true",children:[(0,s.jsx)("div",{className:"empty-glyph-grid",children:Array.from({length:36}).map((e,a)=>{let n=["2","4","9","11","16","18","23","25","27","30","33"].includes(String(a));return(0,s.jsx)("span",{className:"px"+(n?" on":"")},a)})}),(0,s.jsx)("div",{className:"empty-glyph-label",children:"▮▮ no sessions found"})]}),(0,s.jsx)("h3",{className:"empty-headline",children:"install hooks first."}),(0,s.jsx)("p",{className:"empty-sub",children:"failproofai couldn't find any transcripts to scan on this machine. install the hooks for at least one CLI and come back."}),(0,s.jsxs)("div",{className:"empty-actions",children:[(0,s.jsx)("a",{href:"https://docs.befailproof.ai/getting-started",target:"_blank",rel:"noopener noreferrer",className:"btn btn-primary btn-press empty-cta",onClick:()=>{i("audit_install_guide_clicked",{source:"empty_state",mode:e})},children:"[ install guide → ]"}),(0,s.jsx)("span",{className:"empty-meta",children:"takes about 30 seconds · one command per CLI"})]})]})]})}let m=[{label:"discovering transcripts",detail:"walking ~/.claude, ~/.codex, ~/.cursor, …"},{label:"parsing session logs",detail:"reading JSONL + sqlite session stores"},{label:"running policy checks",detail:"replaying every policy against each tool call"},{label:"aggregating results",detail:"counting hits, ranking by frequency"}];function g(){let[e,n]=(0,a.useState)(0),[t,r]=(0,a.useState)(0);(0,a.useEffect)(()=>{let e=setInterval(()=>{n(e=>Math.min(e+1,m.length-1))},4e3),s=setInterval(()=>r(e=>(e+1)%4),350);return()=>{clearInterval(e),clearInterval(s)}},[]);let i=".".repeat(t+1),l=e===m.length-1,c=Math.round(100*Math.min(.9,(e+1)/m.length*.9));return(0,s.jsxs)("section",{className:"section running-section","data-screen-label":"00 Running",children:[(0,s.jsxs)("div",{className:"section-mast",children:[(0,s.jsxs)("div",{className:"section-label",children:[(0,s.jsx)("span",{className:"glyph",children:"━━"})," audit"," ",(0,s.jsx)("span",{className:"sep",children:"·"})," in progress"]}),(0,s.jsxs)("div",{className:"section-meta",children:[(0,s.jsx)("span",{style:{color:"var(--accent-pink)"},children:"●"})," scanning"]})]}),(0,s.jsxs)("h2",{className:"section-h",children:["scanning sessions",i]}),(0,s.jsxs)("div",{className:"panel running-panel",children:[(0,s.jsxs)("div",{className:"running-header",children:[(0,s.jsx)("span",{className:"running-prompt",children:"$"}),(0,s.jsx)("span",{className:"running-cmd",children:"failproofai audit"}),(0,s.jsx)("span",{className:"running-cursor","aria-hidden":"true",children:"▮"})]}),(0,s.jsx)("ul",{className:"running-stages",children:m.map((a,n)=>{let r=n<e,i=n===e;return(0,s.jsxs)("li",{className:"running-stage"+(r?" done":i?" active":" queued"),children:[(0,s.jsx)("span",{className:"running-marker","aria-hidden":"true",children:r?"✓":i?"▮▮":"○"}),(0,s.jsxs)("div",{className:"running-stage-body",children:[(0,s.jsx)("div",{className:"running-stage-label",children:a.label}),i&&(0,s.jsx)("div",{className:"running-stage-detail",children:l?"finishing up…":a.detail})]}),i&&(0,s.jsx)("span",{className:"running-stage-spin","aria-hidden":"true",children:["⠋","⠙","⠹","⠸"][t]})]},n)})}),(0,s.jsxs)("div",{className:"running-bar-label",children:[(0,s.jsx)("span",{children:"progress"}),(0,s.jsxs)("span",{style:{color:"var(--dim)"},children:[c,"%"]})]}),(0,s.jsx)("div",{className:"running-bar-track",children:(0,s.jsx)("div",{className:"running-bar-fill",style:{width:`${c}%`}})}),(0,s.jsx)("p",{className:"running-foot",children:"this can take a while depending on how much session history you have."})]})]})}let x={timeout:"audit took too long — try again",network:"network hiccup — check your connection",post_failed:"audit failed — try again"};function f({status:e,onDismiss:n}){let[t,r]=(0,a.useState)(()=>Date.now());if((0,a.useEffect)(()=>{if("running"!==e.kind)return;let s=setInterval(()=>r(Date.now()),1e3);return()=>clearInterval(s)},[e.kind]),"idle"===e.kind)return null;if("running"===e.kind){let a=Math.max(0,t-e.startedAt);return(0,s.jsxs)("div",{className:"audit-progress-strip",role:"status","aria-live":"polite",children:[(0,s.jsxs)("div",{className:"audit-progress-strip__inner",children:[(0,s.jsxs)("span",{className:"audit-progress-strip__label",children:[(0,s.jsx)("span",{className:"audit-progress-strip__spinner","aria-hidden":"true"}),"re-auditing your sessions"]}),(0,s.jsx)("span",{className:"audit-progress-strip__elapsed",children:function(e){if(e<0)return"00:00";let s=Math.floor(e/1e3),a=Math.floor(s/60).toString().padStart(2,"0"),n=(s%60).toString().padStart(2,"0");return`${a}:${n}`}(a)})]}),(0,s.jsx)("div",{className:"audit-progress-strip__pulse","aria-hidden":"true"})]})}return(0,s.jsx)("div",{className:"audit-progress-strip audit-progress-strip--failed",role:"alert",children:(0,s.jsxs)("div",{className:"audit-progress-strip__inner",children:[(0,s.jsxs)("span",{className:"audit-progress-strip__label",children:[(0,s.jsx)("span",{className:"audit-progress-strip__x","aria-hidden":"true",children:"×"}),x[e.reason]]}),(0,s.jsx)("button",{type:"button",className:"audit-progress-strip__dismiss",onClick:n,"aria-label":"dismiss",children:"[ dismiss ]"})]})})}function j({result:e,cachedAt:n,projectFromUrl:t,isRunning:l,rerunStatus:c,onRerun:d,onDismissRerun:o}){let{capture:h}=(0,r.usePostHog)(),{detectorsTriggered:p,missing:m}=(0,a.useMemo)(()=>{let s=0,a=0;for(let n of e.results)n.hits>0&&s++,"builtin"===n.source&&!n.enabledInConfig&&n.hits>0&&a++;return{detectorsTriggered:s,missing:a}},[e]),g=(0,a.useRef)(!1);return(0,a.useEffect)(()=>{g.current||(g.current=!0,h("audit_dashboard_viewed",{missing:m,transcripts_scanned:e.transcripts.scanned,results_count:e.results.length,detectors_triggered:p}))},[h,m,e.transcripts.scanned,e.results.length,p]),(0,s.jsxs)("div",{className:"app",children:[(0,s.jsx)(f,{status:c,onDismiss:o}),(0,s.jsxs)("div",{className:"app-shell",children:[(0,s.jsxs)("div",{className:"report",children:[(0,s.jsx)(y,{transcripts:e.transcripts.scanned,events:e.eventsScanned??0,projects:e.projectsScanned.length,isRunning:l||"running"===c.kind,onRerun:()=>d("scan_header")}),(0,s.jsx)(u,{})]}),(0,s.jsx)(i,{cachedAt:n})]})]})}function y({transcripts:e,events:a,projects:n,isRunning:t,onRerun:r}){return(0,s.jsxs)("section",{className:"audit-sec","data-screen-label":"01 Scan",children:[(0,s.jsxs)("div",{className:"audit-sec-head",children:[(0,s.jsxs)("span",{className:"audit-sec-eyebrow",children:[(0,s.jsx)("span",{className:"ix",children:"01"}),"// scan"]}),(0,s.jsx)("button",{type:"button",className:"scan-rerun",onClick:r,disabled:t,children:t?"scanning…":"run again"})]}),(0,s.jsx)("h2",{className:"audit-sec-title",children:"scan complete"}),(0,s.jsxs)("div",{className:"audit-sec-sub",children:[a.toLocaleString()," tool call",1===a?"":"s"," across ",e.toLocaleString()," ","transcript",1===e?"":"s",n>0?` \xb7 ${n.toLocaleString()} project${1===n?"":"s"}`:""]}),(0,s.jsxs)("div",{className:"audit-sec-sub",children:["// the report is being rebuilt around leak detection. the scan, the",(0,s.jsx)("br",{}),"// 12-CLI transcript reader and the emailed digest are unaffected."]})]})}function N({running:e,mode:a="no-cache",rerunStatus:n,onDismissRerun:t,onStarted:r,onCompleted:l}){return(0,s.jsxs)("div",{className:"app",children:[(0,s.jsx)(f,{status:n,onDismiss:t}),(0,s.jsxs)("div",{className:"app-shell",children:[(0,s.jsx)("div",{className:"report",children:e?(0,s.jsx)(g,{}):(0,s.jsx)(p,{mode:a,running:e,onStarted:r,onCompleted:l})}),(0,s.jsx)(i,{cachedAt:null,fixed:!0})]})]})}e.s(["AuditDashboard",0,function({initial:e,projectFromUrl:n,totalCatalogSize:i}){let[l,c]=(0,a.useState)(e),[d,o]=(0,a.useState)(!1),[u,p]=(0,a.useState)({kind:"idle"}),{capture:m}=(0,r.usePostHog)(),g=(0,a.useRef)(null),x=(0,a.useRef)(!1),f="cached"===l.status?l.result.transcripts.scanned:0,y="cached"===l.status?l.result.results.length:0,b=(0,a.useCallback)(async()=>{c(await t())},[]),v=(0,a.useCallback)(async e=>{if(d)return;m("audit_rerun_clicked",{source:e,since:"all"}),o(!0);let s=Date.now();p({kind:"running",startedAt:s});try{await (0,h.triggerRun)({cli:[],since:"all",noCache:!0}),await b(),m("audit_rerun_succeeded",{source:e,since:"all",duration_ms:Date.now()-s}),p({kind:"idle"})}catch(n){let a=n instanceof h.RerunError?n.kind:"network";m("audit_rerun_failed",{kind:a,source:e,since:"all",cli_filter:"all",duration_ms:Date.now()-s}),p({kind:"failed",reason:a,failedAt:Date.now()})}finally{o(!1)}},[m,b,d]),k=(0,a.useCallback)(()=>{p({kind:"idle"})},[]);if((0,a.useEffect)(()=>(document.body.classList.add("audit-body"),()=>document.body.classList.remove("audit-body")),[]),(0,a.useEffect)(()=>{let e=d?"running":"empty"===l.status?"empty":0===f?"zero_sessions":"report";g.current!==e&&(g.current=e,m("audit_page_viewed",{state:e,has_cache:"cached"===l.status}))},[l.status,m,d,f]),(0,a.useEffect)(()=>{x.current=!1},[l.status,d,f]),(0,a.useEffect)(()=>{if(d||"cached"!==l.status||0===f)return;let e=0,s=()=>{if(e=0,x.current)return;let s=document.documentElement.scrollHeight-window.innerHeight;s<=0||window.scrollY>=s-24&&(x.current=!0,m("audit_page_scrolled_to_end",{results_count:y,transcripts_scanned:f}))},a=()=>{0===e&&(e=requestAnimationFrame(s))};return s(),window.addEventListener("scroll",a,{passive:!0}),()=>{window.removeEventListener("scroll",a),0!==e&&cancelAnimationFrame(e)}},[l.status,m,y,d,f]),"empty"===l.status&&!d)return(0,s.jsx)(N,{running:!1,rerunStatus:u,onDismissRerun:k,onStarted:()=>o(!0),onCompleted:async()=>{o(!1),await b()}});if("empty"===l.status&&d)return(0,s.jsx)(N,{running:!0,rerunStatus:u,onDismissRerun:k,onStarted:()=>{},onCompleted:async()=>{o(!1),await b()}});let w="cached"===l.status?l.result:null;if(!w)return null;let S="cached"===l.status?l.cachedAt:null;return 0===w.transcripts.scanned?(0,s.jsx)(N,{running:d,mode:"zero-sessions",rerunStatus:u,onDismissRerun:k,onStarted:()=>o(!0),onCompleted:async()=>{o(!1),await b()}}):(0,s.jsx)(j,{result:w,cachedAt:S,projectFromUrl:n,isRunning:d,rerunStatus:u,onRerun:e=>v(e),onDismissRerun:k})}],24358)},86616,29054,e=>{"use strict";function s(e,a={},n=15e3){let t=AbortSignal.timeout(n),r=a.signal?AbortSignal.any([a.signal,t]):t;return fetch(e,{...a,signal:r})}function a(e){if(null===e||"object"!=typeof e)return!1;let s=e.name;return"AbortError"===s||"TimeoutError"===s}e.s(["fetchWithTimeout",0,s,"isAbortError",0,a],29054);class n extends Error{kind;constructor(e,s){super(s),this.kind=e,this.name="RerunError"}}async function t(e){try{let a=await s("/api/audit/run",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({cli:e.cli.length>0?e.cli:void 0,since:"all"===e.since?void 0:e.since,noCache:!!e.noCache||void 0})});if(!a.ok&&409!==a.status){let e=await a.text().catch(()=>"");throw console.error("audit run failed:",a.status,e),new n("post_failed",`audit run failed (${a.status})`)}}catch(e){if(e instanceof n)throw e;throw console.error("audit run request failed:",e),new n(a(e)?"timeout":"network","audit run request failed")}let t=0;for(;;){await new Promise(e=>setTimeout(e,1e3));let e=null;try{let a=await s("/api/audit/status",{cache:"no-store"});a.ok&&(e=await a.json())}catch{}if(null===e){if(++t>=10)throw new n("network","audit status poll lost the server");continue}if(t=0,!e.running){if(e.error)throw new n("post_failed",e.error);return}}}e.s(["RerunError",0,n,"triggerRun",0,t],86616)}]);
|
package/src/audit/leak-notice.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deciding whether a finding still owes the user a notice — exactly once.
|
|
3
|
-
*
|
|
4
|
-
* ## Why this is not a field on the record
|
|
5
|
-
*
|
|
6
|
-
* The obvious design is `notifiedAt` on each finding, written back after the
|
|
7
|
-
* notice goes out. Measured against this repo's real `writeJsonAtomically`
|
|
8
|
-
* semantics, that fails 100% of the time under the conditions it actually runs
|
|
9
|
-
* in: two concurrent sessions with the same finding produced two notices, four
|
|
10
|
-
* produced four, eight produced eight. Worse, two concurrent sessions with
|
|
11
|
-
* DIFFERENT findings produced two notices and a surviving watermark containing
|
|
12
|
-
* only one of them — the loser's mark permanently gone, so it re-notified
|
|
13
|
-
* forever afterwards. `lib/atomic-write.ts` says so in its own header: atomic
|
|
14
|
-
* per FILE, last-writer-wins per CONTENT, i.e. a lost update on a map.
|
|
15
|
-
*
|
|
16
|
-
* Running several agents at once in one project is not an edge case here; it is
|
|
17
|
-
* how the tool is used.
|
|
18
|
-
*
|
|
19
|
-
* So the claim is one marker FILE per finding, created with `wx` — O_EXCL,
|
|
20
|
-
* which the kernel makes atomic across processes. Eight concurrent claimants
|
|
21
|
-
* produce exactly one winner, verified 3/3 trials. Concurrent claims on
|
|
22
|
-
* different findings all succeed and all survive, because they touch different
|
|
23
|
-
* files.
|
|
24
|
-
*
|
|
25
|
-
* ## Claim ordering, and which way to fail
|
|
26
|
-
*
|
|
27
|
-
* The marker is written BEFORE the notice reaches the stream, and the caller
|
|
28
|
-
* only keeps it if the notice was actually shaped. The alternative orderings
|
|
29
|
-
* were both measured: emit-then-mark nags forever if the process dies between
|
|
30
|
-
* them (5 sessions → 5 notices), and mark-then-never-emit swallows the alert
|
|
31
|
-
* permanently with nothing on disk saying so (total delivered: 0).
|
|
32
|
-
*
|
|
33
|
-
* Between "told twice" and "never told", a security tool picks told twice.
|
|
34
|
-
*/
|
|
35
|
-
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, rmSync } from "node:fs";
|
|
36
|
-
import { resolve } from "node:path";
|
|
37
|
-
|
|
38
|
-
import { auditDir } from "../hooks/fp-home";
|
|
39
|
-
import { readLeakRecord, activeFindings, readLeakIdentity } from "./leak-store";
|
|
40
|
-
import { isFindingId } from "./leak-fingerprint";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Which way the user was told. Each channel claims independently.
|
|
44
|
-
*
|
|
45
|
-
* They are NOT one guarantee. The desktop banner and the in-CLI notice reach a
|
|
46
|
-
* person at different moments and fail in unrelated ways — and the banner has a
|
|
47
|
-
* failure it cannot detect at all: on a locked screen the shell accepts the
|
|
48
|
-
* call and returns an id while nobody sees anything. Letting a successful
|
|
49
|
-
* `Notify` claim the finding would then silence the in-session notice too, and
|
|
50
|
-
* the only channel that actually reached a human is the one we suppressed.
|
|
51
|
-
*
|
|
52
|
-
* So each channel gets its own marker directory and its own once-per-finding
|
|
53
|
-
* guarantee. A user at a desktop sees the banner AND the next session's notice;
|
|
54
|
-
* that is a bounded, deliberate duplicate, and it is the right side of the
|
|
55
|
-
* trade this file already made once (told twice beats never told).
|
|
56
|
-
*/
|
|
57
|
-
export type NoticeChannel = "cli" | "desktop";
|
|
58
|
-
|
|
59
|
-
/** One empty file per claimed finding, per channel. Cheap: a few hundred
|
|
60
|
-
* zero-byte files at the very worst, and the record itself is capped at 500
|
|
61
|
-
* findings. "cli" keeps the original directory name so an existing machine's
|
|
62
|
-
* claims survive the upgrade — renaming it would re-notify for every finding
|
|
63
|
-
* already delivered. */
|
|
64
|
-
const NOTICE_DIR = (home?: string, channel: NoticeChannel = "cli") =>
|
|
65
|
-
resolve(auditDir(home), channel === "cli" ? "notified" : `notified-${channel}`);
|
|
66
|
-
|
|
67
|
-
/** Markers older than this are swept, so the directory cannot grow forever on a
|
|
68
|
-
* machine that has been running for years. Comfortably longer than the
|
|
69
|
-
* record's own 90-day TTL, so a marker never outlives its finding. */
|
|
70
|
-
const MARKER_TTL_MS = 120 * 86_400_000;
|
|
71
|
-
|
|
72
|
-
export interface PendingNotice {
|
|
73
|
-
/** Findings that have never been notified about and are not dismissed. */
|
|
74
|
-
ids: string[];
|
|
75
|
-
count: number;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Findings that still owe the user a notice.
|
|
80
|
-
*
|
|
81
|
-
* Reads the record and subtracts what has already been claimed. Never throws:
|
|
82
|
-
* on any failure it returns "nothing pending", because a hook that cannot read
|
|
83
|
-
* its bookkeeping must stay silent rather than guess.
|
|
84
|
-
*/
|
|
85
|
-
export function pendingLeakNotice(home?: string, channel: NoticeChannel = "cli"): PendingNotice {
|
|
86
|
-
try {
|
|
87
|
-
const dir = NOTICE_DIR(home, channel);
|
|
88
|
-
const claimed = new Set(existsSync(dir) ? readdirSync(dir) : []);
|
|
89
|
-
const ids = activeFindings(readLeakRecord(home), home)
|
|
90
|
-
.map((f) => f.id)
|
|
91
|
-
.filter((id) => !claimed.has(id));
|
|
92
|
-
return { ids, count: ids.length };
|
|
93
|
-
} catch {
|
|
94
|
-
return { ids: [], count: 0 };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Claim these findings, atomically and per-finding.
|
|
100
|
-
*
|
|
101
|
-
* Returns the ids this process actually won. A caller that loses a race simply
|
|
102
|
-
* gets a shorter list — it never blocks, and it never overwrites a claim made
|
|
103
|
-
* by a concurrent session.
|
|
104
|
-
*/
|
|
105
|
-
export function markLeakNoticeDelivered(
|
|
106
|
-
ids: string[],
|
|
107
|
-
home?: string,
|
|
108
|
-
channel: NoticeChannel = "cli",
|
|
109
|
-
): string[] {
|
|
110
|
-
const won: string[] = [];
|
|
111
|
-
try {
|
|
112
|
-
const dir = NOTICE_DIR(home, channel);
|
|
113
|
-
mkdirSync(dir, { recursive: true });
|
|
114
|
-
for (const id of ids) {
|
|
115
|
-
// An id is about to become a filename. Anything that is not the exact
|
|
116
|
-
// shape `fingerprintId` mints is refused rather than resolved — see
|
|
117
|
-
// `isFindingId`. Refusing also means NOT reporting it as won, so a caller
|
|
118
|
-
// treats it as unclaimed and never records it as delivered.
|
|
119
|
-
if (!isFindingId(id)) continue;
|
|
120
|
-
try {
|
|
121
|
-
// `wx` is O_EXCL: it throws EEXIST if another process got here first.
|
|
122
|
-
// That throw IS the lock — there is no read-then-write window for two
|
|
123
|
-
// processes to both pass through.
|
|
124
|
-
writeFileSync(resolve(dir, id), "", { flag: "wx", mode: 0o600 });
|
|
125
|
-
won.push(id);
|
|
126
|
-
} catch {
|
|
127
|
-
// Already claimed by a concurrent session. Correct outcome: the user
|
|
128
|
-
// is told once, by whichever session got there first.
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
} catch {
|
|
132
|
-
// Cannot claim at all — the caller has already decided to emit, and being
|
|
133
|
-
// told twice beats never being told.
|
|
134
|
-
}
|
|
135
|
-
return won;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Release claims this process won when delivery failed, so the next scheduled
|
|
139
|
-
* run retries instead of turning a transient missing desktop into permanent
|
|
140
|
-
* silence. Only validated ids in the requested channel are touched. */
|
|
141
|
-
export function releaseLeakNoticeClaims(
|
|
142
|
-
ids: string[],
|
|
143
|
-
home?: string,
|
|
144
|
-
channel: NoticeChannel = "cli",
|
|
145
|
-
): void {
|
|
146
|
-
try {
|
|
147
|
-
const dir = NOTICE_DIR(home, channel);
|
|
148
|
-
for (const id of ids) {
|
|
149
|
-
if (!isFindingId(id)) continue;
|
|
150
|
-
rmSync(resolve(dir, id), { force: true });
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
// Retry bookkeeping must never turn a completed audit into a failure.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Drop markers for findings that no longer exist, and very old ones. */
|
|
158
|
-
export function pruneNoticeMarkers(home?: string, nowMs = Date.now()): void {
|
|
159
|
-
try {
|
|
160
|
-
const live = new Set(readLeakRecord(home).findings.map((f) => f.id));
|
|
161
|
-
for (const channel of ["cli", "desktop"] as const) {
|
|
162
|
-
const dir = NOTICE_DIR(home, channel);
|
|
163
|
-
if (!existsSync(dir)) continue;
|
|
164
|
-
for (const name of readdirSync(dir)) {
|
|
165
|
-
const path = resolve(dir, name);
|
|
166
|
-
let stale = !live.has(name);
|
|
167
|
-
if (!stale) {
|
|
168
|
-
try {
|
|
169
|
-
stale = nowMs - statSync(path).mtimeMs > MARKER_TTL_MS;
|
|
170
|
-
} catch {
|
|
171
|
-
stale = false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (stale) rmSync(path, { force: true });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// Housekeeping only.
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Should this session's agent tell the user about unviewed credentials?
|
|
184
|
-
*
|
|
185
|
-
* ## Why this is not the per-finding claim used for the desktop banner
|
|
186
|
-
*
|
|
187
|
-
* The per-finding marker answers "have we emitted this once?" — and emitting is
|
|
188
|
-
* not the same as arriving. Twice in one day it recorded every finding as
|
|
189
|
-
* delivered while the user saw nothing: once because the notice was attached to
|
|
190
|
-
* an event whose channel the host ignores (`SessionStart` takes
|
|
191
|
-
* `additionalContext`, not `systemMessage`), and once because hooks were
|
|
192
|
-
* disabled in the project under test. Neither is detectable from in here, and
|
|
193
|
-
* both were permanent — a claimed finding is never retried.
|
|
194
|
-
*
|
|
195
|
-
* So the in-CLI notice keys on the USER's action instead of on ours. It shows
|
|
196
|
-
* while findings exist that are newer than the last time the report was
|
|
197
|
-
* actually opened, and it stops when they open it. A dropped notice costs one
|
|
198
|
-
* session's silence rather than the alert; the only thing that silences it for
|
|
199
|
-
* good is the outcome we wanted anyway.
|
|
200
|
-
*
|
|
201
|
-
* Bounded by a per-SESSION marker so a long session gets one line, not one per
|
|
202
|
-
* turn. Sessions are cheap and few; findings are many and long-lived.
|
|
203
|
-
*/
|
|
204
|
-
export function sessionNoticePending(sessionId: string, home?: string): PendingNotice {
|
|
205
|
-
try {
|
|
206
|
-
if (!sessionId || !/^[\w.:-]{1,200}$/.test(sessionId)) return { ids: [], count: 0 };
|
|
207
|
-
const dir = resolve(auditDir(home), "notified-sessions");
|
|
208
|
-
if (existsSync(resolve(dir, sessionId))) return { ids: [], count: 0 };
|
|
209
|
-
|
|
210
|
-
const findings = activeFindings(readLeakRecord(home), home);
|
|
211
|
-
if (findings.length === 0) return { ids: [], count: 0 };
|
|
212
|
-
|
|
213
|
-
// Only what the user has not already looked at. `firstSeen`, not
|
|
214
|
-
// `lastSeen`: re-encountering a credential the user has already reviewed is
|
|
215
|
-
// not news, however recently the scan tripped over it again.
|
|
216
|
-
const viewedAt = readLeakIdentity(home).reportViewedAt ?? 0;
|
|
217
|
-
const unviewed = findings.filter((f) => {
|
|
218
|
-
const seen = Date.parse(f.firstSeen);
|
|
219
|
-
return !Number.isFinite(seen) || seen > viewedAt;
|
|
220
|
-
});
|
|
221
|
-
return { ids: unviewed.map((f) => f.id), count: unviewed.length };
|
|
222
|
-
} catch {
|
|
223
|
-
return { ids: [], count: 0 };
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Claim this session, so the notice appears once rather than every turn.
|
|
229
|
-
*
|
|
230
|
-
* Returns whether this caller won. O_EXCL again, for the same reason as the
|
|
231
|
-
* per-finding markers: several agents run in one project at once.
|
|
232
|
-
*/
|
|
233
|
-
export function markSessionNoticed(sessionId: string, home?: string): boolean {
|
|
234
|
-
try {
|
|
235
|
-
if (!sessionId || !/^[\w.:-]{1,200}$/.test(sessionId)) return false;
|
|
236
|
-
const dir = resolve(auditDir(home), "notified-sessions");
|
|
237
|
-
mkdirSync(dir, { recursive: true });
|
|
238
|
-
writeFileSync(resolve(dir, sessionId), "", { flag: "wx", mode: 0o600 });
|
|
239
|
-
return true;
|
|
240
|
-
} catch {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Session markers outlive nothing useful — a session id never recurs. */
|
|
246
|
-
export function pruneSessionMarkers(home?: string, nowMs = Date.now()): void {
|
|
247
|
-
try {
|
|
248
|
-
const dir = resolve(auditDir(home), "notified-sessions");
|
|
249
|
-
if (!existsSync(dir)) return;
|
|
250
|
-
for (const name of readdirSync(dir)) {
|
|
251
|
-
const path = resolve(dir, name);
|
|
252
|
-
try {
|
|
253
|
-
if (nowMs - statSync(path).mtimeMs > 30 * 86_400_000) rmSync(path, { force: true });
|
|
254
|
-
} catch { /* raced */ }
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// Housekeeping only.
|
|
258
|
-
}
|
|
259
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|