failproofai 1.0.4-beta.1 → 1.0.4-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +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 +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 +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +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 +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]__1zndo1i._.js → [externals]__0v1hz15._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0_gzk8q._.js → [externals]__1nb206a._.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]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.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]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0otft92._.js +3 -0
- 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 +14 -21
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +1 -1
- 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_0iu54mz._.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]__00-s7h8._.js +3 -0
- 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]__0wvusu0._.js → [root-of-the-server]__0n_lxhg._.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]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__16gamwq._.js → [root-of-the-server]__0yl9wb5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1byck5v._.js → _0guxnh-._.js} +2 -2
- 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/_0wso5d3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +61 -168
- package/.next/standalone/.next/server/chunks/ssr/{_08f0jd2._.js → _1if9ixi._.js} +1 -1
- 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/04o5zfl7p5lhx.js +1 -0
- package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0jpd8dv2vk930.css} +1 -1
- package/.next/standalone/.next/static/chunks/{1po3lirgk2riw.js → 0po8n4tyotjf2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +2 -0
- package/.next/standalone/.next/static/chunks/0qx7gav1c2_vu.js +1 -0
- package/.next/standalone/.next/static/chunks/17tfki9x5rd_7.js +1 -0
- package/.next/standalone/.next/static/chunks/{1_bxotmf9qngs.js → 1i-i0pxg9q_8y.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1_2jsatfuthrw.js → 2bxr2_2h4kaqq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{01hvwwrhvbzyh.js → 2zafgs90r_leq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{00gv0v91mycm-.js → 323cyb3a2s06y.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0qn9khv0oo9ex.js → 364cqbpeg1y5i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bvwsyu6x__ia.js → 38dqur43qthrl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3_--3ibia6ba1.js → 3l36d73kj18zw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
- package/.next/standalone/.next/static/chunks/{2bnf_4pgyk1rq.js → 40lpt2o2b283e.js} +1 -1
- package/.next/standalone/.next/static/chunks/42gwu2bsz4ske.js +6 -0
- package/.next/standalone/.next/static/chunks/{turbopack-18qf4jodtft6b.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 +109 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +140 -34
- 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 +192 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
- package/.next/standalone/app/audit/audit-styles.css +132 -1
- package/.next/standalone/app/project/[name]/page.tsx +4 -24
- 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/lib/cli-registry.ts +1 -33
- package/.next/standalone/lib/download-session.ts +0 -37
- package/.next/standalone/lib/projects.ts +3 -27
- package/.next/standalone/lib/sqlite-reader.ts +78 -1
- 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/pages-turbo.runtime.prod.js +1 -1
- 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/image-optimizer.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.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 +15 -14
- 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 +18 -18
- package/.next/standalone/server.js +1 -1
- package/README.md +19 -95
- package/bin/failproofai.mjs +8 -25
- package/dist/cli.mjs +20091 -25864
- package/dist/worker.mjs +4881 -6361
- package/lib/auth/api-server-client.ts +28 -0
- package/lib/claude-sessions.ts +101 -27
- package/lib/cli-registry.ts +1 -33
- package/lib/download-session.ts +0 -37
- package/lib/projects.ts +3 -27
- package/lib/sqlite-reader.ts +78 -1
- package/package.json +15 -14
- package/pi-extension/index.ts +61 -8
- package/scripts/changelog-open.py +115 -0
- package/src/audit/cache.ts +6 -0
- package/src/audit/cli-adapters/claude.ts +13 -0
- package/src/audit/cli-adapters/index.ts +0 -24
- package/src/audit/cli-login.ts +4 -1
- package/src/audit/cli.ts +164 -5
- package/src/audit/desktop-notify.ts +420 -0
- package/src/audit/harm-report.ts +101 -0
- package/src/audit/index.ts +284 -15
- package/src/audit/leak-fingerprint.ts +200 -0
- package/src/audit/leak-notice.ts +259 -0
- package/src/audit/leak-record.ts +232 -0
- package/src/audit/leak-scan.ts +268 -0
- package/src/audit/leak-store.ts +265 -0
- package/src/audit/macos-notifier.ts +310 -0
- package/src/audit/redact-example.ts +322 -15
- 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 +47 -0
- package/src/hooks/builtin-policies.ts +71 -8
- package/src/hooks/configure-wizard.ts +20 -0
- package/src/hooks/enforcement-capability.ts +0 -97
- package/src/hooks/fp-config.ts +71 -11
- package/src/hooks/fp-home.ts +27 -0
- package/src/hooks/handler.ts +84 -62
- package/src/hooks/harness-cli.ts +1 -3
- package/src/hooks/integrations.ts +63 -769
- package/src/hooks/normalize-cli-payload.ts +0 -126
- package/src/hooks/notice.ts +191 -0
- package/src/hooks/policy-evaluator.ts +6 -297
- package/src/hooks/resolve-transcript-path.ts +0 -10
- package/src/hooks/tool-name-canonicalize.ts +0 -64
- package/src/hooks/types.ts +2 -533
- package/src/hooks/uninstall-cli.ts +15 -0
- package/.next/standalone/.grok/hooks/failproofai.json +0 -172
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +0 -5
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_00z5uuq._.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/lib_cline-projects_ts_13_hbdd._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.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/0hzmxwb2pvk5p.js +0 -6
- package/.next/standalone/.next/static/chunks/0j884dwnwtyrt.js +0 -1
- package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +0 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
- package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +0 -2
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
- package/.next/standalone/.next/static/chunks/3ws7eljkq25wg.js +0 -69
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
- package/.next/standalone/.qwen/settings.json +0 -251
- package/.next/standalone/assets/logos/cline.png +0 -0
- package/.next/standalone/assets/logos/grok-dark.svg +0 -1
- package/.next/standalone/assets/logos/grok-light.svg +0 -1
- package/.next/standalone/assets/logos/ori-dark.svg +0 -1
- package/.next/standalone/assets/logos/ori-light.svg +0 -1
- package/.next/standalone/assets/logos/qwen.svg +0 -1
- package/.next/standalone/lib/cline-projects.ts +0 -155
- package/.next/standalone/lib/cline-sessions.ts +0 -268
- package/.next/standalone/lib/grok-projects.ts +0 -123
- package/.next/standalone/lib/grok-sessions.ts +0 -355
- package/.next/standalone/lib/ori-projects.ts +0 -141
- package/.next/standalone/lib/ori-sessions.ts +0 -221
- package/.next/standalone/lib/qwen-projects.ts +0 -106
- package/.next/standalone/lib/qwen-sessions.ts +0 -313
- package/lib/cline-projects.ts +0 -155
- package/lib/cline-sessions.ts +0 -268
- package/lib/grok-projects.ts +0 -123
- package/lib/grok-sessions.ts +0 -355
- package/lib/ori-projects.ts +0 -141
- package/lib/ori-sessions.ts +0 -221
- package/lib/qwen-projects.ts +0 -106
- package/lib/qwen-sessions.ts +0 -313
- package/src/audit/cli-adapters/cline.ts +0 -50
- package/src/audit/cli-adapters/grok.ts +0 -70
- package/src/audit/cli-adapters/ori.ts +0 -55
- package/src/audit/cli-adapters/qwen.ts +0 -71
- package/src/hooks/batch-expand.ts +0 -380
- package/src/hooks/batch-fanout.ts +0 -142
- package/src/hooks/risk-patterns.ts +0 -26
- /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → P6icI2V2PvVuePJrqjHCr}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → P6icI2V2PvVuePJrqjHCr}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{qJx3mbOQV-5ElrcRb4Sp7 → P6icI2V2PvVuePJrqjHCr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding whether a finding still owes the user a notice — exactly once.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is not a field on the record
|
|
5
|
+
*
|
|
6
|
+
* The obvious design is `notifiedAt` on each finding, written back after the
|
|
7
|
+
* notice goes out. Measured against this repo's real `writeJsonAtomically`
|
|
8
|
+
* semantics, that fails 100% of the time under the conditions it actually runs
|
|
9
|
+
* in: two concurrent sessions with the same finding produced two notices, four
|
|
10
|
+
* produced four, eight produced eight. Worse, two concurrent sessions with
|
|
11
|
+
* DIFFERENT findings produced two notices and a surviving watermark containing
|
|
12
|
+
* only one of them — the loser's mark permanently gone, so it re-notified
|
|
13
|
+
* forever afterwards. `lib/atomic-write.ts` says so in its own header: atomic
|
|
14
|
+
* per FILE, last-writer-wins per CONTENT, i.e. a lost update on a map.
|
|
15
|
+
*
|
|
16
|
+
* Running several agents at once in one project is not an edge case here; it is
|
|
17
|
+
* how the tool is used.
|
|
18
|
+
*
|
|
19
|
+
* So the claim is one marker FILE per finding, created with `wx` — O_EXCL,
|
|
20
|
+
* which the kernel makes atomic across processes. Eight concurrent claimants
|
|
21
|
+
* produce exactly one winner, verified 3/3 trials. Concurrent claims on
|
|
22
|
+
* different findings all succeed and all survive, because they touch different
|
|
23
|
+
* files.
|
|
24
|
+
*
|
|
25
|
+
* ## Claim ordering, and which way to fail
|
|
26
|
+
*
|
|
27
|
+
* The marker is written BEFORE the notice reaches the stream, and the caller
|
|
28
|
+
* only keeps it if the notice was actually shaped. The alternative orderings
|
|
29
|
+
* were both measured: emit-then-mark nags forever if the process dies between
|
|
30
|
+
* them (5 sessions → 5 notices), and mark-then-never-emit swallows the alert
|
|
31
|
+
* permanently with nothing on disk saying so (total delivered: 0).
|
|
32
|
+
*
|
|
33
|
+
* Between "told twice" and "never told", a security tool picks told twice.
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, rmSync } from "node:fs";
|
|
36
|
+
import { resolve } from "node:path";
|
|
37
|
+
|
|
38
|
+
import { auditDir } from "../hooks/fp-home";
|
|
39
|
+
import { readLeakRecord, activeFindings, readLeakIdentity } from "./leak-store";
|
|
40
|
+
import { isFindingId } from "./leak-fingerprint";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which way the user was told. Each channel claims independently.
|
|
44
|
+
*
|
|
45
|
+
* They are NOT one guarantee. The desktop banner and the in-CLI notice reach a
|
|
46
|
+
* person at different moments and fail in unrelated ways — and the banner has a
|
|
47
|
+
* failure it cannot detect at all: on a locked screen the shell accepts the
|
|
48
|
+
* call and returns an id while nobody sees anything. Letting a successful
|
|
49
|
+
* `Notify` claim the finding would then silence the in-session notice too, and
|
|
50
|
+
* the only channel that actually reached a human is the one we suppressed.
|
|
51
|
+
*
|
|
52
|
+
* So each channel gets its own marker directory and its own once-per-finding
|
|
53
|
+
* guarantee. A user at a desktop sees the banner AND the next session's notice;
|
|
54
|
+
* that is a bounded, deliberate duplicate, and it is the right side of the
|
|
55
|
+
* trade this file already made once (told twice beats never told).
|
|
56
|
+
*/
|
|
57
|
+
export type NoticeChannel = "cli" | "desktop";
|
|
58
|
+
|
|
59
|
+
/** One empty file per claimed finding, per channel. Cheap: a few hundred
|
|
60
|
+
* zero-byte files at the very worst, and the record itself is capped at 500
|
|
61
|
+
* findings. "cli" keeps the original directory name so an existing machine's
|
|
62
|
+
* claims survive the upgrade — renaming it would re-notify for every finding
|
|
63
|
+
* already delivered. */
|
|
64
|
+
const NOTICE_DIR = (home?: string, channel: NoticeChannel = "cli") =>
|
|
65
|
+
resolve(auditDir(home), channel === "cli" ? "notified" : `notified-${channel}`);
|
|
66
|
+
|
|
67
|
+
/** Markers older than this are swept, so the directory cannot grow forever on a
|
|
68
|
+
* machine that has been running for years. Comfortably longer than the
|
|
69
|
+
* record's own 90-day TTL, so a marker never outlives its finding. */
|
|
70
|
+
const MARKER_TTL_MS = 120 * 86_400_000;
|
|
71
|
+
|
|
72
|
+
export interface PendingNotice {
|
|
73
|
+
/** Findings that have never been notified about and are not dismissed. */
|
|
74
|
+
ids: string[];
|
|
75
|
+
count: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Findings that still owe the user a notice.
|
|
80
|
+
*
|
|
81
|
+
* Reads the record and subtracts what has already been claimed. Never throws:
|
|
82
|
+
* on any failure it returns "nothing pending", because a hook that cannot read
|
|
83
|
+
* its bookkeeping must stay silent rather than guess.
|
|
84
|
+
*/
|
|
85
|
+
export function pendingLeakNotice(home?: string, channel: NoticeChannel = "cli"): PendingNotice {
|
|
86
|
+
try {
|
|
87
|
+
const dir = NOTICE_DIR(home, channel);
|
|
88
|
+
const claimed = new Set(existsSync(dir) ? readdirSync(dir) : []);
|
|
89
|
+
const ids = activeFindings(readLeakRecord(home), home)
|
|
90
|
+
.map((f) => f.id)
|
|
91
|
+
.filter((id) => !claimed.has(id));
|
|
92
|
+
return { ids, count: ids.length };
|
|
93
|
+
} catch {
|
|
94
|
+
return { ids: [], count: 0 };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Claim these findings, atomically and per-finding.
|
|
100
|
+
*
|
|
101
|
+
* Returns the ids this process actually won. A caller that loses a race simply
|
|
102
|
+
* gets a shorter list — it never blocks, and it never overwrites a claim made
|
|
103
|
+
* by a concurrent session.
|
|
104
|
+
*/
|
|
105
|
+
export function markLeakNoticeDelivered(
|
|
106
|
+
ids: string[],
|
|
107
|
+
home?: string,
|
|
108
|
+
channel: NoticeChannel = "cli",
|
|
109
|
+
): string[] {
|
|
110
|
+
const won: string[] = [];
|
|
111
|
+
try {
|
|
112
|
+
const dir = NOTICE_DIR(home, channel);
|
|
113
|
+
mkdirSync(dir, { recursive: true });
|
|
114
|
+
for (const id of ids) {
|
|
115
|
+
// An id is about to become a filename. Anything that is not the exact
|
|
116
|
+
// shape `fingerprintId` mints is refused rather than resolved — see
|
|
117
|
+
// `isFindingId`. Refusing also means NOT reporting it as won, so a caller
|
|
118
|
+
// treats it as unclaimed and never records it as delivered.
|
|
119
|
+
if (!isFindingId(id)) continue;
|
|
120
|
+
try {
|
|
121
|
+
// `wx` is O_EXCL: it throws EEXIST if another process got here first.
|
|
122
|
+
// That throw IS the lock — there is no read-then-write window for two
|
|
123
|
+
// processes to both pass through.
|
|
124
|
+
writeFileSync(resolve(dir, id), "", { flag: "wx", mode: 0o600 });
|
|
125
|
+
won.push(id);
|
|
126
|
+
} catch {
|
|
127
|
+
// Already claimed by a concurrent session. Correct outcome: the user
|
|
128
|
+
// is told once, by whichever session got there first.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Cannot claim at all — the caller has already decided to emit, and being
|
|
133
|
+
// told twice beats never being told.
|
|
134
|
+
}
|
|
135
|
+
return won;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Release claims this process won when delivery failed, so the next scheduled
|
|
139
|
+
* run retries instead of turning a transient missing desktop into permanent
|
|
140
|
+
* silence. Only validated ids in the requested channel are touched. */
|
|
141
|
+
export function releaseLeakNoticeClaims(
|
|
142
|
+
ids: string[],
|
|
143
|
+
home?: string,
|
|
144
|
+
channel: NoticeChannel = "cli",
|
|
145
|
+
): void {
|
|
146
|
+
try {
|
|
147
|
+
const dir = NOTICE_DIR(home, channel);
|
|
148
|
+
for (const id of ids) {
|
|
149
|
+
if (!isFindingId(id)) continue;
|
|
150
|
+
rmSync(resolve(dir, id), { force: true });
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Retry bookkeeping must never turn a completed audit into a failure.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Drop markers for findings that no longer exist, and very old ones. */
|
|
158
|
+
export function pruneNoticeMarkers(home?: string, nowMs = Date.now()): void {
|
|
159
|
+
try {
|
|
160
|
+
const live = new Set(readLeakRecord(home).findings.map((f) => f.id));
|
|
161
|
+
for (const channel of ["cli", "desktop"] as const) {
|
|
162
|
+
const dir = NOTICE_DIR(home, channel);
|
|
163
|
+
if (!existsSync(dir)) continue;
|
|
164
|
+
for (const name of readdirSync(dir)) {
|
|
165
|
+
const path = resolve(dir, name);
|
|
166
|
+
let stale = !live.has(name);
|
|
167
|
+
if (!stale) {
|
|
168
|
+
try {
|
|
169
|
+
stale = nowMs - statSync(path).mtimeMs > MARKER_TTL_MS;
|
|
170
|
+
} catch {
|
|
171
|
+
stale = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (stale) rmSync(path, { force: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Housekeeping only.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Should this session's agent tell the user about unviewed credentials?
|
|
184
|
+
*
|
|
185
|
+
* ## Why this is not the per-finding claim used for the desktop banner
|
|
186
|
+
*
|
|
187
|
+
* The per-finding marker answers "have we emitted this once?" — and emitting is
|
|
188
|
+
* not the same as arriving. Twice in one day it recorded every finding as
|
|
189
|
+
* delivered while the user saw nothing: once because the notice was attached to
|
|
190
|
+
* an event whose channel the host ignores (`SessionStart` takes
|
|
191
|
+
* `additionalContext`, not `systemMessage`), and once because hooks were
|
|
192
|
+
* disabled in the project under test. Neither is detectable from in here, and
|
|
193
|
+
* both were permanent — a claimed finding is never retried.
|
|
194
|
+
*
|
|
195
|
+
* So the in-CLI notice keys on the USER's action instead of on ours. It shows
|
|
196
|
+
* while findings exist that are newer than the last time the report was
|
|
197
|
+
* actually opened, and it stops when they open it. A dropped notice costs one
|
|
198
|
+
* session's silence rather than the alert; the only thing that silences it for
|
|
199
|
+
* good is the outcome we wanted anyway.
|
|
200
|
+
*
|
|
201
|
+
* Bounded by a per-SESSION marker so a long session gets one line, not one per
|
|
202
|
+
* turn. Sessions are cheap and few; findings are many and long-lived.
|
|
203
|
+
*/
|
|
204
|
+
export function sessionNoticePending(sessionId: string, home?: string): PendingNotice {
|
|
205
|
+
try {
|
|
206
|
+
if (!sessionId || !/^[\w.:-]{1,200}$/.test(sessionId)) return { ids: [], count: 0 };
|
|
207
|
+
const dir = resolve(auditDir(home), "notified-sessions");
|
|
208
|
+
if (existsSync(resolve(dir, sessionId))) return { ids: [], count: 0 };
|
|
209
|
+
|
|
210
|
+
const findings = activeFindings(readLeakRecord(home), home);
|
|
211
|
+
if (findings.length === 0) return { ids: [], count: 0 };
|
|
212
|
+
|
|
213
|
+
// Only what the user has not already looked at. `firstSeen`, not
|
|
214
|
+
// `lastSeen`: re-encountering a credential the user has already reviewed is
|
|
215
|
+
// not news, however recently the scan tripped over it again.
|
|
216
|
+
const viewedAt = readLeakIdentity(home).reportViewedAt ?? 0;
|
|
217
|
+
const unviewed = findings.filter((f) => {
|
|
218
|
+
const seen = Date.parse(f.firstSeen);
|
|
219
|
+
return !Number.isFinite(seen) || seen > viewedAt;
|
|
220
|
+
});
|
|
221
|
+
return { ids: unviewed.map((f) => f.id), count: unviewed.length };
|
|
222
|
+
} catch {
|
|
223
|
+
return { ids: [], count: 0 };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Claim this session, so the notice appears once rather than every turn.
|
|
229
|
+
*
|
|
230
|
+
* Returns whether this caller won. O_EXCL again, for the same reason as the
|
|
231
|
+
* per-finding markers: several agents run in one project at once.
|
|
232
|
+
*/
|
|
233
|
+
export function markSessionNoticed(sessionId: string, home?: string): boolean {
|
|
234
|
+
try {
|
|
235
|
+
if (!sessionId || !/^[\w.:-]{1,200}$/.test(sessionId)) return false;
|
|
236
|
+
const dir = resolve(auditDir(home), "notified-sessions");
|
|
237
|
+
mkdirSync(dir, { recursive: true });
|
|
238
|
+
writeFileSync(resolve(dir, sessionId), "", { flag: "wx", mode: 0o600 });
|
|
239
|
+
return true;
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Session markers outlive nothing useful — a session id never recurs. */
|
|
246
|
+
export function pruneSessionMarkers(home?: string, nowMs = Date.now()): void {
|
|
247
|
+
try {
|
|
248
|
+
const dir = resolve(auditDir(home), "notified-sessions");
|
|
249
|
+
if (!existsSync(dir)) return;
|
|
250
|
+
for (const name of readdirSync(dir)) {
|
|
251
|
+
const path = resolve(dir, name);
|
|
252
|
+
try {
|
|
253
|
+
if (nowMs - statSync(path).mtimeMs > 30 * 86_400_000) rmSync(path, { force: true });
|
|
254
|
+
} catch { /* raced */ }
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// Housekeeping only.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-credential record — what the audit knows that `AuditCount` cannot say.
|
|
3
|
+
*
|
|
4
|
+
* `AuditCount` aggregates BY RULE: it can report that `sanitize-api-keys` fired
|
|
5
|
+
* 47 times and show three 80-character examples. It cannot enumerate 47 distinct
|
|
6
|
+
* keys, say which of them is the same key seen twice, or answer "which CLI, in
|
|
7
|
+
* which project, at what time, and how did it get there". Every surface the
|
|
8
|
+
* narrowed product needs — the emailed digest, the dashboard's leak view, and
|
|
9
|
+
* the once-per-finding notification guard — asks exactly those questions, so
|
|
10
|
+
* none of them can be built on the rule-level record.
|
|
11
|
+
*
|
|
12
|
+
* ## Grouped by VALUE, not by occurrence — this is the whole bloat story
|
|
13
|
+
*
|
|
14
|
+
* The obvious shape is one row per sighting. Measured against the real corpus
|
|
15
|
+
* that is 10,301 rows, against roughly 266 distinct values, against about 25
|
|
16
|
+
* genuinely real credentials. Two orders of magnitude of duplication, because
|
|
17
|
+
* one pasted key fans out across every session that touched it — the single
|
|
18
|
+
* confirmed-live password in the corpus appeared in 11 files and 22 occurrences,
|
|
19
|
+
* partly because Claude's own checkpoint records replay the prompt that carried
|
|
20
|
+
* it.
|
|
21
|
+
*
|
|
22
|
+
* So the unit is the DISTINCT VALUE, with a bounded list of sightings attached
|
|
23
|
+
* as evidence. A finding seen a thousand times costs the same on disk as one
|
|
24
|
+
* seen twice: `MAX_SIGHTINGS` entries, plus a count. At the corpus's own numbers
|
|
25
|
+
* that is roughly 130 KB for the whole file, which is why this is one file and
|
|
26
|
+
* not a directory — a per-finding file would spend more on inodes and directory
|
|
27
|
+
* entries than on the findings.
|
|
28
|
+
*
|
|
29
|
+
* ## What is deliberately NOT stored
|
|
30
|
+
*
|
|
31
|
+
* The credential itself, ever. The record holds a salted id (for dedupe across
|
|
32
|
+
* scans) and a fingerprint (for a human to recognise the key in a console) and
|
|
33
|
+
* nothing that could reconstruct the value. `~/.failproofai/` is a directory the
|
|
34
|
+
* user did not ask for and cannot audit, so a leak scanner that caches the leaks
|
|
35
|
+
* has made the problem worse rather than better.
|
|
36
|
+
*/
|
|
37
|
+
import type { IntegrationType } from "../hooks/types";
|
|
38
|
+
import type { LeakFingerprint } from "./leak-fingerprint";
|
|
39
|
+
|
|
40
|
+
/** Sightings kept per distinct value. Enough to answer 5W1H for the first and
|
|
41
|
+
* most recent exposure without storing the long tail nobody reads. */
|
|
42
|
+
export const MAX_SIGHTINGS = 5;
|
|
43
|
+
|
|
44
|
+
/** Findings not seen in this many days are dropped on the next write. A key
|
|
45
|
+
* whose transcripts have aged out of the scan window is not evidence of
|
|
46
|
+
* anything current, and keeping it forever grows the file without bound. */
|
|
47
|
+
export const FINDING_TTL_DAYS = 90;
|
|
48
|
+
|
|
49
|
+
/** Cap on distinct findings retained, newest-first. A machine with more than
|
|
50
|
+
* this is not going to triage them, and the census measured a name-only rule
|
|
51
|
+
* emitting thousands — the cap is what stops a bad matcher filling the disk. */
|
|
52
|
+
export const MAX_FINDINGS = 500;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* How a credential came to be where it was found — the "how" of 5W1H.
|
|
56
|
+
*
|
|
57
|
+
* Derived from the tool event rather than authored, so it is always specific:
|
|
58
|
+
* "read from ~/…/.env" beats "detected in a file operation". The path is
|
|
59
|
+
* home-shortened by the caller before it reaches here.
|
|
60
|
+
*/
|
|
61
|
+
export interface LeakMechanism {
|
|
62
|
+
/** Short past-tense phrase, e.g. "read from ~/…/.env". */
|
|
63
|
+
summary: string;
|
|
64
|
+
/** Canonical tool name (Bash, Read, Write, Edit…). */
|
|
65
|
+
toolName: string;
|
|
66
|
+
/** Where the value appeared: the tool's INPUT (the agent sent it) or its
|
|
67
|
+
* RESULT (the agent received it). Different exposures with different
|
|
68
|
+
* remedies — an input is deniable at PreToolUse, a result is not. */
|
|
69
|
+
direction: "input" | "result";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One exposure of one credential: the where/when/who of 5W1H. */
|
|
73
|
+
export interface LeakSighting {
|
|
74
|
+
/** WHO — the harness whose transcript carried it. */
|
|
75
|
+
cli: IntegrationType;
|
|
76
|
+
/** WHERE — session and project. `cwd` is already home-shortened. */
|
|
77
|
+
sessionId: string;
|
|
78
|
+
cwd: string;
|
|
79
|
+
/** WHEN — ISO-8601, from the transcript line. */
|
|
80
|
+
at: string;
|
|
81
|
+
/** HOW. */
|
|
82
|
+
mechanism: LeakMechanism;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One distinct credential, however many times it was seen. */
|
|
86
|
+
export interface LeakFinding {
|
|
87
|
+
/** Salted, machine-local, stable across scans. Never derived from the
|
|
88
|
+
* fingerprint, which is not unique. */
|
|
89
|
+
id: string;
|
|
90
|
+
/** WHAT — the masked rendering, its class, and its length. */
|
|
91
|
+
fingerprint: LeakFingerprint;
|
|
92
|
+
/** The identifier it was assigned to, when it came from an assignment
|
|
93
|
+
* (`COMPOSIO_API_KEY`). For the 230-of-237 first-party class this is the
|
|
94
|
+
* only actionable field there is, since no vendor console exists. */
|
|
95
|
+
name: string | null;
|
|
96
|
+
/** Which rule found it, for explaining and for suppression. */
|
|
97
|
+
rule: string;
|
|
98
|
+
/** How much we trust the attribution — mirrors the pattern census's tiers. */
|
|
99
|
+
confidence: "doc-verified" | "source-verified" | "partner-corroborated" | "observed-only" | "name-only";
|
|
100
|
+
firstSeen: string;
|
|
101
|
+
lastSeen: string;
|
|
102
|
+
/** Total exposures, which is NOT `sightings.length` — see MAX_SIGHTINGS. */
|
|
103
|
+
occurrences: number;
|
|
104
|
+
/** Bounded evidence, oldest first. */
|
|
105
|
+
sightings: LeakSighting[];
|
|
106
|
+
/** Set when the user dismisses it. Dismissed findings are retained (so they
|
|
107
|
+
* do not re-alert) but never notified about again. */
|
|
108
|
+
dismissedAt?: string;
|
|
109
|
+
/** Set when a notice for this finding has been delivered, so the next scan
|
|
110
|
+
* stays quiet. The delivery guard itself is a separate O_EXCL marker; this
|
|
111
|
+
* is the human-readable record of it. */
|
|
112
|
+
notifiedAt?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface LeakRecord {
|
|
116
|
+
schemaVersion: number;
|
|
117
|
+
/** Per-machine HMAC salt for `fingerprintId`. Generated once, never sent. */
|
|
118
|
+
salt: string;
|
|
119
|
+
updatedAt: string;
|
|
120
|
+
findings: LeakFinding[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const LEAK_RECORD_SCHEMA_VERSION = 1;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fold one new sighting into the record.
|
|
127
|
+
*
|
|
128
|
+
* Returns whether this is a **newly seen** credential, which is what the
|
|
129
|
+
* notification guard keys on: a finding whose id is already present must not
|
|
130
|
+
* alert again, however many new sightings it accumulates. Getting this backwards
|
|
131
|
+
* is the failure the census measured — keying on the rule-level examples, which
|
|
132
|
+
* `mergeIncremental` freezes at the first three, silently misses the arrival of
|
|
133
|
+
* a brand-new key while re-alerting for old ones.
|
|
134
|
+
*/
|
|
135
|
+
export function upsertFinding(
|
|
136
|
+
record: LeakRecord,
|
|
137
|
+
incoming: Omit<LeakFinding, "firstSeen" | "lastSeen" | "occurrences" | "sightings"> & {
|
|
138
|
+
sighting: LeakSighting;
|
|
139
|
+
},
|
|
140
|
+
): { record: LeakRecord; isNew: boolean } {
|
|
141
|
+
const { sighting, ...rest } = incoming;
|
|
142
|
+
const existing = record.findings.find((f) => f.id === rest.id);
|
|
143
|
+
|
|
144
|
+
if (!existing) {
|
|
145
|
+
record.findings.push({
|
|
146
|
+
...rest,
|
|
147
|
+
firstSeen: sighting.at,
|
|
148
|
+
lastSeen: sighting.at,
|
|
149
|
+
occurrences: 1,
|
|
150
|
+
sightings: [sighting],
|
|
151
|
+
});
|
|
152
|
+
return { record, isNew: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
existing.occurrences += 1;
|
|
156
|
+
if (sighting.at > existing.lastSeen) existing.lastSeen = sighting.at;
|
|
157
|
+
if (sighting.at < existing.firstSeen) existing.firstSeen = sighting.at;
|
|
158
|
+
|
|
159
|
+
// Keep the FIRST sightings rather than the most recent. The first exposure is
|
|
160
|
+
// the one that explains how the credential got into the transcript at all;
|
|
161
|
+
// later ones are usually the same value being replayed, and a checkpoint
|
|
162
|
+
// record replaying a prompt is not new information.
|
|
163
|
+
if (existing.sightings.length < MAX_SIGHTINGS) {
|
|
164
|
+
const seen = existing.sightings.some(
|
|
165
|
+
(s) => s.sessionId === sighting.sessionId && s.at === sighting.at,
|
|
166
|
+
);
|
|
167
|
+
if (!seen) existing.sightings.push(sighting);
|
|
168
|
+
}
|
|
169
|
+
return { record, isNew: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Drop what is no longer worth keeping, so the file cannot grow without bound.
|
|
174
|
+
*
|
|
175
|
+
* Order matters: age out first, then cap. Capping first would let a burst of
|
|
176
|
+
* stale findings evict fresh ones.
|
|
177
|
+
*/
|
|
178
|
+
export function pruneRecord(record: LeakRecord, nowMs: number): LeakRecord {
|
|
179
|
+
const cutoff = new Date(nowMs - FINDING_TTL_DAYS * 86_400_000).toISOString();
|
|
180
|
+
const live = record.findings.filter((f) => f.lastSeen >= cutoff);
|
|
181
|
+
live.sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : a.lastSeen > b.lastSeen ? -1 : 0));
|
|
182
|
+
record.findings = live.slice(0, MAX_FINDINGS);
|
|
183
|
+
return record;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** An empty record, for a machine that has never scanned. */
|
|
187
|
+
export function emptyRecord(salt: string, nowIso: string): LeakRecord {
|
|
188
|
+
return {
|
|
189
|
+
schemaVersion: LEAK_RECORD_SCHEMA_VERSION,
|
|
190
|
+
salt,
|
|
191
|
+
updatedAt: nowIso,
|
|
192
|
+
findings: [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Describe how a credential came to be in a tool event.
|
|
198
|
+
*
|
|
199
|
+
* Specific by construction: the path or command is what makes a finding
|
|
200
|
+
* recognisable to the person who caused it, and a generic phrase ("detected in
|
|
201
|
+
* tool input") is the kind of alert people learn to ignore.
|
|
202
|
+
*/
|
|
203
|
+
export function describeMechanism(
|
|
204
|
+
toolName: string,
|
|
205
|
+
direction: "input" | "result",
|
|
206
|
+
shortPath: string | null,
|
|
207
|
+
): LeakMechanism {
|
|
208
|
+
const where = shortPath ? ` ${shortPath}` : "";
|
|
209
|
+
let summary: string;
|
|
210
|
+
if (direction === "result") {
|
|
211
|
+
summary = shortPath ? `appeared in the output of reading${where}` : "appeared in tool output";
|
|
212
|
+
} else {
|
|
213
|
+
switch (toolName) {
|
|
214
|
+
case "Read":
|
|
215
|
+
summary = `read from${where || " a file"}`;
|
|
216
|
+
break;
|
|
217
|
+
case "Write":
|
|
218
|
+
summary = `written to${where || " a file"}`;
|
|
219
|
+
break;
|
|
220
|
+
case "Edit":
|
|
221
|
+
case "NotebookEdit":
|
|
222
|
+
summary = `edited into${where || " a file"}`;
|
|
223
|
+
break;
|
|
224
|
+
case "Bash":
|
|
225
|
+
summary = "passed in a shell command";
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
summary = `sent to the ${toolName} tool`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { summary, toolName, direction };
|
|
232
|
+
}
|