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 prefer: self.agent.prefer.clone(),
40 conversations: ConversationLimits {
41 max: self.agent.max_conversations,
42 idle: Duration::from_secs(self.agent.conversation_idle_seconds),
43 },
44 delegation: None,
45 max_background: self.agent.max_background,
46 background: None,
47 chat_attach: None,
48 chat_history: None,
49 }
50 }
51
52 pub(crate) fn episode_gap(&self) -> Duration {
55 Duration::from_secs(self.history.episode_gap_minutes.saturating_mul(60))
56 }
57
58 pub(crate) fn archive_dir(&self) -> std::path::PathBuf {
61 self.history
62 .archive_dir
63 .clone()
64 .unwrap_or_else(|| self.layout().history())
65 }
66
67 pub(crate) fn web_tools(&self) -> Option<WebToolsConfig> {
70 if !self.web.enabled {
71 return None;
72 }
73 let search = match self.web.search {
74 WebSearchMode::Off | WebSearchMode::Provider => None,
75 WebSearchMode::Searxng => self
76 .web
77 .searxng_url
78 .clone()
79 .map(|url| SearchBackend::Searxng { url }),
80 WebSearchMode::Brave => {
81 let api_key = self
82 .web
83 .brave_api_key
84 .clone()
85 .or_else(|| {
86 self.web
87 .brave_api_key_env
88 .as_deref()
89 .and_then(|name| std::env::var(name).ok())
90 .map(scv_client::Secret::from)
91 })
92 .filter(|key| !key.trim().is_empty());
93 if api_key.is_none() {
94 tracing::warn!(
95 "web.search is \"brave\" but no Brave API key is configured; web_search is unavailable"
96 );
97 }
98 api_key.map(|api_key| SearchBackend::Brave {
99 url: self.web.brave_url.clone(),
100 api_key: api_key.into_inner(),
101 })
102 }
103 };
104 Some(WebToolsConfig {
105 fetch_max_bytes: self.web.fetch_max_bytes,
106 fetch_timeout: Duration::from_secs(self.web.fetch_timeout_seconds),
107 max_redirects: self.web.max_redirects,
108 auto_approve_domains: self.web.auto_approve_domains.clone(),
109 allow_private_addresses: self.web.allow_private_addresses,
110 search,
111 max_search_results: self.web.max_search_results,
112 output_limit: self.tools.output_limit_bytes,
113 })
114 }
115
116 pub fn hosted_web_search(&self) -> bool {
118 self.web.enabled && self.web.search == WebSearchMode::Provider
119 }
120
121 pub(crate) fn provider_limits(&self) -> ProviderLimits {
122 ProviderLimits {
123 max_sse_event_bytes: self.provider_limits.max_sse_event_bytes,
124 max_response_bytes: self.provider_limits.max_response_bytes,
125 max_assistant_bytes: self.provider_limits.max_assistant_bytes,
126 max_tool_calls: self.provider_limits.max_tool_calls,
127 max_tool_arguments_bytes: self.provider_limits.max_tool_arguments_bytes,
128 max_retries: self.provider_limits.max_retries,
129 ..ProviderLimits::default()
130 }
131 }
132
133 pub fn adapters(&self) -> HashMap<String, AgentAdapterConfig> {
134 let user_home = dirs::home_dir();
135 self.agents
136 .0
137 .iter()
138 .filter_map(|(name, config)| {
139 let descriptor = scv_tools::adapters::adapter(name)?;
140 let adapter_home = self.layout().agent_home(name);
141 let mut environment = vec![
142 (OsString::from("SCV_HOME"), adapter_home.clone().into()),
143 (OsString::from("HOME"), adapter_home.clone().into()),
144 (
145 OsString::from("XDG_CONFIG_HOME"),
146 adapter_home.join("config").into(),
147 ),
148 (
149 OsString::from("XDG_DATA_HOME"),
150 adapter_home.join("data").into(),
151 ),
152 (
153 OsString::from("XDG_STATE_HOME"),
154 adapter_home.join("state").into(),
155 ),
156 ];
157 for (variable, relative) in descriptor.home_environment {
158 let path = if relative.is_empty() {
159 adapter_home.clone()
160 } else {
161 adapter_home.join(relative)
162 };
163 environment.push((OsString::from(variable), path.into()));
164 }
165 let full = config.permissions == AgentPermissions::Full;
166 environment.extend(
167 descriptor
168 .fixed_environment
169 .iter()
170 .chain(
171 descriptor
172 .full_permission_environment
173 .iter()
174 .filter(|_| full),
175 )
176 .map(|(variable, value)| (OsString::from(variable), OsString::from(value))),
177 );
178 Some((
179 name.clone(),
180 AgentAdapterConfig {
181 command: config.command.clone(),
182 args: config.args.clone(),
183 prompt_args: config.prompt_args.clone(),
184 full_permission_args: full.then(|| {
185 descriptor
186 .full_permission_args
187 .iter()
188 .map(|arg| (*arg).to_owned())
189 .collect()
190 }),
191 model_args: config.model_args.clone(),
192 effort_args: config.effort_args.clone(),
193 model_hint: descriptor.model_hint.into(),
194 environment,
195 search_dirs: user_home
196 .as_deref()
197 .map(|home| scv_tools::adapters::adapter_search_dirs(descriptor, home))
198 .unwrap_or_default(),
199 output: descriptor.output,
200 resume: descriptor.resume,
201 home: Some(adapter_home),
202 transport: descriptor.transport,
203 acp: descriptor
204 .acp
205 .filter(|_| match config.transport {
206 AgentTransport::Acp => true,
207 AgentTransport::Resume => false,
208 AgentTransport::Auto => config.command == descriptor.command,
211 })
212 .map(|launch| scv_tools::AcpAgentLaunch {
213 command: launch.command.to_owned(),
214 args: scv_tools::adapters::acp_args(&launch, full),
215 full_mode: launch.full_mode.filter(|_| full).map(str::to_owned),
216 environment: launch
217 .full_environment
218 .iter()
219 .filter(|_| full)
220 .map(|(variable, value)| {
221 (OsString::from(variable), OsString::from(value))
222 })
223 .collect(),
224 required: config.transport == AgentTransport::Acp,
225 }),
226 use_for: config.use_for.clone(),
227 model: config.model.clone(),
228 effort: config.effort.clone(),
229 },
230 ))
231 })
232 .collect()
233 }
234
235 pub fn layout(&self) -> Layout {
239 Layout::new(&self.instance_home)
240 }
241
242 pub fn prepare_adapter_homes(&self) -> Result<()> {
243 let layout = self.layout();
244 std::fs::create_dir_all(layout.agents()).context("create the agent homes directory")?;
245 ensure_private_dir(&layout.agents())?;
246 for name in self.agents.0.keys() {
247 let path = layout.agent_home(name);
248 std::fs::create_dir_all(&path)
249 .with_context(|| format!("create isolated {name} agent home"))?;
250 ensure_private_dir(&path)
251 .with_context(|| format!("secure isolated {name} agent home"))?;
252 }
253 Ok(())
254 }
255}