Files
hermes-agent/scripts/desktop-cli/launcher-wrapper.py
ethernet 92686159d1 fix(pm): integrate audited runtime and lifecycle repairs
Prepare dependency generations before selecting them. Keep shipped tool
bytes separate from writable additions, and store facts beside their entries.
Validate proposed plugin sets before config publication. Restore the previous
config if the facts write fails.

Consolidate duplicate updater, backup, setup, and voice helpers. Repair
launcher selection, dependency consumers, download ownership, update feeds,
and native Windows process and file handling.

Verification: 206 changed/prior-failing Python files reported 4630 passed,
one failed, and 330 skipped. Fix the remaining Hindsight fixture boundary.
The final targeted rerun reported 234 passed and two skipped. The store
review regression batch reported 83 passed and one skipped. Desktop
TypeScript checks, 56 selected Electron tests, 24 release tests, and the
removed-import/compatibility guards passed.

This is an integration checkpoint, not full audit acceptance. The complete
Python suite has not run on this fixed tree. Crash-atomic plugin publication,
generation cleanup, receipt correlation, and packaged lifecycle acceptance
remain open in docs/pm-audit-status.md.
2026-09-05 22:36:48 -04:00

108 lines
4.5 KiB
Python

"""Bundled-payload CLI entry wrapper — the distlib launcher's zip overlay.
scripts/build-bundled-desktop.mjs reads this file, substitutes the four
HERMES_* placeholders (see the constants below), and hands the result to
a distlib ScriptMaker as
the script text. On win32 the minted artifact is a real PE: the distlib
launcher stub + a shebang whose interpreter is the ``<launcher_dir>``
placeholder form (resolved at runtime relative to the launcher's own
directory) + this script packed as __main__.py in a zip overlay. The
The wrapper replaces everything the old rust shim + its sidecar did
(plan: pm-clean):
* resolve the launcher's own directory from sys.argv[0] (the distlib
launcher puts its exe path there — the shebang's ``<launcher_dir>``
placeholder resolves the interpreter the same way),
* put the payload repo snapshot FIRST and the venv site-packages second
on sys.path (same order, same reason as the old shim: the repo's
hermes_cli wins over anything stale in site-packages — the sealed
payload's venv has no working editable install, its pointer names the
BUILD machine),
* drop inherited PYTHONPATH / PYTHONHOME so foreign installs can never
shadow the bundle,
* default sys.pycache_prefix to the user-level cache (%LOCALAPPDATA%
\\hermes\\pycache on win32, ~/.cache/hermes-pycache elsewhere) when the
user has not set one — the sealed payload must never see __pycache__
writes (signature-breaking on mac, read-only mount on AppImage/MSIX).
The whole file is import-safe so tests can drive configure()/main()
directly (tests/scripts/test_desktop_cli_wrapper.py).
"""
import importlib
import os
import sys
HERMES_ENTRY_MODULE = "__HERMES_ENTRY_MODULE__"
HERMES_ENTRY_FUNC = "__HERMES_ENTRY_FUNC__"
HERMES_REPO_REL = "__HERMES_REPO_REL__"
HERMES_SITE_REL = "__HERMES_SITE_REL__"
def _join_rel(here, rel):
"""Join a forward-slash relative path (the minted-in convention, same
as the payload manifest) onto a host dir without os.sep assumptions."""
return os.path.join(here, *rel.split("/"))
def launcher_dir(argv0):
"""The directory of the launcher itself, from sys.argv[0]. abspath
because cwd is meaningless for a GUI/alias launch."""
return os.path.dirname(os.path.abspath(argv0))
def payload_sys_paths(here):
"""Import roots for the sealed payload: repo first (wins), then the
venv's dependency tree — mirroring the old shim's PYTHONPATH order."""
return [_join_rel(here, HERMES_REPO_REL), _join_rel(here, HERMES_SITE_REL)]
def default_pycache_dir(environ):
"""User-level bytecode cache. The sealed payload is read-only at
runtime (or signature-sealed, on mac), so __pycache__ writes must
land outside it. Only consulted when the user has not set their own
PYTHONPYCACHEPREFIX."""
if sys.platform == "win32":
base = environ.get("LOCALAPPDATA")
return os.path.join(base, "hermes", "pycache") if base else None
base = environ.get("HOME")
return os.path.join(base, ".cache", "hermes-pycache") if base else None
def configure(here, environ=None):
"""Point THIS process (the minted launcher's python) at the payload.
Returns the sys.path entries prepended, for tests."""
environ = os.environ if environ is None else environ
# Same hygiene as the old rust shim: an inherited PYTHONPATH could
# shadow bundled modules with foreign ones; PYTHONHOME would repoint
# the stdlib entirely.
environ.pop("PYTHONPATH", None)
environ.pop("PYTHONHOME", None)
entries = payload_sys_paths(here)
sys.path[0:0] = entries
if not environ.get("PYTHONPYCACHEPREFIX"):
default = default_pycache_dir(environ)
if default:
environ["PYTHONPYCACHEPREFIX"] = default
# The env var is only read at interpreter startup; we ARE at
# startup, but setting it in os.environ cannot retro-activate
# it — sys.pycache_prefix is the live switch.
sys.pycache_prefix = default
return entries
def main(argv=None):
argv = list(sys.argv if argv is None else argv)
here = launcher_dir(argv[0] if argv else sys.argv[0])
configure(here)
importlib.import_module("hermes_bootstrap")
module = importlib.import_module(HERMES_ENTRY_MODULE)
target = getattr(module, HERMES_ENTRY_FUNC)
# main() reads sys.argv; hand it the real argv (argv[0] stays the
# launcher path, exactly like a console-script entry point).
return target()
if __name__ == "__main__":
sys.exit(main())