14 Commits
Author SHA1 Message Date
unsupportedpastels 347833a129 fix(plugins): updates of --allow-removed subdirectory installs keep working
A subdirectory plugin install has no .git, so `hermes plugins update` and the
dashboard/TUI update re-clone it through _install_plugin_core. That core now
checks the kill list, but the re-clone did not pass on the install record's
saved allow_removed exemption, so updating a recalled plugin the user had
installed with --allow-removed was refused.

_pull_plugin_update now forwards the record's allow_removed into
_reclone_plugin_update and on to the core, which writes it back on the new
record. Installs without the exemption are still refused before any re-clone.
2026-11-05 18:28:39 -05:00
Teknium a6efd813d3 fix(plugins): unattended updates re-check the hold and the widened surface at the mutation boundary
- 2.1 hold recorded during the check: update_one/dashboard_update_user_plugin(unattended=True) re-read
  the install record under the per-plugin update lock and skip a --ref hold (HeldWithRef / held);
  _install_plugin_core takes the same lock, so an install --ref cannot publish between the re-check
  and the updater's publication.
- 2.2 custom Git widening: the unattended gate compares the staged candidate's surface (tools, hooks,
  capabilities, ...) with the installed one before publication (git pull and subdirectory re-clone)
  and returns needs_review; a named update still applies it.
- /plugins update <name|all|--yes> refuses extra arguments with a usage line instead of updating
  every plugin.
- hermes plugins update (cmd_update_all) skips catalog installs held with --ref, like every other
  Update all.
2026-11-04 21:58:12 -08:00
Teknium 5819226468 feat(plugins): one switch and one action for unattended plugin updates
main already ships `hermes plugins update` (no name = update all) and the
opt-in `plugins.auto_apply` cadence. This makes both reachable from every
surface and closes the gaps reported on the original PR:

- Catalog installs now ride `plugins.auto_apply` too (it moved only git
  rows, so catalog plugins stayed on their install-day pin). The update
  path still refuses a widened pin or a too-new requires_hermes unattended.
- One updater per plugin: a cross-process lock around update_one and the
  dashboard/Desktop update, so the gateway's auto-apply tick and a manual
  update never run on the same plugin at once; the loser changes nothing.
- `hermes update` names, per profile, the catalog installs its new catalog
  pins left behind and re-arms that profile's plugin check, so an opted-in
  gateway applies them on the next tick.
- New CLI verb `hermes plugins auto-update [on|off]`; `/plugins update` and
  `/plugins auto-update [on|off]` in the classic CLI and the TUI (slash
  worker).
- RPC `plugins.manage` actions `update_all` and `auto_update` (contract +
  regenerated apps/shared outputs) for Desktop.

