use serde::{Deserialize, Serialize};
use sh_layer1::generate_short_id;
use std::collections::HashMap;
pub type PermissionId = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PermissionAction {
CommandExecute { command: String, args: Vec<String> },
FileRead { path: String },
FileWrite {
path: String,
content_preview: Option<String>,
},
FileDelete { path: String },
NetworkRequest { url: String, method: String },
EnvAccess { names: Vec<String> },
PackageInstall { packages: Vec<String> },
SystemAccess { resource: String },
Custom { description: String },
}
impl PermissionAction {
pub fn description(&self) -> String {
match self {
PermissionAction::CommandExecute { command, args } => {
format!("Execute command: {} {}", command, args.join(" "))
}
PermissionAction::FileRead { path } => format!("Read file: {}", path),
PermissionAction::FileWrite {
path,
content_preview,
} => {
if let Some(preview) = content_preview {
let preview = if preview.len() > 100 {
format!("{}...", &preview[..100])
} else {
preview.clone()
};
format!("Write to file: {}\nPreview: {}", path, preview)
} else {
format!("Write to file: {}", path)
}
}
PermissionAction::FileDelete { path } => format!("Delete file: {}", path),
PermissionAction::NetworkRequest { url, method } => {
format!("{} request to: {}", method, url)
}
PermissionAction::EnvAccess { names } => {
format!("Access environment variables: {}", names.join(", "))
}
PermissionAction::PackageInstall { packages } => {
format!("Install packages: {}", packages.join(", "))
}
PermissionAction::SystemAccess { resource } => {
format!("Access system resource: {}", resource)
}
PermissionAction::Custom { description } => description.clone(),
}
}
pub fn category(&self) -> &'static str {
match self {
PermissionAction::CommandExecute { .. } => "command",
PermissionAction::FileRead { .. } => "file_read",
PermissionAction::FileWrite { .. } => "file_write",
PermissionAction::FileDelete { .. } => "file_delete",
PermissionAction::NetworkRequest { .. } => "network",
PermissionAction::EnvAccess { .. } => "environment",
PermissionAction::PackageInstall { .. } => "package",
PermissionAction::SystemAccess { .. } => "system",
PermissionAction::Custom { .. } => "custom",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PermissionContext {
pub agent_id: Option<String>,
pub session_id: Option<String>,
pub task_id: Option<String>,
pub tool_name: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequest {
pub id: PermissionId,
pub action: PermissionAction,
pub context: PermissionContext,
pub batchable: bool,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl PermissionRequest {
pub fn new(action: PermissionAction) -> Self {
Self {
id: generate_short_id(),
action,
context: PermissionContext::default(),
batchable: false,
timestamp: chrono::Utc::now(),
}
}
pub fn with_context(mut self, context: PermissionContext) -> Self {
self.context = context;
self
}
pub fn batchable(mut self) -> Self {
self.batchable = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PermissionDecision {
Allow,
Deny,
AllowOnce,
DenyOnce,
}
impl PermissionDecision {
pub fn is_allowed(&self) -> bool {
matches!(
self,
PermissionDecision::Allow | PermissionDecision::AllowOnce
)
}
pub fn should_remember(&self) -> bool {
matches!(self, PermissionDecision::Allow | PermissionDecision::Deny)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionResponse {
pub request_id: PermissionId,
pub decision: PermissionDecision,
pub reason: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl PermissionResponse {
pub fn allow(request_id: PermissionId) -> Self {
Self {
request_id,
decision: PermissionDecision::Allow,
reason: None,
timestamp: chrono::Utc::now(),
}
}
pub fn deny(request_id: PermissionId, reason: Option<String>) -> Self {
Self {
request_id,
decision: PermissionDecision::Deny,
reason,
timestamp: chrono::Utc::now(),
}
}
pub fn allow_once(request_id: PermissionId) -> Self {
Self {
request_id,
decision: PermissionDecision::AllowOnce,
reason: None,
timestamp: chrono::Utc::now(),
}
}
pub fn deny_once(request_id: PermissionId, reason: Option<String>) -> Self {
Self {
request_id,
decision: PermissionDecision::DenyOnce,
reason,
timestamp: chrono::Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedPermission {
pub action_pattern: String,
pub decision: PermissionDecision,
pub cached_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub use_count: usize,
}
impl CachedPermission {
pub fn is_valid(&self) -> bool {
if let Some(expires) = self.expires_at {
expires > chrono::Utc::now()
} else {
true
}
}
pub fn use_once(&mut self) {
self.use_count += 1;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: String,
pub request: PermissionRequest,
pub response: PermissionResponse,
pub from_cache: bool,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_action_description() {
let action = PermissionAction::FileRead {
path: "/test/file.txt".to_string(),
};
assert_eq!(action.description(), "Read file: /test/file.txt");
assert_eq!(action.category(), "file_read");
}
#[test]
fn test_permission_decision_is_allowed() {
assert!(PermissionDecision::Allow.is_allowed());
assert!(PermissionDecision::AllowOnce.is_allowed());
assert!(!PermissionDecision::Deny.is_allowed());
assert!(!PermissionDecision::DenyOnce.is_allowed());
}
#[test]
fn test_permission_request_builder() {
let request = PermissionRequest::new(PermissionAction::FileRead {
path: "test.txt".to_string(),
})
.batchable();
assert!(request.batchable);
assert!(!request.action.description().is_empty());
}
}