Skip to main content

aegis_delegate/
tree.rs

1// AEGIS Delegate — Delegation Trees
2//
3// Reference: AEGIS Specification v1.0.0 §9.2
4//
5// Maintains a delegation tree rooted at human roots with configurable
6// maximum depth. Enforces no-amplification via scope subsetting.
7
8use openagent_aegis_core::{Delegation, DelegationError, DelegationScope};
9use std::collections::HashMap;
10
11use crate::scope::{intersect_scopes, is_scope_subset};
12
13/// A delegation tree rooted at a human root.
14///
15/// Stores delegations indexed by delegate DID and by delegation ID.
16/// Max depth is configurable (default 8).
17pub struct DelegationTree {
18    /// All delegations indexed by delegate DID.
19    delegations: HashMap<String, Vec<Delegation>>,
20    /// Index from delegation ID to (delegator, delegate) for quick lookup.
21    id_index: HashMap<String, (String, String)>,
22    /// Maximum delegation depth.
23    max_depth: u32,
24}
25
26impl DelegationTree {
27    /// Creates a new empty delegation tree with the given maximum depth.
28    pub fn new(max_depth: u32) -> Self {
29        Self {
30            delegations: HashMap::new(),
31            id_index: HashMap::new(),
32            max_depth,
33        }
34    }
35
36    /// Adds a delegation to the tree.
37    ///
38    /// Validates:
39    /// - The delegation depth does not exceed max_depth
40    /// - The delegation scope is a subset of the delegator's effective scope
41    ///   (no-amplification rule, §9.6)
42    pub fn add_delegation(&mut self, delegation: Delegation) -> Result<(), DelegationError> {
43        // Calculate the depth for the new delegation
44        let current_depth = self.depth(&delegation.delegator);
45        let new_depth = current_depth + 1;
46
47        if new_depth > self.max_depth {
48            return Err(DelegationError::MaxDepthExceeded {
49                depth: new_depth,
50                max_depth: self.max_depth,
51            });
52        }
53
54        // Check scope subset if the delegator is itself a delegate (not a root)
55        if let Some(parent_scope) = self.effective_scope(&delegation.delegator) {
56            if !is_scope_subset(&delegation.scope, &parent_scope) {
57                return Err(DelegationError::ScopeAmplification {
58                    reason: format!(
59                        "delegate {} scope exceeds delegator {} effective scope",
60                        delegation.delegate, delegation.delegator,
61                    ),
62                });
63            }
64        }
65
66        // Index by ID
67        self.id_index.insert(
68            delegation.id.clone(),
69            (delegation.delegator.clone(), delegation.delegate.clone()),
70        );
71
72        // Index by delegate DID
73        self.delegations
74            .entry(delegation.delegate.clone())
75            .or_default()
76            .push(delegation);
77
78        Ok(())
79    }
80
81    /// Gets the delegation chain from a delegate up to the root.
82    ///
83    /// Returns delegations in order from the immediate delegation (closest to
84    /// the delegate) up to the root delegation.
85    pub fn get_delegation_chain(&self, delegate_did: &str) -> Vec<&Delegation> {
86        let mut chain = Vec::new();
87        let mut current = delegate_did;
88
89        loop {
90            match self.delegations.get(current) {
91                Some(delegations) if !delegations.is_empty() => {
92                    // Take the first (most recent) delegation for this delegate
93                    let delegation = &delegations[0];
94                    chain.push(delegation);
95
96                    // Walk up to the delegator
97                    let delegator = delegation.delegator.as_str();
98                    if delegator == current {
99                        // Self-delegation, stop to prevent infinite loop
100                        break;
101                    }
102                    current = delegator;
103                }
104                _ => break,
105            }
106        }
107
108        chain
109    }
110
111    /// Computes the effective scope for a delegate.
112    ///
113    /// This is the intersection of all scopes in the delegation chain,
114    /// implementing the intersection narrowing rule (§9.6 rule 2).
115    pub fn effective_scope(&self, delegate_did: &str) -> Option<DelegationScope> {
116        let chain = self.get_delegation_chain(delegate_did);
117        if chain.is_empty() {
118            return None;
119        }
120
121        let mut effective = chain[0].scope.clone();
122        for delegation in chain.iter().skip(1) {
123            effective = intersect_scopes(&effective, &delegation.scope);
124        }
125
126        Some(effective)
127    }
128
129    /// Returns the depth of a delegate in the tree.
130    ///
131    /// A root entity (not a delegate of anyone) has depth 0.
132    /// A direct delegate of a root has depth 1, and so on.
133    pub fn depth(&self, delegate_did: &str) -> u32 {
134        self.get_delegation_chain(delegate_did).len() as u32
135    }
136
137    /// Revokes a delegation and all sub-delegations (cascade revocation).
138    ///
139    /// Returns the list of all revoked delegation IDs, including the original
140    /// and all transitively dependent delegations.
141    pub fn revoke(&mut self, delegation_id: &str) -> Result<Vec<String>, DelegationError> {
142        // Find the delegation to revoke
143        let (_, delegate_did) =
144            self.id_index
145                .get(delegation_id)
146                .cloned()
147                .ok_or_else(|| DelegationError::Revoked {
148                    delegation_id: delegation_id.to_string(),
149                })?;
150
151        let mut revoked_ids = Vec::new();
152
153        // Collect all DIDs that need revocation (BFS from delegate)
154        let mut dids_to_process = vec![delegate_did.clone()];
155        let mut processed_dids = std::collections::HashSet::new();
156
157        while let Some(did) = dids_to_process.pop() {
158            if !processed_dids.insert(did.clone()) {
159                continue;
160            }
161
162            // Find all delegations FROM this DID (where this DID is a delegator)
163            // by scanning all entries
164            for (sub_delegate_did, delegations) in &self.delegations {
165                for d in delegations {
166                    if d.delegator == did && !processed_dids.contains(sub_delegate_did.as_str()) {
167                        dids_to_process.push(sub_delegate_did.clone());
168                    }
169                }
170            }
171        }
172
173        // Remove the original delegation by ID
174        if let Some(delegations) = self.delegations.get_mut(&delegate_did) {
175            delegations.retain(|d| {
176                if d.id == delegation_id {
177                    revoked_ids.push(d.id.clone());
178                    false
179                } else {
180                    true
181                }
182            });
183            if delegations.is_empty() {
184                self.delegations.remove(&delegate_did);
185            }
186        }
187        self.id_index.remove(delegation_id);
188
189        // Remove all sub-delegations for processed DIDs (except the original DID
190        // which may have other delegations)
191        for did in &processed_dids {
192            if *did == delegate_did {
193                continue; // Already handled above
194            }
195            if let Some(delegations) = self.delegations.get_mut(did) {
196                for d in delegations.iter() {
197                    revoked_ids.push(d.id.clone());
198                    self.id_index.remove(&d.id);
199                }
200            }
201            self.delegations.remove(did);
202        }
203
204        Ok(revoked_ids)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use openagent_aegis_core::DelegationProof;
212    use chrono::Utc;
213
214    fn make_delegation(
215        id: &str,
216        delegator: &str,
217        delegate: &str,
218        actions: Vec<&str>,
219    ) -> Delegation {
220        Delegation {
221            id: id.to_string(),
222            delegator: delegator.to_string(),
223            delegate: delegate.to_string(),
224            scope: DelegationScope {
225                actions: actions.into_iter().map(String::from).collect(),
226                resources: vec![],
227                chains: vec![],
228                limits: None,
229                temporal: None,
230            },
231            created: Utc::now(),
232            expires: None,
233            revocable: true,
234            proof: DelegationProof {
235                proof_type: "AegisDelegationProof2025".to_string(),
236                verification_method: format!("{delegator}#key"),
237                created: Utc::now(),
238                jws: "test-jws".to_string(),
239            },
240        }
241    }
242
243    #[test]
244    fn single_delegation_depth() {
245        let mut tree = DelegationTree::new(8);
246        let d = make_delegation("d1", "did:root", "did:agent1", vec!["transfer"]);
247        let result = tree.add_delegation(d);
248        assert!(result.is_ok());
249        assert_eq!(tree.depth("did:agent1"), 1);
250        assert_eq!(tree.depth("did:root"), 0);
251    }
252
253    #[test]
254    fn chain_depth_and_walk() {
255        let mut tree = DelegationTree::new(8);
256
257        let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer", "approve"]);
258        let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
259        let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
260
261        assert!(tree.add_delegation(d1).is_ok());
262        assert!(tree.add_delegation(d2).is_ok());
263        assert!(tree.add_delegation(d3).is_ok());
264
265        assert_eq!(tree.depth("did:c"), 3);
266
267        let chain = tree.get_delegation_chain("did:c");
268        assert_eq!(chain.len(), 3);
269        assert_eq!(chain[0].delegate, "did:c");
270        assert_eq!(chain[1].delegate, "did:b");
271        assert_eq!(chain[2].delegate, "did:a");
272    }
273
274    #[test]
275    fn max_depth_enforced() {
276        let mut tree = DelegationTree::new(2);
277
278        let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
279        let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
280        let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
281
282        assert!(tree.add_delegation(d1).is_ok());
283        assert!(tree.add_delegation(d2).is_ok());
284
285        // This should fail: depth would be 3, max is 2
286        let result = tree.add_delegation(d3);
287        assert!(result.is_err());
288        match result {
289            Err(DelegationError::MaxDepthExceeded { depth, max_depth }) => {
290                assert_eq!(depth, 3);
291                assert_eq!(max_depth, 2);
292            }
293            _ => panic!("expected MaxDepthExceeded"),
294        }
295    }
296
297    #[test]
298    fn scope_amplification_rejected() {
299        let mut tree = DelegationTree::new(8);
300
301        let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
302
303        // did:a tries to delegate "approve" which it doesn't have
304        let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer", "approve"]);
305
306        assert!(tree.add_delegation(d1).is_ok());
307
308        let result = tree.add_delegation(d2);
309        assert!(result.is_err());
310        match result {
311            Err(DelegationError::ScopeAmplification { .. }) => {}
312            other => panic!("expected ScopeAmplification, got {other:?}"),
313        }
314    }
315
316    #[test]
317    fn effective_scope_is_intersection() {
318        let mut tree = DelegationTree::new(8);
319
320        let d1 = make_delegation(
321            "d1",
322            "did:root",
323            "did:a",
324            vec!["transfer", "approve", "stake"],
325        );
326        let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer", "approve"]);
327
328        assert!(tree.add_delegation(d1).is_ok());
329        assert!(tree.add_delegation(d2).is_ok());
330
331        let scope = tree.effective_scope("did:b");
332        assert!(scope.is_some());
333        if let Some(s) = scope {
334            // Intersection of [transfer,approve] and [transfer,approve,stake]
335            assert!(s.actions.contains(&"transfer".to_string()));
336            assert!(s.actions.contains(&"approve".to_string()));
337            assert!(!s.actions.contains(&"stake".to_string()));
338        }
339    }
340
341    #[test]
342    fn revocation_cascades() {
343        let mut tree = DelegationTree::new(8);
344
345        let d1 = make_delegation("d1", "did:root", "did:a", vec!["transfer"]);
346        let d2 = make_delegation("d2", "did:a", "did:b", vec!["transfer"]);
347        let d3 = make_delegation("d3", "did:b", "did:c", vec!["transfer"]);
348
349        assert!(tree.add_delegation(d1).is_ok());
350        assert!(tree.add_delegation(d2).is_ok());
351        assert!(tree.add_delegation(d3).is_ok());
352
353        // Revoking d1 should cascade to d2 and d3
354        let revoked = tree.revoke("d1");
355        assert!(revoked.is_ok());
356        if let Ok(ids) = revoked {
357            assert!(ids.contains(&"d1".to_string()));
358            assert!(ids.contains(&"d2".to_string()));
359            assert!(ids.contains(&"d3".to_string()));
360        }
361
362        // Tree should be empty now
363        assert_eq!(tree.depth("did:a"), 0);
364        assert_eq!(tree.depth("did:b"), 0);
365        assert_eq!(tree.depth("did:c"), 0);
366    }
367
368    #[test]
369    fn revoke_nonexistent_returns_error() {
370        let mut tree = DelegationTree::new(8);
371        let result = tree.revoke("nonexistent");
372        assert!(result.is_err());
373    }
374
375    #[test]
376    fn no_chain_for_unknown_did() {
377        let tree = DelegationTree::new(8);
378        let chain = tree.get_delegation_chain("did:unknown");
379        assert!(chain.is_empty());
380        assert!(tree.effective_scope("did:unknown").is_none());
381    }
382}