cots 0.2.0

Cots.ai SDK for Rust. cots::agents::AgentClient — configure once with tenant_id/agent_id, then guard() every governed action through the PEP/PDP before executing it.
Documentation
use serde::{Deserialize, Serialize};

/// Configuration for one agent. `tenant_id`/`agent_id` come from onboarding
/// (see `ControlPlaneClient`); the URLs default to this repo's local demo ports.
#[derive(Debug, Clone)]
pub struct AgentConfig {
    pub tenant_id: String,
    pub agent_id: String,
    pub data_plane_url: String,
    pub control_plane_url: String,
    pub approval_timeout_ms: u64,
    pub approval_poll_ms: u64,
    /// This agent's API key, returned once in POST /agents's response at
    /// registration time. Required once the data plane has auth enabled;
    /// leave unset only against a data plane still running with no auth.
    pub api_key: Option<String>,
}

impl AgentConfig {
    pub fn new(tenant_id: impl Into<String>, agent_id: impl Into<String>) -> Self {
        Self {
            tenant_id: tenant_id.into(),
            agent_id: agent_id.into(),
            data_plane_url: "http://localhost:9090".to_string(),
            control_plane_url: "http://localhost:8080/api".to_string(),
            approval_timeout_ms: 30_000,
            approval_poll_ms: 2_000,
            api_key: None,
        }
    }

    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
        self.api_key = Some(api_key.into());
        self
    }

    pub fn with_data_plane_url(mut self, url: impl Into<String>) -> Self {
        self.data_plane_url = url.into();
        self
    }

    pub fn with_control_plane_url(mut self, url: impl Into<String>) -> Self {
        self.control_plane_url = url.into();
        self
    }

    pub fn with_approval_timeout_ms(mut self, ms: u64) -> Self {
        self.approval_timeout_ms = ms;
        self
    }

    pub fn with_approval_poll_ms(mut self, ms: u64) -> Self {
        self.approval_poll_ms = ms;
        self
    }

    /// Reads COTS_TENANT_ID / COTS_AGENT_ID (required) and the optional
    /// COTS_DATA_PLANE_URL / COTS_CONTROL_PLANE_URL / COTS_APPROVAL_TIMEOUT_MS
    /// / COTS_APPROVAL_POLL_MS env vars, matching the Node SDK's convention.
    pub fn from_env() -> Result<Self, crate::SdkError> {
        let tenant_id = std::env::var("COTS_TENANT_ID")
            .map_err(|_| crate::SdkError::Config("COTS_TENANT_ID is not set".into()))?;
        let agent_id = std::env::var("COTS_AGENT_ID")
            .map_err(|_| crate::SdkError::Config("COTS_AGENT_ID is not set".into()))?;
        let mut cfg = Self::new(tenant_id, agent_id);
        if let Ok(v) = std::env::var("COTS_DATA_PLANE_URL") {
            cfg.data_plane_url = v;
        }
        if let Ok(v) = std::env::var("COTS_CONTROL_PLANE_URL") {
            cfg.control_plane_url = v;
        }
        if let Ok(v) = std::env::var("COTS_APPROVAL_TIMEOUT_MS") {
            cfg.approval_timeout_ms = v.parse().unwrap_or(cfg.approval_timeout_ms);
        }
        if let Ok(v) = std::env::var("COTS_APPROVAL_POLL_MS") {
            cfg.approval_poll_ms = v.parse().unwrap_or(cfg.approval_poll_ms);
        }
        if let Ok(v) = std::env::var("COTS_API_KEY") {
            cfg.api_key = Some(v);
        }
        Ok(cfg)
    }
}

/// The normalized action context sent to POST /v1/intercept, before execution.
#[derive(Debug, Clone, Default)]
pub struct NormalizedAction {
    pub target_system: String,
    pub action_type: String,
    pub action_name: Option<String>,
    pub principal_id: Option<String>,
    pub session_id: Option<String>,
    pub data_classification: Option<String>,
    pub risk_score: Option<i32>,
    pub amount: Option<f64>,
    /// Recipient identifier (email address, phone number, etc.) — used by
    /// recipient-domain-style policy rules.
    pub recipient: Option<String>,
}

impl NormalizedAction {
    pub fn new(target_system: impl Into<String>, action_type: impl Into<String>) -> Self {
        Self {
            target_system: target_system.into(),
            action_type: action_type.into(),
            ..Default::default()
        }
    }
}

/// The wire shape the Rust PEP's POST /v1/intercept actually expects
/// (mirrors data-plane's `NormalizedAction` in src/models.rs).
#[derive(Debug, Serialize)]
pub(crate) struct InterceptWireBody {
    pub tenant_id: String,
    pub agent_id: String,
    pub target_system: String,
    pub action_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub action_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub principal_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data_classification: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub risk_score: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub amount: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub recipient: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct InterceptResult {
    pub action_event_id: String,
    pub tenant_id: String,
    pub decision: String, // "allowed" | "blocked" | "require_approval"
    pub status: String,
    pub matched_rules: Vec<String>,
    pub latency_ms: u128,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalOutcome {
    Approved,
    Denied,
    Timeout,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GovernedOutcome {
    Executed,
    Blocked,
    Timeout,
}

/// What `guard()` returns: the decision the PDP made, what actually happened,
/// and (if your callback ran) its result.
#[derive(Debug, Clone)]
pub struct GovernedResult<T> {
    pub action_event_id: String,
    pub decision: String,
    pub outcome: GovernedOutcome,
    pub matched_rules: Vec<String>,
    pub detail: Option<T>,
}

// ---- Control-plane onboarding / approvals surface ----

#[derive(Debug, Clone, Deserialize)]
pub struct Tenant {
    pub tenant_id: String,
    pub name: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct OrgUser {
    pub user_id: String,
    pub email: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TenantRegistration {
    pub tenant: Tenant,
    pub admin_user: OrgUser,
    pub approver_user: OrgUser,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Agent {
    pub agent_id: String,
    pub tenant_id: String,
    pub status: String,
    /// Only present in POST /agents's response, shown once -- store it, it
    /// is never returned again.
    pub api_key: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LoginResult {
    pub user: LoggedInUser,
    pub role_ids: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LoggedInUser {
    pub user_id: String,
    pub tenant_id: String,
    pub email: String,
    pub name: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PepConfig {
    pub agent_id: String,
    pub tenant_id: String,
    pub environment: String,
    pub policy_set_id: String,
    pub pep_endpoint: String,
    pub approval_mode: String,
    pub audit_mode: String,
    pub connector_id: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ActionEventStatus {
    pub action_event_id: String,
    pub status: String,
    pub policy_decision: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ApprovalRequest {
    pub approval_request_id: String,
    pub action_event_id: String,
    pub status: String,
}