feat(secrets): add 1Password SDK-based secret source (onepassword-sdk) - #39445
feat(secrets): add 1Password SDK-based secret source (onepassword-sdk)#39445thesammykins wants to merge 2 commits into
Conversation
Add a 1Password Service Account secret source that uses the onepassword-sdk Python package instead of the op CLI daemon. The SDK authenticates directly via the 1Password REST API and does not require a background daemon. This complements PR NousResearch#36896 (CLI-based 1Password backend) by offering an SDK-based transport option. Both implementations share the same _cache.py substrate introduced here. Key features: - Two mapping modes: explicit env: mapping AND auto-discovery - Two-layer cache (in-process + disk) with rate-limit cooldown - Four in-session tools for vault access (onepassword_list_vaults, onepassword_list_items, onepassword_get_item, onepassword_resolve_field) - Full CLI: hermes secrets onepassword {setup,status,sync,disable, list-vaults} - Fail-open design: missing SDK, expired token, bad refs never block startup The shared _cache.py module (TwoLayerCache, FetchResult, CachedFetch, is_valid_env_name) extracts cache logic that was previously duplicated and can serve both Bitwarden (future) and 1Password backends from one auditable location. Fixes NousResearch#36949
1bb8130 to
30aa126
Compare
… from L1/L2 cache The '1Password: applied N secrets' message printed unconditionally regardless of whether secrets came from cache or a real API fetch, creating the false impression of fresh API calls on every invocation. Now appends ' (cached)' when FetchResult.cache_hit is True, so users can see at a glance whether the fetch hit the two-layer cache (L1 in-process dict or L2 disk JSON) or actually called the 1Password API.
|
1Password support landed on main via PR #59498, which also ships a pluggable SecretSource interface — an ABC + orchestrator that lets multiple secret managers run simultaneously with deterministic precedence, conflict warnings, and per-var provenance. The merged backend is a salvage of #36896 (earliest full-cluster credit to #32254). This PR is superseded by that interface; thanks for the contribution and sorry we couldn't land them all — six independent 1Password PRs is exactly why the interface now exists. If you'd like to build further secret-manager backends, they now plug in as standalone plugins via |
Summary
Adds a 1Password Service Account secret source that uses the
onepassword-sdkPythonpackage instead of the
opCLI daemon.Why the SDK instead of the
opCLI?The
opCLI's daemon-based architecture (op daemon --background)can hang indefinitely on macOS in headless/background contexts — exactly
the environment Hermes runs in (gateway, cron, CLI background tasks).
The SDK authenticates directly via the 1Password REST API, avoiding the
daemon entirely. This also enables in-session tools that don't need to
shell out.
This PR is additive to PR #36896 (@hwrdprkns's CLI-based 1Password
backend) — both implementations share the same
_cache.pysubstrate andcan coexist. Users who prefer the
opCLI can use #36896; users onmacOS or who want in-session tools can use this SDK-based transport.
Fixes #36949.
What changed
New files
agent/secret_sources/_cache.pyTwoLayerCache,FetchResult,CachedFetch) — extracted from patterns first proposed in PR #36896, generalised with an in-process L1 layer and rate-limit cooldown supportagent/secret_sources/onepassword.pytools/onepassword_tool.pyonepassword_list_vaults,onepassword_list_items,onepassword_get_item,onepassword_resolve_field)tests/test_onepassword_secrets.pyModified files
agent/secret_sources/__init__.pyhermes_cli/env_loader.py_apply_bitwarden, add_apply_onepasswordhermes_cli/main.pyhermes secrets onepasswordsubparserhermes_cli/secrets_cli.pyregister_onepassword_cli+ all CLI handlerstoolsets.pyonepasswordtoolsetcli-config.yaml.exampleKey design decisions
Two mapping modes
Explicit — list env-var →
op://references (like Bitwarden):Auto-discovery — scan a vault and map credential fields → env vars:
Both modes can be used together; explicit mappings take precedence on
naming collisions.
Two-layer cache with rate-limit cooldown
The 1Password Service Account API throttles at 1,000 reads/hour.
A naive single-disk-layer cache can't protect against N sibling
processes (gateway + dashboard + slash workers) retrying in lockstep.
429is hit, ALL processes back offfor one hour, preventing
N × retriescall amplificationFail-open
Missing SDK, expired token, bad reference, empty vault — everything
produces a one-line warning and Hermes continues with whatever
credentials
.envalready had. This backend cannot block startup.In-session tools
Four tools registered under the
onepasswordtoolset for mid-sessionvault access. The
onepassword_resolve_fieldtool uses avault-contents cache — on first access it fetches ALL items+fields
from the vault, then resolves from a dict. Subsequent resolves within
the TTL are O(1) dictionary lookups with zero API calls.
Credit
The shared
_cache.pypattern (DiskCache,FetchResult, atomic0600 disk writes) was first proposed by @hwrdprkns in PR #36896.
This module generalises it with two additions:
deployments where the per-call disk overhead adds up)
budgets shared across many long-lived processes)
Testing