garudust_core/tool.rs
1use std::{collections::HashMap, sync::Arc};
2
3use async_trait::async_trait;
4use tokio::sync::RwLock;
5
6use crate::{
7 budget::IterationBudget,
8 config::AgentConfig,
9 error::{AgentError, ToolError},
10 memory::MemoryStore,
11 types::{ToolResult, ToolSchema},
12};
13
14/// Accumulated permissions from all skills loaded in the current session.
15/// Each entry maps a tool name to `true` (allowed) or `false` (denied).
16/// Union semantics: `true` from any skill wins over `false` from another.
17/// Tools absent from the map are not restricted by skill permissions.
18#[derive(Debug, Default, Clone)]
19pub struct SkillPermissions(pub HashMap<String, bool>);
20
21impl SkillPermissions {
22 /// Merge another skill's permissions using union semantics (allow wins).
23 pub fn merge(&mut self, other: &HashMap<String, bool>) {
24 for (tool, allowed) in other {
25 let entry = self.0.entry(tool.clone()).or_insert(false);
26 if *allowed {
27 *entry = true;
28 }
29 }
30 }
31
32 /// Returns `Some(false)` only if the tool is explicitly denied by every
33 /// loaded skill that mentions it. Returns `None` if no skill restricts it.
34 pub fn check(&self, tool_name: &str) -> Option<bool> {
35 self.0.get(tool_name).copied()
36 }
37}
38
39#[async_trait]
40pub trait Tool: Send + Sync + 'static {
41 fn name(&self) -> &str;
42 fn description(&self) -> &str;
43 fn schema(&self) -> serde_json::Value;
44 fn toolset(&self) -> &str;
45
46 /// Returns true for tools that write, delete, or execute — i.e. operations
47 /// that are hard to reverse. The registry uses this to gate approval and
48 /// emit an audit-log entry before dispatch, regardless of how the tool
49 /// encodes its arguments internally.
50 fn is_destructive(&self) -> bool {
51 false
52 }
53
54 /// Parameter-aware variant called by the registry. Override this when
55 /// destructiveness depends on the specific arguments (e.g. a terminal tool
56 /// that can also run read-only commands). The default delegates to
57 /// `is_destructive()` so existing tools need no changes.
58 fn is_destructive_for(&self, _params: &serde_json::Value) -> bool {
59 self.is_destructive()
60 }
61
62 /// Return `true` to opt out of the agent's global `tool_timeout_secs` budget.
63 /// Override on tools that manage their own timeout internally (e.g. `Terminal`).
64 fn bypass_dispatch_timeout(&self) -> bool {
65 false
66 }
67
68 /// Returns a conflict key for parallelism safety.
69 /// Tool calls that return the same non-None key are serialized (run one at a time).
70 /// Calls returning None can run concurrently with any other call.
71 /// Override on tools that read from or write to a shared resource.
72 fn parallelism_key(&self, _params: &serde_json::Value) -> Option<String> {
73 None
74 }
75
76 async fn execute(
77 &self,
78 params: serde_json::Value,
79 ctx: &ToolContext,
80 ) -> Result<ToolResult, ToolError>;
81
82 fn to_schema(&self) -> ToolSchema {
83 ToolSchema {
84 name: self.name().to_string(),
85 description: self.description().to_string(),
86 parameters: self.schema(),
87 }
88 }
89}
90
91#[async_trait]
92pub trait SubAgentRunner: Send + Sync + 'static {
93 async fn run_task(&self, task: &str, session_id: &str) -> Result<String, AgentError>;
94}
95
96pub struct ToolContext {
97 pub session_id: String,
98 pub agent_id: String,
99 pub iteration: u32,
100 pub budget: Arc<IterationBudget>,
101 pub memory: Arc<dyn MemoryStore>,
102 pub config: Arc<AgentConfig>,
103 pub approver: Arc<dyn CommandApprover>,
104 pub sub_agent: Option<Arc<dyn SubAgentRunner>>,
105 /// Accumulated permissions from all skills loaded this session via skill_view.
106 /// Shared across all tool dispatches within the same agent turn.
107 pub skill_permissions: Arc<RwLock<SkillPermissions>>,
108 /// Tools required by skills loaded this session (via required_tools frontmatter).
109 /// skill_view appends to this; the agent checks it at end-turn.
110 pub required_tools: Arc<RwLock<Vec<String>>>,
111}
112
113#[async_trait]
114pub trait CommandApprover: Send + Sync + 'static {
115 /// Called by ToolRegistry::dispatch() for every destructive tool before
116 /// execute(). `tool_name` is the registered tool name; `params` is the
117 /// JSON-serialised parameter object passed by the model.
118 async fn approve(&self, tool_name: &str, params: &str) -> ApprovalDecision;
119}
120
121#[derive(Debug, Clone, PartialEq)]
122pub enum ApprovalDecision {
123 Approved,
124 ApprovedAlways,
125 Denied,
126 Yolo,
127}