Skip to content

Commit 0647691

Browse files
authored
feat: add sandbox endpoint auth headers (#492)
* feat(sdks): passing sandbox endpoint headers * feat: egress endpoint authentication * fix(server): fix resolving internal endpoint when egress activated
1 parent babf835 commit 0647691

33 files changed

Lines changed: 1383 additions & 27 deletions

File tree

sdks/code-interpreter/javascript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"packageManager": "pnpm@9.15.0",
3535
"scripts": {
3636
"build": "tsup",
37+
"test": "pnpm run build && node --test tests/*.test.mjs",
3738
"lint": "eslint src --max-warnings 0",
3839
"clean": "rm -rf dist"
3940
},

sdks/code-interpreter/javascript/src/factory/adapterFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import type { Codes } from "../services/codes.js";
1818
export interface CreateCodesStackOptions {
1919
sandbox: Sandbox;
2020
execdBaseUrl: string;
21+
endpointHeaders?: Record<string, string>;
2122
}
2223

2324
/**
2425
* Factory abstraction for Code Interpreter SDK to decouple from concrete adapters/clients.
2526
*/
2627
export interface AdapterFactory {
2728
createCodes(opts: CreateCodesStackOptions): Codes;
28-
}
29+
}

sdks/code-interpreter/javascript/src/factory/defaultAdapterFactory.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ import type { Codes } from "../services/codes.js";
1919

