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`]) after their deprecated keys are
3//! read as their replacements ([`legacy`]), what each value must satisfy and
4//! what a project may change ([`validate`]), and the settings the runtime,
5//! tools, and provider are built from ([`runtime`]).
6//!
7//! The `scv` command line reads it too: `scv config show` lists every
8//! [`Setting`] with its origin, and `scv agents` finds the agent homes.
9
10mod legacy;
11mod load;
12mod runtime;
13mod schema;
14mod validate;
15
16pub use legacy::Deprecated;
17pub use load::{Setting, read_layer};
18pub use schema::*;
19
20use anyhow::Result;
21use scv_client::Layout;
22
23/// The instance a server process serves and the overrides its command line
24/// chose. Every configuration the process loads starts from these.
25#[derive(Debug, Clone)]
26pub(crate) struct Instance {
27    pub(crate) layout: Layout,
28    pub(crate) overrides: ConfigOverrides,
29}
30
31impl Instance {
32    /// The configuration files alone: the explicit layer, without the
33    /// command line's provider, model, or policy flags.
34    fn files(&self) -> ConfigOverrides {
35        ConfigOverrides {
36            config_file: self.overrides.config_file.clone(),
37            ..ConfigOverrides::default()
38        }
39    }
40
41    /// The configuration files as a session in `workspace` reads them.
42    pub(crate) fn load(&self, workspace: &std::path::Path) -> Result<Config> {
43        Config::load(&self.layout, workspace, self.files())
44    }
45
46    /// The user's configuration files, without a project layer.
47    pub(crate) fn load_user(&self) -> Result<Config> {
48        Config::load_user(&self.layout, self.files())
49    }
50}
51
52/// Warnings for Claude Code billing migrations and machine-wide policy.
53fn claude_warnings_for_layout(layout: &Layout) -> Vec<String> {
54    let mut warnings = Vec::new();
55    let agents = std::fs::read_to_string(layout.config())
56        .ok()
57        .and_then(|text| text.parse::<toml::Value>().ok())
58        .and_then(|value| value.get("agents").cloned())
59        .and_then(|value| value.as_table().cloned());
60    let explicit_claude = agents
61        .as_ref()
62        .is_some_and(|agents| agents.contains_key("claude"));
63    let explicit_claude_ai = agents
64        .as_ref()
65        .is_some_and(|agents| agents.contains_key("claude-ai"));
66    if explicit_claude
67        && !explicit_claude_ai
68        && scv_tools::stores::claude_status(
69            scv_tools::adapters::AgentAuth::ClaudeSubscription,
70            &layout.agent_home("claude"),
71        )
72        .is_ok_and(|status| status.ready)
73    {
74        warnings.push(
75            "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(),
76        );
77    }
78    if std::path::Path::new("/etc/claude-code/managed-settings.json").is_file() {
79        warnings.push(
80            "machine policy /etc/claude-code/managed-settings.json outranks SCV's pinned Claude Code credentials for claude and claude-ai".into(),
81        );
82    }
83    warnings
84}
85
86impl Config {
87    pub fn claude_warnings(&self) -> Vec<String> {
88        claude_warnings_for_layout(&Layout::new(&self.instance_home))
89    }
90}
91
92impl Instance {
93    pub(crate) fn claude_warnings(&self) -> Vec<String> {
94        claude_warnings_for_layout(&self.layout)
95    }
96
97    /// Log the policy mode in effect for the user's configuration and this
98    /// process's flags, each agent with a mode of its own, and a warning for
99    /// each deprecated key, naming the line that replaces it.
100    pub(crate) fn log_policy(&self) {
101        let config = match Config::load_user(&self.layout, self.overrides.clone()) {
102            Ok(config) => config,
103            Err(error) => {
104                tracing::warn!("Cannot read the policy mode: {error:#}");
105                return;
106            }
107        };
108        let agents: Vec<String> = config
109            .agent_modes()
110            .into_iter()
111            .map(|(agent, mode)| format!("{agent} {mode}"))
112            .collect();
113        if agents.is_empty() {
114            tracing::info!("Policy mode {}", config.policy_mode());
115        } else {
116            tracing::info!(
117                "Policy mode {}; agents with their own: {}",
118                config.policy_mode(),
119                agents.join(", ")
120            );
121        }
122        for deprecated in config.deprecated() {
123            tracing::warn!("{}", deprecated.warning());
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests;