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