Files
ruvnet--RuView/v2/crates/homecore-recorder/src/lib.rs
T
rUv 41bee64593 fix(recorder): bound history query (memory-DoS) + add missing transactional purge (disk-DoS); SQL-injection & NaN dims clean (#1084)
* fix(homecore-recorder): bound history query + add transactional purge (memory-DoS + disk-DoS)

Security review of the HA-compat state recorder (ADR-132) found two real
bounding bugs; SQL-injection and NaN-index dimensions confirmed clean.

(1) Memory-DoS: get_state_history carried no LIMIT — a wide [since,until]
    window over a high-frequency entity loaded an unbounded row set into a
    single in-memory Vec. Added LIMIT MAX_HISTORY_ROWS (1,000,000); the
    sibling search paths were already k-bounded.

(2) Disk-DoS / documented-but-missing purge: README advertised
    Recorder::purge(older_than) but no retention path existed -> unbounded
    disk growth. Added a transactional purge with an EXCLUSIVE cutoff
    (idempotent, no off-by-one) that deletes old states+events and
    garbage-collects orphaned state_attributes blobs (dedup-shared blobs
    are kept until their last referencing state is gone). All three deletes
    run in one transaction so a mid-purge failure rolls back cleanly.

Pinning tests (homecore-recorder 19->25 no-default / 25->31 ruvector, 0 failed):
- malicious_entity_id_is_stored_literally_not_executed (SQL injection)
- like_metacharacters_in_query_are_literal_not_wildcards (LIKE escape)
- history_query_carries_a_limit_clause (memory-DoS bound)
- purge_keeps_boundary_row_and_drops_older (exclusive-cutoff, true pin)
- purge_gcs_orphaned_attributes_but_keeps_shared (dedup-safe GC)
- purge_also_removes_old_events

No behaviour change beyond the two fixes. Python deterministic proof
unchanged (recorder is off the signal proof path).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-recorder): record ADR-132 security review findings

Add a "3a. Security review" section to ADR-132 and a CHANGELOG [Unreleased]
Security entry covering the homecore-recorder review: SQL-injection and
NaN-index dimensions confirmed clean with evidence (every query bound; LIKE
pattern bound+escaped; SHA-256->i32->f32 embeddings always finite, empty
index/k=0 probed no-panic), plus the two fixes (unbounded history LIMIT,
transactional exclusive-cutoff purge with orphan-attribute GC).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 21:00:52 -04:00

39 lines
1.5 KiB
Rust

//! homecore-recorder — SQLite state history + semantic search.
//!
//! Implements ADR-132: dual-write architecture. P1 ships SQLite structural
//! persistence with an HA-compatible schema (mirrors HA recorder schema v48).
//! P2 (feature `ruvector`) adds a `SemanticIndex` backed by ruvector
//! embeddings for natural-language state queries.
//!
//! ## P1 architecture
//!
//! ```text
//! StateMachine ──broadcast──► RecorderListener ──► Recorder
//! │
//! ┌───────┴──────────┐
//! states state_attributes
//! events recorder_runs
//! ```
//!
//! ## P2 hand-off (ruvector feature)
//!
//! When the `ruvector` feature is enabled, the `Recorder` additionally
//! calls a `SemanticIndex` implementation that embeds state attributes and
//! stores vectors in ruvector for k-NN semantic search. See [`semantic`].
pub mod db;
pub mod dedup;
pub mod listener;
pub mod schema;
#[cfg(feature = "ruvector")]
pub mod semantic;
// Re-export the primary public API surface.
pub use db::{PurgeStats, Recorder, RecorderError, StateRow, MAX_HISTORY_ROWS};
pub use listener::RecorderListener;
/// Null semantic index used when the `ruvector` feature is off.
/// Satisfies the [`db::SemanticIndex`] trait bound without any allocation.
pub use db::NullSemanticIndex;