Keep Hermes Desktop opening on its runtime after Hermes updates (#718)

* Accept the fifth Hermes desktop launch option

Hermes now returns five values from _desktop_launch_options(), appending the renderer accessibility switch. The launcher unpacked exactly four, so once a user's runtime updated, every launch died with "too many values to unpack" before the app opened. The launcher now takes the first four and reads the fifth when present, so it keeps working with the packaged release and with newer runtimes, and it bridges an explicit accessibility opt-out the same way Hermes' own launcher does. The launch check now also runs a five-value helper, which fails against the previous launcher.

Fixes basecamp/omarchy#13491

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* hermes-desktop: launch against the installed runtime, accept the helper's new arity

_desktop_launch_options() returns five values since Hermes grew a trailing
renderer_accessibility field, so the launcher died with "too many values to
unpack (expected 4)" before os.execve — and gtk-launch discards stderr, so the
app icon became a silent no-op that left no process and no log line behind.

The launcher also exported HERMES_DESKTOP_IGNORE_EXISTING=1 unconditionally
while preferring the runtime's own app binary a few lines above it: with a
runtime installed, Desktop skipped that runtime and offered first-run setup
instead of the user's sessions. Ignoring an existing runtime is only correct for
the bundled /opt app, which is built from the package's release commit and
cannot drive a runtime built from another one.

pkgrel bumped for the changed artifact.

* Tell only the packaged Hermes Desktop to skip an existing install

The runtime's own app now finds its runtime the way `hermes desktop` launches it, through the installed-runtime lookup, instead of being pinned to it with HERMES_DESKTOP_HERMES_ROOT. That variable is upstream's developer override: it resolves before the lookup that Repair install bypasses, so pinning the runtime turned a hard repair into a restart against the same broken venv. HERMES_DESKTOP_IGNORE_EXISTING is set only when the launcher falls back to the packaged /opt app, and an explicit value in the environment still wins, so the in-app updater's relaunch of the runtime app no longer inherits it.

The fifth launch option is read the way it was accepted in the previous commit but one, with the checksums the two earlier commits left stale brought up to date. runtime-test.py now records both variables, so it fails if the unconditional export or the root pin comes back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: manuaudio <manu@arimaka.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Omni <omni@omninova.com.mx>
Co-authored-by: Codex XHigh <noreply@openai.com>
This commit is contained in:
authored and GitHub committed 2026-09-29 16:35:30 -05:00
1 parent 0c1996bb68
commit 5a49a3fab5
3 files changed
+44 -11

No files matched your search

+3 -3
View File
@@ -5,7 +5,7 @@
pkgname=hermes-desktop
pkgver=2026.9.7
pkgrel=2
pkgrel=3
pkgdesc='Native desktop shell for Hermes Agent'
arch=('x86_64')
url='https://github.com/NousResearch/hermes-agent'
@@ -75,11 +75,11 @@ source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz
'runtime.patch'
'runtime-test.py')
sha256sums=('907c2a72db1c5dd637ea8eeae97f4cb5b32cef615c17258f6b190924ec5bf688'
'c68233f93387251f08537559c072c9ec36ba9a304b1669decc56df17c464b252'
'f7519cee8e0f64d9c4859dc1d2eb7e223f94ab22ab522a66cbaada05944ef71b'
'3ef685bfcf366776b025d26c37d32854d8d4aa2023b2bd07c8e08b001ef1e8c4'
'd60d164e24fdcf6532133b8ea43c77a201e4b9e9dbc396187b58d51d8590ef52'
'9d5015d1be762a901f8f64319981ae862e9852fa5cb9a22a2ba1e691f90430a2'
'514a5e7ab2b7262141a2588c5b5036832cb4ba789a9578b8b50b6d79a9d63deb')
'84373e503dc5ba5ce099c57150f281247dda941b477c0cd679c50fe0d3a0bbb1')
build() {
cd "${srcdir}/${_srcdir}"
+11 -3
View File
@@ -2,7 +2,6 @@
set -euo pipefail
unset ELECTRON_RUN_AS_NODE PYTHONPATH PYTHONHOME
export HERMES_DESKTOP_IGNORE_EXISTING=1
hermes_home=$(realpath -ms -- "${HERMES_HOME:-$HOME/.hermes}")
parent=${hermes_home%/*}
@@ -15,6 +14,9 @@ runtime="$hermes_home/hermes-agent"
native="$runtime/apps/desktop/release/linux-unpacked/Hermes"
if [[ ! -x $native || ! -x $runtime/venv/bin/hermes ]]; then
native=/opt/hermes-desktop/Hermes
# Only the packaged app, built from the release commit rather than the runtime's
# checkout, is told to skip an existing Hermes.
export HERMES_DESKTOP_IGNORE_EXISTING=${HERMES_DESKTOP_IGNORE_EXISTING:-1}
fi
# Both app locations use namespaces, never a user-writable setuid helper.
@@ -36,7 +38,7 @@ import sys
native, runtime, *args = sys.argv[1:]
env = os.environ.copy()
flags, gpu, store, ozone = [], "auto", "auto", "auto"
flags, gpu, store, ozone, a11y = [], "auto", "auto", "auto", True
if runtime:
sys.path.insert(0, runtime)
try:
@@ -47,7 +49,10 @@ if runtime:
from hermes_cli.main import _desktop_launch_options
from hermes_constants import with_hermes_node_path
flags, gpu, store, ozone = _desktop_launch_options()
# Newer runtimes append renderer_accessibility; the packaged release returns four.
flags, gpu, store, ozone, *extra = _desktop_launch_options()
if extra:
a11y = extra[0]
env = with_hermes_node_path(env)
except ImportError:
print("Could not load Hermes desktop settings; using launch defaults.", file=sys.stderr)
@@ -58,6 +63,9 @@ if gpu != "auto":
if ozone != "auto":
env.setdefault("ELECTRON_OZONE_PLATFORM_HINT", ozone)
env.setdefault("HERMES_DESKTOP_PASSWORD_STORE", store if store != "auto" else "gnome-libsecret")
# The app keeps its accessibility tree on unless told otherwise, so bridge only the opt-out.
if not a11y:
env.setdefault("HERMES_DESKTOP_RENDERER_ACCESSIBILITY", "0")
# Explicit config, environment and command-line choices override the Wayland default.
if (env.get("WAYLAND_DISPLAY") or env.get("XDG_SESSION_TYPE") == "wayland") and (
+30 -5
View File
@@ -31,6 +31,9 @@ Path(os.environ["TEST_OUTPUT"]).write_text(json.dumps({
"gpu": os.environ.get("HERMES_DESKTOP_DISABLE_GPU"),
"ozone": os.environ.get("ELECTRON_OZONE_PLATFORM_HINT"),
"cwd": os.environ.get("HERMES_DESKTOP_CWD"),
"a11y": os.environ.get("HERMES_DESKTOP_RENDERER_ACCESSIBILITY"),
"ignore": os.environ.get("HERMES_DESKTOP_IGNORE_EXISTING"),
"root": os.environ.get("HERMES_DESKTOP_HERMES_ROOT"),
"inherited": [name for name in ("ELECTRON_RUN_AS_NODE", "PYTHONPATH", "PYTHONHOME") if name in os.environ],
}))
''')
@@ -88,17 +91,22 @@ def with_hermes_node_path(env=None):
"TEST_OUTPUT": str(output), "TEST_FORBIDDEN": str(forbidden_output)}
launch = ["bash", str(launcher.resolve())]
def check_launch(args=(), overrides=None, expected_args=(), store="gnome-libsecret", gpu=None, ozone=None):
def check_launch(args=(), overrides=None, expected_args=(), store="gnome-libsecret", gpu=None, ozone=None,
a11y=None, ignore=None):
subprocess.run(launch + list(args), env={**env, **(overrides or {})}, cwd=home, check=True)
result = json.loads(output.read_text())
assert result == {"args": ["--disable-setuid-sandbox", *expected_args],
"home": str(home / ".hermes"), "store": store, "gpu": gpu,
"ozone": ozone, "cwd": str(home), "inherited": []}, result
"ozone": ozone, "cwd": str(home), "a11y": a11y, "ignore": ignore, "root": None,
"inherited": []}, result
assert not forbidden_output.exists(), "launcher invoked the Omarchy installer or sudo"
output.unlink()
wayland = {"WAYLAND_DISPLAY": "wayland-1"}
check_launch(overrides=wayland, expected_args=["--ozone-platform=wayland"])
# The runtime's own app finds its runtime unaided; an explicit request to skip it still passes through.
check_launch(overrides={**wayland, "HERMES_DESKTOP_IGNORE_EXISTING": "1"},
expected_args=["--ozone-platform=wayland"], ignore="1")
url = "hermes://open?text=a%20b"
check_launch(["--ozone-platform=x11", url], {**wayland, "HERMES_DESKTOP_PASSWORD_STORE": "kwallet6"},
["--ozone-platform=x11", url], store="kwallet6")
@@ -122,6 +130,19 @@ def with_hermes_node_path(env=None):
(module / "main_desktop.py").write_text(helper)
(module / "main.py").write_text('raise AssertionError("old helper import after update")\n')
check_launch([url], wayland, ["--ozone-platform=x11", url], gpu="0")
# Later runtimes return a fifth option, the renderer accessibility switch.
(module / "main_desktop.py").write_text(helper + '''
_first_four = _desktop_launch_options
def _desktop_launch_options():
from hermes_cli.config import load_config
return (*_first_four(), load_config()["desktop"].get("renderer_accessibility", True))
''')
check_launch([url], wayland, ["--ozone-platform=x11", url], gpu="0")
config.write_text(json.dumps({"desktop": {"renderer_accessibility": False}}))
check_launch(overrides=wayland, expected_args=["--ozone-platform=wayland"], a11y="0")
check_launch(overrides={**wayland, "HERMES_DESKTOP_RENDERER_ACCESSIBILITY": "1"},
expected_args=["--ozone-platform=wayland"], a11y="1")
(module / "main_desktop.py").write_text(helper)
config.unlink()
(module / "main_desktop.py").write_text(helper + '\nimport os\nos.environ.update(' + repr({
"ELECTRON_RUN_AS_NODE": "1", "PYTHONPATH": "/invalid", "PYTHONHOME": "/invalid",
@@ -149,11 +170,15 @@ def with_hermes_node_path(env=None):
fallback_launcher.write_text(launcher.read_text().replace("/opt/hermes-desktop/Hermes", str(fallback)))
launch = ["bash", str(fallback_launcher)]
executable.unlink()
check_launch([url], wayland, ["--ozone-platform=wayland", url])
# Only the packaged app is told to skip an existing Hermes, unless the environment already says.
check_launch([url], wayland, ["--ozone-platform=wayland", url], ignore="1")
check_launch([url], {**wayland, "HERMES_DESKTOP_IGNORE_EXISTING": "0"}, ["--ozone-platform=wayland", url],
ignore="0")
(module / "main_desktop.py").write_text('raise ImportError("incomplete Python dependencies")\n')
check_launch([url], wayland, ["--ozone-platform=wayland", url])
check_launch([url], wayland, ["--ozone-platform=wayland", url], ignore="1")
shutil.rmtree(runtime / "venv")
check_launch(overrides={"ELECTRON_RUN_AS_NODE": "1", "PYTHONPATH": "/invalid", "PYTHONHOME": "/invalid"})
check_launch(overrides={"ELECTRON_RUN_AS_NODE": "1", "PYTHONPATH": "/invalid", "PYTHONHOME": "/invalid"},
ignore="1")
check_namespace_failure()
gate = ["bash", str(destination), "--self-test-gate", "--install-root", str(runtime),