Skip to content

fix(desktop-entry): keep the venv interpreter in Exec= instead of its symlink target - #92090

Open
jackulau wants to merge 1 commit into
NousResearch:mainfrom
jackulau:fix/92086-desktop-entry-venv-symlink
Open

jackulau wants to merge 1 commit into
NousResearch:mainfrom
jackulau:fix/92086-desktop-entry-venv-symlink

Conversation

@jackulau

Copy link
Copy Markdown

What does this PR do?

hermes desktop regenerates ~/.local/share/applications/hermes.desktop on
every launch. Its Exec= line was built from Path(sys.executable).resolve().

On POSIX a venv's bin/python is a symlink to the base interpreter - the
default for both python -m venv and uv venv - so .resolve() follows it out
of the venv and names an interpreter that has none of the venv's site-packages:

as-is   : .../venv/bin/python
resolved: .../uv/cpython-3.11/bin/python3.11   <- what went into Exec=

The desktop environment then launches an entry that dies on
import hermes_cli. Because the entry sets Terminal=false, the traceback goes
nowhere: clicking the pinned launcher does nothing at all - no window, no error,
no spinner. #92086 reports exactly that, and its "uv-managed python3.11 shim" is
not a stray interpreter that happened to launch Hermes; it is the reporter's own
venv python with the symlink followed.

The same call broke the guard above it

_needs_interpreter() decides whether the launcher script needs an explicit
interpreter prefix, by checking whether its shebang points inside the running
interpreter's environment:

exe_dir = str(Path(sys.executable).resolve().parent)
return exe_dir not in shebang

exe_dir is the base interpreter's directory, so a console script carrying
#!/.../venv/bin/python - the correct interpreter - fails the comparison and is
classified as needing a prefix. The two failures compose: the code decides to
prefix, and then prefixes the interpreter that cannot import Hermes. Without the
.resolve(), that wrapper is recognised as already correct and the entry is
left as a plain Exec=<wrapper> desktop, which works.

And a third defect the tests surfaced

shebang is lowercased when it is read; the interpreter path it was compared
against was not. So any install path containing an uppercase character
(/home/User/..., ~/Projects/..., anything on Windows) never matched, and
every wrapper on such a host was classified as foreign. The comparison is now
case-folded on both sides.

Why not the fix suggested in the issue

The issue proposes preferring shutil.which("hermes") over resolve_hermes_bin().
That works on the reporter's host, but it resolves the launcher through PATH
rather than through "which Hermes is running", so on a machine with more than
one install the entry can end up pointing at a different install than the one
that wrote it. resolve_hermes_bin() prefers sys.argv[0] precisely for that
reason. This fixes the corrupted interpreter path instead of reordering the
preference around it.

StartupNotify=false, also suggested there, is deliberately not included:
it is a separate UX question about token propagation that I cannot observe, and
it should not ride along on a correctness fix.

Related Issue

Fixes #92086

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/linux_desktop_entry.py
    • New _running_interpreter(): os.path.abspath(sys.executable) - absolute,
      as the entry requires, without following the venv symlink. Both Exec=
      branches (the interpreter prefix and the -m hermes_cli.main fallback) now
      use it.
    • _needs_interpreter() accepts a shebang naming either spelling of the
      interpreter, so a script installed against the base interpreter is still
      recognised, and case-folds the comparison.
  • tests/hermes_cli/test_linux_desktop_entry.py: 6 new tests.

How to Test

  1. pytest tests/hermes_cli/test_linux_desktop_entry.py -q
  2. Mutation proofs that the tests bind the fix rather than the refactor:
    • Restore Path(sys.executable).resolve() in _running_interpreter →
      4 of the new tests fail, including
      test_running_interpreter_keeps_the_venv_python and both Exec= tests.
    • Keep the fix but drop the case-fold → the two
      test_needs_interpreter_accepts_* tests fail.
  3. On a Linux host whose Hermes runs from a venv created by uv venv or
    python -m venv (symlinked bin/python):
    $ hermes desktop
    $ grep '^Exec=' ~/.local/share/applications/hermes.desktop
    
    The path before desktop should be the venv's own bin/python (or the
    wrapper, unprefixed), not the interpreter readlink -f reports for it.
    gtk-launch hermes.desktop should open the app.

