Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,7 @@ def __init__(
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)

headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}

# Execd API does not require authentication
headers = self.execd_endpoint.build_request_headers(self.connection_config)
self._client = Client(
base_url=base_url,
timeout=timeout,
Expand Down Expand Up @@ -194,7 +188,7 @@ def __init__(
)

async def _get_client(self):
"""Return the client for execd API (no auth required)."""
"""Return the client for execd API."""
return self._client

def _get_execd_url(self, path: str) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,7 @@ def __init__(
base_url = f"{self.connection_config.protocol}://{self.endpoint.endpoint}"
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.endpoint.headers,
}
headers = self.endpoint.build_request_headers(self.connection_config)

self._client = Client(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,7 @@ def __init__(
base_url = self._get_execd_base_url()
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._httpx_client = httpx.AsyncClient(
base_url=base_url,
Expand All @@ -121,7 +117,6 @@ def __init__(
transport=self.connection_config.transport,
)

# Execd API does not require authentication
self._client = Client(
base_url=base_url,
timeout=timeout,
Expand All @@ -133,11 +128,11 @@ def _get_execd_base_url(self) -> str:
return f"{protocol}://{self.execd_endpoint.endpoint}"

async def _get_httpx_client(self) -> httpx.AsyncClient:
"""Return adapter-owned httpx client for execd (no auth required)."""
"""Return adapter-owned httpx client for execd."""
return self._httpx_client

async def _get_client(self):
"""Return the client for execd API (no auth required)."""
"""Return the client for execd API."""
return self._client

def _get_execd_url(self, path: str) -> str:
Expand Down
10 changes: 2 additions & 8 deletions sdks/sandbox/python/src/opensandbox/adapters/health_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,7 @@ def __init__(
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)

headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}

# Execd API does not require authentication
headers = self.execd_endpoint.build_request_headers(self.connection_config)
self._client = Client(
base_url=base_url,
timeout=timeout,
Expand All @@ -82,7 +76,7 @@ def __init__(
self._client.set_async_httpx_client(self._httpx_client)

async def _get_client(self):
"""Return the client for execd API (no auth required)."""
"""Return the client for execd API."""
return self._client

async def ping(self, sandbox_id: str) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,7 @@ def __init__(
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)

headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._httpx_client = httpx.AsyncClient(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ def __init__(
base_url = f"{protocol}://{self.execd_endpoint.endpoint}"
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._httpx_client = httpx.AsyncClient(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,8 @@ def __init__(
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)

headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

# Execd API does not require authentication
self._client = Client(
base_url=base_url,
timeout=timeout,
Expand All @@ -91,7 +86,7 @@ def __init__(
self._client.set_async_httpx_client(self._httpx_client)

async def _get_client(self):
"""Return the client for execd API (no auth required)."""
"""Return the client for execd API."""
return self._client

async def get_metrics(self, sandbox_id: str) -> SandboxMetrics:
Expand Down
29 changes: 28 additions & 1 deletion sdks/sandbox/python/src/opensandbox/models/sandboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@

import re
from datetime import datetime
from typing import Literal
from typing import TYPE_CHECKING, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

if TYPE_CHECKING:
from opensandbox.config import ConnectionConfig
from opensandbox.config.connection_sync import ConnectionConfigSync


class SandboxImageAuth(BaseModel):
"""
Expand Down Expand Up @@ -784,6 +788,29 @@ class SandboxEndpoint(BaseModel):
description="Headers that must be included on every request targeting this endpoint (e.g. when the server requires them for routing or auth). Empty if not required.",
)

def build_request_headers(
self,
connection_config: "ConnectionConfig | ConnectionConfigSync",
) -> dict[str, str]:
"""
Default headers for execd-plane requests to this endpoint.

The API key is attached only when the client declared server-proxy
mode (``ConnectionConfig.use_server_proxy``): such requests pass the
server's auth gate. In direct mode execd performs no auth and the
key must never travel into the untrusted sandbox.
"""
headers = {
"User-Agent": connection_config.user_agent,
**connection_config.headers,
**self.headers,
}
if connection_config.use_server_proxy:
api_key = connection_config.get_api_key()
if api_key:
headers["OPEN-SANDBOX-API-KEY"] = api_key
Comment on lines +808 to +811

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate execd API keys in the JavaScript SDK

When a JavaScript client supplies an API key while using the default direct mode, ConnectionConfig inserts that key into its shared headers (sdks/sandbox/javascript/src/config/connection.ts:342-346), and DefaultAdapterFactory.createExecdStack() forwards those headers to execd (sdks/sandbox/javascript/src/factory/defaultAdapterFactory.ts:53-62). Thus the new direct-mode security invariant is Python-only: JavaScript still sends the lifecycle credential into the untrusted sandbox. Apply the same useServerProxy gate to JavaScript execd/egress header construction while retaining authentication on lifecycle requests.

AGENTS.md reference: sdks/AGENTS.md:L122-L122

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense and it's the mirror image of the bug fixed here: instead of missing the key in proxy mode, the JS/C# SDKs attach it unconditionally, so in direct mode the credential travels straight into the untrusted sandbox with no server in the path to strip it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept this PR scoped to Python (single concern) and opened #1689 to track the JS/C# side — the fix direction is the same declaration-gated injection the Go SDK already implements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1691 is already addressing it

return headers


class PaginationInfo(BaseModel):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,7 @@ def __init__(
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)

headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._client = Client(base_url=base_url, timeout=timeout)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,7 @@ def __init__(
base_url = f"{self.connection_config.protocol}://{self.endpoint.endpoint}"
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.endpoint.headers,
}
headers = self.endpoint.build_request_headers(self.connection_config)

self._client = Client(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,7 @@ def __init__(
base_url = self._get_execd_base_url()
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._httpx_client = httpx.Client(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ def __init__(

base_url = f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}"
timeout = httpx.Timeout(self.connection_config.request_timeout.total_seconds())
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._client = Client(base_url=base_url, timeout=timeout)
self._httpx_client = httpx.Client(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,7 @@ def __init__(
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)

headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._httpx_client = httpx.Client(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ def __init__(
base_url = f"{protocol}://{self.execd_endpoint.endpoint}"
timeout_seconds = self.connection_config.request_timeout.total_seconds()
timeout = httpx.Timeout(timeout_seconds)
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._httpx_client = httpx.Client(
base_url=base_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ def __init__(

base_url = f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}"
timeout = httpx.Timeout(self.connection_config.request_timeout.total_seconds())
headers = {
"User-Agent": self.connection_config.user_agent,
**self.connection_config.headers,
**self.execd_endpoint.headers,
}
headers = self.execd_endpoint.build_request_headers(self.connection_config)

self._client = Client(base_url=base_url, timeout=timeout)
self._httpx_client = httpx.Client(
Expand Down
Loading
Loading