failproofai 1.0.0 → 1.0.1-beta.0
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/app-path-routes-manifest.json +0 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +0 -6
- 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 +4 -3
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -3
- 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 +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/run/route.js +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -3
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +6 -5
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +5 -4
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +5 -4
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +6 -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/app-paths-manifest.json +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.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_0jx6cp6._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0f-x2b0._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1f290jp._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0hwvl7e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1g2ji1t._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1ctjdzl._.js} +1 -1
- 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 +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0v5_91z._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
- 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 +4 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/00q6cfzuzpc85.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
- package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
- package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
- package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
- package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
- package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
- package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
- package/.next/standalone/app/api/auth/status/route.ts +7 -17
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
- package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/audit-styles.css +150 -26
- package/.next/standalone/app/globals.css +39 -1
- package/.next/standalone/app/settings/page.tsx +38 -21
- package/.next/standalone/app/settings/settings-client.tsx +583 -392
- package/.next/standalone/app/settings/settings.css +354 -0
- package/.next/standalone/components/navbar.tsx +33 -4
- package/.next/standalone/lib/auth/api-server-client.ts +51 -29
- package/.next/standalone/lib/auth/auth-store.ts +44 -59
- package/.next/standalone/package.json +12 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +6 -0
- package/bin/failproofaid-shim.mjs +0 -0
- package/dist/cli.mjs +1889 -426
- package/dist/worker.mjs +27 -5
- package/lib/auth/api-server-client.ts +51 -29
- package/lib/auth/auth-store.ts +44 -59
- package/package.json +12 -10
- package/scripts/docs-audit.ts +493 -0
- package/scripts/translate-docs/cli.ts +16 -2
- package/scripts/translate-docs/mdx-translator.ts +6 -1
- package/scripts/translate-docs/readme-translator.ts +4 -2
- package/src/audit/cli-login.ts +330 -0
- package/src/audit/cli.ts +124 -6
- package/src/audit/dashboard-cache.ts +44 -8
- package/src/audit/harm-report.ts +260 -0
- package/src/audit/machine-store.ts +120 -0
- package/src/audit/redact-example.ts +312 -0
- package/src/audit/report-harm.ts +167 -0
- package/src/audit/schedule-cli.ts +352 -0
- package/src/hooks/builtin-policies.ts +26 -0
- package/src/hooks/daemon-service.ts +76 -0
- package/src/hooks/fp-config.ts +96 -5
- package/src/hooks/fp-home.ts +118 -8
- package/src/hooks/fp-reset.ts +79 -9
- package/src/hooks/migrations.ts +241 -4
- package/src/hooks/tui.ts +61 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
- package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
- package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
- package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
- package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
- package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
- package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
- package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
- package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an audit result into a harm report the api-server can act on.
|
|
3
|
+
*
|
|
4
|
+
* Runs only after a SCHEDULED scan (`failproofai audit --scheduled`), only when
|
|
5
|
+
* the user has switched emailed reports on, and only ever from the audit child —
|
|
6
|
+
* never the daemon, which holds no human credential precisely so that refresh
|
|
7
|
+
* rotation stays inside the audit lock. See `crates/failproofaid/src/audit_lane.rs`.
|
|
8
|
+
*
|
|
9
|
+
* ## What counts as harm
|
|
10
|
+
*
|
|
11
|
+
* The policies the engine would have BLOCKED, plus the ones that caught a secret
|
|
12
|
+
* on its way into the model's context. In terms of `severityForBuiltin`, that is
|
|
13
|
+
* `deny` and `sanitize` — `block-*` and `sanitize-*` — and NOT `warn-`,
|
|
14
|
+
* `prefer-` or `require-`, which are hygiene.
|
|
15
|
+
*
|
|
16
|
+
* One name is added by hand, and it is worth explaining rather than hiding:
|
|
17
|
+
* `severityForBuiltin` derives severity from the NAME PREFIX, so
|
|
18
|
+
* `protect-env-vars` reads as `warn` despite being a policy that blocks `env` /
|
|
19
|
+
* `printenv` outright. Its whole subject is an agent reaching for the
|
|
20
|
+
* environment, which is the "read my keys" case this feature exists to report.
|
|
21
|
+
* Inheriting a scoring heuristic's blind spot into a security digest would be
|
|
22
|
+
* the wrong kind of consistency.
|
|
23
|
+
*
|
|
24
|
+
* ## The window, and the trap in `--since`
|
|
25
|
+
*
|
|
26
|
+
* `RunAuditOptions.since` filters on transcript MTIME, and that is right for
|
|
27
|
+
* what it does — it decides which files to open. It is WRONG as a window for
|
|
28
|
+
* this: a session left open for a month has a fresh mtime, so `--since 7d`
|
|
29
|
+
* hands back that whole transcript including month-old events, and the first
|
|
30
|
+
* digest would report everything the agent has ever done as though it happened
|
|
31
|
+
* this week.
|
|
32
|
+
*
|
|
33
|
+
* So the window is applied HERE, per event, against the timestamps `AuditCount`
|
|
34
|
+
* already carries — `lastSeen` to decide whether a policy fired in the window at
|
|
35
|
+
* all, and each example's own `timestamp` to decide which examples belong to it.
|
|
36
|
+
* The scan itself stays unfiltered.
|
|
37
|
+
*
|
|
38
|
+
* ## Counts are approximate; the window boundary is not
|
|
39
|
+
*
|
|
40
|
+
* `AuditCount.hits` is a total over everything scanned, and there is no
|
|
41
|
+
* per-event breakdown to subtract from it — the cache stores counts, not event
|
|
42
|
+
* lists. Rather than report a total that spans the wrong period, a policy whose
|
|
43
|
+
* activity straddles the window boundary reports the number of EXAMPLES that
|
|
44
|
+
* fall inside it, which is a real count of real events even though it is capped
|
|
45
|
+
* at three. A policy entirely inside the window reports its true total. The
|
|
46
|
+
* server's threshold reads these, so undercounting is the safe direction: it can
|
|
47
|
+
* delay a digest, never invent one.
|
|
48
|
+
*/
|
|
49
|
+
import type { AuditCount, AuditResult } from "./types";
|
|
50
|
+
import { redactExample } from "./redact-example";
|
|
51
|
+
|
|
52
|
+
/** Severities that mean "the engine would have stopped this". */
|
|
53
|
+
const HARMFUL_SEVERITIES = new Set(["deny", "sanitize"]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Policies whose severity misreads their intent. See the module docs.
|
|
57
|
+
*
|
|
58
|
+
* Kept as an explicit list rather than by rewriting `severityForBuiltin`,
|
|
59
|
+
* because that function feeds the SCORE's gentle/medium buckets and changing it
|
|
60
|
+
* would silently move every historical score.
|
|
61
|
+
*/
|
|
62
|
+
const ALSO_HARMFUL = new Set(["protect-env-vars"]);
|
|
63
|
+
|
|
64
|
+
/** One policy's harmful activity inside the window, as the wire expects it. */
|
|
65
|
+
export interface ReportedPolicy {
|
|
66
|
+
policy: string;
|
|
67
|
+
category: string;
|
|
68
|
+
title: string;
|
|
69
|
+
hits: number;
|
|
70
|
+
first_seen?: string;
|
|
71
|
+
last_seen?: string;
|
|
72
|
+
examples: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface HarmReport {
|
|
76
|
+
window_from?: string;
|
|
77
|
+
window_to: string;
|
|
78
|
+
harmful: ReportedPolicy[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `failproofai/block-rm-rf` → `block-rm-rf`. */
|
|
82
|
+
function shortName(name: string): string {
|
|
83
|
+
const slash = name.indexOf("/");
|
|
84
|
+
return slash === -1 ? name : name.slice(slash + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isHarmful(count: AuditCount): boolean {
|
|
88
|
+
if (count.source !== "builtin") return false;
|
|
89
|
+
const short = shortName(count.name);
|
|
90
|
+
return HARMFUL_SEVERITIES.has(count.severity) || ALSO_HARMFUL.has(short);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Parse an ISO timestamp, or null if it is absent or unusable. */
|
|
94
|
+
function ts(value: string | undefined): number | null {
|
|
95
|
+
if (!value) return null;
|
|
96
|
+
const n = Date.parse(value);
|
|
97
|
+
return Number.isFinite(n) ? n : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Select the harmful policies whose activity falls inside `[from, to]`.
|
|
102
|
+
*
|
|
103
|
+
* `from` undefined means "everything up to `to`", which is now only reachable
|
|
104
|
+
* by an explicit caller — `buildHarmReport` always supplies a bound. See the
|
|
105
|
+
* note there for why.
|
|
106
|
+
*
|
|
107
|
+
* `includeUnplaceable` decides what happens to a policy with NO usable
|
|
108
|
+
* timestamps. It cannot be placed, and the two failure directions are not
|
|
109
|
+
* equal: on a first report, dropping it loses a real finding; on a later one,
|
|
110
|
+
* including it re-reports something already covered. Silence about something
|
|
111
|
+
* new is the worse of the two and repetition is merely annoying, so each window
|
|
112
|
+
* gets the answer that fails the way it can afford to.
|
|
113
|
+
*/
|
|
114
|
+
export function selectHarmful(
|
|
115
|
+
result: AuditResult,
|
|
116
|
+
from: Date | undefined,
|
|
117
|
+
to: Date,
|
|
118
|
+
opts: { includeUnplaceable?: boolean } = {},
|
|
119
|
+
): ReportedPolicy[] {
|
|
120
|
+
const includeUnplaceable = opts.includeUnplaceable ?? from === undefined;
|
|
121
|
+
const fromMs = from ? from.getTime() : null;
|
|
122
|
+
const toMs = to.getTime();
|
|
123
|
+
const out: ReportedPolicy[] = [];
|
|
124
|
+
|
|
125
|
+
for (const count of result.results) {
|
|
126
|
+
if (!isHarmful(count)) continue;
|
|
127
|
+
|
|
128
|
+
const last = ts(count.lastSeen);
|
|
129
|
+
const first = ts(count.firstSeen);
|
|
130
|
+
|
|
131
|
+
// Nothing since the watermark — this policy's whole history predates the
|
|
132
|
+
// window.
|
|
133
|
+
if (fromMs !== null && last !== null && last <= fromMs) continue;
|
|
134
|
+
// Fired entirely after the window closed (a clock skew, or a scan that
|
|
135
|
+
// raced an event). It belongs to the next report, not this one.
|
|
136
|
+
if (first !== null && first > toMs) continue;
|
|
137
|
+
|
|
138
|
+
const inWindow = count.examples.filter((e) => {
|
|
139
|
+
const at = ts(e.timestamp);
|
|
140
|
+
if (at === null) return includeUnplaceable;
|
|
141
|
+
if (fromMs !== null && at <= fromMs) return false;
|
|
142
|
+
return at <= toMs;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const unplaceable = last === null && first === null;
|
|
146
|
+
if (unplaceable && !includeUnplaceable) continue;
|
|
147
|
+
|
|
148
|
+
// Wholly inside the window → the real total. Straddling EITHER edge → the
|
|
149
|
+
// examples that actually fall inside, which undercounts but never invents.
|
|
150
|
+
//
|
|
151
|
+
// Both edges, and the upper one is not symmetry for its own sake. This used
|
|
152
|
+
// to test the lower bound alone, so a policy that started inside the window
|
|
153
|
+
// and was still firing after it closed reported `count.hits` — every hit,
|
|
154
|
+
// including the ones after `to`, while its examples were filtered to the
|
|
155
|
+
// window. Those hits then fell inside the NEXT report's window too, since
|
|
156
|
+
// the watermark advances to `to`, and were counted a second time. A digest
|
|
157
|
+
// that reports tomorrow's findings today and again tomorrow is worse than
|
|
158
|
+
// one that is late.
|
|
159
|
+
//
|
|
160
|
+
// An UNPLACEABLE policy that survived the check above reports its full
|
|
161
|
+
// count: there is nothing to narrow it with, and having decided to include
|
|
162
|
+
// it, reporting zero would be a row claiming nothing happened. It is only
|
|
163
|
+
// reachable on a first report, where over-reporting is the direction that
|
|
164
|
+
// was chosen deliberately.
|
|
165
|
+
const afterLowerEdge = fromMs === null || (first !== null && first > fromMs);
|
|
166
|
+
const beforeUpperEdge = last !== null && last <= toMs;
|
|
167
|
+
const wholly = unplaceable || (afterLowerEdge && beforeUpperEdge);
|
|
168
|
+
// A straddling policy falls back to its in-window EXAMPLES, and the audit
|
|
169
|
+
// keeps at most three of them per policy, chosen in whatever order the
|
|
170
|
+
// transcripts happened to be walked. On a machine that has been running
|
|
171
|
+
// agents for months those three are routinely all old — so a policy that
|
|
172
|
+
// fired an hour ago scored zero and was dropped, and because `firstSeen`
|
|
173
|
+
// stays before the watermark forever, it was dropped from every later report
|
|
174
|
+
// too. Not a delayed digest: a feature that goes quiet on exactly the
|
|
175
|
+
// machines with the most to report.
|
|
176
|
+
//
|
|
177
|
+
// `beforeUpperEdge` having survived the `last <= fromMs` skip above means
|
|
178
|
+
// `lastSeen` itself sits inside the window, and that timestamp IS a real
|
|
179
|
+
// event. One is the floor it proves, which keeps the "never invent a hit"
|
|
180
|
+
// rule intact while making the row exist.
|
|
181
|
+
const floor = beforeUpperEdge ? 1 : 0;
|
|
182
|
+
const hits = wholly ? count.hits : Math.max(inWindow.length, floor);
|
|
183
|
+
if (hits <= 0) continue;
|
|
184
|
+
|
|
185
|
+
out.push({
|
|
186
|
+
policy: shortName(count.name),
|
|
187
|
+
category: count.category,
|
|
188
|
+
title: count.displayTitle ?? "",
|
|
189
|
+
hits,
|
|
190
|
+
first_seen: count.firstSeen,
|
|
191
|
+
last_seen: count.lastSeen,
|
|
192
|
+
examples: inWindow.map((e) => redactExample(e.example)).filter((e) => e.length > 0),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Most active first, so a digest truncated by anything downstream keeps the
|
|
197
|
+
// rows that matter.
|
|
198
|
+
out.sort((a, b) => b.hits - a.hits);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build the report body for one scan.
|
|
204
|
+
*
|
|
205
|
+
* `window_to` is the scan's own `scannedAt` rather than "now": it is the instant
|
|
206
|
+
* the evidence was gathered, and using a later clock reading would advance the
|
|
207
|
+
* watermark past events that happened while the scan was still running — events
|
|
208
|
+
* no report would ever cover.
|
|
209
|
+
*
|
|
210
|
+
* ## A first report is bounded to one interval, not to all of history
|
|
211
|
+
*
|
|
212
|
+
* With no watermark the obvious window is "everything", and that is what this
|
|
213
|
+
* did until it was run against a real machine: the first report covered 230
|
|
214
|
+
* sessions and 22,059 tool calls and came out at **5,815 findings**. Every
|
|
215
|
+
* number in it was true and the digest was still wrong — somebody's first email
|
|
216
|
+
* would describe their agent's entire recorded history as though it were this
|
|
217
|
+
* week's news, and would trip the critical bypass on day one for essentially
|
|
218
|
+
* everyone.
|
|
219
|
+
*
|
|
220
|
+
* A digest is a statement about RECENT behaviour, so the first one covers the
|
|
221
|
+
* same period every later one does: `interval_days` back from the scan. The
|
|
222
|
+
* older findings are not lost, they are simply not news — they are on the
|
|
223
|
+
* dashboard, which is where a full history belongs.
|
|
224
|
+
*
|
|
225
|
+
* `includeUnplaceable` still follows "is this the first report", not "is there a
|
|
226
|
+
* lower bound", so a policy carrying no usable timestamps is reported once on a
|
|
227
|
+
* new machine rather than silently dropped by the bound this now always sets.
|
|
228
|
+
*/
|
|
229
|
+
export function buildHarmReport(
|
|
230
|
+
result: AuditResult,
|
|
231
|
+
lastReportedAt: string | undefined,
|
|
232
|
+
intervalDays: number,
|
|
233
|
+
): HarmReport {
|
|
234
|
+
const to = new Date(Date.parse(result.scannedAt));
|
|
235
|
+
const windowTo = Number.isFinite(to.getTime()) ? to : new Date();
|
|
236
|
+
const watermark = ts(lastReportedAt);
|
|
237
|
+
const isFirstReport = watermark === null;
|
|
238
|
+
|
|
239
|
+
const oneInterval = Math.max(1, intervalDays) * 86_400_000;
|
|
240
|
+
const fallbackFrom = windowTo.getTime() - oneInterval;
|
|
241
|
+
|
|
242
|
+
// The watermark is the SERVER's clock; `windowTo` is this machine's. A
|
|
243
|
+
// backwards jump between them — NTP correcting a fast RTC, a VM restored from
|
|
244
|
+
// a snapshot, a dual-boot machine that wrote localtime to the hardware clock
|
|
245
|
+
// — leaves `from` LATER than `to`, and `selectHarmful` then matches nothing
|
|
246
|
+
// at all. That drops every finding silently and permanently: the watermark
|
|
247
|
+
// only ever moves forward, so the window never re-opens, while the run's
|
|
248
|
+
// outcome line still reads normal. The scheduling lane already repairs this
|
|
249
|
+
// class of jump; the reporting half did not. One interval back is a digest
|
|
250
|
+
// that is narrower than it should be, rather than one that is empty forever.
|
|
251
|
+
const from = new Date(
|
|
252
|
+
isFirstReport || watermark >= windowTo.getTime() ? fallbackFrom : watermark,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
window_from: from.toISOString(),
|
|
257
|
+
window_to: windowTo.toISOString(),
|
|
258
|
+
harmful: selectHarmful(result, from, windowTo, { includeUnplaceable: isFirstReport }),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.failproofai/audit/machine.json` — this machine's report identity.
|
|
3
|
+
*
|
|
4
|
+
* Two fields, and they are together because they share one property: both must
|
|
5
|
+
* outlive a sign-out.
|
|
6
|
+
*
|
|
7
|
+
* - `machine_id` is what the api-server keys reports on. Regenerate it and the
|
|
8
|
+
* server sees a brand-new machine, which burns a slot off the account's cap
|
|
9
|
+
* on every logout and splits one box's history into two.
|
|
10
|
+
* - `last_reported_at` is how far the last digest reached. Reset it and the
|
|
11
|
+
* next report re-covers months of history, and the user gets a digest of
|
|
12
|
+
* everything that ever happened as though it just did.
|
|
13
|
+
*
|
|
14
|
+
* That is why this is a separate file from `session.json` rather than two more
|
|
15
|
+
* keys in it: signing out deletes the session, and neither of these may go with
|
|
16
|
+
* it. `HOME_CLASSES` classifies this `identity` — never deleted, alongside
|
|
17
|
+
* `cursors/` and the telemetry id — while the tokens beside it are `user-typed`
|
|
18
|
+
* and come and go.
|
|
19
|
+
*
|
|
20
|
+
* ## The id is minted here, not borrowed
|
|
21
|
+
*
|
|
22
|
+
* `state/telemetry-id` is already a stable per-machine random id and would have
|
|
23
|
+
* been free to reuse. It is deliberately not reused: that id is the anonymous
|
|
24
|
+
* PostHog person, and sending it alongside a verified email address would link
|
|
25
|
+
* the two the moment somebody turns emailed reports on. Opting into a digest
|
|
26
|
+
* should not de-anonymise telemetry, so this feature gets its own id and the
|
|
27
|
+
* two never meet.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
30
|
+
import { hostname } from "node:os";
|
|
31
|
+
import { randomUUID } from "node:crypto";
|
|
32
|
+
|
|
33
|
+
import { writeJsonAtomically } from "../../lib/atomic-write";
|
|
34
|
+
import { auditMachineFile } from "../hooks/fp-home";
|
|
35
|
+
|
|
36
|
+
export interface MachineIdentity {
|
|
37
|
+
/** Random, minted on first use. Opaque to the server. */
|
|
38
|
+
machine_id: string;
|
|
39
|
+
/** ISO-8601. Absent until the first digest is delivered. */
|
|
40
|
+
last_reported_at?: string;
|
|
41
|
+
/** When this id was minted. Diagnostics only. */
|
|
42
|
+
created_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readMachineIdentity(home?: string): MachineIdentity | null {
|
|
46
|
+
const path = auditMachineFile(home);
|
|
47
|
+
if (!existsSync(path)) return null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<MachineIdentity>;
|
|
50
|
+
if (typeof parsed.machine_id !== "string" || !parsed.machine_id) return null;
|
|
51
|
+
return {
|
|
52
|
+
machine_id: parsed.machine_id,
|
|
53
|
+
last_reported_at:
|
|
54
|
+
typeof parsed.last_reported_at === "string" ? parsed.last_reported_at : undefined,
|
|
55
|
+
created_at: typeof parsed.created_at === "string" ? parsed.created_at : new Date(0).toISOString(),
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
// Absent, unreadable and malformed all read as "no identity yet". The caller
|
|
59
|
+
// mints a new one, which costs a slot off the cap and a re-covered window —
|
|
60
|
+
// bad, but recoverable, and strictly better than refusing to report at all
|
|
61
|
+
// because one file got truncated.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read the identity, creating it on first call.
|
|
68
|
+
*
|
|
69
|
+
* Only ever called from the reporting path, so a machine that never opts into
|
|
70
|
+
* emailed reports never gets an id at all — there is nothing to mint one for.
|
|
71
|
+
*/
|
|
72
|
+
export function ensureMachineIdentity(home?: string): MachineIdentity {
|
|
73
|
+
const existing = readMachineIdentity(home);
|
|
74
|
+
if (existing) return existing;
|
|
75
|
+
const fresh: MachineIdentity = {
|
|
76
|
+
machine_id: randomUUID(),
|
|
77
|
+
created_at: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
writeJsonAtomically(auditMachineFile(home), fresh);
|
|
80
|
+
return fresh;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Record how far the last DELIVERED digest reached.
|
|
85
|
+
*
|
|
86
|
+
* The value is the server's `next_window_from`, not the window this run
|
|
87
|
+
* scanned. The server is authoritative because it knows which reports actually
|
|
88
|
+
* produced an email — a report held by the cooldown, or one whose send failed,
|
|
89
|
+
* must not advance the watermark or its findings are silently dropped from every
|
|
90
|
+
* future digest.
|
|
91
|
+
*/
|
|
92
|
+
export function recordReportWatermark(nextWindowFrom: string, home?: string): void {
|
|
93
|
+
const current = ensureMachineIdentity(home);
|
|
94
|
+
writeJsonAtomically(auditMachineFile(home), {
|
|
95
|
+
...current,
|
|
96
|
+
last_reported_at: nextWindowFrom,
|
|
97
|
+
} satisfies MachineIdentity);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function deleteMachineIdentity(home?: string): void {
|
|
101
|
+
const path = auditMachineFile(home);
|
|
102
|
+
if (existsSync(path)) rmSync(path, { force: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A display name for this machine — its hostname.
|
|
107
|
+
*
|
|
108
|
+
* Shown in the digest so somebody with three boxes can tell which one is
|
|
109
|
+
* misbehaving, which is the whole reason it is sent. Falls back to `undefined`
|
|
110
|
+
* rather than a placeholder: the server keeps whatever label it already has when
|
|
111
|
+
* one is omitted, so guessing here would overwrite a good name with a bad one.
|
|
112
|
+
*/
|
|
113
|
+
export function machineLabel(): string | undefined {
|
|
114
|
+
try {
|
|
115
|
+
const h = hostname().trim();
|
|
116
|
+
return h.length > 0 ? h : undefined;
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|