Files
me be87900794 agent-credential-broker: fork of vaultwarden-agent-broker + OpenBao backend
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.
2026-10-14 13:27:33 -06:00

119 lines
4.1 KiB
Python

"""agent-broker command line.
agent-broker daemon run the unix-socket daemon (foreground)
agent-broker status
agent-broker unlock interactive, TTY, no echo
(or non-interactive when
master_password_file is configured)
agent-broker lock
agent-broker confirm <ticket>
The master password is read with getpass() by default -- it never arrives
via argv or env. If config sets master_password_file, unlock reads it from
that file instead (600 perms + same-owner enforced by read_secret_file).
"""
from __future__ import annotations
import argparse
import getpass
import json
import os
import sys
from . import __version__
from .config import Config, read_secret_file, socket_path
from .errors import BrokerError
from .server import call, serve_forever
def _out(obj):
print(json.dumps(obj, indent=2, sort_keys=True))
def main(argv=None):
ap = argparse.ArgumentParser(prog="agent-broker")
ap.add_argument("--version", action="version", version=__version__)
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("daemon", help="run the unix-socket daemon in the foreground")
sub.add_parser("status")
sub.add_parser("unlock", help="interactive unlock (TTY, no echo)")
sub.add_parser("lock")
sub.add_parser("logout", help="lock and log out the vault backend")
c = sub.add_parser("confirm", help="approve a pending confirmation ticket")
c.add_argument("ticket")
sub.add_parser("doctor", help="check installation prerequisites")
l = sub.add_parser("list-refs")
args = ap.parse_args(argv)
if args.cmd == "daemon":
serve_forever()
return 0
try:
if args.cmd == "status":
_out(call("status"))
elif args.cmd == "unlock":
mpf = Config.load().master_password_file
if mpf:
password = read_secret_file(mpf)
else:
if not (sys.stdin.isatty() and sys.stderr.isatty()):
raise BrokerError("DENIED", "unlock requires an interactive TTY")
password = getpass.getpass("Master Password: ", stream=sys.stderr)
if not password:
raise BrokerError("DENIED", "empty password")
try:
_out(call("unlock", {"password": password}))
finally:
password = None
elif args.cmd == "lock":
_out(call("lock"))
elif args.cmd == "logout":
_out(call("logout"))
elif args.cmd == "confirm":
_out(call("confirm", {"ticket": args.ticket}))
elif args.cmd == "list-refs":
_out(call("list_refs"))
elif args.cmd == "doctor":
_out(doctor())
except BrokerError as e:
out = {"ok": False, "error": e.to_dict()}
if getattr(e, "ticket", None):
out["ticket"] = e.ticket
_out(out)
return 1
return 0
def doctor():
cfg = Config.load()
import shutil
checks = {
"socket": str(socket_path()),
"vault_backend": cfg.vault_backend,
"agent_browser": bool(shutil.which("agent-browser")),
"git": bool(shutil.which("git")),
"ssh": bool(shutil.which("ssh")),
# bitwarden-cli backend
"vault_url": cfg.vault_url,
"bw": bool(shutil.which("bw") or (cfg.bw_path and os.path.exists(cfg.bw_path))),
"client_id_file": cfg.client_id_file.exists(),
"client_secret_file": cfg.client_secret_file.exists(),
# openbao backend
"openbao_addr": cfg.openbao_addr,
"openbao_role_id_file": cfg.openbao_role_id_file.exists(),
"openbao_secret_id_file": cfg.openbao_secret_id_file.exists(),
"openbao_token_file": cfg.openbao_token_file.exists(),
"openbao_token_env": bool(os.environ.get(cfg.openbao_token_env)),
}
try:
from .backends import make_backend
checks["configured"] = make_backend(cfg).configured()
except BrokerError:
checks["configured"] = False
return checks
if __name__ == "__main__":
sys.exit(main())