revert(macos): remove the TCC interpreter anchor — anchored copies could not load libpython

Reverts the interpreter-anchor halves of #95131 and #95478 (the anchor
module, its doctor check, and the update-time refresh). On real Macs the
anchored real-file copy of the uv interpreter dies in dyld: its LC_RPATH
(@executable_path/../lib) resolves into venv/lib/, which holds no
libpython — bricking EVERY hermes command including update and doctor
(#95425), and the re-pointed python3 aliases lost the stdlib
(ModuleNotFoundError: encodings, #95541). Linux CI could not catch this:
the fixture interpreters were one-byte fakes with no dynamic linking.

Kept: managed_uv._macos_sign_managed_python (#82529, @notkisk) — the
identifier-DR signing of repair generations is independent of the anchor
and unaffected by the dyld issue (it signs binaries IN PLACE in their
store, where their rpath is valid).

Added: doctor's check_macos_tcc_anchor_removed() heals venvs the anchor
already converted — restores bin/python to a symlink at the recorded
source (the anchor's own marker file) and re-points aliases; prints the
manual one-liner if the heal itself fails. Users whose CLI is fully
bricked can run the workaround from #95425 directly.

Re-land criteria: a dylib-complete anchor design (bundle libpython or
rewrite LC_RPATH), verified on macOS hardware BEFORE merge. Credit to
@kim-miram (#95358), @kokhlo (#95476), @zengzheqing (#95551) for the
forward-fix diagnoses that mapped the failure, and to the #95425/#95541
reporters.
This commit is contained in:
Teknium
2026-08-26 06:36:18 -07:00
parent 2b8b4542e9
commit 2f9e187001
5 changed files with 131 additions and 749 deletions
+46 -43
View File
@@ -1154,52 +1154,54 @@ def _macos_desktop_dr(app: Path) -> str | None:
return (proc.stdout or "") + (proc.stderr or "")
def check_macos_tcc_anchor(should_fix: bool = False) -> None:
"""macOS TCC anchor check (issue #85345).
def check_macos_tcc_anchor_removed() -> None:
"""Detect and repair a venv bricked by the reverted TCC anchor.
TCC keys permission grants to the interpreter's resolved path; uv-managed
interpreters move on every patch bump, orphaning grants and re-triggering
the permission-prompt storm after each update. A stable real-file copy of
the interpreter inside the venv keeps the TCC client path constant.
Silent on non-macOS; informational when the interpreter already has a
stable path.
The anchor (#95131/#95478, reverted) replaced ``venv/bin/python`` with a
real-file copy of the uv-store interpreter. On real Macs that copy could
not start: its ``LC_RPATH`` (``@executable_path/../lib``) resolved to
``venv/lib/``, which holds no libpython — every hermes command died in
dyld (#95425), and re-pointed aliases lost the stdlib (#95541). The
revert stops NEW anchors; this check heals venvs the anchor already
converted, by restoring ``bin/python`` to a symlink pointing at the
recorded source interpreter (the marker file the anchor wrote).
Silent on non-macOS and on venvs the anchor never touched.
"""
try:
from hermes_cli.macos_tcc_anchor import ensure_tcc_anchor, tcc_anchor_state
status, detail = tcc_anchor_state()
if status == "skip":
if detail == "interpreter not uv-managed (stable path)":
check_ok("Python interpreter path is stable", "(not uv-managed)")
return
if status == "active":
if sys.platform != "darwin":
return
# Resolved at call time via the module global so tests can retarget it.
root = Path(globals()["__file__"]).resolve().parents[1]
for name in ("venv", ".venv"):
venv_bin = root / name / "bin"
marker = venv_bin / ".tcc-anchor-source"
if not marker.is_file():
continue
try:
source = Path(marker.read_text(encoding="utf-8").strip())
venv_py = venv_bin / "python"
if source.is_file() and venv_py.is_file() and not venv_py.is_symlink():
tmp = venv_bin / ".python-unanchor-tmp"
tmp.unlink(missing_ok=True)
os.symlink(source, tmp)
os.replace(tmp, venv_py)
# Restore versioned aliases to point at bin/python.
for alias in venv_bin.glob("python3*"):
if alias.is_symlink() or alias.is_file():
alias_tmp = venv_bin / f".{alias.name}.unanchor-tmp"
alias_tmp.unlink(missing_ok=True)
os.symlink("python", alias_tmp)
os.replace(alias_tmp, alias)
marker.unlink(missing_ok=True)
check_ok(
"macOS TCC anchor active",
f"(interpreter pinned at {detail}; grants survive updates)",
"macOS TCC anchor removed",
f"({name}/bin/python restored to a symlink; the anchor "
"(#95425/#95541) is reverted)",
)
return
label = "stale" if status == "stale" else "missing"
if should_fix:
anchored = ensure_tcc_anchor()
if anchored is not None:
check_ok(
"macOS TCC anchor installed",
f"(interpreter pinned at {anchored}; grants survive updates)",
)
return
except Exception as e: # diagnostics must never crash
check_warn(
"macOS TCC anchor could not be installed",
"macOS will re-prompt for permissions after each Python update",
"macOS TCC anchor cleanup failed",
f"({e}) — restore manually: ln -sf $(cat {marker}) {venv_bin / 'python'}",
)
return
check_warn(
f"macOS TCC anchor {label}",
"the uv-managed interpreter path changes on every Python patch "
"bump, so macOS will re-prompt for permissions after each update. "
"Run `hermes doctor --fix` to pin the interpreter at a stable path.",
)
except Exception as e: # diagnostics must never crash
check_warn(f"macOS TCC anchor check failed: {e}")
def check_macos_full_disk_access() -> None:
@@ -1421,9 +1423,10 @@ def run_doctor(args):
else:
check_warn("Not in virtual environment", "(recommended)")
# macOS TCC anchor (issue #85345): uv-managed interpreter paths move on
# every patch bump and orphan TCC grants. Silent on non-macOS.
check_macos_tcc_anchor(should_fix)
# macOS TCC anchor REVERTED (#95425/#95541: anchored copies couldn't load
# libpython — every hermes command died in dyld). This heals venvs the
# anchor already converted. Silent on non-macOS.
check_macos_tcc_anchor_removed()
# macOS Full Disk Access (issue #52010 follow-up): one grant silences
# every per-folder prompt permanently. Silent on non-macOS.
-337
View File
@@ -1,337 +0,0 @@
"""Stable macOS TCC anchor for the uv-managed Python interpreter (issue #85345).
macOS keys TCC grants (Files & Folders, Photos, Media Library, Automation,
...) to the *resolved absolute path* of the client binary. Hermes' interpreter
is managed by uv and lives at ``~/.local/share/uv/python/cpython-<ver>-macos-*/
bin/python*``; every patch bump materializes a NEW versioned directory, so the
TCC client string changes and every prior grant is orphaned — macOS re-prompts
for all permissions after each update.
Symlinks do not help: TCC resolves through them to the versioned store path
before matching (the venv's ``bin/python`` -> store symlink is exactly why the
client is reported as ``.../cpython-3.11.15-macos-.../bin/python3.11``).
The anchor: replace the venv's ``bin/python`` symlink with a *real-file copy*
of the interpreter binary. The venv path (``<checkout>/venv/bin/python``) is
stable across ``hermes update``, and because it is a regular file there is no
symlink for TCC to resolve — so the TCC client path stays constant across
interpreter patch bumps. ``pyvenv.cfg`` keeps pointing at the uv store (``home``),
which still provides the stdlib exactly as it does today.
The anchor self-heals: when ``hermes update`` / ``hermes doctor`` runs and the
venv python is a symlink again (uv re-created it) or the recorded source no
longer matches the current interpreter (patch bump), the copy is refreshed.
Versioned alias symlinks (``python3``, ``python3.11``, ...) inside the venv bin
dir are re-pointed at the anchor so no alias resolves back into the versioned
store.
All functions are no-ops on non-macOS and for interpreters that are not
uv-managed (Homebrew/system Python has a stable path already). This module is
pure/best-effort: it never raises to callers (update/doctor must never break
because of it).
"""
from __future__ import annotations
import logging
import os
import platform
import shutil
import tempfile
from pathlib import Path
from hermes_constants import venv_python_path
logger = logging.getLogger(__name__)
# Marker file (inside the venv bin dir) recording the uv-store interpreter
# file the anchor copy was taken from. Used to detect patch-bump staleness.
_MARKER_NAME = ".tcc-anchor-source"
def _sibling_names() -> tuple[str, ...]:
"""Alias symlinks uv creates inside the venv bin dir.
Derived from the RUNNING interpreter's version rather than a hardcoded
minor-version list, so a future Python bump can't silently leave an alias
resolving back into the versioned store.
"""
import sys as _sys
return ("python3", f"python3.{_sys.version_info.minor}")
def _store_bin_names() -> tuple[str, ...]:
"""Preferred interpreter file names inside a store ``bin`` dir.
Versioned name first (from the running interpreter) so the real binary is
picked over the ``python3`` alias; generic fallbacks after.
"""
import sys as _sys
return (f"python3.{_sys.version_info.minor}", "python3", "python")
# Path fragments that identify a MANAGED macOS CPython store layout — a
# store whose path changes across updates, orphaning path-keyed TCC grants.
# Two roots qualify:
# - uv store patch bumps: .../uv/python/cpython-<ver>-macos-*/bin/...
# - CVE-repair generations: .../.hermes-runtime/python/generation-*/
# cpython-<ver>-macos-*/bin/...
# (repair_vulnerable_runtime() rebuilds the venv against a generation store,
# replacing the anchored bin/python with a fresh symlink — without the second
# root the anchor would read 'not uv-managed' after every SQLite CVE repair
# and never re-anchor, issue #82427.)
_STORE_COMMON_MARKERS = ("cpython-", "-macos-")
_STORE_ROOT_MARKERS = ("/uv/python/", "/.hermes-runtime/python/")
def is_macos() -> bool:
"""True on macOS (the only platform with TCC)."""
return platform.system() == "Darwin"
def _is_uv_macos_store(path: str | Path) -> bool:
"""True when *path* lives inside a managed macOS CPython store."""
text = str(path).replace("\\", "/")
if not all(marker in text for marker in _STORE_COMMON_MARKERS):
return False
return any(root in text for root in _STORE_ROOT_MARKERS)
def _venv_dir(project_root: Path | None = None) -> Path | None:
"""Return the checkout's venv dir, mirroring ``managed_uv``'s probing.
``venv`` wins when it holds an interpreter (managed layout takes
precedence); otherwise fall back to ``.venv`` (uv-default/dev checkouts).
Returns None when neither holds an interpreter.
"""
root = (
Path(project_root)
if project_root is not None
else Path(__file__).resolve().parents[1]
)
for name in ("venv", ".venv"):
candidate = root / name
venv_py = venv_python_path(candidate)
if venv_py.is_file() or venv_py.is_symlink():
return candidate
return None
def _interpreter_file(src: str | Path) -> Path | None:
"""Return the interpreter binary file at/inside *src*.
*src* is either a resolved store binary path (symlinked venv layout) or a
store ``bin`` dir read from ``pyvenv.cfg`` ``home`` (anchored layout).
"""
p = Path(src)
if p.is_file():
return p
if not p.is_dir():
return None
for name in _store_bin_names():
candidate = p / name
try:
if candidate.is_file():
return candidate
except OSError:
continue
# Any other versioned binary on disk (store built by a different Python
# minor than the one running this code — e.g. after a major bump, or in
# fixtures). Sorted for determinism; versioned names only, so the
# ``python3`` alias never shadows the real binary here.
try:
for candidate in sorted(p.glob("python3.*")):
if candidate.is_file() and not candidate.name.endswith((".dSYM", ".txt")):
return candidate
except OSError:
pass
return None
def _interpreter_source(venv_dir: Path) -> str | None:
"""Return the interpreter file the venv currently resolves to.
A symlinked ``bin/python`` (uv's layout) resolves to the versioned store
binary. A regular-file anchor instead reads ``pyvenv.cfg`` ``home`` (the
base interpreter's bin dir) — that is what the anchor copy was taken from
and where the stdlib still comes from.
"""
venv_py = venv_python_path(venv_dir)
if venv_py.is_symlink():
try:
resolved = venv_py.resolve(strict=False)
except OSError:
return None
if resolved.is_file():
return str(resolved)
return None
cfg = venv_dir / "pyvenv.cfg"
try:
text = cfg.read_text(encoding="utf-8", errors="replace")
except OSError:
return None
for line in text.splitlines():
if line.strip().lower().startswith("home"):
_, _, value = line.partition("=")
home = value.strip()
if home:
return str(_interpreter_file(Path(home)))
return None
def _anchor_marker(venv_bin: Path) -> Path:
return venv_bin / _MARKER_NAME
def _repoint_aliases(venv_bin: Path, anchor: Path) -> None:
"""Re-point uv alias symlinks at the stable anchor.
``python3`` / ``python3.11`` inside the venv bin dir currently resolve into
the versioned store; anything spawned through them would still churn TCC.
Only symlinks that resolve into the uv store are touched.
"""
# Union of the running interpreter's expected aliases and every versioned
# alias actually on disk — a store built by a different Python minor than
# the one running this code must still get its aliases repointed.
names = set(_sibling_names())
try:
names.update(p.name for p in venv_bin.glob("python3.*") if p.is_symlink())
except OSError:
pass
for name in sorted(names):
alias = venv_bin / name
try:
if not alias.is_symlink():
continue
if not _is_uv_macos_store(str(alias.resolve(strict=False))):
continue
tmp = venv_bin / f".{name}.tcc-tmp"
try:
os.symlink(anchor.name, tmp)
os.replace(tmp, alias)
except OSError:
try:
tmp.unlink(missing_ok=True)
except OSError:
pass
except OSError:
continue
def _install_anchor(venv_dir: Path, source_file: Path) -> None:
"""Replace ``bin/python`` with a real-file copy of *source_file*.
Atomic (temp file + rename) so a crash mid-copy cannot leave the venv
interpreter half-written. Writes the source marker and re-points alias
symlinks so the whole venv bin dir resolves to stable paths.
"""
venv_py = venv_python_path(venv_dir)
venv_bin = venv_py.parent
venv_bin.mkdir(parents=True, exist_ok=True)
fd, tmp_name = tempfile.mkstemp(prefix=".python-tcc-", dir=str(venv_bin))
os.close(fd)
tmp_path = Path(tmp_name)
try:
shutil.copy2(source_file, tmp_path)
os.chmod(tmp_path, source_file.stat().st_mode | 0o111)
# Give the anchor copy a stable identifier-pinned signature BEFORE it
# goes live. copy2 carries over the source build's signature, whose
# designated requirement is cdhash-based for ad-hoc/linker-signed
# python-build-standalone binaries — meaning every anchor REFRESH
# (patch bump, CVE repair) would still change the stored csreq and
# orphan the grant despite the stable path. Identifier-DR signing
# (same mechanism as managed_uv's generation signing, #82427) keeps
# the csreq constant across refreshes. Best-effort: a failed sign
# leaves the copy usable, just without refresh-stable signing.
try:
from hermes_cli.managed_uv import _macos_sign_managed_python
_macos_sign_managed_python(tmp_path)
except Exception: # pragma: no cover - never block the anchor
logger.debug("anchor copy signing skipped", exc_info=True)
os.replace(tmp_path, venv_py)
_anchor_marker(venv_bin).write_text(str(source_file), encoding="utf-8")
_repoint_aliases(venv_bin, venv_py)
except Exception:
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
raise
def ensure_tcc_anchor(project_root: Path | None = None) -> Path | None:
"""Pin a stable interpreter anchor for macOS TCC (issue #85345).
No-op (returns None) on non-macOS, when no venv interpreter exists, or when
the interpreter is not uv-managed. Otherwise makes the venv's ``bin/python``
a real-file copy of the current uv-store interpreter and returns its path.
Idempotent: a fresh anchor is returned unchanged. Best-effort — returns
None (and logs) if the copy fails; callers must never depend on success.
"""
if not is_macos():
return None
venv_dir = _venv_dir(project_root)
if venv_dir is None:
return None
venv_py = venv_python_path(venv_dir)
if not (venv_py.is_file() or venv_py.is_symlink()):
return None
source = _interpreter_source(venv_dir)
if source is None or not _is_uv_macos_store(source):
return None
source_file = _interpreter_file(source)
if source_file is None:
return None
if not venv_py.is_symlink():
# Already anchored — refresh only when the interpreter changed.
marker = _anchor_marker(venv_py.parent)
try:
if marker.is_file() and marker.read_text(encoding="utf-8").strip() == str(
source_file
):
return venv_py
except OSError:
pass
try:
_install_anchor(venv_dir, source_file)
except Exception as exc: # best-effort: never break update/doctor
logger.warning("macOS TCC anchor install failed: %s", exc)
return None
return venv_py
def tcc_anchor_state(project_root: Path | None = None) -> tuple[str, str]:
"""Report the anchor state for ``hermes doctor``.
Returns ``(status, detail)`` with status one of:
- ``"skip"`` — not applicable (non-macOS, no venv, or not uv-managed)
- ``"active"`` — venv interpreter is pinned at a stable real-file anchor
- ``"stale"`` — pinned but the interpreter changed since the last copy
- ``"missing"`` — uv-managed interpreter with no stable anchor installed
"""
if not is_macos():
return "skip", "not macOS"
venv_dir = _venv_dir(project_root)
if venv_dir is None:
return "skip", "no venv interpreter"
venv_py = venv_python_path(venv_dir)
if not (venv_py.is_file() or venv_py.is_symlink()):
return "skip", "no venv interpreter"
source = _interpreter_source(venv_dir)
if source is None or not _is_uv_macos_store(source):
return "skip", "interpreter not uv-managed (stable path)"
if not venv_py.is_symlink():
marker = _anchor_marker(venv_py.parent)
try:
if marker.is_file() and marker.read_text(encoding="utf-8").strip() == str(
source
):
return "active", str(venv_py)
except OSError:
pass
return "stale", str(venv_py)
return "missing", str(venv_py)
+7 -14
View File
@@ -7080,20 +7080,13 @@ def _cmd_update_impl(args, gateway_mode: bool):
"fully quit & relaunch once."
)
# ── macOS TCC anchor (issue #85345) ────────────────────────────
# uv-managed interpreters move on every patch bump, orphaning macOS
# TCC grants and re-triggering the permission-prompt storm. Pin a
# real-file copy of the interpreter inside the venv so the TCC client
# path stays stable across updates. Best-effort only; the doctor
# check re-applies it if this runs from pre-fix code.
try:
from hermes_cli.macos_tcc_anchor import ensure_tcc_anchor
tcc_anchored = ensure_tcc_anchor(_m().PROJECT_ROOT)
if tcc_anchored is not None:
print(f" ✓ macOS TCC anchor: interpreter pinned at {tcc_anchored}")
except Exception as _tcc_exc:
logger.debug("macOS TCC anchor refresh failed: %s", _tcc_exc)
# NOTE: the macOS TCC interpreter anchor that used to refresh here
# (#95131/#95478) is REVERTED: the anchored real-file copy could not
# load libpython (LC_RPATH resolved into venv/lib/), bricking every
# hermes command on real Macs (#95425), and re-pointed aliases lost
# the stdlib (#95541). `hermes doctor` now heals already-anchored
# venvs back to symlinks. Re-land requires a dylib-complete design
# verified on macOS hardware first.
# ── Post-update state.db integrity guard (#68474) ─────────────────
# Verify that state.db survived the update intact. If the live file
-355
View File
@@ -1,355 +0,0 @@
"""Tests for the macOS TCC anchor (issue #85345).
The anchor makes the TCC client path stable by replacing the venv's
``bin/python`` symlink (which resolves into uv's versioned store) with a
real-file copy of the interpreter. All tests run on Linux against fake
checkout/uv-store layouts; ``platform.system`` is monkeypatched to simulate
macOS.
"""
import os
from pathlib import Path
import pytest
import hermes_cli.doctor as doctor
import hermes_cli.macos_tcc_anchor as tcc
from hermes_constants import venv_python_path
_STORE_ROOT = "cpython-3.11.15-macos-aarch64-none"
def _darwin(monkeypatch):
monkeypatch.setattr(tcc.platform, "system", lambda: "Darwin")
def _linux(monkeypatch):
monkeypatch.setattr(tcc.platform, "system", lambda: "Linux")
def _build_store(tmp_path, version: str = "3.11.15") -> Path:
store = (
tmp_path
/ "uv-store"
/ "uv"
/ "python"
/ f"cpython-{version}-macos-aarch64-none"
)
store_bin = store / "bin"
store_bin.mkdir(parents=True)
store_py = store_bin / "python3.11"
store_py.write_bytes(f"#!fake interpreter {version}".encode())
store_py.chmod(0o755)
return store_bin
def _build_checkout(
tmp_path,
*,
store_bin: Path | None = None,
version: str = "3.11.15",
anchored: bool = False,
homebrew: bool = False,
) -> Path:
root = tmp_path / "checkout"
venv = root / ".venv"
venv_bin = venv / "bin"
venv_bin.mkdir(parents=True)
if homebrew:
brew = tmp_path / "opt" / "homebrew" / "bin"
brew.mkdir(parents=True)
brew_py = brew / "python3.14"
brew_py.write_bytes(b"#!homebrew")
brew_py.chmod(0o755)
(venv / "pyvenv.cfg").write_text(f"home = {brew}\n")
os.symlink(brew_py, venv_bin / "python")
os.symlink(brew_py, venv_bin / "python3")
return root
if store_bin is None:
store_bin = _build_store(tmp_path, version)
(venv / "pyvenv.cfg").write_text(f"home = {store_bin}\n")
store_py = store_bin / "python3.11"
if anchored:
venv_py = venv_bin / "python"
venv_py.write_bytes(store_py.read_bytes())
venv_py.chmod(0o755)
(venv_bin / ".tcc-anchor-source").write_text(str(store_py), encoding="utf-8")
os.symlink(venv_py, venv_bin / "python3")
else:
os.symlink(store_py, venv_bin / "python")
os.symlink(store_py, venv_bin / "python3")
return root
class TestUvStoreDetection:
def test_matches_uv_macos_store_path(self):
path = (
"/Users/u/.local/share/uv/python/"
"cpython-3.11.15-macos-aarch64-none/bin/python3.11"
)
assert tcc._is_uv_macos_store(path)
def test_matches_hermes_runtime_repair_generation(self):
# repair_vulnerable_runtime() rebuilds the venv against a generation
# store under .hermes-runtime/python/ — no /uv/python/ segment. The
# anchor must recognize it or every SQLite CVE repair silently
# un-anchors the interpreter (issue #82427 integration).
path = (
"/Users/u/hermes-agent/.hermes-runtime/python/"
"generation-a1b2c3/cpython-3.11.15-macos-aarch64-none/bin/python3.11"
)
assert tcc._is_uv_macos_store(path)
def test_rejects_homebrew_interpreter(self):
path = (
"/opt/homebrew/Cellar/python@3.14/3.14.6/Frameworks/"
"Python.framework/Versions/3.14/bin/python3.14"
)
assert not tcc._is_uv_macos_store(path)
def test_rejects_linux_interpreter(self):
assert not tcc._is_uv_macos_store("/usr/bin/python3")
def test_rejects_uv_store_on_linux(self):
path = (
"/home/u/.local/share/uv/python/"
"cpython-3.11.15-x86_64-unknown-linux-gnu/bin/python3.11"
)
assert not tcc._is_uv_macos_store(path)
class TestEnsureTccAnchor:
def test_noop_on_non_macos(self, tmp_path, monkeypatch):
_linux(monkeypatch)
root = _build_checkout(tmp_path, store_bin=_build_store(tmp_path))
venv_py = venv_python_path(root / ".venv")
assert tcc.ensure_tcc_anchor(root) is None
assert venv_py.is_symlink() # untouched
def test_install_signs_the_anchor_copy(self, tmp_path, monkeypatch):
"""The anchor copy gets identifier-DR signing before going live —
without it every refresh changes the stored csreq (cdhash-based for
ad-hoc source builds) and orphans the grant despite the stable path."""
_darwin(monkeypatch)
signed = []
import hermes_cli.managed_uv as managed_uv
monkeypatch.setattr(
managed_uv, "_macos_sign_managed_python", lambda p: signed.append(Path(p)) or True
)
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
anchored = tcc.ensure_tcc_anchor(root)
assert anchored is not None
# Signing ran on the temp copy inside the venv bin dir (pre-rename).
assert len(signed) == 1
assert signed[0].parent == anchored.parent
def test_anchors_repair_generation_interpreter(self, tmp_path, monkeypatch):
"""A venv re-created against a .hermes-runtime CVE-repair generation
store must anchor too (issue #82427 integration)."""
_darwin(monkeypatch)
store = (
tmp_path
/ "checkout"
/ ".hermes-runtime"
/ "python"
/ "generation-a1b2c3"
/ "cpython-3.11.15-macos-aarch64-none"
)
store_bin = store / "bin"
store_bin.mkdir(parents=True)
store_py = store_bin / "python3.11"
store_py.write_bytes(b"#!fake generation interpreter")
store_py.chmod(0o755)
root = _build_checkout(tmp_path, store_bin=store_bin)
venv_py = venv_python_path(root / ".venv")
assert venv_py.is_symlink()
anchored = tcc.ensure_tcc_anchor(root)
assert anchored == venv_py
assert not venv_py.is_symlink()
assert venv_py.read_bytes() == store_py.read_bytes()
def test_anchors_uv_managed_interpreter(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
venv_py = venv_python_path(root / ".venv")
assert venv_py.is_symlink() # preconditions: uv layout
anchored = tcc.ensure_tcc_anchor(root)
assert anchored == venv_py
# The venv interpreter is now a real file, not a symlink into the
# versioned store — the TCC client path is stable.
assert venv_py.is_file() and not venv_py.is_symlink()
assert venv_py.read_bytes() == (store_bin / "python3.11").read_bytes()
assert os.access(venv_py, os.X_OK)
# Marker records the store binary the copy came from.
marker = venv_py.parent / ".tcc-anchor-source"
assert marker.read_text(encoding="utf-8").strip() == str(
store_bin / "python3.11"
)
# Alias symlinks no longer resolve into the versioned store.
alias = venv_py.parent / "python3"
assert not tcc._is_uv_macos_store(str(alias.resolve(strict=False)))
def test_idempotent(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin, anchored=True)
venv_py = venv_python_path(root / ".venv")
marker = venv_py.parent / ".tcc-anchor-source"
before = marker.read_text(encoding="utf-8")
anchored = tcc.ensure_tcc_anchor(root)
assert anchored == venv_py
assert venv_py.is_file() and not venv_py.is_symlink()
assert marker.read_text(encoding="utf-8") == before
def test_reanchors_after_patch_bump(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
old_bin = _build_store(tmp_path, version="3.11.15")
root = _build_checkout(tmp_path, store_bin=old_bin, anchored=True)
venv_py = venv_python_path(root / ".venv")
# Simulate `uv sync` bumping 3.11.15 -> 3.11.16: uv re-links the venv
# interpreter to the new store and rewrites pyvenv.cfg home.
new_bin = _build_store(tmp_path, version="3.11.16")
new_py = new_bin / "python3.11"
venv_py.unlink()
os.symlink(new_py, venv_py)
(root / ".venv" / "pyvenv.cfg").write_text(f"home = {new_bin}\n")
anchored = tcc.ensure_tcc_anchor(root)
assert anchored == venv_py
assert not venv_py.is_symlink()
assert venv_py.read_bytes() == new_py.read_bytes()
marker = venv_py.parent / ".tcc-anchor-source"
assert marker.read_text(encoding="utf-8").strip() == str(new_py)
def test_skips_homebrew_interpreter(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
root = _build_checkout(tmp_path, homebrew=True)
venv_py = venv_python_path(root / ".venv")
assert tcc.ensure_tcc_anchor(root) is None
assert venv_py.is_symlink() # untouched: stable identity already
def test_no_venv_returns_none(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
assert tcc.ensure_tcc_anchor(tmp_path / "missing") is None
def test_preserves_stdlib_source_home(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
cfg = root / ".venv" / "pyvenv.cfg"
tcc.ensure_tcc_anchor(root)
# pyvenv.cfg still points stdlib at the uv store — the anchor only
# changes the executable identity, not where the stdlib loads from.
assert f"home = {store_bin}" in cfg.read_text(encoding="utf-8")
class TestTccAnchorState:
def test_state_missing_then_active(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
status, detail = tcc.tcc_anchor_state(root)
assert status == "missing"
assert str(venv_python_path(root / ".venv")) in detail
tcc.ensure_tcc_anchor(root)
status, detail = tcc.tcc_anchor_state(root)
assert status == "active"
def test_state_skip_on_linux(self, tmp_path, monkeypatch):
_linux(monkeypatch)
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
status, detail = tcc.tcc_anchor_state(root)
assert status == "skip"
assert detail == "not macOS"
def test_state_skip_for_homebrew(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
root = _build_checkout(tmp_path, homebrew=True)
status, detail = tcc.tcc_anchor_state(root)
assert status == "skip"
assert "not uv-managed" in detail
def test_state_stale_after_patch_bump(self, tmp_path, monkeypatch):
_darwin(monkeypatch)
old_bin = _build_store(tmp_path, version="3.11.15")
root = _build_checkout(tmp_path, store_bin=old_bin, anchored=True)
# Simulate a patch bump where pyvenv.cfg now points at a new store
# while the venv still holds the previous anchor copy.
new_bin = _build_store(tmp_path, version="3.11.16")
(root / ".venv" / "pyvenv.cfg").write_text(f"home = {new_bin}\n")
status, _ = tcc.tcc_anchor_state(root)
assert status == "stale"
# ensure_tcc_anchor() refreshes the copy from the new interpreter.
anchored = tcc.ensure_tcc_anchor(root)
assert anchored == venv_python_path(root / ".venv")
assert (root / ".venv" / "bin" / "python").read_bytes() == (
new_bin / "python3.11"
).read_bytes()
status, _ = tcc.tcc_anchor_state(root)
assert status == "active"
class TestDoctorCheck:
def test_missing_warns_without_fix(self, monkeypatch, capsys):
monkeypatch.setattr(
tcc, "tcc_anchor_state", lambda *a, **k: ("missing", "/x/.venv/bin/python")
)
doctor.check_macos_tcc_anchor(should_fix=False)
out = capsys.readouterr().out
assert "macOS TCC anchor missing" in out
def test_fix_installs_anchor(self, monkeypatch, capsys):
monkeypatch.setattr(
tcc, "tcc_anchor_state", lambda *a, **k: ("missing", "/x/.venv/bin/python")
)
monkeypatch.setattr(
tcc, "ensure_tcc_anchor", lambda *a, **k: Path("/x/.venv/bin/python")
)
doctor.check_macos_tcc_anchor(should_fix=True)
out = capsys.readouterr().out
assert "macOS TCC anchor installed" in out
def test_active_reports_ok(self, monkeypatch, capsys):
monkeypatch.setattr(
tcc, "tcc_anchor_state", lambda *a, **k: ("active", "/x/.venv/bin/python")
)
doctor.check_macos_tcc_anchor(should_fix=False)
out = capsys.readouterr().out
assert "macOS TCC anchor active" in out
def test_skip_is_silent_on_non_macos(self, monkeypatch, capsys):
monkeypatch.setattr(
tcc, "tcc_anchor_state", lambda *a, **k: ("skip", "not macOS")
)
doctor.check_macos_tcc_anchor(should_fix=False)
assert capsys.readouterr().out == ""
def test_never_crashes_on_exception(self, monkeypatch, capsys):
def boom(*a, **k):
raise RuntimeError("tccd down")
monkeypatch.setattr(tcc, "tcc_anchor_state", boom)
doctor.check_macos_tcc_anchor(should_fix=False) # must not raise
out = capsys.readouterr().out
assert "macOS TCC anchor check failed" in out
@@ -0,0 +1,78 @@
"""Tests for the TCC-anchor revert heal (#95425 / #95541).
The interpreter anchor replaced venv/bin/python with a real-file copy that
could not load libpython on real Macs, bricking the CLI. The anchor is
reverted; doctor's check_macos_tcc_anchor_removed() restores anchored venvs
to symlinks using the marker the anchor left behind.
"""
import contextlib
import io
import os
from pathlib import Path
import hermes_cli.doctor as doctor_mod
def _capture(fn):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
fn()
return buf.getvalue()
def _build_anchored_checkout(tmp_path):
"""A checkout whose venv the anchor converted: real-file python + marker."""
root = tmp_path / "checkout"
store_bin = tmp_path / "store" / "cpython-3.12.1-macos" / "bin"
store_bin.mkdir(parents=True)
source = store_bin / "python3.12"
source.write_bytes(b"#!store interpreter")
source.chmod(0o755)
venv_bin = root / "venv" / "bin"
venv_bin.mkdir(parents=True)
venv_py = venv_bin / "python"
venv_py.write_bytes(b"#!anchored copy (broken on real macs)")
venv_py.chmod(0o755)
(venv_bin / ".tcc-anchor-source").write_text(str(source), encoding="utf-8")
os.symlink(venv_py, venv_bin / "python3")
return root, source, venv_py
def test_silent_on_non_macos(monkeypatch, tmp_path):
monkeypatch.setattr(doctor_mod.sys, "platform", "linux")
assert _capture(doctor_mod.check_macos_tcc_anchor_removed) == ""
def test_silent_when_never_anchored(monkeypatch, tmp_path):
monkeypatch.setattr(doctor_mod.sys, "platform", "darwin")
root = tmp_path / "checkout"
(root / "venv" / "bin").mkdir(parents=True)
monkeypatch.setattr(
doctor_mod, "__file__", str(root / "hermes_cli" / "doctor.py")
)
out = _capture(doctor_mod.check_macos_tcc_anchor_removed)
assert out == ""
def test_heals_anchored_venv(monkeypatch, tmp_path):
monkeypatch.setattr(doctor_mod.sys, "platform", "darwin")
root, source, venv_py = _build_anchored_checkout(tmp_path)
# Point the check's root resolution at the fixture checkout.
monkeypatch.setattr(
doctor_mod, "__file__", str(root / "hermes_cli" / "doctor.py")
)
out = _capture(doctor_mod.check_macos_tcc_anchor_removed)
assert "TCC anchor removed" in out
assert venv_py.is_symlink()
assert Path(os.readlink(venv_py)) == source
assert not (venv_py.parent / ".tcc-anchor-source").exists()
# Aliases restored to point at bin/python.
alias = venv_py.parent / "python3"
assert alias.is_symlink()
assert os.readlink(alias) == "python"