1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! Tool trait definition
use super::error::Result;
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
/// Execution context for tools
#[derive(Clone)]
pub struct ToolExecutionContext {
/// Session ID
pub session_id: Uuid,
/// Working directory
pub working_directory: std::path::PathBuf,
/// Environment variables
pub env_vars: HashMap<String, String>,
/// Whether auto-approve is enabled
pub auto_approve: bool,
/// Maximum execution timeout in seconds
pub timeout_secs: u64,
/// Callback for requesting sudo password from the user (set by TUI)
pub sudo_callback: Option<crate::brain::agent::SudoCallback>,
/// Callback for requesting an SSH password from the user (set by TUI).
/// Wired to the same dialog plumbing as `sudo_callback` but with a
/// different prompt label so the user knows it's an SSH server.
pub ssh_callback: Option<crate::brain::agent::SshPasswordCallback>,
/// Shared working directory handle — tools can mutate this to change the
/// working directory at runtime (e.g. config_manager set_working_directory).
pub shared_working_directory: Option<Arc<std::sync::RwLock<std::path::PathBuf>>>,
/// Service context — tools use this to create SessionService for /usage stats.
pub service_context: Option<crate::services::ServiceContext>,
/// Callback the `follow_up_question` tool uses to render its
/// question with native buttons (Telegram inline keyboard, Discord
/// components, Slack actions, TUI overlay, WhatsApp numbered text)
/// and block until the user picks an option. None on channels that
/// have no interactive surface (A2A) or sessions where the caller
/// did not wire one.
pub question_callback: Option<crate::brain::agent::QuestionCallback>,
}
impl std::fmt::Debug for ToolExecutionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolExecutionContext")
.field("session_id", &self.session_id)
.field("working_directory", &self.working_directory)
.field("auto_approve", &self.auto_approve)
.field("timeout_secs", &self.timeout_secs)
.field("sudo_callback", &self.sudo_callback.is_some())
.field("ssh_callback", &self.ssh_callback.is_some())
.finish()
}
}
impl ToolExecutionContext {
/// Create a new execution context
pub fn new(session_id: Uuid) -> Self {
Self {
session_id,
working_directory: std::env::current_dir().unwrap_or_default(),
env_vars: HashMap::new(),
auto_approve: false,
timeout_secs: 120,
sudo_callback: None,
ssh_callback: None,
shared_working_directory: None,
service_context: None,
question_callback: None,
}
}
/// Set working directory
pub fn with_working_directory(mut self, dir: std::path::PathBuf) -> Self {
self.working_directory = dir;
self
}
/// Set auto-approve
pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
self.auto_approve = auto_approve;
self
}
/// Set timeout
pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = timeout_secs;
self
}
/// Get the effective working directory.
///
/// Reads from the shared lock if available (so `/cd` changes are visible
/// to tools in the same iteration), falling back to the plain field.
pub fn working_dir(&self) -> std::path::PathBuf {
if let Some(ref shared) = self.shared_working_directory {
shared
.read()
.ok()
.map(|g| (*g).clone())
.unwrap_or_else(|| self.working_directory.clone())
} else {
self.working_directory.clone()
}
}
}
/// Tool result
#[derive(Debug, Clone)]
pub struct ToolResult {
/// Whether the execution was successful
pub success: bool,
/// Output from the tool
pub output: String,
/// Error message if unsuccessful
pub error: Option<String>,
/// Additional metadata
pub metadata: HashMap<String, String>,
/// Optional images to include alongside the text result.
/// Each entry is (media_type, base64_data) — e.g. ("image/png", "<base64>").
/// These are sent as ContentBlock::Image blocks following the ToolResult.
pub images: Vec<(String, String)>,
}
impl ToolResult {
/// Create a successful result
pub fn success(output: String) -> Self {
Self {
success: true,
output,
error: None,
metadata: HashMap::new(),
images: Vec::new(),
}
}
/// Create an error result
pub fn error(error: String) -> Self {
Self {
success: false,
output: String::new(),
error: Some(error),
metadata: HashMap::new(),
images: Vec::new(),
}
}
/// Attach images to the result (sent as ContentBlock::Image alongside the tool result).
pub fn with_images(mut self, images: Vec<(String, String)>) -> Self {
self.images = images;
self
}
/// Add metadata
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
/// Tool capability flags
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCapability {
/// Can read files
ReadFiles,
/// Can write files
WriteFiles,
/// Can execute shell commands
ExecuteShell,
/// Can access network
Network,
/// Can modify system state
SystemModification,
/// Can manage plans and tasks
PlanManagement,
}
/// Tool trait - defines an executable tool
#[async_trait]
pub trait Tool: Send + Sync {
/// Get the tool name
fn name(&self) -> &str;
/// Get the tool description
fn description(&self) -> &str;
/// Get the input schema (JSON Schema format)
fn input_schema(&self) -> Value;
/// Get the tool's capabilities
fn capabilities(&self) -> Vec<ToolCapability>;
/// Check if the tool requires approval before execution
fn requires_approval(&self) -> bool {
// By default, dangerous tools require approval
let dangerous_capabilities = [
ToolCapability::WriteFiles,
ToolCapability::ExecuteShell,
ToolCapability::SystemModification,
];
self.capabilities()
.iter()
.any(|cap| dangerous_capabilities.contains(cap))
}
/// Check if this specific invocation requires approval.
/// Override for tools where only certain operations need approval (e.g. plan finalize).
fn requires_approval_for_input(&self, _input: &Value) -> bool {
self.requires_approval()
}
/// Execute the tool with given input
async fn execute(&self, input: Value, context: &ToolExecutionContext) -> Result<ToolResult>;
/// Validate input before execution
fn validate_input(&self, _input: &Value) -> Result<()> {
// Default implementation - no validation
Ok(())
}
}