use std::sync::Mutex;
use chrono::Utc;
use uuid::Uuid;
use crate::client::AgentTrustClient;
use crate::error::{AgentTrustError, Result};
use crate::models::{ActionCheckRequest, TelemetryEvent};
const AUTO_FLUSH_SIZE: usize = 10;
pub struct AgentTrustGuard {
client: AgentTrustClient,
agent_id: String,
session_id: String,
block_on_deny: bool,
fail_open: bool,
default_action_effect: Option<String>,
buffer: Mutex<Vec<TelemetryEvent>>,
}
impl AgentTrustGuard {
pub fn new(client: AgentTrustClient, agent_id: &str) -> Self {
Self {
client,
agent_id: agent_id.to_string(),
session_id: Uuid::new_v4().to_string(),
block_on_deny: true,
fail_open: false,
default_action_effect: None,
buffer: Mutex::new(Vec::with_capacity(AUTO_FLUSH_SIZE)),
}
}
pub fn builder(client: AgentTrustClient, agent_id: &str) -> AgentTrustGuardBuilder {
AgentTrustGuardBuilder {
client,
agent_id: agent_id.to_string(),
session_id: None,
block_on_deny: true,
fail_open: false,
default_action_effect: None,
}
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn check(&self, tool_name: &str, input_summary: &str) -> Result<()> {
self.do_check(
tool_name,
input_summary,
self.default_action_effect.as_deref(),
)
}
pub fn check_with_effect(
&self,
tool_name: &str,
input_summary: &str,
action_effect: &str,
) -> Result<()> {
self.do_check(tool_name, input_summary, Some(action_effect))
}
fn do_check(
&self,
tool_name: &str,
input_summary: &str,
action_effect: Option<&str>,
) -> Result<()> {
let req = ActionCheckRequest {
agent_id: self.agent_id.clone(),
action: "tool_call".to_string(),
tool_name: tool_name.to_string(),
tool_input_summary: input_summary.to_string(),
session_id: self.session_id.clone(),
action_effect: action_effect.map(|s| s.to_string()),
};
let result = self.client.actions().check(&req);
match result {
Ok(check) => {
if check.elevation_required == Some(true) {
if self.block_on_deny {
return Err(AgentTrustError::ElevationRequired {
message: format!("Tool '{}' requires elevated approval", tool_name,),
approval_id: check.approval_id.unwrap_or_default(),
});
}
return Ok(());
}
if !check.allowed {
if self.block_on_deny {
return Err(AgentTrustError::ActionDenied {
message: format!(
"Tool '{}' denied: {}",
tool_name,
check
.reason
.unwrap_or_else(|| "no reason given".to_string())
),
check_id: check.check_id,
});
}
}
Ok(())
}
Err(AgentTrustError::Network(_)) => {
if self.fail_open {
Ok(())
} else {
Err(AgentTrustError::GuardianUnavailable {
message: "Guardian service is unreachable".to_string(),
})
}
}
Err(e) => Err(e),
}
}
pub fn report(&self, tool_name: &str, success: bool, duration_ms: u64) {
let event_type = if success { "tool_end" } else { "tool_error" };
let event = TelemetryEvent {
event_type: event_type.to_string(),
tool_name: tool_name.to_string(),
duration_ms,
success,
error_type: None,
timestamp: Utc::now().to_rfc3339(),
};
let should_flush = {
let mut buf = self.buffer.lock().unwrap();
buf.push(event);
buf.len() >= AUTO_FLUSH_SIZE
};
if should_flush {
let _ = self.flush();
}
}
pub fn flush(&self) -> Result<()> {
let events = {
let mut buf = self.buffer.lock().unwrap();
if buf.is_empty() {
return Ok(());
}
let events = buf.clone();
buf.clear();
events
};
self.client
.telemetry()
.report(&self.agent_id, &self.session_id, &events)
}
}
impl Drop for AgentTrustGuard {
fn drop(&mut self) {
let _ = self.flush();
}
}
pub struct AgentTrustGuardBuilder {
client: AgentTrustClient,
agent_id: String,
session_id: Option<String>,
block_on_deny: bool,
fail_open: bool,
default_action_effect: Option<String>,
}
impl AgentTrustGuardBuilder {
pub fn session_id(mut self, id: &str) -> Self {
self.session_id = Some(id.to_string());
self
}
pub fn block_on_deny(mut self, block: bool) -> Self {
self.block_on_deny = block;
self
}
pub fn fail_open(mut self, open: bool) -> Self {
self.fail_open = open;
self
}
pub fn action_effect(mut self, effect: &str) -> Self {
self.default_action_effect = Some(effect.to_string());
self
}
pub fn build(self) -> AgentTrustGuard {
AgentTrustGuard {
client: self.client,
agent_id: self.agent_id,
session_id: self
.session_id
.unwrap_or_else(|| Uuid::new_v4().to_string()),
block_on_deny: self.block_on_deny,
fail_open: self.fail_open,
default_action_effect: self.default_action_effect,
buffer: Mutex::new(Vec::with_capacity(AUTO_FLUSH_SIZE)),
}
}
}