1use serde::{Deserialize, Serialize};
6use sh_layer1::generate_short_id;
7use std::collections::HashMap;
8
9pub type PermissionId = String;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum PermissionAction {
15 CommandExecute { command: String, args: Vec<String> },
17 FileRead { path: String },
19 FileWrite {
21 path: String,
22 content_preview: Option<String>,
23 },
24 FileDelete { path: String },
26 NetworkRequest { url: String, method: String },
28 EnvAccess { names: Vec<String> },
30 PackageInstall { packages: Vec<String> },
32 SystemAccess { resource: String },
34 Custom { description: String },
36}
37
38impl PermissionAction {
39 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 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
96pub struct PermissionContext {
97 pub agent_id: Option<String>,
99 pub session_id: Option<String>,
101 pub task_id: Option<String>,
103 pub tool_name: Option<String>,
105 pub metadata: HashMap<String, String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct PermissionRequest {
112 pub id: PermissionId,
114 pub action: PermissionAction,
116 pub context: PermissionContext,
118 pub batchable: bool,
120 pub timestamp: chrono::DateTime<chrono::Utc>,
122}
123
124impl PermissionRequest {
125 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 pub fn with_context(mut self, context: PermissionContext) -> Self {
138 self.context = context;
139 self
140 }
141
142 pub fn batchable(mut self) -> Self {
144 self.batchable = true;
145 self
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum PermissionDecision {
152 Allow,
154 Deny,
156 AllowOnce,
158 DenyOnce,
160}
161
162impl PermissionDecision {
163 pub fn is_allowed(&self) -> bool {
165 matches!(
166 self,
167 PermissionDecision::Allow | PermissionDecision::AllowOnce
168 )
169 }
170
171 pub fn should_remember(&self) -> bool {
173 matches!(self, PermissionDecision::Allow | PermissionDecision::Deny)
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct PermissionResponse {
180 pub request_id: PermissionId,
182 pub decision: PermissionDecision,
184 pub reason: Option<String>,
186 pub timestamp: chrono::DateTime<chrono::Utc>,
188}
189
190impl PermissionResponse {
191 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct CachedPermission {
235 pub action_pattern: String,
237 pub decision: PermissionDecision,
239 pub cached_at: chrono::DateTime<chrono::Utc>,
241 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
243 pub use_count: usize,
245}
246
247impl CachedPermission {
248 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 pub fn use_once(&mut self) {
259 self.use_count += 1;
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct AuditEntry {
266 pub id: String,
268 pub request: PermissionRequest,
270 pub response: PermissionResponse,
272 pub from_cache: bool,
274 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}