Commit Graph

50884 Commits

Author SHA1 Message Date
Teknium
71704ffb49 refactor: move Spotify out of core into the spotify catalog plugin
The spotify toolset (plugins/spotify) and its loopback-PKCE login (hermes_cli/auth_spotify.py)
now live in NousResearch/hermes-spotify, installed from the plugin catalog. Same pattern as the
Home Assistant move (fb9fc8a3dd): a LEFT_CORE row installs the plugin for every home that was
using it (a Spotify login in auth.json, or the spotify toolset in platform_toolsets) on
`hermes update` / first start; the toolset name, tool names, schemas, env vars and stored login
(providers.spotify) are unchanged, so nobody re-authenticates.

The one user-visible change is the login command: plugins cannot extend `hermes auth`, so the
plugin registers `hermes spotify login|status|logout`. LeftCoreFeature gains a `cli` field and
moved_command_hint() turns `hermes auth [status|logout] spotify`, and `hermes spotify` while the
plugin is not loaded, into a line naming the new command and the install/enable step for this
home. The dashboard's "needs login" hint names the new command too.

Core drops the Spotify auth constants/dispatch, the CONFIGURABLE_TOOLSETS row and post-setup
step, the core `spotify` entries in the shared-metrics toolset enums, its tests, and the docs
(en + zh-Hans) now point at `hermes plugins install spotify` / `hermes spotify login`.
2026-10-08 19:00:45 -07:00
Teknium
358ec68b90 Plugin catalog: spotify-desktop notes the Spotify plugin move
Its description named core's `hermes auth spotify`; the pinned sha imports core's
plugins.spotify.client, which the Spotify core move removes. A known_issues line points
at the upstream fix (cruzlxyz/hermes-spotify-desktop#2) until the pin can move to it.
2026-10-08 19:00:21 -07:00
Teknium
aad3fc74e2 Plugin catalog: spotify (official, NousResearch) 2026-10-08 19:00:21 -07:00
Teknium
1e0c7730d7 refactor(gateway): service-definition guards live in gateway_service_owner
Moves the temp-home guard (`temp_home_in_service_definition`,
`refuse_temp_home_service_write`) and the systemd install's ownership
resolution (`unit_belongs_to_caller`) out of the gateway.py facade into
the owner sibling, so the facade shrinks instead of growing. Narrows the
plist read's catch-all to the errors plistlib raises. Temp-home tests
move with their code into test_gateway_service_owner.py.
2026-10-08 17:06:02 -07:00
Teknium
7e17bab684 docs(gateway): a service definition belongs to the home it pins 2026-10-08 17:06:02 -07:00
Teknium
245ce8e0ff refactor(gateway): keep the service-owner fix to the writers
Trims the salvaged ownership guard to the part that closes the bug: a
refresh or install never rewrites a unit/plist that pins another
HERMES_HOME (--force-unit-path is the explicit repoint).

Dropped the new `gateway install` admission gate for homes outside the
platform-default tree. It refused every custom HERMES_HOME (Docker's
/opt/data, any non-default root) on first install unless the user knew
the new flag, which is a behaviour flip for legitimate installs; it also
needed a bypass monkeypatch in five unrelated test files. Tests cut to
the two invariants (foreign home refused, own stale unit refreshed).
2026-10-08 17:06:02 -07:00
ang-fleet-workers[bot]
b6ab666b6d fix(gateway): a service definition is written only by the home it pins
Two homes can resolve to one gateway service name: `<root>/profiles/<name>`
and `~/.hermes/profiles/<name>` both take `hermes-gateway-<name>`
(`ai.hermes.gateway-<name>` on launchd). Every gateway boot, start and
restart calls `refresh_systemd_unit_if_needed` / `refresh_launchd_plist_if_needed`,
which rewrote whatever definition sat at that name with no check of the
HERMES_HOME it pins. A scratch/E2E gateway started from a custom root
therefore repointed the real install's unit at the scratch directory; the
real gateway failed on its next restart (200/CHDIR once the scratch dir was
cleaned up). `systemd_install --force` and `launchd_install --force` did
the same. Uninstall already refused a unit pinning another home
(`_systemd_unit_belongs_to_current_home`); the writers did not.

- hermes_cli/gateway_service_owner.py (new sibling): `pinned_home()` reads
  the HERMES_HOME a unit or plist pins; `definition_belongs_to_home()`
  refuses a write from any other home; `refuse_foreign_home_install()`
  gates `gateway install` for a home outside the platform-default tree.
- Writers refuse a definition pinned to another home: the systemd and
  launchd refreshes (boot/start/restart) and both installers, `--force`
  included. The system-scope check runs on the caller's own home, before
  `systemd_unit_is_current` adopts the unit's home into os.environ (else
  the check compares the unit with itself); a sudo-stripped caller with no
  HERMES_HOME still acts for the unit's pinned home, as before.
