diff --git a/rust/crates/runtime/src/permissions.rs b/rust/crates/runtime/src/permissions.rs index ca513970..81340ddd 100644 --- a/rust/crates/runtime/src/permissions.rs +++ b/rust/crates/runtime/src/permissions.rs @@ -4,338 +4,6 @@ use serde_json::Value; use crate::config::RuntimePermissionRuleConfig; - -/// Machine-readable policy exception scope that an approval token may override. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApprovalScope { - pub policy: String, - pub action: String, - pub repository: Option, - pub branch: Option, -} - -impl ApprovalScope { - #[must_use] - pub fn new(policy: impl Into, action: impl Into) -> Self { - Self { - policy: policy.into(), - action: action.into(), - repository: None, - branch: None, - } - } - - #[must_use] - pub fn with_repository(mut self, repository: impl Into) -> Self { - self.repository = Some(repository.into()); - self - } - - #[must_use] - pub fn with_branch(mut self, branch: impl Into) -> Self { - self.branch = Some(branch.into()); - self - } -} - -/// Actor/session hop recorded when an approval is delegated or consumed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApprovalDelegationHop { - pub actor: String, - pub session_id: Option, - pub reason: String, -} - -impl ApprovalDelegationHop { - #[must_use] - pub fn new(actor: impl Into, reason: impl Into) -> Self { - Self { - actor: actor.into(), - session_id: None, - reason: reason.into(), - } - } - - #[must_use] - pub fn with_session_id(mut self, session_id: impl Into) -> Self { - self.session_id = Some(session_id.into()); - self - } -} - -/// Current lifecycle state for a policy-exception approval token. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ApprovalTokenStatus { - Pending, - Granted, - Consumed, - Expired, - Revoked, -} - -impl ApprovalTokenStatus { - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::Pending => "approval_pending", - Self::Granted => "approval_granted", - Self::Consumed => "approval_consumed", - Self::Expired => "approval_expired", - Self::Revoked => "approval_revoked", - } - } -} - -/// Typed policy errors returned when a token cannot authorize a blocked action. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ApprovalTokenError { - NoApproval, - ApprovalPending, - ApprovalExpired, - ApprovalRevoked, - ApprovalAlreadyConsumed, - ScopeMismatch { expected: ApprovalScope, actual: ApprovalScope }, - UnauthorizedDelegate { expected: String, actual: String }, -} - -impl ApprovalTokenError { - #[must_use] - pub fn as_str(&self) -> &'static str { - match self { - Self::NoApproval => "no_approval", - Self::ApprovalPending => "approval_pending", - Self::ApprovalExpired => "approval_expired", - Self::ApprovalRevoked => "approval_revoked", - Self::ApprovalAlreadyConsumed => "approval_already_consumed", - Self::ScopeMismatch { .. } => "approval_scope_mismatch", - Self::UnauthorizedDelegate { .. } => "approval_unauthorized_delegate", - } - } -} - -/// Approval grant bound to a policy/action scope, approving owner, and executor. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApprovalTokenGrant { - pub token: String, - pub scope: ApprovalScope, - pub approving_actor: String, - pub approved_executor: String, - pub status: ApprovalTokenStatus, - pub expires_at_epoch_seconds: Option, - pub max_uses: u32, - pub uses: u32, - delegation_chain: Vec, -} - -impl ApprovalTokenGrant { - #[must_use] - pub fn pending( - token: impl Into, - scope: ApprovalScope, - approving_actor: impl Into, - approved_executor: impl Into, - ) -> Self { - Self { - token: token.into(), - scope, - approving_actor: approving_actor.into(), - approved_executor: approved_executor.into(), - status: ApprovalTokenStatus::Pending, - expires_at_epoch_seconds: None, - max_uses: 1, - uses: 0, - delegation_chain: Vec::new(), - } - } - - #[must_use] - pub fn granted( - token: impl Into, - scope: ApprovalScope, - approving_actor: impl Into, - approved_executor: impl Into, - ) -> Self { - Self::pending(token, scope, approving_actor, approved_executor).approve() - } - - #[must_use] - pub fn approve(mut self) -> Self { - self.status = ApprovalTokenStatus::Granted; - self - } - - #[must_use] - pub fn expires_at(mut self, epoch_seconds: u64) -> Self { - self.expires_at_epoch_seconds = Some(epoch_seconds); - self - } - - #[must_use] - pub fn with_max_uses(mut self, max_uses: u32) -> Self { - self.max_uses = max_uses.max(1); - self - } - - #[must_use] - pub fn with_delegation_hop(mut self, hop: ApprovalDelegationHop) -> Self { - self.delegation_chain.push(hop); - self - } - - #[must_use] - pub fn delegation_chain(&self) -> &[ApprovalDelegationHop] { - &self.delegation_chain - } -} - -/// Auditable result of verifying or consuming an approval token. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApprovalTokenAudit { - pub token: String, - pub scope: ApprovalScope, - pub approving_actor: String, - pub executing_actor: String, - pub status: ApprovalTokenStatus, - pub delegated_execution: bool, - pub delegation_chain: Vec, - pub uses: u32, - pub max_uses: u32, -} - -/// In-memory approval-token ledger with one-time-use and replay protection. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct ApprovalTokenLedger { - grants: BTreeMap, -} - -impl ApprovalTokenLedger { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - pub fn insert(&mut self, grant: ApprovalTokenGrant) { - self.grants.insert(grant.token.clone(), grant); - } - - #[must_use] - pub fn get(&self, token: &str) -> Option<&ApprovalTokenGrant> { - self.grants.get(token) - } - - pub fn revoke(&mut self, token: &str) -> Result { - let grant = self - .grants - .get_mut(token) - .ok_or(ApprovalTokenError::NoApproval)?; - grant.status = ApprovalTokenStatus::Revoked; - Ok(Self::audit_for(grant, &grant.approved_executor)) - } - - pub fn verify( - &self, - token: &str, - scope: &ApprovalScope, - executing_actor: &str, - now_epoch_seconds: u64, - ) -> Result { - let grant = self.grants.get(token).ok_or(ApprovalTokenError::NoApproval)?; - Self::validate_grant(grant, scope, executing_actor, now_epoch_seconds)?; - Ok(Self::audit_for(grant, executing_actor)) - } - - pub fn consume( - &mut self, - token: &str, - scope: &ApprovalScope, - executing_actor: &str, - now_epoch_seconds: u64, - ) -> Result { - let grant = self - .grants - .get_mut(token) - .ok_or(ApprovalTokenError::NoApproval)?; - Self::validate_grant(grant, scope, executing_actor, now_epoch_seconds)?; - grant.uses += 1; - if grant.uses >= grant.max_uses { - grant.status = ApprovalTokenStatus::Consumed; - } - Ok(Self::audit_for(grant, executing_actor)) - } - - fn validate_grant( - grant: &ApprovalTokenGrant, - scope: &ApprovalScope, - executing_actor: &str, - now_epoch_seconds: u64, - ) -> Result<(), ApprovalTokenError> { - match grant.status { - ApprovalTokenStatus::Pending => return Err(ApprovalTokenError::ApprovalPending), - ApprovalTokenStatus::Consumed => return Err(ApprovalTokenError::ApprovalAlreadyConsumed), - ApprovalTokenStatus::Expired => return Err(ApprovalTokenError::ApprovalExpired), - ApprovalTokenStatus::Revoked => return Err(ApprovalTokenError::ApprovalRevoked), - ApprovalTokenStatus::Granted => {} - } - - if grant - .expires_at_epoch_seconds - .is_some_and(|expires_at| now_epoch_seconds > expires_at) - { - return Err(ApprovalTokenError::ApprovalExpired); - } - - if grant.uses >= grant.max_uses { - return Err(ApprovalTokenError::ApprovalAlreadyConsumed); - } - - if grant.scope != *scope { - return Err(ApprovalTokenError::ScopeMismatch { - expected: grant.scope.clone(), - actual: scope.clone(), - }); - } - - if grant.approved_executor != executing_actor { - return Err(ApprovalTokenError::UnauthorizedDelegate { - expected: grant.approved_executor.clone(), - actual: executing_actor.to_string(), - }); - } - - Ok(()) - } - - fn audit_for(grant: &ApprovalTokenGrant, executing_actor: &str) -> ApprovalTokenAudit { - let mut delegation_chain = grant.delegation_chain.clone(); - if delegation_chain.is_empty() { - delegation_chain.push(ApprovalDelegationHop::new( - grant.approving_actor.clone(), - "approval granted", - )); - } - if grant.approving_actor != executing_actor - && !delegation_chain.iter().any(|hop| hop.actor == executing_actor) - { - delegation_chain.push(ApprovalDelegationHop::new( - executing_actor.to_string(), - "delegated execution", - )); - } - - ApprovalTokenAudit { - token: grant.token.clone(), - scope: grant.scope.clone(), - approving_actor: grant.approving_actor.clone(), - executing_actor: executing_actor.to_string(), - status: grant.status, - delegated_execution: grant.approving_actor != executing_actor, - delegation_chain, - uses: grant.uses, - max_uses: grant.max_uses, - } - } -} - /// Permission level assigned to a tool invocation or runtime session. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum PermissionMode {