magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::security::is_credential_like_key;
use std::{
    collections::BTreeMap,
    ffi::{OsStr, OsString},
    process::Command,
};

/// The explicit environment contract for a deny-by-default child process.
///
/// These profiles are intentionally separate: a child receives only the ambient variables
/// declared by its profile, plus the portable Windows startup baseline where that profile needs
/// it. Values supplied explicitly by MCP configuration are applied after the ambient filter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SubprocessEnvProfile {
    /// Hook and other sanitized shell execution: shell locale and identity variables.
    Shell,
    /// LSP servers intentionally receive PATH only.
    Lsp,
    /// MCP stdio servers receive PATH and the portable baseline before configured values.
    McpStdio,
}

#[derive(Debug, Clone, Copy)]
struct ProfileRules {
    exact: &'static [&'static str],
    locale: &'static [&'static str],
    include_windows_startup: bool,
}

const EMPTY: &[&str] = &[];
const LOCALE_VARIABLES: &[&str] = &[
    "LC_CTYPE",
    "LC_NUMERIC",
    "LC_TIME",
    "LC_COLLATE",
    "LC_MONETARY",
    "LC_MESSAGES",
    "LC_PAPER",
    "LC_NAME",
    "LC_ADDRESS",
    "LC_TELEPHONE",
    "LC_MEASUREMENT",
    "LC_IDENTIFICATION",
    "LC_ALL",
];
const SHELL_VARIABLES: &[&str] = &["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG"];
const PATH_ONLY: &[&str] = &["PATH"];

#[cfg(windows)]
fn matches_environment_name(actual: &str, expected: &str) -> bool {
    actual.eq_ignore_ascii_case(expected)
}

#[cfg(not(windows))]
fn matches_environment_name(actual: &str, expected: &str) -> bool {
    actual == expected
}

impl SubprocessEnvProfile {
    fn rules(self) -> ProfileRules {
        match self {
            Self::Shell => ProfileRules {
                exact: SHELL_VARIABLES,
                locale: LOCALE_VARIABLES,
                include_windows_startup: true,
            },
            Self::Lsp => ProfileRules {
                exact: PATH_ONLY,
                locale: EMPTY,
                include_windows_startup: false,
            },
            Self::McpStdio => ProfileRules {
                exact: PATH_ONLY,
                locale: EMPTY,
                include_windows_startup: true,
            },
        }
    }

    /// Returns whether an ambient variable belongs to this profile.
    ///
    /// Credential-shaped names are rejected even when a future profile allowlist could otherwise
    /// match them. Explicit MCP configuration is deliberately handled separately and is not
    /// treated as ambient input.
    pub(crate) fn allows_ambient(self, key: &OsStr) -> bool {
        let Some(key) = key.to_str() else {
            return false;
        };
        if is_credential_like_key(key) {
            return false;
        }

        let rules = self.rules();
        (rules.include_windows_startup && is_windows_startup_variable(key))
            || rules
                .exact
                .iter()
                .any(|expected| matches_environment_name(key, expected))
            || rules
                .locale
                .iter()
                .any(|expected| matches_environment_name(key, expected))
    }
}

/// Clear inherited variables and apply one explicit ambient environment profile.
pub(crate) fn apply_profile(command: &mut Command, profile: SubprocessEnvProfile) {
    apply_profile_from(command, profile, std::env::vars_os());
}

/// Apply a profile, then overlay explicitly configured MCP values.
///
/// Configured values intentionally have precedence over ambient PATH and platform startup values;
/// this preserves the MCP `env` contract without reopening ambient inheritance.
pub(crate) fn apply_configured_profile(
    command: &mut Command,
    profile: SubprocessEnvProfile,
    configured: &BTreeMap<String, String>,
) {
    apply_configured_profile_from(command, profile, std::env::vars_os(), configured);
}

fn apply_profile_from(
    command: &mut Command,
    profile: SubprocessEnvProfile,
    environment: impl IntoIterator<Item = (OsString, OsString)>,
) {
    command.env_clear();
    for (key, value) in environment {
        if profile.allows_ambient(&key) {
            command.env(key, value);
        }
    }
}

fn apply_configured_profile_from(
    command: &mut Command,
    profile: SubprocessEnvProfile,
    environment: impl IntoIterator<Item = (OsString, OsString)>,
    configured: &BTreeMap<String, String>,
) {
    apply_profile_from(command, profile, environment);
    command.envs(configured);
}

#[cfg(windows)]
fn is_windows_startup_variable(key: &str) -> bool {
    ["ComSpec", "PATHEXT", "SystemRoot", "WINDIR"]
        .iter()
        .any(|name| matches_environment_name(key, name))
}

#[cfg(not(windows))]
fn is_windows_startup_variable(_key: &str) -> bool {
    false
}