//! The approval state machine (spec §15, acceptance-blocking). //! //! A gated tool call becomes a pending approval with the exact payload and //! rendered preview. Decisions are idempotent compare-and-swaps audited in //! the same transaction; approval mints a single-use execution grant the //! executor must consume before the action runs. Suspended runs checkpoint //! their full state and are claimed for resume exactly once. pub mod approvals; pub mod checkpoint; pub mod grants; use cm_domain::{AgentId, GatedCategory, UserId, WorkspaceId}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; #[derive(Debug, thiserror::Error)] pub enum SafetyError { #[error("approval not found")] NotFound, #[error("approval already decided")] AlreadyDecided, #[error("no consumable grant for this approval")] GrantUnavailable, #[error(transparent)] Db(#[from] sqlx::Error), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ApprovalStatus { Pending, Approved, Rejected, Expired, } impl ApprovalStatus { pub fn as_str(&self) -> &'static str { match self { ApprovalStatus::Pending => "pending", ApprovalStatus::Approved => "approved", ApprovalStatus::Rejected => "rejected", ApprovalStatus::Expired => "expired", } } } impl std::str::FromStr for ApprovalStatus { type Err = String; fn from_str(s: &str) -> Result { match s { "pending" => Ok(ApprovalStatus::Pending), "approved" => Ok(ApprovalStatus::Approved), "rejected" => Ok(ApprovalStatus::Rejected), "expired" => Ok(ApprovalStatus::Expired), other => Err(format!("unknown approval status: {other}")), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Decision { Approve, Reject, } /// Input for a new pending approval. #[derive(Debug, Clone)] pub struct NewApproval { pub workspace_id: WorkspaceId, pub run_id: Uuid, pub session_key: String, pub action_type: String, pub category: GatedCategory, /// The exact tool input that will execute on approval. pub payload: serde_json::Value, /// The exact rendering shown to the human (§10 approval card). pub preview: serde_json::Value, pub requested_by_agent: AgentId, pub taint_sources: Vec, pub expires_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Approval { pub id: Uuid, pub workspace_id: WorkspaceId, pub run_id: Uuid, pub session_key: String, pub action_type: String, pub category: GatedCategory, pub payload: serde_json::Value, pub preview: serde_json::Value, pub requested_by_agent: AgentId, pub taint_sources: Vec, pub status: ApprovalStatus, pub decided_by: Option, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, } /// A decided approval whose suspended run has not been resumed yet. #[derive(Debug, Clone, Copy)] pub struct ResumeReady { pub run_id: Uuid, pub approval_id: Uuid, pub approved: bool, }