Skip to content

feat(sdk): restore transform callback with placeholder context#1335

Open
mishushakov wants to merge 24 commits into
mainfrom
mishushakov/network-transform-callback
Open

feat(sdk): restore transform callback with placeholder context#1335
mishushakov wants to merge 24 commits into
mainfrom
mishushakov/network-transform-callback

Conversation

@mishushakov
Copy link
Copy Markdown
Member

@mishushakov mishushakov commented May 19, 2026

Summary

  • Reintroduces the transform callback variant for SandboxNetworkRule (JS + Python): in addition to a static { headers: ... } object, transform can be a function receiving a typed SandboxNetworkTransformContext whose fields (sandboxId, teamId, executionId, identity.jwt / sandbox_id, team_id, execution_id, identity.jwt) are literal placeholder strings (${e2b.sandboxId}, etc.). The callback is invoked at sandbox-creation time; the resolved object — with placeholders intact — goes on the wire, and the egress proxy substitutes them per request.
  • Exports SandboxNetworkTransformContext from both SDKs.
  • Stacked on top of feat(sdk): support structured network rules with per-host transforms #1286, which ships only the static transform shape. Implemented as a straight revert of the stripping commit so the diff is small and the two PRs land cleanly in order.
  • Placeholder-resolution contract tests (transform callback resolves sandboxId placeholder in JS, test_transform_callback_resolves_sandbox_id in Python sync + async) are restored but left commented out / disabled until the proxy-side placeholder resolution ships.

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

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,
                        },
                    },
                },
            ],
        },
    },
)

Notes

  • Callbacks run client-side at create-time; the SDK never resolves the placeholders itself. The wire payload contains the literal ${e2b.*} strings.
  • Depends on egress-proxy support for ${e2b.sandboxId}, ${e2b.teamId}, ${e2b.executionId}, ${e2b.identity.jwt} substitution before the disabled tests can be re-enabled.

Test plan

  • CI green (typecheck/lint/format on both SDKs)
  • Once proxy placeholder substitution ships, re-enable transform callback resolves sandboxId placeholder (JS) and test_transform_callback_resolves_sandbox_id (Python sync + async) and verify end-to-end against httpbin.e2b.team

mishushakov and others added 24 commits April 22, 2026 17:39
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>
- 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>
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>
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>
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>
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>
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>
Comment out the sandboxId placeholder tests until the proxy-side
placeholder resolution (e2b.identity.jwt, e2b.sandboxId, etc.) is in
place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prevents user transform callbacks from mutating the module-level
placeholder context and corrupting subsequent sandbox creations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 1 ships only static `transform: { headers: ... }` rules. The
callback variant — `transform: ({ identity, sandboxId }) => (...)` — and
its placeholder context (TRANSFORM_CONTEXT, SandboxNetworkTransformContext,
SandboxNetworkTransformResolver) are removed for now; they'll return in a
follow-up PR alongside the proxy-side placeholder resolution and tests.

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

changeset-bot Bot commented May 19, 2026

⚠️ No Changeset found

Latest commit: 156c0a9

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 May 19, 2026

PR Summary

Medium Risk
Adds a new callback path in network rule serialization in both JS and Python; incorrect transforms or unexpected placeholder usage could affect egress request behavior. Proxy-side placeholder substitution is not enforced/tested here, increasing integration risk.

Overview
Reintroduces SandboxNetworkRule.transform as a callable in the JS and Python SDKs, but the callback only receives frozen literal placeholder strings and the result is sent to the API without validation. The end-to-end placeholder-resolution tests are present but commented out, so the new callback behavior is not exercised in CI.

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

@github-actions
Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 20f08c4. Download artifacts from this workflow run.

JS SDK (e2b@2.19.2-mishushakov-network-transform-callback.0):

npm install ./e2b-2.19.2-mishushakov-network-transform-callback.0.tgz

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

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

Python SDK (e2b==2.20.1+mishushakov-network-transform-callback):

pip install ./e2b-2.20.1+mishushakov.network.transform.callback-py3-none-any.whl

Base automatically changed from mishushakov/network-allowout-transform to main May 26, 2026 23:35
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.

1 participant