37 Commits

Author SHA1 Message Date
Brooklyn Nicholson
31a68e9235 fix(tools): resolve the live plugin catalog once per plugins.list
The #119975 display-name lookup called get_live_catalog_entry() inside
the per-plugin loop of _plugin_rows, paying a full catalog resolution
(load_catalog_live: fetch or cache read + parse of every in-tree catalog
yaml, no memoization) once per installed plugin. plugins.manage list went
from O(1) to O(installed plugins) catalog resolutions, and a dead
catalog host cost one request timeout per plugin — the exact per-candidate
cost resolved_removed_entries() exists to eliminate.

Hoist the resolution next to pins/versions: catalog_titles() builds
{catalog_name: title} in one resolution and _plugin_server_rows reads
from the pre-resolved map, mirroring catalog_pins/catalog_versions.
Regression test counts load_catalog_live calls across a 3-plugin
listing: 3 before, 1 after.
2026-09-27 06:26:52 -05:00
kshitijk4poor
052d818569 fix(plugins): never walk guard-excluded dirs when carrying user files
The carry walk exempted links under tools.plugin_guard.EXCLUDED_DIRS from
the symlink refusal but still descended into .venv/, node_modules/ and tool
caches and copied every regular file below them. The staged tree got a venv
with pyvenv.cfg but no bin/python and a node_modules without .bin shims; the
carried node_modules also made _refresh_declared_dependencies skip `npm ci`
when the lockfile was unchanged, publishing the broken copy. Base never
carried ignored directories at all.

Prune EXCLUDED_DIRS in the walk's directory filter so the rule lives in one
place, and drop the now-unreachable EXCLUDED_DIRS clause in _user_link.
node_modules stays in _NO_GIT_REVISION_DIRS: _revision_owned_without_git
also uses it for a top-level *file* of that name, so it is not redundant.
The ignored-data test now asserts .venv/ and node_modules/ are not carried
while ignored user data still is.
2026-09-27 01:42:52 +05:30
kshitijk4poor
7be3b39931 fix(plugins): exempt every guard-excluded dir from the symlinked-user-file refusal
0d3e00f744 made symlinks in a git checkout's untracked/ignored set fail
the update, exempting only node_modules/. An ignored .venv/ or venv/
always holds symlinks (bin/python), so any plugin that keeps a local
virtualenv could no longer be updated from the catalog.

Reuse tools.plugin_guard.EXCLUDED_DIRS (node_modules, .venv, venv,
caches) as the exemption: the guard already treats those as
reproducible artefacts it never scans, and links under them are still
never followed into the staged tree. The ignored-data repin test now
keeps an ignored .venv with a bin/python link and must still update.
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
kshitijk4poor
03fae2fac8 fix(plugins): refuse symlinked user files on git-checkout updates
cff26600d2 stopped following symlinks when carrying untracked/ignored
files into a staged catalog update: a link planted after the installer's
scan could point outside the plugin root, past the guard. But it did so
by skipping them silently. Base followed the link and kept the content,
so a user whose ignored config.yaml is a link into their dotfiles now
loses that config on repin with no warning. _carry_user_files promises
to fail before publication rather than drop user state.

In a git checkout, symlinked files or dirs in the ??/!! set now fail the
update before publication, and the error names every such path. Links
are still never followed. Links under node_modules/ are .bin shims that
a reinstall recreates, so they stay skipped rather than blocking every
JS plugin's update. The no-git branch is unchanged: there, links may be
upstream's own.

The existing ignored-data-dir git test gains the case: the update
refuses, names data/link.yaml, and the live plugin keeps its revision,
its link and its data. This also gives the no-follow rule a test that
fails if the link is followed.
2026-09-27 01:42:52 +05:30
kshitijk4poor
1362b95c04 fix(plugins): keep dashboard/ and JS module files revision-owned on no-git carry
A subdirectory install has no .git, so the carry cannot tell removed
upstream code from user files and relies on a deny-list. That list missed
the dashboard surface (web_server_dashboard loads dashboard/manifest.json)
and .mjs/.cjs/.jsx/.tsx, so an upstream that dropped its dashboard or a
hook script got the old files back.

Add dashboard/ to _NO_GIT_REVISION_DIRS and the four extensions as a
carry-local set. They stay out of tools.plugin_guard.CODE_FILE_EXTENSIONS
on purpose: that set exempts code files from env-secret scan patterns, so
widening it there would weaken scanning rather than broaden it.

