1use aor::{
8 ArtifactSink, Event, EventKind, Finding, Gate, GateContext, GateDescriptor, Record, Result,
9 Severity, Store,
10};
11
12pub fn gates() -> Vec<Box<dyn Gate>> {
14 vec![
15 Box::new(aor_close::CloseGate),
16 Box::new(aor_decision::DecisionGate),
17 Box::new(aor_dispatch::DispatchGate),
18 Box::new(aor_handoff::HandoffGate),
19 Box::new(aor_payload::PayloadGate),
20 Box::new(aor_resource::ResourceGate),
21 Box::new(aor_verify::VerifyGate),
22 Box::new(aor_wait::WaitGate),
23 ]
24}
25
26pub fn gate_descriptors() -> Vec<&'static GateDescriptor> {
28 gates().iter().map(|g| g.descriptor()).collect()
29}
30
31pub struct Runtime {
36 store: Box<dyn Store>,
37 artifacts: Box<dyn ArtifactSink>,
38 gates: Vec<Box<dyn Gate>>,
39}
40
41struct Ctx<'a> {
42 store: &'a dyn Store,
43 artifacts: &'a dyn ArtifactSink,
44 emitted: Vec<Record>,
45}
46
47impl GateContext for Ctx<'_> {
48 fn store(&self) -> &dyn Store {
49 self.store
50 }
51 fn artifacts(&self) -> &dyn ArtifactSink {
52 self.artifacts
53 }
54 fn emit(&mut self, record: Record) {
55 self.emitted.push(record);
56 }
57}
58
59impl Runtime {
60 pub fn new(store: Box<dyn Store>, artifacts: Box<dyn ArtifactSink>) -> Self {
61 Self {
62 store,
63 artifacts,
64 gates: gates(),
65 }
66 }
67
68 pub fn dispatch(&self, event: &Event) -> Result<Vec<Finding>> {
71 self.store
72 .append_observation(&format!("{:?}", event.kind), &event.detail, event.ts_ms)?;
73 let mut findings = Vec::new();
74 for gate in &self.gates {
75 let mut ctx = Ctx {
76 store: &*self.store,
77 artifacts: &*self.artifacts,
78 emitted: Vec::new(),
79 };
80 findings.extend(gate.observe(event, &mut ctx)?);
81 for record in ctx.emitted {
82 self.store.append_record(&record)?;
83 }
84 }
85 Ok(findings)
86 }
87
88 pub fn sweep(&self) -> Result<Vec<Finding>> {
91 self.store
92 .append_observation("sweep", &serde_json::Value::Null, aor::now_ms())?;
93 let mut findings = Vec::new();
94 for gate in &self.gates {
95 let mut ctx = Ctx {
96 store: &*self.store,
97 artifacts: &*self.artifacts,
98 emitted: Vec::new(),
99 };
100 findings.extend(gate.sweep(&mut ctx)?);
101 for record in ctx.emitted {
102 self.store.append_record(&record)?;
103 }
104 }
105 Ok(findings)
106 }
107
108 pub fn store(&self) -> &dyn Store {
109 &*self.store
110 }
111
112 pub fn observe_tool(&self, tool: &str, args: serde_json::Value) -> Result<Vec<Finding>> {
115 self.dispatch(&Event::new(
116 EventKind::ToolCall,
117 serde_json::json!({ "tool": tool, "args": args }),
118 ))
119 }
120}
121
122pub fn render_findings(findings: &[Finding]) -> String {
124 findings
125 .iter()
126 .map(|f| {
127 format!(
128 "[{}] ({}) {}",
129 f.gate,
130 match f.severity {
131 Severity::Info => "info",
132 Severity::Warn => "warn",
133 Severity::High => "HIGH",
134 },
135 f.message
136 )
137 })
138 .collect::<Vec<_>>()
139 .join("\n")
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use aor::mem::{MemArtifacts, MemStore};
146 use std::collections::HashSet;
147
148 fn rt() -> (Runtime, std::sync::Arc<MemStore>) {
149 let store = std::sync::Arc::new(MemStore::new());
150 let rt = Runtime::new(
151 Box::new(ArcStore(store.clone())),
152 Box::new(MemArtifacts::new()),
153 );
154 (rt, store)
155 }
156
157 struct ArcStore(std::sync::Arc<MemStore>);
159 impl Store for ArcStore {
160 fn append_record(&self, r: &Record) -> Result<i64> {
161 self.0.append_record(r)
162 }
163 fn records(&self, gate: Option<&str>, limit: usize) -> Result<Vec<Record>> {
164 self.0.records(gate, limit)
165 }
166 fn append_observation(
167 &self,
168 kind: &str,
169 detail: &serde_json::Value,
170 ts_ms: u64,
171 ) -> Result<i64> {
172 self.0.append_observation(kind, detail, ts_ms)
173 }
174 fn counter_hit(&self, key: &str, ts_ms: u64) -> Result<()> {
175 self.0.counter_hit(key, ts_ms)
176 }
177 fn counter_count(&self, key: &str, since_ms: u64) -> Result<u64> {
178 self.0.counter_count(key, since_ms)
179 }
180 fn upsert_todo(&self, t: &aor::Todo) -> Result<()> {
181 self.0.upsert_todo(t)
182 }
183 fn todos(&self, state: Option<&str>) -> Result<Vec<aor::Todo>> {
184 self.0.todos(state)
185 }
186 fn append_memory(&self, m: &aor::MemoryEntry) -> Result<i64> {
187 self.0.append_memory(m)
188 }
189 fn memory(&self, kind: Option<&str>, limit: usize) -> Result<Vec<aor::MemoryEntry>> {
190 self.0.memory(kind, limit)
191 }
192 }
193
194 #[test]
195 fn every_gate_declares_at_least_one_profile() {
196 for d in gate_descriptors() {
197 assert!(!d.profiles.is_empty(), "gate {} has no profiles", d.name);
198 }
199 }
200
201 #[test]
202 fn gate_names_are_unique() {
203 let names: HashSet<_> = gate_descriptors().iter().map(|d| d.name).collect();
204 assert_eq!(names.len(), gate_descriptors().len());
205 }
206
207 #[test]
208 fn no_gate_declares_a_channel_twice() {
209 for d in gate_descriptors() {
210 let channels: HashSet<_> = d.profiles.iter().map(|p| p.enforcement).collect();
211 assert_eq!(
212 channels.len(),
213 d.profiles.len(),
214 "gate {} repeats an enforcement channel",
215 d.name
216 );
217 }
218 }
219
220 #[test]
221 fn dispatch_persists_raw_observation() {
222 let (rt, store) = rt();
223 let _ = rt.dispatch(&Event::new(
224 EventKind::ToolCall,
225 serde_json::json!({"tool": "x"}),
226 ));
227 assert!(store.counter_count("nope", 0).is_ok());
229 drop(store);
230 }
231
232 #[test]
233 fn sweep_runs_without_panic() {
234 let (rt, _store) = rt();
235 rt.sweep().expect("sweep ok");
236 }
237}