On coverage and what I could not run

The new tests build a real base-interpreter-plus-symlinked-venv tree in
tmp_path and drive resolve_exec_command() / _needs_interpreter() directly,
rather than going through install_desktop_entry(), so they assert on the
decision instead of on a rendered Exec line with platform-specific quoting.
They skip if the platform cannot create symlinks.

The venv path in the fixture deliberately contains an uppercase segment, so the
case-fold guard binds on Linux CI and not only on Windows.

I have not run this on Linux. I am on Windows 11, where venvs copy the
interpreter instead of symlinking it, which is exactly why this bug has never
shown up on this platform. Everything above is source reasoning plus tests that
reproduce the symlink topology; the end-to-end gtk-launch check in step 3 is
for someone with the affected host. @the reporter of #92086 - if you run step 3
against this branch I will fold in whatever it shows.

Worth noting the existing test_exec_leaves_venv_shebang_scripts_alone did not
catch this: it builds the script's shebang from Path(sys.executable).resolve(),
the same corrupted value the code used, so the test agreed with the bug.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — docstrings only; no user-facing doc describes this internal
  • N/A — no config keys added or changed
  • N/A — no architecture or workflow change
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — the module is gated behind is_supported() (Linux/BSD). macOS venvs symlink too, so the same defect existed there for anyone reaching this code; Windows venvs copy the interpreter, so abspath and resolve agree and nothing changes
  • N/A — no tool descriptions or schemas touched

Test run honesty

pytest tests/hermes_cli/test_linux_desktop_entry.py -q was run with and
without this change on the same machine:

  • baseline: 7 failed, 9 passed, 1 skipped
  • with this change: 7 failed, 15 passed, 1 skipped

The same 7 fail on both sides. They are the pre-existing Windows-quoting
mismatches in this file (Exec escapes backslashes, so an assertion comparing
against a raw WindowsPath cannot match) and are unrelated to this change.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) area/install-update Installer, updater, packaging, wheels, doctor sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades P2 Medium — degraded but workaround exists labels Aug 22, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: #87269 covers the same venv-symlink preservation goal, while merged #90492 is the current Exec-resolution baseline. This PR also corrects the current _needs_interpreter matching path; maintainers should consolidate the launcher approaches.

@jackulau

Copy link
Copy Markdown
Author

CI caught something my Windows run structurally could not, and it is the same blind spot this PR is about.

test_exec_prefixes_interpreter_for_env_shebang_python_script built its expected interpreter with Path(sys.executable).resolve() — the exact expression this PR removes. On Linux CI, where the venv's bin/python is a symlink, that made the test demand the base interpreter and fail against the corrected Exec= line. On Windows it passed either way, because Windows venvs copy the interpreter, so abspath and resolve are the same string. That is the same reason the underlying bug never shows up on this platform.

So this file had two tests written against the bug, not one. I called out test_exec_leaves_venv_shebang_scripts_alone in the description and missed its neighbour.

Fixed by writing the expectation from what the Exec= line is for — an absolute path to an interpreter that can import hermes_cli — rather than by calling _running_interpreter() back, so it still fails if the helper ever starts resolving again. test_exec_leaves_venv_shebang_scripts_alone keeps its behaviour, because a console script installed against the base interpreter is a real case that must keep working, but it now says that its name promises a venv shebang while it actually writes the base interpreter's path — which is how it passed against the bug it looks like it covers.

Squashed into the one commit and rebased onto current main (fce30d8); the test change is not independent of the fix, it is the fix being finished.

