failproofai 1.0.3 → 1.0.4-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +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-request/route_client-reference-manifest.js +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/login-verify/route_client-reference-manifest.js +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/logout/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.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]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
- package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
- package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
- package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
- package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
- package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
- package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
- package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
- package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
- package/.next/standalone/app/actions/get-leaks.ts +100 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
- package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
- package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
- package/.next/standalone/app/audit/audit-styles.css +114 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
- package/.next/standalone/lib/auth/api-server-client.ts +28 -0
- package/.next/standalone/lib/claude-sessions.ts +101 -27
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
- package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
- package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +11 -11
- package/.next/standalone/package.json +14 -13
- package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
- package/.next/standalone/sdk/python/README.md +8 -0
- package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
- package/.next/standalone/sdk/python/uv.lock +29 -29
- package/.next/standalone/server.js +1 -1
- package/README.md +13 -58
- package/dist/cli.mjs +5368 -8756
- package/dist/worker.mjs +539 -312
- package/lib/auth/api-server-client.ts +28 -0
- package/lib/claude-sessions.ts +101 -27
- package/package.json +14 -13
- package/pi-extension/index.ts +42 -4
- package/scripts/changelog-open.py +115 -0
- package/src/audit/cli.ts +134 -0
- package/src/audit/desktop-notify.ts +420 -0
- package/src/audit/harm-report.ts +101 -0
- package/src/audit/index.ts +141 -13
- package/src/audit/leak-fingerprint.ts +200 -0
- package/src/audit/leak-notice.ts +161 -0
- package/src/audit/leak-record.ts +232 -0
- package/src/audit/leak-scan.ts +292 -0
- package/src/audit/leak-store.ts +217 -0
- package/src/audit/macos-notifier.ts +310 -0
- package/src/audit/redact-example.ts +266 -14
- package/src/audit/report-harm.ts +12 -1
- package/src/audit/report.ts +6 -3
- package/src/audit/schedule-cli.ts +37 -0
- package/src/audit/scoring.ts +49 -0
- package/src/audit/types.ts +41 -0
- package/src/hooks/builtin-policies.ts +67 -2
- package/src/hooks/configure-wizard.ts +20 -0
- package/src/hooks/fp-config.ts +71 -11
- package/src/hooks/fp-home.ts +27 -0
- package/src/hooks/handler.ts +59 -1
- package/src/hooks/integrations.ts +47 -13
- package/src/hooks/notice.ts +155 -0
- package/src/hooks/uninstall-cli.ts +15 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
- package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
- package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
- package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
- package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
- package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
- package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
probeDaemonEndToEnd,
|
|
75
75
|
uninstallDaemonService,
|
|
76
76
|
} from "./daemon-service";
|
|
77
|
+
import { installMacNotifier } from "../audit/macos-notifier";
|
|
77
78
|
import { hookLogWarn } from "./hook-logger";
|
|
78
79
|
import {
|
|
79
80
|
readCloudCredentials,
|
|
@@ -1442,6 +1443,25 @@ export async function runConfigureWizard(
|
|
|
1442
1443
|
// longer referenced by anything. Keeps the previous version for an
|
|
1443
1444
|
// offline rollback.
|
|
1444
1445
|
pruneOldDaemonBinaries();
|
|
1446
|
+
|
|
1447
|
+
// The delivery path for what the scheduled audit finds. macOS only, and a
|
|
1448
|
+
// no-op everywhere else: a Linux box's audit talks to the session bus
|
|
1449
|
+
// itself, while on a Mac the daemon runs outside the GUI session and cannot
|
|
1450
|
+
// reach Notification Center at all — see `audit/macos-notifier.ts`.
|
|
1451
|
+
//
|
|
1452
|
+
// Installed here, with the daemon, because it exists only to deliver what
|
|
1453
|
+
// the daemon's scheduled scan produces, and removed alongside it by
|
|
1454
|
+
// `uninstall`. Silent, and not a step in the wizard: it needs no password,
|
|
1455
|
+
// asks nothing, and narrating it would be an apology for the delivery half
|
|
1456
|
+
// of a feature the user just switched on. What they are actually asked —
|
|
1457
|
+
// does this machine scan, and may it interrupt — are real settings, and
|
|
1458
|
+
// both are theirs to change afterwards.
|
|
1459
|
+
//
|
|
1460
|
+
// Its failure is not the wizard's failure. A Mac where `osacompile` is
|
|
1461
|
+
// missing or launchd refuses the job must still finish installing its
|
|
1462
|
+
// daemon, its hooks and its policies, which are the parts that enforce
|
|
1463
|
+
// anything; the notice in-CLI still reaches them either way.
|
|
1464
|
+
installMacNotifier();
|
|
1445
1465
|
}
|
|
1446
1466
|
|
|
1447
1467
|
// Telemetry runs concurrently with the install (never rejects, 5s-bounded) so
|
package/src/hooks/fp-config.ts
CHANGED
|
@@ -325,12 +325,53 @@ export interface FpConfig {
|
|
|
325
325
|
* refresh token expiring must not silently switch off a background feature
|
|
326
326
|
* somebody configured months ago.
|
|
327
327
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
328
|
+
* ON for a configured machine that has never stated an opinion, OFF for
|
|
329
|
+
* every machine we could not read an opinion from — which is not the same
|
|
330
|
+
* default twice, and the difference is the whole design.
|
|
331
|
+
*
|
|
332
|
+
* The scan reads the CONTENTS of every session transcript on disk, so the
|
|
333
|
+
* conservative instinct is to keep it off until asked. But the thing it
|
|
334
|
+
* looks for now is a leaked credential, and a credential nobody scans for
|
|
335
|
+
* is a credential nobody revokes: a switch that is off by default is a
|
|
336
|
+
* feature that, for most users, never runs. Setup is where the machine's
|
|
337
|
+
* owner is told what it does, so completing setup is the opinion.
|
|
338
|
+
*
|
|
339
|
+
* Hence the split. Once `config.json` exists AND carries an `audit` table,
|
|
340
|
+
* this is on unless it says exactly `false`. Absent file, unparseable JSON,
|
|
341
|
+
* or no `audit` table at all still read as OFF, because those are the three
|
|
342
|
+
* ways of not knowing, and "we could not tell" must never turn a transcript
|
|
343
|
+
* scan on. `crates/failproofaid/src/audit_lane.rs` makes the identical
|
|
344
|
+
* distinction over the identical bytes — the daemon and this reader must
|
|
345
|
+
* agree, or the settings page describes a machine that is doing something
|
|
346
|
+
* else.
|
|
347
|
+
*
|
|
348
|
+
* Turning it on sends NOTHING on its own: the digest is gated on
|
|
349
|
+
* `reportsConsentedAt` below, not on this switch, so a machine that flips
|
|
350
|
+
* on at upgrade scans locally and mails nobody.
|
|
332
351
|
*/
|
|
333
352
|
auto: boolean;
|
|
353
|
+
/**
|
|
354
|
+
* Raise a desktop notification when a scan finds a credential.
|
|
355
|
+
*
|
|
356
|
+
* Separate from `auto` because they answer different questions: `auto` is
|
|
357
|
+
* whether the machine scans, this is whether it interrupts. Somebody who
|
|
358
|
+
* wants the scan and not the banner is a coherent person, and without this
|
|
359
|
+
* their only way to stop the banner is to stop the scan.
|
|
360
|
+
*
|
|
361
|
+
* ON by default, and the direction is the point: this fires at most once
|
|
362
|
+
* per distinct credential ever (`leak-notice.ts` claims each finding with
|
|
363
|
+
* an O_EXCL marker), so the volume it can reach is bounded by how many
|
|
364
|
+
* secrets are actually leaking. A silent default would make the common
|
|
365
|
+
* case — a user who never opens the dashboard and never gave an email —
|
|
366
|
+
* a machine that finds the key and tells nobody.
|
|
367
|
+
*
|
|
368
|
+
* It does NOT silence the in-CLI notice. That one appears inside a session
|
|
369
|
+
* the user is already driving, costs two lines, and is the only channel
|
|
370
|
+
* left when there is no desktop to notify — a headless box, a container, an
|
|
371
|
+
* SSH session. Turning off every channel at once is not an option this
|
|
372
|
+
* offers, because the resulting state is indistinguishable from broken.
|
|
373
|
+
*/
|
|
374
|
+
notify: boolean;
|
|
334
375
|
/**
|
|
335
376
|
* When this machine's owner agreed that a scheduled scan may send what it
|
|
336
377
|
* finds off the box, as epoch ms. Absent means they never did.
|
|
@@ -402,7 +443,9 @@ export const DEFAULT_CONFIG: FpConfig = {
|
|
|
402
443
|
environment: "local",
|
|
403
444
|
},
|
|
404
445
|
telemetry: { enabled: true },
|
|
405
|
-
|
|
446
|
+
// `auto: false` is the no-file case specifically — see the field's doc for
|
|
447
|
+
// why a machine that HAS a config reads the other way.
|
|
448
|
+
audit: { auto: false, notify: true, intervalDays: DEFAULT_AUDIT_INTERVAL_DAYS },
|
|
406
449
|
};
|
|
407
450
|
|
|
408
451
|
/**
|
|
@@ -495,7 +538,11 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
|
495
538
|
const daemon = (parsed.daemon ?? {}) as Record<string, unknown>;
|
|
496
539
|
const collector = (parsed.collector ?? {}) as Record<string, unknown>;
|
|
497
540
|
const telemetry = (parsed.telemetry ?? {}) as Record<string, unknown>;
|
|
498
|
-
|
|
541
|
+
// Presence, not just contents: `audit.auto` reads differently when the
|
|
542
|
+
// table is absent than when it is present-but-silent. See the field's doc.
|
|
543
|
+
const auditPresent =
|
|
544
|
+
!!parsed.audit && typeof parsed.audit === "object" && !Array.isArray(parsed.audit);
|
|
545
|
+
const audit = (auditPresent ? parsed.audit : {}) as Record<string, unknown>;
|
|
499
546
|
return {
|
|
500
547
|
// Anything unrecognised reads as `oss`. The failure direction matters: a
|
|
501
548
|
// corrupt config must not be able to turn cloud reporting ON.
|
|
@@ -525,12 +572,20 @@ export function projectConfig(parsed: Record<string, unknown>): FpConfig {
|
|
|
525
572
|
// reads as on — the shipped default, and what a config with no
|
|
526
573
|
// [telemetry] block at all means.
|
|
527
574
|
telemetry: { enabled: telemetry.enabled !== false },
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
575
|
+
// A configured machine with no stated opinion scans; a machine we could
|
|
576
|
+
// not read an opinion FROM does not. `auditPresent` is what separates
|
|
577
|
+
// them, and it is the reason this is not simply `!== false`: with no
|
|
578
|
+
// `audit` table there is no opinion to have been stated, only an absence,
|
|
579
|
+
// and an absence must not start reading every transcript on disk. Same
|
|
580
|
+
// three-way split as `audit_lane.rs`, over the same file.
|
|
532
581
|
audit: {
|
|
533
|
-
auto: audit.auto
|
|
582
|
+
auto: auditPresent && audit.auto !== false,
|
|
583
|
+
// Default ON, off only on an explicit `false` — a notification the user
|
|
584
|
+
// never sees is the same as no detection at all. Ungated on
|
|
585
|
+
// `auditPresent`, unlike `auto`: this is read at the moment a
|
|
586
|
+
// notification is about to fire, which means a scan already ran and the
|
|
587
|
+
// question is only whether to speak.
|
|
588
|
+
notify: audit.notify !== false,
|
|
534
589
|
intervalDays: readIntervalDays(audit.interval_days),
|
|
535
590
|
// A finite number or nothing. A garbage value reads as absent, which is
|
|
536
591
|
// the direction that sends nothing.
|
|
@@ -584,6 +639,7 @@ const OWNED_CONFIG_KEYS: readonly (readonly string[])[] = [
|
|
|
584
639
|
["collector", "sources", "*", "extra_paths"],
|
|
585
640
|
["telemetry", "enabled"],
|
|
586
641
|
["audit", "auto"],
|
|
642
|
+
["audit", "notify"],
|
|
587
643
|
["audit", "interval_days"],
|
|
588
644
|
["audit", "reports_consented_at"],
|
|
589
645
|
];
|
|
@@ -688,6 +744,10 @@ export function writeConfig(config: FpConfig, raw?: Record<string, unknown>): vo
|
|
|
688
744
|
// total rather than conditional.
|
|
689
745
|
audit: {
|
|
690
746
|
auto: config.audit.auto,
|
|
747
|
+
// Emitted unconditionally, like `auto`. A switch that only appears in the
|
|
748
|
+
// file once you have used it is a switch nobody discovers, and this one
|
|
749
|
+
// exists to be found by somebody the banner annoyed.
|
|
750
|
+
notify: config.audit.notify,
|
|
691
751
|
interval_days: config.audit.intervalDays,
|
|
692
752
|
// Written only once there IS consent, so an untouched machine's config
|
|
693
753
|
// does not grow a key implying it was asked. It is in
|
package/src/hooks/fp-home.ts
CHANGED
|
@@ -299,6 +299,31 @@ export const auditSessionFile = (home?: string) => resolve(auditDir(home), "sess
|
|
|
299
299
|
*/
|
|
300
300
|
export const auditMachineFile = (home?: string) => resolve(auditDir(home), "machine.json");
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The leak record: one entry per DISTINCT credential seen in a transcript.
|
|
304
|
+
*
|
|
305
|
+
* `derived` — every finding in it is reproducible by rescanning, so a reset
|
|
306
|
+
* costs a rescan and nothing else. What is NOT reproducible lives next door in
|
|
307
|
+
* `auditLeakIdentityFile`, because the two have opposite reset semantics and
|
|
308
|
+
* this directory's own header says to classify the children, never the parent.
|
|
309
|
+
*/
|
|
310
|
+
export const auditLeaksFile = (home?: string) => resolve(auditDir(home), "leaks.json");
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The two things about a leak that a rescan cannot recreate: the HMAC salt that
|
|
314
|
+
* gives each finding its stable id, and the ids the user has dismissed.
|
|
315
|
+
*
|
|
316
|
+
* `identity`, and both halves for the same reason. Regenerate the salt and every
|
|
317
|
+
* id changes, so every previously-seen credential reads as brand new and the
|
|
318
|
+
* machine re-alerts for all of them at once. Drop the dismissals and the
|
|
319
|
+
* findings a human already judged not-a-secret come back — which is worse than
|
|
320
|
+
* a missed alert, because it teaches them the tool does not listen.
|
|
321
|
+
*
|
|
322
|
+
* Small by construction: a 32-byte salt and a list of 16-char ids.
|
|
323
|
+
*/
|
|
324
|
+
export const auditLeakIdentityFile = (home?: string) =>
|
|
325
|
+
resolve(auditDir(home), "leak-identity.json");
|
|
326
|
+
|
|
302
327
|
|
|
303
328
|
// ── fp-cloud-cli ───────────────────────────────────────────────────────────────────
|
|
304
329
|
|
|
@@ -628,6 +653,7 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
|
|
|
628
653
|
// history the user was already told about. Kept OUT of `auditSessionFile`
|
|
629
654
|
// precisely so both survive a sign-out.
|
|
630
655
|
{ path: auditMachineFile, class: "identity" },
|
|
656
|
+
{ path: auditLeakIdentityFile, class: "identity" },
|
|
631
657
|
|
|
632
658
|
// ── May be dropped: rebuilt on demand ──
|
|
633
659
|
// NOTE: `auditDir` itself is deliberately absent. Layout 4 made it MIXED — it
|
|
@@ -637,6 +663,7 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
|
|
|
637
663
|
// the delete list.
|
|
638
664
|
{ path: auditDashboardFile, class: "derived" },
|
|
639
665
|
{ path: auditCacheDir, class: "derived" },
|
|
666
|
+
{ path: auditLeaksFile, class: "derived" },
|
|
640
667
|
{ path: auditScheduleFile, class: "derived" },
|
|
641
668
|
{ path: collectorHealthFile, class: "derived" },
|
|
642
669
|
{ path: codexSessionPathsFile, class: "derived" },
|
package/src/hooks/handler.ts
CHANGED
|
@@ -35,6 +35,8 @@ 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 { pendingLeakNotice, markLeakNoticeDelivered } from "../audit/leak-notice";
|
|
38
40
|
import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
|
|
39
41
|
import { resolveCwd } from "./resolve-cwd";
|
|
40
42
|
import { resolvePermissionMode } from "./resolve-permission-mode";
|
|
@@ -104,6 +106,48 @@ export interface HookEventOutcome {
|
|
|
104
106
|
stderr: string;
|
|
105
107
|
}
|
|
106
108
|
|
|
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
|
+
): HookEventOutcome {
|
|
125
|
+
try {
|
|
126
|
+
if (!canDeliverNotice(cli)) return outcome;
|
|
127
|
+
// canDeliverNotice() above already proved cli is set; narrow for the call.
|
|
128
|
+
if (!cli) return outcome;
|
|
129
|
+
const canonical = canonicalizeEventType(eventType, cli);
|
|
130
|
+
if (canonical !== "Stop" && canonical !== "SessionStart") return outcome;
|
|
131
|
+
|
|
132
|
+
const pending = pendingLeakNotice();
|
|
133
|
+
if (pending.count === 0) return outcome;
|
|
134
|
+
|
|
135
|
+
const shaped = shapeNotice(cli, leakNoticeText(pending.count), outcome.stdout);
|
|
136
|
+
if (shaped.stdout === outcome.stdout && !shaped.stderr) return outcome;
|
|
137
|
+
|
|
138
|
+
// Claimed only once the notice is actually on the stream, so a crash
|
|
139
|
+
// between the two re-notifies rather than silently swallowing the alert.
|
|
140
|
+
markLeakNoticeDelivered(pending.ids);
|
|
141
|
+
return {
|
|
142
|
+
exitCode: outcome.exitCode,
|
|
143
|
+
stdout: shaped.stdout,
|
|
144
|
+
stderr: outcome.stderr + shaped.stderr,
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return outcome;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
107
151
|
export interface EvaluateHookEventOptions {
|
|
108
152
|
/**
|
|
109
153
|
* Await pending telemetry POSTs before returning. Defaults to `true`,
|
|
@@ -757,7 +801,21 @@ export async function evaluateHookEvent(
|
|
|
757
801
|
}
|
|
758
802
|
}
|
|
759
803
|
|
|
760
|
-
|
|
804
|
+
// Attach a pending leak notice, if this event is one that can carry it.
|
|
805
|
+
//
|
|
806
|
+
// Emitted HERE rather than in `handleHookEvent`, which is what fixes the
|
|
807
|
+
// bug this replaced: `bin/failproofai.mjs`'s `isDaemonConfigured()` branch
|
|
808
|
+
// writes `result.stdout`/`result.stderr` and EXITS INSIDE THAT BRANCH — it
|
|
809
|
+
// never falls through to `handleHookEvent`. Setup requires the daemon, so
|
|
810
|
+
// the previous notice was dead code on every configured machine, and had
|
|
811
|
+
// been since it shipped. Both paths return this object, so producing the
|
|
812
|
+
// notice here reaches both with no change to either caller.
|
|
813
|
+
const withNotice = attachLeakNotice(eventType, cli, {
|
|
814
|
+
exitCode: result.exitCode,
|
|
815
|
+
stdout: result.stdout,
|
|
816
|
+
stderr: result.stderr,
|
|
817
|
+
});
|
|
818
|
+
return withNotice;
|
|
761
819
|
} finally {
|
|
762
820
|
if (opts?.awaitTelemetryFlush ?? true) {
|
|
763
821
|
// Await any un-awaited (`void trackHookEvent(...)`) events fired during
|
|
@@ -828,7 +828,7 @@ function buildOpenCodePluginShim(binaryPath: string, scope: HookScope): string {
|
|
|
828
828
|
// Re-generate via: failproofai policies --install --cli opencode
|
|
829
829
|
// Plugin shim that bridges OpenCode's plugin API to the failproofai binary.
|
|
830
830
|
// See: https://opencode.ai/docs/plugins/
|
|
831
|
-
import {
|
|
831
|
+
import { spawn } from "node:child_process";
|
|
832
832
|
|
|
833
833
|
// Map opencode bus-event types → canonical failproofai event names.
|
|
834
834
|
// (The binary sees PascalCase — the binary's --cli=opencode flag is for
|
|
@@ -891,18 +891,52 @@ function canonicalizeToolInput(canonicalToolName, args) {
|
|
|
891
891
|
const FAILPROOFAI_BIN = ${escapedBin};
|
|
892
892
|
const USE_NPX = ${useNpx};
|
|
893
893
|
|
|
894
|
-
|
|
894
|
+
/**
|
|
895
|
+
* Run failproofai for one event WITHOUT blocking opencode's event loop.
|
|
896
|
+
*
|
|
897
|
+
* This was spawnSync, and opencode loads the plugin in-process in the TUI —
|
|
898
|
+
* so every hook froze rendering and input for the subprocess's full duration,
|
|
899
|
+
* with a 60s ceiling. The verdicts are unchanged: every caller already awaits
|
|
900
|
+
* applyDecision, so the deny still lands before the tool runs. What changes
|
|
901
|
+
* is that the wait is now a promise rather than a blocked thread, and the TUI
|
|
902
|
+
* keeps painting while it happens.
|
|
903
|
+
*
|
|
904
|
+
* Fail-open on spawn error, and on the timeout — a policy that never ran must
|
|
905
|
+
* not read as a deny.
|
|
906
|
+
*/
|
|
907
|
+
async function runFailproofai(eventName, payload, directory) {
|
|
895
908
|
const cmd = USE_NPX ? "npx" : FAILPROOFAI_BIN;
|
|
896
909
|
const args = USE_NPX
|
|
897
910
|
? ["-y", "failproofai", "--hook", eventName, "--cli", "opencode"]
|
|
898
911
|
: ["--hook", eventName, "--cli", "opencode"];
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
912
|
+
return await new Promise((resolveRun) => {
|
|
913
|
+
let child;
|
|
914
|
+
try {
|
|
915
|
+
child = spawn(cmd, args, { cwd: directory });
|
|
916
|
+
} catch {
|
|
917
|
+
resolveRun({ exitCode: 0, stdout: "", stderr: "" });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
let stdout = "";
|
|
921
|
+
let stderr = "";
|
|
922
|
+
let settled = false;
|
|
923
|
+
const finish = (exitCode) => {
|
|
924
|
+
if (settled) return;
|
|
925
|
+
settled = true;
|
|
926
|
+
clearTimeout(timer);
|
|
927
|
+
resolveRun({ exitCode, stdout, stderr });
|
|
928
|
+
};
|
|
929
|
+
const timer = setTimeout(() => {
|
|
930
|
+
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
931
|
+
finish(0);
|
|
932
|
+
}, 60_000);
|
|
933
|
+
child.stdout.on("data", (d) => { stdout += d; });
|
|
934
|
+
child.stderr.on("data", (d) => { stderr += d; });
|
|
935
|
+
child.on("error", () => finish(0));
|
|
936
|
+
child.on("close", (code) => finish(code ?? 0));
|
|
937
|
+
child.stdin.on("error", () => {});
|
|
938
|
+
child.stdin.end(JSON.stringify(payload));
|
|
904
939
|
});
|
|
905
|
-
return { exitCode: r.status ?? 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
906
940
|
}
|
|
907
941
|
|
|
908
942
|
async function applyDecision(result, ctx, eventName) {
|
|
@@ -967,7 +1001,7 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
967
1001
|
}
|
|
968
1002
|
}
|
|
969
1003
|
if (!prompt) prompt = (info.text || info.content || props.text || "").toString();
|
|
970
|
-
const r = runFailproofai("UserPromptSubmit", {
|
|
1004
|
+
const r = await runFailproofai("UserPromptSubmit", {
|
|
971
1005
|
session_id: sessionID, cwd: directory, hook_event_name: "UserPromptSubmit", prompt,
|
|
972
1006
|
}, directory);
|
|
973
1007
|
await applyDecision(r, { client, sessionID }, "UserPromptSubmit");
|
|
@@ -978,7 +1012,7 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
978
1012
|
if (!claudeEvent) return;
|
|
979
1013
|
const props = event.properties || {};
|
|
980
1014
|
const sessionID = props.sessionID || (props.session && props.session.id) || props.id;
|
|
981
|
-
const r = runFailproofai(claudeEvent, {
|
|
1015
|
+
const r = await runFailproofai(claudeEvent, {
|
|
982
1016
|
session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
|
|
983
1017
|
}, directory);
|
|
984
1018
|
await applyDecision(r, { client, sessionID }, claudeEvent);
|
|
@@ -987,7 +1021,7 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
987
1021
|
// First-class PreToolUse hook. Note: tool args live on output.args (mutable).
|
|
988
1022
|
"tool.execute.before": async (input, output) => {
|
|
989
1023
|
const canonicalTool = canonicalizeTool(input.tool);
|
|
990
|
-
const r = runFailproofai("PreToolUse", {
|
|
1024
|
+
const r = await runFailproofai("PreToolUse", {
|
|
991
1025
|
session_id: input.sessionID,
|
|
992
1026
|
cwd: directory,
|
|
993
1027
|
tool_name: canonicalTool,
|
|
@@ -1000,7 +1034,7 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
1000
1034
|
// First-class PostToolUse hook. Note: tool args live on input.args here.
|
|
1001
1035
|
"tool.execute.after": async (input, output) => {
|
|
1002
1036
|
const canonicalTool = canonicalizeTool(input.tool);
|
|
1003
|
-
const r = runFailproofai("PostToolUse", {
|
|
1037
|
+
const r = await runFailproofai("PostToolUse", {
|
|
1004
1038
|
session_id: input.sessionID,
|
|
1005
1039
|
cwd: directory,
|
|
1006
1040
|
tool_name: canonicalTool,
|
|
@@ -1014,7 +1048,7 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
1014
1048
|
// Cleaner deny UX for prompted tools — mutate output.status instead of throwing.
|
|
1015
1049
|
"permission.ask": async (input, output) => {
|
|
1016
1050
|
const canonicalTool = canonicalizeTool(input.tool);
|
|
1017
|
-
const r = runFailproofai("PermissionRequest", {
|
|
1051
|
+
const r = await runFailproofai("PermissionRequest", {
|
|
1018
1052
|
session_id: input.sessionID,
|
|
1019
1053
|
cwd: directory,
|
|
1020
1054
|
tool_name: canonicalTool || input.command || "permission",
|
|
@@ -0,0 +1,155 @@
|
|
|
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, pi and openclaw have real channels too, but they live inside the
|
|
32
|
+
* plugin shims we ship rather than in a hook's stdout, so they are wired there.
|
|
33
|
+
* cursor, devin, goose, antigravity and hermes have no channel that reaches a
|
|
34
|
+
* human without an unsupported escape hatch — they get nothing here, and their
|
|
35
|
+
* users get the finding from `failproofai audit` and the emailed digest.
|
|
36
|
+
*/
|
|
37
|
+
import type { IntegrationType } from "./types";
|
|
38
|
+
|
|
39
|
+
/** How a notice is delivered on one CLI. */
|
|
40
|
+
type NoticeChannel = "systemMessage" | "stderr" | "stdout" | "none";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Per-CLI channel, from live probes rather than documentation.
|
|
44
|
+
*
|
|
45
|
+
* A CLI absent from this map delivers nothing — deliberately. Guessing a
|
|
46
|
+
* channel produces the worst outcome available: output that looks delivered
|
|
47
|
+
* from our side and reaches nobody, so we would report a leak as "notified"
|
|
48
|
+
* that the user never saw.
|
|
49
|
+
*/
|
|
50
|
+
const NOTICE_CHANNEL: Partial<Record<IntegrationType, NoticeChannel>> = {
|
|
51
|
+
claude: "systemMessage",
|
|
52
|
+
codex: "systemMessage",
|
|
53
|
+
copilot: "stderr",
|
|
54
|
+
factory: "stdout",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Claude truncates `systemMessage` at 4000 characters on the cloud-forwarding
|
|
59
|
+
* path, and a notice that gets cut mid-sentence reads as a broken tool. Ours is
|
|
60
|
+
* two lines by design; this is a backstop, not a budget.
|
|
61
|
+
*/
|
|
62
|
+
const MAX_NOTICE_CHARS = 900;
|
|
63
|
+
|
|
64
|
+
export interface ShapedNotice {
|
|
65
|
+
/** Merged into the outcome's stdout, or empty. */
|
|
66
|
+
stdout: string;
|
|
67
|
+
/** Merged into the outcome's stderr, or empty. */
|
|
68
|
+
stderr: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render a notice for one CLI, merging it into any verdict JSON already bound
|
|
73
|
+
* for stdout.
|
|
74
|
+
*
|
|
75
|
+
* `existingStdout` matters: a Stop-gate deny already emits a JSON object on
|
|
76
|
+
* several CLIs, and emitting a second object would produce two JSON documents
|
|
77
|
+
* on one stream — which every host parses as a syntax error and drops, taking
|
|
78
|
+
* the deny with it. So the notice is merged INTO that object when there is one.
|
|
79
|
+
*/
|
|
80
|
+
export function shapeNotice(
|
|
81
|
+
cli: IntegrationType | undefined,
|
|
82
|
+
notice: string,
|
|
83
|
+
existingStdout = "",
|
|
84
|
+
): ShapedNotice {
|
|
85
|
+
const empty: ShapedNotice = { stdout: existingStdout, stderr: "" };
|
|
86
|
+
if (!notice.trim() || !cli) return empty;
|
|
87
|
+
|
|
88
|
+
const channel = NOTICE_CHANNEL[cli] ?? "none";
|
|
89
|
+
const text = notice.length > MAX_NOTICE_CHARS
|
|
90
|
+
? notice.slice(0, MAX_NOTICE_CHARS - 1) + "…"
|
|
91
|
+
: notice;
|
|
92
|
+
|
|
93
|
+
switch (channel) {
|
|
94
|
+
case "systemMessage": {
|
|
95
|
+
// Merge into the verdict object when one exists, so the stream stays a
|
|
96
|
+
// single JSON document.
|
|
97
|
+
if (existingStdout.trim().startsWith("{")) {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(existingStdout) as Record<string, unknown>;
|
|
100
|
+
parsed.systemMessage = text;
|
|
101
|
+
return { stdout: JSON.stringify(parsed), stderr: "" };
|
|
102
|
+
} catch {
|
|
103
|
+
// Not the JSON we thought it was — leave the verdict untouched
|
|
104
|
+
// rather than risk destroying it to add a courtesy message.
|
|
105
|
+
return empty;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Anything already on stdout that we could not merge into is left
|
|
109
|
+
// exactly as it is, and the notice is dropped instead. The alternative —
|
|
110
|
+
// emitting our own object over the top — DESTROYS it: measured with a
|
|
111
|
+
// JSON array on stdout, which does not start with `{`, so it fell past
|
|
112
|
+
// the merge and was replaced wholesale. A courtesy message is never worth
|
|
113
|
+
// more than whatever the host was already being told.
|
|
114
|
+
if (existingStdout.trim()) return empty;
|
|
115
|
+
return { stdout: JSON.stringify({ systemMessage: text }), stderr: "" };
|
|
116
|
+
}
|
|
117
|
+
case "stderr":
|
|
118
|
+
// Exit code is NOT touched: on copilot a non-zero exit from a tool event
|
|
119
|
+
// is a block, and a notice must never block anything.
|
|
120
|
+
return { stdout: existingStdout, stderr: text + "\n" };
|
|
121
|
+
case "stdout":
|
|
122
|
+
// factory renders plain text under its Hooks block. Only safe when no
|
|
123
|
+
// verdict JSON is already on the stream.
|
|
124
|
+
if (existingStdout.trim()) return empty;
|
|
125
|
+
return { stdout: text + "\n", stderr: "" };
|
|
126
|
+
default:
|
|
127
|
+
return empty;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** True when this CLI has a channel that was proven to reach a human. */
|
|
132
|
+
export function canDeliverNotice(cli: IntegrationType | undefined): boolean {
|
|
133
|
+
return !!cli && (NOTICE_CHANNEL[cli] ?? "none") !== "none";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The notice itself. A FIXED template with no interpolation of scanned content.
|
|
138
|
+
*
|
|
139
|
+
* The count is a number we computed; everything else is a constant. That is not
|
|
140
|
+
* stylistic: the alternative is interpolating a finding's `example`, which is
|
|
141
|
+
* the verbatim text of a command from a repository — attacker-controlled
|
|
142
|
+
* through a README, a Makefile target or an npm script. Tested against the
|
|
143
|
+
* shipped redactor, `echo "IGNORE ALL PRIOR INSTRUCTIONS…"` passes through
|
|
144
|
+
* byte-identical, because that redactor masks secrets, not instructions. On the
|
|
145
|
+
* CLIs where the only channel is the model's context, that string would land
|
|
146
|
+
* directly in the prompt. So nothing scanned ever reaches this template.
|
|
147
|
+
*/
|
|
148
|
+
export function leakNoticeText(newFindings: number): string {
|
|
149
|
+
const n = newFindings === 1 ? "a credential" : `${newFindings} credentials`;
|
|
150
|
+
return (
|
|
151
|
+
`failproofai found ${n} in this project's agent transcripts.\n` +
|
|
152
|
+
"Run `failproofai audit` to see the details, " +
|
|
153
|
+
"or `failproofai audit --schedule` to get these by email."
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import { listInstallableIds, getIntegration } from "./integrations";
|
|
48
48
|
import type { IntegrationType } from "./types";
|
|
49
49
|
import { removeHooks } from "./manager";
|
|
50
|
+
import { uninstallMacNotifier } from "../audit/macos-notifier";
|
|
50
51
|
|
|
51
52
|
export interface UninstallOptions {
|
|
52
53
|
/** Also delete ~/.failproofai (config, credentials, state, audit cache, daemon binary). */
|
|
@@ -346,6 +347,20 @@ export async function runUninstallCommand(opts: UninstallOptions = {}): Promise<
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
// The macOS notification agent, which is the one thing `--purge` alone cannot
|
|
351
|
+
// reach: its bundle lives inside `~/.failproofai` and goes with the home, but
|
|
352
|
+
// its LaunchAgent plist sits in `~/Library/LaunchAgents`, outside everything
|
|
353
|
+
// this command otherwise deletes. Left behind, launchd keeps trying to start
|
|
354
|
+
// an applet that is no longer on disk and logs the failure at every login —
|
|
355
|
+
// precisely the class of leftover an uninstall exists to prevent.
|
|
356
|
+
//
|
|
357
|
+
// No-ops on every other platform, and needs no elevation on macOS: a per-user
|
|
358
|
+
// agent is the user's own to load and unload. Tied to the daemon's removal
|
|
359
|
+
// because it exists only to deliver what the daemon's scheduled audit finds.
|
|
360
|
+
if (opts.purge || (found.serviceInstalled && (opts.purge || opts.yes))) {
|
|
361
|
+
uninstallMacNotifier();
|
|
362
|
+
}
|
|
363
|
+
|
|
349
364
|
if (opts.purge && found.homeExists) {
|
|
350
365
|
// Last, and only after the service is down: the daemon binary and its
|
|
351
366
|
// socket live here, and deleting them out from under a running unit is how
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
module.exports=[18622,(a,b,c)=>{b.exports=a.x("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js",()=>require("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js"))},20635,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/action-async-storage.external.js",()=>require("next/dist/server/app-render/action-async-storage.external.js"))},24725,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/after-task-async-storage.external.js",()=>require("next/dist/server/app-render/after-task-async-storage.external.js"))},43285,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/dynamic-access-async-storage.external.js",()=>require("next/dist/server/app-render/dynamic-access-async-storage.external.js"))},56704,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-async-storage.external.js",()=>require("next/dist/server/app-render/work-async-storage.external.js"))},32319,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-unit-async-storage.external.js",()=>require("next/dist/server/app-render/work-unit-async-storage.external.js"))},59043,(a,b,c)=>{b.exports=a.x("next/dist/server/runtime-reacts.external.js",()=>require("next/dist/server/runtime-reacts.external.js"))},31677,a=>{"use strict";var b=a.i(5050);let c=(0,b.createServerReference)("0021127018a217bb03f53322147dee0b96bd0077f2",b.callServer,void 0,b.findSourceMapURL,"getTelemetryConfig");a.s(["getTelemetryConfig",0,c])},3171,a=>{"use strict";var b=a.i(87924),c=a.i(72131),d=a.i(31677),e=a.i(66036);a.s(["default",0,function({error:a,reset:f}){return(0,c.useEffect)(()=>{(0,d.getTelemetryConfig)().then(b=>{(0,e.setClientTelemetryConfig)(b),(0,e.captureClientEvent)("client_error",{error_message:a.message,error_name:a.name,error_digest:a.digest,boundary:"global"})}).catch(()=>{})},[a]),(0,b.jsx)("html",{children:(0,b.jsx)("body",{children:(0,b.jsx)("main",{style:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:"#031035",color:"#f8fafc",fontFamily:"system-ui, sans-serif"},children:(0,b.jsxs)("div",{style:{textAlign:"center",padding:"2rem",border:"1px solid rgba(239,68,68,0.4)",borderRadius:"0.5rem",maxWidth:"500px"},children:[(0,b.jsx)("h2",{style:{color:"#ef4444",marginBottom:"0.5rem",fontSize:"1.25rem"},children:"Something went wrong"}),(0,b.jsx)("p",{style:{color:"#94a3b8",marginBottom:"1.5rem"},children:a.message||"An unexpected error occurred."}),(0,b.jsx)("button",{onClick:f,style:{padding:"0.5rem 1.25rem",background:"#3b82f6",color:"white",border:"none",borderRadius:"0.375rem",cursor:"pointer",fontSize:"0.875rem"},children:"Try again"})]})})})})}])},66036,a=>{"use strict";a.s(["captureClientEvent",0,function(a,b){},"setClientTelemetryConfig",0,function(a){}])},46058,(a,b,c)=>{"use strict";function d(a){if("function"!=typeof WeakMap)return null;var b=new WeakMap,c=new WeakMap;return(d=function(a){return a?c:b})(a)}c._=function(a,b){if(!b&&a&&a.__esModule)return a;if(null===a||"object"!=typeof a&&"function"!=typeof a)return{default:a};var c=d(b);if(c&&c.has(a))return c.get(a);var e={__proto__:null},f=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var g in a)if("default"!==g&&Object.prototype.hasOwnProperty.call(a,g)){var h=f?Object.getOwnPropertyDescriptor(a,g):null;h&&(h.get||h.set)?Object.defineProperty(e,g,h):e[g]=a[g]}return e.default=a,c&&c.set(a,e),e}},5050,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={callServer:function(){return f.callServer},createServerReference:function(){return h.createServerReference},findSourceMapURL:function(){return g.findSourceMapURL}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f=a.r(20611),g=a.r(1722),h=a.r(38783)},20611,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"callServer",{enumerable:!0,get:function(){return g}});let d=a.r(72131),e=a.r(88347),f=a.r(90841);async function g(a,b){return new Promise((c,g)=>{(0,d.startTransition)(()=>{(0,f.dispatchAppRouterAction)({type:e.ACTION_SERVER_ACTION,actionId:a,actionArgs:b,resolve:c,reject:g})})})}("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},1722,(a,b,c)=>{"use strict";let d;Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"findSourceMapURL",{enumerable:!0,get:function(){return d}});("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},88347,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d,e,f={ACTION_HMR_REFRESH:function(){return l},ACTION_NAVIGATE:function(){return i},ACTION_REFRESH:function(){return h},ACTION_RESTORE:function(){return j},ACTION_SERVER_ACTION:function(){return m},ACTION_SERVER_PATCH:function(){return k},PrefetchKind:function(){return n},ScrollBehavior:function(){return o}};for(var g in f)Object.defineProperty(c,g,{enumerable:!0,get:f[g]});let h="refresh",i="navigate",j="restore",k="server-patch",l="hmr-refresh",m="server-action";var n=((d={}).AUTO="auto",d.FULL="full",d),o=((e={})[e.Default=0]="Default",e[e.NoScroll=1]="NoScroll",e);("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},90841,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={dispatchAppRouterAction:function(){return i},dispatchGestureState:function(){return j},refreshOnInstantNavigationUnlock:function(){return h},useActionQueue:function(){return k}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f=a.r(46058)._(a.r(72131)),g=a.r(67009);a.r(88347);function h(){}function i(a){!0;throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0})}function j(a){!0;throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0})}function k(a){let[b,c]=f.default.useState(a.state),[d,e]=(0,f.useOptimistic)(b),h=(0,f.useMemo)(()=>d,[d]);return(0,g.isThenable)(h)?(0,f.use)(h):h}("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},42602,(a,b,c)=>{"use strict";b.exports=a.r(18622)},9270,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.AppRouterContext},36313,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.HooksClientContext},18341,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.ServerInsertedHtml},35112,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactDOM},87924,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactJsxRuntime},38783,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactServerDOMTurbopackClient},72131,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].React},67009,(a,b,c)=>{"use strict";function d(a){return null!==a&&"object"==typeof a&&"then"in a&&"function"==typeof a.then}Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"isThenable",{enumerable:!0,get:function(){return d}})}];
|
|
2
|
-
|
|
3
|
-
//# sourceMappingURL=%5Broot-of-the-server%5D__1b5jzxr._.js.map
|