Reconciles the "pluginify everything" refactor (moving provider adapters,
platform adapters, and terminal backends out of core into plugins/) with
267 commits of main. main had 515 edits across 635 files; our branch moved
98 files into plugins/. The dangerous class was main editing files at OLD
paths that we relocated — handled below.
STRUCTURAL DECISIONS
- ai-gateway + vercel_sandbox: honored main's PR #33067 deletion. Dropped our
orphaned plugins/terminals/vercel/ dir and all 3 stale vercel refs that
survived in pyproject (workspace member, [all] entry, uv source).
- tools/lazy_deps.py: kept main's version. Our branch deleted it incompletely
(WIP) while main has 30 live callers + active maintenance. main's intent won.
- agent/agent_runtime_helpers.py: COMBINED — main's atomic swap+rollback
snapshot envelope wraps our pluginified registries-based anthropic imports.
- agent/credential_pool.py: took our plugin-hook indirection AND ported main's
api_key_path_explicit OAuth-masquerade security fix INTO the anthropic
credential_pool_hook (it was pluginified from a pre-fix base and would have
been silently dropped otherwise).
- hermes_cli/model_switch.py, hermes_cli/models.py: took main's logic
(cached_provider_model_ids, AI-gateway removal) then re-applied our pluginify
import rewrites (agent.bedrock_adapter/anthropic_adapter -> registries
namespace lookups) since those modules moved to plugins.
- tools/terminal_tool.py: took main's version (vercel_sandbox deletion) then
restored our lazy registry resolution for modal + daytona environments
(both moved to plugins; main's direct imports would crash at runtime).
- nix/checks.nix: kept both — our hermetic-boundary checks + main's
messaging-variant discord.py guard.
- pyproject.toml: plugin extras as workspace members (ours) + main's new wecom
extra; vercel removed throughout.
- uv.lock: regenerated with `uv lock` (not hand-merged); 233 packages.
VERIFICATION
- Zero conflict markers anywhere.
- Exhaustive 17-moved-module grep: no dead imports of relocated modules in core.
- Import smoke test: all hot core modules import clean.
- Targeted tests (21 files incl. credential_pool, anthropic plugin,
run_agent, project_metadata): pass.
- 3 credential_pool security tests rewired to register the real plugin hook
(core tests don't trigger plugin discovery): pass.
- Full suite: remaining failures are pre-existing on premerge-oh-god (telegram
collection error: setUpModule MagicMock __code__) or environmental (matrix
DNS/e2ee-deps, network) — NOT introduced by this merge. See PR notes.
Every provider profile is now a self-contained plugin under
plugins/model-providers/<name>/, mirroring the plugins/platforms/
pattern established for IRC and Teams. The ProviderProfile ABC
stays in providers/; the per-provider profile data moves out.
- plugins/model-providers/<name>/__init__.py calls register_provider()
- plugins/model-providers/<name>/plugin.yaml declares kind: model-provider
- providers/__init__.py._discover_providers() lazily scans bundled plugins
then $HERMES_HOME/plugins/model-providers/<name>/ (user override path)
- User plugins with the same name override bundled ones (last-writer-wins
in register_provider)
- Legacy providers/<name>.py layout still supported for back-compat with
out-of-tree editable installs
- Hermes PluginManager: new kind=model-provider; skipped like memory
plugins (providers/ discovery owns them); standalone plugins with
register_provider+ProviderProfile in their __init__.py auto-coerce to
this kind (same heuristic as memory providers)
- skip_names extended to include 'model-providers' so the general
PluginManager doesn't double-scan the category
- 4 new tests in tests/providers/test_plugin_discovery.py covering
bundled discovery, user override, and general-loader isolation
- Docs updated: website/docs/developer-guide/adding-providers.md,
provider-runtime.md, providers/README.md, plugins/model-providers/README.md
No API break: auth.py / config.py / doctor.py / models.py / runtime_provider.py /
model_metadata.py / auxiliary_client.py / chat_completions.py / run_agent.py
all still consume providers via get_provider_profile() / list_providers() —
they just now see plugin-discovered entries instead of pkgutil-iterated ones.
Third parties can now drop a single directory into
~/.hermes/plugins/model-providers/<name>/ to add or override an inference
provider without touching the repo.