From 4ef3c65a2643a093f6589a45428ebd4f1a2bf79e Mon Sep 17 00:00:00 2001 From: yoon-park-rl Date: Sat, 11 Jul 2026 01:07:41 -0700 Subject: [PATCH 1/3] feat: add HTTP/2 load testing infrastructure (#819) Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Reflex --- loadtest/.gitignore | 2 + loadtest/README.md | 58 ++++++++++++++ loadtest/alpn_check.py | 22 +++++ loadtest/h2_single_conn.py | 59 ++++++++++++++ loadtest/h2_test.py | 111 ++++++++++++++++++++++++++ loadtest/loadtest.py | 159 +++++++++++++++++++++++++++++++++++++ loadtest/raw_fetch_test.py | 99 +++++++++++++++++++++++ pyproject.toml | 1 + 8 files changed, 511 insertions(+) create mode 100644 loadtest/.gitignore create mode 100644 loadtest/README.md create mode 100644 loadtest/alpn_check.py create mode 100644 loadtest/h2_single_conn.py create mode 100644 loadtest/h2_test.py create mode 100644 loadtest/loadtest.py create mode 100644 loadtest/raw_fetch_test.py diff --git a/loadtest/.gitignore b/loadtest/.gitignore new file mode 100644 index 000000000..7a60b85e1 --- /dev/null +++ b/loadtest/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/loadtest/README.md b/loadtest/README.md new file mode 100644 index 000000000..cf3e4669d --- /dev/null +++ b/loadtest/README.md @@ -0,0 +1,58 @@ +# Load tests + +Manual transport comparison scripts for the Runloop Python SDK. These hit the +real API and are not part of `pytest`; run on demand. + +## End-to-end transport comparison (real API) + +All scripts require `RUNLOOP_API_KEY`. Set `RUNLOOP_BASE_URL` to override the +default endpoint (`https://api.runloop.ai`). + +Every script sends `devboxes.create` requests against a **deliberately +nonexistent blueprint** (`bp_nonexistent_loadtest_00000`), so every request +fails fast server-side (HTTP `400`) and **no devboxes are created** — isolating +client + server _request handling_ from provisioning. + +| Script | Transport under test | +| --- | --- | +| `loadtest.py` | The **SDK** itself (`AsyncRunloop`). `USE_HTTP2=0` → HTTP/1.1; default / `USE_HTTP2=1` → HTTP/2 (shared httpx pool). Always runs against the installed package in this checkout. | +| `h2_test.py` | Raw `httpx` HTTP/2, bypassing the SDK. Configurable connection count. | +| `h2_single_conn.py` | Raw `httpx` HTTP/2 on a single warmed connection (50-request burst). | +| `raw_fetch_test.py` | Raw `httpx` HTTP/1.1 keep-alive baseline. | +| `alpn_check.py` | Confirms the origin negotiates `h2` via TLS ALPN. | + +The raw-transport probes compare httpx HTTP/2 multiplexing against HTTP/1.1 +directly — the same comparison that motivates the SDK's shared HTTP/2 pool. +They're kept so the comparison stays reproducible. + +```sh +# Install the SDK from this checkout (editable): +cd /path/to/api-client-python && uv sync + +# SDK: HTTP/2 (default) vs HTTP/1.1, 2000-request burst +source ~/env && REQUEST_COUNT=2000 uv run python loadtest/loadtest.py # HTTP/2 +source ~/env && REQUEST_COUNT=2000 USE_HTTP2=0 uv run python loadtest/loadtest.py # HTTP/1.1 + +# Raw httpx HTTP/2 vs HTTP/1.1 comparison +source ~/env && uv run python loadtest/h2_test.py +source ~/env && uv run python loadtest/raw_fetch_test.py + +# Single-connection burst +source ~/env && uv run python loadtest/h2_single_conn.py + +# ALPN check +source ~/env && uv run python loadtest/alpn_check.py +``` + +HTTP/1.1 opens a socket per in-flight request; for large bursts raise the +file-descriptor limit (`ulimit -n 65536`) or keep `REQUEST_COUNT` small. + +## Environment variables + +| Variable | Default | Description | +| --- | --- | --- | +| `RUNLOOP_API_KEY` | *(required)* | API key | +| `RUNLOOP_BASE_URL` | `https://api.runloop.ai` | Override API endpoint | +| `REQUEST_COUNT` | `100000` (`loadtest.py`) / `10000` (`h2_test.py`) / `500` (`raw_fetch_test.py`) | Total requests | +| `NUM_CONNECTIONS` | `10` (`h2_test.py`) / `20` (`raw_fetch_test.py`) | Parallel connections | +| `USE_HTTP2` | `1` | `0` to force HTTP/1.1 in `loadtest.py` | diff --git a/loadtest/alpn_check.py b/loadtest/alpn_check.py new file mode 100644 index 000000000..e0ab701cd --- /dev/null +++ b/loadtest/alpn_check.py @@ -0,0 +1,22 @@ +"""Confirm the origin negotiates h2 via TLS ALPN.""" + +from __future__ import annotations + +import os +import ssl +import socket + +BASE_URL = os.environ.get("RUNLOOP_BASE_URL", "https://api.runloop.ai") +url_parts = BASE_URL.split("://", 1) +host = url_parts[1].split("/")[0] if len(url_parts) > 1 else url_parts[0] +port = 443 + +print(f"Checking ALPN for {host}:{port}") + +ctx = ssl.create_default_context() +ctx.set_alpn_protocols(["h2", "http/1.1"]) + +with socket.create_connection((host, port)) as sock: + with ctx.wrap_socket(sock, server_hostname=host) as tls: + print(f"Negotiated protocol: {tls.selected_alpn_protocol()}") + print(f"TLS version: {tls.version()}") diff --git a/loadtest/h2_single_conn.py b/loadtest/h2_single_conn.py new file mode 100644 index 000000000..8df712f19 --- /dev/null +++ b/loadtest/h2_single_conn.py @@ -0,0 +1,59 @@ +"""Raw httpx HTTP/2 single-connection burst: 50 requests on one warmed connection.""" + +from __future__ import annotations + +import os +import time +import asyncio +from typing import cast + +import httpx + +BASE_URL = os.environ.get("RUNLOOP_BASE_URL", "https://api.runloop.ai") +API_KEY = os.environ["RUNLOOP_API_KEY"] + +BODY = { + "blueprint_id": "bp_nonexistent_loadtest_00000", + "name": "loadtest-h2s-0", + "environment_variables": {"TEST_VAR_1": "value_one"}, + "launch_parameters": {"resource_size_request": "SMALL"}, +} + + +async def send_request(client: httpx.AsyncClient) -> dict[str, object]: + start = time.perf_counter() + response = await client.post( + "/v1/devboxes", + json=BODY, + headers={"authorization": f"Bearer {API_KEY}"}, + ) + return {"latency_ms": (time.perf_counter() - start) * 1000, "status": response.status_code} + + +async def main() -> None: + client = httpx.AsyncClient( + base_url=BASE_URL, + http2=True, + limits=httpx.Limits(max_connections=1, max_keepalive_connections=1), + timeout=httpx.Timeout(120.0), + ) + + # Warmup + w = await send_request(client) + print(f"Warmup: status={w['status']}, latency={w['latency_ms']:.0f}ms") + + count = 50 + print(f"\nBursting {count} requests on 1 warmed connection...") + wall_start = time.perf_counter() + results = await asyncio.gather(*(send_request(client) for _ in range(count))) + wall_ms = (time.perf_counter() - wall_start) * 1000 + + await client.aclose() + + lats: list[float] = sorted(cast(float, r["latency_ms"]) for r in results) + print(f"{count} requests in {wall_ms:.0f}ms ({count / (wall_ms / 1000):.1f} req/s)") + print(f"Latency: min={lats[0]:.0f}ms p50={lats[count // 2]:.0f}ms max={lats[-1]:.0f}ms") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/loadtest/h2_test.py b/loadtest/h2_test.py new file mode 100644 index 000000000..5295a2723 --- /dev/null +++ b/loadtest/h2_test.py @@ -0,0 +1,111 @@ +"""Raw httpx HTTP/2 load test: REQUEST_COUNT requests across NUM_CONNECTIONS connections.""" + +from __future__ import annotations + +import os +import math +import time +import asyncio +from typing import cast + +import httpx + +REQUEST_COUNT = int(os.environ.get("REQUEST_COUNT", "10000")) +NUM_CONNECTIONS = int(os.environ.get("NUM_CONNECTIONS", "10")) +BASE_URL = os.environ.get("RUNLOOP_BASE_URL", "https://api.runloop.ai") +API_KEY = os.environ["RUNLOOP_API_KEY"] + +BODY = { + "blueprint_id": "bp_nonexistent_loadtest_00000", + "name": "loadtest-h2-0", + "environment_variables": {"TEST_VAR_1": "value_one", "TEST_VAR_2": "value_two"}, + "metadata": {"test_run": "h2", "index": "0"}, + "launch_parameters": {"resource_size_request": "SMALL", "keep_alive_time_seconds": 300}, +} + + +def percentile(sorted_vals: list[float], p: float) -> float: + idx = math.ceil(p / 100 * len(sorted_vals)) - 1 + return sorted_vals[max(0, idx)] + + +async def send_request(client: httpx.AsyncClient) -> dict[str, object]: + start = time.perf_counter() + response = await client.post( + "/v1/devboxes", + json=BODY, + headers={"authorization": f"Bearer {API_KEY}"}, + ) + return {"latency_ms": (time.perf_counter() - start) * 1000, "status": response.status_code} + + +async def main() -> None: + if NUM_CONNECTIONS < 1: + print(f'NUM_CONNECTIONS must be a positive integer (got "{os.environ.get("NUM_CONNECTIONS")}")') + return + + print(f"HTTP/2 test: {REQUEST_COUNT} requests, {NUM_CONNECTIONS} connections to {BASE_URL}") + + # One client per logical connection — each capped at 1 connection so requests + # are distributed across NUM_CONNECTIONS distinct HTTP/2 sessions. + clients = [ + httpx.AsyncClient( + base_url=BASE_URL, + http2=True, + limits=httpx.Limits(max_connections=1, max_keepalive_connections=1), + timeout=httpx.Timeout(120.0), + ) + for _ in range(NUM_CONNECTIONS) + ] + + print(f"{NUM_CONNECTIONS} connections established\n") + + completed = 0 + + async def progress_printer() -> None: + while True: + await asyncio.sleep(2) + pct = completed / REQUEST_COUNT * 100 + print(f" progress: {completed}/{REQUEST_COUNT} ({pct:.1f}%)") + + async def wrapped(idx: int) -> dict[str, object]: + nonlocal completed + r = await send_request(clients[idx % NUM_CONNECTIONS]) + completed += 1 + return r + + wall_start = time.perf_counter() + progress_task = asyncio.create_task(progress_printer()) + results = await asyncio.gather(*(wrapped(i) for i in range(REQUEST_COUNT))) + progress_task.cancel() + wall_ms = (time.perf_counter() - wall_start) * 1000 + + for c in clients: + await c.aclose() + + latencies: list[float] = sorted(cast(float, r["latency_ms"]) for r in results) + status_counts: dict[int, int] = {} + for r in results: + s = cast(int, r["status"]) + status_counts[s] = status_counts.get(s, 0) + 1 + + print(f"\n=== HTTP/2 Results ===") + print(f"Requests: {REQUEST_COUNT}") + print(f"Connections: {NUM_CONNECTIONS}") + print(f"Wall clock: {wall_ms / 1000:.2f}s") + print(f"Throughput: {REQUEST_COUNT / (wall_ms / 1000):.1f} req/s") + if latencies: + print("\nLatency (ms):") + print(f" min: {latencies[0]:.1f}") + print(f" p50: {percentile(latencies, 50):.1f}") + print(f" p90: {percentile(latencies, 90):.1f}") + print(f" p95: {percentile(latencies, 95):.1f}") + print(f" p99: {percentile(latencies, 99):.1f}") + print(f" max: {latencies[-1]:.1f}") + print("\nStatus codes:") + for s, c_count in sorted(status_counts.items()): + print(f" {s}: {c_count}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/loadtest/loadtest.py b/loadtest/loadtest.py new file mode 100644 index 000000000..9db9cc80e --- /dev/null +++ b/loadtest/loadtest.py @@ -0,0 +1,159 @@ +"""SDK load test: fires devboxes.create bursts via AsyncRunloop. + +Imports the installed package from this checkout so the benchmark always +exercises the exact code here — not a separately published build. + +USE_HTTP2=0 → HTTP/1.1 (httpx with http2=False) +default → HTTP/2 (shared httpx pool with http2=True) +REQUEST_COUNT defaults to 100 000. +""" + +from __future__ import annotations + +import os +import math +import time +import asyncio + +import httpx + +from runloop_api_client import AsyncRunloop + +REQUEST_COUNT = int(os.environ.get("REQUEST_COUNT", "100000")) +RUNLOOP_BASE_URL = os.environ.get("RUNLOOP_BASE_URL") +# HTTP/2 is the SDK default; set USE_HTTP2=0 to benchmark HTTP/1.1. +USE_HTTP2 = os.environ.get("USE_HTTP2", "1") == "1" +PROGRESS_INTERVAL = 2.0 + + +def build_client() -> AsyncRunloop: + kwargs: dict[str, object] = {"max_retries": 0, "timeout": 120.0} + if RUNLOOP_BASE_URL: + kwargs["base_url"] = RUNLOOP_BASE_URL + if not USE_HTTP2: + # A custom http_client disables the shared HTTP/2 pool. + # Set http2: false explicitly — omitting it would select HTTP/2. + kwargs["http_client"] = httpx.AsyncClient( + http2=False, + limits=httpx.Limits(max_connections=None, max_keepalive_connections=100), + timeout=httpx.Timeout(120.0), + ) + return AsyncRunloop(**kwargs) # type: ignore[arg-type] + + +async def send_request(client: AsyncRunloop, index: int, run_id: str) -> dict[str, object]: + start = time.perf_counter() + status: int | None = None + error: str | None = None + try: + await client.devboxes.create( + blueprint_id="bp_nonexistent_loadtest_00000", + name=f"loadtest-{run_id}-{index}", + environment_variables={"TEST_VAR_1": "value_one", "TEST_VAR_2": "value_two"}, + metadata={"test_run": run_id, "index": str(index)}, + launch_parameters={"resource_size_request": "SMALL", "keep_alive_time_seconds": 300}, + ) + status = 200 + except Exception as exc: + status = getattr(exc, "status_code", None) + error = str(exc) or type(exc).__name__ + return { + "index": index, + "latency_ms": (time.perf_counter() - start) * 1000, + "status": status, + "error": error, + } + + +def percentile(sorted_vals: list[float], p: float) -> float: + idx = math.ceil(p / 100 * len(sorted_vals)) - 1 + return sorted_vals[max(0, idx)] + + +def print_metrics(results: list[dict[str, object]], wall_ms: float) -> None: + latencies = sorted(float(r["latency_ms"]) for r in results) # type: ignore[arg-type] + status_counts: dict[str, int] = {} + for r in results: + key = str(r["status"]) if r["status"] is not None else "network_error" + status_counts[key] = status_counts.get(key, 0) + 1 + + print("\n=== Load Test Results ===") + print(f"Requests: {len(results)}") + print(f"Wall clock: {wall_ms / 1000:.2f}s") + print(f"Throughput: {len(results) / (wall_ms / 1000):.1f} req/s") + if latencies: + print("\nLatency (ms):") + print(f" min: {latencies[0]:.1f}") + print(f" p50: {percentile(latencies, 50):.1f}") + print(f" p90: {percentile(latencies, 90):.1f}") + print(f" p95: {percentile(latencies, 95):.1f}") + print(f" p99: {percentile(latencies, 99):.1f}") + print(f" max: {latencies[-1]:.1f}") + print("\nStatus codes:") + for s, c in sorted(status_counts.items()): + print(f" {s}: {c}") + + # Break down the opaque "network_error" bucket by exception message so + # transport-level failures (timeouts, resets, pool exhaustion) are diagnosable. + error_counts: dict[str, int] = {} + for r in results: + if r["status"] is None and r["error"] is not None: + msg = str(r["error"]) + error_counts[msg] = error_counts.get(msg, 0) + 1 + if error_counts: + print("\nErrors (network_error breakdown):") + for msg, c in sorted(error_counts.items(), key=lambda kv: kv[1], reverse=True): + print(f" {c}x {msg}") + + +async def main() -> None: + fd_limit: int | None = None + try: + import resource as _resource + + fd_limit = _resource.getrlimit(_resource.RLIMIT_NOFILE)[0] + except Exception: + pass + + if not USE_HTTP2 and fd_limit is not None and fd_limit < 10000: + print(f"\nWARNING: File descriptor limit is {fd_limit}. For large HTTP/1.1 bursts, run:") + print(" ulimit -n 65536") + print("Or use HTTP/2 multiplexing: USE_HTTP2=1\n") + + client = build_client() + run_id = f"run-{int(time.time())}" + + print(f"Starting load test: {REQUEST_COUNT} concurrent requests") + print(f"Run ID: {run_id}") + print(f"HTTP mode: {'HTTP/2 (shared httpx pool)' if USE_HTTP2 else 'HTTP/1.1 (httpx http2=False)'}") + print(f"Base URL: {RUNLOOP_BASE_URL or '(SDK default)'}") + if fd_limit is not None: + print(f"FD limit: {fd_limit}") + print() + + completed = 0 + + async def progress_printer() -> None: + while True: + await asyncio.sleep(PROGRESS_INTERVAL) + pct = completed / REQUEST_COUNT * 100 + print(f" progress: {completed}/{REQUEST_COUNT} ({pct:.1f}%)") + + async def wrapped(idx: int) -> dict[str, object]: + nonlocal completed + r = await send_request(client, idx, run_id) + completed += 1 + return r + + wall_start = time.perf_counter() + progress_task = asyncio.create_task(progress_printer()) + results = list(await asyncio.gather(*(wrapped(i) for i in range(REQUEST_COUNT)))) + progress_task.cancel() + wall_ms = (time.perf_counter() - wall_start) * 1000 + + await client.close() + print_metrics(results, wall_ms) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/loadtest/raw_fetch_test.py b/loadtest/raw_fetch_test.py new file mode 100644 index 000000000..7400f46b1 --- /dev/null +++ b/loadtest/raw_fetch_test.py @@ -0,0 +1,99 @@ +"""Raw httpx HTTP/1.1 keep-alive baseline.""" + +from __future__ import annotations + +import os +import math +import time +import asyncio +from typing import cast + +import httpx + +REQUEST_COUNT = int(os.environ.get("REQUEST_COUNT", "500")) +NUM_CONNECTIONS = int(os.environ.get("NUM_CONNECTIONS", "20")) +BASE_URL = os.environ.get("RUNLOOP_BASE_URL", "https://api.runloop.ai") +API_KEY = os.environ["RUNLOOP_API_KEY"] + +BODY = { + "blueprint_id": "bp_nonexistent_loadtest_00000", + "name": "loadtest-raw-0", + "environment_variables": {"TEST_VAR_1": "value_one", "TEST_VAR_2": "value_two"}, + "metadata": {"test_run": "raw", "index": "0"}, + "launch_parameters": {"resource_size_request": "SMALL", "keep_alive_time_seconds": 300}, +} + + +def percentile(sorted_vals: list[float], p: float) -> float: + idx = math.ceil(p / 100 * len(sorted_vals)) - 1 + return sorted_vals[max(0, idx)] + + +async def main() -> None: + print(f"HTTP/1.1 test: {REQUEST_COUNT} requests, {NUM_CONNECTIONS} keep-alive connections to {BASE_URL}") + + client = httpx.AsyncClient( + base_url=BASE_URL, + http2=False, + limits=httpx.Limits(max_connections=NUM_CONNECTIONS, max_keepalive_connections=NUM_CONNECTIONS), + timeout=httpx.Timeout(120.0), + ) + + completed = 0 + + async def progress_printer() -> None: + while True: + await asyncio.sleep(2) + pct = completed / REQUEST_COUNT * 100 + print(f" progress: {completed}/{REQUEST_COUNT} ({pct:.1f}%)") + + async def send_one() -> dict[str, object]: + nonlocal completed + start = time.perf_counter() + status: int | None = None + try: + response = await client.post( + "/v1/devboxes", + json=BODY, + headers={"authorization": f"Bearer {API_KEY}"}, + ) + status = response.status_code + except Exception: + pass + finally: + completed += 1 + return {"latency_ms": (time.perf_counter() - start) * 1000, "status": status} + + wall_start = time.perf_counter() + progress_task = asyncio.create_task(progress_printer()) + results = await asyncio.gather(*(send_one() for _ in range(REQUEST_COUNT))) + progress_task.cancel() + wall_ms = (time.perf_counter() - wall_start) * 1000 + + await client.aclose() + + latencies: list[float] = sorted(cast(float, r["latency_ms"]) for r in results) + status_counts: dict[str, int] = {} + for r in results: + key = str(r["status"]) if r["status"] is not None else "network_error" + status_counts[key] = status_counts.get(key, 0) + 1 + + print(f"\n=== HTTP/1.1 Results ===") + print(f"Requests: {REQUEST_COUNT}") + print(f"Wall clock: {wall_ms / 1000:.2f}s") + print(f"Throughput: {REQUEST_COUNT / (wall_ms / 1000):.1f} req/s") + if latencies: + print("\nLatency (ms):") + print(f" min: {latencies[0]:.1f}") + print(f" p50: {percentile(latencies, 50):.1f}") + print(f" p90: {percentile(latencies, 90):.1f}") + print(f" p95: {percentile(latencies, 95):.1f}") + print(f" p99: {percentile(latencies, 99):.1f}") + print(f" max: {latencies[-1]:.1f}") + print("\nStatus codes:") + for s, c_count in sorted(status_counts.items()): + print(f" {s}: {c_count}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 909d5e9c3..c15f2287b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -256,3 +256,4 @@ known-first-party = ["runloop_api_client", "tests"] "scripts/**.py" = ["T201", "T203"] "tests/**.py" = ["T201", "T203"] "examples/**.py" = ["T201", "T203"] +"loadtest/**.py" = ["T201", "T203"] From 0000c52205bb8942942b8dec9f91972e89eebafb Mon Sep 17 00:00:00 2001 From: yoon-park-rl Date: Mon, 13 Jul 2026 10:40:08 -0700 Subject: [PATCH 2/3] feat(loadtest): add pool_check.py to verify shared sync connection pool (#822) Co-authored-by: Claude Sonnet 4.6 --- loadtest/README.md | 5 ++++ loadtest/pool_check.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 loadtest/pool_check.py diff --git a/loadtest/README.md b/loadtest/README.md index cf3e4669d..9e85f5ce2 100644 --- a/loadtest/README.md +++ b/loadtest/README.md @@ -20,11 +20,16 @@ client + server _request handling_ from provisioning. | `h2_single_conn.py` | Raw `httpx` HTTP/2 on a single warmed connection (50-request burst). | | `raw_fetch_test.py` | Raw `httpx` HTTP/1.1 keep-alive baseline. | | `alpn_check.py` | Confirms the origin negotiates `h2` via TLS ALPN. | +| `pool_check.py` | Verifies that multiple sync `Runloop` instances share a single connection pool (transport object identity check, no real requests). | The raw-transport probes compare httpx HTTP/2 multiplexing against HTTP/1.1 directly — the same comparison that motivates the SDK's shared HTTP/2 pool. They're kept so the comparison stays reproducible. +`pool_check.py` is a lightweight sanity check (no API key, no real requests) that +confirms multiple sync `Runloop` instances reuse a single underlying transport +object — preventing file-descriptor exhaustion when many clients are instantiated. + ```sh # Install the SDK from this checkout (editable): cd /path/to/api-client-python && uv sync diff --git a/loadtest/pool_check.py b/loadtest/pool_check.py new file mode 100644 index 000000000..966857c57 --- /dev/null +++ b/loadtest/pool_check.py @@ -0,0 +1,58 @@ +"""Verify that multiple Runloop (sync) instances share a single connection pool. + +Spins up N SDK instances and checks that the number of open connections to +api.runloop.ai does not grow linearly with instance count — it should stay at +one (or a small fixed number) because all instances share the same underlying +httpx transport. + +Usage: + uv run python loadtest/pool_check.py + +No API key is required — we only check the transport object identity and the +OS-level connection count, not make real requests. +""" + +from __future__ import annotations + +import os +import sys +import resource + +from runloop_api_client import Runloop + +HOST = "api.runloop.ai" +N = 20 + + +def main() -> None: + fd_before = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + print(f"FD limit: {fd_before}") + print(f"Creating {N} Runloop instances...\n") + + clients: list[Runloop] = [] + for _ in range(N): + clients.append(Runloop(bearer_token=os.environ.get("RUNLOOP_API_KEY", "dummy"))) + + # All instances should reference the same shared transport object. + transport_ids: set[int] = set() + for c in clients: + t = getattr(c._client, "_transport", None) + if t is not None: + transport_ids.add(id(t)) + + print(f"Distinct transport objects across {N} instances: {len(transport_ids)}") + if len(transport_ids) == 1: + print("PASS — all instances share one transport (connection pool).") + else: + print("FAIL — instances have separate transports; FD exhaustion is possible.") + sys.exit(1) + + # Close all clients. + for c in clients: + c.close() + + print("\nDone.") + + +if __name__ == "__main__": + main() From 6b9e1dc99dd85abd7dc7ab9af2ac58f97ccc88cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:27:10 +0000 Subject: [PATCH 3/3] release: 1.24.0 (#820) Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +-- .release-please-manifest.json | 2 +- .stats.yml | 6 +-- CHANGELOG.md | 20 +++++++ api.md | 2 + pyproject.toml | 2 +- scripts/lint | 2 +- src/runloop_api_client/_version.py | 2 +- .../resources/axons/axons.py | 8 +++ .../resources/benchmark_jobs.py | 8 +++ .../resources/benchmark_runs.py | 16 ++++++ .../resources/benchmarks.py | 24 +++++++++ .../resources/devboxes/devboxes.py | 2 + .../resources/gateway_configs.py | 8 +++ .../resources/mcp_configs.py | 8 +++ .../resources/network_policies.py | 53 +++++++++++++++++- .../resources/scenarios/runs.py | 8 +++ .../resources/scenarios/scenarios.py | 16 ++++++ .../resources/scenarios/scorers.py | 8 +++ src/runloop_api_client/types/__init__.py | 4 ++ src/runloop_api_client/types/allowed_cidr.py | 21 ++++++++ .../types/allowed_cidr_param.py | 23 ++++++++ .../types/axon_list_params.py | 3 ++ .../types/benchmark_job_list_params.py | 3 ++ .../types/benchmark_list_params.py | 3 ++ .../types/benchmark_list_public_params.py | 6 +++ .../types/benchmark_run_list_params.py | 3 ++ ...benchmark_run_list_scenario_runs_params.py | 3 ++ .../types/devbox_list_params.py | 1 + src/runloop_api_client/types/devbox_view.py | 7 ++- .../types/gateway_config_list_params.py | 3 ++ .../types/mcp_config_list_params.py | 3 ++ .../types/network_policy_create_params.py | 16 +++++- .../types/network_policy_list_params.py | 3 ++ .../types/network_policy_update_params.py | 15 +++++- .../types/network_policy_view.py | 13 +++++ src/runloop_api_client/types/port_rule.py | 24 +++++++++ .../types/port_rule_param.py | 24 +++++++++ .../types/scenario_list_params.py | 3 ++ .../types/scenario_list_public_params.py | 3 ++ .../types/scenarios/run_list_params.py | 3 ++ .../types/scenarios/scorer_list_params.py | 3 ++ .../types/shared/broker_mount.py | 6 +-- .../types/shared_params/broker_mount.py | 6 +-- tests/api_resources/scenarios/test_runs.py | 2 + tests/api_resources/scenarios/test_scorers.py | 2 + tests/api_resources/test_axons.py | 2 + tests/api_resources/test_benchmark_jobs.py | 2 + tests/api_resources/test_benchmark_runs.py | 4 ++ tests/api_resources/test_benchmarks.py | 6 +++ tests/api_resources/test_gateway_configs.py | 2 + tests/api_resources/test_mcp_configs.py | 2 + tests/api_resources/test_network_policies.py | 54 +++++++++++++++++++ tests/api_resources/test_scenarios.py | 4 ++ uv.lock | 2 +- 55 files changed, 463 insertions(+), 22 deletions(-) create mode 100644 src/runloop_api_client/types/allowed_cidr.py create mode 100644 src/runloop_api_client/types/allowed_cidr_param.py create mode 100644 src/runloop_api_client/types/port_rule.py create mode 100644 src/runloop_api_client/types/port_rule_param.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3db832c9d..87c5aabb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/runloop-python' && 'depot-ubuntu-24.04' || 'ubuntu-slim' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-slim' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: runloopai/checkout@main @@ -44,7 +44,7 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/runloop-python' && 'depot-ubuntu-24.04' || 'ubuntu-slim' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-slim' }} steps: - uses: runloopai/checkout@main @@ -81,7 +81,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/runloop-python' && 'depot-ubuntu-24.04' || 'ubuntu-slim' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-slim' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: runloopai/checkout@main diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 75baea2d1..bfaab56f6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.23.3" + ".": "1.24.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 3b0452793..4e9c9d433 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-99b4be5cc4cd6f2e1cfd71d5a9ec5409dd9293fe6084833da76f178010bfdcab.yml -openapi_spec_hash: 4760825b37e131da53c88bf893b60937 -config_hash: 9f32651e6269089b5d6c33594b992232 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-0b7cd0c2fc193b18189cd7f44cf45ece7726b5d485fb72577f7d235266432ea0.yml +openapi_spec_hash: 78c340dbfb9d3d58b24ef318fc2a657b +config_hash: 218b8d25038e627faab98532392ee9a0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 462953953..de0a6c03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 1.24.0 (2026-07-22) + +Full Changelog: [v1.23.3...v1.24.0](https://github.com/runloopai/api-client-python/compare/v1.23.3...v1.24.0) + +### Features + +* add HTTP/2 load testing infrastructure ([#819](https://github.com/runloopai/api-client-python/issues/819)) ([4ef3c65](https://github.com/runloopai/api-client-python/commit/4ef3c65a2643a093f6589a45428ebd4f1a2bf79e)) +* **api:** add codex broker mount protocol and axon attach enum value ([#10186](https://github.com/runloopai/api-client-python/issues/10186)) ([b3a0a91](https://github.com/runloopai/api-client-python/commit/b3a0a91b5463e47787cce33aa6ad803446e41ecd)) +* **devbox:** rename scheduled -> queued ([#10217](https://github.com/runloopai/api-client-python/issues/10217)) ([f6cc5b7](https://github.com/runloopai/api-client-python/commit/f6cc5b714771564aaa7c5424b21fbd3c87286306)) +* **loadtest:** add pool_check.py to verify shared sync connection pool ([#822](https://github.com/runloopai/api-client-python/issues/822)) ([0000c52](https://github.com/runloopai/api-client-python/commit/0000c52205bb8942942b8dec9f91972e89eebafb)) +* **mux:** expose CIDR egress rules on the network policy API ([#10269](https://github.com/runloopai/api-client-python/issues/10269)) ([b77a065](https://github.com/runloopai/api-client-python/commit/b77a065422042d3a7717c02208e16b72fdddc5e3)) +* **network:** add allow_runloop_mirrors egress flag to network policies ([#10350](https://github.com/runloopai/api-client-python/issues/10350)) ([c042398](https://github.com/runloopai/api-client-python/commit/c04239892931cd1b80d926af8664a461ea426aab)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([6a762cb](https://github.com/runloopai/api-client-python/commit/6a762cb53fbf1b6a2d113874227d0b68caa979dc)) + + +### Bug Fixes + +* **internal:** resolve build failures ([a5b61f3](https://github.com/runloopai/api-client-python/commit/a5b61f3b981d74f4707d132513d59beb4bdb33c2)) +* **mux:** uniform id/name search across list endpoints, close cross-tenant IDOR ([#10267](https://github.com/runloopai/api-client-python/issues/10267)) ([4a40176](https://github.com/runloopai/api-client-python/commit/4a401768ae50e1047d4c6f80384c059112299463)) + ## 1.23.3 (2026-07-10) Full Changelog: [v1.23.2...v1.23.3](https://github.com/runloopai/api-client-python/compare/v1.23.2...v1.23.3) diff --git a/api.md b/api.md index 2f3ddb2f8..b7db70b09 100644 --- a/api.md +++ b/api.md @@ -419,10 +419,12 @@ Types: ```python from runloop_api_client.types import ( + AllowedCidr, NetworkPolicyCreateParameters, NetworkPolicyListView, NetworkPolicyUpdateParameters, NetworkPolicyView, + PortRule, ) ``` diff --git a/pyproject.toml b/pyproject.toml index c15f2287b..77d099e9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "runloop_api_client" -version = "1.23.3" +version = "1.24.0" description = "The official Python library for the runloop API" dynamic = ["readme"] license = "MIT" diff --git a/scripts/lint b/scripts/lint index b6390e3ae..d52c11733 100755 --- a/scripts/lint +++ b/scripts/lint @@ -5,7 +5,7 @@ set -e cd "$(dirname "$0")/.." echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py index 6d0c2b60b..5e32f9066 100644 --- a/src/runloop_api_client/_version.py +++ b/src/runloop_api_client/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "runloop_api_client" -__version__ = "1.23.3" # x-release-please-version +__version__ = "1.24.0" # x-release-please-version diff --git a/src/runloop_api_client/resources/axons/axons.py b/src/runloop_api_client/resources/axons/axons.py index 9387bc0c9..fba62a339 100644 --- a/src/runloop_api_client/resources/axons/axons.py +++ b/src/runloop_api_client/resources/axons/axons.py @@ -153,6 +153,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -174,6 +175,8 @@ def list( name: Filter by axon name (prefix match supported). + search: Search by axon ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -198,6 +201,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, axon_list_params.AxonListParams, @@ -424,6 +428,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -445,6 +450,8 @@ def list( name: Filter by axon name (prefix match supported). + search: Search by axon ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -469,6 +476,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, axon_list_params.AxonListParams, diff --git a/src/runloop_api_client/resources/benchmark_jobs.py b/src/runloop_api_client/resources/benchmark_jobs.py index e57e58c33..57dc8b5cd 100644 --- a/src/runloop_api_client/resources/benchmark_jobs.py +++ b/src/runloop_api_client/resources/benchmark_jobs.py @@ -134,6 +134,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -153,6 +154,8 @@ def list( name: Filter by name + search: Search by benchmark job ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -175,6 +178,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, benchmark_job_list_params.BenchmarkJobListParams, @@ -294,6 +298,7 @@ async def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -313,6 +318,8 @@ async def list( name: Filter by name + search: Search by benchmark job ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -335,6 +342,7 @@ async def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, benchmark_job_list_params.BenchmarkJobListParams, diff --git a/src/runloop_api_client/resources/benchmark_runs.py b/src/runloop_api_client/resources/benchmark_runs.py index 66513a855..075246c30 100644 --- a/src/runloop_api_client/resources/benchmark_runs.py +++ b/src/runloop_api_client/resources/benchmark_runs.py @@ -85,6 +85,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, state: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -107,6 +108,8 @@ def list( name: Filter by name + search: Search by benchmark run ID or name. + starting_after: Load the next page of data starting after the item with the given ID. state: Filter by state @@ -133,6 +136,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, "state": state, }, @@ -231,6 +235,7 @@ def list_scenario_runs( *, include_total_count: bool | Omit = omit, limit: int | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, state: Literal["running", "scoring", "scored", "completed", "canceled", "timeout", "failed"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -249,6 +254,8 @@ def list_scenario_runs( limit: The limit of items to return. Default is 20. Max is 5000. + search: Search by scenario run ID or name. + starting_after: Load the next page of data starting after the item with the given ID. state: Filter by Scenario Run state @@ -275,6 +282,7 @@ def list_scenario_runs( { "include_total_count": include_total_count, "limit": limit, + "search": search, "starting_after": starting_after, "state": state, }, @@ -345,6 +353,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, state: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -367,6 +376,8 @@ def list( name: Filter by name + search: Search by benchmark run ID or name. + starting_after: Load the next page of data starting after the item with the given ID. state: Filter by state @@ -393,6 +404,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, "state": state, }, @@ -491,6 +503,7 @@ def list_scenario_runs( *, include_total_count: bool | Omit = omit, limit: int | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, state: Literal["running", "scoring", "scored", "completed", "canceled", "timeout", "failed"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -509,6 +522,8 @@ def list_scenario_runs( limit: The limit of items to return. Default is 20. Max is 5000. + search: Search by scenario run ID or name. + starting_after: Load the next page of data starting after the item with the given ID. state: Filter by Scenario Run state @@ -535,6 +550,7 @@ def list_scenario_runs( { "include_total_count": include_total_count, "limit": limit, + "search": search, "starting_after": starting_after, "state": state, }, diff --git a/src/runloop_api_client/resources/benchmarks.py b/src/runloop_api_client/resources/benchmarks.py index ca5442f52..7ee045266 100644 --- a/src/runloop_api_client/resources/benchmarks.py +++ b/src/runloop_api_client/resources/benchmarks.py @@ -247,6 +247,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -266,6 +267,8 @@ def list( name: Filter by name + search: Search by benchmark ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -289,6 +292,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, benchmark_list_params.BenchmarkListParams, @@ -351,6 +355,8 @@ def list_public( *, include_total_count: bool | Omit = omit, limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -368,6 +374,10 @@ def list_public( limit: The limit of items to return. Default is 20. Max is 5000. + name: Filter by name + + search: Search by benchmark ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -390,6 +400,8 @@ def list_public( { "include_total_count": include_total_count, "limit": limit, + "name": name, + "search": search, "starting_after": starting_after, }, benchmark_list_public_params.BenchmarkListPublicParams, @@ -722,6 +734,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -741,6 +754,8 @@ def list( name: Filter by name + search: Search by benchmark ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -764,6 +779,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, benchmark_list_params.BenchmarkListParams, @@ -826,6 +842,8 @@ def list_public( *, include_total_count: bool | Omit = omit, limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -843,6 +861,10 @@ def list_public( limit: The limit of items to return. Default is 20. Max is 5000. + name: Filter by name + + search: Search by benchmark ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -865,6 +887,8 @@ def list_public( { "include_total_count": include_total_count, "limit": limit, + "name": name, + "search": search, "starting_after": starting_after, }, benchmark_list_public_params.BenchmarkListPublicParams, diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index f919a2147..f100014f8 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -531,6 +531,7 @@ def list( starting_after: str | Omit = omit, status: Literal[ "scheduled", + "queued", "provisioning", "initializing", "running", @@ -2204,6 +2205,7 @@ def list( starting_after: str | Omit = omit, status: Literal[ "scheduled", + "queued", "provisioning", "initializing", "running", diff --git a/src/runloop_api_client/resources/gateway_configs.py b/src/runloop_api_client/resources/gateway_configs.py index 799e730a6..34f0989cf 100644 --- a/src/runloop_api_client/resources/gateway_configs.py +++ b/src/runloop_api_client/resources/gateway_configs.py @@ -209,6 +209,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -231,6 +232,8 @@ def list( name: Filter by name (partial match supported). + search: Search by gateway config ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -255,6 +258,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, gateway_config_list_params.GatewayConfigListParams, @@ -490,6 +494,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -512,6 +517,8 @@ def list( name: Filter by name (partial match supported). + search: Search by gateway config ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -536,6 +543,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, gateway_config_list_params.GatewayConfigListParams, diff --git a/src/runloop_api_client/resources/mcp_configs.py b/src/runloop_api_client/resources/mcp_configs.py index b097db18b..e96647c9c 100644 --- a/src/runloop_api_client/resources/mcp_configs.py +++ b/src/runloop_api_client/resources/mcp_configs.py @@ -211,6 +211,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -232,6 +233,8 @@ def list( name: Filter by name (prefix match supported). + search: Search by MCP config ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -256,6 +259,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, mcp_config_list_params.McpConfigListParams, @@ -493,6 +497,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -514,6 +519,8 @@ def list( name: Filter by name (prefix match supported). + search: Search by MCP config ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -538,6 +545,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, mcp_config_list_params.McpConfigListParams, diff --git a/src/runloop_api_client/resources/network_policies.py b/src/runloop_api_client/resources/network_policies.py index 7ba7abd9e..b02062311 100644 --- a/src/runloop_api_client/resources/network_policies.py +++ b/src/runloop_api_client/resources/network_policies.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Iterable, Optional import httpx @@ -19,6 +19,7 @@ ) from ..pagination import SyncNetworkPoliciesCursorIDPage, AsyncNetworkPoliciesCursorIDPage from .._base_client import AsyncPaginator, make_request_options +from ..types.allowed_cidr_param import AllowedCidrParam from ..types.network_policy_view import NetworkPolicyView __all__ = ["NetworkPoliciesResource", "AsyncNetworkPoliciesResource"] @@ -52,6 +53,8 @@ def create( allow_all: Optional[bool] | Omit = omit, allow_devbox_to_devbox: Optional[bool] | Omit = omit, allow_mcp_gateway: Optional[bool] | Omit = omit, + allow_runloop_mirrors: Optional[bool] | Omit = omit, + allowed_cidrs: Optional[Iterable[AllowedCidrParam]] | Omit = omit, allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, description: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -84,6 +87,13 @@ def create( allow_mcp_gateway: (Optional) If true, allows devbox egress to the MCP hub for MCP server access. Defaults to false. + allow_runloop_mirrors: (Optional) If true, allows devbox egress to Runloop's package/image registry + mirrors. Defaults to false. Implicitly allowed when allow_all is true. + + allowed_cidrs: (Optional) IPv4 CIDR-based allow list with optional port restrictions, additive + with allowed_hostnames. Example: [{'cidr': '10.12.0.0/16', 'ports': [{'port': + 443}]}]. + allowed_hostnames: (Optional) DNS-based allow list with wildcard support. Examples: ['github.com', '*.npmjs.org']. @@ -108,6 +118,8 @@ def create( "allow_all": allow_all, "allow_devbox_to_devbox": allow_devbox_to_devbox, "allow_mcp_gateway": allow_mcp_gateway, + "allow_runloop_mirrors": allow_runloop_mirrors, + "allowed_cidrs": allowed_cidrs, "allowed_hostnames": allowed_hostnames, "description": description, }, @@ -164,6 +176,8 @@ def update( allow_all: Optional[bool] | Omit = omit, allow_devbox_to_devbox: Optional[bool] | Omit = omit, allow_mcp_gateway: Optional[bool] | Omit = omit, + allow_runloop_mirrors: Optional[bool] | Omit = omit, + allowed_cidrs: Optional[Iterable[AllowedCidrParam]] | Omit = omit, allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, description: Optional[str] | Omit = omit, name: Optional[str] | Omit = omit, @@ -188,6 +202,12 @@ def update( allow_mcp_gateway: If true, allows devbox egress to the MCP hub. + allow_runloop_mirrors: If true, allows devbox egress to Runloop's package/image registry mirrors. + Implicitly allowed when allow_all is true. + + allowed_cidrs: Updated IPv4 CIDR-based allow list with optional port restrictions, additive + with allowed_hostnames. + allowed_hostnames: Updated DNS-based allow list with wildcard support. Examples: ['github.com', '*.npmjs.org']. @@ -215,6 +235,8 @@ def update( "allow_all": allow_all, "allow_devbox_to_devbox": allow_devbox_to_devbox, "allow_mcp_gateway": allow_mcp_gateway, + "allow_runloop_mirrors": allow_runloop_mirrors, + "allowed_cidrs": allowed_cidrs, "allowed_hostnames": allowed_hostnames, "description": description, "name": name, @@ -238,6 +260,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -259,6 +282,8 @@ def list( name: Filter by name (partial match supported). + search: Search by network policy ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -283,6 +308,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, network_policy_list_params.NetworkPolicyListParams, @@ -361,6 +387,8 @@ async def create( allow_all: Optional[bool] | Omit = omit, allow_devbox_to_devbox: Optional[bool] | Omit = omit, allow_mcp_gateway: Optional[bool] | Omit = omit, + allow_runloop_mirrors: Optional[bool] | Omit = omit, + allowed_cidrs: Optional[Iterable[AllowedCidrParam]] | Omit = omit, allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, description: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -393,6 +421,13 @@ async def create( allow_mcp_gateway: (Optional) If true, allows devbox egress to the MCP hub for MCP server access. Defaults to false. + allow_runloop_mirrors: (Optional) If true, allows devbox egress to Runloop's package/image registry + mirrors. Defaults to false. Implicitly allowed when allow_all is true. + + allowed_cidrs: (Optional) IPv4 CIDR-based allow list with optional port restrictions, additive + with allowed_hostnames. Example: [{'cidr': '10.12.0.0/16', 'ports': [{'port': + 443}]}]. + allowed_hostnames: (Optional) DNS-based allow list with wildcard support. Examples: ['github.com', '*.npmjs.org']. @@ -417,6 +452,8 @@ async def create( "allow_all": allow_all, "allow_devbox_to_devbox": allow_devbox_to_devbox, "allow_mcp_gateway": allow_mcp_gateway, + "allow_runloop_mirrors": allow_runloop_mirrors, + "allowed_cidrs": allowed_cidrs, "allowed_hostnames": allowed_hostnames, "description": description, }, @@ -473,6 +510,8 @@ async def update( allow_all: Optional[bool] | Omit = omit, allow_devbox_to_devbox: Optional[bool] | Omit = omit, allow_mcp_gateway: Optional[bool] | Omit = omit, + allow_runloop_mirrors: Optional[bool] | Omit = omit, + allowed_cidrs: Optional[Iterable[AllowedCidrParam]] | Omit = omit, allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, description: Optional[str] | Omit = omit, name: Optional[str] | Omit = omit, @@ -497,6 +536,12 @@ async def update( allow_mcp_gateway: If true, allows devbox egress to the MCP hub. + allow_runloop_mirrors: If true, allows devbox egress to Runloop's package/image registry mirrors. + Implicitly allowed when allow_all is true. + + allowed_cidrs: Updated IPv4 CIDR-based allow list with optional port restrictions, additive + with allowed_hostnames. + allowed_hostnames: Updated DNS-based allow list with wildcard support. Examples: ['github.com', '*.npmjs.org']. @@ -524,6 +569,8 @@ async def update( "allow_all": allow_all, "allow_devbox_to_devbox": allow_devbox_to_devbox, "allow_mcp_gateway": allow_mcp_gateway, + "allow_runloop_mirrors": allow_runloop_mirrors, + "allowed_cidrs": allowed_cidrs, "allowed_hostnames": allowed_hostnames, "description": description, "name": name, @@ -547,6 +594,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -568,6 +616,8 @@ def list( name: Filter by name (partial match supported). + search: Search by network policy ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -592,6 +642,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, network_policy_list_params.NetworkPolicyListParams, diff --git a/src/runloop_api_client/resources/scenarios/runs.py b/src/runloop_api_client/resources/scenarios/runs.py index 67c5c4428..cb2e15be0 100644 --- a/src/runloop_api_client/resources/scenarios/runs.py +++ b/src/runloop_api_client/resources/scenarios/runs.py @@ -94,6 +94,7 @@ def list( limit: int | Omit = omit, name: str | Omit = omit, scenario_id: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, state: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -118,6 +119,8 @@ def list( scenario_id: Filter runs associated to Scenario given ID + search: Search by scenario run ID or name. + starting_after: Load the next page of data starting after the item with the given ID. state: Filter by state @@ -145,6 +148,7 @@ def list( "limit": limit, "name": name, "scenario_id": scenario_id, + "search": search, "starting_after": starting_after, "state": state, }, @@ -519,6 +523,7 @@ def list( limit: int | Omit = omit, name: str | Omit = omit, scenario_id: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, state: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -543,6 +548,8 @@ def list( scenario_id: Filter runs associated to Scenario given ID + search: Search by scenario run ID or name. + starting_after: Load the next page of data starting after the item with the given ID. state: Filter by state @@ -570,6 +577,7 @@ def list( "limit": limit, "name": name, "scenario_id": scenario_id, + "search": search, "starting_after": starting_after, "state": state, }, diff --git a/src/runloop_api_client/resources/scenarios/scenarios.py b/src/runloop_api_client/resources/scenarios/scenarios.py index 084fab761..c29613894 100644 --- a/src/runloop_api_client/resources/scenarios/scenarios.py +++ b/src/runloop_api_client/resources/scenarios/scenarios.py @@ -303,6 +303,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, validation_type: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -325,6 +326,8 @@ def list( name: Query for Scenarios with a given name. + search: Search by scenario ID or name. + starting_after: Load the next page of data starting after the item with the given ID. validation_type: Filter by validation type @@ -351,6 +354,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, "validation_type": validation_type, }, @@ -408,6 +412,7 @@ def list_public( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -427,6 +432,8 @@ def list_public( name: Query for Scenarios with a given name. + search: Search by scenario ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -450,6 +457,7 @@ def list_public( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, scenario_list_public_params.ScenarioListPublicParams, @@ -827,6 +835,7 @@ def list( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, validation_type: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -849,6 +858,8 @@ def list( name: Query for Scenarios with a given name. + search: Search by scenario ID or name. + starting_after: Load the next page of data starting after the item with the given ID. validation_type: Filter by validation type @@ -875,6 +886,7 @@ def list( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, "validation_type": validation_type, }, @@ -932,6 +944,7 @@ def list_public( include_total_count: bool | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -951,6 +964,8 @@ def list_public( name: Query for Scenarios with a given name. + search: Search by scenario ID or name. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -974,6 +989,7 @@ def list_public( "include_total_count": include_total_count, "limit": limit, "name": name, + "search": search, "starting_after": starting_after, }, scenario_list_public_params.ScenarioListPublicParams, diff --git a/src/runloop_api_client/resources/scenarios/scorers.py b/src/runloop_api_client/resources/scenarios/scorers.py index 1472d86af..599949a17 100644 --- a/src/runloop_api_client/resources/scenarios/scorers.py +++ b/src/runloop_api_client/resources/scenarios/scorers.py @@ -188,6 +188,7 @@ def list( *, include_total_count: bool | Omit = omit, limit: int | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -205,6 +206,8 @@ def list( limit: The limit of items to return. Default is 20. Max is 5000. + search: Search by scenario scorer ID or type. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -227,6 +230,7 @@ def list( { "include_total_count": include_total_count, "limit": limit, + "search": search, "starting_after": starting_after, }, scorer_list_params.ScorerListParams, @@ -399,6 +403,7 @@ def list( *, include_total_count: bool | Omit = omit, limit: int | Omit = omit, + search: str | Omit = omit, starting_after: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -416,6 +421,8 @@ def list( limit: The limit of items to return. Default is 20. Max is 5000. + search: Search by scenario scorer ID or type. + starting_after: Load the next page of data starting after the item with the given ID. extra_headers: Send extra headers @@ -438,6 +445,7 @@ def list( { "include_total_count": include_total_count, "limit": limit, + "search": search, "starting_after": starting_after, }, scorer_list_params.ScorerListParams, diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py index 3f36e4cc2..4abd94f92 100644 --- a/src/runloop_api_client/types/__init__.py +++ b/src/runloop_api_client/types/__init__.py @@ -17,12 +17,14 @@ LifecycleConfiguration as LifecycleConfiguration, ) from .axon_view import AxonView as AxonView +from .port_rule import PortRule as PortRule from .agent_view import AgentView as AgentView from .devbox_view import DevboxView as DevboxView from .object_view import ObjectView as ObjectView from .secret_view import SecretView as SecretView from .tunnel_view import TunnelView as TunnelView from .account_view import AccountView as AccountView +from .allowed_cidr import AllowedCidr as AllowedCidr from .input_context import InputContext as InputContext from .scenario_view import ScenarioView as ScenarioView from .axon_list_view import AxonListView as AxonListView @@ -31,6 +33,7 @@ from .agent_list_view import AgentListView as AgentListView from .axon_event_view import AxonEventView as AxonEventView from .mcp_config_view import McpConfigView as McpConfigView +from .port_rule_param import PortRuleParam as PortRuleParam from .pty_tunnel_view import PtyTunnelView as PtyTunnelView from .axon_list_params import AxonListParams as AxonListParams from .devbox_list_view import DevboxListView as DevboxListView @@ -42,6 +45,7 @@ from .secret_list_view import SecretListView as SecretListView from .agent_list_params import AgentListParams as AgentListParams from .scenario_run_view import ScenarioRunView as ScenarioRunView +from .allowed_cidr_param import AllowedCidrParam as AllowedCidrParam from .axon_create_params import AxonCreateParams as AxonCreateParams from .benchmark_job_view import BenchmarkJobView as BenchmarkJobView from .benchmark_run_view import BenchmarkRunView as BenchmarkRunView diff --git a/src/runloop_api_client/types/allowed_cidr.py b/src/runloop_api_client/types/allowed_cidr.py new file mode 100644 index 000000000..8688f052d --- /dev/null +++ b/src/runloop_api_client/types/allowed_cidr.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .port_rule import PortRule + +__all__ = ["AllowedCidr"] + + +class AllowedCidr(BaseModel): + """A CIDR-based egress allow rule with optional port restrictions.""" + + cidr: str + """IPv4 CIDR block in canonical form (host bits zero), e.g. '10.12.0.0/16'.""" + + ports: Optional[List[PortRule]] = None + """(Optional) Ports allowed for this CIDR. + + Empty or omitted means all ports and protocols. + """ diff --git a/src/runloop_api_client/types/allowed_cidr_param.py b/src/runloop_api_client/types/allowed_cidr_param.py new file mode 100644 index 000000000..fa37c10cd --- /dev/null +++ b/src/runloop_api_client/types/allowed_cidr_param.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Required, TypedDict + +from .port_rule_param import PortRuleParam + +__all__ = ["AllowedCidrParam"] + + +class AllowedCidrParam(TypedDict, total=False): + """A CIDR-based egress allow rule with optional port restrictions.""" + + cidr: Required[str] + """IPv4 CIDR block in canonical form (host bits zero), e.g. '10.12.0.0/16'.""" + + ports: Optional[Iterable[PortRuleParam]] + """(Optional) Ports allowed for this CIDR. + + Empty or omitted means all ports and protocols. + """ diff --git a/src/runloop_api_client/types/axon_list_params.py b/src/runloop_api_client/types/axon_list_params.py index dbe4cdb80..7ae12827c 100644 --- a/src/runloop_api_client/types/axon_list_params.py +++ b/src/runloop_api_client/types/axon_list_params.py @@ -23,5 +23,8 @@ class AxonListParams(TypedDict, total=False): name: str """Filter by axon name (prefix match supported).""" + search: str + """Search by axon ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_job_list_params.py b/src/runloop_api_client/types/benchmark_job_list_params.py index 9ab17e8eb..fd68abd44 100644 --- a/src/runloop_api_client/types/benchmark_job_list_params.py +++ b/src/runloop_api_client/types/benchmark_job_list_params.py @@ -20,5 +20,8 @@ class BenchmarkJobListParams(TypedDict, total=False): name: str """Filter by name""" + search: str + """Search by benchmark job ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_list_params.py b/src/runloop_api_client/types/benchmark_list_params.py index d8be9aeca..650c93ca3 100644 --- a/src/runloop_api_client/types/benchmark_list_params.py +++ b/src/runloop_api_client/types/benchmark_list_params.py @@ -20,5 +20,8 @@ class BenchmarkListParams(TypedDict, total=False): name: str """Filter by name""" + search: str + """Search by benchmark ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_list_public_params.py b/src/runloop_api_client/types/benchmark_list_public_params.py index 9be7ffdf4..de7426686 100644 --- a/src/runloop_api_client/types/benchmark_list_public_params.py +++ b/src/runloop_api_client/types/benchmark_list_public_params.py @@ -17,5 +17,11 @@ class BenchmarkListPublicParams(TypedDict, total=False): limit: int """The limit of items to return. Default is 20. Max is 5000.""" + name: str + """Filter by name""" + + search: str + """Search by benchmark ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_run_list_params.py b/src/runloop_api_client/types/benchmark_run_list_params.py index 73106e240..697d9db1f 100644 --- a/src/runloop_api_client/types/benchmark_run_list_params.py +++ b/src/runloop_api_client/types/benchmark_run_list_params.py @@ -23,6 +23,9 @@ class BenchmarkRunListParams(TypedDict, total=False): name: str """Filter by name""" + search: str + """Search by benchmark run ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py b/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py index dddfa256a..ac626e6a8 100644 --- a/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py +++ b/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py @@ -17,6 +17,9 @@ class BenchmarkRunListScenarioRunsParams(TypedDict, total=False): limit: int """The limit of items to return. Default is 20. Max is 5000.""" + search: str + """Search by scenario run ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/devbox_list_params.py b/src/runloop_api_client/types/devbox_list_params.py index f845a53b6..e2ad60f94 100644 --- a/src/runloop_api_client/types/devbox_list_params.py +++ b/src/runloop_api_client/types/devbox_list_params.py @@ -22,6 +22,7 @@ class DevboxListParams(TypedDict, total=False): status: Literal[ "scheduled", + "queued", "provisioning", "initializing", "running", diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py index 726f14e72..49ffc1ec8 100644 --- a/src/runloop_api_client/types/devbox_view.py +++ b/src/runloop_api_client/types/devbox_view.py @@ -14,6 +14,7 @@ class StateTransition(BaseModel): status: Optional[ Literal[ "scheduled", + "queued", "provisioning", "initializing", "running", @@ -26,8 +27,9 @@ class StateTransition(BaseModel): ] = None """The status of the Devbox. - scheduled: The Devbox is scheduled to run but infrastructure allocation has not - started yet. provisioning: Runloop is allocating and booting the necessary + scheduled: Deprecated. The Devbox is waiting for infrastructure allocation to + start. Use queued. queued: The Devbox is waiting for infrastructure allocation + to start. provisioning: Runloop is allocating and booting the necessary infrastructure resources. initializing: Runloop defined boot scripts are running to enable the environment for interaction. running: The Devbox is ready for interaction. suspending: The Devbox disk is being snapshotted as part of @@ -90,6 +92,7 @@ class DevboxView(BaseModel): status: Literal[ "scheduled", + "queued", "provisioning", "initializing", "running", diff --git a/src/runloop_api_client/types/gateway_config_list_params.py b/src/runloop_api_client/types/gateway_config_list_params.py index ef69f83b4..58c431c75 100644 --- a/src/runloop_api_client/types/gateway_config_list_params.py +++ b/src/runloop_api_client/types/gateway_config_list_params.py @@ -23,5 +23,8 @@ class GatewayConfigListParams(TypedDict, total=False): name: str """Filter by name (partial match supported).""" + search: str + """Search by gateway config ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/mcp_config_list_params.py b/src/runloop_api_client/types/mcp_config_list_params.py index 684de89ec..782c1b4ca 100644 --- a/src/runloop_api_client/types/mcp_config_list_params.py +++ b/src/runloop_api_client/types/mcp_config_list_params.py @@ -23,5 +23,8 @@ class McpConfigListParams(TypedDict, total=False): name: str """Filter by name (prefix match supported).""" + search: str + """Search by MCP config ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/network_policy_create_params.py b/src/runloop_api_client/types/network_policy_create_params.py index 0d8b1de2a..48b49f456 100644 --- a/src/runloop_api_client/types/network_policy_create_params.py +++ b/src/runloop_api_client/types/network_policy_create_params.py @@ -2,10 +2,11 @@ from __future__ import annotations -from typing import Optional +from typing import Iterable, Optional from typing_extensions import Required, TypedDict from .._types import SequenceNotStr +from .allowed_cidr_param import AllowedCidrParam __all__ = ["NetworkPolicyCreateParams"] @@ -42,6 +43,19 @@ class NetworkPolicyCreateParams(TypedDict, total=False): Defaults to false. """ + allow_runloop_mirrors: Optional[bool] + """ + (Optional) If true, allows devbox egress to Runloop's package/image registry + mirrors. Defaults to false. Implicitly allowed when allow_all is true. + """ + + allowed_cidrs: Optional[Iterable[AllowedCidrParam]] + """ + (Optional) IPv4 CIDR-based allow list with optional port restrictions, additive + with allowed_hostnames. Example: [{'cidr': '10.12.0.0/16', 'ports': [{'port': + 443}]}]. + """ + allowed_hostnames: Optional[SequenceNotStr[str]] """(Optional) DNS-based allow list with wildcard support. diff --git a/src/runloop_api_client/types/network_policy_list_params.py b/src/runloop_api_client/types/network_policy_list_params.py index cdbc84f1a..ee8643b5c 100644 --- a/src/runloop_api_client/types/network_policy_list_params.py +++ b/src/runloop_api_client/types/network_policy_list_params.py @@ -23,5 +23,8 @@ class NetworkPolicyListParams(TypedDict, total=False): name: str """Filter by name (partial match supported).""" + search: str + """Search by network policy ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/network_policy_update_params.py b/src/runloop_api_client/types/network_policy_update_params.py index 0a622ca3e..0bfca295f 100644 --- a/src/runloop_api_client/types/network_policy_update_params.py +++ b/src/runloop_api_client/types/network_policy_update_params.py @@ -2,10 +2,11 @@ from __future__ import annotations -from typing import Optional +from typing import Iterable, Optional from typing_extensions import TypedDict from .._types import SequenceNotStr +from .allowed_cidr_param import AllowedCidrParam __all__ = ["NetworkPolicyUpdateParams"] @@ -23,6 +24,18 @@ class NetworkPolicyUpdateParams(TypedDict, total=False): allow_mcp_gateway: Optional[bool] """If true, allows devbox egress to the MCP hub.""" + allow_runloop_mirrors: Optional[bool] + """If true, allows devbox egress to Runloop's package/image registry mirrors. + + Implicitly allowed when allow_all is true. + """ + + allowed_cidrs: Optional[Iterable[AllowedCidrParam]] + """ + Updated IPv4 CIDR-based allow list with optional port restrictions, additive + with allowed_hostnames. + """ + allowed_hostnames: Optional[SequenceNotStr[str]] """Updated DNS-based allow list with wildcard support. diff --git a/src/runloop_api_client/types/network_policy_view.py b/src/runloop_api_client/types/network_policy_view.py index 0c3e25728..aa6395e5a 100644 --- a/src/runloop_api_client/types/network_policy_view.py +++ b/src/runloop_api_client/types/network_policy_view.py @@ -3,6 +3,7 @@ from typing import List, Optional from .._models import BaseModel +from .allowed_cidr import AllowedCidr __all__ = ["NetworkPolicyView", "Egress"] @@ -25,6 +26,18 @@ class Egress(BaseModel): allow_mcp_gateway: bool """If true, allows devbox egress to the MCP hub for MCP server access.""" + allow_runloop_mirrors: bool + """If true, allows devbox egress to Runloop's package/image registry mirrors. + + Implicitly allowed when allow_all is true. + """ + + allowed_cidrs: List[AllowedCidr] + """ + CIDR-based allow list with optional port restrictions, additive with + allowed_hostnames. + """ + allowed_hostnames: List[str] """DNS-based allow list with wildcard support. diff --git a/src/runloop_api_client/types/port_rule.py b/src/runloop_api_client/types/port_rule.py new file mode 100644 index 000000000..8b9737161 --- /dev/null +++ b/src/runloop_api_client/types/port_rule.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["PortRule"] + + +class PortRule(BaseModel): + """A port or port range allowed for a CIDR egress rule.""" + + port: int + """The allowed port (1-65535), or the start of a port range.""" + + end_port: Optional[int] = None + """(Optional) Inclusive end of the port range (port-65535). + + Omit for a single port. + """ + + protocol: Optional[Literal["TCP", "UDP"]] = None + """L4 protocol for a port rule.""" diff --git a/src/runloop_api_client/types/port_rule_param.py b/src/runloop_api_client/types/port_rule_param.py new file mode 100644 index 000000000..1c7be58a6 --- /dev/null +++ b/src/runloop_api_client/types/port_rule_param.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["PortRuleParam"] + + +class PortRuleParam(TypedDict, total=False): + """A port or port range allowed for a CIDR egress rule.""" + + port: Required[int] + """The allowed port (1-65535), or the start of a port range.""" + + end_port: Optional[int] + """(Optional) Inclusive end of the port range (port-65535). + + Omit for a single port. + """ + + protocol: Optional[Literal["TCP", "UDP"]] + """L4 protocol for a port rule.""" diff --git a/src/runloop_api_client/types/scenario_list_params.py b/src/runloop_api_client/types/scenario_list_params.py index 3d34d711a..cdc21050e 100644 --- a/src/runloop_api_client/types/scenario_list_params.py +++ b/src/runloop_api_client/types/scenario_list_params.py @@ -23,6 +23,9 @@ class ScenarioListParams(TypedDict, total=False): name: str """Query for Scenarios with a given name.""" + search: str + """Search by scenario ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/scenario_list_public_params.py b/src/runloop_api_client/types/scenario_list_public_params.py index a2e71da64..a1714d8a5 100644 --- a/src/runloop_api_client/types/scenario_list_public_params.py +++ b/src/runloop_api_client/types/scenario_list_public_params.py @@ -20,5 +20,8 @@ class ScenarioListPublicParams(TypedDict, total=False): name: str """Query for Scenarios with a given name.""" + search: str + """Search by scenario ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/scenarios/run_list_params.py b/src/runloop_api_client/types/scenarios/run_list_params.py index 6e7373a07..d2de9e951 100644 --- a/src/runloop_api_client/types/scenarios/run_list_params.py +++ b/src/runloop_api_client/types/scenarios/run_list_params.py @@ -26,6 +26,9 @@ class RunListParams(TypedDict, total=False): scenario_id: str """Filter runs associated to Scenario given ID""" + search: str + """Search by scenario run ID or name.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_list_params.py b/src/runloop_api_client/types/scenarios/scorer_list_params.py index a387f1a86..4ae433725 100644 --- a/src/runloop_api_client/types/scenarios/scorer_list_params.py +++ b/src/runloop_api_client/types/scenarios/scorer_list_params.py @@ -17,5 +17,8 @@ class ScorerListParams(TypedDict, total=False): limit: int """The limit of items to return. Default is 20. Max is 5000.""" + search: str + """Search by scenario scorer ID or type.""" + starting_after: str """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/shared/broker_mount.py b/src/runloop_api_client/types/shared/broker_mount.py index ed518c632..adf49c04a 100644 --- a/src/runloop_api_client/types/shared/broker_mount.py +++ b/src/runloop_api_client/types/shared/broker_mount.py @@ -17,16 +17,16 @@ class BrokerMount(BaseModel): agent_binary: Optional[str] = None """Binary to launch the agent (e.g., 'opencode'). - Used by protocols that launch a subprocess (acp, claude_json). + Used by protocols that launch a subprocess (acp, claude_json, codex_json). """ launch_args: Optional[List[str]] = None """Arguments to pass to the agent command (e.g., ['acp']). - Used by protocols that launch a subprocess (acp, claude_json). + Used by protocols that launch a subprocess (acp, claude_json, codex_json). """ - protocol: Optional[Literal["acp", "claude_json"]] = None + protocol: Optional[Literal["acp", "claude_json", "codex_json"]] = None """The protocol used by the broker to deliver events to the agent.""" working_directory: Optional[str] = None diff --git a/src/runloop_api_client/types/shared_params/broker_mount.py b/src/runloop_api_client/types/shared_params/broker_mount.py index 233ae0095..e62bd59e5 100644 --- a/src/runloop_api_client/types/shared_params/broker_mount.py +++ b/src/runloop_api_client/types/shared_params/broker_mount.py @@ -19,16 +19,16 @@ class BrokerMount(TypedDict, total=False): agent_binary: Optional[str] """Binary to launch the agent (e.g., 'opencode'). - Used by protocols that launch a subprocess (acp, claude_json). + Used by protocols that launch a subprocess (acp, claude_json, codex_json). """ launch_args: Optional[SequenceNotStr[str]] """Arguments to pass to the agent command (e.g., ['acp']). - Used by protocols that launch a subprocess (acp, claude_json). + Used by protocols that launch a subprocess (acp, claude_json, codex_json). """ - protocol: Optional[Literal["acp", "claude_json"]] + protocol: Optional[Literal["acp", "claude_json", "codex_json"]] """The protocol used by the broker to deliver events to the agent.""" working_directory: Optional[str] diff --git a/tests/api_resources/scenarios/test_runs.py b/tests/api_resources/scenarios/test_runs.py index 429a815ee..ec3871b27 100644 --- a/tests/api_resources/scenarios/test_runs.py +++ b/tests/api_resources/scenarios/test_runs.py @@ -77,6 +77,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: limit=0, name="name", scenario_id="scenario_id", + search="search", starting_after="starting_after", state="state", ) @@ -329,6 +330,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> limit=0, name="name", scenario_id="scenario_id", + search="search", starting_after="starting_after", state="state", ) diff --git a/tests/api_resources/scenarios/test_scorers.py b/tests/api_resources/scenarios/test_scorers.py index cd15e860d..c7cbbe555 100644 --- a/tests/api_resources/scenarios/test_scorers.py +++ b/tests/api_resources/scenarios/test_scorers.py @@ -151,6 +151,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: scorer = client.scenarios.scorers.list( include_total_count=True, limit=0, + search="search", starting_after="starting_after", ) assert_matches_type(SyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) @@ -309,6 +310,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> scorer = await async_client.scenarios.scorers.list( include_total_count=True, limit=0, + search="search", starting_after="starting_after", ) assert_matches_type(AsyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) diff --git a/tests/api_resources/test_axons.py b/tests/api_resources/test_axons.py index 540fc4baa..8764443bb 100644 --- a/tests/api_resources/test_axons.py +++ b/tests/api_resources/test_axons.py @@ -103,6 +103,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncAxonsCursorIDPage[AxonView], axon, path=["response"]) @@ -314,6 +315,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncAxonsCursorIDPage[AxonView], axon, path=["response"]) diff --git a/tests/api_resources/test_benchmark_jobs.py b/tests/api_resources/test_benchmark_jobs.py index 9b577a0c2..e109ad8dc 100644 --- a/tests/api_resources/test_benchmark_jobs.py +++ b/tests/api_resources/test_benchmark_jobs.py @@ -105,6 +105,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) @@ -220,6 +221,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) diff --git a/tests/api_resources/test_benchmark_runs.py b/tests/api_resources/test_benchmark_runs.py index 68e0df609..3ce64b8eb 100644 --- a/tests/api_resources/test_benchmark_runs.py +++ b/tests/api_resources/test_benchmark_runs.py @@ -71,6 +71,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", state="state", ) @@ -185,6 +186,7 @@ def test_method_list_scenario_runs_with_all_params(self, client: Runloop) -> Non id="id", include_total_count=True, limit=0, + search="search", starting_after="starting_after", state="running", ) @@ -277,6 +279,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", state="state", ) @@ -391,6 +394,7 @@ async def test_method_list_scenario_runs_with_all_params(self, async_client: Asy id="id", include_total_count=True, limit=0, + search="search", starting_after="starting_after", state="running", ) diff --git a/tests/api_resources/test_benchmarks.py b/tests/api_resources/test_benchmarks.py index 4728cbce5..f6de5f2af 100644 --- a/tests/api_resources/test_benchmarks.py +++ b/tests/api_resources/test_benchmarks.py @@ -167,6 +167,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) @@ -248,6 +249,8 @@ def test_method_list_public_with_all_params(self, client: Runloop) -> None: benchmark = client.benchmarks.list_public( include_total_count=True, limit=0, + name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) @@ -557,6 +560,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) @@ -638,6 +642,8 @@ async def test_method_list_public_with_all_params(self, async_client: AsyncRunlo benchmark = await async_client.benchmarks.list_public( include_total_count=True, limit=0, + name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) diff --git a/tests/api_resources/test_gateway_configs.py b/tests/api_resources/test_gateway_configs.py index 5a60422dd..64e35b11f 100644 --- a/tests/api_resources/test_gateway_configs.py +++ b/tests/api_resources/test_gateway_configs.py @@ -172,6 +172,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) @@ -392,6 +393,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) diff --git a/tests/api_resources/test_mcp_configs.py b/tests/api_resources/test_mcp_configs.py index ea6ed337c..2e85d3d45 100644 --- a/tests/api_resources/test_mcp_configs.py +++ b/tests/api_resources/test_mcp_configs.py @@ -166,6 +166,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) @@ -380,6 +381,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) diff --git a/tests/api_resources/test_network_policies.py b/tests/api_resources/test_network_policies.py index e7b5fb038..fb70f4069 100644 --- a/tests/api_resources/test_network_policies.py +++ b/tests/api_resources/test_network_policies.py @@ -35,6 +35,19 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: allow_all=True, allow_devbox_to_devbox=True, allow_mcp_gateway=True, + allow_runloop_mirrors=True, + allowed_cidrs=[ + { + "cidr": "cidr", + "ports": [ + { + "port": 0, + "end_port": 0, + "protocol": "TCP", + } + ], + } + ], allowed_hostnames=["string"], description="description", ) @@ -117,6 +130,19 @@ def test_method_update_with_all_params(self, client: Runloop) -> None: allow_all=True, allow_devbox_to_devbox=True, allow_mcp_gateway=True, + allow_runloop_mirrors=True, + allowed_cidrs=[ + { + "cidr": "cidr", + "ports": [ + { + "port": 0, + "end_port": 0, + "protocol": "TCP", + } + ], + } + ], allowed_hostnames=["string"], description="description", name="name", @@ -166,6 +192,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) @@ -249,6 +276,19 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - allow_all=True, allow_devbox_to_devbox=True, allow_mcp_gateway=True, + allow_runloop_mirrors=True, + allowed_cidrs=[ + { + "cidr": "cidr", + "ports": [ + { + "port": 0, + "end_port": 0, + "protocol": "TCP", + } + ], + } + ], allowed_hostnames=["string"], description="description", ) @@ -331,6 +371,19 @@ async def test_method_update_with_all_params(self, async_client: AsyncRunloop) - allow_all=True, allow_devbox_to_devbox=True, allow_mcp_gateway=True, + allow_runloop_mirrors=True, + allowed_cidrs=[ + { + "cidr": "cidr", + "ports": [ + { + "port": 0, + "end_port": 0, + "protocol": "TCP", + } + ], + } + ], allowed_hostnames=["string"], description="description", name="name", @@ -380,6 +433,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) diff --git a/tests/api_resources/test_scenarios.py b/tests/api_resources/test_scenarios.py index 157f6877a..59ab6c634 100644 --- a/tests/api_resources/test_scenarios.py +++ b/tests/api_resources/test_scenarios.py @@ -325,6 +325,7 @@ def test_method_list_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", validation_type="validation_type", ) @@ -399,6 +400,7 @@ def test_method_list_public_with_all_params(self, client: Runloop) -> None: include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) @@ -821,6 +823,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", validation_type="validation_type", ) @@ -895,6 +898,7 @@ async def test_method_list_public_with_all_params(self, async_client: AsyncRunlo include_total_count=True, limit=0, name="name", + search="search", starting_after="starting_after", ) assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) diff --git a/uv.lock b/uv.lock index a35165b2c..8e67d0307 100644 --- a/uv.lock +++ b/uv.lock @@ -2422,7 +2422,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "1.20.2" +version = "1.23.3" source = { editable = "." } dependencies = [ { name = "anyio" },