Skip to content

feat(secrets): add 1Password SDK-based secret source (onepassword-sdk) - #39445

Closed
thesammykins wants to merge 2 commits into
NousResearch:mainfrom
thesammykins:feat/onepassword-sdk-secret-source
Closed

feat(secrets): add 1Password SDK-based secret source (onepassword-sdk)#39445
thesammykins wants to merge 2 commits into
NousResearch:mainfrom
thesammykins:feat/onepassword-sdk-secret-source

Conversation

@thesammykins

Copy link
Copy Markdown

Summary

Adds a 1Password Service Account secret source that uses the
onepassword-sdk Python
package instead of the op CLI daemon.

Why the SDK instead of the op CLI?
The op CLI'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.py substrate and
can coexist. Users who prefer the op CLI can use #36896; users on
macOS or who want in-session tools can use this SDK-based transport.

Fixes #36949.

What changed

New files

File Purpose
agent/secret_sources/_cache.py Shared cache substrate (TwoLayerCache, FetchResult, CachedFetch) — extracted from patterns first proposed in PR #36896, generalised with an in-process L1 layer and rate-limit cooldown support
agent/secret_sources/onepassword.py SDK-based 1Password backend
tools/onepassword_tool.py Four in-session tools (onepassword_list_vaults, onepassword_list_items, onepassword_get_item, onepassword_resolve_field)
tests/test_onepassword_secrets.py Unit + live-integration tests

Modified files

File Change
agent/secret_sources/__init__.py Docstring update
hermes_cli/env_loader.py Factor out _apply_bitwarden, add _apply_onepassword
hermes_cli/main.py Wire hermes secrets onepassword subparser
hermes_cli/secrets_cli.py Add register_onepassword_cli + all CLI handlers
toolsets.py Register onepassword toolset
cli-config.yaml.example Commented example for both backends

Key design decisions

Two mapping modes

  1. Explicit — list env-var → op:// references (like Bitwarden):

    secrets:
      onepassword:
        enabled: true
        env:
          OPENAI_API_KEY: "op://Private/OpenAI/api key"
  2. Auto-discovery — scan a vault and map credential fields → env vars:

    secrets:
      onepassword:
        enabled: true
        vault: "Private"
        auto_discover: true

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.

  • L1: in-process dict (instant, no I/O)
  • L2: disk-persisted JSON (shared across processes)
  • Rate-limit cooldown: when a 429 is hit, ALL processes back off
    for one hour, preventing N × retries call amplification

Fail-open

Missing SDK, expired token, bad reference, empty vault — everything
produces a one-line warning and Hermes continues with whatever
credentials .env already had. This backend cannot block startup.

In-session tools

Four tools registered under the onepassword toolset for mid-session
vault access. The onepassword_resolve_field tool uses a
vault-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.py pattern (DiskCache, FetchResult, atomic
0600 disk writes) was first proposed by @hwrdprkns in PR #36896.
This module generalises it with two additions:

  • An in-process L1 cache layer (critical for multi-process
    deployments where the per-call disk overhead adds up)
  • Rate-limit cooldown gating (critical for APIs with per-hour
    budgets shared across many long-lived processes)

Testing

# Unit tests (always run)
pytest tests/test_onepassword_secrets.py -v -k "not TestLive"

# Live integration tests (requires OP_SERVICE_ACCOUNT_TOKEN)
OP_SERVICE_ACCOUNT_TOKEN=*** pytest tests/test_onepassword_secrets.py -v

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
@thesammykins
thesammykins force-pushed the feat/onepassword-sdk-secret-source branch from 1bb8130 to 30aa126 Compare June 5, 2026 01:26
@thesammykins
thesammykins marked this pull request as ready for review June 5, 2026 01:27
@alt-glitch alt-glitch added type/feature New feature or request area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have labels Jun 5, 2026
… 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.
@teknium1

teknium1 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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 ctx.register_secret_source() with a conformance kit in tests/secret_sources/conformance.py — see https://hermes-agent.nousresearch.com/docs/user-guide/secrets/ .

@teknium1 teknium1 closed this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: 1Password (op://) secret source backend

3 participants