Skip to main content

orcs_runtime/auth/
command_check.rs

1//! Command check result types.
2//!
3//! Provides [`CommandCheckResult`] for granular permission decisions
4//! that support HIL (Human-in-the-Loop) approval workflows.
5
6use crate::components::ApprovalRequest;
7
8/// Result of a command permission check.
9///
10/// Unlike `can_execute_command` which returns a simple bool, this enum
11/// provides more granular control:
12///
13/// - `Allowed`: Command can execute immediately
14/// - `Denied`: Command is permanently blocked
15/// - `RequiresApproval`: Command needs HIL approval before execution
16///
17/// # HIL Integration
18///
19/// When `RequiresApproval` is returned, the caller should:
20///
21/// 1. Submit the `ApprovalRequest` to `HilComponent`
22/// 2. Wait for user approval/rejection
23/// 3. If approved, call `grants.grant(CommandGrant::persistent(grant_pattern))`
24/// 4. Retry the command (which will now return `Allowed`)
25///
26/// # Example
27///
28/// ```ignore
29/// match checker.check_command(&session, "rm -rf ./temp") {
30///     CommandCheckResult::Allowed => execute(cmd),
31///     CommandCheckResult::Denied(reason) => error!("{}", reason),
32///     CommandCheckResult::RequiresApproval { request, grant_pattern } => {
33///         let id = hil.submit(request);
34///         if await_approval(id) {
35///             session.grant_command(&grant_pattern);
36///             execute(cmd);
37///         }
38///     }
39/// }
40/// ```
41#[derive(Debug, Clone)]
42pub enum CommandCheckResult {
43    /// Command is allowed to execute.
44    Allowed,
45    /// Command is denied with a reason.
46    Denied(String),
47    /// Command requires user approval via HIL.
48    RequiresApproval {
49        /// The approval request to submit to HilComponent.
50        request: ApprovalRequest,
51        /// The pattern to grant if approved (for future commands).
52        grant_pattern: String,
53    },
54}
55
56impl CommandCheckResult {
57    /// Returns `true` if the command is allowed.
58    #[must_use]
59    pub fn is_allowed(&self) -> bool {
60        matches!(self, Self::Allowed)
61    }
62
63    /// Returns `true` if the command is denied.
64    #[must_use]
65    pub fn is_denied(&self) -> bool {
66        matches!(self, Self::Denied(_))
67    }
68
69    /// Returns `true` if the command requires approval.
70    #[must_use]
71    pub fn requires_approval(&self) -> bool {
72        matches!(self, Self::RequiresApproval { .. })
73    }
74
75    /// Returns the denial reason if denied.
76    #[must_use]
77    pub fn denial_reason(&self) -> Option<&str> {
78        match self {
79            Self::Denied(reason) => Some(reason),
80            _ => None,
81        }
82    }
83
84    /// Returns the approval request if requires approval.
85    #[must_use]
86    pub fn approval_request(&self) -> Option<&ApprovalRequest> {
87        match self {
88            Self::RequiresApproval { request, .. } => Some(request),
89            _ => None,
90        }
91    }
92
93    /// Returns the grant pattern if requires approval.
94    #[must_use]
95    pub fn grant_pattern(&self) -> Option<&str> {
96        match self {
97            Self::RequiresApproval { grant_pattern, .. } => Some(grant_pattern),
98            _ => None,
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn allowed_helpers() {
109        let result = CommandCheckResult::Allowed;
110        assert!(result.is_allowed());
111        assert!(!result.is_denied());
112        assert!(!result.requires_approval());
113        assert!(result.denial_reason().is_none());
114        assert!(result.approval_request().is_none());
115        assert!(result.grant_pattern().is_none());
116    }
117
118    #[test]
119    fn denied_helpers() {
120        let result = CommandCheckResult::Denied("test reason".to_string());
121        assert!(!result.is_allowed());
122        assert!(result.is_denied());
123        assert!(!result.requires_approval());
124        assert_eq!(result.denial_reason(), Some("test reason"));
125        assert!(result.approval_request().is_none());
126        assert!(result.grant_pattern().is_none());
127    }
128
129    #[test]
130    fn requires_approval_helpers() {
131        let request = ApprovalRequest::new("bash", "test", serde_json::json!({}));
132        let result = CommandCheckResult::RequiresApproval {
133            request: request.clone(),
134            grant_pattern: "rm -rf".to_string(),
135        };
136        assert!(!result.is_allowed());
137        assert!(!result.is_denied());
138        assert!(result.requires_approval());
139        assert!(result.denial_reason().is_none());
140        assert!(result.approval_request().is_some());
141        assert_eq!(result.grant_pattern(), Some("rm -rf"));
142    }
143}