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}