magi-code 0.80.0

Repository-aware CLI coding agent for terminal work
Documentation
use crate::mcp::{McpError, McpResult};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use std::collections::BTreeMap;

pub(crate) const REDACTED_HEADER_VALUE: &str = "[REDACTED]";

pub(crate) fn resolve_http_headers(headers: &BTreeMap<String, String>) -> McpResult<HeaderMap> {
    let mut resolved = HeaderMap::new();
    for (name, value) in headers {
        let header_name = HeaderName::from_bytes(name.as_bytes())
            .map_err(|_| McpError::Config(format!("invalid MCP HTTP header name '{name}'")))?;
        let header_value = HeaderValue::from_str(value)
            .map_err(|_| McpError::Config(format!("invalid MCP HTTP header value for '{name}'")))?;
        resolved.insert(header_name, header_value);
    }
    Ok(resolved)
}

pub(crate) fn redact_headers(headers: &BTreeMap<String, String>) -> BTreeMap<String, String> {
    headers
        .keys()
        .map(|name| (name.clone(), REDACTED_HEADER_VALUE.to_string()))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn accepts_resolved_sensitive_and_literal_headers_without_expansion() {
        let headers = BTreeMap::from([
            (
                "Authorization".to_string(),
                "Bearer resolved-token".to_string(),
            ),
            ("X-Team".to_string(), "platform".to_string()),
            (
                "X-Template".to_string(),
                "${MCP_HEADER_TEST_MISSING}".to_string(),
            ),
            (
                "X-Legacy".to_string(),
                "{env:MCP_HEADER_TEST_MISSING}".to_string(),
            ),
            ("X-Literal".to_string(), "{env:NOT-A-VAR}".to_string()),
        ]);

        let resolved = resolve_http_headers(&headers).unwrap();

        for (name, value) in &headers {
            assert_eq!(resolved.get(name).unwrap(), value.as_str());
        }
    }

    #[test]
    fn rejects_invalid_header_values_without_echoing_secrets() {
        let headers = BTreeMap::from([(
            "Authorization".to_string(),
            "secret-token\r\nInjected: value".to_string(),
        )]);

        let error = resolve_http_headers(&headers).unwrap_err().to_string();

        assert!(
            error.contains("invalid MCP HTTP header value for 'Authorization'"),
            "{error}"
        );
        assert!(!error.contains("secret-token"), "{error}");
        assert!(!error.contains("Injected"), "{error}");
    }

    #[test]
    fn rejects_invalid_header_names_without_echoing_values() {
        let headers = BTreeMap::from([("Invalid Name".to_string(), "secret-token".to_string())]);

        let error = resolve_http_headers(&headers).unwrap_err().to_string();

        assert!(error.contains("invalid MCP HTTP header name"), "{error}");
        assert!(!error.contains("secret-token"), "{error}");
    }

    #[test]
    fn redacts_all_header_values() {
        let mut headers = BTreeMap::new();
        headers.insert("X-Team".to_string(), "platform".to_string());
        headers.insert("Authorization".to_string(), "resolved-token".to_string());

        let redacted = redact_headers(&headers);

        assert_eq!(redacted["X-Team"], REDACTED_HEADER_VALUE);
        assert_eq!(redacted["Authorization"], REDACTED_HEADER_VALUE);
        assert!(!format!("{redacted:?}").contains("platform"));
    }
}