"""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 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())