Skip to main content

kvlar_proxy/
config.rs

1//! Proxy configuration.
2
3use serde::{Deserialize, Serialize};
4
5/// Transport mode for the proxy.
6#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum TransportMode {
9    /// TCP socket transport (line-delimited JSON over TCP). Default.
10    #[default]
11    Tcp,
12    /// Stdio transport (newline-delimited JSON over stdin/stdout).
13    /// The proxy spawns the upstream server as a subprocess.
14    Stdio,
15}
16
17/// Configuration for the Kvlar MCP proxy.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProxyConfig {
20    /// Address to listen on (e.g., "127.0.0.1:9100"). TCP mode only.
21    pub listen_addr: String,
22
23    /// Upstream MCP server address to forward allowed requests to. TCP mode only.
24    pub upstream_addr: String,
25
26    /// Path to the policy YAML file(s).
27    pub policy_paths: Vec<String>,
28
29    /// Whether to enable audit logging.
30    #[serde(default = "default_true")]
31    pub audit_enabled: bool,
32
33    /// Path to the audit log file (JSONL format).
34    #[serde(default)]
35    pub audit_file: Option<String>,
36
37    /// Whether to fail-open (allow) when the policy engine errors.
38    /// Default: false (fail-closed).
39    #[serde(default)]
40    pub fail_open: bool,
41
42    /// Whether to watch policy files for changes and reload automatically.
43    #[serde(default)]
44    pub hot_reload: bool,
45
46    /// Transport mode (tcp or stdio). Default: tcp.
47    #[serde(default)]
48    pub transport: TransportMode,
49
50    /// Command to spawn for stdio transport (e.g., "npx").
51    /// Only used when transport = stdio.
52    #[serde(default)]
53    pub upstream_command: Option<String>,
54
55    /// Arguments for the upstream command.
56    /// Only used when transport = stdio.
57    #[serde(default)]
58    pub upstream_args: Vec<String>,
59
60    /// Health check endpoint address (e.g., "127.0.0.1:9101").
61    /// Only used in TCP mode. Set to enable `GET /health` liveness probe.
62    #[serde(default)]
63    pub health_addr: Option<String>,
64
65    /// SHIELD cloud base URL (e.g., "https://app.kvlar.io").
66    /// When set with `kvlar_api_key`, enables cloud escalation and audit forwarding.
67    #[serde(default)]
68    pub kvlar_cloud_url: Option<String>,
69
70    /// API key for SHIELD cloud authentication (`kvlar_sk_...`).
71    #[serde(default)]
72    pub kvlar_api_key: Option<String>,
73
74    /// Agent ID registered in SHIELD (UUID).
75    /// Used to associate proxy audit events with a specific agent.
76    #[serde(default)]
77    pub kvlar_agent_id: Option<String>,
78
79    /// RADAR cloud base URL (e.g., "https://radar.kvlar.io").
80    /// When set, audit events are also forwarded to RADAR.
81    #[serde(default)]
82    pub kvlar_radar_url: Option<String>,
83}
84
85fn default_true() -> bool {
86    true
87}
88
89impl Default for ProxyConfig {
90    fn default() -> Self {
91        Self {
92            listen_addr: "127.0.0.1:9100".into(),
93            upstream_addr: "127.0.0.1:3000".into(),
94            policy_paths: vec!["policy.yaml".into()],
95            audit_enabled: true,
96            audit_file: None,
97            fail_open: false,
98            hot_reload: false,
99            transport: TransportMode::default(),
100            upstream_command: None,
101            upstream_args: Vec::new(),
102            health_addr: None,
103            kvlar_cloud_url: None,
104            kvlar_api_key: None,
105            kvlar_agent_id: None,
106            kvlar_radar_url: None,
107        }
108    }
109}
110
111impl ProxyConfig {
112    /// Loads configuration from a YAML file.
113    pub fn from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
114        let yaml = std::fs::read_to_string(path)?;
115        let config: ProxyConfig = serde_yaml::from_str(&yaml)?;
116        Ok(config)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_default_config() {
126        let config = ProxyConfig::default();
127        assert_eq!(config.listen_addr, "127.0.0.1:9100");
128        assert!(config.audit_enabled);
129        assert!(!config.fail_open);
130        assert!(!config.hot_reload);
131        assert!(config.audit_file.is_none());
132    }
133
134    #[test]
135    fn test_config_serde_roundtrip() {
136        let config = ProxyConfig::default();
137        let json = serde_json::to_string(&config).unwrap();
138        let parsed: ProxyConfig = serde_json::from_str(&json).unwrap();
139        assert_eq!(parsed.listen_addr, config.listen_addr);
140        assert_eq!(parsed.upstream_addr, config.upstream_addr);
141    }
142
143    #[test]
144    fn test_config_from_yaml_string() {
145        let yaml = r#"
146listen_addr: "0.0.0.0:8080"
147upstream_addr: "localhost:4000"
148policy_paths:
149  - "policies/prod.yaml"
150  - "policies/custom.yaml"
151audit_enabled: true
152audit_file: "/var/log/kvlar/audit.jsonl"
153fail_open: false
154hot_reload: true
155"#;
156        let config: ProxyConfig = serde_yaml::from_str(yaml).unwrap();
157        assert_eq!(config.listen_addr, "0.0.0.0:8080");
158        assert_eq!(config.policy_paths.len(), 2);
159        assert!(config.hot_reload);
160        assert_eq!(
161            config.audit_file.as_deref(),
162            Some("/var/log/kvlar/audit.jsonl")
163        );
164    }
165}