The docs now list what is actually enforced, and the existing subdir
update test pins dashboard/manifest.json + hooks/run.cjs removed upstream
are not resurrected.
2026-09-27 01:42:52 +05:30
kshitijk4poor
3d5ad396fc refactor(plugins): one conflict/skip helper and a pruned no-git carry walk
Gate review of the user-file carry found the destination-conflict message
copied three times, the no-git branch re-running the same junction/dir-clash
checks the shared path already does, two copies of the preserve-skip test,
and a walk that lstat()s every file under node_modules/, desktop/, skills/
and sidecar/ only to throw each one away.

- _conflict(rel, reason) builds every "left unchanged" refusal; the
  destination checks run once for both branches.
- _skip_preserve(name) is shared by _local_changes and _carry_user_files;
  the walk prunes skipped dirs, so the carry checks only the file name.
- The no-git walk prunes top-level _NO_GIT_REVISION_DIRS and classifies
  revision-owned paths before lstat().
- _local_changes returns (None, []) for a no-git tree, so its one caller
  unpacks directly.

No behaviour change.
2026-09-27 01:42:52 +05:30
kshitijk4poor
42f83867f4 fix(plugins): never carry symlinks into the staged tree on git updates
The no-git branch of _carry_user_files skipped symlinks, but the git
branch copied user-dir links verbatim. A link pointing outside the
plugin root would land in the new revision after the installer's first
scan, and the plugin guard skips symlinks. Only regular files are
durable user state, so both branches now carry regular files only.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-27 01:42:52 +05:30
kshitijk4poor
b2d6fed3f1 fix(plugins): treat every guard code extension as revision-owned on no-git carry
The no-git deny-list only matched `.py`, so a subdir install whose new
revision removed `server.js` / `run.sh` (or any other code file) had the
old copy resurrected into the updated tree. The post-carry re-scan is
pattern-based and cannot reliably block that. Reuse
tools.plugin_guard.CODE_FILE_EXTENSIONS as the single source of truth
for "plugin code" so removed code never survives an update.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
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
Austin Pickett
60696a90ee refactor(plugins): hoist PluginOperationError import and dedupe the dir-clash raise
No behaviour change: one lazy import per function instead of six inline,
a single _dir_clash() builder for the duplicated message, and drop the
redundant '.git' check already covered by _PRESERVE_SKIP.

(cherry picked from commit e77af285a3758c338f88c91748ef385a6df8d871)
2026-09-27 01:42:52 +05:30
JoaoMarcos44
d05b33cb4d fix(plugins): contain user-file carry within staged tree
(cherry picked from commit 97be7ac043a95f20268628e1a3c2e6d22c9c74d1)
2026-09-27 01:42:52 +05:30
JoaoMarcos44
3e03da41e1 fix(plugins): carry user files across staged updates
(cherry picked from commit 8b17cdef5a1cb45a5f9f79ec5590376147ffe239)
2026-09-27 01:42:52 +05:30
ethernet
9b4ed5f5ec refactor(pm): one plugin-input request for sync_venv and venv_is_current
sync_venv took four mutually exclusive plugin kwargs (plugin_dirs,
extra_plugin_dirs, selection, staged_plugin) and venv_is_current two, with
runtime ValueErrors guarding the combinations. Replace them with a single
`plugins=` argument typed as one of pm.plugin_inputs.Members, Candidates,
Selection or StagedUpdate, so a conflicting request cannot be expressed.
The module also owns the worker wire encoding that client.py and worker.py
each duplicated.

pm.install.sync_venv is split into cohesive helpers (feature policy,
install lock, publication snapshot, target selection, commit) with the
same ordering, receipts and recovery; its CC drops from 54 to 16.

All in-tree callers and tests move to the new argument.
2026-09-24 14:08:21 -04:00
ethernet
bc96d6cdc1 fix(plugins): name the plugin when a catalog rename hits a resolver conflict
The rename re-pin admits the renamed selection through the same authority
as enable; without plugin= a conflict there fell back to the generic
"plugin selection conflicts" wording.
2026-09-24 11:50:53 -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
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
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
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
ethernet
44f891a820 fix: repair PM call sites the origin/main merges overwrote with pre-PM code
Merges from origin/main resolved several files by taking main's side, which
re-inlined code paths this branch had already moved to PM. At HEAD that left:

- plugins/memory/hindsight: ImportError at module load (`_export_port_health_
  grace_timeout` and `_MIN_CLIENT_VERSION` no longer exist) — the whole plugin
  failed to import. Restored the side-env daemon client, folded main's
  fail-closed profile-env rewrite guard into it, and dropped the version
  auto-upgrade dance (the extra is pinned).
- mem0 / honcho / google_chat / memory_provider_migration / plugins_cmd_catalog:
  imported the deleted tools.lazy_deps and hermes_cli.plugin_python_deps
  modules; routed through pm.ensure_import / pm.sync_venv / pm.plugins_state.
  scripts/ci/check_lazy_deps_imports.py exits 0 again.