Off by default (legacy behaviour); all writes are profile-scoped through
get_hermes_home()/the scoped RPC.
2026-11-04 21:58:12 -08:00
Teknium 0255dedd1a feat(plugins): enforce the catalog's requires_hermes at install and update
The catalog entry's requires_hermes was display-only: `hermes plugins
install <name>` cloned and published a pin the catalog says needs a newer
Hermes, and `hermes plugins update` re-pinned an installed plugin onto it.
Only the plugin's own plugin.yaml floor was checked, and only at load.

- install_catalog_entry (the one seam every install by catalog name goes
  through: CLI, dashboard/Desktop/TUI plugins.manage, `hermes setup`'s
  importer step, the left-core and memory-provider auto-installs) refuses
  with "<plugin> needs Hermes <spec>; this is <version>. Update Hermes
  (`hermes update`) and install again." before any clone. The connector
  card's pre-check uses the same function.
- repin_catalog_plugin (every catalog update) keeps the installed pin and
  says the update is available after updating Hermes; `hermes plugins
  update` with no name lists such a plugin as held instead of applying it,
  and check-updates marks it.
- `hermes plugins search` / `info` still list incompatible entries,
  marked as needing a newer Hermes.

Portable packages (plugin.json) have no version field, so the catalog
entry's requires_hermes is their only floor; the gate is kind-agnostic and
covers them (tested), and the agent_plugins comment and docs say so.

The verdict is plugins_manifest.requires_hermes_error itself (same
running_hermes_version + version_satisfies), so a dev checkout, canary
build or tagless checkout is judged exactly as the plugin.yaml gate judges
it. No override flag.
2026-10-23 02:22:46 -07:00
alt-glitch 5a00d0dd45 fix(plugins): stamp a saved update check with the installs read before it ran
record_check_results read the install records after the network checks
finished. An update that landed during the check (A to B) was stamped
with B's record, so the A to B result matched and Desktop kept offering
an update that had already finished. Both callers now snapshot the
install records before the check and pass that snapshot in.
2026-10-20 14:33:55 +05:30
alt-glitch c5a3c0f876 fix(plugins): address review — dashboard scan override, feed and pin-aware update signal, subdir coverage
- Dashboard: the update route answered a scan block with a bare HTTP 400,
  so the browser page could not show the findings or send force. It now
  passes scan_blocked through (update route only; install is unchanged),
  and the page's update button runs the same consent-then-scan flow as
  Desktop through web/src/lib/plugin-update.ts. AgentPluginUpdateResponse
  moves to web/src/lib/api-plugins.ts (api.ts is over the size cap).
- Saved update checks now record the install they were checked against
  (source, revision, pin). A result is dropped once any of those changed,
  so a --ref pin or a reinstall from another source no longer shows an
  Update button that update refuses. An update_url feed's version target
  is offered too (the old match compared it to a commit and never hit);
  the row field is update_target, a commit or a version.
- Each finding in the Desktop dialog shows its description.
- Tests: a real subdirectory install exercises the re-clone path (caution
  needs force, dangerous refused even with it, refusals leave the tree and
  record alone); the dashboard route returns findings and applies with
  force; the saved signal drops on update, new source and pin.
- locales/_keys.desktop.json regenerated for the new Desktop strings.
2026-10-20 12:26:35 +05:30
alt-glitch 3ac037670a fix(plugins): Desktop and the dashboard update git-installed plugins through the CLI's path
Desktop's plugins.manage update refused every non-catalog install ("not a
catalog install — update it via the CLI") and kept its own third copy of
the re-pin logic. It now calls dashboard_update_user_plugin, the same
function the dashboard route uses, so catalog and git installs update the
same way on every surface.

A scan block comes back as ok=false + scan_blocked + scan_verdict +
scan_findings (the shape install already returns) with nothing changed;
the client retries a caution verdict with force. The dashboard route reads
force from its body next to accept_capabilities.

The Plugins tab can only offer Update when it knows an update exists. Git
installs had no such signal, so the row never showed a button. The gateway
already runs the update check every plugins.auto_update_check_hours, and
`hermes plugins check-updates` runs it on demand; both now save the results
next to the cadence marker. The plugin list reads that file (no network per
listing) and sets update_available + latest_sha on git rows. A saved result
is ignored once the install's revision moves past the one it compared.
2026-10-20 12:26:35 +05:30
alt-glitch 5bc0767821 fix(plugins): update scans every new revision and a caution verdict can be overridden
`hermes plugins update NAME` blocked a caution scan verdict with no way
past it, and the error told the user to pass a `--force` flag the update
command did not have. The scan ran with force=False and no prompt
(plugins_transaction.py update_plugin), unlike install, which asks.

- update_plugin takes force and scan_decision_cb and passes them to the
  scan. A dangerous verdict still always blocks.
- `hermes plugins update NAME` asks "Update anyway? [y/N]" on a terminal,
  with the same prompt install uses (now a shared helper), and --force
  accepts a caution verdict without asking.
- The re-clone path (subdirectory installs with no .git) used
  _install_plugin_core(force=True), where force meant both "replace the
  tree" and "accept a caution scan", so it skipped the caution check
  silently. A separate accept_caution parameter keeps the replace and the
  scan decision apart, so both update paths follow the same rule.
2026-10-20 12:26:35 +05:30
Teknium e1065b4a9c refactor(plugins): update-all reads a typed outcome, not printed text
Split cmd_update into update_one(name, target, console, interactive) -> bool,
which raises RepinConsentRequired / PluginOperationError on a refusal, and a
thin cmd_update that maps those to exit 1 as before. cmd_update_catalog now
returns whether the pin moved and raises instead of exiting. The update-all
loop calls update_one and catches the typed refusals, so the stdout tee,
ANSI stripping and metadata re-read are gone. Addresses Greptile review.
2026-10-19 06:07:41 -07:00
Teknium ced2a39350 feat(plugins): hermes plugins update with no name offers every plugin update
Running `hermes plugins update` without a name now runs the check-updates
probe, lists the plugins the update path can actually move (catalog re-pin,
git pull), names every one it skips and why (pinned, needs fixing, unknown,
pip distributions Hermes never upgrades — #132494), asks once, and updates
each through cmd_update so per-plugin consent prompts still apply. One
failure does not stop the rest; the summary quotes each failure's reason.

Without a terminal nothing moves unless --yes is passed; --yes runs the
updates with interactive=False, so consent stays fail-closed.
2026-10-19 06:07:41 -07:00
Teknium 41ab94b4e0 revert: plugins uninstall/update work again while the gateway runs (reverts #134189)
This reverts commit 0f0b0a12aa.

#134189 refused plugin update, remove/uninstall and force-reinstall on every
platform whenever the profile gateway was live, so uninstalling a plugin
(notably on Windows, from Desktop) required shutting the gateway down. The
#70473 hazard it guarded (deferred imports in a loaded plugin hitting a
swapped checkout) does not justify blocking uninstall; it will be re-landed
scoped to the paths that need it.
2026-10-07 16:57:01 -07:00
Austin Pickett 0f0b0a12aa fix(plugins): refuse checkout mutations while the gateway is live
Git plugin update/remove/force-reinstall ran git pull or tree swaps
directly in the installed checkout while the messaging gateway could
still be running with that plugin's callbacks loaded. A callback that
defers a relative import until first invocation then fails with
ModuleNotFoundError even though the command reported success (#70473).

One shared guard (hermes_cli.plugins_cmd._refuse_live_gateway_mutation)
now fails every mutating path BEFORE any git operation when the active
profile's gateway is live:

- plugins update (CLI) and dashboard_update_user_plugin
- plugins remove / rm / uninstall, dashboard_remove_user_plugin and the
  plugins.manage RPC (shared _remove_user_plugin tail)
- plugins install --force (a forced reinstall replaces a loaded
  checkout; a brand-new install is not gated)

The liveness read is the canonical profile-scoped probe
(profiles._check_gateway_running -> resolve_gateway_liveness), never
mutating the profile's identity files; a probe failure fails open so an
uninstallable plugin cannot strand. The message gives the
stop/apply/start recovery sequence; --allow-live-gateway on
update/install/remove is the explicit CLI-idiom override.

Integration test (tests/hermes_cli/test_plugin_live_gateway_guard.py)
spawns a real child holding gateway.lock with a gateway-shaped command
line against a temp HERMES_HOME, so the guard is exercised through the
platform's actual liveness probe — not a mock. Stopped-gateway behaviour
is asserted unchanged.
2026-10-06 17:22:18 -04:00
JoaoMarcos44 3e03da41e1 fix(plugins): carry user files across staged updates
(cherry picked from commit 8b17cdef5a1cb45a5f9f79ec5590376147ffe239)
2026-09-27 01:42:52 +05:30
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