Skip to main content

sh_layer2/permission/
types.rs

1//! # Permission Types
2//!
3//! Core types for the permission system.
4
5use serde::{Deserialize, Serialize};
6use sh_layer1::generate_short_id;
7use std::collections::HashMap;
8
9/// Unique identifier for a permission request
10pub type PermissionId = String;
11
12/// Actions that require permission
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum PermissionAction {
15    /// Execute a shell command
16    CommandExecute { command: String, args: Vec<String> },
17    /// Read a file
18    FileRead { path: String },
19    /// Write to a file
20    FileWrite {
21        path: String,
22        content_preview: Option<String>,
23    },
24    /// Delete a file
25    FileDelete { path: String },
26    /// Make a network request
27    NetworkRequest { url: String, method: String },
28    /// Access environment variables
29    EnvAccess { names: Vec<String> },
30    /// Install packages
31    PackageInstall { packages: Vec<String> },
32    /// Access system resources
33    SystemAccess { resource: String },
34    /// Custom action with description
35    Custom { description: String },
36}
37
38impl PermissionAction {
39    /// Get a human-readable description of the action
40    pub fn description(&self) -> String {
41        match self {
42            PermissionAction::CommandExecute { command, args } => {
43                format!("Execute command: {} {}", command, args.join(" "))
44            }
45            PermissionAction::FileRead { path } => format!("Read file: {}", path),
46            PermissionAction::FileWrite {
47                path,
48                content_preview,
49            } => {
50                if let Some(preview) = content_preview {
51                    let preview = if preview.len() > 100 {
52                        format!("{}...", &preview[..100])
53                    } else {
54                        preview.clone()
55                    };
56                    format!("Write to file: {}\nPreview: {}", path, preview)
57                } else {
58                    format!("Write to file: {}", path)
59                }
60            }
61            PermissionAction::FileDelete { path } => format!("Delete file: {}", path),
62            PermissionAction::NetworkRequest { url, method } => {
63                format!("{} request to: {}", method, url)
64            }
65            PermissionAction::EnvAccess { names } => {
66                format!("Access environment variables: {}", names.join(", "))
67            }
68            PermissionAction::PackageInstall { packages } => {
69                format!("Install packages: {}", packages.join(", "))
70            }
71            PermissionAction::SystemAccess { resource } => {
72                format!("Access system resource: {}", resource)
73            }
74            PermissionAction::Custom { description } => description.clone(),
75        }
76    }
77
78    /// Get the category of this action
79    pub fn category(&self) -> &'static str {
80        match self {
81            PermissionAction::CommandExecute { .. } => "command",
82            PermissionAction::FileRead { .. } => "file_read",
83            PermissionAction::FileWrite { .. } => "file_write",
84            PermissionAction::FileDelete { .. } => "file_delete",
85            PermissionAction::NetworkRequest { .. } => "network",
86            PermissionAction::EnvAccess { .. } => "environment",
87            PermissionAction::PackageInstall { .. } => "package",
88            PermissionAction::SystemAccess { .. } => "system",
89            PermissionAction::Custom { .. } => "custom",
90        }
91    }
92}
93
94/// Context information for a permission request
95#[derive(Debug, Clone, Serialize, Deserialize, Default)]
96pub struct PermissionContext {
97    /// Agent ID making the request
98    pub agent_id: Option<String>,
99    /// Session ID where the request originated
100    pub session_id: Option<String>,
101    /// Task ID if applicable
102    pub task_id: Option<String>,
103    /// Tool that triggered the request
104    pub tool_name: Option<String>,
105    /// Additional metadata
106    pub metadata: HashMap<String, String>,
107}
108
109/// A request for permission
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct PermissionRequest {
112    /// Unique request ID
113    pub id: PermissionId,
114    /// The action being requested
115    pub action: PermissionAction,
116    /// Context information
117    pub context: PermissionContext,
118    /// Whether this can be batched with other requests
119    pub batchable: bool,
120    /// Timestamp of the request
121    pub timestamp: chrono::DateTime<chrono::Utc>,
122}
123
124impl PermissionRequest {
125    /// Create a new permission request
126    pub fn new(action: PermissionAction) -> Self {
127        Self {
128            id: generate_short_id(),
129            action,
130            context: PermissionContext::default(),
131            batchable: false,
132            timestamp: chrono::Utc::now(),
133        }
134    }
135
136    /// Add context to the request
137    pub fn with_context(mut self, context: PermissionContext) -> Self {
138        self.context = context;
139        self
140    }
141
142    /// Mark as batchable
143    pub fn batchable(mut self) -> Self {
144        self.batchable = true;
145        self
146    }
147}
148
149/// Decision made for a permission request
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum PermissionDecision {
152    /// Permission granted
153    Allow,
154    /// Permission denied
155    Deny,
156    /// Permission granted for this session only
157    AllowOnce,
158    /// Permission denied for this session only
159    DenyOnce,
160}
161
162impl PermissionDecision {
163    /// Check if this decision allows the action
164    pub fn is_allowed(&self) -> bool {
165        matches!(
166            self,
167            PermissionDecision::Allow | PermissionDecision::AllowOnce
168        )
169    }
170
171    /// Check if this decision should be remembered
172    pub fn should_remember(&self) -> bool {
173        matches!(self, PermissionDecision::Allow | PermissionDecision::Deny)
174    }
175}
176
177/// Response to a permission request
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct PermissionResponse {
180    /// The request being responded to
181    pub request_id: PermissionId,
182    /// The decision made
183    pub decision: PermissionDecision,
184    /// Optional reason for the decision
185    pub reason: Option<String>,
186    /// Timestamp of the response
187    pub timestamp: chrono::DateTime<chrono::Utc>,
188}
189
190impl PermissionResponse {
191    /// Create an allow response
192    pub fn allow(request_id: PermissionId) -> Self {
193        Self {
194            request_id,
195            decision: PermissionDecision::Allow,
196            reason: None,
197            timestamp: chrono::Utc::now(),
198        }
199    }
200
201    /// Create a deny response
202    pub fn deny(request_id: PermissionId, reason: Option<String>) -> Self {
203        Self {
204            request_id,
205            decision: PermissionDecision::Deny,
206            reason,
207            timestamp: chrono::Utc::now(),
208        }
209    }
210
211    /// Create an allow-once response
212    pub fn allow_once(request_id: PermissionId) -> Self {
213        Self {
214            request_id,
215            decision: PermissionDecision::AllowOnce,
216            reason: None,
217            timestamp: chrono::Utc::now(),
218        }
219    }
220
221    /// Create a deny-once response
222    pub fn deny_once(request_id: PermissionId, reason: Option<String>) -> Self {
223        Self {
224            request_id,
225            decision: PermissionDecision::DenyOnce,
226            reason,
227            timestamp: chrono::Utc::now(),
228        }
229    }
230}
231
232/// Cached permission entry
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct CachedPermission {
235    /// The action pattern that was allowed/denied
236    pub action_pattern: String,
237    /// The decision made
238    pub decision: PermissionDecision,
239    /// When this was cached
240    pub cached_at: chrono::DateTime<chrono::Utc>,
241    /// When this cache entry expires (if applicable)
242    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
243    /// Number of times this cached entry has been used
244    pub use_count: usize,
245}
246
247impl CachedPermission {
248    /// Check if this cache entry is still valid
249    pub fn is_valid(&self) -> bool {
250        if let Some(expires) = self.expires_at {
251            expires > chrono::Utc::now()
252        } else {
253            true
254        }
255    }
256
257    /// Mark this entry as used
258    pub fn use_once(&mut self) {
259        self.use_count += 1;
260    }
261}
262
263/// Audit log entry
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct AuditEntry {
266    /// Unique audit entry ID
267    pub id: String,
268    /// The request that was made
269    pub request: PermissionRequest,
270    /// The response that was given
271    pub response: PermissionResponse,
272    /// Whether this was from cache
273    pub from_cache: bool,
274    /// Timestamp
275    pub timestamp: chrono::DateTime<chrono::Utc>,
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_permission_action_description() {
284        let action = PermissionAction::FileRead {
285            path: "/test/file.txt".to_string(),
286        };
287        assert_eq!(action.description(), "Read file: /test/file.txt");
288        assert_eq!(action.category(), "file_read");
289    }
290
291    #[test]
292    fn test_permission_decision_is_allowed() {
293        assert!(PermissionDecision::Allow.is_allowed());
294        assert!(PermissionDecision::AllowOnce.is_allowed());
295        assert!(!PermissionDecision::Deny.is_allowed());
296        assert!(!PermissionDecision::DenyOnce.is_allowed());
297    }
298
299    #[test]
300    fn test_permission_request_builder() {
301        let request = PermissionRequest::new(PermissionAction::FileRead {
302            path: "test.txt".to_string(),
303        })
304        .batchable();
305
306        assert!(request.batchable);
307        assert!(!request.action.description().is_empty());
308    }
309}