Skip to content

Commit 3786f34

Browse files
authored
feat(sdk): support structured network rules with per-host transforms (#1286)
1 parent ba315c0 commit 3786f34

23 files changed

Lines changed: 1375 additions & 47 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'e2b': minor
3+
'@e2b/python-sdk': minor
4+
---
5+
6+
Support structured network rules with per-host transforms

packages/js-sdk/src/api/schema.gen.ts

Lines changed: 72 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/js-sdk/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ export type {
5757
SandboxListOpts,
5858
SandboxPaginator,
5959
SandboxNetworkOpts,
60+
SandboxNetworkInfo,
61+
SandboxNetworkSelector,
62+
SandboxNetworkSelectorContext,
63+
SandboxNetworkRule,
64+
SandboxNetworkRuleInfo,
65+
SandboxNetworkRules,
66+
SandboxNetworkTransform,
6067
SandboxLifecycle,
6168
SandboxInfoLifecycle,
6269
SnapshotInfo,

packages/js-sdk/src/sandbox/sandboxApi.ts

Lines changed: 172 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DEFAULT_SANDBOX_TIMEOUT_MS,
66
} from '../connectionConfig'
77
import { compareVersions } from 'compare-versions'
8+
import { ALL_TRAFFIC } from './network'
89
import {
910
InvalidArgumentError,
1011
SandboxNotFoundError,
@@ -37,23 +38,118 @@ export type GitHubMcpServer = {
3738
}
3839
}
3940

41+
/**
42+
* Transform applied to egress requests matching a {@link SandboxNetworkRule}.
43+
*/
44+
export type SandboxNetworkTransform = {
45+
/**
46+
* Headers to inject into the outbound request. Values override any headers
47+
* already present on the request.
48+
*/
49+
headers?: Record<string, string>
50+
}
51+
52+
/**
53+
* Per-domain rule applied to egress requests.
54+
*/
55+
export type SandboxNetworkRule = {
56+
/**
57+
* Transform applied to requests matching this rule.
58+
*/
59+
transform?: SandboxNetworkTransform
60+
}
61+
62+
/**
63+
* Map of host (or CIDR / IP) to ordered list of rules applied to outbound
64+
* requests for that host. Accepts either a plain object or a `Map`.
65+
* Registering a host here does not allow egress on its own — the host must
66+
* also appear in {@link SandboxNetworkOpts.allowOut}.
67+
*/
68+
export type SandboxNetworkRules =
69+
| Record<string, SandboxNetworkRule[]>
70+
| Map<string, SandboxNetworkRule[]>
71+
72+
/**
73+
* Per-domain rule as returned by the sandbox info endpoint. Mirrors
74+
* {@link SandboxNetworkRule} but with `transform` always materialized to the
75+
* static {@link SandboxNetworkTransform} shape — no callback variant.
76+
*/
77+
export type SandboxNetworkRuleInfo = {
78+
transform?: SandboxNetworkTransform
79+
}
80+
81+
/**
82+
* Context passed to {@link SandboxNetworkOpts.allowOut} and
83+
* {@link SandboxNetworkOpts.denyOut} when they are defined as functions.
84+
*/
85+
export type SandboxNetworkSelectorContext = {
86+
/** All traffic sentinel — equivalent to `'0.0.0.0/0'`. */
87+
allTraffic: string
88+
/** Rules registered in {@link SandboxNetworkOpts.rules}. */
89+
rules: Map<string, SandboxNetworkRule[]>
90+
}
91+
92+
/**
93+
* Egress rule list, either a static array of CIDR blocks / IP addresses /
94+
* hostnames, or a callback that receives `{ allTraffic, rules }` and returns
95+
* the same.
96+
*/
97+
export type SandboxNetworkSelector =
98+
| string[]
99+
| ((ctx: SandboxNetworkSelectorContext) => string[])
100+
40101
export type SandboxNetworkOpts = {
41102
/**
42103
* Allow outbound traffic from the sandbox to the specified addresses.
43104
* If `allowOut` is not specified, all outbound traffic is allowed.
44105
*
106+
* Accepts either a static array of CIDR blocks, IP addresses, or hostnames,
107+
* or a callback that receives `{ allTraffic, rules }` and returns the same.
108+
* `allTraffic` is `'0.0.0.0/0'`; `rules` is a `Map` view of
109+
* {@link SandboxNetworkOpts.rules}.
110+
*
45111
* Examples:
46-
* - To allow traffic to a specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
112+
* - Static list: `["1.1.1.1", "8.8.8.0/24"]`
113+
* - Allow only rule-registered hosts:
114+
* `({ rules }) => [...rules.keys()]`
47115
*/
48-
allowOut?: string[]
116+
allowOut?: SandboxNetworkSelector
49117

50118
/**
51119
* Deny outbound traffic from the sandbox to the specified addresses.
52120
*
121+
* Accepts the same shapes as {@link allowOut}.
122+
*
53123
* Examples:
54-
* - To deny traffic to a specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
124+
* - Static list: `["1.1.1.1", "8.8.8.0/24"]`
125+
* - Block all egress: `({ allTraffic }) => [allTraffic]`
55126
*/
56-
denyOut?: string[]
127+
denyOut?: SandboxNetworkSelector
128+
129+
/**
130+
* Per-domain transform rules applied to matching egress HTTP/HTTPS
131+
* requests. Keys are domains (e.g. `"api.example.com"`); values are
132+
* ordered lists of rules.
133+
*
134+
* Registering a host here does not allow egress on its own — the host must
135+
* also appear in {@link allowOut}. Hosts registered here are exposed to the
136+
* `allowOut`/`denyOut` callbacks via `rules`.
137+
*
138+
* @example
139+
* ```ts
140+
* await Sandbox.create({
141+
* network: {
142+
* allowOut: ({ rules }) => [...rules.keys()],
143+
* rules: {
144+
* 'api.openai.com': [
145+
* { transform: { headers: { Authorization: `Bearer ${token}` } } },
146+
* ],
147+
* },
148+
* },
149+
* })
150+
* ```
151+
*/
152+
rules?: SandboxNetworkRules
57153

58154
/**
59155
* Specify if the sandbox URLs should be accessible only with authentication.
@@ -69,6 +165,19 @@ export type SandboxNetworkOpts = {
69165
maskRequestHost?: string
70166
}
71167

168+
/**
169+
* Network configuration as returned by the sandbox info endpoint. Mirrors
170+
* {@link SandboxNetworkOpts} but with `allowOut`/`denyOut` always materialized
171+
* to plain string arrays.
172+
*/
173+
export type SandboxNetworkInfo = {
174+
allowOut?: string[]
175+
denyOut?: string[]
176+
rules?: Record<string, SandboxNetworkRuleInfo[]>
177+
allowPublicTraffic?: boolean
178+
maskRequestHost?: string
179+
}
180+
72181
export type SandboxLifecycle = {
73182
/**
74183
* Action to take when sandbox timeout is reached.
@@ -360,7 +469,7 @@ export interface SandboxInfo {
360469
/**
361470
* Sandbox network configuration.
362471
*/
363-
network?: SandboxNetworkOpts
472+
network?: SandboxNetworkInfo
364473

365474
/**
366475
* Sandbox lifecycle configuration.
@@ -413,6 +522,62 @@ export interface SandboxMetrics {
413522
diskTotal: number
414523
}
415524

525+
function resolveNetworkSelector(
526+
selector: SandboxNetworkSelector | undefined,
527+
rules: Map<string, SandboxNetworkRule[]>
528+
): string[] | undefined {
529+
if (selector === undefined) {
530+
return undefined
531+
}
532+
533+
if (typeof selector === 'function') {
534+
return selector({ allTraffic: ALL_TRAFFIC, rules })
535+
}
536+
537+
return selector
538+
}
539+
540+
function resolveRulesForBody(
541+
rules: Map<string, SandboxNetworkRule[]>
542+
): Record<string, { transform?: SandboxNetworkTransform }[]> {
543+
const out: Record<string, { transform?: SandboxNetworkTransform }[]> = {}
544+
for (const [host, hostRules] of rules) {
545+
out[host] = hostRules.map((rule) =>
546+
rule.transform === undefined ? {} : { transform: rule.transform }
547+
)
548+
}
549+
return out
550+
}
551+
552+
function buildNetworkBody(
553+
network: SandboxNetworkOpts | undefined
554+
): components['schemas']['SandboxNetworkConfig'] | undefined {
555+
if (!network) {
556+
return undefined
557+
}
558+
559+
const rules =
560+
network.rules instanceof Map
561+
? network.rules
562+
: new Map(Object.entries(network.rules ?? {}))
563+
const allowOut = resolveNetworkSelector(network.allowOut, rules)
564+
const denyOut = resolveNetworkSelector(network.denyOut, rules)
565+
566+
return {
567+
...(allowOut !== undefined ? { allowOut } : {}),
568+
...(denyOut !== undefined ? { denyOut } : {}),
569+
...(network.rules !== undefined
570+
? { rules: resolveRulesForBody(rules) }
571+
: {}),
572+
...(network.allowPublicTraffic !== undefined
573+
? { allowPublicTraffic: network.allowPublicTraffic }
574+
: {}),
575+
...(network.maskRequestHost !== undefined
576+
? { maskRequestHost: network.maskRequestHost }
577+
: {}),
578+
}
579+
}
580+
416581
export class SandboxApi {
417582
protected constructor() {}
418583

@@ -605,6 +770,7 @@ export class SandboxApi {
605770
? {
606771
allowOut: res.data.network.allowOut,
607772
denyOut: res.data.network.denyOut,
773+
rules: res.data.network.rules ?? undefined,
608774
allowPublicTraffic: res.data.network.allowPublicTraffic,
609775
maskRequestHost: res.data.network.maskRequestHost,
610776
}
@@ -786,7 +952,7 @@ export class SandboxApi {
786952
timeout: timeoutToSeconds(timeoutMs),
787953
secure: opts?.secure ?? true,
788954
allow_internet_access: opts?.allowInternetAccess ?? true,
789-
network: opts?.network,
955+
network: buildNetworkBody(opts?.network),
790956
autoPause: onTimeout === 'pause',
791957
autoResume: { enabled: autoResume },
792958
}

0 commit comments

Comments
 (0)