#!/usr/bin/env python3 """Mint, verify, and revoke a coding-agent AppRole token without printing it.""" from __future__ import annotations import argparse import json import os import subprocess import sys import urllib.error import urllib.request from typing import Any DEFAULT_ADDR = "https://bao.coulomb.social" DATA_PATH = "platform/data/workloads/issue-core/issue-core/issue-core-runtime" METADATA_PATH = ( "platform/metadata/workloads/issue-core/issue-core/issue-core-runtime" ) EXPECTED_POLICIES = { "agent-high-risk-boundary", "workload-kv-read-issue-core-runtime", } def bao_json(args: list[str]) -> dict[str, Any]: completed = subprocess.run( ["bao", *args], check=True, capture_output=True, text=True, timeout=30, ) return json.loads(completed.stdout) def api_request( addr: str, method: str, path: str, *, token: str | None = None, payload: dict[str, Any] | None = None, ) -> dict[str, Any]: headers = {"Content-Type": "application/json"} if token: headers["X-Vault-Token"] = token body = json.dumps(payload).encode() if payload is not None else None request = urllib.request.Request( f"{addr.rstrip('/')}/v1/{path}", data=body, headers=headers, method=method ) with urllib.request.urlopen(request, timeout=30) as response: content = response.read() return json.loads(content) if content else {} def verify(role: str, addr: str) -> dict[str, Any]: role_payload = bao_json( ["read", "-format=json", f"auth/approle/role/{role}/role-id"] ) role_id = role_payload["data"]["role_id"] secret_payload = bao_json( ["write", "-format=json", "-f", f"auth/approle/role/{role}/secret-id"] ) secret_id = secret_payload["data"]["secret_id"] agent_token: str | None = None report: dict[str, Any] = {} try: login = api_request( addr, "POST", "auth/approle/login", payload={"role_id": role_id, "secret_id": secret_id}, ) agent_token = login["auth"]["client_token"] lookup = api_request(addr, "GET", "auth/token/lookup-self", token=agent_token) capabilities = api_request( addr, "POST", "sys/capabilities-self", token=agent_token, payload={"paths": [DATA_PATH, METADATA_PATH]}, ) policies = set(lookup["data"].get("policies", [])) ttl = int(lookup["data"].get("ttl", 0)) data_caps = capabilities.get(DATA_PATH, []) metadata_caps = capabilities.get(METADATA_PATH, []) errors: list[str] = [] if policies != EXPECTED_POLICIES: errors.append(f"unexpected policies: {sorted(policies)}") if ttl <= 0 or ttl > 900: errors.append(f"TTL outside 1..900 seconds: {ttl}") if data_caps != ["deny"]: errors.append(f"data capabilities are not deny: {data_caps}") if "read" not in metadata_caps or "deny" in metadata_caps: errors.append(f"metadata capabilities are not read-only: {metadata_caps}") report = { "role": role, "policies": sorted(policies), "ttl": ttl, "data_capabilities": data_caps, "metadata_capabilities": metadata_caps, "deny_wins": data_caps == ["deny"], "errors": errors, "ok": not errors, } finally: if agent_token: try: api_request(addr, "POST", "auth/token/revoke-self", token=agent_token) except (OSError, urllib.error.HTTPError): report.setdefault("errors", []).append("test token self-revocation failed") report["ok"] = False return report def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--role", default="coding-agent-railiance-platform") parser.add_argument("--addr", default=os.environ.get("BAO_ADDR", DEFAULT_ADDR)) args = parser.parse_args() try: report = verify(args.role, args.addr) except ( KeyError, json.JSONDecodeError, OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired, urllib.error.HTTPError, ) as exc: report = {"role": args.role, "ok": False, "errors": [str(exc)]} print(json.dumps(report, indent=2, sort_keys=True)) return 0 if report["ok"] else 1 if __name__ == "__main__": raise SystemExit(main())