Separately, #92095 came in as a duplicate report from a different host and derived the same .resolve() mechanism independently. Worth reading alongside this — it confirms the diagnosis from a machine I do not have.

@vampyren

vampyren commented Aug 22, 2026 •

Copy link
Copy Markdown

Please consider merging this PR as a necessary complement to #90492 which was done by @teknium1

I originally tested and supported #90492 because it correctly fixes the first problem: the generated desktop entry must not run the repository’s #!/usr/bin/env python3 launcher under system Python.

However, after applying #90492 on my affected Ubuntu 26.04 installation, I found that its use of:

Path(sys.executable).resolve()

still produces a broken Exec= line on a normal uv-managed POSIX virtual environment. My Hermes venv uses the expected symlink layout:

~/.hermes/hermes-agent/venv/bin/python
  -> ~/.local/share/uv/python/.../bin/python3.11

Resolving that symlink writes the base uv interpreter into Exec=. That interpreter does not see the Hermes venv’s pyvenv.cfg or site-packages, so launching from the desktop icon still fails silently with missing dependencies such as dotenv.

I verified the difference directly:

  • Resolved base interpreter: ModuleNotFoundError: No module named 'dotenv'
  • Preserved venv interpreter: dotenv, openai, and yaml imports succeed
  • Generated launcher uses the correct venv/bin/python
  • Launcher probe exits successfully
  • Desktop-entry tests: 16 passed, 2 skipped

So #90492 fixes the incorrect repo-script/shebang behavior, while #92090 completes that fix by preserving the venv interpreter path and correcting the related _needs_interpreter() comparison.

This is not a damaged or unusual local environment: symlinked Python executables are normal for POSIX virtual environments and uv installs. Please merge #92090 so the #90492 fix also works reliably on these installations.

Tested on Ubuntu 26.04 with Python 3.11.16 and a uv 0.12.5-managed Hermes venv.

@alt-glitch alt-glitch added the needs-decision Awaiting maintainer decision before any implementation label Aug 22, 2026
… symlink target

