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}
61
62#[derive(Debug, Clone, Serialize, Deserialize, Default)]
63#[serde(default, deny_unknown_fields)]
64pub struct UpdateConfig {
65 pub index_url: Option<String>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
72#[serde(default, deny_unknown_fields)]
73pub(crate) struct NotifyConfig {
74 pub(crate) owner: Vec<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(default, deny_unknown_fields)]
83pub(crate) struct HistoryConfig {
84 pub(crate) episode_gap_minutes: u64,
87 pub(crate) min_free_percent: u8,
91 pub(crate) archive_dir: Option<PathBuf>,
95}
96
97impl Default for HistoryConfig {
98 fn default() -> Self {
99 Self {
100 episode_gap_minutes: 120,
101 min_free_percent: 20,
102 archive_dir: None,
103 }
104 }
105}
106
107impl Default for ProviderConfig {
108 fn default() -> Self {
109 Self {
110 active: None,
111 kind: "openai-compatible".into(),
112 wire_api: "responses".into(),
113 model: "gpt-4.1-mini".into(),
114 base_url: "https://api.openai.com/v1".into(),
115 api_key: None,
116 api_key_env: Some("OPENAI_API_KEY".into()),
117 timeout_seconds: 600,
118 headers: HashMap::new(),
119 image_input: true,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(default, deny_unknown_fields)]
126pub(crate) struct AgentConfig {
127 pub(crate) max_steps: usize,
128 pub(crate) system_prompt: String,
129 pub(crate) max_delegation_depth: u32,
132 pub(crate) max_conversations: usize,
135 pub(crate) conversation_idle_seconds: u64,
137 pub(crate) max_background: usize,
140 pub(crate) prefer: Vec<String>,
144}
145
146impl Default for AgentConfig {
147 fn default() -> Self {
148 Self {
149 max_steps: 128,
150 max_delegation_depth: 2,
151 max_conversations: 8,
152 conversation_idle_seconds: 86400,
153 max_background: 4,
156 prefer: Vec::new(),
157 system_prompt: "You are SCV, a concise and careful agent. Use tools to inspect, change, and verify.".into(),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(default, deny_unknown_fields)]
164pub(crate) struct SessionConfig {
165 pub(crate) max_history_bytes: usize,
166 pub(crate) max_messages: usize,
167}
168
169impl Default for SessionConfig {
170 fn default() -> Self {
171 Self {
172 max_history_bytes: 16 * 1024 * 1024,
173 max_messages: 10_000,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(default, deny_unknown_fields)]
180pub(crate) struct ContextConfigFile {
181 pub(crate) max_tokens: usize,
182 pub(crate) reserve_output_tokens: usize,
183 pub(crate) safety_margin_tokens: usize,
184 pub(crate) bytes_per_token: usize,
185 pub(crate) summary_max_chars: usize,
186}
187
188impl Default for ContextConfigFile {
189 fn default() -> Self {
190 let value = ContextConfig::default();
191 Self {
192 max_tokens: value.max_tokens,
193 reserve_output_tokens: value.reserve_output_tokens,
194 safety_margin_tokens: value.safety_margin_tokens,
195 bytes_per_token: value.bytes_per_token,
196 summary_max_chars: value.summary_max_chars,
197 }
198 }
199}
200
201impl From<&ContextConfigFile> for ContextConfig {
202 fn from(value: &ContextConfigFile) -> Self {
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
213#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
214#[serde(rename_all = "kebab-case")]
215pub enum ApprovalPolicy {
216 OnRisk,
217 Always,
218 Never,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(default, deny_unknown_fields)]
223pub(crate) struct ToolConfig {
224 pub(crate) approval_policy: ApprovalPolicy,
225 pub(crate) command_timeout_seconds: u64,
227 pub(crate) agent_timeout_seconds: u64,
229 pub(crate) max_timeout_seconds: u64,
231 pub(crate) output_limit_bytes: usize,
232 pub(crate) max_read_bytes: usize,
233 pub(crate) max_write_bytes: usize,
234}
235
236impl Default for ToolConfig {
237 fn default() -> Self {
238 Self {
239 approval_policy: ApprovalPolicy::OnRisk,
240 command_timeout_seconds: 600,
241 agent_timeout_seconds: 3600,
242 max_timeout_seconds: 14400,
243 output_limit_bytes: 64 * 1024,
244 max_read_bytes: 256 * 1024,
245 max_write_bytes: 1024 * 1024,
246 }
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(default, deny_unknown_fields)]
252pub(crate) struct ProtocolConfig {
253 pub(crate) max_client_frame_bytes: usize,
254 pub(crate) max_server_frame_bytes: usize,
255}
256
257impl Default for ProtocolConfig {
258 fn default() -> Self {
259 Self {
260 max_client_frame_bytes: 1024 * 1024,
261 max_server_frame_bytes: 8 * 1024 * 1024,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(default, deny_unknown_fields)]
268pub(crate) struct TuiConfig {
269 pub(crate) max_transcript_bytes: usize,
270 pub(crate) max_transcript_items: usize,
271 pub(crate) max_prompt_history_bytes: usize,
272 pub(crate) max_prompt_history_items: usize,
273}
274
275impl Default for TuiConfig {
276 fn default() -> Self {
277 Self {
278 max_transcript_bytes: 8 * 1024 * 1024,
279 max_transcript_items: 10_000,
280 max_prompt_history_bytes: 1024 * 1024,
281 max_prompt_history_items: 200,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(default, deny_unknown_fields)]
288pub(crate) struct ProviderLimitsFile {
289 pub(crate) max_sse_event_bytes: usize,
290 pub(crate) max_response_bytes: usize,
291 pub(crate) max_assistant_bytes: usize,
292 pub(crate) max_tool_calls: usize,
293 pub(crate) max_tool_arguments_bytes: usize,
294 pub(crate) max_retries: usize,
295}
296
297impl Default for ProviderLimitsFile {
298 fn default() -> Self {
299 let value = ProviderLimits::default();
300 Self {
301 max_sse_event_bytes: value.max_sse_event_bytes,
302 max_response_bytes: value.max_response_bytes,
303 max_assistant_bytes: value.max_assistant_bytes,
304 max_tool_calls: value.max_tool_calls,
305 max_tool_arguments_bytes: value.max_tool_arguments_bytes,
306 max_retries: value.max_retries,
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[serde(default, deny_unknown_fields)]
313pub(crate) struct SkillsConfig {
314 pub(crate) user_dir: PathBuf,
315 pub(crate) project_dir: PathBuf,
316 pub(crate) scan_projects: bool,
319 pub(crate) max_skills: usize,
320 pub(crate) max_skill_bytes: usize,
321}
322
323impl Default for SkillsConfig {
324 fn default() -> Self {
325 Self {
326 user_dir: PathBuf::from("~/.scv/skills"),
327 project_dir: PathBuf::from(".scv/skills"),
328 scan_projects: true,
329 max_skills: 128,
330 max_skill_bytes: 256 * 1024,
331 }
332 }
333}
334
335#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
337#[serde(rename_all = "lowercase")]
338pub(crate) enum WebSearchMode {
339 Off,
340 Provider,
342 Searxng,
343 Brave,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347#[serde(default, deny_unknown_fields)]
348pub(crate) struct WebConfig {
349 pub(crate) enabled: bool,
351 pub(crate) fetch_max_bytes: usize,
352 pub(crate) fetch_timeout_seconds: u64,
353 pub(crate) max_redirects: usize,
354 pub(crate) auto_approve_domains: Vec<String>,
356 pub(crate) allow_private_addresses: bool,
358 pub(crate) search: WebSearchMode,
359 pub(crate) searxng_url: Option<String>,
360 pub(crate) brave_url: String,
361 pub(crate) brave_api_key: Option<Secret>,
362 pub(crate) brave_api_key_env: Option<String>,
363 pub(crate) max_search_results: usize,
364}
365
366impl Default for WebConfig {
367 fn default() -> Self {
368 Self {
369 enabled: true,
370 fetch_max_bytes: 2 * 1024 * 1024,
371 fetch_timeout_seconds: 30,
372 max_redirects: 5,
373 auto_approve_domains: [
374 "docs.rs",
375 "crates.io",
376 "doc.rust-lang.org",
377 "docs.python.org",
378 "pypi.org",
379 "developer.mozilla.org",
380 ]
381 .map(String::from)
382 .to_vec(),
383 allow_private_addresses: false,
384 search: WebSearchMode::Off,
385 searxng_url: None,
386 brave_url: "https://api.search.brave.com/res/v1/web/search".into(),
387 brave_api_key: None,
388 brave_api_key_env: Some("BRAVE_SEARCH_API_KEY".into()),
389 max_search_results: 8,
390 }
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, Default)]
395#[serde(default, deny_unknown_fields)]
396pub(crate) struct AdapterConfig {
397 pub(crate) command: String,
398 pub(crate) args: Vec<String>,
399 pub(crate) permissions: AgentPermissions,
401 pub(crate) prompt_args: Vec<String>,
403 pub(crate) model_args: Vec<String>,
405 pub(crate) effort_args: Vec<String>,
407 pub(crate) transport: AgentTransport,
409 pub(crate) use_for: Option<String>,
412 pub(crate) model: Option<String>,
415 pub(crate) effort: Option<String>,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
421#[serde(rename_all = "lowercase")]
422pub(crate) enum AgentTransport {
423 #[default]
425 Auto,
426 Acp,
428 Resume,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
434#[serde(rename_all = "lowercase")]
435pub(crate) enum AgentPermissions {
436 #[default]
438 Default,
439 Full,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446#[serde(transparent)]
447pub(crate) struct AgentsConfig(pub(crate) BTreeMap<String, AdapterConfig>);
448
449impl Default for AgentsConfig {
450 fn default() -> Self {
451 let strings = |values: &[&str]| values.iter().map(|value| (*value).to_owned()).collect();
452 Self(
453 scv_tools::adapters::ADAPTERS
454 .iter()
455 .map(|adapter| {
456 (
457 adapter.name.to_owned(),
458 AdapterConfig {
459 command: adapter.command.into(),
460 args: strings(adapter.args),
461 permissions: AgentPermissions::Default,
462 prompt_args: strings(adapter.prompt_args),
463 model_args: strings(adapter.model_args),
464 effort_args: strings(adapter.effort_args),
465 transport: AgentTransport::Auto,
466 use_for: None,
467 model: None,
468 effort: None,
469 },
470 )
471 })
472 .collect(),
473 )
474 }
475}
476
477#[derive(Debug, Clone, Default)]
480pub struct ConfigOverrides {
481 pub provider: Option<String>,
482 pub model: Option<String>,
483 pub base_url: Option<String>,
484 pub approval_policy: Option<ApprovalPolicy>,
485 pub no_tools: bool,
486 pub config_file: Option<PathBuf>,
489}