diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index 5f05b0fc34328..0d7e8358123dd 100644 --- a/tests/tools/test_mcp_tool.py +++ b/tests/tools/test_mcp_tool.py @@ -1278,16 +1278,19 @@ def test_windows_location_vars_passed_without_secrets(self): fake_env = { "PATH": r"C:\Windows\System32", - "ProgramFiles": r"C:\Program Files", - "ProgramData": r"C:\ProgramData", - "ProgramW6432": r"C:\Program Files", + "PROGRAMFILES": r"C:\Program Files", + "PROGRAMDATA": r"C:\ProgramData", + "PROGRAMW6432": r"C:\Program Files", "LOCALAPPDATA": r"C:\Users\alice\AppData\Local", "APPDATA": r"C:\Users\alice\AppData\Roaming", "USERPROFILE": r"C:\Users\alice", "GITHUB_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "OPENAI_API_KEY": "sk-proj-abc123", } - with patch.dict("os.environ", fake_env, clear=True): + with ( + patch("tools.mcp_tool.os.name", "nt"), + patch.dict("os.environ", fake_env, clear=True), + ): result = _build_safe_env(None) assert result["ProgramFiles"] == r"C:\Program Files" @@ -1299,6 +1302,20 @@ def test_windows_location_vars_passed_without_secrets(self): assert "GITHUB_TOKEN" not in result assert "OPENAI_API_KEY" not in result + def test_posix_preserves_uppercase_windows_location_var_spelling(self): + """POSIX environments retain source case for allowed Windows names.""" + from tools.mcp_tool import _build_safe_env + + fake_env = {"PROGRAMFILES": "/opt/windows-program-files"} + with ( + patch("tools.mcp_tool.os.name", "posix"), + patch.dict("os.environ", fake_env, clear=True), + ): + result = _build_safe_env(None) + + assert result["PROGRAMFILES"] == "/opt/windows-program-files" + assert "ProgramFiles" not in result + # --------------------------------------------------------------------------- # _sanitize_error diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index ffcdff532db1b..676e112044fad 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -652,6 +652,24 @@ def _jittered(seconds: float) -> float: "WINDIR", }) +# Canonical (mixed-case) spellings for the Windows location vars whose +# documented names are not all-uppercase. CPython normalizes every key in +# ``os.environ`` to UPPERCASE on Windows (``os.name == "nt"``), so iterating it +# yields e.g. ``"PROGRAMFILES"`` even though the variable is documented and +# consumed as ``"ProgramFiles"``. Launcher-style tools (and the test contract) +# expect the canonical spelling, so map the upper-cased key back to it on +# Windows only. Keys not in this map keep their original spelling +# (all-uppercase vars like ``APPDATA`` are already canonical). +_WINDOWS_CANONICAL_ENV_KEYS = { + "PROGRAMFILES": "ProgramFiles", + "PROGRAMFILES(X86)": "ProgramFiles(x86)", + "PROGRAMW6432": "ProgramW6432", + "PROGRAMDATA": "ProgramData", + "COMMONPROGRAMFILES": "CommonProgramFiles", + "COMMONPROGRAMFILES(X86)": "CommonProgramFiles(x86)", + "COMMONPROGRAMW6432": "CommonProgramW6432", +} + # Regex for credential patterns to strip from error messages _CREDENTIAL_PATTERN = re.compile( r"(?:" @@ -759,7 +777,14 @@ def _build_safe_env(user_env: Optional[dict]) -> dict: or key.startswith("XDG_") or (get_secret_source is not None and get_secret_source(key)) ): - env[key] = value + # Restore mixed-case Windows location names only where CPython + # normalizes os.environ keys. POSIX must retain the source spelling. + canonical_key = ( + _WINDOWS_CANONICAL_ENV_KEYS.get(key.upper(), key) + if os.name == "nt" + else key + ) + env[canonical_key] = value if user_env: env.update(user_env) return env