Skip to content

feat(sdk): support structured network rules with per-host transforms#1286

Open
mishushakov wants to merge 20 commits into
mainfrom
mishushakov/network-allowout-transform
Open

feat(sdk): support structured network rules with per-host transforms#1286
mishushakov wants to merge 20 commits into
mainfrom
mishushakov/network-allowout-transform

Conversation

@mishushakov
Copy link
Copy Markdown
Member

@mishushakov mishushakov commented Apr 22, 2026

Summary

  • Adds a network.rules map keyed by host (or CIDR / IP) for per-host outbound request transforms (e.g. header injection). Registering a host in rules does not grant egress on its own — it must still be referenced via allowOut. JS accepts either a plain object or a Map.
  • Extends network.allowOut / network.denyOut to accept either a static list or a selector callback receiving { allTraffic, rules } (Python: ctx.all_traffic, ctx.rules). allTraffic is '0.0.0.0/0'; rules is a Map (Python Mapping) view of network.rules, so policies can be composed against the registered rule hosts without duplicating them.
  • transform on a rule accepts either a static object or a callback that receives a typed SandboxNetworkTransformContext. The context fields are literal placeholder strings (${e2b.sandboxId}, ${e2b.teamId}, ${e2b.executionId}, ${e2b.identity.jwt}) that the egress proxy resolves per request — so users get typed access without hardcoding template strings.
  • Updates the OpenAPI spec, regenerates JS and Python API clients, and surfaces new types: SandboxNetworkRule, SandboxNetworkRules, SandboxNetworkTransform, SandboxNetworkTransformContext, SandboxNetworkSelector, SandboxNetworkSelectorContext, SandboxNetworkInfo.
  • Adds contract tests (JS + async/sync Python) that create a sandbox with a transform.headers rule for httpbin.org, run curl https://httpbin.org/headers, and assert the injected header is reflected back.

Examples

Inject a static header on requests to a specific host (TypeScript)

import { Sandbox } from 'e2b'

await Sandbox.create({
  network: {
    // Only allow egress to hosts that have rules registered.
    allowOut: ({ rules }) => [...rules.keys()],
    rules: {
      'api.openai.com': [
        {
          transform: {
            headers: { 'X-Header': 'Content' },
          },
        },
      ],
    },
  },
})

Same thing in Python

import os
from e2b import Sandbox

sandbox = Sandbox(
    network={
        "allow_out": lambda ctx: list(ctx.rules.keys()),
        "rules": {
            "api.openai.com": [
                {
                    "transform": {
                        "headers": {"X-Header": "Content"},
                    },
                },
            ],
        },
    },
)

Transform callback — inject the sandbox's identity JWT (resolved per request)

The proxy substitutes ${e2b.identity.jwt} (and the other placeholder fields) at egress time, so the same template is reused across all requests without ever materializing the secret in your code:

await Sandbox.create({
  network: {
    allowOut: ({ rules }) => [...rules.keys()],
    rules: {
      'api.internal.example.com': [
        {
          transform: ({ identity, sandboxId }) => ({
            headers: {
              Authorization: `Bearer ${identity.jwt}`,
              'X-Sandbox-Id': sandboxId,
            },
          }),
        },
      ],
    },
  },
})
Sandbox(
    network={
        "allow_out": lambda ctx: list(ctx.rules.keys()),
        "rules": {
            "api.internal.example.com": [
                {
                    "transform": lambda ctx: {
                        "headers": {
                            "Authorization": f"Bearer {ctx.identity.jwt}",
                            "X-Sandbox-Id": ctx.sandbox_id,
                        },
                    },
                },
            ],
        },
    },
)

network.rules as a Map (TypeScript)

const rules = new Map([
  ['api.openai.com', [{ transform: { headers: { 'X-Trace': 'on' } } }]],
])

await Sandbox.create({
  network: { allowOut: ({ rules }) => [...rules.keys()], rules },
})

Block all egress except an explicit allowlist

await Sandbox.create({
  network: {
    denyOut: ({ allTraffic }) => [allTraffic],   // allTraffic === '0.0.0.0/0'
    allowOut: ['1.1.1.1', '8.8.8.0/24'],
  },
})
Sandbox(
    network={
        "deny_out": lambda ctx: [ctx.all_traffic],
        "allow_out": ["1.1.1.1", "8.8.8.0/24"],
    },
)

Static list (unchanged, still supported)

await Sandbox.create({
  network: { allowOut: ['1.1.1.1', '8.8.8.0/24'] },
})
Sandbox(network={"allow_out": ["1.1.1.1", "8.8.8.0/24"]})

Notes

  • SDK/spec-only; the sandbox egress proxy still needs to honor transform.headers and resolve the placeholder strings for the httpbin assertion to pass end-to-end — the new test locks the wire shape as a contract.
  • ALL_TRAFFIC is still exported but the selector form (({ allTraffic }) => [allTraffic] / lambda ctx: [ctx.all_traffic]) is the recommended way to express "everything".
  • Transform callbacks are evaluated client-side at sandbox creation; the resolved object (containing the literal ${e2b.*} placeholders) is sent on the wire. The proxy substitutes the placeholders per request — they are not resolved by the SDK.

