agent_air_runtime/controller/tools/
ask_for_permissions.rs1use std::collections::HashMap;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use super::types::{
12 DisplayConfig, DisplayResult, Executable, ResultContentType, ToolContext, ToolType,
13};
14use crate::permissions::{GrantTarget, PermissionLevel, PermissionRegistry, PermissionRequest};
15
16pub const ASK_FOR_PERMISSIONS_TOOL_NAME: &str = "ask_for_permissions";
18
19pub const ASK_FOR_PERMISSIONS_TOOL_DESCRIPTION: &str = "Request permission from the user before performing sensitive actions like file writes, \
21 deletions, network operations, or system commands. The user can grant permission for \
22 this request only (once) or for the remainder of the session.";
23
24pub const ASK_FOR_PERMISSIONS_TOOL_SCHEMA: &str = r#"{
28 "type": "object",
29 "properties": {
30 "target_type": {
31 "type": "string",
32 "enum": ["path", "domain", "command"],
33 "description": "Type of resource: 'path' for files/directories, 'domain' for network endpoints, 'command' for shell commands"
34 },
35 "target": {
36 "type": "string",
37 "description": "The resource being accessed: file path, domain pattern, or command pattern"
38 },
39 "level": {
40 "type": "string",
41 "enum": ["read", "write", "execute", "admin"],
42 "description": "Permission level required: 'read' for viewing, 'write' for modification, 'execute' for running, 'admin' for deletion/full control"
43 },
44 "recursive": {
45 "type": "boolean",
46 "description": "For path targets only: whether to include subdirectories",
47 "default": false
48 },
49 "description": {
50 "type": "string",
51 "description": "Human-readable description of the action requiring permission"
52 },
53 "reason": {
54 "type": "string",
55 "description": "Why this action is needed"
56 }
57 },
58 "required": ["target_type", "target", "level", "description"]
59}"#;
60
61pub struct AskForPermissionsTool {
63 registry: Arc<PermissionRegistry>,
65}
66
67impl AskForPermissionsTool {
68 pub fn new(registry: Arc<PermissionRegistry>) -> Self {
73 Self { registry }
74 }
75}
76
77impl Executable for AskForPermissionsTool {
78 fn name(&self) -> &str {
79 ASK_FOR_PERMISSIONS_TOOL_NAME
80 }
81
82 fn description(&self) -> &str {
83 ASK_FOR_PERMISSIONS_TOOL_DESCRIPTION
84 }
85
86 fn input_schema(&self) -> &str {
87 ASK_FOR_PERMISSIONS_TOOL_SCHEMA
88 }
89
90 fn tool_type(&self) -> ToolType {
91 ToolType::UserInteraction
92 }
93
94 fn execute(
95 &self,
96 context: ToolContext,
97 input: HashMap<String, serde_json::Value>,
98 ) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> {
99 let registry = self.registry.clone();
100
101 Box::pin(async move {
102 let target_type = input
104 .get("target_type")
105 .and_then(|v| v.as_str())
106 .ok_or_else(|| "Missing 'target_type' field".to_string())?;
107
108 let target_value = input
110 .get("target")
111 .and_then(|v| v.as_str())
112 .ok_or_else(|| "Missing 'target' field".to_string())?
113 .to_string();
114
115 let level_str = input
117 .get("level")
118 .and_then(|v| v.as_str())
119 .ok_or_else(|| "Missing 'level' field".to_string())?;
120
121 let level = match level_str {
122 "read" => PermissionLevel::Read,
123 "write" => PermissionLevel::Write,
124 "execute" => PermissionLevel::Execute,
125 "admin" => PermissionLevel::Admin,
126 _ => {
127 return Err(format!(
128 "Invalid level '{}': must be read, write, execute, or admin",
129 level_str
130 ));
131 }
132 };
133
134 let recursive = input
136 .get("recursive")
137 .and_then(|v| v.as_bool())
138 .unwrap_or(false);
139
140 let description = input
142 .get("description")
143 .and_then(|v| v.as_str())
144 .ok_or_else(|| "Missing 'description' field".to_string())?
145 .to_string();
146
147 if description.trim().is_empty() {
149 return Err("Description cannot be empty".to_string());
150 }
151
152 let reason = input
154 .get("reason")
155 .and_then(|v| v.as_str())
156 .map(|s| s.to_string());
157
158 let target = match target_type {
160 "path" => GrantTarget::path(&target_value, recursive),
161 "domain" => GrantTarget::Domain {
162 pattern: target_value,
163 },
164 "command" => GrantTarget::Command {
165 pattern: target_value,
166 },
167 _ => {
168 return Err(format!(
169 "Invalid target_type '{}': must be path, domain, or command",
170 target_type
171 ));
172 }
173 };
174
175 let mut request =
177 PermissionRequest::new(&context.tool_use_id, target, level, &description);
178 if let Some(r) = reason {
179 request = request.with_reason(&r);
180 }
181 request = request.with_tool(ASK_FOR_PERMISSIONS_TOOL_NAME);
182
183 let response_rx = registry
185 .request_permission(context.session_id, request, context.turn_id)
186 .await
187 .map_err(|e| format!("Failed to request permission: {}", e))?;
188
189 let response = response_rx
191 .await
192 .map_err(|_| "User declined to grant permission".to_string())?;
193
194 serde_json::to_string(&response)
196 .map_err(|e| format!("Failed to serialize response: {}", e))
197 })
198 }
199
200 fn display_config(&self) -> DisplayConfig {
201 DisplayConfig {
202 display_name: "Permission Request".to_string(),
203 display_title: Box::new(|input| {
204 let target_type = input
205 .get("target_type")
206 .and_then(|v| v.as_str())
207 .unwrap_or("unknown");
208 let level = input
209 .get("level")
210 .and_then(|v| v.as_str())
211 .unwrap_or("unknown");
212
213 format!(
214 "{} {}",
215 capitalize(level),
216 match target_type {
217 "path" => "Path",
218 "domain" => "Domain",
219 "command" => "Command",
220 _ => "Permission",
221 }
222 )
223 }),
224 display_content: Box::new(|input, _result| {
225 let description = input
226 .get("description")
227 .and_then(|v| v.as_str())
228 .unwrap_or("Unknown action");
229
230 let target = input
231 .get("target")
232 .and_then(|v| v.as_str())
233 .map(|t| format!("\nTarget: {}", t))
234 .unwrap_or_default();
235
236 let reason = input
237 .get("reason")
238 .and_then(|v| v.as_str())
239 .map(|r| format!("\nReason: {}", r))
240 .unwrap_or_default();
241
242 let content = format!("{}{}{}", description, target, reason);
243
244 DisplayResult {
245 content,
246 content_type: ResultContentType::PlainText,
247 is_truncated: false,
248 full_length: 0,
249 }
250 }),
251 }
252 }
253
254 fn compact_summary(&self, input: &HashMap<String, serde_json::Value>, result: &str) -> String {
255 let target_type = input
256 .get("target_type")
257 .and_then(|v| v.as_str())
258 .unwrap_or("unknown");
259
260 let level = input
261 .get("level")
262 .and_then(|v| v.as_str())
263 .unwrap_or("unknown");
264
265 let granted = result.contains("\"granted\":true");
266 let status = if granted { "granted" } else { "denied" };
267
268 format!("[Permission {} {} {}]", level, target_type, status)
269 }
270
271 fn handles_own_permissions(&self) -> bool {
272 true }
274}
275
276fn capitalize(s: &str) -> String {
278 let mut chars = s.chars();
279 match chars.next() {
280 None => String::new(),
281 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_schema_has_required_fields() {
291 let schema: serde_json::Value =
292 serde_json::from_str(ASK_FOR_PERMISSIONS_TOOL_SCHEMA).unwrap();
293
294 let required = schema.get("required").unwrap().as_array().unwrap();
295 assert!(required.contains(&serde_json::Value::String("target_type".to_string())));
296 assert!(required.contains(&serde_json::Value::String("target".to_string())));
297 assert!(required.contains(&serde_json::Value::String("level".to_string())));
298 assert!(required.contains(&serde_json::Value::String("description".to_string())));
299 }
300
301 #[test]
302 fn test_schema_target_type_enum() {
303 let schema: serde_json::Value =
304 serde_json::from_str(ASK_FOR_PERMISSIONS_TOOL_SCHEMA).unwrap();
305
306 let target_type_enum = schema
307 .get("properties")
308 .unwrap()
309 .get("target_type")
310 .unwrap()
311 .get("enum")
312 .unwrap()
313 .as_array()
314 .unwrap();
315
316 assert!(target_type_enum.contains(&serde_json::Value::String("path".to_string())));
317 assert!(target_type_enum.contains(&serde_json::Value::String("domain".to_string())));
318 assert!(target_type_enum.contains(&serde_json::Value::String("command".to_string())));
319 }
320
321 #[test]
322 fn test_schema_level_enum() {
323 let schema: serde_json::Value =
324 serde_json::from_str(ASK_FOR_PERMISSIONS_TOOL_SCHEMA).unwrap();
325
326 let level_enum = schema
327 .get("properties")
328 .unwrap()
329 .get("level")
330 .unwrap()
331 .get("enum")
332 .unwrap()
333 .as_array()
334 .unwrap();
335
336 assert!(level_enum.contains(&serde_json::Value::String("read".to_string())));
337 assert!(level_enum.contains(&serde_json::Value::String("write".to_string())));
338 assert!(level_enum.contains(&serde_json::Value::String("execute".to_string())));
339 assert!(level_enum.contains(&serde_json::Value::String("admin".to_string())));
340 }
341
342 #[test]
343 fn test_capitalize() {
344 assert_eq!(capitalize("read"), "Read");
345 assert_eq!(capitalize("write"), "Write");
346 assert_eq!(capitalize(""), "");
347 assert_eq!(capitalize("ADMIN"), "ADMIN");
348 }
349
350 #[test]
351 fn test_compact_summary_format() {
352 let tool = AskForPermissionsTool::new(Arc::new(PermissionRegistry::new(
353 tokio::sync::mpsc::channel(1).0,
354 )));
355
356 let mut input = HashMap::new();
357 input.insert("target_type".to_string(), serde_json::json!("path"));
358 input.insert("level".to_string(), serde_json::json!("write"));
359
360 let summary = tool.compact_summary(&input, r#"{"granted":true}"#);
361 assert_eq!(summary, "[Permission write path granted]");
362
363 let summary = tool.compact_summary(&input, r#"{"granted":false}"#);
364 assert_eq!(summary, "[Permission write path denied]");
365 }
366}