Skip to content

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

Draft
mishushakov wants to merge 14 commits intomainfrom
mishushakov/network-allowout-transform
Draft

feat(sdk): support structured network rules with per-host transforms#1286
mishushakov wants to merge 14 commits intomainfrom
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: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
          },
        },
      ],
    },
  },
})

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": {"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
                    },
                },
            ],
        },
    },
)

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

⚠️ No Changeset found

Latest commit: 0653579

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 22, 2026

PR Summary

Medium Risk
Extends SDK/network configuration types and request-body building logic, which can change the wire format sent to the API and affect sandbox egress behavior. Risk is moderate because changes span OpenAPI spec, generated clients, and both JS/Python SDK surfaces, but remain SDK/spec-scoped.

Overview
Adds support for structured sandbox egress configuration via network.rules (per-host rule lists with optional request transform such as header injection) and expands allowOut/denyOut to accept either a static list or a selector callback receiving { allTraffic, rules }.

Updates the OpenAPI spec and regenerated JS/Python clients accordingly, including a new PUT /sandboxes/{sandboxID}/network endpoint and new schema/types (SandboxNetworkRule, SandboxNetworkTransform, SandboxNetworkInfo), plus adds contract tests (JS + sync/async Python) validating header injection behavior against httpbin.org. Also extends node/metrics schema with NodeStatus: standby and a new sandbox metric field memCache.

Reviewed by Cursor Bugbot for commit 0653579. 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 17ec9fb. Download artifacts from this workflow run.

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

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

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

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

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

pip install ./e2b-2.20.2+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>
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.

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 f8ea7fa. Configure here.

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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant