Mechanical rename (package agent_credential_broker; entry points
agent-broker{,-mcp,-plugin}; env AGENT_BROKER_*; paths agent-broker;
agent-browser credential provider 'broker'; MCP server name agent-broker).
New backend 'openbao': stdlib-only HTTP client for OpenBao/Vault KV v2
(approle login or token file/env; metadata-only ref index; lazy re-login
on 401/403; token renewal). bitwarden-cli backend kept as transition.
agent-credential-broker
Capability layer between AI agents and a credential vault: agents use credentials, they can never read them.
Forked from the Vaultwarden-era vaultwarden-agent-broker (same socket
protocol, MCP surface, policy model and adapters; renamed package,
binaries, env prefix and paths). The design rationale for the backend
split lives in backlog/02_password_manager/broker-design.md.
Backends
| Backend | Status | Auth | Notes |
|---|---|---|---|
openbao |
target backend | AppRole (role_id/secret_id files, 0600) or direct token (0600 file / AGENT_BROKER_OPENBAO_TOKEN) |
KV v2 at <mount>/data/<prefix>/<ref>; the ref index is built only from metadata/*, so unlock never transits plaintext |
bitwarden-cli |
transition | bw subprocess, API key + master password |
unchanged; kept for the migration window, will be dropped with the old vault |
Select with vault_backend in the config JSON.
Item contract (OpenBao)
secret/data/agent/<ref> {username, password, totp, uris, notes, ...}
secret/metadata/agent/<ref> custom_metadata:
agent.ref, agent.adapters, agent.allowed_urls,
agent.allowed_hosts, agent.auth_type,
agent.confirmation, agent.*_selector,
agent.ssh_user, agent.ssh_key_path,
agent.cli_profile, agent.insecure_http
label (display name), uris (csv or JSON list)
agent.ref is the stable, unique, exact-match reference. Every
agent.* field and its effect is documented in
agent_credential_broker/policy.py. Paths may nest
(agent/<sub>/<ref>) — the item id is the path relative to the prefix.
Minimal AppRole policy:
path "secret/data/agent/*" { capabilities = ["read"] }
path "secret/metadata/agent/*" { capabilities = ["read", "list"] }
Quick start
pip3 install --user -e .
# 1. credentials dir + AppRole files (or openbao-token.txt)
install -d -m 700 ~/.config/devin/credentials
# openbao-role-id.txt / openbao-secret-id.txt (600)
# 2. config -- ~/.config/agent-broker/config.json (or AGENT_BROKER_CONFIG)
# {
# "vault_backend": "openbao",
# "openbao_addr": "http://127.0.0.1:18200",
# "openbao_mount": "secret",
# "openbao_prefix": "agent",
# "openbao_role_id_file": "~/.config/devin/credentials/openbao-role-id.txt",
# "openbao_secret_id_file": "~/.config/devin/credentials/openbao-secret-id.txt",
# "openbao_ca_cert": "/path/ca.pem", # optional, https only
# "openbao_tls_skip_verify": false, # dev only
# "openbao_namespace": null, # -> X-Vault-Namespace
# "unlock_ttl_seconds": 3600
# }
# 3. daemon (unix socket only)
agent-broker daemon & # or: make service-install
# 4. unlock -- openbao ignores the password value (non-interactive by
# design, the 0600 files are the credential); enter anything or set
# master_password_file for unattended unlock
agent-broker unlock
# 5. refs only
agent-broker list-refs
agent-broker status
agent-broker lock
Interfaces
| Consumer | Entry point |
|---|---|
| Agent (MCP) | agent-broker-mcp (stdio, server name agent-broker) |
| agent-browser | --credential-provider broker -> agent-broker-plugin |
| Local human | agent-broker CLI (daemon / status / unlock / lock / confirm) |
Threat model (unchanged)
- Unix socket only (
$XDG_RUNTIME_DIR/agent-broker/broker.sock, 0600, dir 0700), same-UID enforced viaSO_PEERCRED. No TCP listener. - Decrypted items exist only in daemon RAM, wiped after each use
(
wipe_item), auto-locked afterunlock_ttl_seconds. - The ref index holds metadata only (id/type/name/uris/
agent.*fields). On OpenBao it is built frommetadata/*endpoints — no plaintext transit at all. - The agent-facing surface (MCP tools, socket methods) is a static
allowlist; a startup assert forbids any tool named like a secret
reader (
get_*,reveal,dump, ...). - Adapters inject secrets via child env / askpass-on-tmpfs / the
agent-browser plugin — never argv — and every byte that crosses the
boundary is sanitized (
sanitize.py). - Same-UID local malware remains out of scope: it can read the 0600 credential files directly. The boundary is the agent, not the host.
Tests
make test # or: python -m pytest tests/ -x -q
make lint
All tests are offline fixtures (tests/fakebw.py, tests/fake_bao.py)
— no real vault, no real credentials.