43 Commits
Author SHA1 Message Date
unsupportedpastels ca17481680 fix(plugins): plugin packs and dashboard/TUI enable respect the plugin kill list
The kill list (removed.yaml) was checked by each caller. Plugin packs
installed and enabled recalled plugins, and the dashboard/TUI enable
path skipped the check the CLI enable command runs, so a recalled
plugin's Python dependencies could reach the shared venv before the
load-time gate refused it.

The check now runs inside the shared cores every surface uses:

- _install_plugin_core refuses a kill-list match by source, git URL,
  manifest name or catalog name once the manifest is read and before
  the scan or publication, unless allow_removed is set.
- _set_plugin_enabled refuses enabling an installed non-bundled plugin
  on the kill list (an install record carrying allow_removed stays
  exempt). Disable is unchanged.

Callers that caught only AdmissionRefused around an enable (dashboard
and TUI toggle, plugin packs, the install-card enable) now also turn
the refusal into their normal error result. The CLI pre-checks and
messages are unchanged; the load-time gate stays as a backstop.

Code Health PMF-03, plan step A1.6.
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 43ee1b0872 revert(plugins): Desktop, Webapp and dashboard installs take every source the CLI takes
Maintainer ruling: Desktop and the Webapp must keep installing private
plugins. The https-only source rule this branch added
(guiInstallSourceRefusal in the native install IPC, gui_install_source_refusal
in plugins.manage install and POST /api/dashboard/agent-plugins/install)
refused git@/ssh:// private repos, http:// and file:// local folders. It
goes, with its tests, and the user guide says Install from Git takes any
git source `hermes plugins install` accepts.

The symlink / plain-tree / 64 MB refusal in publishDesktopTree, the
reconcile refusal report in the install dialog and the forced-reinstall
fix stay.
2026-10-28 19:53:28 -07:00
Teknium 12944611f0 fix(plugins): Desktop and Webapp agent-plugin installs clone https only
plugins.manage install (native Desktop, the Webapp) and the dashboard's
POST /api/dashboard/agent-plugins/install handed any identifier to
dashboard_install_plugin, so a click could clone a file:// folder, an
ssh/git@ source through the host's ssh-agent, or plain http://. Both GUI
doors now refuse a non-https custom source before cloning, through one
helper (gui_install_source_refusal). A catalog_name install clones the
catalog's own source and is unchanged; `hermes plugins install` keeps
every scheme it documents.
2026-10-28 19:53:28 -07:00
Teknium e0c52c4fbf fix(plugins): catalog installs keep the inspected commit; older backends ask; partial installs need Force
Three follow-ups to the install dialog holding an install to the commit it showed.

- Catalog picks. The backend reloads the live catalog entry at Install, so a
  re-pin between the dialog's inspection and the click installed (and, through
  the one checkout, published the desktop half of) a tree nobody reviewed;
  the renderer sent no inspected_sha for catalog sources and neither the
  catalog install nor reuse of a disabled tree read it. Now the catalog
  install compares the commit it actually checked out with the inspected sha
  peeled through the clone (annotated-tag pins still install), a disabled tree
  is reused only at that commit (or its recorded tag pin), and catalog dialogs
  send it: the backend's inspect sha in the Webapp, the shown reviewed pin on
  native Desktop (whose Electron probe reads the branch tip). After a re-pin
  refusal the native dialog re-reads the catalog for the new pin. The record
  stays the catalog's pinned record. Electron's desktop clone uses the same
  reviewed commit and peels a tag pin.
- Older backends. A backend that refuses inspected_sha at admission is no
  longer retried without it behind the user's back: the dialog explains that
  this backend cannot guarantee the reviewed version is what gets installed
  (update it for that guarantee) and installs only on an explicit
  "Install anyway" (profile kept); Cancel installs nothing.
- Partial installs. Native Desktop on a remote backend could install the agent
  half at commit A, fail the desktop half (source moved), re-review to B and
  let a second Install put the desktop half down at B beside the agent half at
  A. That is now reported as a partial install (agent half in at A, desktop
  half not); Install stays disabled until Force reinstall, which replaces both
  halves with the version shown.
2026-10-28 17:21:32 -07:00
Teknium e4b0ab2365 fix(plugins): retry only a real admission refusal; re-review a moved source
Two follow-ups to the install dialog holding an install to the commit it
inspected.

