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