Commit Graph

24 Commits

Author SHA1 Message Date
ethernet
8f6d98e4c3 fix activation of devenv, use /usr/bin/env bash everywhere 2026-09-11 11:17:18 -04:00
Teknium
a662f9d513 fix(desktop-update): discard failed profile allocation output 2026-09-07 06:09:56 -07:00
Teknium
ca812ba3b5 fix(desktop-update): atomically claim the temporary browser profile
Use mktemp -d before launching the optional UI; skip UI if allocation fails. Native Chrome collision and allocation-failure probes preserve preexisting directories.
2026-09-07 06:09:56 -07:00
Teknium
1bd7364d8e fix(desktop-update): clean only the captured shim profile
Track the path actually launched, preserving the no-UI case and unrelated profiles. Adapted the ownership approach from #104362.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-07 06:09:56 -07:00
Rohith Pariki
97f74b8361 fix(desktop-update): clean up throwaway browser profile directory
Deletes the temporary --user-data-dir used by the update UI shim when the browser process is shut down, preventing ~100MB leaks per update. Fixes issue #104350.
2026-09-07 06:09:56 -07:00
Teknium
28044757aa fix(desktop-update): self-heal a TCC-anchor-bricked venv in the posix hand-off (#95759)
Installs converted by the reverted macOS TCC interpreter anchor
(#95425/#95541) are left with a real-file venv/bin/python copy, a
.tcc-anchor-source marker, and python3/python3.N aliases that die at
interpreter init ("No module named 'encodings'"). venv/bin/hermes execs
venv/bin/python3, so EVERY CLI entrypoint is dead — hermes doctor and
hermes update included — and the desktop hand-off loops on "Update failed
(exit 1)" forever. No Python-side heal can ever run on this class; the
hand-off shell is the last surface that still executes, so the heal lives
there.

posix.sh gains, before the update invocation:

* tcc_anchor_heal — probe-gated (only fires when venv/bin/python3 fails
  a scrubbed-env `import encodings` boot probe), marker-validated
  (absolute path, outside the venv), staged with per-attempt backups and
  full rollback if the repaired interpreter still fails its probe.
  Two repair shapes:
  - alias-brick (#95541 class): the anchored copy boots — re-materialize
    python3* as REAL FILES of the anchor (hardlink/copy; an alias symlink
    onto the copy is the crash shape). Marker kept: this is exactly the
    layout ensure_tcc_anchor marks "active", so no anchor ping-pong.
  - full brick: restore python → symlink to the marker-recorded store
    interpreter (if it boots) and aliases → symlinks; marker removed.
    The unblocked `hermes update` then re-installs a boot-gated healthy
    anchor — one-shot convergence, not a loop.
  Fail-closed on missing/unbootable source (vanished uv store class),
  missing marker, or relative/in-venv marker paths.
* tcc_pick_update_invoke — if aliases stay dead but venv/bin/python
  boots (the launchd-gateway shape), drive the update via
  `venv/bin/python -m hermes_cli.main` instead of the dead hermes shim.
* Honest terminal message: an unrecoverable dead interpreter is reported
  as a venv repair problem instead of a generic "Update failed (exit 1)".
* --self-test-tcc-heal runs the real heal + invoke selection against an
  --install-root and reports, for the test harness.

Tests (tests/test_desktop_update_tcc_heal.py) drive the REAL posix.sh
functions on Linux against synthetic venv trees: healthy no-op, alias
heal, symlink restore, fail-closed classes, rollback on failed
verification, invoke fallback, and an A/B of the reported loop (bricked
venv/bin/hermes fails with the exact field error before, boots after).
Sabotage-verified (re-introducing the alias-symlink bug fails 2 tests).

NOT mac-live-tested (no macOS runner); the heal is platform-independent
shell exercised through the self-test path without the uname gate.

Recovery design (validation/staging/rollback/probe pattern) after
@aeonsong's #96231; in-update heal intent from @liuhao1024's #95775
(its target function no longer exists on main and its heal point is
unreachable on dead-CLI installs); heal-point and ping-pong analysis by
@ahrazzle and @tokenfires on #95759.

Fixes #95759
2026-09-01 08:36:03 -07:00
4dlt
3a7f2234a6 fix(cli): use Chromium's namespace sandbox when userns is available on Linux
The desktop launcher demanded a root-owned 4755 chrome-sandbox on every
Linux host and shelled out to sudo to configure it. Launched from the
.desktop entry there is no TTY, so sudo fails silently and `hermes
desktop` exits without a window — and every update rebuilds the helper
user-owned, re-breaking the app (#88032, #51327). The update hand-off's
relaunch gate blocked on the same condition, so post-update auto-relaunch
never fired either (#58593).

On hosts where unprivileged user namespaces work, Chromium uses its
namespace sandbox and never consults the setuid helper. Probe the actual
capability with `unshare --user --map-root-user true` (fails closed) and
skip the sudo path when the probe succeeds; hosts with userns disabled or
AppArmor-restricted (Ubuntu 23.10+) keep the existing setuid-helper and
--no-sandbox fallback behavior unchanged. Sandboxing stays fully enabled
in both cases.

Fixes #88032
Fixes #51327
Fixes #58593

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 02:32:39 -07:00
fred0m
53057f2bc4 fix(update): run config migration on the 'Already up to date' repair path (#91360)
A failed update attempt can pull fresh code onto disk and then die before
the config-migration block (e.g. a PyPI timeout during the dependency
sync). The desktop hand-off retries; the retry takes the commit_count == 0
branch, repairs deps, prints 'Already up to date!' and returns early -
skipping _run_config_check_fresh / migrate_config entirely. The fresh
code (requiring a newer _config_version) then refuses to start against
the old config until 'hermes doctor --fix' is run.

Fix: _maybe_migrate_config_on_current() mirrors the version_bump_only
handling (silent, non-interactive) and is called on both repair-path
completion points before claiming success.

Also: scripts/desktop-update/posix.sh no longer retries when the update
was deliberately SKIPPED (checkout parked on a non-target branch) -, the
retry is deterministic and only wastes time. Uses a dedicated non-
colliding exit code (8) and an honest message instead of 'Update failed'.

New tests: tests/hermes_cli/test_update_config_migration_on_current.py
(5 cases: migrate-when-behind, noop-current, noop-ahead, warning re-
surface, silent check failure).
2026-08-26 17:42:50 -07:00
Teknium
da57f49237 fix(desktop-update): shim window only opens in the user's own browser family
A Safari/Firefox/Helium user who merely had Chrome installed watched Chrome open on every desktop update (community report). start_ui now checks the system default browser (LaunchServices https handler on macOS, xdg-settings on Linux) and skips the shim window unless the default is Chromium-family; notify_fallback and the durable result file still carry the outcome. Detection is best-effort: any failure keeps the old behavior.
2026-08-24 03:14:24 -07:00
jeremyrandria-debug
60bb2bb719 fix(update): auto-close desktop-update shim window after error/manual outcomes
On error/manual outcomes stop_ui('leave-window') kept the browser shim
window open indefinitely, so an aborted update left a Chrome window on
screen until the user closed it by hand; repeated update attempts piled
up more windows.

stop_ui now always closes the shim. leave-window paths keep it up for a
short grace period (HERMES_UPDATE_SHIM_GRACE_SECONDS, default 15) so a
watching user can read the message, then close it. The success path is
unchanged. The error/manual outcome is durably written to
.hermes-update-result.json and surfaced in a dialog on the next Desktop
boot, so closing the shim loses no information.
2026-08-24 03:14:24 -07:00
liuhao1024
eb21740b06 Also skip Brave: its P3A bar paints over the update shim window
Brave renders its own P3A privacy-notice bar ("Got it" / "Disable" /
"Learn more") at the top of the throwaway-profile window the posix shim
opens, cramped to unreadability at the shim's small size - the same
window-pollution class as Edge's MSA sync notice, and equally immune to
the throwaway --user-data-dir (#88682). Drop Brave from the candidates
on both platforms; Chrome and Chromium stay.

Covers #88682 on top of #88410
2026-08-24 03:14:24 -07:00
liuhao1024
f329f9e40d fix(desktop-update): never render the posix update shim in Edge
Edge's OS-level Microsoft-account integration signs even a fresh
throwaway profile into the user's MSA and renders its own "syncing
your browsing data" notification — the user's MSA email included —
inside the update window, which is titled "Hermes" (#88410). The
throwaway --user-data-dir start_ui already passes cannot block that
OS-account path, so the only reliable protection is to not pick Edge
at all: drop it from the browser candidates on both macOS and Linux.
The update UI is a best-effort layer — with no other Chromium-family
browser installed, start_ui falls back to its existing
"no renderer; skipping UI" path and the update itself is unaffected.

Fixes #88410
2026-08-24 03:14:24 -07:00
fangliquanflq
ee6a9f8326 fix(updater): carry acquisition age through scripts 2026-08-20 12:52:21 -05:00
Teknium
5dd221d442 feat: desktop updates no longer re-apply local source edits (--keep-stash)
The desktop updater ran `hermes update --yes`, which auto-restored any
uncommitted source-tree edits onto the freshly updated checkout. On dirty
from-source installs this silently carried local modifications across every
update and could break the rebuilt app (field report: Windows update handoff
leaving the app 'crashed').

New `hermes update --keep-stash`: local changes are still autostashed so the
update can proceed, but are never re-applied — they stay parked in git stash
with printed recovery guidance. Both desktop handoff scripts (windows.ps1,
posix.sh) now pass it, probing `update --help` first so older installed
backends without the flag keep working. Failure paths are unchanged (stash
preserved, no restore); updates.non_interactive_local_changes: discard still
wins.

Tests: park/restore/failure-path coverage incl. a sabotage-verified
regression test; docs updated.
2026-08-20 04:54:14 -07:00
Brooklyn Nicholson
e602225d86 test(update): cover the progress contract without a test hook in the updater
The self-test grew a branch that spawns a Python child so pytest could prove
progress advances during one. It doesn't need to: /progress is answered from
its own runspace, so the existing hold already blocks the main thread, and
the spawn only exercised Invoke-HermesStep, which nothing here changes.

The Windows test now asserts the invariant instead of the self-test's stage
string, and the posix half -- previously untested, and the half that broke --
gets real coverage: serve-ui.py's wire shape, and posix.sh driven end to end
with a stub `hermes` that reports which stage was on screen while it ran.
2026-08-19 13:16:13 -05:00
Brooklyn Nicholson
cf2ab8522a fix(update): one elapsed clock, served to the shim by both orchestrators
The shim is a shared page, but only windows.ps1 publishes a stage and an
elapsed count. On mac and Linux posix.sh publishes `running` with an empty
message and no clock, so the running branch rendered the h2 back into the
muted line ("Updating Hermes" twice) and started a clock in the browser,
losing "Hermes will open once done." on both platforms.

A clock started in the page measures when the window painted, not how long
the update has been running -- on posix that is the only clock there is, and
it reads zero after the desktop-exit wait has already burned 30s. That is the
hardcoded-milestone problem #75895 removed, in a new costume.

So: elapsed comes from the orchestrator or is not shown. serve-ui.py stamps
it per request from the hand-off start (a value written into the status file
would freeze between publishes, which are minutes apart -- exactly the stall
the line exists to disprove), matching what Windows' in-process listener
already does. posix.sh gets the stages it was missing, at the four gates it
genuinely waits on. Absent a stage the page keeps the settled copy, and an
old orchestrator that sends no clock simply shows no clock.
2026-08-19 13:16:13 -05:00
Yasushi Fukutake
b44f956dff fix(desktop-update): put --daemonized ahead of ORIGINAL_ARGS in posix.sh re-exec
Appending --daemonized after ORIGINAL_ARGS put it past the `--`
relaunch-args separator on Linux, so it was absorbed into
RELAUNCH_ARGS instead of being parsed as a flag. HANDOFF_DAEMONIZED
never got set, so the one-shot self-detach block re-fired on every
re-exec -- an unbounded self-exec loop (thousands of iterations/sec,
100%+ CPU, argv growing until execve fails with E2BIG) whenever
relaunch args were present, which is the normal invocation shape on
Linux.

Fixes #86957

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 06:25:21 -07:00
Alvis
cf8b505531 fix(desktop-update): make posix hand-off survive Electron quit teardown (macOS)
The Desktop-spawned hand-off consistently died during Electron's quit
teardown on macOS: the orchestrator process group was terminated right
after `running: hermes update ...`, so no exit code, result file, bundle
swap, or relaunch ever happened, and the loopback shim window surfaced
the death as ERR_CONNECTION_REFUSED or "Aw, Snap!" error code 15
(reproductions in #66753).

- Re-exec the orchestrator through a one-shot setsid child and let the
  direct Electron child exit immediately; the real orchestrator is owned
  by launchd (PPID 1), outside Electron's teardown, same marker/result
  protocol.
- Hold TERM ignored across the `hermes update` invocation and
  log-and-ignore the single teardown TERM that can still arrive after
  the desktop PID dies (durable SIGNAL breadcrumb for diagnosis).
- Delay start_ui until the desktop PID is gone plus 1s so the shim
  server/window are never born inside the teardown window.
- Run both UI processes in their own sessions; keep SIGTERM/SIGHUP
  ignored in the shim server and stop it with SIGKILL, so a stray TERM
  can no longer leave the progress window on a dead loopback URL while
  the update continues.

Verified on a production git install (macOS arm64, Darwin 27.0,
v0.20.1): six consecutive Desktop-triggered/production-shape updates
completed end-to-end including a full desktop rebuild + codesign; the
shim survived a deliberately injected TERM+HUP mid-update and a full
`hermes desktop --force-build --build-only` running alongside it.

Fixes the macOS reproductions in #66753.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 21:48:22 -07:00
Brooklyn Nicholson
968ec6c6f4 fix(update): manual-result protocol so gated outcomes reach the user
Round 4 of helix4u's review — the durable fallback is now real:

- Result protocol gains `manual`: an ok result the user still must act
  on (reopen the app, reinstall the GUI package, fix the sandbox helper).
  Both orchestrators set it on every DONE_NOTE/downgrade path; the Desktop
  consumer surfaces manual results in a real dialog on next boot instead
  of a log line — the browserless-Linux disappearance now ends at a
  visible dialog, worst case one boot later. Older result files without
  the field parse as manual:false (covered).
- notify ladder verifies EXECUTION, not existence: zenity/kdialog must
  survive their first second (an instant death means no display and falls
  through); the no-surface case is an explicit best-effort contract whose
  guaranteed channel is the result dialog.
- mac DONE_NOTE + failed relaunch of the kept/rolled-back bundle is no
  longer swallowed (`|| true` dropped): the durable message carries both
  facts.
- launch/gate matrices assert `manual` in the result JSON; consumer
  round-trip tested in handoff-result.test.ts.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson
ba28a18b95 fix(update): fail-closed cd, rejected-launch semantics, guaranteed recovery surface
gille's round 3:

- cd into the install root FAILS CLOSED (set -u without set -e let a
  failed cd continue hermes update in the caller's tree -- the exact
  wrong-tree class the correction exists to kill). Honest result, exit 3.
- A supplied mac relaunch target that is missing is a REJECTED launch ->
  manual downgrade; the launch matrix asserts the downgrade instead of
  codifying the old false success. A mac swap-failure DONE_NOTE now still
  relaunches the kept/rolled-back bundle before publishing manual.
- notify_fallback: every rung falls through on EXECUTION failure (a
  notify-send that can't reach D-Bus no longer eats the message), mac
  gets osascript (present on every macOS -- Safari-only machines have no
  chromium shim), and the no-surface terminal case is an explicit logged
  contract: the result file carries the outcome to the next boot.
- update:repro:fresh passes --non-interactive explicitly (prompt_yes_no
  falls back to /dev/tty, so </dev/null was not equivalent).
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson
1dd5c9de83 fix(update): launch acceptance before the terminal event, on both orchestrators
gille's round-2 review: the terminal lifecycle claimed outcomes the
launch hadn't delivered yet.

- posix finish() reorders: outcome -> durable result+marker -> LAUNCH
  WITH ACCEPTANCE -> terminal event. mac acceptance is open's exit code
  (launchd rejects broken bundles loudly); linux verifies the setsid
  child is still alive 1.5s after spawn, so an instant exec failure
  downgrades to a held 'manual' state + truthful result instead of a
  vanished 'done'. Gated skew/manual outcomes publish a real 'manual'
  event (new third shim state -- still zero logic in the page).
- Renderer-free linux recovery: when no chromium-family browser exists,
  manual/error outcomes fire notify-send/zenity/kdialog best-effort so a
  gated non-relaunch is never a silent disappearance.
- windows.ps1 mirrors the contract: Start-DesktopRelaunch returns
  verified acceptance (WMI pid alive / fallback process alive; dying
  before the window appears counts as failure), and the finally block
  downgrades to Show-ManualFinale + rewritten result when the launch
  didn't land. Error path still relaunches after showing itself.
- repro.sh launch / npm run update:repro:launch: real-orchestrator
  matrix for instant-exit relaunch downgrade and skew-message surfacing.
- posix.sh cds into the install root before hermes update (found by the
  sandboxed behind-repro: parts of the update resolve the mutated tree
  from cwd, which is the Desktop's cwd -- it updated the DEVELOPER'S
  checkout while reporting success against the sandbox).
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson
f121cd8a06 fix(update): run hermes update from the install root + unbreak fresh repro
The posix orchestrator inherited the Desktop's cwd, and parts of the
update pipeline resolve the tree they mutate from the working directory
-- the sandboxed behind-repro caught it updating the DEVELOPER'S primary
checkout (cwd at spawn time) while reporting success against the
sandbox. cd "$INSTALL_ROOT" before running hermes update, matching the
cwd:updateRoot contract of the deleted in-app path. Verified: rerun
leaves the outside checkout untouched (reflog clean).

repro.sh fresh used a --no-interactive flag install.sh doesn't have;
non-TTY stdin (</dev/null) + --skip-setup is the real non-interactive
contract.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson
bdb4cfd35e fix(update): posix hand-off truth ordering, relaunch-gate port, JSON escaping
Address helix4u's review:

- finish() now delivers the outcome BEFORE publishing it: mac bundle swap
  and the linux relaunch gate run first, then the result file, marker
  removal, and the shim event -- the app launch itself goes last so it
  can't race the result write. A gated/skewed linux install (AppImage/
  deb/rpm, broken sandbox helper) surfaces its message in the result file
  AND holds the shim window open with it instead of closing on a false
  'Opening Hermes...'.
- mac swap is transactional with a checked rollback; a failed install
  restores the previous bundle and the result says so (exit 7 when even
  rollback fails). Failed 'open' rewrites the result truthfully.
- linux gate is an exact port of the deleted update-relaunch.ts logic:
  anchored path-segment match on <root>/apps/desktop/release/linux-unpacked,
  chrome-sandbox absent = namespace build = fine, present = root+setuid
  required, with the real opt-outs (ELECTRON_DISABLE_SANDBOX, --no-sandbox
  among replayed args, or the Desktop vouching) instead of the invented
  HERMES_DESKTOP_NO_SANDBOX. collectRelaunchArgs/sandboxFallbackFromEnv
  live in updater-process.ts again; the Desktop passes filtered launch
  args (after --) and --relaunch-cwd so a deep-link or --no-sandbox
  launch survives the update.
- result/status JSON strings are escaped (git permits '"' in branch
  names) and the result write is atomic (tmp + rename).
- coverage: resolvePosixScriptHandoff + ported helpers in
  updater-process.test.ts (19 pass); repro.sh gate / npm run
  update:repro:gate asserts the whole gate matrix and round-trips a
  hostile branch name through the result JSON.
2026-08-11 11:07:17 -05:00
Brooklyn Nicholson
c991e3f62f feat(update): posix hand-off orchestrator (mac/linux quit-first updates)
scripts/desktop-update/posix.sh is the mac/linux twin of windows.ps1:
the Desktop spawns it detached and QUITS; it waits the app out, runs
plain hermes update (retry-once across the update boundary, truthful
desktop-rebuild completion), swaps/relaunches the .app bundle (mac) or
the release/*-unpacked binary when its sandbox helper is launchable
(linux), writes .hermes-update-result.json, and drives the same shim.
Repo-owned, so every update refreshes the code that drives the next one.

resolvePosixScriptHandoff mirrors the Windows resolver (with the
flat-path fallback covering the scripts/ reorg skew).
2026-08-11 11:07:17 -05:00