diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 962cc958d..e041e8c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -266,6 +266,13 @@ jobs: if: steps.crates.outputs.present == 'true' run: cargo clippy --workspace --all-targets -- -D warnings + # AgentEye's server depends on fpai-collect WITHOUT the `sqlite` feature + # (its sqlx links a different libsqlite3-sys). The workspace build always + # has the feature on, so nothing else here would notice that shape break. + - name: cargo clippy (fpai-collect without sqlite) + if: steps.crates.outputs.present == 'true' + run: cargo clippy -p fpai-collect --no-default-features --all-targets -- -D warnings + - name: cargo test if: steps.crates.outputs.present == 'true' run: cargo test --workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index 159a37af2..31440938d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The stable release of logging in to FailproofAI Cloud from `config` in your brow - **Every agent on the machine knows FailproofAI.** `config` adds a small `failproofai` skill to each agent it finds (and any you tick), in that agent's own skills folder: Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, every Hermes and OpenClaw profile, droid, Devin, Antigravity and Goose. It points the agent at the FailproofAI skills on GitHub, which it reads when a FailproofAI task comes up, so what it reads is always the current version with nothing to update. Each folder was checked against the agent itself. A `failproofai` skill you installed yourself (for example with `npx skills add FailproofAI/skills`) is left as it is, and Codex, which would list one in `~/.agents/skills` beside its own copy, is left to that one. `failproofai uninstall` removes only the ones `config` wrote (#920) +- `fpai-collect` can now build without SQLite: its SQLite pollers are behind a default-on `sqlite` Cargo feature. AgentEye uses `default-features = false` to import Claude Code and Codex transcripts with the daemon's converters and redaction, while the daemon's default build remains unchanged (#929) + ### Fixes - `config` on a machine with no saved connection, whose browser login was approved but whose key could not be claimed, no longer says "This machine keeps its previous connection." and reports cloud mode with no key on disk; it finishes set up as open source (#920) diff --git a/crates/fpai-collect/Cargo.toml b/crates/fpai-collect/Cargo.toml index f0745e446..8ca2ed51e 100644 --- a/crates/fpai-collect/Cargo.toml +++ b/crates/fpai-collect/Cargo.toml @@ -45,7 +45,12 @@ notify = "8" # A real libsqlite3 is not optional here: goose's sessions.db main file contains # ZERO tables, with all 1.25 MB of schema and rows in an uncheckpointed -wal. # Anything that parses the main file directly reads an empty database. -rusqlite = { version = "0.40", features = ["bundled"] } +# +# Optional (the `sqlite` feature, on by default) so a consumer that only needs +# the file-tailer converters can leave it out. AgentEye's server imports Claude +# Code / Codex transcripts with these converters, and its sqlx links a different +# libsqlite3-sys — two crates linking `sqlite3` cannot share one build. +rusqlite = { version = "0.40", features = ["bundled"], optional = true } # OpenClaw 2026.9.6+ stores a large transcript event zstd-compressed in # `event_zstd` and leaves `event_json` NULL. A pure-Rust decoder, for the same # reason rustls is chosen above: no C toolchain on the four cross-compiled legs. @@ -57,9 +62,44 @@ time = { version = "0.3", default-features = false, features = ["std", "formatti getrandom = "0.4" sha2 = "0.11" +[features] +default = ["sqlite"] +# The SQLite pollers (goose, opencode, hermes, devin, OpenClaw 2026.9.2+). +# failproofaid always has it; the file tailers and redaction never need it. +sqlite = ["dep:rusqlite"] + [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync", "test-util", "net"] } wiremock = "0.6" # The HTTPS server behind the private-CA upload test. Already in the lockfile # under reqwest; `ring` rather than the default aws-lc-rs, which is not. tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } + +# Tests that build real SQLite databases. Skipped, not failed, without `sqlite`. +[[test]] +name = "devin_source" +required-features = ["sqlite"] + +[[test]] +name = "extra_paths_engine" +required-features = ["sqlite"] + +[[test]] +name = "goose_source" +required-features = ["sqlite"] + +[[test]] +name = "hermes_source" +required-features = ["sqlite"] + +[[test]] +name = "human_input" +required-features = ["sqlite"] + +[[test]] +name = "openclaw_source" +required-features = ["sqlite"] + +[[test]] +name = "opencode_source" +required-features = ["sqlite"] diff --git a/crates/fpai-collect/src/lib.rs b/crates/fpai-collect/src/lib.rs index 838f50cd4..c32c2fa8a 100644 --- a/crates/fpai-collect/src/lib.rs +++ b/crates/fpai-collect/src/lib.rs @@ -22,6 +22,7 @@ pub mod otlp; pub mod redact; pub mod sources; pub mod spool; +#[cfg(feature = "sqlite")] pub mod sqlitepoll; pub mod supervisor; #[cfg(test)] diff --git a/crates/fpai-collect/src/sources/cursor/transform.rs b/crates/fpai-collect/src/sources/cursor/transform.rs index 50e39ecf3..6d9fcd7d1 100644 --- a/crates/fpai-collect/src/sources/cursor/transform.rs +++ b/crates/fpai-collect/src/sources/cursor/transform.rs @@ -17,7 +17,7 @@ use crate::filetail::Ctx; // Reused, not re-cloned: `sanitize_id_part` MUST agree with the hook source's // agent-id scheme, and `to_rfc3339_micros` is a pure epoch→RFC3339 formatter. use crate::sources::claude::transform::sanitize_id_part; -use crate::sources::goose::transform::to_rfc3339_micros; +use crate::sources::to_rfc3339_micros; /// Longest session goal (the first user prompt) kept. const MAX_GOAL_CHARS: usize = 500; diff --git a/crates/fpai-collect/src/sources/goose/transform.rs b/crates/fpai-collect/src/sources/goose/transform.rs index 63e1fd7fb..b159243f9 100644 --- a/crates/fpai-collect/src/sources/goose/transform.rs +++ b/crates/fpai-collect/src/sources/goose/transform.rs @@ -140,20 +140,7 @@ pub fn slot_micros(row_id: i64, slot: u32) -> u32 { base + slot.min(SLOT_END) } -/// Format epoch milliseconds as the RFC3339-with-microseconds string ingest -/// requires. -/// -/// Microseconds are forced rather than left to RFC3339's variable precision, so -/// every source emits one shape and the hook stream and this one sort against -/// each other on a shared session timeline. -pub fn to_rfc3339_micros(epoch_ms: i64, micros: u32) -> Option { - let nanos = (epoch_ms as i128) * 1_000_000 + (micros as i128) * 1_000; - let dt = time::OffsetDateTime::from_unix_timestamp_nanos(nanos).ok()?; - const FMT: &[time::format_description::BorrowedFormatItem<'_>] = time::macros::format_description!( - "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z" - ); - dt.format(FMT).ok() -} +pub use crate::sources::to_rfc3339_micros; /// A row's timestamp for one slot, or `None` when the row's time is unusable. /// diff --git a/crates/fpai-collect/src/sources/mod.rs b/crates/fpai-collect/src/sources/mod.rs index 39840f079..6c7c72cf0 100644 --- a/crates/fpai-collect/src/sources/mod.rs +++ b/crates/fpai-collect/src/sources/mod.rs @@ -22,17 +22,39 @@ pub mod claude; pub mod codex; pub mod copilot; pub mod cursor; +#[cfg(feature = "sqlite")] pub mod devin; pub mod factory; +#[cfg(feature = "sqlite")] pub mod goose; +#[cfg(feature = "sqlite")] pub mod hermes; pub mod hooks; pub mod openclaw; +#[cfg(feature = "sqlite")] pub mod opencode; pub mod pi; use serde_json::{Map, Value, json}; +/// Format epoch milliseconds as the RFC3339-with-microseconds string ingest +/// requires. +/// +/// Microseconds are forced rather than left to RFC3339's variable precision, so +/// every source emits one shape and the hook stream and this one sort against +/// each other on a shared session timeline. +/// +/// Lives here rather than in `goose` (which re-exports it) so the cursor file +/// source keeps it when the `sqlite` feature, and with it goose, is off. +pub fn to_rfc3339_micros(epoch_ms: i64, micros: u32) -> Option { + let nanos = (epoch_ms as i128) * 1_000_000 + (micros as i128) * 1_000; + let dt = time::OffsetDateTime::from_unix_timestamp_nanos(nanos).ok()?; + const FMT: &[time::format_description::BorrowedFormatItem<'_>] = time::macros::format_description!( + "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z" + ); + dt.format(FMT).ok() +} + /// The `human_input` twin of a prompt a person typed. /// /// Every harness writes whatever reached the model as `role: user` — cron diff --git a/crates/fpai-collect/src/sources/openclaw/mod.rs b/crates/fpai-collect/src/sources/openclaw/mod.rs index bc9fe1d65..8593c9855 100644 --- a/crates/fpai-collect/src/sources/openclaw/mod.rs +++ b/crates/fpai-collect/src/sources/openclaw/mod.rs @@ -57,6 +57,7 @@ //! model snapshots) advance the session clock but emit nothing — they describe //! the harness, not the conversation. +#[cfg(feature = "sqlite")] pub mod sqlite; pub mod transform;