Skip to main content

vta_cli_common/commands/
config.rs

1use std::collections::HashMap;
2
3use vta_sdk::prelude::*;
4use vta_sdk::protocols::vta_management::update_config::UpdateConfigBody;
5
6/// Print the configuration registry as canonical `config/show/0.1` returns it.
7///
8/// Boot-stable keys are marked, so an operator can see before patching that a
9/// change will not take effect until a restart.
10pub async fn cmd_config_get(
11    client: &VtaClient,
12    label_prefix: &str,
13) -> Result<(), Box<dyn std::error::Error>> {
14    let resp = client.get_config().await?;
15    // Pad to the longest key so the rate-limit keys line up with the short ones.
16    let width = resp
17        .config
18        .fields
19        .iter()
20        .map(|f| f.key.len() + 1)
21        .max()
22        .unwrap_or(0)
23        .max(12);
24    for field in &resp.config.fields {
25        let value = match &field.value {
26            serde_json::Value::Null => "(not set)".to_string(),
27            serde_json::Value::String(s) => s.clone(),
28            other => other.to_string(),
29        };
30        let restart = if field.requires_restart {
31            "  (requires restart)"
32        } else {
33            ""
34        };
35        println!(
36            "{label_prefix}{:<width$} {value}  [{}]{restart}",
37            format!("{}:", field.key),
38            field.source
39        );
40    }
41    Ok(())
42}
43
44/// Patch configuration keys.
45///
46/// `vta_did` is deliberately **not** a parameter: the VTA's own identity is
47/// set at setup and is immutable at runtime, so there is no flag to attempt
48/// it with. A caller that names it anyway (over the wire) is answered with a
49/// rejection, which this command prints — the operator learns the rule rather
50/// than silently re-pointing the agent's identity, which is what the
51/// pre-canonical surface did.
52pub async fn cmd_config_update(
53    client: &VtaClient,
54    label_prefix: &str,
55    vta_name: Option<String>,
56    public_url: Option<String>,
57) -> Result<(), Box<dyn std::error::Error>> {
58    cmd_config_patch(
59        client,
60        label_prefix,
61        vta_name,
62        public_url,
63        RateLimitOverrides::default(),
64    )
65    .await
66}
67
68/// The VTA's runtime rate-limit keys, each optional. Intervals are **seconds
69/// per token** (lower is looser), not rates. The VTA bounds them (intervals
70/// 1-3600, bursts 1-10000) and applies them without a restart.
71#[derive(Debug, Default, Clone, Copy)]
72#[non_exhaustive]
73pub struct RateLimitOverrides {
74    /// `rate_limit_interval_secs` — the auth limiter's seconds per token.
75    pub rate_limit_interval_secs: Option<u64>,
76    /// `rate_limit_burst` — the auth limiter's burst.
77    pub rate_limit_burst: Option<u32>,
78    /// `did_log_rate_limit_interval_secs` — the DID-log limiter's seconds per
79    /// token.
80    pub did_log_rate_limit_interval_secs: Option<u64>,
81    /// `did_log_rate_limit_burst` — the DID-log limiter's burst.
82    pub did_log_rate_limit_burst: Option<u32>,
83}
84
85impl RateLimitOverrides {
86    /// `(registry key, value)` for every field that is set.
87    fn entries(&self) -> Vec<(&'static str, serde_json::Value)> {
88        [
89            (
90                vta_sdk::rate_limit::VTA_INTERVAL_KEY,
91                self.rate_limit_interval_secs.map(serde_json::Value::from),
92            ),
93            (
94                vta_sdk::rate_limit::VTA_BURST_KEY,
95                self.rate_limit_burst.map(serde_json::Value::from),
96            ),
97            (
98                vta_sdk::rate_limit::VTA_DID_LOG_INTERVAL_KEY,
99                self.did_log_rate_limit_interval_secs
100                    .map(serde_json::Value::from),
101            ),
102            (
103                vta_sdk::rate_limit::VTA_DID_LOG_BURST_KEY,
104                self.did_log_rate_limit_burst.map(serde_json::Value::from),
105            ),
106        ]
107        .into_iter()
108        .filter_map(|(k, v)| v.map(|v| (k, v)))
109        .collect()
110    }
111}
112
113/// [`cmd_config_update`] plus the runtime rate-limit keys. Rate-limit values
114/// travel as JSON integers.
115pub async fn cmd_config_patch(
116    client: &VtaClient,
117    label_prefix: &str,
118    vta_name: Option<String>,
119    public_url: Option<String>,
120    rate_limits: RateLimitOverrides,
121) -> Result<(), Box<dyn std::error::Error>> {
122    let mut overrides = HashMap::new();
123    if let Some(v) = vta_name {
124        overrides.insert("vta_name".to_string(), serde_json::Value::String(v));
125    }
126    if let Some(v) = public_url {
127        overrides.insert("public_url".to_string(), serde_json::Value::String(v));
128    }
129    for (key, value) in rate_limits.entries() {
130        overrides.insert(key.to_string(), value);
131    }
132    if overrides.is_empty() {
133        println!(
134            "Nothing to update — pass at least one of --vta-name, --public-url, \
135             --rate-limit-interval-secs, --rate-limit-burst, \
136             --did-log-rate-limit-interval-secs or --did-log-rate-limit-burst."
137        );
138        return Ok(());
139    }
140
141    let resp = client
142        .update_config(UpdateConfigRequest {
143            patch: UpdateConfigBody::new(overrides),
144        })
145        .await?;
146
147    if !resp.applied.is_empty() {
148        println!(
149            "{label_prefix}Applied:          {}",
150            resp.applied.join(", ")
151        );
152    }
153    if !resp.pending_restart.is_empty() {
154        println!(
155            "{label_prefix}Pending restart:  {}",
156            resp.pending_restart.join(", ")
157        );
158        println!("{label_prefix}  Stored, but not in effect until the VTA restarts.");
159    }
160    for rejected in &resp.rejected {
161        println!(
162            "{label_prefix}Rejected {}: {}",
163            rejected.key, rejected.reason
164        );
165    }
166    Ok(())
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn rate_limit_overrides_emit_only_set_keys_as_integers() {
175        assert!(RateLimitOverrides::default().entries().is_empty());
176
177        let o = RateLimitOverrides {
178            rate_limit_burst: Some(30),
179            did_log_rate_limit_interval_secs: Some(2),
180            ..Default::default()
181        };
182        let entries = o.entries();
183        assert_eq!(
184            entries,
185            vec![
186                ("rate_limit_burst", serde_json::json!(30)),
187                ("did_log_rate_limit_interval_secs", serde_json::json!(2)),
188            ]
189        );
190    }
191}