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
package/src/audit/index.ts
CHANGED
|
@@ -16,6 +16,11 @@ import { INTEGRATION_TYPES, type IntegrationType } from "../hooks/types";
|
|
|
16
16
|
import { ADAPTERS } from "./cli-adapters";
|
|
17
17
|
import { AUDIT_DETECTORS } from "./detectors";
|
|
18
18
|
import { severityForBuiltin } from "./features";
|
|
19
|
+
import { findSecrets, flattenToolInput } from "./leak-scan";
|
|
20
|
+
import { fingerprintSecret, fingerprintId } from "./leak-fingerprint";
|
|
21
|
+
import { readLeakIdentity, readLeakRecord, writeLeakRecord } from "./leak-store";
|
|
22
|
+
import { upsertFinding, describeMechanism, emptyRecord } from "./leak-record";
|
|
23
|
+
import { shortenPaths } from "./redact-example";
|
|
19
24
|
import { readCachedTranscript, writeCachedTranscriptResult } from "./cache";
|
|
20
25
|
import { initReplay, replayEvent, restoreReplay } from "./replay";
|
|
21
26
|
import {
|
|
@@ -32,6 +37,64 @@ import {
|
|
|
32
37
|
|
|
33
38
|
const TRANSCRIPT_CONCURRENCY = 8;
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Raw transcript bytes allowed in flight at once.
|
|
42
|
+
*
|
|
43
|
+
* The concurrency above counts FILES, which is the wrong unit. `streamEventsFrom`
|
|
44
|
+
* returns an array — despite the name it materialises every event of a
|
|
45
|
+
* transcript into JS objects — and a 57 MB JSONL becomes several hundred MB of
|
|
46
|
+
* them. On a real 1.15 GB corpus, 7 files hold 224 MB and the largest is
|
|
47
|
+
* 57.5 MB, so eight workers could put a quarter of the corpus in memory
|
|
48
|
+
* simultaneously. That machine printed `Aborted(OOM)` 22 times in one run.
|
|
49
|
+
*
|
|
50
|
+
* This is NOT what fixed those lines, and the distinction is worth keeping
|
|
51
|
+
* honest: they were measured at ~22 per run, appear within the first two
|
|
52
|
+
* seconds, are unchanged by this budget at 48 MB or at 16 MB, and do not fail
|
|
53
|
+
* the run — its counts come back correct. Their source is still unidentified.
|
|
54
|
+
* This bounds a real and separate failure mode (eight of the seven largest
|
|
55
|
+
* files in flight at once); it should not be read as having fixed the other.
|
|
56
|
+
*
|
|
57
|
+
* 48 MB of raw input is the budget because the expansion factor from JSONL to
|
|
58
|
+
* parsed objects is roughly 5-10x, which keeps the peak inside a default heap
|
|
59
|
+
* with room for the eight small-file workers this is meant not to slow down.
|
|
60
|
+
* Chosen to bound memory, not to be exactly right: the failure it prevents is a
|
|
61
|
+
* crash, and the cost of being conservative is that one very large file scans
|
|
62
|
+
* alone for a moment.
|
|
63
|
+
*/
|
|
64
|
+
const TRANSCRIPT_BYTE_BUDGET = 48 * 1024 * 1024;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Admission control by weight, so a worker waits for MEMORY as well as a slot.
|
|
68
|
+
*
|
|
69
|
+
* A file bigger than the whole budget is admitted alone rather than refused —
|
|
70
|
+
* otherwise the largest transcript on the machine could never be scanned, which
|
|
71
|
+
* is precisely the one most likely to hold something.
|
|
72
|
+
*/
|
|
73
|
+
class ByteGate {
|
|
74
|
+
private inFlight = 0;
|
|
75
|
+
private waiting: Array<() => void> = [];
|
|
76
|
+
|
|
77
|
+
constructor(private readonly budget: number) {}
|
|
78
|
+
|
|
79
|
+
async acquire(bytes: number): Promise<void> {
|
|
80
|
+
const want = Math.max(0, bytes);
|
|
81
|
+
while (this.inFlight > 0 && this.inFlight + want > this.budget) {
|
|
82
|
+
await new Promise<void>((resolve) => this.waiting.push(resolve));
|
|
83
|
+
}
|
|
84
|
+
this.inFlight += want;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
release(bytes: number): void {
|
|
88
|
+
this.inFlight -= Math.max(0, bytes);
|
|
89
|
+
if (this.inFlight < 0) this.inFlight = 0;
|
|
90
|
+
// Wake everyone and let each re-test its own weight: a small task behind a
|
|
91
|
+
// large one should not be held by a queue position it does not need.
|
|
92
|
+
const waiters = this.waiting;
|
|
93
|
+
this.waiting = [];
|
|
94
|
+
for (const wake of waiters) wake();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
35
98
|
/** Canonicalize a policy name to its short, qualified form for display
|
|
36
99
|
* (`failproofai/foo` → `foo`). */
|
|
37
100
|
function shortPolicyName(name: string): string {
|
|
@@ -113,6 +176,50 @@ interface ScanOutcome {
|
|
|
113
176
|
resumed: boolean;
|
|
114
177
|
}
|
|
115
178
|
|
|
179
|
+
const CREDENTIAL_RESEARCH_MARKERS: ReadonlyArray<RegExp> = [
|
|
180
|
+
/\b(?:secret|credential)[-_ ](?:scanner|detection|detector|pattern|regex|corpus)\b/i,
|
|
181
|
+
/\b(?:trufflehog|trufflesecurity|gitleaks|detect-secrets|secret-detection-rules)\b/i,
|
|
182
|
+
/\b(?:SECRET_PATTERNS|findSecrets|isDocsLiteral|leak-scan)\b/,
|
|
183
|
+
/\b(?:fixture|placeholder|synthetic|sample) (?:api )?(?:key|token|secret|credential)s?\b/i,
|
|
184
|
+
/\b(?:grep|rg|ripgrep|scan|hunt|search)\b[^\n]{0,240}\b(?:secret|credential|token)s?\b/i,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const CREDENTIAL_RESEARCH_PURPOSE =
|
|
188
|
+
/\b(?:hunt|scan|search|research|test)(?:ing)?\b[^\n]{0,80}\b(?:secret|credential|token)s?\b|\b(?:secret|credential|token)s?\b[^\n]{0,80}\b(?:scanner|detector|detection|research|fixtures?|corpus)\b/i;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Deliberate secret-detector research is full of realistic fake credentials.
|
|
192
|
+
* Reporting those as leaks is the feedback loop that made one measured session
|
|
193
|
+
* contribute 455 of the 500 rows in its own audit.
|
|
194
|
+
*
|
|
195
|
+
* Require independent signals across tool INPUTS. Results are excluded from
|
|
196
|
+
* classification: a normal command can print hostile text, while commands and
|
|
197
|
+
* URLs state what the agent intentionally set out to inspect. Two signals keep
|
|
198
|
+
* an ordinary discussion that happens to say "secret scanner" from silencing a
|
|
199
|
+
* session; dedicated detector/test work reliably carries several.
|
|
200
|
+
*/
|
|
201
|
+
export function isCredentialResearchSession(
|
|
202
|
+
events: NormalizedToolEvent[],
|
|
203
|
+
sessionDescription?: string,
|
|
204
|
+
): boolean {
|
|
205
|
+
if (sessionDescription && CREDENTIAL_RESEARCH_PURPOSE.test(sessionDescription)) return true;
|
|
206
|
+
const matched = new Set<number>();
|
|
207
|
+
let evidenceEvents = 0;
|
|
208
|
+
for (const event of events) {
|
|
209
|
+
const intent = flattenToolInput(event.toolInput);
|
|
210
|
+
let eventMatched = false;
|
|
211
|
+
for (let i = 0; i < CREDENTIAL_RESEARCH_MARKERS.length; i++) {
|
|
212
|
+
if (CREDENTIAL_RESEARCH_MARKERS[i].test(intent)) {
|
|
213
|
+
matched.add(i);
|
|
214
|
+
eventMatched = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (eventMatched) evidenceEvents++;
|
|
218
|
+
if (matched.size >= 2 || evidenceEvents >= 3) return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
116
223
|
async function scanOneTranscript(
|
|
117
224
|
meta: TranscriptMetadata,
|
|
118
225
|
resume?: { fromByte: number; detectorState: DetectorSessionState },
|
|
@@ -181,21 +288,27 @@ async function scanOneTranscript(
|
|
|
181
288
|
// Capture the session's cwd from the first event that carried one — every
|
|
182
289
|
// event in a single transcript shares the same cwd by construction.
|
|
183
290
|
result.cwd = result.cwd || events[0].cwd || "";
|
|
291
|
+
const suppressLeakScan = isCredentialResearchSession(events, meta.sessionDescription);
|
|
292
|
+
if (suppressLeakScan) result.leakScanSuppressed = "credential-research";
|
|
184
293
|
|
|
185
294
|
for (const event of events) {
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
295
|
+
// The 8 behavioural detectors are switched off with the rest of the old
|
|
296
|
+
// audit — see the header of `src/audit/scoring.ts`. The modules and their
|
|
297
|
+
// unit tests are untouched; nothing calls them. `sessionState` is still
|
|
298
|
+
// threaded through so a resumed scan keeps its shape and restoring this
|
|
299
|
+
// loop needs no plumbing.
|
|
300
|
+
// for (const detector of AUDIT_DETECTORS) {
|
|
301
|
+
// const hit = detector.detect(event, sessionState);
|
|
302
|
+
// if (!hit) continue;
|
|
303
|
+
// recordHit(
|
|
304
|
+
// result,
|
|
305
|
+
// detector.name,
|
|
306
|
+
// event.timestamp,
|
|
307
|
+
// event.cwd,
|
|
308
|
+
// truncateExample(hit.example),
|
|
309
|
+
// );
|
|
310
|
+
// }
|
|
311
|
+
// Replay through every builtin policy.
|
|
199
312
|
let replayHits;
|
|
200
313
|
try {
|
|
201
314
|
replayHits = await replayEvent(event);
|
|
@@ -212,11 +325,129 @@ async function scanOneTranscript(
|
|
|
212
325
|
truncateExample(example),
|
|
213
326
|
);
|
|
214
327
|
}
|
|
328
|
+
|
|
329
|
+
// Credential VALUES, which the replay above cannot report: it returns a
|
|
330
|
+
// decision per policy, never the text that matched. Scanned separately so
|
|
331
|
+
// the leak report can name WHICH key, and so detection can be tuned for
|
|
332
|
+
// precision without dragging the redactor's recall down with it.
|
|
333
|
+
if (!suppressLeakScan) recordLeaks(result, event);
|
|
215
334
|
}
|
|
216
335
|
|
|
217
336
|
return { result, bytesScanned, detectorState: sessionState, resumed };
|
|
218
337
|
}
|
|
219
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Fingerprint every credential in one event and attach the sightings.
|
|
341
|
+
*
|
|
342
|
+
* The raw value exists only inside this function. It is read out of the event,
|
|
343
|
+
* hashed into a salted id, rendered into a mask, and dropped — nothing that
|
|
344
|
+
* leaves here can reconstruct it.
|
|
345
|
+
*
|
|
346
|
+
* Input and result are scanned separately and tagged, because they are
|
|
347
|
+
* different exposures: an input is what the agent SENT (deniable at PreToolUse
|
|
348
|
+
* on 12/12 CLIs, and 39.5% of measured appearances) while a result is what it
|
|
349
|
+
* RECEIVED (blockable on 2/12, and only after the fact).
|
|
350
|
+
*/
|
|
351
|
+
function recordLeaks(result: TranscriptAuditResult, event: NormalizedToolEvent): void {
|
|
352
|
+
const salt = leakSalt();
|
|
353
|
+
if (!salt) return;
|
|
354
|
+
|
|
355
|
+
const filePath = (event.toolInput as { file_path?: unknown }).file_path;
|
|
356
|
+
const shortPath = typeof filePath === "string" ? shortenPaths(filePath) : null;
|
|
357
|
+
|
|
358
|
+
const scan = (text: string, direction: "input" | "result"): void => {
|
|
359
|
+
for (const match of findSecrets(text)) {
|
|
360
|
+
(result.leaks ??= []).push({
|
|
361
|
+
id: fingerprintId(match.value, salt),
|
|
362
|
+
fingerprint: fingerprintSecret(match.value, match.rule),
|
|
363
|
+
name: match.name,
|
|
364
|
+
rule: match.rule,
|
|
365
|
+
shaped: match.shaped,
|
|
366
|
+
timestamp: event.timestamp,
|
|
367
|
+
cwd: event.cwd,
|
|
368
|
+
toolName: event.toolName,
|
|
369
|
+
direction,
|
|
370
|
+
path: shortPath,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
scan(flattenToolInput(event.toolInput), "input");
|
|
376
|
+
if (event.toolResultText !== undefined) scan(event.toolResultText, "result");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The machine's HMAC salt, read once per process.
|
|
381
|
+
*
|
|
382
|
+
* Cached because `scanOneTranscript` runs this per EVENT — 340,918 of them on
|
|
383
|
+
* the measured corpus — and re-reading a file that many times would dominate
|
|
384
|
+
* the scan. `null` when the store is unreadable, which disables leak recording
|
|
385
|
+
* rather than failing the audit: a scan that cannot fingerprint is still a scan
|
|
386
|
+
* that can count.
|
|
387
|
+
*/
|
|
388
|
+
let cachedSalt: string | null | undefined;
|
|
389
|
+
function leakSalt(): string | null {
|
|
390
|
+
if (cachedSalt === undefined) {
|
|
391
|
+
try {
|
|
392
|
+
cachedSalt = readLeakIdentity().salt;
|
|
393
|
+
} catch {
|
|
394
|
+
cachedSalt = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return cachedSalt;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Merge this scan's sightings into `~/.failproofai/audit/leaks.json`.
|
|
402
|
+
*
|
|
403
|
+
* Returns the ids seen for the FIRST time, which is the set worth telling a
|
|
404
|
+
* human about. Never throws: a scan that finds leaks it cannot write down has
|
|
405
|
+
* still found them, and the caller reports on the in-memory result either way.
|
|
406
|
+
*/
|
|
407
|
+
export function persistLeaks(perTranscript: TranscriptAuditResult[], replaceExisting: boolean): string[] {
|
|
408
|
+
const fresh: string[] = [];
|
|
409
|
+
try {
|
|
410
|
+
const previous = readLeakRecord();
|
|
411
|
+
const previousIds = new Set(previous.findings.map((f) => f.id));
|
|
412
|
+
// A successful all-history scan is authoritative. Rebuilding is what lets
|
|
413
|
+
// detector precision fixes remove findings they now reject; merging made
|
|
414
|
+
// every false positive live for 90 days even after the code was fixed.
|
|
415
|
+
// Scoped or incomplete scans still merge because absence outside their
|
|
416
|
+
// coverage says nothing.
|
|
417
|
+
const record = replaceExisting
|
|
418
|
+
? emptyRecord(previous.salt, new Date().toISOString())
|
|
419
|
+
: previous;
|
|
420
|
+
for (const t of perTranscript) {
|
|
421
|
+
for (const leak of t.leaks ?? []) {
|
|
422
|
+
const { isNew } = upsertFinding(record, {
|
|
423
|
+
id: leak.id,
|
|
424
|
+
fingerprint: leak.fingerprint,
|
|
425
|
+
name: leak.name,
|
|
426
|
+
rule: leak.rule,
|
|
427
|
+
// A vendor shape is what licenses an alert to name a console; a
|
|
428
|
+
// name-only match knows something leaked, not who issued it.
|
|
429
|
+
confidence: leak.shaped ? "doc-verified" : "name-only",
|
|
430
|
+
sighting: {
|
|
431
|
+
cli: t.cli,
|
|
432
|
+
sessionId: t.sessionId,
|
|
433
|
+
cwd: shortenPaths(leak.cwd),
|
|
434
|
+
at: leak.timestamp,
|
|
435
|
+
mechanism: describeMechanism(leak.toolName, leak.direction, leak.path),
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
// During an authoritative rebuild every insertion is new to the empty
|
|
439
|
+
// record, but only ids absent from the previous record are new to the
|
|
440
|
+
// HUMAN and eligible for a notification.
|
|
441
|
+
if (isNew && !previousIds.has(leak.id)) fresh.push(leak.id);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
writeLeakRecord(record);
|
|
445
|
+
} catch {
|
|
446
|
+
// Bookkeeping failure must not fail the audit — see leak-store.ts.
|
|
447
|
+
}
|
|
448
|
+
return fresh;
|
|
449
|
+
}
|
|
450
|
+
|
|
220
451
|
function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): string {
|
|
221
452
|
if (event.toolName === "Bash") {
|
|
222
453
|
const command = (event.toolInput as { command?: unknown }).command;
|
|
@@ -236,7 +467,7 @@ function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): s
|
|
|
236
467
|
* came from the FIRST event in the file and the tail's came from the first
|
|
237
468
|
* event after the offset, so the older one wins.
|
|
238
469
|
*/
|
|
239
|
-
function mergeIncremental(
|
|
470
|
+
export function mergeIncremental(
|
|
240
471
|
cached: TranscriptAuditResult,
|
|
241
472
|
tail: TranscriptAuditResult,
|
|
242
473
|
): TranscriptAuditResult {
|
|
@@ -250,6 +481,12 @@ function mergeIncremental(
|
|
|
250
481
|
examplesByName: {},
|
|
251
482
|
rangeByName: { ...cached.rangeByName },
|
|
252
483
|
};
|
|
484
|
+
if (cached.leakScanSuppressed || tail.leakScanSuppressed) {
|
|
485
|
+
out.leakScanSuppressed = "credential-research";
|
|
486
|
+
out.leaks = [];
|
|
487
|
+
} else {
|
|
488
|
+
out.leaks = [...(cached.leaks ?? []), ...(tail.leaks ?? [])];
|
|
489
|
+
}
|
|
253
490
|
for (const [name, list] of Object.entries(cached.examplesByName)) {
|
|
254
491
|
out.examplesByName[name] = [...list];
|
|
255
492
|
}
|
|
@@ -432,18 +669,22 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
|
|
|
432
669
|
|
|
433
670
|
// 1. Discover transcripts across all selected CLIs.
|
|
434
671
|
const allTranscripts: TranscriptMetadata[] = [];
|
|
672
|
+
let discoveryErrors = 0;
|
|
435
673
|
for (const cli of clis) {
|
|
436
674
|
const adapter = ADAPTERS[cli];
|
|
437
675
|
let list: TranscriptMetadata[];
|
|
438
676
|
try {
|
|
439
677
|
list = await adapter.listTranscripts({ projects: opts.projects, sinceMs });
|
|
440
678
|
} catch {
|
|
679
|
+
discoveryErrors++;
|
|
441
680
|
continue; // adapter failures shouldn't kill the whole audit
|
|
442
681
|
}
|
|
443
682
|
allTranscripts.push(...list);
|
|
444
683
|
}
|
|
445
684
|
|
|
446
|
-
// 2. Scan each transcript (cache-aware), 8
|
|
685
|
+
// 2. Scan each transcript (cache-aware), 8 at a time and within a memory
|
|
686
|
+
// budget — see TRANSCRIPT_BYTE_BUDGET for why a file count is the wrong unit.
|
|
687
|
+
const gate = new ByteGate(TRANSCRIPT_BYTE_BUDGET);
|
|
447
688
|
let skipped = 0;
|
|
448
689
|
let errors = 0;
|
|
449
690
|
const tasks = allTranscripts.map((meta) => async (): Promise<TranscriptAuditResult> => {
|
|
@@ -457,6 +698,9 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
|
|
|
457
698
|
: undefined;
|
|
458
699
|
cachedPrefix = found?.kind === "resume" ? found.result : null;
|
|
459
700
|
}
|
|
701
|
+
// Memory admission. Taken AFTER the cache check above, so a cache hit — the
|
|
702
|
+
// common case on a warm machine — never waits for a slot it does not use.
|
|
703
|
+
await gate.acquire(meta.sizeBytes);
|
|
460
704
|
try {
|
|
461
705
|
const scan = await scanOneTranscript(meta, resume);
|
|
462
706
|
// A resumed scan produced hits for the TAIL only; the cached result holds
|
|
@@ -500,6 +744,11 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
|
|
|
500
744
|
examplesByName: {},
|
|
501
745
|
rangeByName: {},
|
|
502
746
|
};
|
|
747
|
+
} finally {
|
|
748
|
+
// Released on every path, including the error one — a task that threw
|
|
749
|
+
// still freed its memory, and holding its weight would shrink the budget
|
|
750
|
+
// permanently over a long scan until nothing could be admitted at all.
|
|
751
|
+
gate.release(meta.sizeBytes);
|
|
503
752
|
}
|
|
504
753
|
});
|
|
505
754
|
|
|
@@ -527,8 +776,28 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
|
|
|
527
776
|
eventsScanned += t.eventsScanned ?? 0;
|
|
528
777
|
}
|
|
529
778
|
|
|
779
|
+
// Fold every sighting into the persistent record. This is the ONLY place the
|
|
780
|
+
// leak record is written: `scanOneTranscript` runs 8-way concurrent and a
|
|
781
|
+
// per-transcript write would be eight processes racing one file — the exact
|
|
782
|
+
// shape that lost updates when it was measured (2 concurrent writers, 5/5
|
|
783
|
+
// trials, the loser's entry gone permanently).
|
|
784
|
+
//
|
|
785
|
+
// `newFindingIds` is what a notice keys on: an id already in the record must
|
|
786
|
+
// not alert again however many fresh sightings it accumulates, and a
|
|
787
|
+
// credential seen for the first time must alert even though its rule has
|
|
788
|
+
// fired a thousand times before.
|
|
789
|
+
const coversAllHistory =
|
|
790
|
+
opts.clis === undefined &&
|
|
791
|
+
opts.projects === undefined &&
|
|
792
|
+
opts.since === undefined &&
|
|
793
|
+
discoveryErrors === 0 &&
|
|
794
|
+
errors === 0 &&
|
|
795
|
+
skipped === 0;
|
|
796
|
+
const newFindingIds = persistLeaks(perTranscript, coversAllHistory);
|
|
797
|
+
|
|
530
798
|
const auditResult: AuditResult = {
|
|
531
799
|
version: 2,
|
|
800
|
+
newLeakIds: newFindingIds,
|
|
532
801
|
scannedAt: new Date(startedAt).toISOString(),
|
|
533
802
|
scope: {
|
|
534
803
|
cli: clis,
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a leaked credential is NAMED once it leaves the machine.
|
|
3
|
+
*
|
|
4
|
+
* Every other redactor in this codebase exists to destroy a value. This one
|
|
5
|
+
* exists to describe one, because the leak report has the opposite job: a
|
|
6
|
+
* finding the reader cannot tie to a row in a vendor console is a finding they
|
|
7
|
+
* cannot act on. `[REDACTED: GitHub token]` tells you nothing you can revoke.
|
|
8
|
+
*
|
|
9
|
+
* The rule is: **enough to identify, never enough to use.**
|
|
10
|
+
*
|
|
11
|
+
* ## Why the mask character matters, which is not obvious
|
|
12
|
+
*
|
|
13
|
+
* The three most natural masking characters — `X`, `x` and `0` — RE-MATCH every
|
|
14
|
+
* vendor prefix pattern we ship. `AKIA` + sixteen `X`s satisfies
|
|
15
|
+
* `AKIA[A-Z0-9]{16}`, so a masked AWS key is reported by failproofai as a live
|
|
16
|
+
* AWS key on the next scan. The same holds for a masked connection string,
|
|
17
|
+
* whose pattern keys on `://…@` and does not care what sits between. So the
|
|
18
|
+
* mask is `•` (U+2022), which is in no credential charset anywhere, and the
|
|
19
|
+
* unit tests assert that every masked form fails to re-match.
|
|
20
|
+
*
|
|
21
|
+
* That is not a hypothetical: this product measured its own audit output
|
|
22
|
+
* feeding back into its own corpus, one leaked credential becoming seven
|
|
23
|
+
* reported findings across four sessions.
|
|
24
|
+
*
|
|
25
|
+
* ## Why the last four, and why not always
|
|
26
|
+
*
|
|
27
|
+
* Last-4 is what vendor consoles display next to a key, so it is the token that
|
|
28
|
+
* lets a human find the right row and press revoke. But it is only safe when
|
|
29
|
+
* what remains is genuinely unguessable. For a 40-character GitHub token,
|
|
30
|
+
* showing `ghp_` and four trailing characters leaves ~32 unknown base62
|
|
31
|
+
* characters — no help to anyone. For a 13-character human-chosen password,
|
|
32
|
+
* last-4 is nearly a third of the secret, and the corpus's one confirmed-live
|
|
33
|
+
* password was exactly 13 characters at 3.19 bits per character.
|
|
34
|
+
*
|
|
35
|
+
* So the tail is shown only when the value clears BOTH bars: a recognised
|
|
36
|
+
* public vendor prefix (which proves it is a minted token, not a typed
|
|
37
|
+
* password) AND enough residual length. Everything else is described by class
|
|
38
|
+
* and length alone.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** The mask glyph. Never `X`, `x` or `0` — see the module header. */
|
|
42
|
+
const MASK_CHAR = "•";
|
|
43
|
+
|
|
44
|
+
/** Longest run of mask glyphs rendered, so a 200-char token stays readable. */
|
|
45
|
+
const MAX_MASK_RUN = 8;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Minimum total length before any trailing characters are shown.
|
|
49
|
+
*
|
|
50
|
+
* 24 is chosen so that even the shortest minted token we recognise keeps well
|
|
51
|
+
* over 100 bits of residual entropy after the prefix and tail are removed.
|
|
52
|
+
* Human-chosen passwords essentially never reach it, which is the point.
|
|
53
|
+
*/
|
|
54
|
+
const MIN_LEN_FOR_TAIL = 24;
|
|
55
|
+
|
|
56
|
+
/** How many trailing characters a vendor console typically shows. */
|
|
57
|
+
const TAIL_CHARS = 4;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Public, minted prefixes — the part of a credential that carries no secret.
|
|
61
|
+
*
|
|
62
|
+
* Each of these is published by its issuer and identical across every key they
|
|
63
|
+
* mint, so showing it discloses nothing while telling the reader which console
|
|
64
|
+
* to open. Ordered longest-first, so `sk-ant-api03-` wins over `sk-ant-` and
|
|
65
|
+
* `sk-`; the first match is the one rendered.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately NOT the full detection vocabulary. This list only has to be
|
|
68
|
+
* right about what is safe to PRINT — a prefix missing from it costs a less
|
|
69
|
+
* specific label, never a disclosure.
|
|
70
|
+
*/
|
|
71
|
+
const PUBLIC_PREFIXES: ReadonlyArray<readonly [string, string]> = [
|
|
72
|
+
["sk-ant-api03-", "Anthropic API key"],
|
|
73
|
+
["sk-ant-oat01-", "Anthropic OAuth token"],
|
|
74
|
+
["sk-ant-", "Anthropic key"],
|
|
75
|
+
["sk-proj-", "OpenAI project key"],
|
|
76
|
+
["sk-svcacct-", "OpenAI service-account key"],
|
|
77
|
+
["sk-admin-", "OpenAI admin key"],
|
|
78
|
+
["sk-or-v1-", "OpenRouter key"],
|
|
79
|
+
["github_pat_", "GitHub fine-grained token"],
|
|
80
|
+
["ghp_", "GitHub personal access token"],
|
|
81
|
+
["gho_", "GitHub OAuth token"],
|
|
82
|
+
["ghu_", "GitHub user-to-server token"],
|
|
83
|
+
["ghs_", "GitHub server-to-server token"],
|
|
84
|
+
["ghr_", "GitHub refresh token"],
|
|
85
|
+
["glpat-", "GitLab personal access token"],
|
|
86
|
+
["xoxb-", "Slack bot token"],
|
|
87
|
+
["xoxp-", "Slack user token"],
|
|
88
|
+
["xapp-", "Slack app-level token"],
|
|
89
|
+
["sk_live_", "Stripe live secret key"],
|
|
90
|
+
["sk_test_", "Stripe test secret key"],
|
|
91
|
+
["rk_live_", "Stripe restricted key"],
|
|
92
|
+
["AKIA", "AWS access key ID"],
|
|
93
|
+
["ASIA", "AWS temporary access key ID"],
|
|
94
|
+
["AIza", "Google API key"],
|
|
95
|
+
["hf_", "Hugging Face token"],
|
|
96
|
+
["gsk_", "Groq API key"],
|
|
97
|
+
["r8_", "Replicate token"],
|
|
98
|
+
["xai-", "xAI key"],
|
|
99
|
+
["npm_", "npm access token"],
|
|
100
|
+
["dop_v1_", "DigitalOcean token"],
|
|
101
|
+
["shpat_", "Shopify access token"],
|
|
102
|
+
["sbp_", "Supabase token"],
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
export interface LeakFingerprint {
|
|
106
|
+
/** What the reader sees: `ghp_••••••••4f2a` or `[13-char password]`. */
|
|
107
|
+
display: string;
|
|
108
|
+
/** Human label for the class, e.g. "GitHub personal access token". */
|
|
109
|
+
label: string;
|
|
110
|
+
/** Total character length of the original value. */
|
|
111
|
+
length: number;
|
|
112
|
+
/** True when a minted public prefix was recognised — i.e. we can name a
|
|
113
|
+
* console to revoke at. False means "we know it leaked, not who issued it". */
|
|
114
|
+
attributed: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The recognised public prefix for a value, longest match first. */
|
|
118
|
+
function matchPublicPrefix(value: string): readonly [string, string] | null {
|
|
119
|
+
for (const entry of PUBLIC_PREFIXES) {
|
|
120
|
+
if (value.startsWith(entry[0])) return entry;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render a credential as something a human can recognise and nobody can use.
|
|
127
|
+
*
|
|
128
|
+
* `fallbackLabel` names the class when no public prefix is recognised — pass
|
|
129
|
+
* the detecting rule's own label (e.g. "assigned secret", "database password")
|
|
130
|
+
* so an unattributed finding still says what KIND of thing leaked.
|
|
131
|
+
*/
|
|
132
|
+
export function fingerprintSecret(value: string, fallbackLabel = "secret"): LeakFingerprint {
|
|
133
|
+
const length = value.length;
|
|
134
|
+
const matched = matchPublicPrefix(value);
|
|
135
|
+
|
|
136
|
+
if (!matched) {
|
|
137
|
+
// No minted prefix: this is a password, a first-party key, or a format we
|
|
138
|
+
// do not recognise. Nothing but the class and the length is safe to print,
|
|
139
|
+
// and the length is what lets the owner recognise their own value.
|
|
140
|
+
return {
|
|
141
|
+
display: `[${length}-char ${fallbackLabel}]`,
|
|
142
|
+
label: fallbackLabel,
|
|
143
|
+
length,
|
|
144
|
+
attributed: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const [prefix, label] = matched;
|
|
149
|
+
const bodyLength = length - prefix.length;
|
|
150
|
+
const run = MASK_CHAR.repeat(Math.max(1, Math.min(MAX_MASK_RUN, bodyLength)));
|
|
151
|
+
|
|
152
|
+
// The tail is the actionable half — but only when what stays hidden is
|
|
153
|
+
// genuinely unguessable. See the module header.
|
|
154
|
+
if (length >= MIN_LEN_FOR_TAIL && bodyLength > TAIL_CHARS * 2) {
|
|
155
|
+
return {
|
|
156
|
+
display: `${prefix}${run}${value.slice(-TAIL_CHARS)}`,
|
|
157
|
+
label,
|
|
158
|
+
length,
|
|
159
|
+
attributed: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { display: `${prefix}${run}`, label, length, attributed: true };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A stable identity for one distinct credential, for deduping across scans.
|
|
168
|
+
*
|
|
169
|
+
* Salted with a per-machine secret so the digest cannot be used as a lookup
|
|
170
|
+
* oracle: without the salt, an attacker holding a guessable candidate (and the
|
|
171
|
+
* corpus is full of dictionary passwords) could confirm it by comparing
|
|
172
|
+
* hashes. With it, the id is meaningful only on the machine that produced it.
|
|
173
|
+
*
|
|
174
|
+
* The salt therefore forecloses cross-machine dedupe until an org-scoped key
|
|
175
|
+
* exists — a deliberate trade recorded here so nobody reads a per-machine salt
|
|
176
|
+
* as an oversight.
|
|
177
|
+
*/
|
|
178
|
+
/**
|
|
179
|
+
* Every id `fingerprintId` can produce, and nothing else.
|
|
180
|
+
*
|
|
181
|
+
* Ids become FILENAMES — one marker file per notified finding, one queue file
|
|
182
|
+
* per pending macOS banner — so an id is a path component, and a path component
|
|
183
|
+
* built from unvalidated input is a directory traversal. Measured: passing
|
|
184
|
+
* `"../../../../tmp/PWNED"` to `markLeakNoticeDelivered` created that file.
|
|
185
|
+
*
|
|
186
|
+
* Ids are HMAC hex today, so nothing in a normal run can carry a `/`. That is
|
|
187
|
+
* not the same as safe: the record is JSON read off disk, and `leaks.json` is a
|
|
188
|
+
* file a user (or anything running as them) can edit. The guard is one regex
|
|
189
|
+
* and it makes the property structural instead of incidental.
|
|
190
|
+
*/
|
|
191
|
+
export function isFindingId(id: string): boolean {
|
|
192
|
+
return /^[0-9a-f]{16}$/.test(id);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function fingerprintId(value: string, machineSalt: string): string {
|
|
196
|
+
// Lazily required so this module stays importable in the browser bundle,
|
|
197
|
+
// where the dashboard renders findings but never computes ids.
|
|
198
|
+
const { createHmac } = require("node:crypto") as typeof import("node:crypto");
|
|
199
|
+
return createHmac("sha256", machineSalt).update(value).digest("hex").slice(0, 16);
|
|
200
|
+
}
|