- tools/skills_hub: the PLUGIN-COMPAT __getattr__ had been moved above
  `_plugin_compat_prev_getattr = __getattr__`, so `SKILLS_DIR` recursed forever.
- tools/tirith_security: `import time` dropped; the circuit breaker NameError'd.
- hermes_cli/local_runtime/binaries: json/platform/os used without imports;
  the manifest.json scan was for a layout PM no longer writes — the boot gate
  now asks PM for an installed engine.
2026-09-18 19:06:52 -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
f971bbf512 fix(plugins): removed_annotation takes the resolved kill list as a required argument
The Optional default with an on-demand resolved_removed_entries() fallback had no
production caller (plugins_cmd.py and web_server_dashboard.py both pass the list)
and re-opened the per-row live-catalog fetch this PR removes. Make the parameter
required so a future per-row caller fails loudly instead of silently fetching.
2026-09-18 09:43:19 -07:00
teknium1
a89ef71b10 refactor(plugins): kill-list annotation takes the pre-resolved list directly; trim to two invariants
Follow-up to the salvaged #113687 / #113682 commits:

- `removed_annotation(name, dir_path, removed_entries=None)` resolves the kill list once itself
  when no list is passed and matches against it; the `removed_annotation_batch()` wrapper and its
  name-keyed dict are gone (a hub row keyed by `name` could shadow a same-named nested plugin).
  Both surfaces (`plugins list`, `_merged_plugins_hub`) call `resolved_removed_entries()` once
  and pass the list per row.
- The negative cache is one module float deadline instead of a URL-keyed dict plus two helpers;
  the duplicated stale-cache read is one `_stale_live_cache()`.
- Tests trimmed to two invariants: failure memory honours the TTL and still serves a stale copy;
  hub rebuild + `plugins list` each cost one network attempt and still annotate an in-tree removal.
  The #113682 route test's fake gains the new third argument.
- Docs: the live-refresh section says a failed fetch is remembered for a minute.
2026-09-18 09:43:19 -07:00
Konstantin Khlopkov
88b3163742 fix(plugins): one kill-list resolution per plugins hub rebuild and CLI listing 2026-09-18 09:43:19 -07:00
teknium1
f253f25b78 feat(plugins validate): --install-deps installs the declaration before the capability probe
Catalog CI validates each pinned tree in a venv that has only hermes-agent, so any plugin whose
code arrives through pyproject dependencies (the wrapper shape from #113851) failed the probe with
"No module named ...". The flag runs the same constrained install `plugins install` would, then
probes; the workflow passes it. Live: mnemosyne pin fails without the flag, passes with it.
2026-09-17 11:21:42 -07:00
ethernet
b4a294fff9 Merge origin/main; keep PM as plugin dependency owner
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
2026-09-17 13:52:05 -04:00
teknium1
96e8a23222 feat(plugins): install declared Python dependencies and re-apply them after hermes update
A directory plugin's pyproject.toml [project].dependencies (or manifest
python_dependencies) are now installed into the Hermes venv on install/enable/
update, resolved under a constraints file built from Hermes' own pinned
dependencies together with every enabled peer's declarations. A candidate that
cannot resolve is refused before its tree is moved into place; nothing is
installed and no other plugin is touched. --no-deps opts a single install out.

hermes update rebuilds the venv from Hermes' lock and strips everything else, so
its git, zip and both repair paths now re-apply the union of every profile's
enabled plugins. When the union no longer resolves, each plugin is resolved on
its own so only culprits are dropped (never an alphabetical neighbour); a
plugin-vs-plugin conflict peels non-memory plugins first because a Hermes that
boots without memory reads as data loss. Dropped plugins are disabled through
the real config writer with a loud message naming the fix.

