OpenHands / OpenHands/software-agent-sdk
Support OS keyring for secret storage
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Feature request: store LLM/provider secrets in the OS keyring
Design comparison:
Secrets: keyring references vs. encrypted-at-rest
Problem
Today the SDK persists secrets (LLM api_key, aws_secret_access_key, etc.) via
SecretStr + a Fernet cipher (utils/cipher.py, utils/pydantic_secrets.py): plaintext
in memory, and either plaintext or encrypted-at-rest when serialized. Encrypted-at-rest
still means the key material — plus the cipher key to unlock it — lives on disk. It's a lot
of machinery to make on-disk secrets a bit less bad.
Proposal
Add a small, pluggable SecretStore abstraction and persist only a reference
({service, account}) to a secret, not the value. Resolve the real value from the OS
keyring at use time. Provider-scoped keys by default, with per-profile overrides.
SecretStore { get(ref), set(ref, value), has(ref), delete(ref) }
SecretRef { service = "openhands", account = "llm-provider:openai" }
Config/settings serialize the ref; the value never touches settings files.
OS support
| OS | Backend | How | Status |
|---|---|---|---|
| macOS | Keychain | security CLI (find/add/delete-generic-password) |
✅ works, no daemon |
| Windows | Credential Manager | keyring (wincred) / native |
✅ works on desktop |
| Linux desktop | Secret Service (libsecret) | GNOME Keyring / KWallet over D-Bus | ✅ only if a D-Bus session + unlocked keyring is present |
| Linux headless / server | — | no Secret Service by default | ⚠️ needs kernel keyring (keyctl, ephemeral) or a fallback |
| Docker container | — | no Keychain, no D-Bus/gnome-keyring | ❌ OS keyring not available out of the box |
The Docker question (and whether we can drop the old code)
We can't rely on OS keyring inside a stock container — there's no keychain and no
D-Bus session. But the right answer there isn't "keep the encrypted-file store." It's:
- Host / desktop (macOS, Windows, Linux-with-session): OS keyring is the primary store.
On these, the plaintext + Fernet path can be dropped. - Containers / headless: inject secrets at runtime (env vars / mounted Docker
secrets / vault sidecar) and never persist them to disk in the container. If nothing
is persisted, there's nothing to encrypt — so the Fernet machinery isn't needed here either.
So the encrypted-at-rest cipher is only "needed" for the worst case: persisting secrets to
disk in an environment that has neither a keyring nor runtime injection. That case
should be discouraged, not designed for.
Recommendation: land the SecretStore interface with (1) OS-keyring backends per the
table and (2) an env/injected backend for containers. Then the plaintext + Fernet
encrypted-at-rest code can be removed. Keep the old path only if we confirm a real
deployment needs on-disk persistence with no keyring and no injection.
Prior art
A fresh TypeScript transpilation of this SDK already implements exactly this
(SecretStore + SecretRef, keyring-only, macOS Keychain backend, provider/profile
scoping). Happy to share a design comparison.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading utils/cipher.py and utils/pydantic_secrets.py to map the current persistence path. Compare the requested SecretStore and SecretRef design with the referenced TypeScript implementation and determine the supported host and container flows. Done means the scope, backends, runtime injection behavior, and fate of the Fernet path are resolved and covered by tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, python
- Domain
- backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100