use serde::{Deserialize, Serialize};
use crate::profile::AgentBaseline;
pub trait BaselineExporter: Send + Sync {
fn emit_otel(&self, b: &AgentBaseline) -> serde_json::Value;
fn emit_ocsf(&self, b: &AgentBaseline) -> serde_json::Value;
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonExporter;
impl BaselineExporter for JsonExporter {
fn emit_otel(&self, b: &AgentBaseline) -> serde_json::Value {
serde_json::json!({
"name": "mnemo.baseline",
"kind": "internal",
"attributes": {
"agent.id": b.agent,
"agent.window_secs": b.window.as_secs(),
"agent.recall_rate_per_min": b.recall_rate_per_min,
"agent.write_rate_per_min": b.write_rate_per_min,
"agent.namespace_fanout": b.namespace_fanout,
"agent.hmac_continuity": b.hmac_continuity,
"agent.tool_mix_keys": b.tool_mix.keys().cloned().collect::<Vec<_>>(),
},
})
}
fn emit_ocsf(&self, b: &AgentBaseline) -> serde_json::Value {
serde_json::json!({
"category_uid": 6, "class_uid": 6004,
"type_uid": 600401, "activity_id": 1,
"severity_id": 1,
"metadata": {
"version": "1.4.0",
"product": {
"name": "mnemo-baseline",
"vendor_name": "mnemo",
},
},
"actor": {
"user": {
"name": b.agent,
"type": "Agent",
},
},
"enrichments": [
{"name": "recall_rate_per_min", "value": b.recall_rate_per_min},
{"name": "write_rate_per_min", "value": b.write_rate_per_min},
{"name": "namespace_fanout", "value": b.namespace_fanout},
{"name": "hmac_continuity", "value": b.hmac_continuity},
],
})
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::profile::AgentBaseline;
fn fake_baseline() -> AgentBaseline {
let mut b = AgentBaseline::new("agent-prod-42", Duration::from_secs(300));
b.recall_rate_per_min = 12.0;
b.write_rate_per_min = 4.5;
b.namespace_fanout = 2.0;
b.hmac_continuity = 1.0;
b.tool_mix.insert("recall".into(), 0.7);
b.tool_mix.insert("write".into(), 0.3);
b
}
#[test]
fn otel_payload_carries_agent_attributes() {
let exp = JsonExporter;
let v = exp.emit_otel(&fake_baseline());
assert_eq!(v["name"], "mnemo.baseline");
assert_eq!(v["attributes"]["agent.id"], "agent-prod-42");
assert_eq!(v["attributes"]["agent.recall_rate_per_min"], 12.0);
}
#[test]
fn ocsf_payload_validates_against_class_6004() {
let exp = JsonExporter;
let v = exp.emit_ocsf(&fake_baseline());
assert_eq!(v["category_uid"], 6);
assert_eq!(v["class_uid"], 6004);
assert_eq!(v["actor"]["user"]["name"], "agent-prod-42");
}
#[test]
fn no_pii_or_memory_content_in_payloads() {
let exp = JsonExporter;
let b = fake_baseline();
let otel = exp.emit_otel(&b).to_string();
let ocsf = exp.emit_ocsf(&b).to_string();
let leak_re =
regex::Regex::new(r"(?i)(content|body|text|memory_text|raw|payload_text)").unwrap();
assert!(
!leak_re.is_match(&otel),
"OTel payload contains a banned field: {otel}"
);
assert!(
!leak_re.is_match(&ocsf),
"OCSF payload contains a banned field: {ocsf}"
);
}
}