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