use openagent_aegis_core::{Delegation, DelegationError, DelegationScope};
use std::collections::HashMap;
use crate::scope::{intersect_scopes, is_scope_subset};
pub struct DelegationTree {
delegations: HashMap<String, Vec<Delegation>>,
id_index: HashMap<String, (String, String)>,
max_depth: u32,
}
impl DelegationTree {
pub fn new(max_depth: u32) -> Self {
Self {
delegations: HashMap::new(),
id_index: HashMap::new(),
max_depth,
}
}
pub fn add_delegation(&mut self, delegation: Delegation) -> Result<(), DelegationError> {
let current_depth = self.depth(&delegation.delegator);
let new_depth = current_depth + 1;
if new_depth > self.max_depth {
return Err(DelegationError::MaxDepthExceeded {
depth: new_depth,
max_depth: self.max_depth,
});
}
if let Some(parent_scope) = self.effective_scope(&delegation.delegator) {
if !is_scope_subset(&delegation.scope, &parent_scope) {
return Err(DelegationError::ScopeAmplification {
reason: format!(
"delegate {} scope exceeds delegator {} effective scope",
delegation.delegate, delegation.delegator,
),
});
}
}
self.id_index.insert(
delegation.id.clone(),
(delegation.delegator.clone(), delegation.delegate.clone()),
);
self.delegations
.entry(delegation.delegate.clone())
.or_default()
.push(delegation);
Ok(())
}
pub fn get_delegation_chain(&self, delegate_did: &str) -> Vec<&Delegation> {
let mut chain = Vec::new();
let mut current = delegate_did;
loop {
match self.delegations.get(current) {
Some(delegations) if !delegations.is_empty() => {
let delegation = &delegations[0];
chain.push(delegation);
let delegator = delegation.delegator.as_str();
if delegator == current {
break;
}
current = delegator;
}
_ => break,
}
}
chain
}
pub fn effective_scope(&self, delegate_did: &str) -> Option<DelegationScope> {
let chain = self.get_delegation_chain(delegate_did);
if chain.is_empty() {
return None;
}
let mut effective = chain[0].scope.clone();
for delegation in chain.iter().skip(1) {
effective = intersect_scopes(&effective, &delegation.scope);
}
Some(effective)
}
pub fn depth(&self, delegate_did: &str) -> u32 {
self.get_delegation_chain(delegate_did).len() as u32
}
pub fn revoke(&mut self, delegation_id: &str) -> Result<Vec<String>, DelegationError> {
let (_, delegate_did) =
self.id_index
.get(delegation_id)
.cloned()
.ok_or_else(|| DelegationError::Revoked {
delegation_id: delegation_id.to_string(),
})?;
let mut revoked_ids = Vec::new();
let mut dids_to_process = vec![delegate_did.clone()];
let mut processed_dids = std::collections::HashSet::new();
while let Some(did) = dids_to_process.pop() {
if !processed_dids.insert(did.clone()) {
continue;
}
for (sub_delegate_did, delegations) in &self.delegations {
for d in delegations {
if d.delegator == did && !processed_dids.contains(sub_delegate_did.as_str()) {
dids_to_process.push(sub_delegate_did.clone());
}
}
}
}
if let Some(delegations) = self.delegations.get_mut(&delegate_did) {
delegations.retain(|d| {
if d.id == delegation_id {
revoked_ids.push(d.id.clone());
false
} else {
true
}
});
if delegations.is_empty() {
self.delegations.remove(&delegate_did);
}
}
self.id_index.remove(delegation_id);
for did in &processed_dids {
if *did == delegate_did {
continue; }
if let Some(delegations) = self.delegations.get_mut(did) {
for d in delegations.iter() {
revoked_ids.push(d.id.clone());
self.id_index.remove(&d.id);
}
}
self.delegations.remove(did);
}
Ok(revoked_ids)
}
}
#[cfg(test)]
mod tests {
use super::*;
use openagent_aegis_core::DelegationProof;
use chrono::Utc;
fn make_delegation(
id: &str,
delegator: &str,
delegate: &str,
actions: Vec<&str>,
) -> Delegation {
Delegation {
id: id.to_string(),
delegator: delegator.to_string(),
delegate: delegate.to_string(),
scope: DelegationScope {
actions: actions.into_iter().map(String::from).collect(),
resources: vec![],
chains: vec![],
limits: None,
temporal: None,
},
created: Utc::now(),
expires: None,
revocable: true,
proof: DelegationProof {
proof_type: "AegisDelegationProof2025".to_string(),
verification_method: format!("{delegator}#key"),
created: Utc::now(),
jws: "test-jws".to_string(),
},
}
}
#[test]
fn single_delegation_depth() {
let mut tree = DelegationTree::new(8);
let d = make_delegation("d1", "did:root", "did:agent1", vec!["transfer"]);
let result = tree.add_delegation(d);
assert!(result.is_ok());
assert_eq!(tree.depth("did:agent1"), 1);
assert_eq!(tree.depth("did:root"), 0);
}
#[test]
fn chain_depth_and_walk() {
let mut tree = DelegationTree::new(8);
let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer", "approve"]);
let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
assert!(tree.add_delegation(d1).is_ok());
assert!(tree.add_delegation(d2).is_ok());
assert!(tree.add_delegation(d3).is_ok());
assert_eq!(tree.depth("did:c"), 3);
let chain = tree.get_delegation_chain("did:c");
assert_eq!(chain.len(), 3);
assert_eq!(chain[0].delegate, "did:c");
assert_eq!(chain[1].delegate, "did:b");
assert_eq!(chain[2].delegate, "did:a");
}
#[test]
fn max_depth_enforced() {
let mut tree = DelegationTree::new(2);
let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
assert!(tree.add_delegation(d1).is_ok());
assert!(tree.add_delegation(d2).is_ok());
let result = tree.add_delegation(d3);
assert!(result.is_err());
match result {
Err(DelegationError::MaxDepthExceeded { depth, max_depth }) => {
assert_eq!(depth, 3);
assert_eq!(max_depth, 2);
}
_ => panic!("expected MaxDepthExceeded"),
}
}
#[test]
fn scope_amplification_rejected() {
let mut tree = DelegationTree::new(8);
let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer", "approve"]);
assert!(tree.add_delegation(d1).is_ok());
let result = tree.add_delegation(d2);
assert!(result.is_err());
match result {
Err(DelegationError::ScopeAmplification { .. }) => {}
other => panic!("expected ScopeAmplification, got {other:?}"),
}
}
#[test]
fn effective_scope_is_intersection() {
let mut tree = DelegationTree::new(8);
let d1 = make_delegation(
"d1",
"did:root",
"did:a",
vec!["transfer", "approve", "stake"],
);
let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer", "approve"]);
assert!(tree.add_delegation(d1).is_ok());
assert!(tree.add_delegation(d2).is_ok());
let scope = tree.effective_scope("did:b");
assert!(scope.is_some());
if let Some(s) = scope {
assert!(s.actions.contains(&"transfer".to_string()));
assert!(s.actions.contains(&"approve".to_string()));
assert!(!s.actions.contains(&"stake".to_string()));
}
}
#[test]
fn revocation_cascades() {
let mut tree = DelegationTree::new(8);
let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
assert!(tree.add_delegation(d1).is_ok());
assert!(tree.add_delegation(d2).is_ok());
assert!(tree.add_delegation(d3).is_ok());
let revoked = tree.revoke("d1");
assert!(revoked.is_ok());
if let Ok(ids) = revoked {
assert!(ids.contains(&"d1".to_string()));
assert!(ids.contains(&"d2".to_string()));
assert!(ids.contains(&"d3".to_string()));
}
assert_eq!(tree.depth("did:a"), 0);
assert_eq!(tree.depth("did:b"), 0);
assert_eq!(tree.depth("did:c"), 0);
}
#[test]
fn revoke_nonexistent_returns_error() {
let mut tree = DelegationTree::new(8);
let result = tree.revoke("nonexistent");
assert!(result.is_err());
}
#[test]
fn no_chain_for_unknown_did() {
let tree = DelegationTree::new(8);
let chain = tree.get_delegation_chain("did:unknown");
assert!(chain.is_empty());
assert!(tree.effective_scope("did:unknown").is_none());
}
}