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