"""Managed Modal environment backed by tool-gateway.""" from __future__ import annotations import json import logging import os import requests import uuid from dataclasses import dataclass from typing import Any, Dict, Optional from tools.environments.modal_common import ( BaseModalExecutionEnvironment, ModalExecStart, PreparedModalExec, ) from tools.managed_tool_gateway import resolve_managed_tool_gateway logger = logging.getLogger(__name__) def _request_timeout_env(name: str, default: float) -> float: try: value = float(os.getenv(name, str(default))) return value if value > 0 else default except (TypeError, ValueError): return default @dataclass(frozen=True) class _ManagedModalExecHandle: exec_id: str class ManagedModalEnvironment(BaseModalExecutionEnvironment): """Gateway-owned Modal sandbox with Hermes-compatible execute/cleanup.""" _CONNECT_TIMEOUT_SECONDS = _request_timeout_env("TERMINAL_MANAGED_MODAL_CONNECT_TIMEOUT_SECONDS", 1.0) _POLL_READ_TIMEOUT_SECONDS = _request_timeout_env("TERMINAL_MANAGED_MODAL_POLL_READ_TIMEOUT_SECONDS", 5.0) _CANCEL_READ_TIMEOUT_SECONDS = _request_timeout_env("TERMINAL_MANAGED_MODAL_CANCEL_READ_TIMEOUT_SECONDS", 5.0) _client_timeout_grace_seconds = 10.0 _interrupt_output = "[Command interrupted - Modal sandbox exec cancelled]" _unexpected_error_prefix = "Managed Modal exec failed" def __init__( self, image: str, cwd: str = "/root", timeout: int = 60, modal_sandbox_kwargs: Optional[Dict[str, Any]] = None, persistent_filesystem: bool = True, task_id: str = "default", ): super().__init__(cwd=cwd, timeout=timeout) self._guard_unsupported_credential_passthrough() gateway = resolve_managed_tool_gateway("modal") if gateway is None: raise ValueError("Managed Modal requires a configured tool gateway and Nous user token") self._gateway_origin = gateway.gateway_origin.rstrip("/") self._nous_user_token = gateway.nous_user_token self._task_id = task_id self._persistent = persistent_filesystem self._image = image self._sandbox_kwargs = dict(modal_sandbox_kwargs or {}) self._create_idempotency_key = str(uuid.uuid4()) self._sandbox_id = self._create_sandbox() def _start_modal_exec(self, prepared: PreparedModalExec) -> ModalExecStart: exec_id = str(uuid.uuid4()) payload: Dict[str, Any] = { "execId": exec_id, "command": prepared.command, "cwd": prepared.cwd, "timeoutMs": int(prepared.timeout * 1000), } if prepared.stdin_data is not None: payload["stdinData"] = prepared.stdin_data try: response = self._request( "POST", f"/v1/sandboxes/{self._sandbox_id}/execs", json=payload, timeout=10, ) except Exception as exc: return ModalExecStart( immediate_result=self._error_result(f"Managed Modal exec failed: {exc}") ) if response.status_code >= 400: return ModalExecStart( immediate_result=self._error_result( self._format_error("Managed Modal exec failed", response) ) ) body = response.json() status = body.get("status") if status in {"completed", "failed", "cancelled", "timeout"}: return ModalExecStart( immediate_result=self._result( body.get("output", ""), body.get("returncode", 1), ) ) if body.get("execId") != exec_id: return ModalExecStart( immediate_result=self._error_result( "Managed Modal exec start did not return the expected exec id" ) ) return ModalExecStart(handle=_ManagedModalExecHandle(exec_id=exec_id)) def _poll_modal_exec(self, handle: _ManagedModalExecHandle) -> dict | None: try: status_response = self._request( "GET", f"/v1/sandboxes/{self._sandbox_id}/execs/{handle.exec_id}", timeout=(self._CONNECT_TIMEOUT_SECONDS, self._POLL_READ_TIMEOUT_SECONDS), ) except Exception as exc: return self._error_result(f"Managed Modal exec poll failed: {exc}") if status_response.status_code == 404: return self._error_result("Managed Modal exec not found") if status_response.status_code >= 400: return self._error_result( self._format_error("Managed Modal exec poll failed", status_response) ) status_body = status_response.json() status = status_body.get("status") if status in {"completed", "failed", "cancelled", "timeout"}: return self._result( status_body.get("output", ""), status_body.get("returncode", 1), ) return None def _cancel_modal_exec(self, handle: _ManagedModalExecHandle) -> None: self._cancel_exec(handle.exec_id) def _timeout_result_for_modal(self, timeout: int) -> dict: return self._result(f"Managed Modal exec timed out after {timeout}s", 124) def cleanup(self): if not getattr(self, "_sandbox_id", None): return try: self._request( "POST", f"/v1/sandboxes/{self._sandbox_id}/terminate", json={ "snapshotBeforeTerminate": self._persistent, }, timeout=60, ) except Exception as exc: logger.warning("Managed Modal cleanup failed: %s", exc) finally: self._sandbox_id = None def _create_sandbox(self) -> str: cpu = self._coerce_number(self._sandbox_kwargs.get("cpu"), 1) memory = self._coerce_number( self._sandbox_kwargs.get("memoryMiB", self._sandbox_kwargs.get("memory")), 5120, ) disk = self._coerce_number( self._sandbox_kwargs.get("ephemeral_disk", self._sandbox_kwargs.get("diskMiB")), None, ) create_payload = { "image": self._image, "cwd": self.cwd, "cpu": cpu, "memoryMiB": memory, "timeoutMs": 3_600_000, "idleTimeoutMs": max(300_000, int(self.timeout * 1000)), "persistentFilesystem": self._persistent, "logicalKey": self._task_id, } if disk is not None: create_payload["diskMiB"] = disk response = self._request( "POST", "/v1/sandboxes", json=create_payload, timeout=60, extra_headers={ "x-idempotency-key": self._create_idempotency_key, }, ) if response.status_code >= 400: raise RuntimeError(self._format_error("Managed Modal create failed", response)) body = response.json() sandbox_id = body.get("id") if not isinstance(sandbox_id, str) or not sandbox_id: raise RuntimeError("Managed Modal create did not return a sandbox id") return sandbox_id def _guard_unsupported_credential_passthrough(self) -> None: """Managed Modal does not sync or mount host credential files.""" try: from tools.credential_files import get_credential_file_mounts except Exception: return mounts = get_credential_file_mounts() if mounts: raise ValueError( "Managed Modal does not support host credential-file passthrough. " "Use TERMINAL_MODAL_MODE=direct when skills or config require " "credential files inside the sandbox." ) def _request(self, method: str, path: str, *, json: Dict[str, Any] | None = None, timeout: int = 30, extra_headers: Dict[str, str] | None = None) -> requests.Response: headers = { "Authorization": f"Bearer {self._nous_user_token}", "Content-Type": "application/json", } if extra_headers: headers.update(extra_headers) return requests.request( method, f"{self._gateway_origin}{path}", headers=headers, json=json, timeout=timeout, ) def _cancel_exec(self, exec_id: str) -> None: try: self._request( "POST", f"/v1/sandboxes/{self._sandbox_id}/execs/{exec_id}/cancel", timeout=(self._CONNECT_TIMEOUT_SECONDS, self._CANCEL_READ_TIMEOUT_SECONDS), ) except Exception as exc: logger.warning("Managed Modal exec cancel failed: %s", exc) @staticmethod def _coerce_number(value: Any, default: float) -> float: try: if value is None: return default return float(value) except (TypeError, ValueError): return default @staticmethod def _format_error(prefix: str, response: requests.Response) -> str: try: payload = response.json() if isinstance(payload, dict): message = payload.get("error") or payload.get("message") or payload.get("code") if isinstance(message, str) and message: return f"{prefix}: {message}" return f"{prefix}: {json.dumps(payload, ensure_ascii=False)}" except Exception: pass text = response.text.strip() if text: return f"{prefix}: {text}" return f"{prefix}: HTTP {response.status_code}"