Skip to main content

deepseek/agent/
permissions.rs

1//! Permission gating for tool calls.
2//!
3//! Mirrors Claude Code's permission modes. Every tool call passes through
4//! [`PermissionMode::evaluate`]; if a [`PreToolHook`] is supplied it can
5//! override the mode-default behaviour.
6
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// What to do with a single tool call before it executes.
12#[derive(Debug, Clone)]
13pub enum PermissionDecision {
14    /// Run the tool.
15    Allow,
16    /// Synthesize a tool_result with `is_error=true` carrying `reason`.
17    Deny(String),
18    /// Defer to the mode default. Returned only by `PreToolHook`.
19    Ask,
20}
21
22/// Permission modes — same set as the Claude Agent SDK doc.
23#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub enum PermissionMode {
26    /// Calls the hook; deny if no hook is set.
27    #[default]
28    Default,
29    /// Auto-approves Read/Write/Edit/Glob/Grep and falls back to the hook
30    /// for everything else.
31    AcceptEdits,
32    /// Read-only — denies any tool whose `read_only_hint()` is false.
33    Plan,
34    /// Never asks; everything not pre-allowed is denied.
35    DontAsk,
36    /// Allow everything. Use only in sandboxes.
37    BypassPermissions,
38}
39
40/// Hook invoked before every tool call. Implementations may inspect the tool
41/// name and arguments and return a [`PermissionDecision`].
42#[async_trait]
43pub trait PreToolHook: Send + Sync {
44    async fn check(&self, tool_name: &str, args: &Value) -> PermissionDecision;
45}
46
47/// Names of the built-in edit-class tools that `AcceptEdits` auto-approves.
48pub(crate) const EDIT_TOOLS: &[&str] = &["Read", "Write", "Edit", "Glob", "Grep"];
49
50impl PermissionMode {
51    /// Apply the mode to a single tool call. `read_only_hint` reflects the
52    /// tool's annotation. Returns `Allow`, `Deny(reason)`, or `Ask` (only the
53    /// `Default` mode and `AcceptEdits` fallback ever return `Ask`).
54    pub fn evaluate(self, tool_name: &str, read_only_hint: bool) -> PermissionDecision {
55        match self {
56            Self::BypassPermissions => PermissionDecision::Allow,
57            Self::Plan => {
58                if read_only_hint {
59                    PermissionDecision::Allow
60                } else {
61                    PermissionDecision::Deny(format!(
62                        "Plan mode: tool `{tool_name}` is not read-only"
63                    ))
64                }
65            }
66            Self::AcceptEdits => {
67                if EDIT_TOOLS.contains(&tool_name) || read_only_hint {
68                    PermissionDecision::Allow
69                } else {
70                    PermissionDecision::Ask
71                }
72            }
73            Self::DontAsk => PermissionDecision::Deny(format!(
74                "Permission mode dontAsk: `{tool_name}` is not pre-approved"
75            )),
76            Self::Default => PermissionDecision::Ask,
77        }
78    }
79}