diff --git a/.github/scripts/run_examples.sh b/.github/scripts/run_examples.sh index 36716029a5..f535b7cdf2 100755 --- a/.github/scripts/run_examples.sh +++ b/.github/scripts/run_examples.sh @@ -4,7 +4,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" PID_FILE="$ROOT/.tmp/examples-auto-run.pid" LOG_DIR="$ROOT/.tmp/examples-start-logs" -DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal runloop temporal" +DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal temporal" build_uv_prefix() { UV_RUN=(uv run) @@ -169,7 +169,7 @@ Commands: Environment overrides: EXAMPLES_INTERACTIVE_MODE (default auto) EXAMPLES_INCLUDE_SERVER/INTERACTIVE/AUDIO/EXTERNAL (defaults: 0/1/0/0) - EXAMPLES_UV_EXTRAS (default: litellm any-llm sqlalchemy redis blaxel modal runloop temporal; set empty to disable) + EXAMPLES_UV_EXTRAS (default: litellm any-llm sqlalchemy redis blaxel modal temporal; set empty to disable) APPLY_PATCH_AUTO_APPROVE, AUTO_APPROVE_MCP (default 1 in auto mode) EOF } diff --git a/docs/ref/extensions/sandbox/runloop/mounts.md b/docs/ref/extensions/sandbox/runloop/mounts.md deleted file mode 100644 index fe5b77c1d7..0000000000 --- a/docs/ref/extensions/sandbox/runloop/mounts.md +++ /dev/null @@ -1,3 +0,0 @@ -# `Mounts` - -::: agents.extensions.sandbox.runloop.mounts diff --git a/docs/ref/extensions/sandbox/runloop/sandbox.md b/docs/ref/extensions/sandbox/runloop/sandbox.md deleted file mode 100644 index 89ab3401d7..0000000000 --- a/docs/ref/extensions/sandbox/runloop/sandbox.md +++ /dev/null @@ -1,3 +0,0 @@ -# `Sandbox` - -::: agents.extensions.sandbox.runloop.sandbox diff --git a/examples/run_examples.py b/examples/run_examples.py index 064288b3c6..1800459737 100644 --- a/examples/run_examples.py +++ b/examples/run_examples.py @@ -93,8 +93,6 @@ "examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py", "examples/sandbox/extensions/e2b_runner.py", "examples/sandbox/extensions/modal_runner.py", - "examples/sandbox/extensions/runloop/capabilities.py", - "examples/sandbox/extensions/runloop/runner.py", # This local workflow gives model-selected shell commands access to the host. "examples/sandbox/extensions/temporal/local_hello_workflow.py", "examples/sandbox/extensions/temporal/temporal_sandbox_agent.py", diff --git a/examples/sandbox/extensions/README.md b/examples/sandbox/extensions/README.md index 3037543df4..18bd248afb 100644 --- a/examples/sandbox/extensions/README.md +++ b/examples/sandbox/extensions/README.md @@ -6,10 +6,18 @@ They intentionally keep the flow simple: 1. Build a tiny manifest in memory. 2. Create a `SandboxAgent` that inspects that workspace through one shell tool. -3. Run the agent against E2B, Modal, Daytona, Cloudflare, Runloop, Blaxel, or Vercel. +3. Run the agent against E2B, Modal, Daytona, Cloudflare, Blaxel, or Vercel. All of these examples require `OPENAI_API_KEY`, because they call the model through the normal `Runner` path. Each cloud backend also needs its own provider credentials. +## Migrating from Runloop + +Runloop support has been removed from `openai-agents`. Imports from `agents.extensions.sandbox.runloop` and Runloop exports from `agents.extensions.sandbox`, including `RunloopSandboxClient`, no longer work. Applications that still need Runloop can stay on a previously released compatible version while migrating. + +The [Modal example](#modal) uses `ModalSandboxClient` and `ModalSandboxClientOptions` from `agents.extensions.sandbox`. Install `openai-agents[modal]`, configure Modal authentication, and set an app name. Replace the client and options in `SandboxRunConfig`, and adapt provider-specific mounts and platform operations. `ModalCloudBucketMountStrategy` provides native cloud bucket mounts; review and test its provider-specific behavior and credentials for your workload. + +Before switching, preserve any data you need from existing Runloop sandboxes. Runloop sandbox IDs, snapshots, and session state are provider-specific; do not assume Modal can resume them. Create a new Modal sandbox and restore the data through a supported mechanism. + ## E2B ### Setup @@ -228,84 +236,6 @@ export DAYTONA_API_KEY=... uv run python examples/sandbox/extensions/daytona/daytona_runner.py --stream ``` -## Runloop - -### Setup - -Install the repo extra: - -```bash -uv sync --extra runloop -``` - -Sign up for Runloop, no credit card required and $50 in credits @ [platform.runloop.ai](https://platform.runloop.ai/). -Export the required environment variables: - -```bash -export OPENAI_API_KEY=... -export RUNLOOP_API_KEY=... -``` - -### Run - -```bash -uv run python examples/sandbox/extensions/runloop/runner.py --stream -``` - -Useful flags: - -- `--blueprint-name ` -- `--pause-on-exit` -- `--root` - -Runloop-specific SDK features are also available directly on -`RunloopSandboxClientOptions` and `RunloopSandboxClient.platform`. Example: - -```python -from agents.extensions.sandbox.runloop import ( - RunloopAfterIdle, - RunloopGatewaySpec, - RunloopLaunchParameters, - RunloopMcpSpec, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopTunnelConfig, -) - -client = RunloopSandboxClient() -sandbox = await client.create( - options=RunloopSandboxClientOptions( - blueprint_name="python-3-12", - launch_parameters=RunloopLaunchParameters( - network_policy_id="np_123", - resource_size_request="MEDIUM", - after_idle=RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend"), - ), - tunnel=RunloopTunnelConfig(auth_mode="authenticated"), - gateways={ - "OPENAI_GATEWAY": RunloopGatewaySpec( - gateway="openai", - secret="OPENAI_GATEWAY_SECRET", - ) - }, - mcp={ - "GITHUB_MCP": RunloopMcpSpec( - mcp_config="github-readonly", - secret="GITHUB_MCP_SECRET", - ) - }, - managed_secrets={"OPENAI_API_KEY": "..."}, - metadata={"team": "agents"}, - ) -) - -public_blueprints = await client.platform.blueprints.list_public() -public_benchmarks = await client.platform.benchmarks.list_public() -``` - -`managed_secrets` are stored as Runloop account secrets and only secret references are persisted in session state. An entry's value may instead be `RunloopExistingSecret()`, which attaches a secret that is already stored on the account by name, uploading nothing and leaving the stored value untouched. The platform facade also exposes Runloop-native helpers for blueprints, benchmarks, secrets, network policies, and axons. - -If you enable `--root`, Runloop launches the devbox with `launch_parameters.user_parameters={"username":"root","uid":0}`. In that mode, the default home and working directory become `/root`, so the example also uses `/root` as its manifest workspace root. If you configure root launch in your own code, either rely on that root-mode default or explicitly choose a `manifest.root` under `/root`. ## Blaxel ### Setup diff --git a/examples/sandbox/extensions/runloop/__init__.py b/examples/sandbox/extensions/runloop/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/sandbox/extensions/runloop/capabilities.py b/examples/sandbox/extensions/runloop/capabilities.py deleted file mode 100644 index 17d1fff4a9..0000000000 --- a/examples/sandbox/extensions/runloop/capabilities.py +++ /dev/null @@ -1,1015 +0,0 @@ -from __future__ import annotations - -import argparse -import asyncio -import io -import json -import os -import sys -import time -import urllib.error -import urllib.request -import uuid -from pathlib import Path -from typing import Any, Literal, cast -from urllib.parse import urljoin - -from openai.types.responses import ResponseTextDeltaEvent -from pydantic import BaseModel - -from agents import ( - Agent, - ModelSettings, - Runner, -) -from agents.decorators import tool -from agents.run import RunConfig -from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig - -if __package__ is None or __package__ == "": - sys.path.insert(0, str(Path(__file__).resolve().parents[4])) - -from examples.sandbox.misc.example_support import text_manifest, tool_call_name -from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability - -try: - from agents.extensions.sandbox import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopAfterIdle, - RunloopGatewaySpec, - RunloopLaunchParameters, - RunloopMcpSpec, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopSandboxSessionState, - RunloopTunnelConfig, - RunloopUserParameters, - ) -except Exception as exc: # pragma: no cover - import path depends on optional extras - raise SystemExit( - "Runloop sandbox examples require the optional repo extra.\n" - "Install it with: uv sync --extra runloop" - ) from exc - - -DEFAULT_MODEL = "gpt-5.6-sol" -DEFAULT_HTTP_PORT = 8123 -DEFAULT_AGENT_PROMPT = ( - "Inspect this Runloop sandbox workspace, verify the configuration using the shell tool, " - "and summarize which Runloop-specific capabilities were exercised." -) -EXAMPLE_RESOURCE_SLUG = "runloop-capabilities-example" -PERSISTENT_SECRET_NAME = "RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN" -PERSISTENT_SECRET_VALUE = "runloop-capabilities-example-token" -PERSISTENT_NETWORK_POLICY_NAME = "runloop-capabilities-example-policy" -HTTP_LOG_PATH = Path(".runloop-http.log") -RUNTIME_CONTEXT_PATH = Path("runtime_context.json") -AGENT_PROOF_PATH = Path("verification/agent-proof.txt") - - -class RunloopResourceQueryResult(BaseModel): - resource_type: Literal["secret", "network_policy"] - name: str - found: bool - id: str | None = None - description: str | None = None - - -class RunloopResourceBootstrapResult(BaseModel): - resource_type: Literal["secret", "network_policy"] - name: str - action: Literal["created", "reused", "override"] - id: str | None = None - found_before_bootstrap: bool - - -def _phase(title: str) -> None: - print(f"\n=== {title} ===", flush=True) - - -def _require_env(name: str) -> None: - if os.environ.get(name): - return - raise SystemExit(f"{name} must be set before running this example.") - - -def _run_id() -> str: - return uuid.uuid4().hex[:8] - - -def _summarize_resource(item: object, fields: tuple[str, ...]) -> dict[str, object]: - summary: dict[str, object] = {} - for field in fields: - value = getattr(item, field, None) - if value is not None: - summary[field] = value - return summary - - -async def _collect_async_items(items: Any, *, limit: int) -> list[Any]: - collected: list[Any] = [] - async for item in items: - collected.append(item) - if len(collected) >= limit: - break - return collected - - -def _status_code(exc: BaseException) -> int | None: - status_code = getattr(exc, "status_code", None) - if isinstance(status_code, int): - return status_code - response = getattr(exc, "response", None) - response_status = getattr(response, "status_code", None) - return response_status if isinstance(response_status, int) else None - - -def _is_not_found(exc: BaseException) -> bool: - return _status_code(exc) == 404 - - -def _error_message(exc: BaseException) -> str | None: - message = getattr(exc, "message", None) - if isinstance(message, str): - return message - body = getattr(exc, "body", None) - if isinstance(body, dict): - body_message = body.get("message") - if isinstance(body_message, str): - return body_message - return None - - -def _is_conflict(exc: BaseException) -> bool: - status_code = _status_code(exc) - if status_code == 409: - return True - if status_code == 400: - message = _error_message(exc) - return isinstance(message, str) and "already exists" in message.lower() - return False - - -async def _collect_maybe_async_items(items: Any, *, limit: int) -> list[Any]: - if hasattr(items, "__aiter__"): - return await _collect_async_items(items, limit=limit) - return list(items)[:limit] - - -async def _read_text(session: Any, path: Path) -> str: - data = await session.read(path) - try: - payload = data.read() - finally: - data.close() - if isinstance(payload, bytes): - return payload.decode("utf-8") - return str(payload) - - -async def _write_json(session: Any, path: Path, payload: dict[str, object]) -> None: - await session.write( - path, io.BytesIO(json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")) - ) - - -def _build_manifest(*, workspace_root: str, context: dict[str, object]) -> Manifest: - manifest = text_manifest( - { - "README.md": ( - "# Runloop Capabilities Example\n\n" - "This workspace is used to validate the Runloop-specific sandbox integration end " - "to end.\n" - ), - "checklist.md": ( - "# Checklist\n\n" - "1. Inspect the workspace.\n" - "2. Verify the resource discovery results in the context files.\n" - "3. Confirm the managed secret is available without printing its full value.\n" - "4. Confirm the HTTP preview server and verification file.\n" - "5. Summarize what Runloop-native features were exercised and whether persistent " - "resources were reused or created.\n" - ), - "platform_context.json": json.dumps(context, indent=2, sort_keys=True) + "\n", - } - ) - return Manifest(root=workspace_root, entries=manifest.entries) - - -def _build_sandbox_agent( - *, model: str, manifest: Manifest, managed_secret_name: str -) -> SandboxAgent: - return SandboxAgent( - name="Runloop Capabilities Guide", - model=model, - instructions=( - "Inspect the Runloop sandbox workspace carefully before answering. Use the shell tool " - "to verify what happened in the environment and keep the final response concise. " - "Follow this sequence:\n" - "1. Run `pwd` and `find . -maxdepth 3 -type f | sort`.\n" - "2. Read `README.md`, `checklist.md`, `platform_context.json`, and `runtime_context.json`.\n" - "3. Report whether the managed secret and network policy existed before bootstrap by " - "reading the query/bootstrap summaries from the context files.\n" - f"4. Confirm whether `${managed_secret_name}` is set, but never print the full value. " - "Only report whether it exists and its character length.\n" - f"5. Read `{HTTP_LOG_PATH.as_posix()}` and confirm the HTTP server started.\n" - f"6. Create `{AGENT_PROOF_PATH.as_posix()}` with these exact lines:\n" - " runloop_capabilities_verified=true\n" - " managed_secret_checked=true\n" - " tunnel_verified=true\n" - "7. Print that verification file from the shell.\n" - "8. Final answer: 2 short sentences naming the specific Runloop features exercised, " - "including whether the persistent secret and policy were reused or created.\n" - "Only mention facts you verified from files, environment inspection, or shell output." - ), - default_manifest=manifest, - capabilities=[WorkspaceShellCapability()], - model_settings=ModelSettings(tool_choice="required"), - ) - - -def _build_query_agent( - *, - model: str, - query_secret_tool: Any, - query_policy_tool: Any, - managed_secret_name: str, - network_policy_name: str, -) -> Agent: - return Agent( - name="Runloop Resource Discovery Guide", - model=model, - instructions=( - "Use the provided Runloop query tools to check whether the persistent example " - "resources already exist before any create step. Keep the final answer concise." - ), - tools=[query_secret_tool, query_policy_tool], - model_settings=ModelSettings(tool_choice="required"), - ).clone( - instructions=( - "Use the provided Runloop query tools to check whether the persistent example " - "resources already exist before any create step. Keep the final answer concise." - ), - handoff_description=None, - output_type=None, - ) - - -def _stream_event_banner(event_name: str) -> str | None: - if event_name == "tool_called": - return "[tool call]" - if event_name == "tool_output": - return "[tool output]" - return None - - -def _runloop_state(session: Any) -> RunloopSandboxSessionState: - return cast(RunloopSandboxSessionState, session.state) - - -async def _run_plain_agent( - *, - agent: Agent, - prompt: str, - workflow_name: str, - stream: bool, -) -> str: - if not stream: - result = await Runner.run(agent, prompt, run_config=RunConfig(workflow_name=workflow_name)) - print(result.final_output) - return str(result.final_output) - - stream_result = Runner.run_streamed( - agent, - prompt, - run_config=RunConfig(workflow_name=workflow_name), - ) - saw_text_delta = False - saw_any_text = False - - async for event in stream_result.stream_events(): - if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): - if not saw_text_delta: - print("assistant> ", end="", flush=True) - saw_text_delta = True - print(event.data.delta, end="", flush=True) - saw_any_text = True - continue - - if event.type != "run_item_stream_event": - continue - - banner = _stream_event_banner(event.name) - if banner is None: - continue - if saw_text_delta: - print() - saw_text_delta = False - print(f"{banner}: {tool_call_name(event.item.raw_item) or 'tool'}", flush=True) - - if saw_text_delta: - print() - if not saw_any_text: - print(stream_result.final_output) - return str(stream_result.final_output) - - -async def _run_sandbox_agent( - *, - agent: SandboxAgent, - prompt: str, - session: Any, - workflow_name: str, - stream: bool, -) -> str: - if not stream: - result = await Runner.run( - agent, - prompt, - run_config=RunConfig( - sandbox=SandboxRunConfig(session=session), - workflow_name=workflow_name, - ), - ) - print(result.final_output) - return str(result.final_output) - - stream_result = Runner.run_streamed( - agent, - prompt, - run_config=RunConfig( - sandbox=SandboxRunConfig(session=session), - workflow_name=workflow_name, - ), - ) - saw_text_delta = False - saw_any_text = False - - async for event in stream_result.stream_events(): - if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): - if not saw_text_delta: - print("assistant> ", end="", flush=True) - saw_text_delta = True - print(event.data.delta, end="", flush=True) - saw_any_text = True - continue - - if event.type != "run_item_stream_event": - continue - - banner = _stream_event_banner(event.name) - if banner is None: - continue - if saw_text_delta: - print() - saw_text_delta = False - print(f"{banner}: {tool_call_name(event.item.raw_item) or 'tool'}", flush=True) - - if saw_text_delta: - print() - if not saw_any_text: - print(stream_result.final_output) - return str(stream_result.final_output) - - -async def _start_http_server(session: Any, *, port: int, workspace_root: str) -> None: - command = ( - "python -m http.server " - f"{port} --bind 0.0.0.0 --directory {workspace_root} " - f"> {HTTP_LOG_PATH.as_posix()} 2>&1 &" - ) - result = await session.exec(command, shell=True, timeout=10) - if not result.ok(): - raise RuntimeError(result.stderr.decode("utf-8", errors="replace")) - - -def _build_endpoint_url(endpoint: Any) -> str: - scheme = "https" if endpoint.tls else "http" - port = endpoint.port - host = endpoint.host - if (scheme == "https" and port == 443) or (scheme == "http" and port == 80): - return f"{scheme}://{host}/" - return f"{scheme}://{host}:{port}/" - - -async def _fetch_text(url: str, *, timeout_s: float) -> str: - def _fetch() -> str: - with urllib.request.urlopen(url, timeout=timeout_s) as response: - payload = response.read() - if isinstance(payload, bytes): - return payload.decode("utf-8", errors="replace") - return str(payload) - - return await asyncio.to_thread(_fetch) - - -async def _poll_http_preview(url: str, *, expected_substring: str, timeout_s: float) -> str: - deadline = time.monotonic() + timeout_s - last_error: Exception | None = None - while time.monotonic() < deadline: - try: - body = await _fetch_text(url, timeout_s=5.0) - if expected_substring in body: - return body - except (urllib.error.URLError, TimeoutError) as exc: - last_error = exc - await asyncio.sleep(2) - if last_error is not None: - raise RuntimeError(f"HTTP preview never became ready: {last_error}") from last_error - raise RuntimeError("HTTP preview never returned the expected content.") - - -async def _preflight_public_resources(client: RunloopSandboxClient) -> dict[str, object]: - blueprints = await _collect_async_items( - await client.platform.blueprints.list_public(limit=3), - limit=3, - ) - benchmarks = await _collect_async_items( - await client.platform.benchmarks.list_public(limit=3), - limit=3, - ) - - blueprint_summaries = [ - _summarize_resource(item, ("id", "name", "status")) for item in blueprints - ] - benchmark_summaries = [ - _summarize_resource(item, ("id", "name", "description")) for item in benchmarks - ] - - if blueprint_summaries: - print("public blueprints:") - for summary in blueprint_summaries: - print(f" - {summary}") - else: - print("public blueprints: none returned") - - if benchmark_summaries: - print("public benchmarks:") - for summary in benchmark_summaries: - print(f" - {summary}") - else: - print("public benchmarks: none returned") - - return { - "public_blueprints": blueprint_summaries, - "public_benchmarks": benchmark_summaries, - } - - -async def _query_runloop_secret( - client: RunloopSandboxClient, - *, - name: str, -) -> RunloopResourceQueryResult: - try: - secret = cast(Any, await client.platform.secrets.get(name)) - except Exception as exc: - if _is_not_found(exc): - return RunloopResourceQueryResult(resource_type="secret", name=name, found=False) - raise - - return RunloopResourceQueryResult( - resource_type="secret", - name=name, - found=True, - id=cast(str | None, getattr(secret, "id", None)), - ) - - -async def _query_runloop_network_policy( - client: RunloopSandboxClient, - *, - name: str, -) -> RunloopResourceQueryResult: - policies = await _collect_maybe_async_items( - await client.platform.network_policies.list(name=name, limit=10), - limit=10, - ) - for policy in policies: - policy_id = cast(str | None, getattr(policy, "id", None)) - if policy_id is None: - continue - info = cast(Any, await client.platform.network_policies.get(policy_id).get_info()) - if getattr(info, "name", None) != name: - continue - return RunloopResourceQueryResult( - resource_type="network_policy", - name=name, - found=True, - id=cast(str | None, getattr(info, "id", None)) or policy_id, - description=cast(str | None, getattr(info, "description", None)), - ) - - return RunloopResourceQueryResult(resource_type="network_policy", name=name, found=False) - - -def _build_resource_query_tools( - client: RunloopSandboxClient, - *, - managed_secret_name: str, - network_policy_name: str, -) -> tuple[list[Any], dict[str, RunloopResourceQueryResult]]: - query_results: dict[str, RunloopResourceQueryResult] = {} - - @tool - async def query_runloop_secret(name: str) -> RunloopResourceQueryResult: - """Query whether a Runloop secret exists by name and return non-sensitive metadata.""" - - result = await _query_runloop_secret(client, name=name) - query_results["secret"] = result - return result - - @tool - async def query_runloop_network_policy(name: str) -> RunloopResourceQueryResult: - """Query whether a Runloop network policy exists by name and return basic metadata.""" - - result = await _query_runloop_network_policy(client, name=name) - query_results["network_policy"] = result - return result - - tools = [query_runloop_secret, query_runloop_network_policy] - _ = (managed_secret_name, network_policy_name) - return tools, query_results - - -async def _run_resource_query_phase( - client: RunloopSandboxClient, - *, - model: str, - stream: bool, - managed_secret_name: str, - network_policy_name: str, -) -> tuple[dict[str, RunloopResourceQueryResult], str]: - tools, query_results = _build_resource_query_tools( - client, - managed_secret_name=managed_secret_name, - network_policy_name=network_policy_name, - ) - query_agent = Agent( - name="Runloop Resource Discovery Guide", - model=model, - instructions=( - "Use both query tools before answering. You are checking whether the persistent " - "Runloop example resources already exist before any create step.\n\n" - f"1. Call `query_runloop_secret` with `{managed_secret_name}`.\n" - f"2. Call `query_runloop_network_policy` with `{network_policy_name}`.\n" - "3. Final answer in 2 short sentences stating whether each resource already exists." - ), - tools=tools, - model_settings=ModelSettings(tool_choice="required"), - ) - prompt = ( - "Check whether the persistent Runloop secret and network policy for this example already " - "exist before the script attempts any create or reuse step." - ) - output = await _run_plain_agent( - agent=query_agent, - prompt=prompt, - workflow_name="Runloop resource query example", - stream=stream, - ) - if "secret" not in query_results or "network_policy" not in query_results: - raise RuntimeError("The query agent did not call both Runloop resource query tools.") - return query_results, output - - -async def _bootstrap_persistent_resources( - client: RunloopSandboxClient, - *, - managed_secret_name: str, - managed_secret_value: str, - network_policy_name: str, - network_policy_id_override: str | None, - query_results: dict[str, RunloopResourceQueryResult], - axon_name: str | None, -) -> dict[str, object]: - secret_query = query_results["secret"] - policy_query = query_results["network_policy"] - - bootstrap: dict[str, object] = { - "managed_secret_value": managed_secret_value, - "secret": RunloopResourceBootstrapResult( - resource_type="secret", - name=managed_secret_name, - action="reused" if secret_query.found else "created", - id=secret_query.id, - found_before_bootstrap=secret_query.found, - ), - "network_policy": RunloopResourceBootstrapResult( - resource_type="network_policy", - name=network_policy_name, - action="override" - if network_policy_id_override - else ("reused" if policy_query.found else "created"), - id=network_policy_id_override or policy_query.id, - found_before_bootstrap=policy_query.found, - ), - "axon_id": None, - "axon_name": axon_name, - } - - secret_result = cast(RunloopResourceBootstrapResult, bootstrap["secret"]) - if not secret_query.found: - created_secret = cast( - Any, - await client.platform.secrets.create( - name=managed_secret_name, value=managed_secret_value - ), - ) - secret_result.id = cast(str | None, getattr(created_secret, "id", None)) - print( - "persistent secret bootstrap:", - secret_result.model_dump(mode="json"), - ) - - policy_result = cast(RunloopResourceBootstrapResult, bootstrap["network_policy"]) - if network_policy_id_override is None and not policy_query.found: - try: - created_policy = cast( - Any, - await client.platform.network_policies.create( - name=network_policy_name, - allow_all=True, - description="Persistent network policy for the Runloop capabilities example.", - ), - ) - except Exception as exc: - if not _is_conflict(exc): - raise - policy_result.action = "reused" - policy_result.found_before_bootstrap = True - refreshed_policy = await _query_runloop_network_policy(client, name=network_policy_name) - if not refreshed_policy.found or refreshed_policy.id is None: - raise RuntimeError( - "Runloop reported a network policy conflict, but the existing policy " - f"{network_policy_name!r} could not be resolved." - ) from exc - policy_result.id = refreshed_policy.id - else: - policy_result.id = cast(str | None, getattr(created_policy, "id", None)) - if policy_result.id is None: - refreshed_policy = await _query_runloop_network_policy( - client, name=network_policy_name - ) - policy_result.id = refreshed_policy.id - if policy_result.id is None: - raise RuntimeError( - f"Runloop network policy {network_policy_name!r} did not resolve to an ID." - ) - print( - "persistent network policy bootstrap:", - policy_result.model_dump(mode="json"), - ) - - if axon_name is not None: - axon = cast(Any, await client.platform.axons.create(name=axon_name)) - await client.platform.axons.query_sql( - cast(str, axon.id), - sql="CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL)", - ) - await client.platform.axons.batch_sql( - cast(str, axon.id), - statements=[ - {"sql": "INSERT INTO events (kind) VALUES (?)", "params": ["capabilities"]}, - {"sql": "INSERT INTO events (kind) VALUES (?)", "params": ["agent_guided"]}, - ], - ) - query_result = cast( - Any, - await client.platform.axons.query_sql( - cast(str, axon.id), - sql="SELECT COUNT(*) AS total_events FROM events", - ), - ) - publish_result = cast( - Any, - await client.platform.axons.publish( - cast(str, axon.id), - event_type="capabilities_example", - origin="AGENT_EVENT", - payload=json.dumps({"axon_name": axon_name}), - source="openai-agents-python", - ), - ) - bootstrap["axon_id"] = cast(str, axon.id) - print( - "axon demo created:", - { - "id": cast(str, axon.id), - "name": axon_name, - "rows": query_result.rows, - "published": getattr(publish_result, "published", None), - }, - ) - - return bootstrap - - -def _optional_gateways(args: argparse.Namespace) -> dict[str, RunloopGatewaySpec]: - if not (args.gateway_env_var and args.gateway_name and args.gateway_secret_name): - return {} - return { - args.gateway_env_var: RunloopGatewaySpec( - gateway=args.gateway_name, - secret=args.gateway_secret_name, - ) - } - - -def _optional_mcp(args: argparse.Namespace) -> dict[str, RunloopMcpSpec]: - if not (args.mcp_env_var and args.mcp_config and args.mcp_secret_name): - return {} - return { - args.mcp_env_var: RunloopMcpSpec( - mcp_config=args.mcp_config, - secret=args.mcp_secret_name, - ) - } - - -async def main(args: argparse.Namespace) -> None: - _require_env("OPENAI_API_KEY") - _require_env("RUNLOOP_API_KEY") - - workspace_root = ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT if args.root else DEFAULT_RUNLOOP_WORKSPACE_ROOT - ) - run_id = _run_id() - metadata = { - "example": "runloop-capabilities", - "run_id": run_id, - } - - client = RunloopSandboxClient() - session = None - resumed = None - session_closed = False - resumed_closed = False - - try: - _phase("Public Resource Discovery") - public_context = await _preflight_public_resources(client) - - _phase("Agent Resource Discovery") - query_results, query_agent_output = await _run_resource_query_phase( - client, - model=args.model, - stream=args.stream, - managed_secret_name=PERSISTENT_SECRET_NAME, - network_policy_name=PERSISTENT_NETWORK_POLICY_NAME, - ) - print( - "resource query results:", - {key: value.model_dump(mode="json") for key, value in query_results.items()}, - ) - - _phase("Persistent Resource Bootstrap") - axon_name = f"{EXAMPLE_RESOURCE_SLUG}-axon-{run_id}" if args.with_axon_demo else None - bootstrap = await _bootstrap_persistent_resources( - client, - managed_secret_name=PERSISTENT_SECRET_NAME, - managed_secret_value=PERSISTENT_SECRET_VALUE, - network_policy_name=PERSISTENT_NETWORK_POLICY_NAME, - network_policy_id_override=args.network_policy_id, - query_results=query_results, - axon_name=axon_name, - ) - secret_bootstrap = cast(RunloopResourceBootstrapResult, bootstrap["secret"]) - network_policy_bootstrap = cast(RunloopResourceBootstrapResult, bootstrap["network_policy"]) - network_policy_id = network_policy_bootstrap.id - - context = { - "example_slug": EXAMPLE_RESOURCE_SLUG, - "workspace_root": workspace_root, - "requested_blueprint_name": args.blueprint_name, - "public_resources": public_context, - "resource_query_agent_output": query_agent_output, - "resource_queries": { - key: value.model_dump(mode="json") for key, value in query_results.items() - }, - "resource_bootstrap": { - "secret": secret_bootstrap.model_dump(mode="json"), - "network_policy": network_policy_bootstrap.model_dump(mode="json"), - "axon_id": bootstrap["axon_id"], - "axon_name": bootstrap["axon_name"], - }, - "managed_secret_env_var": PERSISTENT_SECRET_NAME, - "network_policy_id": network_policy_id, - "metadata": metadata, - "gateway_bindings": sorted(_optional_gateways(args)), - "mcp_bindings": sorted(_optional_mcp(args)), - } - - manifest = _build_manifest(workspace_root=workspace_root, context=context) - agent = _build_sandbox_agent( - model=args.model, - manifest=manifest, - managed_secret_name=PERSISTENT_SECRET_NAME, - ) - options = RunloopSandboxClientOptions( - blueprint_name=args.blueprint_name, - pause_on_exit=True, - exposed_ports=(args.http_port,), - user_parameters=(RunloopUserParameters(username="root", uid=0) if args.root else None), - launch_parameters=RunloopLaunchParameters( - network_policy_id=network_policy_id, - resource_size_request=args.resource_size, - after_idle=RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend"), - launch_commands=["echo runloop-capabilities-example"], - ), - tunnel=RunloopTunnelConfig( - auth_mode="open", - http_keep_alive=True, - wake_on_http=True, - ), - gateways=_optional_gateways(args), - mcp=_optional_mcp(args), - metadata=metadata, - managed_secrets={PERSISTENT_SECRET_NAME: PERSISTENT_SECRET_VALUE}, - ) - - _phase("Sandbox Create") - session = await client.create(manifest=manifest, options=options) - await session.start() - session_state = _runloop_state(session) - print( - "session started:", - { - "devbox_id": session_state.devbox_id, - "secret_refs": session_state.secret_refs, - "metadata": session_state.metadata, - }, - ) - - _phase("Tunnel Check") - await _write_json( - session, - RUNTIME_CONTEXT_PATH, - { - **context, - "devbox_id": session_state.devbox_id, - "secret_refs": session_state.secret_refs, - "runtime_phase": "before_tunnel_check", - }, - ) - await _start_http_server(session, port=args.http_port, workspace_root=workspace_root) - endpoint = await session.resolve_exposed_port(args.http_port) - preview_url = urljoin(_build_endpoint_url(endpoint), "README.md") - preview_body = await _poll_http_preview( - preview_url, - expected_substring="Runloop Capabilities Example", - timeout_s=45.0, - ) - print("resolved tunnel:", preview_url) - await _write_json( - session, - RUNTIME_CONTEXT_PATH, - { - **context, - "devbox_id": session_state.devbox_id, - "secret_refs": session_state.secret_refs, - "tunnel_url": preview_url, - "http_preview_contains_readme": "Runloop Capabilities Example" in preview_body, - "runtime_phase": "before_agent_run", - }, - ) - - _phase("Agent Verification") - await _run_sandbox_agent( - agent=agent, - prompt=args.prompt, - session=session, - workflow_name="Runloop capabilities example", - stream=args.stream, - ) - proof_text = await _read_text(session, AGENT_PROOF_PATH) - print("agent proof:") - print(proof_text.rstrip()) - - _phase("Suspend") - await session.aclose() - session_closed = True - print("session persisted and suspended") - - _phase("Resume Check") - resumed = await client.resume(session.state) - await resumed.start() - resumed_state = _runloop_state(resumed) - resumed_runtime_context = await _read_text(resumed, RUNTIME_CONTEXT_PATH) - resumed_proof_text = await _read_text(resumed, AGENT_PROOF_PATH) - print("resumed runtime context bytes:", len(resumed_runtime_context.encode("utf-8"))) - print("resumed proof:") - print(resumed_proof_text.rstrip()) - resumed_state.pause_on_exit = False - await resumed.aclose() - resumed_closed = True - print("resumed session cleaned up with delete semantics") - - _phase("Persistent Resource Summary") - print( - "persistent resources retained:", - { - "secret": secret_bootstrap.model_dump(mode="json"), - "network_policy": network_policy_bootstrap.model_dump(mode="json"), - }, - ) - if bootstrap["axon_id"] is not None: - print( - "axon retained for manual cleanup:", - { - "axon_id": bootstrap["axon_id"], - "axon_name": bootstrap["axon_name"], - }, - ) - finally: - if resumed is not None and not resumed_closed: - try: - _runloop_state(resumed).pause_on_exit = False - await resumed.aclose() - except Exception as exc: - print(f"warning: failed to close resumed session cleanly: {exc}") - elif session is not None and not session_closed: - try: - _runloop_state(session).pause_on_exit = False - await session.aclose() - except Exception as exc: - print(f"warning: failed to close initial session cleanly: {exc}") - elif session is not None and session_closed and resumed is None: - try: - cleanup_session = await client.resume(session.state) - _runloop_state(cleanup_session).pause_on_exit = False - await cleanup_session.aclose() - except Exception as exc: - print(f"warning: failed to resume suspended session for cleanup: {exc}") - - await client.close() - - -def _build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser() - parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.") - parser.add_argument( - "--prompt", default=DEFAULT_AGENT_PROMPT, help="Prompt to send to the agent." - ) - parser.add_argument("--blueprint-name", default=None, help="Optional Runloop blueprint name.") - parser.add_argument( - "--resource-size", - default="MEDIUM", - choices=["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"], - help="Runloop resource size request for the devbox.", - ) - parser.add_argument( - "--network-policy-id", - default=None, - help="Optional Runloop network policy id override. Without this flag, the example reuses or creates the persistent example policy by name.", - ) - parser.add_argument( - "--http-port", - type=int, - default=DEFAULT_HTTP_PORT, - help="Port used by the preview HTTP server.", - ) - parser.add_argument( - "--root", - action="store_true", - default=False, - help="Launch the Runloop devbox as root. The workspace root becomes /root.", - ) - parser.add_argument( - "--stream", - action="store_true", - default=False, - help="Stream the agent response and tool activity.", - ) - parser.add_argument( - "--with-axon-demo", - action="store_true", - default=False, - help="Also create and use a temporary Axon. This leaves the Axon behind for manual cleanup.", - ) - parser.add_argument( - "--gateway-env-var", default=None, help="Env var name for a gateway binding." - ) - parser.add_argument( - "--gateway-name", default=None, help="Runloop gateway name for the binding." - ) - parser.add_argument( - "--gateway-secret-name", - default=None, - help="Runloop secret name used by the gateway binding.", - ) - parser.add_argument("--mcp-env-var", default=None, help="Env var name for an MCP binding.") - parser.add_argument( - "--mcp-config", default=None, help="Runloop MCP config name for the binding." - ) - parser.add_argument( - "--mcp-secret-name", - default=None, - help="Runloop secret name used by the MCP binding.", - ) - return parser - - -if __name__ == "__main__": - asyncio.run(main(_build_parser().parse_args())) diff --git a/examples/sandbox/extensions/runloop/runner.py b/examples/sandbox/extensions/runloop/runner.py deleted file mode 100644 index 9f7438d608..0000000000 --- a/examples/sandbox/extensions/runloop/runner.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -Minimal Runloop-backed sandbox example for manual validation. - -This mirrors the other cloud extension examples: it creates a tiny workspace, asks a sandboxed -agent to inspect it through one shell tool, and prints a short answer. -""" - -import argparse -import asyncio -import os -import sys -from pathlib import Path - -from openai.types.responses import ResponseTextDeltaEvent - -from agents import ModelSettings, Runner -from agents.run import RunConfig -from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig -from agents.sandbox.snapshot import NoopSnapshotSpec - -if __package__ is None or __package__ == "": - sys.path.insert(0, str(Path(__file__).resolve().parents[4])) - -from examples.sandbox.misc.example_support import text_manifest -from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability - -try: - from agents.extensions.sandbox import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopUserParameters, - ) -except Exception as exc: # pragma: no cover - import path depends on optional extras - raise SystemExit( - "Runloop sandbox examples require the optional repo extra.\n" - "Install it with: uv sync --extra runloop" - ) from exc - - -DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences." - - -def _build_manifest(*, workspace_root: str) -> Manifest: - manifest = text_manifest( - { - "README.md": ( - "# Runloop Demo Workspace\n\n" - "This workspace exists to validate the Runloop sandbox backend manually.\n" - ), - "launch.md": ( - "# Launch\n\n" - "- Customer: Contoso Logistics.\n" - "- Goal: validate the remote sandbox agent path.\n" - "- Current status: Runloop backend smoke and app-server connectivity are passing.\n" - ), - "tasks.md": ( - "# Tasks\n\n" - "1. Inspect the workspace files.\n" - "2. Summarize the setup and any notable status in two sentences.\n" - ), - } - ) - return Manifest(root=workspace_root, entries=manifest.entries) - - -def _require_env(name: str) -> None: - if os.environ.get(name): - return - raise SystemExit(f"{name} must be set before running this example.") - - -async def main( - *, - model: str, - question: str, - pause_on_exit: bool, - blueprint_name: str | None, - root: bool, - stream: bool, -) -> None: - _require_env("OPENAI_API_KEY") - _require_env("RUNLOOP_API_KEY") - - workspace_root = DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT if root else DEFAULT_RUNLOOP_WORKSPACE_ROOT - manifest = _build_manifest(workspace_root=workspace_root) - agent = SandboxAgent( - name="Runloop Sandbox Assistant", - model=model, - instructions=( - "Answer questions about the sandbox workspace. Inspect the files before answering " - "and keep the response concise. " - "Do not invent files or statuses that are not present in the workspace. Cite the " - "file names you inspected." - ), - default_manifest=manifest, - capabilities=[WorkspaceShellCapability()], - model_settings=ModelSettings(tool_choice="required"), - ) - - client = RunloopSandboxClient() - run_config = RunConfig( - sandbox=SandboxRunConfig( - client=client, - options=RunloopSandboxClientOptions( - blueprint_name=blueprint_name, - pause_on_exit=pause_on_exit, - user_parameters=(RunloopUserParameters(username="root", uid=0) if root else None), - ), - snapshot=NoopSnapshotSpec(), - ), - workflow_name="Runloop sandbox example", - ) - - try: - if not stream: - result = await Runner.run(agent, question, run_config=run_config) - print(result.final_output) - return - - stream_result = Runner.run_streamed(agent, question, run_config=run_config) - saw_text_delta = False - async for event in stream_result.stream_events(): - if event.type == "raw_response_event" and isinstance( - event.data, ResponseTextDeltaEvent - ): - if not saw_text_delta: - print("assistant> ", end="", flush=True) - saw_text_delta = True - print(event.data.delta, end="", flush=True) - - if saw_text_delta: - print() - finally: - await client.close() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.") - parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.") - parser.add_argument( - "--pause-on-exit", - action="store_true", - default=False, - help="Suspend the Runloop devbox on shutdown instead of deleting it.", - ) - parser.add_argument( - "--blueprint-name", - default=None, - help="Optional Runloop blueprint name to use when creating the devbox.", - ) - parser.add_argument( - "--root", - action="store_true", - default=False, - help="Launch the Runloop devbox as root. The default home/workspace root becomes /root.", - ) - parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.") - args = parser.parse_args() - - asyncio.run( - main( - model=args.model, - question=args.question, - pause_on_exit=args.pause_on_exit, - blueprint_name=args.blueprint_name, - root=args.root, - stream=args.stream, - ) - ) diff --git a/pyproject.toml b/pyproject.toml index 13b925cace..944cbfda4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,6 @@ modal = [ "protobuf>=5.29.6,!=6.30.*,!=6.31.*,!=6.32.*,!=6.33.0.*,!=6.33.1.*,!=6.33.2.*,!=6.33.3.*,!=6.33.4.*", "pygments>=2.20.0", ] -runloop = ["runloop_api_client>=1.16.0,<2.0.0"] vercel = ["vercel-sandbox>=0.7.0,<0.9", "cbor2>=5.9.0"] s3 = ["boto3>=1.34"] temporal = [ @@ -212,10 +211,6 @@ ignore_missing_imports = true module = ["daytona", "daytona.*"] ignore_missing_imports = true -[[tool.mypy.overrides]] -module = ["runloop_api_client", "runloop_api_client.*"] -ignore_missing_imports = true - [[tool.mypy.overrides]] module = ["blaxel", "blaxel.*"] ignore_missing_imports = true diff --git a/src/agents/extensions/sandbox/__init__.py b/src/agents/extensions/sandbox/__init__.py index 9ea2eb8dc3..82e754aa2b 100644 --- a/src/agents/extensions/sandbox/__init__.py +++ b/src/agents/extensions/sandbox/__init__.py @@ -74,30 +74,6 @@ except Exception: # pragma: no cover _HAS_CLOUDFLARE = False -try: - from .runloop import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT as DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT as DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopAfterIdle as RunloopAfterIdle, - RunloopCloudBucketMountStrategy as RunloopCloudBucketMountStrategy, - RunloopExistingSecret as RunloopExistingSecret, - RunloopGatewaySpec as RunloopGatewaySpec, - RunloopLaunchParameters as RunloopLaunchParameters, - RunloopMcpSpec as RunloopMcpSpec, - RunloopPlatformClient as RunloopPlatformClient, - RunloopSandboxClient as RunloopSandboxClient, - RunloopSandboxClientOptions as RunloopSandboxClientOptions, - RunloopSandboxSession as RunloopSandboxSession, - RunloopSandboxSessionState as RunloopSandboxSessionState, - RunloopTimeouts as RunloopTimeouts, - RunloopTunnelConfig as RunloopTunnelConfig, - RunloopUserParameters as RunloopUserParameters, - ) - - _HAS_RUNLOOP = True -except Exception: # pragma: no cover - _HAS_RUNLOOP = False - try: from .vercel import ( VercelCloudBucketMountStrategy as VercelCloudBucketMountStrategy, @@ -189,25 +165,3 @@ "VercelSandboxSessionState", ] ) - -if _HAS_RUNLOOP: - __all__.extend( - [ - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters", - ] - ) diff --git a/src/agents/extensions/sandbox/runloop/__init__.py b/src/agents/extensions/sandbox/runloop/__init__.py deleted file mode 100644 index 9070307bd0..0000000000 --- a/src/agents/extensions/sandbox/runloop/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from .mounts import RunloopCloudBucketMountStrategy -from .sandbox import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopAfterIdle, - RunloopExistingSecret, - RunloopGatewaySpec, - RunloopLaunchParameters, - RunloopMcpSpec, - RunloopPlatformAxonsClient, - RunloopPlatformBenchmarksClient, - RunloopPlatformBlueprintsClient, - RunloopPlatformClient, - RunloopPlatformNetworkPoliciesClient, - RunloopPlatformSecretsClient, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopSandboxSession, - RunloopSandboxSessionState, - RunloopTimeouts, - RunloopTunnelConfig, - RunloopUserParameters, - _decode_runloop_snapshot_ref, - _encode_runloop_snapshot_ref, -) - -__all__ = [ - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformAxonsClient", - "RunloopPlatformBenchmarksClient", - "RunloopPlatformBlueprintsClient", - "RunloopPlatformClient", - "RunloopPlatformNetworkPoliciesClient", - "RunloopPlatformSecretsClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters", - "_decode_runloop_snapshot_ref", - "_encode_runloop_snapshot_ref", -] diff --git a/src/agents/extensions/sandbox/runloop/mounts.py b/src/agents/extensions/sandbox/runloop/mounts.py deleted file mode 100644 index 3fde66171d..0000000000 --- a/src/agents/extensions/sandbox/runloop/mounts.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Mount strategy for Runloop sandboxes.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Literal - -from ....sandbox._mount_security import ( - redact_mount_error_data, - validate_mount_activation_credential_boundary, -) -from ....sandbox.entries.mounts.base import InContainerMountStrategy, Mount, MountStrategyBase -from ....sandbox.entries.mounts.patterns import RcloneMountPattern -from ....sandbox.errors import MountConfigError -from ....sandbox.materialization import MaterializedFile -from ....sandbox.session.base_sandbox_session import BaseSandboxSession -from .._rclone import ( - ensure_rclone as _ensure_rclone, - rclone_pattern_for_session as _rclone_pattern_for_session, -) - -_APT = "DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0" -_INSTALL_FUSE_COMMANDS = ( - f"{_APT} update -qq", - f"{_APT} install -y -qq fuse3", -) -_FUSE_ALLOW_OTHER = ( - "chmod a+rw /dev/fuse && " - "touch /etc/fuse.conf && " - "(grep -qxF user_allow_other /etc/fuse.conf || " - "printf '\\nuser_allow_other\\n' >> /etc/fuse.conf)" -) - - -async def _ensure_fuse_support(session: BaseSandboxSession) -> None: - dev_fuse = await session.exec("sh", "-lc", "test -c /dev/fuse", shell=False) - if not dev_fuse.ok(): - raise MountConfigError( - message="Runloop cloud bucket mounts require FUSE support", - context={"missing": "/dev/fuse"}, - ) - - kmod = await session.exec("sh", "-lc", "grep -qw fuse /proc/filesystems", shell=False) - if not kmod.ok(): - raise MountConfigError( - message="Runloop cloud bucket mounts require FUSE support", - context={"missing": "fuse in /proc/filesystems"}, - ) - - fusermount = await session.exec( - "sh", - "-lc", - "command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - shell=False, - ) - if not fusermount.ok(): - apt = await session.exec("sh", "-lc", "command -v apt-get >/dev/null 2>&1", shell=False) - if not apt.ok(): - raise MountConfigError( - message="fusermount is not installed and apt-get is unavailable; preinstall fuse3", - context={"package": "fuse3"}, - ) - for command in _INSTALL_FUSE_COMMANDS: - install = await session.exec( - "sh", - "-lc", - command, - shell=False, - timeout=300, - user="root", - ) - if not install.ok(): - raise MountConfigError( - message="failed to install fuse3", - context={"package": "fuse3", "exit_code": install.exit_code}, - ) - - fusermount = await session.exec( - "sh", - "-lc", - "command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - shell=False, - ) - if not fusermount.ok(): - raise MountConfigError( - message="fuse3 was installed but fusermount is still not available", - context={"package": "fuse3"}, - ) - - chmod_result = await session.exec( - "sh", - "-lc", - _FUSE_ALLOW_OTHER, - shell=False, - timeout=30, - user="root", - ) - if not chmod_result.ok(): - raise MountConfigError( - message="failed to make /dev/fuse accessible", - context={"exit_code": chmod_result.exit_code}, - ) - - -def _assert_runloop_session(session: BaseSandboxSession) -> None: - if type(session).__name__ != "RunloopSandboxSession": - raise MountConfigError( - message="runloop cloud bucket mounts require a RunloopSandboxSession", - context={"session_type": type(session).__name__}, - ) - - -class RunloopCloudBucketMountStrategy(MountStrategyBase): - """Mount rclone-backed cloud storage in Runloop sandboxes.""" - - type: Literal["runloop_cloud_bucket"] = "runloop_cloud_bucket" - pattern: RcloneMountPattern = RcloneMountPattern(mode="fuse") - - def _delegate(self) -> InContainerMountStrategy: - return InContainerMountStrategy(pattern=self.pattern) - - async def _delegate_for_session(self, session: BaseSandboxSession) -> InContainerMountStrategy: - return InContainerMountStrategy( - pattern=await _rclone_pattern_for_session(session, self.pattern) - ) - - def validate_mount(self, mount: Mount) -> None: - self._delegate().validate_mount(mount) - - @redact_mount_error_data - async def activate( - self, - mount: Mount, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> list[MaterializedFile]: - validate_mount_activation_credential_boundary( - mount, - self, - manifest=getattr(getattr(session, "state", None), "manifest", None), - mount_path=lambda: mount._resolve_mount_path(session, dest), - provider_backend_id="runloop", - ) - _assert_runloop_session(session) - if self.pattern.mode == "fuse": - await _ensure_fuse_support(session) - await _ensure_rclone(session) - delegate = await self._delegate_for_session(session) - return await delegate.activate(mount, session, dest, base_dir) - - @redact_mount_error_data - async def deactivate( - self, - mount: Mount, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> None: - _assert_runloop_session(session) - await self._delegate().deactivate(mount, session, dest, base_dir) - - @redact_mount_error_data - async def teardown_for_snapshot( - self, - mount: Mount, - session: BaseSandboxSession, - path: Path, - ) -> None: - _assert_runloop_session(session) - await self._delegate().teardown_for_snapshot(mount, session, path) - - @redact_mount_error_data - async def restore_after_snapshot( - self, - mount: Mount, - session: BaseSandboxSession, - path: Path, - ) -> None: - validate_mount_activation_credential_boundary( - mount, - self, - manifest=getattr(getattr(session, "state", None), "manifest", None), - mount_path=path, - provider_backend_id="runloop", - ) - _assert_runloop_session(session) - if self.pattern.mode == "fuse": - await _ensure_fuse_support(session) - await _ensure_rclone(session) - delegate = await self._delegate_for_session(session) - await delegate.restore_after_snapshot(mount, session, path) - - def build_docker_volume_driver_config( - self, - mount: Mount, - ) -> tuple[str, dict[str, str], bool] | None: - return None - - -__all__ = [ - "RunloopCloudBucketMountStrategy", -] diff --git a/src/agents/extensions/sandbox/runloop/sandbox.py b/src/agents/extensions/sandbox/runloop/sandbox.py deleted file mode 100644 index 2d3477218c..0000000000 --- a/src/agents/extensions/sandbox/runloop/sandbox.py +++ /dev/null @@ -1,1763 +0,0 @@ -""" -Runloop sandbox (https://runloop.ai) implementation. - -This module provides a Runloop-backed sandbox client/session implementation backed by -`runloop_api_client.sdk.AsyncRunloopSDK`. - -The `runloop_api_client` dependency is optional, so package-level exports should guard imports of -this module. Within this module, Runloop SDK imports are lazy so users without the extra can still -import the package. -""" - -from __future__ import annotations - -import asyncio -import base64 -import io -import json -import logging -import posixpath -import shlex -import uuid -from collections.abc import Sequence -from dataclasses import dataclass -from pathlib import Path, PurePosixPath -from typing import TYPE_CHECKING, Any, Literal, cast -from urllib.parse import urlsplit - -from pydantic import BaseModel, Field -from runloop_api_client.types import ( - AfterIdle as _RunloopSdkAfterIdle, - LaunchParameters as _RunloopSdkLaunchParameters, -) -from runloop_api_client.types.shared.launch_parameters import ( - UserParameters as _RunloopSdkUserParameters, -) - -from ....sandbox._mount_security import redact_mount_error_data -from ....sandbox.entries import Mount -from ....sandbox.errors import ( - ExecTimeoutError, - ExecTransportError, - ExposedPortUnavailableError, - WorkspaceArchiveReadError, - WorkspaceArchiveWriteError, - WorkspaceReadNotFoundError, - WorkspaceWriteTypeError, -) -from ....sandbox.manifest import Manifest -from ....sandbox.session import SandboxSession, SandboxSessionState -from ....sandbox.session.base_sandbox_session import BaseSandboxSession -from ....sandbox.session.bounded_read import collect_bounded -from ....sandbox.session.dependencies import Dependencies -from ....sandbox.session.manager import Instrumentation -from ....sandbox.session.runtime_helpers import RESOLVE_WORKSPACE_PATH_HELPER, RuntimeHelperScript -from ....sandbox.session.sandbox_client import BaseSandboxClient, BaseSandboxClientOptions -from ....sandbox.snapshot import SnapshotBase, SnapshotSpec, resolve_snapshot -from ....sandbox.types import ExecResult, ExposedPortEndpoint, User -from ....sandbox.util.retry import iter_exception_chain -from ....sandbox.util.tar_utils import UnsafeTarMemberError, validate_tar_bytes -from ....sandbox.workspace_paths import coerce_posix_path, posix_path_as_path, sandbox_path_str - -if TYPE_CHECKING: - from runloop_api_client.sdk.async_execution_result import ( - AsyncExecutionResult as RunloopAsyncExecutionResult, - ) - from runloop_api_client.sdk.async_snapshot import AsyncSnapshot as RunloopAsyncSnapshot - from runloop_api_client.types.devbox_view import DevboxView as RunloopDevboxView - -DEFAULT_RUNLOOP_WORKSPACE_ROOT = "/home/user" -DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT = "/root" -_RUNLOOP_DEFAULT_HOME = PurePosixPath("/home/user") -_RUNLOOP_ROOT_HOME = PurePosixPath("/root") -_RUNLOOP_SANDBOX_SNAPSHOT_MAGIC = b"RUNLOOP_SANDBOX_SNAPSHOT_V1\n" - -logger = logging.getLogger(__name__) - -RunloopAfterIdle = _RunloopSdkAfterIdle -RunloopLaunchParameters = _RunloopSdkLaunchParameters -RunloopUserParameters = _RunloopSdkUserParameters - - -@dataclass(frozen=True) -class _RunloopSdkImports: - async_sdk: type[Any] - api_connection_error: type[BaseException] - api_response_validation_error: type[BaseException] - api_status_error: type[BaseException] - api_timeout_error: type[BaseException] - authentication_error: type[BaseException] - bad_request_error: type[BaseException] - internal_server_error: type[BaseException] - not_found_error: type[BaseException] - permission_denied_error: type[BaseException] - polling_config: type[Any] | None - polling_timeout: type[BaseException] | None - rate_limit_error: type[BaseException] - runloop_error: type[BaseException] - unprocessable_entity_error: type[BaseException] - - -_RUNLOOP_SDK_IMPORTS: _RunloopSdkImports | None = None - - -def _import_runloop_sdk() -> _RunloopSdkImports: - global _RUNLOOP_SDK_IMPORTS - if _RUNLOOP_SDK_IMPORTS is not None: - return _RUNLOOP_SDK_IMPORTS - - try: - from runloop_api_client import ( - APIConnectionError, - APIResponseValidationError, - APIStatusError, - APITimeoutError, - AuthenticationError, - BadRequestError, - InternalServerError, - NotFoundError, - PermissionDeniedError, - RateLimitError, - RunloopError, - UnprocessableEntityError, - ) - from runloop_api_client.sdk import AsyncRunloopSDK - except ImportError as e: - raise ImportError( - "RunloopSandboxClient requires the optional `runloop_api_client` dependency.\n" - "Install the Runloop extra before using this sandbox backend." - ) from e - - polling_config: type[Any] | None = None - polling_timeout: type[BaseException] | None = None - try: - from runloop_api_client.lib.polling import ( - PollingConfig as RunloopPollingConfig, - PollingTimeout as RunloopPollingTimeout, - ) - except ImportError: - pass - else: - polling_config = RunloopPollingConfig - polling_timeout = RunloopPollingTimeout - - _RUNLOOP_SDK_IMPORTS = _RunloopSdkImports( - async_sdk=AsyncRunloopSDK, - api_connection_error=APIConnectionError, - api_response_validation_error=APIResponseValidationError, - api_status_error=APIStatusError, - api_timeout_error=APITimeoutError, - authentication_error=AuthenticationError, - bad_request_error=BadRequestError, - internal_server_error=InternalServerError, - not_found_error=NotFoundError, - permission_denied_error=PermissionDeniedError, - polling_config=polling_config, - polling_timeout=polling_timeout, - rate_limit_error=RateLimitError, - runloop_error=RunloopError, - unprocessable_entity_error=UnprocessableEntityError, - ) - return _RUNLOOP_SDK_IMPORTS - - -def _encode_runloop_snapshot_ref(*, snapshot_id: str) -> bytes: - body = json.dumps({"snapshot_id": snapshot_id}, separators=(",", ":"), sort_keys=True).encode( - "utf-8" - ) - return _RUNLOOP_SANDBOX_SNAPSHOT_MAGIC + body - - -def _decode_runloop_snapshot_ref(raw: bytes) -> str | None: - if not raw.startswith(_RUNLOOP_SANDBOX_SNAPSHOT_MAGIC): - return None - body = raw[len(_RUNLOOP_SANDBOX_SNAPSHOT_MAGIC) :] - try: - obj = json.loads(body.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError): - return None - snapshot_id = obj.get("snapshot_id") if isinstance(obj, dict) else None - return snapshot_id if isinstance(snapshot_id, str) and snapshot_id else None - - -def _runloop_json_safe_body(body: object) -> tuple[str, object] | None: - if isinstance(body, str | int | float | bool) or body is None: - return ("provider_body", body) - if isinstance(body, dict | list): - try: - json.dumps(body) - except TypeError: - return ("provider_body_repr", repr(body)) - return ("provider_body", body) - return ("provider_body_repr", repr(body)) - - -def _runloop_error_context( - exc: BaseException, - *, - backend_detail: str | None = None, -) -> dict[str, object]: - context: dict[str, object] = { - "backend": "runloop", - "cause_type": type(exc).__name__, - } - if backend_detail is not None: - context["detail"] = backend_detail - - message = getattr(exc, "message", None) - if isinstance(message, str) and message: - context["provider_message"] = message - else: - provider_message = str(exc) - if provider_message: - context["provider_message"] = provider_message - - status_code = getattr(exc, "status_code", None) - response = getattr(exc, "response", None) - if not isinstance(status_code, int): - response_status = getattr(response, "status_code", None) - if isinstance(response_status, int): - status_code = response_status - if isinstance(status_code, int): - context["http_status"] = status_code - - request = getattr(exc, "request", None) - request_url = getattr(request, "url", None) - if request_url is not None: - context["request_url"] = str(request_url) - request_method = getattr(request, "method", None) - if isinstance(request_method, str) and request_method: - context["request_method"] = request_method - - if hasattr(exc, "body"): - safe_body = _runloop_json_safe_body(getattr(exc, "body", None)) - if safe_body is not None: - context[safe_body[0]] = safe_body[1] - - return context - - -def _is_runloop_timeout(exc: BaseException) -> bool: - polling_timeout = _import_runloop_sdk().polling_timeout - if polling_timeout is not None and isinstance(exc, polling_timeout): - return True - if isinstance(exc, _import_runloop_sdk().api_timeout_error): - return True - if isinstance(exc, _import_runloop_sdk().api_status_error): - status_code = getattr(exc, "status_code", None) - response = getattr(exc, "response", None) - if not isinstance(status_code, int): - response_status = getattr(response, "status_code", None) - if isinstance(response_status, int): - status_code = response_status - return status_code == 408 - return False - - -def _runloop_status_code(exc: BaseException) -> int | None: - status_code = getattr(exc, "status_code", None) - response = getattr(exc, "response", None) - if not isinstance(status_code, int): - response_status = getattr(response, "status_code", None) - if isinstance(response_status, int): - status_code = response_status - return status_code if isinstance(status_code, int) else None - - -def _runloop_error_message(exc: BaseException) -> str | None: - body = getattr(exc, "body", None) - if isinstance(body, dict): - message = body.get("message") or body.get("error") - if isinstance(message, str) and message: - return message - - message = getattr(exc, "message", None) - if isinstance(message, str) and message: - return message - - if exc.args: - first = exc.args[0] - if isinstance(first, str) and first: - return first - - return None - - -_RUNLOOP_HTTP_STATUS_RETRYABLE: dict[int, bool] = { - 400: False, - 401: False, - 403: False, - 404: False, - 408: True, - 422: False, - 429: True, - 500: True, - 502: True, - 503: True, - 504: True, -} - - -def _runloop_retryable_error_types() -> tuple[type[BaseException], ...]: - sdk_imports = _import_runloop_sdk() - return ( - sdk_imports.api_connection_error, - sdk_imports.api_timeout_error, - sdk_imports.internal_server_error, - sdk_imports.rate_limit_error, - ) - - -def _runloop_non_retryable_error_types() -> tuple[type[BaseException], ...]: - sdk_imports = _import_runloop_sdk() - return ( - sdk_imports.authentication_error, - sdk_imports.bad_request_error, - sdk_imports.not_found_error, - sdk_imports.permission_denied_error, - sdk_imports.unprocessable_entity_error, - ) - - -def _runloop_provider_retryability(exc: BaseException) -> bool | None: - retryable_error_types = _runloop_retryable_error_types() - non_retryable_error_types = _runloop_non_retryable_error_types() - for candidate in iter_exception_chain(exc): - if isinstance(candidate, retryable_error_types): - return True - if isinstance(candidate, non_retryable_error_types): - return False - status_code = _runloop_status_code(candidate) - if status_code in _RUNLOOP_HTTP_STATUS_RETRYABLE: - return _RUNLOOP_HTTP_STATUS_RETRYABLE[status_code] - return None - - -def _runloop_provider_error_types() -> tuple[type[BaseException], ...]: - sdk_imports = _import_runloop_sdk() - return ( - sdk_imports.api_connection_error, - sdk_imports.api_response_validation_error, - sdk_imports.api_status_error, - sdk_imports.runloop_error, - ) - - -def _is_runloop_not_found(exc: BaseException) -> bool: - return isinstance(exc, _import_runloop_sdk().not_found_error) - - -def _is_runloop_conflict(exc: BaseException) -> bool: - if not isinstance(exc, _import_runloop_sdk().api_status_error): - return False - - status_code = _runloop_status_code(exc) - if status_code == 409: - return True - - message = _runloop_error_message(exc) - if status_code == 400 and isinstance(message, str): - return "already exists" in message.lower() - - return False - - -def _runloop_polling_config(*, timeout_s: float | None) -> object | None: - if timeout_s is None: - return None - polling_config = _import_runloop_sdk().polling_config - if polling_config is None: - return None - return cast(object, polling_config(timeout_seconds=max(float(timeout_s), 0.001))) - - -def _is_runloop_provider_error(exc: BaseException) -> bool: - return isinstance( - exc, - _runloop_provider_error_types(), - ) - - -class RunloopTimeouts(BaseModel): - """Timeout configuration for Runloop sandbox operations.""" - - model_config = {"frozen": True} - - exec_timeout_unbounded_s: float = Field(default=24 * 60 * 60, ge=1) - create_s: float = Field(default=300.0, ge=1) - keepalive_s: float = Field(default=10.0, ge=1) - cleanup_s: float = Field(default=30.0, ge=1) - fast_op_s: float = Field(default=30.0, ge=1) - file_upload_s: float = Field(default=1800.0, ge=1) - file_download_s: float = Field(default=1800.0, ge=1) - snapshot_s: float = Field(default=300.0, ge=1) - suspend_s: float = Field(default=120.0, ge=1) - resume_s: float = Field(default=300.0, ge=1) - - -class RunloopTunnelConfig(BaseModel): - """Runloop public tunnel configuration.""" - - model_config = {"frozen": True} - - auth_mode: Literal["open", "authenticated"] | None = None - http_keep_alive: bool | None = None - wake_on_http: bool | None = None - - -class RunloopGatewaySpec(BaseModel): - """Runloop agent gateway binding.""" - - model_config = {"frozen": True} - - gateway: str = Field(min_length=1) - secret: str = Field(min_length=1) - - -class RunloopMcpSpec(BaseModel): - """Runloop MCP gateway binding.""" - - model_config = {"frozen": True} - - mcp_config: str = Field(min_length=1) - secret: str = Field(min_length=1) - - -class RunloopExistingSecret(BaseModel): - """A `managed_secrets` entry that is already stored on the Runloop account. - - Used in place of a secret value to attach an account secret to the devbox by name, - leaving the stored value untouched. - """ - - model_config = {"frozen": True, "extra": "forbid"} - - -def _normalize_runloop_user_parameters( - user_parameters: RunloopUserParameters | dict[str, object] | None, -) -> RunloopUserParameters | None: - if isinstance(user_parameters, RunloopUserParameters): - return user_parameters - if user_parameters is None: - return None - if isinstance(user_parameters, BaseModel): - return RunloopUserParameters.model_validate(user_parameters.model_dump(mode="json")) - return RunloopUserParameters.model_validate(user_parameters) - - -def _normalize_runloop_launch_parameters( - launch_parameters: RunloopLaunchParameters | dict[str, object] | None, -) -> RunloopLaunchParameters | None: - if isinstance(launch_parameters, RunloopLaunchParameters): - return launch_parameters - if launch_parameters is None: - return None - if isinstance(launch_parameters, BaseModel): - return RunloopLaunchParameters.model_validate(launch_parameters.model_dump(mode="json")) - return RunloopLaunchParameters.model_validate(launch_parameters) - - -def _normalize_runloop_tunnel_config( - tunnel: RunloopTunnelConfig | dict[str, object] | None, -) -> RunloopTunnelConfig | None: - if isinstance(tunnel, RunloopTunnelConfig): - return tunnel - if tunnel is None: - return None - if isinstance(tunnel, BaseModel): - return RunloopTunnelConfig.model_validate(tunnel.model_dump(mode="json")) - return RunloopTunnelConfig.model_validate(tunnel) - - -class RunloopSandboxClientOptions(BaseSandboxClientOptions): - """Client options for the Runloop sandbox.""" - - type: Literal["runloop"] = "runloop" - blueprint_id: str | None = None - blueprint_name: str | None = None - env_vars: dict[str, str] | None = None - pause_on_exit: bool = False - name: str | None = None - timeouts: RunloopTimeouts | dict[str, object] | None = None - exposed_ports: tuple[int, ...] = () - user_parameters: RunloopUserParameters | dict[str, object] | None = None - launch_parameters: RunloopLaunchParameters | dict[str, object] | None = None - tunnel: RunloopTunnelConfig | dict[str, object] | None = None - gateways: dict[str, RunloopGatewaySpec] | None = None - mcp: dict[str, RunloopMcpSpec] | None = None - metadata: dict[str, str] | None = None - managed_secrets: dict[str, str | RunloopExistingSecret] | None = None - - def __init__( - self, - blueprint_id: str | None = None, - blueprint_name: str | None = None, - env_vars: dict[str, str] | None = None, - pause_on_exit: bool = False, - name: str | None = None, - timeouts: RunloopTimeouts | dict[str, object] | None = None, - exposed_ports: tuple[int, ...] = (), - user_parameters: RunloopUserParameters | dict[str, object] | None = None, - launch_parameters: RunloopLaunchParameters | dict[str, object] | None = None, - tunnel: RunloopTunnelConfig | dict[str, object] | None = None, - gateways: dict[str, RunloopGatewaySpec] | None = None, - mcp: dict[str, RunloopMcpSpec] | None = None, - metadata: dict[str, str] | None = None, - managed_secrets: dict[str, str | RunloopExistingSecret] | None = None, - *, - type: Literal["runloop"] = "runloop", - ) -> None: - super().__init__( - type=type, - blueprint_id=blueprint_id, - blueprint_name=blueprint_name, - env_vars=env_vars, - pause_on_exit=pause_on_exit, - name=name, - timeouts=timeouts, - exposed_ports=exposed_ports, - user_parameters=user_parameters, - launch_parameters=launch_parameters, - tunnel=tunnel, - gateways=gateways, - mcp=mcp, - metadata=metadata, - managed_secrets=managed_secrets, - ) - - -class RunloopSandboxSessionState(SandboxSessionState): - """Serializable state for a Runloop-backed session.""" - - type: Literal["runloop"] = "runloop" - devbox_id: str - blueprint_id: str | None = None - blueprint_name: str | None = None - base_env_vars: dict[str, str] = Field(default_factory=dict) - pause_on_exit: bool = False - name: str | None = None - timeouts: RunloopTimeouts = Field(default_factory=RunloopTimeouts) - user_parameters: RunloopUserParameters | None = None - launch_parameters: RunloopLaunchParameters | None = None - tunnel: RunloopTunnelConfig | None = None - gateways: dict[str, RunloopGatewaySpec] = Field(default_factory=dict) - mcp: dict[str, RunloopMcpSpec] = Field(default_factory=dict) - metadata: dict[str, str] = Field(default_factory=dict) - secret_refs: dict[str, str] = Field(default_factory=dict) - - -@dataclass(frozen=True) -class RunloopPlatformBlueprintsClient: - _sdk: Any - - async def list(self, **params: object) -> object: - return await self._sdk.blueprint.list(**params) - - async def list_public(self, **params: object) -> object: - return await self._sdk.api.blueprints.list_public(**params) - - def get(self, blueprint_id: str) -> Any: - return self._sdk.blueprint.from_id(blueprint_id) - - async def logs(self, blueprint_id: str, **params: object) -> object: - return await self._sdk.api.blueprints.logs(blueprint_id, **params) - - async def create(self, **params: object) -> object: - return await self._sdk.blueprint.create(**params) - - async def await_build_complete(self, blueprint_id: str, **params: object) -> object: - return await self._sdk.api.blueprints.await_build_complete(blueprint_id, **params) - - async def delete(self, blueprint_id: str, **params: object) -> object: - return await self.get(blueprint_id).delete(**params) - - -@dataclass(frozen=True) -class RunloopPlatformBenchmarksClient: - _sdk: Any - - async def list(self, **params: object) -> object: - return await self._sdk.benchmark.list(**params) - - async def list_public(self, **params: object) -> object: - return await self._sdk.api.benchmarks.list_public(**params) - - def get(self, benchmark_id: str) -> Any: - return self._sdk.benchmark.from_id(benchmark_id) - - async def create(self, **params: object) -> object: - return await self._sdk.benchmark.create(**params) - - async def update(self, benchmark_id: str, **params: object) -> object: - return await self.get(benchmark_id).update(**params) - - async def definitions(self, benchmark_id: str, **params: object) -> object: - return await self._sdk.api.benchmarks.definitions(benchmark_id, **params) - - async def start_run(self, benchmark_id: str, **params: object) -> object: - return await self.get(benchmark_id).start_run(**params) - - async def update_scenarios( - self, - benchmark_id: str, - *, - scenarios_to_add: tuple[str, ...] | Sequence[str] | None = None, - scenarios_to_remove: tuple[str, ...] | Sequence[str] | None = None, - **params: object, - ) -> object: - return await self._sdk.api.benchmarks.update_scenarios( - benchmark_id, - scenarios_to_add=scenarios_to_add, - scenarios_to_remove=scenarios_to_remove, - **params, - ) - - -@dataclass(frozen=True) -class RunloopPlatformSecretsClient: - _sdk: Any - - async def create(self, *, name: str, value: str, **params: object) -> object: - return await self._sdk.secret.create(name=name, value=value, **params) - - async def list(self, **params: object) -> object: - return await self._sdk.secret.list(**params) - - async def get(self, name: str, **params: object) -> object: - return await self._sdk.api.secrets.retrieve(name, **params) - - async def update(self, *, name: str, value: str, **params: object) -> object: - return await self._sdk.secret.update(name, value=value, **params) - - async def delete(self, name: str, **params: object) -> object: - return await self._sdk.secret.delete(name, **params) - - -@dataclass(frozen=True) -class RunloopPlatformNetworkPoliciesClient: - _sdk: Any - - async def create(self, **params: object) -> object: - return await self._sdk.network_policy.create(**params) - - async def list(self, **params: object) -> object: - return await self._sdk.network_policy.list(**params) - - def get(self, network_policy_id: str) -> Any: - return self._sdk.network_policy.from_id(network_policy_id) - - async def update(self, network_policy_id: str, **params: object) -> object: - return await self.get(network_policy_id).update(**params) - - async def delete(self, network_policy_id: str, **params: object) -> object: - return await self.get(network_policy_id).delete(**params) - - -@dataclass(frozen=True) -class RunloopPlatformAxonsClient: - _sdk: Any - - async def create(self, **params: object) -> object: - return await self._sdk.axon.create(**params) - - async def list(self, **params: object) -> object: - return await self._sdk.axon.list(**params) - - def get(self, axon_id: str) -> Any: - return self._sdk.axon.from_id(axon_id) - - async def publish(self, axon_id: str, **params: object) -> object: - return await self.get(axon_id).publish(**params) - - async def query_sql(self, axon_id: str, **params: object) -> object: - return await self.get(axon_id).sql.query(**params) - - async def batch_sql(self, axon_id: str, **params: object) -> object: - return await self.get(axon_id).sql.batch(**params) - - -@dataclass(frozen=True) -class RunloopPlatformClient: - """Thin facade over the Runloop SDK's non-devbox platform resources.""" - - _sdk: Any - - @property - def blueprints(self) -> RunloopPlatformBlueprintsClient: - return RunloopPlatformBlueprintsClient(self._sdk) - - @property - def benchmarks(self) -> RunloopPlatformBenchmarksClient: - return RunloopPlatformBenchmarksClient(self._sdk) - - @property - def secrets(self) -> RunloopPlatformSecretsClient: - return RunloopPlatformSecretsClient(self._sdk) - - @property - def network_policies(self) -> RunloopPlatformNetworkPoliciesClient: - return RunloopPlatformNetworkPoliciesClient(self._sdk) - - @property - def axons(self) -> RunloopPlatformAxonsClient: - return RunloopPlatformAxonsClient(self._sdk) - - -class RunloopSandboxSession(BaseSandboxSession): - """Runloop-backed sandbox session implementation.""" - - state: RunloopSandboxSessionState - _sdk: Any - _devbox: Any - _skip_start: bool - - def __init__(self, *, state: RunloopSandboxSessionState, sdk: Any, devbox: Any) -> None: - self.state = state - self._sdk = sdk - self._devbox = devbox - self._skip_start = False - - @classmethod - def from_state( - cls, - state: RunloopSandboxSessionState, - *, - sdk: Any, - devbox: Any, - ) -> RunloopSandboxSession: - return cls(state=state, sdk=sdk, devbox=devbox) - - @property - def devbox_id(self) -> str: - return self.state.devbox_id - - @property - def runloop_home(self) -> PurePosixPath: - return _effective_runloop_home(self.state.user_parameters) - - async def _resolved_envs(self) -> dict[str, str]: - manifest_envs = await self.state.manifest.environment.resolve() - return {**self.state.base_env_vars, **manifest_envs} - - def _coerce_exec_timeout(self, timeout_s: float | None) -> float: - if timeout_s is None: - return float(self.state.timeouts.exec_timeout_unbounded_s) - if timeout_s <= 0: - return 0.001 - return float(timeout_s) - - @redact_mount_error_data - async def start(self) -> None: - """Resume a reconnected Runloop devbox without replaying full setup when possible. - - `resume()` marks `_skip_start` when it successfully reconnects to a suspended devbox. - In that path, Runloop reuses the live machine and only reapplies snapshot or ephemeral - manifest state if the cached workspace fingerprint no longer matches. - """ - await self._validate_manifest_application() - if self._skip_start: - if await self.state.snapshot.restorable(dependencies=self.dependencies): - is_running = await self.running() - fingerprints_match = await self._can_skip_snapshot_restore_on_resume( - is_running=is_running - ) - if fingerprints_match: - await self._reapply_ephemeral_manifest_on_resume() - else: - await self._restore_snapshot_into_workspace_on_resume() - if self.should_provision_manifest_accounts_on_resume(): - await self.provision_manifest_accounts() - await self._reapply_ephemeral_manifest_on_resume() - else: - await self._reapply_ephemeral_manifest_on_resume() - return - await super().start() - - async def shutdown(self) -> None: - """Suspend or delete the underlying Runloop devbox as the final session cleanup step. - - `pause_on_exit=True` maps to Runloop suspension so the same devbox can be resumed later. - Otherwise the session shuts the devbox down and treats it as disposable. - """ - try: - if self.state.pause_on_exit: - await self._devbox.suspend(timeout=self.state.timeouts.suspend_s) - await self._devbox.await_suspended() - else: - await self._devbox.shutdown(timeout=self.state.timeouts.cleanup_s) - except Exception: - pass - - def supports_pty(self) -> bool: - return False - - async def _validate_path_access(self, path: Path | str, *, for_write: bool = False) -> Path: - return await self._validate_remote_path_access(path, for_write=for_write) - - def _runtime_helpers(self) -> tuple[RuntimeHelperScript, ...]: - return (RESOLVE_WORKSPACE_PATH_HELPER,) - - async def _wrap_command_in_workspace_context(self, command: str) -> str: - root_q = shlex.quote(self.state.manifest.root) - envs = await self._resolved_envs() - if not envs: - return f"cd {root_q} && {command}" - - env_assignments = " ".join( - shlex.quote(f"{key}={value}") for key, value in sorted(envs.items()) - ) - return f"cd {root_q} && env -- {env_assignments} {command}" - - async def _exec_internal( - self, - *command: str | Path, - timeout: float | None = None, - ) -> ExecResult: - cmd_str = await self._wrap_command_in_workspace_context(shlex.join(str(c) for c in command)) - return await self._run_exec_command( - cmd_str, - command=command, - timeout=timeout, - ) - - async def _run_exec_command( - self, - cmd_str: str, - *, - command: tuple[str | Path, ...], - timeout: float | None, - ) -> ExecResult: - caller_timeout = self._coerce_exec_timeout(timeout) - request_timeout = min(caller_timeout, self.state.timeouts.fast_op_s) - polling_config = _runloop_polling_config(timeout_s=caller_timeout) - - try: - result: RunloopAsyncExecutionResult = await asyncio.wait_for( - self._devbox.cmd.exec( - cmd_str, - timeout=request_timeout, - polling_config=polling_config, - ), - timeout=caller_timeout, - ) - stdout = (await result.stdout()).encode("utf-8", errors="replace") - stderr = (await result.stderr()).encode("utf-8", errors="replace") - exit_code = int(result.exit_code or 0) - return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code) - except asyncio.TimeoutError as e: - raise ExecTimeoutError( - command=command, - timeout_s=timeout, - context=_runloop_error_context(e, backend_detail="exec_timeout"), - cause=e, - ) from e - except Exception as e: - if _is_runloop_timeout(e): - raise ExecTimeoutError( - command=command, - timeout_s=timeout, - context=_runloop_error_context(e, backend_detail="exec_timeout"), - cause=e, - ) from e - if _is_runloop_provider_error(e): - raise ExecTransportError( - command=command, - context=_runloop_error_context(e, backend_detail="exec_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise ExecTransportError(command=command, cause=e) from e - - async def _ensure_tunnel_url(self, port: int) -> str: - try: - url = await self._devbox.get_tunnel_url(port, timeout=self.state.timeouts.fast_op_s) - except Exception as e: - if _is_runloop_provider_error(e): - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context=_runloop_error_context(e, backend_detail="get_tunnel_url_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise - if isinstance(url, str) and url: - return url - - try: - await self._devbox.net.enable_tunnel( - auth_mode="open", - http_keep_alive=True, - wake_on_http=False, - timeout=self.state.timeouts.fast_op_s, - ) - except Exception as e: - if _is_runloop_provider_error(e): - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context=_runloop_error_context(e, backend_detail="enable_tunnel_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise - try: - url = await self._devbox.get_tunnel_url(port, timeout=self.state.timeouts.fast_op_s) - except Exception as e: - if _is_runloop_provider_error(e): - context = _runloop_error_context(e, backend_detail="get_tunnel_url_failed") - context["phase"] = "post_enable" - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context=context, - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise - if not isinstance(url, str) or not url: - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context={"backend": "runloop", "detail": "missing_tunnel_url"}, - ) - return url - - async def resolve_exposed_port(self, port: int) -> ExposedPortEndpoint: - """Resolve an exposed Runloop port through the provider-managed tunnel endpoint. - - Runloop may not have a tunnel enabled for a devbox yet, so exposed-port resolution can - trigger tunnel creation before returning the public host, port, and TLS settings. - """ - - return await super().resolve_exposed_port(port) - - async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint: - try: - url = await self._ensure_tunnel_url(port) - split = urlsplit(url) - host = split.hostname - if host is None: - raise ValueError("missing hostname") - port_value = split.port or (443 if split.scheme == "https" else 80) - return ExposedPortEndpoint(host=host, port=port_value, tls=split.scheme == "https") - except ExposedPortUnavailableError: - raise - except Exception as e: - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context={"backend": "runloop", "detail": "invalid_tunnel_url"}, - cause=e, - ) from e - - async def _read_bounded(self, path: Path, *, max_bytes: int) -> bytes: - normalized_path = await self._validate_path_access(path) - try: - async with self._sdk.api.devboxes.with_streaming_response.download_file( - self.devbox_id, - path=sandbox_path_str(normalized_path), - timeout=self.state.timeouts.file_download_s, - ) as response: - return await collect_bounded(response.iter_bytes(chunk_size=65536), max_bytes) - except Exception as error: - if _is_runloop_not_found(error): - raise WorkspaceReadNotFoundError(path=path) from None - if _is_runloop_provider_error(error): - raise WorkspaceArchiveReadError( - path=path, retryable=_runloop_provider_retryability(error) - ) from None - raise - - async def read(self, path: Path | str, *, user: str | User | None = None) -> io.IOBase: - """Read a file via Runloop's binary file API.""" - error_path = posix_path_as_path(coerce_posix_path(path)) - if user is not None: - await self._check_read_with_exec(path, user=user) - - normalized_path = await self._validate_path_access(path) - try: - payload = await self._devbox.file.download( - path=sandbox_path_str(normalized_path), - timeout=self.state.timeouts.file_download_s, - ) - return io.BytesIO(bytes(payload)) - except Exception as e: - if _is_runloop_not_found(e): - raise WorkspaceReadNotFoundError( - path=error_path, - context=_runloop_error_context(e, backend_detail="file_download_failed"), - cause=e, - ) from e - if _is_runloop_provider_error(e): - raise WorkspaceArchiveReadError( - path=error_path, - context=_runloop_error_context(e, backend_detail="file_download_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise WorkspaceArchiveReadError(path=error_path, cause=e) from e - - async def write( - self, - path: Path | str, - data: io.IOBase, - *, - user: str | User | None = None, - ) -> None: - """Write a file through Runloop's upload API using manifest-root workspace paths.""" - error_path = posix_path_as_path(coerce_posix_path(path)) - if user is not None: - await self._check_write_with_exec(path, user=user) - - payload = data.read() - if isinstance(payload, str): - payload = payload.encode("utf-8") - if not isinstance(payload, bytes | bytearray): - raise WorkspaceWriteTypeError(path=error_path, actual_type=type(payload).__name__) - - workspace_path = await self._validate_path_access(path, for_write=True) - await self.mkdir(workspace_path.parent, parents=True) - try: - await self._devbox.file.upload( - path=sandbox_path_str(workspace_path), - file=bytes(payload), - timeout=self.state.timeouts.file_upload_s, - ) - except Exception as e: - if _is_runloop_provider_error(e): - raise WorkspaceArchiveWriteError( - path=workspace_path, - context=_runloop_error_context(e, backend_detail="file_upload_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise WorkspaceArchiveWriteError(path=workspace_path, cause=e) from e - - async def running(self) -> bool: - """Report whether the current Runloop devbox is still in the `running` backend state. - - Resume logic relies on this backend status check before deciding whether a suspended devbox - can be reused directly or whether snapshot restore must rebuild the workspace elsewhere. - """ - try: - info: RunloopDevboxView = await self._devbox.get_info( - timeout=self.state.timeouts.keepalive_s - ) - return cast(str, info.status) == "running" - except Exception: - return False - - async def mkdir( - self, - path: Path | str, - *, - parents: bool = False, - user: str | User | None = None, - ) -> None: - """Create directories via raw exec so workspace-root creation does not depend on `cd`.""" - - if user is not None: - path = await self._check_mkdir_with_exec(path, parents=parents, user=user) - else: - path = await self._validate_path_access(path, for_write=True) - cmd = ["mkdir"] - if parents: - cmd.append("-p") - cmd.extend(["--", sandbox_path_str(path)]) - result = await self._run_exec_command( - shlex.join(cmd), - command=tuple(cmd), - timeout=self.state.timeouts.fast_op_s, - ) - if not result.ok(): - raise WorkspaceArchiveWriteError( - path=path, - context={ - "reason": "mkdir_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - - async def _backup_plain_skip_paths(self, plain_skip: set[Path]) -> bytes | None: - if not plain_skip: - return None - - root = sandbox_path_str(self.state.manifest.root) - root_q = shlex.quote(root) - checks = "\n".join( - ( - f"if [ -e {shlex.quote(rel.as_posix())} ]; then " - f'set -- "$@" {shlex.quote(rel.as_posix())}; fi' - ) - for rel in sorted(plain_skip, key=lambda p: p.as_posix()) - ) - command = ( - f"cd {root_q}\n" - "set --\n" - f"{checks}\n" - 'if [ "$#" -eq 0 ]; then exit 0; fi\n' - 'tar -cf - "$@" | base64 -w0\n' - ) - result = await self.exec(command, shell=True, timeout=self.state.timeouts.snapshot_s) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=self._workspace_root_path(), - context={ - "reason": "ephemeral_backup_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - encoded = result.stdout.decode("utf-8", "replace").strip() - if not encoded: - return None - try: - return io.BytesIO(base64.b64decode(encoded.encode("utf-8"), validate=True)).read() - except Exception as e: - raise WorkspaceArchiveReadError( - path=self._workspace_root_path(), - context={"reason": "ephemeral_backup_invalid_base64"}, - cause=e, - ) from e - - async def _remove_plain_skip_paths(self, plain_skip: set[Path]) -> None: - if not plain_skip: - return - root = self._workspace_root_path() - command = ["rm", "-rf", "--"] + [(root / rel).as_posix() for rel in sorted(plain_skip)] - result = await self.exec(*command, shell=False, timeout=self.state.timeouts.cleanup_s) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "ephemeral_remove_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - - async def _restore_plain_skip_paths(self, backup: bytes | None) -> None: - if not backup: - return - root = self._workspace_root_path() - temp_path = root / f".sandbox-runloop-restore-{self.state.session_id.hex}.tar" - await self.write(temp_path, io.BytesIO(backup)) - try: - result = await self.exec( - "mkdir", - "-p", - root.as_posix(), - shell=False, - timeout=self.state.timeouts.cleanup_s, - ) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "ephemeral_restore_mkdir_failed", - "exit_code": result.exit_code, - }, - ) - result = await self.exec( - "tar", - "-xf", - sandbox_path_str(temp_path), - "-C", - root.as_posix(), - shell=False, - timeout=self.state.timeouts.snapshot_s, - ) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "ephemeral_restore_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - finally: - try: - await self.exec("rm", "-f", "--", sandbox_path_str(temp_path), shell=False) - except Exception: - pass - - async def persist_workspace(self) -> io.IOBase: - """Persist the workspace with a native Runloop disk snapshot. - - Before snapshotting, the session temporarily removes ephemeral skip paths and tears down - ephemeral mounts so the saved disk image contains only durable workspace state, then it - restores those local-only artifacts afterward. - """ - root = self._workspace_root_path() - skip = self._persist_workspace_skip_relpaths() - mount_targets = self.state.manifest.ephemeral_mount_targets() - mount_skip_rel_paths: set[Path] = set() - for _mount_entry, mount_path in mount_targets: - try: - mount_skip_rel_paths.add(mount_path.relative_to(root)) - except ValueError: - continue - plain_skip = skip - mount_skip_rel_paths - - backup: bytes | None = None - unmounted_mounts: list[tuple[Mount, Path]] = [] - snapshot_error: WorkspaceArchiveReadError | None = None - snapshot_id: str | None = None - - try: - backup = await self._backup_plain_skip_paths(plain_skip) - await self._remove_plain_skip_paths(plain_skip) - - for mount_entry, mount_path in mount_targets: - await mount_entry.mount_strategy.teardown_for_snapshot( - mount_entry, - self, - mount_path, - ) - unmounted_mounts.append((mount_entry, mount_path)) - - snapshot: RunloopAsyncSnapshot = await self._devbox.snapshot_disk( - name=f"sandbox-{self.state.session_id.hex[:12]}", - metadata={"openai_agents_session_id": self.state.session_id.hex}, - timeout=self.state.timeouts.snapshot_s, - ) - snapshot_id = snapshot.id - if not snapshot_id: - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "snapshot_unexpected_return", - "type": type(snapshot).__name__, - }, - ) - except WorkspaceArchiveReadError as e: - snapshot_error = e - except Exception as e: - retryable = None - if _is_runloop_provider_error(e): - retryable = _runloop_provider_retryability(e) - snapshot_error = WorkspaceArchiveReadError( - path=root, - context={"reason": "snapshot_failed"}, - cause=e, - retryable=retryable, - ) - finally: - remount_error: WorkspaceArchiveReadError | None = None - for mount_entry, mount_path in reversed(unmounted_mounts): - try: - await mount_entry.mount_strategy.restore_after_snapshot( - mount_entry, self, mount_path - ) - except Exception as e: - current_error = WorkspaceArchiveReadError(path=root, cause=e) - if remount_error is None: - remount_error = current_error - else: - additional = remount_error.context.setdefault( - "additional_remount_errors", [] - ) - assert isinstance(additional, list) - additional.append( - { - "message": current_error.message, - "cause_type": type(e).__name__, - "cause": str(e), - } - ) - try: - await self._restore_plain_skip_paths(backup) - except Exception as e: - restore_error = WorkspaceArchiveReadError(path=root, cause=e) - if remount_error is None: - remount_error = restore_error - else: - additional = remount_error.context.setdefault("additional_restore_errors", []) - assert isinstance(additional, list) - additional.append( - { - "message": restore_error.message, - "cause_type": type(e).__name__, - "cause": str(e), - } - ) - - if remount_error is not None: - if snapshot_error is not None: - remount_error.context["snapshot_error_before_restore_corruption"] = { - "message": snapshot_error.message - } - raise remount_error - - if snapshot_error is not None: - raise snapshot_error - - assert snapshot_id is not None - return io.BytesIO(_encode_runloop_snapshot_ref(snapshot_id=snapshot_id)) - - async def hydrate_workspace(self, data: io.IOBase) -> None: - """Replace the current devbox from a Runloop snapshot reference or tar archive. - - Runloop restore creates a new devbox from the saved disk snapshot and treats that snapshot - filesystem as authoritative, including any tools or files that originally came from the - source blueprint, so restore does not reselect a blueprint. Non-native payloads fall back - to tar hydration so cross-provider snapshots and file snapshots keep working. - """ - root = self._workspace_root_path() - raw = data.read() - if isinstance(raw, str): - raw = raw.encode("utf-8") - if not isinstance(raw, bytes | bytearray): - raise WorkspaceWriteTypeError(path=root, actual_type=type(raw).__name__) - - snapshot_id = _decode_runloop_snapshot_ref(bytes(raw)) - if snapshot_id is None: - await self._hydrate_workspace_via_tar(bytes(raw)) - return - - try: - try: - await self._devbox.shutdown(timeout=self.state.timeouts.cleanup_s) - except Exception: - pass - envs = await self._resolved_envs() - create_kwargs = _runloop_create_kwargs( - blueprint_id=None, - blueprint_name=None, - env_vars=envs, - name=self.state.name, - user_parameters=self.state.user_parameters, - launch_parameters=self.state.launch_parameters, - tunnel=self.state.tunnel, - gateways=self.state.gateways, - mcp=self.state.mcp, - metadata=self.state.metadata, - secrets=self.state.secret_refs, - ) - devbox = await self._sdk.devbox.create_from_snapshot( - snapshot_id, - timeout=self.state.timeouts.resume_s, - **create_kwargs, - ) - self._devbox = devbox - self.state.devbox_id = devbox.id - except Exception as e: - context: dict[str, object] = { - "reason": "snapshot_restore_failed", - "snapshot_id": snapshot_id, - } - if _is_runloop_provider_error(e): - context.update(_runloop_error_context(e, backend_detail="snapshot_restore_failed")) - raise WorkspaceArchiveWriteError( - path=root, - context=context, - cause=e, - retryable=_runloop_provider_retryability(e) - if _is_runloop_provider_error(e) - else None, - ) from e - - async def _restore_snapshot_into_workspace_on_resume(self) -> None: - """Restore snapshots on resume, preserving Runloop's native disk-snapshot fast path.""" - - root = self._workspace_root_path() - workspace_archive = await self.state.snapshot.restore(dependencies=self.dependencies) - try: - raw = workspace_archive.read() - if isinstance(raw, str): - raw = raw.encode("utf-8") - if not isinstance(raw, bytes | bytearray): - raise WorkspaceWriteTypeError(path=root, actual_type=type(raw).__name__) - - payload = bytes(raw) - if _decode_runloop_snapshot_ref(payload) is None: - # Most providers restore tar snapshots by clearing the workspace first, then - # extracting into an empty root. Runloop differs only for its native snapshot - # refs, which already replace the entire devbox disk and therefore should not - # pre-clear the workspace root on resume. - await self._clear_workspace_root_on_resume() - await self.hydrate_workspace(io.BytesIO(payload)) - finally: - try: - workspace_archive.close() - except Exception: - pass - - async def _hydrate_workspace_via_tar(self, payload: bytes) -> None: - root = self._workspace_root_path() - archive_path = root / f".sandbox-runloop-hydrate-{self.state.session_id.hex}.tar" - - try: - validate_tar_bytes( - payload, - allow_external_symlink_targets=False, - ) - except UnsafeTarMemberError as e: - raise WorkspaceArchiveWriteError( - path=root, - context={ - "reason": "unsafe_or_invalid_tar", - "member": e.member, - "detail": str(e), - }, - cause=e, - ) from e - - try: - await self.mkdir(root, parents=True) - await self.write(archive_path, io.BytesIO(payload)) - result = await self.exec( - "tar", - "-C", - root.as_posix(), - "-xf", - archive_path.as_posix(), - shell=False, - timeout=self.state.timeouts.snapshot_s, - ) - if not result.ok(): - raise WorkspaceArchiveWriteError( - path=root, - context={ - "reason": "tar_extract_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", errors="replace"), - }, - ) - except WorkspaceArchiveWriteError: - raise - except Exception as e: - raise WorkspaceArchiveWriteError(path=root, cause=e) from e - finally: - try: - await self.exec( - "rm", - "-f", - "--", - archive_path.as_posix(), - shell=False, - timeout=self.state.timeouts.cleanup_s, - ) - except Exception: - pass - - -def _runloop_create_kwargs( - *, - blueprint_id: str | None, - blueprint_name: str | None, - env_vars: dict[str, str] | None, - name: str | None, - user_parameters: RunloopUserParameters | None, - launch_parameters: RunloopLaunchParameters | None, - tunnel: RunloopTunnelConfig | None, - gateways: dict[str, RunloopGatewaySpec], - mcp: dict[str, RunloopMcpSpec], - metadata: dict[str, str], - secrets: dict[str, str], -) -> dict[str, object]: - kwargs: dict[str, object] = {} - if blueprint_id is not None: - kwargs["blueprint_id"] = blueprint_id - if blueprint_name is not None: - kwargs["blueprint_name"] = blueprint_name - if env_vars: - kwargs["environment_variables"] = env_vars - if name: - kwargs["name"] = name - launch_parameters_payload = _runloop_launch_parameters_payload( - launch_parameters=launch_parameters, - user_parameters=user_parameters, - ) - if launch_parameters_payload is not None: - kwargs["launch_parameters"] = launch_parameters_payload - if tunnel is not None: - kwargs["tunnel"] = tunnel.model_dump(mode="json", exclude_none=True) - if gateways: - kwargs["gateways"] = { - key: value.model_dump(mode="json", exclude_none=True) for key, value in gateways.items() - } - if mcp: - kwargs["mcp"] = { - key: value.model_dump(mode="json", exclude_none=True) for key, value in mcp.items() - } - if metadata: - kwargs["metadata"] = metadata - if secrets: - kwargs["secrets"] = secrets - return kwargs - - -def _runloop_launch_parameters_payload( - *, - launch_parameters: RunloopLaunchParameters | None, - user_parameters: RunloopUserParameters | None, -) -> dict[str, object] | None: - payload = ( - launch_parameters.to_dict(mode="json", exclude_none=True, exclude_defaults=True) - if launch_parameters is not None - else {} - ) - if user_parameters is not None: - payload["user_parameters"] = user_parameters.to_dict(mode="json", exclude_none=True) - return payload or None - - -async def _resolve_runloop_managed_secret_refs( - sdk: Any, - *, - managed_secrets: dict[str, str | RunloopExistingSecret] | None, - timeout_s: float, -) -> dict[str, str]: - if not managed_secrets: - return {} - - secret_refs: dict[str, str] = {} - for env_var, secret_value in sorted(managed_secrets.items()): - if not isinstance(secret_value, RunloopExistingSecret): - try: - await sdk.secret.create(name=env_var, value=secret_value, timeout=timeout_s) - except Exception as e: - if _is_runloop_conflict(e): - await sdk.secret.update(env_var, value=secret_value, timeout=timeout_s) - else: - raise - secret_refs[env_var] = env_var - return secret_refs - - -def _effective_runloop_home(user_parameters: RunloopUserParameters | None) -> PurePosixPath: - if user_parameters is None: - return _RUNLOOP_DEFAULT_HOME - if user_parameters.username == "root" and user_parameters.uid == 0: - return _RUNLOOP_ROOT_HOME - return PurePosixPath("/home") / user_parameters.username - - -def _default_runloop_manifest_root(user_parameters: RunloopUserParameters | None) -> str: - return str(_effective_runloop_home(user_parameters)) - - -def _validate_runloop_manifest_root( - manifest: Manifest, *, user_parameters: RunloopUserParameters | None -) -> None: - root = PurePosixPath(posixpath.normpath(manifest.root)) - runloop_home = _effective_runloop_home(user_parameters) - try: - root.relative_to(runloop_home) - except ValueError as e: - raise ValueError( - "RunloopSandboxClient requires manifest.root to be the effective Runloop home " - f"({runloop_home}) or a subdirectory of it." - ) from e - - -class RunloopSandboxClient(BaseSandboxClient[RunloopSandboxClientOptions | None]): - """Runloop sandbox client managing devbox lifecycle via AsyncRunloopSDK.""" - - backend_id = "runloop" - supports_default_options = True - _instrumentation: Instrumentation - _platform: RunloopPlatformClient - - def __init__( - self, - *, - bearer_token: str | None = None, - base_url: str | None = None, - instrumentation: Instrumentation | None = None, - dependencies: Dependencies | None = None, - ) -> None: - self._sdk = _import_runloop_sdk().async_sdk(bearer_token=bearer_token, base_url=base_url) - self._platform = RunloopPlatformClient(self._sdk) - self._instrumentation = ( - instrumentation if instrumentation is not None else Instrumentation() - ) - self._dependencies = dependencies - - @property - def platform(self) -> RunloopPlatformClient: - return self._platform - - @redact_mount_error_data - async def create( - self, - *, - snapshot: SnapshotSpec | SnapshotBase | None = None, - manifest: Manifest | None = None, - options: RunloopSandboxClientOptions | None, - ) -> SandboxSession: - """Create a Runloop devbox and bind it to a manifest rooted under the active home. - - Runloop defaults to the `user` account at `/home/user`, but explicit user parameters can - switch the active home, including root launch at `/root`. Client creation validates the - manifest root against that effective home, merges environment variables, and applies any - configured blueprint selection or user profile when provisioning the devbox. The returned - session follows the shared sandbox lifecycle and must be started before direct operations. - """ - resolved_options = options if options is not None else RunloopSandboxClientOptions() - if ( - resolved_options.blueprint_id is not None - and resolved_options.blueprint_name is not None - ): - raise ValueError( - "RunloopSandboxClientOptions cannot set both blueprint_id and blueprint_name" - ) - - user_parameters = _normalize_runloop_user_parameters(resolved_options.user_parameters) - manifest = ( - manifest - if manifest is not None - else Manifest(root=_default_runloop_manifest_root(user_parameters)) - ) - _validate_runloop_manifest_root(manifest, user_parameters=user_parameters) - self._validate_manifest_for_create(manifest) - - timeouts_in = resolved_options.timeouts - if isinstance(timeouts_in, RunloopTimeouts): - timeouts = timeouts_in - elif timeouts_in is None: - timeouts = RunloopTimeouts() - else: - timeouts = RunloopTimeouts.model_validate(timeouts_in) - - secret_refs = await _resolve_runloop_managed_secret_refs( - self._sdk, - managed_secrets=resolved_options.managed_secrets, - timeout_s=timeouts.fast_op_s, - ) - launch_parameters = _normalize_runloop_launch_parameters(resolved_options.launch_parameters) - tunnel = _normalize_runloop_tunnel_config(resolved_options.tunnel) - base_envs = dict(resolved_options.env_vars or {}) - manifest_envs = await manifest.environment.resolve() - envs = {**base_envs, **manifest_envs} or None - - create_kwargs = _runloop_create_kwargs( - blueprint_id=resolved_options.blueprint_id, - blueprint_name=resolved_options.blueprint_name, - env_vars=envs, - name=resolved_options.name, - user_parameters=user_parameters, - launch_parameters=launch_parameters, - tunnel=tunnel, - gateways=dict(resolved_options.gateways or {}), - mcp=dict(resolved_options.mcp or {}), - metadata=dict(resolved_options.metadata or {}), - secrets=secret_refs, - ) - devbox = await self._sdk.devbox.create(timeout=timeouts.create_s, **create_kwargs) - - session_id = uuid.uuid4() - snapshot_instance = resolve_snapshot(snapshot, str(session_id)) - state = RunloopSandboxSessionState( - session_id=session_id, - manifest=manifest, - snapshot=snapshot_instance, - devbox_id=devbox.id, - blueprint_id=resolved_options.blueprint_id, - blueprint_name=resolved_options.blueprint_name, - base_env_vars=base_envs, - pause_on_exit=resolved_options.pause_on_exit, - name=resolved_options.name, - timeouts=timeouts, - exposed_ports=resolved_options.exposed_ports, - user_parameters=user_parameters, - launch_parameters=launch_parameters, - tunnel=tunnel, - gateways=dict(resolved_options.gateways or {}), - mcp=dict(resolved_options.mcp or {}), - metadata=dict(resolved_options.metadata or {}), - secret_refs=secret_refs, - ) - inner = RunloopSandboxSession.from_state(state, sdk=self._sdk, devbox=devbox) - return self._wrap_session(inner, instrumentation=self._instrumentation) - - async def close(self) -> None: - """Close the shared AsyncRunloopSDK client used for devbox operations.""" - await self._sdk.aclose() - - async def __aenter__(self) -> RunloopSandboxClient: - return self - - async def __aexit__(self, *_: object) -> None: - await self.close() - - async def delete(self, session: SandboxSession) -> SandboxSession: - """Best-effort release the Runloop devbox when callers delete the session.""" - inner = session._inner - if not isinstance(inner, RunloopSandboxSession): - raise TypeError("RunloopSandboxClient.delete expects a RunloopSandboxSession") - try: - await inner.shutdown() - except Exception: - pass - return session - - @redact_mount_error_data - async def resume( - self, - state: SandboxSessionState, - ) -> SandboxSession: - """Resume a persisted Runloop session by reconnecting or reprovisioning a devbox. - - The client first tries to reconnect to the stored devbox id, including after an unclean - process/client shutdown where the devbox is still running and `shutdown()` was never - called. If reconnect fails, it creates a fresh devbox with the stored blueprint and - environment settings. - """ - if not isinstance(state, RunloopSandboxSessionState): - raise TypeError("RunloopSandboxClient.resume expects a RunloopSandboxSessionState") - state.assert_path_grants_rebound() - - devbox = None - reconnected = False - try: - devbox = self._sdk.devbox.from_id(state.devbox_id) - info: RunloopDevboxView = await devbox.get_info(timeout=state.timeouts.keepalive_s) - status = info.status - resume_polling_config = _runloop_polling_config(timeout_s=state.timeouts.resume_s) - if status == "suspended": - await devbox.resume(timeout=state.timeouts.resume_s) - await devbox.await_running(polling_config=resume_polling_config) - elif status == "resuming": - await devbox.await_running(polling_config=resume_polling_config) - elif status != "running": - raise RuntimeError(f"unexpected_status:{status}") - reconnected = True - except Exception: - devbox = None - - if devbox is None: - manifest_envs = await state.manifest.environment.resolve() - envs = {**state.base_env_vars, **manifest_envs} or None - create_kwargs = _runloop_create_kwargs( - blueprint_id=state.blueprint_id, - blueprint_name=state.blueprint_name, - env_vars=envs, - name=state.name, - user_parameters=state.user_parameters, - launch_parameters=state.launch_parameters, - tunnel=state.tunnel, - gateways=state.gateways, - mcp=state.mcp, - metadata=state.metadata, - secrets=state.secret_refs, - ) - devbox = await self._sdk.devbox.create(timeout=state.timeouts.create_s, **create_kwargs) - state.devbox_id = devbox.id - - inner = RunloopSandboxSession.from_state(state, sdk=self._sdk, devbox=devbox) - inner._skip_start = state.pause_on_exit and reconnected - inner._set_start_state_preserved(reconnected, system=reconnected) - return self._wrap_session(inner, instrumentation=self._instrumentation) - - def deserialize_session_state(self, payload: dict[str, object]) -> SandboxSessionState: - return self._deserialize_session_state_payload(payload, RunloopSandboxSessionState) diff --git a/src/agents/sandbox/_mount_security.py b/src/agents/sandbox/_mount_security.py index 3fe32fccc8..bf799d228b 100644 --- a/src/agents/sandbox/_mount_security.py +++ b/src/agents/sandbox/_mount_security.py @@ -242,12 +242,6 @@ "agents.extensions.sandbox.modal.mounts", "ModalCloudBucketMountStrategy", ), - "runloop_cloud_bucket": ( - "in_container", - "runloop", - "agents.extensions.sandbox.runloop.mounts", - "RunloopCloudBucketMountStrategy", - ), "vercel_cloud_bucket": ( "in_container", "vercel", @@ -264,7 +258,6 @@ "daytona_cloud_bucket": frozenset({"type", "pattern"}), "e2b_cloud_bucket": frozenset({"type", "pattern"}), "modal_cloud_bucket": frozenset({"type", "secret_name", "secret_environment_name"}), - "runloop_cloud_bucket": frozenset({"type", "pattern"}), "vercel_cloud_bucket": frozenset({"type"}), } _SERIALIZED_PATTERN_CLASS_BY_TYPE: dict[str, type[MountPatternBase]] = { @@ -295,7 +288,6 @@ class _InContainerMountCredentialCapability: "in_container", "daytona_cloud_bucket", "e2b_cloud_bucket", - "runloop_cloud_bucket", } ) _IN_CONTAINER_MOUNT_CREDENTIAL_CAPABILITIES: tuple[_InContainerMountCredentialCapability, ...] = ( diff --git a/tests/extensions/sandbox/test_runloop.py b/tests/extensions/sandbox/test_runloop.py deleted file mode 100644 index 5f6efd85e8..0000000000 --- a/tests/extensions/sandbox/test_runloop.py +++ /dev/null @@ -1,3147 +0,0 @@ -from __future__ import annotations - -import asyncio -import builtins -import importlib -import io -import json -import shlex -import sys -import tarfile -import types -from pathlib import Path, PurePosixPath -from typing import Any, Literal, cast - -import pytest -from pydantic import BaseModel, Field, PrivateAttr - -from agents import Agent -from agents.run_context import RunContextWrapper -from agents.run_state import RunState -from agents.sandbox import Manifest, SandboxPathGrant -from agents.sandbox.capabilities import Shell -from agents.sandbox.capabilities.tools.shell_tool import ExecCommandArgs, ExecCommandTool -from agents.sandbox.entries import ( - File, - InContainerMountStrategy, - Mount, - MountpointMountPattern, - RcloneMountPattern, - S3Mount, -) -from agents.sandbox.entries.mounts.base import InContainerMountAdapter -from agents.sandbox.errors import MountConfigError -from agents.sandbox.manifest import Environment -from agents.sandbox.materialization import MaterializedFile -from agents.sandbox.session.base_sandbox_session import BaseSandboxSession -from agents.sandbox.session.dependencies import Dependencies -from agents.sandbox.session.sandbox_client import BaseSandboxClientOptions -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.snapshot import NoopSnapshot, SnapshotBase -from agents.sandbox.types import ExposedPortEndpoint -from tests.utils.factories import make_run_state - - -class _RestorableSnapshot(SnapshotBase): - type: Literal["test-restorable-runloop"] = "test-restorable-runloop" - payload: bytes = b"restored" - - async def persist( - self, - data: io.IOBase, - *, - dependencies: Dependencies | None = None, - ) -> None: - _ = (data, dependencies) - - async def restore(self, *, dependencies: Dependencies | None = None) -> io.IOBase: - _ = dependencies - return io.BytesIO(self.payload) - - async def restorable(self, *, dependencies: Dependencies | None = None) -> bool: - _ = dependencies - return True - - -class _DependencyAwareSnapshot(SnapshotBase): - type: Literal["test-restorable-runloop-deps"] = "test-restorable-runloop-deps" - payload: bytes = b"restored" - _restorable_dependencies: list[Dependencies | None] = PrivateAttr(default_factory=list) - _restore_dependencies: list[Dependencies | None] = PrivateAttr(default_factory=list) - - @property - def restorable_dependencies(self) -> list[Dependencies | None]: - return self._restorable_dependencies - - @property - def restore_dependencies(self) -> list[Dependencies | None]: - return self._restore_dependencies - - async def persist( - self, - data: io.IOBase, - *, - dependencies: Dependencies | None = None, - ) -> None: - _ = (data, dependencies) - - async def restore(self, *, dependencies: Dependencies | None = None) -> io.IOBase: - self._restore_dependencies.append(dependencies) - return io.BytesIO(self.payload) - - async def restorable(self, *, dependencies: Dependencies | None = None) -> bool: - self._restorable_dependencies.append(dependencies) - return True - - -class _FakeRunloopError(Exception): - pass - - -class _FakeAPIError(_FakeRunloopError): - def __init__( - self, - message: str, - *, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - body: object | None = None, - ) -> None: - super().__init__(message) - self.message = message - self.request = types.SimpleNamespace(url=url, method=method) - self.body = body - - -class _FakeAPIConnectionError(_FakeAPIError): - def __init__( - self, - message: str = "Connection error.", - *, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(message, url=url, method=method, body=None) - - -class _FakeAPITimeoutError(_FakeAPIConnectionError): - def __init__( - self, - *, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__("Request timed out.", url=url, method=method) - - -class _FakeAPIStatusError(_FakeAPIError): - def __init__( - self, - status_code: int, - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - message: str | None = None, - ) -> None: - super().__init__(message or f"HTTP {status_code}", url=url, method=method, body=body) - self.status_code = status_code - self.response = types.SimpleNamespace( - status_code=status_code, - request=types.SimpleNamespace(url=url, method=method), - ) - - -class _FakeAPIResponseValidationError(_FakeAPIError): - def __init__( - self, - *, - status_code: int = 500, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - message: str = "Data returned by API invalid for expected schema.", - ) -> None: - super().__init__(message, url=url, method=method, body=body) - self.status_code = status_code - self.response = types.SimpleNamespace( - status_code=status_code, - request=types.SimpleNamespace(url=url, method=method), - ) - - -class _FakeAuthenticationError(_FakeAPIStatusError): - def __init__( - self, - message: str = "authentication failed", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(401, body=body, url=url, method=method, message=message) - - -class _FakeBadRequestError(_FakeAPIStatusError): - def __init__( - self, - message: str = "bad request", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(400, body=body, url=url, method=method, message=message) - - -class _FakeInternalServerError(_FakeAPIStatusError): - def __init__( - self, - message: str = "internal server error", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(500, body=body, url=url, method=method, message=message) - - -class _FakeNotFoundError(_FakeAPIStatusError): - def __init__( - self, - message: str = "not found", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "GET", - ) -> None: - super().__init__(404, body=body, url=url, method=method, message=message) - - -class _FakePermissionDeniedError(_FakeAPIStatusError): - def __init__( - self, - message: str = "permission denied", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(403, body=body, url=url, method=method, message=message) - - -class _FakeRateLimitError(_FakeAPIStatusError): - def __init__( - self, - message: str = "rate limited", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(429, body=body, url=url, method=method, message=message) - - -class _FakeUnprocessableEntityError(_FakeAPIStatusError): - def __init__( - self, - message: str = "unprocessable entity", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(422, body=body, url=url, method=method, message=message) - - -class _FakeExecutionResult: - def __init__(self, *, stdout: str = "", stderr: str = "", exit_code: int | None = 0) -> None: - self._stdout = stdout - self._stderr = stderr - self.exit_code = exit_code - - async def stdout(self, num_lines: int | None = None) -> str: - _ = num_lines - return self._stdout - - async def stderr(self, num_lines: int | None = None) -> str: - _ = num_lines - return self._stderr - - -class _FakeExecution: - _counter = 0 - - def __init__( - self, - *, - devbox: _FakeDevbox, - devbox_id: str, - command: str, - stdout_cb: object | None, - stderr_cb: object | None, - shell_name: str | None, - attach_stdin: bool, - home_dir: str, - ) -> None: - type(self)._counter += 1 - self._devbox = devbox - self.execution_id = f"exec-{type(self)._counter}" - self.devbox_id = devbox_id - self.command = command - self.shell_name = shell_name - self.attach_stdin = attach_stdin - self._stdout_cb = stdout_cb - self._stderr_cb = stderr_cb - self._done = asyncio.Event() - self._stdout = "" - self._stderr = "" - self._exit_code: int | None = None - self._killed = False - self._home_dir = home_dir - self._interactive = attach_stdin and ( - "python3 -i" in command or "python3" == command.strip() - ) - self._sleep_forever = "sleep-forever" in command - if self._interactive: - self._emit(stdout_cb, ">>> ") - elif "emit-after-result" in command: - asyncio.get_running_loop().call_soon(self._emit, stdout_cb, "final chunk\n") - self._exit_code = 0 - self._done.set() - elif "echo hello" in command: - self._stdout = "hello\n" - self._emit(stdout_cb, self._stdout) - self._exit_code = 0 - self._done.set() - elif " tar -C " in command or command.startswith("tar -C "): - self._apply_tar_extract() - self._exit_code = 0 - self._done.set() - elif self._is_resolve_workspace_path_command(command): - self._resolve_workspace_path(command) - self._done.set() - elif " cat -- " in command or command.startswith("cat -- "): - self._stdout = self._read_file_text(command) - self._emit(stdout_cb, self._stdout) - self._exit_code = 0 - self._done.set() - elif " rm -f -- " in command or command.startswith("rm -f -- "): - self._remove_file(command) - self._exit_code = 0 - self._done.set() - elif "pwd" in command: - self._stdout = f"{self._home_dir}\n" - self._emit(stdout_cb, self._stdout) - self._exit_code = 0 - self._done.set() - elif self._sleep_forever: - return - else: - self._exit_code = 0 - self._done.set() - - def _emit(self, callback: object | None, text: str) -> None: - if callback is None: - return - cast(Any, callback)(text) - - def _command_tokens(self) -> list[str]: - return shlex.split(self.command) - - def _path_relative_to_home(self, raw_path: str) -> str: - normalized = PurePosixPath(raw_path) - home = PurePosixPath(self._home_dir) - try: - relative = normalized.relative_to(home) - except ValueError: - return normalized.as_posix().lstrip("/") - rel_str = relative.as_posix() - return rel_str if rel_str else "." - - def _is_resolve_workspace_path_command(self, command: str) -> bool: - tokens = shlex.split(command) - return any( - token.startswith("/tmp/openai-agents/bin/resolve-workspace-path-") - and len(tokens) >= index + 4 - for index, token in enumerate(tokens) - ) - - def _resolve_fake_path(self, raw_path: str, *, depth: int = 0) -> PurePosixPath: - if depth > 64: - raise RuntimeError(f"symlink resolution depth exceeded: {raw_path}") - - path = PurePosixPath(raw_path) - if not path.is_absolute(): - path = PurePosixPath(self._home_dir) / path - - parts = path.parts - current = PurePosixPath("/") - for index, part in enumerate(parts[1:], start=1): - current = current / part - target = self._devbox.symlinks.get(current.as_posix()) - if target is None: - continue - - target_path = PurePosixPath(target) - if not target_path.is_absolute(): - target_path = current.parent / target_path - for remaining in parts[index + 1 :]: - target_path /= remaining - return self._resolve_fake_path(target_path.as_posix(), depth=depth + 1) - - return path - - @staticmethod - def _fake_path_is_under(path: PurePosixPath, root: PurePosixPath) -> bool: - return path == root or root in path.parents - - def _resolve_workspace_path(self, command: str) -> None: - tokens = self._command_tokens() - helper_index = next( - index - for index, token in enumerate(tokens) - if token.startswith("/tmp/openai-agents/bin/resolve-workspace-path-") - ) - root = self._resolve_fake_path(tokens[helper_index + 1]) - candidate = self._resolve_fake_path(tokens[helper_index + 2]) - for_write = tokens[helper_index + 3] - grant_tokens = tokens[helper_index + 4 :] - - if self._fake_path_is_under(candidate, root): - self._stdout = f"{candidate.as_posix()}\n" - self._exit_code = 0 - return - - best_grant: tuple[PurePosixPath, str, str] | None = None - for index in range(0, len(grant_tokens), 2): - grant_original = grant_tokens[index] - read_only = grant_tokens[index + 1] - grant_root = self._resolve_fake_path(grant_original) - if not self._fake_path_is_under(candidate, grant_root): - continue - if best_grant is None or len(grant_root.parts) > len(best_grant[0].parts): - best_grant = (grant_root, grant_original, read_only) - - if best_grant is not None: - _grant_root, grant_original, read_only = best_grant - if for_write == "1" and read_only == "1": - self._stderr = ( - f"read-only extra path grant: {grant_original}\n" - f"resolved path: {candidate.as_posix()}\n" - ) - self._exit_code = 114 - return - self._stdout = f"{candidate.as_posix()}\n" - self._exit_code = 0 - return - - self._stderr = f"workspace escape: {candidate.as_posix()}\n" - self._exit_code = 111 - - def _apply_tar_extract(self) -> None: - tokens = self._command_tokens() - tar_index = tokens.index("tar") - root = tokens[tar_index + 2] - archive_path = tokens[tar_index + 4] - archive_rel = self._path_relative_to_home(archive_path) - root_rel = self._path_relative_to_home(root) - payload = self._devbox.files[archive_rel] - with tarfile.open(fileobj=io.BytesIO(payload), mode="r:*") as archive: - for member in archive.getmembers(): - if member.isdir(): - continue - fileobj = archive.extractfile(member) - if fileobj is None: - continue - target = PurePosixPath(member.name) - if root_rel != ".": - target = PurePosixPath(root_rel) / target - self._devbox.files[target.as_posix()] = fileobj.read() - - def _read_file_text(self, command: str) -> str: - tokens = shlex.split(command) - path = tokens[-1] - rel_path = self._path_relative_to_home(path) - return self._devbox.files.get(rel_path, b"").decode("utf-8", errors="replace") - - def _remove_file(self, command: str) -> None: - tokens = shlex.split(command) - path = tokens[-1] - rel_path = self._path_relative_to_home(path) - self._devbox.files.pop(rel_path, None) - - async def result(self, timeout: float | None = None) -> _FakeExecutionResult: - _ = timeout - await self._done.wait() - return _FakeExecutionResult( - stdout=self._stdout, - stderr=self._stderr, - exit_code=self._exit_code, - ) - - async def kill(self, timeout: float | None = None) -> None: - _ = timeout - self._killed = True - self._exit_code = -9 - self._done.set() - - async def send_input(self, text: str) -> None: - if not self._interactive: - return - if text == "5 + 5\n": - self._stdout += "10\n>>> " - self._emit(self._stdout_cb, "10\n>>> ") - return - if text in {"exit()\n", "exit\n"}: - self._exit_code = 0 - self._done.set() - return - - -class _FakeExecutionsAPI: - send_std_in_calls: list[tuple[str, str, str]] - - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.send_std_in_calls = [] - - async def send_std_in( - self, - execution_id: str, - *, - devbox_id: str, - text: str | None = None, - timeout: float | None = None, - **_: object, - ) -> object: - del timeout - self.send_std_in_calls.append((execution_id, devbox_id, text or "")) - execution = self._owner.executions[execution_id] - await execution.send_input(text or "") - return types.SimpleNamespace(success=True) - - -class _FakeFileInterface: - def __init__(self, devbox: _FakeDevbox) -> None: - self._devbox = devbox - - def _file_key(self, path: str) -> str: - normalized = PurePosixPath(path) - home = PurePosixPath(self._devbox.home_dir) - try: - relative = normalized.relative_to(home) - except ValueError: - return normalized.as_posix() - rel_str = relative.as_posix() - return rel_str if rel_str else "." - - async def download(self, *, path: str, timeout: float | None = None, **_: object) -> bytes: - del timeout - self._devbox.file_download_paths.append(path) - key = self._file_key(path) - if key not in self._devbox.files: - raise _FakeNotFoundError(path) - return self._devbox.files[key] - - async def upload( - self, - *, - path: str, - file: bytes, - timeout: float | None = None, - **_: object, - ) -> object: - del timeout - self._devbox.file_upload_paths.append(path) - self._devbox.files[self._file_key(path)] = bytes(file) - return {} - - -class _FakeNetworkInterface: - def __init__(self, devbox: _FakeDevbox) -> None: - self._devbox = devbox - - async def enable_tunnel(self, **params: object) -> object: - self._devbox.enable_tunnel_calls.append(dict(params)) - self._devbox.tunnel_key = "test-key" - return types.SimpleNamespace(tunnel_key="test-key") - - -class _FakeCommandInterface: - def __init__(self, devbox: _FakeDevbox) -> None: - self._devbox = devbox - - async def exec(self, command: str, **params: object) -> _FakeExecutionResult: - execution = _FakeExecution( - devbox=self._devbox, - devbox_id=self._devbox.id, - command=command, - stdout_cb=params.get("stdout"), - stderr_cb=params.get("stderr"), - shell_name=cast(str | None, params.get("shell_name")), - attach_stdin=bool(params.get("attach_stdin", False)), - home_dir=self._devbox.home_dir, - ) - self._devbox.owner.executions[execution.execution_id] = execution - self._devbox.exec_calls.append((command, dict(params))) - return await execution.result() - - async def exec_async(self, command: str, **params: object) -> _FakeExecution: - execution = _FakeExecution( - devbox=self._devbox, - devbox_id=self._devbox.id, - command=command, - stdout_cb=params.get("stdout"), - stderr_cb=params.get("stderr"), - shell_name=cast(str | None, params.get("shell_name")), - attach_stdin=bool(params.get("attach_stdin", False)), - home_dir=self._devbox.home_dir, - ) - self._devbox.owner.executions[execution.execution_id] = execution - self._devbox.exec_async_calls.append((command, dict(params))) - return execution - - -class _FakeDevbox: - def __init__( - self, - owner: _FakeAsyncRunloopSDK, - *, - devbox_id: str, - status: str = "running", - snapshot_source_id: str | None = None, - environment_variables: dict[str, str] | None = None, - launch_parameters: dict[str, object] | None = None, - ) -> None: - self.owner = owner - self.id = devbox_id - self.status = status - self.snapshot_source_id = snapshot_source_id - self.environment_variables = dict(environment_variables or {}) - self.launch_parameters = dict(launch_parameters or {}) - user_parameters = self.launch_parameters.get("user_parameters") - if isinstance(user_parameters, dict): - username = user_parameters.get("username") - uid = user_parameters.get("uid") - if username == "root" and uid == 0: - self.home_dir = "/root" - elif isinstance(username, str) and username: - self.home_dir = f"/home/{username}" - else: - self.home_dir = "/home/user" - else: - self.home_dir = "/home/user" - self.files: dict[str, bytes] = {} - self.symlinks: dict[str, str] = {} - self.file_download_paths: list[str] = [] - self.file_upload_paths: list[str] = [] - self.tunnel_key: str | None = None - self.enable_tunnel_calls: list[dict[str, object]] = [] - self.exec_calls: list[tuple[str, dict[str, object]]] = [] - self.exec_async_calls: list[tuple[str, dict[str, object]]] = [] - self.snapshot_calls: list[dict[str, object]] = [] - self.shutdown_calls = 0 - self.suspend_calls = 0 - self.resume_calls = 0 - self.await_running_calls = 0 - self.resume_returns_before_running = False - self.cmd = _FakeCommandInterface(self) - self.file = _FakeFileInterface(self) - self.net = _FakeNetworkInterface(self) - - async def get_info(self, timeout: float | None = None, **_: object) -> object: - del timeout - tunnel = ( - types.SimpleNamespace(tunnel_key=self.tunnel_key) - if self.tunnel_key is not None - else None - ) - return types.SimpleNamespace(status=self.status, tunnel=tunnel) - - async def get_tunnel_url( - self, - port: int, - timeout: float | None = None, - **_: object, - ) -> str | None: - del timeout - if self.tunnel_key is None: - return None - return f"https://{port}-{self.tunnel_key}.tunnel.runloop.ai" - - async def snapshot_disk(self, **params: object) -> object: - self.snapshot_calls.append(dict(params)) - snapshot_id = f"snap-{len(self.snapshot_calls)}" - return types.SimpleNamespace(id=snapshot_id) - - async def shutdown(self, timeout: float | None = None, **_: object) -> object: - del timeout - self.shutdown_calls += 1 - self.status = "shutdown" - return types.SimpleNamespace(status=self.status) - - async def suspend(self, timeout: float | None = None, **_: object) -> object: - del timeout - self.suspend_calls += 1 - self.status = "suspended" - return types.SimpleNamespace(status=self.status) - - async def await_suspended(self) -> object: - return types.SimpleNamespace(status="suspended") - - async def await_running(self, **_: object) -> object: - self.await_running_calls += 1 - self.status = "running" - return types.SimpleNamespace(status=self.status) - - async def resume(self, timeout: float | None = None, **_: object) -> object: - del timeout - self.resume_calls += 1 - if self.resume_returns_before_running: - self.status = "resuming" - return types.SimpleNamespace(status=self.status) - self.status = "running" - return types.SimpleNamespace(status=self.status) - - -class _FakeDevboxOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.create_from_snapshot_calls: list[tuple[str, dict[str, object]]] = [] - self.from_id_calls: list[str] = [] - self.devboxes: dict[str, _FakeDevbox] = {} - self._counter = 0 - - def _new_devbox( - self, - *, - snapshot_source_id: str | None = None, - environment_variables: dict[str, str] | None = None, - launch_parameters: dict[str, object] | None = None, - ) -> _FakeDevbox: - self._counter += 1 - devbox = _FakeDevbox( - self._owner, - devbox_id=f"devbox-{self._counter}", - snapshot_source_id=snapshot_source_id, - environment_variables=environment_variables, - launch_parameters=launch_parameters, - ) - self.devboxes[devbox.id] = devbox - return devbox - - async def create(self, **params: object) -> _FakeDevbox: - self.create_calls.append(dict(params)) - return self._new_devbox( - environment_variables=cast(dict[str, str] | None, params.get("environment_variables")), - launch_parameters=cast(dict[str, object] | None, params.get("launch_parameters")), - ) - - async def create_from_snapshot(self, snapshot_id: str, **params: object) -> _FakeDevbox: - self.create_from_snapshot_calls.append((snapshot_id, dict(params))) - return self._new_devbox( - snapshot_source_id=snapshot_id, - environment_variables=cast(dict[str, str] | None, params.get("environment_variables")), - launch_parameters=cast(dict[str, object] | None, params.get("launch_parameters")), - ) - - def from_id(self, devbox_id: str) -> _FakeDevbox: - self.from_id_calls.append(devbox_id) - if devbox_id not in self.devboxes: - raise _FakeNotFoundError(devbox_id) - return self.devboxes[devbox_id] - - -class _FakeBlueprint: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, blueprint_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = blueprint_id - self.name = name or blueprint_id - self.logs_calls: list[dict[str, object]] = [] - self.delete_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name, status="build_complete") - - async def logs(self, **params: object) -> object: - self.logs_calls.append(dict(params)) - return types.SimpleNamespace(items=[f"log:{self.id}"]) - - async def delete(self, **params: object) -> object: - self.delete_calls.append(dict(params)) - return types.SimpleNamespace(id=self.id, deleted=True) - - -class _FakeBlueprintOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.blueprints: dict[str, _FakeBlueprint] = {} - self._counter = 0 - - def _new_blueprint(self, *, name: str | None = None) -> _FakeBlueprint: - self._counter += 1 - blueprint = _FakeBlueprint( - self._owner, - blueprint_id=f"blueprint-{self._counter}", - name=name, - ) - self.blueprints[blueprint.id] = blueprint - return blueprint - - async def create(self, **params: object) -> _FakeBlueprint: - self.create_calls.append(dict(params)) - return self._new_blueprint(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeBlueprint]: - self.list_calls.append(dict(params)) - return list(self.blueprints.values()) - - def from_id(self, blueprint_id: str) -> _FakeBlueprint: - self.from_id_calls.append(blueprint_id) - return self.blueprints.setdefault( - blueprint_id, - _FakeBlueprint(self._owner, blueprint_id=blueprint_id, name=blueprint_id), - ) - - -class _FakeBlueprintsAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.list_public_calls: list[dict[str, object]] = [] - self.logs_calls: list[tuple[str, dict[str, object]]] = [] - self.await_build_complete_calls: list[tuple[str, dict[str, object]]] = [] - - async def list_public(self, **params: object) -> object: - self.list_public_calls.append(dict(params)) - return types.SimpleNamespace(data=list(self._owner.blueprint.blueprints.values())) - - async def logs(self, blueprint_id: str, **params: object) -> object: - self.logs_calls.append((blueprint_id, dict(params))) - return types.SimpleNamespace(items=[f"log:{blueprint_id}"]) - - async def await_build_complete(self, blueprint_id: str, **params: object) -> object: - self.await_build_complete_calls.append((blueprint_id, dict(params))) - blueprint = self._owner.blueprint.from_id(blueprint_id) - return types.SimpleNamespace(id=blueprint.id, status="build_complete") - - -class _FakeBenchmarkRun: - def __init__(self, *, run_id: str, benchmark_id: str) -> None: - self.id = run_id - self.benchmark_id = benchmark_id - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, benchmark_id=self.benchmark_id) - - -class _FakeBenchmark: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, benchmark_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = benchmark_id - self.name = name or benchmark_id - self.update_calls: list[dict[str, object]] = [] - self.start_run_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def update(self, **params: object) -> object: - self.update_calls.append(dict(params)) - return types.SimpleNamespace(id=self.id, name=params.get("name", self.name)) - - async def start_run(self, **params: object) -> _FakeBenchmarkRun: - self.start_run_calls.append(dict(params)) - return _FakeBenchmarkRun(run_id=f"run-{self.id}", benchmark_id=self.id) - - async def list_runs(self, **_: object) -> list[_FakeBenchmarkRun]: - return [_FakeBenchmarkRun(run_id=f"run-{self.id}", benchmark_id=self.id)] - - -class _FakeBenchmarkOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.benchmarks: dict[str, _FakeBenchmark] = {} - self._counter = 0 - - def _new_benchmark(self, *, name: str | None = None) -> _FakeBenchmark: - self._counter += 1 - benchmark = _FakeBenchmark( - self._owner, benchmark_id=f"benchmark-{self._counter}", name=name - ) - self.benchmarks[benchmark.id] = benchmark - return benchmark - - async def create(self, **params: object) -> _FakeBenchmark: - self.create_calls.append(dict(params)) - return self._new_benchmark(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeBenchmark]: - self.list_calls.append(dict(params)) - return list(self.benchmarks.values()) - - def from_id(self, benchmark_id: str) -> _FakeBenchmark: - self.from_id_calls.append(benchmark_id) - return self.benchmarks.setdefault( - benchmark_id, - _FakeBenchmark(self._owner, benchmark_id=benchmark_id, name=benchmark_id), - ) - - -class _FakeBenchmarksAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.list_public_calls: list[dict[str, object]] = [] - self.definitions_calls: list[tuple[str, dict[str, object]]] = [] - self.update_scenarios_calls: list[tuple[str, dict[str, object]]] = [] - - async def list_public(self, **params: object) -> object: - self.list_public_calls.append(dict(params)) - return types.SimpleNamespace(data=list(self._owner.benchmark.benchmarks.values())) - - async def definitions(self, benchmark_id: str, **params: object) -> object: - self.definitions_calls.append((benchmark_id, dict(params))) - return types.SimpleNamespace(definitions=[types.SimpleNamespace(id=f"def-{benchmark_id}")]) - - async def update_scenarios(self, benchmark_id: str, **params: object) -> object: - self.update_scenarios_calls.append((benchmark_id, dict(params))) - return types.SimpleNamespace(id=benchmark_id, **dict(params)) - - -class _FakeSecret: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, name: str, value: str, secret_id: str - ) -> None: - self.owner = owner - self.name = name - self.value = value - self.id = secret_id - self.update_calls: list[tuple[str, dict[str, object]]] = [] - self.delete_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def update(self, value: str, **params: object) -> _FakeSecret: - self.update_calls.append((value, dict(params))) - self.value = value - return self - - async def delete(self, **params: object) -> object: - self.delete_calls.append(dict(params)) - self.owner.secret.secrets.pop(self.name, None) - return types.SimpleNamespace(id=self.id, name=self.name, deleted=True) - - -class _FakeSecretOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[tuple[str, str, dict[str, object]]] = [] - self.update_calls: list[tuple[str, str, dict[str, object]]] = [] - self.delete_calls: list[tuple[str, dict[str, object]]] = [] - self.list_calls: list[dict[str, object]] = [] - self.secrets: dict[str, _FakeSecret] = {} - self._counter = 0 - self.conflict_status_code = 409 - self.conflict_body: object | None = {"error": "secret exists"} - self.conflict_message: str | None = None - - def _new_secret(self, *, name: str, value: str) -> _FakeSecret: - self._counter += 1 - secret = _FakeSecret( - self._owner, name=name, value=value, secret_id=f"secret-{self._counter}" - ) - self.secrets[name] = secret - return secret - - async def create(self, name: str, value: str, **params: object) -> _FakeSecret: - self.create_calls.append((name, value, dict(params))) - if name in self.secrets: - raise _FakeAPIStatusError( - self.conflict_status_code, - body=self.conflict_body, - message=self.conflict_message, - ) - return self._new_secret(name=name, value=value) - - async def list(self, **params: object) -> list[_FakeSecret]: - self.list_calls.append(dict(params)) - return list(self.secrets.values()) - - async def update(self, secret: _FakeSecret | str, value: str, **params: object) -> _FakeSecret: - name = secret.name if isinstance(secret, _FakeSecret) else secret - self.update_calls.append((name, value, dict(params))) - secret_obj = self.secrets[name] - secret_obj.value = value - return secret_obj - - async def delete(self, secret: _FakeSecret | str, **params: object) -> object: - name = secret.name if isinstance(secret, _FakeSecret) else secret - self.delete_calls.append((name, dict(params))) - secret_obj = self.secrets.pop(name) - return types.SimpleNamespace(id=secret_obj.id, name=name, deleted=True) - - -class _FakeSecretsAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.retrieve_calls: list[tuple[str, dict[str, object]]] = [] - - async def retrieve(self, name: str, **params: object) -> object: - self.retrieve_calls.append((name, dict(params))) - secret = self._owner.secret.secrets[name] - return types.SimpleNamespace(id=secret.id, name=secret.name) - - -class _FakeNetworkPolicy: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, policy_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = policy_id - self.name = name or policy_id - self.update_calls: list[dict[str, object]] = [] - self.delete_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def update(self, **params: object) -> object: - self.update_calls.append(dict(params)) - return types.SimpleNamespace(id=self.id, name=params.get("name", self.name)) - - async def delete(self, **params: object) -> object: - self.delete_calls.append(dict(params)) - self.owner.network_policy.policies.pop(self.id, None) - return types.SimpleNamespace(id=self.id, deleted=True) - - -class _FakeNetworkPolicyOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.policies: dict[str, _FakeNetworkPolicy] = {} - self._counter = 0 - - def _new_policy(self, *, name: str | None = None) -> _FakeNetworkPolicy: - self._counter += 1 - policy = _FakeNetworkPolicy(self._owner, policy_id=f"policy-{self._counter}", name=name) - self.policies[policy.id] = policy - return policy - - async def create(self, **params: object) -> _FakeNetworkPolicy: - self.create_calls.append(dict(params)) - return self._new_policy(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeNetworkPolicy]: - self.list_calls.append(dict(params)) - return list(self.policies.values()) - - def from_id(self, network_policy_id: str) -> _FakeNetworkPolicy: - self.from_id_calls.append(network_policy_id) - return self.policies.setdefault( - network_policy_id, - _FakeNetworkPolicy(self._owner, policy_id=network_policy_id, name=network_policy_id), - ) - - -class _FakeNetworkPoliciesAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.retrieve_calls: list[tuple[str, dict[str, object]]] = [] - - async def retrieve(self, network_policy_id: str, **params: object) -> object: - self.retrieve_calls.append((network_policy_id, dict(params))) - policy = self._owner.network_policy.from_id(network_policy_id) - return types.SimpleNamespace(id=policy.id, name=policy.name) - - -class _FakeAxonSql: - def __init__(self) -> None: - self.query_calls: list[dict[str, object]] = [] - self.batch_calls: list[dict[str, object]] = [] - - async def query(self, **params: object) -> object: - self.query_calls.append(dict(params)) - return types.SimpleNamespace(rows=[["ok"]]) - - async def batch(self, **params: object) -> object: - self.batch_calls.append(dict(params)) - return types.SimpleNamespace(results=[types.SimpleNamespace(success=True)]) - - -class _FakeAxon: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, axon_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = axon_id - self.name = name or axon_id - self.publish_calls: list[dict[str, object]] = [] - self.sql = _FakeAxonSql() - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def publish(self, **params: object) -> object: - self.publish_calls.append(dict(params)) - return types.SimpleNamespace(published=True) - - -class _FakeAxonOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.axons: dict[str, _FakeAxon] = {} - self._counter = 0 - - def _new_axon(self, *, name: str | None = None) -> _FakeAxon: - self._counter += 1 - axon = _FakeAxon(self._owner, axon_id=f"axon-{self._counter}", name=name) - self.axons[axon.id] = axon - return axon - - async def create(self, **params: object) -> _FakeAxon: - self.create_calls.append(dict(params)) - return self._new_axon(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeAxon]: - self.list_calls.append(dict(params)) - return list(self.axons.values()) - - def from_id(self, axon_id: str) -> _FakeAxon: - self.from_id_calls.append(axon_id) - return self.axons.setdefault( - axon_id, - _FakeAxon(self._owner, axon_id=axon_id, name=axon_id), - ) - - -class _FakeLaunchAfterIdle(BaseModel): - idle_time_seconds: int - on_idle: Literal["shutdown", "suspend"] - - def to_dict( - self, - *, - mode: str = "python", - exclude_none: bool = False, - exclude_defaults: bool = False, - ) -> dict[str, object]: - return cast( - dict[str, object], - self.model_dump( - mode=cast(Literal["json", "python"], mode), - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ), - ) - - -class _FakeUserParameters(BaseModel): - username: str - uid: int - - def to_dict( - self, - *, - mode: str = "python", - exclude_none: bool = False, - exclude_defaults: bool = False, - ) -> dict[str, object]: - return cast( - dict[str, object], - self.model_dump( - mode=cast(Literal["json", "python"], mode), - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ), - ) - - -class _FakeLaunchParameters(BaseModel): - network_policy_id: str | None = None - resource_size_request: ( - Literal["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"] | None - ) = None - custom_cpu_cores: float | None = None - custom_gb_memory: int | None = None - custom_disk_size: int | None = None - architecture: Literal["x86_64", "arm64"] | None = None - keep_alive_time_seconds: int | None = None - after_idle: _FakeLaunchAfterIdle | dict[str, object] | None = None - launch_commands: list[str] | tuple[str, ...] | None = None - required_services: list[str] | tuple[str, ...] | None = None - user_parameters: dict[str, object] | None = None - - def to_dict( - self, - *, - mode: str = "python", - exclude_none: bool = False, - exclude_defaults: bool = False, - ) -> dict[str, object]: - return cast( - dict[str, object], - self.model_dump( - mode=cast(Literal["json", "python"], mode), - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ), - ) - - -class _FakeAsyncRunloopSDK: - created_instances: list[_FakeAsyncRunloopSDK] = [] - - def __init__( - self, - *, - bearer_token: str | None = None, - base_url: str | None = None, - **_: object, - ) -> None: - self.bearer_token = bearer_token - self.base_url = base_url or "https://api.runloop.ai" - self.executions: dict[str, _FakeExecution] = {} - self.devbox = _FakeDevboxOps(self) - self.blueprint = _FakeBlueprintOps(self) - self.benchmark = _FakeBenchmarkOps(self) - self.secret = _FakeSecretOps(self) - self.network_policy = _FakeNetworkPolicyOps(self) - self.axon = _FakeAxonOps(self) - self.api = types.SimpleNamespace( - devboxes=types.SimpleNamespace(executions=_FakeExecutionsAPI(self)), - blueprints=_FakeBlueprintsAPI(self), - benchmarks=_FakeBenchmarksAPI(self), - secrets=_FakeSecretsAPI(self), - network_policies=_FakeNetworkPoliciesAPI(self), - ) - type(self).created_instances.append(self) - - async def aclose(self) -> None: - return None - - -def _load_runloop_module(monkeypatch: pytest.MonkeyPatch) -> Any: - _FakeAsyncRunloopSDK.created_instances.clear() - _FakeExecution._counter = 0 - fake_runloop: Any = types.ModuleType("runloop_api_client") - fake_runloop.APIConnectionError = _FakeAPIConnectionError - fake_runloop.APIResponseValidationError = _FakeAPIResponseValidationError - fake_runloop.APITimeoutError = _FakeAPITimeoutError - fake_runloop.APIStatusError = _FakeAPIStatusError - fake_runloop.AuthenticationError = _FakeAuthenticationError - fake_runloop.BadRequestError = _FakeBadRequestError - fake_runloop.InternalServerError = _FakeInternalServerError - fake_runloop.NotFoundError = _FakeNotFoundError - fake_runloop.PermissionDeniedError = _FakePermissionDeniedError - fake_runloop.RateLimitError = _FakeRateLimitError - fake_runloop.RunloopError = _FakeRunloopError - fake_runloop.UnprocessableEntityError = _FakeUnprocessableEntityError - - fake_sdk: Any = types.ModuleType("runloop_api_client.sdk") - fake_sdk.AsyncRunloopSDK = _FakeAsyncRunloopSDK - - fake_types: Any = types.ModuleType("runloop_api_client.types") - fake_types.AfterIdle = _FakeLaunchAfterIdle - fake_types.LaunchParameters = _FakeLaunchParameters - fake_shared: Any = types.ModuleType("runloop_api_client.types.shared") - fake_launch_parameters_module: Any = types.ModuleType( - "runloop_api_client.types.shared.launch_parameters" - ) - fake_launch_parameters_module.UserParameters = _FakeUserParameters - fake_shared.launch_parameters = fake_launch_parameters_module - fake_types.shared = fake_shared - - monkeypatch.setitem(sys.modules, "runloop_api_client", fake_runloop) - monkeypatch.setitem(sys.modules, "runloop_api_client.sdk", fake_sdk) - monkeypatch.setitem(sys.modules, "runloop_api_client.types", fake_types) - monkeypatch.setitem(sys.modules, "runloop_api_client.types.shared", fake_shared) - monkeypatch.setitem( - sys.modules, - "runloop_api_client.types.shared.launch_parameters", - fake_launch_parameters_module, - ) - # Re-importing an adapter also replaces its registered model classes. - # Record undo before clearing these entries for fresh class registration; - # unrelated registrations must survive teardown. - monkeypatch.setitem( - SandboxSessionState._subclass_registry, - "runloop", - SandboxSessionState._subclass_registry.get("runloop", SandboxSessionState), - ) - del SandboxSessionState._subclass_registry["runloop"] - module_names = ( - "agents.extensions.sandbox.runloop.sandbox", - "agents.extensions.sandbox.runloop", - ) - for name in module_names: - monkeypatch.delitem(sys.modules, name, raising=False) - - module: Any = importlib.import_module("agents.extensions.sandbox.runloop.sandbox") - # Track the fresh imports too, so teardown removes them before restoring - # any original modules alongside the real provider SDK. - for name in module_names: - monkeypatch.setitem(sys.modules, name, sys.modules.pop(name)) - return module - - -def _build_tar_bytes(files: dict[str, bytes]) -> bytes: - buffer = io.BytesIO() - with tarfile.open(fileobj=buffer, mode="w") as archive: - for name, payload in files.items(): - info = tarfile.TarInfo(name=name) - info.size = len(payload) - archive.addfile(info, io.BytesIO(payload)) - return buffer.getvalue() - - -def test_runloop_package_re_exports_backend_symbols(monkeypatch: pytest.MonkeyPatch) -> None: - runloop_module = _load_runloop_module(monkeypatch) - package_module = importlib.import_module("agents.extensions.sandbox.runloop") - - assert package_module.RunloopSandboxClient is runloop_module.RunloopSandboxClient - assert package_module.RunloopPlatformClient is runloop_module.RunloopPlatformClient - assert package_module.RunloopLaunchParameters is runloop_module.RunloopLaunchParameters - assert package_module.RunloopAfterIdle is runloop_module.RunloopAfterIdle - assert package_module.RunloopUserParameters is runloop_module.RunloopUserParameters - - -@pytest.fixture(autouse=True) -def _trust_recording_mounts_for_tests(monkeypatch: pytest.MonkeyPatch) -> None: - from agents.sandbox import _mount_security - - original = _mount_security._mount_class_is_trusted - monkeypatch.setattr( - _mount_security, - "_mount_class_is_trusted", - lambda mount: isinstance(mount, _RecordingMount) or original(mount), - ) - - -class _RecordingMount(Mount): - type: str = "runloop_recording_mount" - mount_strategy: InContainerMountStrategy = Field( - default_factory=lambda: InContainerMountStrategy(pattern=MountpointMountPattern()) - ) - _mounted_paths: list[Path] = PrivateAttr(default_factory=list) - _unmounted_paths: list[Path] = PrivateAttr(default_factory=list) - - def supported_in_container_patterns( - self, - ) -> tuple[builtins.type[MountpointMountPattern], ...]: - return (MountpointMountPattern,) - - def in_container_adapter(self) -> InContainerMountAdapter: - mount = self - - class _Adapter(InContainerMountAdapter): - def validate(self, strategy: InContainerMountStrategy) -> None: - _ = strategy - - async def activate( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> list[MaterializedFile]: - _ = (strategy, session, base_dir) - path = mount._resolve_mount_path(session, dest) - mount._mounted_paths.append(path) - return [] - - async def deactivate( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> None: - _ = (strategy, session, base_dir) - path = mount._resolve_mount_path(session, dest) - mount._unmounted_paths.append(path) - - async def teardown_for_snapshot( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - path: Path, - ) -> None: - _ = (strategy, session) - mount._unmounted_paths.append(path) - - async def restore_after_snapshot( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - path: Path, - ) -> None: - _ = (strategy, session) - mount._mounted_paths.append(path) - - return _Adapter(self) - - -class TestRunloopSandbox: - @pytest.mark.asyncio - async def test_runloop_does_not_advertise_pty_support( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - - assert session.supports_pty() is False - - @pytest.mark.asyncio - async def test_create_uses_runloop_default_workspace_root( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - - assert session.state.manifest.root == runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT - - @pytest.mark.asyncio - async def test_create_uses_root_workspace_root_when_root_launch_enabled( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - user_parameters=runloop_module.RunloopUserParameters( - username="root", - uid=0, - ), - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert session.state.manifest.root == runloop_module.DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "user_parameters": {"username": "root", "uid": 0} - } - - def test_runloop_sdk_backed_user_parameters_construct_from_extension_exports( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - user_parameters = runloop_module.RunloopUserParameters(username="user", uid=1000) - - assert user_parameters.username == "user" - assert user_parameters.uid == 1000 - assert user_parameters.to_dict(mode="json", exclude_none=True) == { - "username": "user", - "uid": 1000, - } - - @pytest.mark.asyncio - async def test_create_normalizes_dict_user_parameters( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - user_parameters={"username": "root", "uid": 0}, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "user_parameters": {"username": "root", "uid": 0} - } - assert session.state.user_parameters is not None - assert session.state.user_parameters.username == "root" - assert session.state.user_parameters.uid == 0 - - @pytest.mark.asyncio - async def test_empty_manifest_exec_succeeds_immediately_after_start_non_root( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project"), - options=runloop_module.RunloopSandboxClientOptions(), - ) - await session.start() - result = await session.exec("pwd", shell=False) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - command, _ = devbox.exec_calls[-1] - - assert result.ok() - assert "cd /home/user/project &&" in command - - @pytest.mark.asyncio - async def test_empty_manifest_exec_succeeds_immediately_after_start_root( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root="/root/project"), - options=runloop_module.RunloopSandboxClientOptions( - user_parameters=runloop_module.RunloopUserParameters( - username="root", - uid=0, - ) - ), - ) - await session.start() - result = await session.exec("pwd", shell=False) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - command, _ = devbox.exec_calls[-1] - - assert result.ok() - assert "cd /root/project &&" in command - - @pytest.mark.asyncio - async def test_create_merges_env_vars_with_manifest_precedence( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - await client.create( - manifest=Manifest( - root=runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT, - environment=Environment(value={"SHARED": "manifest", "ONLY_MANIFEST": "1"}), - ), - options=runloop_module.RunloopSandboxClientOptions( - env_vars={"SHARED": "option", "ONLY_OPTION": "1"}, - ), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls - create_params = sdk.devbox.create_calls[0] - assert create_params["environment_variables"] == { - "SHARED": "manifest", - "ONLY_MANIFEST": "1", - "ONLY_OPTION": "1", - } - - def test_runloop_client_options_preserve_positional_exposed_ports( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - options = runloop_module.RunloopSandboxClientOptions( - None, - None, - None, - False, - None, - None, - (8765,), - ) - - assert options.exposed_ports == (8765,) - - def test_runloop_client_options_append_new_fields_after_existing_positionals( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - options = runloop_module.RunloopSandboxClientOptions( - None, - None, - None, - False, - None, - None, - (8765,), - None, - launch_parameters=runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - ), - managed_secrets={"API_KEY": "secret"}, - ) - - assert options.exposed_ports == (8765,) - assert options.launch_parameters is not None - assert options.launch_parameters.network_policy_id == "np-123" - assert options.managed_secrets == {"API_KEY": "secret"} - - def test_runloop_sdk_backed_launch_models_construct_from_extension_exports( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - after_idle = runloop_module.RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend") - launch_parameters = runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - after_idle=after_idle, - launch_commands=["echo hi"], - ) - - assert after_idle.idle_time_seconds == 300 - assert launch_parameters.after_idle is not None - assert launch_parameters.after_idle.on_idle == "suspend" - assert launch_parameters.to_dict(mode="json", exclude_none=True)["launch_commands"] == [ - "echo hi" - ] - - def test_runloop_tunnel_config_remains_extension_model( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - tunnel = runloop_module.RunloopTunnelConfig(auth_mode="authenticated") - - assert isinstance(tunnel, BaseModel) - assert tunnel.model_dump(mode="json", exclude_none=True) == {"auth_mode": "authenticated"} - - @pytest.mark.asyncio - async def test_create_passes_runloop_native_launch_options_and_persists_secret_refs( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - name="native-runloop", - user_parameters=runloop_module.RunloopUserParameters(username="user", uid=1000), - launch_parameters=runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - resource_size_request="MEDIUM", - custom_cpu_cores=2, - custom_gb_memory=8, - custom_disk_size=16, - architecture="arm64", - keep_alive_time_seconds=600, - after_idle=runloop_module.RunloopAfterIdle( - idle_time_seconds=300, - on_idle="suspend", - ), - launch_commands=("echo hi",), - required_services=("postgres",), - ), - tunnel=runloop_module.RunloopTunnelConfig( - auth_mode="authenticated", - http_keep_alive=True, - wake_on_http=True, - ), - gateways={ - "GWS_OPENAI": runloop_module.RunloopGatewaySpec( - gateway="openai-gateway", - secret="OPENAI_GATEWAY_SECRET", - ) - }, - mcp={ - "MCP_TOKEN": runloop_module.RunloopMcpSpec( - mcp_config="github-readonly", - secret="MCP_SECRET", - ) - }, - metadata={"team": "agents"}, - managed_secrets={"API_KEY": "super-secret"}, - ), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.secret.create_calls == [("API_KEY", "super-secret", {"timeout": 30.0})] - assert sdk.devbox.create_calls - create_params = sdk.devbox.create_calls[0] - assert create_params["launch_parameters"] == { - "network_policy_id": "np-123", - "resource_size_request": "MEDIUM", - "custom_cpu_cores": 2.0, - "custom_gb_memory": 8, - "custom_disk_size": 16, - "architecture": "arm64", - "keep_alive_time_seconds": 600, - "after_idle": {"idle_time_seconds": 300, "on_idle": "suspend"}, - "launch_commands": ["echo hi"], - "required_services": ["postgres"], - "user_parameters": {"username": "user", "uid": 1000}, - } - assert create_params["tunnel"] == { - "auth_mode": "authenticated", - "http_keep_alive": True, - "wake_on_http": True, - } - assert create_params["gateways"] == { - "GWS_OPENAI": {"gateway": "openai-gateway", "secret": "OPENAI_GATEWAY_SECRET"} - } - assert create_params["mcp"] == { - "MCP_TOKEN": {"mcp_config": "github-readonly", "secret": "MCP_SECRET"} - } - assert create_params["metadata"] == {"team": "agents"} - assert create_params["secrets"] == {"API_KEY": "API_KEY"} - assert session.state.secret_refs == {"API_KEY": "API_KEY"} - assert session.state.metadata == {"team": "agents"} - assert "super-secret" not in json.dumps(session.state.model_dump(mode="json")) - - @pytest.mark.asyncio - async def test_create_normalizes_dict_launch_parameters_and_tunnel_options( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - launch_parameters={ - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - }, - tunnel={ - "auth_mode": "authenticated", - "wake_on_http": True, - }, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - } - assert sdk.devbox.create_calls[0]["tunnel"] == { - "auth_mode": "authenticated", - "wake_on_http": True, - } - assert session.state.launch_parameters is not None - assert session.state.launch_parameters.network_policy_id == "np-123" - assert session.state.tunnel is not None - assert session.state.tunnel.auth_mode == "authenticated" - - @pytest.mark.asyncio - async def test_create_normalizes_dict_launch_parameters_and_tunnel_from_parsed_options( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - options = cast( - Any, - BaseSandboxClientOptions.parse( - { - "type": "runloop", - "launch_parameters": { - "network_policy_id": "np-456", - "required_services": ["postgres"], - }, - "tunnel": { - "auth_mode": "open", - "http_keep_alive": True, - }, - } - ), - ) - - assert options.type == "runloop" - assert options.launch_parameters is not None - assert options.tunnel is not None - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=options) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "network_policy_id": "np-456", - "required_services": ["postgres"], - } - assert sdk.devbox.create_calls[0]["tunnel"] == { - "auth_mode": "open", - "http_keep_alive": True, - } - assert session.state.launch_parameters is not None - assert session.state.launch_parameters.network_policy_id == "np-456" - assert session.state.tunnel is not None - assert session.state.tunnel.auth_mode == "open" - - @pytest.mark.asyncio - async def test_run_state_round_trip_preserves_runloop_session_state_without_secret_values( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - agent = Agent(name="TestAgent") - context: RunContextWrapper[dict[str, str]] = RunContextWrapper(context={}) - state: RunState[dict[str, str], Agent[Any]] = make_run_state( - agent, - context=context, - original_input="test", - ) - client = runloop_module.RunloopSandboxClient(bearer_token="test-token") - session_state = runloop_module.RunloopSandboxSessionState( - manifest=Manifest(), - snapshot=NoopSnapshot(id="runloop-state"), - devbox_id="devbox-123", - launch_parameters=runloop_module.RunloopLaunchParameters(network_policy_id="np-123"), - secret_refs={"API_KEY": "API_KEY"}, - ) - serialized_session_state = client.serialize_session_state(session_state) - state._sandbox = { - "backend_id": "runloop", - "current_agent_key": agent.name, - "current_agent_name": agent.name, - "session_state": serialized_session_state, - "sessions_by_agent": { - agent.name: { - "agent_name": agent.name, - "session_state": serialized_session_state, - } - }, - } - - restored = await RunState.from_json(agent, state.to_json()) - - assert restored._sandbox is not None - restored_session_payload = cast(dict[str, object], restored._sandbox["session_state"]) - assert restored_session_payload["secret_refs"] == {"API_KEY": "API_KEY"} - assert "managed_secrets" not in restored_session_payload - assert "secret-value" not in json.dumps(restored_session_payload) - - restored_session_state = client.deserialize_session_state(restored_session_payload) - assert isinstance(restored_session_state, runloop_module.RunloopSandboxSessionState) - assert restored_session_state.secret_refs == {"API_KEY": "API_KEY"} - assert restored_session_state.launch_parameters is not None - assert restored_session_state.launch_parameters.network_policy_id == "np-123" - - await client.close() - - @pytest.mark.asyncio - async def test_create_upserts_managed_secret_when_secret_exists( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.secret._new_secret(name="API_KEY", value="old-value") - - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={"API_KEY": "new-value"}, - ) - ) - - assert sdk.secret.create_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert sdk.secret.update_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert session.state.secret_refs == {"API_KEY": "API_KEY"} - - @pytest.mark.asyncio - async def test_create_upserts_managed_secret_when_runloop_returns_bad_request_exists( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.secret._new_secret(name="API_KEY", value="old-value") - sdk.secret.conflict_status_code = 400 - sdk.secret.conflict_body = { - "message": "Secret with name 'API_KEY' already exists", - } - sdk.secret.conflict_message = "Secret with name 'API_KEY' already exists" - - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={"API_KEY": "new-value"}, - ) - ) - - assert sdk.secret.create_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert sdk.secret.update_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert session.state.secret_refs == {"API_KEY": "API_KEY"} - - @pytest.mark.asyncio - async def test_create_references_existing_managed_secrets_without_uploading_values( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={ - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - "API_KEY": runloop_module.RunloopExistingSecret(), - }, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.secret.create_calls == [] - assert sdk.secret.update_calls == [] - create_params = sdk.devbox.create_calls[0] - assert create_params["secrets"] == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - assert session.state.secret_refs == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - - @pytest.mark.asyncio - async def test_create_mixes_existing_and_uploaded_managed_secrets( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={ - "API_KEY": "super-secret", - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - }, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.secret.create_calls == [("API_KEY", "super-secret", {"timeout": 30.0})] - assert sdk.secret.update_calls == [] - assert "SHARED_TOKEN" not in sdk.secret.secrets - create_params = sdk.devbox.create_calls[0] - assert create_params["secrets"] == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - assert session.state.secret_refs == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - assert "super-secret" not in json.dumps(session.state.model_dump(mode="json")) - - def test_runloop_client_options_round_trip_existing_managed_secrets( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - options = runloop_module.RunloopSandboxClientOptions( - managed_secrets={ - "API_KEY": "super-secret", - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - }, - ) - - restored = runloop_module.RunloopSandboxClientOptions.model_validate( - json.loads(json.dumps(options.model_dump(mode="json"))) - ) - - assert restored.managed_secrets == { - "API_KEY": "super-secret", - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - } - - @pytest.mark.asyncio - async def test_resume_and_snapshot_restore_reuse_runloop_native_options( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - name="native-runloop", - launch_parameters=runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - launch_commands=("echo hi",), - ), - tunnel=runloop_module.RunloopTunnelConfig(auth_mode="open"), - gateways={ - "GWS_OPENAI": runloop_module.RunloopGatewaySpec( - gateway="openai-gateway", - secret="OPENAI_GATEWAY_SECRET", - ) - }, - mcp={ - "MCP_TOKEN": runloop_module.RunloopMcpSpec( - mcp_config="github-readonly", - secret="MCP_SECRET", - ) - }, - metadata={"team": "agents"}, - managed_secrets={"API_KEY": "super-secret"}, - ), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.devboxes[session.state.devbox_id].status = "shutdown" - sdk.devbox.create_calls.clear() - - resumed = await client.resume(session.state) - await resumed._inner.hydrate_workspace( # noqa: SLF001 - io.BytesIO(runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-123")) # noqa: SLF001 - ) - - assert sdk.devbox.create_calls == [ - { - "timeout": session.state.timeouts.create_s, - "name": "native-runloop", - "launch_parameters": { - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - }, - "tunnel": {"auth_mode": "open"}, - "gateways": { - "GWS_OPENAI": { - "gateway": "openai-gateway", - "secret": "OPENAI_GATEWAY_SECRET", - } - }, - "mcp": { - "MCP_TOKEN": { - "mcp_config": "github-readonly", - "secret": "MCP_SECRET", - } - }, - "metadata": {"team": "agents"}, - "secrets": {"API_KEY": "API_KEY"}, - } - ] - assert sdk.devbox.create_from_snapshot_calls == [ - ( - "snap-123", - { - "timeout": session.state.timeouts.resume_s, - "name": "native-runloop", - "launch_parameters": { - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - }, - "tunnel": {"auth_mode": "open"}, - "gateways": { - "GWS_OPENAI": { - "gateway": "openai-gateway", - "secret": "OPENAI_GATEWAY_SECRET", - } - }, - "mcp": { - "MCP_TOKEN": { - "mcp_config": "github-readonly", - "secret": "MCP_SECRET", - } - }, - "metadata": {"team": "agents"}, - "secrets": {"API_KEY": "API_KEY"}, - }, - ) - ] - - @pytest.mark.asyncio - async def test_platform_blueprints_and_benchmarks_clients( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - blueprint = await client.platform.blueprints.create(name="bp1") - listed_blueprints = await client.platform.blueprints.list(limit=5) - public_blueprints = await client.platform.blueprints.list_public(limit=10) - await client.platform.blueprints.logs(blueprint.id) - build_info = await client.platform.blueprints.await_build_complete(blueprint.id) - await client.platform.blueprints.delete(blueprint.id) - - benchmark = await client.platform.benchmarks.create( - name="bm1", - required_secret_names=["API_KEY"], - ) - listed_benchmarks = await client.platform.benchmarks.list(limit=5) - public_benchmarks = await client.platform.benchmarks.list_public(limit=10) - await client.platform.benchmarks.update(benchmark.id, description="desc") - definitions = await client.platform.benchmarks.definitions(benchmark.id) - run = await client.platform.benchmarks.start_run(benchmark.id, run_name="eval") - scenario_update = await client.platform.benchmarks.update_scenarios( - benchmark.id, - scenarios_to_add=["scenario-1"], - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert blueprint in listed_blueprints - assert public_blueprints.data - assert build_info.status == "build_complete" - assert sdk.api.blueprints.logs_calls == [(blueprint.id, {})] - assert sdk.api.blueprints.await_build_complete_calls == [(blueprint.id, {})] - assert benchmark in listed_benchmarks - assert public_benchmarks.data - assert definitions.definitions[0].id == f"def-{benchmark.id}" - assert run.benchmark_id == benchmark.id - assert scenario_update.scenarios_to_add == ["scenario-1"] - - @pytest.mark.asyncio - async def test_platform_secrets_network_policies_and_axons_clients( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - assert not hasattr(client.platform.axons, "subscribe_sse") - secret = await client.platform.secrets.create(name="SECRET_A", value="secret-value") - listed_secrets = await client.platform.secrets.list() - secret_info = await client.platform.secrets.get("SECRET_A") - updated_secret = await client.platform.secrets.update( - name="SECRET_A", - value="secret-value-2", - ) - deleted_secret = await client.platform.secrets.delete("SECRET_A") - - policy = await client.platform.network_policies.create(name="policy-a", allow_all=True) - listed_policies = await client.platform.network_policies.list() - await client.platform.network_policies.update(policy.id, description="limited") - deleted_policy = await client.platform.network_policies.delete(policy.id) - - axon = await client.platform.axons.create(name="axon-a") - listed_axons = await client.platform.axons.list() - publish_result = await client.platform.axons.publish( - axon.id, - event_type="task_done", - origin="AGENT_EVENT", - payload="{}", - source="agent", - ) - query_result = await client.platform.axons.query_sql(axon.id, sql="select 1") - batch_result = await client.platform.axons.batch_sql( - axon.id, - statements=[{"sql": "select 1"}], - ) - - assert secret in listed_secrets - assert secret_info.name == "SECRET_A" - assert updated_secret.name == "SECRET_A" - assert deleted_secret.name == "SECRET_A" - assert policy in listed_policies - assert deleted_policy.id == policy.id - assert axon in listed_axons - assert publish_result.published is True - assert query_result.rows == [["ok"]] - assert batch_result.results[0].success is True - - @pytest.mark.asyncio - async def test_resume_reconnects_suspended_devbox_and_skips_start( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(pause_on_exit=True), - ) - state = cast( - Any, - client.deserialize_session_state(client.serialize_session_state(session.state)), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.create_calls.clear() - sdk.devbox.devboxes[state.devbox_id].status = "suspended" - - resumed = await client.resume(state) - - assert sdk.devbox.from_id_calls == [state.devbox_id] - assert sdk.devbox.create_calls == [] - assert resumed._inner._skip_start is True # noqa: SLF001 - - @pytest.mark.asyncio - async def test_skip_start_rejects_unsafe_mount_before_provider_work( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - sentinel = "runloop-start-secret" - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - session.state.manifest = Manifest( - entries={ - "data": S3Mount( - bucket="bucket", - access_key_id="access-key", - secret_access_key=sentinel, - mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()), - ) - } - ) - session._inner._skip_start = True # noqa: SLF001 - exec_calls_before = len(devbox.exec_calls) - resume_calls_before = devbox.resume_calls - snapshot_calls_before = len(devbox.snapshot_calls) - - with pytest.raises(MountConfigError) as exc: - await session.start() - - assert len(devbox.exec_calls) == exec_calls_before - assert devbox.resume_calls == resume_calls_before - assert len(devbox.snapshot_calls) == snapshot_calls_before - assert sentinel not in str(exc.value) - - @pytest.mark.asyncio - async def test_resume_reconnects_running_devbox_without_pause( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[state.devbox_id] - devbox.files["existing.txt"] = b"keep" - sdk.devbox.create_calls.clear() - - resumed = await client.resume(state) - await resumed.start() - - assert sdk.devbox.from_id_calls == [state.devbox_id] - assert sdk.devbox.create_calls == [] - assert resumed.state.devbox_id == state.devbox_id - assert resumed._inner._skip_start is False # noqa: SLF001 - assert devbox.files["existing.txt"] == b"keep" - - @pytest.mark.asyncio - async def test_resume_reconnected_devbox_without_pause_does_not_reprovision_accounts( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - session.state.snapshot = _RestorableSnapshot(id="snapshot-mismatch") - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.create_calls.clear() - - resumed = await client.resume(state) - inner = resumed._inner - provision_called = False - - async def _cannot_skip(self: object, *, is_running: bool) -> bool: - return False - - async def _restore(self: object) -> None: - return None - - async def _provision_accounts() -> None: - nonlocal provision_called - provision_called = True - - async def _reapply(self: object) -> None: - return None - - monkeypatch.setattr( - inner, - "_can_skip_snapshot_restore_on_resume", - types.MethodType(_cannot_skip, inner), - ) - monkeypatch.setattr( - inner, - "_restore_snapshot_into_workspace_on_resume", - types.MethodType(_restore, inner), - ) - monkeypatch.setattr(inner, "provision_manifest_accounts", _provision_accounts) - monkeypatch.setattr( - inner, - "_reapply_ephemeral_manifest_on_resume", - types.MethodType(_reapply, inner), - ) - - await resumed.start() - - assert sdk.devbox.from_id_calls == [state.devbox_id] - assert sdk.devbox.create_calls == [] - assert provision_called is False - - @pytest.mark.asyncio - async def test_resume_recreates_terminal_devbox_without_pause( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.devboxes[state.devbox_id].status = "shutdown" - sdk.devbox.create_calls.clear() - original_devbox_id = state.devbox_id - - resumed = await client.resume(state) - - assert sdk.devbox.from_id_calls == [original_devbox_id] - assert len(sdk.devbox.create_calls) == 1 - assert resumed.state.devbox_id != original_devbox_id - assert resumed._inner._skip_start is False # noqa: SLF001 - - @pytest.mark.asyncio - async def test_resume_waits_for_devbox_running_before_skip_start( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(pause_on_exit=True), - ) - session.state.snapshot = _RestorableSnapshot(id="resume-race") - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.create_calls.clear() - devbox = sdk.devbox.devboxes[state.devbox_id] - devbox.status = "suspended" - devbox.resume_returns_before_running = True - - resumed = await client.resume(state) - inner = resumed._inner - - async def _can_skip(self: object, *, is_running: bool) -> bool: - return is_running - - async def _reapply(self: object) -> None: - return None - - async def _restore(self: object) -> None: - raise AssertionError("resume should wait for running instead of restoring snapshot") - - monkeypatch.setattr( - inner, - "_can_skip_snapshot_restore_on_resume", - types.MethodType(_can_skip, inner), - ) - monkeypatch.setattr( - inner, - "_reapply_ephemeral_manifest_on_resume", - types.MethodType(_reapply, inner), - ) - monkeypatch.setattr( - inner, - "_restore_snapshot_into_workspace_on_resume", - types.MethodType(_restore, inner), - ) - - await resumed.start() - - assert devbox.resume_calls == 1 - assert devbox.await_running_calls == 1 - assert devbox.status == "running" - assert sdk.devbox.create_calls == [] - assert resumed._inner._skip_start is True # noqa: SLF001 - - @pytest.mark.asyncio - async def test_skip_start_resume_passes_dependencies_to_snapshot_restorable( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - dependencies = Dependencies().bind_value("test.dep", object()) - - async with runloop_module.RunloopSandboxClient(dependencies=dependencies) as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(pause_on_exit=True), - ) - snapshot = _DependencyAwareSnapshot(id="dep-aware") - session.state.snapshot = snapshot - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.devboxes[state.devbox_id].status = "suspended" - - resumed = await client.resume(state) - inner = resumed._inner - - async def _can_skip(self: object, *, is_running: bool) -> bool: - return is_running - - async def _reapply(self: object) -> None: - return None - - monkeypatch.setattr( - inner, - "_can_skip_snapshot_restore_on_resume", - types.MethodType(_can_skip, inner), - ) - monkeypatch.setattr( - inner, - "_reapply_ephemeral_manifest_on_resume", - types.MethodType(_reapply, inner), - ) - - await resumed.start() - - assert snapshot.restorable_dependencies - assert snapshot.restorable_dependencies[-1] is not None - - @pytest.mark.asyncio - async def test_root_launch_exec_and_io_use_root_home( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root="/root/project"), - options=runloop_module.RunloopSandboxClientOptions( - user_parameters=runloop_module.RunloopUserParameters( - username="root", - uid=0, - ) - ), - ) - await session.start() - await session.exec("pwd && echo hello", shell=True) - exec_sdk = _FakeAsyncRunloopSDK.created_instances[-1] - exec_devbox = exec_sdk.devbox.devboxes[session.state.devbox_id] - command, _ = exec_devbox.exec_calls[-1] - await session.write("/root/project/output.txt", io.BytesIO(b"hello")) - payload = await session.read("/root/project/output.txt") - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert payload.read() == b"hello" - assert "cd /root/project &&" in command - assert devbox.files["project/output.txt"] == b"hello" - - @pytest.mark.asyncio - async def test_delete_shuts_down_runloop_devbox( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - await client.delete(session) - - assert devbox.shutdown_calls == 1 - assert devbox.status == "shutdown" - - @pytest.mark.asyncio - async def test_resolve_exposed_port_enables_tunnel_and_formats_endpoint( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(exposed_ports=(4500,)), - ) - await session.start() - endpoint = await session.resolve_exposed_port(4500) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert endpoint == ExposedPortEndpoint( - host="4500-test-key.tunnel.runloop.ai", - port=443, - tls=True, - ) - assert devbox.enable_tunnel_calls - - @pytest.mark.asyncio - async def test_exec_timeout_raises_for_runloop_one_shot_exec( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - with pytest.raises(runloop_module.ExecTimeoutError): - await session.exec("sleep-forever", shell=False, timeout=0.01) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - executions = list(sdk.executions.values()) - - assert executions - assert any("sleep-forever" in execution.command for execution in executions) - - @pytest.mark.asyncio - async def test_exec_maps_runloop_http_408_to_timeout_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_timeout(*args: object, **kwargs: object) -> object: - _ = (args, kwargs) - raise _FakeAPIStatusError( - 408, - body={"error": "execution timed out"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute", - method="POST", - ) - - monkeypatch.setattr(devbox.cmd, "exec", _raise_timeout) - - with pytest.raises(runloop_module.ExecTimeoutError) as exc_info: - await session.exec("pwd", shell=False, timeout=3.0) - - assert exc_info.value.context["http_status"] == 408 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["request_method"] == "POST" - assert exc_info.value.context["request_url"] == ( - f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute" - ) - assert exc_info.value.context["provider_body"] == {"error": "execution timed out"} - - @pytest.mark.asyncio - async def test_exec_maps_runloop_http_error_to_transport_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_rate_limit(*args: object, **kwargs: object) -> object: - _ = (args, kwargs) - raise _FakeAPIStatusError( - 429, - body={"error": "rate limited"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute", - method="POST", - ) - - monkeypatch.setattr(devbox.cmd, "exec", _raise_rate_limit) - - with pytest.raises(runloop_module.ExecTransportError) as exc_info: - await session.exec("pwd", shell=False) - - assert exc_info.value.context["http_status"] == 429 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "rate limited"} - assert exc_info.value.context["detail"] == "exec_failed" - assert exc_info.value.retryable is True - - @pytest.mark.asyncio - async def test_exec_marks_typed_runloop_bad_request_non_retryable( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_bad_request(*args: object, **kwargs: object) -> object: - _ = (args, kwargs) - raise _FakeBadRequestError( - body={"error": "invalid command"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute", - method="POST", - ) - - monkeypatch.setattr(devbox.cmd, "exec", _raise_bad_request) - - with pytest.raises(runloop_module.ExecTransportError) as exc_info: - await session.exec("pwd", shell=False) - - assert exc_info.value.context["http_status"] == 400 - assert exc_info.value.context["cause_type"] == "_FakeBadRequestError" - assert exc_info.value.context["provider_body"] == {"error": "invalid command"} - assert exc_info.value.context["detail"] == "exec_failed" - assert exc_info.value.retryable is False - - @pytest.mark.parametrize( - ("status", "expected_retryable"), - [ - (400, False), - (401, False), - (403, False), - (404, False), - (408, True), - (422, False), - (429, True), - (500, True), - (502, True), - (503, True), - (504, True), - ], - ) - def test_runloop_retryability_status_table( - self, - monkeypatch: pytest.MonkeyPatch, - status: int, - expected_retryable: bool, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - error = _FakeAPIStatusError(status, body={"error": f"HTTP {status}"}) - - assert runloop_module._runloop_provider_retryability(error) is expected_retryable - - @pytest.mark.asyncio - async def test_exec_wraps_command_with_workspace_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project", - environment=Environment(value={"ONLY_MANIFEST": "1"}), - ), - options=runloop_module.RunloopSandboxClientOptions(env_vars={"ONLY_OPTION": "2"}), - ) - await session.start() - await session.exec("pwd && echo hello", shell=True) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert devbox.exec_calls - command, params = devbox.exec_calls[-1] - assert "cd /home/user/project &&" in command - assert "env --" in command - assert "ONLY_MANIFEST=1" in command - assert "ONLY_OPTION=2" in command - assert "attach_stdin" not in params - assert "polling_config" in params - - @pytest.mark.asyncio - async def test_read_and_write_use_normalized_absolute_paths( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - await session.write( - "/home/user/project/output.txt", - io.BytesIO(b"hello"), - ) - payload = await session.read("/home/user/project/output.txt") - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert payload.read() == b"hello" - assert devbox.files["project/output.txt"] == b"hello" - assert devbox.file_upload_paths == ["/home/user/project/output.txt"] - assert devbox.file_download_paths == ["/home/user/project/output.txt"] - - @pytest.mark.asyncio - async def test_read_and_write_extra_path_grant_use_file_api_directly( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root="/home/user/project", - extra_path_grants=(SandboxPathGrant(path="/tmp"),), - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - exec_count = len(devbox.exec_calls) - - await session.write("/tmp/output.txt", io.BytesIO(b"hello")) - payload = await session.read("/tmp/output.txt") - - assert payload.read() == b"hello" - assert devbox.files["/tmp/output.txt"] == b"hello" - assert devbox.file_upload_paths == ["/tmp/output.txt"] - assert devbox.file_download_paths == ["/tmp/output.txt"] - assert len(devbox.exec_calls) == exec_count + 7 - assert devbox.exec_calls[exec_count + 4][0] == "mkdir -p -- /tmp" - - @pytest.mark.asyncio - async def test_write_rejects_workspace_symlink_to_read_only_extra_path_grant( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root="/home/user/project", - extra_path_grants=(SandboxPathGrant(path="/tmp/protected", read_only=True),), - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - devbox.symlinks["/home/user/project/link"] = "/tmp/protected" - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.write("link/result.txt", io.BytesIO(b"blocked")) - - assert devbox.file_upload_paths == [] - assert str(exc_info.value) == ( - "failed to write archive for path: /home/user/project/link/result.txt" - ) - assert exc_info.value.context == { - "path": "/home/user/project/link/result.txt", - "reason": "read_only_extra_path_grant", - "grant_path": "/tmp/protected", - "resolved_path": "/tmp/protected/result.txt", - } - - @pytest.mark.asyncio - async def test_read_wraps_runloop_http_error_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_download_error(**kwargs: object) -> bytes: - _ = kwargs - raise _FakeAPIStatusError( - 500, - body={"error": "download failed"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/files/project/output.txt", - method="GET", - ) - - monkeypatch.setattr(devbox.file, "download", _raise_download_error) - - with pytest.raises(runloop_module.WorkspaceArchiveReadError) as exc_info: - await session.read("/home/user/project/output.txt") - - assert exc_info.value.context["http_status"] == 500 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "download failed"} - assert exc_info.value.context["detail"] == "file_download_failed" - assert exc_info.value.retryable is True - - @pytest.mark.asyncio - async def test_write_wraps_runloop_http_error_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_upload_error(**kwargs: object) -> object: - _ = kwargs - raise _FakeAPIStatusError( - 429, - body={"error": "upload rate limited"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/files/project/output.txt", - method="PUT", - ) - - monkeypatch.setattr(devbox.file, "upload", _raise_upload_error) - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.write("/home/user/project/output.txt", io.BytesIO(b"hello")) - - assert exc_info.value.context["http_status"] == 429 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "upload rate limited"} - assert exc_info.value.context["detail"] == "file_upload_failed" - assert exc_info.value.retryable is True - - @pytest.mark.asyncio - async def test_manifest_apply_preserves_existing_files_in_non_empty_directory( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project", - entries={"new.txt": File(content=b"new")}, - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - devbox.files["project/existing.txt"] = b"keep" - - await session.start() - - assert devbox.files["project/existing.txt"] == b"keep" - assert devbox.files["project/new.txt"] == b"new" - - @pytest.mark.asyncio - async def test_persist_workspace_returns_native_snapshot_ref_and_hydrate_recreates_devbox( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - old_devbox_id = session.state.devbox_id - archive = await session.persist_workspace() - snapshot_id = runloop_module._decode_runloop_snapshot_ref(archive.read()) # noqa: SLF001 - await session.hydrate_workspace( - io.BytesIO(runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-1")) # noqa: SLF001 - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert snapshot_id == "snap-1" - assert sdk.devbox.create_from_snapshot_calls == [ - ("snap-1", {"timeout": session.state.timeouts.resume_s}) - ] - assert session.state.devbox_id != old_devbox_id - - @pytest.mark.asyncio - async def test_restore_snapshot_on_resume_bypasses_workspace_clear( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(), - ) - session.state.snapshot = _RestorableSnapshot( - id="runloop-snapshot", - payload=runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-9"), # noqa: SLF001 - ) - state = session.state - resumed = await client.resume(state) - inner = resumed._inner - - async def _unexpected_clear() -> None: - raise AssertionError("workspace clear should be bypassed for Runloop restore") - - inner._clear_workspace_root_on_resume = _unexpected_clear # noqa: SLF001 - await inner._restore_snapshot_into_workspace_on_resume() # noqa: SLF001 - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_from_snapshot_calls == [ - ("snap-9", {"timeout": state.timeouts.resume_s}) - ] - - @pytest.mark.asyncio - async def test_restore_tar_snapshot_on_resume_clears_workspace_before_hydrate( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project"), - options=runloop_module.RunloopSandboxClientOptions(), - ) - session.state.snapshot = _RestorableSnapshot( - id="tar-snapshot", - payload=_build_tar_bytes({"new.txt": b"new"}), - ) - resumed = await client.resume(session.state) - inner = resumed._inner - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[resumed.state.devbox_id] - devbox.files["project/existing.txt"] = b"stale" - cleared = False - - async def _clear_workspace_root_on_resume() -> None: - nonlocal cleared - cleared = True - devbox.files.pop("project/existing.txt", None) - - inner._clear_workspace_root_on_resume = ( # noqa: SLF001 - _clear_workspace_root_on_resume - ) - await inner._restore_snapshot_into_workspace_on_resume() # noqa: SLF001 - - assert cleared is True - assert devbox.files["project/new.txt"] == b"new" - assert "project/existing.txt" not in devbox.files - - @pytest.mark.asyncio - async def test_restore_snapshot_on_resume_passes_dependencies_to_snapshot_restore( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - dependencies = Dependencies().bind_value("test.dep", object()) - - async with runloop_module.RunloopSandboxClient(dependencies=dependencies) as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - snapshot = _DependencyAwareSnapshot( - id="dep-aware-restore", - payload=runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-dep"), # noqa: SLF001 - ) - session.state.snapshot = snapshot - resumed = await client.resume(session.state) - - await resumed._inner._restore_snapshot_into_workspace_on_resume() # noqa: SLF001 - - assert snapshot.restore_dependencies - assert snapshot.restore_dependencies[-1] is not None - - @pytest.mark.asyncio - async def test_hydrate_workspace_wraps_provider_error_with_snapshot_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - async def _raise_restore_error(snapshot_id: str, **kwargs: object) -> object: - _ = (snapshot_id, kwargs) - raise _FakeAPIStatusError( - 500, - body={"error": "restore failed"}, - url="https://api.runloop.ai/v1/devboxes/from_snapshot", - method="POST", - ) - - monkeypatch.setattr(sdk.devbox, "create_from_snapshot", _raise_restore_error) - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.hydrate_workspace( - io.BytesIO(runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-7")) # noqa: SLF001 - ) - - assert exc_info.value.context["reason"] == "snapshot_restore_failed" - assert exc_info.value.context["snapshot_id"] == "snap-7" - assert exc_info.value.context["http_status"] == 500 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "restore failed"} - - @pytest.mark.asyncio - async def test_hydrate_workspace_accepts_tar_fallback_payload( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - archive = _build_tar_bytes({"notes/output.txt": b"from tar"}) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.hydrate_workspace(io.BytesIO(archive)) - payload = await session.read("/home/user/notes/output.txt") - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert payload.read() == b"from tar" - assert f".sandbox-runloop-hydrate-{session.state.session_id.hex}.tar" not in devbox.files - - @pytest.mark.asyncio - async def test_hydrate_workspace_rejects_invalid_non_snapshot_non_tar_payload( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.hydrate_workspace(io.BytesIO(b"not-a-valid-tar")) - - assert exc_info.value.context["reason"] == "unsafe_or_invalid_tar" - - @pytest.mark.asyncio - async def test_persist_workspace_remounts_mounts_after_snapshot( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - mount = _RecordingMount() - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root=runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT, - entries={"mount": mount}, - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - archive = await session.persist_workspace() - - assert runloop_module._decode_runloop_snapshot_ref(archive.read()) == "snap-1" # noqa: SLF001 - mount_path = Path(f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/mount") - assert mount._unmounted_paths == [mount_path] - assert mount._mounted_paths == [mount_path] - - @pytest.mark.asyncio - async def test_resolve_exposed_port_wraps_provider_error_with_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(exposed_ports=(4500,)) - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_tunnel_error(*args: object, **kwargs: object) -> str | None: - _ = (args, kwargs) - raise _FakeAPIStatusError( - 429, - body={"error": "tunnel rate limited"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}", - method="GET", - ) - - monkeypatch.setattr(devbox, "get_tunnel_url", _raise_tunnel_error) - - with pytest.raises(runloop_module.ExposedPortUnavailableError) as exc_info: - await session.resolve_exposed_port(4500) - - assert exc_info.value.context["http_status"] == 429 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "tunnel rate limited"} - assert exc_info.value.context["detail"] == "get_tunnel_url_failed" - - @pytest.mark.asyncio - async def test_resolve_exposed_port_keeps_invalid_url_detail_for_parse_errors( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(exposed_ports=(4500,)) - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _invalid_tunnel_url(*args: object, **kwargs: object) -> str | None: - _ = (args, kwargs) - return "https://" - - monkeypatch.setattr(devbox, "get_tunnel_url", _invalid_tunnel_url) - - with pytest.raises(runloop_module.ExposedPortUnavailableError) as exc_info: - await session.resolve_exposed_port(4500) - - assert exc_info.value.context["detail"] == "invalid_tunnel_url" - - @pytest.mark.asyncio - async def test_runloop_shell_capability_does_not_expose_write_stdin( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - capability = Shell() - capability.bind(session) - tools = capability.tools() - - assert [tool.name for tool in tools] == ["exec_command"] - - @pytest.mark.asyncio - async def test_exec_command_tool_uses_one_shot_exec_for_tty_requests( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - exec_calls_before = len(devbox.exec_calls) - exec_async_calls_before = len(devbox.exec_async_calls) - - output = await ExecCommandTool(session=session).run( - ExecCommandArgs(cmd="echo hello", tty=True, yield_time_ms=50) - ) - - assert "Process exited with code 0" in output - assert "Process running with session ID" not in output - assert "hello" in output - assert len(devbox.exec_calls) == exec_calls_before + 1 - assert len(devbox.exec_async_calls) == exec_async_calls_before diff --git a/tests/extensions/sandbox/test_runloop_capabilities_example.py b/tests/extensions/sandbox/test_runloop_capabilities_example.py deleted file mode 100644 index 75777cf144..0000000000 --- a/tests/extensions/sandbox/test_runloop_capabilities_example.py +++ /dev/null @@ -1,350 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -import types -from pathlib import Path -from typing import Any, cast - -import pytest - - -def _load_example_module() -> Any: - path = ( - Path(__file__).resolve().parents[3] - / "examples" - / "sandbox" - / "extensions" - / "runloop" - / "capabilities.py" - ) - module_name = "tests.extensions.sandbox.runloop_capabilities_example" - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None - assert spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -class _FakeNotFoundError(Exception): - def __init__(self) -> None: - self.status_code = 404 - self.response = types.SimpleNamespace(status_code=404) - - -class _FakeConflictError(Exception): - def __init__(self, message: str) -> None: - self.status_code = 400 - self.response = types.SimpleNamespace(status_code=400) - self.body = {"message": message} - - -class _FakeSecret: - def __init__(self, name: str, secret_id: str) -> None: - self.id = secret_id - self.name = name - - -class _FakeSecretsClient: - def __init__(self) -> None: - self.secrets: dict[str, _FakeSecret] = {} - self.create_calls: list[tuple[str, str]] = [] - self.delete_calls: list[str] = [] - self._counter = 0 - - def add(self, name: str) -> _FakeSecret: - self._counter += 1 - secret = _FakeSecret(name=name, secret_id=f"secret-{self._counter}") - self.secrets[name] = secret - return secret - - async def get(self, name: str) -> _FakeSecret: - if name not in self.secrets: - raise _FakeNotFoundError() - return self.secrets[name] - - async def create(self, *, name: str, value: str) -> _FakeSecret: - self.create_calls.append((name, value)) - return self.add(name) - - -class _FakePolicy: - def __init__(self, policy_id: str, name: str, description: str | None = None) -> None: - self.id = policy_id - self.name = name - self.description = description - - -class _FakePolicyRef: - def __init__(self, policy: _FakePolicy) -> None: - self._policy = policy - - async def get_info(self) -> object: - return types.SimpleNamespace( - id=self._policy.id, - name=self._policy.name, - description=self._policy.description, - ) - - -class _FakePolicyListItem: - def __init__(self, policy_id: str) -> None: - self.id = policy_id - - -class _FakeNetworkPoliciesClient: - def __init__(self) -> None: - self.policies: dict[str, _FakePolicy] = {} - self.create_calls: list[dict[str, object]] = [] - self.delete_calls: list[str] = [] - self._counter = 0 - - def add(self, name: str, description: str | None = None) -> _FakePolicy: - self._counter += 1 - policy = _FakePolicy( - policy_id=f"np-{self._counter}", - name=name, - description=description, - ) - self.policies[policy.id] = policy - return policy - - async def list(self, **params: object) -> list[_FakePolicyListItem]: - name = params.get("name") - policies = list(self.policies.values()) - if isinstance(name, str): - policies = [policy for policy in policies if policy.name == name] - return [_FakePolicyListItem(policy.id) for policy in policies] - - async def create(self, **params: object) -> _FakePolicy: - self.create_calls.append(dict(params)) - name = str(params["name"]) - if any(policy.name == name for policy in self.policies.values()): - raise _FakeConflictError(f"NetworkPolicy with name '{name}' already exists") - description = cast( - str | None, - params.get("description") if isinstance(params.get("description"), str) else None, - ) - return self.add( - name=name, - description=description, - ) - - def get(self, policy_id: str) -> _FakePolicyRef: - return _FakePolicyRef(self.policies[policy_id]) - - -class _FakePlatformClient: - def __init__(self) -> None: - self.secrets = _FakeSecretsClient() - self.network_policies = _FakeNetworkPoliciesClient() - - -class _FakeRunloopClient: - def __init__(self) -> None: - self.platform = _FakePlatformClient() - - -@pytest.mark.asyncio -async def test_query_runloop_secret_returns_non_sensitive_metadata() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - secret = client.platform.secrets.add("RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN") - - result = await module._query_runloop_secret( # noqa: SLF001 - client, - name=secret.name, - ) - - assert result.found is True - assert result.id == secret.id - assert "value" not in result.model_dump(mode="json") - - -@pytest.mark.asyncio -async def test_query_runloop_secret_reports_missing_before_create() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - - result = await module._query_runloop_secret( # noqa: SLF001 - client, - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - ) - - assert result.found is False - assert result.id is None - - -@pytest.mark.asyncio -async def test_query_runloop_network_policy_reports_existing_resource() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - policy = client.platform.network_policies.add( - "runloop-capabilities-example-policy", - description="Persistent example policy.", - ) - - result = await module._query_runloop_network_policy( # noqa: SLF001 - client, - name=policy.name, - ) - - assert result.found is True - assert result.id == policy.id - assert result.description == "Persistent example policy." - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_reuses_existing_resources_without_cleanup() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - secret = client.platform.secrets.add("RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN") - policy = client.platform.network_policies.add("runloop-capabilities-example-policy") - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name=secret.name, - found=True, - id=secret.id, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name=policy.name, - found=True, - id=policy.id, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name=secret.name, - managed_secret_value="runloop-capabilities-example-token", - network_policy_name=policy.name, - network_policy_id_override=None, - query_results=query_results, - axon_name=None, - ) - - secret_bootstrap = bootstrap["secret"] - network_policy_bootstrap = bootstrap["network_policy"] - assert secret_bootstrap.action == "reused" - assert network_policy_bootstrap.action == "reused" - assert client.platform.secrets.create_calls == [] - assert client.platform.network_policies.create_calls == [] - assert client.platform.secrets.delete_calls == [] - assert client.platform.network_policies.delete_calls == [] - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_creates_missing_resources() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - found=False, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name="runloop-capabilities-example-policy", - found=False, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - managed_secret_value="runloop-capabilities-example-token", - network_policy_name="runloop-capabilities-example-policy", - network_policy_id_override=None, - query_results=query_results, - axon_name=None, - ) - - secret_bootstrap = bootstrap["secret"] - network_policy_bootstrap = bootstrap["network_policy"] - assert secret_bootstrap.action == "created" - assert network_policy_bootstrap.action == "created" - assert client.platform.secrets.create_calls == [ - ("RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", "runloop-capabilities-example-token") - ] - assert client.platform.network_policies.create_calls == [ - { - "name": "runloop-capabilities-example-policy", - "allow_all": True, - "description": "Persistent network policy for the Runloop capabilities example.", - } - ] - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_respects_policy_override() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - found=False, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name="runloop-capabilities-example-policy", - found=False, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - managed_secret_value="runloop-capabilities-example-token", - network_policy_name="runloop-capabilities-example-policy", - network_policy_id_override="np-override", - query_results=query_results, - axon_name=None, - ) - - network_policy_bootstrap = bootstrap["network_policy"] - assert network_policy_bootstrap.action == "override" - assert network_policy_bootstrap.id == "np-override" - assert client.platform.network_policies.create_calls == [] - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_recovers_from_existing_policy_conflict() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - policy = client.platform.network_policies.add( - "runloop-capabilities-example-policy", - description="Persistent example policy.", - ) - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - found=False, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name=policy.name, - found=False, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - managed_secret_value="runloop-capabilities-example-token", - network_policy_name=policy.name, - network_policy_id_override=None, - query_results=query_results, - axon_name=None, - ) - - network_policy_bootstrap = bootstrap["network_policy"] - assert network_policy_bootstrap.action == "reused" - assert network_policy_bootstrap.found_before_bootstrap is True - assert network_policy_bootstrap.id == policy.id diff --git a/tests/extensions/sandbox/test_runloop_mounts.py b/tests/extensions/sandbox/test_runloop_mounts.py deleted file mode 100644 index 8ee619fda9..0000000000 --- a/tests/extensions/sandbox/test_runloop_mounts.py +++ /dev/null @@ -1,226 +0,0 @@ -from __future__ import annotations - -import io -import types -import uuid -from pathlib import Path -from typing import Any, cast - -import pytest - -from agents.sandbox import Manifest -from agents.sandbox.entries import RcloneMountPattern, S3Mount -from agents.sandbox.errors import MountConfigError -from agents.sandbox.session.base_sandbox_session import BaseSandboxSession -from agents.sandbox.types import ExecResult - - -class _FakeRunloopMountSession(BaseSandboxSession): - def __init__(self, results: list[ExecResult] | None = None) -> None: - self.state = cast( - Any, - types.SimpleNamespace( - session_id=uuid.uuid4(), - manifest=Manifest(root="/workspace"), - ), - ) - self._results = list(results or []) - self.exec_calls: list[str] = [] - - async def _exec_internal( - self, - *command: str | Path, - timeout: float | None = None, - ) -> ExecResult: - _ = timeout - cmd_str = " ".join(str(c) for c in command) - self.exec_calls.append(cmd_str) - if self._results: - return self._results.pop(0) - return ExecResult(stdout=b"", stderr=b"", exit_code=0) - - async def read(self, path: Path, *, user: object = None) -> io.IOBase: - _ = (path, user) - return io.BytesIO(b"") - - async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None: - _ = (path, data, user) - - async def persist_workspace(self) -> io.IOBase: - raise AssertionError("not expected") - - async def hydrate_workspace(self, data: io.IOBase) -> None: - _ = data - raise AssertionError("not expected") - - async def running(self) -> bool: - return True - - -_FakeRunloopMountSession.__name__ = "RunloopSandboxSession" - - -def _exec_ok(stdout: bytes = b"") -> ExecResult: - return ExecResult(stdout=stdout, stderr=b"", exit_code=0) - - -def _exec_fail() -> ExecResult: - return ExecResult(stdout=b"", stderr=b"", exit_code=1) - - -def test_runloop_package_re_exports_cloud_bucket_strategy() -> None: - package_module = __import__( - "agents.extensions.sandbox.runloop", - fromlist=["RunloopCloudBucketMountStrategy"], - ) - - assert hasattr(package_module, "RunloopCloudBucketMountStrategy") - - -def test_runloop_extension_re_exports_cloud_bucket_strategy() -> None: - package_module = __import__( - "agents.extensions.sandbox", - fromlist=["RunloopCloudBucketMountStrategy"], - ) - - assert hasattr(package_module, "RunloopCloudBucketMountStrategy") - - -def test_runloop_mount_strategy_type_and_default_pattern() -> None: - from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy - - strategy = RunloopCloudBucketMountStrategy() - - assert strategy.type == "runloop_cloud_bucket" - assert isinstance(strategy.pattern, RcloneMountPattern) - assert strategy.pattern.mode == "fuse" - - -def test_runloop_mount_strategy_round_trips_through_manifest() -> None: - from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy - - manifest = Manifest.model_validate( - { - "root": "/workspace", - "entries": { - "bucket": { - "type": "s3_mount", - "bucket": "my-bucket", - "mount_strategy": {"type": "runloop_cloud_bucket"}, - } - }, - } - ) - - mount = manifest.entries["bucket"] - assert isinstance(mount, S3Mount) - assert isinstance(mount.mount_strategy, RunloopCloudBucketMountStrategy) - - -def test_runloop_session_guard_rejects_wrong_type() -> None: - from agents.extensions.sandbox.runloop.mounts import _assert_runloop_session - - class _WrongSession: - pass - - with pytest.raises(MountConfigError, match="RunloopSandboxSession"): - _assert_runloop_session(_WrongSession()) # type: ignore[arg-type] - - -def test_runloop_session_guard_accepts_correct_type() -> None: - from agents.extensions.sandbox.runloop.mounts import _assert_runloop_session - - _assert_runloop_session(_FakeRunloopMountSession()) - - -@pytest.mark.asyncio -async def test_runloop_ensure_rclone_installs_verified_release() -> None: - from agents.extensions.sandbox._rclone import _RCLONE_VERSION, ensure_rclone - - session = _FakeRunloopMountSession( - [ - _exec_fail(), - _exec_ok(), - _exec_ok(stdout=b"aarch64\n"), - _exec_ok(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - ] - ) - - await ensure_rclone(session) - - assert session.exec_calls[:2] == [ - "sh -lc command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone", - "sh -lc command -v apt-get >/dev/null 2>&1", - ] - assert session.exec_calls[2] == "uname -m" - assert session.exec_calls[3] == ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 update -qq" - ) - assert session.exec_calls[4] == ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 install -y -qq " - "ca-certificates coreutils curl unzip" - ) - assert session.exec_calls[5].startswith("sudo -u root -- sh -lc set -eu\n") - assert f"rclone-v{_RCLONE_VERSION}-linux-arm64.zip" in session.exec_calls[5] - assert "sha256sum --check --strict -" in session.exec_calls[5] - assert session.exec_calls[6] == ( - "sh -lc command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone" - ) - - -@pytest.mark.asyncio -async def test_runloop_ensure_fuse_installs_missing_fusermount() -> None: - from agents.extensions.sandbox.runloop.mounts import _ensure_fuse_support - - session = _FakeRunloopMountSession( - [ - _exec_ok(), - _exec_ok(), - _exec_fail(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - ] - ) - - await _ensure_fuse_support(session) - - assert session.exec_calls == [ - "sh -lc test -c /dev/fuse", - "sh -lc grep -qw fuse /proc/filesystems", - "sh -lc command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - "sh -lc command -v apt-get >/dev/null 2>&1", - ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 update -qq" - ), - ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 install -y -qq fuse3" - ), - "sh -lc command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - ( - "sudo -u root -- sh -lc chmod a+rw /dev/fuse && " - "touch /etc/fuse.conf && " - "(grep -qxF user_allow_other /etc/fuse.conf || " - "printf '\\nuser_allow_other\\n' >> /etc/fuse.conf)" - ), - ] - - -@pytest.mark.asyncio -async def test_runloop_rclone_pattern_adds_fuse_access_args() -> None: - from agents.extensions.sandbox._rclone import rclone_pattern_for_session - - session = _FakeRunloopMountSession([_exec_ok(stdout=b"1000\n1000\n")]) - - pattern = await rclone_pattern_for_session(session, RcloneMountPattern(mode="fuse")) - - assert pattern.extra_args == ["--allow-other", "--uid", "1000", "--gid", "1000"] diff --git a/tests/fixtures/released_api_contract.json b/tests/fixtures/released_api_contract.json index 78b7e0fecb..91be424ad7 100644 --- a/tests/fixtures/released_api_contract.json +++ b/tests/fixtures/released_api_contract.json @@ -18348,2446 +18348,6 @@ } ] }, - "agents.extensions.sandbox.RunloopAfterIdle": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "idle_time_seconds" - }, - { - "default": { - "kind": "required" - }, - "name": "on_idle" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_KEYWORD", - "name": "data" - } - ] - }, - "agents.extensions.sandbox.RunloopCloudBucketMountStrategy": { - "dataclass_fields": [], - "kind": "class", - "members": { - "activate": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "dest" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "base_dir" - } - ] - }, - "build_docker_volume_driver_config": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - } - ] - }, - "deactivate": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "dest" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "base_dir" - } - ] - }, - "parse": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - }, - "restore_after_snapshot": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - } - ] - }, - "supports_native_snapshot_detach": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - } - ] - }, - "teardown_for_snapshot": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - } - ] - }, - "validate_mount": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - } - ] - } - }, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop_cloud_bucket" - }, - "name": "type" - }, - { - "default": { - "kind": "model", - "type": "agents.sandbox.entries.mounts.patterns.RcloneMountPattern", - "value": { - "items": [ - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "type" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "rclone" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "mode" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "fuse" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "remote_name" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "extra_args" - }, - { - "items": [], - "kind": "sequence", - "type": "builtins.list" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_addr" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_mount_options" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "config_file_path" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ] - ], - "kind": "mapping", - "type": "builtins.dict" - } - }, - "name": "pattern" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop_cloud_bucket" - }, - "kind": "KEYWORD_ONLY", - "name": "type" - }, - { - "default": { - "kind": "model", - "type": "agents.sandbox.entries.mounts.patterns.RcloneMountPattern", - "value": { - "items": [ - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "type" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "rclone" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "mode" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "fuse" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "remote_name" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "extra_args" - }, - { - "items": [], - "kind": "sequence", - "type": "builtins.list" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_addr" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_mount_options" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "config_file_path" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ] - ], - "kind": "mapping", - "type": "builtins.dict" - } - }, - "kind": "KEYWORD_ONLY", - "name": "pattern" - } - ] - }, - "agents.extensions.sandbox.RunloopExistingSecret": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [], - "parameters": [] - }, - "agents.extensions.sandbox.RunloopGatewaySpec": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "gateway" - }, - { - "default": { - "kind": "required" - }, - "name": "secret" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "gateway" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "secret" - } - ] - }, - "agents.extensions.sandbox.RunloopLaunchParameters": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "after_idle" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "architecture" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "available_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "custom_cpu_cores" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "custom_disk_size" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "custom_gb_memory" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "keep_alive_time_seconds" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "launch_commands" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "lifecycle" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "network_policy_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "provisioning_tier" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "required_services" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "resource_size_request" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "user_parameters" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_KEYWORD", - "name": "data" - } - ] - }, - "agents.extensions.sandbox.RunloopMcpSpec": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "mcp_config" - }, - { - "default": { - "kind": "required" - }, - "name": "secret" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "mcp_config" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "secret" - } - ] - }, - "agents.extensions.sandbox.RunloopPlatformClient": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "_sdk" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxClient": { - "dataclass_fields": [], - "kind": "class", - "members": { - "close": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "create": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "manifest" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "options" - } - ] - }, - "delete": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - } - ] - }, - "deserialize_session_state": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - }, - "resume": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "state" - } - ] - }, - "serialize_session_state": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "state" - } - ] - } - }, - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "bearer_token" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "base_url" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "instrumentation" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "dependencies" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxClientOptions": { - "dataclass_fields": [], - "kind": "class", - "members": { - "parse": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - } - }, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "name": "type" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "timeouts" - }, - { - "default": { - "items": [], - "kind": "sequence", - "type": "builtins.tuple" - }, - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "tunnel" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "gateways" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "mcp" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "metadata" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "managed_secrets" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "blueprint_name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "timeouts" - }, - { - "default": { - "items": [], - "kind": "sequence", - "type": "builtins.tuple" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "tunnel" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "gateways" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mcp" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "metadata" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "managed_secrets" - }, - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "kind": "KEYWORD_ONLY", - "name": "type" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxSession": { - "dataclass_fields": [], - "kind": "class", - "members": { - "aclose": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "apply_manifest": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "only_ephemeral" - } - ] - }, - "apply_patch": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "operations" - }, - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "v4a" - }, - "kind": "KEYWORD_ONLY", - "name": "patch_format" - } - ] - }, - "describe": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "exec": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_POSITIONAL", - "name": "command" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "timeout" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": true - }, - "kind": "KEYWORD_ONLY", - "name": "shell" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "extract": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "data" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "compression_scheme" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "archive_limits" - } - ] - }, - "from_state": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "state" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "sdk" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "devbox" - } - ] - }, - "hydrate_workspace": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "data" - } - ] - }, - "ls": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "mkdir": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "parents" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "mv": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "source" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "destination" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "normalize_path": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "for_write" - } - ] - }, - "persist_workspace": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "provision_manifest_accounts": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "pty_exec_start": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_POSITIONAL", - "name": "command" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "timeout" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": true - }, - "kind": "KEYWORD_ONLY", - "name": "shell" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "tty" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "yield_time_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "max_output_tokens" - } - ] - }, - "pty_terminate_all": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "pty_write_stdin": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "session_id" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "chars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "yield_time_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "max_output_tokens" - } - ] - }, - "read": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "read_bounded": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "max_bytes" - } - ] - }, - "register_persist_workspace_skip_path": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - } - ] - }, - "register_pre_stop_hook": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "hook" - } - ] - }, - "resolve_exposed_port": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "port" - } - ] - }, - "rm": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "recursive" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "run_pre_stop_hooks": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "running": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "same_file": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "left" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "right" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": true - }, - "kind": "KEYWORD_ONLY", - "name": "follow_symlinks" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "set_dependencies": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "dependencies" - } - ] - }, - "should_provision_manifest_accounts_on_resume": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "shutdown": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "start": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "stop": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "supports_docker_volume_mounts": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "supports_pty": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "write": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "data" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - } - }, - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "state" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "sdk" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "devbox" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxSessionState": { - "dataclass_fields": [], - "kind": "class", - "members": { - "assert_path_grants_rebound": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "model_post_init": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_ONLY", - "name": "context" - } - ] - }, - "model_validate_json": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "json_data" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "strict" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "extra" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "context" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "by_alias" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "by_name" - } - ] - }, - "parse": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - }, - "rebind_persisted_mount_authority": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "trusted_manifest" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "provider_backend_id" - } - ] - }, - "rebind_persisted_path_grants": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "trusted_manifest" - } - ] - } - }, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "name": "type" - }, - { - "default": { - "factory": "uuid.uuid4", - "kind": "factory" - }, - "name": "session_id" - }, - { - "default": { - "kind": "required" - }, - "name": "snapshot" - }, - { - "default": { - "kind": "required" - }, - "name": "manifest" - }, - { - "default": { - "factory": "builtins.tuple", - "kind": "factory" - }, - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "snapshot_fingerprint" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "snapshot_fingerprint_version" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "name": "workspace_root_ready" - }, - { - "default": { - "kind": "required" - }, - "name": "devbox_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_name" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "base_env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "name" - }, - { - "default": { - "factory": "agents.extensions.sandbox.runloop.sandbox.RunloopTimeouts", - "kind": "factory" - }, - "name": "timeouts" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "tunnel" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "gateways" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "mcp" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "metadata" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "secret_refs" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "kind": "KEYWORD_ONLY", - "name": "type" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "session_id" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "manifest" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot_fingerprint" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot_fingerprint_version" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "workspace_root_ready" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "devbox_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "blueprint_name" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "base_env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "name" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "timeouts" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "tunnel" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "gateways" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "mcp" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "metadata" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "secret_refs" - } - ] - }, - "agents.extensions.sandbox.RunloopTimeouts": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.int", - "value": 86400 - }, - "name": "exec_timeout_unbounded_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "name": "create_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 10.0 - }, - "name": "keepalive_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "name": "cleanup_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "name": "fast_op_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "name": "file_upload_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "name": "file_download_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "name": "snapshot_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 120.0 - }, - "name": "suspend_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "name": "resume_s" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.int", - "value": 86400 - }, - "kind": "KEYWORD_ONLY", - "name": "exec_timeout_unbounded_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "kind": "KEYWORD_ONLY", - "name": "create_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 10.0 - }, - "kind": "KEYWORD_ONLY", - "name": "keepalive_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "kind": "KEYWORD_ONLY", - "name": "cleanup_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "kind": "KEYWORD_ONLY", - "name": "fast_op_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "kind": "KEYWORD_ONLY", - "name": "file_upload_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "kind": "KEYWORD_ONLY", - "name": "file_download_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 120.0 - }, - "kind": "KEYWORD_ONLY", - "name": "suspend_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "kind": "KEYWORD_ONLY", - "name": "resume_s" - } - ] - }, - "agents.extensions.sandbox.RunloopTunnelConfig": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "auth_mode" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "http_keep_alive" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "wake_on_http" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "auth_mode" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "http_keep_alive" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "wake_on_http" - } - ] - }, - "agents.extensions.sandbox.RunloopUserParameters": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "uid" - }, - { - "default": { - "kind": "required" - }, - "name": "username" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_KEYWORD", - "name": "data" - } - ] - }, "agents.extensions.sandbox.VercelCloudBucketMountStrategy": { "dataclass_fields": [], "kind": "class", @@ -46603,54 +44163,6 @@ "module": "agents.extensions.sandbox", "name": "ModalSandboxClientOptions" }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopAfterIdle", - "module": "agents.extensions.sandbox", - "name": "RunloopAfterIdle" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopGatewaySpec", - "module": "agents.extensions.sandbox", - "name": "RunloopGatewaySpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopLaunchParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopLaunchParameters" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopMcpSpec", - "module": "agents.extensions.sandbox", - "name": "RunloopMcpSpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClient" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClientOptions", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClientOptions" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopTunnelConfig", - "module": "agents.extensions.sandbox", - "name": "RunloopTunnelConfig" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopUserParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopUserParameters" - }, { "canonical_module": "agents.extensions.sandbox.vercel", "canonical_name": "VercelSandboxClient", @@ -47148,24 +44660,6 @@ "stateful_request" ] }, - { - "class_name": "RunloopPlatformClient", - "module": "agents.extensions.sandbox", - "names": [ - "axons", - "benchmarks", - "blueprints", - "network_policies", - "secrets" - ] - }, - { - "class_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "names": [ - "platform" - ] - }, { "class_name": "SandboxSessionState", "module": "agents.sandbox.session.sandbox_session_state", @@ -47714,23 +45208,7 @@ "VercelSandboxClient", "VercelSandboxClientOptions", "VercelSandboxSession", - "VercelSandboxSessionState", - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters" + "VercelSandboxSessionState" ], "optional_bindings": {}, "optional_exports": { @@ -47740,27 +45218,11 @@ "CloudflareSandboxClientOptions": "aiohttp", "CloudflareSandboxSession": "aiohttp", "CloudflareSandboxSessionState": "aiohttp", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT": "runloop_api_client", - "DEFAULT_RUNLOOP_WORKSPACE_ROOT": "runloop_api_client", "ModalCloudBucketMountStrategy": "modal", "ModalSandboxClient": "modal", "ModalSandboxClientOptions": "modal", "ModalSandboxSession": "modal", "ModalSandboxSessionState": "modal", - "RunloopAfterIdle": "runloop_api_client", - "RunloopCloudBucketMountStrategy": "runloop_api_client", - "RunloopExistingSecret": "runloop_api_client", - "RunloopGatewaySpec": "runloop_api_client", - "RunloopLaunchParameters": "runloop_api_client", - "RunloopMcpSpec": "runloop_api_client", - "RunloopPlatformClient": "runloop_api_client", - "RunloopSandboxClient": "runloop_api_client", - "RunloopSandboxClientOptions": "runloop_api_client", - "RunloopSandboxSession": "runloop_api_client", - "RunloopSandboxSessionState": "runloop_api_client", - "RunloopTimeouts": "runloop_api_client", - "RunloopTunnelConfig": "runloop_api_client", - "RunloopUserParameters": "runloop_api_client", "VercelCloudBucketMountStrategy": "vercel", "VercelSandboxClient": "vercel", "VercelSandboxClientOptions": "vercel", diff --git a/tests/fixtures/released_api_contract_policy.json b/tests/fixtures/released_api_contract_policy.json index 471e6ca3f2..c47d5ae574 100644 --- a/tests/fixtures/released_api_contract_policy.json +++ b/tests/fixtures/released_api_contract_policy.json @@ -138,54 +138,6 @@ "module": "agents.extensions.sandbox", "name": "ModalSandboxClientOptions" }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopAfterIdle", - "module": "agents.extensions.sandbox", - "name": "RunloopAfterIdle" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopGatewaySpec", - "module": "agents.extensions.sandbox", - "name": "RunloopGatewaySpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopLaunchParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopLaunchParameters" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopMcpSpec", - "module": "agents.extensions.sandbox", - "name": "RunloopMcpSpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClient" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClientOptions", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClientOptions" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopTunnelConfig", - "module": "agents.extensions.sandbox", - "name": "RunloopTunnelConfig" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopUserParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopUserParameters" - }, { "canonical_module": "agents.extensions.sandbox.vercel", "canonical_name": "VercelSandboxClient", @@ -386,9 +338,6 @@ "redis": { "extra": "redis" }, - "runloop_api_client": { - "extra": "runloop" - }, "sqlalchemy": { "extra": "sqlalchemy" }, @@ -422,27 +371,11 @@ "CloudflareSandboxClientOptions": "aiohttp", "CloudflareSandboxSession": "aiohttp", "CloudflareSandboxSessionState": "aiohttp", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT": "runloop_api_client", - "DEFAULT_RUNLOOP_WORKSPACE_ROOT": "runloop_api_client", "ModalCloudBucketMountStrategy": "modal", "ModalSandboxClient": "modal", "ModalSandboxClientOptions": "modal", "ModalSandboxSession": "modal", "ModalSandboxSessionState": "modal", - "RunloopAfterIdle": "runloop_api_client", - "RunloopCloudBucketMountStrategy": "runloop_api_client", - "RunloopExistingSecret": "runloop_api_client", - "RunloopGatewaySpec": "runloop_api_client", - "RunloopLaunchParameters": "runloop_api_client", - "RunloopMcpSpec": "runloop_api_client", - "RunloopPlatformClient": "runloop_api_client", - "RunloopSandboxClient": "runloop_api_client", - "RunloopSandboxClientOptions": "runloop_api_client", - "RunloopSandboxSession": "runloop_api_client", - "RunloopSandboxSessionState": "runloop_api_client", - "RunloopTimeouts": "runloop_api_client", - "RunloopTunnelConfig": "runloop_api_client", - "RunloopUserParameters": "runloop_api_client", "VercelCloudBucketMountStrategy": "vercel", "VercelSandboxClient": "vercel", "VercelSandboxClientOptions": "vercel", @@ -742,24 +675,6 @@ "agent_registration" ] }, - { - "class_name": "RunloopPlatformClient", - "module": "agents.extensions.sandbox", - "names": [ - "axons", - "benchmarks", - "blueprints", - "network_policies", - "secrets" - ] - }, - { - "class_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "names": [ - "platform" - ] - }, { "class_name": "SandboxSessionState", "module": "agents.sandbox.session.sandbox_session_state", diff --git a/tests/sandbox/test_bounded_read.py b/tests/sandbox/test_bounded_read.py index 7fe6d7e124..aaf39cfeae 100644 --- a/tests/sandbox/test_bounded_read.py +++ b/tests/sandbox/test_bounded_read.py @@ -70,10 +70,9 @@ def close(self) -> None: self.closed = True -PROVIDERS = ["e2b", "runloop", "vercel", "daytona", "blaxel", "cloudflare"] +PROVIDERS = ["e2b", "vercel", "daytona", "blaxel", "cloudflare"] CLASSES = { "e2b": "E2BSandboxSession", - "runloop": "RunloopSandboxSession", "vercel": "VercelSandboxSession", "daytona": "DaytonaSandboxSession", "blaxel": "BlaxelSandboxSession", @@ -94,19 +93,6 @@ def _session(provider: str, content: _Content, *, status: int = 200) -> Any: session._sandbox = SimpleNamespace( files=SimpleNamespace(read=AsyncMock(return_value=content)) ) - elif provider == "runloop": - session.state = SimpleNamespace( - devbox_id="test", timeouts=SimpleNamespace(file_download_s=30) - ) - session._sdk = SimpleNamespace( - api=SimpleNamespace( - devboxes=SimpleNamespace( - with_streaming_response=SimpleNamespace( - download_file=MagicMock(return_value=content) - ) - ) - ) - ) elif provider == "vercel": @asynccontextmanager @@ -346,7 +332,7 @@ async def chunks(self, chunk_size: int = 3) -> AsyncIterator[bytes]: "provider,retryable", [ (provider, retryable) - for provider in ["cloudflare", "vercel", "runloop", "modal", "daytona", "e2b"] + for provider in ["cloudflare", "vercel", "modal", "daytona", "e2b"] for retryable in [True, False] ] + [("blaxel", True), ("blaxel", None)], @@ -405,26 +391,6 @@ async def test_bounded_provider_read_preserves_retry_policy( inner._ensure_sandbox.return_value.iter_file.side_effect = error_cls( httpx.Response(429 if retryable else 403), "synthetic-private-response" ) - elif provider == "runloop": - import runloop_api_client - - from agents.extensions.sandbox.runloop.sandbox import RunloopSandboxSessionState - - inner.state = RunloopSandboxSessionState( - devbox_id="test", manifest=Manifest(), snapshot=NoopSnapshot(id="test") - ) - error_cls = ( - runloop_api_client.RateLimitError - if retryable - else runloop_api_client.PermissionDeniedError - ) - inner._sdk.api.devboxes.with_streaming_response.download_file.side_effect = error_cls( - "synthetic-private-response", - response=httpx.Response( - 429 if retryable else 403, request=httpx.Request("GET", "https://example.invalid") - ), - body={"detail": "synthetic-private-response"}, - ) events: list[SandboxSessionEvent] = [] wrapper = SandboxSession( inner, @@ -517,40 +483,6 @@ async def readexactly(self, size: int) -> bytes: assert not session.mount_active -@pytest.mark.asyncio -async def test_runloop_bounded_read_uses_streaming_sdk_response() -> None: - import httpx - - pytest.importorskip("runloop_api_client") - from runloop_api_client import AsyncRunloop - - consumed = 0 - closed = False - - class Body(httpx.AsyncByteStream): - async def __aiter__(self) -> AsyncIterator[bytes]: - nonlocal consumed - for _ in range(10): - consumed += 1 - yield b"x" * 65536 - - async def aclose(self) -> None: - nonlocal closed - closed = True - - def respond(request: httpx.Request) -> httpx.Response: - assert request.url.path.endswith("/download_file") - return httpx.Response(200, stream=Body()) - - async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http: - async with AsyncRunloop(bearer_token="synthetic-test-key", http_client=http) as api: - session = _session("runloop", _Content(b"unused")) - session._sdk = SimpleNamespace(api=api) - assert await session.read_bounded(Path("out.jsonl"), max_bytes=5) == b"xxxxx" - assert consumed == 1 - assert closed - - @pytest.mark.asyncio async def test_e2b_bounded_read_closes_real_sdk_stream() -> None: import httpx diff --git a/tests/sandbox/test_compatibility_guards.py b/tests/sandbox/test_compatibility_guards.py index b1ff30771f..67df891adb 100644 --- a/tests/sandbox/test_compatibility_guards.py +++ b/tests/sandbox/test_compatibility_guards.py @@ -299,34 +299,6 @@ def test_core_sandbox_public_export_surface_is_stable() -> None: "CloudflareSandboxSessionState", }, ), - ( - "agents.extensions.sandbox.runloop", - { - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformAxonsClient", - "RunloopPlatformBenchmarksClient", - "RunloopPlatformBlueprintsClient", - "RunloopPlatformClient", - "RunloopPlatformNetworkPoliciesClient", - "RunloopPlatformSecretsClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters", - "_decode_runloop_snapshot_ref", - "_encode_runloop_snapshot_ref", - }, - ), ( "agents.extensions.sandbox.vercel", { @@ -480,26 +452,6 @@ def test_optional_sandbox_dataclass_constructor_field_order_is_stable( "exposed_port_url_ttl_s", ), ), - ( - "agents.extensions.sandbox.runloop", - "RunloopSandboxClientOptions", - ( - "blueprint_id", - "blueprint_name", - "env_vars", - "pause_on_exit", - "name", - "timeouts", - "exposed_ports", - "user_parameters", - "launch_parameters", - "tunnel", - "gateways", - "mcp", - "metadata", - "managed_secrets", - ), - ), ( "agents.extensions.sandbox.vercel", "VercelSandboxClientOptions", @@ -704,34 +656,6 @@ def test_optional_sandbox_client_options_positional_field_order_is_stable( "exposed_port_url_ttl_s", ), ), - ( - "agents.extensions.sandbox.runloop", - "RunloopSandboxSessionState", - ( - "type", - "session_id", - "snapshot", - "manifest", - "exposed_ports", - "snapshot_fingerprint", - "snapshot_fingerprint_version", - "workspace_root_ready", - "devbox_id", - "blueprint_id", - "blueprint_name", - "base_env_vars", - "pause_on_exit", - "name", - "timeouts", - "user_parameters", - "launch_parameters", - "tunnel", - "gateways", - "mcp", - "metadata", - "secret_refs", - ), - ), ( "agents.extensions.sandbox.vercel", "VercelSandboxSessionState", @@ -798,7 +722,6 @@ def test_sandbox_session_state_field_order_is_stable( "cloudflare", ), ("agents.extensions.sandbox.daytona", "DaytonaSandboxClientOptions", (), "daytona"), - ("agents.extensions.sandbox.runloop", "RunloopSandboxClientOptions", (), "runloop"), ("agents.extensions.sandbox.vercel", "VercelSandboxClientOptions", (), "vercel"), ], ) @@ -855,11 +778,6 @@ def test_optional_sandbox_client_options_json_round_trip_preserves_type( "BlaxelSandboxSessionState", {"sandbox_name": "sandbox-123"}, ), - ( - "agents.extensions.sandbox.runloop", - "RunloopSandboxSessionState", - {"devbox_id": "devbox-123"}, - ), ( "agents.extensions.sandbox.vercel", "VercelSandboxSessionState", @@ -969,11 +887,6 @@ def test_mount_strategy_type_strings_round_trip_through_registry( "blaxel_cloud_bucket", ), ("agents.extensions.sandbox.blaxel", "BlaxelDriveMountStrategy", "blaxel_drive"), - ( - "agents.extensions.sandbox.runloop", - "RunloopCloudBucketMountStrategy", - "runloop_cloud_bucket", - ), ( "agents.extensions.sandbox.vercel", "VercelCloudBucketMountStrategy", diff --git a/tests/sandbox/test_mount_security.py b/tests/sandbox/test_mount_security.py index 8c6876705c..14979787f4 100644 --- a/tests/sandbox/test_mount_security.py +++ b/tests/sandbox/test_mount_security.py @@ -15,7 +15,6 @@ from agents.extensions.sandbox.daytona.mounts import DaytonaCloudBucketMountStrategy from agents.extensions.sandbox.e2b.mounts import E2BCloudBucketMountStrategy from agents.extensions.sandbox.modal.mounts import ModalCloudBucketMountStrategy -from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy from agents.run_config import SandboxRunConfig from agents.sandbox import Manifest from agents.sandbox._mount_security import ( @@ -1146,7 +1145,6 @@ def test_preserves_credentialless_in_container_and_credentialed_docker_mounts() ("blaxel", BlaxelCloudBucketMountStrategy()), ("daytona", DaytonaCloudBucketMountStrategy()), ("e2b", E2BCloudBucketMountStrategy()), - ("runloop", RunloopCloudBucketMountStrategy()), ], ) def test_preserves_credentialless_hosted_mount_strategies( diff --git a/tests/test_released_api_contract.py b/tests/test_released_api_contract.py index d443878bf2..0b0dd5c8ed 100644 --- a/tests/test_released_api_contract.py +++ b/tests/test_released_api_contract.py @@ -3613,14 +3613,6 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None: ("agents.extensions.sandbox", "ModalCloudBucketMountStrategy"), ("agents.extensions.sandbox", "ModalSandboxClient"), ("agents.extensions.sandbox", "ModalSandboxClientOptions"), - ("agents.extensions.sandbox", "RunloopAfterIdle"), - ("agents.extensions.sandbox", "RunloopGatewaySpec"), - ("agents.extensions.sandbox", "RunloopLaunchParameters"), - ("agents.extensions.sandbox", "RunloopMcpSpec"), - ("agents.extensions.sandbox", "RunloopSandboxClient"), - ("agents.extensions.sandbox", "RunloopSandboxClientOptions"), - ("agents.extensions.sandbox", "RunloopTunnelConfig"), - ("agents.extensions.sandbox", "RunloopUserParameters"), ("agents.extensions.sandbox", "VercelSandboxClient"), ("agents.extensions.sandbox", "VercelSandboxClientOptions"), } @@ -3649,16 +3641,6 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None: "module": "agents.retry", "names": ["response_started", "replay_safety", "stateful_request"], }, - { - "class_name": "RunloopPlatformClient", - "module": "agents.extensions.sandbox", - "names": ["axons", "benchmarks", "blueprints", "network_policies", "secrets"], - }, - { - "class_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "names": ["platform"], - }, { "class_name": "SandboxSessionState", "module": "agents.sandbox.session.sandbox_session_state", diff --git a/tests/test_repository_workflow_interfaces.py b/tests/test_repository_workflow_interfaces.py index 74431d6736..981d9d34d4 100644 --- a/tests/test_repository_workflow_interfaces.py +++ b/tests/test_repository_workflow_interfaces.py @@ -94,10 +94,7 @@ def test_repository_example_script_preserves_runner_contract() -> None: assert 'PID_FILE="$ROOT/.tmp/examples-auto-run.pid"' in runner assert 'LOG_DIR="$ROOT/.tmp/examples-start-logs"' in runner - assert ( - 'DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal runloop temporal"' - in runner - ) + assert 'DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal temporal"' in runner for required_argument in ("--auto-mode", "--main-log", "--logs-dir"): assert required_argument in runner for optional_mode in ( diff --git a/tests/test_run_examples_script.py b/tests/test_run_examples_script.py index c0ea88036e..2e81bfecad 100644 --- a/tests/test_run_examples_script.py +++ b/tests/test_run_examples_script.py @@ -68,8 +68,6 @@ def test_extra_credential_examples_are_skipped_in_default_auto_runs( "examples/sandbox/extensions/daytona/usaspending_text2sql/agent.py", "examples/sandbox/extensions/e2b_runner.py", "examples/sandbox/extensions/modal_runner.py", - "examples/sandbox/extensions/runloop/capabilities.py", - "examples/sandbox/extensions/runloop/runner.py", ] monkeypatch.delenv("EXAMPLES_AUTO_SKIP", raising=False) monkeypatch.setenv( @@ -83,7 +81,6 @@ def test_extra_credential_examples_are_skipped_in_default_auto_runs( "E2B_API_KEY", "MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET", - "RUNLOOP_API_KEY", "POSTGRES_PASSWORD", ): monkeypatch.setenv(name, "synthetic-test-value") diff --git a/uv.lock b/uv.lock index 451f205d38..37cbc065b9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -1032,15 +1032,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -1661,11 +1652,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[package.optional-dependencies] -http2 = [ - { name = "h2" }, -] - [[package]] name = "httpx2" version = "2.12.0" @@ -2810,9 +2796,6 @@ realtime = [ redis = [ { name = "redis" }, ] -runloop = [ - { name = "runloop-api-client" }, -] s3 = [ { name = "boto3" }, ] @@ -2924,7 +2907,6 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.31" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=7" }, { name = "requests", specifier = ">=2.0,<3" }, - { name = "runloop-api-client", marker = "extra == 'runloop'", specifier = ">=1.16.0,<2.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'sqlalchemy'", specifier = ">=2.0" }, { name = "starlette", specifier = ">=1.3.1" }, { name = "temporalio", marker = "extra == 'temporal'", specifier = "==1.34.0" }, @@ -2936,7 +2918,7 @@ requires-dist = [ { name = "websockets", marker = "extra == 'realtime'", specifier = ">=15.0,<17" }, { name = "websockets", marker = "extra == 'voice'", specifier = ">=15.0,<17" }, ] -provides-extras = ["voice", "viz", "litellm", "any-llm", "realtime", "sqlalchemy", "encrypt", "redis", "dapr", "mongodb", "docker", "blaxel", "daytona", "cloudflare", "e2b", "modal", "runloop", "vercel", "s3", "temporal"] +provides-extras = ["voice", "viz", "litellm", "any-llm", "realtime", "sqlalchemy", "encrypt", "redis", "dapr", "mongodb", "docker", "blaxel", "daytona", "cloudflare", "e2b", "modal", "vercel", "s3", "temporal"] [package.metadata.requires-dev] dev = [ @@ -4130,24 +4112,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] -[[package]] -name = "runloop-api-client" -version = "1.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx", extra = ["http2"] }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/d8/e85232182d50fa8374f495bb518e29b0d7de8aacfc86e4598019eb8718e9/runloop_api_client-1.32.0.tar.gz", hash = "sha256:1f2077fb5a99b5ef30dbc1d96d47c1ca1d3f77895b9e186d985e47365fe85634", size = 670118, upload-time = "2026-09-08T23:00:33.741Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/17/7c9bd35638526ab83d2a40e67454b3445d5de5a74a0e56e52541edefa3a5/runloop_api_client-1.32.0-py3-none-any.whl", hash = "sha256:23c36910de7423f73aef91d4a963abea80a6b2f5b2df5bb8e439d788d5486cc7", size = 414368, upload-time = "2026-09-08T23:00:35.085Z" }, -] - [[package]] name = "s3transfer" version = "0.19.2" @@ -4577,35 +4541,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] -[[package]] -name = "uuid-utils" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, - { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, - { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, - { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, -] - [[package]] name = "uvicorn" version = "0.35.0"