"""Environment-only configuration for authenticated Agent result writeback.""" from __future__ import annotations import base64 import binascii import os from dataclasses import dataclass from typing import Mapping, Optional from arr_ingestion.contracts import OPAQUE_ID_RE from arr_processing.errors import ProcessingError from arr_processing.signatures import SignedResultCodec @dataclass(frozen=True) class ResultVerificationConfig: key_id: str secret: bytes max_age_seconds: int = 600 @classmethod def from_environment( cls, environment: Optional[Mapping[str, str]] = None, ) -> "ResultVerificationConfig": values = environment if environment is not None else os.environ key_id = str(values.get("ARR_AGENT_RESULT_HMAC_KEY_ID", "")).strip() encoded = str(values.get("ARR_AGENT_RESULT_HMAC_KEY_B64", "")).strip() age_text = str(values.get("ARR_AGENT_RESULT_MAX_AGE_SECONDS", "600")).strip() if not OPAQUE_ID_RE.fullmatch(key_id): raise ProcessingError( "PROCESSING_SIGNATURE_CONFIG_MISSING", "Agent result signing key identifier is missing", ) try: secret = base64.b64decode(encoded, validate=True) max_age = int(age_text) except (binascii.Error, ValueError): raise ProcessingError( "PROCESSING_SIGNATURE_CONFIG_INVALID", "Agent result signing configuration is invalid", ) from None if len(secret) < 32 or not 1 <= max_age <= 3600: raise ProcessingError( "PROCESSING_SIGNATURE_CONFIG_INVALID", "Agent result signing configuration is invalid", ) return cls(key_id=key_id, secret=secret, max_age_seconds=max_age) def verifier(self) -> SignedResultCodec: return SignedResultCodec( {self.key_id: self.secret}, max_age_seconds=self.max_age_seconds, ) def signer(self) -> SignedResultCodec: return SignedResultCodec( {self.key_id: self.secret}, signing_key_id=self.key_id, max_age_seconds=self.max_age_seconds, )