`resolve_exec_command()` wrote `Path(sys.executable).resolve()` into the
generated `hermes.desktop`. On POSIX a venv's `bin/python` is a symlink to
the base interpreter - the default for both `python -m venv` and
`uv venv` - so resolving it leaves the venv and names an interpreter with
none of the venv's site-packages. The desktop environment then launches an
entry that dies on `import hermes_cli`, and because the entry sets
`Terminal=false` the launcher simply does nothing: no window, no error
(NousResearch#92086).

The same call broke `_needs_interpreter()` one layer up. It compared a
script's shebang against `Path(sys.executable).resolve().parent`, so a
console script carrying the venv's own `bin/python` - the correct
interpreter - failed the comparison and was classified as needing a
prefix. The two compose: the code decides to prefix, then prefixes the
interpreter that cannot import Hermes.

`_running_interpreter()` returns `os.path.abspath(sys.executable)`:
absolute, as the entry requires, without leaving the environment that can
actually run Hermes. `_needs_interpreter()` now accepts a shebang naming
either spelling, so a script installed against the base interpreter is
still recognised and NousResearch#90292's foreign-shebang prefix still fires.

Also case-folds that comparison. The shebang was already lowercased on
read while the interpreter path was not, so any install path containing an
uppercase character never matched and every wrapper was classified as
foreign.

Two existing tests had built their expectations from the same
`Path(sys.executable).resolve()` the fix removes, so they agreed with the
bug. `test_exec_prefixes_interpreter_for_env_shebang_python_script` now
writes its expectation from what the Exec line is for - an absolute path
to an interpreter that can import `hermes_cli` - rather than by calling
the helper back, so it still fails if the helper starts resolving again.
`test_exec_leaves_venv_shebang_scripts_alone` keeps its behaviour, since a
script installed against the base interpreter is a real case, but says so:
its name promised a venv shebang while it wrote the base interpreter's
path, which is how it passed against the bug it looks like it covers.

Neither reproduces on Windows, where venvs copy the interpreter and
`abspath` and `resolve` agree - the same reason the underlying bug never
shows up on this platform.

Fixes NousResearch#92086
@jackulau
jackulau force-pushed the fix/92086-desktop-entry-venv-symlink branch from 8cb027a to f08fef4 Compare August 22, 2026 11:19
@jackulau

Copy link
Copy Markdown
Author

@vampyren thank you, and specifically thank you for the numbers rather than a
"works for me". I want to be clear about what your comment does for this PR
that I could not do myself.

I am on Windows. Windows venvs copy the interpreter into Scripts\, they
do not symlink it, so abspath and resolve are the same string there and this
bug cannot reproduce on my machine at all. Every test I wrote is a fixture that
simulates the POSIX symlink layout. Your run is the first time the fix has
been exercised against a real one:

  • resolved base interpreter -> ModuleNotFoundError: No module named 'dotenv'
  • preserved venv interpreter -> dotenv, openai, yaml all import
  • generated launcher uses venv/bin/python
  • launcher probe exits 0

That is the causal chain the PR asserts, observed end to end on hardware I do
not have. It is now the strongest evidence on this PR, so I have said so here
rather than leaving it buried in a thread.

On the framing: I agree with you that this completes #90492 rather than
competing with it, and I have just rebased onto current main (261a4efb) so
the two are readable together. #90492 is merged and its behaviour is intact
here. The if _needs_interpreter(resolved): branch and @teknium1's comment
explaining #90292 are untouched; the only thing that changed inside it is which
interpreter gets named, via a new _running_interpreter() helper. So the diff
now reads as "keep #90492's decision, fix the expression it decides with",
which is what it actually is.

Two things worth stating plainly for whoever reviews this:

Path(sys.executable).resolve() is still on current main in three places
(linux_desktop_entry.py lines 81, 85 and 109 before this PR). Two of them
build Exec=. The third is inside _needs_interpreter, and it is the reason
the second half of this PR exists: comparing a shebang against only the
resolved directory misclassifies a correct venv console script as foreign,
because a console script installed into a venv carries the venv's own
bin/python in its shebang. On your layout that means the entry gets an
interpreter prefix it does not need, naming the interpreter that cannot import
Hermes. Both spellings now count.

It is not an unusual environment, as you say. Symlinked bin/python is the
default for python -m venv on POSIX and for uv venv; the resolved layout is
the exception, not the rule. So the failure is the common case on Linux, and it
is silent because the entry sets Terminal=false.

Local run on the rebased head: tests/hermes_cli/test_linux_desktop_entry.py
is 15 passed / 7 failed / 1 skipped, and the same 7 fail identically on clean
261a4efb with the PR reverted. They are POSIX-path assertions that cannot pass
on Windows, which is the same platform gap your run just covered from the other
side. Your 16 passed / 2 skipped is the number that matters for this file, and I
would rather cite yours than mine.

Ready for review whenever a maintainer has time.

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference, author can ignore or act on any point.

Excellent root-cause work on a nasty silent failure. The core insight — POSIX venvs symlink bin/python to the base interpreter, so .resolve() answers "which interpreter owns this binary" when the entry needs "which environment can import hermes_cli" — is argued precisely, and the way it composes with _needs_interpreter's mismatched comparison (resolved-vs-unresolved dirs classifying a correct venv shebang as foreign, then prefixing the interpreter that can't import Hermes) explains why the launcher died invisibly twice over. The case-fold defect on top is a genuine third find. Rejecting the issue's shutil.which("hermes") suggestion with the multi-install argument is correct, and keeping StartupNotify out of scope is good discipline. Mutation proofs (revert .resolve() → 4 fail; drop case-fold → 2 fail) show the tests bind the fix, and rewriting the previously self-agreeing test from purpose ("an absolute path to an interpreter that can import Hermes") rather than implementation is exemplary.

