Skip to content

feat(secrets): add Proton Pass (pass://) secret source - #52621

Closed
jhbarnett wants to merge 1 commit into
NousResearch:mainfrom
jhbarnett:feat/protonpass-secret-source
Closed

feat(secrets): add Proton Pass (pass://) secret source#52621
jhbarnett wants to merge 1 commit into
NousResearch:mainfrom
jhbarnett:feat/protonpass-secret-source

Conversation

@jhbarnett

Copy link
Copy Markdown

What does this PR do?

Adds a Proton Pass secret source so provider credentials can be resolved at startup from pass://vault/item/field references via the official pass-cli — alongside the existing Bitwarden Secrets Manager source, and mirroring the 1Password source proposed in #36949 / #36896.

Users map environment-variable names to references under secrets.protonpass.env:

secrets:
  protonpass:
    enabled: true
    env:
      OPENAI_API_KEY: "pass://Private/OpenAI/api key"
      ANTHROPIC_API_KEY: "pass://Private/Anthropic/credential"

After ~/.hermes/.env loads, each reference is resolved and injected into os.environ — the same startup hook the Bitwarden source uses. It is startup-safe and fail-open: a missing binary, a login failure, a bad reference, or an empty value each surface a one-line warning and Hermes continues with whatever .env already had.

Why this approach

Two things make Proton Pass differ from the bws / op integrations, and drove the design:

  1. Persistent session, not a per-invocation token. pass-cli logs in once (pass-cli login, which reads PROTON_PASS_PERSONAL_ACCESS_TOKEN) and stores a session in a platform keyring. So Hermes reuses an existing session and only attempts a pass-cli login — token passed via the child env, never argv — when a resolve fails for an auth-shaped reason, then retries once. You can also establish the session out of band (e.g. in a container entrypoint) and leave the token unset.
  2. No single-value read command. Resolution goes through the documented run substitution: pass-cli run --no-masking -- <python> -c "<echo the env var>", with the reference placed in that env var. --no-masking is required (otherwise run replaces the value with <concealed by Proton Pass> on stdout). Hermes wraps its own Python interpreter rather than a POSIX builtin like printenv, so resolution is identical on Linux, macOS, and Windows.

Hermes never downloads pass-cli; it shells out to your already-installed, already-trusted CLI.

Related Issue

Relates to #36949 (1Password request, same pluggable interface). No dedicated Proton Pass issue yet.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change) — env_loader split into per-backend handlers

Changes Made

  • agent/secret_sources/protonpass.py (new) — reference validation, pass-cli run resolution, lazy pass-cli login + retry, allowlisted child env, in-process + disk cache, fail-open apply_protonpass_secrets().
  • hermes_cli/env_loader.py — refactor _apply_external_secret_sources into per-backend handlers (_apply_bitwarden, _apply_protonpass) each in its own guard, so a disabled/broken section can't abort startup or short-circuit the other sources; add a (from Proton Pass) origin label.
  • hermes_cli/config.pysecrets.protonpass defaults + a v30→31 seeding migration.
  • agent/secret_sources/__init__.py — document the new backend.
  • website/docs/user-guide/secrets/{index,protonpass}.md — docs.
  • tests/test_protonpass_secrets.py (new) + additions to tests/test_env_loader_secret_sources.py — hermetic, subprocess mocked.

Note on the cache substrate

This backend keeps its FetchResult / disk-cache plumbing self-contained (mirroring bitwarden.py on main) so it merges without depending on the unmerged shared agent/secret_sources/_cache.py from #36896. Happy to rebase onto that substrate once it lands.

How to Test

pytest tests/test_protonpass_secrets.py tests/test_env_loader_secret_sources.py -q

Live (optional): pass-cli login with a personal access token, add a secrets.protonpass block mapping an env var to a pass:// reference, then start any hermes command and confirm the one-line "Proton Pass: applied N secret(s)" status and that the value is present in the environment.

Checklist

Code

  • My commit messages follow Conventional Commits (feat(secrets): …)
  • My PR contains only changes related to this feature
  • I've run the relevant tests and they pass (78 passed: new suite + existing bitwarden/env_loader suites)
  • I've added tests for my changes
  • Tested on macOS; resolution is interpreter-wrapped for cross-platform parity

Documentation & Housekeeping

  • Updated docs (website/docs/user-guide/secrets/)
  • cli-config.yaml.example — N/A (it does not document the secrets: section; defaults live in config.py DEFAULT_CONFIG, consistent with the Bitwarden source)
  • Cross-platform impact considered (interpreter-wrapped resolve; Windows env vars in the child allowlist)

Resolve provider credentials at startup from Proton Pass
`pass://vault/item/field` references via the official `pass-cli`, alongside
the existing Bitwarden Secrets Manager source.

Users map env-var names to references under `secrets.protonpass.env`. After
`.env` loads, each reference is resolved with
`pass-cli run --no-masking -- <python> -c <echo>` (documented substitution
semantics; the wrapped interpreter keeps resolution cross-platform) and
injected into `os.environ`. Auth uses Proton Pass's persistent session: an
existing session is reused, and `pass-cli login` (consuming
`PROTON_PASS_PERSONAL_ACCESS_TOKEN` via the child env, never argv) is only
attempted on an auth-shaped failure, then retried once.

Fail-open and startup-safe: a missing binary, login failure, bad reference,
or empty value each surface a one-line warning and Hermes continues with
whatever `.env` already had. Successful complete pulls are cached in-process
and on disk (`<hermes_home>/cache/protonpass_cache.json`, atomic 0600, TTL);
only resolved values are stored, the token is fingerprinted into the cache
key. The `env_loader` hook is refactored into per-backend handlers so a
disabled Bitwarden section no longer short-circuits other sources.

Adds the backend module, config schema + migration (v31), docs, and
hermetic tests (subprocess mocked). Self-contained against main; can be
rebased onto the shared agent/secret_sources/_cache.py from NousResearch#36896 once it
lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have labels Jun 25, 2026
@jhbarnett jhbarnett closed this Jun 25, 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 comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard 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.

2 participants