Releases: e2b-dev/E2B
Release list
@e2b/python-sdk@2.44.0
Minor Changes
-
5759f17: Add an
E2Bclient that binds a connection config once and exposes the resource surfaces off it, so a single process can talk to several API keys, domains or deployments. The classes it exposes are per-client subclasses of the realSandbox/Volume/Template/Secretclasses, so they behave exactly like the top-level ones — per-call options still win over the client's options, which win over the environment variables. The named top-level exports are unchanged and keep reading the environment.Nothing existing changes:
Templateis now theTemplateBaseclass made callable as a factory, soTemplate(...), the statics andinstanceofkeep working, and the default export is stillSandbox.import { E2B } from 'e2b' const { Sandbox, Volume, Template, Secret } = new E2B({ apiKey: 'e2b_***', domain: 'e2b.dev', }) const sandbox = await Sandbox.create() const volume = await Volume.create('my-volume') const exists = await Template.exists('my-template') await Template.build(Template().fromPythonImage('3'), 'my-env') await Secret.create('openai-api-key', 'sk-***')
from e2b import E2B client = E2B(api_key="e2b_***", domain="e2b.dev") Sandbox, Volume, Template = client.Sandbox, client.Volume, client.Template Secret = client.Secret sandbox = Sandbox.create() volume = Volume.create("my-volume") exists = Template.exists("my-template") secret = Secret.create("openai-api-key", "sk-***") # Async variants are exposed too. AsyncSandbox = client.AsyncSandbox async_sandbox = await AsyncSandbox.create()
@e2b/python-sdk@2.43.0
Minor Changes
- f89f8c3: Add Secrets Management to the SDK. The
Secretclass (andAsyncSecretin Python) now manages E2B secrets:createandupdatestore secret values (write-only — no read surface returns them),getInfo/get_infoand the paginatedlistread metadata,existsanddestroyare idempotent existence and lifecycle helpers, andfillformats the${e2b.secrets.name}placeholder that the runtime resolves to the secret's current value.
Patch Changes
- 2be6c12: Internal refactor: the template API operations resolve their connection config through a class-level hook, so a
TemplateBasesubclass can carry bound connection options. No behavior change —Template/AsyncTemplatekeep reading config from per-call options and environment variables. In the Python SDK the terminal template operations (build,build_in_background,get_build_status,exists,alias_exists,assign_tags,remove_tags,get_tags) becameclassmethods, with signatures unchanged for callers. - 05aa03c: Add typed not-found errors for volumes:
VolumeNotFoundError/VolumeNotFoundException(thrown when a volume is not found) andVolumePathNotFoundError/VolumePathNotFoundException(thrown when a path inside a volume is not found). All subclass the existingNotFoundError/NotFoundException, so existing catches keep working.
e2b@2.42.0
Minor Changes
-
7af41e9: Refresh the MCP server types from the current MCP gateway catalog: 49 servers are new (
n8n,neo4j,okta,temporal,proxmox,zscaler, the AWS Labs family, ...), 61 titles and 10 descriptions were rewritten, and 4 servers changed their options (awsDiagram,context7,neo4jCypher,onlyofficeDocspace).Six servers the catalog no longer publishes are gone from
McpServer:postgres,root,tembo,flexprice,triplewhale,cdataConnectcloud.awsDiagramandcontext7now require an option (outputDirandapiKey), soawsDiagram: {}andcontext7: {}stop type-checking, andonlyofficeDocspaceis down tobaseUrlanddocspaceApiKey. The removals also narrowMcpServerName, soTemplate().addMcpServer('postgres')stops compiling. The config is still passed to the gateway as written, so a dropped server can be kept by casting past the type — whether it starts is up to the gateway.import { Sandbox } from 'e2b' const sandbox = await Sandbox.create({ mcp: { n8n: { apiKey: process.env.N8N_API_KEY!, apiUrl: 'https://n8n.example.com/api/v1', }, }, })
Patch Changes
-
15bd48b: Omit
autoPausefrom the create-sandbox request when no timeout lifecycle is configured, and omitautoPauseMemoryunlesskeepMemory/keep_memorywas chosen. Sending the SDK's local defaults for those fields was indistinguishable from an explicit choice, so the API could not tell "no preference" from a client choice and own its defaults. Explicit values are still always sent:import { Sandbox } from 'e2b' // No timeout lifecycle: autoPause is omitted, the API applies its default. await Sandbox.create() // Explicit action: autoPause: false / autoPause: true, as before. await Sandbox.create({ lifecycle: { onTimeout: 'kill' } }) await Sandbox.create({ lifecycle: { onTimeout: 'pause' } }) // Snapshot kind is only sent when keepMemory is set. await Sandbox.create({ lifecycle: { onTimeout: { action: 'pause', keepMemory: false } }, })
from e2b import Sandbox # No timeout lifecycle: auto_pause is omitted, the API applies its default. Sandbox.create() # Explicit action: autoPause: false / autoPause: true, as before. Sandbox.create(lifecycle={"on_timeout": "kill"}) Sandbox.create(lifecycle={"on_timeout": "pause"}) # Snapshot kind is only sent when keep_memory is set. Sandbox.create( lifecycle={"on_timeout": {"action": "pause", "keep_memory": False}} )
-
5367693: Omit
autoResumefrom thePOST /sandboxesrequest whenlifecycle.autoResume/lifecycle["auto_resume"]is not configured, instead of sending the SDK's local default as{ "autoResume": { "enabled": false } }. The API can now tell an unset preference from an explicit opt-out and own the default itself. Explicit values are unchanged on the wire.import { Sandbox } from 'e2b' // autoResume is left out of the request entirely — the API's default applies await Sandbox.create({ lifecycle: { onTimeout: 'pause' } }) // an explicit choice is still sent as before await Sandbox.create({ lifecycle: { onTimeout: 'pause', autoResume: true } })
from e2b import Sandbox # auto_resume is left out of the request entirely — the API's default applies Sandbox.create(lifecycle={"on_timeout": "pause"}) # an explicit choice is still sent as before Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True})
-
2daced6: Tag the package homepage and README links with UTM parameters (
utm_source=npm/pypi) so registry traffic to e2b.dev is attributed correctly. No functional change.
e2b@2.41.0
Minor Changes
-
6824cdf: Add
network.egressProxy/network["egress_proxy"]for routing a sandbox's outbound TCP through a SOCKS5 proxy you operate ("bring your own proxy"). Tunneling happens on the host after theallowOut/denyOutlists are evaluated, so nothing runs inside the sandbox and code running there can neither see the proxy nor route around it. UDP-based traffic — DNS and QUIC/HTTP3 — is not tunneled.import { Sandbox } from 'e2b' const sandbox = await Sandbox.create({ network: { egressProxy: { address: 'proxy.example.com:1080', username: 'proxy-user', password: 'proxy-password', }, }, })
from e2b import Sandbox sandbox = Sandbox.create( network={ "egress_proxy": { "address": "proxy.example.com:1080", "username": "proxy-user", "password": "proxy-password", }, }, )
It combines with the rest of the network configuration — here everything except
api.example.comis denied, and the traffic that is allowed goes through your proxy:await Sandbox.create({ network: { allowOut: ['api.example.com'], denyOut: ({ allTraffic }) => [allTraffic], egressProxy: { address: 'proxy.example.com:1080' }, }, })
Sandbox.create( network={ "allow_out": ["api.example.com"], "deny_out": lambda ctx: [ctx.all_traffic], "egress_proxy": {"address": "proxy.example.com:1080"}, }, )
updateNetwork/update_networksets or replaces the proxy on a sandbox that is already running, with no restart. The update replaces the whole configuration instead of merging into it, so an update that leaves the proxy out stops tunneling — repeat it in every update that should keep it.// Start tunneling on the running sandbox await sandbox.updateNetwork({ allowOut: ['api.example.com'], denyOut: ({ allTraffic }) => [allTraffic], egressProxy: { address: 'proxy.example.com:1080' }, }) // Stop tunneling: an update without egressProxy clears it await sandbox.updateNetwork({})
# Start tunneling on the running sandbox sandbox.update_network({ "allow_out": ["api.example.com"], "deny_out": lambda ctx: [ctx.all_traffic], "egress_proxy": {"address": "proxy.example.com:1080"}, }) # Stop tunneling: an update without egress_proxy clears it sandbox.update_network({})
getInfo/get_inforeports the proxy the sandbox's egress is currently tunneled through. The password is never returned, so the returnedSandboxEgressProxyInfodoes not have the field at all:const info = await sandbox.getInfo() console.log(info.network?.egressProxy) // { address: 'proxy.example.com:1080', username: 'proxy-user' }
info = sandbox.get_info() print(info.network["egress_proxy"]) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'}
Egress fails closed: when the proxy is unreachable or does not speak SOCKS5, outbound connections fail rather than falling back to a direct connection. The address is validated server-side when the sandbox is created — a rejected create leaves nothing behind. Available on E2B Cloud and in BYOC deployments; a sandbox that names a proxy on a deployment built from the open source
e2b-dev/infrarepository is rejected as unsupported.
@e2b/python-sdk@2.42.0
Minor Changes
-
7af41e9: Refresh the MCP server types from the current MCP gateway catalog: 49 servers are new (
n8n,neo4j,okta,temporal,proxmox,zscaler, the AWS Labs family, ...), 61 titles and 10 descriptions were rewritten, and 4 servers changed their options (awsDiagram,context7,neo4jCypher,onlyofficeDocspace).Six servers the catalog no longer publishes are gone from
McpServer:postgres,root,tembo,flexprice,triplewhale,cdataConnectcloud.awsDiagramandcontext7now require an option (outputDirandapiKey), soawsDiagram: {}andcontext7: {}stop type-checking, andonlyofficeDocspaceis down tobaseUrlanddocspaceApiKey. The removals also narrowMcpServerName, soTemplate().addMcpServer('postgres')stops compiling. The config is still passed to the gateway as written, so a dropped server can be kept by casting past the type — whether it starts is up to the gateway.import { Sandbox } from 'e2b' const sandbox = await Sandbox.create({ mcp: { n8n: { apiKey: process.env.N8N_API_KEY!, apiUrl: 'https://n8n.example.com/api/v1', }, }, })
Patch Changes
-
15bd48b: Omit
autoPausefrom the create-sandbox request when no timeout lifecycle is configured, and omitautoPauseMemoryunlesskeepMemory/keep_memorywas chosen. Sending the SDK's local defaults for those fields was indistinguishable from an explicit choice, so the API could not tell "no preference" from a client choice and own its defaults. Explicit values are still always sent:import { Sandbox } from 'e2b' // No timeout lifecycle: autoPause is omitted, the API applies its default. await Sandbox.create() // Explicit action: autoPause: false / autoPause: true, as before. await Sandbox.create({ lifecycle: { onTimeout: 'kill' } }) await Sandbox.create({ lifecycle: { onTimeout: 'pause' } }) // Snapshot kind is only sent when keepMemory is set. await Sandbox.create({ lifecycle: { onTimeout: { action: 'pause', keepMemory: false } }, })
from e2b import Sandbox # No timeout lifecycle: auto_pause is omitted, the API applies its default. Sandbox.create() # Explicit action: autoPause: false / autoPause: true, as before. Sandbox.create(lifecycle={"on_timeout": "kill"}) Sandbox.create(lifecycle={"on_timeout": "pause"}) # Snapshot kind is only sent when keep_memory is set. Sandbox.create( lifecycle={"on_timeout": {"action": "pause", "keep_memory": False}} )
-
5367693: Omit
autoResumefrom thePOST /sandboxesrequest whenlifecycle.autoResume/lifecycle["auto_resume"]is not configured, instead of sending the SDK's local default as{ "autoResume": { "enabled": false } }. The API can now tell an unset preference from an explicit opt-out and own the default itself. Explicit values are unchanged on the wire.import { Sandbox } from 'e2b' // autoResume is left out of the request entirely — the API's default applies await Sandbox.create({ lifecycle: { onTimeout: 'pause' } }) // an explicit choice is still sent as before await Sandbox.create({ lifecycle: { onTimeout: 'pause', autoResume: true } })
from e2b import Sandbox # auto_resume is left out of the request entirely — the API's default applies Sandbox.create(lifecycle={"on_timeout": "pause"}) # an explicit choice is still sent as before Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True})
-
666241d: Run every persistent HTTP stack in the SDK on one shared pyqwest connection pool
instead of four: the control-plane REST API, the envd HTTP API, the envd RPC
clients, and the volume content API now all draw from
e2b.api.client_sync/client_async, keyed on the three knobs that are fixed
when a pyqwest transport is built — proxy, idle read bound, and HTTP version.
reqwest pools per host internally, so one pool serves the API host and every
per-sandbox host without interference — and since envd RPC and the envd HTTP API
hit the same host, an active sandbox now needs a single HTTP/2 connection instead
of one per stack. Streamed downloads keep a pool of their own, the only one
carrying the idleread_timeout: reqwest's read timer runs during body send and
TTFB, so on a shared pool it would cut off long uploads. No signature changes —
get_transportandget_envd_transportkeep thehttp2parameter restored in
2.39.1, and the two are now the same pool per key rather than two. -
2daced6: Tag the package homepage and README links with UTM parameters (
utm_source=npm/pypi) so registry traffic to e2b.dev is attributed correctly. No functional change.
@e2b/python-sdk@2.41.0
Minor Changes
-
6824cdf: Add
network.egressProxy/network["egress_proxy"]for routing a sandbox's outbound TCP through a SOCKS5 proxy you operate ("bring your own proxy"). Tunneling happens on the host after theallowOut/denyOutlists are evaluated, so nothing runs inside the sandbox and code running there can neither see the proxy nor route around it. UDP-based traffic — DNS and QUIC/HTTP3 — is not tunneled.import { Sandbox } from 'e2b' const sandbox = await Sandbox.create({ network: { egressProxy: { address: 'proxy.example.com:1080', username: 'proxy-user', password: 'proxy-password', }, }, })
from e2b import Sandbox sandbox = Sandbox.create( network={ "egress_proxy": { "address": "proxy.example.com:1080", "username": "proxy-user", "password": "proxy-password", }, }, )
It combines with the rest of the network configuration — here everything except
api.example.comis denied, and the traffic that is allowed goes through your proxy:await Sandbox.create({ network: { allowOut: ['api.example.com'], denyOut: ({ allTraffic }) => [allTraffic], egressProxy: { address: 'proxy.example.com:1080' }, }, })
Sandbox.create( network={ "allow_out": ["api.example.com"], "deny_out": lambda ctx: [ctx.all_traffic], "egress_proxy": {"address": "proxy.example.com:1080"}, }, )
updateNetwork/update_networksets or replaces the proxy on a sandbox that is already running, with no restart. The update replaces the whole configuration instead of merging into it, so an update that leaves the proxy out stops tunneling — repeat it in every update that should keep it.// Start tunneling on the running sandbox await sandbox.updateNetwork({ allowOut: ['api.example.com'], denyOut: ({ allTraffic }) => [allTraffic], egressProxy: { address: 'proxy.example.com:1080' }, }) // Stop tunneling: an update without egressProxy clears it await sandbox.updateNetwork({})
# Start tunneling on the running sandbox sandbox.update_network({ "allow_out": ["api.example.com"], "deny_out": lambda ctx: [ctx.all_traffic], "egress_proxy": {"address": "proxy.example.com:1080"}, }) # Stop tunneling: an update without egress_proxy clears it sandbox.update_network({})
getInfo/get_inforeports the proxy the sandbox's egress is currently tunneled through. The password is never returned, so the returnedSandboxEgressProxyInfodoes not have the field at all:const info = await sandbox.getInfo() console.log(info.network?.egressProxy) // { address: 'proxy.example.com:1080', username: 'proxy-user' }
info = sandbox.get_info() print(info.network["egress_proxy"]) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'}
Egress fails closed: when the proxy is unreachable or does not speak SOCKS5, outbound connections fail rather than falling back to a direct connection. The address is validated server-side when the sandbox is created — a rejected create leaves nothing behind. Available on E2B Cloud and in BYOC deployments; a sandbox that names a proxy on a deployment built from the open source
e2b-dev/infrarepository is rejected as unsupported.
Patch Changes
-
02ba746: Raise the
h2floor to>=4.4.1so it can no longer resolve to a version affected by CVE-2026-71554, where a duplicateHostheader is forwarded to the consuming application and becomes a request smuggling primitive once HTTP/2 is downgraded to HTTP/1.1. -
e2eebd5: Fix URL encoding of namespaced template names and aliases in the Python SDK.
The endpoints that take a template ID also accept a template name, and names may
be namespaced (e.g.namespace/name). The SDK interpolated them into the request
path without encoding, so a call likeTemplate.exists("namespace/name")hit
/templates/aliases/namespace/nameinstead of
/templates/aliases/namespace%2Fname— the slash split the route rather than
staying inside one path segment. Every method that takes a template ID or name,
an alias, or a snapshot ID in the path —Template.exists/alias_exists,
get_tags, the build/upload/status calls, andSandbox.delete_snapshot(whose
snapshot IDs arenamespace/name:tag) — now percent-encodes the value, matching
the JavaScript SDK (which already encodes path parameters via
encodeURIComponent).from e2b import Template, Sandbox # Namespaced templates now resolve correctly Template.exists("my-team/my-template") Template.get_tags("my-team/my-template") # Namespaced snapshots can now be deleted Sandbox.delete_snapshot("my-team/my-snapshot:default")
@e2b/cli@2.16.3
Patch Changes
- 2daced6: Tag the package homepage and README links with UTM parameters (
utm_source=npm/pypi) so registry traffic to e2b.dev is attributed correctly. No functional change. - Updated dependencies [15bd48b]
- Updated dependencies [5367693]
- Updated dependencies [7af41e9]
- Updated dependencies [2daced6]
- e2b@2.42.0
e2b@2.40.0
Minor Changes
-
6248b12: Remove the deprecated
accessToken/access_tokenoption and itsE2B_ACCESS_TOKENenvironment fallback. E2B access tokens are no longer accepted for API authentication, so the SDKs no longer resolve one or send it as anAuthorization: Bearerheader — requests authenticate with the API key alone.If you were relying on the option to send a bearer token to a custom deployment, pass the header directly, which is what the deprecation notice already pointed to:
// Before const sandbox = await Sandbox.create({ accessToken: token }) // After const sandbox = await Sandbox.create({ apiHeaders: { Authorization: `Bearer ${token}` }, })
# Before config = ConnectionConfig(access_token=token) # After config = ConnectionConfig(api_headers={"Authorization": f"Bearer {token}"})
Note that
Sandbox.envd_access_token/traffic_access_tokenare unrelated per-sandbox tokens and are unaffected.
@e2b/python-sdk@2.40.0
Minor Changes
-
6248b12: Remove the deprecated
accessToken/access_tokenoption and itsE2B_ACCESS_TOKENenvironment fallback. E2B access tokens are no longer accepted for API authentication, so the SDKs no longer resolve one or send it as anAuthorization: Bearerheader — requests authenticate with the API key alone.If you were relying on the option to send a bearer token to a custom deployment, pass the header directly, which is what the deprecation notice already pointed to:
// Before const sandbox = await Sandbox.create({ accessToken: token }) // After const sandbox = await Sandbox.create({ apiHeaders: { Authorization: `Bearer ${token}` }, })
# Before config = ConnectionConfig(access_token=token) # After config = ConnectionConfig(api_headers={"Authorization": f"Bearer {token}"})
Note that
Sandbox.envd_access_token/traffic_access_tokenare unrelated per-sandbox tokens and are unaffected.
e2b@2.39.0
Minor Changes
-
07eb9be: Allow a network rule's
transformto be a callback, so a workload identity token from theiamoption can be injected into egress requests without the SDK ever seeing its value. The callback receives placeholder strings that the egress proxy resolves per request —iam.tokens.awsis${e2b.identity.tokens.aws}on the wire — and referencing a token that is not registered iniam.tokensfails withInvalidArgumentError/InvalidArgumentExceptioninstead of silently sending a placeholder no token will ever replace.updateNetwork/update_networkaccepts the same callbacks, but its payload carries noiamconfig, so token names cannot be checked there and every name resolves to its placeholder.Token names are validated where they are registered and again before they are interpolated: a name cannot be empty or contain
{,}or control characters, since the proxy reads a placeholder up to its first}and a brace in the name would resolve a different token than the one referenced.import { Sandbox, Secret } from 'e2b' const sandbox = await Sandbox.create({ iam: { tokens: { aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID', }), }, }, network: { allowOut: ({ rules }) => [...rules.keys()], rules: { 'api.internal.example.com': [ { transform: ({ iam }) => ({ headers: { Authorization: `Bearer ${iam.tokens.aws}` }, }), }, ], }, }, })
from e2b import Sandbox, Secret sandbox = Sandbox.create( iam={ "tokens": { "aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID"), }, }, network={ "allow_out": lambda ctx: list(ctx.rules.keys()), "rules": { "api.internal.example.com": [ { "transform": lambda ctx: { "headers": {"Authorization": f"Bearer {ctx.iam.tokens['aws']}"}, }, }, ], }, }, )
-
64b25bb: Add the
iamoption toSandbox.createfor configuring sandbox workload identity, and aSecretclass with aniamToken/iam_tokenmethod for defining the workload tokens. Passing a non-emptytokensmap (name →{ audience, tokenType }) enables workload identity for the sandbox:import { Sandbox, Secret } from 'e2b' const sandbox = await Sandbox.create({ iam: { tokens: { aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID', }), }, }, })
from e2b import Sandbox, Secret sandbox = Sandbox.create( iam={ "tokens": { "aws": Secret.iam_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.