跳到主要内容

Browser Vault Plugins

A plugin can add a password manager (KeePass, a team vault, a hardware-backed store) as a login source for the browser_vault tool, when it comes from a verified source: the manager's vendor through the plugin catalog, or Nous (see Who can register a backend). The agent then lists and fills that manager's logins the same way it does for the built-in Hermes vault, 1Password and Bitwarden: it sees handles and metadata, never a password.

Browser login backends are separate from secret sources. A SecretSource registration does not make a password manager available to browser_vault.

Registering a backend​

An in-process plugin subclasses agent.vault_backends.base.LoginBackend and registers one instance:

from agent.vault_backends.base import LoginBackend
from agent.vault_store import VaultItemMeta


class KeePassBackend(LoginBackend):
name = "keepass" # lowercase identifier; also the config key
prefix = "keepass:" # must be exactly "<name>:"
display_name = "KeePass"

def list_items(self) -> list[VaultItemMeta]:
... # metadata only: ids "keepass:<opaque>", exact HTTPS origins, identifier

def get_meta(self, handle: str) -> VaultItemMeta | None:
...

def resolve_password(self, handle: str) -> str:
... # called once per fill, after the page origin matched the item


def register(ctx):
ctx.register_login_backend(KeePassBackend())

The backend stays off until the user turns it on for the profile:

vault:
keepass:
enabled: true # exactly true; installed code is never enabled automatically

Construct the backend without fetching credentials or reading profile state, and read the active profile's configuration lazily. Registration is profile-scoped and removed when the plugin unloads.

Who can register a backend​

Password-manager integrations come only from verified sources. ctx.register_login_backend is accepted from:

  • a plugin bundled with Hermes, or
  • a plugin installed from the Hermes plugin catalog at tier: verified (published by the password-manager vendor itself) or tier: official (maintained by Nous), at the catalog's reviewed pin.

Every other plugin is refused at registration with a log line naming this rule: a plugin dropped into ~/.hermes/plugins/ by hand, a Git URL install, a community-tier catalog entry, a catalog install moved off its pin with --ref, one installed with --allow-removed, a project plugin and a pip entry-point plugin. Provenance is read from Hermes's own install record (plugins/.install-metadata.json, written by the installer), never from a file inside the plugin, so a repository that ships its own .hermes-catalog.json gains nothing. No config key relaxes the rule.

If you build a password manager and want it in Hermes, submit the plugin to the catalog from your own repository and ask for verified tier (catalog admission rule 18). Integrations for managers whose vendor has not published one come from Nous as official entries.

Isolation rules​

Hermes wraps every plugin backend in a core-owned adapter before the vault tool sees it. These rules hold however the plugin implements its class:

RuleWhat it means for your backend
Own namespace onlyYour handles are <name>:…. Hermes decides ownership from the registered name, so it never calls your owns() and never passes you a vault_… (Hermes vault), op:…, bw:… or another plugin's handle. Items your list_items() returns with an id outside your namespace are dropped, and a get_meta() answer whose id is not the requested handle is ignored.
Handle in, value outlist_items() gets no arguments; get_meta(), resolve_password(), resolve_otp() and resolve_secret() get your handle and nothing else. You never receive the page, the CDP/browser session, the task, other vault entries, or decrypted values from any other backend.
No master-password promptHermes' unlock prompt is reserved for built-in managers. A backend with needs_unlock = True is refused at registration, and a plugin backend is never offered the unlock action, so the user is never asked to type a master password into a dialog a plugin labelled. Authenticate through your manager's own agent, session or OS keychain.
Reserved namespacesThe names local, onepassword, bitwarden, vault, op and bw, and any prefix inside a built-in prefix (vault_x:), are refused. So is a name another plugin already holds in the profile: no backend, built-in or plugin, can be replaced or shadowed. name and prefix must be plain string attributes, not properties, and the class may not override __getattribute__ or __class__: Hermes judges the registration without running your code.
Off by defaultOnly vault.<name>.enabled: true turns a backend on.
In-process onlyctx.register_login_backend is refused under plugins.isolation: host, because fill_context() is a server-local context manager.
Same fill pathPlugin items (passwords and automatic one-time codes) go through the same exact-origin pre-check, in-page origin re-assertion, supervisor CDP secret evaluation (never argv) and payment confirmation as built-in items.
Secrets stay out of the conversationValues you return are registered with the browser-result redaction boundary before they reach the page. Any exception your backend raises is replaced by a fixed message (The <name> login backend failed.) before it can reach a tool result, a log line or an exception chain, so a secret in your error text cannot reach the model. A value of the wrong type is refused the same way. Hermes copies what you return (metadata fields, passwords, codes) into its own plain objects inside that same guard, so an error raised while a field is read gets the fixed message too.

An in-process plugin is ordinary Python running inside Hermes, so these rules are an API contract, not a sandbox. They guarantee that Hermes never hands your backend another backend's data; they do not stop arbitrary code in the same process from reading files. The provenance rule above decides which plugins Hermes offers this seam to; it is not a sandbox either.

Fill context​

fill_context() defaults to a no-op. A backend may return its own context manager to lock fills and freeze credential-reference/origin configuration for the whole operation, so a reload cannot pair old metadata with a newly configured secret. Hermes enters it only for fills of your own handles (a password fill, and an automatic one-time code for browser_vault action=enter_code, both through the in-page write), and only after the page origin matched your item's get_meta(): a page on another origin never opens your context. Inside the context Hermes calls get_meta() again and refuses the fill unless the frozen item still binds that page origin, so the origins checked and the secret resolved come from one snapshot. It calls fill_context() with no arguments and exits it with (None, None, None), so you never see the core's exception. Release on exit even when the fill failed. A fill lock does not serialize unrelated browser navigation.

Writing errors​

Raise exceptions with fixed diagnostics. Hermes discards your exception text, but your own logs may still record it, so never put credential values or secret-manager response bodies in it. Never expose a model-facing tool that returns a password: it would bypass every rule above.