- An install is resent without `inspected_sha` only for an older backend's
  admission refusal: a JsonRpcGatewayError with code 4000 whose message
  opens with `validate_params`' prefix. Matching the text anywhere also
  matched a 5026 install failure that forwards a git server's clone error,
  so that server could get the reviewed commit dropped.
- A refusal because the source moved past the reviewed commit now carries a
  structured marker: the backend raises PluginSourceMoved and
  `plugins.manage` answers 5026 with `data.reason == "source_moved"` (agent
  install and `install_desktop`); Electron's desktop install returns
  `sourceMoved: true` when the reviewed commit cannot be checked out. The
  dialog answers it by dropping its inspection, reading the source again
  and naming the new commit; Install waits for it. Retry used to resend the
  old commit and fail the same way every time. A pin the user typed is not
  re-reviewed, and nothing from the stale review installs (the desktop half
  is skipped when the agent half was refused).
2026-10-28 17:21:32 -07:00
Teknium 2c64c7fdb7 fix(plugins): an install dialog installs only the commit it inspected
The dialog inspects a source and shows its commit, but the agent half
cloned the branch tip at Install time: a branch that moved in between put
down a tree nobody reviewed, and the desktop half copied out of that
checkout followed it. Native Electron's probe returned no commit at all, so
its desktop clone had the same gap.

- plugins.manage install takes `inspected_sha` (custom sources without a
  pin). The clone stays an ordinary branch checkout and is refused when its
  tip is not that commit: "The source changed since you reviewed it ...
  Re-open the install dialog". Not `ref`, which records a pin that refuses
  every later `update`; not a detached checkout at the old commit, which
  `git pull` cannot update. Catalog picks keep their pin.
- That install drops inspect's kept answer for the source, so re-opening
  the dialog reads the moved branch instead of the stale commit.
- Electron: probePluginRepo returns the commit it read; installDesktopPlugin
  takes it and installs exactly that tree (fetch + verify) or refuses.
2026-10-28 17:21:32 -07:00
Teknium 0af496ae76 fix(plugins): a "y" at the install prompt loads the plugin into the running gateway
`hermes plugins install` only called report_activation (the running gateway's
reload-plugins nudge, the Desktop backend activation, and the restart hint) when
--enable was passed. Answering "y" at "Enable now?" enabled the plugin in config
but nudged nothing and printed no restart hint, so a gateway started before the
install never loaded it: its portable MCP servers stayed absent from every CLI,
Desktop and messaging session that gateway serves (observed live with the meshy
and blender plugins), while fresh processes such as cron workers saw them.
Any enable that committed now activates, flag or prompt.
2026-10-24 18:55:33 -07:00
Teknium 8d327d9d92 fix(plugins): say which app is missing when an install is refused
`hermes plugins install blender` on a host without Blender ended with
"server 'blender' is unavailable: missing_app." — a raw state code with no
action. The install gate now reuses the sentence the runtime surfaces already
compose (tools/mcp_liveness.describe), with the catalog title as the app name
and the declared minimum version in the action:

  Can't install plugin 'blender': Blender is not installed. Install Blender
  5.1 or newer, then try again.

The availability -> unavailable-state mapping moves into one helper shared by
status() and the install gate.
2026-10-23 14:01:02 -07:00
Teknium 71d00c30cd fix(plugins): no "restart the gateway" after an install when no gateway exists; brief install for setup
`hermes plugins install/enable` printed "Restart the gateway for the plugin to take effect" whenever
no gateway answered the reload, including when none runs or is installed for the profile (every
first-time `hermes setup`): a gateway started later loads the plugin at boot. The hint now needs a
live gateway pid for the profile (or the root serving it) or an installed gateway service.

