133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
from typing import Any, Callable
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from .models import Decision
|
|
|
|
|
|
GATEWAY_DECISION_URL = "http://127.0.0.1:18920/v1/orchestrator/decisions"
|
|
SECRET_REF_RE = re.compile(r"^secret://[A-Za-z0-9._/-]{1,480}$")
|
|
|
|
|
|
class CallbackBridgeError(RuntimeError):
|
|
def __init__(self, category: str, status_code: int) -> None:
|
|
super().__init__(category)
|
|
self.category = category
|
|
self.status_code = status_code
|
|
|
|
|
|
def _trusted_resolver(path: Path) -> Path:
|
|
if not path.is_absolute():
|
|
raise CallbackBridgeError("CALLBACK_CONFIGURATION_UNAVAILABLE", 503)
|
|
try:
|
|
info = path.lstat()
|
|
resolved = path.resolve(strict=True)
|
|
except OSError as exc:
|
|
raise CallbackBridgeError("CALLBACK_CONFIGURATION_UNAVAILABLE", 503) from exc
|
|
if (
|
|
resolved != path
|
|
or stat.S_ISLNK(info.st_mode)
|
|
or not stat.S_ISREG(info.st_mode)
|
|
or info.st_uid != 0
|
|
or stat.S_IMODE(info.st_mode) & 0o022
|
|
or not stat.S_IMODE(info.st_mode) & 0o111
|
|
):
|
|
raise CallbackBridgeError("CALLBACK_CONFIGURATION_UNAVAILABLE", 503)
|
|
return resolved
|
|
|
|
|
|
def resolve_secret_ref(secret_ref: str, resolver: Path) -> str:
|
|
if not isinstance(secret_ref, str) or not SECRET_REF_RE.fullmatch(secret_ref):
|
|
raise CallbackBridgeError("CALLBACK_CONFIGURATION_UNAVAILABLE", 503)
|
|
executable = _trusted_resolver(resolver)
|
|
request = json.dumps(
|
|
{"operation": "resolve", "secret_ref": secret_ref},
|
|
separators=(",", ":"),
|
|
sort_keys=True,
|
|
).encode()
|
|
try:
|
|
result = subprocess.run(
|
|
[str(executable)],
|
|
input=request,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"},
|
|
timeout=5,
|
|
check=False,
|
|
)
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
raise CallbackBridgeError("CALLBACK_CREDENTIAL_UNAVAILABLE", 503) from exc
|
|
token = result.stdout.removesuffix(b"\n")
|
|
if (
|
|
result.returncode != 0
|
|
or not token
|
|
or len(token) > 16_384
|
|
or b"\n" in token
|
|
or b"\r" in token
|
|
):
|
|
raise CallbackBridgeError("CALLBACK_CREDENTIAL_UNAVAILABLE", 503)
|
|
try:
|
|
return token.decode("utf-8")
|
|
except UnicodeDecodeError as exc:
|
|
raise CallbackBridgeError("CALLBACK_CREDENTIAL_UNAVAILABLE", 503) from exc
|
|
|
|
|
|
def submit_orchestration_decision(
|
|
payload: dict[str, Any],
|
|
*,
|
|
contract_commit: str,
|
|
secret_ref: str,
|
|
resolver: Path,
|
|
request_open: Callable[..., Any] = urllib.request.urlopen,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
decision = Decision.from_dict(payload)
|
|
except Exception as exc:
|
|
raise CallbackBridgeError("INVALID_DECISION", 422) from exc
|
|
if decision.contract_commit != contract_commit:
|
|
raise CallbackBridgeError("CONTRACT_MISMATCH", 409)
|
|
token = resolve_secret_ref(secret_ref, resolver)
|
|
request = urllib.request.Request(
|
|
GATEWAY_DECISION_URL,
|
|
data=json.dumps(
|
|
decision.to_dict(), separators=(",", ":"), sort_keys=True
|
|
).encode(),
|
|
method="POST",
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
"Idempotency-Key": decision.event_id,
|
|
"User-Agent": "aios-chatgpt-actions/decision-bridge-v1",
|
|
},
|
|
)
|
|
try:
|
|
with request_open(request, timeout=10) as response:
|
|
raw = response.read(65_537)
|
|
if response.status != 202 or len(raw) > 65_536:
|
|
raise CallbackBridgeError("CALLBACK_HTTP_ERROR", 502)
|
|
except urllib.error.HTTPError as exc:
|
|
if exc.code == 409:
|
|
raise CallbackBridgeError("STALE_STATE", 409) from exc
|
|
raise CallbackBridgeError("CALLBACK_HTTP_ERROR", 502) from exc
|
|
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
raise CallbackBridgeError("CALLBACK_NETWORK_ERROR", 502) from exc
|
|
try:
|
|
value = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise CallbackBridgeError("CALLBACK_RESPONSE_INVALID", 502) from exc
|
|
if (
|
|
not isinstance(value, dict)
|
|
or value.get("accepted") is not True
|
|
or not isinstance(value.get("state_version"), int)
|
|
):
|
|
raise CallbackBridgeError("CALLBACK_RESPONSE_INVALID", 502)
|
|
return {"accepted": True, "state_version": value["state_version"]}
|