diff --git a/v2/Cargo.lock b/v2/Cargo.lock index f328f429..05a322d1 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -3214,6 +3214,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -3676,7 +3695,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -3700,6 +3719,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "httparse", @@ -3726,6 +3746,21 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.37", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -3760,9 +3795,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -6769,11 +6806,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-rustls", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -6787,7 +6824,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", "tower-service", @@ -6807,16 +6844,20 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", + "hyper-rustls 0.27.9", "hyper-tls", "hyper-util", "js-sys", "log", + "mime", "mime_guess", "native-tls", "percent-encoding", @@ -7426,7 +7467,9 @@ dependencies = [ "nalgebra", "ort", "rand 0.8.5", + "reqwest 0.12.28", "serde", + "serde_json", "sha2", "thiserror 2.0.18", "tokio", @@ -8560,7 +8603,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -8573,6 +8627,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -9227,6 +9291,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + [[package]] name = "tokio-serial" version = "5.4.5" @@ -11365,6 +11439,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.1.2" diff --git a/v2/crates/ruview-swarm/Cargo.toml b/v2/crates/ruview-swarm/Cargo.toml index afc98444..64912e90 100644 --- a/v2/crates/ruview-swarm/Cargo.toml +++ b/v2/crates/ruview-swarm/Cargo.toml @@ -16,12 +16,14 @@ onnx = ["dep:ort"] simulation = [] demo = ["simulation"] full = ["mavlink", "onnx", "demo", "itar-unrestricted"] +ruflo = ["dep:reqwest", "dep:serde_json"] [dependencies] wifi-densepose-core = { path = "../wifi-densepose-core" } # Serialization serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", optional = true } toml = "0.8" # Async runtime @@ -34,6 +36,9 @@ mavlink = { version = "0.13", optional = true } # ONNX Runtime (optional — for MARL actor inference) ort = { version = "2.0.0-rc.11", optional = true } +# HTTP client (optional — for Ruflo HTTP backend) +reqwest = { version = "0.12", features = ["json"], optional = true } + # Crypto — MAVLink v2 HMAC-SHA256 signing hmac = "0.12" sha2 = "0.10" diff --git a/v2/crates/ruview-swarm/src/lib.rs b/v2/crates/ruview-swarm/src/lib.rs index 7d43e551..b0f87de2 100644 --- a/v2/crates/ruview-swarm/src/lib.rs +++ b/v2/crates/ruview-swarm/src/lib.rs @@ -16,6 +16,7 @@ pub mod demo; pub mod integration; pub mod bench_support; pub mod orchestrator; +pub mod ruflo; pub use types::{ ClusterId, CsiDetection, DroneState, FailSafeState, GridCell, NodeId, diff --git a/v2/crates/ruview-swarm/src/orchestrator/mod.rs b/v2/crates/ruview-swarm/src/orchestrator/mod.rs index b6d97b71..c06d3332 100644 --- a/v2/crates/ruview-swarm/src/orchestrator/mod.rs +++ b/v2/crates/ruview-swarm/src/orchestrator/mod.rs @@ -37,6 +37,13 @@ pub struct SwarmOrchestrator { pub peer_detections: Vec, /// Accumulated mission statistics. pub stats: MissionStats, + /// Optional Ruflo backend for AgentDB, AIDefence, and SONA intelligence. + /// When None (default), all Ruflo calls are no-ops — existing behaviour preserved. + #[cfg(feature = "ruflo")] + pub ruflo: Option>, + /// Active trajectory ID issued by the Ruflo intelligence hooks. + #[cfg(feature = "ruflo")] + pub trajectory_id: Option, } /// Accumulated metrics for one mission run. @@ -102,6 +109,10 @@ impl SwarmOrchestrator { peer_states: HashMap::new(), peer_detections: Vec::new(), stats: MissionStats::default(), + #[cfg(feature = "ruflo")] + ruflo: None, + #[cfg(feature = "ruflo")] + trajectory_id: None, } } @@ -176,6 +187,94 @@ impl SwarmOrchestrator { self.peer_detections.push(det); } + /// Attach a Ruflo backend for AgentDB pattern learning, AIDefence, and SONA. + /// + /// Call after `new_demo()`: + /// ```ignore + /// let orch = SwarmOrchestrator::new_demo(...) + /// .with_ruflo(Box::new(MockRufloBackend::new())); + /// ``` + #[cfg(feature = "ruflo")] + pub fn with_ruflo(mut self, backend: Box) -> Self { + self.ruflo = Some(backend); + self + } + + /// Start a Ruflo intelligence trajectory for this mission node. + /// + /// Call before the mission loop begins. If no backend is attached this is a no-op. + #[cfg(feature = "ruflo")] + pub async fn start_trajectory(&mut self, mission_desc: &str) { + if let Some(ruflo) = &self.ruflo { + match ruflo.trajectory_start(mission_desc, "swarm-specialist").await { + Ok(tid) => self.trajectory_id = Some(tid), + Err(e) => tracing::warn!("trajectory_start failed: {}", e), + } + } + } + + /// End the Ruflo trajectory and persist the mission summary in AgentDB. + /// + /// Stores both a searchable memory entry and a pattern-learned description. + /// If no backend is attached this is a no-op. + #[cfg(feature = "ruflo")] + pub async fn finish_trajectory(&mut self, success: bool, mission_key: &str) { + if let Some(ruflo) = &self.ruflo { + let tid = self.trajectory_id.take(); + if let Some(tid) = &tid { + let _ = ruflo.trajectory_end(tid, success, None).await; + } + // Build and serialise mission summary. + let summary = crate::ruflo::MissionSummary::from_stats( + &self.stats, + &self.config.mission.profile, + 1, // single drone; caller sets correct count via separate API if needed + self.config.mission.area_width_m, + self.config.mission.area_height_m, + 0, // caller sets victims_total; 0 = unknown + self.probability_grid.coverage_pct(), + ); + if let Ok(json) = serde_json::to_string(&summary) { + let _ = ruflo.store_mission(mission_key, &json, "swarm-missions").await; + } + let _ = ruflo.store_pattern( + &summary.to_pattern_description(), + summary.pattern_type(), + summary.pattern_confidence(), + ).await; + } + } + + /// AIDefence-checked variant of `receive_peer_detection`. + /// + /// Returns `true` and enqueues the detection if it passes the safety check. + /// Returns `false` (and drops the detection) if AIDefence flags it as unsafe. + /// Falls back to `true` (accept) if the Ruflo backend is not attached or the + /// check itself errors (fail-open to avoid blocking legitimate traffic). + #[cfg(feature = "ruflo")] + pub async fn receive_peer_detection_checked(&mut self, det: CsiDetection) -> bool { + if let Some(ruflo) = &self.ruflo { + // Serialise the detection to a string for AIDefence inspection. + let repr = format!( + "drone_id={:?} confidence={:.3} victim={:?}", + det.drone_id, det.confidence, det.victim_position + ); + match ruflo.mavlink_is_safe(&repr).await { + Ok(false) => { + tracing::warn!( + "aidefence rejected peer detection from {:?}", + det.drone_id + ); + return false; + } + Err(e) => tracing::debug!("aidefence check failed (proceeding): {}", e), + _ => {} + } + } + self.receive_peer_detection(det); + true + } + /// Returns true when the mission is considered complete. pub fn is_mission_complete(&self) -> bool { self.probability_grid.coverage_pct() > 0.95 diff --git a/v2/crates/ruview-swarm/src/ruflo/backend.rs b/v2/crates/ruview-swarm/src/ruflo/backend.rs new file mode 100644 index 00000000..f124351a --- /dev/null +++ b/v2/crates/ruview-swarm/src/ruflo/backend.rs @@ -0,0 +1,69 @@ +//! RufloBackend trait and shared types. +use async_trait::async_trait; + +/// Error type for Ruflo backend operations. +#[derive(Debug, thiserror::Error)] +pub enum RufloError { + #[error("network error: {0}")] + Network(String), + #[error("tool error: {0}")] + Tool(String), + #[error("serialization error: {0}")] + Serialize(String), +} + +/// A past mission retrieved from AgentDB memory. +#[derive(Debug, Clone, serde::Deserialize, Default)] +pub struct MissionMemoryEntry { + pub key: String, + pub value: String, // JSON-encoded mission summary + pub score: f32, +} + +/// A coordination pattern retrieved from AgentDB pattern store. +#[derive(Debug, Clone, serde::Deserialize, Default)] +pub struct PatternEntry { + pub pattern: String, + pub pattern_type: String, + pub confidence: f32, + pub score: f32, +} + +/// Result of an AIDefence MAVLink message scan. +#[derive(Debug, Clone)] +pub struct MavlinkScanResult { + pub safe: bool, + pub threats: Vec, +} + +/// Core Ruflo capability trait. +/// +/// Two implementations: +/// - `HttpRufloBackend` (feature=ruflo): calls the claude-flow daemon at localhost:3000 +/// - `MockRufloBackend`: in-memory mock for testing (always available) +#[async_trait] +pub trait RufloBackend: Send + Sync { + // ── MissionMemory (claude-flow: memory_store / memory_search) ──── + async fn store_mission(&self, key: &str, summary: &str, namespace: &str) + -> Result<(), RufloError>; + async fn search_missions(&self, query: &str, limit: usize, namespace: &str) + -> Result, RufloError>; + + // ── PatternLearner (agentdb_pattern-store / agentdb_pattern-search) ─ + async fn store_pattern(&self, pattern: &str, pattern_type: &str, confidence: f32) + -> Result<(), RufloError>; + async fn search_patterns(&self, query: &str, top_k: usize, min_confidence: f32) + -> Result, RufloError>; + + // ── MavlinkDefence (aidefence_is_safe / aidefence_scan) ────────── + async fn mavlink_is_safe(&self, message_repr: &str) -> Result; + async fn mavlink_scan(&self, message_repr: &str) -> Result; + + // ── IntelligenceHooks (hooks_intelligence_trajectory-*) ────────── + async fn trajectory_start(&self, task: &str, agent: &str) + -> Result; // returns trajectoryId + async fn trajectory_step(&self, trajectory_id: &str, action: &str, result: &str, quality: f32) + -> Result<(), RufloError>; + async fn trajectory_end(&self, trajectory_id: &str, success: bool, feedback: Option<&str>) + -> Result<(), RufloError>; +} diff --git a/v2/crates/ruview-swarm/src/ruflo/http_backend.rs b/v2/crates/ruview-swarm/src/ruflo/http_backend.rs new file mode 100644 index 00000000..c40d8e46 --- /dev/null +++ b/v2/crates/ruview-swarm/src/ruflo/http_backend.rs @@ -0,0 +1,164 @@ +//! HTTP backend that calls the claude-flow daemon via JSON-RPC 2.0. +//! Default endpoint: http://localhost:3000/rpc +//! +//! Start the daemon with: npx @claude-flow/cli@latest daemon start + +use async_trait::async_trait; +use std::sync::atomic::{AtomicU64, Ordering}; +use super::backend::*; + +pub struct HttpRufloBackend { + client: reqwest::Client, + base_url: String, + request_id: AtomicU64, +} + +impl HttpRufloBackend { + pub fn new(base_url: &str) -> Self { + Self { + client: reqwest::Client::new(), + base_url: base_url.trim_end_matches('/').to_string(), + request_id: AtomicU64::new(1), + } + } + + pub fn localhost() -> Self { Self::new("http://localhost:3000") } + + async fn call_tool( + &self, + tool: &str, + args: serde_json::Value, + ) -> Result { + let id = self.request_id.fetch_add(1, Ordering::SeqCst); + let body = serde_json::json!({ + "jsonrpc": "2.0", + "method": "tools/call", + "id": id, + "params": { "name": tool, "arguments": args } + }); + + let resp = self.client + .post(format!("{}/rpc", self.base_url)) + .json(&body) + .send() + .await + .map_err(|e| RufloError::Network(e.to_string()))?; + + let json: serde_json::Value = resp.json().await + .map_err(|e| RufloError::Serialize(e.to_string()))?; + + if let Some(err) = json.get("error") { + return Err(RufloError::Tool(err.to_string())); + } + + Ok(json["result"].clone()) + } +} + +#[async_trait] +impl RufloBackend for HttpRufloBackend { + async fn store_mission(&self, key: &str, value: &str, namespace: &str) + -> Result<(), RufloError> + { + self.call_tool("memory_store", serde_json::json!({ + "key": key, "value": value, "namespace": namespace + })).await?; + Ok(()) + } + + async fn search_missions(&self, query: &str, limit: usize, namespace: &str) + -> Result, RufloError> + { + let result = self.call_tool("memory_search", serde_json::json!({ + "query": query, "namespace": namespace, "limit": limit + })).await?; + let entries: Vec = serde_json::from_value(result) + .unwrap_or_default(); + Ok(entries) + } + + async fn store_pattern(&self, pattern: &str, pattern_type: &str, confidence: f32) + -> Result<(), RufloError> + { + self.call_tool("agentdb_pattern-store", serde_json::json!({ + "pattern": pattern, "type": pattern_type, "confidence": confidence + })).await?; + Ok(()) + } + + async fn search_patterns(&self, query: &str, top_k: usize, min_confidence: f32) + -> Result, RufloError> + { + let result = self.call_tool("agentdb_pattern-search", serde_json::json!({ + "query": query, "topK": top_k, "minConfidence": min_confidence + })).await?; + let entries: Vec = serde_json::from_value( + result["results"].clone() + ).unwrap_or_default(); + Ok(entries) + } + + async fn mavlink_is_safe(&self, message_repr: &str) -> Result { + let result = self.call_tool("aidefence_is_safe", serde_json::json!({ + "input": message_repr + })).await?; + Ok(result["safe"].as_bool().unwrap_or(true)) + } + + async fn mavlink_scan(&self, message_repr: &str) -> Result { + let result = self.call_tool("aidefence_scan", serde_json::json!({ + "input": message_repr, "quick": false + })).await?; + let safe = result["safe"].as_bool().unwrap_or(true); + let threats: Vec = result["threats"] + .as_array() + .map(|a| a.iter().filter_map(|v| v["type"].as_str().map(String::from)).collect()) + .unwrap_or_default(); + Ok(MavlinkScanResult { safe, threats }) + } + + async fn trajectory_start(&self, task: &str, agent: &str) + -> Result + { + let result = self.call_tool("hooks_intelligence_trajectory-start", serde_json::json!({ + "task": task, "agent": agent + })).await?; + Ok(result["trajectoryId"] + .as_str() + .unwrap_or("unknown-traj") + .to_string()) + } + + async fn trajectory_step( + &self, + trajectory_id: &str, + action: &str, + result_str: &str, + quality: f32, + ) -> Result<(), RufloError> { + self.call_tool("hooks_intelligence_trajectory-step", serde_json::json!({ + "trajectoryId": trajectory_id, + "action": action, + "result": result_str, + "quality": quality + })).await?; + Ok(()) + } + + async fn trajectory_end( + &self, + trajectory_id: &str, + success: bool, + feedback: Option<&str>, + ) -> Result<(), RufloError> { + let mut args = serde_json::json!({ + "trajectoryId": trajectory_id, + "success": success + }); + if let Some(fb) = feedback { + args["feedback"] = fb.into(); + } + self.call_tool("hooks_intelligence_trajectory-end", args).await?; + Ok(()) + } +} diff --git a/v2/crates/ruview-swarm/src/ruflo/mission_summary.rs b/v2/crates/ruview-swarm/src/ruflo/mission_summary.rs new file mode 100644 index 00000000..ed351de6 --- /dev/null +++ b/v2/crates/ruview-swarm/src/ruflo/mission_summary.rs @@ -0,0 +1,125 @@ +//! Serializable mission summary stored in AgentDB memory after each completed mission. +use serde::{Deserialize, Serialize}; +use crate::orchestrator::MissionStats; + +/// Serializable summary of a completed mission stored in AgentDB. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MissionSummary { + pub mission_profile: String, + pub num_drones: usize, + pub area_width_m: f64, + pub area_height_m: f64, + pub victims_total: usize, + pub victims_confirmed: u32, + pub cells_covered: u32, + pub coverage_pct: f64, + pub elapsed_secs: f64, + pub collision_events: u32, + pub localization_error_m: Option, +} + +impl MissionSummary { + pub fn from_stats( + stats: &MissionStats, + profile: &str, + num_drones: usize, + area_width: f64, + area_height: f64, + victims_total: usize, + coverage_pct: f64, + ) -> Self { + Self { + mission_profile: profile.to_string(), + num_drones, + area_width_m: area_width, + area_height_m: area_height, + victims_total, + victims_confirmed: stats.victims_confirmed, + cells_covered: stats.cells_covered, + coverage_pct, + elapsed_secs: stats.elapsed_secs, + collision_events: stats.collision_events, + localization_error_m: None, + } + } + + /// Pattern description for AgentDB pattern-store — human-readable. + pub fn to_pattern_description(&self) -> String { + format!( + "{} mission: {} drones over {}x{}m, {} victims confirmed in {:.1}s, {:.0}% coverage, {} collisions", + self.mission_profile, + self.num_drones, + self.area_width_m as u32, + self.area_height_m as u32, + self.victims_confirmed, + self.elapsed_secs, + self.coverage_pct * 100.0, + self.collision_events, + ) + } + + /// Pattern type tag for AgentDB. + pub fn pattern_type(&self) -> &str { + match self.mission_profile.as_str() { + "sar" => "sar-mission", + "inspection" => "inspection-mission", + "mine" => "mine-mission", + _ => "swarm-mission", + } + } + + /// Confidence score (0-1) for AgentDB based on mission outcomes. + pub fn pattern_confidence(&self) -> f32 { + let victim_score = if self.victims_total > 0 { + self.victims_confirmed as f32 / self.victims_total as f32 + } else { + 0.5 + }; + let coverage_score = self.coverage_pct as f32; + let collision_penalty = (self.collision_events as f32 * 0.1).min(0.5); + ((victim_score * 0.5 + coverage_score * 0.5) - collision_penalty).clamp(0.0, 1.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_stats(victims_confirmed: u32, cells_covered: u32, collision_events: u32) -> MissionStats { + MissionStats { + cells_covered, + victims_confirmed, + collision_events, + steps: 100, + elapsed_secs: 30.0, + } + } + + #[test] + fn test_pattern_type_tags() { + let stats = make_stats(2, 80, 0); + let s = MissionSummary::from_stats(&stats, "sar", 4, 400.0, 400.0, 3, 0.85); + assert_eq!(s.pattern_type(), "sar-mission"); + + let s2 = MissionSummary::from_stats(&stats, "custom", 2, 200.0, 200.0, 0, 0.5); + assert_eq!(s2.pattern_type(), "swarm-mission"); + } + + #[test] + fn test_pattern_confidence_penalises_collisions() { + let no_collisions = make_stats(3, 80, 0); + let with_collisions = make_stats(3, 80, 4); + let s_good = MissionSummary::from_stats(&no_collisions, "sar", 4, 400.0, 400.0, 3, 0.9); + let s_bad = MissionSummary::from_stats(&with_collisions, "sar", 4, 400.0, 400.0, 3, 0.9); + assert!(s_good.pattern_confidence() > s_bad.pattern_confidence()); + } + + #[test] + fn test_to_pattern_description_contains_profile() { + let stats = make_stats(1, 50, 0); + let s = MissionSummary::from_stats(&stats, "inspection", 2, 100.0, 100.0, 1, 0.75); + let desc = s.to_pattern_description(); + assert!(desc.contains("inspection"), "description should include profile: {desc}"); + assert!(desc.contains("2 drones"), "description should include drone count: {desc}"); + } +} diff --git a/v2/crates/ruview-swarm/src/ruflo/mock_backend.rs b/v2/crates/ruview-swarm/src/ruflo/mock_backend.rs new file mode 100644 index 00000000..6a70c13b --- /dev/null +++ b/v2/crates/ruview-swarm/src/ruflo/mock_backend.rs @@ -0,0 +1,158 @@ +//! In-memory mock RufloBackend for testing — no network, zero latency. +use async_trait::async_trait; +use std::sync::{Arc, Mutex}; +use super::backend::*; + +/// Configurable mock. All writes go to in-memory vecs; searches return stored items. +pub struct MockRufloBackend { + pub missions: Arc>>, // (key, value) + pub patterns: Arc>>, // (pattern, type, confidence) + pub scan_safe: bool, // set false to simulate a detected threat + pub traj_ids: Arc>>, +} + +impl Default for MockRufloBackend { + fn default() -> Self { + Self { + missions: Arc::new(Mutex::new(Vec::new())), + patterns: Arc::new(Mutex::new(Vec::new())), + scan_safe: true, + traj_ids: Arc::new(Mutex::new(Vec::new())), + } + } +} + +impl MockRufloBackend { + pub fn new() -> Self { Self::default() } + + /// Pre-load a past mission for search to return. + pub fn seed_mission(&self, key: &str, value: &str) { + self.missions.lock().unwrap().push((key.to_string(), value.to_string())); + } + + /// Pre-load a pattern for search to return. + pub fn seed_pattern(&self, pattern: &str, ptype: &str, confidence: f32) { + self.patterns.lock().unwrap().push((pattern.to_string(), ptype.to_string(), confidence)); + } + + /// Configure the scanner to reject the next message. + pub fn reject_next(self) -> Self { Self { scan_safe: false, ..self } } +} + +#[async_trait] +impl RufloBackend for MockRufloBackend { + async fn store_mission(&self, key: &str, value: &str, _ns: &str) -> Result<(), RufloError> { + self.missions.lock().unwrap().push((key.to_string(), value.to_string())); + Ok(()) + } + + async fn search_missions(&self, query: &str, limit: usize, _ns: &str) + -> Result, RufloError> + { + let missions = self.missions.lock().unwrap(); + Ok(missions.iter().take(limit).map(|(k, v)| MissionMemoryEntry { + key: k.clone(), + value: v.clone(), + score: if v.contains(query) { 0.9 } else { 0.5 }, + }).collect()) + } + + async fn store_pattern(&self, pattern: &str, ptype: &str, confidence: f32) + -> Result<(), RufloError> + { + self.patterns.lock().unwrap().push((pattern.to_string(), ptype.to_string(), confidence)); + Ok(()) + } + + async fn search_patterns(&self, _query: &str, top_k: usize, min_conf: f32) + -> Result, RufloError> + { + let patterns = self.patterns.lock().unwrap(); + Ok(patterns.iter() + .filter(|(_, _, c)| *c >= min_conf) + .take(top_k) + .map(|(p, t, c)| PatternEntry { + pattern: p.clone(), + pattern_type: t.clone(), + confidence: *c, + score: *c, + }) + .collect()) + } + + async fn mavlink_is_safe(&self, _msg: &str) -> Result { + Ok(self.scan_safe) + } + + async fn mavlink_scan(&self, _msg: &str) -> Result { + Ok(MavlinkScanResult { + safe: self.scan_safe, + threats: if self.scan_safe { + vec![] + } else { + vec!["suspicious_coordinates".into()] + }, + }) + } + + async fn trajectory_start(&self, task: &str, _agent: &str) + -> Result + { + let id = format!("mock-traj-{}", task.len()); // deterministic for testing + self.traj_ids.lock().unwrap().push(id.clone()); + Ok(id) + } + + async fn trajectory_step(&self, _id: &str, _act: &str, _res: &str, _q: f32) + -> Result<(), RufloError> { Ok(()) } + + async fn trajectory_end(&self, _id: &str, _ok: bool, _fb: Option<&str>) + -> Result<(), RufloError> { Ok(()) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_mock_store_and_search_mission() { + let mock = MockRufloBackend::new(); + mock.store_mission("m1", r#"{"victims":2}"#, "swarm-missions").await.unwrap(); + let results = mock.search_missions("victims", 5, "swarm-missions").await.unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].key, "m1"); + assert!(results[0].score > 0.5, "keyword match should score high"); + } + + #[tokio::test] + async fn test_mock_pattern_lifecycle() { + let mock = MockRufloBackend::new(); + mock.store_pattern("approach from 3 angles when P > 0.7", "sar-trajectory", 0.9).await.unwrap(); + let results = mock.search_patterns("SAR convergence", 5, 0.5).await.unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].confidence, 0.9); + } + + #[tokio::test] + async fn test_mock_mavlink_defence_safe() { + let mock = MockRufloBackend::new(); + assert!(mock.mavlink_is_safe(r#"{"drone_id":1,"confidence":0.8}"#).await.unwrap()); + } + + #[tokio::test] + async fn test_mock_mavlink_defence_rejected() { + let mock = MockRufloBackend { scan_safe: false, ..Default::default() }; + let scan = mock.mavlink_scan("SUSPICIOUS MESSAGE").await.unwrap(); + assert!(!scan.safe); + assert!(!scan.threats.is_empty()); + } + + #[tokio::test] + async fn test_mock_trajectory_lifecycle() { + let mock = MockRufloBackend::new(); + let tid = mock.trajectory_start("SAR 400x400", "swarm-specialist").await.unwrap(); + mock.trajectory_step(&tid, "scan (5,3)", "prob=0.6", 0.7).await.unwrap(); + mock.trajectory_end(&tid, true, Some("victim found")).await.unwrap(); + assert!(!mock.traj_ids.lock().unwrap().is_empty()); + } +} diff --git a/v2/crates/ruview-swarm/src/ruflo/mod.rs b/v2/crates/ruview-swarm/src/ruflo/mod.rs new file mode 100644 index 00000000..c1e7dc41 --- /dev/null +++ b/v2/crates/ruview-swarm/src/ruflo/mod.rs @@ -0,0 +1,22 @@ +//! Ruflo AI-agent capabilities integration. +//! +//! Integrates the claude-flow daemon's AgentDB, AIDefence, and SONA intelligence +//! hooks into the ruview-swarm orchestrator via a trait-based backend. +//! +//! Feature gate: `ruflo`. The `RufloBackend` trait and `MockRufloBackend` are always +//! compiled so tests can use them without enabling the `ruflo` feature. Only +//! `HttpRufloBackend` (which requires `reqwest` + `serde_json`) is gated. + +pub mod backend; +pub mod mock_backend; +pub mod mission_summary; + +#[cfg(feature = "ruflo")] +pub mod http_backend; + +pub use backend::{RufloBackend, RufloError, MissionMemoryEntry, PatternEntry, MavlinkScanResult}; +pub use mock_backend::MockRufloBackend; +pub use mission_summary::MissionSummary; + +#[cfg(feature = "ruflo")] +pub use http_backend::HttpRufloBackend;