"""Build the program message consumed by the runtime ``fetch_oss_file`` tool.""" from __future__ import annotations from dataclasses import dataclass from typing import Any, Dict, Protocol from urllib.parse import quote, urlparse from arr_ingestion.contracts import ArtifactRef, ROLE_CONTRACTS from arr_processing.contracts import ProcessingRequest, canonical_json_bytes from arr_processing.errors import ProcessingError from arr_storage.aliyun_oss_v2 import AliyunOssConfig from arr_storage.contracts import valid_object_key PROGRAM_INPUT_VERSION = "arr-opera-daily-program-input-3" class ProcessingSourceResolver(Protocol): def source_for_attempt(self, job_id: str, attempt_no: int) -> ArtifactRef: ... @dataclass(frozen=True) class OssProcessingMessageBuilder: """Resolve one ARR-owned source to its public-read HTTPS object URL. The Agent receives an attachment-shaped descriptor containing the exact URL that ``fetch_oss_file`` must use, plus non-secret provenance and integrity fields. AccessKey, Secret, local paths and signed URLs never enter the message. """ config: AliyunOssConfig sources: ProcessingSourceResolver def message(self, request: ProcessingRequest) -> str: source = self.sources.source_for_attempt(request.job_id, request.attempt_no) self._validate_source(request, source) endpoint_host = self._endpoint_host() if request.submission_grant is None: raise ProcessingError( "PROCESSING_SUBMISSION_GRANT_MISSING", "direct result submission grant is unavailable", ) payload: Dict[str, Any] = { "contract_version": PROGRAM_INPUT_VERSION, "job_id": request.job_id, "attempt_no": request.attempt_no, "source_file_id": request.source_file_id, "processor_version": request.processor_version, "rule_set_sha256": request.rule_set_sha256, "oss_attachments": [ { "id": request.source_file_id, "name": source.original_filename, "content_type": source.mime_type, "size": source.byte_size, "sha256": source.sha256, "current_or_history": "current", "source_ref": {"source": "oss_attachments", "index": 0}, "oss": { "bucket": self.config.bucket, "endpoint": endpoint_host, "object_key": source.object_key, "url": self._public_object_url( endpoint_host, source.object_key, ), }, } ], "oss_inline_images": [], "attachment_fetch_policy": { "tool_name": "fetch_oss_file", "required_before_skill_call": True, "applies_to": ["oss_attachments"], "output_field": "current_attachments", }, "result_submission": { "tool_name": "arr_submit_processing_result", "submission_grant": request.submission_grant, "job_id": request.job_id, "attempt_no": request.attempt_no, "payload_source": "structured-result.json", "required_after_successful_skill_call": True, }, } return canonical_json_bytes(payload).decode("utf-8") @staticmethod def _validate_source(request: ProcessingRequest, source: ArtifactRef) -> None: expected_mime = ROLE_CONTRACTS["source_xml"][2] if ( not isinstance(source, ArtifactRef) or source.role != "source_xml" or source.file_kind != "opera_xml" or source.mime_type != expected_mime or source.original_filename != "source.xml" or not valid_object_key(source.object_key) or request.job_id not in source.object_key.split("/") ): raise ProcessingError( "PROCESSING_SOURCE_INVALID", "processing source object is unavailable", ) def _endpoint_host(self) -> str: expected_host = f"oss-{self.config.region}.aliyuncs.com" if self.config.endpoint: parsed = urlparse(self.config.endpoint) if ( parsed.scheme != "https" or parsed.hostname != expected_host or parsed.username is not None or parsed.password is not None or parsed.port is not None ): raise ProcessingError( "PROCESSING_SOURCE_INVALID", "public OSS endpoint is unavailable", ) return expected_host def _public_object_url(self, endpoint_host: str, object_key: str) -> str: encoded_key = quote(object_key, safe="/") return f"https://{self.config.bucket}.{endpoint_host}/{encoded_key}"