From 45db70a80a17999607a7d6e3155480186b5840ef Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:58:07 -0700 Subject: [PATCH] fix(computer-use): fail closed on unverified CuaDriver.app + background launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardening on top of the TCC daemon-identity salvage: - _validate_cua_driver_app_signature: codesign -dv gate requiring EXACT Identifier=com.trycua.driver and the official team (4YEC26S9KF) before /usr/bin/open hands the bundle to LaunchServices — the identity fix must not double as a launcher for arbitrary/impostor bundles (suffixed identifiers and wrong teams rejected; unsigned dev builds only via computer_use.allow_unsigned_driver: true in config.yaml). - _resolve_cua_driver_app_path: derive the bundle ONLY from the resolved driver binary — the /Applications fallback could launch a DIFFERENT install than the manifest resolved. - open -n -g: don't activate/steal focus when launching the daemon. - 7 new tests incl. sabotage-verified exact-match assertions. Grafted from #76433's review direction (@Chadmc9889's original fail-closed validation requirement). Co-authored-by: Chadmc9889 --- hermes_cli/config_defaults.py | 6 ++ ...test_computer_use_browser_authorization.py | 84 +++++++++++++++- tools/computer_use/cua_backend.py | 95 ++++++++++++++++--- 3 files changed, 170 insertions(+), 15 deletions(-) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 0fb1488316..4a03422609 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -3646,6 +3646,12 @@ DEFAULT_CONFIG = { # flags when it launches the runtime. See # https://cua.ai/docs/reference/cua-driver/permission-modes "capability_manifest": "", + # macOS only: allow launching an UNSIGNED (ad-hoc / TeamIdentifier + # not set) CuaDriver.app for the private-session daemon. The default + # (false) fails closed unless the bundle is signed with the official + # cua-driver identity (com.trycua.driver / team 4YEC26S9KF). Enable + # only when developing the driver locally from source. + "allow_unsigned_driver": False, # Pre-authorize existing-profile browser attachment in standard mode # (cua-driver's trusted-launcher `--grant existing-profile`). When # true, the agent can attach to your already-running, signed-in diff --git a/tests/tools/test_computer_use_browser_authorization.py b/tests/tools/test_computer_use_browser_authorization.py index 0b3dbc6f86..216743bd52 100644 --- a/tests/tools/test_computer_use_browser_authorization.py +++ b/tests/tools/test_computer_use_browser_authorization.py @@ -248,7 +248,9 @@ def test_schema_does_not_expose_approval_token(): # ── bounded embedded daemon ───────────────────────────────────────────── -def test_macos_embedded_daemon_launches_through_cuadriver_app(): +def test_macos_embedded_daemon_launches_through_cuadriver_app(monkeypatch): + validated = [] + monkeypatch.setattr(cb, "_validate_cua_driver_app_signature", lambda app: validated.append(app)) command = cb._embedded_daemon_spawn_command( "/tmp/cua-driver", ["serve", "--embedded", "--socket", "/tmp/private.sock"], @@ -256,9 +258,12 @@ def test_macos_embedded_daemon_launches_through_cuadriver_app(): app_path="/Applications/CuaDriver.app", ) + # Signature validation is mandatory before any launch command is built. + assert validated == ["/Applications/CuaDriver.app"] assert command == [ "/usr/bin/open", "-n", + "-g", "-a", "/Applications/CuaDriver.app", "--args", @@ -269,6 +274,83 @@ def test_macos_embedded_daemon_launches_through_cuadriver_app(): ] +def _codesign_proc(returncode=0, stderr=""): + import subprocess as _sp + + return _sp.CompletedProcess(["codesign"], returncode, stdout="", stderr=stderr) + + +def _patch_codesign(monkeypatch, proc): + monkeypatch.setattr(cb.shutil, "which", lambda name: "/usr/bin/codesign") + monkeypatch.setattr(cb.subprocess, "run", lambda *a, **kw: proc) + + +def test_driver_signature_valid_official_identity(monkeypatch): + _patch_codesign( + monkeypatch, + _codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=4YEC26S9KF\n"), + ) + cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") # no raise + + +def test_driver_signature_rejects_suffixed_identifier(monkeypatch): + import pytest + + _patch_codesign( + monkeypatch, + _codesign_proc(stderr="Identifier=com.trycua.driver.evil\nTeamIdentifier=4YEC26S9KF\n"), + ) + with pytest.raises(RuntimeError, match="identifier"): + cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") + + +def test_driver_signature_rejects_wrong_team(monkeypatch): + import pytest + + _patch_codesign( + monkeypatch, + _codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=EVIL000000\n"), + ) + with pytest.raises(RuntimeError, match="team"): + cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") + + +def test_driver_signature_unsigned_rejected_by_default(monkeypatch): + import pytest + + _patch_codesign( + monkeypatch, + _codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=not set\n"), + ) + monkeypatch.setattr(cb, "_computer_use_cfg", lambda: {}) + with pytest.raises(RuntimeError, match="team"): + cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") + + +def test_driver_signature_unsigned_allowed_by_config_opt_in(monkeypatch): + _patch_codesign( + monkeypatch, + _codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=not set\n"), + ) + monkeypatch.setattr(cb, "_computer_use_cfg", lambda: {"allow_unsigned_driver": True}) + cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") # no raise + + +def test_driver_signature_rejects_unsigned_bundle(monkeypatch): + import pytest + + _patch_codesign(monkeypatch, _codesign_proc(returncode=1, stderr="code object is not signed at all")) + with pytest.raises(RuntimeError, match="not code-signed"): + cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") + + +def test_resolve_app_path_has_no_applications_fallback(tmp_path): + # A driver binary OUTSIDE any .app bundle must resolve to None — the old + # /Applications fallback could launch a DIFFERENT install than the + # resolved driver. + assert cb._resolve_cua_driver_app_path(str(tmp_path / "cua-driver")) is None + + def test_non_macos_embedded_daemon_keeps_direct_binary_launch(): command = cb._embedded_daemon_spawn_command( "/tmp/cua-driver", diff --git a/tools/computer_use/cua_backend.py b/tools/computer_use/cua_backend.py index 624a743f83..b34e1376f9 100644 --- a/tools/computer_use/cua_backend.py +++ b/tools/computer_use/cua_backend.py @@ -593,25 +593,90 @@ def _wsl_windows_path_to_posix(path: str) -> str: def _resolve_cua_driver_app_path(driver_cmd: str) -> Optional[str]: - """Return the installed CuaDriver.app carrying *driver_cmd*, if present.""" + """Return the CuaDriver.app bundle that CARRIES *driver_cmd*, if any. + + Deliberately derived from the resolved driver binary path only — no + /Applications or ~/Applications fallback. A fallback candidate can be a + DIFFERENT install than the driver the manifest resolved (stale copy, + side-by-side version), and launching it would run code the resolution + chain never validated. If the resolved driver does not live inside an + app bundle, the caller fails closed with install guidance. + """ marker = ".app/Contents/MacOS/" - candidates: List[str] = [] marker_index = driver_cmd.find(marker) - if marker_index >= 0: - candidates.append(driver_cmd[: marker_index + len(".app")]) - candidates.extend( - [ - "/Applications/CuaDriver.app", - os.path.expanduser("~/Applications/CuaDriver.app"), - ] - ) - for candidate in candidates: - executable = os.path.join(candidate, "Contents", "MacOS", "cua-driver") - if os.path.isfile(executable) and os.access(executable, os.X_OK): - return candidate + if marker_index < 0: + return None + candidate = driver_cmd[: marker_index + len(".app")] + executable = os.path.join(candidate, "Contents", "MacOS", "cua-driver") + if os.path.isfile(executable) and os.access(executable, os.X_OK): + return candidate return None +# The only bundle identity the private daemon may launch through, and the +# team that signs official cua-driver releases. Exact matches only: a +# suffixed identifier ("com.trycua.driver.evil") or a different non-empty +# team is an impostor bundle, not a variant. +_CUA_DRIVER_BUNDLE_ID = "com.trycua.driver" +_CUA_DRIVER_TEAM_ID = "4YEC26S9KF" + + +def _validate_cua_driver_app_signature(app_path: str) -> None: + """Fail closed unless *app_path* is the genuinely-signed CuaDriver.app. + + Launching via ``/usr/bin/open`` hands LaunchServices whatever bundle sits + at the path, so the TCC-identity fix must not become a launcher for + arbitrary apps: require ``codesign -dv`` to report EXACTLY + ``Identifier=com.trycua.driver`` and the expected TeamIdentifier. + ``TeamIdentifier=not set`` (unsigned/ad-hoc dev builds) is allowed only + when ``computer_use.allow_unsigned_driver: true`` is set in config.yaml — + the escape hatch for local driver development, never the default. Raises + RuntimeError on any mismatch or when codesign is unavailable/fails. + """ + codesign = shutil.which("codesign") + if not codesign: + raise RuntimeError( + "codesign is required to verify CuaDriver.app before launching it." + ) + try: + proc = subprocess.run( + [codesign, "-dv", app_path], + capture_output=True, + text=True, + timeout=15, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise RuntimeError(f"could not verify CuaDriver.app signature: {exc}") from exc + if proc.returncode != 0: + raise RuntimeError( + f"CuaDriver.app at {app_path} is not code-signed; refusing to launch it " + f"({(proc.stderr or '').strip()})" + ) + # codesign -dv reports on stderr. + fields = {} + for line in (proc.stderr or "").splitlines(): + key, sep, value = line.partition("=") + if sep: + fields.setdefault(key.strip(), value.strip()) + identifier = fields.get("Identifier", "") + team = fields.get("TeamIdentifier", "") + if identifier != _CUA_DRIVER_BUNDLE_ID: + raise RuntimeError( + f"CuaDriver.app at {app_path} has identifier {identifier!r}, " + f"expected {_CUA_DRIVER_BUNDLE_ID!r}; refusing to launch it." + ) + if team == _CUA_DRIVER_TEAM_ID: + return + if team in ("", "not set") and _computer_use_cfg().get("allow_unsigned_driver") is True: + return + raise RuntimeError( + f"CuaDriver.app at {app_path} is signed by team {team!r}, expected " + f"{_CUA_DRIVER_TEAM_ID!r}; refusing to launch it. (Set " + "computer_use.allow_unsigned_driver: true in config.yaml only for " + "local unsigned driver builds.)" + ) + + def _embedded_daemon_spawn_command( driver_cmd: str, serve_args: List[str], @@ -628,9 +693,11 @@ def _embedded_daemon_spawn_command( "CuaDriver.app is required for private computer-use sessions on macOS. " "Run `hermes computer-use install` to restore it." ) + _validate_cua_driver_app_signature(resolved_app) return [ "/usr/bin/open", "-n", + "-g", "-a", resolved_app, "--args",