magi-code 0.102.2

Repository-aware CLI coding agent for terminal work
Documentation
use std::{io::Read, path::Path};

use anyhow::Context;
use hmac::{Hmac, KeyInit, Mac};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::Sha256;

use super::super::services::{DEFAULT_MCP_TIMEOUT_SECONDS, McpServerConfig};
use crate::persistence::{CrossProcessFileLock, atomic_write_with_permissions, open_regular_file};

/// Only this version's normalized, unexpanded definition contract is accepted.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub(crate) struct McpDefinitionFingerprint {
    #[schemars(range(min = 1, max = 1))]
    pub version: u32,
    #[schemars(regex(pattern = "^[0-9a-f]{64}$"))]
    pub digest: String,
}

impl std::fmt::Debug for McpDefinitionFingerprint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("McpDefinitionFingerprint(<redacted>)")
    }
}

#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub(crate) enum McpApproval {
    Definition(McpDefinitionFingerprint),
    Legacy(bool),
    // Retain corrupt entries without letting them authorize execution or leak through Debug.
    #[schemars(skip)]
    Invalid(serde_json::Value),
}

impl std::fmt::Debug for McpApproval {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("McpApproval(<redacted>)")
    }
}

impl McpApproval {
    pub(super) fn status(&self, expected: &McpDefinitionFingerprint) -> &'static str {
        match self {
            Self::Legacy(false) => "revoked",
            Self::Legacy(true) => "legacy approval requires renewal",
            Self::Definition(saved)
                if saved.version == 1
                    && saved.digest.len() == 64
                    && saved
                        .digest
                        .bytes()
                        .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) =>
            {
                if saved == expected {
                    "approved"
                } else {
                    "definition changed"
                }
            }
            _ => "invalid approval state",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct McpApprovalIdentity {
    pub source: String,
    pub fingerprint: McpDefinitionFingerprint,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct McpApprovalSnapshot {
    pub source: String,
    pub fingerprint: McpDefinitionFingerprint,
    pub status: &'static str,
    /// Held only in memory for inspection; never written into approval records.
    pub definition: McpServerConfig,
}

impl McpApprovalSnapshot {
    pub(crate) fn identity(&self) -> McpApprovalIdentity {
        McpApprovalIdentity {
            source: self.source.clone(),
            fingerprint: self.fingerprint.clone(),
        }
    }
}

pub(super) fn fingerprint(
    key: &[u8; 32],
    source: &str,
    name: &str,
    definition: &McpServerConfig,
) -> anyhow::Result<McpDefinitionFingerprint> {
    // Typed serialization includes every supported launch/connection field. BTreeMap fields
    // order keys; defaults and enabled are normalized before serialization. Strings/arrays
    // stay exact, including environment expressions and secret-bearing literal values.
    let mut definition = definition.clone();
    definition.set_enabled(false);
    match &mut definition {
        McpServerConfig::Stdio(config) => {
            config.timeout = Some(config.timeout.unwrap_or(DEFAULT_MCP_TIMEOUT_SECONDS));
        }
        McpServerConfig::Http(config) => {
            config.timeout = Some(config.timeout.unwrap_or(DEFAULT_MCP_TIMEOUT_SECONDS));
        }
    }
    let bytes = serde_json::to_vec(&("magi-code:mcp-approval:v1", source, name, definition))?;
    let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts a 32-byte key");
    mac.update(&bytes);
    let digest = mac.finalize().into_bytes();
    Ok(McpDefinitionFingerprint {
        version: 1,
        digest: digest.iter().map(|byte| format!("{byte:02x}")).collect(),
    })
}

pub(super) fn approval_key(root: &Path) -> anyhow::Result<[u8; 32]> {
    let path = root.join("mcp-approval.key");
    if let Some(key) = read_key(&path)? {
        return Ok(key);
    }
    let _lock = CrossProcessFileLock::acquire(&path)?;
    if let Some(key) = read_key(&path)? {
        return Ok(key);
    }
    let mut key = [0; 32];
    getrandom::fill(&mut key).map_err(|_| anyhow::anyhow!("cannot generate MCP approval key"))?;
    atomic_write_with_permissions(&path, &key, Some(0o600))?;
    Ok(key)
}

fn read_key(path: &Path) -> anyhow::Result<Option<[u8; 32]>> {
    let file = match open_regular_file(path, false) {
        Ok(file) => file,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(error) => return Err(error).context("cannot read MCP approval key"),
    };
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        let metadata = file.metadata()?;
        // SAFETY: geteuid takes no arguments and has no memory-safety preconditions.
        let owner = unsafe { libc::geteuid() };
        anyhow::ensure!(
            metadata.mode() & 0o077 == 0 && metadata.uid() == owner,
            "MCP approval key must be private and owned by the current user"
        );
    }
    let mut bytes = Vec::new();
    file.take(33).read_to_end(&mut bytes)?;
    let key = bytes
        .try_into()
        .map_err(|_| anyhow::anyhow!("invalid MCP approval key"))?;
    Ok(Some(key))
}