Skip to content

Commit b1d126e

Browse files
FisherXZclaude
andcommitted
sdk: allow envs when connecting to a sandbox
Adds optional envs parameter to Sandbox.connect() in JS and Python SDKs. Maps to envVars on the wire, matching create-time convention. Uses UNSET (not None) in Python so the field is omitted when not supplied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6217b16 commit b1d126e

8 files changed

Lines changed: 124 additions & 2 deletions

File tree

packages/js-sdk/src/sandbox/sandboxApi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ export type SandboxConnectOpts = ConnectionOpts & {
200200
* @default 300_000 // 5 minutes
201201
*/
202202
timeoutMs?: number
203+
/**
204+
* Custom environment variables to set in the sandbox on reconnect.
205+
* These are merged into the sandbox environment and applied to processes started after resume.
206+
*/
207+
envs?: Record<string, string>
203208
}
204209

205210
/**
@@ -845,6 +850,7 @@ export class SandboxApi {
845850
},
846851
body: {
847852
timeout: timeoutToSeconds(timeoutMs),
853+
...(opts?.envs !== undefined ? { envVars: opts.envs } : {}),
848854
},
849855
signal: config.getSignal(opts?.requestTimeoutMs),
850856
})

packages/js-sdk/tests/sandbox/configPropagation.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,43 @@ describe('Sandbox API config propagation', () => {
5757
assert.equal(opts?.debug, baseConfig.debug)
5858
})
5959
})
60+
61+
describe('Sandbox connect envs propagation', () => {
62+
const mockConnectResult = {
63+
sandboxId: 'sbx-test',
64+
sandboxDomain: 'sandbox.e2b.dev',
65+
envdVersion: '0.2.4',
66+
envdAccessToken: 'tok',
67+
trafficAccessToken: 'tok',
68+
}
69+
70+
afterEach(() => {
71+
vi.restoreAllMocks()
72+
})
73+
74+
test('forwards envs to connectSandbox when provided', async () => {
75+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76+
const connectSpy = vi
77+
.spyOn(SandboxApi as any, 'connectSandbox')
78+
.mockResolvedValue(mockConnectResult)
79+
const sandbox = createSandbox()
80+
81+
await sandbox.connect({ envs: { MY_KEY: 'my_value' } })
82+
83+
const opts = connectSpy.mock.calls[0][1]
84+
assert.deepEqual(opts?.envs, { MY_KEY: 'my_value' })
85+
})
86+
87+
test('does not include envs in opts when not provided', async () => {
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
const connectSpy = vi
90+
.spyOn(SandboxApi as any, 'connectSandbox')
91+
.mockResolvedValue(mockConnectResult)
92+
const sandbox = createSandbox()
93+
94+
await sandbox.connect()
95+
96+
const opts = connectSpy.mock.calls[0][1]
97+
assert.isUndefined(opts?.envs)
98+
})
99+
})

packages/python-sdk/e2b/sandbox_async/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ async def create(
248248
async def connect(
249249
self,
250250
timeout: Optional[int] = None,
251+
envs: Optional[Dict[str, str]] = None,
251252
**opts: Unpack[ApiParams],
252253
) -> Self:
253254
"""
@@ -258,6 +259,8 @@ async def connect(
258259
259260
:param timeout: Timeout for the sandbox in **seconds**
260261
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
262+
:param envs: Custom environment variables to set in the sandbox on reconnect.
263+
Merged into the sandbox environment and applied to processes started after resume.
261264
:return: A running sandbox instance
262265
263266
@example
@@ -276,6 +279,7 @@ async def connect(
276279
async def connect(
277280
sandbox_id: str,
278281
timeout: Optional[int] = None,
282+
envs: Optional[Dict[str, str]] = None,
279283
**opts: Unpack[ApiParams],
280284
) -> "AsyncSandbox":
281285
"""
@@ -287,6 +291,8 @@ async def connect(
287291
:param sandbox_id: Sandbox ID
288292
:param timeout: Timeout for the sandbox in **seconds**
289293
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
294+
:param envs: Custom environment variables to set in the sandbox on reconnect.
295+
Merged into the sandbox environment and applied to processes started after resume.
290296
:return: A running sandbox instance
291297
292298
@example
@@ -304,6 +310,7 @@ async def connect(
304310
async def connect(
305311
self,
306312
timeout: Optional[int] = None,
313+
envs: Optional[Dict[str, str]] = None,
307314
**opts: Unpack[ApiParams],
308315
) -> Self:
309316
"""
@@ -314,6 +321,8 @@ async def connect(
314321
315322
:param timeout: Timeout for the sandbox in **seconds**
316323
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
324+
:param envs: Custom environment variables to set in the sandbox on reconnect.
325+
Merged into the sandbox environment and applied to processes started after resume.
317326
:return: A running sandbox instance
318327
319328
@example
@@ -328,6 +337,7 @@ async def connect(
328337
await SandboxApi._cls_connect(
329338
sandbox_id=self.sandbox_id,
330339
timeout=timeout,
340+
envs=envs,
331341
**self.connection_config.get_api_params(**opts),
332342
)
333343

packages/python-sdk/e2b/sandbox_async/sandbox_api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ async def _cls_connect(
378378
cls,
379379
sandbox_id: str,
380380
timeout: Optional[int] = None,
381+
envs: Optional[Dict[str, str]] = None,
381382
**opts: Unpack[ApiParams],
382383
) -> Sandbox:
383384
timeout = timeout or SandboxBase.default_sandbox_timeout
@@ -395,7 +396,10 @@ async def _cls_connect(
395396
res = await post_sandboxes_sandbox_id_connect.asyncio_detailed(
396397
sandbox_id,
397398
client=api_client,
398-
body=ConnectSandbox(timeout=timeout),
399+
body=ConnectSandbox(
400+
timeout=timeout,
401+
env_vars=envs if envs is not None else UNSET,
402+
),
399403
)
400404

401405
if res.status_code == 404:

packages/python-sdk/e2b/sandbox_sync/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def create(
246246
def connect(
247247
self,
248248
timeout: Optional[int] = None,
249+
envs: Optional[Dict[str, str]] = None,
249250
**opts: Unpack[ApiParams],
250251
) -> Self:
251252
"""
@@ -256,6 +257,8 @@ def connect(
256257
257258
:param timeout: Timeout for the sandbox in **seconds**
258259
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
260+
:param envs: Custom environment variables to set in the sandbox on reconnect.
261+
Merged into the sandbox environment and applied to processes started after resume.
259262
:return: A running sandbox instance
260263
261264
@example
@@ -275,6 +278,7 @@ def connect(
275278
def connect(
276279
sandbox_id: str,
277280
timeout: Optional[int] = None,
281+
envs: Optional[Dict[str, str]] = None,
278282
**opts: Unpack[ApiParams],
279283
) -> "Sandbox":
280284
"""
@@ -286,6 +290,8 @@ def connect(
286290
:param sandbox_id: Sandbox ID
287291
:param timeout: Timeout for the sandbox in **seconds**.
288292
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
293+
:param envs: Custom environment variables to set in the sandbox on reconnect.
294+
Merged into the sandbox environment and applied to processes started after resume.
289295
:return: A running sandbox instance
290296
291297
@example
@@ -303,6 +309,7 @@ def connect(
303309
def connect(
304310
self,
305311
timeout: Optional[int] = None,
312+
envs: Optional[Dict[str, str]] = None,
306313
**opts: Unpack[ApiParams],
307314
) -> Self:
308315
"""
@@ -313,6 +320,8 @@ def connect(
313320
314321
:param timeout: Timeout for the sandbox in **seconds**.
315322
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
323+
:param envs: Custom environment variables to set in the sandbox on reconnect.
324+
Merged into the sandbox environment and applied to processes started after resume.
316325
:return: A running sandbox instance
317326
318327
@example
@@ -327,6 +336,7 @@ def connect(
327336
SandboxApi._cls_connect(
328337
sandbox_id=self.sandbox_id,
329338
timeout=timeout,
339+
envs=envs,
330340
**self.connection_config.get_api_params(**opts),
331341
)
332342

packages/python-sdk/e2b/sandbox_sync/sandbox_api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def _cls_connect(
287287
cls,
288288
sandbox_id: str,
289289
timeout: Optional[int] = None,
290+
envs: Optional[Dict[str, str]] = None,
290291
**opts: Unpack[ApiParams],
291292
) -> Sandbox:
292293
timeout = timeout or SandboxBase.default_sandbox_timeout
@@ -303,7 +304,10 @@ def _cls_connect(
303304
res = post_sandboxes_sandbox_id_connect.sync_detailed(
304305
sandbox_id,
305306
client=api_client,
306-
body=ConnectSandbox(timeout=timeout),
307+
body=ConnectSandbox(
308+
timeout=timeout,
309+
env_vars=envs if envs is not None else UNSET,
310+
),
307311
)
308312

309313
if res.status_code == 404:

packages/python-sdk/tests/async/sandbox_async/test_config_propagation.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,30 @@ async def test_pause_passes_connection_config_without_overrides(monkeypatch):
6767
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]
6868

6969

70+
@pytest.mark.skip_debug()
71+
async def test_connect_forwards_envs(monkeypatch):
72+
mock_connect = AsyncMock(return_value=None)
73+
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)
74+
75+
sandbox = create_sandbox(monkeypatch)
76+
await sandbox.connect(envs={"MY_KEY": "my_value"})
77+
78+
mock_connect.assert_awaited_once()
79+
assert mock_connect.call_args.kwargs["envs"] == {"MY_KEY": "my_value"}
80+
81+
82+
@pytest.mark.skip_debug()
83+
async def test_connect_envs_is_none_when_not_provided(monkeypatch):
84+
mock_connect = AsyncMock(return_value=None)
85+
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)
86+
87+
sandbox = create_sandbox(monkeypatch)
88+
await sandbox.connect()
89+
90+
mock_connect.assert_awaited_once()
91+
assert mock_connect.call_args.kwargs.get("envs") is None
92+
93+
7094
@pytest.mark.skip_debug()
7195
async def test_pause_applies_overrides(monkeypatch):
7296
mock_pause = AsyncMock(return_value="sbx-test")

packages/python-sdk/tests/sync/sandbox_sync/test_config_propagation.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,30 @@ def test_pause_passes_connection_config_without_overrides(monkeypatch):
6565
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]
6666

6767

68+
@pytest.mark.skip_debug()
69+
def test_connect_forwards_envs(monkeypatch):
70+
mock_connect = Mock(return_value=None)
71+
monkeypatch.setattr(sandbox_sync_main.SandboxApi, "_cls_connect", mock_connect)
72+
73+
sandbox = create_sandbox(monkeypatch)
74+
sandbox.connect(envs={"MY_KEY": "my_value"})
75+
76+
mock_connect.assert_called_once()
77+
assert mock_connect.call_args.kwargs["envs"] == {"MY_KEY": "my_value"}
78+
79+
80+
@pytest.mark.skip_debug()
81+
def test_connect_envs_is_none_when_not_provided(monkeypatch):
82+
mock_connect = Mock(return_value=None)
83+
monkeypatch.setattr(sandbox_sync_main.SandboxApi, "_cls_connect", mock_connect)
84+
85+
sandbox = create_sandbox(monkeypatch)
86+
sandbox.connect()
87+
88+
mock_connect.assert_called_once()
89+
assert mock_connect.call_args.kwargs.get("envs") is None
90+
91+
6892
@pytest.mark.skip_debug()
6993
def test_pause_applies_overrides(monkeypatch):
7094
mock_pause = Mock(return_value="sbx-test")

0 commit comments

Comments
 (0)