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.
119 lines
4.1 KiB
Python
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())
|