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>,
115}
116
117impl Default for AgentConfig {
118 fn default() -> Self {
119 Self {
120 max_steps: 128,
121 max_delegation_depth: 2,
122 max_conversations: 8,
123 conversation_idle_seconds: 86400,
124 max_background: 4,
127 prefer: Vec::new(),
128 system_prompt: "You are SCV, a concise and careful agent. Use tools to inspect, change, and verify.".into(),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(default, deny_unknown_fields)]
135pub(crate) struct SessionConfig {
136 pub(crate) max_history_bytes: usize,
137 pub(crate) max_messages: usize,
138}
139
140impl Default for SessionConfig {
141 fn default() -> Self {
142 Self {
143 max_history_bytes: 16 * 1024 * 1024,
144 max_messages: 10_000,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(default, deny_unknown_fields)]
151pub(crate) struct ContextConfigFile {
152 pub(crate) max_tokens: usize,
153 pub(crate) reserve_output_tokens: usize,
154 pub(crate) safety_margin_tokens: usize,
155 pub(crate) bytes_per_token: usize,
156 pub(crate) summary_max_chars: usize,
157}
158
159impl Default for ContextConfigFile {
160 fn default() -> Self {
161 let value = ContextConfig::default();
162 Self {
163 max_tokens: value.max_tokens,
164 reserve_output_tokens: value.reserve_output_tokens,
165 safety_margin_tokens: value.safety_margin_tokens,
166 bytes_per_token: value.bytes_per_token,
167 summary_max_chars: value.summary_max_chars,
168 }
169 }
170}
171
172impl From<&ContextConfigFile> for ContextConfig {
173 fn from(value: &ContextConfigFile) -> Self {
174 Self {
175 max_tokens: value.max_tokens,
176 reserve_output_tokens: value.reserve_output_tokens,
177 safety_margin_tokens: value.safety_margin_tokens,
178 bytes_per_token: value.bytes_per_token,
179 summary_max_chars: value.summary_max_chars,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
185#[serde(rename_all = "kebab-case")]
186pub enum ApprovalPolicy {
187 OnRisk,
188 Always,
189 Never,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(default, deny_unknown_fields)]
194pub(crate) struct ToolConfig {
195 pub(crate) approval_policy: ApprovalPolicy,
196 pub(crate) command_timeout_seconds: u64,
198 pub(crate) agent_timeout_seconds: u64,
200 pub(crate) max_timeout_seconds: u64,
202 pub(crate) output_limit_bytes: usize,
203 pub(crate) max_read_bytes: usize,
204 pub(crate) max_write_bytes: usize,
205}
206
207impl Default for ToolConfig {
208 fn default() -> Self {
209 Self {
210 approval_policy: ApprovalPolicy::OnRisk,
211 command_timeout_seconds: 600,
212 agent_timeout_seconds: 3600,
213 max_timeout_seconds: 14400,
214 output_limit_bytes: 64 * 1024,
215 max_read_bytes: 256 * 1024,
216 max_write_bytes: 1024 * 1024,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(default, deny_unknown_fields)]
223pub(crate) struct ProtocolConfig {
224 pub(crate) max_client_frame_bytes: usize,
225 pub(crate) max_server_frame_bytes: usize,
226}
227
228impl Default for ProtocolConfig {
229 fn default() -> Self {
230 Self {
231 max_client_frame_bytes: 1024 * 1024,
232 max_server_frame_bytes: 8 * 1024 * 1024,
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(default, deny_unknown_fields)]
239pub(crate) struct TuiConfig {
240 pub(crate) max_transcript_bytes: usize,
241 pub(crate) max_transcript_items: usize,
242 pub(crate) max_prompt_history_bytes: usize,
243 pub(crate) max_prompt_history_items: usize,
244}
245
246impl Default for TuiConfig {
247 fn default() -> Self {
248 Self {
249 max_transcript_bytes: 8 * 1024 * 1024,
250 max_transcript_items: 10_000,
251 max_prompt_history_bytes: 1024 * 1024,
252 max_prompt_history_items: 200,
253 }
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258#[serde(default, deny_unknown_fields)]
259pub(crate) struct ProviderLimitsFile {
260 pub(crate) max_sse_event_bytes: usize,
261 pub(crate) max_response_bytes: usize,
262 pub(crate) max_assistant_bytes: usize,
263 pub(crate) max_tool_calls: usize,
264 pub(crate) max_tool_arguments_bytes: usize,
265 pub(crate) max_retries: usize,
266}
267
268impl Default for ProviderLimitsFile {
269 fn default() -> Self {
270 let value = ProviderLimits::default();
271 Self {
272 max_sse_event_bytes: value.max_sse_event_bytes,
273 max_response_bytes: value.max_response_bytes,
274 max_assistant_bytes: value.max_assistant_bytes,
275 max_tool_calls: value.max_tool_calls,
276 max_tool_arguments_bytes: value.max_tool_arguments_bytes,
277 max_retries: value.max_retries,
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(default, deny_unknown_fields)]
284pub(crate) struct SkillsConfig {
285 pub(crate) user_dir: PathBuf,
286 pub(crate) project_dir: PathBuf,
287 pub(crate) scan_projects: bool,
290 pub(crate) max_skills: usize,
291 pub(crate) max_skill_bytes: usize,
292}
293
294impl Default for SkillsConfig {
295 fn default() -> Self {
296 Self {
297 user_dir: PathBuf::from("~/.scv/skills"),
298 project_dir: PathBuf::from(".scv/skills"),
299 scan_projects: true,
300 max_skills: 128,
301 max_skill_bytes: 256 * 1024,
302 }
303 }
304}
305
306#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
308#[serde(rename_all = "lowercase")]
309pub(crate) enum WebSearchMode {
310 Off,
311 Provider,
313 Searxng,
314 Brave,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(default, deny_unknown_fields)]
319pub(crate) struct WebConfig {
320 pub(crate) enabled: bool,
322 pub(crate) fetch_max_bytes: usize,
323 pub(crate) fetch_timeout_seconds: u64,
324 pub(crate) max_redirects: usize,
325 pub(crate) auto_approve_domains: Vec<String>,
327 pub(crate) allow_private_addresses: bool,
329 pub(crate) search: WebSearchMode,
330 pub(crate) searxng_url: Option<String>,
331 pub(crate) brave_url: String,
332 pub(crate) brave_api_key: Option<Secret>,
333 pub(crate) brave_api_key_env: Option<String>,
334 pub(crate) max_search_results: usize,
335}
336
337impl Default for WebConfig {
338 fn default() -> Self {
339 Self {
340 enabled: true,
341 fetch_max_bytes: 2 * 1024 * 1024,
342 fetch_timeout_seconds: 30,
343 max_redirects: 5,
344 auto_approve_domains: [
345 "docs.rs",
346 "crates.io",
347 "doc.rust-lang.org",
348 "docs.python.org",
349 "pypi.org",
350 "developer.mozilla.org",
351 ]
352 .map(String::from)
353 .to_vec(),
354 allow_private_addresses: false,
355 search: WebSearchMode::Off,
356 searxng_url: None,
357 brave_url: "https://api.search.brave.com/res/v1/web/search".into(),
358 brave_api_key: None,
359 brave_api_key_env: Some("BRAVE_SEARCH_API_KEY".into()),
360 max_search_results: 8,
361 }
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, Default)]
366#[serde(default, deny_unknown_fields)]
367pub(crate) struct AdapterConfig {
368 pub(crate) command: String,
369 pub(crate) args: Vec<String>,
370 pub(crate) permissions: AgentPermissions,
372 pub(crate) prompt_args: Vec<String>,
374 pub(crate) model_args: Vec<String>,
376 pub(crate) effort_args: Vec<String>,
378 pub(crate) transport: AgentTransport,
380 pub(crate) use_for: Option<String>,
383 pub(crate) model: Option<String>,
386 pub(crate) effort: Option<String>,
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
392#[serde(rename_all = "lowercase")]
393pub(crate) enum AgentTransport {
394 #[default]
396 Auto,
397 Acp,
399 Resume,
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
405#[serde(rename_all = "lowercase")]
406pub(crate) enum AgentPermissions {
407 #[default]
409 Default,
410 Full,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
417#[serde(transparent)]
418pub(crate) struct AgentsConfig(pub(crate) BTreeMap<String, AdapterConfig>);
419
420impl Default for AgentsConfig {
421 fn default() -> Self {
422 let strings = |values: &[&str]| values.iter().map(|value| (*value).to_owned()).collect();
423 Self(
424 scv_tools::adapters::ADAPTERS
425 .iter()
426 .map(|adapter| {
427 (
428 adapter.name.to_owned(),
429 AdapterConfig {
430 command: adapter.command.into(),
431 args: strings(adapter.args),
432 permissions: AgentPermissions::Default,
433 prompt_args: strings(adapter.prompt_args),
434 model_args: strings(adapter.model_args),
435 effort_args: strings(adapter.effort_args),
436 transport: AgentTransport::Auto,
437 use_for: None,
438 model: None,
439 effort: None,
440 },
441 )
442 })
443 .collect(),
444 )
445 }
446}
447
448#[derive(Debug, Clone, Default)]
451pub struct ConfigOverrides {
452 pub provider: Option<String>,
453 pub model: Option<String>,
454 pub base_url: Option<String>,
455 pub approval_policy: Option<ApprovalPolicy>,
456 pub no_tools: bool,
457 pub config_file: Option<PathBuf>,
460}