A plugin.yaml with no __init__.py, desktop/plugin.js or plugin.json beside it
installs "successfully" and does nothing (pip-layout repos whose code sits under
src/ behind an entry point). The validator and catalog CI now fail that shape
and check declared Python dependencies parse and are index specs; direct-URL
requirements warn.
Tests: conflicting candidate refused with peers untouched; post-update re-apply
drops only the culprit and keeps the memory provider. Loader wording test
updated to the new hint.
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.
Two false positives in `hermes plugins validate` that block catalog admission
for plugins that are correct at runtime:
- `kind: model-provider` plugins register at import via
providers.register_provider(ProviderProfile); the PluginManager never calls a
register(ctx) on them (plugins_discovery skips the kind). The probe demanded
register() anyway, so every provider plugin -- including the in-tree
plugins/model-providers/* -- failed with "no register() function". The probe
now records register_provider calls for that kind and fails only when the
import registers nothing.
- RecordingContext returned a no-op callable for ANY attribute, so
`getattr(ctx, "profile_path", None)` was truthy under validation alone and
register() crashed with an error the real PluginContext never produces. The
parent now passes the real PluginContext method names into the probe; other
names raise AttributeError exactly like the real object.
Surfaced by the 2026-09-15 catalog sweep (Gondola provider, hermes-persona).
- 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.
New hermes_cli/plugin_validate.py — the checks behind
'hermes plugins validate <dir>': manifest fields, strict requires_hermes
spec parsing, config: shape, UPPER_SNAKE requires_env, a capability
probe that imports the plugin and calls register(ctx) against a
recording stub in a scratch subprocess (throwaway HERMES_HOME, 30s
timeout) and diffs actual registrations against provides_* (undeclared
= fail, unregistered = warn), plus built-in tool collision checks via
the discovered tool registry.