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:
3 files changed
+44
-11
No files matched your search
@@ -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}"
|
||||
|
||||
Regular → Executable
+11
-3
@@ -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 (
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user