Commit Graph

18 Commits

Author SHA1 Message Date
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
teknium1
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