failproofai 1.0.4-beta.4 → 1.0.4-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +5 -5
- package/.next/standalone/.next/server/app/audit/page.js +3 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
- package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0-4ja6q._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__00cr3xu._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__04px77e._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__190won8._.js → [root-of-the-server]__0dxf928._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s740oi._.js → [root-of-the-server]__0egr5s1._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01wy8d-._.js → [root-of-the-server]__0i0l1di._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0kokc8z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ctpynv._.js → [root-of-the-server]__0n2txhk._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__0r3c1_0._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__17xnxlx._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__1ckpmkk._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00-s7h8._.js → [root-of-the-server]__1fvfw59._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yl9wb5._.js → [root-of-the-server]__1ui5ur8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0guxnh-._.js → _02b3i98._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1u8-lu2._.js → _07t3jol._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _0s-77v5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +4 -14
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
- package/.next/standalone/.next/static/chunks/{364cqbpeg1y5i.js → 07c6l1lq5_69b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bxr2_2h4kaqq.js → 12bsgz8czvcnn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{38dqur43qthrl.js → 1gn08w2pgqh09.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1i-i0pxg9q_8y.js → 1xgq22tri86an.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17tfki9x5rd_7.js → 20xkiqzvfkuvm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42gwu2bsz4ske.js → 2a84ug32tzik2.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0po8n4tyotjf2.js → 32m2ryfankf_2.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ioub_lpen-sq.js +1 -0
- package/.next/standalone/.next/static/chunks/{323cyb3a2s06y.js → 3jxd133cw2vm3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0qx7gav1c2_vu.js → 44ncu88eq6z71.js} +1 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -5
- package/.next/standalone/app/actions/update-scheduled-audit.ts +4 -56
- package/.next/standalone/app/api/auth/status/route.ts +3 -2
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +13 -39
- package/.next/standalone/lib/auth/api-server-client.ts +6 -6
- package/.next/standalone/lib/codex-sessions.ts +20 -4
- package/.next/standalone/lib/copilot-sessions.ts +32 -1
- package/.next/standalone/lib/cursor-sessions.ts +62 -5
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +952 -1063
- package/dist/worker.mjs +173 -380
- package/lib/auth/api-server-client.ts +6 -6
- package/lib/codex-sessions.ts +20 -4
- package/lib/copilot-sessions.ts +32 -1
- package/lib/cursor-sessions.ts +62 -5
- package/package.json +9 -9
- package/pi-extension/index.ts +10 -18
- package/src/audit/cli.ts +72 -81
- package/src/audit/harm-report.ts +12 -6
- package/src/audit/index.ts +2 -0
- package/src/audit/leak-fingerprint.ts +2 -4
- package/src/audit/leak-record.ts +13 -9
- package/src/audit/leak-scan.ts +1 -1
- package/src/audit/leak-store.ts +1 -46
- package/src/audit/machine-store.ts +8 -9
- package/src/audit/macos-notifier.ts +2 -2
- package/src/audit/redact-example.ts +2 -0
- package/src/audit/report-harm.ts +31 -46
- package/src/audit/schedule-cli.ts +17 -33
- package/src/audit/types.ts +5 -0
- package/src/hooks/configure-wizard.ts +28 -0
- package/src/hooks/fp-config.ts +29 -54
- package/src/hooks/handler.ts +1 -79
- package/src/hooks/integrations.ts +4 -15
- package/src/hooks/notice.ts +12 -179
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +0 -3
- package/.next/standalone/.next/static/chunks/04o5zfl7p5lhx.js +0 -1
- package/src/audit/leak-notice.ts +0 -259
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{P6icI2V2PvVuePJrqjHCr → aeM365dNTtUZOZvNkX8ba}/_ssgManifest.js +0 -0
package/src/hooks/fp-config.ts
CHANGED
|
@@ -308,22 +308,15 @@ export interface FpConfig {
|
|
|
308
308
|
};
|
|
309
309
|
audit: {
|
|
310
310
|
/**
|
|
311
|
-
* Scan this machine on a schedule
|
|
312
|
-
*
|
|
311
|
+
* Scan this machine on a schedule. Delivery is deliberately separate:
|
|
312
|
+
* desktop notifications are controlled by `notify`, while a verified local
|
|
313
|
+
* email identity adds email alerts without changing the timer.
|
|
313
314
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* are the same decision and take the same switch.
|
|
320
|
-
*
|
|
321
|
-
* A signed-out machine with this on is therefore a real, expected state
|
|
322
|
-
* rather than a contradiction: it keeps scanning and keeps its local
|
|
323
|
-
* dashboard current, and the UI says "signed out" until somebody signs back
|
|
324
|
-
* in. Auth gates SETTING it up, never the machine's ongoing work — a
|
|
325
|
-
* refresh token expiring must not silently switch off a background feature
|
|
326
|
-
* somebody configured months ago.
|
|
315
|
+
* A signed-out machine with this on is therefore the normal local-only
|
|
316
|
+
* state rather than a contradiction: it keeps scanning, keeps its local
|
|
317
|
+
* dashboard current, and raises desktop notifications for credential leaks.
|
|
318
|
+
* A refresh token expiring must not silently switch off a background
|
|
319
|
+
* security feature.
|
|
327
320
|
*
|
|
328
321
|
* ON for a configured machine that has never stated an opinion, OFF for
|
|
329
322
|
* every machine we could not read an opinion from — which is not the same
|
|
@@ -345,9 +338,9 @@ export interface FpConfig {
|
|
|
345
338
|
* agree, or the settings page describes a machine that is doing something
|
|
346
339
|
* else.
|
|
347
340
|
*
|
|
348
|
-
* Turning it on sends
|
|
349
|
-
*
|
|
350
|
-
*
|
|
341
|
+
* Turning it on sends nothing remotely on its own. Email requires a valid
|
|
342
|
+
* identity in `audit/session.json`; without one, scans and notifications
|
|
343
|
+
* remain fully local.
|
|
351
344
|
*/
|
|
352
345
|
auto: boolean;
|
|
353
346
|
/**
|
|
@@ -358,40 +351,22 @@ export interface FpConfig {
|
|
|
358
351
|
* wants the scan and not the banner is a coherent person, and without this
|
|
359
352
|
* their only way to stop the banner is to stop the scan.
|
|
360
353
|
*
|
|
361
|
-
* ON by default
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* a machine that finds the key and tells nobody.
|
|
354
|
+
* ON by default. Every scheduled scan that still sees a leak may notify,
|
|
355
|
+
* which keeps a credential that remains exposed from disappearing after
|
|
356
|
+
* its first alert. A silent default would make the common case — a user who
|
|
357
|
+
* never opens the dashboard and never gave an email — a machine that finds
|
|
358
|
+
* the key and tells nobody.
|
|
367
359
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* SSH session. Turning off every channel at once is not an option this
|
|
372
|
-
* offers, because the resulting state is indistinguishable from broken.
|
|
360
|
+
* On a headless box, container or SSH-only session there may be no desktop
|
|
361
|
+
* server to receive the banner. The scheduled scan still completes and
|
|
362
|
+
* records its result; notification failure never changes the scan outcome.
|
|
373
363
|
*/
|
|
374
364
|
notify: boolean;
|
|
375
365
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* and it is never drawn as one. `auto` is what a person sets; this is a
|
|
381
|
-
* record of the disclosure they were shown when they set it. The two cannot
|
|
382
|
-
* drift, because every path that turns `auto` on stamps this in the same
|
|
383
|
-
* call and nothing stamps it alone.
|
|
384
|
-
*
|
|
385
|
-
* It exists because `auto` CHANGED MEANING. Through 1.0.0 it meant "scan
|
|
386
|
-
* this machine on a timer" and nothing more — no account, no network, and
|
|
387
|
-
* the toggle that wrote it said as much in as many words. Harm digests gave
|
|
388
|
-
* the same stored bit a second job: uploading redacted transcript excerpts
|
|
389
|
-
* to the api-server and mailing them. Without a separate record, every
|
|
390
|
-
* machine that opted into the old meaning would have started sending on
|
|
391
|
-
* upgrade, having agreed to nothing of the kind, with the only notice a
|
|
392
|
-
* line in the journal. Gating on the stamp rather than the switch is what
|
|
393
|
-
* keeps that upgrade silent in the safe direction: those machines keep
|
|
394
|
-
* scanning locally and send nothing until somebody opts in again.
|
|
366
|
+
* Legacy migration marker from the earlier harm-digest consent model.
|
|
367
|
+
* Retained when reading and writing old configs so upgrades are lossless,
|
|
368
|
+
* but it no longer gates scheduling or credential-alert email. A verified
|
|
369
|
+
* email identity is now the delivery opt-in.
|
|
395
370
|
*/
|
|
396
371
|
reportsConsentedAt?: number;
|
|
397
372
|
/** Days between scheduled runs. Wall clock, so it survives suspend. */
|
|
@@ -587,8 +562,8 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
|
587
562
|
// question is only whether to speak.
|
|
588
563
|
notify: audit.notify !== false,
|
|
589
564
|
intervalDays: readIntervalDays(audit.interval_days),
|
|
590
|
-
//
|
|
591
|
-
//
|
|
565
|
+
// Preserve the legacy marker when it is valid; current behavior does
|
|
566
|
+
// not consult it.
|
|
592
567
|
reportsConsentedAt:
|
|
593
568
|
typeof audit.reports_consented_at === "number" &&
|
|
594
569
|
Number.isFinite(audit.reports_consented_at)
|
|
@@ -738,7 +713,8 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
|
|
|
738
713
|
...(config.telemetry.enabled ? {} : { telemetry: { enabled: false } }),
|
|
739
714
|
// Written ALWAYS, unlike telemetry directly above — the two are opposites
|
|
740
715
|
// on purpose. Telemetry ships on and is deliberately not advertised;
|
|
741
|
-
// the scheduled audit
|
|
716
|
+
// the scheduled audit is configured during setup and is meant to be FOUND,
|
|
717
|
+
// and a switch
|
|
742
718
|
// nobody can see is the same as a switch that does not exist. Emitting both
|
|
743
719
|
// keys unconditionally also makes "a user's setting survives a rewrite"
|
|
744
720
|
// total rather than conditional.
|
|
@@ -749,9 +725,8 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
|
|
|
749
725
|
// exists to be found by somebody the banner annoyed.
|
|
750
726
|
notify: config.audit.notify,
|
|
751
727
|
interval_days: config.audit.intervalDays,
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
// `OWNED_CONFIG_KEYS`, so omitting it here really removes it.
|
|
728
|
+
// Preserve the legacy consent marker when present. It is not used by the
|
|
729
|
+
// current local-schedule + optional-email model.
|
|
755
730
|
...(config.audit.reportsConsentedAt === undefined
|
|
756
731
|
? {}
|
|
757
732
|
: { reports_consented_at: config.audit.reportsConsentedAt }),
|
package/src/hooks/handler.ts
CHANGED
|
@@ -35,8 +35,6 @@ import { loadAllCustomHooks } from "./custom-hooks-loader";
|
|
|
35
35
|
import type { CustomHook } from "./policy-types";
|
|
36
36
|
import { persistHookActivity } from "./hook-activity-store";
|
|
37
37
|
import { deliveryHealth, deliveryHealthLine } from "./delivery-health";
|
|
38
|
-
import { shapeNotice, canDeliverNotice, leakNoticeText } from "./notice";
|
|
39
|
-
import { sessionNoticePending, markSessionNoticed } from "../audit/leak-notice";
|
|
40
38
|
import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
|
|
41
39
|
import { resolveCwd } from "./resolve-cwd";
|
|
42
40
|
import { resolvePermissionMode } from "./resolve-permission-mode";
|
|
@@ -106,67 +104,6 @@ export interface HookEventOutcome {
|
|
|
106
104
|
stderr: string;
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
/**
|
|
110
|
-
* Merge a pending leak notice into an outcome, once.
|
|
111
|
-
*
|
|
112
|
-
* Anchored on Stop wherever the host renders there, and SessionStart only where
|
|
113
|
-
* it does not: Stop fires once per turn, AFTER the answer, so it never
|
|
114
|
-
* interrupts what the user was reading — and at session open the background
|
|
115
|
-
* scan may not have produced the finding yet.
|
|
116
|
-
*
|
|
117
|
-
* Every failure is swallowed. A courtesy message must never be the reason a
|
|
118
|
-
* hook misbehaves, and the finding is on disk either way.
|
|
119
|
-
*/
|
|
120
|
-
function attachLeakNotice(
|
|
121
|
-
eventType: string,
|
|
122
|
-
cli: IntegrationType | undefined,
|
|
123
|
-
outcome: HookEventOutcome,
|
|
124
|
-
sessionId: string | undefined,
|
|
125
|
-
): HookEventOutcome {
|
|
126
|
-
try {
|
|
127
|
-
if (!canDeliverNotice(cli)) return outcome;
|
|
128
|
-
// canDeliverNotice() above already proved cli is set; narrow for the call.
|
|
129
|
-
if (!cli) return outcome;
|
|
130
|
-
const canonical = canonicalizeEventType(eventType, cli);
|
|
131
|
-
// STOP ONLY, and the narrowing is the fix for a measured failure rather
|
|
132
|
-
// than caution. `notice.ts` derived each host's channel from a live probe
|
|
133
|
-
// on **Stop**; SessionStart was allowed here on the assumption that a
|
|
134
|
-
// channel proven on one event works on another. It does not. On a real
|
|
135
|
-
// machine 499 findings were CLAIMED and the user saw nothing: SessionStart
|
|
136
|
-
// fires first in a session, consumed every claim, and the host dropped the
|
|
137
|
-
// field — Claude Code documents `hookSpecificOutput.additionalContext` for
|
|
138
|
-
// SessionStart, not `systemMessage`.
|
|
139
|
-
//
|
|
140
|
-
// That is the worst outcome this design has: a finding recorded as
|
|
141
|
-
// delivered that reached nobody, and never retried. Stop fires at the end
|
|
142
|
-
// of every assistant turn, so nothing is lost by waiting for it — the user
|
|
143
|
-
// is reading output at that moment anyway. Add SessionStart back only with
|
|
144
|
-
// a probe showing the notice actually rendered there.
|
|
145
|
-
if (canonical !== "Stop") return outcome;
|
|
146
|
-
|
|
147
|
-
// Keyed on the SESSION and on whether the user has opened the report, not
|
|
148
|
-
// on a per-finding "we emitted this once" marker — see
|
|
149
|
-
// `sessionNoticePending` for the two ways that marker lost the alert
|
|
150
|
-
// permanently. A dropped notice now costs one session's silence instead.
|
|
151
|
-
const pending = sessionNoticePending(sessionId ?? "");
|
|
152
|
-
if (pending.count === 0) return outcome;
|
|
153
|
-
|
|
154
|
-
const shaped = shapeNotice(cli, leakNoticeText(pending.count), outcome.stdout);
|
|
155
|
-
if (shaped.stdout === outcome.stdout && !shaped.stderr) return outcome;
|
|
156
|
-
|
|
157
|
-
// Claimed only once the notice is actually on the stream, so a crash
|
|
158
|
-
// between the two re-notifies rather than silently swallowing the alert.
|
|
159
|
-
markSessionNoticed(sessionId ?? "");
|
|
160
|
-
return {
|
|
161
|
-
exitCode: outcome.exitCode,
|
|
162
|
-
stdout: shaped.stdout,
|
|
163
|
-
stderr: outcome.stderr + shaped.stderr,
|
|
164
|
-
};
|
|
165
|
-
} catch {
|
|
166
|
-
return outcome;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
107
|
export interface EvaluateHookEventOptions {
|
|
171
108
|
/**
|
|
172
109
|
* Await pending telemetry POSTs before returning. Defaults to `true`,
|
|
@@ -820,22 +757,7 @@ export async function evaluateHookEvent(
|
|
|
820
757
|
}
|
|
821
758
|
}
|
|
822
759
|
|
|
823
|
-
|
|
824
|
-
//
|
|
825
|
-
// Emitted HERE rather than in `handleHookEvent`, which is what fixes the
|
|
826
|
-
// bug this replaced: `bin/failproofai.mjs`'s `isDaemonConfigured()` branch
|
|
827
|
-
// writes `result.stdout`/`result.stderr` and EXITS INSIDE THAT BRANCH — it
|
|
828
|
-
// never falls through to `handleHookEvent`. Setup requires the daemon, so
|
|
829
|
-
// the previous notice was dead code on every configured machine, and had
|
|
830
|
-
// been since it shipped. Both paths return this object, so producing the
|
|
831
|
-
// notice here reaches both with no change to either caller.
|
|
832
|
-
const withNotice = attachLeakNotice(
|
|
833
|
-
eventType,
|
|
834
|
-
cli,
|
|
835
|
-
{ exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
836
|
-
sessionId,
|
|
837
|
-
);
|
|
838
|
-
return withNotice;
|
|
760
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
839
761
|
} finally {
|
|
840
762
|
if (opts?.awaitTelemetryFlush ?? true) {
|
|
841
763
|
// Await any un-awaited (`void trackHookEvent(...)`) events fired during
|
|
@@ -784,7 +784,6 @@ export const cursor: Integration = {
|
|
|
784
784
|
// • exit 2 OR `permissionDecision: "deny"` → `throw new Error(reason)`
|
|
785
785
|
// (which OpenCode surfaces as a tool-call failure to the agent)
|
|
786
786
|
// • `additionalContext` → `client.session.prompt(...)` (fire-and-forget)
|
|
787
|
-
// • `failproofaiNotice` → `client.tui.showToast(...)` (user-visible)
|
|
788
787
|
// • everything else → no-op (allow)
|
|
789
788
|
//
|
|
790
789
|
// Settings paths:
|
|
@@ -957,19 +956,6 @@ async function applyDecision(result, ctx, eventName) {
|
|
|
957
956
|
if (out && out.decision && out.decision.behavior === "deny") {
|
|
958
957
|
throw new Error((out.decision.message) || "Blocked by failproofai");
|
|
959
958
|
}
|
|
960
|
-
const notice = parsed.failproofaiNotice;
|
|
961
|
-
if (typeof notice === "string" && notice && ctx && ctx.client && ctx.client.tui) {
|
|
962
|
-
try {
|
|
963
|
-
await ctx.client.tui.showToast({
|
|
964
|
-
body: {
|
|
965
|
-
title: "failproofai audit",
|
|
966
|
-
message: notice,
|
|
967
|
-
variant: "warning",
|
|
968
|
-
duration: 12_000,
|
|
969
|
-
},
|
|
970
|
-
});
|
|
971
|
-
} catch { /* a courtesy notice must never break the host hook */ }
|
|
972
|
-
}
|
|
973
959
|
// Forward additional context as a prompt to the session. For Stop /
|
|
974
960
|
// SubagentStop the prompt is the only force-retry channel (session.idle
|
|
975
961
|
// already fired), so AWAIT to ensure the SDK round-trip completes before
|
|
@@ -1025,7 +1011,10 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
1025
1011
|
const claudeEvent = BUS_EVENT_MAP[event.type];
|
|
1026
1012
|
if (!claudeEvent) return;
|
|
1027
1013
|
const props = event.properties || {};
|
|
1028
|
-
const
|
|
1014
|
+
const info = props.info || props.session || {};
|
|
1015
|
+
|
|
1016
|
+
const sessionID = info.id || info.sessionID || info.sessionId || info.session_id ||
|
|
1017
|
+
props.sessionID || props.sessionId || props.session_id || props.id;
|
|
1029
1018
|
const r = await runFailproofai(claudeEvent, {
|
|
1030
1019
|
session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
|
|
1031
1020
|
}, directory);
|
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
|
}
|
|
@@ -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`}])}]);
|