`cmd_install(brief=True)` is for a caller that already said what it installs and why (setup's
agent-import step): the catalog pin line, prompts, warnings, errors and the outcome still print;
the ~20-line catalog description, the clone line and the default "Installed" panel do not.
2026-10-22 11:22:12 -07:00
Teknium 6f59555536 fix(plugins): no dependency consent for a pyproject that installs nothing
A plugin whose pyproject.toml declares `dependencies = []` reached the
Python-dependency consent question with nothing to list. Non-interactive
`hermes plugins install --enable` refused it ("dependency install skipped
(non-interactive)"), and in a terminal a default answer left it installed
but disabled, so official plugins had to drop their pyproject.toml.

The gate keyed on workspace membership, which every pyproject has. Ask
only when admission would put packages in the shared environment
(`PythonDeclaration.installs_packages`): applicable dependencies, a
buildable plugin package, extras, or dependency groups (uv sync
--all-packages installs a member's default groups). The install gate,
the active-replacement veto at publication and the update re-consent
all read it. PM membership and requires-python handling are unchanged.

Consent-test fixtures that used an empty pyproject to stand for "declares
Python dependencies" now declare one; their assertions are unchanged.
2026-10-21 08:26:29 -07:00
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 4070f011a2 feat(plugins): core-made Codex requests so plugins never hold the token
Plugins that need the user's ChatGPT/Codex subscription (hermes-live-voice mints
an OpenAI Realtime client secret) vendored their own reader + refresher for
~/.codex/auth.json. Codex refresh tokens are single-use, so that second refresher
races Hermes's locked refresh and can revoke the user's whole token family.

hermes_cli.plugin_provider_requests.credentialed_provider_request() lets core make
the request instead: it resolves the credential through
resolve_codex_runtime_credentials (Hermes's locked refresh, profile-scoped),
attaches Authorization itself, sends only to chatgpt.com / api.openai.com, never
follows redirects, and returns status/headers/body. Callers are attributed by the
plugin directory on the call stack and must declare `requires_auth: [openai-codex]`
in plugin.yaml; the declaration shows at install, in `hermes plugins show`, and a
catalog re-pin that adds it asks for consent. A plain importable function so
dashboard plugin_api.py modules can use it.
2026-10-10 10:43:32 -07:00
Maciej Lisiewski 2ff58849f4 ci: ruff UP037 fixes 2026-10-09 17:34:38 +02:00
Teknium d2639c0dc8 fix(plugins): a linked plugins/<name> slot reads "already installed", not a bad manifest
A memory provider's own installer (mnemosyne-hermes install, its README's
manual `ln -s`) leaves ~/.hermes/plugins/<name> as a symlink into a venv. The
catalog install of the same name then failed in _sanitize_plugin_name with
"Invalid plugin name 'mnemosyne': resolves outside the plugins directory" and
was recorded as failure_class=manifest_invalid, though the catalog manifest is
valid (hermes plugins validate passes it). Name the link and its target, say how
to free the slot, and record already_installed.
2026-10-09 02:42:26 -07:00
teknium1 8f463316fd feat(plugins): one name resolver for every verb; one switch turns a unified package on
- hermes_cli: _find_plugin_entry is the single resolver (key / manifest name / unique leaf /
  catalog name via the installer-owned record). enable/disable/show/capabilities/update/remove,
  the dashboard and plugins.manage settings all go through it, so the catalog name you
  installed with keeps working (17 catalog entries install under another manifest name).
  _catalog_install_on_disk reuses the same catalog lookup.
- Desktop hub: join agent rows to desktop halves by package folder (what Electron stamps),
  not manifest name, so a self-cloned package is one row instead of two.
- Desktop: turning a unified package on (install dialog 'Enable after install' or the Agent
  switch) also turns its opt-in desktop half on, unless the user switched it off. Off stays
  per half. Capability consent is untouched.
- docs: plugin-catalog page.
2026-10-08 04:12:02 -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
kshitijk4poor 7abd2e5fec refactor(plugins): one per-home enable lock, not two
_set_plugin_enabled now takes its own per-home _selection_lock for every caller
(CLI, dashboard, install card). The install card's older _enable_lock wrapped the
same call with a second, identical per-home lock; drop it.
2026-10-08 00:41:21 +05:30
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
alt-glitch 3383a1d8b3 fix(bundle): the agent's terminal runs a working hermes inside the desktop payload
Inside a packaged desktop build, bare `hermes` in the terminal tool resolved
to <payload>/venv/Scripts/hermes.exe. That venv was built on CI, and its
Windows redirector reads pyvenv.cfg `home`, which still names the CI path,
so every call failed with "did not find executable at C:\a\hermes-agent\...".
The working launcher in <payload>/bin carries the channel name and was never
on the agent's PATH.

- activate_dependencies puts the payload's own launcher dir (from the
  manifest's runtime.commands) first on PATH, and on Windows no longer adds
  the shipped venv's Scripts dir: every redirector in it names the CI
  interpreter.
- _resolve_hermes_bin_dir answers with that launcher dir in a sealed payload,
  so terminal and cron children do not depend on PATH order.
- stageDesktopLaunchers copies the canonical launchers to their qualified
  names instead of renaming them. Only the qualified names are published.
- The "plugin already exists" hint names the real CLI command.
- Every manifest-relative path (venv, store, hermes launcher) goes through one
  resolve-and-contain check, and a manifest.json that is unreadable or not a
  payload manifest reads as "not a payload" instead of raising, since every
  install now asks.

Source and dev installs are unchanged: they have no payload manifest.
2026-10-07 01:29:02 +05:30
alt-glitch 901da1cb6a fix(catalog): sessions off the desktop no longer see manage_catalog through tool_search (review)
An agent stores its session platform's gated toolsets in disabled_toolsets at load, so the tool_search listing, the bridge's search, MCP refreshes and delegate children drop manage_catalog too.
InstallPhase moves to the leaf hermes_cli/plugin_install_phase.py; the gateway contracts no longer import the installer.
The per-home enable lock moves from the plugins_cmd facade to plugins_cmd_install._enable_placed.
Reusing an installed catalog tree reads the install record through catalog_install_record, so older top-level catalog_name records match.
A failed row no longer redacts the Advanced choices (profile name, commit); only credentials are redacted.
The profiles.py role comment no longer says a role grants the setup toolset.
2026-10-07 01:01:14 +05:30
alt-glitch 8ce4d4bf95 fix(catalog): installs use the approved commit and settled retries show their own result (review)
Catalog plugin installs pass the commit approved under Advanced to the clone; a disabled tree on disk is reused only when it is at that commit.
A settled Try again on a plugin row draws the tools, setup notes and enabled state from that retry's install result.
A manage_catalog search row that reuses an install's call id no longer draws the install card.
Connection cards kept for a session are cleared when its runtime is dropped and on a gateway re-home; turnHasReply moves to lib/chat-messages to keep session-states.ts under its size cap.
2026-10-07 01:01:14 +05:30
alt-glitch c2b13dc3d8 feat(desktop): every desktop chat can install catalog plugins; tools gated by session platform
Toolsets that exist only for one kind of session now say so on their spec:
`platforms=frozenset({"desktop"})`. `TOOLSET_SESSION_PLATFORMS` is derived
from the specs that carry the key, `all` / `*` / no selection skip those
toolsets, and `toolsets.agent_tool_drops(agent)` removes their tools from any
agent whose own session platform is not listed. `agent_init._load_tools` and
`tools/mcp_tool_agent.py` (MCP refresh and prefix restore) call that one
helper. `tool_search._DIRECT_SURFACE_TOOLSETS` is derived from
`CLIENT_SURFACE_TOOLSETS` and `TOOLSET_SESSION_PLATFORMS`.

This replaces the profile-role gate: `profile_role_toolsets`, the role code in
`_with_session_toolsets` and the explicit-pin branch, the strip step in
`_select_tool_names`, and main's `setup` toolset (whose only tool moves out)
are deleted. `manage_catalog` moves into a new `catalog` toolset, which the
desktop client folds into every desktop chat like `project` and `desktop_ui`.
It is deferred behind tool_search by default (`config_defaults` defer list).
The shared-metrics schemas' toolset enum follows (`setup` out, `catalog` in).

The install card works outside the setup chat (#129650, with review fixes):
- Parallel installs: `_set_plugin_enabled` holds a per-home lock around the
  read-then-admit section, so rows enabled at once no longer refuse each other.
- A catalog plugin already on disk and not enabled is enabled in place, so Try
  again after a refused enable can finish.
- A failed catalog row counts as resolved (`contract.resolves`), so it no longer
  holds the turn to the 300 s deadline. The snapshot carries `resolved`, and the
  card reads it instead of a hand copy of the backend's rule.
- Each card draws its own operation by op id, because models reuse call ids.
- The row carries the approved non-secret choices (`approved`: force, enable,
  ref) and the chosen profile, so Try again after the card settled repeats them.
- Install phases are ids (`InstallPhase`, wire `CatalogInstallPhase`) and the
  installed-row notes are fields (`enabled`, `missing_env`, `server_errors`,
  `already_installed`, `requires_hermes`); the card words them in English
  strings (translations follow later). Failure reasons stay raw text, and
  `detail` keeps the English notes for the model.
- Installs and search target the calling chat's own home, carried as a path;
  the row's profile name is only a label, with no `'default'` fallback in
  Python or TypeScript.
- `dashboard_install_plugin` is split into `_resolve_source`, `_place_tree` and
  `_enable_placed`.

Desktop English strings for the card move to `en_catalog_install.ts` /
`types_catalog_install.ts`, which offsets the growth of `en.ts` and `types.ts`.
2026-10-07 01:01:14 +05:30
Teknium d6813a5dd7 fix(telemetry): extension failure classifiers are inert; the contract rejects contradictory install rows
- `_publish_failure_class`, `probe_failure_class` and `exception_failure_class` read a raise
  site's `failure_class` with `inspect.getattr_static` (`tagged_failure_class`), and the MCP
  type walk falls back to `connect_failed`. A third-party exception whose `__getattr__` or
  property raised replaced the user's PluginOperationError with RuntimeError (they run at the
  raise site, collection on or off), and the metric row was dropped; it now reads `exception`.
- `counter_dimensions_are_valid` adds the cross-field rule the per-field enums cannot state:
  failure_class in the kind's own set, `none` exactly on success, registry only on skill rows.
  Pre-split rows (no failure_class/registry) still validate.
- A refused dependency consent returns its reason as `_ConsentRefusal`, a str carrying the
  closed class (`non_interactive`, `deps_declined`, `manifest_invalid`), so publication no
  longer classifies by matching the user-facing copy. The text the user reads is unchanged.
- `extension_registry` imports the hub's `_SOURCE_ID_ALIASES` instead of a hand copy.
- test file: schema read uses utf-8-sig (windows-footguns).
2026-10-06 11:10:43 -07:00
Teknium 1b76735a5f feat(telemetry): extension installs and update runs say why they failed; hub skills name their registry
hermes.extension.install.count gains failure_class (closed per-kind set named at each failure exit
of the skills hub, plugin installer and MCP catalog emitters; raised exceptions classify by their
tagged class or TYPE, exception:<Type> collapses to exception, never the message) and registry
(the skills-hub adapter SOURCE_ID that resolved/served a skill; none/unresolved/other otherwise).

hermes.update.run gains failure_class derived from the FINAL receipt only (admission step,
stop_reason exception type, exit_code, stage marks, fleet states, gateway_restart). failed_stage no
longer reads 'other' for runs whose every mark passed: a 'partial' receipt (written only by the
post-restart verification) synthesizes a failed verify stage, and a failed run ending on a skipped
restart reads restart.

Package schema v4 is extended in place (canary-only): the pre-split field sets stay valid as the
second oneOf shape and in _LEGACY_METRIC_FIELDS so pending rows drain.
2026-10-06 11:10:43 -07:00
Teknium 03f545ebcf fix(memory): catalog memory-provider installs stop failing on every start and every Install click
Two install routes refused a catalog memory provider that config already
names (memory.provider), so the install could never succeed and was
retried on every agent start / every click — the hindsight (91%) and
mnemosyne (87%) failed rows in hermes.extension.install.count.

1. Agent init (`recover_at_startup`) installs the configured provider
   through the consent gate. Under the classic CLI that gate's input()
   runs on the agent-build thread behind prompt_toolkit, so the turn
   hangs on "Initializing agent..." forever; with no terminal (Desktop,
   gateway, `chat -q`) it is refused — "Install declined: dependency
   install skipped (non-interactive)" — and recorded failed on every
   process start. Agent init now never asks: a provider that shipped in
   core installs with its built-in's consent (security.allow_lazy_installs
   still gates it), any other catalog provider is not attempted and the
   user is told the one command that installs it.

