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) policy: PolicyConfig,
28 pub(crate) tools: ToolConfig,
29 pub(crate) protocol: ProtocolConfig,
30 pub(crate) tui: TuiConfig,
31 pub update: UpdateConfig,
32 pub(crate) notify: NotifyConfig,
33 pub(crate) history: HistoryConfig,
34 pub(crate) provider_limits: ProviderLimitsFile,
35 pub(crate) skills: SkillsConfig,
36 pub(crate) agents: AgentsConfig,
37 pub(crate) web: WebConfig,
38 pub(crate) channels: BTreeMap<String, BTreeMap<String, AccountSettings>>,
42 #[serde(skip)]
44 pub(crate) instance_home: PathBuf,
45 #[serde(skip)]
47 pub(crate) deprecated: Vec<super::Deprecated>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(default, deny_unknown_fields)]
52pub struct ProviderConfig {
53 pub(crate) active: Option<String>,
54 pub kind: String,
55 pub wire_api: String,
56 pub model: String,
57 pub base_url: String,
58 pub api_key: Option<Secret>,
59 pub api_key_env: Option<String>,
60 pub timeout_seconds: u64,
61 pub headers: HashMap<String, Secret>,
63 pub(crate) image_input: bool,
67 pub reasoning_effort: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, Default)]
74#[serde(default, deny_unknown_fields)]
75pub struct UpdateConfig {
76 pub index_url: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
83#[serde(default, deny_unknown_fields)]
84pub(crate) struct NotifyConfig {
85 pub(crate) owner: Vec<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(default, deny_unknown_fields)]
94pub(crate) struct HistoryConfig {
95 pub(crate) episode_gap_minutes: u64,
98 pub(crate) min_free_percent: u8,
102 pub(crate) archive_dir: Option<PathBuf>,
106}
107
108impl Default for HistoryConfig {
109 fn default() -> Self {
110 Self {
111 episode_gap_minutes: 120,
112 min_free_percent: 20,
113 archive_dir: None,
114 }
115 }
116}
117
118impl Default for ProviderConfig {
119 fn default() -> Self {
120 Self {
121 active: None,
122 kind: "openai-compatible".into(),
123 wire_api: "responses".into(),
124 model: "gpt-4.1-mini".into(),
125 base_url: "https://api.openai.com/v1".into(),
126 api_key: None,
127 api_key_env: Some("OPENAI_API_KEY".into()),
128 timeout_seconds: 600,
129 headers: HashMap::new(),
130 image_input: true,
131 reasoning_effort: None,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(default, deny_unknown_fields)]
138pub(crate) struct AgentConfig {
139 pub(crate) max_steps: usize,
140 pub(crate) system_prompt: String,
141 pub(crate) max_delegation_depth: u32,
144 pub(crate) max_conversations: usize,
147 pub(crate) conversation_idle_seconds: u64,
149 pub(crate) max_background: usize,
152 pub(crate) on_busy: BusyBehavior,
153 pub(crate) steer_fallback: BusyBehavior,
154 pub(crate) max_queued_turns: usize,
155 pub(crate) prefer: Vec<String>,
159}
160
161impl Default for AgentConfig {
162 fn default() -> Self {
163 Self {
164 max_steps: 128,
165 max_delegation_depth: 2,
166 max_conversations: 8,
167 conversation_idle_seconds: 86400,
168 max_background: 4,
171 on_busy: BusyBehavior::Queue,
172 steer_fallback: BusyBehavior::Queue,
173 max_queued_turns: 4,
174 prefer: Vec::new(),
175 system_prompt: "You are SCV, a concise and careful agent. Use tools to inspect, change, and verify.".into(),
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(default, deny_unknown_fields)]
182pub(crate) struct SessionConfig {
183 pub(crate) max_history_bytes: usize,
184 pub(crate) max_messages: usize,
185}
186
187impl Default for SessionConfig {
188 fn default() -> Self {
189 Self {
190 max_history_bytes: 16 * 1024 * 1024,
191 max_messages: 10_000,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(default, deny_unknown_fields)]
198pub(crate) struct ContextConfigFile {
199 pub(crate) max_tokens: usize,
200 pub(crate) reserve_output_tokens: usize,
201 pub(crate) safety_margin_tokens: usize,
202 pub(crate) bytes_per_token: usize,
203 pub(crate) summary_max_chars: usize,
204}
205
206impl Default for ContextConfigFile {
207 fn default() -> Self {
208 let value = ContextConfig::default();
209 Self {
210 max_tokens: value.max_tokens,
211 reserve_output_tokens: value.reserve_output_tokens,
212 safety_margin_tokens: value.safety_margin_tokens,
213 bytes_per_token: value.bytes_per_token,
214 summary_max_chars: value.summary_max_chars,
215 }
216 }
217}
218
219impl From<&ContextConfigFile> for ContextConfig {
220 fn from(value: &ContextConfigFile) -> Self {
221 Self {
222 max_tokens: value.max_tokens,
223 reserve_output_tokens: value.reserve_output_tokens,
224 safety_margin_tokens: value.safety_margin_tokens,
225 bytes_per_token: value.bytes_per_token,
226 summary_max_chars: value.summary_max_chars,
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
235#[serde(rename_all = "kebab-case")]
236pub enum PolicyMode {
237 #[default]
240 Auto,
241 OnRisk,
244 Always,
246 ReadOnly,
248}
249
250impl PolicyMode {
251 pub fn as_str(self) -> &'static str {
253 match self {
254 Self::Auto => "auto",
255 Self::OnRisk => "on-risk",
256 Self::Always => "always",
257 Self::ReadOnly => "read-only",
258 }
259 }
260}
261
262impl std::fmt::Display for PolicyMode {
263 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 formatter.write_str(self.as_str())
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(default, deny_unknown_fields)]
273pub(crate) struct PolicyConfig {
274 pub(crate) mode: PolicyMode,
275 pub(crate) publish_crate_versions_per_day: u64,
278}
279
280impl Default for PolicyConfig {
281 fn default() -> Self {
282 Self {
283 mode: PolicyMode::Auto,
284 publish_crate_versions_per_day: 20,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(default, deny_unknown_fields)]
291pub(crate) struct ToolConfig {
292 pub(crate) allow_no_timeout: bool,
295 pub(crate) command_timeout_seconds: u64,
297 pub(crate) agent_timeout_seconds: u64,
299 pub(crate) max_timeout_seconds: u64,
301 pub(crate) output_limit_bytes: usize,
302 pub(crate) max_read_bytes: usize,
303 pub(crate) max_write_bytes: usize,
304}
305
306impl Default for ToolConfig {
307 fn default() -> Self {
308 Self {
309 allow_no_timeout: false,
310 command_timeout_seconds: 600,
311 agent_timeout_seconds: 3600,
312 max_timeout_seconds: 14400,
313 output_limit_bytes: 64 * 1024,
314 max_read_bytes: 256 * 1024,
315 max_write_bytes: 1024 * 1024,
316 }
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(default, deny_unknown_fields)]
322pub(crate) struct ProtocolConfig {
323 pub(crate) max_client_frame_bytes: usize,
324 pub(crate) max_server_frame_bytes: usize,
325}
326
327impl Default for ProtocolConfig {
328 fn default() -> Self {
329 Self {
330 max_client_frame_bytes: 1024 * 1024,
331 max_server_frame_bytes: 8 * 1024 * 1024,
332 }
333 }
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[serde(default, deny_unknown_fields)]
338pub(crate) struct TuiConfig {
339 pub(crate) max_transcript_bytes: usize,
340 pub(crate) max_transcript_items: usize,
341 pub(crate) max_prompt_history_bytes: usize,
342 pub(crate) max_prompt_history_items: usize,
343 pub(crate) keys: BTreeMap<String, String>,
348}
349
350impl Default for TuiConfig {
351 fn default() -> Self {
352 Self {
353 max_transcript_bytes: 8 * 1024 * 1024,
354 max_transcript_items: 10_000,
355 max_prompt_history_bytes: 1024 * 1024,
356 max_prompt_history_items: 200,
357 keys: BTreeMap::new(),
358 }
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
363#[serde(default, deny_unknown_fields)]
364pub(crate) struct ProviderLimitsFile {
365 pub(crate) max_sse_event_bytes: usize,
366 pub(crate) max_response_bytes: usize,
367 pub(crate) max_assistant_bytes: usize,
368 pub(crate) max_tool_calls: usize,
369 pub(crate) max_tool_arguments_bytes: usize,
370 pub(crate) max_retries: usize,
371}
372
373impl Default for ProviderLimitsFile {
374 fn default() -> Self {
375 let value = ProviderLimits::default();
376 Self {
377 max_sse_event_bytes: value.max_sse_event_bytes,
378 max_response_bytes: value.max_response_bytes,
379 max_assistant_bytes: value.max_assistant_bytes,
380 max_tool_calls: value.max_tool_calls,
381 max_tool_arguments_bytes: value.max_tool_arguments_bytes,
382 max_retries: value.max_retries,
383 }
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(default, deny_unknown_fields)]
389pub(crate) struct SkillsConfig {
390 pub(crate) user_dir: PathBuf,
391 pub(crate) project_dir: PathBuf,
392 pub(crate) scan_projects: bool,
395 pub(crate) max_skills: usize,
396 pub(crate) max_skill_bytes: usize,
397}
398
399impl Default for SkillsConfig {
400 fn default() -> Self {
401 Self {
402 user_dir: PathBuf::from("~/.scv/skills"),
403 project_dir: PathBuf::from(".scv/skills"),
404 scan_projects: true,
405 max_skills: 128,
406 max_skill_bytes: 256 * 1024,
407 }
408 }
409}
410
411#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
413#[serde(rename_all = "lowercase")]
414pub(crate) enum WebSearchMode {
415 Off,
416 Provider,
418 Searxng,
419 Brave,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
423#[serde(default, deny_unknown_fields)]
424pub(crate) struct WebConfig {
425 pub(crate) enabled: bool,
427 pub(crate) fetch_max_bytes: usize,
428 pub(crate) fetch_timeout_seconds: u64,
429 pub(crate) max_redirects: usize,
430 pub(crate) auto_approve_domains: Vec<String>,
432 pub(crate) allow_private_addresses: bool,
434 pub(crate) search: WebSearchMode,
435 pub(crate) searxng_url: Option<String>,
436 pub(crate) brave_url: String,
437 pub(crate) brave_api_key: Option<Secret>,
438 pub(crate) brave_api_key_env: Option<String>,
439 pub(crate) max_search_results: usize,
440}
441
442impl Default for WebConfig {
443 fn default() -> Self {
444 Self {
445 enabled: true,
446 fetch_max_bytes: 2 * 1024 * 1024,
447 fetch_timeout_seconds: 30,
448 max_redirects: 5,
449 auto_approve_domains: [
450 "docs.rs",
451 "crates.io",
452 "doc.rust-lang.org",
453 "docs.python.org",
454 "pypi.org",
455 "developer.mozilla.org",
456 ]
457 .map(String::from)
458 .to_vec(),
459 allow_private_addresses: false,
460 search: WebSearchMode::Off,
461 searxng_url: None,
462 brave_url: "https://api.search.brave.com/res/v1/web/search".into(),
463 brave_api_key: None,
464 brave_api_key_env: Some("BRAVE_SEARCH_API_KEY".into()),
465 max_search_results: 8,
466 }
467 }
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, Default)]
471#[serde(default, deny_unknown_fields)]
472pub(crate) struct AdapterConfig {
473 pub(crate) command: String,
474 pub(crate) args: Vec<String>,
475 #[serde(skip_serializing_if = "Option::is_none")]
478 pub(crate) mode: Option<PolicyMode>,
479 pub(crate) prompt_args: Vec<String>,
481 pub(crate) model_args: Vec<String>,
483 pub(crate) effort_args: Vec<String>,
485 pub(crate) transport: AgentTransport,
487 pub(crate) use_for: Option<String>,
490 pub(crate) model: Option<String>,
492 pub(crate) effort: Option<String>,
494 pub(crate) hard_task_effort: Option<String>,
496 pub(crate) on_busy: Option<BusyBehavior>,
497 pub(crate) steer_fallback: Option<BusyBehavior>,
498 pub(crate) max_queued_turns: Option<usize>,
499}
500
501impl AdapterConfig {
502 pub(crate) fn defaults(&self) -> scv_tools::AgentDefaults {
504 scv_tools::AgentDefaults {
505 model: self.model.clone(),
506 effort: self.effort.clone(),
507 hard_task_effort: self.hard_task_effort.clone(),
508 }
509 }
510
511 pub(crate) fn acp_launch(
516 &self,
517 descriptor: &scv_tools::adapters::AdapterDescriptor,
518 ) -> Option<scv_tools::adapters::AcpLaunch> {
519 descriptor.acp.filter(|_| match self.transport {
520 AgentTransport::Acp => true,
521 AgentTransport::Resume => false,
522 AgentTransport::Auto => self.command == descriptor.command || !descriptor.cli_turns,
524 })
525 }
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
530#[serde(rename_all = "lowercase")]
531pub(crate) enum AgentTransport {
532 #[default]
534 Auto,
535 Acp,
537 Resume,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(transparent)]
544pub(crate) struct AgentsConfig(pub(crate) BTreeMap<String, AdapterConfig>);
545
546impl Default for AgentsConfig {
547 fn default() -> Self {
548 let strings = |values: &[&str]| values.iter().map(|value| (*value).to_owned()).collect();
549 Self(
550 scv_tools::adapters::ADAPTERS
551 .iter()
552 .map(|adapter| {
553 (
554 adapter.name.to_owned(),
555 AdapterConfig {
556 command: adapter.command.into(),
557 args: strings(adapter.args),
558 mode: None,
559 prompt_args: strings(adapter.prompt_args),
560 model_args: strings(adapter.model_args),
561 effort_args: strings(adapter.effort_args),
562 transport: AgentTransport::Auto,
563 use_for: None,
564 model: None,
565 effort: None,
566 hard_task_effort: None,
567 on_busy: None,
568 steer_fallback: None,
569 max_queued_turns: None,
570 },
571 )
572 })
573 .collect(),
574 )
575 }
576}
577
578#[derive(Debug, Clone, Default)]
581pub struct ConfigOverrides {
582 pub provider: Option<String>,
583 pub model: Option<String>,
584 pub base_url: Option<String>,
585 pub mode: Option<PolicyMode>,
587 pub no_tools: bool,
588 pub config_file: Option<PathBuf>,
591}