agentmux_srv/
config.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4
5use clap::Parser;
6
7#[derive(Parser, Debug)]
8#[command(name = "agentmux-srv", about = "AgentMux Rust backend server")]
9pub struct CliArgs {
10    /// Path to wave data directory (overrides AGENTMUX_DATA_HOME)
11    #[arg(long = "wavedata")]
12    pub wavedata: Option<String>,
13
14    /// Instance identifier (used for multi-version coexistence)
15    #[arg(long = "instance", default_value = "default")]
16    pub instance: String,
17
18}
19
20#[derive(Debug, Clone)]
21pub struct Config {
22    pub auth_key: String,
23    pub data_home: String,
24    pub config_home: String,
25    pub app_path: String,
26    #[allow(dead_code)]
27    pub is_dev: bool,
28    pub version: &'static str,
29    pub build_time: &'static str,
30    pub instance_id: String,
31}
32
33impl Config {
34    /// Build config from env vars + CLI args.
35    /// Removes AGENTMUX_AUTH_KEY from the environment after reading (matching Go behavior).
36    pub fn from_env_and_args(args: &CliArgs) -> Result<Self, String> {
37        let auth_key = std::env::var("AGENTMUX_AUTH_KEY")
38            .map_err(|_| "AGENTMUX_AUTH_KEY environment variable is required".to_string())?;
39
40        if auth_key.is_empty() {
41            return Err("AGENTMUX_AUTH_KEY must not be empty".to_string());
42        }
43
44        // Remove from env after read (matching Go authkey.go:50)
45        std::env::remove_var("AGENTMUX_AUTH_KEY");
46
47        // CLI flag wins over env. The launcher sets the canonical
48        // `AGENTMUX_DATA_DIR` and `AGENTMUX_CONFIG_DIR` via
49        // `agentmux_common::DataPaths::to_env_vars`. Pre-unification
50        // names (`AGENTMUX_DATA_HOME`, `AGENTMUX_CONFIG_HOME`) are no
51        // longer set — no fallback (symmetry; partial-rollout isn't a
52        // supported scenario per spec §3.4 "no migration").
53        let data_home = args
54            .wavedata
55            .clone()
56            .or_else(|| std::env::var("AGENTMUX_DATA_DIR").ok())
57            .unwrap_or_default();
58
59        let config_home = std::env::var("AGENTMUX_CONFIG_DIR").unwrap_or_default();
60        let app_path = std::env::var("AGENTMUX_APP_PATH").unwrap_or_default();
61        // is_dev is now derived from AGENTMUX_RUNTIME_MODE (the
62        // canonical env var emitted by the unified DataPaths layer).
63        // Legacy AGENTMUX_DEV is no longer set by the launcher.
64        let is_dev = matches!(
65            agentmux_common::RuntimeMode::from_env(),
66            Some(agentmux_common::RuntimeMode::Dev { .. })
67        );
68
69        Ok(Config {
70            auth_key,
71            data_home,
72            config_home,
73            app_path,
74            is_dev,
75            version: env!("CARGO_PKG_VERSION"),
76            build_time: option_env!("BUILD_TIME").unwrap_or("dev"),
77            instance_id: args.instance.clone(),
78        })
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::sync::Mutex;
86
87    // Serialize config tests — they mutate process-global env vars
88    static ENV_LOCK: Mutex<()> = Mutex::new(());
89
90    /// Lock helper that recovers from poisoned mutex. A panic in any
91    /// test would otherwise propagate poison to all later tests via
92    /// `lock().unwrap()` and produce noise unrelated to the actual
93    /// failing test.
94    fn lock() -> std::sync::MutexGuard<'static, ()> {
95        ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
96    }
97
98    /// Clear every env var our config reads so each test starts from
99    /// a known state, regardless of leakage from prior tests.
100    fn clear_env() {
101        for k in [
102            "AGENTMUX_AUTH_KEY",
103            "AGENTMUX_DATA_DIR",
104            "AGENTMUX_DATA_HOME",
105            "AGENTMUX_CONFIG_DIR",
106            "AGENTMUX_CONFIG_HOME",
107            "AGENTMUX_APP_PATH",
108            "AGENTMUX_RUNTIME_MODE",
109            "AGENTMUX_DEV",
110        ] {
111            std::env::remove_var(k);
112        }
113    }
114
115    #[test]
116    fn missing_auth_key_errors() {
117        let _lock = lock();
118        clear_env();
119        let args = CliArgs { wavedata: None, instance: "default".to_string() };
120        let result = Config::from_env_and_args(&args);
121        assert!(result.is_err());
122        assert!(result.unwrap_err().contains("AGENTMUX_AUTH_KEY"));
123    }
124
125    #[test]
126    fn empty_auth_key_errors() {
127        let _lock = lock();
128        clear_env();
129        std::env::set_var("AGENTMUX_AUTH_KEY", "");
130        let args = CliArgs { wavedata: None, instance: "default".to_string() };
131        let result = Config::from_env_and_args(&args);
132        assert!(result.is_err());
133        clear_env();
134    }
135
136    #[test]
137    fn cli_wavedata_overrides_env() {
138        let _lock = lock();
139        clear_env();
140        std::env::set_var("AGENTMUX_AUTH_KEY", "test-key-12345");
141        std::env::set_var("AGENTMUX_DATA_DIR", "/from/env");
142        let args = CliArgs {
143            wavedata: Some("/from/cli".to_string()),
144            instance: "default".to_string(),
145        };
146        let config = Config::from_env_and_args(&args).unwrap();
147        assert_eq!(config.data_home, "/from/cli");
148        assert!(std::env::var("AGENTMUX_AUTH_KEY").is_err());
149        clear_env();
150    }
151
152    #[test]
153    fn env_var_parsing() {
154        let _lock = lock();
155        clear_env();
156        std::env::set_var("AGENTMUX_AUTH_KEY", "test-key-67890");
157        std::env::set_var("AGENTMUX_DATA_DIR", "/data");
158        std::env::set_var("AGENTMUX_CONFIG_DIR", "/config");
159        std::env::set_var("AGENTMUX_APP_PATH", "/app");
160        std::env::set_var("AGENTMUX_RUNTIME_MODE", "dev:main");
161        let args = CliArgs { wavedata: None, instance: "default".to_string() };
162        let config = Config::from_env_and_args(&args).unwrap();
163        assert_eq!(config.data_home, "/data");
164        assert_eq!(config.config_home, "/config");
165        assert_eq!(config.app_path, "/app");
166        assert!(config.is_dev);
167        clear_env();
168    }
169}