mixtape_core/agent/permission.rs
1//! Authorization Handling for Tool Execution
2//!
3//! This module provides the authorization system for controlling tool execution.
4//!
5//! ## Default Behavior
6//!
7//! By default, tools without grants are **denied immediately**. This is secure
8//! for non-interactive environments (scripts, CI/CD, automated agents).
9//!
10//! ## Interactive Mode
11//!
12//! For REPLs and CLIs where a human can approve tools, use `.interactive()`:
13//!
14//! ```ignore
15//! use mixtape_core::Agent;
16//!
17//! // Interactive mode: prompts user for tools without grants
18//! let agent = Agent::builder()
19//! .bedrock(ClaudeSonnet4_5)
20//! .interactive() // Enable permission prompts
21//! .build()
22//! .await?;
23//! ```
24//!
25//! ## Pre-Granting Tools
26//!
27//! For both interactive and non-interactive use, you can pre-grant tools:
28//!
29//! ```ignore
30//! use mixtape_core::{Agent, MemoryGrantStore};
31//!
32//! // Pre-grant some tools
33//! let store = MemoryGrantStore::new();
34//! store.grant_tool("echo").await?;
35//! store.grant_tool("read_file").await?;
36//!
37//! let agent = Agent::builder()
38//! .bedrock(ClaudeSonnet4_5)
39//! .with_grant_store(store)
40//! .build()
41//! .await?;
42//! ```
43//!
44//! ## Handling Permission Events
45//!
46//! In interactive mode, respond to [`AgentEvent::PermissionRequired`] using:
47//! - [`Agent::respond_to_authorization()`] - Full control with [`AuthorizationResponse`]
48//! - [`Agent::authorize_once()`] - One-time authorization
49//! - [`Agent::deny_authorization()`] - Deny the request
50
51use std::time::Duration;
52
53use super::builder::AgentBuilder;
54use super::types::PermissionError;
55use super::Agent;
56use crate::permission::{
57 AuthorizationResponse, Grant, GrantStore, Scope, ToolAuthorizationPolicy, ToolCallAuthorizer,
58};
59
60impl Agent {
61 /// Get the authorizer to grant/revoke permissions.
62 pub fn authorizer(&self) -> &tokio::sync::RwLock<ToolCallAuthorizer> {
63 &self.authorizer
64 }
65
66 /// Respond to an authorization request with a choice.
67 ///
68 /// Use this to respond to [`crate::AgentEvent::PermissionRequired`] events.
69 pub async fn respond_to_authorization(
70 &self,
71 proposal_id: &str,
72 response: AuthorizationResponse,
73 ) -> Result<(), PermissionError> {
74 let pending = self.pending_authorizations.read().await;
75
76 if let Some(tx) = pending.get(proposal_id) {
77 tx.send(response)
78 .await
79 .map_err(|_| PermissionError::ChannelClosed)?;
80 Ok(())
81 } else {
82 Err(PermissionError::RequestNotFound(proposal_id.to_string()))
83 }
84 }
85
86 /// Grant permission to trust this tool entirely.
87 ///
88 /// This saves a tool-wide grant that will auto-authorize all future calls.
89 pub async fn grant_tool_permission(
90 &self,
91 proposal_id: &str,
92 tool_name: &str,
93 scope: Scope,
94 ) -> Result<(), PermissionError> {
95 let grant = Grant::tool(tool_name).with_scope(scope);
96 self.respond_to_authorization(proposal_id, AuthorizationResponse::Trust { grant })
97 .await
98 }
99
100 /// Grant permission for this exact call.
101 ///
102 /// This saves an exact-match grant for the specific parameters.
103 pub async fn grant_params_permission(
104 &self,
105 proposal_id: &str,
106 tool_name: &str,
107 params_hash: &str,
108 scope: Scope,
109 ) -> Result<(), PermissionError> {
110 let grant = Grant::exact(tool_name, params_hash).with_scope(scope);
111 self.respond_to_authorization(proposal_id, AuthorizationResponse::Trust { grant })
112 .await
113 }
114
115 /// Authorize a request once (don't save).
116 pub async fn authorize_once(&self, proposal_id: &str) -> Result<(), PermissionError> {
117 self.respond_to_authorization(proposal_id, AuthorizationResponse::Once)
118 .await
119 }
120
121 /// Deny an authorization request.
122 pub async fn deny_authorization(
123 &self,
124 proposal_id: &str,
125 reason: Option<String>,
126 ) -> Result<(), PermissionError> {
127 self.respond_to_authorization(proposal_id, AuthorizationResponse::Deny { reason })
128 .await
129 }
130}
131
132impl AgentBuilder {
133 /// Set a custom grant store for tool authorization.
134 ///
135 /// By default, the agent uses an in-memory store. Use this to provide
136 /// a persistent store (e.g., [`crate::FileGrantStore`]).
137 ///
138 /// # Example
139 ///
140 /// ```ignore
141 /// use mixtape_core::{Agent, FileGrantStore};
142 ///
143 /// let agent = Agent::builder()
144 /// .bedrock(ClaudeSonnet4_5)
145 /// .with_grant_store(FileGrantStore::new("./grants.json"))
146 /// .build()
147 /// .await?;
148 /// ```
149 pub fn with_grant_store(mut self, store: impl GrantStore + 'static) -> Self {
150 self.grant_store = Some(Box::new(store));
151 self
152 }
153
154 /// Set the timeout for authorization requests.
155 ///
156 /// If an authorization request is not responded to within this duration,
157 /// it will be automatically denied.
158 ///
159 /// Default: 5 minutes
160 pub fn with_authorization_timeout(mut self, timeout: Duration) -> Self {
161 self.authorization_timeout = timeout;
162 self
163 }
164
165 /// Enable interactive authorization prompts.
166 ///
167 /// By default, tools without grants are **denied immediately**. This is
168 /// secure for non-interactive environments (scripts, CI/CD, automated agents).
169 ///
170 /// Call `.interactive()` to enable prompting users for authorization when
171 /// a tool has no matching grant. The agent will emit [`crate::AgentEvent::PermissionRequired`]
172 /// events that can be handled to approve or deny tool calls.
173 ///
174 /// # Example
175 ///
176 /// ```ignore
177 /// use mixtape_core::Agent;
178 ///
179 /// // Interactive mode: prompts user for tools without grants
180 /// let agent = Agent::builder()
181 /// .bedrock(ClaudeSonnet4_5)
182 /// .interactive()
183 /// .build()
184 /// .await?;
185 /// ```
186 ///
187 /// This can be combined with a grant store for hybrid behavior:
188 ///
189 /// ```ignore
190 /// use mixtape_core::{Agent, FileGrantStore};
191 ///
192 /// // Pre-approved tools run immediately, others prompt user
193 /// let agent = Agent::builder()
194 /// .bedrock(ClaudeSonnet4_5)
195 /// .interactive()
196 /// .with_grant_store(FileGrantStore::new("./grants.json"))
197 /// .build()
198 /// .await?;
199 /// ```
200 pub fn interactive(mut self) -> Self {
201 self.authorization_policy = ToolAuthorizationPolicy::Interactive;
202 self
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_builder_authorization_timeout() {
212 let timeout = Duration::from_secs(60);
213 let builder = Agent::builder().with_authorization_timeout(timeout);
214 assert_eq!(builder.authorization_timeout, timeout);
215 }
216
217 #[test]
218 fn test_builder_grant_store() {
219 use crate::permission::MemoryGrantStore;
220 let builder = Agent::builder().with_grant_store(MemoryGrantStore::new());
221 assert!(builder.grant_store.is_some());
222 }
223}