Skip to main content

scv_server/config/
mod.rs

1//! SCV's configuration: the schema of `config.toml` ([`schema`]), how the
2//! layers are read and merged ([`load`]), what each value must satisfy and
3//! what a project may change ([`validate`]), and the settings the runtime,
4//! tools, and provider are built from ([`runtime`]).
5//!
6//! The `scv` command line reads it too: `scv config show` lists every
7//! [`Setting`] with its origin, and `scv agents` finds the agent homes.
8
9mod load;
10mod runtime;
11mod schema;
12mod validate;
13
14pub use load::{Setting, read_layer};
15pub use schema::*;
16
17use anyhow::Result;
18use scv_client::Layout;
19
20/// The instance a server process serves and the overrides its command line
21/// chose. Every configuration the process loads starts from these.
22#[derive(Debug, Clone)]
23pub(crate) struct Instance {
24    pub(crate) layout: Layout,
25    pub(crate) overrides: ConfigOverrides,
26}
27
28impl Instance {
29    /// The configuration files alone: the explicit layer, without the
30    /// command line's provider, model, or policy flags.
31    fn files(&self) -> ConfigOverrides {
32        ConfigOverrides {
33            config_file: self.overrides.config_file.clone(),
34            ..ConfigOverrides::default()
35        }
36    }
37
38    /// The configuration files as a session in `workspace` reads them.
39    pub(crate) fn load(&self, workspace: &std::path::Path) -> Result<Config> {
40        Config::load(&self.layout, workspace, self.files())
41    }
42
43    /// The user's configuration files, without a project layer.
44    pub(crate) fn load_user(&self) -> Result<Config> {
45        Config::load_user(&self.layout, self.files())
46    }
47}
48
49/// Warnings for Claude Code billing migrations and machine-wide policy.
50fn claude_warnings_for_layout(layout: &Layout) -> Vec<String> {
51    let mut warnings = Vec::new();
52    let agents = std::fs::read_to_string(layout.config())
53        .ok()
54        .and_then(|text| text.parse::<toml::Value>().ok())
55        .and_then(|value| value.get("agents").cloned())
56        .and_then(|value| value.as_table().cloned());
57    let explicit_claude = agents
58        .as_ref()
59        .is_some_and(|agents| agents.contains_key("claude"));
60    let explicit_claude_ai = agents
61        .as_ref()
62        .is_some_and(|agents| agents.contains_key("claude-ai"));
63    if explicit_claude
64        && !explicit_claude_ai
65        && scv_tools::stores::claude_status(
66            scv_tools::adapters::AgentAuth::ClaudeSubscription,
67            &layout.agent_home("claude"),
68        )
69        .is_ok_and(|status| status.ready)
70    {
71        warnings.push(
72            "claude now bills an API key and claude-ai uses the Claude subscription. agents/claude holds a Claude subscription login, so claude will not run. To keep using the subscription, move agents/claude to agents/claude-ai and [agents.claude] to [agents.claude-ai] (see docs/configuration.md).".into(),
73        );
74    }
75    if std::path::Path::new("/etc/claude-code/managed-settings.json").is_file() {
76        warnings.push(
77            "machine policy /etc/claude-code/managed-settings.json outranks SCV's pinned Claude Code credentials for claude and claude-ai".into(),
78        );
79    }
80    warnings
81}
82
83impl Config {
84    pub fn claude_warnings(&self) -> Vec<String> {
85        claude_warnings_for_layout(&Layout::new(&self.instance_home))
86    }
87}
88
89impl Instance {
90    pub(crate) fn claude_warnings(&self) -> Vec<String> {
91        claude_warnings_for_layout(&self.layout)
92    }
93}
94
95#[cfg(test)]
96mod tests;