# 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 `/data//`; 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/ {username, password, totp, uris, notes, ...} secret/metadata/agent/ 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//`) — the item id is the path relative to the prefix. Minimal AppRole policy: ```hcl path "secret/data/agent/*" { capabilities = ["read"] } path "secret/metadata/agent/*" { capabilities = ["read", "list"] } ``` ## Quick start ```bash 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 via `SO_PEERCRED`. No TCP listener. - Decrypted items exist only in daemon RAM, wiped after each use (`wipe_item`), auto-locked after `unlock_ttl_seconds`. - The ref index holds metadata only (id/type/name/uris/`agent.*` fields). On OpenBao it is built from `metadata/*` 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 ```bash 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.