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::{AgentTransport, Config, PolicyMode, 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 mode = self.agent_mode(name);
174 let full = mode == PolicyMode::Auto;
175 environment.extend(
176 descriptor
177 .fixed_environment
178 .iter()
179 .chain(
180 descriptor
181 .full_permission_environment
182 .iter()
183 .filter(|_| full),
184 )
185 .map(|(variable, value)| (OsString::from(variable), OsString::from(value))),
186 );
187 Some((
188 name.clone(),
189 AgentAdapterConfig {
190 command: config.command.clone(),
191 args: if descriptor.transport == scv_tools::adapters::Transport::ScvProtocol
192 {
193 nested_scv_args(&config.args)
194 } else {
195 config.args.clone()
196 },
197 prompt_args: config.prompt_args.clone(),
198 full_permission_args: full.then(|| {
199 descriptor
200 .full_permission_args
201 .iter()
202 .map(|arg| (*arg).to_owned())
203 .collect()
204 }),
205 policy: mode.as_str().into(),
206 mode_report: descriptor.mode_report,
207 mode_switch: descriptor.mode_switch(),
208 model_args: config.model_args.clone(),
209 effort_args: config.effort_args.clone(),
210 model_hint: descriptor.model_hint.into(),
211 environment,
212 search_dirs: user_home
213 .as_deref()
214 .map(|home| scv_tools::adapters::adapter_search_dirs(descriptor, home))
215 .unwrap_or_default(),
216 output: descriptor.output,
217 resume: descriptor.resume,
218 home: Some(adapter_home),
219 auth: match descriptor.auth {
220 scv_tools::adapters::AgentAuth::None => AgentAuthPlan::None,
221 mode => {
222 let settings = match scv_tools::claude_auth::claude_settings(
223 mode,
224 &self.layout().agent_home(name),
225 ) {
226 Ok(Some(settings)) => settings,
227 Ok(None) => return None,
228 Err(error) => {
229 tracing::error!(
230 agent = %name,
231 %error,
232 "cannot build pinned Claude Code settings"
233 );
234 return None;
235 }
236 };
237 AgentAuthPlan::Claude {
238 mode,
239 settings,
240 home: self.layout().agent_home(name),
241 }
242 }
243 },
244 transport: descriptor.transport,
245 acp: config.acp_launch(descriptor).map(|launch| {
246 scv_tools::AcpAgentLaunch {
247 command: launch.command.to_owned(),
248 args: scv_tools::adapters::acp_args(&launch, full),
249 full_mode: launch.full_mode.map(str::to_owned),
250 environment: launch
251 .full_environment
252 .iter()
253 .filter(|_| full)
254 .map(|(variable, value)| {
255 (OsString::from(variable), OsString::from(value))
256 })
257 .collect(),
258 required: config.transport == AgentTransport::Acp
261 || !descriptor.cli_turns,
262 session_options: launch.session_options,
263 }
264 }),
265 use_for: config.use_for.clone(),
266 defaults: config.defaults(),
267 options_file: Some(self.layout().agent_options(name)),
268 busy: BusyConfig {
269 behavior: config.on_busy.unwrap_or(self.agent.on_busy),
270 steer_fallback: config
271 .steer_fallback
272 .unwrap_or(self.agent.steer_fallback),
273 max_queued_turns: config
274 .max_queued_turns
275 .unwrap_or(self.agent.max_queued_turns),
276 },
277 },
278 ))
279 })
280 .collect()
281 }
282
283 pub(crate) fn policy(&self) -> crate::approval::Policy {
286 crate::approval::Policy {
287 mode: self.policy.mode,
288 }
289 }
290
291 pub fn policy_mode(&self) -> PolicyMode {
293 self.policy.mode
294 }
295
296 pub fn agent_mode(&self, name: &str) -> PolicyMode {
299 self.agents
300 .0
301 .get(name)
302 .and_then(|agent| agent.mode)
303 .unwrap_or(self.policy.mode)
304 }
305
306 pub fn agent_modes(&self) -> Vec<(String, PolicyMode)> {
308 self.agents
309 .0
310 .iter()
311 .filter_map(|(name, agent)| Some((name.clone(), agent.mode?)))
312 .collect()
313 }
314
315 pub fn publish_crate_versions_per_day(&self) -> u64 {
318 self.policy.publish_crate_versions_per_day
319 }
320
321 pub fn deprecated(&self) -> &[super::Deprecated] {
323 &self.deprecated
324 }
325
326 pub fn layout(&self) -> Layout {
330 Layout::new(&self.instance_home)
331 }
332
333 pub fn prepare_adapter_homes(&self) -> Result<()> {
334 let layout = self.layout();
335 std::fs::create_dir_all(layout.agents()).context("create the agent homes directory")?;
336 ensure_private_dir(&layout.agents())?;
337 for name in self.agents.0.keys() {
338 let path = layout.agent_home(name);
339 std::fs::create_dir_all(&path)
340 .with_context(|| format!("create isolated {name} agent home"))?;
341 ensure_private_dir(&path)
342 .with_context(|| format!("secure isolated {name} agent home"))?;
343 }
344 Ok(())
345 }
346}
347
348fn nested_scv_args(args: &[String]) -> Vec<String> {
353 let mut kept = Vec::with_capacity(args.len() + 2);
354 let mut mode = PolicyMode::OnRisk;
355 let mut args = args.iter();
356 while let Some(arg) = args.next() {
357 let value = match arg.split_once('=') {
358 Some(("--policy" | "--approval-policy", value)) => value,
359 _ if matches!(arg.as_str(), "--policy" | "--approval-policy") => {
360 args.next().map_or("", String::as_str)
361 }
362 _ => {
363 kept.push(arg.clone());
364 continue;
365 }
366 };
367 let named = match value {
368 "always" => PolicyMode::Always,
369 "read-only" | "never" => PolicyMode::ReadOnly,
370 _ => PolicyMode::OnRisk,
371 };
372 if named.strictness() > mode.strictness() {
373 mode = named;
374 }
375 }
376 kept.extend(["--policy".to_owned(), mode.as_str().to_owned()]);
377 kept
378}