docs: Plugins subcategory under Extending + secret-source plugin guide + 1Password sidebar fix (#59613)

* docs(secrets): secret-source plugin developer guide + sidebar registration for 1Password page

- New developer-guide/secret-source-plugin.md: SecretSource contract
  (never raises/prompts, fetch-only, timeout budget), framework-vs-plugin
  ownership table, mapped-vs-bulk shape guidance, run_secret_cli()
  subprocess-safety, registration + timing note, conformance kit usage,
  ErrorKind reference.
- Register user-guide/secrets/onepassword in the sidebar (page shipped
  in #59498 but was not listed, so it was unreachable from nav).
- Cross-link the user-guide plugin section to the new dev guide.

* docs: group all plugin guides under a Plugins subcategory in Extending

- Move guides/build-a-hermes-plugin.md -> developer-guide/plugins/index.md
  (both locales) and make it the category landing page (slug pinned to
  /developer-guide/plugins).
- New sidebar subcategory Developer Guide > Extending > Plugins holding
  the general guide + all 8 provider-plugin docs (llm-access, memory,
  context-engine, secret-source, model, image-gen, video-gen, web-search);
  provider-doc URLs unchanged.
- Client redirect /guides/build-a-hermes-plugin -> /developer-guide/plugins.
- Update 30 cross-links across both locales.
This commit is contained in:
Teknium 2026-07-06 12:11:31 -07:00 committed by GitHub
parent 1ea0bbbb0d
commit b24ff550c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 249 additions and 65 deletions

View file

@ -14,7 +14,7 @@ If you want a personal, project-local, or otherwise custom tool without
modifying Hermes core, use the plugin route instead:
- [Plugins](/user-guide/features/plugins)
- [Build a Hermes Plugin](/guides/build-a-hermes-plugin)
- [Build a Hermes Plugin](/developer-guide/plugins)
Default to plugins for most custom tool creation. Only follow this page when
you explicitly want to ship a new built-in tool in `tools/` and `toolsets.py`.

View file

@ -231,7 +231,7 @@ Long-running process with 20 platform adapters, unified session routing, user au
Three discovery sources: `~/.hermes/plugins/` (user), `.hermes/plugins/` (project), and pip entry points. Plugins register tools, hooks, and CLI commands through a context API. Two specialized plugin types exist: memory providers (`plugins/memory/`) and context engines (`plugins/context_engine/`). Both are single-select — only one of each can be active at a time, configured via `hermes plugins` or `config.yaml`.
→ [Plugin Guide](/guides/build-a-hermes-plugin), [Memory Provider Plugin](./memory-provider-plugin.md)
→ [Plugin Guide](/developer-guide/plugins), [Memory Provider Plugin](./memory-provider-plugin.md)
### Cron

View file

@ -22,7 +22,7 @@ We value contributions in this order:
## Common contribution paths
- Building a custom/local tool without modifying Hermes core? Start with [Build a Hermes Plugin](../guides/build-a-hermes-plugin.md)
- Building a custom/local tool without modifying Hermes core? Start with [Build a Hermes Plugin](../developer-guide/plugins/index.md)
- Building a new built-in core tool for Hermes itself? Start with [Adding Tools](./adding-tools.md)
- Building a new skill? Start with [Creating Skills](./creating-skills.md)
- Building a new inference provider? Start with [Adding Providers](./adding-providers.md)

View file

@ -9,7 +9,7 @@ description: "How to build an image-generation backend plugin for Hermes Agent"
Image-gen provider plugins register a backend that services every `image_generate` tool call — DALL·E, gpt-image, Grok, Flux, Imagen, Stable Diffusion, fal, Replicate, a local ComfyUI rig, anything. Built-in providers (OpenAI, OpenAI-Codex, xAI) all ship as plugins. You can add a new one, or override a bundled one, by dropping a directory into `plugins/image_gen/<name>/`.
:::tip
Image-gen is one of several **backend plugins** Hermes supports. The others (with more specialized ABCs) are [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/guides/build-a-hermes-plugin).
Image-gen is one of several **backend plugins** Hermes supports. The others (with more specialized ABCs) are [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/developer-guide/plugins).
:::
## How discovery works
@ -307,10 +307,10 @@ Or interactively: `hermes tools` → "Image Generation" → select `my-backend`
my-backend-imggen = "my_backend_imggen_package"
```
`my_backend_imggen_package` must expose a top-level `register` function. See [Distribute via pip](/guides/build-a-hermes-plugin#distribute-via-pip) in the general plugin guide for the full setup.
`my_backend_imggen_package` must expose a top-level `register` function. See [Distribute via pip](/developer-guide/plugins#distribute-via-pip) in the general plugin guide for the full setup.
## Related pages
- [Image Generation](/user-guide/features/image-generation) — user-facing feature documentation
- [Plugins overview](/user-guide/features/plugins) — all plugin types at a glance
- [Build a Hermes Plugin](/guides/build-a-hermes-plugin) — general tools/hooks/slash commands guide
- [Build a Hermes Plugin](/developer-guide/plugins) — general tools/hooks/slash commands guide

View file

@ -257,7 +257,7 @@ acme-inference = "acme_hermes_plugin:register"
…where `acme_hermes_plugin:register` is a function that calls `register_provider(profile)`. The general PluginManager picks up entry-point plugins during `discover_and_load()`. For `kind: model-provider` pip plugins, you still need to declare the kind in your manifest (or rely on the source-text heuristic).
See [Building a Hermes Plugin](/guides/build-a-hermes-plugin#distribute-via-pip) for the full entry-points setup.
See [Building a Hermes Plugin](/developer-guide/plugins#distribute-via-pip) for the full entry-points setup.
## Related pages
@ -265,4 +265,4 @@ See [Building a Hermes Plugin](/guides/build-a-hermes-plugin#distribute-via-pip)
- [Adding Providers](/developer-guide/adding-providers) — end-to-end checklist for new inference backends (covers both the fast plugin path and the full CLI/auth integration)
- [Memory Provider Plugins](/developer-guide/memory-provider-plugin)
- [Context Engine Plugins](/developer-guide/context-engine-plugin)
- [Building a Hermes Plugin](/guides/build-a-hermes-plugin) — general plugin authoring
- [Building a Hermes Plugin](/developer-guide/plugins) — general plugin authoring

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,171 @@
---
sidebar_position: 9
title: "Secret Source Plugins"
description: "How to build a secret-manager backend plugin for Hermes Agent"
---
# Building a Secret Source Plugin
Secret sources resolve provider credentials from an external secret manager (a vault, a password manager, an OS keystore, a custom script) into environment variables at process startup — after `~/.hermes/.env` loads, before Hermes reads credentials. Bitwarden and 1Password ship in-tree; **every other backend is a plugin**. This guide covers building one.
:::tip
The bundled set is deliberately closed, same policy as [memory providers](/developer-guide/memory-provider-plugin): PRs adding new vault backends under `agent/secret_sources/` are closed with a pointer to this guide. Publish your backend as a standalone plugin repo and share it in the Nous Research Discord (`#plugins-skills-and-skins`).
:::
## What the framework owns vs. what you own
The orchestrator (`agent.secret_sources.registry.apply_all`) owns everything security- and precedence-sensitive, so a backend cannot get it wrong:
| Framework owns | You own |
|---|---|
| Source ordering, mapped-vs-bulk precedence | Fetching values from your backend |
| First-claim-wins conflict handling + warnings | Validating your reference format |
| `override_existing` semantics (never crosses sources) | Talking to your CLI/SDK/API |
| Protected bootstrap tokens | Declaring which env var IS your bootstrap token |
| Per-source wall-clock timeout | Keeping `fetch()` reasonably fast |
| Per-var provenance + `(from X)` labels | A human-readable `label` |
| `os.environ` writes | Nothing — you never touch the environment |
## Directory structure
```
~/.hermes/plugins/my-vault/
├── plugin.yaml # name, description
└── __init__.py # SecretSource subclass + register(ctx)
```
## The SecretSource ABC
Implement `agent.secret_sources.base.SecretSource`. One method is required:
```python
from pathlib import Path
from agent.secret_sources.base import (
ErrorKind,
FetchResult,
SecretSource,
run_secret_cli,
)
class MyVaultSource(SecretSource):
name = "myvault" # config section key: secrets.myvault
label = "My Vault" # used in startup lines + provenance labels
shape = "mapped" # "mapped" (explicit VAR→ref map) or "bulk" (project dump)
scheme = "mv" # optional: unique URI scheme you own (mv://...)
def fetch(self, cfg: dict, home_path: Path) -> FetchResult:
"""Resolve secrets. MUST NOT raise. MUST NOT prompt."""
result = FetchResult()
token = os.environ.get("MYVAULT_TOKEN", "").strip()
if not token:
result.error = "secrets.myvault.enabled is true but MYVAULT_TOKEN is not set."
result.error_kind = ErrorKind.NOT_CONFIGURED
return result
try:
proc = run_secret_cli(
["myvault-cli", "export", "--json"],
allow_env=["MYVAULT_TOKEN"], # ONLY your auth vars — never full os.environ
timeout=30,
)
except RuntimeError as exc: # spawn failure / timeout
result.error = str(exc)
result.error_kind = ErrorKind.BINARY_MISSING
return result
if proc.returncode != 0:
result.error = f"myvault-cli exited {proc.returncode}: {proc.stderr[:200]}"
result.error_kind = ErrorKind.AUTH_FAILED
return result
result.secrets = parse_your_output(proc.stdout) # {ENV_VAR: value}
return result
def protected_env_vars(self, cfg: dict):
# Your bootstrap token — no source (including yours) may ever overwrite it.
return frozenset({"MYVAULT_TOKEN"})
```
### Contract rules (enforced, not suggestions)
- **`fetch()` never raises.** Errors go in `result.error` + `result.error_kind`. A raising fetch is contained by the orchestrator and reported as `INTERNAL` — a contract violation, not a feature.
- **`fetch()` never prompts.** Startup runs in non-TTY contexts (gateway, cron, Docker). `run_secret_cli()` closes stdin so a prompting helper fails fast. Interactive auth belongs in your CLI setup flow, never on the startup path.
- **Sync, within budget.** The orchestrator enforces a wall-clock timeout (default 120s, user-tunable via `secrets.<name>.timeout_seconds`). Exceeding it reports `TIMEOUT` and your result is discarded.
- **You fetch; the orchestrator applies.** Return the mapping you *would* contribute. Never write `os.environ` yourself — you'd bypass precedence, conflict detection, and provenance.
- **API versioning.** `SecretSource.api_version` defaults to the current `SECRET_SOURCE_API_VERSION`. The registry skips (with a warning) sources built against a different version instead of crashing startup.
### Choosing your `shape`
- `mapped` — the user explicitly binds env-var names to references in config (like 1Password's `env:` map). Strongest intent: mapped claims beat bulk claims on contested vars.
- `bulk` — you inject a whole project/folder of secrets implicitly (like Bitwarden BSM). Yields to mapped sources.
### Optional hooks
| Method | Default | Override when |
|---|---|---|
| `is_enabled(cfg)` | `cfg.get("enabled")` | Custom activation logic |
| `override_existing(cfg)` | `cfg.get("override_existing", False)` | You want a different default (both bundled sources default `True` for rotation) |
| `protected_env_vars(cfg)` | empty | You have a bootstrap token (you almost certainly do) |
| `fetch_timeout_seconds(cfg)` | 120s | Your backend needs a different budget |
| `config_schema()` | `{}` | Declare config keys for setup surfaces |
## Subprocess safety: use `run_secret_cli()`
If your backend shells out to a CLI, use the shared helper instead of `subprocess.run` directly. It gives you the audited posture for free: argv-only (no `shell=True`), a **minimal allowlisted child environment** (by the time sources run, `os.environ` holds every credential Hermes knows — never hand that to a child process), `NO_COLOR` + ANSI-scrubbed stderr, stdin closed, timeout → clean `RuntimeError`. Pass user-supplied reference strings after a `--` terminator in your argv so they can never parse as flags.
## Registering
```python
# __init__.py
def register(ctx):
ctx.register_secret_source(MyVaultSource())
```
Registration is rejected (with a log warning, never a crash) for: non-`SecretSource` instances, invalid/duplicate names, a `scheme` another source owns, wrong `api_version`, or a `shape` outside `mapped`/`bulk`.
:::note Timing
Plugin discovery runs later in startup than the first `load_hermes_dotenv()` call, so a plugin source is not consulted by the very first env load of the process that discovers it. It IS consulted by every subsequently spawned Hermes process (gateway children, cron sessions, subagents). Bundled sources cover first-process bootstrap.
:::
## Users configure it like any other source
```yaml
secrets:
sources: [myvault, bitwarden] # optional ordering
myvault:
enabled: true
# ... your config_schema keys
```
Multi-source precedence, conflict warnings, and `(from My Vault)` provenance labels all work automatically — see the [user-facing secrets docs](/user-guide/secrets/) for the precedence ladder.
## Validate with the conformance kit
Subclass the kit from the Hermes repo (`tests/secret_sources/conformance.py`) in your plugin's tests:
```python
import pytest
from tests.secret_sources.conformance import SecretSourceConformance
class TestMyVaultConformance(SecretSourceConformance):
@pytest.fixture
def source(self):
return MyVaultSource()
```
It checks the rules that break other people when violated: never-raises on malformed config, machine-readable error kinds, disabled-by-default, positive timeouts, valid protected-var names, and a full `apply_all()` round trip. Green conformance is the review bar for calling a backend contract-compliant.
## ErrorKind reference
| Kind | Meaning |
|---|---|
| `NOT_CONFIGURED` | Enabled but missing token / project / map |
| `BINARY_MISSING` | Helper CLI not found or not executable |
| `AUTH_FAILED` / `AUTH_EXPIRED` | Bad / expired credentials |
| `REF_INVALID` | A secret reference failed validation |
| `NETWORK` | Transport-level failure |
| `EMPTY_VALUE` | Backend returned nothing for a ref — never apply `""` over a good credential |
| `TIMEOUT` | Fetch exceeded its budget |
| `INTERNAL` | Anything else (bug, unexpected shape) |

View file

@ -9,7 +9,7 @@ description: "How to build a web-search/extract/crawl backend plugin for Hermes
Web-search provider plugins register a backend that services `web_search`, `web_extract`, and (optionally) deep-crawl tool calls. Built-in providers — Firecrawl, SearXNG, Tavily, Exa, Parallel, Brave Search (free tier), xAI, and DDGS — all ship as plugins under `plugins/web/<name>/`. You can add a new one, or override a bundled one, by dropping a directory next to them.
:::tip
Web search is one of several **backend plugins** Hermes supports. The others (with their own ABCs) are [Image Generation Provider Plugins](/developer-guide/image-gen-provider-plugin), [Video Generation Provider Plugins](/developer-guide/video-gen-provider-plugin), [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/guides/build-a-hermes-plugin).
Web search is one of several **backend plugins** Hermes supports. The others (with their own ABCs) are [Image Generation Provider Plugins](/developer-guide/image-gen-provider-plugin), [Video Generation Provider Plugins](/developer-guide/video-gen-provider-plugin), [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/developer-guide/plugins).
:::
## How discovery works
@ -141,7 +141,7 @@ requires_env:
|---|---|
| `kind: backend` | Routes the plugin through the backend-loading path |
| `provides_web_providers` | List of provider `name`s this plugin registers — used by the loader to advertise the plugin in `hermes tools` even before `register()` runs |
| `requires_env` | Interactive credential prompt during `hermes plugins install` (see [Build a Hermes Plugin](/guides/build-a-hermes-plugin#gate-on-environment-variables) for the rich format) |
| `requires_env` | Interactive credential prompt during `hermes plugins install` (see [Build a Hermes Plugin](/developer-guide/plugins#gate-on-environment-variables) for the rich format) |
## ABC reference
@ -233,7 +233,7 @@ Errors surface as the tool result; the LLM decides how to explain them. If no pr
## Lazy-installing optional dependencies
If your provider wraps a third-party SDK (like DDGS does with the `ddgs` package), don't `import` it at module top level. Use `tools.lazy_deps.ensure(...)` inside `is_available()` or `search()` — Hermes will install the package on first use, gated by `security.allow_lazy_installs`. See [Build a Hermes Plugin → Lazy-install](/guides/build-a-hermes-plugin#lazy-install-optional-python-dependencies) for the security model.
If your provider wraps a third-party SDK (like DDGS does with the `ddgs` package), don't `import` it at module top level. Use `tools.lazy_deps.ensure(...)` inside `is_available()` or `search()` — Hermes will install the package on first use, gated by `security.allow_lazy_installs`. See [Build a Hermes Plugin → Lazy-install](/developer-guide/plugins#lazy-install-optional-python-dependencies) for the security model.
## Reference implementations
@ -251,10 +251,10 @@ If your provider wraps a third-party SDK (like DDGS does with the `ddgs` package
my-backend-web = "my_backend_web_package"
```
`my_backend_web_package` must expose a top-level `register` function. See [Distribute via pip](/guides/build-a-hermes-plugin#distribute-via-pip) in the general plugin guide for the full setup.
`my_backend_web_package` must expose a top-level `register` function. See [Distribute via pip](/developer-guide/plugins#distribute-via-pip) in the general plugin guide for the full setup.
## Related pages
- [Web Search](/user-guide/features/web-search) — user-facing feature documentation and per-backend configuration
- [Plugins overview](/user-guide/features/plugins) — all plugin types at a glance
- [Build a Hermes Plugin](/guides/build-a-hermes-plugin) — general tools/hooks/slash commands guide
- [Build a Hermes Plugin](/developer-guide/plugins) — general tools/hooks/slash commands guide