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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
//! Collector configuration: where to send events, and what to collect.
|
|
2
|
+
//!
|
|
3
|
+
//! # Two files, on purpose
|
|
4
|
+
//!
|
|
5
|
+
//! The credential lives alone in `~/.failproofai/ingest.json` at mode 0600.
|
|
6
|
+
//! Everything else — which sources are on, backfill window, hook verbosity —
|
|
7
|
+
//! lives in `policies-config.json` beside the rest of failproofai's settings,
|
|
8
|
+
//! where it is readable, diffable and safe to commit to a dotfiles repo.
|
|
9
|
+
//!
|
|
10
|
+
//! That split is not tidiness. `policies-config.json` is written by
|
|
11
|
+
//! `hooks-config.ts` with a bare `writeFileSync`, so it inherits the umask and
|
|
12
|
+
//! lands at 0664 on a normal machine — inside `~/.failproofai/`, which is
|
|
13
|
+
//! itself 0775. Putting an API key there would publish it to every local user
|
|
14
|
+
//! on the box. `~/.agenteye/cli.json` already stores its session token at 0600,
|
|
15
|
+
//! so the correct precedent existed; this follows it.
|
|
16
|
+
//!
|
|
17
|
+
//! # Disabled is the default, and it is a real default
|
|
18
|
+
//!
|
|
19
|
+
//! No `ingest.json`, or one with no key, means collection is off: no tasks, so
|
|
20
|
+
//! no thread and no runtime (see [`crate::supervisor::spawn_supervised`]). A
|
|
21
|
+
//! machine that has not opted in pays nothing for this code existing. Session
|
|
22
|
+
//! collection additionally requires its own explicit opt-in, because
|
|
23
|
+
//! transcripts carry prompts, file contents and whatever the user pasted into
|
|
24
|
+
//! a terminal — configuring a key must not silently start shipping those.
|
|
25
|
+
|
|
26
|
+
use std::fs;
|
|
27
|
+
use std::io;
|
|
28
|
+
use std::path::{Path, PathBuf};
|
|
29
|
+
|
|
30
|
+
use serde::{Deserialize, Serialize};
|
|
31
|
+
|
|
32
|
+
/// The hosted ingest endpoint. A COMPLETE endpoint including its path, not a
|
|
33
|
+
/// base to join onto — byte-identical to what `agenteye-collector` defaults
|
|
34
|
+
/// to, so a migrated config needs no rewriting and a self-hoster replaces the
|
|
35
|
+
/// whole value with one of their own.
|
|
36
|
+
pub const DEFAULT_INGEST_URL: &str = "https://server.befailproof.ai/events";
|
|
37
|
+
|
|
38
|
+
/// Filename of the credential file inside the failproofai home.
|
|
39
|
+
const INGEST_FILE: &str = "ingest.json";
|
|
40
|
+
/// Filename of the shared settings file, owned by the TypeScript side.
|
|
41
|
+
const POLICIES_FILE: &str = "policies-config.json";
|
|
42
|
+
|
|
43
|
+
/// Mode the credential file must have. Anything wider is a finding.
|
|
44
|
+
#[cfg(unix)]
|
|
45
|
+
const CREDENTIAL_MODE: u32 = 0o600;
|
|
46
|
+
|
|
47
|
+
/// What the daemon needs in order to ship anything at all.
|
|
48
|
+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
|
49
|
+
pub struct Ingest {
|
|
50
|
+
/// Complete endpoint URL. Absent in the file means [`DEFAULT_INGEST_URL`].
|
|
51
|
+
#[serde(default = "default_url")]
|
|
52
|
+
pub url: String,
|
|
53
|
+
/// An `events:add` API key, sent as a bearer token.
|
|
54
|
+
pub key: String,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn default_url() -> String {
|
|
58
|
+
DEFAULT_INGEST_URL.to_string()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// How much hook activity to ship.
|
|
62
|
+
///
|
|
63
|
+
/// Measured on a real machine: 19,339 rows over 17 days from one developer on
|
|
64
|
+
/// one CLI, of which 99.1% were plain `allow`. Emitting a triggered/completed
|
|
65
|
+
/// pair per row is ~38,700 events for that one machine, almost all of it
|
|
66
|
+
/// no-ops — hence a default that keeps every decision exact and rolls the rest
|
|
67
|
+
/// up rather than dropping it.
|
|
68
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
|
|
69
|
+
#[serde(rename_all = "lowercase")]
|
|
70
|
+
pub enum HooksVerbosity {
|
|
71
|
+
/// Every hook invocation, in full.
|
|
72
|
+
All,
|
|
73
|
+
/// `deny` and `instruct` in full; `allow` aggregated per (session, event,
|
|
74
|
+
/// tool) per minute. The aggregate carries a count, so the denominator
|
|
75
|
+
/// survives — "we evaluated 19,000 calls and blocked 15" stays answerable.
|
|
76
|
+
#[default]
|
|
77
|
+
Decisions,
|
|
78
|
+
/// No hook events at all.
|
|
79
|
+
Off,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Client-side redaction strength.
|
|
83
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
|
|
84
|
+
#[serde(rename_all = "lowercase")]
|
|
85
|
+
pub enum Redact {
|
|
86
|
+
/// A fixed, deterministic pattern set over tool inputs and outputs.
|
|
87
|
+
/// Deterministic is load-bearing, not incidental: the server dedups on a
|
|
88
|
+
/// content hash, so a redaction that varied between reads of the same
|
|
89
|
+
/// bytes would defeat it.
|
|
90
|
+
#[default]
|
|
91
|
+
Minimal,
|
|
92
|
+
/// Ship verbatim.
|
|
93
|
+
Off,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// The non-secret half, read from `policies-config.json` under `"collector"`.
|
|
97
|
+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
|
98
|
+
#[serde(rename_all = "camelCase")]
|
|
99
|
+
pub struct Settings {
|
|
100
|
+
/// Ship agent session transcripts. Defaults to FALSE and is deliberately
|
|
101
|
+
/// separate from having a key: transcripts carry prompts, file contents
|
|
102
|
+
/// and pasted credentials, so configuring ingest must not silently start
|
|
103
|
+
/// sending them.
|
|
104
|
+
#[serde(default)]
|
|
105
|
+
pub sessions: bool,
|
|
106
|
+
/// Ship hook activity. Defaults to TRUE once ingest is configured — it
|
|
107
|
+
/// carries decisions and tool names, never file contents, and it is the
|
|
108
|
+
/// capability that justifies collecting from inside failproofai at all.
|
|
109
|
+
#[serde(default = "default_true")]
|
|
110
|
+
pub hooks: bool,
|
|
111
|
+
#[serde(default)]
|
|
112
|
+
pub hooks_verbosity: HooksVerbosity,
|
|
113
|
+
#[serde(default)]
|
|
114
|
+
pub redact: Redact,
|
|
115
|
+
/// Label stamped on every event. Rejected if it contains a comma: the
|
|
116
|
+
/// ingest endpoint skips any line whose `environment` has one, so a comma
|
|
117
|
+
/// here would silently drop every event this machine produced.
|
|
118
|
+
#[serde(default = "default_environment")]
|
|
119
|
+
pub environment: String,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn default_true() -> bool {
|
|
123
|
+
true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn default_environment() -> String {
|
|
127
|
+
"local".to_string()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
impl Default for Settings {
|
|
131
|
+
fn default() -> Self {
|
|
132
|
+
Settings {
|
|
133
|
+
sessions: false,
|
|
134
|
+
hooks: true,
|
|
135
|
+
hooks_verbosity: HooksVerbosity::default(),
|
|
136
|
+
redact: Redact::default(),
|
|
137
|
+
environment: default_environment(),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Everything the collector needs, resolved.
|
|
143
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
144
|
+
pub struct CollectorConfig {
|
|
145
|
+
/// `None` means collection is off for this process.
|
|
146
|
+
pub ingest: Option<Ingest>,
|
|
147
|
+
pub settings: Settings,
|
|
148
|
+
/// Directories watched for ready-to-upload event batches, in the order
|
|
149
|
+
/// they are scanned.
|
|
150
|
+
pub spool_dirs: Vec<PathBuf>,
|
|
151
|
+
/// Where this daemon writes its own derived batches. Always the first
|
|
152
|
+
/// entry of `spool_dirs`.
|
|
153
|
+
pub own_spool_dir: PathBuf,
|
|
154
|
+
/// Batches the server rejected, parked for retry.
|
|
155
|
+
pub failed_dir: PathBuf,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl CollectorConfig {
|
|
159
|
+
/// True when there is a usable credential AND at least one stream enabled.
|
|
160
|
+
/// This is what `collector_tasks()` keys off, so an unconfigured machine
|
|
161
|
+
/// starts no thread and no runtime.
|
|
162
|
+
pub fn is_enabled(&self) -> bool {
|
|
163
|
+
self.ingest.is_some() && (self.settings.sessions || self.settings.hooks)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[derive(Debug)]
|
|
168
|
+
pub enum ConfigError {
|
|
169
|
+
Io(io::Error),
|
|
170
|
+
/// The file exists but is not valid JSON. Deliberately NOT treated as
|
|
171
|
+
/// "absent": silently disabling collection because someone fat-fingered a
|
|
172
|
+
/// comma is the kind of quiet failure this project exists to remove.
|
|
173
|
+
Malformed {
|
|
174
|
+
path: PathBuf,
|
|
175
|
+
detail: String,
|
|
176
|
+
},
|
|
177
|
+
/// A value is present but unusable.
|
|
178
|
+
Invalid(String),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl std::fmt::Display for ConfigError {
|
|
182
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
183
|
+
match self {
|
|
184
|
+
ConfigError::Io(e) => write!(f, "{e}"),
|
|
185
|
+
ConfigError::Malformed { path, detail } => {
|
|
186
|
+
write!(f, "{} is not valid JSON: {detail}", path.display())
|
|
187
|
+
}
|
|
188
|
+
ConfigError::Invalid(m) => write!(f, "{m}"),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl std::error::Error for ConfigError {}
|
|
194
|
+
|
|
195
|
+
impl From<io::Error> for ConfigError {
|
|
196
|
+
fn from(e: io::Error) -> Self {
|
|
197
|
+
ConfigError::Io(e)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Resolve the collector's configuration.
|
|
202
|
+
///
|
|
203
|
+
/// `home` is the failproofai home (`~/.failproofai`), passed rather than
|
|
204
|
+
/// derived so tests need no environment mutation.
|
|
205
|
+
///
|
|
206
|
+
/// Precedence for the endpoint and key is env → file → default, matching every
|
|
207
|
+
/// other knob in this codebase. The env override exists for containers and for
|
|
208
|
+
/// local development against a stack on `localhost`, where writing a file to a
|
|
209
|
+
/// real home directory would be the wrong shape.
|
|
210
|
+
pub fn load(home: &Path) -> Result<CollectorConfig, ConfigError> {
|
|
211
|
+
let ingest = load_ingest(home)?;
|
|
212
|
+
let settings = load_settings(home)?;
|
|
213
|
+
|
|
214
|
+
if settings.environment.contains(',') {
|
|
215
|
+
// Fail here rather than let ingest silently skip every line. The
|
|
216
|
+
// server splits on commas, so this is unrecoverable downstream and
|
|
217
|
+
// invisible upstream — exactly the shape of bug worth erroring on.
|
|
218
|
+
return Err(ConfigError::Invalid(format!(
|
|
219
|
+
"collector environment {:?} contains a comma, which the ingest endpoint rejects; \
|
|
220
|
+
every event from this machine would be silently dropped",
|
|
221
|
+
settings.environment
|
|
222
|
+
)));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let own_spool_dir = home.join("spool");
|
|
226
|
+
let failed_dir = home.join("failed");
|
|
227
|
+
|
|
228
|
+
// Watch our own derived batches first, then the directory the Python SDK
|
|
229
|
+
// and any custom agent already write to. Watching both is what lets
|
|
230
|
+
// failproofaid supersede agenteye-collector without every SDK user having
|
|
231
|
+
// to reconfigure anything: their events keep being collected, by a
|
|
232
|
+
// different daemon, with no change on their side.
|
|
233
|
+
let mut spool_dirs = vec![own_spool_dir.clone()];
|
|
234
|
+
let sdk_spool = agenteye_events_dir();
|
|
235
|
+
if !spool_dirs.contains(&sdk_spool) {
|
|
236
|
+
spool_dirs.push(sdk_spool);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Ok(CollectorConfig {
|
|
240
|
+
ingest,
|
|
241
|
+
settings,
|
|
242
|
+
spool_dirs,
|
|
243
|
+
own_spool_dir,
|
|
244
|
+
failed_dir,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// The directory the AgentEye Python SDK drops event batches into. Honors
|
|
249
|
+
/// `AGENTEYE_HOME` for the same reason the SDK and collector do.
|
|
250
|
+
fn agenteye_events_dir() -> PathBuf {
|
|
251
|
+
let base = std::env::var_os("AGENTEYE_HOME")
|
|
252
|
+
.map(PathBuf::from)
|
|
253
|
+
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".agenteye")))
|
|
254
|
+
.unwrap_or_else(|| PathBuf::from(".agenteye"));
|
|
255
|
+
base.join("events")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn load_ingest(home: &Path) -> Result<Option<Ingest>, ConfigError> {
|
|
259
|
+
let path = home.join(INGEST_FILE);
|
|
260
|
+
|
|
261
|
+
let from_file: Option<Ingest> = match fs::read_to_string(&path) {
|
|
262
|
+
Ok(text) => {
|
|
263
|
+
warn_if_world_readable(&path);
|
|
264
|
+
match serde_json::from_str::<Ingest>(&text) {
|
|
265
|
+
Ok(v) => Some(v),
|
|
266
|
+
Err(e) => {
|
|
267
|
+
return Err(ConfigError::Malformed {
|
|
268
|
+
path,
|
|
269
|
+
detail: e.to_string(),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
|
|
275
|
+
Err(e) => return Err(ConfigError::Io(e)),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
let key = env_nonempty("FAILPROOFAI_INGEST_KEY")
|
|
279
|
+
.or_else(|| from_file.as_ref().map(|i| i.key.clone()));
|
|
280
|
+
let url = env_nonempty("FAILPROOFAI_INGEST_URL")
|
|
281
|
+
.or_else(|| from_file.as_ref().map(|i| i.url.clone()))
|
|
282
|
+
.unwrap_or_else(default_url);
|
|
283
|
+
|
|
284
|
+
// A file with an empty key is "configured but not really" — treat it as
|
|
285
|
+
// absent rather than sending `Authorization: Bearer ` and collecting 401s.
|
|
286
|
+
match key {
|
|
287
|
+
Some(key) if !key.trim().is_empty() => Ok(Some(Ingest { url, key })),
|
|
288
|
+
_ => Ok(None),
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn load_settings(home: &Path) -> Result<Settings, ConfigError> {
|
|
293
|
+
let path = home.join(POLICIES_FILE);
|
|
294
|
+
let text = match fs::read_to_string(&path) {
|
|
295
|
+
Ok(t) => t,
|
|
296
|
+
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Settings::default()),
|
|
297
|
+
Err(e) => return Err(ConfigError::Io(e)),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
let root: serde_json::Value =
|
|
301
|
+
serde_json::from_str(&text).map_err(|e| ConfigError::Malformed {
|
|
302
|
+
path: path.clone(),
|
|
303
|
+
detail: e.to_string(),
|
|
304
|
+
})?;
|
|
305
|
+
|
|
306
|
+
match root.get("collector") {
|
|
307
|
+
None => Ok(Settings::default()),
|
|
308
|
+
Some(v) => serde_json::from_value(v.clone()).map_err(|e| ConfigError::Malformed {
|
|
309
|
+
path,
|
|
310
|
+
detail: format!("the \"collector\" block is not usable: {e}"),
|
|
311
|
+
}),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn env_nonempty(name: &str) -> Option<String> {
|
|
316
|
+
std::env::var(name).ok().filter(|v| !v.trim().is_empty())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Log loudly if the credential file is readable by anyone but its owner.
|
|
320
|
+
///
|
|
321
|
+
/// Warn rather than refuse: the key still works, and disabling collection over
|
|
322
|
+
/// a permission bit would be a confusing failure for something the operator
|
|
323
|
+
/// can fix in one command. But it is never silent — a credential readable by
|
|
324
|
+
/// every local user is worth a line in the journal every startup.
|
|
325
|
+
#[cfg(unix)]
|
|
326
|
+
fn warn_if_world_readable(path: &Path) {
|
|
327
|
+
use std::os::unix::fs::PermissionsExt;
|
|
328
|
+
let Ok(meta) = fs::metadata(path) else { return };
|
|
329
|
+
let mode = meta.permissions().mode() & 0o777;
|
|
330
|
+
if mode & 0o077 != 0 {
|
|
331
|
+
tracing::warn!(
|
|
332
|
+
path = %path.display(),
|
|
333
|
+
mode = format!("{mode:o}"),
|
|
334
|
+
"the ingest credential is readable by other users on this machine; \
|
|
335
|
+
tighten it with `chmod 600`"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#[cfg(not(unix))]
|
|
341
|
+
fn warn_if_world_readable(_path: &Path) {}
|
|
342
|
+
|
|
343
|
+
/// Write the credential file with owner-only permissions.
|
|
344
|
+
///
|
|
345
|
+
/// Used by the setup wizard through the daemon, and by tests. The mode is set
|
|
346
|
+
/// at CREATE time rather than chmod-ed afterwards, so the key is never briefly
|
|
347
|
+
/// world-readable between the write and the fix.
|
|
348
|
+
pub fn write_ingest(home: &Path, ingest: &Ingest) -> Result<PathBuf, ConfigError> {
|
|
349
|
+
fs::create_dir_all(home)?;
|
|
350
|
+
tighten_home(home);
|
|
351
|
+
|
|
352
|
+
let path = home.join(INGEST_FILE);
|
|
353
|
+
let body = serde_json::to_string_pretty(ingest)
|
|
354
|
+
.map_err(|e| ConfigError::Invalid(format!("could not serialize ingest config: {e}")))?;
|
|
355
|
+
|
|
356
|
+
write_private(&path, body.as_bytes())?;
|
|
357
|
+
Ok(path)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[cfg(unix)]
|
|
361
|
+
fn write_private(path: &Path, bytes: &[u8]) -> Result<(), ConfigError> {
|
|
362
|
+
use std::io::Write;
|
|
363
|
+
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|
364
|
+
let mut f = fs::OpenOptions::new()
|
|
365
|
+
.write(true)
|
|
366
|
+
.create(true)
|
|
367
|
+
.truncate(true)
|
|
368
|
+
.mode(CREDENTIAL_MODE)
|
|
369
|
+
.open(path)?;
|
|
370
|
+
f.write_all(bytes)?;
|
|
371
|
+
f.sync_all()?;
|
|
372
|
+
// `mode()` only applies when the file is CREATED, so an existing file that
|
|
373
|
+
// was already too permissive would keep its mode. Set it explicitly too.
|
|
374
|
+
fs::set_permissions(path, fs::Permissions::from_mode(CREDENTIAL_MODE))?;
|
|
375
|
+
Ok(())
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[cfg(not(unix))]
|
|
379
|
+
fn write_private(path: &Path, bytes: &[u8]) -> Result<(), ConfigError> {
|
|
380
|
+
fs::write(path, bytes)?;
|
|
381
|
+
Ok(())
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Best-effort tightening of the failproofai home to owner-only.
|
|
385
|
+
///
|
|
386
|
+
/// It is 0775 on a normal machine because nothing ever needed it otherwise.
|
|
387
|
+
/// Once a credential lives inside it, a world-traversable parent undermines
|
|
388
|
+
/// the 0600 on the file itself. Best-effort because failing to chmod a
|
|
389
|
+
/// directory we do not own must not stop the daemon starting.
|
|
390
|
+
#[cfg(unix)]
|
|
391
|
+
fn tighten_home(home: &Path) {
|
|
392
|
+
use std::os::unix::fs::PermissionsExt;
|
|
393
|
+
let Ok(meta) = fs::metadata(home) else { return };
|
|
394
|
+
let mode = meta.permissions().mode() & 0o777;
|
|
395
|
+
if mode & 0o077 != 0 {
|
|
396
|
+
match fs::set_permissions(home, fs::Permissions::from_mode(0o700)) {
|
|
397
|
+
Ok(()) => tracing::info!(
|
|
398
|
+
path = %home.display(),
|
|
399
|
+
was = format!("{mode:o}"),
|
|
400
|
+
"tightened the failproofai home to 0700 so the ingest credential inside it is not world-readable"
|
|
401
|
+
),
|
|
402
|
+
Err(err) => tracing::warn!(
|
|
403
|
+
path = %home.display(),
|
|
404
|
+
%err,
|
|
405
|
+
"could not tighten the failproofai home; the ingest credential's directory stays group/world-traversable"
|
|
406
|
+
),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#[cfg(not(unix))]
|
|
412
|
+
fn tighten_home(_home: &Path) {}
|