Skip to content

Commit b7763ba

Browse files
feat(mux): mint agent gateway tokens for running devboxes (#10872)
1 parent a02b75f commit b7763ba

7 files changed

Lines changed: 283 additions & 4 deletions

File tree

.stats.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
configured_endpoints: 122
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-e5f0e0681128eb37d05a98356a9d0df494ac490f717abc9911ea8a637be6b8e9.yml
3-
openapi_spec_hash: d95f699402c754e0d1623888526062b1
4-
config_hash: 5cc6fa1369335945123744d5210963b1
1+
configured_endpoints: 123
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-d93d04979b6fb3a12da8d7a30fef35d3fe51716f4e9998a7cca5b591187203fc.yml
3+
openapi_spec_hash: e0c6980655d2614c9951c9f231f99cbc
4+
config_hash: a24309fd705de8a4c9c014e8a54d6099

api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ from runloop_api_client.types import (
214214
DevboxSnapshotListView,
215215
DevboxSnapshotView,
216216
DevboxView,
217+
GatewayTokenView,
217218
McpTokenView,
218219
PtyTunnelView,
219220
TunnelView,
@@ -229,6 +230,7 @@ Methods:
229230
- <code title="get /v1/devboxes/{id}">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">retrieve</a>(id) -> <a href="./src/runloop_api_client/types/devbox_view.py">DevboxView</a></code>
230231
- <code title="post /v1/devboxes/{id}">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">update</a>(id, \*\*<a href="src/runloop_api_client/types/devbox_update_params.py">params</a>) -> <a href="./src/runloop_api_client/types/devbox_view.py">DevboxView</a></code>
231232
- <code title="get /v1/devboxes">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">list</a>(\*\*<a href="src/runloop_api_client/types/devbox_list_params.py">params</a>) -> <a href="./src/runloop_api_client/types/devbox_view.py">SyncDevboxesCursorIDPage[DevboxView]</a></code>
233+
- <code title="post /v1/devboxes/{id}/create_gateway_token">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">create_gateway_token</a>(id, \*\*<a href="src/runloop_api_client/types/devbox_create_gateway_token_params.py">params</a>) -> <a href="./src/runloop_api_client/types/gateway_token_view.py">GatewayTokenView</a></code>
232234
- <code title="post /v1/devboxes/{id}/create_mcp_token">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">create_mcp_token</a>(id, \*\*<a href="src/runloop_api_client/types/devbox_create_mcp_token_params.py">params</a>) -> <a href="./src/runloop_api_client/types/mcp_token_view.py">McpTokenView</a></code>
233235
- <code title="post /v1/devboxes/{id}/create_pty_tunnel">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">create_pty_tunnel</a>(id) -> <a href="./src/runloop_api_client/types/pty_tunnel_view.py">PtyTunnelView</a></code>
234236
- <code title="post /v1/devboxes/{id}/create_ssh_key">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">create_ssh_key</a>(id) -> <a href="./src/runloop_api_client/types/devbox_create_ssh_key_response.py">DevboxCreateSSHKeyResponse</a></code>

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
devbox_list_disk_snapshots_params,
3838
devbox_snapshot_disk_async_params,
3939
devbox_write_file_contents_params,
40+
devbox_create_gateway_token_params,
4041
)
4142
from ..._files import deepcopy_with_paths
4243
from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
@@ -94,6 +95,7 @@
9495
from ...lib.wait_for_status import wait_for_status, async_wait_for_status
9596
from ...types.mcp_token_view import McpTokenView
9697
from ...types.pty_tunnel_view import PtyTunnelView
98+
from ...types.gateway_token_view import GatewayTokenView
9799
from ...types.shared_params.mount import Mount
98100
from ...types.devbox_snapshot_view import DevboxSnapshotView
99101
from ...types.shared.launch_parameters import LaunchParameters as SharedLaunchParameters
@@ -610,6 +612,66 @@ def list(
610612
model=DevboxView,
611613
)
612614

615+
def create_gateway_token(
616+
self,
617+
id: str,
618+
*,
619+
gateway: str,
620+
secret: str,
621+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
622+
# The extra values given here take precedence over values defined on the client or passed to this method.
623+
extra_headers: Headers | None = None,
624+
extra_query: Query | None = None,
625+
extra_body: Body | None = None,
626+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
627+
idempotency_key: str | None = None,
628+
) -> GatewayTokenView:
629+
"""
630+
Mint a token that lets a running Devbox call an external API through the Runloop
631+
agent gateway, using the credential in the supplied secret. The gateway applies
632+
the credential to proxied requests, so the real API key is never exposed to the
633+
Devbox.
634+
635+
The token is bound to this Devbox and is only accepted for requests that
636+
originate from it. Nothing is stored on the Devbox: the token is returned to the
637+
caller and is not re-issued when the Devbox is resumed.
638+
639+
Args:
640+
gateway: The gateway config to use. Can be a gateway config ID (gwc_xxx) or name.
641+
642+
secret: The secret containing the credential. Can be a secret ID or name.
643+
644+
extra_headers: Send extra headers
645+
646+
extra_query: Add additional query parameters to the request
647+
648+
extra_body: Add additional JSON properties to the request
649+
650+
timeout: Override the client-level default timeout for this request, in seconds
651+
652+
idempotency_key: Specify a custom idempotency key for this request
653+
"""
654+
if not id:
655+
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
656+
return self._post(
657+
path_template("/v1/devboxes/{id}/create_gateway_token", id=id),
658+
body=maybe_transform(
659+
{
660+
"gateway": gateway,
661+
"secret": secret,
662+
},
663+
devbox_create_gateway_token_params.DevboxCreateGatewayTokenParams,
664+
),
665+
options=make_request_options(
666+
extra_headers=extra_headers,
667+
extra_query=extra_query,
668+
extra_body=extra_body,
669+
timeout=timeout,
670+
idempotency_key=idempotency_key,
671+
),
672+
cast_to=GatewayTokenView,
673+
)
674+
613675
def create_mcp_token(
614676
self,
615677
id: str,
@@ -2386,6 +2448,66 @@ def list(
23862448
model=DevboxView,
23872449
)
23882450

2451+
async def create_gateway_token(
2452+
self,
2453+
id: str,
2454+
*,
2455+
gateway: str,
2456+
secret: str,
2457+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
2458+
# The extra values given here take precedence over values defined on the client or passed to this method.
2459+
extra_headers: Headers | None = None,
2460+
extra_query: Query | None = None,
2461+
extra_body: Body | None = None,
2462+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
2463+
idempotency_key: str | None = None,
2464+
) -> GatewayTokenView:
2465+
"""
2466+
Mint a token that lets a running Devbox call an external API through the Runloop
2467+
agent gateway, using the credential in the supplied secret. The gateway applies
2468+
the credential to proxied requests, so the real API key is never exposed to the
2469+
Devbox.
2470+
2471+
The token is bound to this Devbox and is only accepted for requests that
2472+
originate from it. Nothing is stored on the Devbox: the token is returned to the
2473+
caller and is not re-issued when the Devbox is resumed.
2474+
2475+
Args:
2476+
gateway: The gateway config to use. Can be a gateway config ID (gwc_xxx) or name.
2477+
2478+
secret: The secret containing the credential. Can be a secret ID or name.
2479+
2480+
extra_headers: Send extra headers
2481+
2482+
extra_query: Add additional query parameters to the request
2483+
2484+
extra_body: Add additional JSON properties to the request
2485+
2486+
timeout: Override the client-level default timeout for this request, in seconds
2487+
2488+
idempotency_key: Specify a custom idempotency key for this request
2489+
"""
2490+
if not id:
2491+
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
2492+
return await self._post(
2493+
path_template("/v1/devboxes/{id}/create_gateway_token", id=id),
2494+
body=await async_maybe_transform(
2495+
{
2496+
"gateway": gateway,
2497+
"secret": secret,
2498+
},
2499+
devbox_create_gateway_token_params.DevboxCreateGatewayTokenParams,
2500+
),
2501+
options=make_request_options(
2502+
extra_headers=extra_headers,
2503+
extra_query=extra_query,
2504+
extra_body=extra_body,
2505+
timeout=timeout,
2506+
idempotency_key=idempotency_key,
2507+
),
2508+
cast_to=GatewayTokenView,
2509+
)
2510+
23892511
async def create_mcp_token(
23902512
self,
23912513
id: str,
@@ -3700,6 +3822,9 @@ def __init__(self, devboxes: DevboxesResource) -> None:
37003822
self.list = to_raw_response_wrapper(
37013823
devboxes.list,
37023824
)
3825+
self.create_gateway_token = to_raw_response_wrapper(
3826+
devboxes.create_gateway_token,
3827+
)
37033828
self.create_mcp_token = to_raw_response_wrapper(
37043829
devboxes.create_mcp_token,
37053830
)
@@ -3802,6 +3927,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
38023927
self.list = async_to_raw_response_wrapper(
38033928
devboxes.list,
38043929
)
3930+
self.create_gateway_token = async_to_raw_response_wrapper(
3931+
devboxes.create_gateway_token,
3932+
)
38053933
self.create_mcp_token = async_to_raw_response_wrapper(
38063934
devboxes.create_mcp_token,
38073935
)
@@ -3904,6 +4032,9 @@ def __init__(self, devboxes: DevboxesResource) -> None:
39044032
self.list = to_streamed_response_wrapper(
39054033
devboxes.list,
39064034
)
4035+
self.create_gateway_token = to_streamed_response_wrapper(
4036+
devboxes.create_gateway_token,
4037+
)
39074038
self.create_mcp_token = to_streamed_response_wrapper(
39084039
devboxes.create_mcp_token,
39094040
)
@@ -4006,6 +4137,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
40064137
self.list = async_to_streamed_response_wrapper(
40074138
devboxes.list,
40084139
)
4140+
self.create_gateway_token = async_to_streamed_response_wrapper(
4141+
devboxes.create_gateway_token,
4142+
)
40094143
self.create_mcp_token = async_to_streamed_response_wrapper(
40104144
devboxes.create_mcp_token,
40114145
)

src/runloop_api_client/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from .benchmark_job_view import BenchmarkJobView as BenchmarkJobView
5454
from .benchmark_run_view import BenchmarkRunView as BenchmarkRunView
5555
from .devbox_list_params import DevboxListParams as DevboxListParams
56+
from .gateway_token_view import GatewayTokenView as GatewayTokenView
5657
from .object_list_params import ObjectListParams as ObjectListParams
5758
from .pty_connect_params import PtyConnectParams as PtyConnectParams
5859
from .pty_control_params import PtyControlParams as PtyControlParams
@@ -150,6 +151,7 @@
150151
from .devbox_snapshot_disk_async_params import DevboxSnapshotDiskAsyncParams as DevboxSnapshotDiskAsyncParams
151152
from .devbox_write_file_contents_params import DevboxWriteFileContentsParams as DevboxWriteFileContentsParams
152153
from .devbox_async_execution_detail_view import DevboxAsyncExecutionDetailView as DevboxAsyncExecutionDetailView
154+
from .devbox_create_gateway_token_params import DevboxCreateGatewayTokenParams as DevboxCreateGatewayTokenParams
153155
from .devbox_read_file_contents_response import DevboxReadFileContentsResponse as DevboxReadFileContentsResponse
154156
from .benchmark_run_list_scenario_runs_params import (
155157
BenchmarkRunListScenarioRunsParams as BenchmarkRunListScenarioRunsParams,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import Required, TypedDict
6+
7+
__all__ = ["DevboxCreateGatewayTokenParams"]
8+
9+
10+
class DevboxCreateGatewayTokenParams(TypedDict, total=False):
11+
gateway: Required[str]
12+
"""The gateway config to use. Can be a gateway config ID (gwc_xxx) or name."""
13+
14+
secret: Required[str]
15+
"""The secret containing the credential. Can be a secret ID or name."""
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from .._models import BaseModel
4+
from .shared.auth_mechanism import AuthMechanism
5+
6+
__all__ = ["GatewayTokenView"]
7+
8+
9+
class GatewayTokenView(BaseModel):
10+
token: str
11+
"""The token to send to the gateway as a Bearer token in the Authorization header.
12+
13+
Only accepted for requests originating from the bound Devbox.
14+
"""
15+
16+
auth_mechanism: AuthMechanism
17+
"""How the gateway applies the credential to proxied requests."""
18+
19+
devbox_id: str
20+
"""The Devbox the token is bound to."""
21+
22+
endpoint: str
23+
"""The target API endpoint the gateway proxies to."""
24+
25+
gateway_config_id: str
26+
"""The ID of the gateway config the token proxies through."""
27+
28+
url: str
29+
"""The gateway URL to send requests to.
30+
31+
Matches the value of the &#123;prefix&#125;\\__URL environment variable inside the
32+
Devbox.
33+
"""

tests/api_resources/test_devboxes.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
TunnelView,
1818
McpTokenView,
1919
PtyTunnelView,
20+
GatewayTokenView,
2021
DevboxSnapshotView,
2122
DevboxResourceUsageView,
2223
DevboxExecutionDetailView,
@@ -275,6 +276,52 @@ def test_streaming_response_list(self, client: Runloop) -> None:
275276

276277
assert cast(Any, response.is_closed) is True
277278

279+
@parametrize
280+
def test_method_create_gateway_token(self, client: Runloop) -> None:
281+
devbox = client.devboxes.create_gateway_token(
282+
id="id",
283+
gateway="gateway",
284+
secret="secret",
285+
)
286+
assert_matches_type(GatewayTokenView, devbox, path=["response"])
287+
288+
@parametrize
289+
def test_raw_response_create_gateway_token(self, client: Runloop) -> None:
290+
response = client.devboxes.with_raw_response.create_gateway_token(
291+
id="id",
292+
gateway="gateway",
293+
secret="secret",
294+
)
295+
296+
assert response.is_closed is True
297+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
298+
devbox = response.parse()
299+
assert_matches_type(GatewayTokenView, devbox, path=["response"])
300+
301+
@parametrize
302+
def test_streaming_response_create_gateway_token(self, client: Runloop) -> None:
303+
with client.devboxes.with_streaming_response.create_gateway_token(
304+
id="id",
305+
gateway="gateway",
306+
secret="secret",
307+
) as response:
308+
assert not response.is_closed
309+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
310+
311+
devbox = response.parse()
312+
assert_matches_type(GatewayTokenView, devbox, path=["response"])
313+
314+
assert cast(Any, response.is_closed) is True
315+
316+
@parametrize
317+
def test_path_params_create_gateway_token(self, client: Runloop) -> None:
318+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
319+
client.devboxes.with_raw_response.create_gateway_token(
320+
id="",
321+
gateway="gateway",
322+
secret="secret",
323+
)
324+
278325
@parametrize
279326
def test_method_create_mcp_token(self, client: Runloop) -> None:
280327
devbox = client.devboxes.create_mcp_token(
@@ -2084,6 +2131,52 @@ async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None
20842131

20852132
assert cast(Any, response.is_closed) is True
20862133

2134+
@parametrize
2135+
async def test_method_create_gateway_token(self, async_client: AsyncRunloop) -> None:
2136+
devbox = await async_client.devboxes.create_gateway_token(
2137+
id="id",
2138+
gateway="gateway",
2139+
secret="secret",
2140+
)
2141+
assert_matches_type(GatewayTokenView, devbox, path=["response"])
2142+
2143+
@parametrize
2144+
async def test_raw_response_create_gateway_token(self, async_client: AsyncRunloop) -> None:
2145+
response = await async_client.devboxes.with_raw_response.create_gateway_token(
2146+
id="id",
2147+
gateway="gateway",
2148+
secret="secret",
2149+
)
2150+
2151+
assert response.is_closed is True
2152+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
2153+
devbox = await response.parse()
2154+
assert_matches_type(GatewayTokenView, devbox, path=["response"])
2155+
2156+
@parametrize
2157+
async def test_streaming_response_create_gateway_token(self, async_client: AsyncRunloop) -> None:
2158+
async with async_client.devboxes.with_streaming_response.create_gateway_token(
2159+
id="id",
2160+
gateway="gateway",
2161+
secret="secret",
2162+
) as response:
2163+
assert not response.is_closed
2164+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
2165+
2166+
devbox = await response.parse()
2167+
assert_matches_type(GatewayTokenView, devbox, path=["response"])
2168+
2169+
assert cast(Any, response.is_closed) is True
2170+
2171+
@parametrize
2172+
async def test_path_params_create_gateway_token(self, async_client: AsyncRunloop) -> None:
2173+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
2174+
await async_client.devboxes.with_raw_response.create_gateway_token(
2175+
id="",
2176+
gateway="gateway",
2177+
secret="secret",
2178+
)
2179+
20872180
@parametrize
20882181
async def test_method_create_mcp_token(self, async_client: AsyncRunloop) -> None:
20892182
devbox = await async_client.devboxes.create_mcp_token(

0 commit comments

Comments
 (0)