use crate::core::capabilities::{check_capabilities, role_capabilities};
use crate::core::events::{EventKind, LeanCtxEvent};
use serde_json::{json, Value};
pub fn export_event(event: &LeanCtxEvent) -> Option<Value> {
let kind = export_event_kind(&event.kind)?;
Some(json!({
"id": event.id,
"timestamp": event.timestamp,
"kind": kind,
}))
}
fn export_event_kind(kind: &EventKind) -> Option<Value> {
match kind {
EventKind::ToolCall {
tool,
tokens_original,
tokens_saved,
mode,
duration_ms,
path,
} => Some(json!({
"type": "tool_call",
"tool": tool,
"tokens_original": tokens_original,
"tokens_saved": tokens_saved,
"mode": mode,
"duration_ms": duration_ms,
"path": path,
})),
EventKind::AgentAction {
agent_id,
action,
tool,
} => Some(json!({
"type": "agent_action",
"agent_id": agent_id,
"action": action,
"tool": tool,
})),
EventKind::KnowledgeUpdate {
category,
key,
action,
} => Some(json!({
"type": "knowledge_update",
"category": category,
"key": key,
"action": action,
})),
EventKind::BudgetWarning {
role,
dimension,
used,
limit,
percent,
} => Some(json!({
"type": "budget_warning",
"role": role,
"dimension": dimension,
"used": used,
"limit": limit,
"percent": percent,
})),
EventKind::BudgetExhausted {
role,
dimension,
used,
limit,
} => Some(json!({
"type": "budget_exhausted",
"role": role,
"dimension": dimension,
"used": used,
"limit": limit,
})),
EventKind::PolicyViolation { role, tool, reason } => Some(json!({
"type": "policy_violation",
"role": role,
"tool": tool,
"reason": reason,
})),
EventKind::RoleChanged { from, to } => Some(json!({
"type": "role_changed",
"from": from,
"to": to,
})),
_ => None,
}
}
pub fn capability_grant_set(role_name: &str) -> Value {
let mut caps: Vec<&'static str> = role_capabilities(role_name)
.into_iter()
.map(|c| c.display_name())
.collect();
caps.sort_unstable();
json!({ "subject": role_name, "capabilities": caps })
}
pub fn capability_check_result(role_name: &str, tool_name: &str) -> Value {
let result = check_capabilities(role_name, tool_name);
let missing: Vec<&'static str> = result
.missing
.iter()
.map(super::capabilities::Capability::display_name)
.collect();
json!({ "allowed": result.allowed, "missing": missing })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn governance_events_export_with_snake_case_tags() {
let event = LeanCtxEvent {
id: 1,
timestamp: chrono::Utc::now().to_rfc3339(),
kind: EventKind::PolicyViolation {
role: "reviewer".into(),
tool: "ctx_shell".into(),
reason: "denied".into(),
},
};
let exported = export_event(&event).expect("governance event must export");
assert_eq!(exported["kind"]["type"], "policy_violation");
}
#[test]
fn non_governance_events_are_not_exported() {
let event = LeanCtxEvent {
id: 2,
timestamp: chrono::Utc::now().to_rfc3339(),
kind: EventKind::CacheHit {
path: "src/lib.rs".into(),
saved_tokens: 10,
},
};
assert!(export_event(&event).is_none());
}
#[test]
fn grant_set_uses_registry_identifiers() {
let grant = capability_grant_set("admin");
let caps = grant["capabilities"].as_array().unwrap();
assert!(caps.iter().any(|c| c == "exec:unrestricted"));
assert!(caps.iter().any(|c| c == "fs:read"));
}
#[test]
fn check_result_lists_missing_on_denial() {
let denied = capability_check_result("minimal", "ctx_shell");
assert_eq!(denied["allowed"], false);
assert!(!denied["missing"].as_array().unwrap().is_empty());
let allowed = capability_check_result("admin", "ctx_shell");
assert_eq!(allowed["allowed"], true);
assert!(allowed["missing"].as_array().unwrap().is_empty());
}
}