2. The dashboard/Desktop Install click (`dashboard_install_plugin`) on a
   plugin memory.provider already selects hits the publication veto that
   is meant for replacing an active plugin, so it is refused every time
   while the same plugin unselected installs and enables. The click now
   carries the consent the fresh-install enable already acts on; a forced
   reinstall keeps the veto.
2026-10-05 16:06:32 -07:00
Teknium 7c46cd9ff7 fix(plugins): decide "memory provider" on install from code, not a text match
cmd_install chose the memory.provider path with _is_memory_provider_dir, a
substring search for "MemoryProvider" in __init__.py. A hook plugin whose
docstring says "Not a MemoryProvider" passed, and --enable replaced the
user's memory.provider with a name load_memory_provider returns None for
(exit 0). _defines_memory_provider parses the package's modules (no import)
for what _load_provider_from_dir can load: a register_memory_provider(...)
call or a MemoryProvider subclass.
2026-10-05 15:38:19 -07:00
Teknium 512ebece93 fix(plugins): --enable leaves an installed memory provider loadable
An explicit --enable on a memory-provider install now lifts a
plugins.disabled entry (the loader refuses a disabled provider, so the
"set as memory.provider" report was false), through the same admission
transaction plugins enable uses. It also selects the provider when PM
already treats the plugin as active: that state is exactly the old bug
(provider listed in plugins.enabled, memory.provider unset), and a
reinstall with --enable could not repair it. PM's already-active
shortcut still skips the dependency re-prompt; it no longer vetoes an
explicit memory selection.
2026-10-05 15:38:19 -07:00
Teknium 59224a20f0 fix(plugins): only a real memory-provider tree selects memory.provider on install
Catalog category "memory" also holds hook plugins, context engines and
skill packages. Deciding from the category made `--enable` overwrite the
user's memory.provider with an unloadable name and never enable the plugin.
Decide from the installed tree (_is_memory_provider_dir), and save
memory.provider only when find_provider_dir resolves the name.
2026-10-05 15:38:19 -07:00
Konstantin Khlopkov 7f43fb2611 fix(plugins): a memory-provider install activates through memory.provider
A memory provider is activated by memory.provider alone; plugins/memory never
reads plugins.enabled. cmd_install still asked "Enable now?" and wrote
plugins.enabled, so `plugins install hindsight --enable` printed "enabled"
while `hermes memory status` kept showing Provider: none (#119909).

For a catalog entry with category: memory (or, for a URL install, a tree
that satisfies the provider contract) the prompt becomes "Use '<name>' as the
memory provider now?"; yes / --enable prepares its Python deps through PM (as
`hermes memory setup` does) and writes memory.provider; no / --no-enable
points at `hermes memory setup` instead of the dead-end `plugins enable` hint.
The gateway hot-activation step is skipped for providers (new sessions pick
them up).

