Skip to main content

moloch_core/agent/
capability.rs

1//! Capability-based authorization for agents.
2//!
3//! The capability model defines what an agent is permitted to do. It answers:
4//! "Was this action within the agent's authorized scope?"
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::Duration;
9
10use crate::crypto::{hash, Hash, PublicKey, SecretKey, Sig};
11use crate::error::{Error, Result};
12use crate::event::{ResourceId, ResourceKind};
13
14use super::principal::PrincipalId;
15
16/// Unique capability identifier.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct CapabilityId(pub [u8; 16]);
19
20impl CapabilityId {
21    /// Generate a new random capability ID.
22    pub fn generate() -> Self {
23        use rand::RngCore;
24        let mut bytes = [0u8; 16];
25        rand::thread_rng().fill_bytes(&mut bytes);
26        Self(bytes)
27    }
28
29    /// Create from bytes.
30    pub fn from_bytes(bytes: [u8; 16]) -> Self {
31        Self(bytes)
32    }
33
34    /// Get the bytes.
35    pub fn as_bytes(&self) -> &[u8; 16] {
36        &self.0
37    }
38
39    /// Convert to hex string.
40    pub fn to_hex(&self) -> String {
41        hex::encode(self.0)
42    }
43
44    /// Parse from hex string.
45    pub fn from_hex(s: &str) -> Result<Self> {
46        let bytes = hex::decode(s).map_err(|_| Error::invalid_input("invalid hex"))?;
47        if bytes.len() != 16 {
48            return Err(Error::invalid_input("capability ID must be 16 bytes"));
49        }
50        let mut arr = [0u8; 16];
51        arr.copy_from_slice(&bytes);
52        Ok(Self(arr))
53    }
54}
55
56impl std::fmt::Display for CapabilityId {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.to_hex())
59    }
60}
61
62/// Categories of capabilities.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum CapabilityKind {
66    // Data capabilities
67    /// Read data from resources.
68    Read,
69    /// Write/modify data in resources.
70    Write,
71    /// Delete data from resources.
72    Delete,
73
74    // Execution capabilities
75    /// Execute commands or code.
76    Execute,
77
78    // Tool capabilities
79    /// Invoke a specific tool.
80    InvokeTool { tool_id: String },
81
82    // Agent capabilities
83    /// Spawn child agents.
84    SpawnAgent,
85    /// Delegate capabilities to other agents.
86    DelegateCapability,
87
88    // Communication capabilities
89    /// Send messages on a channel.
90    SendMessage { channel: String },
91    /// Receive messages from a channel.
92    ReceiveMessage { channel: String },
93
94    // Financial capabilities
95    /// Spend currency up to a maximum amount.
96    Spend { currency: String, max_amount: u64 },
97
98    // Administrative capabilities
99    /// Modify permissions.
100    ModifyPermissions,
101    /// View audit logs.
102    ViewAuditLog,
103}
104
105impl CapabilityKind {
106    /// Check if this kind matches an action kind.
107    pub fn matches(&self, other: &CapabilityKind) -> bool {
108        match (self, other) {
109            (CapabilityKind::Read, CapabilityKind::Read) => true,
110            (CapabilityKind::Write, CapabilityKind::Write) => true,
111            (CapabilityKind::Delete, CapabilityKind::Delete) => true,
112            (CapabilityKind::Execute, CapabilityKind::Execute) => true,
113            (
114                CapabilityKind::InvokeTool { tool_id: cap_tool },
115                CapabilityKind::InvokeTool {
116                    tool_id: action_tool,
117                },
118            ) => cap_tool == action_tool || cap_tool == "*",
119            (CapabilityKind::SpawnAgent, CapabilityKind::SpawnAgent) => true,
120            (CapabilityKind::DelegateCapability, CapabilityKind::DelegateCapability) => true,
121            (
122                CapabilityKind::SendMessage { channel: cap_ch },
123                CapabilityKind::SendMessage { channel: action_ch },
124            ) => cap_ch == action_ch || cap_ch == "*",
125            (
126                CapabilityKind::ReceiveMessage { channel: cap_ch },
127                CapabilityKind::ReceiveMessage { channel: action_ch },
128            ) => cap_ch == action_ch || cap_ch == "*",
129            (
130                CapabilityKind::Spend {
131                    currency: cap_cur,
132                    max_amount: cap_max,
133                },
134                CapabilityKind::Spend {
135                    currency: action_cur,
136                    max_amount: action_amount,
137                },
138            ) => cap_cur == action_cur && action_amount <= cap_max,
139            (CapabilityKind::ModifyPermissions, CapabilityKind::ModifyPermissions) => true,
140            (CapabilityKind::ViewAuditLog, CapabilityKind::ViewAuditLog) => true,
141            _ => false,
142        }
143    }
144}
145
146/// Scope of resources a capability applies to.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(tag = "type", rename_all = "snake_case")]
149pub enum ResourceScope {
150    /// Specific resource by ID.
151    Specific { resource: String },
152
153    /// Pattern match (e.g., "repo:org/*").
154    Pattern { pattern: String },
155
156    /// All resources of a kind.
157    Kind { kind: ResourceKind },
158
159    /// All resources (dangerous, requires explicit grant).
160    All,
161}
162
163impl ResourceScope {
164    /// Create a specific resource scope.
165    pub fn specific(resource: impl Into<String>) -> Self {
166        Self::Specific {
167            resource: resource.into(),
168        }
169    }
170
171    /// Create a pattern scope.
172    pub fn pattern(pattern: impl Into<String>) -> Self {
173        Self::Pattern {
174            pattern: pattern.into(),
175        }
176    }
177
178    /// Create a kind scope.
179    pub fn kind(kind: ResourceKind) -> Self {
180        Self::Kind { kind }
181    }
182
183    /// Create an all scope.
184    pub fn all() -> Self {
185        Self::All
186    }
187
188    /// Check if this scope matches a resource.
189    pub fn matches(&self, resource: &ResourceId) -> bool {
190        match self {
191            ResourceScope::Specific { resource: r } => {
192                // Match against the resource's string representation
193                let resource_str = Self::resource_to_string(resource);
194                &resource_str == r
195            }
196            ResourceScope::Pattern { pattern } => {
197                let resource_str = Self::resource_to_string(resource);
198                self.glob_match(pattern, &resource_str)
199            }
200            ResourceScope::Kind { kind } => resource.kind == *kind,
201            ResourceScope::All => true,
202        }
203    }
204
205    /// Convert a resource to a string for matching.
206    fn resource_to_string(resource: &ResourceId) -> String {
207        let kind_str = match resource.kind {
208            ResourceKind::Repository => "repository",
209            ResourceKind::Commit => "commit",
210            ResourceKind::Branch => "branch",
211            ResourceKind::Tag => "tag",
212            ResourceKind::PullRequest => "pull_request",
213            ResourceKind::Issue => "issue",
214            ResourceKind::File => "file",
215            ResourceKind::User => "user",
216            ResourceKind::Organization => "organization",
217            ResourceKind::Credential => "credential",
218            ResourceKind::Config => "config",
219            ResourceKind::Document => "document",
220            ResourceKind::Other => "other",
221        };
222        format!("{}:{}", kind_str, resource.id)
223    }
224
225    /// Simple glob matching for patterns.
226    fn glob_match(&self, pattern: &str, s: &str) -> bool {
227        if pattern == "*" {
228            return true;
229        }
230
231        // Simple glob: only supports trailing * for now
232        if let Some(prefix) = pattern.strip_suffix('*') {
233            s.starts_with(prefix)
234        } else if let Some(suffix) = pattern.strip_prefix('*') {
235            s.ends_with(suffix)
236        } else {
237            pattern == s
238        }
239    }
240}
241
242/// Rate limit configuration.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct RateLimit {
245    /// Maximum requests per period.
246    pub max_requests: u64,
247    /// Period in milliseconds.
248    pub period_ms: u64,
249}
250
251impl RateLimit {
252    /// Create a new rate limit.
253    pub fn new(max_requests: u64, period: Duration) -> Self {
254        Self {
255            max_requests,
256            period_ms: period.as_millis() as u64,
257        }
258    }
259
260    /// Get the period as a Duration.
261    pub fn period(&self) -> Duration {
262        Duration::from_millis(self.period_ms)
263    }
264}
265
266/// Time of day (hours, minutes, seconds).
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
268pub struct TimeOfDay {
269    pub hour: u8,
270    pub minute: u8,
271    pub second: u8,
272}
273
274impl TimeOfDay {
275    /// Create a new time of day.
276    pub fn new(hour: u8, minute: u8, second: u8) -> Self {
277        Self {
278            hour,
279            minute,
280            second,
281        }
282    }
283
284    /// Get seconds since midnight.
285    pub fn seconds_since_midnight(&self) -> u32 {
286        (self.hour as u32) * 3600 + (self.minute as u32) * 60 + (self.second as u32)
287    }
288}
289
290/// Day of week.
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(rename_all = "lowercase")]
293pub enum DayOfWeek {
294    Monday,
295    Tuesday,
296    Wednesday,
297    Thursday,
298    Friday,
299    Saturday,
300    Sunday,
301}
302
303/// Time window constraint.
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct TimeWindow {
306    /// Start time of day.
307    pub start: TimeOfDay,
308    /// End time of day.
309    pub end: TimeOfDay,
310    /// Days when the window is active.
311    pub days: Vec<DayOfWeek>,
312    /// Timezone name (e.g., "UTC", "America/New_York").
313    pub timezone: String,
314}
315
316impl TimeWindow {
317    /// Create a weekday business hours window.
318    pub fn weekday_business_hours() -> Self {
319        Self {
320            start: TimeOfDay::new(9, 0, 0),
321            end: TimeOfDay::new(17, 0, 0),
322            days: vec![
323                DayOfWeek::Monday,
324                DayOfWeek::Tuesday,
325                DayOfWeek::Wednesday,
326                DayOfWeek::Thursday,
327                DayOfWeek::Friday,
328            ],
329            timezone: "UTC".to_string(),
330        }
331    }
332
333    /// Create a custom time window.
334    pub fn new(
335        start: TimeOfDay,
336        end: TimeOfDay,
337        days: Vec<DayOfWeek>,
338        timezone: impl Into<String>,
339    ) -> Self {
340        Self {
341            start,
342            end,
343            days,
344            timezone: timezone.into(),
345        }
346    }
347
348    /// Check if a given timestamp (milliseconds since Unix epoch) is within this window.
349    ///
350    /// This implementation:
351    /// 1. Converts the timestamp to the specified timezone
352    /// 2. Checks if the day of week is in the allowed list
353    /// 3. Checks if the time of day is between start and end
354    ///
355    /// Returns false if the timezone is invalid.
356    pub fn is_within(&self, timestamp_ms: i64) -> bool {
357        use chrono::{Datelike, TimeZone, Timelike};
358        use chrono_tz::Tz;
359
360        // Parse the timezone
361        let tz: Tz = match self.timezone.parse() {
362            Ok(tz) => tz,
363            Err(_) => {
364                // If timezone is invalid, deny access (fail-secure)
365                return false;
366            }
367        };
368
369        // Convert timestamp to DateTime in the specified timezone
370        let timestamp_secs = timestamp_ms / 1000;
371        let datetime = match tz.timestamp_opt(timestamp_secs, 0).single() {
372            Some(dt) => dt,
373            None => return false, // Ambiguous or invalid timestamp
374        };
375
376        // Check day of week
377        let weekday = datetime.weekday();
378        let day_of_week = match weekday {
379            chrono::Weekday::Mon => DayOfWeek::Monday,
380            chrono::Weekday::Tue => DayOfWeek::Tuesday,
381            chrono::Weekday::Wed => DayOfWeek::Wednesday,
382            chrono::Weekday::Thu => DayOfWeek::Thursday,
383            chrono::Weekday::Fri => DayOfWeek::Friday,
384            chrono::Weekday::Sat => DayOfWeek::Saturday,
385            chrono::Weekday::Sun => DayOfWeek::Sunday,
386        };
387
388        if !self.days.contains(&day_of_week) {
389            return false;
390        }
391
392        // Check time of day
393        let current_seconds = datetime.hour() * 3600 + datetime.minute() * 60 + datetime.second();
394
395        let start_seconds = self.start.seconds_since_midnight();
396        let end_seconds = self.end.seconds_since_midnight();
397
398        // Handle both normal windows (9:00-17:00) and overnight windows (22:00-06:00)
399        if start_seconds <= end_seconds {
400            // Normal window: start <= current < end
401            current_seconds >= start_seconds && current_seconds < end_seconds
402        } else {
403            // Overnight window: current >= start OR current < end
404            current_seconds >= start_seconds || current_seconds < end_seconds
405        }
406    }
407}
408
409/// Constraints on capability usage.
410#[derive(Debug, Clone, Default, Serialize, Deserialize)]
411pub struct CapabilityConstraints {
412    /// Maximum invocations.
413    pub max_uses: Option<u64>,
414
415    /// Current usage count.
416    pub current_uses: u64,
417
418    /// Rate limit.
419    pub rate_limit: Option<RateLimit>,
420
421    /// Recent usage timestamps for rate limit enforcement (Unix ms).
422    /// Entries older than the rate limit period are pruned on check.
423    #[serde(default)]
424    pub recent_use_timestamps: Vec<i64>,
425
426    /// Time windows when capability is valid.
427    pub time_windows: Vec<TimeWindow>,
428
429    /// Required approval for each use.
430    pub requires_approval: bool,
431
432    /// Approval timeout in milliseconds.
433    pub approval_timeout_ms: Option<u64>,
434
435    /// Custom constraints as key-value pairs.
436    pub custom: HashMap<String, String>,
437}
438
439impl CapabilityConstraints {
440    /// Create empty constraints.
441    pub fn new() -> Self {
442        Self::default()
443    }
444
445    /// Set max uses.
446    pub fn with_max_uses(mut self, max: u64) -> Self {
447        self.max_uses = Some(max);
448        self
449    }
450
451    /// Set rate limit.
452    pub fn with_rate_limit(mut self, limit: RateLimit) -> Self {
453        self.rate_limit = Some(limit);
454        self
455    }
456
457    /// Add a time window.
458    pub fn with_time_window(mut self, window: TimeWindow) -> Self {
459        self.time_windows.push(window);
460        self
461    }
462
463    /// Require approval for each use.
464    pub fn with_requires_approval(mut self, timeout: Duration) -> Self {
465        self.requires_approval = true;
466        self.approval_timeout_ms = Some(timeout.as_millis() as u64);
467        self
468    }
469
470    /// Add a custom constraint.
471    pub fn with_custom(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
472        self.custom.insert(key.into(), value.into());
473        self
474    }
475
476    /// Get approval timeout as Duration.
477    pub fn approval_timeout(&self) -> Option<Duration> {
478        self.approval_timeout_ms.map(Duration::from_millis)
479    }
480
481    /// Check if usage limit is reached.
482    pub fn is_usage_limit_reached(&self) -> bool {
483        match self.max_uses {
484            Some(max) => self.current_uses >= max,
485            None => false,
486        }
487    }
488
489    /// Increment usage count. Returns error if limit reached.
490    pub fn increment_usage(&mut self) -> Result<()> {
491        if self.is_usage_limit_reached() {
492            return Err(Error::invalid_input("Usage limit reached"));
493        }
494        self.current_uses += 1;
495        Ok(())
496    }
497
498    /// Check if the rate limit is exceeded at the given timestamp.
499    ///
500    /// Returns true if the number of recent uses within the rate limit
501    /// period meets or exceeds `max_requests`.
502    pub fn is_rate_limited(&self, now: i64) -> bool {
503        match &self.rate_limit {
504            None => false,
505            Some(limit) => {
506                let window_start = now.saturating_sub(limit.period_ms as i64);
507                let recent_count = self
508                    .recent_use_timestamps
509                    .iter()
510                    .filter(|&&ts| ts >= window_start)
511                    .count();
512                recent_count >= limit.max_requests as usize
513            }
514        }
515    }
516
517    /// Record a usage for rate limiting purposes.
518    ///
519    /// Prunes timestamps older than the rate limit window.
520    pub fn record_rate_limit_use(&mut self, now: i64) {
521        if let Some(limit) = &self.rate_limit {
522            let window_start = now.saturating_sub(limit.period_ms as i64);
523            self.recent_use_timestamps.retain(|&ts| ts >= window_start);
524        }
525        self.recent_use_timestamps.push(now);
526    }
527}
528
529/// Lifecycle state of a capability per Section 5.4.
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531pub enum CapabilityState {
532    /// Capability is currently valid and usable.
533    Active,
534    /// Capability has passed its expiry time.
535    Expired,
536    /// Capability was explicitly revoked.
537    Revoked,
538}
539
540/// A specific permission granted to an agent.
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct Capability {
543    /// Unique capability identifier.
544    id: CapabilityId,
545
546    /// What kind of capability this is.
547    kind: CapabilityKind,
548
549    /// Resource scope.
550    scope: ResourceScope,
551
552    /// Constraints on usage.
553    constraints: CapabilityConstraints,
554
555    /// Who granted this capability.
556    grantor: PrincipalId,
557
558    /// When this capability was granted (Unix timestamp ms).
559    granted_at: i64,
560
561    /// When this capability expires (Unix timestamp ms).
562    expires_at: Option<i64>,
563
564    /// Whether this capability can be delegated.
565    delegatable: bool,
566
567    /// Maximum delegation depth.
568    max_delegation_depth: u32,
569
570    /// Current delegation depth (0 for root capabilities).
571    #[serde(default)]
572    delegation_depth: u32,
573
574    /// Parent capability ID (for delegated capabilities).
575    #[serde(default)]
576    parent_capability_id: Option<CapabilityId>,
577
578    /// When this capability was revoked (Unix timestamp ms).
579    #[serde(default)]
580    revoked_at: Option<i64>,
581
582    /// Reason for revocation.
583    #[serde(default)]
584    revocation_reason: Option<String>,
585
586    /// Signature from grantor.
587    signature: Sig,
588}
589
590impl Capability {
591    /// Create a new capability builder.
592    pub fn builder() -> CapabilityBuilder {
593        CapabilityBuilder::new()
594    }
595
596    /// Get the capability ID.
597    pub fn id(&self) -> CapabilityId {
598        self.id
599    }
600
601    /// Get the capability kind.
602    pub fn kind(&self) -> &CapabilityKind {
603        &self.kind
604    }
605
606    /// Get the resource scope.
607    pub fn scope(&self) -> &ResourceScope {
608        &self.scope
609    }
610
611    /// Get the constraints.
612    pub fn constraints(&self) -> &CapabilityConstraints {
613        &self.constraints
614    }
615
616    /// Get mutable constraints (for usage tracking).
617    pub fn constraints_mut(&mut self) -> &mut CapabilityConstraints {
618        &mut self.constraints
619    }
620
621    /// Get the grantor.
622    pub fn grantor(&self) -> &PrincipalId {
623        &self.grantor
624    }
625
626    /// Get when this was granted.
627    pub fn granted_at(&self) -> i64 {
628        self.granted_at
629    }
630
631    /// Get when this expires.
632    pub fn expires_at(&self) -> Option<i64> {
633        self.expires_at
634    }
635
636    /// Check if this capability is delegatable.
637    pub fn is_delegatable(&self) -> bool {
638        self.delegatable
639    }
640
641    /// Get maximum delegation depth.
642    pub fn max_delegation_depth(&self) -> u32 {
643        self.max_delegation_depth
644    }
645
646    /// Get the signature.
647    pub fn signature(&self) -> &Sig {
648        &self.signature
649    }
650
651    /// Get the current delegation depth (0 for root capabilities).
652    pub fn delegation_depth(&self) -> u32 {
653        self.delegation_depth
654    }
655
656    /// Get the parent capability ID (for delegated capabilities).
657    pub fn parent_capability_id(&self) -> Option<CapabilityId> {
658        self.parent_capability_id
659    }
660
661    /// Revoke this capability with a reason.
662    pub fn revoke(&mut self, reason: impl Into<String>) {
663        self.revoked_at = Some(chrono::Utc::now().timestamp_millis());
664        self.revocation_reason = Some(reason.into());
665    }
666
667    /// Check if this capability has been revoked.
668    pub fn is_revoked(&self) -> bool {
669        self.revoked_at.is_some()
670    }
671
672    /// Get the revocation timestamp if revoked.
673    pub fn revoked_at(&self) -> Option<i64> {
674        self.revoked_at
675    }
676
677    /// Get the revocation reason if revoked.
678    pub fn revocation_reason(&self) -> Option<&str> {
679        self.revocation_reason.as_deref()
680    }
681
682    /// Get the lifecycle state at a given time per Section 5.4.
683    pub fn lifecycle_state(&self, now_ms: i64) -> CapabilityState {
684        if self.is_revoked() {
685            CapabilityState::Revoked
686        } else if let Some(exp) = self.expires_at {
687            if now_ms >= exp {
688                CapabilityState::Expired
689            } else {
690                CapabilityState::Active
691            }
692        } else {
693            CapabilityState::Active
694        }
695    }
696
697    /// Check if this capability is valid at a given time.
698    ///
699    /// A capability is valid if it is not revoked and not expired.
700    pub fn is_valid_at(&self, timestamp: i64) -> bool {
701        if self.is_revoked() {
702            return false;
703        }
704        match self.expires_at {
705            Some(exp) => timestamp < exp,
706            None => true, // Never expires
707        }
708    }
709
710    /// Check if this capability matches an action.
711    pub fn matches(&self, action_kind: &CapabilityKind, resource: &ResourceId) -> bool {
712        self.kind.matches(action_kind) && self.scope.matches(resource)
713    }
714
715    /// Compute the canonical bytes for signing/verification.
716    pub fn canonical_bytes(&self) -> Vec<u8> {
717        let mut data = Vec::new();
718        data.extend_from_slice(&self.id.0);
719        // Include kind, scope, constraints, etc.
720        let kind_json = serde_json::to_vec(&self.kind).unwrap_or_default();
721        data.extend_from_slice(&kind_json);
722        let scope_json = serde_json::to_vec(&self.scope).unwrap_or_default();
723        data.extend_from_slice(&scope_json);
724        let grantor_json = serde_json::to_vec(&self.grantor).unwrap_or_default();
725        data.extend_from_slice(&grantor_json);
726        data.extend_from_slice(&self.granted_at.to_le_bytes());
727        if let Some(exp) = self.expires_at {
728            data.extend_from_slice(&exp.to_le_bytes());
729        }
730        data.push(if self.delegatable { 1 } else { 0 });
731        data.extend_from_slice(&self.max_delegation_depth.to_le_bytes());
732        data.extend_from_slice(&self.delegation_depth.to_le_bytes());
733        if let Some(parent_id) = &self.parent_capability_id {
734            data.extend_from_slice(&parent_id.0);
735        }
736        data
737    }
738
739    /// Compute the hash of this capability.
740    pub fn hash(&self) -> Hash {
741        hash(&self.canonical_bytes())
742    }
743
744    /// Delegate this capability to another agent, creating a child capability.
745    ///
746    /// Enforces:
747    /// - INV-CAP-3: child scope must be a subset of parent scope, child expiry must not exceed parent
748    /// - INV-CAP-4: delegation depth must not exceed max_delegation_depth
749    /// - Rule 5.3.3: delegated capabilities must be a subset of the delegator's
750    pub fn delegate(
751        &self,
752        delegator_key: &SecretKey,
753        scope: Option<ResourceScope>,
754        expiry: Option<Duration>,
755    ) -> Result<Capability> {
756        if !self.delegatable {
757            return Err(Error::invalid_input("capability is not delegatable"));
758        }
759
760        if self.delegation_depth + 1 > self.max_delegation_depth {
761            return Err(Error::invalid_input(format!(
762                "delegation depth {} would exceed max {}",
763                self.delegation_depth + 1,
764                self.max_delegation_depth
765            )));
766        }
767
768        // Determine child scope (must be subset of parent)
769        let child_scope = match scope {
770            Some(s) => {
771                if !Self::is_scope_subset(&s, &self.scope) {
772                    return Err(Error::invalid_input(
773                        "child scope must be a subset of parent scope",
774                    ));
775                }
776                s
777            }
778            None => self.scope.clone(),
779        };
780
781        // Determine child expiry (must not exceed parent)
782        let child_expires_at = match expiry {
783            Some(dur) => {
784                let now = chrono::Utc::now().timestamp_millis();
785                let proposed = now + dur.as_millis() as i64;
786                if let Some(parent_exp) = self.expires_at {
787                    if proposed > parent_exp {
788                        return Err(Error::invalid_input(
789                            "child expiry must not exceed parent expiry",
790                        ));
791                    }
792                }
793                Some(proposed)
794            }
795            None => self.expires_at,
796        };
797
798        let mut child = Capability {
799            id: CapabilityId::generate(),
800            kind: self.kind.clone(),
801            scope: child_scope,
802            constraints: CapabilityConstraints::default(),
803            grantor: self.grantor.clone(),
804            granted_at: chrono::Utc::now().timestamp_millis(),
805            expires_at: child_expires_at,
806            delegatable: self.delegatable,
807            max_delegation_depth: self.max_delegation_depth,
808            delegation_depth: self.delegation_depth + 1,
809            parent_capability_id: Some(self.id),
810            revoked_at: None,
811            revocation_reason: None,
812            signature: Sig::empty(),
813        };
814
815        let canonical = child.canonical_bytes();
816        child.signature = delegator_key.sign(&canonical);
817
818        Ok(child)
819    }
820
821    /// Check if `child` scope is a subset of `parent` scope.
822    fn is_scope_subset(child: &ResourceScope, parent: &ResourceScope) -> bool {
823        match (child, parent) {
824            // All is only a subset of All
825            (ResourceScope::All, ResourceScope::All) => true,
826            (ResourceScope::All, _) => false,
827            // Everything is a subset of All
828            (_, ResourceScope::All) => true,
829            // Specific is subset of Specific if equal
830            (ResourceScope::Specific { resource: c }, ResourceScope::Specific { resource: p }) => {
831                c == p
832            }
833            // Specific is subset of Pattern if it matches the pattern
834            (ResourceScope::Specific { resource: c }, ResourceScope::Pattern { pattern: p }) => {
835                p.ends_with('*') && c.starts_with(&p[..p.len() - 1]) || c == p
836            }
837            // Pattern is subset of Pattern if child is more specific
838            (ResourceScope::Pattern { pattern: c }, ResourceScope::Pattern { pattern: p }) => {
839                p.ends_with('*') && c.starts_with(&p[..p.len() - 1]) || c == p
840            }
841            // Kind is subset of Kind if equal
842            (ResourceScope::Kind { kind: c }, ResourceScope::Kind { kind: p }) => c == p,
843            // Cross-type: generally not a subset
844            _ => false,
845        }
846    }
847}
848
849/// Builder for Capability.
850#[derive(Debug, Default)]
851pub struct CapabilityBuilder {
852    id: Option<CapabilityId>,
853    kind: Option<CapabilityKind>,
854    scope: Option<ResourceScope>,
855    constraints: CapabilityConstraints,
856    grantor: Option<PrincipalId>,
857    granted_at: Option<i64>,
858    expires_at: Option<i64>,
859    delegatable: bool,
860    max_delegation_depth: u32,
861}
862
863impl CapabilityBuilder {
864    /// Create a new builder.
865    pub fn new() -> Self {
866        Self {
867            max_delegation_depth: 3, // Default
868            ..Default::default()
869        }
870    }
871
872    /// Set the capability ID.
873    pub fn id(mut self, id: CapabilityId) -> Self {
874        self.id = Some(id);
875        self
876    }
877
878    /// Set the capability kind.
879    pub fn kind(mut self, kind: CapabilityKind) -> Self {
880        self.kind = Some(kind);
881        self
882    }
883
884    /// Set the resource scope.
885    pub fn scope(mut self, scope: ResourceScope) -> Self {
886        self.scope = Some(scope);
887        self
888    }
889
890    /// Set the constraints.
891    pub fn constraints(mut self, constraints: CapabilityConstraints) -> Self {
892        self.constraints = constraints;
893        self
894    }
895
896    /// Set the grantor.
897    pub fn grantor(mut self, grantor: PrincipalId) -> Self {
898        self.grantor = Some(grantor);
899        self
900    }
901
902    /// Set when this was granted.
903    pub fn granted_at(mut self, timestamp: i64) -> Self {
904        self.granted_at = Some(timestamp);
905        self
906    }
907
908    /// Set when this expires.
909    pub fn expires_at(mut self, timestamp: i64) -> Self {
910        self.expires_at = Some(timestamp);
911        self
912    }
913
914    /// Set expiry duration from now.
915    pub fn expires_in(mut self, duration: Duration) -> Self {
916        let now = chrono::Utc::now().timestamp_millis();
917        self.expires_at = Some(now + duration.as_millis() as i64);
918        self
919    }
920
921    /// Make this capability delegatable.
922    pub fn delegatable(mut self, max_depth: u32) -> Self {
923        self.delegatable = true;
924        self.max_delegation_depth = max_depth;
925        self
926    }
927
928    /// Sign and build the capability.
929    pub fn sign(self, _grantor_key: &SecretKey) -> Result<Capability> {
930        let id = self.id.unwrap_or_else(CapabilityId::generate);
931
932        let kind = self
933            .kind
934            .ok_or_else(|| Error::invalid_input("kind is required"))?;
935
936        let scope = self.scope.unwrap_or(ResourceScope::All);
937
938        let grantor = self
939            .grantor
940            .ok_or_else(|| Error::invalid_input("grantor is required"))?;
941
942        let granted_at = self
943            .granted_at
944            .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
945
946        let mut capability = Capability {
947            id,
948            kind,
949            scope,
950            constraints: self.constraints,
951            grantor,
952            granted_at,
953            expires_at: self.expires_at,
954            delegatable: self.delegatable,
955            max_delegation_depth: self.max_delegation_depth,
956            delegation_depth: 0,
957            parent_capability_id: None,
958            revoked_at: None,
959            revocation_reason: None,
960            signature: Sig::empty(),
961        };
962
963        // Sign the canonical bytes
964        let canonical = capability.canonical_bytes();
965        capability.signature = _grantor_key.sign(&canonical);
966
967        Ok(capability)
968    }
969}
970
971/// Result of a capability check.
972#[derive(Debug, Clone, PartialEq, Eq)]
973pub enum CapabilityCheck {
974    /// Action is permitted.
975    Permitted { capability_id: CapabilityId },
976    /// Action is denied.
977    Denied { reason: DenialReason },
978    /// Action requires human approval.
979    RequiresApproval {
980        capability_id: CapabilityId,
981        timeout: Duration,
982    },
983}
984
985/// Reason for denying a capability.
986#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(rename_all = "snake_case")]
988pub enum DenialReason {
989    /// No matching capability found.
990    NoMatchingCapability,
991    /// Capability has expired.
992    Expired,
993    /// Rate limit exceeded.
994    RateLimitExceeded,
995    /// Usage limit exceeded.
996    UsageLimitExceeded,
997    /// Outside allowed time window.
998    OutsideTimeWindow,
999    /// Resource not in scope.
1000    ScopeViolation,
1001    /// Delegation depth exceeded.
1002    DelegationDepthExceeded,
1003    /// Capability has been revoked.
1004    Revoked,
1005}
1006
1007impl std::fmt::Display for DenialReason {
1008    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1009        match self {
1010            DenialReason::NoMatchingCapability => write!(f, "No matching capability found"),
1011            DenialReason::Expired => write!(f, "Capability has expired"),
1012            DenialReason::RateLimitExceeded => write!(f, "Rate limit exceeded"),
1013            DenialReason::UsageLimitExceeded => write!(f, "Usage limit exceeded"),
1014            DenialReason::OutsideTimeWindow => write!(f, "Outside allowed time window"),
1015            DenialReason::ScopeViolation => write!(f, "Resource not in capability scope"),
1016            DenialReason::DelegationDepthExceeded => write!(f, "Delegation depth exceeded"),
1017            DenialReason::Revoked => write!(f, "Capability has been revoked"),
1018        }
1019    }
1020}
1021
1022/// Hash of a capability set.
1023#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1024pub struct CapabilitySetId(pub Hash);
1025
1026/// A collection of capabilities granted to an agent.
1027#[derive(Debug, Clone, Serialize, Deserialize)]
1028pub struct CapabilitySet {
1029    /// The capabilities in this set.
1030    capabilities: Vec<Capability>,
1031
1032    /// The agent these capabilities are granted to.
1033    grantee: PublicKey,
1034
1035    /// Parent capability set (for delegation chains).
1036    parent: Option<CapabilitySetId>,
1037
1038    /// Current delegation depth.
1039    delegation_depth: u32,
1040}
1041
1042impl CapabilitySet {
1043    /// Create a new empty capability set.
1044    pub fn new(grantee: PublicKey) -> Self {
1045        Self {
1046            capabilities: Vec::new(),
1047            grantee,
1048            parent: None,
1049            delegation_depth: 0,
1050        }
1051    }
1052
1053    /// Create with capabilities.
1054    pub fn with_capabilities(grantee: PublicKey, capabilities: Vec<Capability>) -> Self {
1055        Self {
1056            capabilities,
1057            grantee,
1058            parent: None,
1059            delegation_depth: 0,
1060        }
1061    }
1062
1063    /// Add a capability to the set.
1064    pub fn add(&mut self, capability: Capability) {
1065        self.capabilities.push(capability);
1066    }
1067
1068    /// Get the grantee.
1069    pub fn grantee(&self) -> &PublicKey {
1070        &self.grantee
1071    }
1072
1073    /// Get the capabilities.
1074    pub fn capabilities(&self) -> &[Capability] {
1075        &self.capabilities
1076    }
1077
1078    /// Get the parent capability set ID.
1079    pub fn parent(&self) -> Option<CapabilitySetId> {
1080        self.parent
1081    }
1082
1083    /// Get the delegation depth.
1084    pub fn delegation_depth(&self) -> u32 {
1085        self.delegation_depth
1086    }
1087
1088    /// Compute the hash of this capability set.
1089    pub fn hash(&self) -> Hash {
1090        let mut data = Vec::new();
1091        data.extend_from_slice(&self.grantee.as_bytes());
1092        for cap in &self.capabilities {
1093            data.extend_from_slice(cap.hash().as_bytes());
1094        }
1095        hash(&data)
1096    }
1097
1098    /// Get the capability set ID.
1099    pub fn id(&self) -> CapabilitySetId {
1100        CapabilitySetId(self.hash())
1101    }
1102
1103    /// Check if an action is permitted.
1104    pub fn permits(
1105        &self,
1106        action_kind: &CapabilityKind,
1107        resource: &ResourceId,
1108        timestamp: i64,
1109    ) -> CapabilityCheck {
1110        for cap in &self.capabilities {
1111            // Check if capability matches the action
1112            if !cap.matches(action_kind, resource) {
1113                continue;
1114            }
1115
1116            // Check revocation before expiry
1117            if cap.is_revoked() {
1118                return CapabilityCheck::Denied {
1119                    reason: DenialReason::Revoked,
1120                };
1121            }
1122
1123            // Check expiry
1124            if !cap.is_valid_at(timestamp) {
1125                return CapabilityCheck::Denied {
1126                    reason: DenialReason::Expired,
1127                };
1128            }
1129
1130            // Check usage limits
1131            if cap.constraints().is_usage_limit_reached() {
1132                return CapabilityCheck::Denied {
1133                    reason: DenialReason::UsageLimitExceeded,
1134                };
1135            }
1136
1137            // Check rate limit (G-5.1)
1138            if cap.constraints().is_rate_limited(timestamp) {
1139                return CapabilityCheck::Denied {
1140                    reason: DenialReason::RateLimitExceeded,
1141                };
1142            }
1143
1144            // Check time windows
1145            if !cap.constraints().time_windows.is_empty() {
1146                let in_window = cap
1147                    .constraints()
1148                    .time_windows
1149                    .iter()
1150                    .any(|w| w.is_within(timestamp));
1151                if !in_window {
1152                    return CapabilityCheck::Denied {
1153                        reason: DenialReason::OutsideTimeWindow,
1154                    };
1155                }
1156            }
1157
1158            // Check if approval is required
1159            if cap.constraints().requires_approval {
1160                let timeout = cap
1161                    .constraints()
1162                    .approval_timeout()
1163                    .unwrap_or(Duration::from_secs(300));
1164                return CapabilityCheck::RequiresApproval {
1165                    capability_id: cap.id(),
1166                    timeout,
1167                };
1168            }
1169
1170            // All checks passed
1171            return CapabilityCheck::Permitted {
1172                capability_id: cap.id(),
1173            };
1174        }
1175
1176        // No matching capability found
1177        CapabilityCheck::Denied {
1178            reason: DenialReason::NoMatchingCapability,
1179        }
1180    }
1181
1182    /// Find the capability that permits an action (if any).
1183    pub fn find_capability(
1184        &self,
1185        action_kind: &CapabilityKind,
1186        resource: &ResourceId,
1187    ) -> Option<&Capability> {
1188        self.capabilities
1189            .iter()
1190            .find(|cap| cap.matches(action_kind, resource))
1191    }
1192
1193    /// Create a delegated subset of capabilities.
1194    pub fn delegate(
1195        &self,
1196        capability_ids: Vec<CapabilityId>,
1197        new_grantee: PublicKey,
1198    ) -> Result<CapabilitySet> {
1199        let mut delegated_caps = Vec::new();
1200
1201        for id in capability_ids {
1202            let cap = self
1203                .capabilities
1204                .iter()
1205                .find(|c| c.id() == id)
1206                .ok_or_else(|| Error::invalid_input("Capability not found in set"))?;
1207
1208            if !cap.is_delegatable() {
1209                return Err(Error::invalid_input("Capability is not delegatable"));
1210            }
1211
1212            if self.delegation_depth + 1 > cap.max_delegation_depth() {
1213                return Err(Error::invalid_input("Delegation depth exceeded"));
1214            }
1215
1216            delegated_caps.push(cap.clone());
1217        }
1218
1219        Ok(CapabilitySet {
1220            capabilities: delegated_caps,
1221            grantee: new_grantee,
1222            parent: Some(self.id()),
1223            delegation_depth: self.delegation_depth + 1,
1224        })
1225    }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231    use crate::crypto::SecretKey;
1232
1233    fn test_grantor() -> PrincipalId {
1234        PrincipalId::user("test-user").unwrap()
1235    }
1236
1237    fn test_resource(kind: &str, id: &str) -> ResourceId {
1238        let kind = match kind {
1239            "repo" | "repository" => ResourceKind::Repository,
1240            "file" => ResourceKind::File,
1241            "commit" => ResourceKind::Commit,
1242            "branch" => ResourceKind::Branch,
1243            _ => ResourceKind::Other,
1244        };
1245        ResourceId::new(kind, id)
1246    }
1247
1248    // === CapabilityId Tests ===
1249
1250    #[test]
1251    fn capability_id_generates_unique() {
1252        let id1 = CapabilityId::generate();
1253        let id2 = CapabilityId::generate();
1254        assert_ne!(id1, id2);
1255    }
1256
1257    #[test]
1258    fn capability_id_hex_roundtrip() {
1259        let id = CapabilityId::generate();
1260        let hex = id.to_hex();
1261        let restored = CapabilityId::from_hex(&hex).unwrap();
1262        assert_eq!(id, restored);
1263    }
1264
1265    // === ResourceScope Tests ===
1266
1267    #[test]
1268    fn scope_specific_matches_exact_resource() {
1269        let scope = ResourceScope::specific("repository:org/project");
1270        assert!(scope.matches(&test_resource("repository", "org/project")));
1271        assert!(!scope.matches(&test_resource("repository", "org/other")));
1272    }
1273
1274    #[test]
1275    fn scope_pattern_matches_glob() {
1276        let scope = ResourceScope::pattern("repository:org/*");
1277        assert!(scope.matches(&test_resource("repository", "org/project")));
1278        assert!(scope.matches(&test_resource("repository", "org/other")));
1279        assert!(!scope.matches(&test_resource("repository", "other/project")));
1280    }
1281
1282    #[test]
1283    fn scope_kind_matches_all_of_kind() {
1284        let scope = ResourceScope::kind(ResourceKind::Repository);
1285        assert!(scope.matches(&test_resource("repository", "anything")));
1286        assert!(!scope.matches(&test_resource("file", "anything")));
1287    }
1288
1289    #[test]
1290    fn scope_all_matches_everything() {
1291        let scope = ResourceScope::all();
1292        assert!(scope.matches(&test_resource("repository", "anything")));
1293        assert!(scope.matches(&test_resource("file", "anything")));
1294    }
1295
1296    // === CapabilityConstraints Tests ===
1297
1298    #[test]
1299    fn constraint_max_uses_enforced() {
1300        let mut constraints = CapabilityConstraints::new().with_max_uses(5);
1301
1302        for _ in 0..5 {
1303            assert!(constraints.increment_usage().is_ok());
1304        }
1305        assert!(constraints.increment_usage().is_err());
1306    }
1307
1308    #[test]
1309    fn constraint_unlimited_uses() {
1310        let mut constraints = CapabilityConstraints::new();
1311
1312        for _ in 0..1000 {
1313            assert!(constraints.increment_usage().is_ok());
1314        }
1315    }
1316
1317    // === CapabilityKind Tests ===
1318
1319    #[test]
1320    fn capability_kind_matches_same() {
1321        assert!(CapabilityKind::Read.matches(&CapabilityKind::Read));
1322        assert!(CapabilityKind::Write.matches(&CapabilityKind::Write));
1323        assert!(CapabilityKind::Execute.matches(&CapabilityKind::Execute));
1324    }
1325
1326    #[test]
1327    fn capability_kind_different_not_match() {
1328        assert!(!CapabilityKind::Read.matches(&CapabilityKind::Write));
1329        assert!(!CapabilityKind::Write.matches(&CapabilityKind::Execute));
1330    }
1331
1332    #[test]
1333    fn capability_kind_tool_matches_specific() {
1334        let cap_kind = CapabilityKind::InvokeTool {
1335            tool_id: "bash".to_string(),
1336        };
1337        let action_kind = CapabilityKind::InvokeTool {
1338            tool_id: "bash".to_string(),
1339        };
1340        assert!(cap_kind.matches(&action_kind));
1341
1342        let other_tool = CapabilityKind::InvokeTool {
1343            tool_id: "read_file".to_string(),
1344        };
1345        assert!(!cap_kind.matches(&other_tool));
1346    }
1347
1348    #[test]
1349    fn capability_kind_tool_wildcard() {
1350        let cap_kind = CapabilityKind::InvokeTool {
1351            tool_id: "*".to_string(),
1352        };
1353        let action_kind = CapabilityKind::InvokeTool {
1354            tool_id: "bash".to_string(),
1355        };
1356        assert!(cap_kind.matches(&action_kind));
1357    }
1358
1359    #[test]
1360    fn capability_kind_spend_checks_amount() {
1361        let cap_kind = CapabilityKind::Spend {
1362            currency: "USD".to_string(),
1363            max_amount: 100,
1364        };
1365        let action_ok = CapabilityKind::Spend {
1366            currency: "USD".to_string(),
1367            max_amount: 50,
1368        };
1369        let action_too_much = CapabilityKind::Spend {
1370            currency: "USD".to_string(),
1371            max_amount: 150,
1372        };
1373
1374        assert!(cap_kind.matches(&action_ok));
1375        assert!(!cap_kind.matches(&action_too_much));
1376    }
1377
1378    // === Capability Tests ===
1379
1380    #[test]
1381    fn capability_created_successfully() {
1382        let grantor_key = SecretKey::generate();
1383        let cap = Capability::builder()
1384            .kind(CapabilityKind::Read)
1385            .scope(ResourceScope::all())
1386            .grantor(test_grantor())
1387            .sign(&grantor_key)
1388            .unwrap();
1389
1390        assert_eq!(cap.kind(), &CapabilityKind::Read);
1391        assert_eq!(cap.scope(), &ResourceScope::all());
1392    }
1393
1394    #[test]
1395    fn capability_expiry_checked() {
1396        let grantor_key = SecretKey::generate();
1397        let now = chrono::Utc::now().timestamp_millis();
1398
1399        let cap = Capability::builder()
1400            .kind(CapabilityKind::Read)
1401            .scope(ResourceScope::all())
1402            .grantor(test_grantor())
1403            .granted_at(now)
1404            .expires_at(now + 3600 * 1000)
1405            .sign(&grantor_key)
1406            .unwrap();
1407
1408        assert!(cap.is_valid_at(now));
1409        assert!(cap.is_valid_at(now + 1800 * 1000));
1410        assert!(!cap.is_valid_at(now + 3600 * 1000));
1411    }
1412
1413    #[test]
1414    fn capability_matches_action() {
1415        let grantor_key = SecretKey::generate();
1416        let cap = Capability::builder()
1417            .kind(CapabilityKind::Read)
1418            .scope(ResourceScope::kind(ResourceKind::Repository))
1419            .grantor(test_grantor())
1420            .sign(&grantor_key)
1421            .unwrap();
1422
1423        assert!(cap.matches(&CapabilityKind::Read, &test_resource("repository", "test")));
1424        assert!(!cap.matches(&CapabilityKind::Write, &test_resource("repository", "test")));
1425        assert!(!cap.matches(&CapabilityKind::Read, &test_resource("file", "test")));
1426    }
1427
1428    // === CapabilitySet Tests ===
1429
1430    #[test]
1431    fn capability_set_permits_matching() {
1432        let grantor_key = SecretKey::generate();
1433        let agent_key = SecretKey::generate();
1434
1435        let cap = Capability::builder()
1436            .kind(CapabilityKind::Read)
1437            .scope(ResourceScope::all())
1438            .grantor(test_grantor())
1439            .sign(&grantor_key)
1440            .unwrap();
1441
1442        let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1443        let now = chrono::Utc::now().timestamp_millis();
1444
1445        let check = set.permits(
1446            &CapabilityKind::Read,
1447            &test_resource("repository", "test"),
1448            now,
1449        );
1450
1451        assert!(matches!(check, CapabilityCheck::Permitted { .. }));
1452    }
1453
1454    #[test]
1455    fn capability_set_denies_no_matching() {
1456        let grantor_key = SecretKey::generate();
1457        let agent_key = SecretKey::generate();
1458
1459        let cap = Capability::builder()
1460            .kind(CapabilityKind::Read)
1461            .scope(ResourceScope::all())
1462            .grantor(test_grantor())
1463            .sign(&grantor_key)
1464            .unwrap();
1465
1466        let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1467        let now = chrono::Utc::now().timestamp_millis();
1468
1469        let check = set.permits(
1470            &CapabilityKind::Write,
1471            &test_resource("repository", "test"),
1472            now,
1473        );
1474
1475        assert!(matches!(
1476            check,
1477            CapabilityCheck::Denied {
1478                reason: DenialReason::NoMatchingCapability
1479            }
1480        ));
1481    }
1482
1483    #[test]
1484    fn capability_set_requires_approval() {
1485        let grantor_key = SecretKey::generate();
1486        let agent_key = SecretKey::generate();
1487
1488        let cap = Capability::builder()
1489            .kind(CapabilityKind::Write)
1490            .scope(ResourceScope::all())
1491            .grantor(test_grantor())
1492            .constraints(
1493                CapabilityConstraints::new().with_requires_approval(Duration::from_secs(300)),
1494            )
1495            .sign(&grantor_key)
1496            .unwrap();
1497
1498        let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1499        let now = chrono::Utc::now().timestamp_millis();
1500
1501        let check = set.permits(
1502            &CapabilityKind::Write,
1503            &test_resource("repository", "test"),
1504            now,
1505        );
1506
1507        assert!(matches!(check, CapabilityCheck::RequiresApproval { .. }));
1508    }
1509
1510    #[test]
1511    fn capability_set_denies_expired() {
1512        let grantor_key = SecretKey::generate();
1513        let agent_key = SecretKey::generate();
1514        let now = chrono::Utc::now().timestamp_millis();
1515
1516        let cap = Capability::builder()
1517            .kind(CapabilityKind::Read)
1518            .scope(ResourceScope::all())
1519            .grantor(test_grantor())
1520            .granted_at(now - 7200 * 1000)
1521            .expires_at(now - 3600 * 1000) // Expired 1 hour ago
1522            .sign(&grantor_key)
1523            .unwrap();
1524
1525        let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1526
1527        let check = set.permits(
1528            &CapabilityKind::Read,
1529            &test_resource("repository", "test"),
1530            now,
1531        );
1532
1533        assert!(matches!(
1534            check,
1535            CapabilityCheck::Denied {
1536                reason: DenialReason::Expired
1537            }
1538        ));
1539    }
1540
1541    #[test]
1542    fn capability_set_delegation() {
1543        let grantor_key = SecretKey::generate();
1544        let agent1_key = SecretKey::generate();
1545        let agent2_key = SecretKey::generate();
1546
1547        let cap = Capability::builder()
1548            .kind(CapabilityKind::Read)
1549            .scope(ResourceScope::all())
1550            .grantor(test_grantor())
1551            .delegatable(3)
1552            .sign(&grantor_key)
1553            .unwrap();
1554
1555        let cap_id = cap.id();
1556        let set = CapabilitySet::with_capabilities(agent1_key.public_key(), vec![cap]);
1557
1558        let delegated = set.delegate(vec![cap_id], agent2_key.public_key()).unwrap();
1559
1560        assert_eq!(delegated.grantee(), &agent2_key.public_key());
1561        assert_eq!(delegated.delegation_depth(), 1);
1562        assert_eq!(delegated.capabilities().len(), 1);
1563    }
1564
1565    #[test]
1566    fn capability_set_delegation_depth_enforced() {
1567        let grantor_key = SecretKey::generate();
1568        let agent1_key = SecretKey::generate();
1569        let agent2_key = SecretKey::generate();
1570        let agent3_key = SecretKey::generate();
1571
1572        // Capability with max delegation depth of 1
1573        let cap = Capability::builder()
1574            .kind(CapabilityKind::Read)
1575            .scope(ResourceScope::all())
1576            .grantor(test_grantor())
1577            .delegatable(1)
1578            .sign(&grantor_key)
1579            .unwrap();
1580
1581        let cap_id = cap.id();
1582        let set1 = CapabilitySet::with_capabilities(agent1_key.public_key(), vec![cap]);
1583
1584        // First delegation succeeds
1585        let set2 = set1
1586            .delegate(vec![cap_id], agent2_key.public_key())
1587            .unwrap();
1588
1589        // Second delegation should fail
1590        let result = set2.delegate(vec![cap_id], agent3_key.public_key());
1591        assert!(result.is_err());
1592    }
1593
1594    #[test]
1595    fn capability_set_non_delegatable_rejected() {
1596        let grantor_key = SecretKey::generate();
1597        let agent1_key = SecretKey::generate();
1598        let agent2_key = SecretKey::generate();
1599
1600        // Non-delegatable capability
1601        let cap = Capability::builder()
1602            .kind(CapabilityKind::Read)
1603            .scope(ResourceScope::all())
1604            .grantor(test_grantor())
1605            .sign(&grantor_key)
1606            .unwrap();
1607
1608        let cap_id = cap.id();
1609        let set = CapabilitySet::with_capabilities(agent1_key.public_key(), vec![cap]);
1610
1611        let result = set.delegate(vec![cap_id], agent2_key.public_key());
1612        assert!(result.is_err());
1613    }
1614
1615    #[test]
1616    fn capability_set_hash_deterministic() {
1617        let grantor_key = SecretKey::generate();
1618        let agent_key = SecretKey::generate();
1619
1620        let cap = Capability::builder()
1621            .id(CapabilityId::from_bytes([1u8; 16]))
1622            .kind(CapabilityKind::Read)
1623            .scope(ResourceScope::all())
1624            .grantor(test_grantor())
1625            .granted_at(1000000)
1626            .sign(&grantor_key)
1627            .unwrap();
1628
1629        let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1630
1631        let h1 = set.hash();
1632        let h2 = set.hash();
1633        assert_eq!(h1, h2);
1634    }
1635
1636    // === TimeWindow Tests ===
1637
1638    #[test]
1639    fn time_window_within_business_hours() {
1640        let window = TimeWindow::weekday_business_hours();
1641
1642        // Wednesday, 2024-01-10 at 10:00:00 UTC (clearly within 9-17)
1643        // 1704880800000 ms = Wed Jan 10 2024 10:00:00 UTC
1644        let timestamp = 1704880800000i64;
1645        assert!(window.is_within(timestamp));
1646    }
1647
1648    #[test]
1649    fn time_window_outside_business_hours() {
1650        let window = TimeWindow::weekday_business_hours();
1651
1652        // Wednesday, 2024-01-10 at 18:00:00 UTC (outside 9-17)
1653        // 1704909600000 ms = Wed Jan 10 2024 18:00:00 UTC
1654        let timestamp = 1704909600000i64;
1655        assert!(!window.is_within(timestamp));
1656    }
1657
1658    #[test]
1659    fn time_window_weekend_denied() {
1660        let window = TimeWindow::weekday_business_hours();
1661
1662        // Saturday, 2024-01-13 at 12:00:00 UTC (weekend, even during hours)
1663        // 1705147200000 ms = Sat Jan 13 2024 12:00:00 UTC
1664        let timestamp = 1705147200000i64;
1665        assert!(!window.is_within(timestamp));
1666    }
1667
1668    #[test]
1669    fn time_window_custom_timezone() {
1670        // Create a window for 9am-5pm in America/New_York
1671        let window = TimeWindow::new(
1672            TimeOfDay::new(9, 0, 0),
1673            TimeOfDay::new(17, 0, 0),
1674            vec![DayOfWeek::Monday, DayOfWeek::Tuesday, DayOfWeek::Wednesday],
1675            "America/New_York",
1676        );
1677
1678        // Monday, 2024-01-08 at 14:00:00 UTC = 9:00 AM EST (within window)
1679        let timestamp_within = 1704722400000i64;
1680        assert!(window.is_within(timestamp_within));
1681
1682        // Monday, 2024-01-08 at 12:00:00 UTC = 7:00 AM EST (before window)
1683        let timestamp_before = 1704715200000i64;
1684        assert!(!window.is_within(timestamp_before));
1685    }
1686
1687    #[test]
1688    fn time_window_invalid_timezone_denied() {
1689        let window = TimeWindow::new(
1690            TimeOfDay::new(9, 0, 0),
1691            TimeOfDay::new(17, 0, 0),
1692            vec![DayOfWeek::Monday],
1693            "Invalid/Timezone",
1694        );
1695
1696        // Should deny access for invalid timezone (fail-secure)
1697        let timestamp = 1704722400000i64;
1698        assert!(!window.is_within(timestamp));
1699    }
1700
1701    #[test]
1702    fn time_window_overnight() {
1703        // Create an overnight window (22:00 to 06:00)
1704        let window = TimeWindow::new(
1705            TimeOfDay::new(22, 0, 0),
1706            TimeOfDay::new(6, 0, 0),
1707            vec![
1708                DayOfWeek::Monday,
1709                DayOfWeek::Tuesday,
1710                DayOfWeek::Wednesday,
1711                DayOfWeek::Thursday,
1712                DayOfWeek::Friday,
1713                DayOfWeek::Saturday,
1714                DayOfWeek::Sunday,
1715            ],
1716            "UTC",
1717        );
1718
1719        // Wednesday at 23:00 UTC (within overnight window)
1720        // 1704931200000 ms = Wed Jan 10 2024 23:00:00 UTC
1721        let timestamp_late = 1704927600000i64;
1722        assert!(window.is_within(timestamp_late));
1723
1724        // Thursday at 03:00 UTC (within overnight window)
1725        // 1704942000000 ms = Thu Jan 11 2024 03:00:00 UTC
1726        let timestamp_early = 1704942000000i64;
1727        assert!(window.is_within(timestamp_early));
1728
1729        // Wednesday at 12:00 UTC (outside overnight window)
1730        let timestamp_midday = 1704888000000i64;
1731        assert!(!window.is_within(timestamp_midday));
1732    }
1733
1734    #[test]
1735    fn time_of_day_seconds_calculation() {
1736        let morning = TimeOfDay::new(9, 30, 45);
1737        assert_eq!(morning.seconds_since_midnight(), 9 * 3600 + 30 * 60 + 45);
1738
1739        let midnight = TimeOfDay::new(0, 0, 0);
1740        assert_eq!(midnight.seconds_since_midnight(), 0);
1741
1742        let end_of_day = TimeOfDay::new(23, 59, 59);
1743        assert_eq!(
1744            end_of_day.seconds_since_midnight(),
1745            23 * 3600 + 59 * 60 + 59
1746        );
1747    }
1748
1749    // === Capability Revocation Tests (Finding 2.1) ===
1750
1751    #[test]
1752    fn capability_revoke_transitions_to_revoked() {
1753        let key = SecretKey::generate();
1754        let mut cap = Capability::builder()
1755            .kind(CapabilityKind::Read)
1756            .scope(ResourceScope::all())
1757            .grantor(test_grantor())
1758            .sign(&key)
1759            .unwrap();
1760
1761        assert!(!cap.is_revoked());
1762        cap.revoke("policy violation");
1763        assert!(cap.is_revoked());
1764    }
1765
1766    #[test]
1767    fn capability_revoked_at_recorded() {
1768        let key = SecretKey::generate();
1769        let mut cap = Capability::builder()
1770            .kind(CapabilityKind::Read)
1771            .scope(ResourceScope::all())
1772            .grantor(test_grantor())
1773            .sign(&key)
1774            .unwrap();
1775
1776        assert!(cap.revoked_at().is_none());
1777        let before = chrono::Utc::now().timestamp_millis();
1778        cap.revoke("test");
1779        let after = chrono::Utc::now().timestamp_millis();
1780
1781        let ts = cap.revoked_at().expect("revoked_at should be set");
1782        assert!(ts >= before && ts <= after);
1783    }
1784
1785    #[test]
1786    fn capability_revocation_reason_preserved() {
1787        let key = SecretKey::generate();
1788        let mut cap = Capability::builder()
1789            .kind(CapabilityKind::Write)
1790            .scope(ResourceScope::all())
1791            .grantor(test_grantor())
1792            .sign(&key)
1793            .unwrap();
1794
1795        cap.revoke("agent exceeded spending limit");
1796        assert_eq!(
1797            cap.revocation_reason(),
1798            Some("agent exceeded spending limit")
1799        );
1800    }
1801
1802    #[test]
1803    fn capability_lifecycle_states() {
1804        let key = SecretKey::generate();
1805        let now = chrono::Utc::now().timestamp_millis();
1806
1807        // Active: not expired, not revoked
1808        let cap = Capability::builder()
1809            .kind(CapabilityKind::Read)
1810            .scope(ResourceScope::all())
1811            .grantor(test_grantor())
1812            .expires_at(now + 60_000) // 1 minute from now
1813            .sign(&key)
1814            .unwrap();
1815        assert_eq!(cap.lifecycle_state(now), CapabilityState::Active);
1816
1817        // Expired: past expiry
1818        assert_eq!(cap.lifecycle_state(now + 120_000), CapabilityState::Expired);
1819
1820        // Revoked: explicitly revoked (takes precedence over active)
1821        let mut cap2 = Capability::builder()
1822            .kind(CapabilityKind::Read)
1823            .scope(ResourceScope::all())
1824            .grantor(test_grantor())
1825            .expires_at(now + 60_000)
1826            .sign(&key)
1827            .unwrap();
1828        cap2.revoke("security incident");
1829        assert_eq!(cap2.lifecycle_state(now), CapabilityState::Revoked);
1830    }
1831
1832    #[test]
1833    fn capability_revoked_is_invalid() {
1834        let key = SecretKey::generate();
1835        let now = chrono::Utc::now().timestamp_millis();
1836        let mut cap = Capability::builder()
1837            .kind(CapabilityKind::Read)
1838            .scope(ResourceScope::all())
1839            .grantor(test_grantor())
1840            .expires_at(now + 60_000)
1841            .sign(&key)
1842            .unwrap();
1843
1844        assert!(cap.is_valid_at(now));
1845        cap.revoke("compromised");
1846        assert!(!cap.is_valid_at(now));
1847    }
1848
1849    // === Delegation Chain Tests (Finding 2.2) ===
1850
1851    #[test]
1852    fn delegate_creates_child_capability() {
1853        let key = SecretKey::generate();
1854        let cap = Capability::builder()
1855            .kind(CapabilityKind::Read)
1856            .scope(ResourceScope::pattern("repository:org/*"))
1857            .grantor(test_grantor())
1858            .delegatable(3)
1859            .sign(&key)
1860            .unwrap();
1861
1862        let child = cap.delegate(&key, None, None).unwrap();
1863
1864        assert_eq!(child.delegation_depth(), 1);
1865        assert_eq!(child.parent_capability_id(), Some(cap.id()));
1866        assert!(child.kind().matches(&CapabilityKind::Read));
1867        assert_ne!(child.id(), cap.id());
1868    }
1869
1870    #[test]
1871    fn delegate_rejects_exceeding_max_depth() {
1872        let key = SecretKey::generate();
1873        // Create a capability with max_delegation_depth = 1
1874        let cap = Capability::builder()
1875            .kind(CapabilityKind::Read)
1876            .scope(ResourceScope::all())
1877            .grantor(test_grantor())
1878            .delegatable(1)
1879            .sign(&key)
1880            .unwrap();
1881
1882        // First delegation (depth 0 -> 1): OK
1883        let child = cap.delegate(&key, None, None).unwrap();
1884        assert_eq!(child.delegation_depth(), 1);
1885
1886        // Second delegation (depth 1 -> 2): exceeds max 1
1887        let err = child.delegate(&key, None, None).unwrap_err();
1888        assert!(err.to_string().contains("delegation depth"));
1889    }
1890
1891    #[test]
1892    fn delegate_scope_must_be_subset() {
1893        let key = SecretKey::generate();
1894        let cap = Capability::builder()
1895            .kind(CapabilityKind::Read)
1896            .scope(ResourceScope::pattern("repository:org/*"))
1897            .grantor(test_grantor())
1898            .delegatable(3)
1899            .sign(&key)
1900            .unwrap();
1901
1902        // Valid subset: specific resource under the pattern
1903        let child = cap.delegate(
1904            &key,
1905            Some(ResourceScope::specific("repository:org/project")),
1906            None,
1907        );
1908        assert!(child.is_ok());
1909
1910        // Invalid: broader scope (All > Pattern)
1911        let err = cap
1912            .delegate(&key, Some(ResourceScope::all()), None)
1913            .unwrap_err();
1914        assert!(err.to_string().contains("subset"));
1915    }
1916
1917    #[test]
1918    fn delegate_expiry_must_not_exceed_parent() {
1919        let key = SecretKey::generate();
1920        let now = chrono::Utc::now().timestamp_millis();
1921        let cap = Capability::builder()
1922            .kind(CapabilityKind::Read)
1923            .scope(ResourceScope::all())
1924            .grantor(test_grantor())
1925            .delegatable(3)
1926            .expires_at(now + 10_000) // expires in 10 seconds
1927            .sign(&key)
1928            .unwrap();
1929
1930        // Requesting 60 seconds expiry exceeds parent's 10 second remaining
1931        let err = cap
1932            .delegate(&key, None, Some(Duration::from_secs(60)))
1933            .unwrap_err();
1934        assert!(err.to_string().contains("expiry"));
1935
1936        // Requesting 5 seconds should succeed
1937        let child = cap.delegate(&key, None, Some(Duration::from_secs(5)));
1938        assert!(child.is_ok());
1939    }
1940
1941    #[test]
1942    fn delegate_non_delegatable_capability_fails() {
1943        let key = SecretKey::generate();
1944        // Default delegatable is false
1945        let cap = Capability::builder()
1946            .kind(CapabilityKind::Read)
1947            .scope(ResourceScope::all())
1948            .grantor(test_grantor())
1949            .sign(&key)
1950            .unwrap();
1951
1952        assert!(!cap.is_delegatable());
1953        let err = cap.delegate(&key, None, None).unwrap_err();
1954        assert!(err.to_string().contains("not delegatable"));
1955    }
1956
1957    #[test]
1958    fn is_scope_subset_correctness() {
1959        // All is subset of All
1960        assert!(Capability::is_scope_subset(
1961            &ResourceScope::All,
1962            &ResourceScope::All
1963        ));
1964
1965        // All is NOT subset of Pattern
1966        assert!(!Capability::is_scope_subset(
1967            &ResourceScope::All,
1968            &ResourceScope::pattern("repo:*")
1969        ));
1970
1971        // Specific is subset of All
1972        assert!(Capability::is_scope_subset(
1973            &ResourceScope::specific("repo:a"),
1974            &ResourceScope::All
1975        ));
1976
1977        // Specific is subset of matching Pattern
1978        assert!(Capability::is_scope_subset(
1979            &ResourceScope::specific("repo:org/project"),
1980            &ResourceScope::pattern("repo:org/*")
1981        ));
1982
1983        // Specific is NOT subset of non-matching Pattern
1984        assert!(!Capability::is_scope_subset(
1985            &ResourceScope::specific("repo:other/project"),
1986            &ResourceScope::pattern("repo:org/*")
1987        ));
1988
1989        // Equal specific scopes
1990        assert!(Capability::is_scope_subset(
1991            &ResourceScope::specific("repo:a"),
1992            &ResourceScope::specific("repo:a")
1993        ));
1994
1995        // Different specific scopes
1996        assert!(!Capability::is_scope_subset(
1997            &ResourceScope::specific("repo:a"),
1998            &ResourceScope::specific("repo:b")
1999        ));
2000    }
2001
2002    #[test]
2003    fn capability_set_denies_revoked() {
2004        let key = SecretKey::generate();
2005        let grantor = test_grantor();
2006
2007        let mut cap = Capability::builder()
2008            .kind(CapabilityKind::Read)
2009            .scope(ResourceScope::all())
2010            .grantor(grantor.clone())
2011            .sign(&key)
2012            .unwrap();
2013
2014        cap.revoke("policy violation");
2015
2016        let set = CapabilitySet::with_capabilities(key.public_key(), vec![cap]);
2017        let resource = test_resource("repository", "org/project");
2018        let now = chrono::Utc::now().timestamp_millis();
2019
2020        let result = set.permits(&CapabilityKind::Read, &resource, now);
2021        assert_eq!(
2022            result,
2023            CapabilityCheck::Denied {
2024                reason: DenialReason::Revoked
2025            }
2026        );
2027    }
2028
2029    #[test]
2030    fn delegate_multi_level_chain() {
2031        let key = SecretKey::generate();
2032        let cap = Capability::builder()
2033            .kind(CapabilityKind::Read)
2034            .scope(ResourceScope::all())
2035            .grantor(test_grantor())
2036            .delegatable(3)
2037            .sign(&key)
2038            .unwrap();
2039
2040        // Level 0 -> 1
2041        let child1 = cap.delegate(&key, None, None).unwrap();
2042        assert_eq!(child1.delegation_depth(), 1);
2043        assert_eq!(child1.parent_capability_id(), Some(cap.id()));
2044
2045        // Level 1 -> 2
2046        let child2 = child1.delegate(&key, None, None).unwrap();
2047        assert_eq!(child2.delegation_depth(), 2);
2048        assert_eq!(child2.parent_capability_id(), Some(child1.id()));
2049
2050        // Level 2 -> 3
2051        let child3 = child2.delegate(&key, None, None).unwrap();
2052        assert_eq!(child3.delegation_depth(), 3);
2053        assert_eq!(child3.parent_capability_id(), Some(child2.id()));
2054
2055        // Level 3 -> 4: exceeds max_delegation_depth=3
2056        let err = child3.delegate(&key, None, None).unwrap_err();
2057        assert!(err.to_string().contains("delegation depth"));
2058    }
2059
2060    #[test]
2061    fn revoke_idempotent() {
2062        let key = SecretKey::generate();
2063        let mut cap = Capability::builder()
2064            .kind(CapabilityKind::Read)
2065            .scope(ResourceScope::all())
2066            .grantor(test_grantor())
2067            .sign(&key)
2068            .unwrap();
2069
2070        cap.revoke("first reason");
2071        let ts1 = cap.revoked_at().unwrap();
2072        let reason1 = cap.revocation_reason().unwrap().to_string();
2073
2074        // Second revoke overwrites (latest revoke wins)
2075        std::thread::sleep(std::time::Duration::from_millis(2));
2076        cap.revoke("updated reason");
2077        let ts2 = cap.revoked_at().unwrap();
2078        let reason2 = cap.revocation_reason().unwrap().to_string();
2079
2080        // Remains revoked with updated info
2081        assert!(cap.is_revoked());
2082        assert!(ts2 >= ts1);
2083        assert_eq!(reason2, "updated reason");
2084        assert_ne!(reason1, reason2);
2085    }
2086
2087    #[test]
2088    fn lifecycle_state_revoked_takes_precedence_over_expired() {
2089        let key = SecretKey::generate();
2090        let now = chrono::Utc::now().timestamp_millis();
2091
2092        let mut cap = Capability::builder()
2093            .kind(CapabilityKind::Read)
2094            .scope(ResourceScope::all())
2095            .grantor(test_grantor())
2096            .expires_at(now - 1000) // already expired
2097            .sign(&key)
2098            .unwrap();
2099
2100        // Without revocation, it's Expired
2101        assert_eq!(cap.lifecycle_state(now), CapabilityState::Expired);
2102
2103        // After revocation, Revoked takes precedence
2104        cap.revoke("also revoked");
2105        assert_eq!(cap.lifecycle_state(now), CapabilityState::Revoked);
2106    }
2107
2108    #[test]
2109    fn delegate_inherits_scope_when_none() {
2110        let key = SecretKey::generate();
2111        let parent_scope = ResourceScope::pattern("repository:org/*");
2112        let cap = Capability::builder()
2113            .kind(CapabilityKind::Read)
2114            .scope(parent_scope.clone())
2115            .grantor(test_grantor())
2116            .delegatable(3)
2117            .sign(&key)
2118            .unwrap();
2119
2120        let child = cap.delegate(&key, None, None).unwrap();
2121        assert_eq!(child.scope(), &parent_scope);
2122    }
2123
2124    #[test]
2125    fn delegate_inherits_expiry_when_none() {
2126        let key = SecretKey::generate();
2127        let now = chrono::Utc::now().timestamp_millis();
2128        let parent_expiry = now + 60_000;
2129
2130        let cap = Capability::builder()
2131            .kind(CapabilityKind::Read)
2132            .scope(ResourceScope::all())
2133            .grantor(test_grantor())
2134            .delegatable(3)
2135            .expires_at(parent_expiry)
2136            .sign(&key)
2137            .unwrap();
2138
2139        let child = cap.delegate(&key, None, None).unwrap();
2140        assert_eq!(child.expires_at(), Some(parent_expiry));
2141    }
2142}