Notes:

  1. Coordinate with fix(desktop): resolve a Hermes-capable interpreter for the .desktop Exec #92088 — it claims the same issue Linux .desktop launcher fails silently when sys.executable lacks hermes_cli (e.g. uv-managed install) #92086 with a different theory (sys.executable being a genuinely foreign interpreter) and touches this same function. Worth stating in both PRs which host topology each covers; ideally the final merge takes your abspath semantics plus a capability check, since each fix alone misses the other's scenario (yours prefixes a foreign shim verbatim; theirs keeps .resolve(), which reproduces this bug on symlinked venvs with no PATH wrapper).
  2. Since you disclosed 7 pre-existing Windows-quoting failures in this test file: a small follow-up gating those assertions properly on the platform (or comparing quoting-aware output) would take the file green everywhere and stop the noise from masking real regressions like this one.
  3. The uppercase-segment fixture forcing the case-fold path to bind on Linux CI is a thoughtful touch — that guard would otherwise be untestable off-Windows.

Copy link
Copy Markdown

Tested this exact PR head (f08fef416079449902869ca012a1bcfe6a533982) end-to-end on an affected Linux host.

Environment:

  • Ubuntu 26.04 LTS, GNOME/Wayland, x86_64
  • Hermes Agent v0.20.5, git/uv install
  • Python 3.11.16
  • <install>/venv/bin/python is a symlink to the uv-managed base interpreter

Validation:

scripts/run_tests.sh tests/hermes_cli/test_linux_desktop_entry.py -q
21 passed, 0 failed, 2 skipped

I generated the launcher from this PR in an isolated XDG_DATA_HOME, leaving the normal menu entry untouched. It retained the venv interpreter:

Exec=<install>/venv/bin/python <install>/hermes desktop

For comparison:

sys.executable = <install>/venv/bin/python
resolved       = <uv-cache>/cpython-3.11.16-linux-x86_64-gnu/bin/python3.11

Then:

XDG_DATA_HOME=<temporary-xdg-dir> gtk-launch hermes
exit status: 0

The Electron process remained alive, and its backend used the correct venv path:

<install>/venv/bin/python -m hermes_cli.main serve --host 127.0.0.1 --port 0

desktop.log confirmed HERMES_BACKEND_READY followed by Hermes backend is ready. Finalizing desktop startup.

This is a successful real-host Linux/uv regression and end-to-end validation of the PR.

@jackulau

Copy link
Copy Markdown
Author

@uncrayon this is the second independent real-host confirmation on this PR and the more complete of the two, because you ran the launcher rather than only the suite. Thank you.

The line that matters most is the one nobody could have produced from my machine:

sys.executable = <install>/venv/bin/python
resolved       = <uv-cache>/cpython-3.11.16-linux-x86_64-gnu/bin/python3.11

Two different interpreters, and only the first can import Hermes. On Windows those are the same string, because Windows venvs copy the interpreter into Scripts\ instead of symlinking it, so this bug is not reachable on my hardware at all. Every test I wrote simulates your layout; you have now measured it, and gtk-launch exiting 0 with HERMES_BACKEND_READY in desktop.log closes the loop from the click through to the backend.

On coordinating with #92088, which I think is the important open question here

@Enough1122's first note asks that #92088 and this PR say which host topology each covers. Having read theirs properly, I do not think this is two fixes for one bug so much as two halves of one, and the seam between them is sharp enough to name.

#92088's theory: sys.executable may be a genuinely foreign interpreter (a uv or system shim) that cannot import hermes_cli, so choose the prefix by capability rather than trusting it. That is a real scenario and this PR does not handle it: I keep sys.executable and would prefix a foreign shim verbatim.

This PR's theory: sys.executable is the right interpreter and .resolve() is what destroys it, because a POSIX venv's bin/python is a symlink to the base interpreter. Your two lines above are that claim, measured.

The two compose badly right now, and specifically at one expression. #92088's ladder is:

