Commit Graph

40935 Commits

Author SHA1 Message Date
ethernet
be3d68b20f test: keep the scoped plugin chore out of the drain-scope recording
The plugin update check now runs under profile_scoped_chore, so a one-tick
housekeeping run binds the launch scope once more before draining.
2026-09-21 20:27:58 -04:00
ethernet
5f1d3294b9 Merge branch 'rev/tests-infra' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
bb39409858 Merge branch 'rev/desktop-elec' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
1afa348e1c Merge branch 'rev/pm-core' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
843a0095ec Merge branch 'rev/termux' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
226d9a9e45 Merge branch 'rev/updater-py' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
9989e9e6e8 Merge branch 'rev/plugins-gw' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
04d7fe31fc Merge branch 'rev/shell-tls' into ethie/pm-clean 2026-09-21 19:52:58 -04:00
ethernet
017e6f6507 Merge branch 'rev/ci-release' into ethie/pm-clean 2026-09-21 19:52:57 -04:00
ethernet
81f04cc104 Merge branch 'rev/desktop-i18n' into ethie/pm-clean 2026-09-21 19:52:57 -04:00
ethernet
75bbb461d5 Merge branch 'rev/docs' into ethie/pm-clean 2026-09-21 19:52:57 -04:00
ethernet
fe1e5c2638 tui_gateway: resolve the launch home at call time for launch-profile secrets
model_switch bound launch-profile turns to server._hermes_home, the home
frozen at import, and read <that home>/.env through launch_secret_scope.
The launch state.db handle already resolves the launch home at call time
(#112692: the patched _hermes_home when a test changed it, else the live
process home) precisely so a harness that re-homes the process after
import is honoured; the secrets path did not, so under a developer shell
with a custom HERMES_HOME every turn read the import-time home's .env —
a guarded root for the suite — and test_compute_host_turn_protocol ran
2 failed + 1 error under a bare pytest. Share one _launch_home() between
both paths.

Restore that file's _wait timeout to main's 5 s: the 20 s bump only
stretched the wait for a turn.end that the guard error had already made
impossible.
2026-09-21 19:51:24 -04:00
ethernet
01e34cda7b fix(steward): one install-stamp resolver; identity fallback uses the process home
steward.read_install_stamp read <root>/install-stamp.json literally while
version_info._resolve_stamp_file honoured HERMES_INSTALL_ROOT. A Nix package
bakes its stamp outside the store's package dir, so `hermes --version` said
nix while sealed_steward said "unknown" and uninstall printed "managed by
unknown". steward.install_stamp_path now owns the location: the executing
tree resolves through pm.paths.install_root, any other tree is literal.

