From 8722033f07f78d9c58f00ecfda6f646325e4d84d Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:17:44 +0200 Subject: [PATCH 1/7] feat(sdk): add iam workload identity option to Sandbox.create Adds the `iam` creation option from the infra spec (SandboxIam / SandboxIamTokens / SandboxIamToken, already present in the pinned spec and generated clients): a non-empty `tokens` map of name -> { audience, tokenType } enables workload identity for the sandbox. - JS: `SandboxIamOpts` / `SandboxIamToken` types, `iam` on `SandboxOpts`, passed through in the POST /sandboxes body. - Python (sync + async): `SandboxIamOpts` / `SandboxIamToken` TypedDicts, `iam` kwarg on `Sandbox.create` / `AsyncSandbox.create`, converted to the generated client models via shared `build_iam_config`. Co-Authored-By: Claude Fable 5 --- .changeset/sandbox-iam-tokens.md | 26 ++++++++++ packages/js-sdk/src/index.ts | 2 + packages/js-sdk/src/sandbox/sandboxApi.ts | 44 ++++++++++++++++ packages/js-sdk/tests/sandbox/iam.test.ts | 52 +++++++++++++++++++ packages/python-sdk/e2b/__init__.py | 5 ++ .../python-sdk/e2b/sandbox/sandbox_api.py | 52 +++++++++++++++++++ packages/python-sdk/e2b/sandbox_async/main.py | 6 +++ .../e2b/sandbox_async/sandbox_api.py | 4 ++ packages/python-sdk/e2b/sandbox_sync/main.py | 6 +++ .../e2b/sandbox_sync/sandbox_api.py | 4 ++ .../tests/sync/sandbox_sync/test_create.py | 30 ++++++++++- 11 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 .changeset/sandbox-iam-tokens.md create mode 100644 packages/js-sdk/tests/sandbox/iam.test.ts diff --git a/.changeset/sandbox-iam-tokens.md b/.changeset/sandbox-iam-tokens.md new file mode 100644 index 0000000000..1f935ed8e7 --- /dev/null +++ b/.changeset/sandbox-iam-tokens.md @@ -0,0 +1,26 @@ +--- +'e2b': minor +'@e2b/python-sdk': minor +--- + +Add the `iam` option to `Sandbox.create` for configuring sandbox workload identity. Passing a non-empty `tokens` map (name → `{ audience, tokenType }`) enables workload identity for the sandbox: + +```ts +const sandbox = await Sandbox.create({ + iam: { + tokens: { + aws: { audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }, + }, + }, +}) +``` + +```python +sandbox = Sandbox.create( + iam={ + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "token_type": "JWT-SVID"}, + }, + }, +) +``` diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 074aa3fc9b..8724409792 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -62,6 +62,8 @@ export type { SandboxState, SandboxListOpts, SandboxPaginator, + SandboxIamOpts, + SandboxIamToken, SandboxNetworkOpts, SandboxNetworkInfo, SandboxNetworkSelector, diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 8cccdb67d4..b7d22b1b8b 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -200,6 +200,32 @@ export type SandboxNetworkUpdate = { allowInternetAccess?: boolean } +/** + * Workload token definition for sandbox workload identity. + */ +export interface SandboxIamToken { + /** + * Audience of the workload token, stored exactly as provided. + */ + audience: string + + /** + * Workload token type. + */ + tokenType: string +} + +/** + * Sandbox workload identity configuration. A non-empty `tokens` map enables + * workload identity for the sandbox. + */ +export interface SandboxIamOpts { + /** + * Named workload-token definitions, keyed by a caller-chosen token name. + */ + tokens?: Record +} + /** * What happens when the sandbox timeout is reached. Either the bare action * (`'pause'` / `'kill'`), or an object form that also controls the pause @@ -405,6 +431,23 @@ export interface SandboxOpts extends ConnectionOpts { */ network?: SandboxNetworkOpts + /** + * Sandbox workload identity configuration. Providing a non-empty + * `tokens` map enables workload identity for the sandbox. + * + * @example + * ```ts + * const sandbox = await Sandbox.create({ + * iam: { + * tokens: { + * aws: { audience: 'sts.amazonaws.com', tokenType: 'ID-JAG' }, + * }, + * }, + * }) + * ``` + */ + iam?: SandboxIamOpts + /** * Volume mounts for the sandbox. * @@ -1211,6 +1254,7 @@ export class SandboxApi { secure: opts?.secure ?? true, allow_internet_access: opts?.allowInternetAccess ?? true, network: buildNetworkBody(opts?.network), + iam: opts?.iam, autoPause: action === 'pause', autoPauseMemory: action === 'pause' ? keepMemory : undefined, autoResume: { enabled: autoResume }, diff --git a/packages/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts new file mode 100644 index 0000000000..47a10b8588 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/iam.test.ts @@ -0,0 +1,52 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +import { Sandbox } from '../../src' +import { TEST_API_KEY, apiUrl } from '../setup' + +let lastCreateBody: Record | undefined + +const server = setupServer( + http.post(apiUrl('/sandboxes'), async ({ request }) => { + lastCreateBody = (await request.json()) as Record + return HttpResponse.json({ + sandboxID: 'test-sandbox-id', + templateID: 'base', + envdVersion: '0.2.4', + }) + }) +) + +beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })) + +afterAll(() => server.close()) + +afterEach(() => { + lastCreateBody = undefined + server.resetHandlers() +}) + +test('Sandbox.create sends iam tokens in the request body', async () => { + await Sandbox.create('base', { + apiKey: TEST_API_KEY, + iam: { + tokens: { + aws: { audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }, + }, + }, + }) + + expect(lastCreateBody?.iam).toEqual({ + tokens: { + aws: { audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }, + }, + }) +}) + +test('Sandbox.create omits iam from the request body when not provided', async () => { + await Sandbox.create('base', { apiKey: TEST_API_KEY }) + + expect(lastCreateBody).toBeDefined() + expect(lastCreateBody).not.toHaveProperty('iam') +}) diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index c8b65aa74a..514e62cb2e 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -75,6 +75,8 @@ GitHubMcpServer, GitHubMcpServerConfig, McpServer, + SandboxIamOpts, + SandboxIamToken, SandboxInfo, SandboxInfoLifecycle, SandboxMetrics, @@ -204,6 +206,9 @@ "SandboxLifecycle", "SandboxOnTimeout", "ALL_TRAFFIC", + # IAM + "SandboxIamOpts", + "SandboxIamToken", # Snapshot "SnapshotInfo", "SnapshotPaginator", diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index f5aaaffb6a..e85b4cdc90 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -38,6 +38,15 @@ from e2b.api.client.models import ( SandboxNetworkTransformHeaders as ClientSandboxNetworkTransformHeaders, ) +from e2b.api.client.models import ( + SandboxIam as ClientSandboxIam, +) +from e2b.api.client.models import ( + SandboxIamToken as ClientSandboxIamToken, +) +from e2b.api.client.models import ( + SandboxIamTokens as ClientSandboxIamTokens, +) from e2b.api.client.models import ( SandboxNetworkUpdateConfig, ) @@ -226,6 +235,28 @@ class SandboxNetworkUpdate(TypedDict, total=False): """ +class SandboxIamToken(TypedDict): + """ + Workload token definition for sandbox workload identity. + """ + + audience: str + """Audience of the workload token, stored exactly as provided.""" + + token_type: str + """Workload token type.""" + + +class SandboxIamOpts(TypedDict, total=False): + """ + Sandbox workload identity configuration. A non-empty ``tokens`` map enables + workload identity for the sandbox. + """ + + tokens: Dict[str, SandboxIamToken] + """Named workload-token definitions, keyed by a caller-chosen token name.""" + + class SandboxNetworkInfo(TypedDict, total=False): """ Network configuration as returned by the sandbox info endpoint. @@ -406,6 +437,27 @@ def build_network_config( return body +def build_iam_config( + iam: Optional[SandboxIamOpts], +) -> Optional[ClientSandboxIam]: + """Resolve a :class:`SandboxIamOpts` into the API client body.""" + if iam is None: + return None + + body = ClientSandboxIam() + tokens = iam.get("tokens") + if tokens is not None: + client_tokens = ClientSandboxIamTokens() + for name, token in tokens.items(): + client_tokens[name] = ClientSandboxIamToken( + audience=token["audience"], + token_type=token["token_type"], + ) + body.tokens = client_tokens + + return body + + def build_network_update_body( network: SandboxNetworkUpdate, ) -> SandboxNetworkUpdateConfig: diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 40ff5a2e1f..ae7b945031 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -22,6 +22,7 @@ from e2b.sandbox.main import SandboxOpts from e2b.sandbox.sandbox_api import ( McpServer, + SandboxIamOpts, SandboxLifecycle, SandboxMetrics, SandboxNetworkOpts, @@ -179,6 +180,7 @@ async def create( allow_internet_access: bool = True, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, + iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, volume_mounts: Optional[SandboxAsyncVolumeMount] = None, logger: Optional[logging.Logger] = None, @@ -197,6 +199,7 @@ async def create( :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``. + :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox. Example: ``{"tokens": {"aws": {"audience": "sts.amazonaws.com", "token_type": "ID-JAG"}}}`` :param lifecycle: Sandbox lifecycle configuration — ``on_timeout``: ``"kill"`` (default) or ``"pause"``, or an object ``{"action": "pause"|"kill", "keep_memory": bool}`` where ``keep_memory`` (default ``True``) set to ``False`` makes a timeout auto-pause filesystem-only (cold-boots on resume; cannot be combined with ``auto_resume``); ``auto_resume``: ``False`` (default) or ``True`` (only when ``on_timeout`` action is ``"pause"``). Example: ``{"on_timeout": {"action": "pause", "keep_memory": False}}`` :param volume_mounts: Dictionary mapping mount paths to AsyncVolume instances or volume names :param logger: Logger used for request and response logging for this sandbox. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. @@ -229,6 +232,7 @@ async def create( allow_internet_access=allow_internet_access, mcp=mcp, network=network, + iam=iam, lifecycle=lifecycle, volume_mounts=transformed_mounts, logger=logger, @@ -1099,6 +1103,7 @@ async def _create( allow_internet_access: bool, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, + iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, volume_mounts: Optional[list] = None, logger: Optional[logging.Logger] = None, @@ -1123,6 +1128,7 @@ async def _create( allow_internet_access=allow_internet_access, mcp=mcp, network=network, + iam=iam, lifecycle=lifecycle, volume_mounts=volume_mounts, logger=logger, diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 9df7a5a956..a540e8c523 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -49,6 +49,7 @@ from e2b.sandbox.sandbox_api import ( build_network_update_body, McpServer, + SandboxIamOpts, SandboxInfo, SandboxLifecycle, SandboxMetrics, @@ -56,6 +57,7 @@ SandboxNetworkUpdate, SandboxQuery, SnapshotInfo, + build_iam_config, build_network_config, ) from e2b.sandbox_async.paginator import AsyncSandboxPaginator @@ -207,6 +209,7 @@ async def _create_sandbox( secure: bool, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, + iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, volume_mounts: Optional[List[SandboxVolumeMountAPI]] = None, logger: Optional[logging.Logger] = None, @@ -267,6 +270,7 @@ async def _create_sandbox( secure=secure, allow_internet_access=allow_internet_access, network=SandboxNetworkConfig(**network_body) if network_body else UNSET, + iam=build_iam_config(iam) or UNSET, volume_mounts=volume_mounts if volume_mounts else UNSET, ) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 4141eeda86..0e3c35f16e 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -20,6 +20,7 @@ from e2b.sandbox.main import SandboxOpts from e2b.sandbox.sandbox_api import ( McpServer, + SandboxIamOpts, SandboxLifecycle, SandboxMetrics, SandboxNetworkOpts, @@ -168,6 +169,7 @@ def create( allow_internet_access: bool = True, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, + iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, volume_mounts: Optional[SandboxVolumeMount] = None, logger: Optional[logging.Logger] = None, @@ -186,6 +188,7 @@ def create( :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``. + :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox. Example: ``{"tokens": {"aws": {"audience": "sts.amazonaws.com", "token_type": "ID-JAG"}}}`` :param lifecycle: Sandbox lifecycle configuration — ``on_timeout``: ``"kill"`` (default) or ``"pause"``, or an object ``{"action": "pause"|"kill", "keep_memory": bool}`` where ``keep_memory`` (default ``True``) set to ``False`` makes a timeout auto-pause filesystem-only (cold-boots on resume; cannot be combined with ``auto_resume``); ``auto_resume``: ``False`` (default) or ``True`` (only when ``on_timeout`` action is ``"pause"``). Example: ``{"on_timeout": {"action": "pause", "keep_memory": False}}`` :param volume_mounts: Dictionary mapping mount paths to Volume instances or volume names :param logger: Logger used for request and response logging for this sandbox. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. @@ -218,6 +221,7 @@ def create( allow_internet_access=allow_internet_access, mcp=mcp, network=network, + iam=iam, lifecycle=lifecycle, volume_mounts=transformed_mounts, logger=logger, @@ -1090,6 +1094,7 @@ def _create( allow_internet_access: bool, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, + iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, volume_mounts: Optional[list] = None, logger: Optional[logging.Logger] = None, @@ -1114,6 +1119,7 @@ def _create( allow_internet_access=allow_internet_access, mcp=mcp, network=network, + iam=iam, lifecycle=lifecycle, volume_mounts=volume_mounts, logger=logger, diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index c7bf34e827..d11876f321 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -48,6 +48,7 @@ from e2b.sandbox.sandbox_api import ( build_network_update_body, McpServer, + SandboxIamOpts, SandboxInfo, SandboxLifecycle, SandboxMetrics, @@ -55,6 +56,7 @@ SandboxNetworkUpdate, SandboxQuery, SnapshotInfo, + build_iam_config, build_network_config, ) from e2b.sandbox_sync.paginator import SandboxPaginator, get_api_client @@ -206,6 +208,7 @@ def _create_sandbox( secure: bool, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, + iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, volume_mounts: Optional[List[SandboxVolumeMountAPI]] = None, logger: Optional[logging.Logger] = None, @@ -266,6 +269,7 @@ def _create_sandbox( secure=secure, allow_internet_access=allow_internet_access, network=SandboxNetworkConfig(**network_body) if network_body else UNSET, + iam=build_iam_config(iam) or UNSET, volume_mounts=volume_mounts if volume_mounts else UNSET, ) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index 737e346d1a..dda6b607ce 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -9,8 +9,9 @@ NewSandbox, SandboxAutoResumeConfig, ) +from e2b.api.client.types import UNSET from e2b.exceptions import InvalidArgumentException -from e2b.sandbox.sandbox_api import SandboxQuery +from e2b.sandbox.sandbox_api import SandboxQuery, build_iam_config @pytest.mark.skip_debug() @@ -61,6 +62,33 @@ def test_create_payload_deserializes_auto_resume_enabled(): assert body.auto_resume.to_dict() == {"enabled": False} +def test_create_payload_serializes_iam_tokens(): + iam = build_iam_config( + { + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "token_type": "JWT-SVID"}, + }, + } + ) + assert iam is not None + + body = NewSandbox(template_id="template-id", iam=iam) + + assert body.to_dict()["iam"] == { + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "tokenType": "JWT-SVID"}, + }, + } + + +def test_create_payload_omits_iam_when_not_provided(): + assert build_iam_config(None) is None + + body = NewSandbox(template_id="template-id", iam=UNSET) + + assert "iam" not in body.to_dict() + + @pytest.mark.skip_debug() def test_filesystem_only_auto_pause_rejects_auto_resume(): # A filesystem-only auto-pause snapshot can only be resumed explicitly, so From 449756768079c3ef2c0d67f4efd1c6eada4b245b Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:24:02 +0200 Subject: [PATCH 2/7] feat(sdk): add Secret class with idToken for iam workload tokens Adds the `Secret` class from the SDK design, exported from both main packages, with a static `idToken` (JS) / `id_token` (Python) method that returns a workload-token definition passable as a value in `iam.tokens` on `Sandbox.create`. Plain `{ audience, tokenType }` objects remain accepted too. Co-Authored-By: Claude Fable 5 --- .changeset/sandbox-iam-tokens.md | 12 +++-- packages/js-sdk/src/index.ts | 3 ++ packages/js-sdk/src/sandbox/sandboxApi.ts | 6 ++- packages/js-sdk/src/secret.ts | 50 +++++++++++++++++++ packages/js-sdk/tests/sandbox/iam.test.ts | 22 +++++++- packages/python-sdk/e2b/__init__.py | 2 + .../python-sdk/e2b/sandbox/sandbox_api.py | 5 +- packages/python-sdk/e2b/sandbox_async/main.py | 2 +- packages/python-sdk/e2b/sandbox_sync/main.py | 2 +- packages/python-sdk/e2b/secret.py | 34 +++++++++++++ .../tests/sync/sandbox_sync/test_create.py | 23 ++++++++- 11 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 packages/js-sdk/src/secret.ts create mode 100644 packages/python-sdk/e2b/secret.py diff --git a/.changeset/sandbox-iam-tokens.md b/.changeset/sandbox-iam-tokens.md index 1f935ed8e7..26d5b87bb4 100644 --- a/.changeset/sandbox-iam-tokens.md +++ b/.changeset/sandbox-iam-tokens.md @@ -3,24 +3,30 @@ '@e2b/python-sdk': minor --- -Add the `iam` option to `Sandbox.create` for configuring sandbox workload identity. Passing a non-empty `tokens` map (name → `{ audience, tokenType }`) enables workload identity for the sandbox: +Add the `iam` option to `Sandbox.create` for configuring sandbox workload identity, and a `Secret` class with an `idToken` / `id_token` method for defining the workload tokens. Passing a non-empty `tokens` map (name → `{ audience, tokenType }`) enables workload identity for the sandbox: ```ts +import { Sandbox, Secret } from 'e2b' + const sandbox = await Sandbox.create({ iam: { tokens: { - aws: { audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }, + aws: Secret.idToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }), }, }, }) ``` ```python +from e2b import Sandbox, Secret + sandbox = Sandbox.create( iam={ "tokens": { - "aws": {"audience": "sts.amazonaws.com", "token_type": "JWT-SVID"}, + "aws": Secret.id_token(audience="sts.amazonaws.com", token_type="JWT-SVID"), }, }, ) ``` + +Plain `{ audience, tokenType }` objects (`{"audience": ..., "token_type": ...}` dicts in Python) are accepted as token values too. diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 8724409792..8d26c5040b 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -84,6 +84,9 @@ export type { export type { McpServer } from './sandbox/mcp' +export { Secret } from './secret' +export type { SecretIdTokenOpts } from './secret' + export { ALL_TRAFFIC } from './sandbox/network' export type { diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index b7d22b1b8b..b670eb4d60 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -222,6 +222,7 @@ export interface SandboxIamToken { export interface SandboxIamOpts { /** * Named workload-token definitions, keyed by a caller-chosen token name. + * Values can be created with `Secret.idToken()`. */ tokens?: Record } @@ -440,7 +441,10 @@ export interface SandboxOpts extends ConnectionOpts { * const sandbox = await Sandbox.create({ * iam: { * tokens: { - * aws: { audience: 'sts.amazonaws.com', tokenType: 'ID-JAG' }, + * aws: Secret.idToken({ + * audience: 'sts.amazonaws.com', + * tokenType: 'JWT-SVID', + * }), * }, * }, * }) diff --git a/packages/js-sdk/src/secret.ts b/packages/js-sdk/src/secret.ts new file mode 100644 index 0000000000..cf2c3db9a1 --- /dev/null +++ b/packages/js-sdk/src/secret.ts @@ -0,0 +1,50 @@ +import type { SandboxIamToken } from './sandbox/sandboxApi' + +/** + * Options for {@link Secret.idToken}. + */ +export interface SecretIdTokenOpts { + /** + * Audience of the workload token, stored exactly as provided. + */ + audience: string + + /** + * Workload token type. + */ + tokenType: string +} + +/** + * Secrets and workload identity helpers. + */ +export class Secret { + /** + * Define a workload identity token to pass to `iam.tokens` when creating + * a sandbox. + * + * @param opts workload token definition. + * + * @returns a token definition passable to `iam.tokens`. + * + * @example + * ```ts + * const sandbox = await Sandbox.create({ + * iam: { + * tokens: { + * aws: Secret.idToken({ + * audience: 'sts.amazonaws.com', + * tokenType: 'JWT-SVID', + * }), + * }, + * }, + * }) + * ``` + */ + static idToken(opts: SecretIdTokenOpts): SandboxIamToken { + return { + audience: opts.audience, + tokenType: opts.tokenType, + } + } +} diff --git a/packages/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts index 47a10b8588..4afd7c37ad 100644 --- a/packages/js-sdk/tests/sandbox/iam.test.ts +++ b/packages/js-sdk/tests/sandbox/iam.test.ts @@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import { Sandbox } from '../../src' +import { Sandbox, Secret } from '../../src' import { TEST_API_KEY, apiUrl } from '../setup' let lastCreateBody: Record | undefined @@ -44,6 +44,26 @@ test('Sandbox.create sends iam tokens in the request body', async () => { }) }) +test('Sandbox.create sends Secret.idToken tokens in the request body', async () => { + await Sandbox.create('base', { + apiKey: TEST_API_KEY, + iam: { + tokens: { + aws: Secret.idToken({ + audience: 'sts.amazonaws.com', + tokenType: 'JWT-SVID', + }), + }, + }, + }) + + expect(lastCreateBody?.iam).toEqual({ + tokens: { + aws: { audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }, + }, + }) +}) + test('Sandbox.create omits iam from the request body when not provided', async () => { await Sandbox.create('base', { apiKey: TEST_API_KEY }) diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index 514e62cb2e..6580324786 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -100,6 +100,7 @@ from .sandbox_async.main import AsyncSandbox from .sandbox_async.paginator import AsyncSandboxPaginator, AsyncSnapshotPaginator from .sandbox_async.utils import OutputHandler +from .secret import Secret from .sandbox_sync.commands.command_handle import CommandHandle from .sandbox_sync.filesystem.watch_handle import WatchHandle from .sandbox_sync.main import Sandbox @@ -209,6 +210,7 @@ # IAM "SandboxIamOpts", "SandboxIamToken", + "Secret", # Snapshot "SnapshotInfo", "SnapshotPaginator", diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index e85b4cdc90..d0de9c1f2f 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -254,7 +254,10 @@ class SandboxIamOpts(TypedDict, total=False): """ tokens: Dict[str, SandboxIamToken] - """Named workload-token definitions, keyed by a caller-chosen token name.""" + """ + Named workload-token definitions, keyed by a caller-chosen token name. + Values can be created with :meth:`Secret.id_token`. + """ class SandboxNetworkInfo(TypedDict, total=False): diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index ae7b945031..fab5703ee9 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -199,7 +199,7 @@ async def create( :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``. - :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox. Example: ``{"tokens": {"aws": {"audience": "sts.amazonaws.com", "token_type": "ID-JAG"}}}`` + :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.id_token`. Example: ``{"tokens": {"aws": Secret.id_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}`` :param lifecycle: Sandbox lifecycle configuration — ``on_timeout``: ``"kill"`` (default) or ``"pause"``, or an object ``{"action": "pause"|"kill", "keep_memory": bool}`` where ``keep_memory`` (default ``True``) set to ``False`` makes a timeout auto-pause filesystem-only (cold-boots on resume; cannot be combined with ``auto_resume``); ``auto_resume``: ``False`` (default) or ``True`` (only when ``on_timeout`` action is ``"pause"``). Example: ``{"on_timeout": {"action": "pause", "keep_memory": False}}`` :param volume_mounts: Dictionary mapping mount paths to AsyncVolume instances or volume names :param logger: Logger used for request and response logging for this sandbox. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 0e3c35f16e..230da7fd5c 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -188,7 +188,7 @@ def create( :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``. - :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox. Example: ``{"tokens": {"aws": {"audience": "sts.amazonaws.com", "token_type": "ID-JAG"}}}`` + :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.id_token`. Example: ``{"tokens": {"aws": Secret.id_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}`` :param lifecycle: Sandbox lifecycle configuration — ``on_timeout``: ``"kill"`` (default) or ``"pause"``, or an object ``{"action": "pause"|"kill", "keep_memory": bool}`` where ``keep_memory`` (default ``True``) set to ``False`` makes a timeout auto-pause filesystem-only (cold-boots on resume; cannot be combined with ``auto_resume``); ``auto_resume``: ``False`` (default) or ``True`` (only when ``on_timeout`` action is ``"pause"``). Example: ``{"on_timeout": {"action": "pause", "keep_memory": False}}`` :param volume_mounts: Dictionary mapping mount paths to Volume instances or volume names :param logger: Logger used for request and response logging for this sandbox. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. diff --git a/packages/python-sdk/e2b/secret.py b/packages/python-sdk/e2b/secret.py new file mode 100644 index 0000000000..bad13b9712 --- /dev/null +++ b/packages/python-sdk/e2b/secret.py @@ -0,0 +1,34 @@ +from e2b.sandbox.sandbox_api import SandboxIamToken + + +class Secret: + """ + Secrets and workload identity helpers. + """ + + @staticmethod + def id_token(*, audience: str, token_type: str) -> SandboxIamToken: + """ + Define a workload identity token to pass to ``iam.tokens`` when + creating a sandbox. + + :param audience: Audience of the workload token, stored exactly as provided. + :param token_type: Workload token type. + + :return: A token definition passable to ``iam.tokens``. + + Example: + ```python + sandbox = Sandbox.create( + iam={ + "tokens": { + "aws": Secret.id_token( + audience="sts.amazonaws.com", + token_type="JWT-SVID", + ), + }, + }, + ) + ``` + """ + return SandboxIamToken(audience=audience, token_type=token_type) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index dda6b607ce..3c2a0ff1c5 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -4,7 +4,7 @@ import httpx import pytest -from e2b import Sandbox, SandboxState +from e2b import Sandbox, SandboxState, Secret from e2b.api.client.models import ( NewSandbox, SandboxAutoResumeConfig, @@ -81,6 +81,27 @@ def test_create_payload_serializes_iam_tokens(): } +def test_create_payload_serializes_secret_id_token(): + iam = build_iam_config( + { + "tokens": { + "aws": Secret.id_token( + audience="sts.amazonaws.com", token_type="JWT-SVID" + ), + }, + } + ) + assert iam is not None + + body = NewSandbox(template_id="template-id", iam=iam) + + assert body.to_dict()["iam"] == { + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "tokenType": "JWT-SVID"}, + }, + } + + def test_create_payload_omits_iam_when_not_provided(): assert build_iam_config(None) is None From 1f074edaa5ca3ea82a424cbae5d58b566bdf89e3 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:30:57 +0200 Subject: [PATCH 3/7] refactor(sdk): rename Secret.idToken to Secret.iamToken Aligns the token factory with the rest of the feature's name family (`iam.tokens` option, `SandboxIamToken` schema): `Secret.iamToken` / `Secret.iam_token`, and `SecretIdTokenOpts` -> `SecretIamTokenOpts`. Co-Authored-By: Claude Fable 5 --- .changeset/sandbox-iam-tokens.md | 6 +++--- packages/js-sdk/src/index.ts | 2 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 4 ++-- packages/js-sdk/src/secret.ts | 8 ++++---- packages/js-sdk/tests/sandbox/iam.test.ts | 4 ++-- packages/python-sdk/e2b/sandbox/sandbox_api.py | 2 +- packages/python-sdk/e2b/sandbox_async/main.py | 2 +- packages/python-sdk/e2b/sandbox_sync/main.py | 2 +- packages/python-sdk/e2b/secret.py | 4 ++-- .../python-sdk/tests/sync/sandbox_sync/test_create.py | 4 ++-- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.changeset/sandbox-iam-tokens.md b/.changeset/sandbox-iam-tokens.md index 26d5b87bb4..9883c27160 100644 --- a/.changeset/sandbox-iam-tokens.md +++ b/.changeset/sandbox-iam-tokens.md @@ -3,7 +3,7 @@ '@e2b/python-sdk': minor --- -Add the `iam` option to `Sandbox.create` for configuring sandbox workload identity, and a `Secret` class with an `idToken` / `id_token` method for defining the workload tokens. Passing a non-empty `tokens` map (name → `{ audience, tokenType }`) enables workload identity for the sandbox: +Add the `iam` option to `Sandbox.create` for configuring sandbox workload identity, and a `Secret` class with an `iamToken` / `iam_token` method for defining the workload tokens. Passing a non-empty `tokens` map (name → `{ audience, tokenType }`) enables workload identity for the sandbox: ```ts import { Sandbox, Secret } from 'e2b' @@ -11,7 +11,7 @@ import { Sandbox, Secret } from 'e2b' const sandbox = await Sandbox.create({ iam: { tokens: { - aws: Secret.idToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }), + aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }), }, }, }) @@ -23,7 +23,7 @@ from e2b import Sandbox, Secret sandbox = Sandbox.create( iam={ "tokens": { - "aws": Secret.id_token(audience="sts.amazonaws.com", token_type="JWT-SVID"), + "aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID"), }, }, ) diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 8d26c5040b..5859027121 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -85,7 +85,7 @@ export type { export type { McpServer } from './sandbox/mcp' export { Secret } from './secret' -export type { SecretIdTokenOpts } from './secret' +export type { SecretIamTokenOpts } from './secret' export { ALL_TRAFFIC } from './sandbox/network' diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index b670eb4d60..d3c86b4501 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -222,7 +222,7 @@ export interface SandboxIamToken { export interface SandboxIamOpts { /** * Named workload-token definitions, keyed by a caller-chosen token name. - * Values can be created with `Secret.idToken()`. + * Values can be created with `Secret.iamToken()`. */ tokens?: Record } @@ -441,7 +441,7 @@ export interface SandboxOpts extends ConnectionOpts { * const sandbox = await Sandbox.create({ * iam: { * tokens: { - * aws: Secret.idToken({ + * aws: Secret.iamToken({ * audience: 'sts.amazonaws.com', * tokenType: 'JWT-SVID', * }), diff --git a/packages/js-sdk/src/secret.ts b/packages/js-sdk/src/secret.ts index cf2c3db9a1..264fb1d400 100644 --- a/packages/js-sdk/src/secret.ts +++ b/packages/js-sdk/src/secret.ts @@ -1,9 +1,9 @@ import type { SandboxIamToken } from './sandbox/sandboxApi' /** - * Options for {@link Secret.idToken}. + * Options for {@link Secret.iamToken}. */ -export interface SecretIdTokenOpts { +export interface SecretIamTokenOpts { /** * Audience of the workload token, stored exactly as provided. */ @@ -32,7 +32,7 @@ export class Secret { * const sandbox = await Sandbox.create({ * iam: { * tokens: { - * aws: Secret.idToken({ + * aws: Secret.iamToken({ * audience: 'sts.amazonaws.com', * tokenType: 'JWT-SVID', * }), @@ -41,7 +41,7 @@ export class Secret { * }) * ``` */ - static idToken(opts: SecretIdTokenOpts): SandboxIamToken { + static iamToken(opts: SecretIamTokenOpts): SandboxIamToken { return { audience: opts.audience, tokenType: opts.tokenType, diff --git a/packages/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts index 4afd7c37ad..f3c9fb4f33 100644 --- a/packages/js-sdk/tests/sandbox/iam.test.ts +++ b/packages/js-sdk/tests/sandbox/iam.test.ts @@ -44,12 +44,12 @@ test('Sandbox.create sends iam tokens in the request body', async () => { }) }) -test('Sandbox.create sends Secret.idToken tokens in the request body', async () => { +test('Sandbox.create sends Secret.iamToken tokens in the request body', async () => { await Sandbox.create('base', { apiKey: TEST_API_KEY, iam: { tokens: { - aws: Secret.idToken({ + aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID', }), diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index d0de9c1f2f..7c207df1ee 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -256,7 +256,7 @@ class SandboxIamOpts(TypedDict, total=False): tokens: Dict[str, SandboxIamToken] """ Named workload-token definitions, keyed by a caller-chosen token name. - Values can be created with :meth:`Secret.id_token`. + Values can be created with :meth:`Secret.iam_token`. """ diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index fab5703ee9..f22eaa729d 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -199,7 +199,7 @@ async def create( :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``. - :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.id_token`. Example: ``{"tokens": {"aws": Secret.id_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}`` + :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}`` :param lifecycle: Sandbox lifecycle configuration — ``on_timeout``: ``"kill"`` (default) or ``"pause"``, or an object ``{"action": "pause"|"kill", "keep_memory": bool}`` where ``keep_memory`` (default ``True``) set to ``False`` makes a timeout auto-pause filesystem-only (cold-boots on resume; cannot be combined with ``auto_resume``); ``auto_resume``: ``False`` (default) or ``True`` (only when ``on_timeout`` action is ``"pause"``). Example: ``{"on_timeout": {"action": "pause", "keep_memory": False}}`` :param volume_mounts: Dictionary mapping mount paths to AsyncVolume instances or volume names :param logger: Logger used for request and response logging for this sandbox. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 230da7fd5c..db4d46d3ba 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -188,7 +188,7 @@ def create( :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``. - :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.id_token`. Example: ``{"tokens": {"aws": Secret.id_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}`` + :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}`` :param lifecycle: Sandbox lifecycle configuration — ``on_timeout``: ``"kill"`` (default) or ``"pause"``, or an object ``{"action": "pause"|"kill", "keep_memory": bool}`` where ``keep_memory`` (default ``True``) set to ``False`` makes a timeout auto-pause filesystem-only (cold-boots on resume; cannot be combined with ``auto_resume``); ``auto_resume``: ``False`` (default) or ``True`` (only when ``on_timeout`` action is ``"pause"``). Example: ``{"on_timeout": {"action": "pause", "keep_memory": False}}`` :param volume_mounts: Dictionary mapping mount paths to Volume instances or volume names :param logger: Logger used for request and response logging for this sandbox. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. diff --git a/packages/python-sdk/e2b/secret.py b/packages/python-sdk/e2b/secret.py index bad13b9712..c9415c7d97 100644 --- a/packages/python-sdk/e2b/secret.py +++ b/packages/python-sdk/e2b/secret.py @@ -7,7 +7,7 @@ class Secret: """ @staticmethod - def id_token(*, audience: str, token_type: str) -> SandboxIamToken: + def iam_token(*, audience: str, token_type: str) -> SandboxIamToken: """ Define a workload identity token to pass to ``iam.tokens`` when creating a sandbox. @@ -22,7 +22,7 @@ def id_token(*, audience: str, token_type: str) -> SandboxIamToken: sandbox = Sandbox.create( iam={ "tokens": { - "aws": Secret.id_token( + "aws": Secret.iam_token( audience="sts.amazonaws.com", token_type="JWT-SVID", ), diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index 3c2a0ff1c5..349e84ceb6 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -81,11 +81,11 @@ def test_create_payload_serializes_iam_tokens(): } -def test_create_payload_serializes_secret_id_token(): +def test_create_payload_serializes_secret_iam_token(): iam = build_iam_config( { "tokens": { - "aws": Secret.id_token( + "aws": Secret.iam_token( audience="sts.amazonaws.com", token_type="JWT-SVID" ), }, From b26c8df600e8f328adf314b8487d50f4b325a12a Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:35:20 +0200 Subject: [PATCH 4/7] feat(sdk): type iam tokenType as an open literal union The spec deliberately leaves tokenType an open string, and the backend accepts only "JWT-SVID" in this version (iamTokenTypeJWTSVID in infra's sandbox_create.go). SandboxIamTokenType gives autocomplete for the known value without closing the set, so newer or self-hosted backends can accept types this SDK version doesn't know about. Co-Authored-By: Claude Fable 5 --- packages/js-sdk/src/index.ts | 1 + packages/js-sdk/src/sandbox/sandboxApi.ts | 9 ++++++++- packages/js-sdk/src/secret.ts | 4 ++-- packages/python-sdk/e2b/__init__.py | 2 ++ packages/python-sdk/e2b/sandbox/sandbox_api.py | 9 ++++++++- packages/python-sdk/e2b/secret.py | 4 ++-- 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 5859027121..8d1eee4d12 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -64,6 +64,7 @@ export type { SandboxPaginator, SandboxIamOpts, SandboxIamToken, + SandboxIamTokenType, SandboxNetworkOpts, SandboxNetworkInfo, SandboxNetworkSelector, diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index d3c86b4501..dc60122a85 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -200,6 +200,13 @@ export type SandboxNetworkUpdate = { allowInternetAccess?: boolean } +/** + * Workload token type. `'JWT-SVID'` is the only type the API accepts in this + * version; the set is defined server-side and may grow, so any string is + * allowed. + */ +export type SandboxIamTokenType = 'JWT-SVID' | (string & {}) + /** * Workload token definition for sandbox workload identity. */ @@ -212,7 +219,7 @@ export interface SandboxIamToken { /** * Workload token type. */ - tokenType: string + tokenType: SandboxIamTokenType } /** diff --git a/packages/js-sdk/src/secret.ts b/packages/js-sdk/src/secret.ts index 264fb1d400..7fb1476f2e 100644 --- a/packages/js-sdk/src/secret.ts +++ b/packages/js-sdk/src/secret.ts @@ -1,4 +1,4 @@ -import type { SandboxIamToken } from './sandbox/sandboxApi' +import type { SandboxIamToken, SandboxIamTokenType } from './sandbox/sandboxApi' /** * Options for {@link Secret.iamToken}. @@ -12,7 +12,7 @@ export interface SecretIamTokenOpts { /** * Workload token type. */ - tokenType: string + tokenType: SandboxIamTokenType } /** diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index 6580324786..86f912f0e0 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -77,6 +77,7 @@ McpServer, SandboxIamOpts, SandboxIamToken, + SandboxIamTokenType, SandboxInfo, SandboxInfoLifecycle, SandboxMetrics, @@ -210,6 +211,7 @@ # IAM "SandboxIamOpts", "SandboxIamToken", + "SandboxIamTokenType", "Secret", # Snapshot "SnapshotInfo", diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index 7c207df1ee..2ace8920be 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -235,6 +235,13 @@ class SandboxNetworkUpdate(TypedDict, total=False): """ +SandboxIamTokenType = Union[Literal["JWT-SVID"], str] +""" +Workload token type. ``"JWT-SVID"`` is the only type the API accepts in this +version; the set is defined server-side and may grow, so any string is allowed. +""" + + class SandboxIamToken(TypedDict): """ Workload token definition for sandbox workload identity. @@ -243,7 +250,7 @@ class SandboxIamToken(TypedDict): audience: str """Audience of the workload token, stored exactly as provided.""" - token_type: str + token_type: SandboxIamTokenType """Workload token type.""" diff --git a/packages/python-sdk/e2b/secret.py b/packages/python-sdk/e2b/secret.py index c9415c7d97..d895bc468c 100644 --- a/packages/python-sdk/e2b/secret.py +++ b/packages/python-sdk/e2b/secret.py @@ -1,4 +1,4 @@ -from e2b.sandbox.sandbox_api import SandboxIamToken +from e2b.sandbox.sandbox_api import SandboxIamToken, SandboxIamTokenType class Secret: @@ -7,7 +7,7 @@ class Secret: """ @staticmethod - def iam_token(*, audience: str, token_type: str) -> SandboxIamToken: + def iam_token(*, audience: str, token_type: SandboxIamTokenType) -> SandboxIamToken: """ Define a workload identity token to pass to ``iam.tokens`` when creating a sandbox. From ce48d4020e2a3699641436b256774c91401a8940 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:44:39 +0200 Subject: [PATCH 5/7] fix(sdk): omit empty iam configs and mirror iam tests to async suite Addresses review feedback on #1606: - build_iam_config returns None for a config with no tokens, so iam={} / iam={"tokens": {}} omit the field like the sibling network config instead of sending "iam": {} (ClientSandboxIam is an attrs class and always truthy, so `or UNSET` alone never omitted it); the JS body builder now applies the same rule for parity. - Mirror the three iam payload tests into the async test suite, matching the auto_resume test convention. Co-Authored-By: Claude Fable 5 --- packages/js-sdk/src/sandbox/sandboxApi.ts | 5 +- packages/js-sdk/tests/sandbox/iam.test.ts | 12 +++++ .../python-sdk/e2b/sandbox/sandbox_api.py | 27 ++++++---- .../tests/async/sandbox_async/test_create.py | 54 ++++++++++++++++++- .../tests/sync/sandbox_sync/test_create.py | 4 +- 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index dc60122a85..99f967afad 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -1265,7 +1265,10 @@ export class SandboxApi { secure: opts?.secure ?? true, allow_internet_access: opts?.allowInternetAccess ?? true, network: buildNetworkBody(opts?.network), - iam: opts?.iam, + // Only a non-empty tokens map enables workload identity, so an empty + // iam config is omitted from the payload entirely. + iam: + Object.keys(opts?.iam?.tokens ?? {}).length > 0 ? opts?.iam : undefined, autoPause: action === 'pause', autoPauseMemory: action === 'pause' ? keepMemory : undefined, autoResume: { enabled: autoResume }, diff --git a/packages/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts index f3c9fb4f33..b3c7323989 100644 --- a/packages/js-sdk/tests/sandbox/iam.test.ts +++ b/packages/js-sdk/tests/sandbox/iam.test.ts @@ -70,3 +70,15 @@ test('Sandbox.create omits iam from the request body when not provided', async ( expect(lastCreateBody).toBeDefined() expect(lastCreateBody).not.toHaveProperty('iam') }) + +test('Sandbox.create omits an empty iam config from the request body', async () => { + await Sandbox.create('base', { apiKey: TEST_API_KEY, iam: {} }) + + expect(lastCreateBody).toBeDefined() + expect(lastCreateBody).not.toHaveProperty('iam') + + await Sandbox.create('base', { apiKey: TEST_API_KEY, iam: { tokens: {} } }) + + expect(lastCreateBody).toBeDefined() + expect(lastCreateBody).not.toHaveProperty('iam') +}) diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index 2ace8920be..e7aaeb41b5 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -450,21 +450,28 @@ def build_network_config( def build_iam_config( iam: Optional[SandboxIamOpts], ) -> Optional[ClientSandboxIam]: - """Resolve a :class:`SandboxIamOpts` into the API client body.""" + """Resolve a :class:`SandboxIamOpts` into the API client body. + + Returns ``None`` for a config with no tokens — only a non-empty tokens + map enables workload identity, so an empty config is omitted from the + request payload entirely. + """ if iam is None: return None - body = ClientSandboxIam() tokens = iam.get("tokens") - if tokens is not None: - client_tokens = ClientSandboxIamTokens() - for name, token in tokens.items(): - client_tokens[name] = ClientSandboxIamToken( - audience=token["audience"], - token_type=token["token_type"], - ) - body.tokens = client_tokens + if not tokens: + return None + client_tokens = ClientSandboxIamTokens() + for name, token in tokens.items(): + client_tokens[name] = ClientSandboxIamToken( + audience=token["audience"], + token_type=token["token_type"], + ) + + body = ClientSandboxIam() + body.tokens = client_tokens return body diff --git a/packages/python-sdk/tests/async/sandbox_async/test_create.py b/packages/python-sdk/tests/async/sandbox_async/test_create.py index aa17c2c52f..2f9af4c46e 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_create.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_create.py @@ -4,12 +4,14 @@ import httpx import pytest -from e2b import AsyncSandbox, SandboxQuery, SandboxState +from e2b import AsyncSandbox, SandboxQuery, SandboxState, Secret from e2b.api.client.models import ( NewSandbox, SandboxAutoResumeConfig, ) +from e2b.api.client.types import UNSET from e2b.exceptions import InvalidArgumentException +from e2b.sandbox.sandbox_api import build_iam_config @pytest.mark.skip_debug() @@ -60,6 +62,56 @@ def test_create_payload_deserializes_auto_resume_enabled(): assert body.auto_resume.to_dict() == {"enabled": False} +def test_create_payload_serializes_iam_tokens(): + iam = build_iam_config( + { + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "token_type": "JWT-SVID"}, + }, + } + ) + assert iam is not None + + body = NewSandbox(template_id="template-id", iam=iam) + + assert body.to_dict()["iam"] == { + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "tokenType": "JWT-SVID"}, + }, + } + + +def test_create_payload_serializes_secret_iam_token(): + iam = build_iam_config( + { + "tokens": { + "aws": Secret.iam_token( + audience="sts.amazonaws.com", token_type="JWT-SVID" + ), + }, + } + ) + assert iam is not None + + body = NewSandbox(template_id="template-id", iam=iam) + + assert body.to_dict()["iam"] == { + "tokens": { + "aws": {"audience": "sts.amazonaws.com", "tokenType": "JWT-SVID"}, + }, + } + + +def test_create_payload_omits_iam_when_not_provided_or_empty(): + assert build_iam_config(None) is None + assert build_iam_config({}) is None + assert build_iam_config({"tokens": {}}) is None + + body = NewSandbox(template_id="template-id", iam=UNSET) + + assert "iam" not in body.to_dict() + + @pytest.mark.skip_debug() async def test_filesystem_only_auto_pause_rejects_auto_resume(): # A filesystem-only auto-pause snapshot can only be resumed explicitly, so diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index 349e84ceb6..46f2ad06da 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -102,8 +102,10 @@ def test_create_payload_serializes_secret_iam_token(): } -def test_create_payload_omits_iam_when_not_provided(): +def test_create_payload_omits_iam_when_not_provided_or_empty(): assert build_iam_config(None) is None + assert build_iam_config({}) is None + assert build_iam_config({"tokens": {}}) is None body = NewSandbox(template_id="template-id", iam=UNSET) From 5916b264d2c66f2aa4138bac6b270fd7a3405afd Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:49:44 +0200 Subject: [PATCH 6/7] refactor(js-sdk): extract buildIamBody alongside buildNetworkBody Moves the inline empty-iam omission out of the create body literal into a buildIamBody helper, mirroring buildNetworkBody and Python's build_iam_config. Co-Authored-By: Claude Fable 5 --- packages/js-sdk/src/sandbox/sandboxApi.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 99f967afad..315a5ff18d 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -792,6 +792,18 @@ function buildNetworkBody( } } +function buildIamBody( + iam: SandboxIamOpts | undefined +): components['schemas']['SandboxIam'] | undefined { + // Only a non-empty tokens map enables workload identity, so an empty + // iam config is omitted from the payload entirely. + if (!iam?.tokens || Object.keys(iam.tokens).length === 0) { + return undefined + } + + return iam +} + function buildNetworkUpdateBody( network: SandboxNetworkUpdate ): components['schemas']['SandboxNetworkUpdateConfig'] { @@ -1265,10 +1277,7 @@ export class SandboxApi { secure: opts?.secure ?? true, allow_internet_access: opts?.allowInternetAccess ?? true, network: buildNetworkBody(opts?.network), - // Only a non-empty tokens map enables workload identity, so an empty - // iam config is omitted from the payload entirely. - iam: - Object.keys(opts?.iam?.tokens ?? {}).length > 0 ? opts?.iam : undefined, + iam: buildIamBody(opts?.iam), autoPause: action === 'pause', autoPauseMemory: action === 'pause' ? keepMemory : undefined, autoResume: { enabled: autoResume }, From d4724618fbf3eb21379474564d28132a511d025a Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:16:48 +0200 Subject: [PATCH 7/7] fix(sdk): harden iam config building from code review - buildIamBody rebuilds the request body from known fields instead of passing the caller's object by reference, skips undefined token values so they don't defeat the omit-empty guard, and rejects tokens missing string audience/tokenType with InvalidArgumentError - build_iam_config raises InvalidArgumentException naming the snake_case token_type key instead of a bare KeyError on malformed token dicts - Secret.iamToken takes SandboxIamToken directly, dropping the duplicated SecretIamTokenOpts interface - iam msw suite fails loudly on unhandled requests (error, not bypass) Co-Authored-By: Claude Fable 5 --- packages/js-sdk/src/index.ts | 1 - packages/js-sdk/src/sandbox/sandboxApi.ts | 27 +++++++++- packages/js-sdk/src/secret.ts | 26 ++-------- packages/js-sdk/tests/sandbox/iam.test.ts | 50 ++++++++++++++++++- .../python-sdk/e2b/sandbox/sandbox_api.py | 14 ++++++ .../tests/async/sandbox_async/test_create.py | 22 ++++++++ .../tests/sync/sandbox_sync/test_create.py | 22 ++++++++ 7 files changed, 135 insertions(+), 27 deletions(-) diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 8d1eee4d12..54aa9a83ff 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -86,7 +86,6 @@ export type { export type { McpServer } from './sandbox/mcp' export { Secret } from './secret' -export type { SecretIamTokenOpts } from './secret' export { ALL_TRAFFIC } from './sandbox/network' diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 315a5ff18d..a11c955078 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -795,13 +795,36 @@ function buildNetworkBody( function buildIamBody( iam: SandboxIamOpts | undefined ): components['schemas']['SandboxIam'] | undefined { + // Rebuild the body from the known fields so stray properties on the + // caller's objects never reach the wire and later mutations of the + // caller's map cannot alter the in-flight request. + const tokens: components['schemas']['SandboxIamTokens'] = {} + for (const [name, token] of Object.entries(iam?.tokens ?? {})) { + // Untyped callers can leave holes in the map (conditionally included + // tokens); those don't count toward a non-empty config. + if (!token) { + continue + } + + if ( + typeof token.audience !== 'string' || + typeof token.tokenType !== 'string' + ) { + throw new InvalidArgumentError( + `iam token '${name}' must have string 'audience' and 'tokenType' properties.` + ) + } + + tokens[name] = { audience: token.audience, tokenType: token.tokenType } + } + // Only a non-empty tokens map enables workload identity, so an empty // iam config is omitted from the payload entirely. - if (!iam?.tokens || Object.keys(iam.tokens).length === 0) { + if (Object.keys(tokens).length === 0) { return undefined } - return iam + return { tokens } } function buildNetworkUpdateBody( diff --git a/packages/js-sdk/src/secret.ts b/packages/js-sdk/src/secret.ts index 7fb1476f2e..3347c90c01 100644 --- a/packages/js-sdk/src/secret.ts +++ b/packages/js-sdk/src/secret.ts @@ -1,19 +1,4 @@ -import type { SandboxIamToken, SandboxIamTokenType } from './sandbox/sandboxApi' - -/** - * Options for {@link Secret.iamToken}. - */ -export interface SecretIamTokenOpts { - /** - * Audience of the workload token, stored exactly as provided. - */ - audience: string - - /** - * Workload token type. - */ - tokenType: SandboxIamTokenType -} +import type { SandboxIamToken } from './sandbox/sandboxApi' /** * Secrets and workload identity helpers. @@ -23,7 +8,7 @@ export class Secret { * Define a workload identity token to pass to `iam.tokens` when creating * a sandbox. * - * @param opts workload token definition. + * @param token workload token definition. * * @returns a token definition passable to `iam.tokens`. * @@ -41,10 +26,7 @@ export class Secret { * }) * ``` */ - static iamToken(opts: SecretIamTokenOpts): SandboxIamToken { - return { - audience: opts.audience, - tokenType: opts.tokenType, - } + static iamToken(token: SandboxIamToken): SandboxIamToken { + return { ...token } } } diff --git a/packages/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts index b3c7323989..2cc568d06e 100644 --- a/packages/js-sdk/tests/sandbox/iam.test.ts +++ b/packages/js-sdk/tests/sandbox/iam.test.ts @@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import { Sandbox, Secret } from '../../src' +import { InvalidArgumentError, Sandbox, Secret } from '../../src' import { TEST_API_KEY, apiUrl } from '../setup' let lastCreateBody: Record | undefined @@ -18,7 +18,7 @@ const server = setupServer( }) ) -beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })) +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterAll(() => server.close()) @@ -82,3 +82,49 @@ test('Sandbox.create omits an empty iam config from the request body', async () expect(lastCreateBody).toBeDefined() expect(lastCreateBody).not.toHaveProperty('iam') }) + +test('Sandbox.create treats a tokens map with only undefined values as empty', async () => { + await Sandbox.create('base', { + apiKey: TEST_API_KEY, + iam: { + // Untyped callers can build the map conditionally, leaving holes. + tokens: { aws: undefined as never }, + }, + }) + + expect(lastCreateBody).toBeDefined() + expect(lastCreateBody).not.toHaveProperty('iam') +}) + +test('Sandbox.create strips unknown token properties from the request body', async () => { + await Sandbox.create('base', { + apiKey: TEST_API_KEY, + iam: { + tokens: { + aws: { + audience: 'sts.amazonaws.com', + tokenType: 'JWT-SVID', + filePath: '/run/token', + } as never, + }, + }, + }) + + expect(lastCreateBody?.iam).toEqual({ + tokens: { + aws: { audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }, + }, + }) +}) + +test('Sandbox.create rejects a token missing audience or tokenType', async () => { + await expect( + Sandbox.create('base', { + apiKey: TEST_API_KEY, + iam: { + // The wire-format casing an untyped caller might copy from a payload. + tokens: { aws: { audience: 'sts.amazonaws.com' } as never }, + }, + }) + ).rejects.toThrowError(InvalidArgumentError) +}) diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index e7aaeb41b5..699ec8040a 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -55,6 +55,7 @@ ) from e2b.api.client.types import Unset from e2b.connection_config import ApiParams +from e2b.exceptions import InvalidArgumentException from e2b.sandbox.mcp import McpServer as BaseMcpServer from e2b.sandbox.network import ALL_TRAFFIC from e2b.paginator import PaginatorBase @@ -465,6 +466,19 @@ def build_iam_config( client_tokens = ClientSandboxIamTokens() for name, token in tokens.items(): + # Re-check at runtime for callers that bypass the TypedDict — a bare + # KeyError from deep inside create would not name the option or the + # snake_case key the wire's camelCase 'tokenType' maps to. + if ( + not isinstance(token, dict) + or "audience" not in token + or "token_type" not in token + ): + raise InvalidArgumentException( + f"iam token {name!r} must be a dict with 'audience' and " + "'token_type' keys (snake_case 'token_type', not 'tokenType')." + ) + client_tokens[name] = ClientSandboxIamToken( audience=token["audience"], token_type=token["token_type"], diff --git a/packages/python-sdk/tests/async/sandbox_async/test_create.py b/packages/python-sdk/tests/async/sandbox_async/test_create.py index 2f9af4c46e..3b18c6f884 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_create.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_create.py @@ -112,6 +112,28 @@ def test_create_payload_omits_iam_when_not_provided_or_empty(): assert "iam" not in body.to_dict() +def test_create_payload_rejects_malformed_iam_tokens(): + # The wire-format casing a user might copy from the JS example or a + # serialized payload must fail with an actionable error, not a KeyError. + with pytest.raises(InvalidArgumentException, match="token_type"): + build_iam_config( + cast( + Any, + { + "tokens": { + "aws": { + "audience": "sts.amazonaws.com", + "tokenType": "JWT-SVID", + }, + }, + }, + ) + ) + + with pytest.raises(InvalidArgumentException): + build_iam_config(cast(Any, {"tokens": {"aws": None}})) + + @pytest.mark.skip_debug() async def test_filesystem_only_auto_pause_rejects_auto_resume(): # A filesystem-only auto-pause snapshot can only be resumed explicitly, so diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index 46f2ad06da..e3c2ba2fa6 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -112,6 +112,28 @@ def test_create_payload_omits_iam_when_not_provided_or_empty(): assert "iam" not in body.to_dict() +def test_create_payload_rejects_malformed_iam_tokens(): + # The wire-format casing a user might copy from the JS example or a + # serialized payload must fail with an actionable error, not a KeyError. + with pytest.raises(InvalidArgumentException, match="token_type"): + build_iam_config( + cast( + Any, + { + "tokens": { + "aws": { + "audience": "sts.amazonaws.com", + "tokenType": "JWT-SVID", + }, + }, + }, + ) + ) + + with pytest.raises(InvalidArgumentException): + build_iam_config(cast(Any, {"tokens": {"aws": None}})) + + @pytest.mark.skip_debug() def test_filesystem_only_auto_pause_rejects_auto_resume(): # A filesystem-only auto-pause snapshot can only be resumed explicitly, so