From e34209ff7f8bcda26f76c3104a7995da0b046737 Mon Sep 17 00:00:00 2001 From: bellman Date: Thu, 14 May 2026 18:05:25 +0900 Subject: [PATCH] omx(team): auto-checkpoint worker-2 [3] --- rust/crates/runtime/src/approval_tokens.rs | 13 +- rust/crates/runtime/src/lib.rs | 9 +- rust/crates/runtime/src/permissions.rs | 444 +-------------------- 3 files changed, 18 insertions(+), 448 deletions(-) diff --git a/rust/crates/runtime/src/approval_tokens.rs b/rust/crates/runtime/src/approval_tokens.rs index e220e442..62340b0a 100644 --- a/rust/crates/runtime/src/approval_tokens.rs +++ b/rust/crates/runtime/src/approval_tokens.rs @@ -240,7 +240,10 @@ impl ApprovalTokenLedger { executing_actor: &str, now_epoch_seconds: u64, ) -> Result { - let grant = self.grants.get(token).ok_or(ApprovalTokenError::NoApproval)?; + 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)) } @@ -272,7 +275,9 @@ impl ApprovalTokenLedger { ) -> Result<(), ApprovalTokenError> { match grant.status { ApprovalTokenStatus::Pending => return Err(ApprovalTokenError::ApprovalPending), - ApprovalTokenStatus::Consumed => return Err(ApprovalTokenError::ApprovalAlreadyConsumed), + ApprovalTokenStatus::Consumed => { + return Err(ApprovalTokenError::ApprovalAlreadyConsumed) + } ApprovalTokenStatus::Expired => return Err(ApprovalTokenError::ApprovalExpired), ApprovalTokenStatus::Revoked => return Err(ApprovalTokenError::ApprovalRevoked), ApprovalTokenStatus::Granted => {} @@ -315,7 +320,9 @@ impl ApprovalTokenLedger { )); } if grant.approving_actor != executing_actor - && !delegation_chain.iter().any(|hop| hop.actor == executing_actor) + && !delegation_chain + .iter() + .any(|hop| hop.actor == executing_actor) { delegation_chain.push(ApprovalDelegationHop::new( executing_actor.to_string(), diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index 925b1dc7..697f514c 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -4,6 +4,7 @@ //! MCP plumbing, tool-facing file operations, and the core conversation loop //! that drives interactive and one-shot turns. +mod approval_tokens; mod bash; pub mod bash_validation; mod bootstrap; @@ -50,6 +51,10 @@ mod trust_resolver; mod usage; pub mod worker_boot; +pub use approval_tokens::{ + ApprovalDelegationHop, ApprovalScope, ApprovalTokenAudit, ApprovalTokenError, + ApprovalTokenGrant, ApprovalTokenLedger, ApprovalTokenStatus, +}; pub use bash::{execute_bash, BashCommandInput, BashCommandOutput}; pub use bootstrap::{BootstrapPhase, BootstrapPlan}; pub use branch_lock::{detect_branch_lock_collisions, BranchLockCollision, BranchLockIntent}; @@ -121,9 +126,7 @@ pub use oauth::{ PkceChallengeMethod, PkceCodePair, }; pub use permissions::{ - ApprovalDelegationHop, ApprovalScope, ApprovalTokenAudit, ApprovalTokenError, - ApprovalTokenGrant, ApprovalTokenLedger, ApprovalTokenStatus, PermissionContext, - PermissionMode, PermissionOutcome, PermissionOverride, PermissionPolicy, + PermissionContext, PermissionMode, PermissionOutcome, PermissionOverride, PermissionPolicy, PermissionPromptDecision, PermissionPrompter, PermissionRequest, }; pub use plugin_lifecycle::{ diff --git a/rust/crates/runtime/src/permissions.rs b/rust/crates/runtime/src/permissions.rs index 30060c4a..81340ddd 100644 --- a/rust/crates/runtime/src/permissions.rs +++ b/rust/crates/runtime/src/permissions.rs @@ -468,310 +468,11 @@ fn extract_permission_subject(input: &str) -> Option { (!input.trim().is_empty()).then(|| input.to_string()) } -/// 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, -} - -/// Approval token record used to prove owner approval, delegated execution, and replay safety. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApprovalTokenGrant { - pub token: String, - pub scope: ApprovalScope, - pub approving_actor: String, - pub executing_actor: String, - pub status: ApprovalTokenStatus, - pub expires_at: Option, - pub uses: u32, - pub delegation_chain: Vec, -} - -impl ApprovalTokenGrant { - #[must_use] - pub fn pending( - token: impl Into, - scope: ApprovalScope, - approving_actor: impl Into, - executing_actor: impl Into, - ) -> Self { - Self::new( - token, - scope, - approving_actor, - executing_actor, - ApprovalTokenStatus::Pending, - ) - } - - #[must_use] - pub fn granted( - token: impl Into, - scope: ApprovalScope, - approving_actor: impl Into, - executing_actor: impl Into, - ) -> Self { - Self::new( - token, - scope, - approving_actor, - executing_actor, - ApprovalTokenStatus::Granted, - ) - } - - fn new( - token: impl Into, - scope: ApprovalScope, - approving_actor: impl Into, - executing_actor: impl Into, - status: ApprovalTokenStatus, - ) -> Self { - let approving_actor = approving_actor.into(); - Self { - token: token.into(), - scope, - executing_actor: executing_actor.into(), - delegation_chain: vec![ApprovalDelegationHop::new( - approving_actor.clone(), - "owner approval", - )], - approving_actor, - status, - expires_at: None, - uses: 0, - } - } - - #[must_use] - pub fn expires_at(mut self, expires_at: u64) -> Self { - self.expires_at = Some(expires_at); - self - } - - #[must_use] - pub fn with_delegation_hop(mut self, hop: ApprovalDelegationHop) -> Self { - self.delegation_chain.push(hop); - self - } -} - -/// Immutable audit returned when a token is verified or consumed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApprovalTokenAudit { - pub token: String, - pub status: ApprovalTokenStatus, - pub scope: ApprovalScope, - pub approving_actor: String, - pub executing_actor: String, - pub delegated_execution: bool, - pub uses: u32, - pub delegation_chain: Vec, -} - -/// Verification errors for policy approval tokens. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ApprovalTokenError { - NoApproval, - ApprovalPending, - ApprovalAlreadyConsumed, - ApprovalExpired, - ApprovalRevoked, - ScopeMismatch { - expected: ApprovalScope, - actual: ApprovalScope, - }, - UnauthorizedDelegate { - expected: String, - actual: String, - }, -} - -/// In-memory approval token ledger for deterministic policy handoff validation. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -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(grant)) - } - - pub fn verify( - &self, - token: &str, - scope: &ApprovalScope, - executing_actor: &str, - now: u64, - ) -> Result { - let grant = self - .grants - .get(token) - .ok_or(ApprovalTokenError::NoApproval)?; - Self::validate_grant(grant, scope, executing_actor, now)?; - Ok(Self::audit(grant)) - } - - pub fn consume( - &mut self, - token: &str, - scope: &ApprovalScope, - executing_actor: &str, - now: u64, - ) -> Result { - let grant = self - .grants - .get_mut(token) - .ok_or(ApprovalTokenError::NoApproval)?; - Self::validate_grant(grant, scope, executing_actor, now)?; - grant.status = ApprovalTokenStatus::Consumed; - grant.uses = grant.uses.saturating_add(1); - Ok(Self::audit(grant)) - } - - fn validate_grant( - grant: &ApprovalTokenGrant, - scope: &ApprovalScope, - executing_actor: &str, - now: u64, - ) -> Result<(), ApprovalTokenError> { - if grant.scope != *scope { - return Err(ApprovalTokenError::ScopeMismatch { - expected: grant.scope.clone(), - actual: scope.clone(), - }); - } - if grant.executing_actor != executing_actor { - return Err(ApprovalTokenError::UnauthorizedDelegate { - expected: grant.executing_actor.clone(), - actual: executing_actor.to_string(), - }); - } - if grant.expires_at.is_some_and(|expires_at| now > expires_at) { - return Err(ApprovalTokenError::ApprovalExpired); - } - match grant.status { - ApprovalTokenStatus::Granted => Ok(()), - ApprovalTokenStatus::Pending => Err(ApprovalTokenError::ApprovalPending), - ApprovalTokenStatus::Consumed => Err(ApprovalTokenError::ApprovalAlreadyConsumed), - ApprovalTokenStatus::Expired => Err(ApprovalTokenError::ApprovalExpired), - ApprovalTokenStatus::Revoked => Err(ApprovalTokenError::ApprovalRevoked), - } - } - - fn audit(grant: &ApprovalTokenGrant) -> ApprovalTokenAudit { - let mut delegation_chain = grant.delegation_chain.clone(); - if delegation_chain - .last() - .is_none_or(|hop| hop.actor != grant.executing_actor) - { - delegation_chain.push(ApprovalDelegationHop::new( - grant.executing_actor.clone(), - "approved execution", - )); - } - ApprovalTokenAudit { - token: grant.token.clone(), - status: grant.status, - scope: grant.scope.clone(), - approving_actor: grant.approving_actor.clone(), - executing_actor: grant.executing_actor.clone(), - delegated_execution: grant.approving_actor != grant.executing_actor, - uses: grant.uses, - delegation_chain, - } - } -} - #[cfg(test)] mod tests { use super::{ - ApprovalDelegationHop, ApprovalScope, ApprovalTokenError, ApprovalTokenGrant, - ApprovalTokenLedger, ApprovalTokenStatus, PermissionContext, PermissionMode, - PermissionOutcome, PermissionOverride, PermissionPolicy, PermissionPromptDecision, - PermissionPrompter, PermissionRequest, + PermissionContext, PermissionMode, PermissionOutcome, PermissionOverride, PermissionPolicy, + PermissionPromptDecision, PermissionPrompter, PermissionRequest, }; use crate::config::RuntimePermissionRuleConfig; @@ -979,145 +680,4 @@ mod tests { Some("hook requested confirmation") ); } - - #[test] - fn approval_token_blocks_until_owner_grants_policy_exception() { - let mut ledger = ApprovalTokenLedger::new(); - let scope = ApprovalScope::new("main_push_forbidden", "git push") - .with_repository("sisyphus/claw-code") - .with_branch("main"); - ledger.insert(ApprovalTokenGrant::pending( - "tok-pending", - scope.clone(), - "repo-owner", - "release-bot", - )); - - assert!(matches!( - ledger.verify("tok-missing", &scope, "release-bot", 10), - Err(ApprovalTokenError::NoApproval) - )); - assert!(matches!( - ledger.verify("tok-pending", &scope, "release-bot", 10), - Err(ApprovalTokenError::ApprovalPending) - )); - - ledger.insert(ApprovalTokenGrant::granted( - "tok-granted", - scope.clone(), - "repo-owner", - "release-bot", - )); - let audit = ledger - .verify("tok-granted", &scope, "release-bot", 10) - .expect("owner approval should verify"); - - assert_eq!(audit.status, ApprovalTokenStatus::Granted); - assert_eq!(audit.approving_actor, "repo-owner"); - assert_eq!(audit.executing_actor, "release-bot"); - assert!(audit.delegated_execution); - } - - #[test] - fn approval_token_is_one_time_use_and_rejects_replay() { - let mut ledger = ApprovalTokenLedger::new(); - let scope = ApprovalScope::new("release_requires_owner", "release publish") - .with_repository("sisyphus/claw-code"); - ledger.insert(ApprovalTokenGrant::granted( - "tok-once", - scope.clone(), - "owner", - "release-bot", - )); - - let first = ledger - .consume("tok-once", &scope, "release-bot", 10) - .expect("first use should consume token"); - assert_eq!(first.status, ApprovalTokenStatus::Consumed); - assert_eq!(first.uses, 1); - - assert!(matches!( - ledger.consume("tok-once", &scope, "release-bot", 11), - Err(ApprovalTokenError::ApprovalAlreadyConsumed) - )); - assert_eq!( - ledger.get("tok-once").map(|grant| grant.status), - Some(ApprovalTokenStatus::Consumed) - ); - } - - #[test] - fn approval_token_rejects_scope_expansion_expiry_and_revocation() { - let mut ledger = ApprovalTokenLedger::new(); - let scope = ApprovalScope::new("main_push_forbidden", "git push") - .with_repository("sisyphus/claw-code") - .with_branch("main"); - let dev_scope = ApprovalScope::new("main_push_forbidden", "git push") - .with_repository("sisyphus/claw-code") - .with_branch("dev"); - - ledger.insert( - ApprovalTokenGrant::granted("tok-expiring", scope.clone(), "owner", "bot") - .expires_at(20), - ); - - assert!(matches!( - ledger.verify("tok-expiring", &dev_scope, "bot", 10), - Err(ApprovalTokenError::ScopeMismatch { .. }) - )); - assert!(matches!( - ledger.verify("tok-expiring", &scope, "bot", 21), - Err(ApprovalTokenError::ApprovalExpired) - )); - - ledger.insert(ApprovalTokenGrant::granted( - "tok-revoked", - scope.clone(), - "owner", - "bot", - )); - let revoked = ledger - .revoke("tok-revoked") - .expect("revocation should be audited"); - assert_eq!(revoked.status, ApprovalTokenStatus::Revoked); - assert!(matches!( - ledger.verify("tok-revoked", &scope, "bot", 10), - Err(ApprovalTokenError::ApprovalRevoked) - )); - } - - #[test] - fn approval_token_preserves_delegation_traceability() { - let mut ledger = ApprovalTokenLedger::new(); - let scope = ApprovalScope::new("deploy_requires_owner", "deploy prod"); - ledger.insert( - ApprovalTokenGrant::granted("tok-delegated", scope.clone(), "owner", "deploy-bot") - .with_delegation_hop( - ApprovalDelegationHop::new("lead-agent", "handoff to deploy bot") - .with_session_id("session-lead"), - ), - ); - - assert!(matches!( - ledger.verify("tok-delegated", &scope, "unexpected-bot", 10), - Err(ApprovalTokenError::UnauthorizedDelegate { expected, actual }) - if expected == "deploy-bot" && actual == "unexpected-bot" - )); - - let audit = ledger - .consume("tok-delegated", &scope, "deploy-bot", 10) - .expect("approved delegate should consume token"); - let actors = audit - .delegation_chain - .iter() - .map(|hop| hop.actor.as_str()) - .collect::>(); - - assert!(audit.delegated_execution); - assert_eq!(actors, vec!["owner", "lead-agent", "deploy-bot"]); - assert_eq!( - audit.delegation_chain[1].session_id.as_deref(), - Some("session-lead") - ); - } }