Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions tests/tools/test_mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
27 changes: 26 additions & 1 deletion tools/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"(?:"
Expand Down Expand Up @@ -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
Expand Down
Loading