feat(swarm): integrate Ruflo AI-agent capabilities into ruview-swarm

Adds a feature-gated Ruflo integration layer connecting ruview-swarm to the
claude-flow daemon's AgentDB, AIDefence, and SONA intelligence subsystems.
Default build is unaffected (all paths behind `Option<Box<dyn RufloBackend>>`).

## New module: src/ruflo/

- backend.rs: RufloBackend trait (9 async methods) + RufloError, MissionMemoryEntry,
  PatternEntry, MavlinkScanResult types (always compiled)
- mock_backend.rs: MockRufloBackend in-memory impl for testing (always compiled, 5 tests)
- http_backend.rs: HttpRufloBackend — JSON-RPC 2.0 → claude-flow daemon localhost:3000
  (gated behind `ruflo` feature, requires reqwest)
- mission_summary.rs: MissionSummary serializer with pattern description + confidence
  scoring from victim recall, coverage %, collision penalty (always compiled, 3 tests)

## 4 capability areas

1. MissionMemory   → memory_store / memory_search       (cross-mission victim memory)
2. PatternLearner  → agentdb_pattern-store / -search     (HNSW SONA trajectory patterns)
3. MavlinkDefence  → aidefence_is_safe / aidefence_scan  (scan MAVLink before accepting)
4. IntelligenceHooks → trajectory-start/step/end          (SONA learning loop)

## SwarmOrchestrator integration

- with_ruflo(backend): builder to attach a backend
- start_trajectory(task) / finish_trajectory(success, key): SONA mission lifecycle
- receive_peer_detection_checked(): AIDefence scan before accepting peer detections

## Cargo feature

`ruflo = ["dep:reqwest", "dep:serde_json"]` — optional, not in default

## Tests

- --no-default-features: 82/82 pass (8 new ruflo tests)
- --features ruflo,itar-unrestricted: 94/94 pass

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-30 01:49:45 -04:00
parent b392aded6c
commit 4aee5c9fb1
9 changed files with 733 additions and 5 deletions

95
v2/Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -37,6 +37,13 @@ pub struct SwarmOrchestrator {
pub peer_detections: Vec<CsiDetection>,
/// 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<Box<dyn crate::ruflo::RufloBackend>>,
/// Active trajectory ID issued by the Ruflo intelligence hooks.
#[cfg(feature = "ruflo")]
pub trajectory_id: Option<String>,
}
/// 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<dyn crate::ruflo::RufloBackend>) -> 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

View File

@ -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<String>,
}
/// 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<Vec<MissionMemoryEntry>, 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<Vec<PatternEntry>, RufloError>;
// ── MavlinkDefence (aidefence_is_safe / aidefence_scan) ──────────
async fn mavlink_is_safe(&self, message_repr: &str) -> Result<bool, RufloError>;
async fn mavlink_scan(&self, message_repr: &str) -> Result<MavlinkScanResult, RufloError>;
// ── IntelligenceHooks (hooks_intelligence_trajectory-*) ──────────
async fn trajectory_start(&self, task: &str, agent: &str)
-> Result<String, RufloError>; // 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>;
}

View File

@ -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<serde_json::Value, RufloError> {
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<Vec<MissionMemoryEntry>, RufloError>
{
let result = self.call_tool("memory_search", serde_json::json!({
"query": query, "namespace": namespace, "limit": limit
})).await?;
let entries: Vec<MissionMemoryEntry> = 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<Vec<PatternEntry>, RufloError>
{
let result = self.call_tool("agentdb_pattern-search", serde_json::json!({
"query": query, "topK": top_k, "minConfidence": min_confidence
})).await?;
let entries: Vec<PatternEntry> = serde_json::from_value(
result["results"].clone()
).unwrap_or_default();
Ok(entries)
}
async fn mavlink_is_safe(&self, message_repr: &str) -> Result<bool, RufloError> {
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<MavlinkScanResult, RufloError> {
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<String> = 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<String, RufloError>
{
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(())
}
}

View File

@ -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<f64>,
}
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}");
}
}

View File

@ -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<Mutex<Vec<(String, String)>>>, // (key, value)
pub patterns: Arc<Mutex<Vec<(String, String, f32)>>>, // (pattern, type, confidence)
pub scan_safe: bool, // set false to simulate a detected threat
pub traj_ids: Arc<Mutex<Vec<String>>>,
}
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<Vec<MissionMemoryEntry>, 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<Vec<PatternEntry>, 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<bool, RufloError> {
Ok(self.scan_safe)
}
async fn mavlink_scan(&self, _msg: &str) -> Result<MavlinkScanResult, RufloError> {
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<String, RufloError>
{
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());
}
}

View File

@ -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;