"""Value objects for one claimed task and its run result. These are the wire types the Go worker agent hands to the per-task Python entry point; the daemon lifecycle itself lives in the Go agent. """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any from urllib.parse import urlsplit from uuid import UUID def _required_string(value: object, field: str) -> str: if not isinstance(value, str) or not value.strip(): raise ValueError(f"{field} must be a non-empty string") return value def _coordinator_uri(value: object, field: str) -> str: uri = _required_string(value, field) parsed = urlsplit(uri) if uri.startswith("/"): # ``//host/path`` is a network-path reference: urljoin would resolve # it to another origin. Dot segments are rejected for the same reason # we reject unsafe local task identifiers. if parsed.netloc or any(segment == ".." for segment in parsed.path.split("/")): raise ValueError(f"{field} must be a safe coordinator path") return uri if parsed.scheme not in {"http", "https"} or not parsed.hostname: raise ValueError(f"{field} must be an absolute HTTP(S) URL or coordinator path") return uri def _sha256(value: object, field: str) -> str: digest = _required_string(value, field).lower() if len(digest) != 64 or any( character not in "0123456789abcdef" for character in digest ): raise ValueError(f"{field} must be a SHA-256 hex digest") return digest @dataclass(frozen=True) class InputArtifact: uri: str sha256: str @dataclass(frozen=True) class ClaimedTask: task_id: str attempt: int lease_expires_at: str workload: str input: InputArtifact parameters: dict[str, Any] @classmethod def from_json(cls, data: dict[str, Any]) -> "ClaimedTask": try: input_data = data["input"] if not isinstance(input_data, dict): raise ValueError("input must be an object") raw_attempt = data["attempt"] if ( isinstance(raw_attempt, bool) or not isinstance(raw_attempt, int) or raw_attempt < 1 ): raise ValueError("attempt must be a positive integer") task_id = str(UUID(_required_string(data["task_id"], "task_id"))) lease_expires_at = _required_string( data["lease_expires_at"], "lease_expires_at" ) if ( datetime.fromisoformat(lease_expires_at.replace("Z", "+00:00")).tzinfo is None ): raise ValueError("lease_expires_at must include a timezone") parameters = data.get("parameters", {}) if not isinstance(parameters, dict): raise ValueError("parameters must be an object") return cls( task_id=task_id, attempt=raw_attempt, lease_expires_at=lease_expires_at, workload=_required_string(data["workload"], "workload"), input=InputArtifact( uri=_coordinator_uri(input_data["uri"], "input.uri"), sha256=_sha256(input_data["sha256"], "input.sha256"), ), parameters=parameters, ) except (KeyError, TypeError, ValueError) as error: raise ValueError("invalid claimed-task response") from error @dataclass(frozen=True) class ProducedArtifact: path: Path content_type: str @dataclass(frozen=True) class RunResult: artifacts: tuple[ProducedArtifact, ...] metrics: dict[str, int | float]