mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-14 14:12:44 +00:00
feat(secrets): pluggable SecretSource interface + multi-source orchestrator
Introduces a first-class secret-source contract so password managers (Bitwarden today, 1Password next, third-party vaults as plugins) plug into one orchestrated startup path instead of each hardcoding into env_loader. - agent/secret_sources/base.py: SecretSource ABC (fetch-only contract: never raises, never prompts, sync with orchestrator-enforced timeout), shared ErrorKind taxonomy, FetchResult, run_secret_cli() minimal-env subprocess helper, API versioning for plugin compatibility. - agent/secret_sources/registry.py: registration gating (name/scheme uniqueness, api_version, shape), apply_all() orchestrator owning precedence (mapped-beats-bulk, first-claim-wins, override_existing never crosses sources, protected bootstrap tokens), conflict warnings, per-var provenance, per-source wall-clock timeout. - Bitwarden converted to a registered BitwardenSource (bulk shape); behavior unchanged, apply_bitwarden_secrets kept as legacy shim. - env_loader._apply_external_secret_sources now drives the orchestrator; provenance labels resolve through registry (e.g. '(from 1Password)'). - PluginContext.register_secret_source() for external backends. - secrets.sources optional ordering key in DEFAULT_CONFIG + example. - tests/secret_sources/: 47 new tests incl. reusable conformance kit (SecretSourceConformance) that plugin authors run against their source.
This commit is contained in:
parent
d345b9fbfe
commit
2d16ec7fb7
14 changed files with 1616 additions and 110 deletions
|
|
@ -795,6 +795,53 @@ class PluginContext:
|
|||
self.manifest.name, provider.name,
|
||||
)
|
||||
|
||||
# -- secret source registration -------------------------------------------
|
||||
|
||||
def register_secret_source(self, source) -> None:
|
||||
"""Register an external secret-manager backend.
|
||||
|
||||
``source`` must be an instance of
|
||||
:class:`agent.secret_sources.base.SecretSource`. Registered
|
||||
sources run during ``load_hermes_dotenv()`` startup — after
|
||||
``~/.hermes/.env`` loads, before Hermes reads credentials — when
|
||||
their ``secrets.<source.name>`` config section is enabled. The
|
||||
orchestrator (``agent.secret_sources.registry.apply_all``) owns
|
||||
ordering, mapped-vs-bulk precedence, conflict warnings, and
|
||||
provenance; the source only fetches.
|
||||
|
||||
NOTE ON TIMING: plugin discovery happens later in startup than
|
||||
the first ``load_hermes_dotenv()`` call, so a plugin-registered
|
||||
source is not consulted by the initial env load of the process
|
||||
that discovers it. It IS consulted by every subsequently
|
||||
spawned Hermes process (gateway children, cron sessions,
|
||||
subagents), and immediately after a
|
||||
``reset_secret_source_cache()`` re-pull. Plugin sources are
|
||||
therefore best for supplying credentials to the running fleet;
|
||||
the bundled sources cover first-process bootstrap.
|
||||
|
||||
Contract requirements (rejected with a warning otherwise):
|
||||
inherit from ``SecretSource``, ``api_version`` matching
|
||||
``SECRET_SOURCE_API_VERSION``, lowercase unique ``name``,
|
||||
``shape`` of ``"mapped"`` or ``"bulk"``, unique ``scheme`` (when
|
||||
set), and a ``fetch()`` that never raises and never prompts.
|
||||
See the base-module docstring for the full contract.
|
||||
"""
|
||||
from agent.secret_sources.base import SecretSource
|
||||
from agent.secret_sources.registry import register_source
|
||||
|
||||
if not isinstance(source, SecretSource):
|
||||
logger.warning(
|
||||
"Plugin '%s' tried to register a secret source that does "
|
||||
"not inherit from SecretSource. Ignoring.",
|
||||
self.manifest.name,
|
||||
)
|
||||
return
|
||||
if register_source(source):
|
||||
logger.info(
|
||||
"Plugin '%s' registered secret source: %s",
|
||||
self.manifest.name, source.name,
|
||||
)
|
||||
|
||||
# -- TTS provider registration -------------------------------------------
|
||||
|
||||
def register_tts_provider(self, provider) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue