feat: prepare ARR for controlled public deployment
This commit is contained in:
77
arr_storage/exchange.py
Normal file
77
arr_storage/exchange.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user