9eb13d07b6
On macOS with TCC (Transparency, Consent, and Control), os.getcwd() raises PermissionError: [Errno 1] Operation not permitted — not FileNotFoundError — when the process CWD is under a protected location (~/Documents, ~/Desktop, ~/Downloads) and the calling process lacks Full Disk Access. _safe_getcwd() only caught FileNotFoundError (deleted CWD), so the terminal-tool cleanup thread, which calls _get_env_config() → _safe_getcwd() every 60 s, logged a full stack trace on every tick. This accumulated hundreds of MB of noise in mcp-stderr.log (observed 184 MB on a single-day session) without breaking functionality — the cleanup thread's outer try/except swallowed the exception, but exc_info=True kept emitting the traceback. Fix: add PermissionError to the existing except clause so the fallback chain (TERMINAL_CWD → $HOME) runs, matching the existing pattern for deleted-CWD recovery (#17558). Complements #66306, which handles PermissionError from subprocess.Popen(cwd=...) for an inaccessible configured cwd on Linux; this handles the distinct case where the live process CWD itself is TCC-blocked. Tests cover: PermissionError fallback to $HOME, TERMINAL_CWD priority, FileNotFoundError regression, happy path unchanged, and unrelated OSError (NotADirectoryError) still propagating instead of being swallowed.
92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""Regression tests for _safe_getcwd() PermissionError handling (macOS TCC).
|
|
|
|
Background: on macOS, when the process CWD is under a TCC-protected location
|
|
(``~/Documents``, ``~/Desktop``, ``~/Downloads``) and the calling process
|
|
lacks Full Disk Access, ``os.getcwd()`` raises ``PermissionError: [Errno 1]
|
|
Operation not permitted`` — not ``FileNotFoundError``.
|
|
|
|
Before the fix, ``_safe_getcwd`` only caught ``FileNotFoundError``, so the
|
|
terminal-tool cleanup thread (which calls ``_get_env_config()`` →
|
|
``_safe_getcwd()`` every 60 s) logged a full stack trace on every tick,
|
|
accumulating hundreds of MB of noise in ``mcp-stderr.log`` without breaking
|
|
functionality. After the fix, ``PermissionError`` falls back to
|
|
``TERMINAL_CWD`` or ``$HOME`` just like a deleted CWD already does.
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
import tools.terminal_tool as terminal_tool
|
|
|
|
|
|
class _GetcwdPatcher:
|
|
"""Context manager that temporarily replaces ``os.getcwd`` with *fn*."""
|
|
|
|
def __init__(self, fn):
|
|
self.fn = fn
|
|
self._original = None
|
|
|
|
def __enter__(self):
|
|
self._original = os.getcwd
|
|
os.getcwd = self.fn
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
os.getcwd = self._original
|
|
|
|
|
|
def _raise(exc):
|
|
def _fn():
|
|
raise exc
|
|
|
|
return _fn
|
|
|
|
|
|
def test_permission_error_falls_back_to_home(monkeypatch):
|
|
"""macOS TCC EPERM on os.getcwd() must fall back to $HOME, not propagate."""
|
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
|
|
|
with _GetcwdPatcher(_raise(PermissionError(1, "Operation not permitted"))):
|
|
result = terminal_tool._safe_getcwd()
|
|
|
|
assert result == os.path.expanduser("~")
|
|
|
|
|
|
def test_permission_error_prefers_terminal_cwd(monkeypatch):
|
|
"""TERMINAL_CWD takes priority over $HOME when the live CWD is TCC-blocked."""
|
|
monkeypatch.setenv("TERMINAL_CWD", "/custom/from/env")
|
|
|
|
with _GetcwdPatcher(_raise(PermissionError(1, "Operation not permitted"))):
|
|
result = terminal_tool._safe_getcwd()
|
|
|
|
assert result == "/custom/from/env"
|
|
|
|
|
|
def test_file_not_found_still_handled(monkeypatch):
|
|
"""Regression guard: the existing FileNotFoundError path must keep working."""
|
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
|
|
|
with _GetcwdPatcher(_raise(FileNotFoundError(2, "No such file or directory"))):
|
|
result = terminal_tool._safe_getcwd()
|
|
|
|
assert result == os.path.expanduser("~")
|
|
|
|
|
|
def test_happy_path_unchanged():
|
|
"""Normal os.getcwd() must pass through untouched."""
|
|
# Don't patch getcwd — use the real one
|
|
result = terminal_tool._safe_getcwd()
|
|
assert result == os.getcwd()
|
|
|
|
|
|
def test_os_error_not_swallowed(monkeypatch):
|
|
"""Unrelated OSError subclasses must still propagate (don't over-catch)."""
|
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
|
|
|
# NotADirectoryError is an OSError but neither FileNotFoundError nor
|
|
# PermissionError — it should escape so callers see the real problem.
|
|
with pytest.raises(OSError):
|
|
with _GetcwdPatcher(_raise(NotADirectoryError(20, "Not a directory"))):
|
|
terminal_tool._safe_getcwd()
|