if _can_import_hermes_cli(Path(sys.executable).resolve()):
    return [str(Path(sys.executable).resolve()), str(script), "desktop"]
wrapper = shutil.which("hermes")
if wrapper:
    return [str(Path(wrapper).resolve()), "desktop"]
return [str(Path(sys.executable).resolve()), "-m", "hermes_cli.main", "desktop"]

Every branch names Path(sys.executable).resolve(). On your host that is the uv-cache interpreter, so:

  • The capability probe runs against the base interpreter, which is the one that cannot import Hermes. It correctly answers "no" — and in doing so discards sys.executable, the one interpreter on the box that would have worked.
  • It then falls to shutil.which("hermes"). That may well succeed on a host that has ~/.local/bin/hermes, which is why this may not have shown up in their testing.
  • If PATH has no wrapper, the last branch runs <uv-cache python> -m hermes_cli.main, which is the same interpreter the probe just proved cannot import hermes_cli. That branch cannot succeed on a symlinked venv; it is unreachable-by-correctness rather than merely unlikely.

So on the default POSIX layout, #92088 either routes around the venv or terminates in a branch that is dead by construction, and it does so because of the .resolve() this PR is about.

The composition is one substitution, not a merge conflict. If #92088 probes and emits os.path.abspath(sys.executable) instead of Path(sys.executable).resolve(), branch 1 fires on your host and emits <install>/venv/bin/python — the same string this PR produces — and branches 2 and 3 are then reached only in the genuinely-foreign case that #92088 exists to handle. Their capability gate covers my gap; my abspath semantics make their ladder correct on the common layout. Neither is redundant.

I have no stake in which PR carries it. If a maintainer prefers #92088 as the base, this PR's _running_interpreter() helper and the _needs_interpreter case-fold fix drop into it cleanly and I will do that work; if they prefer this one, their capability check is a small addition on top. What I would not want is either landing alone, since each reintroduces the other's bug. I will say the same on #92088 so both threads have it.

The other review note

@Enough1122's second point, about the 7 pre-existing Windows-quoting failures in test_linux_desktop_entry.py, is fair and I agree it masks real regressions. It is unrelated to this fix, so I would rather not widen this diff with it; I will take it as its own PR.

@uncrayon

Copy link
Copy Markdown

Thanks! I was just annoyed of running all the time hermes desktop to open the client.

@jackulau

Copy link
Copy Markdown
Author

That is the useful half of the report, actually. "I was annoyed of running hermes desktop all the time" is a workflow complaint, and it is what makes this worth fixing rather than documenting: the failure was not that the command is hard, it was that you had to keep issuing it.

Thanks for filing it with enough detail to reproduce on the first try.

