Files
ruvnet--RuView/v2/crates/homecore-assist/src/lib.rs
T
rUv 9b126e927e harden(assist security): bound untrusted utterance (DoS); cmd-injection/ReDoS/NaN/fail-open all proven clean with evidence (#1086)
* fix(homecore-assist): bound untrusted utterance length, fail closed (ADR-133 security)

The intent recognizers accept utterances from untrusted callers (voice
transcripts, the WebSocket `assist` command). Neither the regex nor the
semantic path bounded utterance length, so a pathological multi-megabyte
utterance forced an unbounded `to_lowercase()` clone plus a per-registered-
pattern scan (and, in the semantic path, full tokenisation + feature-hash
embedding) — an allocation/CPU amplification on attacker-controlled input.
The `regex` crate is linear-time (no catastrophic backtracking), so this was
a throughput/memory DoS rather than a hang, but it was still unbounded.

Fix: introduce MAX_UTTERANCE_BYTES (4 KiB — far above any real spoken
command) and check it at both recognizer boundaries BEFORE any allocation or
scan. An over-length utterance fails closed: Ok(None) (no intent, no action),
identical to an unrecognised phrase. No legitimate command is affected.

Pinned by fails-on-old tests:
  - recognizer::over_length_utterance_fails_closed — an over-length utterance
    that contains a valid command resolves to None (would have matched before)
  - semantic_recognizer::over_length_utterance_fails_closed_semantic

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

* test(homecore-assist): pin clean security dimensions with evidence (ADR-133)

Adds regression tests documenting the dimensions reviewed and found clean,
so the properties cannot silently regress:

  - runner: no subprocess surface exists. RufloRunnerOpts.{script_path,env}
    are inert and never executed; even a hostile script_path/env spawns
    nothing. And the entity_id capture class [a-z0-9_ .] strips every shell
    metacharacter, so a resolved slot can never carry ; | & $ ` / etc into a
    (future) argv — sanitisation by construction.
    (shell_metachars_never_survive_into_a_resolved_slot,
     runner_opts_are_inert_no_process_spawned)
  - recognizer: the regex crate is a linear-time finite automaton; a classic
    catastrophic-backtracking shape (a+)+$ on adversarial input completes in
    bounded time — no ReDoS.
    (pathological_backtracking_pattern_completes_in_bounded_time)
  - embedding: embeddings are structurally finite (FNV feature-hash + guarded
    L2 normalise, no external float input, no unguarded division), so a crafted
    utterance cannot inject NaN/Inf to poison cosine k-NN; cosine against the
    zero vector is a finite 0.0, never NaN.
    (embeddings_are_structurally_finite, cosine_with_zero_vector_is_finite_not_nan,
     empty_utterance_against_empty_index_no_panic_no_match)
  - pipeline: injection-shaped utterances never deliver a metacharacter into a
    service call; the worst case resolves to a clean entity token, and an
    unrecognised utterance fails closed to not_understood (no action).
    (pipeline_injection_shaped_utterance_carries_no_metachars_to_service)

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

* docs(homecore-assist): record ADR-133 security review (HC-ASSIST-01 + clean dims)

CHANGELOG [Unreleased] Security entry + ADR-133 section 6 review notes for the
homecore-assist voice/intent pipeline review.

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

62 lines
2.6 KiB
Rust

//! HOMECORE-ASSIST — Voice/intent pipeline + ruflo agent bridge.
//!
//! Implements [ADR-133](../../../docs/adr/ADR-133-homecore-assist-ruflo.md):
//! the Assist pipeline that takes a voice utterance through intent
//! recognition, intent handling, and response synthesis.
//!
//! ## Module layout
//!
//! - [`intent`] — `IntentName`, `Intent`, `IntentResponse`, `Card`
//! - [`recognizer`] — `IntentRecognizer` trait + `RegexIntentRecognizer`
//! - [`semantic_recognizer`] — `SemanticIntentRecognizer`: real embedding +
//! ruvector-core HNSW search over enrolled intent exemplars (`semantic` feature)
//! - [`embedding`] — deterministic feature-hash text embedding (`semantic` feature)
//! - [`handler`] — `IntentHandler` trait + 5 built-in HA-mirroring handlers
//! - [`runner`] — `RufloRunner` trait + `LocalRunner` (real recognizer-backed
//! resolution) + honest `NoopRunner`
//! - [`pipeline`] — `AssistPipeline`: wires recognizer → handler → response
//!
//! ## Implemented capability
//!
//! - Regex-based intent recognition (HA classic intent matching).
//! - Semantic intent recognition: utterance embedding + HNSW nearest-neighbour
//! match against enrolled exemplars, with a configurable similarity threshold
//! and regex fallback below it.
//! - Built-in handlers: `HassTurnOn`, `HassTurnOff`, `HassLightSet`,
//! `HassNevermind`, `HassCancelAll`.
//! - `LocalRunner`: resolves intents locally and returns a real `RufloResponse`
//! with no external process. `NoopRunner` is an explicit, honest no-op (typed
//! `NotStarted` before spawn; explicit empty-response after).
//!
//! ## Data-gated / future
//!
//! - A live `node ruflo-agent.js` LLM subprocess runner (Windows-safe teardown
//! per ADR-133 §Q3) is gated on that script existing; `LocalRunner` is the
//! honest path until it ships.
//! - STT/TTS bridge and satellite protocol (P3).
pub mod intent;
pub mod recognizer;
pub mod semantic_recognizer;
pub mod handler;
pub mod runner;
pub mod pipeline;
/// Deterministic text embedding used by [`semantic_recognizer::SemanticIntentRecognizer`].
#[cfg(feature = "semantic")]
pub mod embedding;
pub use intent::{Card, Intent, IntentName, IntentResponse};
pub use recognizer::{
IntentRecognizer, RecognizerError, RegexIntentRecognizer, MAX_UTTERANCE_BYTES,
};
pub use semantic_recognizer::{SemanticIntentRecognizer, DEFAULT_SIMILARITY_THRESHOLD};
pub use handler::{
HandlerError, HassCancelAll, HassLightSet, HassNevermind, HassTurnOff, HassTurnOn,
IntentHandler,
};
pub use runner::{
AssistError, LocalRunner, NoopRunner, RufloResponse, RufloRunner, RufloRunnerOpts,
};
pub use pipeline::AssistPipeline;