rig_resources/
patterns.rs1use std::sync::Arc;
4
5use async_trait::async_trait;
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use rig_compose::{
11 Evidence, InvestigationContext, KernelError, NextAction, Skill, SkillOutcome, ToolRegistry,
12};
13
14pub type PatternId = String;
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
19pub struct PatternRule {
20 #[serde(default)]
21 pub required: Vec<String>,
22 #[serde(default)]
23 pub forbidden: Vec<String>,
24}
25
26impl PatternRule {
27 pub fn matches(&self, ctx: &InvestigationContext) -> bool {
28 self.required.iter().all(|s| ctx.has_signal(s))
29 && self.forbidden.iter().all(|s| !ctx.has_signal(s))
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct BehaviorPattern {
36 pub id: PatternId,
37 pub version: u32,
38 pub description: String,
39 pub rule: PatternRule,
40 pub confidence_delta: f32,
41 #[serde(default)]
42 pub conclude: bool,
43}
44
45impl BehaviorPattern {
46 pub fn new(id: impl Into<String>, version: u32, rule: PatternRule, delta: f32) -> Self {
47 Self {
48 id: id.into(),
49 version,
50 description: String::new(),
51 rule,
52 confidence_delta: delta,
53 conclude: false,
54 }
55 }
56
57 pub fn with_description(mut self, description: impl Into<String>) -> Self {
58 self.description = description.into();
59 self
60 }
61
62 pub fn concluding(mut self) -> Self {
63 self.conclude = true;
64 self
65 }
66}
67
68#[derive(Clone, Default)]
71pub struct BehaviorRegistry {
72 inner: Arc<RwLock<Vec<BehaviorPattern>>>,
73}
74
75impl BehaviorRegistry {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn register(&self, pattern: BehaviorPattern) {
81 let mut guard = self.inner.write();
82 if let Some(existing) = guard.iter_mut().find(|p| p.id == pattern.id) {
83 if pattern.version >= existing.version {
84 *existing = pattern;
85 }
86 } else {
87 guard.push(pattern);
88 }
89 }
90
91 pub fn extend<I: IntoIterator<Item = BehaviorPattern>>(&self, patterns: I) {
92 for pattern in patterns {
93 self.register(pattern);
94 }
95 }
96
97 pub fn len(&self) -> usize {
98 self.inner.read().len()
99 }
100
101 pub fn is_empty(&self) -> bool {
102 self.inner.read().is_empty()
103 }
104
105 pub fn snapshot(&self) -> Vec<BehaviorPattern> {
106 self.inner.read().clone()
107 }
108}
109
110pub struct BehaviorPatternSkill {
113 registry: BehaviorRegistry,
114}
115
116impl BehaviorPatternSkill {
117 pub const ID: &'static str = "knowledge.behavior_pattern";
118
119 pub fn new(registry: BehaviorRegistry) -> Self {
120 Self { registry }
121 }
122}
123
124#[async_trait]
125impl Skill for BehaviorPatternSkill {
126 fn id(&self) -> &str {
127 Self::ID
128 }
129 fn description(&self) -> &str {
130 "Evaluates a behavioural-pattern registry against the investigation context."
131 }
132 fn applies(&self, _ctx: &InvestigationContext) -> bool {
133 !self.registry.is_empty()
134 }
135 async fn execute(
136 &self,
137 ctx: &mut InvestigationContext,
138 _tools: &ToolRegistry,
139 ) -> Result<SkillOutcome, KernelError> {
140 let _span = tracing::debug_span!(
141 "rig_resources.patterns.behavior_eval",
142 patterns = self.registry.len(),
143 )
144 .entered();
145 let matched: Vec<BehaviorPattern> = self
146 .registry
147 .snapshot()
148 .into_iter()
149 .filter(|pattern| pattern.rule.matches(ctx))
150 .collect();
151 let mut total = 0.0f32;
152 let mut conclude = false;
153 for pattern in matched {
154 total += pattern.confidence_delta;
155 conclude |= pattern.conclude;
156 ctx.evidence.push(
157 Evidence::new(Self::ID, format!("pattern:{}", pattern.id)).with_detail(json!({
158 "version": pattern.version,
159 "delta": pattern.confidence_delta,
160 })),
161 );
162 }
163 let mut outcome = SkillOutcome::default().with_delta(total);
164 if conclude {
165 outcome = outcome.with_next(NextAction::Conclude);
166 }
167 Ok(outcome)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn rule(required: &[&str]) -> PatternRule {
176 PatternRule {
177 required: required.iter().map(|s| s.to_string()).collect(),
178 forbidden: vec![],
179 }
180 }
181
182 #[tokio::test]
183 async fn matching_pattern_lifts_and_records_evidence() {
184 let reg = BehaviorRegistry::new();
185 reg.register(
186 BehaviorPattern::new("brute", 1, rule(&["auth.failure.burst"]), 0.25)
187 .with_description("password spray"),
188 );
189 let skill = BehaviorPatternSkill::new(reg);
190 let mut ctx = InvestigationContext::new("e", "p").with_signal("auth.failure.burst");
191 let tools = ToolRegistry::new();
192 let outcome = skill.execute(&mut ctx, &tools).await.unwrap();
193 assert!((outcome.confidence_delta - 0.25).abs() < 1e-6);
194 assert_eq!(ctx.evidence.len(), 1);
195 }
196
197 #[tokio::test]
198 async fn nonmatching_pattern_is_inert() {
199 let reg = BehaviorRegistry::new();
200 reg.register(BehaviorPattern::new("x", 1, rule(&["never"]), 0.5));
201 let skill = BehaviorPatternSkill::new(reg);
202 let mut ctx = InvestigationContext::new("e", "p");
203 let tools = ToolRegistry::new();
204 let outcome = skill.execute(&mut ctx, &tools).await.unwrap();
205 assert_eq!(outcome.confidence_delta, 0.0);
206 assert!(ctx.evidence.is_empty());
207 }
208
209 #[test]
210 fn registry_keeps_highest_version() {
211 let registry = BehaviorRegistry::new();
212 registry.register(BehaviorPattern::new("p", 1, PatternRule::default(), 0.1));
213 registry.register(BehaviorPattern::new("p", 2, PatternRule::default(), 0.2));
214 registry.register(BehaviorPattern::new("p", 1, PatternRule::default(), 0.9));
215 let snapshot = registry.snapshot();
216 assert_eq!(snapshot.len(), 1);
217 assert_eq!(snapshot[0].version, 2);
218 assert!((snapshot[0].confidence_delta - 0.2).abs() < 1e-6);
219 }
220
221 #[test]
222 fn forbidden_signal_blocks_match() {
223 let rule = PatternRule {
224 required: vec!["a".into()],
225 forbidden: vec!["b".into()],
226 };
227 let ctx_ok = InvestigationContext::new("e", "p").with_signal("a");
228 let ctx_block = InvestigationContext::new("e", "p")
229 .with_signal("a")
230 .with_signal("b");
231 assert!(rule.matches(&ctx_ok));
232 assert!(!rule.matches(&ctx_block));
233 }
234}