use crate::generated::approval_lifecycle::{
ApprovalLifecycleDecision, ApprovalLifecycleMachineAuthority, ApprovalLifecycleOutcome,
ApprovalLifecycleRejectionReason, ApprovalLifecycleStatus,
};
use crate::lifecycle::identifiers::RunId;
use crate::{SessionId, SurfaceMetadata, ToolCallId};
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ApprovalId(#[cfg_attr(feature = "schema", schemars(with = "String"))] pub Uuid);
impl ApprovalId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for ApprovalId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ApprovalId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for ApprovalId {
type Err = uuid::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(value)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ApprovalPrincipalId(String);
impl ApprovalPrincipalId {
pub fn new(value: impl Into<String>) -> Result<Self, ApprovalError> {
let value = value.into();
if value.trim().is_empty() {
return Err(ApprovalError::InvalidPrincipal);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ApprovalPrincipalId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ApprovalMobRef(String);
impl ApprovalMobRef {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ApprovalMobRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ApprovalMemberRef(String);
impl ApprovalMemberRef {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ApprovalMemberRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ApprovalResourceId(String);
impl ApprovalResourceId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ApprovalResourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "owner_type", rename_all = "snake_case")]
pub enum ApprovalOwnerRef {
Runtime,
Session {
session_id: SessionId,
},
Mob {
mob_id: ApprovalMobRef,
},
Run {
run_id: RunId,
},
ToolCall {
tool_call_id: ToolCallId,
},
ExternalMember {
mob_id: ApprovalMobRef,
member_ref: ApprovalMemberRef,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ApprovalResourceKind {
File,
ShellCommand,
ToolCall,
Device,
Runtime,
Network,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApprovalResourceRef {
pub kind: ApprovalResourceKind,
pub id: ApprovalResourceId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ApprovalActionKind {
ShellCommand,
FileWrite,
FileDelete,
NetworkCall,
DeviceControl,
ToolCall,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApprovalProposedAction {
pub kind: ApprovalActionKind,
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ApprovalRisk {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ApprovalDecision {
Approve,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ApprovalStatus {
Pending,
Approved,
Denied,
Expired,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApprovalDecisionRecord {
pub decision: ApprovalDecision,
pub actor: ApprovalPrincipalId,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub decided_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provenance: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApprovalRecord {
pub approval_id: ApprovalId,
pub status: ApprovalStatus,
pub requester: ApprovalPrincipalId,
pub owner: ApprovalOwnerRef,
pub resource: ApprovalResourceRef,
pub proposed_action: ApprovalProposedAction,
pub risk: ApprovalRisk,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_body: Option<serde_json::Value>,
pub allowed_decisions: BTreeSet<ApprovalDecision>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
pub expires_at: Option<DateTime<Utc>>,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub created_at: DateTime<Utc>,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub updated_at: DateTime<Utc>,
pub metadata: SurfaceMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_provenance: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decision: Option<ApprovalDecisionRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApprovalRequest {
pub requester: ApprovalPrincipalId,
pub owner: ApprovalOwnerRef,
pub resource: ApprovalResourceRef,
pub proposed_action: ApprovalProposedAction,
pub risk: ApprovalRisk,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_body: Option<serde_json::Value>,
pub allowed_decisions: BTreeSet<ApprovalDecision>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
pub expires_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "SurfaceMetadata::is_empty")]
pub metadata: SurfaceMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_provenance: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ApprovalListFilter {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<ApprovalStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ApprovalError {
#[error("approval not found: {approval_id}")]
NotFound { approval_id: ApprovalId },
#[error("approval has already been decided: {approval_id}")]
AlreadyDecided { approval_id: ApprovalId },
#[error("approval is expired: {approval_id}")]
Expired { approval_id: ApprovalId },
#[error("decision is not allowed for approval: {decision:?}")]
InvalidDecision { decision: ApprovalDecision },
#[error("approval request must allow at least one decision")]
EmptyAllowedDecisions,
#[error("approval principal id must not be empty")]
InvalidPrincipal,
#[error(transparent)]
InvalidMetadata(#[from] crate::SurfaceMetadataError),
#[error("approval store error: {0}")]
Store(String),
}
pub trait ApprovalStore: Send + Sync {
fn load_all(&self) -> Result<Vec<ApprovalRecord>, ApprovalStoreError>;
fn put(&self, record: &ApprovalRecord) -> Result<(), ApprovalStoreError>;
fn is_persistent(&self) -> bool;
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ApprovalStoreError {
#[error("{0}")]
Backend(String),
}
impl From<ApprovalStoreError> for ApprovalError {
fn from(value: ApprovalStoreError) -> Self {
Self::Store(value.to_string())
}
}
#[derive(Debug, Default)]
pub struct InMemoryApprovalStore {
records: RwLock<BTreeMap<ApprovalId, ApprovalRecord>>,
}
impl InMemoryApprovalStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl ApprovalStore for InMemoryApprovalStore {
fn load_all(&self) -> Result<Vec<ApprovalRecord>, ApprovalStoreError> {
Ok(self.records.read().values().cloned().collect())
}
fn put(&self, record: &ApprovalRecord) -> Result<(), ApprovalStoreError> {
self.records
.write()
.insert(record.approval_id.clone(), record.clone());
Ok(())
}
fn is_persistent(&self) -> bool {
false
}
}
#[derive(Debug, Clone)]
struct ApprovalServiceState {
records: BTreeMap<ApprovalId, ApprovalRecord>,
authority: ApprovalLifecycleMachineAuthority,
}
impl ApprovalServiceState {
fn empty() -> Self {
Self {
records: BTreeMap::new(),
authority: ApprovalLifecycleMachineAuthority::new(),
}
}
fn from_records(records: Vec<ApprovalRecord>) -> Result<Self, ApprovalError> {
let mut state = Self::empty();
for record in records {
restore_record_into_authority(&mut state.authority, &record)?;
state.records.insert(record.approval_id.clone(), record);
}
Ok(state)
}
}
fn approval_lifecycle_status(status: ApprovalStatus) -> ApprovalLifecycleStatus {
match status {
ApprovalStatus::Pending => ApprovalLifecycleStatus::Pending,
ApprovalStatus::Approved => ApprovalLifecycleStatus::Approved,
ApprovalStatus::Denied => ApprovalLifecycleStatus::Denied,
ApprovalStatus::Expired => ApprovalLifecycleStatus::Expired,
ApprovalStatus::Cancelled => ApprovalLifecycleStatus::Cancelled,
}
}
fn approval_status_from_lifecycle(status: ApprovalLifecycleStatus) -> ApprovalStatus {
match status {
ApprovalLifecycleStatus::Pending => ApprovalStatus::Pending,
ApprovalLifecycleStatus::Approved => ApprovalStatus::Approved,
ApprovalLifecycleStatus::Denied => ApprovalStatus::Denied,
ApprovalLifecycleStatus::Expired => ApprovalStatus::Expired,
ApprovalLifecycleStatus::Cancelled => ApprovalStatus::Cancelled,
}
}
fn approval_lifecycle_decision(decision: ApprovalDecision) -> ApprovalLifecycleDecision {
match decision {
ApprovalDecision::Approve => ApprovalLifecycleDecision::Approve,
ApprovalDecision::Deny => ApprovalLifecycleDecision::Deny,
}
}
fn allowed_decision_flags(allowed_decisions: &BTreeSet<ApprovalDecision>) -> (bool, bool) {
(
allowed_decisions.contains(&ApprovalDecision::Approve),
allowed_decisions.contains(&ApprovalDecision::Deny),
)
}
fn lifecycle_rejection_error(
approval_id: &ApprovalId,
reason: ApprovalLifecycleRejectionReason,
decision: Option<ApprovalDecision>,
) -> ApprovalError {
match reason {
ApprovalLifecycleRejectionReason::NotFound => ApprovalError::NotFound {
approval_id: approval_id.clone(),
},
ApprovalLifecycleRejectionReason::AlreadyDecided => ApprovalError::AlreadyDecided {
approval_id: approval_id.clone(),
},
ApprovalLifecycleRejectionReason::Expired => ApprovalError::Expired {
approval_id: approval_id.clone(),
},
ApprovalLifecycleRejectionReason::InvalidDecision => {
let Some(decision) = decision else {
return ApprovalError::Store(format!(
"generated approval lifecycle rejected {approval_id} with InvalidDecision but no decision context"
));
};
ApprovalError::InvalidDecision { decision }
}
ApprovalLifecycleRejectionReason::EmptyAllowedDecisions => {
ApprovalError::EmptyAllowedDecisions
}
ApprovalLifecycleRejectionReason::AlreadyExists
| ApprovalLifecycleRejectionReason::InvalidRestoredRecord => ApprovalError::Store(format!(
"generated approval lifecycle authority rejected {approval_id} with {reason:?}"
)),
}
}
fn lifecycle_status_from_outcome(
approval_id: &ApprovalId,
outcome: ApprovalLifecycleOutcome,
decision: Option<ApprovalDecision>,
) -> Result<ApprovalStatus, ApprovalError> {
match outcome {
ApprovalLifecycleOutcome::Status(status) => Ok(approval_status_from_lifecycle(status)),
ApprovalLifecycleOutcome::Rejected(reason) => {
Err(lifecycle_rejection_error(approval_id, reason, decision))
}
}
}
fn lifecycle_error(error: impl std::fmt::Display) -> ApprovalError {
ApprovalError::Store(format!(
"generated approval lifecycle authority failed: {error}"
))
}
fn restored_lifecycle_decision(record: &ApprovalRecord) -> Option<ApprovalLifecycleDecision> {
record
.decision
.as_ref()
.map(|decision| approval_lifecycle_decision(decision.decision))
}
fn restore_record_into_authority(
authority: &mut ApprovalLifecycleMachineAuthority,
record: &ApprovalRecord,
) -> Result<(), ApprovalError> {
let (approve_allowed, deny_allowed) = allowed_decision_flags(&record.allowed_decisions);
let outcome = authority
.restore_approval(
record.approval_id.to_string(),
approval_lifecycle_status(record.status),
approve_allowed,
deny_allowed,
record.expires_at.is_some(),
restored_lifecycle_decision(record),
)
.map_err(lifecycle_error)?;
let restored_status = lifecycle_status_from_outcome(
&record.approval_id,
outcome,
record.decision.as_ref().map(|decision| decision.decision),
)?;
if restored_status != record.status {
return Err(ApprovalError::Store(format!(
"generated approval lifecycle restored {} as {:?}, stored {:?}",
record.approval_id, restored_status, record.status
)));
}
Ok(())
}
#[derive(Clone)]
pub struct ApprovalService {
state: Arc<RwLock<ApprovalServiceState>>,
store: Arc<dyn ApprovalStore>,
unavailable_reason: Option<Arc<str>>,
}
impl ApprovalService {
#[must_use]
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(ApprovalServiceState::empty())),
store: Arc::new(InMemoryApprovalStore::new()),
unavailable_reason: None,
}
}
pub fn with_store(store: Arc<dyn ApprovalStore>) -> Result<Self, ApprovalError> {
let state = ApprovalServiceState::from_records(store.load_all()?)?;
Ok(Self {
state: Arc::new(RwLock::new(state)),
store,
unavailable_reason: None,
})
}
#[must_use]
pub fn unavailable(reason: impl Into<String>) -> Self {
Self {
state: Arc::new(RwLock::new(ApprovalServiceState::empty())),
store: Arc::new(InMemoryApprovalStore::new()),
unavailable_reason: Some(Arc::from(reason.into())),
}
}
#[must_use]
pub fn is_persistent(&self) -> bool {
self.store.is_persistent()
}
fn ensure_available(&self) -> Result<(), ApprovalError> {
if let Some(reason) = &self.unavailable_reason {
return Err(ApprovalError::Store(format!(
"approval service unavailable: {reason}"
)));
}
Ok(())
}
pub fn request(&self, request: ApprovalRequest) -> Result<ApprovalRecord, ApprovalError> {
self.ensure_available()?;
if request.requester.as_str().trim().is_empty() {
return Err(ApprovalError::InvalidPrincipal);
}
request.metadata.validate_public()?;
let now = Utc::now();
let approval_id = ApprovalId::new();
let (approve_allowed, deny_allowed) = allowed_decision_flags(&request.allowed_decisions);
let mut state = self.state.write();
let mut authority = state.authority.clone();
let outcome = authority
.create_approval(
approval_id.to_string(),
approve_allowed,
deny_allowed,
request.expires_at.is_some(),
)
.map_err(lifecycle_error)?;
let status = lifecycle_status_from_outcome(&approval_id, outcome, None)?;
let record = ApprovalRecord {
approval_id,
status,
requester: request.requester,
owner: request.owner,
resource: request.resource,
proposed_action: request.proposed_action,
risk: request.risk,
request_body: request.request_body,
allowed_decisions: request.allowed_decisions,
expires_at: request.expires_at,
created_at: now,
updated_at: now,
metadata: request.metadata,
request_provenance: request.request_provenance,
decision: None,
};
self.store.put(&record)?;
state.authority = authority;
state
.records
.insert(record.approval_id.clone(), record.clone());
Ok(record)
}
pub fn get(&self, approval_id: &ApprovalId) -> Result<ApprovalRecord, ApprovalError> {
self.ensure_available()?;
self.refresh_expiry(approval_id)?;
self.state
.read()
.records
.get(approval_id)
.cloned()
.ok_or_else(|| ApprovalError::NotFound {
approval_id: approval_id.clone(),
})
}
pub fn list(&self, filter: ApprovalListFilter) -> Result<Vec<ApprovalRecord>, ApprovalError> {
self.ensure_available()?;
self.refresh_all_expiry()?;
Ok(self
.state
.read()
.records
.values()
.filter(|record| filter.status.is_none_or(|status| record.status == status))
.cloned()
.collect())
}
pub fn decide(
&self,
approval_id: &ApprovalId,
decision: ApprovalDecision,
actor: ApprovalPrincipalId,
reason: Option<String>,
provenance: Option<serde_json::Value>,
) -> Result<ApprovalRecord, ApprovalError> {
self.ensure_available()?;
if actor.as_str().trim().is_empty() {
return Err(ApprovalError::InvalidPrincipal);
}
let now = Utc::now();
let mut state = self.state.write();
if !state.records.contains_key(approval_id) {
let mut authority = state.authority.clone();
let outcome = authority
.decide_approval(
approval_id.to_string(),
approval_lifecycle_decision(decision),
)
.map_err(lifecycle_error)?;
return match outcome {
ApprovalLifecycleOutcome::Rejected(reason) => Err(lifecycle_rejection_error(
approval_id,
reason,
Some(decision),
)),
ApprovalLifecycleOutcome::Status(status) => Err(ApprovalError::Store(format!(
"generated approval lifecycle emitted {status:?} for missing approval {approval_id}"
))),
};
}
self.refresh_expiry_locked(&mut state, approval_id, now)?;
let record =
state
.records
.get(approval_id)
.cloned()
.ok_or_else(|| ApprovalError::NotFound {
approval_id: approval_id.clone(),
})?;
let mut authority = state.authority.clone();
let outcome = authority
.decide_approval(
approval_id.to_string(),
approval_lifecycle_decision(decision),
)
.map_err(lifecycle_error)?;
let status = lifecycle_status_from_outcome(approval_id, outcome, Some(decision))?;
let mut decided_record = record;
decided_record.status = status;
decided_record.updated_at = now;
decided_record.decision = Some(ApprovalDecisionRecord {
decision,
actor,
decided_at: now,
reason,
provenance,
});
self.store.put(&decided_record)?;
state.authority = authority;
state
.records
.insert(approval_id.clone(), decided_record.clone());
Ok(decided_record)
}
fn refresh_expiry(&self, approval_id: &ApprovalId) -> Result<(), ApprovalError> {
let now = Utc::now();
let mut state = self.state.write();
self.refresh_expiry_locked(&mut state, approval_id, now)
}
fn refresh_expiry_locked(
&self,
state: &mut ApprovalServiceState,
approval_id: &ApprovalId,
now: DateTime<Utc>,
) -> Result<(), ApprovalError> {
let Some(record) = state.records.get(approval_id).cloned() else {
return Ok(());
};
let expired = record
.expires_at
.is_some_and(|expires_at| expires_at <= now);
let mut authority = state.authority.clone();
let outcome = authority
.observe_approval_expiry(approval_id.to_string(), expired)
.map_err(lifecycle_error)?;
let status = lifecycle_status_from_outcome(approval_id, outcome, None)?;
if status == record.status {
state.authority = authority;
} else {
let mut expired_record = record;
expired_record.status = status;
expired_record.updated_at = now;
self.store.put(&expired_record)?;
state.authority = authority;
state.records.insert(approval_id.clone(), expired_record);
}
Ok(())
}
fn refresh_all_expiry(&self) -> Result<(), ApprovalError> {
let ids = self
.state
.read()
.records
.keys()
.cloned()
.collect::<Vec<_>>();
for id in ids {
self.refresh_expiry(&id)?;
}
Ok(())
}
}
impl Default for ApprovalService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use chrono::Duration;
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug)]
struct TestApprovalStore {
records: RwLock<BTreeMap<ApprovalId, ApprovalRecord>>,
put_calls: AtomicUsize,
fail_on_put_call: Option<usize>,
}
impl TestApprovalStore {
fn new(fail_on_put_call: Option<usize>) -> Self {
Self {
records: RwLock::new(BTreeMap::new()),
put_calls: AtomicUsize::new(0),
fail_on_put_call,
}
}
fn record(&self, approval_id: &ApprovalId) -> Option<ApprovalRecord> {
self.records.read().get(approval_id).cloned()
}
}
impl ApprovalStore for TestApprovalStore {
fn load_all(&self) -> Result<Vec<ApprovalRecord>, ApprovalStoreError> {
Ok(self.records.read().values().cloned().collect())
}
fn put(&self, record: &ApprovalRecord) -> Result<(), ApprovalStoreError> {
let put_call = self.put_calls.fetch_add(1, Ordering::SeqCst) + 1;
if self
.fail_on_put_call
.is_some_and(|fail_on_put_call| fail_on_put_call == put_call)
{
return Err(ApprovalStoreError::Backend(
"injected approval store failure".to_string(),
));
}
self.records
.write()
.insert(record.approval_id.clone(), record.clone());
Ok(())
}
fn is_persistent(&self) -> bool {
true
}
}
fn principal(value: &str) -> ApprovalPrincipalId {
ApprovalPrincipalId::new(value).expect("valid principal")
}
fn request_with_allowed(allowed_decisions: BTreeSet<ApprovalDecision>) -> ApprovalRequest {
ApprovalRequest {
requester: principal("human:alice"),
owner: ApprovalOwnerRef::Session {
session_id: SessionId::new(),
},
resource: ApprovalResourceRef {
kind: ApprovalResourceKind::ShellCommand,
id: ApprovalResourceId::new("shell:rm"),
},
proposed_action: ApprovalProposedAction {
kind: ApprovalActionKind::ShellCommand,
summary: "run destructive command".to_string(),
body: Some(json!({"cmd": "rm -rf target/tmp"})),
},
risk: ApprovalRisk::High,
request_body: Some(json!({"why": "cleanup"})),
allowed_decisions,
expires_at: None,
metadata: SurfaceMetadata::default(),
request_provenance: Some(json!({"tool_call_id": "call-1"})),
}
}
fn request() -> ApprovalRequest {
request_with_allowed(BTreeSet::from([
ApprovalDecision::Approve,
ApprovalDecision::Deny,
]))
}
#[test]
fn approval_request_creates_pending_auditable_record() {
let service = ApprovalService::new();
let record = service.request(request()).expect("request accepted");
assert_eq!(record.status, ApprovalStatus::Pending);
assert_eq!(record.requester.as_str(), "human:alice");
assert_eq!(
record.request_provenance,
Some(json!({"tool_call_id": "call-1"}))
);
assert!(record.decision.is_none());
}
#[test]
fn decide_preserves_request_provenance_and_records_decision_audit() {
let service = ApprovalService::new();
let record = service.request(request()).expect("request accepted");
let decided = service
.decide(
&record.approval_id,
ApprovalDecision::Approve,
principal("human:bob"),
Some("looks intentional".to_string()),
Some(json!({"client": "mobile"})),
)
.expect("decision accepted");
assert_eq!(decided.status, ApprovalStatus::Approved);
assert_eq!(decided.request_provenance, record.request_provenance);
let decision = decided.decision.expect("decision audit");
assert_eq!(decision.actor.as_str(), "human:bob");
assert_eq!(decision.provenance, Some(json!({"client": "mobile"})));
}
#[test]
fn invalid_decision_is_rejected() {
let service = ApprovalService::new();
let record = service
.request(request_with_allowed(BTreeSet::from([
ApprovalDecision::Deny,
])))
.expect("request accepted");
let err = service
.decide(
&record.approval_id,
ApprovalDecision::Approve,
principal("human:bob"),
None,
None,
)
.expect_err("approval should reject disallowed decision");
assert!(matches!(
err,
ApprovalError::InvalidDecision {
decision: ApprovalDecision::Approve
}
));
}
#[test]
fn empty_allowed_decisions_are_rejected_by_generated_authority() {
let service = ApprovalService::new();
let err = service
.request(request_with_allowed(BTreeSet::new()))
.expect_err("empty allowed decisions rejected");
assert!(matches!(err, ApprovalError::EmptyAllowedDecisions));
}
#[test]
fn duplicate_decision_is_rejected() {
let service = ApprovalService::new();
let record = service.request(request()).expect("request accepted");
service
.decide(
&record.approval_id,
ApprovalDecision::Deny,
principal("human:bob"),
None,
None,
)
.expect("first decision accepted");
let err = service
.decide(
&record.approval_id,
ApprovalDecision::Deny,
principal("human:bob"),
None,
None,
)
.expect_err("duplicate rejected");
assert!(matches!(err, ApprovalError::AlreadyDecided { .. }));
}
#[test]
fn failed_decision_persist_keeps_approval_pending_for_retry() {
let store = Arc::new(TestApprovalStore::new(Some(2)));
let service = ApprovalService::with_store(store.clone()).expect("service");
let record = service.request(request()).expect("request accepted");
let err = service
.decide(
&record.approval_id,
ApprovalDecision::Approve,
principal("human:bob"),
None,
None,
)
.expect_err("decision write should fail");
assert!(matches!(err, ApprovalError::Store(_)));
let cached = service.get(&record.approval_id).expect("cached record");
assert_eq!(cached.status, ApprovalStatus::Pending);
assert!(cached.decision.is_none());
let persisted = store.record(&record.approval_id).expect("persisted record");
assert_eq!(persisted.status, ApprovalStatus::Pending);
assert!(persisted.decision.is_none());
let retried = service
.decide(
&record.approval_id,
ApprovalDecision::Deny,
principal("human:bob"),
Some("changed my mind".to_string()),
None,
)
.expect("retry should decide approval");
assert_eq!(retried.status, ApprovalStatus::Denied);
}
#[test]
fn restore_rejects_inconsistent_persisted_status() {
let store = Arc::new(TestApprovalStore::new(None));
let mut record = ApprovalService::new()
.request(request())
.expect("request accepted");
record.status = ApprovalStatus::Approved;
record.updated_at = Utc::now();
store
.records
.write()
.insert(record.approval_id.clone(), record);
let restored = ApprovalService::with_store(store);
assert!(matches!(restored, Err(ApprovalError::Store(_))));
}
#[test]
fn unavailable_approval_service_fails_closed() {
let service = ApprovalService::unavailable("persistent approval restore failed");
let err = service
.list(ApprovalListFilter::default())
.expect_err("unavailable service should reject reads");
assert!(matches!(
err,
ApprovalError::Store(message) if message.contains("persistent approval restore failed")
));
let err = service
.request(request())
.expect_err("unavailable service should reject writes");
assert!(matches!(
err,
ApprovalError::Store(message) if message.contains("persistent approval restore failed")
));
}
#[test]
fn expired_approval_cannot_be_decided() {
let service = ApprovalService::new();
let mut request = request();
request.expires_at = Some(Utc::now() - Duration::seconds(1));
let record = service.request(request).expect("request accepted");
let err = service
.decide(
&record.approval_id,
ApprovalDecision::Approve,
principal("human:bob"),
None,
None,
)
.expect_err("expired approval rejected");
assert!(matches!(err, ApprovalError::Expired { .. }));
assert_eq!(
service.get(&record.approval_id).expect("record").status,
ApprovalStatus::Expired
);
}
#[test]
fn deciding_expired_approval_persists_expiry_transition() {
let store = Arc::new(TestApprovalStore::new(None));
let service = ApprovalService::with_store(store.clone()).expect("service");
let mut request = request();
request.expires_at = Some(Utc::now() - Duration::seconds(1));
let record = service.request(request).expect("request accepted");
let err = service
.decide(
&record.approval_id,
ApprovalDecision::Approve,
principal("human:bob"),
None,
None,
)
.expect_err("expired approval rejected");
assert!(matches!(err, ApprovalError::Expired { .. }));
let persisted = store.record(&record.approval_id).expect("persisted record");
assert_eq!(persisted.status, ApprovalStatus::Expired);
assert!(persisted.decision.is_none());
}
#[test]
fn nonexistent_approval_cannot_be_decided() {
let service = ApprovalService::new();
let err = service
.decide(
&ApprovalId::new(),
ApprovalDecision::Approve,
principal("human:bob"),
None,
None,
)
.expect_err("unknown approval rejected");
assert!(matches!(err, ApprovalError::NotFound { .. }));
}
#[test]
fn reserved_metadata_spoofing_is_rejected() {
let service = ApprovalService::new();
let mut request = request();
request
.metadata
.labels
.insert("meerkat.approval_id".to_string(), "spoof".to_string());
let err = service
.request(request)
.expect_err("reserved metadata rejected");
assert!(matches!(
err,
ApprovalError::InvalidMetadata(crate::SurfaceMetadataError::ReservedLabelKey { .. })
));
}
}