Skip to main content

agent_first_pay/
config.rs

1use crate::types::*;
2use agent_first_data::cli_parse_log_filters;
3use std::path::Path;
4
5pub const VERSION: &str = env!("CARGO_PKG_VERSION");
6
7pub fn enabled_features() -> Vec<&'static str> {
8    let features: &[Option<&str>] = &[
9        #[cfg(feature = "redb")]
10        Some("redb"),
11        #[cfg(feature = "postgres")]
12        Some("postgres"),
13        #[cfg(feature = "cashu")]
14        Some("cashu"),
15        #[cfg(feature = "ln-nwc")]
16        Some("ln-nwc"),
17        #[cfg(feature = "ln-phoenixd")]
18        Some("ln-phoenixd"),
19        #[cfg(feature = "ln-lnbits")]
20        Some("ln-lnbits"),
21        #[cfg(feature = "sol")]
22        Some("sol"),
23        #[cfg(feature = "evm")]
24        Some("evm"),
25        #[cfg(feature = "btc-esplora")]
26        Some("btc-esplora"),
27        #[cfg(feature = "btc-core")]
28        Some("btc-core"),
29        #[cfg(feature = "btc-electrum")]
30        Some("btc-electrum"),
31        #[cfg(feature = "interactive")]
32        Some("interactive"),
33        #[cfg(feature = "rest")]
34        Some("rest"),
35    ];
36    features.iter().copied().flatten().collect()
37}
38
39/// Single source of truth for startup log — always includes env.features.
40pub fn build_startup_log(
41    argv: Option<Vec<String>>,
42    config: Option<&RuntimeConfig>,
43    args: serde_json::Value,
44) -> Output {
45    Output::Log {
46        event: "startup".to_string(),
47        request_id: None,
48        version: Some(VERSION.to_string()),
49        argv: argv.map(sanitize_startup_argv),
50        config: config.map(|c| serde_json::to_value(c).unwrap_or(serde_json::Value::Null)),
51        args: Some(args),
52        env: Some(serde_json::json!({
53            "features": enabled_features(),
54        })),
55        trace: Trace::from_duration(0),
56    }
57}
58
59fn sanitize_startup_argv(argv: Vec<String>) -> Vec<String> {
60    const SECRET_FLAGS: &[&str] = &[
61        "--admin-key-secret",
62        "--btc-core-auth-secret",
63        "--mnemonic-secret",
64        "--nwc-uri-secret",
65        "--password-secret",
66        "--pg-url-secret",
67        "--rest-api-key",
68        "--rpc-secret",
69    ];
70
71    let mut redact_next = false;
72    argv.into_iter()
73        .map(|arg| {
74            if redact_next {
75                redact_next = false;
76                return "***".to_string();
77            }
78            if SECRET_FLAGS.iter().any(|flag| arg == *flag) {
79                redact_next = true;
80                return arg;
81            }
82            for flag in SECRET_FLAGS {
83                let prefix = format!("{flag}=");
84                if arg.starts_with(&prefix) {
85                    return format!("{flag}=***");
86                }
87            }
88            arg
89        })
90        .collect()
91}
92
93/// Decide whether startup log should be emitted for this process.
94/// Startup is emitted when explicit startup logging is requested or any log filter is set.
95pub fn should_emit_startup_log(log_filters: &[String], startup_requested: bool) -> bool {
96    startup_requested || !log_filters.is_empty()
97}
98
99/// Unified startup log builder + gate used by all runtime modes.
100pub fn maybe_startup_log(
101    log_filters: &[String],
102    startup_requested: bool,
103    argv: Option<Vec<String>>,
104    config: Option<&RuntimeConfig>,
105    args: serde_json::Value,
106) -> Option<Output> {
107    if !should_emit_startup_log(log_filters, startup_requested) {
108        return None;
109    }
110    Some(build_startup_log(argv, config, args))
111}
112
113impl RuntimeConfig {
114    /// Load config from `{data_dir}/config.toml`. Falls back to defaults if file missing.
115    pub fn load_from_dir(data_dir: &str) -> Result<Self, String> {
116        let path = Path::new(data_dir).join("config.toml");
117        if !path.exists() {
118            return Ok(Self {
119                data_dir: data_dir.to_string(),
120                ..Self::default()
121            });
122        }
123        let contents =
124            std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?;
125        let mut cfg: Self =
126            toml::from_str(&contents).map_err(|e| format!("parse {}: {e}", path.display()))?;
127        // Ensure data_dir reflects the actual directory (config file may omit it)
128        cfg.data_dir = data_dir.to_string();
129        Ok(cfg)
130    }
131
132    #[allow(dead_code)]
133    pub fn apply_update(&mut self, patch: ConfigPatch) {
134        if let Some(v) = patch.data_dir {
135            self.data_dir = v;
136        }
137        if let Some(v) = patch.log {
138            self.log = cli_parse_log_filters(&v);
139        }
140        if let Some(rpc_nodes) = patch.afpay_rpc {
141            for (name, cfg) in rpc_nodes {
142                self.afpay_rpc.insert(name, cfg);
143            }
144        }
145        if let Some(providers) = patch.providers {
146            for (network, rpc_name) in providers {
147                self.providers.insert(network, rpc_name);
148            }
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn maybe_startup_log_disabled_without_filters_or_request() {
159        let out = maybe_startup_log(&[], false, None, None, serde_json::json!({"mode": "test"}));
160        assert!(out.is_none());
161    }
162
163    #[test]
164    fn maybe_startup_log_enabled_with_filters() {
165        let filters = vec!["cashu".to_string()];
166        let out = maybe_startup_log(
167            &filters,
168            false,
169            None,
170            None,
171            serde_json::json!({"mode": "test"}),
172        );
173        assert!(out.is_some());
174    }
175
176    #[test]
177    fn maybe_startup_log_enabled_with_explicit_request() {
178        let out = maybe_startup_log(&[], true, None, None, serde_json::json!({"mode": "test"}));
179        assert!(out.is_some());
180    }
181
182    #[test]
183    fn startup_log_redacts_secret_argv_values() -> Result<(), Box<dyn std::error::Error>> {
184        let out = build_startup_log(
185            Some(vec![
186                "afpay".to_string(),
187                "--rpc-secret".to_string(),
188                "rpc-secret-value".to_string(),
189                "--rest-api-key=rest-secret-value".to_string(),
190                "--data-dir".to_string(),
191                "/tmp/afpay".to_string(),
192            ]),
193            None,
194            serde_json::json!({"mode": "test"}),
195        );
196        let value = serde_json::to_value(out)?;
197        let rendered = value.to_string();
198        assert!(!rendered.contains("rpc-secret-value"));
199        assert!(!rendered.contains("rest-secret-value"));
200        assert!(rendered.contains("***"));
201        assert!(rendered.contains("/tmp/afpay"));
202        Ok(())
203    }
204}