fix(update): refresh stale managed-uv catalog so SQLite runtime repair can succeed

The managed uv is installed with UV_UNMANAGED_INSTALL, which disables
'uv self update' by design — the swallowed failure left its embedded
python-build-standalone catalog frozen at bootstrap age forever.
python-build-standalone re-releases existing patch versions with fixed
SQLite (3.11.15 was re-cut with 3.53.1), so a stale catalog resolves
the same version number to the OLD vulnerable build, the probe rejects
it, and the patch-retry loop cannot recover because the fixed build
carries no newer number to try. Result: 'hermes update' printed a
guaranteed-failure provisioning warning on every run (issue #72093).

- When provisioning fails, re-bootstrap the Hermes-managed uv binary
  via the official installer (the only supported refresh for unmanaged
  installs) and retry provisioning once — only when the binary version
  actually changed, so no wasted download cycles.
- Never touch a caller-supplied uv outside the managed path.
- Soften the failure report from alarming ⚠ to informational ℹ and say
  why it is safe to wait: the WAL gate keeps databases out of WAL on
  vulnerable builds, and the next update retries.

Verified: 56 unit tests green; sabotage run (retry block removed) fails
the 3 new retry tests; live E2E replaced a fake managed uv via the real
astral installer and the refreshed binary resolved the 3.11 catalog.

Fixes #72093
This commit is contained in:
teknium1 2026-07-26 17:08:04 -07:00 committed by Teknium
parent e2fbd0dcd7
commit c476ad35ae
2 changed files with 225 additions and 1 deletions

View file

@ -138,9 +138,14 @@ class _RepairLock:
def _report_runtime_repair_failure(repair: RuntimeRepairResult) -> None:
if repair.backup_venv is None:
print(
" Managed Python runtime was not replaced; "
" Managed Python runtime was not replaced; "
f"the existing venv is unchanged ({repair.detail})."
)
print(
" Sessions stay protected meanwhile: Hermes keeps databases "
"out of WAL mode on this SQLite build. The next `hermes update` "
"will retry."
)
return
print(f" ✗ Managed Python runtime cutover needs manual recovery: {repair.detail}")
print(f" Previous venv: {repair.backup_venv}")
@ -874,6 +879,64 @@ def _windows_runtime_holders() -> tuple[bool, str]:
return False, ""
def _uv_version_string(uv_bin: str) -> str:
"""Return ``uv --version`` output, or ``""`` when it cannot be read."""
try:
result = subprocess.run(
[uv_bin, "--version"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
timeout=15,
)
except Exception:
return ""
if result.returncode != 0:
return ""
return (result.stdout or "").strip()
def _refresh_managed_uv_catalog(uv_bin: str) -> bool:
"""Re-bootstrap the managed uv binary to refresh its Python catalog.
The managed uv is installed with ``UV_UNMANAGED_INSTALL``, which disables
``uv self update`` by design so its embedded python-build-standalone
download catalog stays frozen at bootstrap age. python-build-standalone
re-releases existing CPython patch versions with newer SQLite (e.g. the
3.11.15 build was re-cut with SQLite 3.53.x), so a stale catalog can make
every provisioning attempt resolve to a vulnerable build even though a
fixed build of the SAME patch version exists (issue #72093). The
patch-retry loop cannot recover from that: the fixed build carries no
newer version number to retry with.
Re-running the official installer is the only supported refresh path for
unmanaged installs. Only the Hermes-managed binary is ever refreshed;
a caller-supplied foreign uv path is left alone.
Returns ``True`` when the binary's version actually changed — i.e. a
provisioning retry can now see a different catalog. ``False`` means a
retry would resolve identically and is not worth the download cycle.
"""
managed = managed_uv_path()
try:
if Path(uv_bin).resolve() != managed.resolve():
return False
except OSError:
return False
before = _uv_version_string(uv_bin)
try:
_install_uv(managed)
except Exception as exc:
logger.warning("managed uv refresh failed: %s", exc)
return False
after = _uv_version_string(uv_bin)
if not after:
return False
return after != before
def repair_vulnerable_runtime(
uv_bin: str,
*,
@ -948,6 +1011,19 @@ def repair_vulnerable_runtime(
project_root=root,
current=current,
)
if provisioned is None:
# Likely a stale managed-uv catalog: python-build-standalone
# re-releases the same patch versions with fixed SQLite, but a
# frozen catalog keeps resolving the old vulnerable build and the
# patch-retry loop has no newer number to try (issue #72093).
# Refresh the managed binary and retry once.
if _refresh_managed_uv_catalog(uv_bin):
print(" → Managed uv refreshed; retrying provisioning...")
provisioned = _install_safe_python_generation(
uv_bin,
project_root=root,
current=current,
)
if provisioned is None:
return RuntimeRepairResult(
"failed",

View file

@ -1067,3 +1067,151 @@ class TestListAvailablePatches:
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
assert managed_uv._list_available_patches("uv", "3.11", cwd=tmp_path, env={}) == []
# ---------------------------------------------------------------------------
# _refresh_managed_uv_catalog + provisioning retry (issue #72093)
# ---------------------------------------------------------------------------
class TestRefreshManagedUvCatalog:
"""The managed uv is UV_UNMANAGED_INSTALL'd, so `uv self update` is
disabled and its python-build-standalone catalog freezes at bootstrap
age. python-build-standalone re-releases the same patch versions with
fixed SQLite, so a stale catalog makes provisioning fail forever with
no newer patch number to retry (issue #72093)."""
def test_foreign_uv_path_is_never_refreshed(self, tmp_path):
import hermes_cli.managed_uv as managed_uv
_make_executable(tmp_path / "bin" / "uv")
foreign = tmp_path / "elsewhere" / "uv"
_make_executable(foreign)
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"), \
patch("hermes_cli.managed_uv._install_uv") as mock_install:
assert managed_uv._refresh_managed_uv_catalog(str(foreign)) is False
mock_install.assert_not_called()
def test_version_change_reports_true(self, tmp_path):
import hermes_cli.managed_uv as managed_uv
uv_path = tmp_path / "bin" / "uv"
_make_executable(uv_path)
versions = iter(["uv 0.1.0", "uv 0.2.0"])
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"), \
patch("hermes_cli.managed_uv._install_uv"), \
patch(
"hermes_cli.managed_uv._uv_version_string",
side_effect=lambda _uv: next(versions),
):
assert managed_uv._refresh_managed_uv_catalog(str(uv_path)) is True
def test_same_version_reports_false(self, tmp_path):
import hermes_cli.managed_uv as managed_uv
uv_path = tmp_path / "bin" / "uv"
_make_executable(uv_path)
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"), \
patch("hermes_cli.managed_uv._install_uv"), \
patch(
"hermes_cli.managed_uv._uv_version_string",
return_value="uv 0.1.0",
):
assert managed_uv._refresh_managed_uv_catalog(str(uv_path)) is False
def test_installer_failure_reports_false(self, tmp_path):
import hermes_cli.managed_uv as managed_uv
uv_path = tmp_path / "bin" / "uv"
_make_executable(uv_path)
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.platform.system", return_value="Linux"), \
patch(
"hermes_cli.managed_uv._install_uv",
side_effect=RuntimeError("network down"),
):
assert managed_uv._refresh_managed_uv_catalog(str(uv_path)) is False
class TestRepairRetriesAfterUvRefresh:
def _run_repair(self, tmp_path, *, refresh_result, second_attempt):
"""Drive repair with the first provisioning attempt failing."""
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel = _make_runtime_install(tmp_path)
current = _runtime_info(live / "bin" / "python", (3, 50, 4))
attempts = []
def fake_install(uv_bin, *, project_root, current):
attempts.append(uv_bin)
if len(attempts) == 1:
return None
return second_attempt(project_root)
with patch("hermes_cli.managed_uv.platform.system", return_value="Linux"), \
patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
side_effect=fake_install,
), \
patch(
"hermes_cli.managed_uv._refresh_managed_uv_catalog",
return_value=refresh_result,
) as mock_refresh, \
patch(
"hermes_cli.managed_uv._stage_candidate_venv",
return_value=None,
):
result = repair_vulnerable_runtime("uv", project_root=root)
return result, attempts, mock_refresh, sentinel
def test_no_retry_when_refresh_did_not_change_uv(self, tmp_path):
result, attempts, mock_refresh, sentinel = self._run_repair(
tmp_path,
refresh_result=False,
second_attempt=lambda root: None,
)
assert result.status == "failed"
assert len(attempts) == 1
mock_refresh.assert_called_once_with("uv")
assert sentinel.read_text(encoding="utf-8") == "live"
def test_retries_once_after_successful_refresh(self, tmp_path):
result, attempts, mock_refresh, sentinel = self._run_repair(
tmp_path,
refresh_result=True,
second_attempt=lambda root: None,
)
# Second attempt ran (and also failed) — exactly one retry, no loop.
assert result.status == "failed"
assert len(attempts) == 2
mock_refresh.assert_called_once_with("uv")
assert sentinel.read_text(encoding="utf-8") == "live"
def test_retry_success_proceeds_to_staging(self, tmp_path):
def second_attempt(root):
generation = root / ".hermes-runtime" / "python" / "generation-retry"
candidate_python = generation / "bin" / "python"
candidate_python.parent.mkdir(parents=True)
candidate_python.write_text("candidate", encoding="utf-8")
return generation, candidate_python, _runtime_info(
candidate_python, (3, 53, 1)
)
result, attempts, mock_refresh, sentinel = self._run_repair(
tmp_path,
refresh_result=True,
second_attempt=second_attempt,
)
# Provisioning succeeded on retry; staging (mocked to None) is what
# failed — proving the retry result flows into the normal pipeline.
assert result.status == "failed"
assert "replacement environment" in result.detail
assert len(attempts) == 2
assert sentinel.read_text(encoding="utf-8") == "live"