version_info._resolve_repo_dir fell back to the context-local profile home
for a value cached process-wide; the running code's identity is a process
fact, so it now reads the process home.
2026-09-21 19:38:22 -04:00
ethernet
525c09142f fix(pm): compile bytecode when uv syncs the environment
uv writes no __pycache__ (pip does), so every module of a fresh generation
was compiled in the foreground of the first user request that imported it
(#100461; main's f298911467 / d380651a9f were lost with lazy_deps).
--compile-bytecode on the sync covers the whole install, transitive deps
included. Also drops the stray copy of that rationale that a merge left
inside gateway/status.py::_posix_is_zombie.
2026-09-21 19:35:42 -04:00
ethernet
d1b7d5689f fix(update_stage): look the shim marker up in the process home, not ~/.hermes
update_stage rebuilt the home from HERMES_HOME with a literal ~/.hermes
fallback while update_lock resolves the same marker through
get_process_hermes_home(). Where the platform default differs (sudo invoker,
data-dir suffix) the two disagreed and the old-shim UI fallback found no
marker, leaving the hand-off window frozen. hermes_constants is stdlib-only,
so the lazy import keeps this module usable under -I -S -B.
2026-09-21 19:32:37 -04:00
ethernet
d500d05184 fix(bootstrap): launch-time completion is owed by a marker, not by currency
prepare_launch treated "dependencies current" as "update finished", but the
sync commits the generation before the product builds and the maintenance
run. A crash between the two left a current install that never built anything
and never would. The tail is now owed by a pending marker written before the
sync and cleared after the tail succeeds, so a repeat launch finishes it.

That tail imports the application, whose entry point is this same function:
inside the launching process's update-lock claim (holder pid is our ancestor)
prepare_launch is a no-op, otherwise the marker recursed forever. The claim
also keeps `hermes update` from racing the launch-time completion.

Completion output goes to stderr: it runs in front of whatever the user
typed, which may be emitting machine-readable stdout. Metadata queries
(--version, -V, --help) skip completion entirely; they read no dependencies.

A completion that fails offline no longer exits 1 from hermes_bootstrap: the
previous generation is still selected (a failed sync commits nothing), so
warn, point at `hermes update`, and launch.
2026-09-21 19:30:37 -04:00
ethernet
a76e856804 docs: fix the platforms() example and drop MERGE-CHECK notes
AGENTS.md showed three stacked platforms() decorators as the pattern —
exactly what the conftest rejects at collection. Show one marker per
test with the any-of form instead, and describe the lane selector as
resolving specs (posix reaches the macOS lane) rather than grepping for
the literal word, which is no longer how it works.

Remove the three MERGE-CHECK notes (AGENTS.md, hermes_cli/web_server.py)
left over from the origin/main merge; they were reviewer prompts, not
documentation of the code.
2026-09-21 19:18:23 -04:00
ethernet
59624b7ace tests: replace every bare host skipif with platforms()
The OS lanes are marker-driven: list_os_marked_tests.py picks the files
a lane imports from their platforms() specs and the lane selects with
-m platforms. A test gated with skipif(sys.platform != "win32") is
therefore never imported on the Windows lane and skipped everywhere else
— it runs on no host. skipif(sys.platform == "win32") tests were merely
invisible to the lane bookkeeping, but the rule the tree now follows is
one host marker, never a bare skipif.

Mechanical mapping, semantics preserved: skip-on-Windows → "posix",
skip-off-Windows → "windows", skip-off-Linux → "linux", skip-on-macOS →
"not macos". The former skip reasons stay as trailing comments. A
non-host condition (os.geteuid() == 0) stays a separate skipif beside
the marker, spelled getattr(os, "geteuid", ...) so the decorator still
imports on Windows.

Where the conversion would stack two platforms() marks on one test (the
conftest rejects that at collection) the narrower mark wins:
- test_update_wedged_gateway: the class is already platforms("linux");
  its per-test "needs UNIX sockets" marks were redundant and are gone.
- test_process_registry.TestSystemdCgroupIsolation: the class-level
  skip-on-Windows moves onto the 11 methods that had no host mark; the
  11 platforms("linux") methods keep theirs.
- test_file_ops_single_roundtrip: the two fifo tests drop their
  platforms("linux") in favour of the module's "posix" (mkfifo exists on
  macOS; both tests already skip when it does not).
- test_linux_desktop_entry / test_gateway_job_teardown_live: duplicate
  or wider marks removed.
2026-09-21 19:18:15 -04:00
ethernet
97eab53215 test(pm): explicit encodings in the lease re-selection fixture 2026-09-21 19:13:18 -04:00
ethernet
963566d703 fix(pm): cmd_install tolerates callers that omit --extra
Tests and in-process callers build the args namespace by hand, as the
existing --target lookup already allows for.
2026-09-21 19:11:40 -04:00
ethernet
2250b501b4 refactor(pm): rename cache_lock.py to uv_cache_prune.py
The module prunes uv caches to the lock; it takes no lock. Two importers.
2026-09-21 19:09:56 -04:00
ethernet
029cabb466 feat(pm): hermes pm install --extra NAME; one shared hint for missing extras
Twenty-five call sites told users to run
`python -c "from pm import sync_venv; sync_venv(['x'], explicit=True)"`
because `hermes pm install` only took package names. Add `--extra`
(repeatable; syncs the venv with the named extras and nothing else) and
pm.install_hint(extra), the single builder every site now uses, so the
advice stays correct when the command changes.

