1use crate::core::capabilities::{check_capabilities, role_capabilities};
13use crate::core::events::{EventKind, LeanCtxEvent};
14use serde_json::{Value, json};
15
16pub fn export_event(event: &LeanCtxEvent) -> Option<Value> {
23 let kind = export_event_kind(&event.kind)?;
24 Some(json!({
25 "id": event.id,
26 "timestamp": event.timestamp,
27 "kind": kind,
28 }))
29}
30
31fn export_event_kind(kind: &EventKind) -> Option<Value> {
32 match kind {
33 EventKind::ToolCall {
34 tool,
35 tokens_original,
36 tokens_saved,
37 mode,
38 duration_ms,
39 path,
40 } => Some(json!({
41 "type": "tool_call",
42 "tool": tool,
43 "tokens_original": tokens_original,
44 "tokens_saved": tokens_saved,
45 "mode": mode,
46 "duration_ms": duration_ms,
47 "path": path,
48 })),
49 EventKind::AgentAction {
50 agent_id,
51 action,
52 tool,
53 } => Some(json!({
54 "type": "agent_action",
55 "agent_id": agent_id,
56 "action": action,
57 "tool": tool,
58 })),
59 EventKind::KnowledgeUpdate {
60 category,
61 key,
62 action,
63 } => Some(json!({
64 "type": "knowledge_update",
65 "category": category,
66 "key": key,
67 "action": action,
68 })),
69 EventKind::PolicyViolation { role, tool, reason } => Some(json!({
70 "type": "policy_violation",
71 "role": role,
72 "tool": tool,
73 "reason": reason,
74 })),
75 EventKind::RoleChanged { from, to } => Some(json!({
76 "type": "role_changed",
77 "from": from,
78 "to": to,
79 })),
80 _ => None,
81 }
82}
83
84pub fn capability_grant_set(role_name: &str) -> Value {
86 let mut caps: Vec<&'static str> = role_capabilities(role_name)
87 .into_iter()
88 .map(|c| c.display_name())
89 .collect();
90 caps.sort_unstable();
91 json!({ "subject": role_name, "capabilities": caps })
92}
93
94pub fn capability_check_result(role_name: &str, tool_name: &str) -> Value {
96 let result = check_capabilities(role_name, tool_name);
97 let missing: Vec<&'static str> = result
98 .missing
99 .iter()
100 .map(super::capabilities::Capability::display_name)
101 .collect();
102 json!({ "allowed": result.allowed, "missing": missing })
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn governance_events_export_with_snake_case_tags() {
111 let event = LeanCtxEvent {
112 id: 1,
113 timestamp: chrono::Utc::now().to_rfc3339(),
114 kind: EventKind::PolicyViolation {
115 role: "reviewer".into(),
116 tool: "ctx_shell".into(),
117 reason: "denied".into(),
118 },
119 };
120 let exported = export_event(&event).expect("governance event must export");
121 assert_eq!(exported["kind"]["type"], "policy_violation");
122 }
123
124 #[test]
125 fn non_governance_events_are_not_exported() {
126 let event = LeanCtxEvent {
127 id: 2,
128 timestamp: chrono::Utc::now().to_rfc3339(),
129 kind: EventKind::CacheHit {
130 path: "src/lib.rs".into(),
131 saved_tokens: 10,
132 },
133 };
134 assert!(export_event(&event).is_none());
135 }
136
137 #[test]
138 fn grant_set_uses_registry_identifiers() {
139 let grant = capability_grant_set("admin");
140 let caps = grant["capabilities"].as_array().unwrap();
141 assert!(caps.iter().any(|c| c == "exec:unrestricted"));
142 assert!(caps.iter().any(|c| c == "fs:read"));
143 }
144
145 #[test]
146 fn check_result_lists_missing_on_denial() {
147 let denied = capability_check_result("minimal", "ctx_shell");
148 assert_eq!(denied["allowed"], false);
149 assert!(
150 !denied["missing"].as_array().unwrap().is_empty(),
151 "`denied[\"missing\"].as_array().unwrap()` is empty"
152 );
153
154 let allowed = capability_check_result("admin", "ctx_shell");
155 assert_eq!(allowed["allowed"], true);
156 {
157 let found = &allowed["missing"].as_array().unwrap();
158 assert!(found.is_empty(), "{found:?}");
159 }
160 }
161}