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