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}