diff --git a/.changeset/seven-cooks-tie.md b/.changeset/seven-cooks-tie.md new file mode 100644 index 0000000000..119f267b16 --- /dev/null +++ b/.changeset/seven-cooks-tie.md @@ -0,0 +1,6 @@ +--- +"e2b": patch +"@e2b/python-sdk": patch +--- + +Support overriding sandbox API URL diff --git a/.github/workflows/cli_tests.yml b/.github/workflows/cli_tests.yml index 59ba494477..fd105a721b 100644 --- a/.github/workflows/cli_tests.yml +++ b/.github/workflows/cli_tests.yml @@ -14,9 +14,6 @@ permissions: jobs: test: - defaults: - run: - working-directory: ./packages/cli name: CLI - Build runs-on: ubuntu-22.04 steps: @@ -32,7 +29,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - id: pnpm-install with: version: '${{ env.TOOL_VERSION_PNPM }}' @@ -47,15 +43,20 @@ jobs: - name: Configure pnpm run: | pnpm config set auto-install-peers true - pnpm config set exclude-links-from-lockfile true - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Test build + - name: Build the SDK (pre-requisite for the tests) run: pnpm build + working-directory: ./packages/js-sdk + + - name: Build the CLI + run: pnpm build + working-directory: ./packages/cli - name: Run tests run: pnpm test + working-directory: ./packages/cli env: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} diff --git a/packages/js-sdk/src/connectionConfig.ts b/packages/js-sdk/src/connectionConfig.ts index edcb9ef222..298dff1dd9 100644 --- a/packages/js-sdk/src/connectionConfig.ts +++ b/packages/js-sdk/src/connectionConfig.ts @@ -35,6 +35,12 @@ export interface ConnectionOpts { * @default E2B_API_URL // environment variable or `https://api.${domain}` */ apiUrl?: string + /** + * Sandbox Url to use for the API. + * @internal + * @default E2B_SANDBOX_URL // environment variable or `https://${port}-${sandboxID}.${domain}` + */ + sandboxUrl?: string /** * If true the SDK starts in the debug mode and connects to the local envd API server. * @internal @@ -62,9 +68,12 @@ export interface ConnectionOpts { * Configuration for connecting to the API. */ export class ConnectionConfig { + public static envdPort = 49983 + readonly debug: boolean readonly domain: string readonly apiUrl: string + readonly sandboxUrl?: string readonly logger?: Logger readonly requestTimeoutMs: number @@ -88,6 +97,8 @@ export class ConnectionConfig { opts?.apiUrl || ConnectionConfig.apiUrl || (this.debug ? 'http://localhost:3000' : `https://api.${this.domain}`) + + this.sandboxUrl = opts?.sandboxUrl || ConnectionConfig.sandboxUrl } private static get domain() { @@ -98,6 +109,10 @@ export class ConnectionConfig { return getEnvVar('E2B_API_URL') } + private static get sandboxUrl() { + return getEnvVar('E2B_SANDBOX_URL') + } + private static get debug() { return (getEnvVar('E2B_DEBUG') || 'false').toLowerCase() === 'true' } @@ -115,6 +130,25 @@ export class ConnectionConfig { return timeout ? AbortSignal.timeout(timeout) : undefined } + + getSandboxUrl( + sandboxId: string, + opts: { sandboxDomain: string; envdPort: number } + ) { + if (this.sandboxUrl) { + return this.sandboxUrl + } + + return `${this.debug ? 'http' : 'https'}://${this.getHost(sandboxId, opts.envdPort, opts.sandboxDomain)}` + } + + getHost(sandboxId: string, port: number, sandboxDomain: string) { + if (this.debug) { + return `localhost:${port}` + } + + return `${port}-${sandboxId}.${sandboxDomain ?? this.domain}` + } } /** diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index 9d08bed898..524bf33602 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -123,9 +123,15 @@ export class Sandbox extends SandboxApi { this.sandboxDomain = opts.sandboxDomain ?? this.connectionConfig.domain this.envdAccessToken = opts.envdAccessToken - this.envdApiUrl = `${ - this.connectionConfig.debug ? 'http' : 'https' - }://${this.getHost(this.envdPort)}` + this.envdApiUrl = this.connectionConfig.getSandboxUrl(this.sandboxId, { + sandboxDomain: this.sandboxDomain, + envdPort: this.envdPort, + }) + + const sandboxHeaders = { + 'E2b-Sandbox-Id': this.sandboxId, + 'E2b-Sandbox-Port': this.envdPort.toString(), + } const rpcTransport = createConnectTransport({ baseUrl: this.envdApiUrl, @@ -141,6 +147,9 @@ export class Sandbox extends SandboxApi { new Headers(options?.headers).forEach((value, key) => headers.append(key, value) ) + new Headers(sandboxHeaders).forEach((value, key) => + headers.append(key, value) + ) if (this.envdAccessToken) { headers.append('X-Access-Token', this.envdAccessToken) @@ -459,11 +468,11 @@ export class Sandbox extends SandboxApi { * ``` */ getHost(port: number) { - if (this.connectionConfig.debug) { - return `localhost:${port}` - } - - return `${port}-${this.sandboxId}.${this.sandboxDomain}` + return this.connectionConfig.getHost( + this.sandboxId, + port, + this.sandboxDomain + ) } /** diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 6599d46e83..0c49d349b2 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -90,6 +90,11 @@ export interface SandboxOpts extends ConnectionOpts { * @default undefined */ mcp?: McpServer + + /** + * Sandbox URL. Used for local development + */ + sandboxUrl?: string } export type SandboxBetaCreateOpts = SandboxOpts & { diff --git a/packages/python-sdk/e2b/connection_config.py b/packages/python-sdk/e2b/connection_config.py index e20ae4ba07..efc89091a6 100644 --- a/packages/python-sdk/e2b/connection_config.py +++ b/packages/python-sdk/e2b/connection_config.py @@ -40,12 +40,17 @@ class ApiParams(TypedDict, total=False): proxy: Optional[ProxyTypes] """Proxy to use for the request. In case of a sandbox it applies to all **requests made to the returned sandbox**.""" + sandbox_url: Optional[str] + """URL to connect to sandbox, defaults to `E2B_SANDBOX_URL` environment variable.""" + class ConnectionConfig: """ Configuration for the connection to the API. """ + envd_port = 49983 + @staticmethod def _domain(): return os.getenv("E2B_DOMAIN") or "e2b.app" @@ -62,6 +67,10 @@ def _api_key(): def _api_url(): return os.getenv("E2B_API_URL") + @staticmethod + def _sandbox_url(): + return os.getenv("E2B_SANDBOX_URL") + @staticmethod def _access_token(): return os.getenv("E2B_ACCESS_TOKEN") @@ -72,6 +81,7 @@ def __init__( debug: Optional[bool] = None, api_key: Optional[str] = None, api_url: Optional[str] = None, + sandbox_url: Optional[str] = None, access_token: Optional[str] = None, request_timeout: Optional[float] = None, headers: Optional[Dict[str, str]] = None, @@ -106,6 +116,8 @@ def __init__( or ("http://localhost:3000" if self.debug else f"https://api.{self.domain}") ) + self._sandbox_url = sandbox_url or ConnectionConfig._sandbox_url() + @staticmethod def _get_request_timeout( default_timeout: Optional[float], @@ -121,6 +133,28 @@ def _get_request_timeout( def get_request_timeout(self, request_timeout: Optional[float] = None): return self._get_request_timeout(self.request_timeout, request_timeout) + def get_sandbox_url(self, sandbox_id: str, sandbox_domain: str) -> str: + if self._sandbox_url: + return self._sandbox_url + + return f"{'http' if self.debug else 'https'}://{self.get_host(sandbox_id, sandbox_domain, self.envd_port)}" + + def get_host(self, sandbox_id: str, sandbox_domain: str, port: int) -> str: + """ + Get the host address to connect to the sandbox. + You can then use this address to connect to the sandbox port from outside the sandbox via HTTP or WebSocket. + + :param port: Port to connect to + :param sandbox_domain: Domain to connect to + :param sandbox_id: Sandbox to connect to + + :return: Host address to connect to + """ + if self.debug: + return f"localhost:{port}" + + return f"{port}-{sandbox_id}.{sandbox_domain}" + def get_api_params( self, **opts: Unpack[ApiParams], diff --git a/packages/python-sdk/e2b/sandbox/main.py b/packages/python-sdk/e2b/sandbox/main.py index b0a5f3b221..c1b028a705 100644 --- a/packages/python-sdk/e2b/sandbox/main.py +++ b/packages/python-sdk/e2b/sandbox/main.py @@ -15,6 +15,7 @@ class SandboxOpts(TypedDict): sandbox_domain: Optional[str] envd_version: Version envd_access_token: Optional[str] + sandbox_url: Optional[str] connection_config: ConnectionConfig @@ -25,7 +26,6 @@ class SandboxBase: keepalive_expiry=300, ) - envd_port = 49983 mcp_port = 50005 default_sandbox_timeout = 300 @@ -46,7 +46,9 @@ def __init__( self.__sandbox_domain = sandbox_domain or self.connection_config.domain self.__envd_version = envd_version self.__envd_access_token = envd_access_token - self.__envd_api_url = f"{'http' if self.connection_config.debug else 'https'}://{self.get_host(self.envd_port)}" + self.__envd_api_url = self.connection_config.get_sandbox_url( + self.sandbox_id, self.sandbox_domain + ) self.__mcp_token: Optional[str] = None @property @@ -195,10 +197,9 @@ def get_host(self, port: int) -> str: :return: Host address to connect to """ - if self.connection_config.debug: - return f"localhost:{port}" - - return f"{port}-{self.sandbox_id}.{self.sandbox_domain}" + return self.connection_config.get_host( + self.sandbox_id, self.sandbox_domain, port + ) def get_mcp_url(self) -> str: """ diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 66ad933064..6e514c9222 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -95,7 +95,9 @@ def __init__(self, **opts: Unpack[SandboxOpts]): limits=self._limits, proxy=self.connection_config.proxy ) self._envd_api = httpx.AsyncClient( - base_url=self.envd_api_url, + base_url=self.connection_config.get_sandbox_url( + self.sandbox_id, self.sandbox_domain + ), transport=self._transport, headers=self.connection_config.sandbox_headers, ) @@ -721,6 +723,9 @@ async def _create( ): extra_sandbox_headers["X-Access-Token"] = envd_access_token + extra_sandbox_headers["E2b-Sandbox-Id"] = sandbox_id + extra_sandbox_headers["E2b-Sandbox-Port"] = str(ConnectionConfig.envd_port) + connection_config = ConnectionConfig( extra_sandbox_headers=extra_sandbox_headers, **opts, diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index a41b75f27e..84bf11fa2b 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -310,6 +310,10 @@ async def _cls_connect( async with AsyncApiClient( config, limits=SandboxBase._limits, + headers={ + "E2b-Sandbox-Id": sandbox_id, + "E2b-Sandbox-Port": config.envd_port, + }, ) as api_client: res = await post_sandboxes_sandbox_id_connect.asyncio_detailed( sandbox_id, diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 9c39f74327..1e82a4bca5 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -718,6 +718,9 @@ def _create( ): extra_sandbox_headers["X-Access-Token"] = envd_access_token + extra_sandbox_headers["E2b-Sandbox-Id"] = sandbox_id + extra_sandbox_headers["E2b-Sandbox-Port"] = str(ConnectionConfig.envd_port) + connection_config = ConnectionConfig( extra_sandbox_headers=extra_sandbox_headers, **opts, diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 7052a1e9ff..1600e19dcd 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -264,6 +264,10 @@ def _cls_connect( with ApiClient( config, limits=SandboxBase._limits, + headers={ + "E2b-Sandbox-Id": sandbox_id, + "E2b-Sandbox-Port": config.envd_port, + }, ) as api_client: res = post_sandboxes_sandbox_id_connect.sync_detailed( sandbox_id, diff --git a/packages/python-sdk/e2b_connect/client.py b/packages/python-sdk/e2b_connect/client.py index 863ddf3d61..034a8478df 100644 --- a/packages/python-sdk/e2b_connect/client.py +++ b/packages/python-sdk/e2b_connect/client.py @@ -287,9 +287,10 @@ def _prepare_server_stream_request( req, request_timeout=None, timeout=None, - headers={}, + headers=None, **opts, ): + headers = headers or {} data = self._codec.encode(req) flags = EnvelopeFlags(0) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6707e60ff..54724ea9f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,7 +273,7 @@ importers: version: 0.6.1 e2b: specifier: ^2.6.1 - version: 2.6.1 + version: link:../js-sdk handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -4078,10 +4078,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - e2b@2.6.1: - resolution: {integrity: sha512-KYQOza4bYNWTbbwbTHNlgOVWXKtlq2c1H0I5jW1Q1kEFYqoKxjfZGtkuJjnOtmVpeiIKzsMtQ9n0e3WKHBDysQ==} - engines: {node: '>=20'} - e2b@2.6.4: resolution: {integrity: sha512-IhtNZxXomub24lSmq/eldOC4HfK8YqTpxGqL3MAxNlF4ggyS6hde9D0euW40zZL3VoCFD8wjcwKrdYtM1tVDZw==} engines: {node: '>=20'} @@ -12158,19 +12154,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - e2b@2.6.1: - dependencies: - '@bufbuild/protobuf': 2.6.2 - '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.6.2) - '@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.6.2)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.6.2)) - chalk: 5.3.0 - compare-versions: 6.1.1 - dockerfile-ast: 0.7.1 - glob: 11.0.3 - openapi-fetch: 0.14.1 - platform: 1.3.6 - tar: 7.4.3 - e2b@2.6.4: dependencies: '@bufbuild/protobuf': 2.6.2