@alt-glitch alt-glitch removed the comp/desktop Electron desktop app (apps/desktop/*) label Aug 22, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: #92088 handles the genuinely foreign-interpreter case, while this PR preserves the valid symlinked venv interpreter. The final repair should combine the capability check with unresolved interpreter semantics; #90492 is the merged baseline.

@vampyren

Copy link
Copy Markdown

Thanks! I was just annoyed of running all the time hermes desktop to open the client.

hehe exactly, but still we have that issue since the first time after update it ask for password so its still not perfect. I haven't figured out why....

@autumn8-builds

Copy link
Copy Markdown

@jackulau — cross-posting from #92122: I've incorporated your os.path.abspath() fix (and the _needs_interpreter case-fold) into #92122, since our two PRs target the same root cause. No ask to close this one — whichever the maintainers prefer as the base is fine with me, and I'd rather not have two fixes racing. Thanks for the thorough write-up; the symlink-vs-abspath reasoning is exactly the part I'd have missed.

@alt-glitch alt-glitch added the comp/desktop Electron desktop app (apps/desktop/*) label Aug 23, 2026
@jackulau

Copy link
Copy Markdown
Author

@autumn8-builds Appreciated, and agreed on not racing.

I have reviewed #92122 and left the detail there rather than here. Short version:
it is the more general fix and I am happy for it to be the base. There is one
thing I would want closed first — _interpreter_for's second rung returns
shutil.which("hermes") resolved and unprefixed, which on this repo's
resolve_hermes_bin() ordering is the same file that _needs_interpreter()
just rejected one frame earlier, so on exactly the machines where
sys.executable cannot import hermes_cli the generated Exec= silently
reverts to the broken form. Not a hard problem (gate the rung, or drop it and
let rung 3 answer), and it does not change my view on which PR should land.

Maintainers: treat this as my recommendation to prefer #92122 and close this
one. I will close it myself the moment that rung is settled, rather than now,
only so the narrower fix stays available if you would rather take the small
change first and the interpreter ladder separately. Both are MERGEABLE and green
against the same two files, so whichever you take, the other rebases to nothing.

For the record on what is not duplicated: the symlink point is now in both
PRs, but the case-fold in _needs_interpreter and the .desktop quoting
behaviour are independent of the interpreter ladder, so nothing is lost either
way.

@monerostar monerostar left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ubuntu 26.04 on a local 5800X box (kernel 7.0.0-30-generic). Hermes v0.20.3, CPython 3.11.15 from the managed runtime.

This install is the uv-style venv symlink case:

  • sys.executable = /home/hermes/.hermes/hermes-agent/venv/bin/python (symlink)
  • Path.resolve() = .../.hermes-runtime/python/generation-1785724843-18242-a8c8abe2/cpython-3.11.15-linux-x86_64-gnu/bin/python3.11

Live import check:

  • venv python: import yaml ok
  • resolved base: ModuleNotFoundError: No module named 'yaml'

Forced #!/usr/bin/env python3 launcher (the prefix path):

  • main resolve_exec_command() still writes the resolved base into Exec=
  • this PR writes the venv symlink path instead

pytest tests/hermes_cli/test_linux_desktop_entry.py -o addopts=

  • main: 15 passed, 2 skipped
  • this PR: 21 passed, 2 skipped

Siblings #92516, #92278, #92285 hit the same Exec prefix idea. Prefer this one for the fuller matrix (venv shebang, resolved shebang, env python3, module fallback).

Looks good.

@autumn8-builds

Copy link
Copy Markdown

@monerostar — thank you for the independent real-host confirmation. Especially useful that your box is on the exact same kernel (7.0.0-30-generic) as the original reporter's, so this is a true repro of the same environment rather than a near-miss. Your import matrix — venv python imports yaml; the resolve()d base throws ModuleNotFoundError: No module named 'yaml' — is precisely the failure the capability ladder in #92122 was built to catch, and your "15 → 21 passing" delta on #92090 lines up with the sibling fix landing.

For visibility: #92122 (the base @jackulau recommended) already folds in #92090's os.path.abspath semantics plus the _needs_interpreter case-fold and the rung-2 gate he flagged, so the combined fix covers the venv-shebang, resolved-shebang, foreign-python3, and -m fallback paths in one place. Jack indicated he'd close #92090 in favor of #92122 once rung 2 was settled — and it now is — so we're ready for that consolidation whenever he's satisfied. Appreciate you putting the matrix in writing.

@NeoAiLabs

Copy link
Copy Markdown

Confirmed the underlying failure mode on an affected Arch Linux installation (CPython 3.11.16 / uv-managed venv)… preserving the symlink spelling for desktop-entry Exec= is necessary on this platform too

@cosminfuica

Copy link
Copy Markdown

Ran the end-to-end verification you asked for in step 3, on an affected host. Everything holds. Details below so this is reproducible.

Host: Arch-based Linux (Omarchy, Hyprland/Wayland), Hermes venv created by uv, Python 3.11.16, pytest 9.1.1.

The venv topology is exactly the shape this PR targets — with a detail worth noting:

$ ls -l venv/bin/python
venv/bin/python -> ~/.local/share/uv/python/cpython-3.11-linux-x86_64-gnu/bin/python3.11
$ readlink -f venv/bin/python
~/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/bin/python3.11

The symlink itself names the minor-versioned store dir, but .resolve() follows a second hop and lands on the patch-pinned cpython-3.11.16 path — the one that vanishes when uv upgrades the patch version. So the old code was strictly worse than even the visible symlink target.

1. Test suite

  • This branch: pytest tests/hermes_cli/test_linux_desktop_entry.py -q → 21 passed, 2 skipped. The 7 Windows-quoting failures from your baseline run don't exist here — the file is fully green on Linux.
  • Mutation proof: your test file copied onto the merge-base (261a4efb9, pre-fix) → 6 failed, 15 passed, 2 skipped. All six failures are the new coverage:
    test_exec_prefixes_interpreter_for_env_shebang_python_script, test_running_interpreter_keeps_the_venv_python, test_needs_interpreter_accepts_a_shebang_naming_the_venv_python, test_needs_interpreter_accepts_a_shebang_naming_the_resolved_interpreter, test_exec_prefix_names_the_venv_python_not_its_target, test_module_fallback_names_the_venv_python_too.

2. resolve_exec_command() on the live install — merge-base vs. this branch

Driven directly with PYTHONPATH pointing at each tree and sys.argv[0] set per scenario (… = /home/<u>/.hermes/hermes-agent, UV = ~/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/bin/python3.11):

argv[0] scenario merge-base emitted this branch emits
venv console script (…/venv/bin/hermes, shebang #!…/venv/bin/python3) UV …/venv/bin/hermes desktop …/venv/bin/hermes desktop
bash wrapper (~/.local/bin/hermes) ~/.local/bin/hermes desktop ~/.local/bin/hermes desktop (unchanged, as claimed)
repo script (…/hermes, shebang #!/usr/bin/env python3) UV …/hermes desktop …/venv/bin/python …/hermes desktop
module fallback (no launcher on PATH) UV -m hermes_cli.main desktop …/venv/bin/python -m hermes_cli.main desktop

Both defects visible on baseline: the needless prefix on a correct venv shebang, and every prefix naming the interpreter that can't import Hermes. This branch fixes both, and the wrapper path is untouched.

3. Launching the emitted commands under env -i

(No PATH, no venv activation — what the DE actually does.)

# merge-base's emitted line:
$ env -i HOME=$HOME <UV> …/venv/bin/hermes --version
ModuleNotFoundError: No module named 'hermes_cli'

# this branch's lines:
$ env -i HOME=$HOME …/venv/bin/hermes --version            # ✅ runs, prints version
$ env -i HOME=$HOME …/venv/bin/python …/hermes --version   # ✅ runs, prints version

One honesty note: I substituted --version for desktop as the trailing arg because Hermes is currently running from this same venv and I didn't want to spawn a second GUI instance mid-session. The bug's failure point — the base interpreter never seeing the venv's site-packages — is upstream of argument handling, and the baseline line dies exactly there while this branch's lines get through the full import chain and execute. (The baseline dies on hermes_cli rather than yaml for the console script because venv/bin is the script dir; the repo-script variant dies on yaml. Same class.)

4. Full install_desktop_entry() into a clean XDG_DATA_HOME

$ grep '^Exec=' <tmp-xdg>/applications/hermes.desktop
Exec=/home/<u>/.hermes/hermes-agent/venv/bin/hermes desktop
$ desktop-file-validate <tmp-xdg>/applications/hermes.desktop   # ✅ passes

No uv store path, no patch version anywhere in the entry.


Disclosure: I'm the author of #96396, one of the duplicates of this PR — closing it in favor of this one, since this is the earlier canonical fix and it now has the Linux end-to-end run it was missing. #96777 carries a comparison table of all six open PRs for this bug class, for whoever consolidates.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux .desktop launcher fails silently when sys.executable lacks hermes_cli (e.g. uv-managed install)

9 participants