omx(team): auto-checkpoint worker-2 [3]

This commit is contained in:
bellman
2026-05-14 18:05:25 +09:00
parent ff37d395bb
commit e34209ff7f
3 changed files with 18 additions and 448 deletions

View File

@@ -240,7 +240,10 @@ impl ApprovalTokenLedger {
executing_actor: &str,
now_epoch_seconds: u64,
) -> Result<ApprovalTokenAudit, ApprovalTokenError> {
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(),

View File

@@ -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::{

View File

@@ -468,310 +468,11 @@ fn extract_permission_subject(input: &str) -> Option<String> {
(!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<String>,
pub branch: Option<String>,
}
impl ApprovalScope {
#[must_use]
pub fn new(policy: impl Into<String>, action: impl Into<String>) -> Self {
Self {
policy: policy.into(),
action: action.into(),
repository: None,
branch: None,
}
}
#[must_use]
pub fn with_repository(mut self, repository: impl Into<String>) -> Self {
self.repository = Some(repository.into());
self
}
#[must_use]
pub fn with_branch(mut self, branch: impl Into<String>) -> 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<String>,
pub reason: String,
}
impl ApprovalDelegationHop {
#[must_use]
pub fn new(actor: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
actor: actor.into(),
session_id: None,
reason: reason.into(),
}
}
#[must_use]
pub fn with_session_id(mut self, session_id: impl Into<String>) -> 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<u64>,
pub uses: u32,
pub delegation_chain: Vec<ApprovalDelegationHop>,
}
impl ApprovalTokenGrant {
#[must_use]
pub fn pending(
token: impl Into<String>,
scope: ApprovalScope,
approving_actor: impl Into<String>,
executing_actor: impl Into<String>,
) -> Self {
Self::new(
token,
scope,
approving_actor,
executing_actor,
ApprovalTokenStatus::Pending,
)
}
#[must_use]
pub fn granted(
token: impl Into<String>,
scope: ApprovalScope,
approving_actor: impl Into<String>,
executing_actor: impl Into<String>,
) -> Self {
Self::new(
token,
scope,
approving_actor,
executing_actor,
ApprovalTokenStatus::Granted,
)
}
fn new(
token: impl Into<String>,
scope: ApprovalScope,
approving_actor: impl Into<String>,
executing_actor: impl Into<String>,
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<ApprovalDelegationHop>,
}
/// 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<String, ApprovalTokenGrant>,
}
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<ApprovalTokenAudit, ApprovalTokenError> {
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<ApprovalTokenAudit, ApprovalTokenError> {
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<ApprovalTokenAudit, ApprovalTokenError> {
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::<Vec<_>>();
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")
);
}
}