atlas-iac/services/hermes/scm-common/scripts/scm_broker_client.py
jenkins 578239a496 hermes: bound SCM control calls with hard deadlines
Run every Gitea API and broker control exchange inside a killable helper
process whose connect, send, and read share one absolute wall-clock
deadline, and add a watchdog that force-closes streaming connections at
expiry. Redirects are rejected before authentication headers can move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 15:15:35 -03:00

94 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Call the credential-isolated Hermes SCM broker over its fixed cluster origin."""
from __future__ import annotations
import json
import urllib.request
from collections.abc import Callable
import deadline_http
from gitea_api_policy import PolicyError
BROKER_ORIGIN = "http://hermes-scm-broker.hermes-scm.svc.cluster.local:9081"
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
def _open(request: urllib.request.Request, timeout: int):
"""Open one broker exchange under a killable absolute deadline.
The helper process keeps every request on its fixed in-cluster origin by
rejecting redirects before they are followed.
"""
return deadline_http.open_bounded(
request, maximum=MAX_RESPONSE_BYTES, timeout=timeout
)
def request(
endpoint: str,
payload: dict[str, object],
*,
opener: Callable[..., object] = _open,
) -> bytes:
"""Send one bounded broker operation without any repository credential."""
if endpoint not in {"/v1/metadata", "/v1/drafts"}:
raise PolicyError("SCM broker operation is outside the client allowlist")
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
if len(body) > 64 * 1024:
raise PolicyError("SCM broker request exceeds the safe size limit")
outgoing = urllib.request.Request(
BROKER_ORIGIN + endpoint,
data=body,
method="POST",
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "hermes-scm-broker-client/1",
},
)
with opener(outgoing, timeout=30) as response: # type: ignore[attr-defined]
status = getattr(response, "status", None)
if status is None and hasattr(response, "getcode"):
status = response.getcode() # type: ignore[attr-defined]
if status != 200:
raise PolicyError("SCM broker returned an unexpected HTTP status")
content_type = response.headers.get_content_type() # type: ignore[attr-defined]
if content_type != "application/json":
raise PolicyError("SCM broker returned an unexpected response type")
result = response.read(MAX_RESPONSE_BYTES + 1) # type: ignore[attr-defined]
if len(result) > MAX_RESPONSE_BYTES:
raise PolicyError("SCM broker response exceeds the safe size limit")
json.loads(result)
return result
def read(path: str, *, opener: Callable[..., object] = _open) -> bytes:
"""Read explicitly allowed Atlas metadata through the broker."""
return request("/v1/metadata", {"path": path}, opener=opener)
def create_draft(
repo: str,
*,
base: str,
head: str,
head_sha: str,
title: str,
body: str,
opener: Callable[..., object] = _open,
) -> bytes:
"""Create one verified draft through the broker for human review."""
return request(
"/v1/drafts",
{
"base": base,
"body": body,
"head": head,
"head_sha": head_sha,
"repo": repo,
"title": title,
},
opener=opener,
)