failproofai 1.0.0-beta.2 → 1.0.0-beta.3
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.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.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.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.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.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.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/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- 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]__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]__0qucxyj._.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]__1psjybo._.js → [root-of-the-server]__10s_z4l._.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]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- 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_0jfts82.js} +1 -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/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- 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 +2 -2
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2195 -770
- package/dist/worker.mjs +365 -52
- package/package.json +2 -2
- 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/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/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_ssgManifest.js +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
mod cloud_client;
|
|
2
|
+
pub mod cloud_policies;
|
|
1
3
|
mod lock;
|
|
2
4
|
mod paths;
|
|
3
5
|
mod server;
|
|
@@ -5,6 +7,7 @@ mod worker;
|
|
|
5
7
|
|
|
6
8
|
use std::sync::Arc;
|
|
7
9
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
10
|
+
use std::time::Duration;
|
|
8
11
|
|
|
9
12
|
fn main() {
|
|
10
13
|
let args: Vec<String> = std::env::args().collect();
|
|
@@ -19,7 +22,27 @@ fn main() {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
/// Install a log subscriber, or the collector's diagnostics go nowhere.
|
|
26
|
+
///
|
|
27
|
+
/// `tracing` drops every event when no subscriber is registered, silently. The
|
|
28
|
+
/// uploader reports "the server accepted the request but stored NONE of its
|
|
29
|
+
/// events" through it — the single most important signal that a transform is
|
|
30
|
+
/// systematically malformed — so without this that failure is invisible.
|
|
31
|
+
///
|
|
32
|
+
/// Writes to stderr, which is where the daemon's existing `eprintln!` output
|
|
33
|
+
/// already goes and what systemd/launchd capture. `RUST_LOG` overrides the
|
|
34
|
+
/// default level.
|
|
35
|
+
fn init_logging() {
|
|
36
|
+
use tracing_subscriber::EnvFilter;
|
|
37
|
+
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
|
38
|
+
let _ = tracing_subscriber::fmt()
|
|
39
|
+
.with_env_filter(filter)
|
|
40
|
+
.with_writer(std::io::stderr)
|
|
41
|
+
.try_init();
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
45
|
+
init_logging();
|
|
23
46
|
let lock_path = paths::lock_path()?;
|
|
24
47
|
paths::ensure_run_dir()?;
|
|
25
48
|
let _singleton = lock::acquire(&lock_path)?;
|
|
@@ -40,16 +63,442 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
40
63
|
let warm_worker = worker.clone();
|
|
41
64
|
std::thread::spawn(move || warm_worker.warm());
|
|
42
65
|
}
|
|
66
|
+
// The shutdown flag and its signal handler are installed BEFORE anything
|
|
67
|
+
// long-running starts, so the collector below can observe the same flag
|
|
68
|
+
// the socket server does. One SIGTERM then stops both; a second signal
|
|
69
|
+
// path would be one more thing to get wrong during shutdown.
|
|
70
|
+
let shutdown = Arc::new(AtomicBool::new(false));
|
|
71
|
+
install_signal_handler(shutdown.clone());
|
|
72
|
+
|
|
73
|
+
// Cloud policy integrity is a maintenance-lane responsibility, never a
|
|
74
|
+
// hook-path operation. The monitor is useful before cloud transport lands:
|
|
75
|
+
// it keeps the active generation and content-addressed artifact cache in
|
|
76
|
+
// agreement and reports when both verified copies have been lost.
|
|
77
|
+
let cloud_policy_store = cloud_policies::PolicyStore::new(paths::cloud_managed_policy_dir()?);
|
|
78
|
+
// One lane, resolving enrolment per tick rather than once at startup.
|
|
79
|
+
// `failproofai config --connect` writes its credential file without root,
|
|
80
|
+
// and this is a SYSTEM unit — so noticing it only on restart would put
|
|
81
|
+
// `sudo systemctl restart` back into a flow built to avoid it. The lane
|
|
82
|
+
// also does integrity repair whether or not the machine is enrolled.
|
|
83
|
+
let cloud_monitor = cloud_client::spawn_maintenance(
|
|
84
|
+
cloud_policy_store,
|
|
85
|
+
shutdown.clone(),
|
|
86
|
+
cloud_client::poll_interval_from_env(),
|
|
87
|
+
cloud_policy_reconcile_interval(),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Log/hook collection. Runs on its own thread with its own Tokio runtime
|
|
91
|
+
// so it can never share fate with the accept loop — this daemon fails
|
|
92
|
+
// closed, so a collector fault would otherwise deny every tool call on the
|
|
93
|
+
// machine (see fpai_collect::supervisor). It observes the same `shutdown`
|
|
94
|
+
// flag the server and the cloud monitor do, so one SIGTERM stops all three.
|
|
95
|
+
//
|
|
96
|
+
// INERT until ingest is configured: `collector_tasks()` returns an empty
|
|
97
|
+
// list, `spawn_supervised` then declines to start a thread or a runtime at
|
|
98
|
+
// all, and the daemon behaves byte-for-byte as it did before this existed.
|
|
99
|
+
let collector = fpai_collect::spawn_supervised(collector_tasks(), shutdown.clone());
|
|
100
|
+
|
|
43
101
|
let srv = server::Server::bind(&socket_path, worker)?;
|
|
44
102
|
eprintln!("[failproofaid] listening on {}", socket_path.display());
|
|
45
103
|
|
|
46
|
-
let
|
|
47
|
-
|
|
104
|
+
let run_result = srv.run_until(shutdown);
|
|
105
|
+
|
|
106
|
+
// Drain both background workers before returning. The collector gets a
|
|
107
|
+
// bounded budget rather than an unbounded join: process exit must not wait
|
|
108
|
+
// on a task that has wedged. Done before `?` so a server error still gets
|
|
109
|
+
// the collector a chance to flush instead of dropping buffered events.
|
|
110
|
+
if let Some(collector) = collector {
|
|
111
|
+
collector.join_with_flush(fpai_collect::DEFAULT_FLUSH_BUDGET);
|
|
112
|
+
}
|
|
113
|
+
let _ = cloud_monitor.join();
|
|
48
114
|
|
|
49
|
-
|
|
115
|
+
run_result?;
|
|
50
116
|
Ok(())
|
|
51
117
|
}
|
|
52
118
|
|
|
119
|
+
/// The collector tasks to supervise for this process.
|
|
120
|
+
///
|
|
121
|
+
/// Returns an empty list — and therefore starts no thread and no runtime —
|
|
122
|
+
/// unless an ingest credential is configured AND at least one stream is
|
|
123
|
+
/// enabled. That is the normal state, so an install that has not opted in
|
|
124
|
+
/// pays nothing for this code path existing.
|
|
125
|
+
///
|
|
126
|
+
/// A configuration error is logged and treated as "off" rather than being
|
|
127
|
+
/// propagated: collection failing to start must never stop the daemon from
|
|
128
|
+
/// serving the socket, because the CLI fails closed and a daemon that refused
|
|
129
|
+
/// to boot over a malformed `ingest.json` would deny every tool call on the
|
|
130
|
+
/// machine. The error is loud so it is fixable, not silent.
|
|
131
|
+
fn collector_tasks() -> Vec<fpai_collect::TaskSpec> {
|
|
132
|
+
let home = match paths::failproofai_home() {
|
|
133
|
+
Ok(home) => home,
|
|
134
|
+
Err(err) => {
|
|
135
|
+
eprintln!("[failproofaid] collector disabled: {err}");
|
|
136
|
+
return Vec::new();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let cfg = match fpai_collect::config::load(&home) {
|
|
141
|
+
Ok(cfg) => cfg,
|
|
142
|
+
Err(err) => {
|
|
143
|
+
eprintln!("[failproofaid] collector disabled: {err}");
|
|
144
|
+
return Vec::new();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if !cfg.is_enabled() {
|
|
149
|
+
return Vec::new();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// `is_enabled()` already established there is one.
|
|
153
|
+
let Some(ingest) = cfg.ingest.clone() else {
|
|
154
|
+
return Vec::new();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let uploader = match fpai_collect::Uploader::new(
|
|
158
|
+
ingest.url.clone(),
|
|
159
|
+
ingest.key.clone(),
|
|
160
|
+
cfg.failed_dir.clone(),
|
|
161
|
+
) {
|
|
162
|
+
Ok(u) => std::sync::Arc::new(u),
|
|
163
|
+
Err(err) => {
|
|
164
|
+
eprintln!("[failproofaid] collector disabled: {err}");
|
|
165
|
+
return Vec::new();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
eprintln!(
|
|
170
|
+
"[failproofaid] collector enabled: sessions={} hooks={} ({:?}) -> {}",
|
|
171
|
+
cfg.settings.sessions, cfg.settings.hooks, cfg.settings.hooks_verbosity, ingest.url,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// One `Delivery` shared by both tasks, so they share an upload semaphore
|
|
175
|
+
// and an in-flight set. Separate ones would let the watcher and a
|
|
176
|
+
// concurrent sweep POST the same batch twice.
|
|
177
|
+
let delivery = std::sync::Arc::new(fpai_collect::Delivery::new(uploader));
|
|
178
|
+
|
|
179
|
+
let watch_delivery = delivery.clone();
|
|
180
|
+
let watch_dirs = cfg.spool_dirs.clone();
|
|
181
|
+
let sweep_delivery = delivery;
|
|
182
|
+
let sweep_dirs = cfg.spool_dirs.clone();
|
|
183
|
+
let failed_dir = cfg.failed_dir.clone();
|
|
184
|
+
|
|
185
|
+
let mut tasks = Vec::new();
|
|
186
|
+
|
|
187
|
+
// Install the process health registry BEFORE any source starts, so no poll
|
|
188
|
+
// reports into a registry that does not exist yet. The writer publishes it
|
|
189
|
+
// on an interval and deletes it on clean shutdown — absence means "no
|
|
190
|
+
// daemon", where a stale file makes a stopped daemon look like a running
|
|
191
|
+
// one whose sources all went quiet.
|
|
192
|
+
let health = std::sync::Arc::new(fpai_collect::Health::new());
|
|
193
|
+
fpai_collect::health::install(health.clone());
|
|
194
|
+
let health_file = fpai_collect::health_path(&home);
|
|
195
|
+
tasks.push(fpai_collect::TaskSpec::new("health", move |sd| {
|
|
196
|
+
fpai_collect::health::writer_task(
|
|
197
|
+
health.clone(),
|
|
198
|
+
health_file.clone(),
|
|
199
|
+
fpai_collect::health::WRITE_INTERVAL,
|
|
200
|
+
sd,
|
|
201
|
+
)
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
if cfg.settings.hooks {
|
|
205
|
+
// Hook activity: one source covering every CLI failproofai is
|
|
206
|
+
// installed in, because the store is CLI-agnostic — each row names its
|
|
207
|
+
// own integration. Reads the same store the dashboard's activity tab
|
|
208
|
+
// does, and never writes to it.
|
|
209
|
+
let store_dir = home.join("cache").join("hook-activity");
|
|
210
|
+
let state_dir = home.join("cursors").join("hooks");
|
|
211
|
+
let spool_dir = cfg.own_spool_dir.clone();
|
|
212
|
+
let verbosity = cfg.settings.hooks_verbosity;
|
|
213
|
+
let environment = cfg.settings.environment.clone();
|
|
214
|
+
tasks.push(fpai_collect::TaskSpec::new("hook-activity", move |sd| {
|
|
215
|
+
fpai_collect::sources::hooks::run(
|
|
216
|
+
store_dir.clone(),
|
|
217
|
+
state_dir.clone(),
|
|
218
|
+
spool_dir.clone(),
|
|
219
|
+
verbosity,
|
|
220
|
+
environment.clone(),
|
|
221
|
+
sd,
|
|
222
|
+
)
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if cfg.settings.sessions {
|
|
227
|
+
// Session transcripts, gated on the `sessions` opt-in because — unlike
|
|
228
|
+
// hook activity — these carry prompts, file contents and whatever was
|
|
229
|
+
// pasted into a terminal.
|
|
230
|
+
//
|
|
231
|
+
// Every source is registered the same way regardless of which engine it
|
|
232
|
+
// uses, so adding one is a row here plus its own module.
|
|
233
|
+
let spool = cfg.own_spool_dir.clone();
|
|
234
|
+
let env = cfg.settings.environment.clone();
|
|
235
|
+
let cursors = home.join("cursors");
|
|
236
|
+
|
|
237
|
+
use fpai_collect::sources::{claude, codex, copilot, openclaw, pi};
|
|
238
|
+
file_source(
|
|
239
|
+
&mut tasks,
|
|
240
|
+
"claude",
|
|
241
|
+
claude::FORMAT,
|
|
242
|
+
vec![claude_projects_root()],
|
|
243
|
+
claude::DEFAULT_AGENT_ID,
|
|
244
|
+
&spool,
|
|
245
|
+
&cursors,
|
|
246
|
+
&env,
|
|
247
|
+
);
|
|
248
|
+
// Subagent transcripts live under the SAME root, claimed by a second
|
|
249
|
+
// format. A separate source, not a second predicate: `is_source_file`
|
|
250
|
+
// is a bare fn, and each source needs its own cursor store — one store
|
|
251
|
+
// writes its whole map atomically, so two sharing a file would clobber
|
|
252
|
+
// each other and the loser would re-ship from zero after every restart.
|
|
253
|
+
file_source(
|
|
254
|
+
&mut tasks,
|
|
255
|
+
"claude-subagent",
|
|
256
|
+
claude::SUBAGENT_FORMAT,
|
|
257
|
+
vec![claude_projects_root()],
|
|
258
|
+
claude::SUBAGENT_DEFAULT_AGENT_ID,
|
|
259
|
+
&spool,
|
|
260
|
+
&cursors,
|
|
261
|
+
&env,
|
|
262
|
+
);
|
|
263
|
+
file_source(
|
|
264
|
+
&mut tasks,
|
|
265
|
+
"codex",
|
|
266
|
+
codex::FORMAT,
|
|
267
|
+
vec![codex_sessions_root()],
|
|
268
|
+
codex::DEFAULT_AGENT_ID,
|
|
269
|
+
&spool,
|
|
270
|
+
&cursors,
|
|
271
|
+
&env,
|
|
272
|
+
);
|
|
273
|
+
file_source(
|
|
274
|
+
&mut tasks,
|
|
275
|
+
"copilot",
|
|
276
|
+
copilot::FORMAT,
|
|
277
|
+
vec![copilot::session_state_root()],
|
|
278
|
+
copilot::DEFAULT_AGENT_ID,
|
|
279
|
+
&spool,
|
|
280
|
+
&cursors,
|
|
281
|
+
&env,
|
|
282
|
+
);
|
|
283
|
+
file_source(
|
|
284
|
+
&mut tasks,
|
|
285
|
+
"openclaw",
|
|
286
|
+
openclaw::FORMAT,
|
|
287
|
+
openclaw::default_roots(),
|
|
288
|
+
openclaw::DEFAULT_AGENT_ID,
|
|
289
|
+
&spool,
|
|
290
|
+
&cursors,
|
|
291
|
+
&env,
|
|
292
|
+
);
|
|
293
|
+
file_source(
|
|
294
|
+
&mut tasks,
|
|
295
|
+
"pi",
|
|
296
|
+
pi::FORMAT,
|
|
297
|
+
vec![pi::sessions_root()],
|
|
298
|
+
pi::DEFAULT_AGENT_ID,
|
|
299
|
+
&spool,
|
|
300
|
+
&cursors,
|
|
301
|
+
&env,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
use fpai_collect::sources::{goose, hermes, opencode};
|
|
305
|
+
sqlite_source(
|
|
306
|
+
&mut tasks,
|
|
307
|
+
"goose",
|
|
308
|
+
goose::FORMAT,
|
|
309
|
+
goose::db_path(),
|
|
310
|
+
goose::DEFAULT_AGENT_ID,
|
|
311
|
+
&spool,
|
|
312
|
+
cursors.join("goose"),
|
|
313
|
+
&env,
|
|
314
|
+
);
|
|
315
|
+
sqlite_source(
|
|
316
|
+
&mut tasks,
|
|
317
|
+
"opencode",
|
|
318
|
+
opencode::FORMAT,
|
|
319
|
+
opencode::default_db_path(),
|
|
320
|
+
opencode::DEFAULT_AGENT_ID,
|
|
321
|
+
&spool,
|
|
322
|
+
cursors.join("opencode"),
|
|
323
|
+
&env,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Hermes profiles are SEPARATE databases, and the SQLite poller keys its
|
|
327
|
+
// cursor on a fixed synthetic id — so two profiles sharing one state
|
|
328
|
+
// directory would clobber each other's watermark and each would re-read
|
|
329
|
+
// from zero after every restart. Each database gets its own.
|
|
330
|
+
for (i, db) in hermes::default_db_paths().into_iter().enumerate() {
|
|
331
|
+
let state = cursors.join("hermes").join(profile_dir_name(&db, i));
|
|
332
|
+
sqlite_source(
|
|
333
|
+
&mut tasks,
|
|
334
|
+
"hermes",
|
|
335
|
+
hermes::FORMAT,
|
|
336
|
+
db,
|
|
337
|
+
hermes::DEFAULT_AGENT_ID,
|
|
338
|
+
&spool,
|
|
339
|
+
state,
|
|
340
|
+
&env,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
tasks.extend([
|
|
346
|
+
// Latency: delivers a batch within milliseconds of it being published.
|
|
347
|
+
fpai_collect::TaskSpec::new("spool-watcher", move |sd| {
|
|
348
|
+
fpai_collect::delivery::watch(watch_delivery.clone(), watch_dirs.clone(), sd)
|
|
349
|
+
}),
|
|
350
|
+
// Guarantee: delivers anything the watcher never saw — published while
|
|
351
|
+
// the daemon was stopped, or on a filesystem with no event support —
|
|
352
|
+
// and retries parked batches on a much slower cadence.
|
|
353
|
+
fpai_collect::TaskSpec::new("spool-sweeper", move |sd| {
|
|
354
|
+
fpai_collect::delivery::sweep(
|
|
355
|
+
sweep_delivery.clone(),
|
|
356
|
+
sweep_dirs.clone(),
|
|
357
|
+
failed_dir.clone(),
|
|
358
|
+
sd,
|
|
359
|
+
)
|
|
360
|
+
}),
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
tasks
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Where Claude Code keeps its transcripts.
|
|
367
|
+
///
|
|
368
|
+
/// `CLAUDE_PROJECTS_PATH` overrides it, matching the env var the TypeScript
|
|
369
|
+
/// side already honours, so a machine that has moved the directory is captured
|
|
370
|
+
/// by both halves rather than one.
|
|
371
|
+
fn claude_projects_root() -> std::path::PathBuf {
|
|
372
|
+
if let Some(p) = std::env::var_os("CLAUDE_PROJECTS_PATH") {
|
|
373
|
+
return std::path::PathBuf::from(p);
|
|
374
|
+
}
|
|
375
|
+
let home = std::env::var_os("HOME")
|
|
376
|
+
.map(std::path::PathBuf::from)
|
|
377
|
+
.unwrap_or_default();
|
|
378
|
+
home.join(".claude").join("projects")
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// Register one file-tailing source.
|
|
382
|
+
#[allow(clippy::too_many_arguments)]
|
|
383
|
+
fn file_source(
|
|
384
|
+
tasks: &mut Vec<fpai_collect::TaskSpec>,
|
|
385
|
+
name: &'static str,
|
|
386
|
+
format: fpai_collect::filetail::Format,
|
|
387
|
+
roots: Vec<std::path::PathBuf>,
|
|
388
|
+
default_agent_id: &'static str,
|
|
389
|
+
spool_dir: &std::path::Path,
|
|
390
|
+
cursor_root: &std::path::Path,
|
|
391
|
+
environment: &str,
|
|
392
|
+
) {
|
|
393
|
+
let spool_dir = spool_dir.to_path_buf();
|
|
394
|
+
// One cursor store per source, never shared: the store writes its whole map
|
|
395
|
+
// atomically, so two sources sharing a file would clobber each other and the
|
|
396
|
+
// loser would re-read from zero after every restart.
|
|
397
|
+
let state_dir = cursor_root.join(name);
|
|
398
|
+
let environment = environment.to_string();
|
|
399
|
+
tasks.push(fpai_collect::TaskSpec::new(name, move |sd| {
|
|
400
|
+
fpai_collect::filetail::run(
|
|
401
|
+
fpai_collect::filetail::Spec {
|
|
402
|
+
format,
|
|
403
|
+
roots: roots.clone(),
|
|
404
|
+
spool_dir: spool_dir.clone(),
|
|
405
|
+
state_dir: state_dir.clone(),
|
|
406
|
+
poll_interval: std::time::Duration::from_secs(2),
|
|
407
|
+
params: fpai_collect::filetail::Params {
|
|
408
|
+
agent_id: default_agent_id.to_string(),
|
|
409
|
+
environment: environment.clone(),
|
|
410
|
+
end_idle_mins: 10,
|
|
411
|
+
max_read_bytes: 32 * 1024 * 1024,
|
|
412
|
+
max_batch_bytes: fpai_collect::spool::DEFAULT_MAX_BATCH_BYTES,
|
|
413
|
+
// Never the whole history by default. A normal machine holds
|
|
414
|
+
// hundreds of megabytes of transcripts, and shipping all of
|
|
415
|
+
// it on first start is not a reasonable default.
|
|
416
|
+
since_days: Some(7),
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
sd,
|
|
420
|
+
)
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// Register one SQLite-polling source.
|
|
425
|
+
#[allow(clippy::too_many_arguments)]
|
|
426
|
+
fn sqlite_source(
|
|
427
|
+
tasks: &mut Vec<fpai_collect::TaskSpec>,
|
|
428
|
+
name: &'static str,
|
|
429
|
+
format: fpai_collect::sqlitepoll::SqliteFormat,
|
|
430
|
+
db_path: std::path::PathBuf,
|
|
431
|
+
default_agent_id: &'static str,
|
|
432
|
+
spool_dir: &std::path::Path,
|
|
433
|
+
state_dir: std::path::PathBuf,
|
|
434
|
+
environment: &str,
|
|
435
|
+
) {
|
|
436
|
+
let spool_dir = spool_dir.to_path_buf();
|
|
437
|
+
let environment = environment.to_string();
|
|
438
|
+
tasks.push(fpai_collect::TaskSpec::new(name, move |sd| {
|
|
439
|
+
fpai_collect::sqlitepoll::run(
|
|
440
|
+
fpai_collect::sqlitepoll::Spec {
|
|
441
|
+
format,
|
|
442
|
+
db_path: db_path.clone(),
|
|
443
|
+
spool_dir: spool_dir.clone(),
|
|
444
|
+
state_dir: state_dir.clone(),
|
|
445
|
+
poll_interval: std::time::Duration::from_secs(5),
|
|
446
|
+
params: fpai_collect::sqlitepoll::Params {
|
|
447
|
+
agent_id: default_agent_id.to_string(),
|
|
448
|
+
environment: environment.clone(),
|
|
449
|
+
max_rows_per_poll: 2000,
|
|
450
|
+
max_batch_bytes: fpai_collect::spool::DEFAULT_MAX_BATCH_BYTES,
|
|
451
|
+
max_drain_passes: 20,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
sd,
|
|
455
|
+
)
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// A stable, filesystem-safe directory name for one Hermes profile database.
|
|
460
|
+
///
|
|
461
|
+
/// Derived from the profile directory rather than an index alone, so adding a
|
|
462
|
+
/// profile cannot renumber an existing one and silently reset its watermark.
|
|
463
|
+
fn profile_dir_name(db: &std::path::Path, index: usize) -> String {
|
|
464
|
+
let name = db
|
|
465
|
+
.parent()
|
|
466
|
+
.and_then(|p| p.file_name())
|
|
467
|
+
.and_then(|n| n.to_str())
|
|
468
|
+
.unwrap_or("default");
|
|
469
|
+
let safe: String = name
|
|
470
|
+
.chars()
|
|
471
|
+
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
|
472
|
+
.take(32)
|
|
473
|
+
.collect();
|
|
474
|
+
if safe.trim_matches('-').is_empty() {
|
|
475
|
+
format!("profile-{index}")
|
|
476
|
+
} else {
|
|
477
|
+
safe
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// Where OpenAI Codex keeps its rollout logs.
|
|
482
|
+
fn codex_sessions_root() -> std::path::PathBuf {
|
|
483
|
+
if let Some(p) = std::env::var_os("CODEX_HOME") {
|
|
484
|
+
return std::path::PathBuf::from(p).join("sessions");
|
|
485
|
+
}
|
|
486
|
+
let home = std::env::var_os("HOME")
|
|
487
|
+
.map(std::path::PathBuf::from)
|
|
488
|
+
.unwrap_or_default();
|
|
489
|
+
home.join(".codex").join("sessions")
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fn cloud_policy_reconcile_interval() -> Duration {
|
|
493
|
+
const DEFAULT_MS: u64 = 30_000;
|
|
494
|
+
const MINIMUM_MS: u64 = 100;
|
|
495
|
+
let configured = std::env::var("FAILPROOFAI_CLOUD_POLICY_RECONCILE_MS")
|
|
496
|
+
.ok()
|
|
497
|
+
.and_then(|value| value.parse::<u64>().ok())
|
|
498
|
+
.unwrap_or(DEFAULT_MS);
|
|
499
|
+
Duration::from_millis(configured.max(MINIMUM_MS))
|
|
500
|
+
}
|
|
501
|
+
|
|
53
502
|
/// Requests a clean shutdown (socket file removal, lock release via Drop)
|
|
54
503
|
/// on SIGTERM/SIGINT instead of dying mid-accept-loop — this is how a
|
|
55
504
|
/// systemd `stop`/launchd unload is expected to end the process.
|
|
@@ -49,6 +49,35 @@ pub fn worker_socket_path() -> io::Result<PathBuf> {
|
|
|
49
49
|
Ok(run_dir()?.join("worker.sock"))
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/// Local cache for cloud-managed policy generations. The override keeps
|
|
53
|
+
/// tests and development runs away from a user's real policy directory.
|
|
54
|
+
pub fn cloud_managed_policy_dir() -> io::Result<PathBuf> {
|
|
55
|
+
if let Some(path) = std::env::var_os("FAILPROOFAI_CLOUD_POLICY_DIR") {
|
|
56
|
+
return Ok(PathBuf::from(path));
|
|
57
|
+
}
|
|
58
|
+
let home = std::env::var_os("HOME").ok_or_else(|| {
|
|
59
|
+
io::Error::other("HOME is not set; cannot resolve cloud policy directory")
|
|
60
|
+
})?;
|
|
61
|
+
Ok(PathBuf::from(home)
|
|
62
|
+
.join(".failproofai")
|
|
63
|
+
.join("policies")
|
|
64
|
+
.join("cloud-managed"))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// `~/.failproofai` — the root the collector reads its configuration from.
|
|
68
|
+
///
|
|
69
|
+
/// `FAILPROOFAI_HOME` overrides it so tests and development runs never touch a
|
|
70
|
+
/// real user's config, and so a containerised daemon can be pointed at a
|
|
71
|
+
/// mounted volume.
|
|
72
|
+
pub fn failproofai_home() -> io::Result<PathBuf> {
|
|
73
|
+
if let Some(path) = std::env::var_os("FAILPROOFAI_HOME") {
|
|
74
|
+
return Ok(PathBuf::from(path));
|
|
75
|
+
}
|
|
76
|
+
let home = std::env::var_os("HOME")
|
|
77
|
+
.ok_or_else(|| io::Error::other("HOME is not set; cannot resolve the failproofai home"))?;
|
|
78
|
+
Ok(PathBuf::from(home).join(".failproofai"))
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
/// Creates the run directory (`0700`) if it doesn't exist yet. This
|
|
53
82
|
/// directory holds a socket that evaluates security-relevant decisions, so
|
|
54
83
|
/// a freshly created one is always locked to owner-only.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "fpai-collect"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license-file.workspace = true
|
|
6
|
+
repository.workspace = true
|
|
7
|
+
description = "Session-log and hook-activity collection for failproofaid: fault-isolated supervisor, spool, and uploader. Carries no policy logic and never shares fate with the enforcement socket."
|
|
8
|
+
publish = false
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
serde = { version = "1", features = ["derive"] }
|
|
12
|
+
serde_json = "1"
|
|
13
|
+
# The collector is the only async part of the daemon. `failproofaid`'s socket
|
|
14
|
+
# server stays blocking std threads on purpose (see supervisor.rs), so this
|
|
15
|
+
# runtime is created here and never leaks into the enforcement path.
|
|
16
|
+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "fs", "sync", "io-util"] }
|
|
17
|
+
tracing = "0.1"
|
|
18
|
+
# `rustls-tls` rather than the default native-tls: the daemon cross-compiles to
|
|
19
|
+
# four targets and pulling OpenSSL onto each of them is the kind of build
|
|
20
|
+
# dependency that only breaks on the leg nobody tests. `failproofaid` already
|
|
21
|
+
# depends on reqwest with the same TLS backend, so this adds no new C code.
|
|
22
|
+
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
|
23
|
+
# inotify on Linux, FSEvents on macOS. Only ever a latency optimisation — the
|
|
24
|
+
# sweeper is what guarantees delivery, so a watcher that fails to register
|
|
25
|
+
# degrades to interval-only rather than losing anything.
|
|
26
|
+
notify = "8"
|
|
27
|
+
# Epoch-millis to RFC3339 with microseconds. Hand-rolling civil date maths is
|
|
28
|
+
# a silent-failure risk — a leap-year slip would put every event on the wrong
|
|
29
|
+
# day with nothing to notice it — and this pulls no C code onto any of the
|
|
30
|
+
# four cross-compiled targets.
|
|
31
|
+
# `bundled` vendors libsqlite3 rather than linking the host's, so the four
|
|
32
|
+
# cross-compiled release binaries do not depend on whatever sqlite the target
|
|
33
|
+
# happens to ship. It compiles C, which `ring` already does on every leg.
|
|
34
|
+
#
|
|
35
|
+
# A real libsqlite3 is not optional here: goose's sessions.db main file contains
|
|
36
|
+
# ZERO tables, with all 1.25 MB of schema and rows in an uncheckpointed -wal.
|
|
37
|
+
# Anything that parses the main file directly reads an empty database.
|
|
38
|
+
rusqlite = { version = "0.40", features = ["bundled"] }
|
|
39
|
+
time = { version = "0.3", default-features = false, features = ["std", "formatting", "macros"] }
|
|
40
|
+
|
|
41
|
+
[dev-dependencies]
|
|
42
|
+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync", "test-util"] }
|
|
43
|
+
wiremock = "0.6"
|