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
//! Tool execution context and MCP pool wiring.
use super::*;
impl Engine {
pub(super) fn build_tool_context(&self, mode: AppMode, auto_approve: bool) -> ToolContext {
// Load the per-workspace trusted-paths list (#29) on every tool-context
// build. Cheap (a small JSON file) and always reflects the latest
// `/trust add` / `/trust remove` mutations without an explicit cache
// refresh hook.
let trusted = crate::workspace_trust::WorkspaceTrust::load_for(&self.session.workspace);
let mut trusted_paths = trusted.paths().to_vec();
for root in crate::skills::trusted_skill_roots(&self.session.workspace) {
if !trusted_paths
.iter()
.any(|existing| crate::tools::spec::path_has_prefix(existing, &root))
{
trusted_paths.push(root);
}
}
let mut ctx = ToolContext::with_auto_approve(
self.session.workspace.clone(),
self.session.trust_mode,
self.session.notes_path.clone(),
self.session.mcp_config_path.clone(),
mode == AppMode::Yolo || auto_approve,
)
.with_state_namespace(self.session.id.clone())
.with_features(self.config.features.clone())
.with_shell_manager(self.runtime_ext().shell_manager.clone())
.with_runtime_services(self.config_ext().runtime_services.clone())
.with_cancel_token(self.cancel_token.clone())
.with_trusted_external_paths(trusted_paths);
// Hand the user-memory path to tools so the model-callable
// `remember` tool can append entries (#489). `None` when the
// feature is disabled — tools short-circuit on that.
if self.config.memory_enabled {
ctx.memory_path = Some(self.config.memory_path.clone());
}
if let Some(decider) = self.config_ext().network_policy.as_ref() {
ctx = ctx.with_network_policy(decider.clone());
}
// Wire the search provider so the web_search tool uses the configured backend.
ctx = ctx.with_search_config(
self.config_ext().search_provider.clone(),
self.config_ext().search_api_key.clone(),
);
// Wire the large-output router (#548). Only attaches when the
// [workshop] config table is present; sub-agents don't inherit the
// router (their ToolContext is built separately) to prevent recursive
// routing of the synthesis call itself.
if let Some(workshop_cfg) = self.config_ext().workshop.as_ref()
&& let Some(vars_arc) = self.runtime_ext().workshop_vars.as_ref()
{
let router =
crate::tools::large_output_router::LargeOutputRouter::new(workshop_cfg.clone());
ctx = ctx.with_large_output_router(router, vars_arc.clone());
}
// Wire the external sandbox backend (#516). exec_shell checks this
// field and routes commands through the backend instead of spawning
// a local process when it's set.
if let Some(backend) = self.sandbox.backend() {
ctx = ctx.with_sandbox_backend(std::sync::Arc::clone(backend));
}
if self.config.scratchpad.enabled {
ctx = ctx.with_audit_scratchpad_run_id(self.scratchpad_run_id.clone());
}
ctx = ctx.with_subagent_default_step_timeout_ms(
self.config.subagent_step_timeout.as_millis() as u64,
);
// Sub-agents clone this ToolContext via `SubAgentRuntime::child_runtime`;
// wire LSP here so `diagnostics` and post-edit hooks work in child turns.
let lsp_manager = &self.runtime_ext().lsp_manager;
if lsp_manager.config().enabled {
ctx = ctx.with_lsp_manager(std::sync::Arc::clone(lsp_manager));
}
match mode {
// Plan mode is read-only investigation; the shell tool is not
// registered, so leaving the sandbox policy at the seatbelt-strict
// default is fine.
AppMode::Plan => ctx,
// Agent registers the shell tool and runs each command through
// the per-mode sandbox + per-tool approval flow. The sandbox
// default would deny all outbound network — including DNS —
// which breaks ordinary developer commands (cargo fetch, npm
// install, curl, yt-dlp, …) without buying the user any safety
// the approval flow doesn't already provide. Elevate to
// workspace-write + network. (#273)
AppMode::Agent => {
ctx.with_elevated_sandbox_policy(crate::sandbox::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![self.session.workspace.clone()],
network_access: true,
exclude_tmpdir: false,
exclude_slash_tmp: false,
})
}
// YOLO is the explicit "no guardrails" mode — auto-approve all
// tools, trust mode on, no sandbox. Workspace-write was still
// intercepting commands that wanted to write outside the
// workspace (rare but legitimate: pipx install, npm install
// -g, brew, package-manager state under ~/.cache, sub-agent
// workspaces, …) which forced approval round-trips and
// contradicts the YOLO contract. The user opted into YOLO
// deliberately; trust them.
AppMode::Yolo => {
ctx.with_elevated_sandbox_policy(crate::sandbox::SandboxPolicy::DangerFullAccess)
}
}
}
pub(super) async fn ensure_mcp_pool(&mut self) -> Result<Arc<AsyncMutex<McpPool>>, ToolError> {
if let Some(pool) = self.runtime_ext().mcp_pool.clone() {
return Ok(pool);
}
let pool = if let Some(shared) = crate::mcp_shared::shared_mcp_pool() {
shared
} else {
let network_policy = self.config_ext().network_policy.clone();
let mut pool =
McpPool::from_config_path(&self.session.mcp_config_path).map_err(|e| {
ToolError::execution_failed(format!("Failed to load MCP config: {e}"))
})?;
if let Some(decider) = network_policy.as_ref() {
pool = pool.with_network_policy(decider.clone());
}
Arc::new(AsyncMutex::new(pool))
};
self.runtime_ext_mut().mcp_pool = Some(Arc::clone(&pool));
Ok(pool)
}
pub(super) async fn mcp_tools(&mut self) -> Vec<Tool> {
let pool = match self.ensure_mcp_pool().await {
Ok(pool) => pool,
Err(err) => {
let _ = self.tx_event.send(Event::status(err.to_string())).await;
return Vec::new();
}
};
let mut pool = pool.lock().await;
let errors = pool.connect_all().await;
for (server, err) in errors {
let _ = self
.tx_event
.send(Event::status(format!(
"Failed to connect MCP server '{server}': {err}"
)))
.await;
}
pool.to_api_tools()
}
}