Re-ported from the pre-split plugins_cmd.py into plugins_cmd_install.py.
2026-10-05 15:38:19 -07:00
John Paul Soliva ddde067010 fix(plugins): a forced reinstall of the same source keeps the user's files
install --force --ref is the documented way to move a pinned plugin, but it replaced the whole plugin directory: untracked and git-ignored files (plugin state, local config) were deleted and tracked edits were lost with no copy. A reinstall from the source the plugin was installed from now carries user files like update does and copies tracked edits to plugins-backup/. A different source still starts clean.
2026-10-03 21:40:42 -04:00
alt-glitch f1c79255fe feat(plugins): requires.gpu gates a plugin server on the host's GPU vendor
A portable plugin can now declare `requires: {gpu: nvidia}` for an MCP
server. On a host without an NVIDIA GPU, Hermes then refuses the
install, hides the server's tools, and the catalog card says "needs an
NVIDIA GPU". The check is the existing
`hermes_platform.host.facts.gpu_class()` read.

The requirement is independent of `app:`. The RTX Remix Toolkit is the
first consumer: it is a package the user unpacks anywhere, so there is
no install location to check, but it needs an NVIDIA GPU.

An unreadable GPU (`unknown`) passes, so a failed registry or sysfs
read does not lock out a machine that has the GPU. Only `nvidia` is
accepted: gpu_class() reports the highest-priority vendor, which
answers the question exactly for NVIDIA and not for AMD or Intel.

