diff --git a/sdks/sandbox/csharp/src/OpenSandbox/Factory/DefaultAdapterFactory.cs b/sdks/sandbox/csharp/src/OpenSandbox/Factory/DefaultAdapterFactory.cs
index 0f0bc0d09..0fc576fef 100644
--- a/sdks/sandbox/csharp/src/OpenSandbox/Factory/DefaultAdapterFactory.cs
+++ b/sdks/sandbox/csharp/src/OpenSandbox/Factory/DefaultAdapterFactory.cs
@@ -13,6 +13,7 @@
// limitations under the License.
using OpenSandbox.Adapters;
+using OpenSandbox.Config;
using OpenSandbox.Core;
using OpenSandbox.Internal;
using Microsoft.Extensions.Logging;
@@ -58,7 +59,7 @@ public LifecycleStack CreateLifecycleStack(CreateLifecycleStackOptions options)
///
public ExecdStack CreateExecdStack(CreateExecdStackOptions options)
{
- var headers = options.ExecdHeaders ?? options.ConnectionConfig.Headers;
+ var headers = BuildDataPlaneHeaders(options.ConnectionConfig, options.ExecdHeaders);
var clientWrapper = new HttpClientWrapper(
options.HttpClientProvider.HttpClient,
@@ -100,7 +101,7 @@ public ExecdStack CreateExecdStack(CreateExecdStackOptions options)
///
public EgressStack CreateEgressStack(CreateEgressStackOptions options)
{
- var headers = options.EgressHeaders ?? options.ConnectionConfig.Headers;
+ var headers = BuildDataPlaneHeaders(options.ConnectionConfig, options.EgressHeaders);
var clientWrapper = new HttpClientWrapper(
options.HttpClientProvider.HttpClient,
@@ -115,4 +116,35 @@ public EgressStack CreateEgressStack(CreateEgressStackOptions options)
CredentialVault = egress
};
}
+
+ internal static IReadOnlyDictionary BuildDataPlaneHeaders(
+ ConnectionConfig connectionConfig,
+ IReadOnlyDictionary? endpointHeaders)
+ {
+ var headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var header in connectionConfig.Headers)
+ {
+ headers[header.Key] = header.Value;
+ }
+
+ if (endpointHeaders != null)
+ {
+ foreach (var header in endpointHeaders)
+ {
+ headers[header.Key] = header.Value;
+ }
+ }
+
+ if (!connectionConfig.UseServerProxy)
+ {
+ headers.Remove(Constants.ApiKeyHeader);
+ }
+ else if (!headers.ContainsKey(Constants.ApiKeyHeader) &&
+ connectionConfig.ApiKey is { Length: > 0 } apiKey)
+ {
+ headers[Constants.ApiKeyHeader] = apiKey;
+ }
+
+ return headers;
+ }
}
diff --git a/sdks/sandbox/csharp/src/OpenSandbox/HttpClientProvider.cs b/sdks/sandbox/csharp/src/OpenSandbox/HttpClientProvider.cs
index 522983e62..a0d51165e 100644
--- a/sdks/sandbox/csharp/src/OpenSandbox/HttpClientProvider.cs
+++ b/sdks/sandbox/csharp/src/OpenSandbox/HttpClientProvider.cs
@@ -13,6 +13,7 @@
// limitations under the License.
using OpenSandbox.Config;
+using OpenSandbox.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -32,6 +33,12 @@ internal HttpClientProvider(ConnectionConfig connectionConfig, ILoggerFactory lo
_logger.LogDebug("Creating HTTP clients for SDK instance");
HttpClient = connectionConfig.CreateHttpClient();
SseHttpClient = connectionConfig.CreateSseHttpClient();
+
+ // These clients are shared by lifecycle and data-plane adapters. Keep
+ // tenant credentials request-scoped so direct execd/egress calls cannot
+ // inherit them through DefaultRequestHeaders.
+ HttpClient.DefaultRequestHeaders.Remove(Constants.ApiKeyHeader);
+ SseHttpClient.DefaultRequestHeaders.Remove(Constants.ApiKeyHeader);
}
///
diff --git a/sdks/sandbox/csharp/tests/OpenSandbox.Tests/DefaultAdapterFactoryTests.cs b/sdks/sandbox/csharp/tests/OpenSandbox.Tests/DefaultAdapterFactoryTests.cs
new file mode 100644
index 000000000..8265986de
--- /dev/null
+++ b/sdks/sandbox/csharp/tests/OpenSandbox.Tests/DefaultAdapterFactoryTests.cs
@@ -0,0 +1,98 @@
+// Copyright 2026 Alibaba Group Holding Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using OpenSandbox.Config;
+using OpenSandbox.Core;
+using OpenSandbox.Factory;
+using Xunit;
+
+namespace OpenSandbox.Tests;
+
+public class DefaultAdapterFactoryTests
+{
+ [Fact]
+ public void BuildDataPlaneHeaders_InDirectMode_ShouldRemoveApiKeyCaseInsensitively()
+ {
+ var config = new ConnectionConfig(new ConnectionConfigOptions
+ {
+ ApiKey = "tenant-secret",
+ Headers = new Dictionary
+ {
+ ["open-sandbox-api-key"] = "explicit-secret",
+ ["X-Custom-Header"] = "custom-value"
+ },
+ UseServerProxy = false
+ });
+
+ var headers = DefaultAdapterFactory.BuildDataPlaneHeaders(
+ config,
+ new Dictionary { ["X-Endpoint-Token"] = "route-token" });
+
+ headers.Keys.Should().NotContain(key =>
+ key.Equals(Constants.ApiKeyHeader, StringComparison.OrdinalIgnoreCase));
+ headers["X-Custom-Header"].Should().Be("custom-value");
+ headers["X-Endpoint-Token"].Should().Be("route-token");
+ }
+
+ [Fact]
+ public void BuildDataPlaneHeaders_InServerProxyMode_ShouldRetainApiKey()
+ {
+ var config = new ConnectionConfig(new ConnectionConfigOptions
+ {
+ ApiKey = "tenant-secret",
+ UseServerProxy = true
+ });
+
+ var headers = DefaultAdapterFactory.BuildDataPlaneHeaders(config, null);
+
+ headers[Constants.ApiKeyHeader].Should().Be("tenant-secret");
+ }
+
+ [Fact]
+ public void BuildDataPlaneHeaders_InServerProxyMode_ShouldPreferEndpointApiKey()
+ {
+ var config = new ConnectionConfig(new ConnectionConfigOptions
+ {
+ ApiKey = "tenant-secret",
+ UseServerProxy = true
+ });
+
+ var headers = DefaultAdapterFactory.BuildDataPlaneHeaders(
+ config,
+ new Dictionary
+ {
+ ["open-sandbox-api-key"] = "endpoint-secret"
+ });
+
+ headers[Constants.ApiKeyHeader].Should().Be("endpoint-secret");
+ }
+
+ [Fact]
+ public void SharedHttpClients_ShouldNotCarryTenantApiKeyByDefault()
+ {
+ var config = new ConnectionConfig(new ConnectionConfigOptions
+ {
+ ApiKey = "tenant-secret",
+ UseServerProxy = false
+ });
+
+ using var provider = new HttpClientProvider(config, NullLoggerFactory.Instance);
+
+ provider.HttpClient.DefaultRequestHeaders.Contains(Constants.ApiKeyHeader).Should().BeFalse();
+ provider.SseHttpClient.DefaultRequestHeaders.Contains(Constants.ApiKeyHeader).Should().BeFalse();
+ config.Headers[Constants.ApiKeyHeader].Should().Be("tenant-secret");
+ }
+}
diff --git a/sdks/sandbox/javascript/src/factory/defaultAdapterFactory.ts b/sdks/sandbox/javascript/src/factory/defaultAdapterFactory.ts
index 2465006ca..2c9cc401c 100644
--- a/sdks/sandbox/javascript/src/factory/defaultAdapterFactory.ts
+++ b/sdks/sandbox/javascript/src/factory/defaultAdapterFactory.ts
@@ -34,6 +34,41 @@ import type {
LifecycleStack,
} from "./adapterFactory.js";
+const API_KEY_HEADER = "OPEN-SANDBOX-API-KEY";
+
+function createDataPlaneHeaders(
+ connectionHeaders: Record,
+ endpointHeaders: Record | undefined,
+ useServerProxy: boolean,
+ apiKey: string | undefined,
+): Record {
+ const headers: Record = {
+ ...connectionHeaders,
+ ...(endpointHeaders ?? {}),
+ };
+ const endpointApiKey = Object.entries(endpointHeaders ?? {}).find(
+ ([key]) => key.toLowerCase() === API_KEY_HEADER.toLowerCase(),
+ );
+
+ if (!useServerProxy || endpointApiKey || apiKey) {
+ for (const key of Object.keys(headers)) {
+ if (key.toLowerCase() === API_KEY_HEADER.toLowerCase()) {
+ delete headers[key];
+ }
+ }
+ }
+
+ if (useServerProxy) {
+ if (endpointApiKey) {
+ headers[endpointApiKey[0]] = endpointApiKey[1];
+ } else if (apiKey) {
+ headers[API_KEY_HEADER] = apiKey;
+ }
+ }
+
+ return headers;
+}
+
export class DefaultAdapterFactory implements AdapterFactory {
createLifecycleStack(opts: CreateLifecycleStackOptions): LifecycleStack {
const lifecycleClient = createLifecycleClient({
@@ -51,10 +86,12 @@ export class DefaultAdapterFactory implements AdapterFactory {
}
createExecdStack(opts: CreateExecdStackOptions): ExecdStack {
- const headers: Record = {
- ...(opts.connectionConfig.headers ?? {}),
- ...(opts.endpointHeaders ?? {}),
- };
+ const headers = createDataPlaneHeaders(
+ opts.connectionConfig.headers,
+ opts.endpointHeaders,
+ opts.connectionConfig.useServerProxy,
+ opts.connectionConfig.apiKey,
+ );
const execdClient = createExecdClient({
baseUrl: opts.execdBaseUrl,
headers,
@@ -91,10 +128,12 @@ export class DefaultAdapterFactory implements AdapterFactory {
}
createEgressStack(opts: CreateEgressStackOptions): EgressStack {
- const headers: Record = {
- ...(opts.connectionConfig.headers ?? {}),
- ...(opts.endpointHeaders ?? {}),
- };
+ const headers = createDataPlaneHeaders(
+ opts.connectionConfig.headers,
+ opts.endpointHeaders,
+ opts.connectionConfig.useServerProxy,
+ opts.connectionConfig.apiKey,
+ );
const egressClient = createEgressClient({
baseUrl: opts.egressBaseUrl,
headers,
diff --git a/sdks/sandbox/javascript/tests/data-plane-auth.test.mjs b/sdks/sandbox/javascript/tests/data-plane-auth.test.mjs
new file mode 100644
index 000000000..4db3b18af
--- /dev/null
+++ b/sdks/sandbox/javascript/tests/data-plane-auth.test.mjs
@@ -0,0 +1,132 @@
+// Copyright 2026 Alibaba Group Holding Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { ConnectionConfig, DefaultAdapterFactory } from "../dist/index.js";
+
+const API_KEY_HEADER = "open-sandbox-api-key";
+
+function createConfig(useServerProxy, requests, includeExplicitApiKey = false) {
+ const fetchImpl = async (input, init) => {
+ const request = new Request(input, init);
+ requests.push(request);
+
+ if (new URL(request.url).pathname === "/ping") {
+ return new Response("", { status: 200 });
+ }
+ if (new URL(request.url).pathname === "/policy") {
+ return Response.json({ policy: { defaultAction: "deny", egress: [] } });
+ }
+ return Response.json({
+ id: "sandbox-1",
+ status: { state: "Running" },
+ entrypoint: ["sleep", "infinity"],
+ createdAt: "2026-09-01T00:00:00Z",
+ expiresAt: null,
+ });
+ };
+
+ const headers = { "X-Custom-Header": "custom-value" };
+ if (includeExplicitApiKey) {
+ headers["open-sandbox-api-key"] = "explicit-secret";
+ }
+
+ const config = new ConnectionConfig({
+ domain: "api.opensandbox.test",
+ apiKey: "tenant-secret",
+ headers,
+ useServerProxy,
+ });
+ config._fetch = fetchImpl;
+ config._sseFetch = fetchImpl;
+ return config;
+}
+
+async function sendDataPlaneRequests(useServerProxy, useEndpointApiKeys = false) {
+ const requests = [];
+ const connectionConfig = createConfig(
+ useServerProxy,
+ requests,
+ !useServerProxy,
+ );
+ const factory = new DefaultAdapterFactory();
+ const execd = factory.createExecdStack({
+ connectionConfig,
+ execdBaseUrl: "http://execd.opensandbox.test",
+ endpointHeaders: {
+ "X-Endpoint-Token": "execd-token",
+ ...(useEndpointApiKeys
+ ? { "open-sandbox-api-key": "execd-secret" }
+ : {}),
+ },
+ });
+ const egress = factory.createEgressStack({
+ connectionConfig,
+ egressBaseUrl: "http://egress.opensandbox.test",
+ endpointHeaders: {
+ "X-Endpoint-Token": "egress-token",
+ ...(useEndpointApiKeys
+ ? { "open-sandbox-api-key": "egress-secret" }
+ : {}),
+ },
+ });
+
+ await execd.health.ping();
+ await egress.egress.getPolicy();
+ return requests;
+}
+
+test("direct execd and egress requests omit the tenant API key", async () => {
+ const requests = await sendDataPlaneRequests(false);
+
+ assert.equal(requests.length, 2);
+ for (const request of requests) {
+ assert.equal(request.headers.has(API_KEY_HEADER), false);
+ assert.equal(request.headers.get("x-custom-header"), "custom-value");
+ assert.ok(request.headers.get("x-endpoint-token"));
+ }
+});
+
+test("server-proxied execd and egress requests retain the tenant API key", async () => {
+ const requests = await sendDataPlaneRequests(true);
+
+ assert.equal(requests.length, 2);
+ for (const request of requests) {
+ assert.equal(request.headers.get(API_KEY_HEADER), "tenant-secret");
+ }
+});
+
+test("server-proxied requests preserve endpoint-specific API keys", async () => {
+ const requests = await sendDataPlaneRequests(true, true);
+
+ assert.equal(requests.length, 2);
+ assert.equal(requests[0].headers.get(API_KEY_HEADER), "execd-secret");
+ assert.equal(requests[1].headers.get(API_KEY_HEADER), "egress-secret");
+});
+
+test("lifecycle requests retain the tenant API key in direct mode", async () => {
+ const requests = [];
+ const connectionConfig = createConfig(false, requests);
+ const lifecycle = new DefaultAdapterFactory().createLifecycleStack({
+ connectionConfig,
+ lifecycleBaseUrl: connectionConfig.getBaseUrl(),
+ });
+
+ await lifecycle.sandboxes.getSandbox("sandbox-1");
+
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].headers.get(API_KEY_HEADER), "tenant-secret");
+});