failproofai 1.0.0-beta.2 → 1.0.0-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 +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- 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 +3 -3
- 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/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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/run/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +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-verify/route.js.nft.json +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/reminder/route.js.nft.json +1 -1
- 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 +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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +1 -1
- 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 +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- 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 +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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 +3 -3
- 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 +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 +3 -3
- 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 +1 -1
- 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 +3 -3
- 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/chunks/[externals]__0h9_83k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
- 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]__1zqz4v8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- 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/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_1q9xxca.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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 +32 -20
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
- package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
- package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
- package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/Cargo.lock +1992 -198
- package/.next/standalone/Cargo.toml +1 -1
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/policies/hooks-client.tsx +81 -6
- package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
- package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
- package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
- package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
- package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
- package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
- package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
- package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
- package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
- package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
- package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
- package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
- package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
- package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
- package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
- package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
- package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
- package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
- package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
- package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
- package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
- package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
- package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
- package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
- package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
- package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
- package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
- package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
- package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
- package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
- package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
- package/.next/standalone/package.json +15 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2255 -779
- package/dist/worker.mjs +365 -52
- package/package.json +15 -9
- package/scripts/build-daemon-packages.mjs +210 -0
- package/scripts/daemon-platforms.mjs +42 -0
- package/scripts/prune-standalone.mjs +6 -0
- package/scripts/publish-aliases.mjs +62 -41
- package/src/hooks/builtin-policies.ts +139 -0
- package/src/hooks/cloud-connection.ts +205 -0
- package/src/hooks/cloud-enrollment-cli.ts +227 -0
- package/src/hooks/cloud-enrollment.ts +173 -0
- package/src/hooks/cloud-managed-policies.ts +138 -0
- package/src/hooks/collector-config.ts +226 -0
- package/src/hooks/configure-wizard.ts +221 -5
- package/src/hooks/custom-hooks-loader.ts +69 -25
- package/src/hooks/daemon-client.ts +18 -0
- package/src/hooks/daemon-download.ts +137 -27
- package/src/hooks/daemon-service.ts +23 -8
- package/src/hooks/handler.ts +144 -6
- package/src/hooks/hook-activity-store.ts +50 -0
- package/src/hooks/loader-utils.ts +6 -4
- package/src/hooks/policy-types.ts +35 -0
- package/src/hooks/session-pause-cli.ts +157 -0
- package/src/hooks/session-pause.ts +236 -0
- package/src/hooks/tui.ts +115 -0
- package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
//! Fault-isolated host for every collector task.
|
|
2
|
+
//!
|
|
3
|
+
//! # Why this exists at all
|
|
4
|
+
//!
|
|
5
|
+
//! `failproofai` fails CLOSED. Once a machine is daemon-configured, the CLI
|
|
6
|
+
//! turns an unreachable daemon into a *deny* rather than a fallback — see
|
|
7
|
+
//! `bin/failproofai.mjs`. So anything that can take the daemon process down,
|
|
8
|
+
//! or wedge it, blocks every tool call across every supported agent CLI on
|
|
9
|
+
//! that machine.
|
|
10
|
+
//!
|
|
11
|
+
//! Collection is strictly lower-value than enforcement: shipping a session log
|
|
12
|
+
//! late, or not at all, is a gap in a dashboard. Denying every tool call is a
|
|
13
|
+
//! developer who cannot work. This module exists to make that asymmetry
|
|
14
|
+
//! structural rather than aspirational, so a bug in a transform for some
|
|
15
|
+
//! agent's log format can never become an outage.
|
|
16
|
+
//!
|
|
17
|
+
//! # The three guarantees
|
|
18
|
+
//!
|
|
19
|
+
//! 1. **Own thread, own runtime.** The collector never runs on the socket
|
|
20
|
+
//! server's threads. `failproofaid`'s accept loop stays blocking `std`,
|
|
21
|
+
//! unchanged — converting it would mean rewriting the connection handling
|
|
22
|
+
//! whose non-blocking/BSD-`accept` subtleties are the reason macOS worked
|
|
23
|
+
//! at all, and re-earning trust in the exact code path that gates tool
|
|
24
|
+
//! calls. Not worth it to host a background uploader.
|
|
25
|
+
//! 2. **A panic is contained and logged, never propagated.** Every task body
|
|
26
|
+
//! runs inside `catch_unwind`. A panicking task is restarted with backoff;
|
|
27
|
+
//! it does not unwind into the runtime, does not abort the process, and
|
|
28
|
+
//! does not mark the daemon unhealthy.
|
|
29
|
+
//! 3. **Shutdown is bounded.** The supervisor observes the same shutdown flag
|
|
30
|
+
//! the socket server does, and `join_with_flush` waits only a fixed budget
|
|
31
|
+
//! for tasks to drain before giving up on them. A collector task that hangs
|
|
32
|
+
//! delays process exit by that budget and no more.
|
|
33
|
+
//!
|
|
34
|
+
//! # What this deliberately does NOT do
|
|
35
|
+
//!
|
|
36
|
+
//! It does not restart forever at full speed, and it does not treat a task
|
|
37
|
+
//! that exits cleanly as a failure. A task returning `Ok(())` has finished its
|
|
38
|
+
//! work; only an `Err` or a panic is a restart, and each consecutive one backs
|
|
39
|
+
//! off further so a task that is broken rather than merely unlucky cannot spin
|
|
40
|
+
//! the CPU behind a user's back.
|
|
41
|
+
|
|
42
|
+
use std::future::Future;
|
|
43
|
+
use std::panic::AssertUnwindSafe;
|
|
44
|
+
use std::pin::Pin;
|
|
45
|
+
use std::sync::Arc;
|
|
46
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
47
|
+
use std::thread::JoinHandle;
|
|
48
|
+
use std::time::Duration;
|
|
49
|
+
|
|
50
|
+
/// How long `join_with_flush` waits for the collector thread after asking it
|
|
51
|
+
/// to stop. Long enough for an in-flight spool write to finish; short enough
|
|
52
|
+
/// that a wedged task cannot hold up a `systemctl stop` past its own timeout.
|
|
53
|
+
pub const DEFAULT_FLUSH_BUDGET: Duration = Duration::from_secs(5);
|
|
54
|
+
|
|
55
|
+
/// First restart delay after a task fails. Doubles per consecutive failure.
|
|
56
|
+
const BACKOFF_START: Duration = Duration::from_secs(1);
|
|
57
|
+
/// Ceiling for the restart delay. A permanently broken task settles here
|
|
58
|
+
/// rather than retrying tightly forever.
|
|
59
|
+
const BACKOFF_MAX: Duration = Duration::from_secs(60);
|
|
60
|
+
/// How often the supervisor re-checks the shutdown flag while idle or backing
|
|
61
|
+
/// off. Shutdown latency is bounded by this, so it stays well under the flush
|
|
62
|
+
/// budget.
|
|
63
|
+
const SHUTDOWN_POLL: Duration = Duration::from_millis(100);
|
|
64
|
+
|
|
65
|
+
/// Worker threads for the collector runtime. Two is deliberate: the workload
|
|
66
|
+
/// is IO-bound (file reads, HTTP), and every thread here is one the OS
|
|
67
|
+
/// schedules against the socket server's handler threads.
|
|
68
|
+
const RUNTIME_WORKER_THREADS: usize = 2;
|
|
69
|
+
|
|
70
|
+
type TaskFuture = Pin<Box<dyn Future<Output = Result<(), TaskError>> + Send>>;
|
|
71
|
+
type TaskFactory = Box<dyn Fn(Shutdown) -> TaskFuture + Send + Sync>;
|
|
72
|
+
|
|
73
|
+
/// A task's view of "should I stop?".
|
|
74
|
+
///
|
|
75
|
+
/// Every collector task is a poll loop, so the supervisor checking between
|
|
76
|
+
/// attempts is not enough on its own — a loop that never returns would only be
|
|
77
|
+
/// noticed once its flush budget expired, and it would be killed mid-iteration
|
|
78
|
+
/// rather than stopping at a safe point. Handing this to the task body lets it
|
|
79
|
+
/// exit at the top of a loop, after its cursor has been persisted.
|
|
80
|
+
///
|
|
81
|
+
/// Cheap to clone; both flags are shared. `daemon` is the socket server's own
|
|
82
|
+
/// flag, so a SIGTERM is observed here without a second signal path.
|
|
83
|
+
#[derive(Clone)]
|
|
84
|
+
pub struct Shutdown {
|
|
85
|
+
daemon: Arc<AtomicBool>,
|
|
86
|
+
collector: Arc<AtomicBool>,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
impl Shutdown {
|
|
90
|
+
/// Build one directly from a daemon flag, for tests and for callers that
|
|
91
|
+
/// drive a task outside the supervisor. Production code receives this from
|
|
92
|
+
/// the supervisor instead, which is why the fields stay private.
|
|
93
|
+
#[doc(hidden)]
|
|
94
|
+
pub fn for_test(daemon: Arc<AtomicBool>) -> Self {
|
|
95
|
+
Shutdown {
|
|
96
|
+
daemon,
|
|
97
|
+
collector: Arc::new(AtomicBool::new(false)),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// True once either the daemon or the collector has been asked to stop.
|
|
102
|
+
pub fn is_set(&self) -> bool {
|
|
103
|
+
self.daemon.load(Ordering::Relaxed) || self.collector.load(Ordering::Relaxed)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Sleep for `dur`, returning early if shutdown is requested.
|
|
107
|
+
///
|
|
108
|
+
/// Returns `true` if the full duration elapsed, `false` if it was cut
|
|
109
|
+
/// short. Poll loops should use this rather than `tokio::time::sleep` so a
|
|
110
|
+
/// 60-second poll interval does not become 60 seconds of shutdown latency.
|
|
111
|
+
pub async fn sleep(&self, dur: Duration) -> bool {
|
|
112
|
+
let deadline = tokio::time::Instant::now() + dur;
|
|
113
|
+
loop {
|
|
114
|
+
if self.is_set() {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
let now = tokio::time::Instant::now();
|
|
118
|
+
if now >= deadline {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
tokio::time::sleep(SHUTDOWN_POLL.min(deadline - now)).await;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Why a task stopped. Only used for logging and the restart decision — the
|
|
127
|
+
/// supervisor treats every failure the same way.
|
|
128
|
+
#[derive(Debug)]
|
|
129
|
+
pub struct TaskError(pub String);
|
|
130
|
+
|
|
131
|
+
impl std::fmt::Display for TaskError {
|
|
132
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
133
|
+
write!(f, "{}", self.0)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
impl std::error::Error for TaskError {}
|
|
138
|
+
|
|
139
|
+
impl From<String> for TaskError {
|
|
140
|
+
fn from(s: String) -> Self {
|
|
141
|
+
TaskError(s)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
impl From<&str> for TaskError {
|
|
146
|
+
fn from(s: &str) -> Self {
|
|
147
|
+
TaskError(s.to_string())
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// One supervised unit of collector work.
|
|
152
|
+
///
|
|
153
|
+
/// `factory` is called afresh for every attempt rather than the future being
|
|
154
|
+
/// built once, because a restarted task must start from a clean state — a
|
|
155
|
+
/// future that already failed cannot be polled again, and reusing one would
|
|
156
|
+
/// silently make "restart" mean "give up".
|
|
157
|
+
pub struct TaskSpec {
|
|
158
|
+
pub name: &'static str,
|
|
159
|
+
factory: TaskFactory,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
impl TaskSpec {
|
|
163
|
+
pub fn new<F, Fut>(name: &'static str, factory: F) -> Self
|
|
164
|
+
where
|
|
165
|
+
F: Fn(Shutdown) -> Fut + Send + Sync + 'static,
|
|
166
|
+
Fut: Future<Output = Result<(), TaskError>> + Send + 'static,
|
|
167
|
+
{
|
|
168
|
+
TaskSpec {
|
|
169
|
+
name,
|
|
170
|
+
factory: Box::new(move |sd| Box::pin(factory(sd))),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Counters a caller (or a test) can read without stopping the supervisor.
|
|
176
|
+
/// Public because the daemon's health record reports them: a task that is
|
|
177
|
+
/// quietly restarting in a loop must be visible, not merely absent.
|
|
178
|
+
#[derive(Debug, Default)]
|
|
179
|
+
pub struct SupervisorMetrics {
|
|
180
|
+
/// Times a task returned `Err`.
|
|
181
|
+
pub failures: AtomicUsize,
|
|
182
|
+
/// Times a task panicked. Tracked separately from `failures` because a
|
|
183
|
+
/// panic is a bug in the collector, while an `Err` is usually the
|
|
184
|
+
/// environment (a vanished directory, a refused connection).
|
|
185
|
+
pub panics: AtomicUsize,
|
|
186
|
+
/// Times a task was started, including the first.
|
|
187
|
+
pub starts: AtomicUsize,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Handle to a running collector. Dropping it does NOT stop the collector —
|
|
191
|
+
/// call [`CollectorHandle::join_with_flush`], so shutdown stays explicit and
|
|
192
|
+
/// ordered relative to the socket server's own teardown.
|
|
193
|
+
pub struct CollectorHandle {
|
|
194
|
+
thread: Option<JoinHandle<()>>,
|
|
195
|
+
stop: Arc<AtomicBool>,
|
|
196
|
+
metrics: Arc<SupervisorMetrics>,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
impl CollectorHandle {
|
|
200
|
+
pub fn metrics(&self) -> Arc<SupervisorMetrics> {
|
|
201
|
+
self.metrics.clone()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Ask every task to stop, then wait up to `budget` for the collector
|
|
205
|
+
/// thread to finish.
|
|
206
|
+
///
|
|
207
|
+
/// Returns `true` if it drained in time. On `false` the thread is
|
|
208
|
+
/// abandoned deliberately rather than blocked on: the process is already
|
|
209
|
+
/// exiting, and the alternative — waiting indefinitely on a task that has
|
|
210
|
+
/// wedged — is the hang this whole module exists to prevent.
|
|
211
|
+
pub fn join_with_flush(mut self, budget: Duration) -> bool {
|
|
212
|
+
self.stop.store(true, Ordering::Relaxed);
|
|
213
|
+
let Some(thread) = self.thread.take() else {
|
|
214
|
+
return true;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// `JoinHandle` has no timed join in std, so poll `is_finished` and
|
|
218
|
+
// only then join (which is guaranteed not to block once finished).
|
|
219
|
+
let deadline = std::time::Instant::now() + budget;
|
|
220
|
+
while std::time::Instant::now() < deadline {
|
|
221
|
+
if thread.is_finished() {
|
|
222
|
+
let _ = thread.join();
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
std::thread::sleep(SHUTDOWN_POLL);
|
|
226
|
+
}
|
|
227
|
+
tracing::warn!(
|
|
228
|
+
budget_ms = budget.as_millis() as u64,
|
|
229
|
+
"collector did not drain within its flush budget; abandoning it so process exit is not blocked"
|
|
230
|
+
);
|
|
231
|
+
false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Start the collector on its own thread with its own Tokio runtime.
|
|
236
|
+
///
|
|
237
|
+
/// `shutdown` is the daemon's existing flag, observed rather than owned, so a
|
|
238
|
+
/// SIGTERM stops the collector and the socket server through one signal path.
|
|
239
|
+
///
|
|
240
|
+
/// Returns `None` when there is nothing to run. That is the normal state on a
|
|
241
|
+
/// machine that has not opted in to collection: no thread is spawned, no
|
|
242
|
+
/// runtime is created, and the daemon behaves exactly as it does today.
|
|
243
|
+
pub fn spawn_supervised(
|
|
244
|
+
tasks: Vec<TaskSpec>,
|
|
245
|
+
shutdown: Arc<AtomicBool>,
|
|
246
|
+
) -> Option<CollectorHandle> {
|
|
247
|
+
if tasks.is_empty() {
|
|
248
|
+
tracing::debug!("collector has no enabled tasks; not starting a runtime");
|
|
249
|
+
return None;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
253
|
+
let metrics = Arc::new(SupervisorMetrics::default());
|
|
254
|
+
|
|
255
|
+
let thread_stop = stop.clone();
|
|
256
|
+
let thread_metrics = metrics.clone();
|
|
257
|
+
let thread = std::thread::Builder::new()
|
|
258
|
+
.name("fpai-collect".to_string())
|
|
259
|
+
.spawn(move || {
|
|
260
|
+
// Building the runtime can fail (thread/fd exhaustion). That must
|
|
261
|
+
// degrade to "no collection" and never to a panic on a thread the
|
|
262
|
+
// daemon does not join.
|
|
263
|
+
let runtime = match tokio::runtime::Builder::new_multi_thread()
|
|
264
|
+
.worker_threads(RUNTIME_WORKER_THREADS)
|
|
265
|
+
.enable_all()
|
|
266
|
+
.thread_name("fpai-collect-rt")
|
|
267
|
+
.build()
|
|
268
|
+
{
|
|
269
|
+
Ok(rt) => rt,
|
|
270
|
+
Err(err) => {
|
|
271
|
+
tracing::error!(%err, "collector runtime failed to start; collection is off for this process");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
runtime.block_on(run_all(tasks, shutdown, thread_stop, thread_metrics));
|
|
277
|
+
|
|
278
|
+
// Do not let a task that ignored shutdown hold the thread here:
|
|
279
|
+
// the caller's flush budget is the only bound we promise, and
|
|
280
|
+
// `Runtime::drop` would otherwise wait for every blocking task.
|
|
281
|
+
runtime.shutdown_timeout(Duration::from_secs(1));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
match thread {
|
|
285
|
+
Ok(thread) => Some(CollectorHandle {
|
|
286
|
+
thread: Some(thread),
|
|
287
|
+
stop,
|
|
288
|
+
metrics,
|
|
289
|
+
}),
|
|
290
|
+
Err(err) => {
|
|
291
|
+
tracing::error!(%err, "could not spawn the collector thread; collection is off for this process");
|
|
292
|
+
None
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Supervise every task concurrently until either shutdown flag is set.
|
|
298
|
+
async fn run_all(
|
|
299
|
+
tasks: Vec<TaskSpec>,
|
|
300
|
+
shutdown: Arc<AtomicBool>,
|
|
301
|
+
stop: Arc<AtomicBool>,
|
|
302
|
+
metrics: Arc<SupervisorMetrics>,
|
|
303
|
+
) {
|
|
304
|
+
let mut set = tokio::task::JoinSet::new();
|
|
305
|
+
let task_count = tasks.len();
|
|
306
|
+
for spec in tasks {
|
|
307
|
+
let shutdown = shutdown.clone();
|
|
308
|
+
let stop = stop.clone();
|
|
309
|
+
let metrics = metrics.clone();
|
|
310
|
+
set.spawn(async move { supervise_one(spec, shutdown, stop, metrics).await });
|
|
311
|
+
}
|
|
312
|
+
tracing::info!(tasks = task_count, "collector started");
|
|
313
|
+
|
|
314
|
+
// Every arm returns only on shutdown, so this drains rather than races.
|
|
315
|
+
while set.join_next().await.is_some() {}
|
|
316
|
+
tracing::info!("collector stopped");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Run one task, restarting it with backoff on failure, until shutdown.
|
|
320
|
+
async fn supervise_one(
|
|
321
|
+
spec: TaskSpec,
|
|
322
|
+
shutdown: Arc<AtomicBool>,
|
|
323
|
+
stop: Arc<AtomicBool>,
|
|
324
|
+
metrics: Arc<SupervisorMetrics>,
|
|
325
|
+
) {
|
|
326
|
+
let mut backoff = BACKOFF_START;
|
|
327
|
+
|
|
328
|
+
loop {
|
|
329
|
+
if is_stopping(&shutdown, &stop) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
metrics.starts.fetch_add(1, Ordering::Relaxed);
|
|
334
|
+
|
|
335
|
+
// `AssertUnwindSafe` is the honest choice rather than a papered-over
|
|
336
|
+
// one: a panic mid-task can leave whatever it was mutating in an
|
|
337
|
+
// arbitrary state. The factory builds a fresh future per attempt, so
|
|
338
|
+
// nothing observable survives the panic into the retry — anything
|
|
339
|
+
// durable lives on disk behind an atomic rename, and a torn in-memory
|
|
340
|
+
// buffer is discarded with the future that owned it.
|
|
341
|
+
let sd = Shutdown {
|
|
342
|
+
daemon: shutdown.clone(),
|
|
343
|
+
collector: stop.clone(),
|
|
344
|
+
};
|
|
345
|
+
let outcome = futures_catch_unwind(AssertUnwindSafe((spec.factory)(sd))).await;
|
|
346
|
+
|
|
347
|
+
match outcome {
|
|
348
|
+
Ok(Ok(())) => {
|
|
349
|
+
// Finished its work. Not a failure, so no backoff — but also
|
|
350
|
+
// nothing to restart, so stop supervising it.
|
|
351
|
+
tracing::debug!(task = spec.name, "collector task completed");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
Ok(Err(err)) => {
|
|
355
|
+
metrics.failures.fetch_add(1, Ordering::Relaxed);
|
|
356
|
+
tracing::warn!(
|
|
357
|
+
task = spec.name,
|
|
358
|
+
%err,
|
|
359
|
+
retry_in_ms = backoff.as_millis() as u64,
|
|
360
|
+
"collector task failed; restarting"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
Err(panic_msg) => {
|
|
364
|
+
metrics.panics.fetch_add(1, Ordering::Relaxed);
|
|
365
|
+
// Logged at error, not warn: an Err is usually the
|
|
366
|
+
// environment, a panic is always a bug worth chasing.
|
|
367
|
+
tracing::error!(
|
|
368
|
+
task = spec.name,
|
|
369
|
+
panic = %panic_msg,
|
|
370
|
+
retry_in_ms = backoff.as_millis() as u64,
|
|
371
|
+
"collector task PANICKED; contained and restarting. Enforcement is unaffected"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let sd = Shutdown {
|
|
377
|
+
daemon: shutdown.clone(),
|
|
378
|
+
collector: stop.clone(),
|
|
379
|
+
};
|
|
380
|
+
if !sd.sleep(backoff).await {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
backoff = (backoff * 2).min(BACKOFF_MAX);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
fn is_stopping(shutdown: &AtomicBool, stop: &AtomicBool) -> bool {
|
|
388
|
+
shutdown.load(Ordering::Relaxed) || stop.load(Ordering::Relaxed)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// Await `fut`, converting a panic into an `Err` carrying its message.
|
|
392
|
+
///
|
|
393
|
+
/// Hand-rolled rather than pulling in `futures` for one combinator: the
|
|
394
|
+
/// dependency budget for a binary that ships on four cross-compiled targets is
|
|
395
|
+
/// worth spending deliberately, and this is the only place the daemon needs it.
|
|
396
|
+
async fn futures_catch_unwind<F, T>(fut: AssertUnwindSafe<F>) -> Result<T, String>
|
|
397
|
+
where
|
|
398
|
+
F: Future<Output = T> + Send + 'static,
|
|
399
|
+
T: Send + 'static,
|
|
400
|
+
{
|
|
401
|
+
// A panic inside `poll` unwinds through `await`, so the guard has to wrap
|
|
402
|
+
// each poll rather than the whole future — which is exactly what awaiting
|
|
403
|
+
// inside a plain `catch_unwind` cannot do. Spawning gives us that
|
|
404
|
+
// per-poll boundary for free: Tokio already catches a panicking task and
|
|
405
|
+
// surfaces it as a `JoinError`. (`AssertUnwindSafe` implements `Future`
|
|
406
|
+
// itself, forwarding to the inner one, so it can be spawned directly.)
|
|
407
|
+
match tokio::task::spawn(fut).await {
|
|
408
|
+
Ok(v) => Ok(v),
|
|
409
|
+
Err(join_err) if join_err.is_panic() => {
|
|
410
|
+
let panic = join_err.into_panic();
|
|
411
|
+
let msg = if let Some(s) = panic.downcast_ref::<&str>() {
|
|
412
|
+
(*s).to_string()
|
|
413
|
+
} else if let Some(s) = panic.downcast_ref::<String>() {
|
|
414
|
+
s.clone()
|
|
415
|
+
} else {
|
|
416
|
+
"non-string panic payload".to_string()
|
|
417
|
+
};
|
|
418
|
+
Err(msg)
|
|
419
|
+
}
|
|
420
|
+
// Cancellation: the runtime is going away. Treat as a clean stop.
|
|
421
|
+
Err(_) => Err("task cancelled".to_string()),
|
|
422
|
+
}
|
|
423
|
+
}
|