Commit Graph

164 Commits

Author SHA1 Message Date
unsupportedpastels
80309111b4 fix(plugins): a user model provider reports enabled only where provider discovery loads it
#133012 made status call every user `kind: model-provider` plugin enabled,
but providers/ discovery (_scan_home_layer) only loads
plugins/model-providers/<name>/ children and flat plugins/<name>/ children
whose kind is exactly `model-provider`. A provider in a category directory
(plugins/custom/foo/), or a flat one with a differently-cased kind, showed
ON in `hermes plugins list`, the TUI hub and Desktop while it never loaded.

Status now applies the loader's rule per location: flat children only when
the loader's own kind check (providers._declares_model_provider_kind)
accepts them; model-providers/ children when their kind is model-provider
(discovery imports any child there, but never registers a non-provider);
anywhere else, never.
2026-10-05 09:37:36 -04:00
xxxigm
e473f5a9c9 fix(plugins): skip default-on checks for entry-point paths
Status was opening module:attr as a plugin directory. Windows rejects the
colon, so one pip plugin hid every other plugin on profiles that had not
already enabled it.
2026-10-05 08:52:28 -04:00
Teknium
6cd2817a6d fix(plugins): a user-installed model provider reports enabled, as the loader treats it
gate_manifest loads kind: model-provider plugins from any source without a
plugins.enabled entry, but _plugin_status only granted that default to
bundled plugins. A catalog-installed provider (claude-subscription-directsdk)
was live yet showed 'not enabled' in hermes plugins list and as a switched-off
row in the Desktop Plugins hub.
2026-10-04 18:19:16 -07:00
liuhao1024
169e77039a fix(plugins): let --yes-deps answer the consent gate for headless installs
A non-interactive `hermes plugins install <name> --enable` had no path
forward (#122134): the y/N gate for Python deps refuses without a TTY,
the publication of an active replacement is declined the same way
("Reinstall declined: dependency install skipped (non-interactive)"),
and the suggested `hermes plugins enable` recovery cannot publish a
plugin that was never installed.

Add an explicit `--yes-deps` flag (mutually exclusive with `--no-deps`)
that carries the user's own answer through both consent gates: the
enable-time `_consent_python_deps` prompt and the publication-time
active-replacement veto in `publish_plugin`. The fail-closed default is
unchanged — an unattended install without the flag is still refused
rather than consented to on the user's behalf.
2026-10-02 03:40:17 -07:00
Alex Fournier
55e102a763 Merge main into chore/relay-0.9
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-09-28 12:18:42 -07:00
Teknium
a5bd246865 Old pre-decomposition import paths are gone: plugin compat layer removed on schedule (#126164)
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule

The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:

- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
  names, restored dead definitions) and the three re-export stub modules
  (gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
  the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
  `plugins.allow_deprecated_imports` escape hatch

An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.

hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).

In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.

* chore: retrigger CI (zero-job startup_failure phantom)

* test: drop resolution allowlist rows for the two deleted which() sites

hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
2026-09-28 10:21:41 -07:00
Alex Fournier
f5448560e9 Merge origin/main into chore/relay-0.9
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-09-27 11:52:04 -04:00
kshitijk4poor
062a6bfc57 refactor(plugins): one helper attributes scan blocks to preserved user files
64860c9c20 pasted the same try/except around the security scan in
_install_plugin_core and update_plugin, and plugins_transaction reached
into plugins_cmd_install for the private _preserved_files_note.

_scan_merged_tree now lives next to _scan_plugin_tree/PluginScanBlocked
in plugins_cmd and both sites call it once. Message, scan_result and the
chained cause are unchanged (checked for matched, unmatched, empty and
missing-scan_result cases). _preserved_files_note is typed
(PluginScanBlocked, list[str]) and drops the nested getattr/str() guards
for the module's usual `scan_result.findings if scan_result is not None`
form.
2026-09-27 01:42:52 +05:30
kshitijk4poor
131d4a4634 fix(plugins): picker tick purges every alias of the disable; report real flips
Follow-up to the ported picker fix (#121623):

- Ticking a row re-enables it even when plugins.disabled holds the manifest
  name (e.g. telegram-platform, written by pickers before #40190). The save
  only dropped the key and its bare leaf, so the gate still matched the
  manifest name and the plugin stayed off. It now purges every alias like
  `hermes plugins enable/disable` (_apply_activation, shared with
  _set_plugin_enabled), with one discovery scan per save.
- The success line counts the rows actually turned on/off instead of treating
  every unticked row as "disabled"; the unused new_enabled return is gone.
- _entry_status shares the per-row status call between the picker
  preselection and `hermes plugins list --enabled`.
2026-09-25 21:19:04 +05:30
ethernet
d18382441f fix(plugins): name the plugin when admission hits a resolver conflict
No caller handled ResolutionConflict: enabling a plugin whose deps cannot
co-install with core surfaced uv's raw lock output (hashed workspace member
names) plus a "run hermes pm install" hint that cannot fix a conflict.

admit_plugin_set_change now turns ResolutionConflict into
DependencyConflict (an AdmissionRefused subclass, so every existing refusal
path still applies) whose message names the plugin being admitted, says it
conflicts with the pinned dependencies, and keeps the resolver's cause. The
CLI prints that message without the misleading retry hint; the dashboard
and TUI surfaces already relay str(exc). Selection and environment stay
unchanged as before.
2026-09-24 11:50:29 -04:00
ethernet
1e76db800c refactor(cli): split plugins_cmd.py into topical siblings
plugins_cmd.py had grown to 2,858 lines, past the ~2,000-line gate. Move
each verb family into a plugins_cmd_<topic>.py sibling: git (install
metadata + git plumbing), install, update (plus adopt / trust-update-url /
check-updates), remove, capabilities, toggle (composite UI) and listing.
The facade keeps the shared primitives, enable/disable selection,
discovery and the dispatch table, and re-exports the names other modules,
tests and the old-updater surface import (978 lines now).

Siblings never import the facade at module level; they read facade names
through _pc() at call time, so monkeypatching plugins_cmd.<name> still
intercepts calls made from a sibling. No behaviour change. Tests that
imported three sibling-only helpers now import them from the defining
module, and two subprocess.run patches target subprocess directly.
2026-09-24 11:50:29 -04:00
ethernet
66d54ebf51 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/docker.yml
#	Dockerfile
#	apps/desktop/src/app/settings/about-settings.tsx
#	docker/stage2-hook.sh
2026-09-24 02:23:44 -04:00
teknium1
72d18ce59f fix(plugins): subdirectory installs download only that folder
A plugin that lives in a monorepo subdirectory (the Hindsight catalog entry:
vectorize-io/hindsight#hindsight-integrations/hermes) was cloned with every
file in the repository even at --depth 1: 170 MB for a 2 MB plugin folder.
On a slow link that exceeds any reasonable deadline, so `hermes update`'s
Hindsight migration failed with "Git clone timed out" every time.

Subdirectory installs now do a blobless clone (--filter=blob:none
--no-checkout) and a sparse checkout of just that folder, written as the
classic info/sparse-checkout file so older Git clients work too. Servers
without partial-clone support ignore the filter and fall back to the old
download.

Because a partial clone downloads file contents at checkout time, the
checkout step (pinned and unpinned) now gets the configured network
deadline and the credential fallback, same as clone and fetch. Every
timeout error names plugins.clone_timeout_seconds so users can find the
knob.

Existing test fakes of _clone_plugin_repo accept the new subdir argument.
2026-09-23 21:29:21 -07:00
Gille
5dbbc9a9c0 fix(cli): make plugin clone timeout configurable 2026-09-23 21:29:21 -07:00
John Paul Soliva
16db511ed5 perf(config): route the config and manifest loaders through fast_safe_load
utils.fast_safe_load already exists, is pinned by tests/test_fast_safe_load.py, and
its comment names exactly these payers: 'startup parses config.yaml and every plugin
manifest, so the slow path cost ~0.9 s of cold start'. The migration was started —
hermes_cli/config.py uses it eight times, hermes_cli/main.py and hermes_cli/plugins.py
too — but the file-level loaders it was written for were never converted.

The cost is config SIZE, and the size is the installer's doing: it seeds config.yaml
by copying cli-config.yaml.example, 120,897 bytes of mostly comments. Nothing caches
load_gateway_config() and it has 238 production call sites.

Profiled before assuming a cause — reader.forward 34 ms, scanner.scan_to_next_token
28 ms, reader.peek 13 ms: the pure-Python PyYAML scanner, nothing else.

Measured A/B on one realistic pass (gateway config + every bundled plugin
description), medians of five runs, __pycache__ cleared between arms:
48.8 ms -> 2.46 ms. Per path: load_gateway_config 48.3 -> 1.84 ms, managed
config.yaml 44.4 -> 1.03 ms, 105 plugin.yaml manifests 59.3 -> 5.9 ms.

Same parse, same restricted tag set, same result — only the loader changes. Drops the
three 'import yaml' statements the swap orphaned.

(cherry picked from commit a4efd6a4506043159310eba02c32b673c455b88b)
2026-09-23 21:26:49 +05:30
Alex Fournier
f1cddf129a Merge origin/main into chore/relay-0.9
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-09-23 10:59:09 -04:00
ethernet
8509df133f Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	hermes_cli/plugins_cmd.py
#	hermes_cli/plugins_cmd_catalog.py
2026-09-23 03:36:33 -04:00
alt-glitch
d275e422dc fix: plugins installed a second apart both keep their install record
Each install read .install-metadata.json before its clone and wrote that
snapshot back after it, so the Desktop install card's second row erased the
first row's record (nvidia-app then showed source 'user'). Every writer now
re-reads the sidecar and changes only its own plugin's record under a
cross-process file lock; install rollback no longer rewrites a stale snapshot.
2026-09-23 11:45:29 +05:30
Alex Fournier
440fe3ff24 Merge branch 'main' into chore/relay-0.9
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-09-22 14:43:35 -04:00
ethernet
1f48a3d036 Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-22 13:03:25 -04:00
teknium1
21d0b12958 feat(plugins): late-loaded plugins wire their platform handlers live (#87770)
A plugin that finished loading after an adapter connected never got its platform
handlers (slash commands, button callbacks, inbound transforms) registered until a
gateway restart, silently. Three pieces, one seam shared by every surface:

1. Discovery listener: PluginManager.on_plugin_loaded(cb) fires from INSIDE
   discover_and_load for the plugins a sweep newly loaded (diff of the loaded set),
   with a per-plugin activation summary (hermes_cli/plugins_activation.py):
   activated_now {gateway_commands, gateway_transforms, hooks, callbacks} vs
   deferred {tools, prompt, mcp_servers}. Every mid-run load path now performs a real
   discover_plugins(force=True): CLI install/enable (via the gateway), Desktop/TUI
   plugins.manage install/toggle/update, dashboard REST install, tool-triggered
   force re-discovery, the new `reload-plugins` control-socket verb. A non-forced
   discover_plugins() short-circuits on _discovered, which is why reload.mcp after
   a mid-run install used to reload the OLD server set.
2. Idempotent re-wire: BasePlatformAdapter.rewire_plugin_handlers() runs only
   factories not yet wired on the live native client (keyed (plugin, qualname);
   a force reload hands back new function objects). Telegram hoists late handlers
   ahead of core's catch-all filters.COMMAND / CallbackQueryHandler (PTB dispatches
   the first match per group) and re-wires on the transient-init rebuild; Slack
   dedupes register_slack_action_handler per AsyncApp. The gateway runner
   subscribes per served profile and re-wires on the loop.
3. Scope limit + honest messaging: handlers only. Tools/prompt stay deferred to
   the next session (prompt-cache invariant), MCP servers to mcp.reload; the CLI
   hint and plugins.manage results (activation, gateway_reloaded,
   restart_required only when no gateway answered) say exactly that.
2026-09-22 09:50:22 -07:00
ethernet
207f8fedfd Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	hermes_cli/local_runtime/binaries.py
#	hermes_cli/plugins_cmd.py
#	hermes_constants.py
#	tests/test_hermes_constants.py
#	tests/tools/test_clipboard.py
#	tests/tools/test_voice_wsl_pipewire.py
#	tools/computer_use/cua_backend.py
#	tools/voice_mode.py
2026-09-22 09:55:47 -04:00
Siddharth Balyan
adefd99671 Portable plugins declare the application each MCP server needs; install refuses on an unsupported host (NS-942) (#119049)
* feat: enforce portable server declarations

* test: cover portable plugin host gates
2026-09-22 17:54:41 +05:30
ethernet
c13287c915 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/electron/main.ts
#	hermes_cli/backup.py
#	hermes_cli/config.py
#	hermes_cli/plugin_catalog.py
#	hermes_cli/plugins_cmd.py
#	hermes_cli/plugins_cmd_catalog.py
#	hermes_cli/plugins_discovery.py
#	hermes_cli/profiles.py
#	hermes_cli/update_cmd_deps.py
#	pyproject.toml
#	tests/gateway/test_dm_topics.py
#	tests/hermes_cli/test_config.py
#	tests/hermes_cli/test_plugins_cmd.py
#	tests/hermes_cli/test_update_autostash.py
#	tests/tools/test_lazy_deps.py
#	tools/lazy_deps.py
#	tools/skill_ledger.py
#	utils.py
#	website/docs/user-guide/security.md
2026-09-22 05:16:50 -04:00
teknium1
db8dcbe94c fix: catalog re-pins ask before widening a plugin; annotated-tag pins keep reviewed trust
Annotated-tag pins (F8): a catalog `sha` recorded as `git rev-parse <tag>` names
the TAG object, while HEAD can only ever be the commit it points at. The scan
trust check compared HEAD against the unpeeled sha (so every tag-pinned entry
lost the reviewed-pin bypass and prompted on caution findings) and the sidecar
recorded the peeled commit, so `update_available` was true forever and every
`update` re-installed. The installer now peels the pin (`<sha>^{commit}`) for
trust, records `pin` on the catalog block only when the checkout satisfies it
(empty for an off-pin `--ref` install), and every at-pin check goes through
`at_catalog_pin(sidecar, entry_sha)` (repin, dashboard payload, TUI rows).

Re-pin consent (F10): `hermes plugins update` on a catalog install replaced the
tree without asking, even when the new pin declared new tools, hooks, Python
dependencies, host capabilities or a Desktop half. `repin_catalog_plugin` now
diffs the installed manifest against the staged clone BEFORE anything moves
(`_install_plugin_core(before_swap=...)`) and, on a widening:
- CLI: prints the delta and asks y/N (non-interactive → not applied, fail
  closed); after a changed re-pin it runs the same `_run_capability_consent`
  grant path as the git-pull `update`.
- `plugins.manage update` RPC and the dashboard REST route answer
  `{ok: false, consent_required: true, delta, delta_lines}` with nothing
  changed; a retry with `accept_capabilities: true` applies it. Desktop shows
  the delta in its confirm dialog; the web dashboard uses `window.confirm`.
- Gateway contract regenerated (`accept_capabilities` param; `consent_required`,
  `delta`, `delta_lines`, `error` result fields).

Catalog audit findings F8 and F10 (low severity, no issue filed).
2026-09-22 01:00:09 -07:00
teknium1
a8b3fe60ac fix(plugins): restart_required on every toggle surface; trim salvage to invariant tests
Salvage of #66777 (@chrisyoung2005): the dashboard toggle toast now says a restart is needed
(#71595). Moved the ``restart_required`` stamp from a dashboard router wrapper into
dashboard_set_agent_plugin_enabled so the ``plugins.manage`` RPC (TUI/Desktop) carries it too;
contract + generated TS regenerated. The salvaged endpoint tests stubbed the toggle they were
checking — replaced by invariant tests that run the real commands against a temp HERMES_HOME
(all red on origin/main): platform disable gates the loader, alias toggle writes the canonical
key, status honours bundled defaults + memory.provider, remove forgets config / resets
memory.provider / unlinks a symlink only, disabled memory provider is not loaded.
test_deferred_platform_client_tools: bundled a2a is keyed ``platforms/a2a`` now.
2026-09-22 00:13:50 -07:00
teknium1
643dea487b fix(plugins): canonical activation keys + remove/toggle bookkeeping across CLI, dashboard and RPC
Symptoms fixed (all live-reproduced on origin/main in a fake HERMES_HOME):
- `hermes plugins disable photon-platform` wrote `platforms/photon` while the loader keyed the
  bundled adapter `photon-platform`, so the disable never applied (#27548). The loader now keys
  bundled platforms `platforms/<dir>` like every other category (one call site in
  plugins_discovery.py); the manifest name stays an accepted alias through gate_manifest.
- `plugins.manage toggle` / dashboard toggle wrote the raw identifier: enabling by bare leaf or
  manifest name returned ok while a stale canonical key in plugins.disabled kept the plugin off.
  dashboard_set_agent_plugin_enabled resolves the canonical key and purges every alias from the
  opposing list (shared _activate_key, also used by cmd_enable/cmd_disable); the RPC reports the key.
- remove/uninstall (CLI, dashboard, RPC) left the plugin in plugins.enabled/disabled and
  plugins.entries (#54336), left memory.provider dangling so the next agent init re-cloned the
  provider from the catalog (uninstall silently reverted), and, for a symlink inside the plugins
  dir, deleted the TARGET plugin and its metadata while the link stayed dangling. _remove_user_plugin
  is the shared tail: unlink a link only, then forget config under every alias and reset
  memory.provider (reported as cleared_memory_provider).
- `hermes plugins list` / dashboard hub / TUI hub called bundled backends, bundled platforms and the
  live memory.provider "not enabled" (#73131, #82898): _plugin_status mirrors gate_manifest.
- A user-installed memory provider parked in plugins.disabled kept loading: load_memory_provider
  honours the deny-list (name, dir name or manifest name) and says so once.
2026-09-22 00:13:50 -07:00
teknium1
92e6cc189b fix(update,plugins): address autostash entries by sha / bare index, never stash@{N}
On native Windows the MSYS runtime re-parses git.exe's argv when the parent is a
non-MSYS process (always the case for python.exe) and strips the braces, so
`stash@{0}` reaches git as `stash@0` and is rejected. For `hermes plugins update`
the autostash apply failed every time and the user's edits sat in a stash the tool
claimed to have handled; for `hermes update` the drop failed and stash entries
piled up unbounded.

- plugins_cmd: `_autostash_dirty_tree` returns the autostash commit sha;
  `_reapply_stash` applies that sha and drops positionally (bare `git stash drop`)
  only while refs/stash is still that sha. No brace selector in any argv.
- update_cmd_stash: `_resolve_stash_selector` returns the bare index `N` git
  accepts wherever `stash@{N}` is valid; the copy-pasteable guidance drops the
  braces too.

Existing assertion change: `test_autostash_dirty_tree_promotes_intent_to_add_entries`
asserted `(True, "")`; the first element is now the stash sha, asserted against
`git rev-parse refs/stash`.

Fixes #87542. Credit: @PRATHAMESH75 (#87571) for the diagnosis and the parent-process table.
2026-09-22 00:06:29 -07:00
teknium1
4a564706da fix(plugins): subdir installs stay updatable via re-install from the recorded source
A plugin installed from a repository subdirectory ships only <clone>/<subdir>; the
.git stays behind in the temp clone, so `hermes plugins update <name>` (and the
dashboard update) refused with "not installed from git" forever. The install
metadata already records the canonical source (URL#subdir), so when the tree has
no .git the shared update core re-runs the installer from that source with
force=True and swaps the fresh tree in; revision bookkeeping is the installer's.
Pinned installs are still refused first, unchanged.

Fixes #65314. Credit: @webtecnica (#65337) for the report and first fix attempt.
2026-09-22 00:06:29 -07:00
ethernet
d0d4e91434 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	hermes_cli/plugins_cmd.py
#	hermes_cli/plugins_cmd_catalog.py
#	tests/hermes_cli/test_web_plugins_catalog.py
#	website/docs/reference/cli-commands.md
#	website/docs/user-guide/features/plugin-catalog.md
2026-09-22 02:59:10 -04:00
teknium1
77f80d31ee fix(plugins): catalog provenance is installer-owned; kill list covers update/enable/load; re-pin keeps user files
Catalog trust bugs from the 2026-09-21 plugin audit (lane 3, F1-F6, F9):

- F1 (high): a URL-installed repo shipping its own .hermes-catalog.json rendered
  as catalog:official everywhere and marked the real entry "installed". Provenance
  now lives on the installer-owned .install-metadata.json record (catalog block
  written by _install_plugin_core, sha = checked-out commit); read_catalog_sidecar
  never reads the tree. Pre-fix installs are adopted once when the installer
  record agrees (pinned at the sidecar sha, cloned from the entry's repo).
- F2: removed.yaml bypassed by git@/ssh:///http:///www. spellings. _normalize_repo
  canonicalises to host/owner/repo (scheme, user, www., .git, slashes dropped).
- F6: removed.yaml consulted for INSTALLED plugins too: git-pull update, enable and
  gate_manifest (load) refuse recalled plugins, offline (in-tree + cached live
  list). --allow-removed is recorded on the install record and exempts it.
- F5: install NAME --ref X recorded the catalog pin, so list/TUI/update claimed
  the reviewed pin while HEAD differed. Recorded sha is the checked-out one.
- F3: re-pin replaced the whole tree, losing the installer-created config.yaml,
  data and user patches with no warning. Untracked/ignored files are carried
  into the new tree; edits to tracked files are copied to
  <HERMES_HOME>/plugins-backup/<name>-<sha8>/ with a warning.
- F4: a manifest rename between pins left the OLD dir installed and enabled.
  The stale dir is removed and the enabled flag follows the new name.
- F9: dashboard payload `removed` list now includes live removals.

repin_catalog_plugin returns RepinResult(sha, changed, installed_name, warnings);
CLI, dashboard and TUI callers surface the warnings and the new name.
2026-09-21 23:17:05 -07: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
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
Alex Fournier
ab8a99c730 fix(relay): honor ambient plugin configuration
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-09-21 14:02:05 -07:00
ethernet
f622ea76b3 merge: integrate origin/main while preserving PM ownership 2026-09-21 13:09:28 -04:00
sy0u1ti
b79b530eec fix(windows): delete read-only Git trees instead of failing with WinError 5
shutil.rmtree stops at the first entry it cannot unlink, and Git leaves loose object files read-only on Windows (r--r--r-- trees on POSIX package installs behave the same). Checkpoint clear, MCP catalog uninstall/reinstall and git-installed plugin removal all deleted clones with a bare rmtree, so each aborted mid-tree and left partial state.

Add utils.rmtree_readonly: clear the write bit on the failing entry and its parent, retry that one operation, and keep rmtree semantics for every other failure (both the 3.11 onerror and 3.12+ onexc callback shapes).
2026-09-21 08:58:37 -07:00
ethernet
9f2ba1b74d merge origin/main (779 commits) into ethie/pm-clean
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).

Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.

uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
2026-09-21 00:58:39 -04:00
tobenwarrior
dff92f3d78 fix(plugins): peel an annotated-tag pin before verifying the checkout
A pin is validated as 40 hex, but that does not make it a commit: a tag
object has a sha of its own, and an entry recorded from 'git rev-parse <tag>'
names the tag object rather than the commit it points at. Git resolves the
ref and detaches at the commit, so the revision guard compared a commit
against a tag-object sha and refused a CORRECT checkout — the entry was
uninstallable, with no way through (see the kiro-acp catalog entry).

Peel the pin to its commit before comparing. The guard keeps its teeth: HEAD
must still be exactly the commit the pin names, and a checkout that lands
anywhere else is still rejected. An unresolvable pin falls back to the raw
string, so a genuinely wrong sha fails exactly as before.

Tests: an annotated-tag pin installs at its commit (and records the commit,
not the tag object, so 'plugins update' drift checks stay exact); a
checkout that lands on another commit is still rejected. Red on base:
'Checked-out revision "..." does not match requested commit "..."'.

(cherry picked from commit 66baa043b70c4f66ae59c0a1147e74ba694140ef)
2026-09-20 15:56:55 -07:00
webdevtodayjason
118984d7a0 fix(plugins): share the loader's SUPPORTED_MANIFEST_VERSION with the installer (#85879)
The installer carried a private manifest-version cap of 1 while the runtime loader (and the
docs) accept manifest_version 2, so 'hermes plugins install' refused every v2 plugin the loader
happily loads. Read the shared constant instead. The alignment landed once in e834ee8971 and
was reverted by the follow-up 5bad3d54ee.

Salvaged from PR #85893 by @webdevtodayjason; rebased onto the decomposed plugins_cmd.py.
2026-09-20 14:29:39 -07:00
teknium1
b5d7a7b15d fix(config): hermes doctor and plugins enable read a list-literal platform_toolsets string
#116691 taught the runtime (`_get_platform_tools`) to read the
`'["browser", "terminal"]'` string an older `hermes config set` stored as
the user's real toolset selection, but the two other readers of the section
kept the old `isinstance(raw, list)` gate:

- `validate_platform_toolsets` (hermes doctor / startup warnings) reported
  "invalid toolset value ... falling back to 'hermes-cli'" for a value the
  runtime resolves to browser+terminal — a false warning.
- `plugins_cmd._toggle_plugin_toolset` skipped the string entry, so
  `hermes plugins enable <p>` never reached that platform.
- `nous_subscription._toolset_enabled` treated it as an empty list.

Move the list-literal parse into one function,
`toolset_validation.parse_platform_toolsets_value`, and route all four
readers through it; the plugin toggle re-saves the entry as a real list so
the string never persists past the next write. `_coerce_platform_toolsets_value`
keeps its once-per-platform warning for values that do not parse.

Why one parser: three readers with three notions of "configured" is exactly
how #115866 went unnoticed. Follow-up to #116691 (independent review finding).
2026-09-20 12:44:48 -07:00
funky-xamarin
09bcf17801 fix(plugins): only prompt for declared capabilities on enable 2026-09-20 11:52:45 -07:00
ethernet
9f837d298b Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Conflicts resolved toward the PM model: main's lazy_deps/update_cmd_deps/npm
stamp machinery stays deleted (PM + scripts/build/node-deps.mjs own it), the
systemd ExecStop stop-mark rides the installation launcher, legacy
linux_only/macos_only/windows_only markers are rewritten to platforms(), and
finalize_update_receipt carries pending manual-serve obligations forward
again (lost when the ContextVar receipt rewrite crossed c0aa3ce354).

Test harness: the real-home I/O guard exempts /proc/<pid>/fd metadata reads
(deleted-WAL holder scans) and run_tests.sh drops ~/.hermes PATH entries so
shutil.which() cannot trip the tripwire.
2026-09-20 10:07:50 -04:00
neron82
db64d1f109 fix(update): make intent-to-add entries stashable so hermes update can run
`git stash push` refuses a worktree holding an intent-to-add entry (`git add -N`): such an
entry carries the empty blob with zeroed stat data, so it is never "uptodate" and the whole
stash fails with

    error: Entry 'tests/...' not uptodate. Cannot merge.
    Cannot save the current worktree state

which aborts the update before the pull, on an index state editors create routinely when they
show a new file in diffs. The abort message names the proximate symptom ("commit, stash, or
clean up your local changes") and nothing points at the index entry, so the only way out is
git archaeology.

The autostash already repairs one neighbouring index state - unmerged entries from an
interrupted merge, two lines above - and this is the same class, so it is handled in the same
place: paths are read from porcelain (" A" is intent-to-add, "A " is staged, "??" untracked)
and promoted to real staged adds before the stash. Content is preserved; after the restore
they return as staged additions, which is the closest `git stash` can represent.

Both autostash sites are covered: `hermes update`'s and the plugin updater's
`_autostash_dirty_tree`, which had the same dead end and no test coverage at all.

Tests (2, behavioural against real git, both red before the fix):
  tests/hermes_cli/test_update_autostash.py::test_autostash_survives_intent_to_add_entries
  tests/hermes_cli/test_plugins_cmd.py::test_autostash_dirty_tree_promotes_intent_to_add_entries

Verified with scripts/run_tests.sh: base 79 passed / 2 failed (each with the reported error
reproduced); with the fix 81 passed / 0 failed.
2026-09-19 23:58:35 -07:00
jinli.yl
5bad3d54ee chore(plugins): pin ReMe catalog to merged integration 2026-09-19 11:58:06 -07:00
jinli.yl
e834ee8971 feat(plugins): list ReMe and align manifest install support 2026-09-19 11:58:06 -07:00
ethernet
a00ca233f8 fix(pm): voice enables audio-io through PM; public plugin-state readers; PM tests isolate the machine home
- voice_mode: `_import_audio` asks `pm.ensure_import("audio-io")` before importing
  sounddevice, so `/voice on` turns the feature on (or reports exactly why PM
  refused: lazy installs off, platform gate, restart needed) instead of printing a
  `python -c "from pm import sync_venv…"` one-liner for the user to run.
- pm.plugins_state: `read_home_selection` and `dependency_homes` are the public
  readers; memory_provider_migration and plugins_cmd stop importing underscored names.
- pm.store: the `sha256_file` shim is gone; the authority test asserts the Store has no
  file hash of its own rather than patching one.
- tests/pm/conftest: `isolated_machine_home` is autouse (Path.home, HOME, USERPROFILE,
  HERMES_HOME all under tmp_path); `@pytest.mark.real_machine_home` opts a module out
  — the four suites that spawn children with a home they build themselves.
- activate / activate.ps1 / Dockerfile followed hermes_cli.runtime_paths to
  pm.environments (the earlier sweep only covered .py).
- pm.registry loads builtins through importlib so a test patching `pm.packages` in
  sys.modules still registers the security packages.
2026-09-19 00:48:11 -04:00
ethernet
ee2b165e78 chore: drop merge-resolution notes, dead imports and a dead probe module
- 15 `MERGE-CHECK:` conflict-resolution comments removed from prod code (two were
  TODOs already done: the utf-8-sig sessions.json read lives in session_persistence,
  the pm-aware cron script helpers in scheduler_script).
- 49 imports the branch left unused (ruff F401, none present at the merge base,
  none inside PLUGIN-COMPAT blocks). update_cmd's frozen-surface re-exports are
  trimmed to the names tests/compat/old_updater_surface.json actually lists under
  hermes_cli.update_cmd; the rest resolve through hermes_cli.main.__getattr__.
- tools/environments/local_gitbash_probe.py: nothing imported it once _find_bash
  delegated to pm.shell().
- Three try/except wrappers around calls that cannot raise (install_truststore,
  get_hermes_home, and a duplicated except clause in supermemory).
2026-09-18 19:31:50 -04:00
ethernet
a6ae6ace51 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/js-tests.yml
#	agent/model_metadata.py
#	apps/desktop/electron/main.ts
#	apps/desktop/scripts/bundle-electron-main.mjs
#	apps/desktop/src/app/settings/about-settings.tsx
#	apps/desktop/src/app/settings/gateway-settings.test.tsx
#	apps/desktop/src/app/settings/gateway-settings.tsx
#	apps/desktop/src/app/updates-overlay.tsx
#	gateway/shutdown_flush.py
#	hermes_bootstrap.py
#	hermes_cli/local_runtime/binaries.py
#	hermes_cli/main.py
#	hermes_cli/managed_uv.py
#	hermes_cli/update_cmd.py
#	hermes_cli/update_cmd_deps.py
#	hermes_cli/update_cmd_fleet.py
#	hermes_cli/update_cmd_maint.py
#	hermes_cli/update_receipt.py
#	hermes_cli/update_serve_obligations.py
#	hermes_constants.py
#	tests/hermes_cli/test_doctor.py
#	tests/hermes_cli/test_managed_uv.py
#	tests/hermes_cli/test_pending_supervisor_recovery.py
#	tests/hermes_cli/test_startup_fast_guards.py
#	tests/hermes_cli/test_update_desktop_stale_warning.py
#	tests/hermes_cli/test_update_fleet_restart_pending.py
#	tests/hermes_state/test_hermes_state.py
#	tests/tools/test_tirith_security.py
#	tools/bot_relay.py
#	tools/checkpoint_manager.py
#	tools/write_approval.py
#	website/docs/getting-started/updating.md
#	website/docs/reference/environment-variables.md
2026-09-18 17:26:10 -04:00
teknium1
9b55e7e6ac fix(plugins): attach stored git credentials only after an anonymous refusal, on every network verb
Widen the anonymous-first decision from #114545 to the whole class. The
clone-only wrapper (_clone_with_auth_fallback) folds into one runner,
git_credentials.run_git_with_credential_fallback, used by every site that
attached a stored credential unconditionally:

  - plugins_cmd._run_plugin_git -> clone, the pinned --ref fetch in
    _checkout_exact_revision, and the pull in _git_pull_plugin_dir
    (`hermes plugins update`), which the thread on #114526 showed still
    failing with "could not read Username ... terminal prompts disabled"
    after the clone succeeded anonymously
  - mcp_catalog._do_git_install (catalog MCP git installs)
  - profile_distribution._git_clone (profile distributions from a git URL)

Why the runner and not per-caller guards: the decision belongs to the
credential layer once; a rejected credential sent pre-emptively is what
turns a working anonymous clone into a 401 that git can only answer with
the prompt noninteractive_git_env disables. The credential-required
classifier also matches git's "returned error: 401" spelling (the picked
version needed a trailing space that git never prints) and bytes output.

Side effect: `gh auth token` is now shelled out only after a refusal, so
public installs no longer pay that subprocess at all.

Tests: the sibling verbs (fetch, pull) x (public, refused) and the negative
that a non-credential failure never resolves a credential; the SSH no-op
case from the pick is already asserted by the private-remote test. Docs:
website/docs/user-guide/features/plugins.md.

Fixes #114526
Co-authored-by: holny <holny@users.noreply.github.com>
2026-09-18 10:01:01 -07:00
holny
c082b01395 fix(plugins): clone anonymously first; only attach stored credential on 401/403/Username-prompt (#114526)
hermes plugins install was broken for every public catalog repo the moment the
user had `gh auth login`'d: _clone_plugin_repo unconditionally passed auth_url to
_run_plugin_git, which injected an Authorization: basic <gh-token> into every
clone attempt — even against public GitHub URLs. GitHub rejects the Basic
header on a public clone endpoint, git falls back to a Username prompt, and
GIT_TERMINAL_PROMPT=0 (set by noninteractive_git_env) blocks it with
"could not read Username ... terminal prompts disabled". The reporter's
smoking gun was `git ls-remote https://github.com/robbyczgw-cla/...` working
anonymously while the installer failed with that env.

Split the path: an anonymous clone runs first; only when it fails with a
credential-required error (Username prompt / 401 / 403 / auth failed) does
the fallback retry with the stored credential. Public repos stay on the
single anonymous attempt; private repos reach the fallback naturally when
anonymous access is refused — the existing #9886f6e53b private-repo path is
preserved.

Tests (tests/hermes_cli/test_plugin_private_repo_auth.py):
  - test_private_clone_falls_back_to_auth_after_credential_required_error
    (renamed from the unconditional-auth test; first attempt anonymous +
    "could not read Username" stubbed; fallback carries the Authorization)
  - test_public_clone_attempts_anonymously_when_credential_resolves
    (github.com URL + resolve_git_basic_auth returns x-access-token token;
    exactly one clone call, zero Authorization headers)
  - test_non_https_clone_skips_auth_header_even_when_credential_resolves
    (SSH URL guard — the https path is the no-op)

Domain suite: 473 tests across 32 hermes_cli/test_plugin*.py files pass;
plus test_noninteractive_git.py + test_mcp_catalog.py + test_plugins_cmd.py
+ test_plugin_install_ref.py + test_plugin_scanner_recursion.py (136 more).
ruff check clean on both changed files.
2026-09-18 10:01:01 -07:00