fix(computer-use): auto-repair an installed driver that fails the runtime contract

A same-day version-floor bump (0.20 runtime contract) left every install
with an older cua-driver hard-failing on all computer_use calls: the
start() gate fails closed, while the `hermes update` refresh defers to the
driver's own check-update verb — whose ~20h cache routinely answers "no
update available" right after we raise the floor. Hermes knew it required
0.20+ but never acted on that knowledge.

Two changes:

- tools_config.install_cua_driver(): a contract-failed installed driver is
  repaired on the upgrade=True path too (previously only upgrade=False).
  The contract failure itself is the confirmation, so the
  require_confirmed_update gate and the check-update short-circuit are
  bypassed for repairs — an indeterminate or stale-cached check can no
  longer pin users on an unusable driver.

- cua_backend.CuaDriverBackend.start(): when the contract gate fails on an
  installed binary, attempt one automatic repair per process via the
  standard install path, then re-probe. HERMES_CUA_DRIVER_CMD overrides
  are never repaired (explicit override is authoritative even when broken)
  and a missing binary still just reports the install hint. A failing
  installer can't loop: the second start() surfaces the original error.

Tests: contract-repair coverage in test_computer_use.py (auto-repair
success, failed repair surfaces the original error, once-per-process
guard, override never repaired, missing binary never repaired) and
test_install_cua_driver.py (incompatible driver repairs despite an
indeterminate check-update, check-update not consulted). All new tests
verified to fail against the unfixed source (sabotage run).
This commit is contained in:
Teknium
2026-08-16 12:52:42 -07:00
parent a01d2ee21a
commit 00c12dac61
4 changed files with 250 additions and 3 deletions
+9 -3
View File
@@ -1072,10 +1072,16 @@ def install_cua_driver(
# baked in by CD and errors cleanly on missing-arch assets.
return _run_cua_driver_installer(label="Installing")
# An installed driver that fails Hermes' runtime contract (version floor,
# missing manifest verbs) is repaired regardless of the caller's mode.
# Hermes' own minimum requirement IS the confirmation that an upgrade is
# needed, so the ``upgrade=True`` path must not defer to the driver's
# ``check-update`` verb here — a cached/indeterminate "no update" answer
# would otherwise pin users on an unusable driver forever (observed:
# 0.19.3 installs hard-failing every computer_use call after the 0.20
# contract landed, with `hermes update` declining to refresh).
contract = _cua_driver_contract_status(binary) if binary else None
repair_existing = bool(
binary and not upgrade and contract and not contract.get("ready")
)
repair_existing = bool(binary and contract and not contract.get("ready"))
# A compatible existing installation needs no download. Finish the small
# host-specific setup that the upstream installer normally owns.
@@ -149,6 +149,11 @@ class TestInstallCuaDriverUpgrade:
with patch.object(tools_config.shutil, "which",
side_effect=lambda n: "/usr/local/bin/" + n
if n in {"cua-driver", "curl"} else None), \
patch.object(
tools_config,
"_cua_driver_contract_status",
return_value={"ready": True, "version": "0.20.0", "reason": ""},
), \
patch.object(tools_config, "_run_cua_driver_installer",
return_value=True) as runner, \
patch("subprocess.run"):
@@ -245,6 +250,11 @@ class TestInstallCuaDriverUpgrade:
else None
),
), \
patch.object(
tools_config,
"_cua_driver_contract_status",
return_value={"ready": True, "version": "0.20.0", "reason": ""},
), \
patch.object(
tools_config,
"_run_cua_driver_installer",
@@ -450,6 +460,11 @@ class TestRequireConfirmedUpdate:
return_value="/x/cua-driver"), \
patch.object(tools_config, "_cua_install_target_writable",
return_value=True), \
patch.object(
tools_config,
"_cua_driver_contract_status",
return_value={"ready": True, "version": "0.20.0", "reason": ""},
), \
patch("tools.computer_use.cua_backend.cua_driver_update_check",
return_value=check_state), \
patch.object(tools_config, "_run_cua_driver_installer",
@@ -503,6 +518,55 @@ class TestRequireConfirmedUpdate:
assert ok is True
runner.assert_called_once()
def test_incompatible_driver_repairs_despite_indeterminate_check(self):
"""Hermes' own version floor is the confirmation. When the installed
driver fails the runtime contract, the `hermes update` refresh must
repair it even though ``check-update`` can't confirm a newer release
(its ~20h cache routinely lags a same-day floor bump — the 0.19.3
wedge)."""
from unittest.mock import MagicMock
from hermes_cli import tools_config
incompatible = {
"ready": False,
"version": "0.19.3",
"reason": "Hermes computer use requires cua-driver 0.20.0 or newer",
}
with patch.object(tools_config.shutil, "which",
side_effect=lambda n: "/x/" + n
if n in {"cua-driver", "curl", "powershell"} else None), \
patch.object(tools_config, "_resolved_cua_driver_cmd",
return_value="/x/cua-driver"), \
patch.object(tools_config, "_cua_install_target_writable",
return_value=True), \
patch.object(
tools_config,
"_cua_driver_contract_status",
side_effect=[incompatible,
{"ready": True, "version": "0.20.0", "reason": ""}],
), \
patch("tools.computer_use.cua_backend.cua_driver_update_check",
return_value=None) as check, \
patch.object(tools_config, "_run_cua_driver_installer",
return_value=True) as runner, \
patch("subprocess.run",
return_value=MagicMock(stdout="cua-driver 0.19.3",
returncode=0)), \
patch.object(tools_config, "_print_success"), \
patch.object(tools_config, "_print_warning"), \
patch.object(tools_config, "_print_info"):
ok = tools_config.install_cua_driver(
upgrade=True, require_confirmed_update=True
)
assert ok is True
runner.assert_called_once()
assert runner.call_args.kwargs["label"] == "Repairing"
# The confirmed-update gate must not even consult check-update:
# the contract failure already confirmed the need.
check.assert_not_called()
class TestUpdateCheckTimeoutDefaults:
"""cua_driver_update_check: platform-sensitive default timeout.
@@ -623,6 +687,11 @@ class TestArchProbeRemoval:
with patch.object(tools_config.shutil, "which",
side_effect=lambda n: "/usr/local/bin/" + n
if n in ("cua-driver", "curl", "powershell") else None), \
patch.object(
tools_config,
"_cua_driver_contract_status",
return_value={"ready": True, "version": "0.20.0", "reason": ""},
), \
patch("urllib.request.urlopen") as urlopen, \
patch("subprocess.run"), \
patch.object(tools_config, "_run_cua_driver_installer",
@@ -1055,6 +1124,11 @@ class TestConfirmedVersionPinning:
return_value="/x/cua-driver"), \
patch.object(tools_config, "_cua_install_target_writable",
return_value=True), \
patch.object(
tools_config,
"_cua_driver_contract_status",
return_value={"ready": True, "version": "0.20.0", "reason": ""},
), \
patch("tools.computer_use.cua_backend.cua_driver_update_check",
return_value=check_state), \
patch.object(tools_config, "_run_cua_driver_installer",
+116
View File
@@ -819,6 +819,122 @@ class TestLazyMcpInstall:
mock_sess_start.assert_not_called() # never reaches the MCP session
class TestContractAutoRepair:
"""An installed-but-incompatible driver is repaired automatically, once.
The 0.20 runtime-contract gate fails closed; when the failure is an old
installed driver (a state Hermes' own version-floor bump created),
start() runs the standard install/repair path once instead of failing
every computer_use call until the user runs the CLI by hand.
"""
def _incompatible(self):
return {
"ready": False,
"binary": "/usr/local/bin/cua-driver",
"version": "0.19.3",
"reason": "Hermes computer use requires cua-driver 0.20.0 or newer",
}
def test_start_auto_repairs_incompatible_driver(self, monkeypatch):
from unittest.mock import MagicMock, patch
from tools.computer_use import cua_backend
monkeypatch.setattr(cua_backend, "_contract_repair_attempted", False)
backend = cua_backend.CuaDriverBackend()
backend._session = MagicMock()
with patch.object(
cua_backend,
"cua_driver_runtime_contract_status",
side_effect=[self._incompatible(), {"ready": True}],
), \
patch("hermes_cli.tools_config.install_cua_driver",
return_value=True) as installer, \
patch.object(cua_backend, "_maybe_nudge_update"), \
patch("tools.lazy_deps.ensure"):
backend.start()
installer.assert_called_once_with(
upgrade=False, show_installer_progress=False
)
backend._session.start.assert_called_once()
def test_failed_repair_surfaces_original_error(self, monkeypatch):
from unittest.mock import patch
from tools.computer_use import cua_backend
monkeypatch.setattr(cua_backend, "_contract_repair_attempted", False)
with patch.object(
cua_backend,
"cua_driver_runtime_contract_status",
return_value=self._incompatible(),
), \
patch("hermes_cli.tools_config.install_cua_driver",
return_value=False), \
patch("tools.lazy_deps.ensure") as mock_ensure:
with pytest.raises(RuntimeError, match="0.20.0 or newer"):
cua_backend.CuaDriverBackend().start()
mock_ensure.assert_not_called()
def test_repair_is_attempted_once_per_process(self, monkeypatch):
from unittest.mock import patch
from tools.computer_use import cua_backend
monkeypatch.setattr(cua_backend, "_contract_repair_attempted", False)
with patch.object(
cua_backend,
"cua_driver_runtime_contract_status",
return_value=self._incompatible(),
), \
patch("hermes_cli.tools_config.install_cua_driver",
return_value=False) as installer, \
patch("tools.lazy_deps.ensure"):
for _ in range(2):
with pytest.raises(RuntimeError):
cua_backend.CuaDriverBackend().start()
installer.assert_called_once()
def test_explicit_override_is_never_repaired(self, monkeypatch):
from unittest.mock import patch
from tools.computer_use import cua_backend
monkeypatch.setattr(cua_backend, "_contract_repair_attempted", False)
monkeypatch.setenv("HERMES_CUA_DRIVER_CMD", "/opt/custom/cua-driver")
with patch.object(
cua_backend,
"cua_driver_runtime_contract_status",
return_value=self._incompatible(),
), \
patch("hermes_cli.tools_config.install_cua_driver") as installer, \
patch("tools.lazy_deps.ensure"):
with pytest.raises(RuntimeError, match="HERMES_CUA_DRIVER_CMD"):
cua_backend.CuaDriverBackend().start()
installer.assert_not_called()
def test_missing_binary_is_not_repaired(self, monkeypatch):
from unittest.mock import patch
from tools.computer_use import cua_backend
monkeypatch.setattr(cua_backend, "_contract_repair_attempted", False)
state = {
"ready": False,
"binary": None,
"version": None,
"reason": "cua-driver is not installed",
}
with patch.object(
cua_backend,
"cua_driver_runtime_contract_status",
return_value=state,
), \
patch("hermes_cli.tools_config.install_cua_driver") as installer, \
patch("tools.lazy_deps.ensure"):
with pytest.raises(RuntimeError, match="not installed"):
cua_backend.CuaDriverBackend().start()
installer.assert_not_called()
class TestCaptureAfterAppContext:
"""Bug 2: capture_after=True loses app context after actions.
+51
View File
@@ -1191,6 +1191,52 @@ def cua_driver_update_nudge() -> Optional[str]:
_update_checked = False
# One auto-repair attempt per process. The runtime-contract gate in
# ``CuaDriverBackend.start()`` fails closed on an incompatible driver; when
# the incompatibility is something a reinstall fixes (old version, missing
# manifest verbs) we run the standard install/repair path once instead of
# telling the user to do it by hand. Guarded so a failing installer can't
# loop — the second start() in the same process goes straight to the error.
_contract_repair_attempted = False
def _maybe_repair_runtime_contract(contract: Dict[str, Any]) -> Dict[str, Any]:
"""Try one automatic driver repair for a failed runtime contract.
Returns the post-repair contract state (or the original state when no
repair was attempted / the repair failed). Never raises. An explicit
``HERMES_CUA_DRIVER_CMD`` override is authoritative even when broken, and
a missing binary means installation was never requested — both are left
for the caller's error message.
"""
global _contract_repair_attempted
if contract.get("ready"):
return contract
if _contract_repair_attempted:
return contract
if os.environ.get(_CUA_DRIVER_CMD_ENV, "").strip():
return contract
if not contract.get("binary"):
return contract
_contract_repair_attempted = True
logger.info(
"computer_use: installed cua-driver is not usable (%s); "
"attempting automatic repair",
contract.get("reason") or "runtime contract is incomplete",
)
try:
from hermes_cli.tools_config import install_cua_driver
if not install_cua_driver(upgrade=False, show_installer_progress=False):
return contract
except Exception as exc:
logger.warning("computer_use: automatic cua-driver repair failed: %s", exc)
return contract
try:
return cua_driver_runtime_contract_status()
except Exception:
return contract
def _maybe_nudge_update() -> None:
"""Emit an update nudge at most once per process, off-thread so the
@@ -2526,6 +2572,11 @@ class CuaDriverBackend(ComputerUseBackend):
# ── Lifecycle ──────────────────────────────────────────────────
def start(self) -> None:
contract = cua_driver_runtime_contract_status()
if not contract.get("ready"):
# An installed-but-incompatible driver (e.g. predating a Hermes
# version-floor bump) is a state we created — repair it once
# automatically instead of failing every computer_use call.
contract = _maybe_repair_runtime_contract(contract)
if not contract.get("ready"):
reason = contract.get("reason") or "runtime contract is incomplete"
if os.environ.get(_CUA_DRIVER_CMD_ENV, "").strip():