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
6 changes: 6 additions & 0 deletions .changeset/connect-envs-on-reconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'e2b': minor
'@e2b/python-sdk': minor
---

Add support for passing environment variables when reconnecting to a sandbox via `Sandbox.connect()`.
2 changes: 2 additions & 0 deletions packages/js-sdk/src/api/schema.gen.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ export type SandboxConnectOpts = ConnectionOpts & {
* @default 300_000 // 5 minutes
*/
timeoutMs?: number
/**
* Custom environment variables to set in the sandbox on reconnect.
* These are merged into the sandbox environment and applied to processes started after resume.
*/
envs?: Record<string, string>
}

/**
Expand Down Expand Up @@ -852,6 +857,7 @@ export class SandboxApi {
},
body: {
timeout: timeoutToSeconds(timeoutMs),
envVars: opts?.envs,
},
signal: config.getSignal(opts?.requestTimeoutMs),
})
Expand Down
40 changes: 40 additions & 0 deletions packages/js-sdk/tests/sandbox/configPropagation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,43 @@ describe('Sandbox API config propagation', () => {
assert.equal(opts?.debug, baseConfig.debug)
})
})

describe('Sandbox connect envs propagation', () => {
const mockConnectResult = {
sandboxId: 'sbx-test',
sandboxDomain: 'sandbox.e2b.dev',
envdVersion: '0.2.4',
envdAccessToken: 'tok',
trafficAccessToken: 'tok',
}

afterEach(() => {
vi.restoreAllMocks()
})

test('forwards envs to connectSandbox when provided', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const connectSpy = vi
.spyOn(SandboxApi as any, 'connectSandbox')
.mockResolvedValue(mockConnectResult)
const sandbox = createSandbox()

await sandbox.connect({ envs: { MY_KEY: 'my_value' } })

const opts = connectSpy.mock.calls[0][1]
assert.deepEqual(opts?.envs, { MY_KEY: 'my_value' })
})

test('does not include envs in opts when not provided', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const connectSpy = vi
.spyOn(SandboxApi as any, 'connectSandbox')
.mockResolvedValue(mockConnectResult)
const sandbox = createSandbox()

await sandbox.connect()

const opts = connectSpy.mock.calls[0][1]
assert.isUndefined(opts?.envs)
})
})
13 changes: 12 additions & 1 deletion packages/python-sdk/e2b/api/client/models/connect_sandbox.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions packages/python-sdk/e2b/sandbox_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ async def create(
async def connect(
self,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Self:
"""
Expand All @@ -259,6 +260,8 @@ async def connect(

:param timeout: Timeout for the sandbox in **seconds**
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
:param envs: Custom environment variables to set in the sandbox on reconnect.
Merged into the sandbox environment and applied to processes started after resume.
:return: A running sandbox instance

@example
Expand All @@ -277,6 +280,7 @@ async def connect(
async def connect(
sandbox_id: str,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
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 Handle envs in async class connect path

The async class-style API has the same regression: AsyncSandbox.connect("...", envs={...}) routes through _cls_connect_sandbox, which doesn't accept envs, so envs remains in opts and is passed to ConnectionConfig(..., **opts), causing TypeError: unexpected keyword argument 'envs'. As a result, the newly added async classmethod envs parameter fails at runtime.

Useful? React with 👍 / 👎.

**opts: Unpack[ApiParams],
) -> "AsyncSandbox":
"""
Expand All @@ -288,6 +292,8 @@ async def connect(
:param sandbox_id: Sandbox ID
:param timeout: Timeout for the sandbox in **seconds**
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
:param envs: Custom environment variables to set in the sandbox on reconnect.
Merged into the sandbox environment and applied to processes started after resume.
:return: A running sandbox instance

@example
Expand All @@ -305,6 +311,7 @@ async def connect(
async def connect(
self,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Self:
"""
Expand All @@ -315,6 +322,8 @@ async def connect(

:param timeout: Timeout for the sandbox in **seconds**
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
:param envs: Custom environment variables to set in the sandbox on reconnect.
Merged into the sandbox environment and applied to processes started after resume.
:return: A running sandbox instance

@example
Expand All @@ -329,6 +338,7 @@ async def connect(
await SandboxApi._cls_connect(
sandbox_id=self.sandbox_id,
timeout=timeout,
envs=envs,
**self.connection_config.get_api_params(**opts),
)

Expand Down Expand Up @@ -853,11 +863,13 @@ async def _cls_connect_sandbox(
cls,
sandbox_id: str,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Self:
sandbox = await SandboxApi._cls_connect(
sandbox_id=sandbox_id,
timeout=timeout,
envs=envs,
**opts,
)

Expand Down
6 changes: 5 additions & 1 deletion packages/python-sdk/e2b/sandbox_async/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ async def _cls_connect(
cls,
sandbox_id: str,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Sandbox:
timeout = timeout or SandboxBase.default_sandbox_timeout
Expand All @@ -395,7 +396,10 @@ async def _cls_connect(
res = await post_sandboxes_sandbox_id_connect.asyncio_detailed(
sandbox_id,
client=api_client,
body=ConnectSandbox(timeout=timeout),
body=ConnectSandbox(
timeout=timeout,
env_vars=envs if envs is not None else UNSET,
),
)

if res.status_code == 404:
Expand Down
12 changes: 12 additions & 0 deletions packages/python-sdk/e2b/sandbox_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ def create(
def connect(
self,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Self:
"""
Expand All @@ -257,6 +258,8 @@ def connect(

:param timeout: Timeout for the sandbox in **seconds**
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
:param envs: Custom environment variables to set in the sandbox on reconnect.
Merged into the sandbox environment and applied to processes started after resume.
:return: A running sandbox instance

@example
Expand All @@ -276,6 +279,7 @@ def connect(
def connect(
sandbox_id: str,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
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 Handle envs in sync class connect path

Adding envs to Sandbox.connect(sandbox_id, ...) exposes a runtime failure for class-style calls: when users call Sandbox.connect("...", envs={...}), the decorator routes to _cls_connect_sandbox, which does not declare envs, so envs stays inside opts and is later expanded into ConnectionConfig(..., **opts), raising TypeError for unexpected keyword envs. This makes the newly documented sync classmethod envs option unusable.

Useful? React with 👍 / 👎.

**opts: Unpack[ApiParams],
) -> "Sandbox":
"""
Expand All @@ -287,6 +291,8 @@ def connect(
:param sandbox_id: Sandbox ID
:param timeout: Timeout for the sandbox in **seconds**.
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
:param envs: Custom environment variables to set in the sandbox on reconnect.
Merged into the sandbox environment and applied to processes started after resume.
:return: A running sandbox instance

@example
Expand All @@ -304,6 +310,7 @@ def connect(
def connect(
self,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Self:
"""
Expand All @@ -314,6 +321,8 @@ def connect(

:param timeout: Timeout for the sandbox in **seconds**.
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
:param envs: Custom environment variables to set in the sandbox on reconnect.
Merged into the sandbox environment and applied to processes started after resume.
:return: A running sandbox instance

@example
Expand All @@ -328,6 +337,7 @@ def connect(
SandboxApi._cls_connect(
sandbox_id=self.sandbox_id,
timeout=timeout,
envs=envs,
**self.connection_config.get_api_params(**opts),
)

Expand Down Expand Up @@ -848,11 +858,13 @@ def _cls_connect_sandbox(
cls,
sandbox_id: str,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Self:
sandbox = SandboxApi._cls_connect(
sandbox_id=sandbox_id,
timeout=timeout,
envs=envs,
**opts,
)

Expand Down
6 changes: 5 additions & 1 deletion packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def _cls_connect(
cls,
sandbox_id: str,
timeout: Optional[int] = None,
envs: Optional[Dict[str, str]] = None,
**opts: Unpack[ApiParams],
) -> Sandbox:
timeout = timeout or SandboxBase.default_sandbox_timeout
Expand All @@ -303,7 +304,10 @@ def _cls_connect(
res = post_sandboxes_sandbox_id_connect.sync_detailed(
sandbox_id,
client=api_client,
body=ConnectSandbox(timeout=timeout),
body=ConnectSandbox(
timeout=timeout,
env_vars=envs if envs is not None else UNSET,
),
)

if res.status_code == 404:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,58 @@ async def test_pause_passes_connection_config_without_overrides(monkeypatch):
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]


@pytest.mark.skip_debug()
async def test_connect_forwards_envs(monkeypatch):
mock_connect = AsyncMock(return_value=None)
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)

sandbox = create_sandbox(monkeypatch)
await sandbox.connect(envs={"MY_KEY": "my_value"})

mock_connect.assert_awaited_once()
assert mock_connect.call_args.kwargs["envs"] == {"MY_KEY": "my_value"}


@pytest.mark.skip_debug()
async def test_connect_envs_is_none_when_not_provided(monkeypatch):
mock_connect = AsyncMock(return_value=None)
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)

sandbox = create_sandbox(monkeypatch)
await sandbox.connect()

mock_connect.assert_awaited_once()
assert mock_connect.call_args.kwargs.get("envs") is None


@pytest.mark.skip_debug()
async def test_classmethod_connect_forwards_envs_without_polluting_opts(monkeypatch):
# Regression: AsyncSandbox.connect(id, envs=...) previously passed envs into
# **opts, which then reached ConnectionConfig(**opts) and raised TypeError.
mock_cls_connect = AsyncMock(return_value=SimpleNamespace(
sandbox_id="sbx-test",
domain="sandbox.e2b.dev",
envd_version="0.2.4",
envd_access_token="tok",
traffic_access_token="tok",
))
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_cls_connect)
monkeypatch.setattr(
sandbox_async_main, "get_transport", lambda *_args, **_kwargs: SimpleNamespace(pool=object())
)
monkeypatch.setattr(sandbox_async_main.httpx, "AsyncClient", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_async_main, "Filesystem", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_async_main, "Commands", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_async_main, "Pty", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_async_main, "Git", lambda *args, **kwargs: object())

# This must not raise TypeError: ConnectionConfig() got unexpected keyword 'envs'
await sandbox_async_main.AsyncSandbox.connect("sbx-test", envs={"MY_KEY": "my_value"}, api_key="test-key")

mock_cls_connect.assert_awaited_once()
assert mock_cls_connect.call_args.kwargs["envs"] == {"MY_KEY": "my_value"}


@pytest.mark.skip_debug()
async def test_pause_applies_overrides(monkeypatch):
mock_pause = AsyncMock(return_value="sbx-test")
Expand Down
Loading