orcs_auth/permission.rs
1//! Command permission types.
2//!
3//! Provides [`CommandPermission`] for trait-level permission results
4//! that don't depend on runtime-specific types like `ApprovalRequest`.
5//!
6//! # Architecture
7//!
8//! ```text
9//! CommandPermission (orcs-auth) <- trait-level, runtime-independent
10//! │
11//! └── CommandCheckResult (orcs-runtime) <- runtime, includes ApprovalRequest
12//! ```
13//!
14//! `CommandPermission` is used by:
15//! - `ChildContext::check_command_permission()` (in `orcs-component`) — trait boundary
16//! - [`crate::PermissionPolicy::check_command_permission()`] — abstract policy
17//!
18//! `CommandCheckResult` extends this with HIL-specific fields and stays in `orcs-runtime`.
19
20/// Result of a command permission check (trait-level type).
21///
22/// This is a simplified, runtime-independent version suitable for trait boundaries.
23/// Runtime implementations that need HIL integration use `CommandCheckResult`
24/// (in `orcs-runtime`) which includes `ApprovalRequest`.
25///
26/// # Variants
27///
28/// - `Allowed`: Command can execute immediately
29/// - `Denied`: Command is permanently blocked (e.g., denylist)
30/// - `RequiresApproval`: Command needs user approval before execution
31///
32/// # Example
33///
34/// ```
35/// use orcs_auth::CommandPermission;
36///
37/// let perm = CommandPermission::Allowed;
38/// assert!(perm.is_allowed());
39///
40/// let perm = CommandPermission::Denied("blocked pattern".to_string());
41/// assert!(perm.is_denied());
42/// assert_eq!(perm.status_str(), "denied");
43///
44/// let perm = CommandPermission::RequiresApproval {
45/// grant_pattern: "rm -rf".to_string(),
46/// description: "destructive operation".to_string(),
47/// };
48/// assert!(perm.requires_approval());
49/// ```
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum CommandPermission {
52 /// Command is allowed to execute.
53 Allowed,
54 /// Command is denied with a reason.
55 Denied(String),
56 /// Command requires user approval via HIL.
57 RequiresApproval {
58 /// The pattern to grant if approved.
59 grant_pattern: String,
60 /// Human-readable description of why approval is needed.
61 description: String,
62 },
63}
64
65impl CommandPermission {
66 /// Returns `true` if the command is allowed.
67 #[must_use]
68 pub fn is_allowed(&self) -> bool {
69 matches!(self, Self::Allowed)
70 }
71
72 /// Returns `true` if the command is denied.
73 #[must_use]
74 pub fn is_denied(&self) -> bool {
75 matches!(self, Self::Denied(_))
76 }
77
78 /// Returns `true` if the command requires approval.
79 #[must_use]
80 pub fn requires_approval(&self) -> bool {
81 matches!(self, Self::RequiresApproval { .. })
82 }
83
84 /// Returns the status as a string ("allowed", "denied", "requires_approval").
85 #[must_use]
86 pub fn status_str(&self) -> &'static str {
87 match self {
88 Self::Allowed => "allowed",
89 Self::Denied(_) => "denied",
90 Self::RequiresApproval { .. } => "requires_approval",
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn allowed_helpers() {
101 let p = CommandPermission::Allowed;
102 assert!(p.is_allowed());
103 assert!(!p.is_denied());
104 assert!(!p.requires_approval());
105 assert_eq!(p.status_str(), "allowed");
106 }
107
108 #[test]
109 fn denied_helpers() {
110 let p = CommandPermission::Denied("blocked".to_string());
111 assert!(!p.is_allowed());
112 assert!(p.is_denied());
113 assert!(!p.requires_approval());
114 assert_eq!(p.status_str(), "denied");
115 }
116
117 #[test]
118 fn requires_approval_helpers() {
119 let p = CommandPermission::RequiresApproval {
120 grant_pattern: "rm -rf".to_string(),
121 description: "destructive operation".to_string(),
122 };
123 assert!(!p.is_allowed());
124 assert!(!p.is_denied());
125 assert!(p.requires_approval());
126 assert_eq!(p.status_str(), "requires_approval");
127 }
128
129 #[test]
130 fn equality() {
131 assert_eq!(CommandPermission::Allowed, CommandPermission::Allowed);
132 assert_eq!(
133 CommandPermission::Denied("x".into()),
134 CommandPermission::Denied("x".into())
135 );
136 assert_ne!(
137 CommandPermission::Allowed,
138 CommandPermission::Denied("x".into())
139 );
140 }
141}