#!/usr/bin/env python3 """Call Firefox Agent Bridge with a registered Ed25519 client key.""" from __future__ import annotations import argparse import json import os import sys import urllib.error import urllib.request from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from fab_auth import load_key_bundle, sign_headers # noqa: E402 DEFAULT_BASE = os.environ.get("FAB_URL", "http://127.0.0.1:17634") def key_path() -> Path: env = os.environ.get("FAB_KEY_FILE") if env: return Path(env) raise SystemExit( "no client key: set FAB_KEY_FILE or pass --key " "(python tools/register_client.py add --name NAME --write-key PATH)" ) def call( method: str, args: list[Any] | None = None, base: str = DEFAULT_BASE, key_file: Path | None = None, ) -> Any: payload = json.dumps({"method": method, "args": args or []}).encode("utf-8") path = "/v1/call" bundle = load_key_bundle(key_file or key_path()) headers = { "Content-Type": "application/json", **sign_headers(bundle, "POST", path, payload), } req = urllib.request.Request( base.rstrip("/") + path, data=payload, method="POST", headers=headers, ) try: with urllib.request.urlopen(req, timeout=20) as resp: body = json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace") raise SystemExit(f"bridge HTTP {exc.code}: {detail}") from exc except urllib.error.URLError as exc: raise SystemExit( f"bridge unreachable at {base} ({exc}). " "Firefox must be open with the extension loaded." ) from exc if not body.get("ok"): raise SystemExit(body.get("error") or body) return body.get("result") def main() -> int: parser = argparse.ArgumentParser(description="Call Firefox Agent Bridge") parser.add_argument("method", help="e.g. bookmarks.search or meta.methods") parser.add_argument("args_json", nargs="?", default="[]", help="JSON array of arguments") parser.add_argument("--url", default=DEFAULT_BASE) parser.add_argument("--key", type=Path, help="client key bundle (or FAB_KEY_FILE)") ns = parser.parse_args() args = json.loads(ns.args_json) if not isinstance(args, list): raise SystemExit("args_json must be a JSON array") print(json.dumps(call(ns.method, args, base=ns.url, key_file=ns.key), indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())