use std::future::Future;
use std::time::{Duration, Instant};
use super::control_plane::ControlPlaneClient;
use super::types::{
AgentConfig, ApprovalOutcome, GovernedOutcome, GovernedResult, InterceptResult,
InterceptWireBody, NormalizedAction,
};
use crate::SdkError;
#[derive(Debug, Clone)]
pub struct AgentClient {
config: AgentConfig,
pub control_plane: ControlPlaneClient,
http: reqwest::Client,
}
impl AgentClient {
pub fn new(config: AgentConfig) -> Self {
let control_plane = ControlPlaneClient::with_api_key(config.control_plane_url.clone(), config.api_key.clone());
Self { config, control_plane, http: reqwest::Client::new() }
}
pub async fn intercept(&self, action: NormalizedAction) -> Result<InterceptResult, SdkError> {
let body = InterceptWireBody {
tenant_id: self.config.tenant_id.clone(),
agent_id: self.config.agent_id.clone(),
target_system: action.target_system,
action_type: action.action_type,
action_name: action.action_name,
principal_id: action.principal_id,
session_id: action.session_id,
data_classification: action.data_classification,
risk_score: action.risk_score,
amount: action.amount,
recipient: action.recipient,
};
let url = format!("{}/v1/intercept", self.config.data_plane_url);
let mut req = self.http.post(&url).json(&body);
if let Some(key) = &self.config.api_key {
req = req.header("x-cots-api-key", key);
}
let res = req.send().await?;
let status = res.status();
let text = res.text().await?;
if !status.is_success() {
return Err(SdkError::Api { status: status.as_u16(), body: text });
}
serde_json::from_str(&text).map_err(|e| SdkError::Api { status: status.as_u16(), body: e.to_string() })
}
pub async fn wait_for_approval(&self, action_event_id: &str) -> Result<ApprovalOutcome, SdkError> {
let deadline = Instant::now() + Duration::from_millis(self.config.approval_timeout_ms);
while Instant::now() < deadline {
if let Ok(action) = self.control_plane.get_action(action_event_id).await {
match action.status.as_str() {
"executed" => return Ok(ApprovalOutcome::Approved),
"denied" | "blocked" => return Ok(ApprovalOutcome::Denied),
_ => {}
}
}
tokio::time::sleep(Duration::from_millis(self.config.approval_poll_ms)).await;
}
Ok(ApprovalOutcome::Timeout)
}
pub async fn guard<F, Fut, T>(
&self,
action: NormalizedAction,
execute: F,
) -> Result<GovernedResult<T>, SdkError>
where
F: FnOnce() -> Fut,
Fut: Future<Output = T>,
{
let result = self.intercept(action).await?;
if result.decision == "blocked" {
return Ok(GovernedResult {
action_event_id: result.action_event_id,
decision: result.decision,
outcome: GovernedOutcome::Blocked,
matched_rules: result.matched_rules,
detail: None,
});
}
if result.decision == "allowed" {
let detail = execute().await;
return Ok(GovernedResult {
action_event_id: result.action_event_id,
decision: result.decision,
outcome: GovernedOutcome::Executed,
matched_rules: result.matched_rules,
detail: Some(detail),
});
}
match self.wait_for_approval(&result.action_event_id).await? {
ApprovalOutcome::Approved => {
let detail = execute().await;
Ok(GovernedResult {
action_event_id: result.action_event_id,
decision: result.decision,
outcome: GovernedOutcome::Executed,
matched_rules: result.matched_rules,
detail: Some(detail),
})
}
ApprovalOutcome::Denied => Ok(GovernedResult {
action_event_id: result.action_event_id,
decision: result.decision,
outcome: GovernedOutcome::Blocked,
matched_rules: result.matched_rules,
detail: None,
}),
ApprovalOutcome::Timeout => Ok(GovernedResult {
action_event_id: result.action_event_id,
decision: result.decision,
outcome: GovernedOutcome::Timeout,
matched_rules: result.matched_rules,
detail: None,
}),
}
}
}