Skip to content

Commit b52eb3c

Browse files
mishushakovclaude
andauthored
fix: debug-mode Sandbox.connect() and Unset token handling in Python connect() (#1428)
## Description `Sandbox.connect()` now short-circuits in debug mode instead of calling the control plane, matching `Sandbox.create()` — fixed in the JS SDK and both sync/async Python SDKs (static and instance variants). The Python SDK's `connect()` also previously passed the generated client's `Unset` sentinel through as the envd/traffic access tokens when they were absent (non-secure sandboxes), which made `download_url()`/`upload_url()` emit broken signed URLs; `_cls_connect` now normalizes the response into `SandboxCreateResponse` with proper `None` values, the same pattern `_create_sandbox` already uses. Dead `Unset` checks and the now-unused generated `Sandbox` model import were cleaned up, and unit tests cover both behaviors in all three implementations. Debug mode is resolved through `ConnectionConfig`, so the `E2B_DEBUG` env var triggers the short-circuit in both `connect()` and `create()`, not just an explicit `debug=True`. ## Usage ```ts // JS: works fully offline with E2B_DEBUG / debug: true (no control plane call) const sbx = await Sandbox.connect(sandboxId, { debug: true }) ``` ```python # Python: non-secure sandboxes get unsigned URLs again instead of broken signatures sbx = Sandbox.connect(sandbox_id) print(sbx.download_url("file.txt")) # no garbage signature when envd token is absent # Debug mode skips the control plane, like Sandbox.create() sbx = Sandbox.connect(sandbox_id, debug=True) ``` ## Testing New unit tests in `tests/sandbox/connect.test.ts`, `tests/sync/sandbox_sync/test_connect.py`, and `tests/async/sandbox_async/test_connect.py`; existing connect integration suites pass against the live API (8/8 sync Python, 8/8 async Python, 6/6 JS). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e0ed071 commit b52eb3c

11 files changed

Lines changed: 273 additions & 47 deletions

File tree

.changeset/wise-otters-connect.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"e2b": patch
3+
"@e2b/python-sdk": patch
4+
---
5+
6+
Skip the control plane request in `Sandbox.connect()` when running in debug mode, matching the behavior of `Sandbox.create()`. In the Python SDK, `Sandbox.connect()` now also normalizes missing envd and traffic access tokens to `None` instead of leaking the `Unset` sentinel, which previously broke `download_url()`/`upload_url()` for non-secure sandboxes.

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,16 @@ export class Sandbox extends SandboxApi {
357357
sandboxId: string,
358358
opts?: SandboxConnectOpts
359359
): Promise<InstanceType<S>> {
360-
const sandbox = await SandboxApi.connectSandbox(sandboxId, opts)
361360
const config = new ConnectionConfig(opts)
361+
if (config.debug) {
362+
return new this({
363+
sandboxId,
364+
envdVersion: ENVD_DEBUG_FALLBACK,
365+
...config,
366+
}) as InstanceType<S>
367+
}
368+
369+
const sandbox = await SandboxApi.connectSandbox(sandboxId, opts)
362370

363371
return new this({
364372
sandboxId,
@@ -390,6 +398,11 @@ export class Sandbox extends SandboxApi {
390398
* ```
391399
*/
392400
async connect(opts?: SandboxConnectOpts): Promise<this> {
401+
if (this.connectionConfig.debug) {
402+
// Skip connecting to the sandbox in debug mode
403+
return this
404+
}
405+
393406
await SandboxApi.connectSandbox(this.sandboxId, this.resolveApiOpts(opts))
394407

395408
return this

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
1-
import { assert, test, expect } from 'vitest'
1+
import { assert, test, expect, vi } from 'vitest'
22

33
import { Sandbox } from '../../src'
44
import { isDebug, sandboxTest, template } from '../setup.js'
55

6+
test('connect in debug mode does not call the API', async () => {
7+
const fetchSpy = vi.fn(() => {
8+
throw new Error('unexpected request in debug mode')
9+
})
10+
vi.stubGlobal('fetch', fetchSpy)
11+
12+
try {
13+
const sbx = await Sandbox.connect('debug-sandbox-id', {
14+
debug: true,
15+
apiKey: 'test-api-key',
16+
})
17+
assert.equal(sbx.sandboxId, 'debug-sandbox-id')
18+
19+
const sameSbx = await sbx.connect()
20+
assert.strictEqual(sameSbx, sbx)
21+
22+
expect(fetchSpy).not.toHaveBeenCalled()
23+
} finally {
24+
vi.unstubAllGlobals()
25+
}
26+
})
27+
628
test.skipIf(isDebug)('connect', async () => {
729
const sbx = await Sandbox.create(template, { timeoutMs: 10_000 })
830

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from packaging.version import Version
1010
from typing_extensions import Self, Unpack
1111

12-
from e2b.api.client.types import Unset
1312
from e2b.api.client_async import get_envd_transport as get_transport
1413
from e2b.connection_config import ApiParams, ConnectionConfig
1514
from e2b.envd.api import ENVD_API_HEALTH_ROUTE, ahandle_envd_api_exception
@@ -328,6 +327,10 @@ async def connect(
328327
same_sandbox = await sandbox.connect()
329328
```
330329
"""
330+
if self.connection_config.debug:
331+
# Skip connecting to the sandbox in debug mode
332+
return self
333+
331334
await SandboxApi._cls_connect(
332335
sandbox_id=self.sandbox_id,
333336
timeout=timeout,
@@ -850,18 +853,30 @@ async def _cls_connect_sandbox(
850853
timeout: Optional[int] = None,
851854
**opts: Unpack[ApiParams],
852855
) -> Self:
853-
sandbox = await SandboxApi._cls_connect(
854-
sandbox_id=sandbox_id,
855-
timeout=timeout,
856-
**opts,
857-
)
856+
debug = ConnectionConfig(**opts).debug
857+
if debug:
858+
sandbox_domain = None
859+
envd_version = ENVD_DEBUG_FALLBACK
860+
envd_access_token = None
861+
traffic_access_token = None
862+
else:
863+
sandbox = await SandboxApi._cls_connect(
864+
sandbox_id=sandbox_id,
865+
timeout=timeout,
866+
**opts,
867+
)
868+
869+
sandbox_id = sandbox.sandbox_id
870+
sandbox_domain = sandbox.sandbox_domain
871+
envd_version = Version(sandbox.envd_version)
872+
envd_access_token = sandbox.envd_access_token
873+
traffic_access_token = sandbox.traffic_access_token
858874

859875
sandbox_headers = {
860-
"E2b-Sandbox-Id": sandbox.sandbox_id,
876+
"E2b-Sandbox-Id": sandbox_id,
861877
"E2b-Sandbox-Port": str(ConnectionConfig.envd_port),
862878
}
863-
envd_access_token = sandbox.envd_access_token
864-
if envd_access_token is not None and not isinstance(envd_access_token, Unset):
879+
if envd_access_token is not None:
865880
sandbox_headers["X-Access-Token"] = envd_access_token
866881

867882
connection_config = ConnectionConfig(
@@ -870,11 +885,11 @@ async def _cls_connect_sandbox(
870885
)
871886

872887
return cls(
873-
sandbox_id=sandbox.sandbox_id,
874-
sandbox_domain=sandbox.domain,
875-
envd_version=Version(sandbox.envd_version),
888+
sandbox_id=sandbox_id,
889+
sandbox_domain=sandbox_domain,
890+
envd_version=envd_version,
876891
envd_access_token=envd_access_token,
877-
traffic_access_token=sandbox.traffic_access_token,
892+
traffic_access_token=traffic_access_token,
878893
connection_config=connection_config,
879894
)
880895

@@ -895,7 +910,7 @@ async def _create(
895910
) -> Self:
896911
extra_sandbox_headers = {}
897912

898-
debug = opts.get("debug")
913+
debug = ConnectionConfig(**opts).debug
899914
if debug:
900915
sandbox_id = "debug_sandbox_id"
901916
sandbox_domain = None
@@ -923,9 +938,7 @@ async def _create(
923938
envd_access_token = response.envd_access_token
924939
traffic_access_token = response.traffic_access_token
925940

926-
if envd_access_token is not None and not isinstance(
927-
envd_access_token, Unset
928-
):
941+
if envd_access_token is not None:
929942
extra_sandbox_headers["X-Access-Token"] = envd_access_token
930943

931944
extra_sandbox_headers["E2b-Sandbox-Id"] = sandbox_id

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
NewSandbox,
2424
PostSandboxesSandboxIDSnapshotsBody,
2525
PostSandboxesSandboxIDTimeoutBody,
26-
Sandbox,
2726
SandboxAutoResumeConfig,
2827
SandboxNetworkConfig,
2928
SandboxVolumeMount as SandboxVolumeMountAPI,
@@ -416,7 +415,7 @@ async def _cls_connect(
416415
sandbox_id: str,
417416
timeout: Optional[int] = None,
418417
**opts: Unpack[ApiParams],
419-
) -> Sandbox:
418+
) -> SandboxCreateResponse:
420419
timeout = timeout or SandboxBase.default_sandbox_timeout
421420

422421
# Sandbox is not running, resume it
@@ -442,4 +441,22 @@ async def _cls_connect(
442441
if res.parsed is None:
443442
raise Exception("Body of the request is None")
444443

445-
return res.parsed
444+
domain = res.parsed.domain if isinstance(res.parsed.domain, str) else None
445+
envd_token = (
446+
res.parsed.envd_access_token
447+
if isinstance(res.parsed.envd_access_token, str)
448+
else None
449+
)
450+
traffic_token = (
451+
res.parsed.traffic_access_token
452+
if isinstance(res.parsed.traffic_access_token, str)
453+
else None
454+
)
455+
456+
return SandboxCreateResponse(
457+
sandbox_id=res.parsed.sandbox_id,
458+
sandbox_domain=domain,
459+
envd_version=res.parsed.envd_version,
460+
envd_access_token=envd_token,
461+
traffic_access_token=traffic_token,
462+
)

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from packaging.version import Version
1010
from typing_extensions import Self, Unpack
1111

12-
from e2b.api.client.types import Unset
1312
from e2b.connection_config import ApiParams, ConnectionConfig
1413
from e2b.envd.api import ENVD_API_HEALTH_ROUTE, handle_envd_api_exception
1514
from e2b.envd.versions import ENVD_DEBUG_FALLBACK
@@ -317,6 +316,10 @@ def connect(
317316
same_sandbox = sandbox.connect()
318317
```
319318
"""
319+
if self.connection_config.debug:
320+
# Skip connecting to the sandbox in debug mode
321+
return self
322+
320323
SandboxApi._cls_connect(
321324
sandbox_id=self.sandbox_id,
322325
timeout=timeout,
@@ -840,18 +843,30 @@ def _cls_connect_sandbox(
840843
timeout: Optional[int] = None,
841844
**opts: Unpack[ApiParams],
842845
) -> Self:
843-
sandbox = SandboxApi._cls_connect(
844-
sandbox_id=sandbox_id,
845-
timeout=timeout,
846-
**opts,
847-
)
846+
debug = ConnectionConfig(**opts).debug
847+
if debug:
848+
sandbox_domain = None
849+
envd_version = ENVD_DEBUG_FALLBACK
850+
envd_access_token = None
851+
traffic_access_token = None
852+
else:
853+
sandbox = SandboxApi._cls_connect(
854+
sandbox_id=sandbox_id,
855+
timeout=timeout,
856+
**opts,
857+
)
858+
859+
sandbox_id = sandbox.sandbox_id
860+
sandbox_domain = sandbox.sandbox_domain
861+
envd_version = Version(sandbox.envd_version)
862+
envd_access_token = sandbox.envd_access_token
863+
traffic_access_token = sandbox.traffic_access_token
848864

849865
sandbox_headers = {
850-
"E2b-Sandbox-Id": sandbox.sandbox_id,
866+
"E2b-Sandbox-Id": sandbox_id,
851867
"E2b-Sandbox-Port": str(ConnectionConfig.envd_port),
852868
}
853-
envd_access_token = sandbox.envd_access_token
854-
if envd_access_token is not None and not isinstance(envd_access_token, Unset):
869+
if envd_access_token is not None:
855870
sandbox_headers["X-Access-Token"] = envd_access_token
856871

857872
connection_config = ConnectionConfig(
@@ -860,11 +875,11 @@ def _cls_connect_sandbox(
860875
)
861876

862877
return cls(
863-
sandbox_id=sandbox.sandbox_id,
864-
sandbox_domain=sandbox.domain,
865-
envd_version=Version(sandbox.envd_version),
878+
sandbox_id=sandbox_id,
879+
sandbox_domain=sandbox_domain,
880+
envd_version=envd_version,
866881
envd_access_token=envd_access_token,
867-
traffic_access_token=sandbox.traffic_access_token,
882+
traffic_access_token=traffic_access_token,
868883
connection_config=connection_config,
869884
)
870885

@@ -885,7 +900,7 @@ def _create(
885900
) -> Self:
886901
extra_sandbox_headers = {}
887902

888-
debug = opts.get("debug")
903+
debug = ConnectionConfig(**opts).debug
889904
if debug:
890905
sandbox_id = "debug_sandbox_id"
891906
sandbox_domain = None
@@ -913,9 +928,7 @@ def _create(
913928
envd_access_token = response.envd_access_token
914929
traffic_access_token = response.traffic_access_token
915930

916-
if envd_access_token is not None and not isinstance(
917-
envd_access_token, Unset
918-
):
931+
if envd_access_token is not None:
919932
extra_sandbox_headers["X-Access-Token"] = envd_access_token
920933

921934
extra_sandbox_headers["E2b-Sandbox-Id"] = sandbox_id

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
NewSandbox,
2424
PostSandboxesSandboxIDSnapshotsBody,
2525
PostSandboxesSandboxIDTimeoutBody,
26-
Sandbox,
2726
SandboxAutoResumeConfig,
2827
SandboxNetworkConfig,
2928
SandboxVolumeMount as SandboxVolumeMountAPI,
@@ -320,7 +319,7 @@ def _cls_connect(
320319
sandbox_id: str,
321320
timeout: Optional[int] = None,
322321
**opts: Unpack[ApiParams],
323-
) -> Sandbox:
322+
) -> SandboxCreateResponse:
324323
timeout = timeout or SandboxBase.default_sandbox_timeout
325324

326325
config = ConnectionConfig(**opts)
@@ -344,7 +343,25 @@ def _cls_connect(
344343
if res.parsed is None:
345344
raise Exception("Body of the request is None")
346345

347-
return res.parsed
346+
domain = res.parsed.domain if isinstance(res.parsed.domain, str) else None
347+
envd_token = (
348+
res.parsed.envd_access_token
349+
if isinstance(res.parsed.envd_access_token, str)
350+
else None
351+
)
352+
traffic_token = (
353+
res.parsed.traffic_access_token
354+
if isinstance(res.parsed.traffic_access_token, str)
355+
else None
356+
)
357+
358+
return SandboxCreateResponse(
359+
sandbox_id=res.parsed.sandbox_id,
360+
sandbox_domain=domain,
361+
envd_version=res.parsed.envd_version,
362+
envd_access_token=envd_token,
363+
traffic_access_token=traffic_token,
364+
)
348365

349366
@classmethod
350367
def _cls_create_snapshot(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from packaging.version import Version
66

77
from e2b import AsyncSandbox
8+
from e2b.api import SandboxCreateResponse
89
from e2b.connection_config import ConnectionConfig
910
import e2b.sandbox_async.main as sandbox_async_main
1011

@@ -93,9 +94,9 @@ async def test_pause_applies_overrides(monkeypatch, test_api_key):
9394
@pytest.mark.skip_debug()
9495
async def test_connect_sets_stable_host_routing_headers(monkeypatch, test_api_key):
9596
mock_connect = AsyncMock(
96-
return_value=SimpleNamespace(
97+
return_value=SandboxCreateResponse(
9798
sandbox_id="sbx-test",
98-
domain="e2b.app",
99+
sandbox_domain="e2b.app",
99100
envd_version="0.4.0",
100101
envd_access_token="tok",
101102
traffic_access_token="traffic",

0 commit comments

Comments
 (0)