"""Broker-only refresh of an owned draft PR's title and body.""" from __future__ import annotations import base64 import json import urllib.request from typing import Any import deadline_http from gitea_api import CANONICAL_BASE_URL from gitea_api_policy import PolicyError, _draft_title, _validate_body, _validate_pr_number def request_fields(value: dict[str, Any], token: str) -> tuple[str, int, str, str]: if set(value) != {"grant", "pr_number", "title", "body"} or not isinstance(value["grant"], str): raise PolicyError("draft update fields are invalid") return value["grant"], _validate_pr_number(value["pr_number"]), _draft_title(value["title"], forbidden=(token,)), _validate_body(value["body"], forbidden=(token,)) def matches_pull(value: Any, claims: dict[str, Any], number: int) -> None: if not isinstance(value, dict) or _validate_pr_number(value.get("number")) != number: raise PolicyError("draft pull request is invalid") head, base = value.get("head"), value.get("base") if not isinstance(head, dict) or not isinstance(base, dict): raise PolicyError("draft pull request is invalid") repo, base_repo = head.get("repo"), base.get("repo") name = repo.get("full_name") if isinstance(repo, dict) else "" base_name = base_repo.get("full_name") if isinstance(base_repo, dict) else "" if value.get("state") != "open" or head.get("ref") != claims["ref"] or head.get("sha") != claims["new_head"] or base.get("ref") != claims["base"] or name != f"titan/{claims['repo']}" or base_name != f"titan/{claims['repo']}": raise PolicyError("draft pull request does not match its task grant") def update(token: str, repo: str, number: int, title: str, body: str) -> bytes: """PATCH only safe draft prose; caller already authenticated ownership.""" payload = json.dumps({"title": title, "body": body}, separators=(",", ":")).encode() auth = base64.b64encode(f"hermes-automation:{token}".encode()).decode() request = urllib.request.Request( f"{CANONICAL_BASE_URL}/api/v1/repos/titan/{repo}/pulls/{number}", payload, method="PATCH", headers={"Authorization": f"Basic {auth}", "Content-Type": "application/json", "Accept": "application/json"}, ) with deadline_http.open_bounded(request, maximum=2 * 1024 * 1024, timeout=30) as response: if getattr(response, "status", None) != 200 or response.headers.get_content_type() != "application/json": raise PolicyError("draft update upstream response is invalid") result = response.read(2 * 1024 * 1024 + 1) if len(result) > 2 * 1024 * 1024 or token.encode() in result: raise PolicyError("draft update upstream response is invalid") json.loads(result) return result