diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3e9af1b3a..fbd9082d7 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.4.0"
+ ".": "1.5.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index e2f90d495..26ecaabc0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 111
+configured_endpoints: 112
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-84e997ca5716b9378a58a1bdf3d6616cf3be80156a6aaed1bed469fe93ba2c95.yml
openapi_spec_hash: b44a4ba1c2c3cb775c14545f2bab05a8
-config_hash: 22f65246be4646c23dde9f69f51252e7
+config_hash: 6c26299fd9ef01fb4713612a9a2ad17c
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6adca673a..eacbba506 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 1.5.0 (2026-01-30)
+
+Full Changelog: [v1.4.0...v1.5.0](https://github.com/runloopai/api-client-python/compare/v1.4.0...v1.5.0)
+
+### Features
+
+* **devbox:** add enable_tunnel API ([#7236](https://github.com/runloopai/api-client-python/issues/7236)) ([bb58bfc](https://github.com/runloopai/api-client-python/commit/bb58bfc40c93c5832634ddb82c255ada7214c7f4))
+
## 1.4.0 (2026-01-30)
Full Changelog: [v1.3.2...v1.4.0](https://github.com/runloopai/api-client-python/compare/v1.3.2...v1.4.0)
diff --git a/api.md b/api.md
index 44ba928b2..2720824ad 100644
--- a/api.md
+++ b/api.md
@@ -132,6 +132,7 @@ from runloop_api_client.types import (
DevboxSnapshotView,
DevboxTunnelView,
DevboxView,
+ TunnelView,
DevboxCreateSSHKeyResponse,
DevboxReadFileContentsResponse,
)
@@ -148,6 +149,7 @@ Methods:
- client.devboxes.create_tunnel(id, \*\*params) -> DevboxTunnelView
- client.devboxes.delete_disk_snapshot(id) -> object
- client.devboxes.download_file(id, \*\*params) -> BinaryAPIResponse
+- client.devboxes.enable_tunnel(id, \*\*params) -> TunnelView
- client.devboxes.execute(id, \*\*params) -> DevboxAsyncExecutionDetailView
- client.devboxes.execute_async(id, \*\*params) -> DevboxAsyncExecutionDetailView
- client.devboxes.execute_sync(id, \*\*params) -> DevboxExecutionDetailView
diff --git a/pyproject.toml b/pyproject.toml
index 2879cccda..2b4148401 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
-version = "1.4.0"
+version = "1.5.0"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py
index 0d6b7e275..cfd147a9e 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.4.0" # x-release-please-version
+__version__ = "1.5.0" # x-release-please-version
diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py
index ea563fa59..524e0cc74 100644
--- a/src/runloop_api_client/resources/devboxes/devboxes.py
+++ b/src/runloop_api_client/resources/devboxes/devboxes.py
@@ -28,6 +28,7 @@
devbox_execute_sync_params,
devbox_create_tunnel_params,
devbox_download_file_params,
+ devbox_enable_tunnel_params,
devbox_execute_async_params,
devbox_remove_tunnel_params,
devbox_snapshot_disk_params,
@@ -99,6 +100,7 @@
)
from ...lib.polling_async import async_poll_until
from ...types.devbox_view import DevboxView
+from ...types.tunnel_view import TunnelView
from ...types.devbox_tunnel_view import DevboxTunnelView
from ...types.shared_params.mount import Mount
from ...types.devbox_snapshot_view import DevboxSnapshotView
@@ -787,6 +789,55 @@ def download_file(
cast_to=BinaryAPIResponse,
)
+ def enable_tunnel(
+ self,
+ id: str,
+ *,
+ auth_mode: Optional[Literal["open", "authenticated"]] | 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.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> TunnelView:
+ """Create a V2 tunnel for an existing running Devbox.
+
+ Tunnels provide encrypted
+ URL-based access to the Devbox without exposing internal IDs. The tunnel URL
+ format is: https://{port}-{tunnel_key}.tunnel.runloop.ai
+
+ Each Devbox can have one tunnel.
+
+ Args:
+ auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ f"/v1/devboxes/{id}/enable_tunnel",
+ body=maybe_transform({"auth_mode": auth_mode}, devbox_enable_tunnel_params.DevboxEnableTunnelParams),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=TunnelView,
+ )
+
def execute(
self,
id: str,
@@ -2347,6 +2398,57 @@ async def download_file(
cast_to=AsyncBinaryAPIResponse,
)
+ async def enable_tunnel(
+ self,
+ id: str,
+ *,
+ auth_mode: Optional[Literal["open", "authenticated"]] | 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.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> TunnelView:
+ """Create a V2 tunnel for an existing running Devbox.
+
+ Tunnels provide encrypted
+ URL-based access to the Devbox without exposing internal IDs. The tunnel URL
+ format is: https://{port}-{tunnel_key}.tunnel.runloop.ai
+
+ Each Devbox can have one tunnel.
+
+ Args:
+ auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ f"/v1/devboxes/{id}/enable_tunnel",
+ body=await async_maybe_transform(
+ {"auth_mode": auth_mode}, devbox_enable_tunnel_params.DevboxEnableTunnelParams
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=TunnelView,
+ )
+
async def execute(
self,
id: str,
@@ -3293,6 +3395,9 @@ def __init__(self, devboxes: DevboxesResource) -> None:
devboxes.download_file,
BinaryAPIResponse,
)
+ self.enable_tunnel = to_raw_response_wrapper(
+ devboxes.enable_tunnel,
+ )
self.execute = to_raw_response_wrapper(
devboxes.execute,
)
@@ -3395,6 +3500,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
devboxes.download_file,
AsyncBinaryAPIResponse,
)
+ self.enable_tunnel = async_to_raw_response_wrapper(
+ devboxes.enable_tunnel,
+ )
self.execute = async_to_raw_response_wrapper(
devboxes.execute,
)
@@ -3497,6 +3605,9 @@ def __init__(self, devboxes: DevboxesResource) -> None:
devboxes.download_file,
StreamedBinaryAPIResponse,
)
+ self.enable_tunnel = to_streamed_response_wrapper(
+ devboxes.enable_tunnel,
+ )
self.execute = to_streamed_response_wrapper(
devboxes.execute,
)
@@ -3599,6 +3710,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
devboxes.download_file,
AsyncStreamedBinaryAPIResponse,
)
+ self.enable_tunnel = async_to_streamed_response_wrapper(
+ devboxes.enable_tunnel,
+ )
self.execute = async_to_streamed_response_wrapper(
devboxes.execute,
)
diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py
index be02c8312..8fb72317f 100644
--- a/src/runloop_api_client/types/__init__.py
+++ b/src/runloop_api_client/types/__init__.py
@@ -16,6 +16,7 @@
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 .input_context import InputContext as InputContext
from .scenario_view import ScenarioView as ScenarioView
from .benchmark_view import BenchmarkView as BenchmarkView
@@ -91,6 +92,7 @@
from .benchmark_job_create_params import BenchmarkJobCreateParams as BenchmarkJobCreateParams
from .devbox_create_tunnel_params import DevboxCreateTunnelParams as DevboxCreateTunnelParams
from .devbox_download_file_params import DevboxDownloadFileParams as DevboxDownloadFileParams
+from .devbox_enable_tunnel_params import DevboxEnableTunnelParams as DevboxEnableTunnelParams
from .devbox_execute_async_params import DevboxExecuteAsyncParams as DevboxExecuteAsyncParams
from .devbox_remove_tunnel_params import DevboxRemoveTunnelParams as DevboxRemoveTunnelParams
from .devbox_snapshot_disk_params import DevboxSnapshotDiskParams as DevboxSnapshotDiskParams
diff --git a/src/runloop_api_client/types/devbox_enable_tunnel_params.py b/src/runloop_api_client/types/devbox_enable_tunnel_params.py
new file mode 100644
index 000000000..9ccc53e75
--- /dev/null
+++ b/src/runloop_api_client/types/devbox_enable_tunnel_params.py
@@ -0,0 +1,13 @@
+# 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, TypedDict
+
+__all__ = ["DevboxEnableTunnelParams"]
+
+
+class DevboxEnableTunnelParams(TypedDict, total=False):
+ auth_mode: Optional[Literal["open", "authenticated"]]
+ """Authentication mode for the tunnel. Defaults to 'public' if not specified."""
diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py
index 50422db1f..68a96dbd7 100644
--- a/src/runloop_api_client/types/devbox_view.py
+++ b/src/runloop_api_client/types/devbox_view.py
@@ -4,9 +4,10 @@
from typing_extensions import Literal
from .._models import BaseModel
+from .tunnel_view import TunnelView
from .shared.launch_parameters import LaunchParameters
-__all__ = ["DevboxView", "StateTransition", "GatewaySpecs", "Tunnel"]
+__all__ = ["DevboxView", "StateTransition", "GatewaySpecs"]
class StateTransition(BaseModel):
@@ -38,30 +39,6 @@ class GatewaySpecs(BaseModel):
"""The ID of the secret containing the credential."""
-class Tunnel(BaseModel):
- """
- V2 tunnel information if a tunnel was created at launch time or via the createTunnel API.
- """
-
- auth_mode: Literal["public_", "authenticated"]
- """The authentication mode for the tunnel."""
-
- create_time_ms: int
- """Creation time of the tunnel (Unix timestamp milliseconds)."""
-
- tunnel_key: str
- """The encrypted tunnel key used to construct the tunnel URL.
-
- URL format: https://{port}-{tunnel_key}.tunnel.runloop.{domain}
- """
-
- auth_token: Optional[str] = None
- """Bearer token for tunnel authentication.
-
- Only present when auth_mode is 'authenticated'.
- """
-
-
class DevboxView(BaseModel):
"""A Devbox represents a virtual development environment.
@@ -137,7 +114,7 @@ class DevboxView(BaseModel):
Snapshot.
"""
- tunnel: Optional[Tunnel] = None
+ tunnel: Optional[TunnelView] = None
"""
V2 tunnel information if a tunnel was created at launch time or via the
createTunnel API.
diff --git a/src/runloop_api_client/types/tunnel_view.py b/src/runloop_api_client/types/tunnel_view.py
new file mode 100644
index 000000000..beb43daf9
--- /dev/null
+++ b/src/runloop_api_client/types/tunnel_view.py
@@ -0,0 +1,34 @@
+# 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__ = ["TunnelView"]
+
+
+class TunnelView(BaseModel):
+ """A V2 tunnel provides secure HTTP access to services running on a Devbox.
+
+ Tunnels allow external clients to reach web servers, APIs, or other HTTP services running inside a Devbox without requiring direct network access. Each tunnel is uniquely identified by an encrypted tunnel_key and can be configured for either open (public) or authenticated access.
+ Usage: https://{port}-{tunnel_key}.tunnel.runloop.ai
+ """
+
+ auth_mode: Literal["public_", "authenticated"]
+ """The authentication mode for the tunnel."""
+
+ create_time_ms: int
+ """Creation time of the tunnel (Unix timestamp milliseconds)."""
+
+ tunnel_key: str
+ """The encrypted tunnel key used to construct the tunnel URL.
+
+ URL format: https://{port}-{tunnel_key}.tunnel.runloop.{domain}
+ """
+
+ auth_token: Optional[str] = None
+ """Bearer token for tunnel authentication.
+
+ Only present when auth_mode is 'authenticated'.
+ """
diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py
index 8af777181..fcfc5c1de 100644
--- a/tests/api_resources/test_devboxes.py
+++ b/tests/api_resources/test_devboxes.py
@@ -14,6 +14,7 @@
from runloop_api_client import Runloop, AsyncRunloop
from runloop_api_client.types import (
DevboxView,
+ TunnelView,
DevboxTunnelView,
DevboxSnapshotView,
DevboxExecutionDetailView,
@@ -427,6 +428,52 @@ def test_path_params_download_file(self, client: Runloop) -> None:
path="path",
)
+ @parametrize
+ def test_method_enable_tunnel(self, client: Runloop) -> None:
+ devbox = client.devboxes.enable_tunnel(
+ id="id",
+ )
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ @parametrize
+ def test_method_enable_tunnel_with_all_params(self, client: Runloop) -> None:
+ devbox = client.devboxes.enable_tunnel(
+ id="id",
+ auth_mode="open",
+ )
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ @parametrize
+ def test_raw_response_enable_tunnel(self, client: Runloop) -> None:
+ response = client.devboxes.with_raw_response.enable_tunnel(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ devbox = response.parse()
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ @parametrize
+ def test_streaming_response_enable_tunnel(self, client: Runloop) -> None:
+ with client.devboxes.with_streaming_response.enable_tunnel(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ devbox = response.parse()
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_enable_tunnel(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.devboxes.with_raw_response.enable_tunnel(
+ id="",
+ )
+
@parametrize
def test_method_execute(self, client: Runloop) -> None:
devbox = client.devboxes.execute(
@@ -1993,6 +2040,52 @@ async def test_path_params_download_file(self, async_client: AsyncRunloop) -> No
path="path",
)
+ @parametrize
+ async def test_method_enable_tunnel(self, async_client: AsyncRunloop) -> None:
+ devbox = await async_client.devboxes.enable_tunnel(
+ id="id",
+ )
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ @parametrize
+ async def test_method_enable_tunnel_with_all_params(self, async_client: AsyncRunloop) -> None:
+ devbox = await async_client.devboxes.enable_tunnel(
+ id="id",
+ auth_mode="open",
+ )
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ @parametrize
+ async def test_raw_response_enable_tunnel(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.devboxes.with_raw_response.enable_tunnel(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ devbox = await response.parse()
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_enable_tunnel(self, async_client: AsyncRunloop) -> None:
+ async with async_client.devboxes.with_streaming_response.enable_tunnel(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ devbox = await response.parse()
+ assert_matches_type(TunnelView, devbox, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_enable_tunnel(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.devboxes.with_raw_response.enable_tunnel(
+ id="",
+ )
+
@parametrize
async def test_method_execute(self, async_client: AsyncRunloop) -> None:
devbox = await async_client.devboxes.execute(