- `gateway install` refuses a HERMES_HOME outside the account's Hermes
  tree that owns no bare service name and has no service installed yet: a
  service registered for a scratch home outlives the run.
- New `gateway install --force-unit-path` overrides both refusals (an
  explicit repoint of the definition at this home).
- Refused installs exit 1, so `gateway install` no longer goes on to start
  the other home's service; the setup wizard reports the refusal and
  continues, as ensure_gateway_service already did.
- Existing tests whose sandbox HERMES_HOME is a tmp dir opt in to the
  admission check (5 files, one monkeypatch each).

Tests: tests/hermes_cli/test_gateway_service_owner.py, 18 tests. On
e36a818033 (sources reverted, tests kept): 11 failed, 7 passed (foreign
unit/plist rewritten on refresh and --force, refused install starts the
service, scratch install proceeds, no --force-unit-path). With this change:
18 passed. test_colliding_profile_names_share_a_unit pins the premise on
the real name resolution.
2026-10-08 17:06:02 -07:00
ethernet
8ac5c74432 Harder linter fixes (#135281)
* ci: fix B004 linter rule

* ci: prefix unused variables with underscore (RUF059)

* ci: don't cast int to int

* ci: remove unnecessary if/else where both blocks yield same result

* ci: RUF024

* ci: fix RUF017, RUF018, RUF022

* ci: RUF013, RUF015

* ci: RUF007

* ci: RUF009

* ci: address code review
2026-10-08 18:56:50 -04:00
teknium1
28af0872b8 chore(plugin-catalog): delist antigravity-oauth-plus (rule 16, one listing per lineage)
The entry declares itself derived from antigravity-subscription-directsdk and
antigravity-oauth, both listed. Its additions (live model discovery, quota
caching, a reset dock) run on the same agy transport, so under rule 16 they go
upstream rather than into a competing listing. Plain delist: no removed.yaml
entry, existing installs keep working. Author notified:
taylorwtf/hermes-antigravity-oauth-plus#1
2026-10-08 15:47:05 -07:00
Teknium
9d05e7ff92 test(desktop): model settings tests clear their toast timers before jsdom tears down
The main-model apply test raises a success toast, which arms a 5 s
auto-dismiss timer. Nothing cleared it, so when it fired after the file's
jsdom window was gone, vitest reported "window is not defined" from
dismissNotification as an unhandled error and failed JS & TS checks with
every test green. afterEach now calls clearNotifications().
2026-10-08 15:20:58 -07:00
kshitijk4poor
d2cbfcf7db test(desktop): the React Compiler cache is its own module with focused tests
The cache lived inline in vite.config.ts, and the renderer builder tests use a bare fixture
config, so nothing exercised it: a broken warm path would still pass every test.

Move it to scripts/compiler-cache.mjs (behaviour unchanged; toolchain files and cache root are
now parameters, and an existing closeBundle hook is chained instead of replaced) and test hit
vs fresh output, toolchain/NODE_ENV generations, corrupt entries, pruning, incremental (watch)
rebuilds and the dev-server bypass. A cold and warm production build of the real app stay
byte-identical. Reported by Greptile on #135092.
2026-10-08 15:20:58 -07:00
kshitijk4poor
1f6d8b6ba8 fix(desktop): a node-pty layout without its macOS spawn-helper is an incomplete native tree
nativeTreeComplete decides whether a later build may skip restaging, but never looked at
node-pty. A partially extracted package with pty.node and no spawn-helper was recorded
complete; repairing the installed package without a lockfile change then left the staged
copy broken, and every later build shipped a terminal that fails on its first spawn.

Check the layout node-pty itself loads (build/Release, build/Debug, then prebuilds, as in
lib/utils.js) for a binding, and on macOS for the spawn-helper beside it (lib/unixTerminal.js).
Reported by Greptile on #135092.
2026-10-08 15:20:58 -07:00
kshitijk4poor
bc0758c393 fix(desktop): a renderer built under other VITE_*/NODE_ENV settings is never reused
Vite inlines every VITE_* variable into the renderer and NODE_ENV selects its mode, but the
desktop receipt recorded only sources and prepared inputs. After `VITE_PERF_PROBE=1 npm run
build`, the next plain build reused (and productCurrent certified) the renderer that still
carries the perf probe; a NODE_ENV change had the same gap.

The desktop build inputs now record the renderer environment (VITE_* plus NODE_ENV, unset
read as vite's production default), so both rendererCurrent and productCurrent require it to
match. Reported by Greptile on #135092.
2026-10-08 15:20:58 -07:00
kshitijk4poor
27e0f189ac fix(desktop): the React Compiler cache serves production builds only, keyed by NODE_ENV
The compiler emits HMR cache-reset code when NODE_ENV is development, and the cache key ignored it, so a dev-server session could seed entries a later production build reused. A dev server also wrote one entry per save with no build to prune them. The cache now wraps only 'vite build', NODE_ENV joins the toolchain key, and entries drop the map's sourcesContent (the module text the key already hashes; restored on a hit): 22 MB -> 15 MB. Verified: warm == cold byte-identical without sourcemaps; with --sourcemap two cold builds already differ in one map's __VITE_ASSET__ placeholder order, so that diff is vite's, not the cache's.
2026-10-08 15:20:58 -07:00
kshitijk4poor
3fc37481eb refactor(desktop): one receipt reader, one output list, no native double copy on renderer reuse
rendererCurrent and productCurrent share hostReceipt() for the schema/product/host checks. The stamped outputs come from bundleElectronMain's own electronOutputs list, so a renamed or added bundle can never be copied stale from the previous dist. The reuse copy skips node_modules/ and native/, which copyNativeTree recopies from nativeDeps right after, and a --typecheck build never reuses a renderer that skipped tsc.
2026-10-08 15:20:58 -07:00
kshitijk4poor
c3b06d1443 refactor(update): trust the npm closure through pm.ensure(verify=False)
The previous commit pre-checked the closure with a hand-rolled walk + is_installed before ensure, which repeated ensure's own check and left a window between them. pm.ensure already has the knob: verify=False trusts the recorded digest (as startup does) and an explicit ensure still installs a missing tool. Thread it through pm.client and source_build_env; the old comment claimed a verification that the interrupted-update finish path never ran.
2026-10-08 15:20:58 -07:00
kshitijk4poor
dcabf76310 fix(install): a pinned fresh clone pins, with retries, before it publishes
With --no-checkout the file download moved out of the retried clone into the single unretried pin checkout, which ran after the empty clone was already published: a throttled pin left INSTALL_DIR with no index, and the next rerun stashed a 'delete every file' autostash. The pin now runs in the staged tree inside the deferred-checkout retry loop (which also stops materializing the tip first), and nothing is published until it succeeds.
2026-10-08 15:20:58 -07:00
kshitijk4poor
136a4282bb fix(desktop): an incomplete native tree restages on every build
Skipping stage-native-deps on an admitted receipt kept a tree staged without a piece forever:
get-windows (or its win32 x64 binding, which only the native installer inside staging can fetch),
the macOS get-windows helper, or the host HUD helper (no X11 toolchain on Linux). The receipt
identity cannot see the host fix that would complete it.

The receipt now records `degraded` from the staged tree itself (nativeTreeComplete: every piece
this host can produce for the target is present), and build.mjs reuses only a complete one
(nativeInputsComplete). Gaps no host fix closes — win32-arm64 has no get-windows prebuild, a
cross-target pack never builds the helpers — do not count. Judging the tree rather than each
builder's return value covers soft-fail paths that still return a path (the win32 binding).
2026-10-08 15:20:58 -07:00
kshitijk4poor
7ec7857b0b perf(update): the product tail trusts the npm closure the PM step just verified 2026-10-08 15:20:58 -07:00
kshitijk4poor
2f6716dd7d perf(install): a pinned install checks out only the pin, not the branch tip first 2026-10-08 15:20:58 -07:00
kshitijk4poor
97260490e1 perf(desktop): skip restaging native inputs whose receipt still matches 2026-10-08 15:20:58 -07:00
kshitijk4poor
6232135eca perf(desktop): cache the React Compiler pass by content, so an update recompiles only the files it changed 2026-10-08 15:20:58 -07:00
kshitijk4poor
1d41d9f610 fix(desktop): native helpers build byte-identical, so the prepared native input stays comparable 2026-10-08 15:20:58 -07:00
kshitijk4poor
eeb8526569 test(desktop): a stamp-only rebuild reuses the renderer; tampered output or a source change recompiles 2026-10-08 15:20:58 -07:00
kshitijk4poor
1a4d7768c5 perf(desktop): reuse the compiled renderer when only the install stamp changed 2026-10-08 15:20:58 -07:00
teknium1
05df0382a5 fix(plugins): re-enabling a plugin restores its toolset in a saved platform_toolsets list
The Desktop/TUI/dashboard toggle edited the saved list before the plugin
loaded, so a backend that scanned while the plugin was off found no toolset
key and left the toolset out: the plugin showed enabled with its tools off.
Enable now edits the list after activation; disable resolves the key before
the disable is written. hermes plugins enable/disable make the same edit.
Only saved lists are touched; a fresh config still gets no list.
2026-10-08 15:15:47 -07:00
Teknium
dbbf98371d perf(install): a pinned install.ps1 clone checks out only the pin, before it publishes
Mirrors the install.sh change from #135092 on Windows. With -Commit (what
the desktop bootstrap passes), the fresh clone runs with --no-checkout and
the pin is checked out with retries inside the staging dir, so the branch
tip is never materialized first and a refused or failed pin never publishes
a checkout. A rerun over an existing checkout keeps pinning after the fetch.
2026-10-08 15:15:33 -07:00
ethernet
51609d827b fix(solstice): load the transport on first use, not at discovery (#134233)
Provider discovery imports every bundled plugin's __init__, and it runs in
interpreters without the app's HTTP stack: PM's runtime calls
lazy_installs_allowed() -> hermes_cli.config, whose import-time
_inject_profile_env_vars() lists providers. solstice/__init__ imported
transport.py at the top, which pulls agent.gemini_native_adapter -> httpx,
so every install logged "Failed to load bundled provider plugin solstice:
No module named 'httpx'" and solstice went unregistered in that process.

INFERENCE_BASE_URL now lives in __init__ and SolsticeClient is imported
inside create_client, matching the other provider plugins.
2026-10-08 18:06:07 -04:00
ethernet
3d7f1090d4 fix(desktop): build-bundle.ps1 parses the claim timestamp and rejects a missing -Python
The tagger-line lookup indexed into a single string, so the epoch was the
character 't'. A -Python path that does not exist left LASTEXITCODE at 0, so the
script printed done and listed a previous build's package.
2026-10-08 18:02:26 -04:00
ethernet
aac3440bac fix(desktop): build-bundle cleans under the lock, restores the shell, never signs
Review findings on the local build scripts:

- Cleanup ran before desktop.py took the checkout lock, so a second run deleted
  the outputs of a build that was still going. desktop.py gains --clean, which
  removes this checkout's outputs after build_lock is held. Both scripts pass it
  instead of deleting files themselves.
- build-bundle.ps1 left RELEASE_CLAIM_* and HERMES_PAYLOAD_* in the caller's
  session. It now saves those variables and restores them in the finally block.
- The scripts said "unsigned" but inherited signing credentials. The ps1 removes
  AZURE_* for the build. The sh removes CSC_* and APPLE_* and sets
  CSC_IDENTITY_AUTO_DISCOVERY=false, the switch the desktop rebuild command uses.
  The wording now says what is ignored.
- The Store bundle step needs the claim timestamp, and the script deletes the
  claim. The ps1 prints it as HERMES_RELEASE_EPOCH, and BUILDING.md documents it.
2026-10-08 18:02:26 -04:00
ethernet
57949d6630 feat(desktop): build-bundle scripts for local test bundles
scripts/build-bundle.sh (macOS, Linux) and scripts/build-bundle.ps1 (Windows)
build the checked-out commit through scripts/bundles/desktop.py --commit. That
path needs no tag and no push.

Both scripts refuse a dirty tree before any work starts. Both delete the
previous build output first. The staged Windows payload is read-only, so a
plain delete in a reused checkout fails with WinError 5.

build-bundle.ps1 -Store builds the Microsoft Store MSIX. The driver admits
Store packaging only for a stable tag. The script makes a local claim tag
rc.<N>-vX.Y.Z for the next patch after the newest published stable tag. It
counts local and remote attempt refs and starts at 900, so it cannot collide
with a real attempt. A finally block deletes the tag. The script never pushes it.
2026-10-08 18:02:26 -04:00
ethernet
d48e598d44 Merge pull request #135276 from NousResearch/fix/termux-build-libcxx-shadow
fix(termux): repin the termux pool so the payload's libc++ matches apt's cmake
2026-10-08 17:53:21 -04:00
ethernet
4f9b741a3a fix(install-e2e): tolerate the update-result consume race and held lock files (#135261)
* fix(install-e2e): tolerate the update-result consume race and held lock files

Two HEAD -> NEXT legs of the stable-release matrix went red for harness reasons,
not product ones.

macOS (app-driven update): source-update-observer listed
.hermes-update-result.json and then read it. The relaunched app consumes
(unlinks) that file on boot, so the read could land after the unlink and throw
ENOENT, which launch-from-spec reported as a failed update although the update
receipt said success. A vanished file is now "not there", the same as before it
was listed.

Windows (user-state verify): verify-user-state hashed every judged file. The
gateway's cron ticker holds an msvcrt byte-range lock on cron/.tick.lock, so
open() raised PermissionError and the upgrade was reported as changing user
state. Proven on a real Windows host: base raises Errno 13, the fix records the
lock as a sentinel. A lock file is held by a live process and is never user
content, so it is recorded without being read. Any other unreadable entry now
surfaces as a ScanError that names the path instead of a bare errno.

* test(install-e2e): an unreadable user file still fails the scan with its path

Skipping lock files must not let a user file the verifier cannot read pass
silently. The test makes a real mode-000 file under memories/ and asserts
snapshot_home raises ScanError naming the path, with the PermissionError kept
as the cause. It goes red when the OSError -> ScanError wrapper is removed.
2026-10-08 17:37:28 -04:00
Teknium
d687605c7b chore: map VictorYXL contributor email 2026-10-08 14:10:45 -07:00
Teknium
a9e2716c9b fix(codex): confirm a pending call whose call_id first appears on its done item
When the announcement carries no call_id and the done item does, the
call_id-exclusive lookup found nothing and left the announcement pending;
response.completed then settled it as a second call. Fall back to the
item/index match only when the matched announcement has no call_id of its
own, so a conflicting call_id still never evicts an unrelated call.
2026-10-08 14:10:45 -07:00
VictorYXL
c3302dd4fb fix(codex): integrate pending call-id aliases with stream correlation
Port PR #94708 to _CodexResponseAssembler, preserving rotating argument IDs and output ordering. Coalesce all matching call_id aliases, inherit earliest announcement metadata, and protect unrelated calls from conflicting item/index identities.

Adapted from commit 70b311ee50a0350b79c6c2b6123754d6aa6505da (PR #94708).

Verification: 130 focused tests passed; independent review passed.

Co-authored-by: DragonLi-Mi <4321063+DragonLi-Mi@users.noreply.github.com>
2026-10-08 14:10:45 -07:00
DragonLi-Mi
5e24de476d fix(codex): preserve calls across rotating stream item IDs
Match pending argument and completion events by output_index when item IDs change. Preserve authoritative arguments and successful output ordering without settling the same call twice.

Verification: independent code review passed; 75 focused tests passed; live Copilot Responses probe passed.
2026-10-08 14:10:45 -07:00
Maciej Lisiewski
9d9a3840d1 ci: address code review 2026-10-08 22:16:26 +02:00
ethernet
7ad6fd69d7 fix(termux): repin the termux pool and drop the libc++ shadow shim
The payload pinned libc++ 29 while the build container's apt cmake needs 30,
so the two disagreed and the shim in the previous commit papered over it. The
pool has since retired 17 of our pins, so repin them all with
`hermes pm update --termux`, which fixes the mismatch at the source.

Repinned: libc++ 29 -> 30 plus 14 more runtime libs (fribidi, glib, harfbuzz,
libaom, libbluray, libexpat, libheif, libmp3lame, libpng, libvidstab, openssl,
pcre2, readline, vulkan-loader-generic) and the bionic lock rows for uv
0.12.15 -> 0.12.23 and ffmpeg 8.1.2-5 -> 8.1.3. The pool serves ffmpeg 8.1.3
only, so that row moves back from 9.0.1 as the pool has it.

Staging with the new table succeeds: 84 packages, 269 .so, same counts as
before. The libc++ SONAME is unchanged. The one unresolved DT_NEEDED
(libharfbuzz-cairo -> libcairo) is present on the old table too.

The shim is removed. What stays from it is the in-container
`cmake -E capabilities` check, which fails in 2 minutes if the table drifts
from the pool again, instead of after 30 minutes of compiling.
2026-10-08 15:50:12 -04:00
Teknium
d94b70f675 fix(cli): hermes model offers reasoning effort when the model ID is unchanged
The post-pick effort step compared only model.default, so switching
provider while keeping the same model ID (OpenRouter -> Nous Portal with
anthropic/claude-opus-5.5) or re-picking the current model skipped the
prompt entirely. Chat /model always offers the effort after a pick.

Count _save_model_choice calls (every flow persists through it) and offer
the effort whenever a flow saved a pick; cancel / "No change." still never
prompts.

Supersedes #110468, which found the same-model provider switch first.

Co-authored-by: ehz0ah <haozhe4547@gmail.com>
2026-10-08 12:41:27 -07:00
Maciej Lisiewski
46f7b999e6 ci: RUF009 2026-10-08 21:38:05 +02:00
Maciej Lisiewski
4ee5e8e0f3 ci: RUF007 2026-10-08 21:35:58 +02:00
ethernet
5d8eafbc88 fix(termux): keep apt's libc++ ahead of the payload's on the build linker path
The wheelhouse build stage still compiled cmake from PyPI source (pillow ->
pybind11 -> cmake bootstrap, exit 11) after the Dockerfile and the apt install
began upgrading libc++ to 30. LD_LIBRARY_PATH put the payload's pinned libc++ 29
first, and a library found there beats the binary's own RUNPATH, so apt's cmake
died with 'cannot locate symbol _ZNSt6__ndk113__hash_memoryEPKvm' and
scikit-build-core read that as 'no cmake'.

Probed in the pinned termux-docker image under qemu with the same apt package
list: payload libc++ 29 first fails, a one-symlink dir to the system libc++ in
front of it starts cmake and clang. The build now makes that dir after apt runs
and fails fast if cmake cannot start, instead of burning 30 minutes compiling
cpython, cryptography and pillow first.
2026-10-08 15:31:36 -04:00
Maciej Lisiewski
4278b6aa24 ci: RUF013, RUF015 2026-10-08 21:29:48 +02:00
Maciej Lisiewski
74f1f17a7e ci: fix RUF017, RUF018, RUF022 2026-10-08 19:54:07 +02:00
ethernet
517b5e10fe Merge pull request #135232 from NousResearch/fix/lefthook-prepare-no-git
Docker image builds no longer fail on lefthook install (follow-up to #135177)
2026-10-08 13:49:31 -04:00
Maciej Lisiewski
2a1a95c8f9 ci: RUF024 2026-10-08 19:42:29 +02:00
ethernet
a50c4ba7c9 fix: skip lefthook install when there is no .git
Docker builds copy the source without .git. npm ci runs the root
prepare script, and lefthook install exits 128 there, so the image
build failed in node-deps.mjs. Install the hooks only when .git
exists. Use node so the check also works with cmd.exe on Windows,
and so it needs no extra file in the Docker build context.
2026-10-08 13:39:31 -04:00
Maciej Lisiewski
0f43723073 ci: remove unnecessary if/else where both blocks yield same result 2026-10-08 19:34:14 +02:00
Maciej Lisiewski
b4e436c115 ci: don't cast int to int 2026-10-08 19:28:23 +02:00