Skip to content
144 changes: 126 additions & 18 deletions gateway/scripts/amw_approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,27 @@ def _refusal_reason(base_url: str) -> str | None:
return f"non-loopback http host {host!r}"


def _has_control_chars(value: str) -> bool:
"""True when value holds C0, DEL, or C1 controls."""
for ch in value:
code = ord(ch)
if code < 0x20 or code == 0x7F or 0x80 <= code <= 0x9F:
return True
return False


def _read_api_key() -> str:
"""First key from API_KEY_ENV; empty when unset or blank."""
"""First key from API_KEY_ENV; empty when unset or blank.

Raises ValueError with a fixed message when the key holds control
characters. The message never includes the value.
"""
for entry in os.environ.get(API_KEY_ENV, "").split(","):
if entry.strip():
return entry.strip()
candidate = entry.strip()
if candidate:
if _has_control_chars(candidate):
raise ValueError("invalid API key in VALID_API_KEYS")
Comment thread
PetrefiedThunder marked this conversation as resolved.
return candidate
return ""


Expand All @@ -149,34 +165,87 @@ def _client(base_url: str, api_key: str) -> httpx.Client:


def _format_time(value: float) -> str:
if isinstance(value, bool):
raise TypeError("bool is not a valid deadline")
return datetime.fromtimestamp(value, tz=timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)


def _is_pending_item(item: object) -> bool:
"""True when item has the keys cmd_list prints, with a renderable deadline."""
if not isinstance(item, dict):
return False
if "nonce" not in item or "tool" not in item or "principal" not in item:
return False
deadline = item.get("deadline")
if not isinstance(deadline, (int, float)):
return False
try:
_format_time(deadline)
except (ValueError, OverflowError, OSError, TypeError):
return False
return True


def cmd_list(args: argparse.Namespace) -> int:
try:
response = _client(args.base_url, args.api_key).get("/v1/approvals/pending")
response.raise_for_status()
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
except ValueError:
print("error: refusing to send the API key", file=sys.stderr)
return 1
except httpx.InvalidURL:
print("error: invalid gateway URL", file=sys.stderr)
return 1
except httpx.HTTPStatusError as exc:
print(
f"gateway request failed: HTTP {exc.response.status_code}",
file=sys.stderr,
)
return 1
except httpx.InvalidURL as exc:
print(f"error: invalid gateway URL: {exc}", file=sys.stderr)
except httpx.TransportError as exc:
print(
f"gateway unreachable at {args.base_url} ({type(exc).__name__})",
file=sys.stderr,
)
return 1
except httpx.HTTPError as exc:
print(
f"error: gateway not reachable at {args.base_url}: {exc}", file=sys.stderr
f"error: gateway request failed ({type(exc).__name__})",
file=sys.stderr,
)
return 1
approvals = response.json().get("approvals", [])
try:
payload = response.json()
except ValueError:
# ValueError covers json.JSONDecodeError from a non-JSON body.
print("gateway returned a non-JSON response", file=sys.stderr)
return 1
if not isinstance(payload, dict):
print("gateway returned an unexpected response", file=sys.stderr)
return 1
if "approvals" not in payload:
print("gateway returned an unexpected response", file=sys.stderr)
return 1
approvals = payload["approvals"]
if not isinstance(approvals, list) or any(
not _is_pending_item(item) for item in approvals
):
print("gateway returned an unexpected response", file=sys.stderr)
return 1
if not approvals:
print("no pending approvals")
return 0
for item in approvals:
try:
rendered = _format_time(item["deadline"])
except (ValueError, OverflowError, OSError, TypeError):
print("gateway returned an unexpected response", file=sys.stderr)
return 1
print(
f"{item['nonce']} tool={item['tool']} principal={item['principal']} "
f"deadline={_format_time(item['deadline'])}"
f"deadline={rendered}"
)
return 0

Expand All @@ -186,15 +255,28 @@ def _decide(args: argparse.Namespace, action: str) -> int:
response = _client(args.base_url, args.api_key).post(
f"/v1/approvals/{args.nonce}/{action}"
)
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
except ValueError:
print("error: refusing to send the API key", file=sys.stderr)
return 1
except httpx.InvalidURL:
print("error: invalid gateway URL", file=sys.stderr)
return 1
except httpx.HTTPStatusError as exc:
print(
f"gateway request failed: HTTP {exc.response.status_code}",
file=sys.stderr,
)
return 1
except httpx.InvalidURL as exc:
print(f"error: invalid gateway URL: {exc}", file=sys.stderr)
except httpx.TransportError as exc:
print(
f"gateway unreachable at {args.base_url} ({type(exc).__name__})",
file=sys.stderr,
)
return 1
except httpx.HTTPError as exc:
print(
f"error: gateway not reachable at {args.base_url}: {exc}", file=sys.stderr
f"error: gateway request failed ({type(exc).__name__})",
file=sys.stderr,
)
return 1
if response.status_code == 404:
Expand All @@ -209,10 +291,32 @@ def _decide(args: argparse.Namespace, action: str) -> int:
return 1
try:
response.raise_for_status()
except httpx.HTTPStatusError as exc:
print(
f"gateway request failed: HTTP {exc.response.status_code}",
file=sys.stderr,
)
return 1
except httpx.HTTPError as exc:
print(f"error: {exc}", file=sys.stderr)
print(
f"error: gateway request failed ({type(exc).__name__})",
file=sys.stderr,
)
return 1
try:
payload = response.json()
except ValueError:
# ValueError covers json.JSONDecodeError from a non-JSON body.
print("gateway returned a non-JSON response", file=sys.stderr)
return 1
if not isinstance(payload, dict):
print("gateway returned an unexpected response", file=sys.stderr)
return 1
status = payload.get("status")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if not isinstance(status, str) or not status:
print("gateway returned an unexpected response", file=sys.stderr)
return 1
print(f"{args.nonce}: {response.json().get('status')}")
print(f"{args.nonce}: {status}")
return 0


Expand Down Expand Up @@ -247,7 +351,11 @@ def main(argv: list[str] | None = None) -> int:
file=sys.stderr,
)
return 1
api_key = _read_api_key()
try:
api_key = _read_api_key()
except ValueError:
print("error: invalid API key in VALID_API_KEYS", file=sys.stderr)
return 1
if not api_key:
print(
"error: VALID_API_KEYS is not set; export the gateway "
Expand Down
Loading
Loading