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) provider_limits: ProviderLimitsFile,
30 pub(crate) skills: SkillsConfig,
31 pub(crate) agents: AgentsConfig,
32 pub(crate) web: WebConfig,
33 pub(crate) channels: BTreeMap<String, BTreeMap<String, AccountSettings>>,
37 #[serde(skip)]
39 pub(crate) instance_home: PathBuf,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(default, deny_unknown_fields)]
44pub struct ProviderConfig {
45 pub(crate) active: Option<String>,
46 pub kind: String,
47 pub wire_api: String,
48 pub model: String,
49 pub base_url: String,
50 pub api_key: Option<Secret>,
51 pub api_key_env: Option<String>,
52 pub timeout_seconds: u64,
53 pub headers: HashMap<String, Secret>,
55 pub(crate) image_input: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62#[serde(default, deny_unknown_fields)]
63pub struct UpdateConfig {
64 pub index_url: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
71#[serde(default, deny_unknown_fields)]
72pub(crate) struct NotifyConfig {
73 pub(crate) owner: Vec<String>,
77}
78
79impl Default for ProviderConfig {
80 fn default() -> Self {
81 Self {
82 active: None,
83 kind: "openai-compatible".into(),
84 wire_api: "responses".into(),
85 model: "gpt-4.1-mini".into(),
86 base_url: "https://api.openai.com/v1".into(),
87 api_key: None,
88 api_key_env: Some("OPENAI_API_KEY".into()),
89 timeout_seconds: 600,
90 headers: HashMap::new(),
91 image_input: true,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(default, deny_unknown_fields)]
98pub(crate) struct AgentConfig {
99 pub(crate) max_steps: usize,
100 pub(crate) system_prompt: String,
101 pub(crate) max_delegation_depth: u32,
104 pub(crate) max_conversations: usize,
107 pub(crate) conversation_idle_seconds: u64,
109 pub(crate) max_background: usize,
112 pub(crate) prefer: Vec<String>,
116}
117
118impl Default for AgentConfig {
119 fn default() -> Self {
120 Self {
121 max_steps: 128,
122 max_delegation_depth: 2,
123 max_conversations: 8,
124 conversation_idle_seconds: 86400,
125 max_background: 4,
128 prefer: Vec::new(),
129 system_prompt: "You are SCV, a concise and careful agent. Use tools to inspect, change, and verify.".into(),
130 }
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(default, deny_unknown_fields)]
136pub(crate) struct SessionConfig {
137 pub(crate) max_history_bytes: usize,
138 pub(crate) max_messages: usize,
139}
140
141impl Default for SessionConfig {
142 fn default() -> Self {
143 Self {
144 max_history_bytes: 16 * 1024 * 1024,
145 max_messages: 10_000,
146 }
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(default, deny_unknown_fields)]
152pub(crate) struct ContextConfigFile {
153 pub(crate) max_tokens: usize,
154 pub(crate) reserve_output_tokens: usize,
155 pub(crate) safety_margin_tokens: usize,
156 pub(crate) bytes_per_token: usize,
157 pub(crate) summary_max_chars: usize,
158}
159
160impl Default for ContextConfigFile {
161 fn default() -> Self {
162 let value = ContextConfig::default();
163 Self {
164 max_tokens: value.max_tokens,
165 reserve_output_tokens: value.reserve_output_tokens,
166 safety_margin_tokens: value.safety_margin_tokens,
167 bytes_per_token: value.bytes_per_token,
168 summary_max_chars: value.summary_max_chars,
169 }
170 }
171}
172
173impl From<&ContextConfigFile> for ContextConfig {
174 fn from(value: &ContextConfigFile) -> Self {
175 Self {
176 max_tokens: value.max_tokens,
177 reserve_output_tokens: value.reserve_output_tokens,
178 safety_margin_tokens: value.safety_margin_tokens,
179 bytes_per_token: value.bytes_per_token,
180 summary_max_chars: value.summary_max_chars,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
186#[serde(rename_all = "kebab-case")]
187pub enum ApprovalPolicy {
188 OnRisk,
189 Always,
190 Never,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(default, deny_unknown_fields)]
195pub(crate) struct ToolConfig {
196 pub(crate) approval_policy: ApprovalPolicy,
197 pub(crate) command_timeout_seconds: u64,
199 pub(crate) agent_timeout_seconds: u64,
201 pub(crate) max_timeout_seconds: u64,
203 pub(crate) output_limit_bytes: usize,
204 pub(crate) max_read_bytes: usize,
205 pub(crate) max_write_bytes: usize,
206}
207
208impl Default for ToolConfig {
209 fn default() -> Self {
210 Self {
211 approval_policy: ApprovalPolicy::OnRisk,
212 command_timeout_seconds: 600,
213 agent_timeout_seconds: 3600,
214 max_timeout_seconds: 14400,
215 output_limit_bytes: 64 * 1024,
216 max_read_bytes: 256 * 1024,
217 max_write_bytes: 1024 * 1024,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(default, deny_unknown_fields)]
224pub(crate) struct ProtocolConfig {
225 pub(crate) max_client_frame_bytes: usize,
226 pub(crate) max_server_frame_bytes: usize,
227}
228
229impl Default for ProtocolConfig {
230 fn default() -> Self {
231 Self {
232 max_client_frame_bytes: 1024 * 1024,
233 max_server_frame_bytes: 8 * 1024 * 1024,
234 }
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(default, deny_unknown_fields)]
240pub(crate) struct TuiConfig {
241 pub(crate) max_transcript_bytes: usize,
242 pub(crate) max_transcript_items: usize,
243 pub(crate) max_prompt_history_bytes: usize,
244 pub(crate) max_prompt_history_items: usize,
245}
246
247impl Default for TuiConfig {
248 fn default() -> Self {
249 Self {
250 max_transcript_bytes: 8 * 1024 * 1024,
251 max_transcript_items: 10_000,
252 max_prompt_history_bytes: 1024 * 1024,
253 max_prompt_history_items: 200,
254 }
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259#[serde(default, deny_unknown_fields)]
260pub(crate) struct ProviderLimitsFile {
261 pub(crate) max_sse_event_bytes: usize,
262 pub(crate) max_response_bytes: usize,
263 pub(crate) max_assistant_bytes: usize,
264 pub(crate) max_tool_calls: usize,
265 pub(crate) max_tool_arguments_bytes: usize,
266 pub(crate) max_retries: usize,
267}
268
269impl Default for ProviderLimitsFile {
270 fn default() -> Self {
271 let value = ProviderLimits::default();
272 Self {
273 max_sse_event_bytes: value.max_sse_event_bytes,
274 max_response_bytes: value.max_response_bytes,
275 max_assistant_bytes: value.max_assistant_bytes,
276 max_tool_calls: value.max_tool_calls,
277 max_tool_arguments_bytes: value.max_tool_arguments_bytes,
278 max_retries: value.max_retries,
279 }
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(default, deny_unknown_fields)]
285pub(crate) struct SkillsConfig {
286 pub(crate) user_dir: PathBuf,
287 pub(crate) project_dir: PathBuf,
288 pub(crate) scan_projects: bool,
291 pub(crate) max_skills: usize,
292 pub(crate) max_skill_bytes: usize,
293}
294
295impl Default for SkillsConfig {
296 fn default() -> Self {
297 Self {
298 user_dir: PathBuf::from("~/.scv/skills"),
299 project_dir: PathBuf::from(".scv/skills"),
300 scan_projects: true,
301 max_skills: 128,
302 max_skill_bytes: 256 * 1024,
303 }
304 }
305}
306
307#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "lowercase")]
310pub(crate) enum WebSearchMode {
311 Off,
312 Provider,
314 Searxng,
315 Brave,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(default, deny_unknown_fields)]
320pub(crate) struct WebConfig {
321 pub(crate) enabled: bool,
323 pub(crate) fetch_max_bytes: usize,
324 pub(crate) fetch_timeout_seconds: u64,
325 pub(crate) max_redirects: usize,
326 pub(crate) auto_approve_domains: Vec<String>,
328 pub(crate) allow_private_addresses: bool,
330 pub(crate) search: WebSearchMode,
331 pub(crate) searxng_url: Option<String>,
332 pub(crate) brave_url: String,
333 pub(crate) brave_api_key: Option<Secret>,
334 pub(crate) brave_api_key_env: Option<String>,
335 pub(crate) max_search_results: usize,
336}
337
338impl Default for WebConfig {
339 fn default() -> Self {
340 Self {
341 enabled: true,
342 fetch_max_bytes: 2 * 1024 * 1024,
343 fetch_timeout_seconds: 30,
344 max_redirects: 5,
345 auto_approve_domains: [
346 "docs.rs",
347 "crates.io",
348 "doc.rust-lang.org",
349 "docs.python.org",
350 "pypi.org",
351 "developer.mozilla.org",
352 ]
353 .map(String::from)
354 .to_vec(),
355 allow_private_addresses: false,
356 search: WebSearchMode::Off,
357 searxng_url: None,
358 brave_url: "https://api.search.brave.com/res/v1/web/search".into(),
359 brave_api_key: None,
360 brave_api_key_env: Some("BRAVE_SEARCH_API_KEY".into()),
361 max_search_results: 8,
362 }
363 }
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize, Default)]
367#[serde(default, deny_unknown_fields)]
368pub(crate) struct AdapterConfig {
369 pub(crate) command: String,
370 pub(crate) args: Vec<String>,
371 pub(crate) permissions: AgentPermissions,
373 pub(crate) prompt_args: Vec<String>,
375 pub(crate) model_args: Vec<String>,
377 pub(crate) effort_args: Vec<String>,
379 pub(crate) transport: AgentTransport,
381 pub(crate) use_for: Option<String>,
384 pub(crate) model: Option<String>,
387 pub(crate) effort: Option<String>,
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
393#[serde(rename_all = "lowercase")]
394pub(crate) enum AgentTransport {
395 #[default]
397 Auto,
398 Acp,
400 Resume,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
406#[serde(rename_all = "lowercase")]
407pub(crate) enum AgentPermissions {
408 #[default]
410 Default,
411 Full,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
418#[serde(transparent)]
419pub(crate) struct AgentsConfig(pub(crate) BTreeMap<String, AdapterConfig>);
420
421impl Default for AgentsConfig {
422 fn default() -> Self {
423 let strings = |values: &[&str]| values.iter().map(|value| (*value).to_owned()).collect();
424 Self(
425 scv_tools::adapters::ADAPTERS
426 .iter()
427 .map(|adapter| {
428 (
429 adapter.name.to_owned(),
430 AdapterConfig {
431 command: adapter.command.into(),
432 args: strings(adapter.args),
433 permissions: AgentPermissions::Default,
434 prompt_args: strings(adapter.prompt_args),
435 model_args: strings(adapter.model_args),
436 effort_args: strings(adapter.effort_args),
437 transport: AgentTransport::Auto,
438 use_for: None,
439 model: None,
440 effort: None,
441 },
442 )
443 })
444 .collect(),
445 )
446 }
447}
448
449#[derive(Debug, Clone, Default)]
452pub struct ConfigOverrides {
453 pub provider: Option<String>,
454 pub model: Option<String>,
455 pub base_url: Option<String>,
456 pub approval_policy: Option<ApprovalPolicy>,
457 pub no_tools: bool,
458 pub config_file: Option<PathBuf>,
461}