"""Runtime transport implementations for the OpenClaw adapter.""" from __future__ import annotations import json import threading import urllib.error import urllib.parse import urllib.request from typing import Any, Callable, Mapping, Protocol from .contract import ( CommandState, OpenClawCommand, OpenClawResult, RuntimeHealth, utc_now, ) class RuntimeTransport(Protocol): def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ... def cancel(self, command_id: str) -> bool: ... def health(self) -> RuntimeHealth: ... class TransportError(RuntimeError): """Runtime transport or protocol failure.""" RuntimeHandler = Callable[[OpenClawCommand], Mapping[str, Any] | OpenClawResult] class InProcessRuntimeTransport: """Deterministic runtime used by tests and controlled local integrations.""" def __init__( self, handler: RuntimeHandler, *, runtime_id: str = "openclaw-in-process", version: str = "v1", workspace_isolated: bool = True, credential_ref_configured: bool = True, ) -> None: self.handler = handler self.runtime_id = runtime_id self.version = version self.workspace_isolated = workspace_isolated self.credential_ref_configured = credential_ref_configured self._lock = threading.RLock() self._running: set[str] = set() self._cancelled: set[str] = set() def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: if timeout_seconds <= 0: raise ValueError("timeout_seconds must be positive") with self._lock: if command.command_id in self._cancelled: return OpenClawResult( command_id=command.command_id, tenant_id=command.tenant_id, task_id=command.task_id, trace_id=command.trace_id, state=CommandState.CANCELLED, error={"code": "cancelled", "message": "cancelled before execution"}, finished_at=utc_now(), runtime_id=self.runtime_id, ) self._running.add(command.command_id) try: raw = self.handler(command) if isinstance(raw, OpenClawResult): return raw if not isinstance(raw, Mapping): raise TransportError("runtime handler must return a mapping or OpenClawResult") return OpenClawResult( command_id=command.command_id, tenant_id=command.tenant_id, task_id=command.task_id, trace_id=command.trace_id, state=CommandState(str(raw.get("state", "COMPLETED"))), output=raw.get("output"), error=raw.get("error"), started_at=str(raw.get("started_at") or utc_now()), finished_at=str(raw.get("finished_at") or utc_now()), runtime_id=str(raw.get("runtime_id") or self.runtime_id), attempts=int(raw.get("attempts", 1)), ) finally: with self._lock: self._running.discard(command.command_id) def cancel(self, command_id: str) -> bool: with self._lock: self._cancelled.add(command_id) return command_id in self._running def health(self) -> RuntimeHealth: with self._lock: active = len(self._running) return RuntimeHealth( runtime="openclaw", runtime_id=self.runtime_id, status="healthy", version=self.version, heartbeat_at=utc_now(), active_tasks=active, workspace_isolated=self.workspace_isolated, credential_ref_configured=self.credential_ref_configured, details={"transport": "in_process"}, ) class HTTPRuntimeTransport: """Private HTTP transport for an OpenClaw runtime service.""" def __init__( self, base_url: str, *, runtime_id: str = "openclaw-http", credential_ref: str | None = None, max_response_bytes: int = 2_097_152, ) -> None: normalized = str(base_url).rstrip("/") if not normalized.startswith(("http://", "https://")): raise ValueError("base_url must use http or https") self.base_url = normalized self.runtime_id = runtime_id self.credential_ref = credential_ref self.max_response_bytes = int(max_response_bytes) def _request( self, method: str, path: str, *, payload: Mapping[str, Any] | None, timeout_seconds: float, ) -> Mapping[str, Any]: body = None headers = {"Accept": "application/json", "User-Agent": "AIOS-OpenClaw/1"} if payload is not None: body = json.dumps(dict(payload), separators=(",", ":")).encode("utf-8") headers["Content-Type"] = "application/json" if self.credential_ref: headers["X-AIOS-Credential-Ref"] = self.credential_ref request = urllib.request.Request( f"{self.base_url}{path}", data=body, method=method, headers=headers, ) try: with urllib.request.urlopen(request, timeout=timeout_seconds) as response: raw = response.read(self.max_response_bytes + 1) except urllib.error.HTTPError as exc: raise TransportError(f"runtime returned HTTP {exc.code}") from exc except urllib.error.URLError as exc: raise TransportError(f"runtime unavailable: {exc.reason}") from exc if len(raw) > self.max_response_bytes: raise TransportError("runtime response exceeded size limit") decoded = json.loads(raw.decode("utf-8")) if not isinstance(decoded, Mapping): raise TransportError("runtime response must be a JSON object") return decoded @staticmethod def _command_payload(command: OpenClawCommand) -> dict[str, Any]: return {**command.unsigned_dict(), "signature": command.signature} def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: value = self._request( "POST", "/v1/execute", payload=self._command_payload(command), timeout_seconds=timeout_seconds, ) return OpenClawResult( command_id=str(value["command_id"]), tenant_id=str(value["tenant_id"]), task_id=str(value["task_id"]), trace_id=str(value["trace_id"]), state=CommandState(str(value["state"])), output=value.get("output"), error=value.get("error"), started_at=value.get("started_at"), finished_at=value.get("finished_at"), runtime_id=str(value.get("runtime_id") or self.runtime_id), attempts=int(value.get("attempts", 1)), ) def cancel(self, command_id: str) -> bool: value = self._request( "POST", f"/v1/commands/{urllib.parse.quote(command_id, safe='')}/cancel", payload={}, timeout_seconds=10.0, ) return bool(value.get("accepted")) def health(self) -> RuntimeHealth: value = self._request("GET", "/v1/health", payload=None, timeout_seconds=5.0) return RuntimeHealth( runtime=str(value.get("runtime") or "openclaw"), runtime_id=str(value.get("runtime_id") or self.runtime_id), status=str(value.get("status") or "unknown"), version=str(value.get("version") or "unknown"), heartbeat_at=str(value.get("heartbeat_at") or utc_now()), active_tasks=int(value.get("active_tasks", 0)), workspace_isolated=bool(value.get("workspace_isolated", False)), credential_ref_configured=bool(value.get("credential_ref_configured", False)), details=value.get("details") if isinstance(value.get("details"), Mapping) else {}, )