Files
wyndham-ARR/arr_storage/exchange.py
2026-07-29 16:38:05 +08:00

78 lines
2.5 KiB
Python

"""Opaque OSS exchange handles used between the trusted Agent runtime and ARR."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping, Optional
from arr_ingestion.contracts import OPAQUE_ID_RE
from arr_storage.contracts import BackendObject, valid_object_key
from arr_storage.remote import CloudClientError, CloudClientPort
def _prefix(value: str) -> str:
normalized = value.strip().strip("/")
probe = normalized + "/file_handle"
if not normalized or not valid_object_key(probe):
raise ValueError("Agent output exchange prefix is invalid")
return normalized
@dataclass(frozen=True)
class OutputExchangeConfig:
prefix: str = "arr-agent-outputs"
def __post_init__(self) -> None:
object.__setattr__(self, "prefix", _prefix(self.prefix))
@classmethod
def from_environment(
cls,
environment: Optional[Mapping[str, str]] = None,
) -> "OutputExchangeConfig":
values = environment if environment is not None else os.environ
return cls(str(values.get("ARR_AGENT_OUTPUT_PREFIX", "arr-agent-outputs")))
def object_key(self, file_handle: str) -> str:
if not OPAQUE_ID_RE.fullmatch(file_handle):
raise ValueError("Agent output file handle is invalid")
return f"{self.prefix}/{file_handle}"
class OutputExchangePublisher:
"""Runtime-side idempotent publisher for one opaque output handle."""
def __init__(self, client: CloudClientPort, config: OutputExchangeConfig) -> None:
self._client = client
self._config = config
def publish(
self,
*,
file_handle: str,
source: Path,
mime_type: str,
metadata: Mapping[str, str],
) -> BackendObject:
object_key = self._config.object_key(file_handle)
try:
return self._client.upload_file_if_absent(
object_key,
str(source),
mime_type,
metadata,
)
except CloudClientError as error:
if error.kind != "conflict":
raise
existing = self._client.stat_object(object_key)
if (
existing.byte_size != int(metadata.get("arr-byte-size", "-1"))
or existing.metadata.get("arr-sha256") != metadata.get("arr-sha256")
or existing.metadata.get("arr-mime-type") != mime_type
):
raise CloudClientError("conflict") from None
return existing