A cold PM runtime under allow_lazy_installs:false now reports the extra
the caller wanted and the command that provisions both, instead of a
bare "pm-runtime: not installed".
2026-09-21 19:08:19 -04:00
ethernet
99e005096f test(skills): re-point OUTPUT_DIR with the other cron store constants
Patching CRON_DIR/JOBS_FILE alone selects the live-constants store in
_current_cron_store(), whose OUTPUT_DIR was still the import-time path;
five parametrizations then wrote under the real home and tripped
home_io_guard whenever HERMES_HOME was set at import.
2026-09-21 19:05:38 -04:00
ethernet
a60bc9cc64 fix(update): completion tail skips the fleet restart it does not owe
Every update route now finishes through update_completion._complete_selected,
which restarted the whole fleet unconditionally -- including the "Already up to
date" route that main sent through the pending-restart catch-up. Net effect:
each cron tick and each profile's `hermes update` drained and re-killed the one
multiplexed gateway.

Port the catch-up path's two live guards into the completion tail:
- host_restart_already_completed(checkout sha): a sibling profile attaches to
  the restart this host already stamped (#95294); the restart phase now stamps
  it via mark_host_restart_completed.
- every planned runtime AND every live fleet row current at the checkout sha
  (#117051, d6b0d37ece). Both are required: the live matrix lists gateways
  only, so a planned serve still on pre-update code keeps the restart.

The already-current route also arms the host obligation with the checkout sha;
an SHA-less arm replaced the standing record and wiped the restarted proof.
2026-09-21 19:03:35 -04:00
ethernet
b1c1aa9a28 fix(platforms): raise, don't assert, when an ensured node/npm has no binary
assert is stripped under python -O, so a pm.ensure that recorded no
selected binary would have fallen through to str(None) as the npm/node
path. The four sites now raise pm.InstallError, which the surrounding
handlers already report.
2026-09-21 19:03:21 -04:00
ethernet
94a47320f1 fix(google_chat): request both extras before surfacing the restart
ensure_import raises InstallError("restart Hermes to activate…") after a
SUCCESSFUL install, so the loop aborted on 'google' and never asked for
'google-chat'; the restart landed back on a still-missing extra. Both
are requested in one pass; the first failure is what the registry logs.
2026-09-21 19:02:23 -04:00
ethernet
15335df680 fix(pm): collect superseded and aborted PM runtime generations
pm-runtime/generations/<uuid> trees were never collected: a kill -9 during
staging orphaned a venv permanently and every input change left the old
runtime behind. `hermes pm gc` now sweeps them under .prepare.lock with the
same lease model as application generations: entry points (worker.py,
launch.py) pin their own runtime, prepare_runtime marks new generations
lease-managed, and the collector removes unpublished stages outright,
superseded generations only once no worker holds a lease, and never a
pre-lease generation.
2026-09-21 19:01:36 -04:00
ethernet
20eebc4afa fmt(desktop): npm run fix over the review-fix files 2026-09-21 19:00:22 -04:00
ethernet
11bd9b512b fix(photon): declare PHOTON_NODE_BIN again
The adapter still honours PHOTON_NODE_BIN ahead of PM's pinned node, but
the pm hand-merge dropped it from optional_env, so the override became
undocumented and unreachable from setup.
2026-09-21 19:00:14 -04:00
ethernet
547ad0ab18 fix(pm): only live profiles feed the shared dependency union
dependency_homes() admitted every directory under profiles/, so a
.work.staging-* directory mid-publication, a tombstoned profile or a
marker-less side-effect dir could pull plugins into the shared venv.
Apply the canonical live-profile predicate (valid id, identity marker,
no tombstone) from hermes_constants, which pm already depends on, so
enumeration stays import-light and complete-union-or-error.
2026-09-21 18:57:57 -04:00
ethernet
a9845b8a04 fix(hindsight): serve the routed profile's env to the daemon; probe once per interpreter
_daemon_subprocess_env copied dict(os.environ), which under multiplex is
the LAUNCH profile's: profile X's daemon started with the default
profile's HERMES_HOME and credentials. It now builds the child env with
served_profile_child_env(inherit_credentials=False) — the manager reads
the LLM keys from the profile's own 0600 env file, so the child needs no
credentials from us.

check_local_runtime spawned 'python -c import ... sentence_transformers'
(a torch cold start) from is_available(), unavailable_reason() and
initialize() on every session. The verdict is now kept per interpreter
path for the process; a reinstall publishes a new PM generation, so a
new path re-probes.
2026-09-21 18:57:44 -04:00
ethernet
6ba45b300c fix(update-check): authenticate api.github.com calls and name the branch-tip failure
The passive source check moved into hermes_cli/source_check.py during the
pm/ rewrite, and with it the GitHub calls left the desktop — but the four
main fixes that lived on the desktop side (gh-CLI token fallback
c61a11474b, anonymous retry e79ba2d7f2, rate-limit headers
c7d4d32800, failure copy 66a13703b3) did not follow. `_request` sent no
Authorization, so every client behind one NAT/VPN exit shared the anonymous
60/hour budget, and `_branch_tip` collapsed every failure into "Could not
resolve the remote branch tip." — a 403 rate limit read as a Hermes bug.

hermes_cli/github_api.py owns the credential ladder (GITHUB_TOKEN, GH_TOKEN,
`gh auth token` cached per process, anonymous; a 401 on a token retries
anonymously) and the failure copy (rate limit with reset time and the
GITHUB_TOKEN remedy, 5xx as GitHub trouble, else the status/transport
error). `_branch_tip` returns that reason and check_for_updates puts it in
the message.

apps/desktop/electron/github-api-auth.ts had no caller left (the desktop no
longer talks to api.github.com) — removed with its test; the behaviour now
lives where the request is made.
2026-09-21 18:56:56 -04:00
ethernet
31d8ba29cc refactor(security): make tirith's release-provenance check public
pm.security_packages calls it from the Tirith package; a leading
underscore hides a cross-module contract. The consumer still owns the
signature semantics (pm/security_packages._SignedBinary), so the lazy
pm→tools→pm reference stays; only the name changes.
2026-09-21 18:56:05 -04:00
ethernet
6c05447469 fix(plugins): provenance probes origin with the resolved git
_git_origin_url spawned a bare 'git' while every other plugin git call
goes through _resolve_git_executable(); from a service with a minimal
PATH (or Windows without Git on PATH) the probe failed where install
and update succeed. No git at all now goes straight to the .git/config
parse.
2026-09-21 18:55:13 -04:00
ethernet
f9ec21538f fix(pm): only loopback-origin downloads may redirect to loopback
The https-across-redirects gate accepted any http://127.0.0.1 / localhost
target so test servers work. That let a production https origin redirect
an unpinned model download (sha256 unset by catalog policy) to whatever is
bound on a local port, unverified. Loopback now requires the request to
have started on loopback; no test-only flag needed.
2026-09-21 18:54:55 -04:00
ethernet
005780481d fix(pm): name workspace members after their plugin dir
Members were keyed as hermes-plugin-<sha256(path)[:16]>, so the only
conflict text a user ever saw ("uv lock exited 1: …hermes-plugin-
88e1872941a051e3…") did not say which plugin to disable. The key is
now <sanitized dir name>-<hash>; the hash still keeps two same-named
plugins from different homes apart.
2026-09-21 18:54:23 -04:00
ethernet
053ec28542 fix(pm): say which lock a queued store writer is waiting on
A second `hermes pm install` behind a long sdist build blocked with no
output for minutes. After 2s without the lock, print the lock path to
stderr, then keep waiting as before.
2026-09-21 18:53:32 -04:00
ethernet
4fda664b0f tests: resolve platforms() specs in the lane selector and runner note
scripts/ci/list_os_marked_tests.py matched the literal lane word inside a
platforms() string, so 80 files gated platforms("posix") never reached the
macOS lane ("posix = linux or macOS" was false for CI), and "any" files
reached none. The selector now resolves specs the way the conftest gate
does (posix ⊇ linux+macos, any ⊇ all, "not X" admits the rest) and only
looks inside mark.platforms(...) calls, dropping a false positive whose
only "macos" was inside a generated-file string.

scripts/run_tests_parallel.py still grepped the retired linux_only/
macos_only/windows_only names, so its "N files SKIPPED on this host" note
had been silent since the migration. It now shares the selector's
resolver and names the spec and the lane(s) it runs on.

Restore the platforms("windows") mark that
test_suppress_platform_ver_console_stubs_syscmd_ver lost in the
windows_only migration (its docstring still declared it); it passed
vacuously on Linux and was deselected on the Windows lane.
2026-09-21 18:53:24 -04:00
ethernet
0408962c80 fix(desktop): count a native macOS release tag as update identity for background notifications
MacStrategy.check() names its target by latestTag, never targetSha; the
notification gate accepted SHA-less results only for App Installer and
Microsoft Store, so a non-channel macOS build never toasted a background
update (manual/About checks still showed it). Recognise latestTag; git-style
checks without a target commit stay quiet.
2026-09-21 18:53:17 -04:00
ethernet
fc140f2598 fix(pm): gc collects abandoned .staging-* scratch dirs
_gc_store skipped every dot-dir, so the mkdtemp scratch a killed installer
left behind (a whole extracted python-build-standalone tree per crash)
was never removed. Scratch dirs live and die under the install lock gc
holds, so any that remain are orphans. .previous-* stays: it is the
restore point the next install of that entry verifies and consumes.
2026-09-21 18:52:33 -04:00
ethernet
d395860ec5 fix(plugins): require an https:// update_url
The saved update_url is what the gateway urlopen()s unattended every
check interval, and the feed decides which origin commit an update
selects. Install copied it into the row inside a try/except-pass with
no scheme check, so http:// (MITM to an old vulnerable commit),
file:// and ftp:// feeds were accepted.

One predicate (plugins_updates.https_update_url) is applied at install
(a refusal before any state exists — the manifest was already read, so
the re-read/except-pass is gone), in trust-update-url, and at
default_fetch itself so rows saved before this rule are refused too.
2026-09-21 18:52:24 -04:00
ethernet
dae4cacbcb fix(desktop): allow HTML5 video fullscreen through the permission handlers again
360c1ee836 + e6aa2e9fe4 were dropped in the merge: the request handler
denied 'fullscreen' and the check handler denied 'automatic-fullscreen', so
the native fullscreen button on <video controls> did nothing.
2026-09-21 18:52:15 -04:00
ethernet
529315649a fix(desktop): route native bearer refresh and login/logout through the access-token coordinator again
native-access-token.ts (22751c8fd9) survived the rewrite with no caller;
main.ts still ran the pre-fix open-coded ensureNativeAccessToken, so
concurrent callers raced /auth/native/refresh (each rotation invalidating
the other's winner) and a refresh landing after a login or logout could
resurrect the identity the user had just replaced. Wire the coordinator:
ensure() is its single-flight, the login handler orders itself with
beginLogin()/NativeAuthChangedError, and logout clears before awaiting
cookie I/O.

fetchJson/fetchPublicJson also lost httpStatusError (8d22781b1c) and threw
bare "<status>: <body>" errors, so readStatusCode() saw NaN on every REST
failure and neither the coordinator's dead-refresh-token check nor
isGatewayAuthRejection could recognise a 401. Restore the structured error.

Not ported here: the requestWithOauthFallback / rejectedAccessToken retry
integration from the same commit — a larger main.ts change, left for a
dedicated round.
2026-09-21 18:51:55 -04:00
ethernet
fa9e899d4e desktop: escape the attribute values interpolated into the MSIX fragments
before-build.mjs builds the msix-extensions.xml fragment and the hidden
CLI <Application> entries from string templates, interpolating the
display name and the payload-declared launcher stems raw. A value with
`&`, `<` or `"` would corrupt the manifest makeappx reads (an opaque
0x80080204 at best, a differently named alias at worst). Every
interpolated attribute now goes through xmlAttribute().
2026-09-21 18:51:46 -04:00
ethernet
b80bd18c76 fix(pm): re-check the selected generation after an unlocked boot lease
When runtime_lock times out, activate_dependencies read the selection and
leased it without the lock. An installer committing a new generation in
between left the reader holding a lease on an unselected tree while
importing from it; that tree is exactly what `hermes pm gc` removes once
the marker is a day old. Re-read the selection after leasing and move the
lease when it changed.

lease_generation now returns a release callable that also unlinks its
lease file, so one zero-byte file per hermes invocation no longer
accumulates under .leases/.
2026-09-21 18:51:17 -04:00
ethernet
8839afd427 fix(plugins): gate an update's new dependencies like an install does
update_plugin published whatever the pulled pyproject/pip_dependencies
declared straight into the shared environment, while install/reinstall
ask "Prepare these with Hermes through PM now? [y/N]". Under
plugins.auto_apply that is unattended from the gateway.

Diff the declared install_requirements before vs staged: added ones go
through the same prompt (extracted as _consent_python_deps) when a
terminal user is present; the dashboard and auto-apply pass
interactive=False and get a refusal with nothing published — matching
the capability re-consent already in cmd_update.

Same seam rebuilds an accepted Node sidecar in the staged copy when
package.json/lock moved: publication swaps the whole tree, so a custom
pull carried a stale copied node_modules and a catalog re-pin dropped
it entirely.
2026-09-21 18:50:39 -04:00
ethernet
db043ff445 fix(desktop): hold boot progress behind a latched remote reauth rejection again
shouldHoldBootProgressForReauth (8d22781b1c, #95701) survived the rewrite
with no caller, so updateBootProgress let a running:true phase or a sibling
attempt's cleared error from an in-flight boot lift the recovery overlay and
re-arm the renderer's retry loop — the Sign in button flicker the helper
exists to stop. Gate updateBootProgress on it; re-emits of the latched
failure still pass so the non-retryable verdict is never lost.
2026-09-21 18:50:17 -04:00
ethernet
37fd644017 fix(desktop): honour updates.pre_update_backup=off in the Desktop updater preflight again
pre-update-backup-config.ts (4de06d1dbf) survived the rewrite with no
caller: the checkout strategy called preflightStateDb unconditionally, so a
user who turned the pre-update backup family off in config.yaml still got
an emergency state.db snapshot on every Desktop update. Gate the preflight
on `hermes config get updates.pre_update_backup` in the composition root;
an unreadable answer keeps the snapshot, as before.
2026-09-21 18:49:39 -04:00
ethernet
241a9ddc8e ci: require Azure Trusted Signing vars before a downloadable Windows build
batch-sign-binaries.mjs and electron-builder.config.cjs::windowsSigning
treat a missing AZURE_SIGN_* set as "sign nothing, warn" — right for a
fork or a local build, but a release-signing lane with the vars
unprovisioned would have published UNSIGNED installers with only a
console.warn in the log. The macOS leg already refuses to build without
CSC/Apple credentials under the publishing gate; the Windows legs now
do the same for the Azure vars, before any payload is built.
2026-09-21 18:49:18 -04:00
ethernet
fdf9cebe2c fix(pm): keep streamed build-backend output without uv's DEBUG spew
The previous commit dropped --verbose and broke the contract that native
build output streams live (tests/pm/test_environment_build.py). uv only
forwards backend output at DEBUG level, so keep --verbose and scope it with
RUST_LOG=uv_build_frontend=debug: the backend's own lines stream, the
interpreter/cache/reflink internals stay silent (2 lines on a no-op sync
instead of ~200 on both uv 0.11 and the pinned 0.12.3).
2026-09-21 18:48:58 -04:00