2020
export class DefaultAdapterFactory implements AdapterFactory {
2121
createCodes(opts: CreateCodesStackOptions): Codes {
22+
const headers: Record<string, string> = {
23+
...(opts.sandbox.connectionConfig.headers ?? {}),
24+
...(opts.endpointHeaders ?? {}),
25+
};
2226
const client = createExecdClient({
2327
baseUrl: opts.execdBaseUrl,
24-
headers: opts.sandbox.connectionConfig.headers,
28+
headers,
2529
fetch: opts.sandbox.connectionConfig.fetch,
2630
});
2731

2832
return new CodesAdapter(client, {
2933
baseUrl: opts.execdBaseUrl,
30-
headers: opts.sandbox.connectionConfig.headers,
34+
headers,
3135
// Streaming calls (SSE) use a dedicated fetch, aligned with Kotlin/Python SDKs.
3236
fetch: opts.sandbox.connectionConfig.sseFetch,
3337
});
@@ -36,4 +40,4 @@ export class DefaultAdapterFactory implements AdapterFactory {
3640

3741
export function createDefaultAdapterFactory(): AdapterFactory {
3842
return new DefaultAdapterFactory();
39-
}
43+
}

sdks/code-interpreter/javascript/src/interpreter.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ export class CodeInterpreter {
3939
) {}
4040

4141
static async create(sandbox: Sandbox, opts: CodeInterpreterCreateOptions = {}): Promise<CodeInterpreter> {
42-
const execdBaseUrl = await sandbox.getEndpointUrl(DEFAULT_EXECD_PORT);
42+
const endpoint = await sandbox.getEndpoint(DEFAULT_EXECD_PORT);
43+
const execdBaseUrl = `${sandbox.connectionConfig.protocol}://${endpoint.endpoint}`;
4344
const adapterFactory = opts.adapterFactory ?? createDefaultAdapterFactory();
44-
const codes = adapterFactory.createCodes({ sandbox, execdBaseUrl });
45+
const codes = adapterFactory.createCodes({
46+
sandbox,
47+
execdBaseUrl,
48+
endpointHeaders: endpoint.headers,
49+
});
4550

4651
return new CodeInterpreter(sandbox, codes);
4752
}
@@ -61,4 +66,4 @@ export class CodeInterpreter {
6166
get metrics() {
6267
return this.sandbox.metrics;
6368
}
64-
}
69+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { DefaultAdapterFactory } from "../dist/index.js";
5+
6+
test("DefaultAdapterFactory merges sandbox and endpoint headers for code requests", async () => {
7+
const recorded = [];
8+
const fetchImpl = async (input, init = {}) => {
9+
const request = input instanceof Request ? input : new Request(input, init);
10+
const url = new URL(request.url);
11+
const headers = Object.fromEntries(request.headers.entries());
12+
recorded.push({
13+
url: request.url,
14+
method: request.method,
15+
headers,
16+
});
17+
18+
if (url.pathname === "/code/context") {
19+
return new Response(JSON.stringify({ id: "ctx-1", language: "python" }), {
20+
status: 200,
21+
headers: { "content-type": "application/json" },
22+
});
23+
}
24+
25+
return new Response(
26+
[
27+
JSON.stringify({ type: "stdout", text: "hello", timestamp: 1 }),
28+
JSON.stringify({ type: "execution_complete", execution_time: 2, timestamp: 2 }),
29+
].join("\n"),
30+
{
31+
status: 200,
32+
headers: { "content-type": "text/event-stream" },
33+
}
34+
);
35+
};
36+
37+
const sandbox = {
38+
connectionConfig: {
39+
headers: { "x-global": "global" },
40+
fetch: fetchImpl,
41+
sseFetch: fetchImpl,
42+
},
43+
};
44+
45+
const factory = new DefaultAdapterFactory();
46+
const codes = factory.createCodes({
47+
sandbox,
48+
execdBaseUrl: "http://sandbox.internal:3456",
49+
endpointHeaders: { "x-endpoint": "endpoint" },
50+
});
51+
52+
const context = await codes.createContext("python");
53+
assert.equal(context.id, "ctx-1");
54+
55+
const execution = await codes.run("print('hello')");
56+
assert.equal(execution.logs.stdout[0]?.text, "hello");
57+
58+
assert.equal(recorded.length, 2);
59+
assert.equal(recorded[0].url, "http://sandbox.internal:3456/code/context");
60+
assert.equal(recorded[0].headers["x-global"], "global");
61+
assert.equal(recorded[0].headers["x-endpoint"], "endpoint");
62+
assert.equal(recorded[1].url, "http://sandbox.internal:3456/code");
63+
assert.equal(recorded[1].headers["x-global"], "global");
64+
assert.equal(recorded[1].headers["x-endpoint"], "endpoint");
65+
assert.equal(recorded[1].headers.accept, "text/event-stream");
66+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { CodeInterpreter } from "../dist/index.js";
5+
import { DEFAULT_EXECD_PORT } from "../../../sandbox/javascript/dist/index.js";
6+
7+
test("CodeInterpreter.create forwards endpoint headers to adapter factory", async () => {
8+
const calls = [];
9+
const sandbox = {
10+
connectionConfig: {
11+
protocol: "https",
12+
headers: { "x-global": "global" },
13+
},
14+
async getEndpoint(port) {
15+
assert.equal(port, DEFAULT_EXECD_PORT);
16+
return {
17+
endpoint: "sandbox.internal:3456",
18+
headers: { "x-endpoint": "endpoint" },
19+
};
20+
},
21+
};
22+
const codes = { kind: "codes" };
23+
const adapterFactory = {
24+
createCodes(opts) {
25+
calls.push(opts);
26+
return codes;
27+
},
28+
};
29+
30+
const interpreter = await CodeInterpreter.create(sandbox, { adapterFactory });
31+
32+
assert.equal(interpreter.codes, codes);
33+
assert.equal(calls.length, 1);
34+
assert.equal(calls[0].execdBaseUrl, "https://sandbox.internal:3456");
35+
assert.deepEqual(calls[0].endpointHeaders, { "x-endpoint": "endpoint" });
36+
});

sdks/code-interpreter/kotlin/code-interpreter/src/main/kotlin/com/alibaba/opensandbox/codeinterpreter/infrastructure/adapters/service/CodesAdapter.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.Executi
3434
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.jsonParser
3535
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.parseSandboxError
3636
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.toSandboxException
37+
import okhttp3.Headers.Companion.toHeaders
3738
import okhttp3.MediaType.Companion.toMediaType
3839
import okhttp3.Request
3940
import okhttp3.RequestBody.Companion.toRequestBody
@@ -49,8 +50,19 @@ class CodesAdapter(
4950
}
5051

5152
private val logger = LoggerFactory.getLogger(CodesAdapter::class.java)
53+
private val baseUrl = "${httpClientProvider.config.protocol}://${execdEndpoint.endpoint}"
54+
private val apiClient =
55+
httpClientProvider.httpClient.newBuilder()
56+
.addInterceptor { chain ->
57+
val requestBuilder = chain.request().newBuilder()
58+
execdEndpoint.headers.forEach { (key, value) ->
59+
requestBuilder.header(key, value)
60+
}
61+
chain.proceed(requestBuilder.build())
62+
}
63+
.build()
5264
private val api =
53-
CodeInterpretingApi("${httpClientProvider.config.protocol}://${execdEndpoint.endpoint}", httpClientProvider.httpClient)
65+
CodeInterpretingApi(baseUrl, apiClient)
5466

5567
override fun getContext(id: String): CodeContext {
5668
try {
@@ -109,10 +121,11 @@ class CodesAdapter(
109121
val apiRequest = request.toApiRunCodeRequest()
110122
val httpRequest =
111123
Request.Builder()
112-
.url("${httpClientProvider.config.protocol}://${execdEndpoint.endpoint}$RUN_CODE_PATH")
124+
.url("$baseUrl$RUN_CODE_PATH")
113125
.post(
114126
jsonParser.encodeToString(apiRequest).toRequestBody("application/json".toMediaType()),
115127
)
128+
.headers(execdEndpoint.headers.toHeaders())
116129
.build()
117130

118131
val execution = Execution()

sdks/code-interpreter/kotlin/code-interpreter/src/test/kotlin/com/alibaba/opensandbox/codeinterpreter/infrastructure/adapters/service/CodesAdapterTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,32 @@ class CodesAdapterTest {
8080
assertEquals("/code/context", request.path)
8181
}
8282

83+
@Test
84+
fun `createContext should include endpoint headers`() {
85+
mockWebServer.enqueue(
86+
MockResponse()
87+
.setResponseCode(200)
88+
.setBody("""{"id":"ctx-123", "language":"python"}"""),
89+
)
90+
91+
val host = mockWebServer.hostName
92+
val port = mockWebServer.port
93+
val config =
94+
ConnectionConfig.builder()
95+
.domain("$host:$port")
96+
.protocol("http")
97+
.build()
98+
val endpoint = SandboxEndpoint("$host:$port", mapOf("X-Endpoint" to "endpoint"))
99+
100+
HttpClientProvider(config).use { provider ->
101+
val adapter = CodesAdapter(endpoint, provider)
102+
adapter.createContext("python")
103+
}
104+
105+
val request = mockWebServer.takeRequest()
106+
assertEquals("endpoint", request.getHeader("X-Endpoint"))
107+
}
108+
83109
@Test
84110
fun `run should stream events correctly`() {
85111
// SSE format
@@ -124,6 +150,41 @@ class CodesAdapterTest {
124150
assertEquals("POST", recordedRequest.method)
125151
}
126152

153+
@Test
154+
fun `run should include endpoint headers`() {
155+
val event1 = """{"type":"stdout","text":"Hello World","timestamp":1672531200000}"""
156+
val event2 = """{"type":"execution_complete","execution_time":100,"timestamp":1672531201000}"""
157+
158+
mockWebServer.enqueue(
159+
MockResponse()
160+
.setResponseCode(200)
161+
.setBody("$event1\n$event2\n"),
162+
)
163+
164+
val host = mockWebServer.hostName
165+
val port = mockWebServer.port
166+
val config =
167+
ConnectionConfig.builder()
168+
.domain("$host:$port")
169+
.protocol("http")
170+
.build()
171+
val endpoint = SandboxEndpoint("$host:$port", mapOf("X-Endpoint" to "endpoint"))
172+
173+
HttpClientProvider(config).use { provider ->
174+
val adapter = CodesAdapter(endpoint, provider)
175+
val request =
176+
RunCodeRequest.builder()
177+
.code("print('Hello World')")
178+
.handlers(ExecutionHandlers.builder().build())
179+
.build()
180+
181+
adapter.run(request)
182+
}
183+
184+
val recordedRequest = mockWebServer.takeRequest()
185+
assertEquals("endpoint", recordedRequest.getHeader("X-Endpoint"))
186+
}
187+
127188
@Test
128189
fun `interrupt should send correct request`() {
129190
mockWebServer.enqueue(MockResponse().setResponseCode(204))

0 commit comments

Comments
 (0)