1use std::{
4 collections::{BTreeMap, HashMap},
5 path::PathBuf,
6};
7
8use scv_channels::state::AccountSettings;
9use scv_client::Secret;
10use scv_core::ContextConfig;
11use scv_provider_openai::ProviderLimits;
12use scv_tools::BusyBehavior;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16#[serde(default, deny_unknown_fields)]
17pub struct Config {
18 pub provider: ProviderConfig,
19 pub(crate) providers: HashMap<String, ProviderConfig>,
21 pub(crate) provider_active: Option<String>,
22 pub(crate) agent: AgentConfig,
23 pub(crate) session: SessionConfig,
24 pub(crate) context: ContextConfigFile,
25 pub(crate) tools: ToolConfig,
26 pub(crate) protocol: ProtocolConfig,
27 pub(crate) tui: TuiConfig,
28 pub update: UpdateConfig,
29 pub(crate) notify: NotifyConfig,
30 pub(crate) history: HistoryConfig,
31 pub(crate) provider_limits: ProviderLimitsFile,
32 pub(crate) skills: SkillsConfig,
33 pub(crate) agents: AgentsConfig,
34 pub(crate) web: WebConfig,
35 pub(crate) channels: BTreeMap<String, BTreeMap<String, AccountSettings>>,
39 #[serde(skip)]
41 pub(crate) instance_home: PathBuf,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(default, deny_unknown_fields)]
46pub struct ProviderConfig {
47 pub(crate) active: Option<String>,
48 pub kind: String,
49 pub wire_api: String,
50 pub model: String,
51 pub base_url: String,
52 pub api_key: Option<Secret>,
53 pub api_key_env: Option<String>,
54 pub timeout_seconds: u64,
55 pub headers: HashMap<String, Secret>,
57 pub(crate) image_input: bool,
61 pub reasoning_effort: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default)]
68#[serde(default, deny_unknown_fields)]
69pub struct UpdateConfig {
70 pub index_url: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Default)]
77#[serde(default, deny_unknown_fields)]
78pub(crate) struct NotifyConfig {
79 pub(crate) owner: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(default, deny_unknown_fields)]
88pub(crate) struct HistoryConfig {
89 pub(crate) episode_gap_minutes: u64,
92 pub(crate) min_free_percent: u8,
96 pub(crate) archive_dir: Option<PathBuf>,
100}
101
102impl Default for HistoryConfig {
103 fn default() -> Self {
104 Self {
105 episode_gap_minutes: 120,
106 min_free_percent: 20,
107 archive_dir: None,
108 }
109 }
110}
111
112impl Default for ProviderConfig {
113 fn default() -> Self {
114 Self {
115 active: None,
116 kind: "openai-compatible".into(),
117 wire_api: "responses".into(),
118 model: "gpt-4.1-mini".into(),
119 base_url: "https://api.openai.com/v1".into(),
120 api_key: None,
121 api_key_env: Some("OPENAI_API_KEY".into()),
122 timeout_seconds: 600,
123 headers: HashMap::new(),
124 image_input: true,
125 reasoning_effort: None,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(default, deny_unknown_fields)]
132pub(crate) struct AgentConfig {
133 pub(crate) max_steps: usize,
134 pub(crate) system_prompt: String,
135 pub(crate) max_delegation_depth: u32,
138 pub(crate) max_conversations: usize,
141 pub(crate) conversation_idle_seconds: u64,
143 pub(crate) max_background: usize,
146 pub(crate) on_busy: BusyBehavior,
147 pub(crate) steer_fallback: BusyBehavior,
148 pub(crate) max_queued_turns: usize,
149 pub(crate) prefer: Vec<String>,
153}
154
155impl Default for AgentConfig {
156 fn default() -> Self {
157 Self {
158 max_steps: 128,
159 max_delegation_depth: 2,
160 max_conversations: 8,
161 conversation_idle_seconds: 86400,
162 max_background: 4,
165 on_busy: BusyBehavior::Queue,
166 steer_fallback: BusyBehavior::Queue,
167 max_queued_turns: 4,
168 prefer: Vec::new(),
169 system_prompt: "You are SCV, a concise and careful agent. Use tools to inspect, change, and verify.".into(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(default, deny_unknown_fields)]
176pub(crate) struct SessionConfig {
177 pub(crate) max_history_bytes: usize,
178 pub(crate) max_messages: usize,
179}
180
181impl Default for SessionConfig {
182 fn default() -> Self {
183 Self {
184 max_history_bytes: 16 * 1024 * 1024,
185 max_messages: 10_000,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(default, deny_unknown_fields)]
192pub(crate) struct ContextConfigFile {
193 pub(crate) max_tokens: usize,
194 pub(crate) reserve_output_tokens: usize,
195 pub(crate) safety_margin_tokens: usize,
196 pub(crate) bytes_per_token: usize,
197 pub(crate) summary_max_chars: usize,
198}
199
200impl Default for ContextConfigFile {
201 fn default() -> Self {
202 let value = ContextConfig::default();
203 Self {
204 max_tokens: value.max_tokens,
205 reserve_output_tokens: value.reserve_output_tokens,
206 safety_margin_tokens: value.safety_margin_tokens,
207 bytes_per_token: value.bytes_per_token,
208 summary_max_chars: value.summary_max_chars,
209 }
210 }
211}
212
213impl From<&ContextConfigFile> for ContextConfig {
214 fn from(value: &ContextConfigFile) -> Self {
215 Self {
216 max_tokens: value.max_tokens,
217 reserve_output_tokens: value.reserve_output_tokens,
218 safety_margin_tokens: value.safety_margin_tokens,
219 bytes_per_token: value.bytes_per_token,
220 summary_max_chars: value.summary_max_chars,
221 }
222 }
223}
224
225#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
226#[serde(rename_all = "kebab-case")]
227pub enum ApprovalPolicy {
228 OnRisk,
229 Always,
230 Never,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(default, deny_unknown_fields)]
235pub(crate) struct ToolConfig {
236 pub(crate) approval_policy: ApprovalPolicy,
237 pub(crate) command_timeout_seconds: u64,
239 pub(crate) agent_timeout_seconds: u64,
241 pub(crate) max_timeout_seconds: u64,
243 pub(crate) output_limit_bytes: usize,
244 pub(crate) max_read_bytes: usize,
245 pub(crate) max_write_bytes: usize,
246}
247
248impl Default for ToolConfig {
249 fn default() -> Self {
250 Self {
251 approval_policy: ApprovalPolicy::OnRisk,
252 command_timeout_seconds: 600,
253 agent_timeout_seconds: 3600,
254 max_timeout_seconds: 14400,
255 output_limit_bytes: 64 * 1024,
256 max_read_bytes: 256 * 1024,
257 max_write_bytes: 1024 * 1024,
258 }
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(default, deny_unknown_fields)]
264pub(crate) struct ProtocolConfig {
265 pub(crate) max_client_frame_bytes: usize,
266 pub(crate) max_server_frame_bytes: usize,
267}
268
269impl Default for ProtocolConfig {
270 fn default() -> Self {
271 Self {
272 max_client_frame_bytes: 1024 * 1024,
273 max_server_frame_bytes: 8 * 1024 * 1024,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279#[serde(default, deny_unknown_fields)]
280pub(crate) struct TuiConfig {
281 pub(crate) max_transcript_bytes: usize,
282 pub(crate) max_transcript_items: usize,
283 pub(crate) max_prompt_history_bytes: usize,
284 pub(crate) max_prompt_history_items: usize,
285}
286
287impl Default for TuiConfig {
288 fn default() -> Self {
289 Self {
290 max_transcript_bytes: 8 * 1024 * 1024,
291 max_transcript_items: 10_000,
292 max_prompt_history_bytes: 1024 * 1024,
293 max_prompt_history_items: 200,
294 }
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(default, deny_unknown_fields)]
300pub(crate) struct ProviderLimitsFile {
301 pub(crate) max_sse_event_bytes: usize,
302 pub(crate) max_response_bytes: usize,
303 pub(crate) max_assistant_bytes: usize,
304 pub(crate) max_tool_calls: usize,
305 pub(crate) max_tool_arguments_bytes: usize,
306 pub(crate) max_retries: usize,
307}
308
309impl Default for ProviderLimitsFile {
310 fn default() -> Self {
311 let value = ProviderLimits::default();
312 Self {
313 max_sse_event_bytes: value.max_sse_event_bytes,
314 max_response_bytes: value.max_response_bytes,
315 max_assistant_bytes: value.max_assistant_bytes,
316 max_tool_calls: value.max_tool_calls,
317 max_tool_arguments_bytes: value.max_tool_arguments_bytes,
318 max_retries: value.max_retries,
319 }
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(default, deny_unknown_fields)]
325pub(crate) struct SkillsConfig {
326 pub(crate) user_dir: PathBuf,
327 pub(crate) project_dir: PathBuf,
328 pub(crate) scan_projects: bool,
331 pub(crate) max_skills: usize,
332 pub(crate) max_skill_bytes: usize,
333}
334
335impl Default for SkillsConfig {
336 fn default() -> Self {
337 Self {
338 user_dir: PathBuf::from("~/.scv/skills"),
339 project_dir: PathBuf::from(".scv/skills"),
340 scan_projects: true,
341 max_skills: 128,
342 max_skill_bytes: 256 * 1024,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
349#[serde(rename_all = "lowercase")]
350pub(crate) enum WebSearchMode {
351 Off,
352 Provider,
354 Searxng,
355 Brave,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(default, deny_unknown_fields)]
360pub(crate) struct WebConfig {
361 pub(crate) enabled: bool,
363 pub(crate) fetch_max_bytes: usize,
364 pub(crate) fetch_timeout_seconds: u64,
365 pub(crate) max_redirects: usize,
366 pub(crate) auto_approve_domains: Vec<String>,
368 pub(crate) allow_private_addresses: bool,
370 pub(crate) search: WebSearchMode,
371 pub(crate) searxng_url: Option<String>,
372 pub(crate) brave_url: String,
373 pub(crate) brave_api_key: Option<Secret>,
374 pub(crate) brave_api_key_env: Option<String>,
375 pub(crate) max_search_results: usize,
376}
377
378impl Default for WebConfig {
379 fn default() -> Self {
380 Self {
381 enabled: true,
382 fetch_max_bytes: 2 * 1024 * 1024,
383 fetch_timeout_seconds: 30,
384 max_redirects: 5,
385 auto_approve_domains: [
386 "docs.rs",
387 "crates.io",
388 "doc.rust-lang.org",
389 "docs.python.org",
390 "pypi.org",
391 "developer.mozilla.org",
392 ]
393 .map(String::from)
394 .to_vec(),
395 allow_private_addresses: false,
396 search: WebSearchMode::Off,
397 searxng_url: None,
398 brave_url: "https://api.search.brave.com/res/v1/web/search".into(),
399 brave_api_key: None,
400 brave_api_key_env: Some("BRAVE_SEARCH_API_KEY".into()),
401 max_search_results: 8,
402 }
403 }
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, Default)]
407#[serde(default, deny_unknown_fields)]
408pub(crate) struct AdapterConfig {
409 pub(crate) command: String,
410 pub(crate) args: Vec<String>,
411 pub(crate) permissions: AgentPermissions,
413 pub(crate) prompt_args: Vec<String>,
415 pub(crate) model_args: Vec<String>,
417 pub(crate) effort_args: Vec<String>,
419 pub(crate) transport: AgentTransport,
421 pub(crate) use_for: Option<String>,
424 pub(crate) model: Option<String>,
426 pub(crate) effort: Option<String>,
428 pub(crate) hard_task_effort: Option<String>,
430 pub(crate) on_busy: Option<BusyBehavior>,
431 pub(crate) steer_fallback: Option<BusyBehavior>,
432 pub(crate) max_queued_turns: Option<usize>,
433}
434
435impl AdapterConfig {
436 pub(crate) fn defaults(&self) -> scv_tools::AgentDefaults {
438 scv_tools::AgentDefaults {
439 model: self.model.clone(),
440 effort: self.effort.clone(),
441 hard_task_effort: self.hard_task_effort.clone(),
442 }
443 }
444
445 pub(crate) fn acp_launch(
450 &self,
451 descriptor: &scv_tools::adapters::AdapterDescriptor,
452 ) -> Option<scv_tools::adapters::AcpLaunch> {
453 descriptor.acp.filter(|_| match self.transport {
454 AgentTransport::Acp => true,
455 AgentTransport::Resume => false,
456 AgentTransport::Auto => self.command == descriptor.command,
457 })
458 }
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
463#[serde(rename_all = "lowercase")]
464pub(crate) enum AgentTransport {
465 #[default]
467 Auto,
468 Acp,
470 Resume,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
476#[serde(rename_all = "lowercase")]
477pub(crate) enum AgentPermissions {
478 #[default]
480 Default,
481 Full,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488#[serde(transparent)]
489pub(crate) struct AgentsConfig(pub(crate) BTreeMap<String, AdapterConfig>);
490
491impl Default for AgentsConfig {
492 fn default() -> Self {
493 let strings = |values: &[&str]| values.iter().map(|value| (*value).to_owned()).collect();
494 Self(
495 scv_tools::adapters::ADAPTERS
496 .iter()
497 .map(|adapter| {
498 (
499 adapter.name.to_owned(),
500 AdapterConfig {
501 command: adapter.command.into(),
502 args: strings(adapter.args),
503 permissions: AgentPermissions::Default,
504 prompt_args: strings(adapter.prompt_args),
505 model_args: strings(adapter.model_args),
506 effort_args: strings(adapter.effort_args),
507 transport: AgentTransport::Auto,
508 use_for: None,
509 model: None,
510 effort: None,
511 hard_task_effort: None,
512 on_busy: None,
513 steer_fallback: None,
514 max_queued_turns: None,
515 },
516 )
517 })
518 .collect(),
519 )
520 }
521}
522
523#[derive(Debug, Clone, Default)]
526pub struct ConfigOverrides {
527 pub provider: Option<String>,
528 pub model: Option<String>,
529 pub base_url: Option<String>,
530 pub approval_policy: Option<ApprovalPolicy>,
531 pub no_tools: bool,
532 pub config_file: Option<PathBuf>,
535}