Test plan

  • Spec + generated clients reviewed for shape correctness
  • Existing network.test.ts / test_network.py cases still pass once backend auth is available
  • New firewall transform injects headers cases pass once egress proxy injects headers
  • Egress proxy resolves ${e2b.sandboxId}, ${e2b.teamId}, ${e2b.executionId}, ${e2b.identity.jwt} placeholders in transform.headers values

Extends SandboxNetworkConfig.allowOut/denyOut to accept objects of the
form { host, transform: [{ headers }] } alongside plain string entries.
Updates OpenAPI spec, regenerates JS + Python clients, surfaces new TS
types (SandboxNetworkRule, SandboxNetworkRuleTransform, SandboxNetworkEntry),
and adds a contract test that curls httpbin.org/headers and asserts an
injected Authorization-style header is reflected back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 708c7b5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Minor
@e2b/python-sdk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 22, 2026

PR Summary

Medium Risk
Introduces new network configuration shapes (selectors, per-host transform rules, and a new PUT /sandboxes/{sandboxID}/network endpoint) across generated clients, which risks breaking consumers relying on previous network types/serialization. Also adds request header injection support and placeholder-based transforms, which are sensitive to backend/proxy behavior and contract mismatches.

Overview
Adds structured egress network.rules keyed by host to apply per-host request transforms (currently header injection), including support for transform callbacks that emit placeholder strings for the proxy to resolve at egress time.

Extends network.allowOut/denyOut (JS) and allow_out/deny_out (Python) to accept selector callbacks that can reference an allTraffic sentinel and the set of rule-registered hosts, and updates SDK request building to materialize these into the API wire format.

Updates the OpenAPI spec and regenerated JS/Python clients to include the new network rule/transform schemas, adds PUT /sandboxes/{sandboxID}/network for updating a running sandbox’s network config, and expands metrics (memCache) and node status (standby). Tests are updated/added to cover selector usage and header-injection transform behavior.

Reviewed by Cursor Bugbot for commit 708c7b5. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

Package Artifacts

Built from 9d859c9. Download artifacts from this workflow run.

JS SDK (e2b@2.20.2-mishushakov-network-allowout-transform.0):

npm install ./e2b-2.20.2-mishushakov-network-allowout-transform.0.tgz

CLI (@e2b/cli@2.10.2-mishushakov-network-allowout-transform.0):

npm install ./e2b-cli-2.10.2-mishushakov-network-allowout-transform.0.tgz

Python SDK (e2b==2.21.1+mishushakov-network-allowout-transform):

pip install ./e2b-2.21.1+mishushakov.network.allowout.transform-py3-none-any.whl

Comment thread packages/js-sdk/tests/sandbox/network.test.ts Outdated
mishushakov and others added 3 commits April 22, 2026 17:49
- Adds SandboxNetworkRule and SandboxNetworkRuleTransform TypedDicts to
  the Python SDK, widens SandboxNetworkOpts.allow_out to accept them,
  and exposes them from the top-level e2b package.
- Mirrors the TS contract: deny_out stays as List[str], only allow_out
  gains the object form with optional per-host transforms.
- Reverts denyOut in the OpenAPI spec to items: string only (allowOut
  keeps the oneOf form with SandboxNetworkRule) and regenerates the
  JS + Python clients to match.
- Adds httpbin.org/headers transform tests for both async and sync
  Python sandbox tests, parallel to the TS case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drops the redundant string-form duplicate of httpbin.org and the
  denyOut/deny_out scoping; the test now exercises just the structured
  rule with header transform.
- Switches the Python tests to plain-dict literals (typed via a local
  SandboxNetworkOpts annotation) instead of TypedDict constructors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…onfig

Replaces the TYPE_CHECKING / in-method imports of SandboxNetworkRule
with a single top-level import. There is no circular-import risk here,
so the openapi-python-client guard is unnecessary noise.