A registered server on such a host reports the new `unsupported_gpu`
state, carried through the gateway contract and the Plugins tab, with
a sentence that names the GPU instead of "Install <app>".
2026-10-03 10:26:12 -07:00
teknium1andchelsealong a5144c71e9 fix(memory): show every memory-provider migration outcome, ask once per update
Agent-start recovery (recover_at_startup) reported installed / refused / failed /
not-in-catalog only to agent.log, and so did a selected provider that reports unavailable,
so a user with security.allow_lazy_installs: false, offline, or who declined the dependency
prompt silently ran without external memory (#119769).

- Startup outcomes go through _emit_startup_warning: printed in the CLI; other drivers get
  a sticky warn AgentNotice (Desktop toasts notices but drops warn-kind status.update), and
  the messaging gateway, which wires notice_callback per turn, receives it on turn 1.
- hermes update labels each line with the profile it is about and gives the exact
  `hermes -p <profile> plugins install <name>` command for non-active homes.
- Profiles sharing one environment get one set of dependency answers per provider
  (shared_dependency_answers): the first install asks the normal Python/Node questions,
  later homes reuse them; a decline or failure names the remaining profiles once instead of
  failing (or re-prompting) one by one; Ctrl-C ends the migration with a message (#125794).
- The success line no longer names memory.<name> (#124038); docs follow.

Co-authored-by: chelsealong <chelsealong@126.com>
2026-10-02 04:09:57 -07:00
teknium1 084a84b544 fix(memory): migrate deps-bearing providers without a terminal under allow_lazy_installs
Desktop, the gateway and a scripted `hermes update` have no TTY, so the
dependency consent gate refused every catalog provider that declares Python
deps: both migration hooks failed with 'Reinstall declined: dependency install
skipped (non-interactive)' and the provider stayed missing. The agent-start
hook exists precisely for those surfaces.

The configured memory.provider plus security.allow_lazy_installs (read for the
home being migrated) is the consent that let the bundled provider install its
deps on demand; carry it through dashboard_install_plugin ->
install_catalog_entry(assume_deps_consent=...). A terminal still prompts, and
allow_lazy_installs: false still refuses with the install one-liner.
2026-10-02 03:40:17 -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
teknium1 ed798cf85c feat(telemetry): record plugin installs as extension-install events
plugins_cmd_install.recorded_install wraps the install attempt in every
plugin install entry point: `hermes plugins install` (catalog name, git
URL or shorthand), the dashboard/TUI/connector install
(dashboard_install_plugin) and plugin packs. Each emits one
hermes.extension.install.count event:

- source: catalog for a catalog entry (name = entry name), local for a
  file:// repo, url for any other git source (name None)
- outcome: failed when the clone/scan/publish raises, success otherwise

A reinstall over an already-installed plugin and `plugins update` (which
calls the installer core directly) are not counted.
2026-09-28 12:43:03 -07:00
teknium1 7f891c44b5 refactor(plugins): fold known_issues loops into one helper, print them once
cmd_install printed each known issue on its own line right after
entry_capability_summary(), which already ends with "Known issues: ...",
so the text appeared twice on the CLI. Keep the summary line only.

dashboard_install_plugin gets the same list through a 6-line
_known_issue_warnings(entry) helper, so neither function's cyclomatic
complexity grows past the main baseline (cmd_install 20, dashboard 11).

Follow-up to #124058 (happy5318); no behaviour change for the dashboard
result dict or the memory_provider_migration path.
2026-09-27 17:06:55 -07:00
happy5318 113a63e853 fix(plugins): make catalog known_issues informational, not a fail-closed gate
Relaxes the fail-closed install gate from #124037 per teknium1 review
2026-09-27 (known_issues informational; guard belongs at mode-selection seam):
- dashboard_install_plugin: no longer refuses known_issues entries — the text
  flows into the result warnings plus a machine-readable "known_issues" key so
  the UI can show it; the memory-provider migration paths (migrate_all_homes /
  recover_at_startup via _install_into) install hindsight again in any mode.
- cmd_install: prints the yellow "Known issue:" lines but drops the TTY
  requirement and the y/N confirmation; non-interactive installs proceed.
- entry_capability_summary: includes "Known issues: ..." so install prompts and
  the catalog UI surface the text.
- scripts/validate_plugin_catalog.py: register known_issues in KNOWN_KEYS.
- tests: keep parse round-trip + install-summary display; drop the live-catalog
  prose pin (test_live_catalog_hindsight_declares_known_issues) and the
  zoneinfo import hack.
- contributors: map 5318happy@users.noreply.github.com -> happy5318.
2026-09-27 17:06:55 -07:00
happy5318 839fb764c2 fix(plugins): gate installs of catalog entries that document known issues (#124037)
## Thinking Path
`plugin-catalog/hindsight.yaml` already documented in prose that
`local_embedded` mode is unsupported on PM-managed Hermes with the current
pin — it still calls the retired lazy-install path for `hindsight-all` and
loops update takeover on every conversation — yet `hermes plugins install`
and the dashboard install path accepted the entry with no gate at all. Users
only discovered the trap after the death loop started. The catalog knew;
the installers didn't act on it.

## What Changed
- `hermes_cli/plugin_catalog.py`: new `known_issues: List[str]` field on
  `PluginCatalogEntry` (parsed from `known_issues` in each `plugin-catalog/*.yaml`,
  serialized into `to_dict()`, defaults to empty for backward compatibility).
- `plugin-catalog/hindsight.yaml`: declares the documented local-embedded trap
  as machine-readable `known_issues`.
- `hermes_cli/plugins_cmd_install.py`:
  - `cmd_install`: for catalog entries with `known_issues`, prints each issue
    and a bold-red notice, then requires explicit confirmation. Non-interactive
    (non-TTY) runs fail closed; interactive `y/N` runs require `y`.
  - `dashboard_install_plugin`: non-interactive path refuses the entry outright
    (no GUI bypass, mirroring the existing kill-list posture) and returns the
    issue list in the error payload.

## Why This Shape
The trap is machine-readable in the catalog, so the enforcement lives in the
install entry points rather than in hindsight-specific code — any future
catalog entry that documents a known trap gets the same gate for free. The
non-interactive path fails closed because it cannot ask for the confirmation
the interactive path requires.

## Verification
- RED/GREEN double proof via git stash: pre-fix 7 failed / 3 passed, post-fix
  10/10 passed (catalog parsing round-trip, hindsight.yaml declares the trap,
  non-TTY refuse, TTY yes proceeds, TTY no cancels, custom-source install not
  gated, dashboard refuse + unaffected path).
- Adjacent suites: test_plugin_catalog.py, test_plugin_validate.py,
  test_plugins_hub_live_catalog.py all green (36 passed). 9 errors in
  test_plugins_cmd_catalog.py reproduce identically with the fix stashed
  (existing local `pm` / uv-sync environment limitation, unrelated).
- ruff clean on all changed files.

## Contract Change
New optional catalog field `known_issues` (list of strings). Entries without
it behave exactly as before. Install behavior changes only for entries that
declare `known_issues`.

## Risks
- Catalog entries that declare `known_issues` become gated installs. Authors
  of future entries should only declare issues they want surfaced — this is
  the intended trade (a documented trap must not install silently).
- `cmd_install` non-TTY automation installing hindsight by name now fails.
  That is intentional: the trap cannot be confirmed non-interactively.

## Model Used
jd-deepseek-v4-flash-0731 (main session); pytest + ruff in the isolated
worktree.

## Related
#124037 (issue). Related: #122326 (resulting takeover loop), #7718
(`local_embedded` needs `hindsight-all`).
2026-09-27 17:06:55 -07: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 5630c223d0 fix(plugins): scan the carried tree once and name preserved files in a block
_install_plugin_core ran the security scan and the portable-package check
on the pristine clone, then ran both again after before_swap had merged in
user files. The second pass existed only because file-count/size limits
apply to the merged tree. before_swap needs only the manifest and the
staged tree, and both exist before the first scan. It now runs there, and
one scan/portable check admits the final bytes. Subdir updates scan once
(probe: 2 scans -> 1, and that scan sees the carried files).

A dangerous finding in carried user data (a cached page, a notes file)
blocked the update with a report that read as if the pristine upstream
revision were malicious. Carry callbacks now return the paths they
preserved. When a scan blocks, the message names the findings that sit in
those files, or, when none of the findings can be matched to them, notes
that the tree included preserved user files. Both the no-git reclone path
and update_plugin's catalog/git carry do this.
2026-09-27 01:42:52 +05:30
JoaoMarcos44 ad7a4e8e53 fix(plugins): harden staged user-state carry
(cherry picked from commit 945d92c489227f0986884bcf35b0150f81e4674a)
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