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}