failproofai 1.0.3 → 1.0.4-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
- package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
- package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
- package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
- package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
- package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
- package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
- package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
- package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
- package/.next/standalone/app/actions/get-leaks.ts +100 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
- package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
- package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
- package/.next/standalone/app/audit/audit-styles.css +114 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
- package/.next/standalone/lib/auth/api-server-client.ts +28 -0
- package/.next/standalone/lib/claude-sessions.ts +101 -27
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
- package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
- package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +11 -11
- package/.next/standalone/package.json +14 -13
- package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
- package/.next/standalone/sdk/python/README.md +8 -0
- package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
- package/.next/standalone/sdk/python/uv.lock +29 -29
- package/.next/standalone/server.js +1 -1
- package/README.md +13 -58
- package/dist/cli.mjs +5368 -8756
- package/dist/worker.mjs +539 -312
- package/lib/auth/api-server-client.ts +28 -0
- package/lib/claude-sessions.ts +101 -27
- package/package.json +14 -13
- package/pi-extension/index.ts +42 -4
- package/scripts/changelog-open.py +115 -0
- package/src/audit/cli.ts +134 -0
- package/src/audit/desktop-notify.ts +420 -0
- package/src/audit/harm-report.ts +101 -0
- package/src/audit/index.ts +141 -13
- package/src/audit/leak-fingerprint.ts +200 -0
- package/src/audit/leak-notice.ts +161 -0
- package/src/audit/leak-record.ts +232 -0
- package/src/audit/leak-scan.ts +292 -0
- package/src/audit/leak-store.ts +217 -0
- package/src/audit/macos-notifier.ts +310 -0
- package/src/audit/redact-example.ts +266 -14
- package/src/audit/report-harm.ts +12 -1
- package/src/audit/report.ts +6 -3
- package/src/audit/schedule-cli.ts +37 -0
- package/src/audit/scoring.ts +49 -0
- package/src/audit/types.ts +41 -0
- package/src/hooks/builtin-policies.ts +67 -2
- package/src/hooks/configure-wizard.ts +20 -0
- package/src/hooks/fp-config.ts +71 -11
- package/src/hooks/fp-home.ts +27 -0
- package/src/hooks/handler.ts +59 -1
- package/src/hooks/integrations.ts +47 -13
- package/src/hooks/notice.ts +155 -0
- package/src/hooks/uninstall-cli.ts +15 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
- package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
- package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
- package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
- package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
- package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
- package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
|
@@ -253,6 +253,34 @@ export interface AuditReportBody {
|
|
|
253
253
|
last_seen?: string;
|
|
254
254
|
examples: string[];
|
|
255
255
|
}[];
|
|
256
|
+
/**
|
|
257
|
+
* Credentials this machine found in its own transcripts.
|
|
258
|
+
*
|
|
259
|
+
* Optional on the wire, so a CLI newer than the api-server sends a field the
|
|
260
|
+
* server ignores rather than failing the whole digest — the harmful counts
|
|
261
|
+
* are the older half of the contract and must keep arriving either way.
|
|
262
|
+
*
|
|
263
|
+
* Every field here is a mask, a label, a length or a location. There is no
|
|
264
|
+
* shape of this object that carries a secret, because the record it is built
|
|
265
|
+
* from never stores one: `recordLeaks` fingerprints at detection time and
|
|
266
|
+
* keeps only the masked rendering.
|
|
267
|
+
*/
|
|
268
|
+
leaks?: {
|
|
269
|
+
id: string;
|
|
270
|
+
display: string;
|
|
271
|
+
label: string;
|
|
272
|
+
length: number;
|
|
273
|
+
attributed: boolean;
|
|
274
|
+
name: string | null;
|
|
275
|
+
confidence: string;
|
|
276
|
+
first_seen: string;
|
|
277
|
+
last_seen: string;
|
|
278
|
+
occurrences: number;
|
|
279
|
+
cli: string;
|
|
280
|
+
project: string;
|
|
281
|
+
mechanism: string;
|
|
282
|
+
direction: "input" | "result";
|
|
283
|
+
}[];
|
|
256
284
|
}
|
|
257
285
|
|
|
258
286
|
export interface AuditReportResult {
|
package/lib/claude-sessions.ts
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
* Claude stores transcripts at:
|
|
5
5
|
* <CLAUDE_PROJECTS_PATH>/<encoded-cwd>/<sessionId>.jsonl
|
|
6
6
|
*
|
|
7
|
-
* Subagent transcripts (when a session spawned subagents) live alongside
|
|
7
|
+
* Subagent transcripts (when a session spawned subagents) live alongside, in two
|
|
8
|
+
* shapes — a direct child, and one nested per workflow run:
|
|
8
9
|
* <CLAUDE_PROJECTS_PATH>/<encoded-cwd>/<sessionId>/subagents/<agentId>.jsonl
|
|
10
|
+
* <CLAUDE_PROJECTS_PATH>/<encoded-cwd>/<sessionId>/subagents/workflows/<runId>/<agentId>.jsonl
|
|
11
|
+
*
|
|
12
|
+
* Everything below `subagents/` is walked to a bounded depth rather than one
|
|
13
|
+
* level, because the second shape arrived after this module was written and the
|
|
14
|
+
* miss was most of the corpus. See `collectSubagentTranscripts`.
|
|
9
15
|
*
|
|
10
16
|
* The parser for these files lives in `lib/log-entries.ts` (`parseLogContent`,
|
|
11
17
|
* `parseSessionLog`). This module exposes discovery only — the audit pipeline
|
|
@@ -70,6 +76,99 @@ export function listClaudeProjects(): ClaudeProjectFolder[] {
|
|
|
70
76
|
/** Lists every JSONL transcript under one Claude project folder, including
|
|
71
77
|
* subagent transcripts under `<sessionId>/subagents/`. Returns [] on missing
|
|
72
78
|
* or unreadable paths. */
|
|
79
|
+
/**
|
|
80
|
+
* Depth of nesting allowed below `subagents/`.
|
|
81
|
+
*
|
|
82
|
+
* The two shapes that exist today need 0 and 2 (`<agent>.jsonl` and
|
|
83
|
+
* `workflows/<runId>/<agent>.jsonl`). The cap is here so an unexpected layout —
|
|
84
|
+
* or a symlink cycle a future Claude build introduces — costs a bounded walk
|
|
85
|
+
* rather than an audit that never returns, and it is deliberately loose enough
|
|
86
|
+
* that a third shape lands inside it without another release.
|
|
87
|
+
*/
|
|
88
|
+
const MAX_SUBAGENT_DEPTH = 5;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Collect every `.jsonl` beneath a session's `subagents/` directory.
|
|
92
|
+
*
|
|
93
|
+
* This used to read only the DIRECT children of `subagents/`, which was correct
|
|
94
|
+
* for the layout Claude shipped when it was written and silently wrong the day
|
|
95
|
+
* workflow runs started nesting their agents one level further down. On a
|
|
96
|
+
* machine that uses them the miss is most of the corpus: 1,839 transcripts on
|
|
97
|
+
* disk, 1,741 under `subagents/`, and 1,679 of those inside
|
|
98
|
+
* `subagents/workflows/<runId>/` — so the audit was walking 160 files, 8.7% of
|
|
99
|
+
* the evidence, and reporting the result as though it had read everything.
|
|
100
|
+
*
|
|
101
|
+
* ## Session ids come from the relative path, not the basename
|
|
102
|
+
*
|
|
103
|
+
* Every workflow run writes a `journal.jsonl` beside its agents, so basenames
|
|
104
|
+
* are NOT unique below this directory — 1,741 files share 1,613 distinct names
|
|
105
|
+
* on the machine this was found on. A basename id would collide those 128 files
|
|
106
|
+
* onto ~one row each, and `sessionId` is what the cache, the per-session
|
|
107
|
+
* detector state and the example attribution are keyed by, so the collision
|
|
108
|
+
* would silently merge unrelated sessions rather than fail.
|
|
109
|
+
*
|
|
110
|
+
* Nor is the path below `subagents/` unique on its own. A workflow run id is
|
|
111
|
+
* reused when its session is resumed, so `wf_3d609e92-a38/journal.jsonl` exists
|
|
112
|
+
* under two different parent sessions in the same project — found by asserting
|
|
113
|
+
* uniqueness over the real corpus, not by reasoning about it.
|
|
114
|
+
*
|
|
115
|
+
* The id is therefore the PARENT SESSION id followed by the path relative to
|
|
116
|
+
* `subagents/`, minus the extension, joined with `__`:
|
|
117
|
+
* `<parentUuid>__workflows__wf_123__agent-abc`. That is a filesystem path
|
|
118
|
+
* within the project, so it is unique by construction rather than by argument,
|
|
119
|
+
* and it names the session the subagent belongs to. `__` is safe as the joiner
|
|
120
|
+
* because no id Claude generates contains one — verified against 1,741 real
|
|
121
|
+
* files, zero hits. Top-level session ids are untouched.
|
|
122
|
+
*
|
|
123
|
+
* Symlinks are not followed. Nothing in the layout uses them, and following one
|
|
124
|
+
* is how a directory walk turns into an infinite loop.
|
|
125
|
+
*/
|
|
126
|
+
function collectSubagentTranscripts(
|
|
127
|
+
dir: string,
|
|
128
|
+
relPrefix: string,
|
|
129
|
+
project: ClaudeProjectFolder,
|
|
130
|
+
out: ClaudeTranscriptFile[],
|
|
131
|
+
depth: number,
|
|
132
|
+
): void {
|
|
133
|
+
if (depth > MAX_SUBAGENT_DEPTH) return;
|
|
134
|
+
let entries: import("node:fs").Dirent[];
|
|
135
|
+
try {
|
|
136
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.isSymbolicLink()) continue;
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
collectSubagentTranscripts(
|
|
144
|
+
join(dir, entry.name),
|
|
145
|
+
relPrefix ? `${relPrefix}__${entry.name}` : entry.name,
|
|
146
|
+
project,
|
|
147
|
+
out,
|
|
148
|
+
depth + 1,
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
153
|
+
const stem = entry.name.slice(0, -".jsonl".length);
|
|
154
|
+
const transcriptPath = join(dir, entry.name);
|
|
155
|
+
try {
|
|
156
|
+
const s = statSync(transcriptPath);
|
|
157
|
+
out.push({
|
|
158
|
+
projectName: project.name,
|
|
159
|
+
cwd: project.cwd,
|
|
160
|
+
sessionId: relPrefix ? `${relPrefix}__${stem}` : stem,
|
|
161
|
+
transcriptPath,
|
|
162
|
+
mtimeMs: s.mtimeMs,
|
|
163
|
+
sizeBytes: s.size,
|
|
164
|
+
isSubagent: true,
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
// unreadable — skip
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
73
172
|
export function listClaudeTranscripts(project: ClaudeProjectFolder): ClaudeTranscriptFile[] {
|
|
74
173
|
const out: ClaudeTranscriptFile[] = [];
|
|
75
174
|
let entries: import("node:fs").Dirent[];
|
|
@@ -99,34 +198,9 @@ export function listClaudeTranscripts(project: ClaudeProjectFolder): ClaudeTrans
|
|
|
99
198
|
// unreadable — skip
|
|
100
199
|
}
|
|
101
200
|
} else if (entry.isDirectory() && UUID_RE.test(entry.name)) {
|
|
102
|
-
// Subagent transcripts at <sessionId>/subagents/<agentId>.jsonl
|
|
103
201
|
const subDir = join(project.path, entry.name, "subagents");
|
|
104
202
|
if (!existsSync(subDir)) continue;
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
subEntries = readdirSync(subDir, { withFileTypes: true });
|
|
108
|
-
} catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
for (const sub of subEntries) {
|
|
112
|
-
if (!sub.isFile() || !sub.name.endsWith(".jsonl")) continue;
|
|
113
|
-
const agentId = sub.name.slice(0, -".jsonl".length);
|
|
114
|
-
const transcriptPath = join(subDir, sub.name);
|
|
115
|
-
try {
|
|
116
|
-
const s = statSync(transcriptPath);
|
|
117
|
-
out.push({
|
|
118
|
-
projectName: project.name,
|
|
119
|
-
cwd: project.cwd,
|
|
120
|
-
sessionId: agentId,
|
|
121
|
-
transcriptPath,
|
|
122
|
-
mtimeMs: s.mtimeMs,
|
|
123
|
-
sizeBytes: s.size,
|
|
124
|
-
isSubagent: true,
|
|
125
|
-
});
|
|
126
|
-
} catch {
|
|
127
|
-
// unreadable — skip
|
|
128
|
-
}
|
|
129
|
-
}
|
|
203
|
+
collectSubagentTranscripts(subDir, entry.name, project, out, 0);
|
|
130
204
|
}
|
|
131
205
|
}
|
|
132
206
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4-beta.2",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -80,14 +80,14 @@
|
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@anthropic-ai/sdk": "^0.
|
|
83
|
+
"@anthropic-ai/sdk": "^0.123.0",
|
|
84
84
|
"@mdx-js/mdx": "^3.1.1",
|
|
85
85
|
"@tailwindcss/postcss": "^4.3.1",
|
|
86
86
|
"@tanstack/react-virtual": "^3.14.10",
|
|
87
87
|
"@testing-library/jest-dom": "^7.0.0",
|
|
88
88
|
"@testing-library/react": "^16.3.2",
|
|
89
89
|
"@testing-library/user-event": "^14.6.5",
|
|
90
|
-
"@types/node": "26.
|
|
90
|
+
"@types/node": "26.4.1",
|
|
91
91
|
"@types/react": "19.2.18",
|
|
92
92
|
"@types/react-dom": "^19.2.3",
|
|
93
93
|
"@vitejs/plugin-react": "^6.1.0",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"tailwind-merge": "^3.4.0",
|
|
103
103
|
"tailwindcss": "^4.3.1",
|
|
104
104
|
"typescript": "6.0.3",
|
|
105
|
-
"vitest": "^
|
|
105
|
+
"vitest": "^5.0.0"
|
|
106
106
|
},
|
|
107
107
|
"dependencies": {
|
|
108
108
|
"html-to-image": "^1.11.13",
|
|
@@ -117,18 +117,19 @@
|
|
|
117
117
|
"vite": "8.0.16",
|
|
118
118
|
"undici": "7.29.0",
|
|
119
119
|
"brace-expansion": "5.0.9",
|
|
120
|
-
"sharp": "0.35.0"
|
|
120
|
+
"sharp": "0.35.0",
|
|
121
|
+
"browserslist": "4.28.8"
|
|
121
122
|
},
|
|
122
123
|
"optionalDependencies": {
|
|
123
|
-
"@failproofai/failproofaid-linux-x64": "1.0.
|
|
124
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.
|
|
125
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.
|
|
126
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.
|
|
124
|
+
"@failproofai/failproofaid-linux-x64": "1.0.4-beta.2",
|
|
125
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.4-beta.2",
|
|
126
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.4-beta.2",
|
|
127
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.4-beta.2"
|
|
127
128
|
},
|
|
128
129
|
"failproofaidBinaries": {
|
|
129
|
-
"linux-x64": "
|
|
130
|
-
"linux-arm64": "
|
|
131
|
-
"darwin-x64": "
|
|
132
|
-
"darwin-arm64": "
|
|
130
|
+
"linux-x64": "3039575242ff240c68a79c4ebd4e0e35e865da99c6d8a46d1a17b2d7962cb47b",
|
|
131
|
+
"linux-arm64": "02fe993dca0faccb528c48472ca1d637274c562992ba7da1bc8d439103dbcc13",
|
|
132
|
+
"darwin-x64": "2be5a422007a773253611f24f2ad9e2a301f075392ed7bc2dc0f6354d3186696",
|
|
133
|
+
"darwin-arm64": "5882a4acd7317289346934290ac8277ca79edadb009e284c9fc0b23b74d321d7"
|
|
133
134
|
}
|
|
134
135
|
}
|
package/pi-extension/index.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* spawns extensions with an undefined cwd contract, so paths are resolved
|
|
26
26
|
* relative to this file via `import.meta.url`, NOT process.cwd().
|
|
27
27
|
*/
|
|
28
|
-
import { spawnSync } from "node:child_process";
|
|
28
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
29
29
|
import { resolve, dirname, join } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
@@ -64,6 +64,44 @@ function debug(msg: string): void {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Forward an event whose VERDICT NOBODY READS, without blocking Pi.
|
|
69
|
+
*
|
|
70
|
+
* `callPolicy` is `spawnSync`, and Pi awaits its handlers serially on the
|
|
71
|
+
* startup path — so a `session_start` forward froze the whole TUI for as long
|
|
72
|
+
* as the subprocess took (measured floor 1.0-1.6s just to boot the binary,
|
|
73
|
+
* ceiling the full 60s timeout) and then threw the answer away. Three call
|
|
74
|
+
* sites are like that by design: `session_start`, `tool_result` and
|
|
75
|
+
* `session_shutdown` forward for the activity feed and for session-id
|
|
76
|
+
* continuity, and Pi cannot act on their return in any case
|
|
77
|
+
* (`ToolResultEventResult` has no `block`; the other two have no Result type
|
|
78
|
+
* at all).
|
|
79
|
+
*
|
|
80
|
+
* So they get a detached `spawn` instead. The event still reaches failproofai,
|
|
81
|
+
* the policies still run and still log, and Pi keeps rendering. Deliberately
|
|
82
|
+
* NOT used for `tool_call`, `user_bash`, `input` or `agent_end`: those four
|
|
83
|
+
* consume the decision, and blocking is what enforcement means.
|
|
84
|
+
*/
|
|
85
|
+
function forwardPolicy(eventName: string, payload: unknown): void {
|
|
86
|
+
const { cmd, args } = resolveSpawn();
|
|
87
|
+
debug(`forwardPolicy (detached) event=${eventName} cmd=${cmd}`);
|
|
88
|
+
try {
|
|
89
|
+
const child = spawn(cmd, [...args, "--hook", eventName, "--cli", "pi"], {
|
|
90
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
91
|
+
detached: false,
|
|
92
|
+
});
|
|
93
|
+
// A child that outlives its parent's interest must not keep the event loop
|
|
94
|
+
// alive, or Pi cannot exit until the subprocess does.
|
|
95
|
+
child.unref();
|
|
96
|
+
// EPIPE if the child died before stdin was consumed — fail-open, silently.
|
|
97
|
+
child.on("error", (err) => debug(`forward error ${err.message}`));
|
|
98
|
+
child.stdin.on("error", () => {});
|
|
99
|
+
child.stdin.end(JSON.stringify(payload));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
debug(`FORWARD EXCEPTION ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
67
105
|
function callPolicy(eventName: string, payload: unknown): { block: boolean; reason: string } {
|
|
68
106
|
const { cmd, args } = resolveSpawn();
|
|
69
107
|
debug(`callPolicy event=${eventName} cmd=${cmd}`);
|
|
@@ -417,7 +455,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
417
455
|
// need session_id continuity see the metadata.
|
|
418
456
|
pi.on("session_start", (event: unknown): unknown => {
|
|
419
457
|
const e = event as PiSessionStartEvent;
|
|
420
|
-
|
|
458
|
+
forwardPolicy("session_start", {
|
|
421
459
|
session_id: resolveSessionId(e.sessionId, resolveCwd(e.cwd)),
|
|
422
460
|
cwd: resolveCwd(e.cwd),
|
|
423
461
|
reason: e.reason,
|
|
@@ -435,7 +473,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
435
473
|
pi.on("tool_result", (event: unknown): unknown => {
|
|
436
474
|
const e = event as PiToolResultEvent;
|
|
437
475
|
const canonicalTool = canonicalizeToolName(e.toolName);
|
|
438
|
-
|
|
476
|
+
forwardPolicy("tool_result", {
|
|
439
477
|
tool_name: canonicalTool,
|
|
440
478
|
tool_input: canonicalizeToolInput(canonicalTool, e.input) ?? {},
|
|
441
479
|
tool_response: { content: e.content, isError: e.isError },
|
|
@@ -500,7 +538,7 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
|
|
|
500
538
|
pi.on("session_shutdown", (event: unknown): unknown => {
|
|
501
539
|
const e = event as PiSessionShutdownEvent;
|
|
502
540
|
const cwd = resolveCwd(e.cwd);
|
|
503
|
-
|
|
541
|
+
forwardPolicy("session_shutdown", {
|
|
504
542
|
session_id: resolveSessionId(e.sessionId, cwd),
|
|
505
543
|
cwd,
|
|
506
544
|
reason: e.reason,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Open a stub CHANGELOG section for the version a release just moved to.
|
|
3
|
+
|
|
4
|
+
changelog-open.py <changelog> <version> <published> [<date>]
|
|
5
|
+
|
|
6
|
+
The companion to `changelog-section.py`, which REFUSES a release whose version has
|
|
7
|
+
no section. Both PyPI publish workflows call this from `bump`, in the same commit
|
|
8
|
+
that moves `_version.py`, so the two files can never disagree about which version
|
|
9
|
+
`main` is open on.
|
|
10
|
+
|
|
11
|
+
WHY THIS EXISTS AS AUTOMATION RATHER THAN A RULE. `bump` used to move the version
|
|
12
|
+
alone, which left `main` on a version with no section — the exact state
|
|
13
|
+
`changelog-section.py` refuses at release time and that
|
|
14
|
+
`__tests__/ci/python-version-pipeline.test.ts` asserts against. Bump commits carry
|
|
15
|
+
a skip-ci marker, so nothing went red until the next unrelated PR ran CI and came
|
|
16
|
+
back failing for a reason that had nothing to do with it. That happened at the
|
|
17
|
+
first publish (repaired by hand in #755, which named the recurrence and left it),
|
|
18
|
+
and again at the second. It is not a rule a maintainer can be asked to remember:
|
|
19
|
+
the commit that creates the state is written by a workflow, not a person.
|
|
20
|
+
|
|
21
|
+
IDEMPOTENT ON PURPOSE. A re-run of `bump` against a `main` that already carries
|
|
22
|
+
the section must be a no-op, not a second heading — two `## <version>` headings
|
|
23
|
+
would put only the first one's body in the GitHub Release, silently. The version
|
|
24
|
+
is matched with the same trailing word boundary the extractor uses, so `0.0.1b1`
|
|
25
|
+
is not answered by `0.0.1b10`.
|
|
26
|
+
|
|
27
|
+
The stub is deliberately non-empty prose, not a bare heading: an empty section
|
|
28
|
+
fails `changelog-section.py` just as a missing one does, so a stub that could not
|
|
29
|
+
release would only move the failure.
|
|
30
|
+
|
|
31
|
+
Stdlib only, same reason as its two siblings: it runs in a job that installs
|
|
32
|
+
nothing precisely because it is handed a token.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
from datetime import datetime, timezone
|
|
40
|
+
|
|
41
|
+
# The heading grammar CLAUDE.md fixes for the root changelog and both per-package
|
|
42
|
+
# files follow — an EM DASH, and a date. Matched with `\b` after the version so a
|
|
43
|
+
# request to open `0.0.1b1` is not satisfied by an existing `0.0.1b10`.
|
|
44
|
+
_HEADING = "## {version} — {date}"
|
|
45
|
+
|
|
46
|
+
#: What lands under the new heading. Non-empty by construction (see the module
|
|
47
|
+
#: docstring), and it says who wrote it so the next reader does not go looking for
|
|
48
|
+
#: the human who did not.
|
|
49
|
+
_STUB = """Open for the next release. `{published}` published on {date} and the `bump` job
|
|
50
|
+
moved the version here automatically; nothing has landed against `{version}` yet.
|
|
51
|
+
Add entries as changes merge — this section becomes the GitHub Release body when
|
|
52
|
+
it ships.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ChangelogError(Exception):
|
|
57
|
+
"""A message a maintainer can act on, printed without a traceback."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def has_section(text: str, version: str) -> bool:
|
|
61
|
+
return re.search(rf"^##[ \t]+{re.escape(version)}\b", text, re.MULTILINE) is not None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def open_section(text: str, version: str, published: str, date: str) -> str:
|
|
65
|
+
"""`text` with a stub section for `version` above the topmost existing one."""
|
|
66
|
+
if has_section(text, version):
|
|
67
|
+
return text
|
|
68
|
+
|
|
69
|
+
block = _HEADING.format(version=version, date=date) + "\n\n" + _STUB.format(
|
|
70
|
+
published=published, version=version, date=date
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Above the newest existing section, never at the end: the file is
|
|
74
|
+
# reverse-chronological, and its preamble (title, and the grammar note the
|
|
75
|
+
# extractor depends on) sits above the first heading and must stay there.
|
|
76
|
+
first = re.search(r"^##[ \t]", text, re.MULTILINE)
|
|
77
|
+
if first:
|
|
78
|
+
return text[: first.start()] + block + "\n" + text[first.start() :]
|
|
79
|
+
|
|
80
|
+
# No sections yet — the first release off a fresh file. Keep the preamble and
|
|
81
|
+
# append, with exactly one blank line between.
|
|
82
|
+
return text.rstrip("\n") + "\n\n" + block
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main(argv: list[str]) -> int:
|
|
86
|
+
if len(argv) < 4:
|
|
87
|
+
print(__doc__ or "", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
path, version, published = argv[1], argv[2], argv[3]
|
|
90
|
+
date = argv[4] if len(argv) >= 5 else datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
91
|
+
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
|
|
92
|
+
raise ChangelogError(f"'{date}' is not a YYYY-MM-DD date")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
with open(path, encoding="utf-8") as handle:
|
|
96
|
+
text = handle.read()
|
|
97
|
+
except OSError as exc:
|
|
98
|
+
raise ChangelogError(f"cannot read {path}: {exc}") from exc
|
|
99
|
+
|
|
100
|
+
if has_section(text, version):
|
|
101
|
+
print(f"{path} already has a '## {version}' section — nothing to open.")
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
105
|
+
handle.write(open_section(text, version, published, date))
|
|
106
|
+
print(f"opened '## {version} — {date}' in {path}")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
try:
|
|
112
|
+
raise SystemExit(main(sys.argv))
|
|
113
|
+
except ChangelogError as error:
|
|
114
|
+
print(f"error: {error}", file=sys.stderr)
|
|
115
|
+
raise SystemExit(1) from None
|
package/src/audit/cli.ts
CHANGED
|
@@ -35,6 +35,10 @@ import { getInstanceId } from "../../lib/telemetry-id";
|
|
|
35
35
|
import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
|
|
36
36
|
import { openWhenReady } from "./open-browser";
|
|
37
37
|
import { describeOutcome, reportHarm } from "./report-harm";
|
|
38
|
+
import { notifyDesktop } from "./desktop-notify";
|
|
39
|
+
import { macNotifierInstalled, pruneMacNotifyQueue, queueMacNotification } from "./macos-notifier";
|
|
40
|
+
import { markLeakNoticeDelivered } from "./leak-notice";
|
|
41
|
+
import { readConfig } from "../hooks/fp-config";
|
|
38
42
|
import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM, helpScreen, helpOptsFor } from "../hooks/tui";
|
|
39
43
|
import { version } from "../../package.json";
|
|
40
44
|
|
|
@@ -106,6 +110,12 @@ export function helpText(): string {
|
|
|
106
110
|
// is not a flag anybody can read or copy.
|
|
107
111
|
["--email <address>", "With --schedule, skips the sign-in prompt."],
|
|
108
112
|
["--no-schedule", "Stop the timer. Leaves you signed in."],
|
|
113
|
+
// Its own pair of flags rather than a clause on --schedule: whether
|
|
114
|
+
// this machine scans and whether it may interrupt you are separate
|
|
115
|
+
// decisions, and nobody silencing a banner should have to re-state
|
|
116
|
+
// their schedule to do it.
|
|
117
|
+
["--notify", "Show a desktop notification when a scan finds a credential. On by default."],
|
|
118
|
+
["--no-notify", "Stop those notifications. Scans and the digest are unaffected."],
|
|
109
119
|
["--status", "Whether scheduling is on, where reports go, the daemon's state, and when the next scan is due."],
|
|
110
120
|
["-h, --help", "Show this help."],
|
|
111
121
|
],
|
|
@@ -334,6 +344,115 @@ function heldByLine(held: AuditLockInfo | null): string {
|
|
|
334
344
|
return `another audit is already running (pid ${held.pid}, started by ${held.source} ${ageS}s ago)`;
|
|
335
345
|
}
|
|
336
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Raise a desktop banner for credentials this scan found and nothing has
|
|
349
|
+
* announced yet.
|
|
350
|
+
*
|
|
351
|
+
* Scheduled runs only, and the reason is the same one that keeps `reportHarm`
|
|
352
|
+
* here: an interactive `failproofai audit` prints its findings to a person who
|
|
353
|
+
* is already looking at them, so a banner on top is noise. The scheduled run is
|
|
354
|
+
* the one with nobody watching, which is exactly when a notification is the
|
|
355
|
+
* only thing that reaches anyone.
|
|
356
|
+
*
|
|
357
|
+
* Every failure is swallowed on purpose. This is an announcement about a
|
|
358
|
+
* completed scan, not part of the scan: a machine with no desktop, a locked
|
|
359
|
+
* session, a bus that hung — none of those make the audit itself a failure, and
|
|
360
|
+
* turning a good scan into exit 1 would make the scheduler back off from
|
|
361
|
+
* running the thing that actually matters.
|
|
362
|
+
*/
|
|
363
|
+
async function announceLeaksOnDesktop(result: AuditResult): Promise<void> {
|
|
364
|
+
try {
|
|
365
|
+
await announceLeaksOrThrow(result);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
// The catch is not decoration. `notifyDesktop` documents that it never
|
|
368
|
+
// throws, but it is doing socket I/O, and so are the config read and the
|
|
369
|
+
// marker write beside it — depending on three modules' promises to keep a
|
|
370
|
+
// completed scan from reporting failure is a dependency this does not need
|
|
371
|
+
// to have.
|
|
372
|
+
process.stderr.write(
|
|
373
|
+
`failproofai: could not announce a leak on the desktop: ` +
|
|
374
|
+
`${err instanceof Error ? err.message : String(err)}\n`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function announceLeaksOrThrow(result: AuditResult): Promise<void> {
|
|
380
|
+
const ids = result.newLeakIds ?? [];
|
|
381
|
+
if (ids.length === 0) return;
|
|
382
|
+
// Read at fire time, not at scan time: the switch answers "should this
|
|
383
|
+
// interrupt me", and it is the user's most recent answer that counts.
|
|
384
|
+
if (!readConfig().audit.notify) return;
|
|
385
|
+
|
|
386
|
+
// Claimed BEFORE the call goes out, and in the desktop channel only. Before,
|
|
387
|
+
// because a process that dies mid-notify would otherwise re-announce the same
|
|
388
|
+
// finding on every scheduled run forever; desktop-only, because a banner the
|
|
389
|
+
// user may never have seen must not also silence the in-session notice.
|
|
390
|
+
const claimed = markLeakNoticeDelivered(ids, undefined, "desktop");
|
|
391
|
+
if (claimed.length === 0) return;
|
|
392
|
+
|
|
393
|
+
const n = claimed.length === 1 ? "a credential" : `${claimed.length} credentials`;
|
|
394
|
+
const title = "failproofai found a leaked credential";
|
|
395
|
+
// Fixed text with a count interpolated — the same rule as the in-CLI notice,
|
|
396
|
+
// for the same reason. A finding's own text comes from a repository this
|
|
397
|
+
// machine cloned, and notification bodies render markup on several Linux
|
|
398
|
+
// desktops.
|
|
399
|
+
//
|
|
400
|
+
// The second sentence is the only place most users are ever offered the
|
|
401
|
+
// digest. The scan is local and needs no account, so nothing else in the
|
|
402
|
+
// product has a reason to ask for an address — which is exactly why the
|
|
403
|
+
// audit's findings have historically reached nobody. A banner someone is
|
|
404
|
+
// already reading, about a key of their own, is the one moment the offer is
|
|
405
|
+
// worth anything.
|
|
406
|
+
//
|
|
407
|
+
// No action buttons, deliberately. `Notify` supports them, but a server
|
|
408
|
+
// delivers the click back as an `ActionInvoked` signal to the sender — and
|
|
409
|
+
// this process exits as soon as the scan finishes, so the button would be
|
|
410
|
+
// dead. Two plain commands the user can copy beat one button that does
|
|
411
|
+
// nothing.
|
|
412
|
+
const body =
|
|
413
|
+
`${n} in your agent transcripts. Run failproofai audit to see them — ` +
|
|
414
|
+
"or failproofai audit --schedule to get them by email.";
|
|
415
|
+
|
|
416
|
+
// The two platforms need opposite things, and the split is not cosmetic. On
|
|
417
|
+
// Linux this process can reach the session bus itself. On macOS it cannot
|
|
418
|
+
// reach Notification Center at all — it is a child of a LaunchDaemon, outside
|
|
419
|
+
// the GUI session — so it hands the message to an agent that lives inside
|
|
420
|
+
// one. See `macos-notifier.ts` for why that agent exists.
|
|
421
|
+
if (process.platform === "darwin") {
|
|
422
|
+
pruneMacNotifyQueue();
|
|
423
|
+
if (!queueMacNotification(claimed[0], title, body)) {
|
|
424
|
+
process.stderr.write(`failproofai: found ${n} but could not queue a notification\n`);
|
|
425
|
+
} else if (!macNotifierInstalled()) {
|
|
426
|
+
// Queued, but nothing will collect it: setup either never ran or could
|
|
427
|
+
// not compile the applet. Worth a line, because the symptom otherwise is
|
|
428
|
+
// simply silence.
|
|
429
|
+
process.stderr.write(
|
|
430
|
+
`failproofai: found ${n}; the notifier is not installed, so nothing will show it ` +
|
|
431
|
+
`(run \`failproofai config\`)\n`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const outcome = await notifyDesktop(
|
|
438
|
+
title,
|
|
439
|
+
body,
|
|
440
|
+
// A stable id would let a repeat scan replace the previous bubble instead
|
|
441
|
+
// of stacking. We do not have one yet — the server assigns it and we would
|
|
442
|
+
// have to persist it — so a fresh notification is correct here, and it is
|
|
443
|
+
// bounded: at most one per distinct credential, ever.
|
|
444
|
+
0,
|
|
445
|
+
);
|
|
446
|
+
if (!outcome.ok) {
|
|
447
|
+
// stderr, so it lands in the journal beside the run it belongs to. Not a
|
|
448
|
+
// failure of the audit — but "we found a key and could not tell you" is
|
|
449
|
+
// exactly the sentence someone debugging this needs to find.
|
|
450
|
+
process.stderr.write(
|
|
451
|
+
`failproofai: found ${n} but could not raise a desktop notification (${outcome.reason})\n`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
337
456
|
// ── Headless (scheduled) audit ───────────────────────────────────────────────
|
|
338
457
|
|
|
339
458
|
/**
|
|
@@ -404,6 +523,11 @@ export async function runScheduledAudit(): Promise<number> {
|
|
|
404
523
|
`${num(result.transcripts.scanned)} sessions, ${num(result.totals.hits)} hits\n`,
|
|
405
524
|
);
|
|
406
525
|
|
|
526
|
+
// Tell whoever owns this machine, on the machine, before anything is sent
|
|
527
|
+
// anywhere. Local first: this is the channel that works with no account, no
|
|
528
|
+
// email and no network, which describes most users.
|
|
529
|
+
await announceLeaksOnDesktop(result);
|
|
530
|
+
|
|
407
531
|
// Report harmful findings upstream, if the user switched emailed reports on.
|
|
408
532
|
//
|
|
409
533
|
// AFTER the dashboard cache is written and AFTER the success line, because
|
|
@@ -552,6 +676,16 @@ export async function runAuditCli(args: string[]): Promise<void> {
|
|
|
552
676
|
process.exit(0);
|
|
553
677
|
}
|
|
554
678
|
|
|
679
|
+
for (const [flag, enable] of [["--notify", true], ["--no-notify", false]] as const) {
|
|
680
|
+
if (args.includes(flag)) {
|
|
681
|
+
const extra = args.find((a) => a !== flag);
|
|
682
|
+
if (extra) die(`\`audit ${flag}\` takes no other arguments (got: ${extra}).`);
|
|
683
|
+
const { runNotifyToggle } = await import("./schedule-cli");
|
|
684
|
+
runNotifyToggle(enable);
|
|
685
|
+
process.exit(0);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
555
689
|
if (args.includes("--no-schedule")) {
|
|
556
690
|
const extra = args.find((a) => a !== "--no-schedule");
|
|
557
691
|
if (extra) die(`\`audit --no-schedule\` takes no other arguments (got: ${extra}).`);
|