1use std::{collections::HashMap, ffi::OsString, time::Duration};
4
5use anyhow::{Context, Result};
6use scv_client::Layout;
7use scv_core::{AgentConfig as CoreAgentConfig, HistoryLimits};
8use scv_provider_openai::ProviderLimits;
9use scv_tools::{
10 AgentAdapterConfig, ToolsConfig,
11 conversation::ConversationLimits,
12 web::{SearchBackend, WebToolsConfig},
13};
14
15use super::{AgentPermissions, AgentTransport, Config, WebSearchMode, load::ensure_private_dir};
16
17impl Config {
18 pub(crate) fn core_agent(&self, system_prompt: String) -> CoreAgentConfig {
19 CoreAgentConfig {
20 system_prompt,
21 max_steps: self.agent.max_steps,
22 history_limits: HistoryLimits {
23 max_bytes: self.session.max_history_bytes,
24 max_messages: self.session.max_messages,
25 note_max_chars: self.context.summary_max_chars,
26 },
27 }
28 }
29
30 pub(crate) fn tools(&self) -> ToolsConfig {
31 ToolsConfig {
32 command_timeout: Duration::from_secs(self.tools.command_timeout_seconds),
33 agent_timeout: Duration::from_secs(self.tools.agent_timeout_seconds),
34 max_timeout: Duration::from_secs(self.tools.max_timeout_seconds),
35 output_limit_bytes: self.tools.output_limit_bytes,
36 max_read_bytes: self.tools.max_read_bytes,
37 max_write_bytes: self.tools.max_write_bytes,
38 max_delegation_depth: self.agent.max_delegation_depth,
39 conversations: ConversationLimits {
40 max: self.agent.max_conversations,
41 idle: Duration::from_secs(self.agent.conversation_idle_seconds),
42 },
43 delegation: None,
44 max_background: self.agent.max_background,
45 background: None,
46 chat_attach: None,
47 }
48 }
49
50 pub(crate) fn web_tools(&self) -> Option<WebToolsConfig> {
53 if !self.web.enabled {
54 return None;
55 }
56 let search = match self.web.search {
57 WebSearchMode::Off | WebSearchMode::Provider => None,
58 WebSearchMode::Searxng => self
59 .web
60 .searxng_url
61 .clone()
62 .map(|url| SearchBackend::Searxng { url }),
63 WebSearchMode::Brave => {
64 let api_key = self
65 .web
66 .brave_api_key
67 .clone()
68 .or_else(|| {
69 self.web
70 .brave_api_key_env
71 .as_deref()
72 .and_then(|name| std::env::var(name).ok())
73 .map(scv_client::Secret::from)
74 })
75 .filter(|key| !key.trim().is_empty());
76 if api_key.is_none() {
77 tracing::warn!(
78 "web.search is \"brave\" but no Brave API key is configured; web_search is unavailable"
79 );
80 }
81 api_key.map(|api_key| SearchBackend::Brave {
82 url: self.web.brave_url.clone(),
83 api_key: api_key.into_inner(),
84 })
85 }
86 };
87 Some(WebToolsConfig {
88 fetch_max_bytes: self.web.fetch_max_bytes,
89 fetch_timeout: Duration::from_secs(self.web.fetch_timeout_seconds),
90 max_redirects: self.web.max_redirects,
91 auto_approve_domains: self.web.auto_approve_domains.clone(),
92 allow_private_addresses: self.web.allow_private_addresses,
93 search,
94 max_search_results: self.web.max_search_results,
95 output_limit: self.tools.output_limit_bytes,
96 })
97 }
98
99 pub fn hosted_web_search(&self) -> bool {
101 self.web.enabled && self.web.search == WebSearchMode::Provider
102 }
103
104 pub(crate) fn provider_limits(&self) -> ProviderLimits {
105 ProviderLimits {
106 max_sse_event_bytes: self.provider_limits.max_sse_event_bytes,
107 max_response_bytes: self.provider_limits.max_response_bytes,
108 max_assistant_bytes: self.provider_limits.max_assistant_bytes,
109 max_tool_calls: self.provider_limits.max_tool_calls,
110 max_tool_arguments_bytes: self.provider_limits.max_tool_arguments_bytes,
111 max_retries: self.provider_limits.max_retries,
112 ..ProviderLimits::default()
113 }
114 }
115
116 pub fn adapters(&self) -> HashMap<String, AgentAdapterConfig> {
117 let user_home = dirs::home_dir();
118 self.agents
119 .0
120 .iter()
121 .filter_map(|(name, config)| {
122 let descriptor = scv_tools::adapters::adapter(name)?;
123 let adapter_home = self.layout().agent_home(name);
124 let mut environment = vec![
125 (OsString::from("SCV_HOME"), adapter_home.clone().into()),
126 (OsString::from("HOME"), adapter_home.clone().into()),
127 (
128 OsString::from("XDG_CONFIG_HOME"),
129 adapter_home.join("config").into(),
130 ),
131 (
132 OsString::from("XDG_DATA_HOME"),
133 adapter_home.join("data").into(),
134 ),
135 (
136 OsString::from("XDG_STATE_HOME"),
137 adapter_home.join("state").into(),
138 ),
139 ];
140 for (variable, relative) in descriptor.home_environment {
141 let path = if relative.is_empty() {
142 adapter_home.clone()
143 } else {
144 adapter_home.join(relative)
145 };
146 environment.push((OsString::from(variable), path.into()));
147 }
148 let full = config.permissions == AgentPermissions::Full;
149 environment.extend(
150 descriptor
151 .fixed_environment
152 .iter()
153 .chain(
154 descriptor
155 .full_permission_environment
156 .iter()
157 .filter(|_| full),
158 )
159 .map(|(variable, value)| (OsString::from(variable), OsString::from(value))),
160 );
161 Some((
162 format!("agent_{name}"),
163 AgentAdapterConfig {
164 command: config.command.clone(),
165 args: config.args.clone(),
166 prompt_args: config.prompt_args.clone(),
167 full_permission_args: full.then(|| {
168 descriptor
169 .full_permission_args
170 .iter()
171 .map(|arg| (*arg).to_owned())
172 .collect()
173 }),
174 model_args: config.model_args.clone(),
175 effort_args: config.effort_args.clone(),
176 model_hint: descriptor.model_hint.into(),
177 environment,
178 search_dirs: user_home
179 .as_deref()
180 .map(|home| scv_tools::adapters::adapter_search_dirs(descriptor, home))
181 .unwrap_or_default(),
182 output: descriptor.output,
183 resume: descriptor.resume,
184 home: Some(adapter_home),
185 transport: descriptor.transport,
186 acp: descriptor
187 .acp
188 .filter(|_| match config.transport {
189 AgentTransport::Acp => true,
190 AgentTransport::Resume => false,
191 AgentTransport::Auto => config.command == descriptor.command,
194 })
195 .map(|launch| scv_tools::AcpAgentLaunch {
196 command: launch.command.to_owned(),
197 args: scv_tools::adapters::acp_args(&launch, full),
198 full_mode: launch.full_mode.filter(|_| full).map(str::to_owned),
199 environment: launch
200 .full_environment
201 .iter()
202 .filter(|_| full)
203 .map(|(variable, value)| {
204 (OsString::from(variable), OsString::from(value))
205 })
206 .collect(),
207 required: config.transport == AgentTransport::Acp,
208 }),
209 use_for: config.use_for.clone(),
210 model: config.model.clone(),
211 effort: config.effort.clone(),
212 },
213 ))
214 })
215 .collect()
216 }
217
218 pub fn layout(&self) -> Layout {
222 Layout::new(&self.instance_home)
223 }
224
225 pub fn prepare_adapter_homes(&self) -> Result<()> {
226 let layout = self.layout();
227 std::fs::create_dir_all(layout.agents()).context("create the agent homes directory")?;
228 ensure_private_dir(&layout.agents())?;
229 for name in self.agents.0.keys() {
230 let path = layout.agent_home(name);
231 std::fs::create_dir_all(&path)
232 .with_context(|| format!("create isolated {name} agent home"))?;
233 ensure_private_dir(&path)
234 .with_context(|| format!("secure isolated {name} agent home"))?;
235 }
236 Ok(())
237 }
238}