55 DEFAULT_SANDBOX_TIMEOUT_MS ,
66} from '../connectionConfig'
77import { compareVersions } from 'compare-versions'
8+ import { ALL_TRAFFIC } from './network'
89import {
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+
40101export 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+
72181export 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+
416581export 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