Skip to main content

scv_server/config/
legacy.rs

1//! The keys `[policy] mode` replaced, accepted as deprecated aliases until a
2//! later breaking release removes them: `[tools] approval_policy` and
3//! `[agents.<name>] permissions`.
4//!
5//! Each layer is rewritten before it is merged, keeping the old meaning:
6//! `on-risk` stays `on-risk`, `always` stays `always`, and `never` becomes
7//! `read-only`; `permissions = "full"` becomes `mode = "auto"`, and
8//! `"default"` becomes `mode = "on-risk"`, so that agent keeps starting with
9//! no switches. A line equal to the old default is honored too, since
10//! nothing tells a copied sample line from a decision. A layer that sets an
11//! old key and its new key is refused.
12
13use anyhow::{Result, bail};
14
15/// One deprecated key a layer set, and what it is read as.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Deprecated {
18    /// The key as written, such as `tools.approval_policy`.
19    pub key: String,
20    /// Its value as written, such as `on-risk`.
21    pub value: String,
22    /// The key it is read as, such as `policy.mode`.
23    pub replacement: String,
24    /// The value it is read as, such as `read-only`.
25    pub mode: String,
26    /// The layer that set it, as `scv config show` names layers.
27    pub origin: String,
28    /// The line pins the old default, which `auto` replaced.
29    pub pins_old_default: bool,
30}
31
32impl Deprecated {
33    /// The startup warning, which names the replacement line.
34    pub fn warning(&self) -> String {
35        let (table, field) = self.replacement.rsplit_once('.').unwrap_or_default();
36        let advice = if self.pins_old_default {
37            "; it pins the old default, so delete it for auto"
38        } else if self.mode == "auto" {
39            ", which is now the default, so it can be deleted"
40        } else {
41            ""
42        };
43        format!(
44            "{}: {} = \"{}\" is deprecated and read as [{table}] {field} = \"{}\"; replace it \
45             with that line{advice}",
46            self.origin, self.key, self.value, self.mode
47        )
48    }
49
50    /// What `scv config show` adds to the setting it became.
51    pub fn note(&self) -> String {
52        let pins = if self.pins_old_default {
53            "; pins the old default; delete it for auto"
54        } else {
55            ""
56        };
57        format!("set as deprecated {} = \"{}\"{pins}", self.key, self.value)
58    }
59}
60
61/// Rewrite the deprecated keys of the layer `origin` into the keys that
62/// replace them, returning what it set.
63pub(super) fn migrate(layer: &mut toml::Value, origin: &str) -> Result<Vec<Deprecated>> {
64    let mut found = Vec::new();
65    let Some(root) = layer.as_table_mut() else {
66        return Ok(found);
67    };
68    let old_policy = root
69        .get_mut("tools")
70        .and_then(toml::Value::as_table_mut)
71        .and_then(|tools| tools.remove("approval_policy"));
72    if let Some(old) = old_policy {
73        let (value, mode, pins) = match old.as_str() {
74            Some("on-risk") => ("on-risk", "on-risk", true),
75            Some("always") => ("always", "always", false),
76            Some("never") => ("never", "read-only", false),
77            _ => bail!(
78                "{origin}: tools.approval_policy (deprecated) must be \"on-risk\", \"always\", or \
79                 \"never\"; set [policy] mode instead"
80            ),
81        };
82        let policy = root
83            .entry("policy")
84            .or_insert_with(|| toml::Value::Table(toml::Table::new()));
85        let Some(policy) = policy.as_table_mut() else {
86            bail!("{origin}: [policy] must be a table");
87        };
88        if policy.contains_key("mode") {
89            bail!(
90                "{origin} sets both tools.approval_policy and policy.mode; keep only [policy] mode \
91                 (tools.approval_policy is deprecated)"
92            );
93        }
94        policy.insert("mode".into(), mode.into());
95        found.push(Deprecated {
96            key: "tools.approval_policy".into(),
97            value: value.into(),
98            replacement: "policy.mode".into(),
99            mode: mode.into(),
100            origin: origin.into(),
101            pins_old_default: pins,
102        });
103    }
104    let agents = root
105        .get_mut("agents")
106        .and_then(toml::Value::as_table_mut)
107        .into_iter()
108        .flat_map(|agents| agents.iter_mut());
109    for (agent, table) in agents {
110        let Some(table) = table.as_table_mut() else {
111            continue;
112        };
113        let Some(old) = table.remove("permissions") else {
114            continue;
115        };
116        let (value, mode, pins) = match old.as_str() {
117            Some("full") => ("full", "auto", false),
118            Some("default") => ("default", "on-risk", true),
119            _ => bail!(
120                "{origin}: agents.{agent}.permissions (deprecated) must be \"default\" or \"full\"; \
121                 set [agents.{agent}] mode instead"
122            ),
123        };
124        if table.contains_key("mode") {
125            bail!(
126                "{origin} sets both agents.{agent}.permissions and agents.{agent}.mode; keep only \
127                 mode (permissions is deprecated)"
128            );
129        }
130        table.insert("mode".into(), mode.into());
131        found.push(Deprecated {
132            key: format!("agents.{agent}.permissions"),
133            value: value.into(),
134            replacement: format!("agents.{agent}.mode"),
135            mode: mode.into(),
136            origin: origin.into(),
137            pins_old_default: pins,
138        });
139    }
140    Ok(found)
141}