Skip to main content

cersei_tools/
permissions.rs

1//! Permission policies for tool execution.
2
3use super::PermissionLevel;
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7// ─── Permission policy trait ─────────────────────────────────────────────────
8
9#[async_trait]
10pub trait PermissionPolicy: Send + Sync {
11    async fn check(&self, request: &PermissionRequest) -> PermissionDecision;
12}
13
14#[derive(Debug, Clone)]
15pub struct PermissionRequest {
16    pub tool_name: String,
17    pub tool_input: serde_json::Value,
18    pub permission_level: PermissionLevel,
19    pub description: String,
20    pub id: String,
21}
22
23#[derive(Debug, Clone)]
24pub enum PermissionDecision {
25    Allow,
26    Deny(String),
27    AllowOnce,
28    AllowForSession,
29}
30
31// ─── Built-in policies ──────────────────────────────────────────────────────
32
33/// Allow all tool invocations. Suitable for CI/headless/trusted environments.
34pub struct AllowAll;
35
36#[async_trait]
37impl PermissionPolicy for AllowAll {
38    async fn check(&self, _request: &PermissionRequest) -> PermissionDecision {
39        PermissionDecision::Allow
40    }
41}
42
43/// Only allow tools with PermissionLevel::None or ReadOnly.
44pub struct AllowReadOnly;
45
46#[async_trait]
47impl PermissionPolicy for AllowReadOnly {
48    async fn check(&self, request: &PermissionRequest) -> PermissionDecision {
49        match request.permission_level {
50            PermissionLevel::None | PermissionLevel::ReadOnly => PermissionDecision::Allow,
51            _ => PermissionDecision::Deny(format!(
52                "Tool '{}' requires {:?} permission (read-only mode)",
53                request.tool_name, request.permission_level
54            )),
55        }
56    }
57}
58
59/// Deny all tool invocations.
60pub struct DenyAll;
61
62#[async_trait]
63impl PermissionPolicy for DenyAll {
64    async fn check(&self, request: &PermissionRequest) -> PermissionDecision {
65        PermissionDecision::Deny(format!(
66            "Tool '{}' blocked by DenyAll policy",
67            request.tool_name
68        ))
69    }
70}
71
72/// Rule-based permission policy with pattern matching.
73pub struct RuleBased {
74    pub rules: Vec<PermissionRule>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PermissionRule {
79    pub tool_name: Option<String>,
80    pub path_pattern: Option<String>,
81    pub action: PermissionAction,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub enum PermissionAction {
86    Allow,
87    Deny,
88}
89
90#[async_trait]
91impl PermissionPolicy for RuleBased {
92    async fn check(&self, request: &PermissionRequest) -> PermissionDecision {
93        for rule in &self.rules {
94            let name_matches = rule
95                .tool_name
96                .as_ref()
97                .map(|n| n == &request.tool_name || n == "all")
98                .unwrap_or(true);
99
100            if name_matches {
101                return match rule.action {
102                    PermissionAction::Allow => PermissionDecision::Allow,
103                    PermissionAction::Deny => PermissionDecision::Deny(format!(
104                        "Tool '{}' blocked by rule",
105                        request.tool_name
106                    )),
107                };
108            }
109        }
110        // Default: allow if no rules match
111        PermissionDecision::Allow
112    }
113}
114
115/// Interactive permission policy that defers to a callback.
116pub struct InteractivePolicy {
117    pub handler: Box<dyn Fn(&PermissionRequest) -> PermissionDecision + Send + Sync>,
118}
119
120impl InteractivePolicy {
121    pub fn new(
122        handler: impl Fn(&PermissionRequest) -> PermissionDecision + Send + Sync + 'static,
123    ) -> Self {
124        Self {
125            handler: Box::new(handler),
126        }
127    }
128
129    /// Create a policy that defers to the AgentStream for interactive decisions.
130    pub fn via_stream() -> StreamDeferredPolicy {
131        StreamDeferredPolicy
132    }
133}
134
135#[async_trait]
136impl PermissionPolicy for InteractivePolicy {
137    async fn check(&self, request: &PermissionRequest) -> PermissionDecision {
138        (self.handler)(request)
139    }
140}
141
142/// Placeholder policy that emits PermissionRequired events via the agent stream.
143pub struct StreamDeferredPolicy;
144
145#[async_trait]
146impl PermissionPolicy for StreamDeferredPolicy {
147    async fn check(&self, _request: &PermissionRequest) -> PermissionDecision {
148        // In practice, the agent loop intercepts this and emits
149        // AgentEvent::PermissionRequired, then waits for a response.
150        PermissionDecision::Allow
151    }
152}