From 28d7c7e0f71034729c8cb865fb6598f609679573 Mon Sep 17 00:00:00 2001
From: td-oai
Date: Wed, 14 Oct 2026 08:14:53 -0700
Subject: [PATCH 1/4] feat(sandbox): remove Runloop provider
---
.github/scripts/run_examples.sh | 4 +-
examples/run_examples.py | 2 -
examples/sandbox/extensions/README.md | 80 +-
.../sandbox/extensions/runloop/__init__.py | 0
.../extensions/runloop/capabilities.py | 1015 ------
examples/sandbox/extensions/runloop/runner.py | 172 -
pyproject.toml | 5 -
src/agents/extensions/sandbox/__init__.py | 46 -
.../extensions/sandbox/runloop/__init__.py | 53 -
.../extensions/sandbox/runloop/mounts.py | 203 --
.../extensions/sandbox/runloop/sandbox.py | 1763 ---------
src/agents/sandbox/_mount_security.py | 8 -
tests/extensions/sandbox/test_runloop.py | 3147 -----------------
.../test_runloop_capabilities_example.py | 350 --
.../extensions/sandbox/test_runloop_mounts.py | 226 --
tests/fixtures/released_api_contract.json | 2540 +------------
.../released_api_contract_policy.json | 85 -
tests/sandbox/test_bounded_read.py | 72 +-
tests/sandbox/test_compatibility_guards.py | 87 -
tests/sandbox/test_mount_security.py | 2 -
tests/test_released_api_contract.py | 18 -
tests/test_repository_workflow_interfaces.py | 5 +-
tests/test_run_examples_script.py | 3 -
uv.lock | 69 +-
24 files changed, 9 insertions(+), 9946 deletions(-)
mode change 100755 => 100644 .github/scripts/run_examples.sh
delete mode 100644 examples/sandbox/extensions/runloop/__init__.py
delete mode 100644 examples/sandbox/extensions/runloop/capabilities.py
delete mode 100644 examples/sandbox/extensions/runloop/runner.py
delete mode 100644 src/agents/extensions/sandbox/runloop/__init__.py
delete mode 100644 src/agents/extensions/sandbox/runloop/mounts.py
delete mode 100644 src/agents/extensions/sandbox/runloop/sandbox.py
delete mode 100644 tests/extensions/sandbox/test_runloop.py
delete mode 100644 tests/extensions/sandbox/test_runloop_capabilities_example.py
delete mode 100644 tests/extensions/sandbox/test_runloop_mounts.py
diff --git a/.github/scripts/run_examples.sh b/.github/scripts/run_examples.sh
old mode 100755
new mode 100644
index 36716029a5..f535b7cdf2
--- 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/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..fa0d330bbf 100644
--- a/examples/sandbox/extensions/README.md
+++ b/examples/sandbox/extensions/README.md
@@ -6,7 +6,7 @@ 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.
@@ -228,84 +228,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 8b20c4e3cf..7d5765bc09 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.8", "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 184e480d2e..8a2b75b5c2 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"
@@ -2805,9 +2791,6 @@ realtime = [
redis = [
{ name = "redis" },
]
-runloop = [
- { name = "runloop-api-client" },
-]
s3 = [
{ name = "boto3" },
]
@@ -2919,7 +2902,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.33.0" },
@@ -2931,7 +2913,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 = [
@@ -4125,24 +4107,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.16.0"
@@ -4572,35 +4536,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"
From 8605aed69822c1832972c012e4ecb6e07344d72c Mon Sep 17 00:00:00 2001
From: td-oai |
Date: Wed, 14 Oct 2026 08:37:25 -0700
Subject: [PATCH 2/4] docs(sandbox): remove obsolete Runloop reference stubs
---
docs/ref/extensions/sandbox/runloop/mounts.md | 3 ---
docs/ref/extensions/sandbox/runloop/sandbox.md | 3 ---
2 files changed, 6 deletions(-)
delete mode 100644 docs/ref/extensions/sandbox/runloop/mounts.md
delete mode 100644 docs/ref/extensions/sandbox/runloop/sandbox.md
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
From f11347ff5aa934e06b5beeece65c03afd97ddc69 Mon Sep 17 00:00:00 2001
From: td-oai |
Date: Wed, 14 Oct 2026 08:42:48 -0700
Subject: [PATCH 3/4] fix(examples): preserve runner executable mode
---
.github/scripts/run_examples.sh | 0
1 file changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 .github/scripts/run_examples.sh
diff --git a/.github/scripts/run_examples.sh b/.github/scripts/run_examples.sh
old mode 100644
new mode 100755
From f3c162662590b25c76d6e85be60c7800037eb5c4 Mon Sep 17 00:00:00 2001
From: td |
Date: Wed, 14 Oct 2026 19:20:08 -0700
Subject: [PATCH 4/4] docs(sandbox): document Runloop migration to Modal
---
examples/sandbox/extensions/README.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/examples/sandbox/extensions/README.md b/examples/sandbox/extensions/README.md
index fa0d330bbf..18bd248afb 100644
--- a/examples/sandbox/extensions/README.md
+++ b/examples/sandbox/extensions/README.md
@@ -10,6 +10,14 @@ They intentionally keep the flow simple:
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
| |