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) allow_no_timeout: bool,
240 pub(crate) command_timeout_seconds: u64,
242 pub(crate) agent_timeout_seconds: u64,
244 pub(crate) max_timeout_seconds: u64,
246 pub(crate) output_limit_bytes: usize,
247 pub(crate) max_read_bytes: usize,
248 pub(crate) max_write_bytes: usize,
249}
250
251impl Default for ToolConfig {
252 fn default() -> Self {
253 Self {
254 approval_policy: ApprovalPolicy::OnRisk,
255 allow_no_timeout: false,
256 command_timeout_seconds: 600,
257 agent_timeout_seconds: 3600,
258 max_timeout_seconds: 14400,
259 output_limit_bytes: 64 * 1024,
260 max_read_bytes: 256 * 1024,
261 max_write_bytes: 1024 * 1024,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(default, deny_unknown_fields)]
268pub(crate) struct ProtocolConfig {
269 pub(crate) max_client_frame_bytes: usize,
270 pub(crate) max_server_frame_bytes: usize,
271}
272
273impl Default for ProtocolConfig {
274 fn default() -> Self {
275 Self {
276 max_client_frame_bytes: 1024 * 1024,
277 max_server_frame_bytes: 8 * 1024 * 1024,
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(default, deny_unknown_fields)]
284pub(crate) struct TuiConfig {
285 pub(crate) max_transcript_bytes: usize,
286 pub(crate) max_transcript_items: usize,
287 pub(crate) max_prompt_history_bytes: usize,
288 pub(crate) max_prompt_history_items: usize,
289 pub(crate) keys: BTreeMap<String, String>,
294}
295
296impl Default for TuiConfig {
297 fn default() -> Self {
298 Self {
299 max_transcript_bytes: 8 * 1024 * 1024,
300 max_transcript_items: 10_000,
301 max_prompt_history_bytes: 1024 * 1024,
302 max_prompt_history_items: 200,
303 keys: BTreeMap::new(),
304 }
305 }
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(default, deny_unknown_fields)]
310pub(crate) struct ProviderLimitsFile {
311 pub(crate) max_sse_event_bytes: usize,
312 pub(crate) max_response_bytes: usize,
313 pub(crate) max_assistant_bytes: usize,
314 pub(crate) max_tool_calls: usize,
315 pub(crate) max_tool_arguments_bytes: usize,
316 pub(crate) max_retries: usize,
317}
318
319impl Default for ProviderLimitsFile {
320 fn default() -> Self {
321 let value = ProviderLimits::default();
322 Self {
323 max_sse_event_bytes: value.max_sse_event_bytes,
324 max_response_bytes: value.max_response_bytes,
325 max_assistant_bytes: value.max_assistant_bytes,
326 max_tool_calls: value.max_tool_calls,
327 max_tool_arguments_bytes: value.max_tool_arguments_bytes,
328 max_retries: value.max_retries,
329 }
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[serde(default, deny_unknown_fields)]
335pub(crate) struct SkillsConfig {
336 pub(crate) user_dir: PathBuf,
337 pub(crate) project_dir: PathBuf,
338 pub(crate) scan_projects: bool,
341 pub(crate) max_skills: usize,
342 pub(crate) max_skill_bytes: usize,
343}
344
345impl Default for SkillsConfig {
346 fn default() -> Self {
347 Self {
348 user_dir: PathBuf::from("~/.scv/skills"),
349 project_dir: PathBuf::from(".scv/skills"),
350 scan_projects: true,
351 max_skills: 128,
352 max_skill_bytes: 256 * 1024,
353 }
354 }
355}
356
357#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
359#[serde(rename_all = "lowercase")]
360pub(crate) enum WebSearchMode {
361 Off,
362 Provider,
364 Searxng,
365 Brave,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369#[serde(default, deny_unknown_fields)]
370pub(crate) struct WebConfig {
371 pub(crate) enabled: bool,
373 pub(crate) fetch_max_bytes: usize,
374 pub(crate) fetch_timeout_seconds: u64,
375 pub(crate) max_redirects: usize,
376 pub(crate) auto_approve_domains: Vec<String>,
378 pub(crate) allow_private_addresses: bool,
380 pub(crate) search: WebSearchMode,
381 pub(crate) searxng_url: Option<String>,
382 pub(crate) brave_url: String,
383 pub(crate) brave_api_key: Option<Secret>,
384 pub(crate) brave_api_key_env: Option<String>,
385 pub(crate) max_search_results: usize,
386}
387
388impl Default for WebConfig {
389 fn default() -> Self {
390 Self {
391 enabled: true,
392 fetch_max_bytes: 2 * 1024 * 1024,
393 fetch_timeout_seconds: 30,
394 max_redirects: 5,
395 auto_approve_domains: [
396 "docs.rs",
397 "crates.io",
398 "doc.rust-lang.org",
399 "docs.python.org",
400 "pypi.org",
401 "developer.mozilla.org",
402 ]
403 .map(String::from)
404 .to_vec(),
405 allow_private_addresses: false,
406 search: WebSearchMode::Off,
407 searxng_url: None,
408 brave_url: "https://api.search.brave.com/res/v1/web/search".into(),
409 brave_api_key: None,
410 brave_api_key_env: Some("BRAVE_SEARCH_API_KEY".into()),
411 max_search_results: 8,
412 }
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, Default)]
417#[serde(default, deny_unknown_fields)]
418pub(crate) struct AdapterConfig {
419 pub(crate) command: String,
420 pub(crate) args: Vec<String>,
421 pub(crate) permissions: AgentPermissions,
423 pub(crate) prompt_args: Vec<String>,
425 pub(crate) model_args: Vec<String>,
427 pub(crate) effort_args: Vec<String>,
429 pub(crate) transport: AgentTransport,
431 pub(crate) use_for: Option<String>,
434 pub(crate) model: Option<String>,
436 pub(crate) effort: Option<String>,
438 pub(crate) hard_task_effort: Option<String>,
440 pub(crate) on_busy: Option<BusyBehavior>,
441 pub(crate) steer_fallback: Option<BusyBehavior>,
442 pub(crate) max_queued_turns: Option<usize>,
443}
444
445impl AdapterConfig {
446 pub(crate) fn defaults(&self) -> scv_tools::AgentDefaults {
448 scv_tools::AgentDefaults {
449 model: self.model.clone(),
450 effort: self.effort.clone(),
451 hard_task_effort: self.hard_task_effort.clone(),
452 }
453 }
454
455 pub(crate) fn acp_launch(
460 &self,
461 descriptor: &scv_tools::adapters::AdapterDescriptor,
462 ) -> Option<scv_tools::adapters::AcpLaunch> {
463 descriptor.acp.filter(|_| match self.transport {
464 AgentTransport::Acp => true,
465 AgentTransport::Resume => false,
466 AgentTransport::Auto => self.command == descriptor.command || !descriptor.cli_turns,
468 })
469 }
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
474#[serde(rename_all = "lowercase")]
475pub(crate) enum AgentTransport {
476 #[default]
478 Auto,
479 Acp,
481 Resume,
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
487#[serde(rename_all = "lowercase")]
488pub(crate) enum AgentPermissions {
489 #[default]
491 Default,
492 Full,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499#[serde(transparent)]
500pub(crate) struct AgentsConfig(pub(crate) BTreeMap<String, AdapterConfig>);
501
502impl Default for AgentsConfig {
503 fn default() -> Self {
504 let strings = |values: &[&str]| values.iter().map(|value| (*value).to_owned()).collect();
505 Self(
506 scv_tools::adapters::ADAPTERS
507 .iter()
508 .map(|adapter| {
509 (
510 adapter.name.to_owned(),
511 AdapterConfig {
512 command: adapter.command.into(),
513 args: strings(adapter.args),
514 permissions: AgentPermissions::Default,
515 prompt_args: strings(adapter.prompt_args),
516 model_args: strings(adapter.model_args),
517 effort_args: strings(adapter.effort_args),
518 transport: AgentTransport::Auto,
519 use_for: None,
520 model: None,
521 effort: None,
522 hard_task_effort: None,
523 on_busy: None,
524 steer_fallback: None,
525 max_queued_turns: None,
526 },
527 )
528 })
529 .collect(),
530 )
531 }
532}
533
534#[derive(Debug, Clone, Default)]
537pub struct ConfigOverrides {
538 pub provider: Option<String>,
539 pub model: Option<String>,
540 pub base_url: Option<String>,
541 pub approval_policy: Option<ApprovalPolicy>,
542 pub no_tools: bool,
543 pub config_file: Option<PathBuf>,
546}