Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdks/sandbox/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type {
NetworkPolicy,
NetworkRule,
NetworkRuleAction,
OSSFS,
PVC,
RenewSandboxExpirationRequest,
RenewSandboxExpirationResponse,
Expand Down
49 changes: 46 additions & 3 deletions sdks/sandbox/javascript/src/models/sandboxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,51 @@ export interface PVC extends Record<string, unknown> {
claimName: string;
}

/**
* Alibaba Cloud OSS mount backend via ossfs.
*
* The runtime mounts a host-side OSS path under `storage.ossfs_mount_root`
* so the container sees the bucket contents at the specified mount path.
*
* In Docker runtime, OSSFS backend requires OpenSandbox Server to run on a Linux host with FUSE support.
*/
export interface OSSFS extends Record<string, unknown> {
/**
* OSS bucket name.
*/
bucket: string;
/**
* OSS endpoint (e.g., "oss-cn-hangzhou.aliyuncs.com").
*/
endpoint: string;
/**
* ossfs major version used by runtime mount integration.
* @default "2.0"
*/
version?: "1.0" | "2.0";
/**
* Additional ossfs mount options.
*
* - `1.0`: mounts with `ossfs ... -o <option>`
* - `2.0`: mounts with `ossfs2 mount ... -c <config-file>` and encodes options as `--<option>` lines in the config file
*/
options?: string[];
/**
* OSS access key ID for inline credentials mode.
*/
accessKeyId: string;
/**
* OSS access key secret for inline credentials mode.
*/
accessKeySecret: string;
}

/**
* Storage mount definition for a sandbox.
*
* Each volume entry contains:
* - A unique name identifier
* - Exactly one backend (host, pvc) with backend-specific fields
* - Exactly one backend (host, pvc, ossfs) with backend-specific fields
* - Common mount settings (mountPath, readOnly, subPath)
*/
export interface Volume extends Record<string, unknown> {
Expand All @@ -106,13 +145,17 @@ export interface Volume extends Record<string, unknown> {
*/
name: string;
/**
* Host path bind mount backend (mutually exclusive with pvc).
* Host path bind mount backend (mutually exclusive with pvc, ossfs).
*/
host?: Host;
/**
* Kubernetes PVC mount backend (mutually exclusive with host).
* Kubernetes PVC mount backend (mutually exclusive with host, ossfs).
*/
pvc?: PVC;
/**
* Alibaba Cloud OSSFS mount backend (mutually exclusive with host, pvc).
*/
ossfs?: OSSFS;
/**
* Absolute path inside the container where the volume is mounted.
*/
Expand Down
6 changes: 3 additions & 3 deletions sdks/sandbox/javascript/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,15 @@ export class Sandbox {
// Validate volumes: exactly one backend must be specified per volume
if (opts.volumes) {
for (const vol of opts.volumes) {
const backendsSpecified = [vol.host, vol.pvc].filter((b) => b !== undefined).length;
const backendsSpecified = [vol.host, vol.pvc, vol.ossfs].filter((b) => b !== undefined).length;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat null backends as absent when counting volume backends

The new backend count includes vol.ossfs but uses b !== undefined, so a payload like { name, mountPath, ossfs: null } is now treated as having one backend and bypasses the SDK's "none provided" check. This is a regression for OSSFS callers that pass JSON-normalized null optionals (common in plain JS), and it can also incorrectly trigger the "multiple backends" path when ossfs: null is present alongside a real backend. Counting only non-null values (e.g., b != null) keeps validation aligned with server semantics.

Useful? React with 👍 / 👎.

if (backendsSpecified === 0) {
throw new Error(
`Volume '${vol.name}' must specify exactly one backend (host, pvc), but none was provided.`
`Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but none was provided.`
);
}
if (backendsSpecified > 1) {
throw new Error(
`Volume '${vol.name}' must specify exactly one backend (host, pvc), but multiple were provided.`
`Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but multiple were provided.`
);
}
}
Expand Down
73 changes: 73 additions & 0 deletions sdks/sandbox/javascript/tests/sandbox.create.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,76 @@ test("Sandbox creates and reuses egress service during sandbox lifecycle", async
assert.equal(egressStackCalls[0].egressBaseUrl, `http://127.0.0.1:${DEFAULT_EGRESS_PORT}`);
assert.deepEqual(egressStackCalls[0].endpointHeaders, { "x-port": String(DEFAULT_EGRESS_PORT) });
});

test("Sandbox.create passes OSSFS volume to request", async () => {
const { adapterFactory, recordedRequests } = createAdapterFactory();

await Sandbox.create({
adapterFactory,
connectionConfig: { domain: "http://127.0.0.1:8080" },
image: "python:3.12",
skipHealthCheck: true,
volumes: [
{
name: "oss-data",
ossfs: {
bucket: "my-bucket",
endpoint: "oss-cn-hangzhou.aliyuncs.com",
version: "2.0",
accessKeyId: "ak-id",
accessKeySecret: "ak-secret",
},
mountPath: "/data",
readOnly: false,
},
],
});

assert.equal(recordedRequests.length, 1);
assert.equal(recordedRequests[0].volumes.length, 1);
assert.equal(recordedRequests[0].volumes[0].name, "oss-data");
assert.equal(recordedRequests[0].volumes[0].ossfs.bucket, "my-bucket");
assert.equal(recordedRequests[0].volumes[0].ossfs.endpoint, "oss-cn-hangzhou.aliyuncs.com");
});

test("Sandbox.create rejects volume with no backend", async () => {
const { adapterFactory } = createAdapterFactory();

await assert.rejects(
Sandbox.create({
adapterFactory,
connectionConfig: { domain: "http://127.0.0.1:8080" },
image: "python:3.12",
skipHealthCheck: true,
volumes: [{ name: "empty", mountPath: "/mnt" }],
}),
/must specify exactly one backend \(host, pvc, ossfs\)/
);
});

test("Sandbox.create rejects volume with multiple backends", async () => {
const { adapterFactory } = createAdapterFactory();

await assert.rejects(
Sandbox.create({
adapterFactory,
connectionConfig: { domain: "http://127.0.0.1:8080" },
image: "python:3.12",
skipHealthCheck: true,
volumes: [
{
name: "conflicting",
host: { path: "/tmp" },
ossfs: {
bucket: "b",
endpoint: "e",
accessKeyId: "id",
accessKeySecret: "secret",
},
mountPath: "/mnt",
},
],
}),
/must specify exactly one backend \(host, pvc, ossfs\)/
);
});