From 7fbc9d02b968c565e0aff37e86c61468704b7f50 Mon Sep 17 00:00:00 2001 From: fengxusong <7008971+fengxsong@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:30:20 +0800 Subject: [PATCH 1/5] feat(server): support accessing sandbox endpoints via server proxy Signed-off-by: fengxusong <7008971+fengxsong@users.noreply.github.com> --- .../opensandbox/adapters/sandboxes_adapter.py | 7 ++- .../src/opensandbox/config/connection.py | 13 ++++ server/pyproject.toml | 4 +- server/src/api/lifecycle.py | 61 ++++++++++++++++++- server/uv.lock | 18 +++++- 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py index 9ac39f43..3fa81a1c 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py @@ -240,7 +240,12 @@ async def get_sandbox_endpoint( from opensandbox.api.lifecycle.models import Endpoint parsed = require_parsed(response_obj, Endpoint, "Get endpoint") - return SandboxModelConverter.to_sandbox_endpoint(parsed) + sandbox_endpoint = SandboxModelConverter.to_sandbox_endpoint(parsed) + if self.connection_config.use_server_proxy: + sandbox_endpoint.endpoint = self.connection_config.get_execd_url( + sandbox_endpoint.endpoint + ) + return sandbox_endpoint except Exception as e: logger.error( diff --git a/sdks/sandbox/python/src/opensandbox/config/connection.py b/sdks/sandbox/python/src/opensandbox/config/connection.py index f013528a..7df94dba 100644 --- a/sdks/sandbox/python/src/opensandbox/config/connection.py +++ b/sdks/sandbox/python/src/opensandbox/config/connection.py @@ -78,6 +78,13 @@ class ConnectionConfig(BaseModel): "with custom settings) to control connection pooling, proxies, retries, etc." ), ) + use_server_proxy: bool = Field( + default=False, + description=( + "Using sandbox server as proxy for process execd requests" + "It's useful when client sdk can't access the created sandbox directly" + ), + ) # Environment variable names _ENV_API_KEY = "OPEN_SANDBOX_API_KEY" @@ -157,3 +164,9 @@ def get_base_url(self) -> str: ): return f"{domain}/{self._API_VERSION}" return f"{self.protocol}://{domain}/{self._API_VERSION}" + + def get_execd_url(self, endpoint: str) -> str: + """Get the actual URL for execd requests.""" + if self.use_server_proxy: + return f"{self.get_domain()}/proxy/{endpoint}" + return endpoint diff --git a/server/pyproject.toml b/server/pyproject.toml index e96e06ce..8cfbd585 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -44,7 +44,7 @@ classifiers = [ dependencies = [ "docker", "fastapi", - "httpx", + "httpx[socks]", "kubernetes", "pydantic", "pydantic-settings", @@ -119,4 +119,4 @@ venvPath = "." venv = ".venv" reportMissingImports = true -reportMissingTypeStubs = false \ No newline at end of file +reportMissingTypeStubs = false diff --git a/server/src/api/lifecycle.py b/server/src/api/lifecycle.py index b5f405a9..43c05de9 100644 --- a/server/src/api/lifecycle.py +++ b/server/src/api/lifecycle.py @@ -21,8 +21,10 @@ from typing import List, Optional -from fastapi import APIRouter,Header, Query, status -from fastapi.responses import Response +import httpx +from fastapi import APIRouter, Header, Query, Request, status +from fastapi.exceptions import HTTPException +from fastapi.responses import Response, StreamingResponse from src.api.schema import ( CreateSandboxRequest, @@ -383,3 +385,58 @@ async def get_sandbox_endpoint( """ # Delegate to the service layer for endpoint resolution return sandbox_service.get_endpoint(sandbox_id, port) + + +client = httpx.AsyncClient(timeout=180.0) + + +@router.api_route( + "/proxy/{endpoint}/{full_path:path}", + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], +) +async def proxy_request(request: Request, endpoint: str, full_path: str): + """ + Receives all incoming requests, determines the target sandbox from path parameter, + and asynchronously proxies the request to it. + """ + + target_host = f"{endpoint}" + target_url = f"http://{target_host}/{full_path}" + + try: + headers = { + key: value + for (key, value) in request.headers.items() + if key.lower() != "host" + } + + req = client.build_request( + method=request.method, + url=target_url, + headers=headers, + content=await request.body(), + ) + + # TODO: support websocket protocol? + # since execd component does not have websocket handler currently, we just raise an error here + if request.method == "GET" and request.headers.get("Upgrade") == "websocket": + raise HTTPException( + status_code=400, detail="Websocket upgrade is not supported yet" + ) + + resp = await client.send(req, stream=True) + + return StreamingResponse( + content=resp.aiter_bytes(), + status_code=resp.status_code, + headers=resp.headers, + ) + except httpx.ConnectError as e: + raise HTTPException( + status_code=502, + detail=f"Could not connect to the backend sandbox {endpoint}: {e}", + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"An internal error occurred in the proxy: {e}" + ) diff --git a/server/uv.lock b/server/uv.lock index b843e46b..007cdb44 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -376,6 +376,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[package.optional-dependencies] +socks = [ + { name = "socksio" }, +] + [[package]] name = "idna" version = "3.11" @@ -439,7 +444,7 @@ source = { editable = "." } dependencies = [ { name = "docker" }, { name = "fastapi" }, - { name = "httpx" }, + { name = "httpx", extra = ["socks"] }, { name = "kubernetes" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -461,7 +466,7 @@ dev = [ requires-dist = [ { name = "docker" }, { name = "fastapi" }, - { name = "httpx" }, + { name = "httpx", extras = ["socks"] }, { name = "kubernetes" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -915,6 +920,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + [[package]] name = "starlette" version = "0.50.0" From 7082d64920ad53e901d17261e25d890e2ec051e2 Mon Sep 17 00:00:00 2001 From: fengxusong <7008971+fengxsong@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:01:18 +0800 Subject: [PATCH 2/5] fix(server): using lifespan to manage http client per request and filter headers be forwarded --- server/src/api/lifecycle.py | 40 ++++++++++++++++++++++++++++--------- server/src/main.py | 11 ++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/server/src/api/lifecycle.py b/server/src/api/lifecycle.py index 43c05de9..cc895c70 100644 --- a/server/src/api/lifecycle.py +++ b/server/src/api/lifecycle.py @@ -41,6 +41,24 @@ ) from src.services.factory import create_sandbox_service +# RFC 2616 Section 13.5.1 +HOP_BY_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", +} + +# Headers that shouldn't be forwarded to untrusted/internal backends +SENSITIVE_HEADERS = { + "authorization", + "cookie", +} + # Initialize router router = APIRouter(tags=["Sandboxes"]) @@ -387,9 +405,6 @@ async def get_sandbox_endpoint( return sandbox_service.get_endpoint(sandbox_id, port) -client = httpx.AsyncClient(timeout=180.0) - - @router.api_route( "/proxy/{endpoint}/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"], @@ -403,18 +418,25 @@ async def proxy_request(request: Request, endpoint: str, full_path: str): target_host = f"{endpoint}" target_url = f"http://{target_host}/{full_path}" + client: httpx.AsyncClient = request.app.state.http_client + try: - headers = { - key: value - for (key, value) in request.headers.items() - if key.lower() != "host" - } + # Filter headers + headers = {} + for key, value in request.headers.items(): + key_lower = key.lower() + if ( + key_lower != "host" + and key_lower not in HOP_BY_HOP_HEADERS + and key_lower not in SENSITIVE_HEADERS + ): + headers[key] = value req = client.build_request( method=request.method, url=target_url, headers=headers, - content=await request.body(), + content=request.stream(), ) # TODO: support websocket protocol? diff --git a/server/src/main.py b/server/src/main.py index df060df6..2888d55f 100644 --- a/server/src/main.py +++ b/server/src/main.py @@ -21,8 +21,10 @@ import copy import logging.config +from contextlib import asynccontextmanager from typing import Any +import httpx from fastapi import FastAPI, Request from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -63,6 +65,14 @@ from src.api.lifecycle import router # noqa: E402 from src.middleware.auth import AuthMiddleware # noqa: E402 + +@asynccontextmanager +async def lifespan(app: FastAPI): + app.state.http_client = httpx.AsyncClient(timeout=180.0) + yield + await app.state.http_client.aclose() + + # Initialize FastAPI application app = FastAPI( title="OpenSandbox Lifecycle API", @@ -71,6 +81,7 @@ "executed, paused, resumed, and finally disposed.", docs_url="/docs", redoc_url="/redoc", + lifespan=lifespan, ) # Add CORS middleware From 010a3c249e6d687cb18cf5d02f1be9cfce306398 Mon Sep 17 00:00:00 2001 From: fengxusong <7008971+fengxsong@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:34:40 +0800 Subject: [PATCH 3/5] refactor(server): refactor server proxy sandbox request logic --- .../opensandbox/adapters/sandboxes_adapter.py | 8 ++----- ...get_sandboxes_sandbox_id_endpoints_port.py | 11 ++++++++++ .../src/opensandbox/config/connection.py | 6 ----- server/src/api/lifecycle.py | 22 +++++++++++++++---- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py index 3fa81a1c..8ac8cc31 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py @@ -231,6 +231,7 @@ async def get_sandbox_endpoint( client=client, sandbox_id=sandbox_id, port=port, + use_server_proxy=self.connection_config.use_server_proxy, ) ) @@ -240,12 +241,7 @@ async def get_sandbox_endpoint( from opensandbox.api.lifecycle.models import Endpoint parsed = require_parsed(response_obj, Endpoint, "Get endpoint") - sandbox_endpoint = SandboxModelConverter.to_sandbox_endpoint(parsed) - if self.connection_config.use_server_proxy: - sandbox_endpoint.endpoint = self.connection_config.get_execd_url( - sandbox_endpoint.endpoint - ) - return sandbox_endpoint + return SandboxModelConverter.to_sandbox_endpoint(parsed) except Exception as e: logger.error( diff --git a/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py b/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py index 6bd5962b..bfed0a37 100644 --- a/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py +++ b/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py @@ -30,6 +30,7 @@ def _get_kwargs( sandbox_id: str, port: int, + use_server_proxy: bool = False, ) -> dict[str, Any]: _kwargs: dict[str, Any] = { "method": "get", @@ -38,6 +39,8 @@ def _get_kwargs( port=quote(str(port), safe=""), ), } + if use_server_proxy: + _kwargs["params"] = {"use_server_proxy": use_server_proxy} return _kwargs @@ -90,6 +93,7 @@ def _build_response( def sync_detailed( sandbox_id: str, port: int, + use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, ) -> Response[Endpoint | ErrorResponse]: @@ -114,6 +118,7 @@ def sync_detailed( kwargs = _get_kwargs( sandbox_id=sandbox_id, port=port, + use_server_proxy=use_server_proxy, ) response = client.get_httpx_client().request( @@ -126,6 +131,7 @@ def sync_detailed( def sync( sandbox_id: str, port: int, + use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, ) -> Endpoint | ErrorResponse | None: @@ -150,6 +156,7 @@ def sync( return sync_detailed( sandbox_id=sandbox_id, port=port, + use_server_proxy=use_server_proxy, client=client, ).parsed @@ -157,6 +164,7 @@ def sync( async def asyncio_detailed( sandbox_id: str, port: int, + use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, ) -> Response[Endpoint | ErrorResponse]: @@ -181,6 +189,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( sandbox_id=sandbox_id, port=port, + use_server_proxy=use_server_proxy, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -191,6 +200,7 @@ async def asyncio_detailed( async def asyncio( sandbox_id: str, port: int, + use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, ) -> Endpoint | ErrorResponse | None: @@ -216,6 +226,7 @@ async def asyncio( await asyncio_detailed( sandbox_id=sandbox_id, port=port, + use_server_proxy=use_server_proxy, client=client, ) ).parsed diff --git a/sdks/sandbox/python/src/opensandbox/config/connection.py b/sdks/sandbox/python/src/opensandbox/config/connection.py index 7df94dba..88e37034 100644 --- a/sdks/sandbox/python/src/opensandbox/config/connection.py +++ b/sdks/sandbox/python/src/opensandbox/config/connection.py @@ -164,9 +164,3 @@ def get_base_url(self) -> str: ): return f"{domain}/{self._API_VERSION}" return f"{self.protocol}://{domain}/{self._API_VERSION}" - - def get_execd_url(self, endpoint: str) -> str: - """Get the actual URL for execd requests.""" - if self.use_server_proxy: - return f"{self.get_domain()}/proxy/{endpoint}" - return endpoint diff --git a/server/src/api/lifecycle.py b/server/src/api/lifecycle.py index cc895c70..1ebab8ef 100644 --- a/server/src/api/lifecycle.py +++ b/server/src/api/lifecycle.py @@ -379,8 +379,10 @@ async def renew_sandbox_expiration( }, ) async def get_sandbox_endpoint( + request: Request, sandbox_id: str, port: int, + use_server_proxy: bool = Query(False, description="Whether to return a server-proxied URL"), x_request_id: Optional[str] = Header(None, alias="X-Request-ID"), ) -> Endpoint: """ @@ -391,8 +393,10 @@ async def get_sandbox_endpoint( for the endpoint to be available. Args: + request: FastAPI request object sandbox_id: Unique sandbox identifier port: Port number where the service is listening inside the sandbox (1-65535) + use_server_proxy: Whether to return a server-proxied URL x_request_id: Unique request identifier for tracing Returns: @@ -402,20 +406,30 @@ async def get_sandbox_endpoint( HTTPException: If sandbox not found or endpoint not available """ # Delegate to the service layer for endpoint resolution - return sandbox_service.get_endpoint(sandbox_id, port) + endpoint = sandbox_service.get_endpoint(sandbox_id, port) + + if use_server_proxy: + # Construct proxy URL + base_url = str(request.base_url).rstrip("/") + base_url = base_url.replace("https://", "").replace("http://", "") + endpoint.endpoint = f"{base_url}/sandboxes/{sandbox_id}/proxy/{port}" + + return endpoint @router.api_route( - "/proxy/{endpoint}/{full_path:path}", + "/sandboxes/{sandbox_id}/proxy/{port}/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"], ) -async def proxy_request(request: Request, endpoint: str, full_path: str): +async def proxy_sandbox_endpoint_request(request: Request, sandbox_id: str, port: int, full_path: str): """ Receives all incoming requests, determines the target sandbox from path parameter, and asynchronously proxies the request to it. """ - target_host = f"{endpoint}" + endpoint = sandbox_service.get_endpoint(sandbox_id, port) + + target_host = endpoint.endpoint target_url = f"http://{target_host}/{full_path}" client: httpx.AsyncClient = request.app.state.http_client From f60904185874baa1ca030733b1121817f31cff09 Mon Sep 17 00:00:00 2001 From: fengxusong <7008971+fengxsong@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:24:40 +0800 Subject: [PATCH 4/5] fix(sdk): add use_server_proxy parameter in open-api spec --- ...get_sandboxes_sandbox_id_endpoints_port.py | 30 ++++++++++++------- specs/sandbox-lifecycle.yml | 6 ++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py b/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py index bfed0a37..76eb08d6 100644 --- a/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py +++ b/sdks/sandbox/python/src/opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py @@ -24,23 +24,29 @@ from ...client import AuthenticatedClient, Client from ...models.endpoint import Endpoint from ...models.error_response import ErrorResponse -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( sandbox_id: str, port: int, - use_server_proxy: bool = False, + *, + use_server_proxy: bool | Unset = False, ) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["use_server_proxy"] = use_server_proxy + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "get", "url": "/sandboxes/{sandbox_id}/endpoints/{port}".format( sandbox_id=quote(str(sandbox_id), safe=""), port=quote(str(port), safe=""), ), + "params": params, } - if use_server_proxy: - _kwargs["params"] = {"use_server_proxy": use_server_proxy} return _kwargs @@ -93,9 +99,9 @@ def _build_response( def sync_detailed( sandbox_id: str, port: int, - use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, + use_server_proxy: bool | Unset = False, ) -> Response[Endpoint | ErrorResponse]: """Get sandbox access endpoint @@ -106,6 +112,7 @@ def sync_detailed( Args: sandbox_id (str): port (int): + use_server_proxy (bool | Unset): Default: False. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -131,9 +138,9 @@ def sync_detailed( def sync( sandbox_id: str, port: int, - use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, + use_server_proxy: bool | Unset = False, ) -> Endpoint | ErrorResponse | None: """Get sandbox access endpoint @@ -144,6 +151,7 @@ def sync( Args: sandbox_id (str): port (int): + use_server_proxy (bool | Unset): Default: False. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -156,17 +164,17 @@ def sync( return sync_detailed( sandbox_id=sandbox_id, port=port, - use_server_proxy=use_server_proxy, client=client, + use_server_proxy=use_server_proxy, ).parsed async def asyncio_detailed( sandbox_id: str, port: int, - use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, + use_server_proxy: bool | Unset = False, ) -> Response[Endpoint | ErrorResponse]: """Get sandbox access endpoint @@ -177,6 +185,7 @@ async def asyncio_detailed( Args: sandbox_id (str): port (int): + use_server_proxy (bool | Unset): Default: False. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -200,9 +209,9 @@ async def asyncio_detailed( async def asyncio( sandbox_id: str, port: int, - use_server_proxy: bool = False, *, client: AuthenticatedClient | Client, + use_server_proxy: bool | Unset = False, ) -> Endpoint | ErrorResponse | None: """Get sandbox access endpoint @@ -213,6 +222,7 @@ async def asyncio( Args: sandbox_id (str): port (int): + use_server_proxy (bool | Unset): Default: False. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -226,7 +236,7 @@ async def asyncio( await asyncio_detailed( sandbox_id=sandbox_id, port=port, - use_server_proxy=use_server_proxy, client=client, + use_server_proxy=use_server_proxy, ) ).parsed diff --git a/specs/sandbox-lifecycle.yml b/specs/sandbox-lifecycle.yml index 8a8efeda..d45ce697 100644 --- a/specs/sandbox-lifecycle.yml +++ b/specs/sandbox-lifecycle.yml @@ -348,6 +348,12 @@ paths: type: integer minimum: 1 maximum: 65535 + - name: use_server_proxy + in: query + description: Whether to return a server-proxied URL + schema: + type: boolean + default: false responses: '200': description: | From 1fa65f452e2897a42af9406b131508ee12ab8a21 Mon Sep 17 00:00:00 2001 From: fengxusong <7008971+fengxsong@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:37:48 +0800 Subject: [PATCH 5/5] refactor(python-sdk): refactor the get_sandbox_endpoint function for sync/async sandboxadapter --- .../opensandbox/adapters/sandboxes_adapter.py | 4 ++-- .../src/opensandbox/config/connection_sync.py | 7 +++++++ sdks/sandbox/python/src/opensandbox/sandbox.py | 10 ++++++---- .../python/src/opensandbox/services/sandbox.py | 3 ++- .../sync/adapters/sandboxes_adapter.py | 5 ++++- .../python/src/opensandbox/sync/sandbox.py | 16 ++++++++++++---- .../src/opensandbox/sync/services/sandbox.py | 5 ++++- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py index 8ac8cc31..690a63e9 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py @@ -215,7 +215,7 @@ async def list_sandboxes(self, filter: SandboxFilter) -> PagedSandboxInfos: raise ExceptionConverter.to_sandbox_exception(e) from e async def get_sandbox_endpoint( - self, sandbox_id: str, port: int + self, sandbox_id: str, port: int, use_server_proxy: bool = False ) -> SandboxEndpoint: """Get network endpoint information for a sandbox service.""" logger.debug(f"Retrieving sandbox endpoint: {sandbox_id}, port {port}") @@ -231,7 +231,7 @@ async def get_sandbox_endpoint( client=client, sandbox_id=sandbox_id, port=port, - use_server_proxy=self.connection_config.use_server_proxy, + use_server_proxy=use_server_proxy, ) ) diff --git a/sdks/sandbox/python/src/opensandbox/config/connection_sync.py b/sdks/sandbox/python/src/opensandbox/config/connection_sync.py index 9eae5d72..70d2ddec 100644 --- a/sdks/sandbox/python/src/opensandbox/config/connection_sync.py +++ b/sdks/sandbox/python/src/opensandbox/config/connection_sync.py @@ -65,6 +65,13 @@ class ConnectionConfigSync(BaseModel): "with custom limits/proxies) to control connection pooling, proxies, retries, etc." ), ) + use_server_proxy: bool = Field( + default=False, + description=( + "Using sandbox server as proxy for process execd requests" + "It's useful when client sdk can't access the created sandbox directly" + ), + ) _ENV_API_KEY = "OPEN_SANDBOX_API_KEY" _ENV_DOMAIN = "OPEN_SANDBOX_DOMAIN" diff --git a/sdks/sandbox/python/src/opensandbox/sandbox.py b/sdks/sandbox/python/src/opensandbox/sandbox.py index 7d746855..dfa80936 100644 --- a/sdks/sandbox/python/src/opensandbox/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/sandbox.py @@ -182,7 +182,9 @@ async def get_endpoint(self, port: int) -> SandboxEndpoint: Raises: SandboxException: if endpoint cannot be retrieved """ - return await self._sandbox_service.get_sandbox_endpoint(self.id, port) + return await self._sandbox_service.get_sandbox_endpoint( + self.id, port, self.connection_config.use_server_proxy + ) async def get_metrics(self) -> SandboxMetrics: """ @@ -422,7 +424,7 @@ async def create( sandbox_id = response.id execd_endpoint = await sandbox_service.get_sandbox_endpoint( - response.id, DEFAULT_EXECD_PORT + response.id, DEFAULT_EXECD_PORT, config.use_server_proxy ) sandbox = cls( @@ -510,7 +512,7 @@ async def connect( try: sandbox_service = factory.create_sandbox_service() execd_endpoint = await sandbox_service.get_sandbox_endpoint( - sandbox_id, DEFAULT_EXECD_PORT + sandbox_id, DEFAULT_EXECD_PORT, config.use_server_proxy ) sandbox = cls( @@ -581,7 +583,7 @@ async def resume( await sandbox_service.resume_sandbox(sandbox_id) execd_endpoint = await sandbox_service.get_sandbox_endpoint( - sandbox_id, DEFAULT_EXECD_PORT + sandbox_id, DEFAULT_EXECD_PORT, config.use_server_proxy ) sandbox = cls( diff --git a/sdks/sandbox/python/src/opensandbox/services/sandbox.py b/sdks/sandbox/python/src/opensandbox/services/sandbox.py index 9b67dcd8..941641e3 100644 --- a/sdks/sandbox/python/src/opensandbox/services/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/services/sandbox.py @@ -106,7 +106,7 @@ async def list_sandboxes(self, filter: SandboxFilter) -> PagedSandboxInfos: ... async def get_sandbox_endpoint( - self, sandbox_id: str, port: int + self, sandbox_id: str, port: int, use_server_proxy: bool = False ) -> SandboxEndpoint: """ Get sandbox endpoint. @@ -114,6 +114,7 @@ async def get_sandbox_endpoint( Args: sandbox_id: Sandbox ID port: Endpoint port number + use_server_proxy: Whether to use server proxy for endpoint Returns: Target sandbox endpoint diff --git a/sdks/sandbox/python/src/opensandbox/sync/adapters/sandboxes_adapter.py b/sdks/sandbox/python/src/opensandbox/sync/adapters/sandboxes_adapter.py index c55982bf..eb5f9c5f 100644 --- a/sdks/sandbox/python/src/opensandbox/sync/adapters/sandboxes_adapter.py +++ b/sdks/sandbox/python/src/opensandbox/sync/adapters/sandboxes_adapter.py @@ -173,7 +173,9 @@ def list_sandboxes(self, filter: SandboxFilter) -> PagedSandboxInfos: logger.error("Failed to list sandboxes", exc_info=e) raise ExceptionConverter.to_sandbox_exception(e) from e - def get_sandbox_endpoint(self, sandbox_id: str, port: int) -> SandboxEndpoint: + def get_sandbox_endpoint( + self, sandbox_id: str, port: int, use_server_proxy: bool = False + ) -> SandboxEndpoint: try: from opensandbox.api.lifecycle.api.sandboxes import ( get_sandboxes_sandbox_id_endpoints_port, @@ -184,6 +186,7 @@ def get_sandbox_endpoint(self, sandbox_id: str, port: int) -> SandboxEndpoint: sandbox_id=sandbox_id, port=port, client=self._get_client(), + use_server_proxy=use_server_proxy, ) handle_api_error(response_obj, f"Get endpoint for sandbox {sandbox_id} port {port}") parsed = require_parsed(response_obj, ApiEndpoint, "Get endpoint") diff --git a/sdks/sandbox/python/src/opensandbox/sync/sandbox.py b/sdks/sandbox/python/src/opensandbox/sync/sandbox.py index 740458f9..38f083ba 100644 --- a/sdks/sandbox/python/src/opensandbox/sync/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/sync/sandbox.py @@ -188,7 +188,9 @@ def get_endpoint(self, port: int) -> SandboxEndpoint: Raises: SandboxException: if endpoint cannot be retrieved """ - return self._sandbox_service.get_sandbox_endpoint(self.id, port) + return self._sandbox_service.get_sandbox_endpoint( + self.id, port, self.connection_config.use_server_proxy + ) def get_metrics(self) -> SandboxMetrics: """ @@ -407,7 +409,9 @@ def create( extensions, ) sandbox_id = response.id - execd_endpoint = sandbox_service.get_sandbox_endpoint(response.id, DEFAULT_EXECD_PORT) + execd_endpoint = sandbox_service.get_sandbox_endpoint( + response.id, DEFAULT_EXECD_PORT, config.use_server_proxy + ) sandbox = cls( sandbox_id=response.id, @@ -484,7 +488,9 @@ def connect( try: sandbox_service = factory.create_sandbox_service() - execd_endpoint = sandbox_service.get_sandbox_endpoint(sandbox_id, DEFAULT_EXECD_PORT) + execd_endpoint = sandbox_service.get_sandbox_endpoint( + sandbox_id, DEFAULT_EXECD_PORT, config.use_server_proxy + ) sandbox = cls( sandbox_id=sandbox_id, @@ -553,7 +559,9 @@ def resume( sandbox_service = factory.create_sandbox_service() sandbox_service.resume_sandbox(sandbox_id) - execd_endpoint = sandbox_service.get_sandbox_endpoint(sandbox_id, DEFAULT_EXECD_PORT) + execd_endpoint = sandbox_service.get_sandbox_endpoint( + sandbox_id, DEFAULT_EXECD_PORT, config.use_server_proxy + ) sandbox = cls( sandbox_id=sandbox_id, diff --git a/sdks/sandbox/python/src/opensandbox/sync/services/sandbox.py b/sdks/sandbox/python/src/opensandbox/sync/services/sandbox.py index 5ad730a7..0ebe7833 100644 --- a/sdks/sandbox/python/src/opensandbox/sync/services/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/sync/services/sandbox.py @@ -106,13 +106,16 @@ def list_sandboxes(self, filter: SandboxFilter) -> PagedSandboxInfos: """ ... - def get_sandbox_endpoint(self, sandbox_id: str, port: int) -> SandboxEndpoint: + def get_sandbox_endpoint( + self, sandbox_id: str, port: int, use_server_proxy: bool = False + ) -> SandboxEndpoint: """ Get sandbox endpoint for an exposed port. Args: sandbox_id: Sandbox id. port: Endpoint port number. + use_server_proxy: Whether to use server proxy for endpoint. Returns: Target sandbox endpoint.