python_runtime: external lets sidecar-venv plugins (Mnemosyne's shape) opt out
of the union; hermes-agent self-dependencies and direct-URL requirements are
never installed (the latter are surfaced for the user to install by hand).
2026-09-17 00:11:27 -07:00
teknium1
260da4ef62 fix: catalog installs no longer block on caution; admission runs the same scanner
Symptom: a third of catalog entries (23 of 71 pinned trees scanned today) could not be
installed from the Desktop. The install path runs plugin_guard on every clone and a
caution verdict needs a confirmation; the CLI prompts, the Desktop/dashboard path
passes no decision callback, so caution became "Security scan blocked".

Root cause: admission (hermes plugins validate + catalog CI) never ran the scanner, so a
pin could be approved by a human and still trip the installer.

Fix, both halves:
- validate_plugin_dir gains a "security scan" check: dangerous fails the entry, caution
  is reported as warnings so the reviewer reads the findings before merging the pin.
  pinned-source-validate in plugin-catalog-ci.yml therefore runs the identical scanner.
- _install_plugin_core accepts reviewed_pin=<catalog sha>; when the checked-out revision
  equals it, caution is accepted without a prompt. dangerous still blocks (a signature
  added after review is what the backstop is for). Raw-URL installs, --ref overrides
  and a checkout at any other revision keep the current behaviour.

Live: dashboard_install_plugin('weather') on origin/main -> BLOCKED caution; after -> ok.
2026-09-16 15:47:44 -07:00
teknium1
034313e7cd feat: plugin catalog entries carry an optional version label and card image
The 40-hex sha stays the release, but nobody reads one. Entries may now add
`version: "1.4.0"` (free-form, <=32 chars, never parsed) and `image:` (an https
URL on raw.githubusercontent.com / github.com / *.githubusercontent.com).

Why GitHub-only: the Desktop catalog browser deliberately never fetches from
third-party hosts, and a raw URL pinned to the entry commit is as immutable as
the sha it decorates.

Readers updated together: PluginCatalogEntry + entry_from_mapping (drop with a
warning, entry survives), validate_plugin_catalog.py (admission error), the
site extractor (drop, never fatal), the /docs/plugins card (banner + version
pill + "1.4.0 @ abcd1234" pin), the CLI table/info (pin_label), the TUI-gateway
plugin row (catalog_version -> Desktop "Update to 1.4.0"), and the Desktop
catalog detail header (image).
2026-09-16 14:18:39 -07:00
teknium1
55dbd7f6e1 feat(plugin-catalog): shelve the catalog by category (Memory, Desktop, Platforms, …)
The catalog page was one undifferentiated grid filtered only by tier, so a
memory provider sat between two Desktop panes. Entries now carry an optional
``category`` (memory | desktop | platform | web | tools | voice | automation |
models | other, default other) that the loader, the admission validator and
the site extractor all understand.

/docs/plugins renders one shelf per category in browse mode, a category pill
row under the tier pills, a clickable category chip on every card, and a
results bar (active category, count, clear) when a filter or search flattens
the view. ``hermes plugins catalog`` gains a Category column and groups by it.
All 18 shipped entries are categorised. Unknown categories fail admission
(same contract as tier) so a typo cannot create a phantom shelf.
2026-09-14 21:00:29 -07:00
ethernet
e4cc7f09d9 merge: integrate upstream catalog with PM publication
Keep upstream's reviewed catalog as the only plugin name index.
Catalog pins and custom update sources share staged PM validation.
Publish code and dependencies with recovery after process death.
Reject a concurrent enablement change before publishing disabled code.

Use the manifest loader's supported version in the installer. Keep
probe cooldowns for timeouts, not TLS failures that a CA change fixes.
Preserve the backup, uninstall, browser and memory-provider repairs.

Verified with the canonical runner on native Windows ARM64, real Git
repositories, local TLS endpoints and UV dependency generations.
Desktop catalog tests and both TypeScript checks pass. The full suite
and native release builds were not run. No remote push.
2026-09-09 16:49:27 -04:00
Teknium
41b4555ed9 feat(plugins): catalog is the sole discovery system — re-port onto main's layout
- hermes_cli/plugins_cmd_catalog.py: new sibling owning resolution, the
  .hermes-catalog.json provenance sidecar, search/info/validate, re-pin on
  update, and the dashboard/TUI payload builders. plugins_cmd.py only
  gains the hooks (cmd_install catalog branch, cmd_update / dashboard
  update re-pin, dashboard_install_plugin catalog_name + kill list,
  dispatch entries); the community index (plugin_index.py) is gone.
- hermes_cli/plugin_catalog.py: catalog_dir parameter replaces the
  test-only HERMES_PLUGIN_CATALOG_DIR env var; live refresh reads ONE
  published document (/docs/api/plugin-catalog.json, 6h cache, in-tree
  fallback) instead of the unauthenticated GitHub contents API (60 req/h,
  1 request per entry); in-tree and live removals are unioned so a stale
  cache can never un-block.
- Catalog route lives in web_routers/dashboard_ui.py (the facade is off
  limits); _plugin_runtime_status shared from web_server_dashboard.py;
  hub rows carry removed_reason. TUI plugins.manage gains catalog_name
  install, catalog row fields and an update action.
- plugin_validate: the probe context honours ctx.get_config defaults
  (real plugins do int(ctx.get_config("timeout", 180)) in register()).
- Installed-state merge matches through the sidecar's catalog_name
  first — catalog names rarely equal manifest names.
- extract-plugins.py emits plugin-catalog.json; deploy-site triggers on
  plugin-catalog/** so entry merges republish it.
2026-09-09 04:38:01 -07:00