oxicode_sdk/middleware/
observability_adapters.rs1use std::future::Future;
18use std::pin::Pin;
19use std::sync::Arc;
20
21use crate::middleware::{
22 Middleware, MiddlewareContext, MiddlewareData, MiddlewarePhase, MiddlewareResult,
23};
24use crate::observability::{AuditEntry, AuditLog};
25use crate::security::Authorizer;
26
27pub struct AuditLogMiddleware {
32 audit: Arc<AuditLog>,
33 agent_id: String,
34}
35
36impl AuditLogMiddleware {
37 pub fn new(audit: Arc<AuditLog>, agent_id: impl Into<String>) -> Self {
39 Self {
40 audit,
41 agent_id: agent_id.into(),
42 }
43 }
44
45 fn handle_before(&self, ctx: &MiddlewareContext) -> MiddlewareResult {
46 let tool_name = match &ctx.data {
47 MiddlewareData::BeforeTool { tool_name, .. } => tool_name.clone(),
48 _ => return MiddlewareResult::pass(),
49 };
50 self.audit.log(AuditEntry::lifecycle(
51 self.agent_id.clone(),
52 format!("tool_start:{}", tool_name),
53 ));
54 MiddlewareResult::pass()
55 }
56
57 fn handle_after(&self, ctx: &MiddlewareContext) -> MiddlewareResult {
58 let tool_name = match &ctx.data {
59 MiddlewareData::AfterTool {
60 tool_name, result, ..
61 } => {
62 let success = !result.is_empty() && !result.starts_with("error:");
63 (tool_name.clone(), success)
64 }
65 _ => return MiddlewareResult::pass(),
66 };
67 let (name, success) = tool_name;
68 self.audit.log(AuditEntry::tool_execution(
69 self.agent_id.clone(),
70 name,
71 "{}".into(),
72 success,
73 0,
74 ));
75 MiddlewareResult::pass()
76 }
77}
78
79impl Middleware for AuditLogMiddleware {
80 fn name(&self) -> &str {
81 "AuditLogMiddleware"
82 }
83
84 fn phases(&self) -> Vec<MiddlewarePhase> {
85 vec![MiddlewarePhase::BeforeTool, MiddlewarePhase::AfterTool]
86 }
87
88 fn handle<'a>(
89 &'a self,
90 ctx: &'a MiddlewareContext,
91 ) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send + 'a>> {
92 let result = match ctx.phase {
93 MiddlewarePhase::BeforeTool => self.handle_before(ctx),
94 MiddlewarePhase::AfterTool => self.handle_after(ctx),
95 _ => MiddlewareResult::pass(),
96 };
97 Box::pin(async move { result })
98 }
99}
100
101pub struct AuthorizerMiddleware {
104 authorizer: Arc<Authorizer>,
105 audit: Option<Arc<AuditLog>>,
106 agent_id: String,
107}
108
109impl AuthorizerMiddleware {
110 pub fn new(authorizer: Arc<Authorizer>, agent_id: impl Into<String>) -> Self {
112 Self {
113 authorizer,
114 audit: None,
115 agent_id: agent_id.into(),
116 }
117 }
118
119 pub fn with_audit(mut self, audit: Arc<AuditLog>) -> Self {
122 self.audit = Some(audit);
123 self
124 }
125}
126
127impl Middleware for AuthorizerMiddleware {
128 fn name(&self) -> &str {
129 "AuthorizerMiddleware"
130 }
131
132 fn phases(&self) -> Vec<MiddlewarePhase> {
133 vec![MiddlewarePhase::BeforeTool]
134 }
135 fn handle<'a>(
136 &'a self,
137 ctx: &'a MiddlewareContext,
138 ) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send + 'a>> {
139 let tool_name = match &ctx.data {
140 MiddlewareData::BeforeTool { tool_name, .. } => tool_name.clone(),
141 _ => {
142 return Box::pin(async move { MiddlewareResult::pass() });
143 }
144 };
145
146 let authorizer = Arc::clone(&self.authorizer);
147 let audit = self.audit.clone();
148 let agent_id = self.agent_id.clone();
149 let cap = crate::security::Capability::ToolUse {
150 tool_name: tool_name.clone(),
151 };
152
153 Box::pin(async move {
154 let subject = crate::security::CapabilitySubject::Agent(agent_id.clone());
155 let granted = authorizer.check(&subject, &cap);
156 if let Some(audit) = audit {
157 audit.log(AuditEntry::security_decision(
158 agent_id,
159 format!("tool:{}", tool_name),
160 granted,
161 ));
162 }
163 if granted {
164 MiddlewareResult::pass()
165 } else {
166 MiddlewareResult::block(format!(
167 "authorizer denied tool `{}` for agent `{}`",
168 tool_name, subject
169 ))
170 }
171 })
172 }
173}
174
175pub fn resolved_agent_id(config: &oxicode_agent::AgentConfig) -> String {
179 if config.name.is_empty() {
180 uuid::Uuid::new_v4().to_string()
181 } else {
182 config.name.clone()
183 }
184}