78 lines
2.5 KiB
Python
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
|