Files
hermes-agent/hermes_cli/main_desktop_tcc.py
Teknium f9769bbe4b refactor(desktop): move --setup-tcc-identity into hermes_cli/main_desktop_tcc.py
main_desktop.py grew past its code-health cap (2088 > 2069 on main). Move the
existing, self-contained `hermes gui --setup-tcc-identity` cluster
(_desktop_macos_setup_tcc_identity, _macos_codesigning_identity_valid,
_macos_create_signing_identity) verbatim into the topical sibling
main_desktop_tcc.py. cmd_gui late-imports it; the sibling late-imports the
facade's _desktop_packaged_executable/_desktop_macos_relaunchable_fixup, so the
existing monkeypatch seams on main_desktop still apply. main_desktop.py is now
1933 lines (>= 15 lines headroom under main). Tests and the resolution
allowlist row follow the moved symbol. Behaviour unchanged.
2026-10-06 05:44:21 -07:00

169 lines
7.6 KiB
Python

"""``hermes gui --setup-tcc-identity``: a persistent self-signed macOS code-signing identity.
Split out of ``hermes_cli/main_desktop.py``. The facade's build/sign helpers are imported lazily
inside the function that uses them (the facade late-imports this module from ``cmd_gui``).
"""
import contextlib
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
def _macos_codesigning_identity_valid(security: str, identity: str) -> bool:
"""True when `identity` is among VALID (``-v``) code-signing identities — the plain listing also
shows untrusted certs codesign refuses. Idempotency probe + postcondition. Never raises."""
try:
result = subprocess.run(
[security, "find-identity", "-v", "-p", "codesigning"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=False,
)
except Exception:
return False
return f'"{identity}"' in (result.stdout or "")
def _macos_create_signing_identity(
openssl: str, security: str, codesign: str, keychain: str, identity: str) -> bool:
"""Create a self-signed code-signing cert (10 years), import it with codesign access, trust it for codeSign."""
tmp_dir = Path(tempfile.mkdtemp(prefix="hermes-tcc-"))
try:
key = tmp_dir / "sign.key"
crt = tmp_dir / "sign.crt"
p12 = tmp_dir / "sign.p12"
subprocess.run(
[
openssl, "req", "-x509", "-newkey", "rsa:2048",
"-keyout", str(key), "-out", str(crt),
"-days", "3650", "-nodes",
"-subj", f"/CN={identity}",
"-addext", "basicConstraints=critical,CA:TRUE",
"-addext", "keyUsage=critical,digitalSignature,keyCertSign",
"-addext", "extendedKeyUsage=codeSigning",
],
capture_output=True, check=True)
# OpenSSL 3 defaults to AES/SHA-2 PKCS#12 that `security import` rejects
# with "MAC verification failed". `-legacy` restores the accepted
# RC2/SHA-1 format but only exists on OpenSSL 3 — so try plain first and
# fall back to `-legacy` when the IMPORT fails with that signature.
# (Verified E2E on macOS 26.3.1 / OpenSSL 3.6.3 by @ctaylor86 on PR #77189.)
def _export_p12(extra_args: list) -> None:
subprocess.run(
[
openssl, "pkcs12", "-export", *extra_args,
"-inkey", str(key), "-in", str(crt),
"-out", str(p12), "-passout", "pass:hermeslocal",
],
capture_output=True, check=True)
def _import_p12():
return subprocess.run(
[
security, "import", str(p12), "-k", keychain,
"-P", "hermeslocal",
"-T", codesign, "-T", "/usr/bin/codesign_allocate",
],
capture_output=True, text=True, encoding="utf-8", errors="replace", check=False)
_export_p12([])
imported = _import_p12()
if imported.returncode != 0 and "MAC verification failed" in (imported.stderr or ""):
# older OpenSSL without -legacy: keep the original failure
with contextlib.suppress(subprocess.CalledProcessError):
_export_p12(["-legacy"])
imported = _import_p12()
if imported.returncode != 0:
print(f" (could not import signing identity into keychain: {imported.stderr.strip()})")
return False
# Without explicit trust for the codeSign policy `find-identity -v`
# reports 0 valid identities. This writes user trust settings, so macOS
# may prompt for the login password ONCE — the one-time cost this
# command exists to front-load.
trusted = subprocess.run(
[security, "add-trusted-cert", "-r", "trustRoot", "-p", "codeSign", "-k", keychain, str(crt)],
capture_output=True, text=True, encoding="utf-8", errors="replace", check=False)
if trusted.returncode != 0:
print(
" (could not trust the certificate for code signing: "
f"{(trusted.stderr or trusted.stdout).strip()})"
)
return False
print(f" → created, imported, and trusted self-signed identity: {identity!r}")
return True
except Exception as exc:
print(f" (certificate creation failed: {exc})")
return False
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
def _desktop_macos_setup_tcc_identity(identity: str = "Hermes Local Signing") -> bool:
"""``--setup-tcc-identity``: create/import a self-signed code-signing cert, point
``desktop.macos_signing_identity`` at it and re-sign the packaged app. TCC grants follow the
signing identity, so a certificate-anchored one is stable across rebuilds (the yabai/skhd
mechanism). Idempotent; never raises."""
from hermes_cli.main import PROJECT_ROOT
from hermes_cli.main_desktop import _desktop_macos_relaunchable_fixup, _desktop_packaged_executable
if sys.platform != "darwin":
print(" (--setup-tcc-identity is macOS-only; skipping)")
return False
openssl = shutil.which("openssl")
security = shutil.which("security")
codesign = shutil.which("codesign")
if not (openssl and security and codesign):
print(
" (--setup-tcc-identity requires openssl, security, and codesign; "
f"found openssl={bool(openssl)} security={bool(security)} codesign={bool(codesign)})"
)
return False
keychain = str(Path.home() / "Library" / "Keychains" / "login.keychain-db")
# Probe with `-v` (valid identities only) so a previously imported-but-
# untrusted cert is repaired rather than reported as done.
if _macos_codesigning_identity_valid(security, identity):
print(f" → identity {identity!r} already valid in keychain")
elif not _macos_create_signing_identity(openssl, security, codesign, keychain, identity):
return False
# Postcondition gate: name-in-output checks pass for invalid identities;
# only macOS agreeing the identity is usable counts.
if not _macos_codesigning_identity_valid(security, identity):
print(
f" (identity {identity!r} was imported but is not a VALID code-signing identity; "
"run `security find-identity -v -p codesigning` to inspect, and see the manual "
"Keychain Access steps in the desktop docs)"
)
return False
# config.yaml, not .env — it's not a secret.
try:
from hermes_cli.config import set_config_value
set_config_value("desktop.macos_signing_identity", identity)
print(f" → set desktop.macos_signing_identity = {identity!r}")
except Exception as exc:
print(f" (could not write desktop.macos_signing_identity: {exc})")
return False
desktop_dir = PROJECT_ROOT / "apps" / "desktop"
if _desktop_packaged_executable(desktop_dir) is not None:
try:
if _desktop_macos_relaunchable_fixup(desktop_dir):
print(
" → packaged app re-signed with certificate-anchored identity; "
"TCC grants persist across rebuilds"
)
except Exception as exc:
print(f" (could not re-sign packaged app: {exc})")
print(
"\n Note: macOS will re-prompt for permissions ONE final time (the identity "
"changed). Grant them and they persist from then on. If a permission gets "
"stuck, reset it with: tccutil reset All com.nousresearch.hermes"
)
return True