Note: this file is generated by openapi-python-client and the guard
will reappear on the next \`make codegen\` run unless we add a
postprocess step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread packages/python-sdk/e2b/api/client/models/sandbox_network_config.py Outdated
mishushakov and others added 9 commits April 23, 2026 11:54
There is no local SandboxNetworkConfig to collide with — the
user-facing equivalent is SandboxNetworkOpts — so the
ClientSandboxNetworkConfig rename was dead noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The generated from_dict pre-initialized allow_out to [] and then
iterated over \`_allow_out or []\`, collapsing the absent-key case
(UNSET, which is falsy) into an empty list. Downstream
from_client_network_config could then set "allow_out": [] in the
user-facing dict, which is semantically "deny all outbound" rather
than "field not provided".

Now we only build the list when the key is actually present, leaving
allow_out as UNSET otherwise. The other oneOf-array fields are not
affected because deny_out remains a plain list[str].

Note: this lives in generated code; \`make codegen\` will reintroduce
the bug until openapi-python-client is patched or a postprocess step
is added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reverts the manual edits to the generated SandboxNetworkConfig
(TYPE_CHECKING hoist + UNSET-preserving from_dict) since that file
is owned by openapi-python-client and will be overwritten on the
next codegen run.

Addresses the same underlying bug — generated from_dict pre-inits
allow_out to [] and then iterates with \`_allow_out or []\`, so an
absent allowOut field deserializes to [] instead of UNSET — by
treating empty allow_out as "not provided" inside the hand-written
from_client_network_config wrapper. This keeps the public dict from
gaining a misleading "allow_out": [] entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t_network_config

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Separate the outbound network policy (allowOut/denyOut) from firewall
rules. Rules are registered under a top-level firewall map keyed by host
but do not grant egress on their own — hosts must still be referenced
via allowOut. Selectors accept either a static list or a callback that
receives { firewallHosts, allHosts } for composable policies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move per-host transform rules from a top-level `firewall` field to
`network.rules`. The selector context now exposes `{ allTraffic, rules }`
where `allTraffic` is `'0.0.0.0/0'` and `rules` is a Map (Mapping in
Python) view of `network.rules`. SandboxFirewall* schemas renamed to
SandboxNetworkRule / SandboxNetworkTransform.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SandboxNetworkRules now accepts both a plain object and a Map. The
helper normalizes either form to a Map for the selector context and
serializes via Object.fromEntries for the wire body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
diskTotal: number
}

function resolveNetworkSelector(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS SDK SandboxMetrics missing new memCache field

Medium Severity

The OpenAPI spec adds memCache as a required field on SandboxMetric (visible in the spec diff and in the generated schema.gen.ts at line 2321), and the Python generated client (sandbox_metric.py) was updated accordingly. However, the JS SDK's hand-written SandboxMetrics interface does not include memCache. Any metrics mapping code will silently drop this field, making it inaccessible to JS SDK consumers.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f8ea7fa. Configure here.

mishushakov and others added 2 commits April 28, 2026 17:55
Network rules can now declare transforms as a callback that receives a
typed context exposing literal placeholder strings (sandboxId, teamId,
executionId, identity.jwt). The proxy resolves these per request at
egress, so the SDK serializes the resolved object as-is and users get
typed access without hardcoding template strings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed label May 13, 2026
@mishushakov mishushakov marked this pull request as ready for review May 13, 2026 18:31
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has reached its monthly code review spending cap.

An organization admin can view or raise the cap at claude.ai/admin-settings/claude-code. The cap resets at the start of the next billing period.

Once the cap resets or is raised, reopen this pull request to trigger a review.

Inject `ctx.sandboxId` as a header on the httpbin rule and verify the
reflected response matches the live `sandbox.sandbox_id`, proving the
proxy substituted `${e2b.sandboxId}` per request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fbefba3. Configure here.

Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts
API responses can only contain plain JSON, so rules in the info shape
must not allow the JS Map variant or callback transforms accepted by
the input types. Introduce SandboxNetworkRuleInfo mirroring
SandboxNetworkRule with transform fixed to the static
SandboxNetworkTransform, and use it for SandboxNetworkInfo.rules in
both SDKs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@nauxliu
Copy link
Copy Markdown

nauxliu commented May 15, 2026

Thanks for working on this. I’m trying to understand the intended pattern for credential brokering and request logging.

For our use case, we want connectors to integrate with third-party platforms like CRMs, Jira, GitHub, etc. Sandboxes need to make outbound API calls to these services, while the actual credentials should stay outside the sandbox. The credential to inject may depend on the actual sandbox, tenant, and user.

Ideally, this would be an egress-time callback where we can access the original outbound request, inspect the URL / method / headers, inject the right token, and log or audit the request.

From my reading, transform is evaluated at sandbox creation time and produces a static config. It does not run at egress time, cannot do async lookup, cannot access the original request, and cannot use the real runtime sandbox context. That makes dynamic credential injection and request logging difficult.

If the design is intentionally static, could E2B at least support updating the network config after sandbox creation, similar to Vercel’s updateNetworkPolicy()? That would allow us to create the sandbox, resolve credentials based on the real sandbox ID, and then patch the transform.

Is that the expected workaround, or is there a better recommended pattern?

@mishushakov
Copy link
Copy Markdown
Member Author

@nauxliu thanks for the feedback!

We will consider making it possible to update the networking configuration after the Sandbox creation - I will check in with the team!

mishushakov and others added 2 commits May 15, 2026 18:31
We host our own httpbin mirror at httpbin.e2b.team — switch the rule
host and curl target away from httpbin.org so tests don't depend on a
third-party service.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants