@alibaba-group/opensandbox 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -32
- package/dist/{chunk-4EF4ODU2.js → chunk-XHEWHFQ6.js} +184 -22
- package/dist/chunk-XHEWHFQ6.js.map +1 -0
- package/dist/cjs/index.cjs +345 -41
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/internal.cjs +182 -21
- package/dist/cjs/internal.cjs.map +1 -1
- package/dist/index.d.ts +47 -7
- package/dist/index.js +163 -20
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +365 -9
- package/dist/internal.js +1 -1
- package/dist/{sandboxes-CLy12BN1.d.ts → sandboxes-DL9uUHGR.d.ts} +296 -134
- package/package.json +3 -2
- package/src/adapters/commandsAdapter.ts +250 -23
- package/src/adapters/egressAdapter.ts +46 -0
- package/src/adapters/sandboxesAdapter.ts +14 -5
- package/src/api/egress.ts +184 -0
- package/src/api/execd.ts +223 -0
- package/src/api/lifecycle.ts +122 -7
- package/src/config/connection.ts +11 -0
- package/src/core/constants.ts +2 -1
- package/src/core/exceptions.ts +5 -4
- package/src/factory/adapterFactory.ts +14 -1
- package/src/factory/defaultAdapterFactory.ts +34 -5
- package/src/index.ts +8 -2
- package/src/models/execd.ts +32 -6
- package/src/models/execution.ts +2 -1
- package/src/models/sandboxes.ts +121 -4
- package/src/openapi/egressClient.ts +45 -0
- package/src/sandbox.ts +114 -12
- package/src/services/egress.ts +27 -0
- package/src/services/execdCommands.ts +41 -2
- package/src/services/sandboxes.ts +6 -2
- package/dist/chunk-4EF4ODU2.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -33,15 +33,19 @@ export type {
|
|
|
33
33
|
CreateSandboxRequest,
|
|
34
34
|
CreateSandboxResponse,
|
|
35
35
|
Endpoint,
|
|
36
|
+
Host,
|
|
36
37
|
ListSandboxesParams,
|
|
37
38
|
ListSandboxesResponse,
|
|
38
39
|
NetworkPolicy,
|
|
39
40
|
NetworkRule,
|
|
40
41
|
NetworkRuleAction,
|
|
42
|
+
OSSFS,
|
|
43
|
+
PVC,
|
|
41
44
|
RenewSandboxExpirationRequest,
|
|
42
45
|
RenewSandboxExpirationResponse,
|
|
43
46
|
SandboxId,
|
|
44
47
|
SandboxInfo,
|
|
48
|
+
Volume,
|
|
45
49
|
} from "./models/sandboxes.js";
|
|
46
50
|
|
|
47
51
|
export type { Sandboxes } from "./services/sandboxes.js";
|
|
@@ -63,8 +67,9 @@ export type {
|
|
|
63
67
|
|
|
64
68
|
export type {
|
|
65
69
|
CommandExecution,
|
|
70
|
+
CommandLogs,
|
|
71
|
+
CommandStatus,
|
|
66
72
|
RunCommandOpts,
|
|
67
|
-
RunCommandRequest,
|
|
68
73
|
ServerStreamEvent,
|
|
69
74
|
CodeContextRequest,
|
|
70
75
|
SupportedLanguage,
|
|
@@ -87,6 +92,7 @@ export { ExecutionEventDispatcher } from "./models/executionEventDispatcher.js";
|
|
|
87
92
|
|
|
88
93
|
export {
|
|
89
94
|
DEFAULT_ENTRYPOINT,
|
|
95
|
+
DEFAULT_EGRESS_PORT,
|
|
90
96
|
DEFAULT_EXECD_PORT,
|
|
91
97
|
DEFAULT_RESOURCE_LIMITS,
|
|
92
98
|
DEFAULT_TIMEOUT_SECONDS,
|
|
@@ -108,4 +114,4 @@ export type {
|
|
|
108
114
|
SetPermissionEntry,
|
|
109
115
|
WriteEntry,
|
|
110
116
|
} from "./models/filesystem.js";
|
|
111
|
-
export type { SandboxFiles } from "./services/filesystem.js";
|
|
117
|
+
export type { SandboxFiles } from "./services/filesystem.js";
|
package/src/models/execd.ts
CHANGED
|
@@ -37,12 +37,6 @@ export interface ServerStreamEvent extends Record<string, unknown> {
|
|
|
37
37
|
error?: Record<string, unknown>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export interface RunCommandRequest extends Record<string, unknown> {
|
|
41
|
-
command: string;
|
|
42
|
-
cwd?: string;
|
|
43
|
-
background?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
40
|
export interface CodeContextRequest extends Record<string, unknown> {
|
|
47
41
|
language: string;
|
|
48
42
|
}
|
|
@@ -64,6 +58,38 @@ export interface RunCommandOpts {
|
|
|
64
58
|
* Run command in detached mode.
|
|
65
59
|
*/
|
|
66
60
|
background?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Maximum execution time in seconds; server will terminate the command when reached.
|
|
63
|
+
* If omitted, the server will not enforce any timeout.
|
|
64
|
+
*/
|
|
65
|
+
timeoutSeconds?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Unix user ID used to run the command process.
|
|
68
|
+
*/
|
|
69
|
+
uid?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Unix group ID used to run the command process. Requires `uid`.
|
|
72
|
+
*/
|
|
73
|
+
gid?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Environment variables injected into the command process.
|
|
76
|
+
*/
|
|
77
|
+
envs?: Record<string, string>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CommandStatus {
|
|
81
|
+
id?: string;
|
|
82
|
+
content?: string;
|
|
83
|
+
running?: boolean;
|
|
84
|
+
exitCode?: number | null;
|
|
85
|
+
error?: string;
|
|
86
|
+
startedAt?: Date;
|
|
87
|
+
finishedAt?: Date | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CommandLogs {
|
|
91
|
+
content: string;
|
|
92
|
+
cursor?: number;
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
export type CommandExecution = Execution;
|
package/src/models/execution.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface Execution {
|
|
|
54
54
|
result: ExecutionResult[];
|
|
55
55
|
error?: ExecutionError;
|
|
56
56
|
complete?: ExecutionComplete;
|
|
57
|
+
exitCode?: number | null;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface ExecutionHandlers {
|
|
@@ -68,4 +69,4 @@ export interface ExecutionHandlers {
|
|
|
68
69
|
onExecutionComplete?: (c: ExecutionComplete) => void | Promise<void>;
|
|
69
70
|
onError?: (err: ExecutionError) => void | Promise<void>;
|
|
70
71
|
onInit?: (init: ExecutionInit) => void | Promise<void>;
|
|
71
|
-
}
|
|
72
|
+
}
|
package/src/models/sandboxes.ts
CHANGED
|
@@ -62,6 +62,114 @@ export interface NetworkPolicy extends Record<string, unknown> {
|
|
|
62
62
|
egress?: NetworkRule[];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Volume Models
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Host path bind mount backend.
|
|
71
|
+
*
|
|
72
|
+
* Maps a directory on the host filesystem into the container.
|
|
73
|
+
* Only available when the runtime supports host mounts.
|
|
74
|
+
*/
|
|
75
|
+
export interface Host extends Record<string, unknown> {
|
|
76
|
+
/**
|
|
77
|
+
* Absolute path on the host filesystem to mount.
|
|
78
|
+
*/
|
|
79
|
+
path: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Kubernetes PersistentVolumeClaim mount backend.
|
|
84
|
+
*
|
|
85
|
+
* References an existing PVC in the same namespace as the sandbox pod.
|
|
86
|
+
* Only available in Kubernetes runtime.
|
|
87
|
+
*/
|
|
88
|
+
export interface PVC extends Record<string, unknown> {
|
|
89
|
+
/**
|
|
90
|
+
* Name of the PersistentVolumeClaim in the same namespace.
|
|
91
|
+
*/
|
|
92
|
+
claimName: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Alibaba Cloud OSS mount backend via ossfs.
|
|
97
|
+
*
|
|
98
|
+
* The runtime mounts a host-side OSS path under `storage.ossfs_mount_root`
|
|
99
|
+
* so the container sees the bucket contents at the specified mount path.
|
|
100
|
+
*
|
|
101
|
+
* In Docker runtime, OSSFS backend requires OpenSandbox Server to run on a Linux host with FUSE support.
|
|
102
|
+
*/
|
|
103
|
+
export interface OSSFS extends Record<string, unknown> {
|
|
104
|
+
/**
|
|
105
|
+
* OSS bucket name.
|
|
106
|
+
*/
|
|
107
|
+
bucket: string;
|
|
108
|
+
/**
|
|
109
|
+
* OSS endpoint (e.g., "oss-cn-hangzhou.aliyuncs.com").
|
|
110
|
+
*/
|
|
111
|
+
endpoint: string;
|
|
112
|
+
/**
|
|
113
|
+
* ossfs major version used by runtime mount integration.
|
|
114
|
+
* @default "2.0"
|
|
115
|
+
*/
|
|
116
|
+
version?: "1.0" | "2.0";
|
|
117
|
+
/**
|
|
118
|
+
* Additional ossfs mount options.
|
|
119
|
+
*
|
|
120
|
+
* - `1.0`: mounts with `ossfs ... -o <option>`
|
|
121
|
+
* - `2.0`: mounts with `ossfs2 mount ... -c <config-file>` and encodes options as `--<option>` lines in the config file
|
|
122
|
+
*/
|
|
123
|
+
options?: string[];
|
|
124
|
+
/**
|
|
125
|
+
* OSS access key ID for inline credentials mode.
|
|
126
|
+
*/
|
|
127
|
+
accessKeyId: string;
|
|
128
|
+
/**
|
|
129
|
+
* OSS access key secret for inline credentials mode.
|
|
130
|
+
*/
|
|
131
|
+
accessKeySecret: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Storage mount definition for a sandbox.
|
|
136
|
+
*
|
|
137
|
+
* Each volume entry contains:
|
|
138
|
+
* - A unique name identifier
|
|
139
|
+
* - Exactly one backend (host, pvc, ossfs) with backend-specific fields
|
|
140
|
+
* - Common mount settings (mountPath, readOnly, subPath)
|
|
141
|
+
*/
|
|
142
|
+
export interface Volume extends Record<string, unknown> {
|
|
143
|
+
/**
|
|
144
|
+
* Unique identifier for the volume within the sandbox.
|
|
145
|
+
*/
|
|
146
|
+
name: string;
|
|
147
|
+
/**
|
|
148
|
+
* Host path bind mount backend (mutually exclusive with pvc, ossfs).
|
|
149
|
+
*/
|
|
150
|
+
host?: Host;
|
|
151
|
+
/**
|
|
152
|
+
* Kubernetes PVC mount backend (mutually exclusive with host, ossfs).
|
|
153
|
+
*/
|
|
154
|
+
pvc?: PVC;
|
|
155
|
+
/**
|
|
156
|
+
* Alibaba Cloud OSSFS mount backend (mutually exclusive with host, pvc).
|
|
157
|
+
*/
|
|
158
|
+
ossfs?: OSSFS;
|
|
159
|
+
/**
|
|
160
|
+
* Absolute path inside the container where the volume is mounted.
|
|
161
|
+
*/
|
|
162
|
+
mountPath: string;
|
|
163
|
+
/**
|
|
164
|
+
* If true, the volume is mounted as read-only. Defaults to false (read-write).
|
|
165
|
+
*/
|
|
166
|
+
readOnly?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Optional subdirectory under the backend path to mount.
|
|
169
|
+
*/
|
|
170
|
+
subPath?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
65
173
|
export type SandboxState =
|
|
66
174
|
| "Creating"
|
|
67
175
|
| "Running"
|
|
@@ -92,7 +200,7 @@ export interface SandboxInfo extends Record<string, unknown> {
|
|
|
92
200
|
/**
|
|
93
201
|
* Sandbox expiration time (server-side TTL).
|
|
94
202
|
*/
|
|
95
|
-
expiresAt: Date;
|
|
203
|
+
expiresAt: Date | null;
|
|
96
204
|
}
|
|
97
205
|
|
|
98
206
|
export interface CreateSandboxRequest extends Record<string, unknown> {
|
|
@@ -101,7 +209,7 @@ export interface CreateSandboxRequest extends Record<string, unknown> {
|
|
|
101
209
|
/**
|
|
102
210
|
* Timeout in seconds (server semantics).
|
|
103
211
|
*/
|
|
104
|
-
timeout
|
|
212
|
+
timeout?: number | null;
|
|
105
213
|
resourceLimits: ResourceLimits;
|
|
106
214
|
env?: Record<string, string>;
|
|
107
215
|
metadata?: Record<string, string>;
|
|
@@ -109,6 +217,10 @@ export interface CreateSandboxRequest extends Record<string, unknown> {
|
|
|
109
217
|
* Optional outbound network policy for the sandbox.
|
|
110
218
|
*/
|
|
111
219
|
networkPolicy?: NetworkPolicy;
|
|
220
|
+
/**
|
|
221
|
+
* Optional list of volume mounts for persistent storage.
|
|
222
|
+
*/
|
|
223
|
+
volumes?: Volume[];
|
|
112
224
|
extensions?: Record<string, unknown>;
|
|
113
225
|
}
|
|
114
226
|
|
|
@@ -119,7 +231,7 @@ export interface CreateSandboxResponse extends Record<string, unknown> {
|
|
|
119
231
|
/**
|
|
120
232
|
* Sandbox expiration time after creation.
|
|
121
233
|
*/
|
|
122
|
-
expiresAt: Date;
|
|
234
|
+
expiresAt: Date | null;
|
|
123
235
|
/**
|
|
124
236
|
* Sandbox creation time.
|
|
125
237
|
*/
|
|
@@ -153,6 +265,11 @@ export interface RenewSandboxExpirationResponse extends Record<string, unknown>
|
|
|
153
265
|
|
|
154
266
|
export interface Endpoint extends Record<string, unknown> {
|
|
155
267
|
endpoint: string;
|
|
268
|
+
/**
|
|
269
|
+
* Headers that must be included on every request targeting this endpoint
|
|
270
|
+
* (e.g. when the server requires them for routing or auth). Omit or empty if not required.
|
|
271
|
+
*/
|
|
272
|
+
headers?: Record<string, string>;
|
|
156
273
|
}
|
|
157
274
|
|
|
158
275
|
export interface ListSandboxesParams {
|
|
@@ -168,4 +285,4 @@ export interface ListSandboxesParams {
|
|
|
168
285
|
metadata?: Record<string, string>;
|
|
169
286
|
page?: number;
|
|
170
287
|
pageSize?: number;
|
|
171
|
-
};
|
|
288
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright 2026 Alibaba Group Holding Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import createClient from "openapi-fetch";
|
|
16
|
+
import type { Client } from "openapi-fetch";
|
|
17
|
+
|
|
18
|
+
import type { paths as EgressPaths } from "../api/egress.js";
|
|
19
|
+
|
|
20
|
+
export type EgressClient = Client<EgressPaths>;
|
|
21
|
+
|
|
22
|
+
export interface CreateEgressClientOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Base URL to the sandbox egress sidecar API.
|
|
25
|
+
*/
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/**
|
|
28
|
+
* Extra headers applied to every request.
|
|
29
|
+
*/
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* Custom fetch implementation.
|
|
33
|
+
*/
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createEgressClient(opts: CreateEgressClientOptions): EgressClient {
|
|
38
|
+
const createClientFn =
|
|
39
|
+
(createClient as unknown as { default?: typeof createClient }).default ?? createClient;
|
|
40
|
+
return createClientFn<EgressPaths>({
|
|
41
|
+
baseUrl: opts.baseUrl,
|
|
42
|
+
headers: opts.headers,
|
|
43
|
+
fetch: opts.fetch,
|
|
44
|
+
});
|
|
45
|
+
}
|
package/src/sandbox.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
DEFAULT_ENTRYPOINT,
|
|
17
|
+
DEFAULT_EGRESS_PORT,
|
|
17
18
|
DEFAULT_EXECD_PORT,
|
|
18
19
|
DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS,
|
|
19
20
|
DEFAULT_READY_TIMEOUT_SECONDS,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
} from "./core/constants.js";
|
|
23
24
|
import { ConnectionConfig, type ConnectionConfigOptions } from "./config/connection.js";
|
|
24
25
|
import type { SandboxFiles } from "./services/filesystem.js";
|
|
26
|
+
import type { Egress } from "./services/egress.js";
|
|
25
27
|
import { createDefaultAdapterFactory } from "./factory/defaultAdapterFactory.js";
|
|
26
28
|
import type { AdapterFactory } from "./factory/adapterFactory.js";
|
|
27
29
|
|
|
@@ -33,9 +35,11 @@ import type {
|
|
|
33
35
|
CreateSandboxRequest,
|
|
34
36
|
Endpoint,
|
|
35
37
|
NetworkPolicy,
|
|
38
|
+
NetworkRule,
|
|
36
39
|
RenewSandboxExpirationResponse,
|
|
37
40
|
SandboxId,
|
|
38
41
|
SandboxInfo,
|
|
42
|
+
Volume,
|
|
39
43
|
} from "./models/sandboxes.js";
|
|
40
44
|
import { SandboxReadyTimeoutException } from "./core/exceptions.js";
|
|
41
45
|
|
|
@@ -73,6 +77,11 @@ export interface SandboxCreateOptions {
|
|
|
73
77
|
* If provided without defaultAction, defaults to "deny".
|
|
74
78
|
*/
|
|
75
79
|
networkPolicy?: NetworkPolicy;
|
|
80
|
+
/**
|
|
81
|
+
* Optional list of volume mounts for persistent storage.
|
|
82
|
+
* Each volume specifies a backend (host path or PVC) and mount configuration.
|
|
83
|
+
*/
|
|
84
|
+
volumes?: Volume[];
|
|
76
85
|
/**
|
|
77
86
|
* Opaque extension parameters passed through to the server as-is.
|
|
78
87
|
*/
|
|
@@ -85,9 +94,9 @@ export interface SandboxCreateOptions {
|
|
|
85
94
|
*/
|
|
86
95
|
resource?: Record<string, string>;
|
|
87
96
|
/**
|
|
88
|
-
* Sandbox timeout in seconds.
|
|
97
|
+
* Sandbox timeout in seconds. Set to `null` to require explicit cleanup.
|
|
89
98
|
*/
|
|
90
|
-
timeoutSeconds?: number;
|
|
99
|
+
timeoutSeconds?: number | null;
|
|
91
100
|
|
|
92
101
|
/**
|
|
93
102
|
* Skip readiness checks during create/connect.
|
|
@@ -181,6 +190,7 @@ export class Sandbox {
|
|
|
181
190
|
adapterFactory: AdapterFactory;
|
|
182
191
|
lifecycleBaseUrl: string;
|
|
183
192
|
execdBaseUrl: string;
|
|
193
|
+
egress: Egress;
|
|
184
194
|
}
|
|
185
195
|
>();
|
|
186
196
|
|
|
@@ -195,6 +205,7 @@ export class Sandbox {
|
|
|
195
205
|
files: SandboxFiles;
|
|
196
206
|
health: ExecdHealth;
|
|
197
207
|
metrics: ExecdMetrics;
|
|
208
|
+
egress: Egress;
|
|
198
209
|
}) {
|
|
199
210
|
this.id = opts.id;
|
|
200
211
|
this.connectionConfig = opts.connectionConfig;
|
|
@@ -202,6 +213,7 @@ export class Sandbox {
|
|
|
202
213
|
adapterFactory: opts.adapterFactory,
|
|
203
214
|
lifecycleBaseUrl: opts.lifecycleBaseUrl,
|
|
204
215
|
execdBaseUrl: opts.execdBaseUrl,
|
|
216
|
+
egress: opts.egress,
|
|
205
217
|
});
|
|
206
218
|
|
|
207
219
|
this.sandboxes = opts.sandboxes;
|
|
@@ -231,10 +243,37 @@ export class Sandbox {
|
|
|
231
243
|
throw err;
|
|
232
244
|
}
|
|
233
245
|
|
|
246
|
+
// Validate volumes: exactly one backend must be specified per volume
|
|
247
|
+
if (opts.volumes) {
|
|
248
|
+
for (const vol of opts.volumes) {
|
|
249
|
+
const backendsSpecified = [vol.host, vol.pvc, vol.ossfs].filter((b) => b != null).length;
|
|
250
|
+
if (backendsSpecified === 0) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but none was provided.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (backendsSpecified > 1) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but multiple were provided.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const rawTimeout = opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
264
|
+
const timeoutSeconds =
|
|
265
|
+
opts.timeoutSeconds === null
|
|
266
|
+
? null
|
|
267
|
+
: Math.floor(rawTimeout);
|
|
268
|
+
if (timeoutSeconds !== null && !Number.isFinite(timeoutSeconds)) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`timeoutSeconds must be a finite number, got ${opts.timeoutSeconds}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
234
274
|
const req: CreateSandboxRequest = {
|
|
235
275
|
image: toImageSpec(opts.image),
|
|
236
276
|
entrypoint: opts.entrypoint ?? DEFAULT_ENTRYPOINT,
|
|
237
|
-
timeout: Math.floor(opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS),
|
|
238
277
|
resourceLimits: opts.resource ?? DEFAULT_RESOURCE_LIMITS,
|
|
239
278
|
env: opts.env ?? {},
|
|
240
279
|
metadata: opts.metadata ?? {},
|
|
@@ -244,8 +283,12 @@ export class Sandbox {
|
|
|
244
283
|
defaultAction: opts.networkPolicy.defaultAction ?? "deny",
|
|
245
284
|
}
|
|
246
285
|
: undefined,
|
|
286
|
+
volumes: opts.volumes,
|
|
247
287
|
extensions: opts.extensions ?? {},
|
|
248
288
|
};
|
|
289
|
+
if (timeoutSeconds !== null) {
|
|
290
|
+
req.timeout = timeoutSeconds;
|
|
291
|
+
}
|
|
249
292
|
|
|
250
293
|
let sandboxId: SandboxId | undefined;
|
|
251
294
|
try {
|
|
@@ -254,15 +297,28 @@ export class Sandbox {
|
|
|
254
297
|
|
|
255
298
|
const endpoint = await sandboxes.getSandboxEndpoint(
|
|
256
299
|
sandboxId,
|
|
257
|
-
DEFAULT_EXECD_PORT
|
|
300
|
+
DEFAULT_EXECD_PORT,
|
|
301
|
+
connectionConfig.useServerProxy
|
|
302
|
+
);
|
|
303
|
+
const egressEndpoint = await sandboxes.getSandboxEndpoint(
|
|
304
|
+
sandboxId,
|
|
305
|
+
DEFAULT_EGRESS_PORT,
|
|
306
|
+
connectionConfig.useServerProxy
|
|
258
307
|
);
|
|
259
308
|
const execdBaseUrl = `${connectionConfig.protocol}://${endpoint.endpoint}`;
|
|
309
|
+
const egressBaseUrl = `${connectionConfig.protocol}://${egressEndpoint.endpoint}`;
|
|
260
310
|
|
|
261
311
|
const { commands, files, health, metrics } =
|
|
262
312
|
adapterFactory.createExecdStack({
|
|
263
313
|
connectionConfig,
|
|
264
314
|
execdBaseUrl,
|
|
315
|
+
endpointHeaders: endpoint.headers,
|
|
265
316
|
});
|
|
317
|
+
const { egress } = adapterFactory.createEgressStack({
|
|
318
|
+
connectionConfig,
|
|
319
|
+
egressBaseUrl,
|
|
320
|
+
endpointHeaders: egressEndpoint.headers,
|
|
321
|
+
});
|
|
266
322
|
|
|
267
323
|
const sbx = new Sandbox({
|
|
268
324
|
id: sandboxId,
|
|
@@ -275,6 +331,7 @@ export class Sandbox {
|
|
|
275
331
|
files,
|
|
276
332
|
health,
|
|
277
333
|
metrics,
|
|
334
|
+
egress,
|
|
278
335
|
});
|
|
279
336
|
|
|
280
337
|
if (!(opts.skipHealthCheck ?? false)) {
|
|
@@ -325,14 +382,27 @@ export class Sandbox {
|
|
|
325
382
|
try {
|
|
326
383
|
const endpoint = await sandboxes.getSandboxEndpoint(
|
|
327
384
|
opts.sandboxId,
|
|
328
|
-
DEFAULT_EXECD_PORT
|
|
385
|
+
DEFAULT_EXECD_PORT,
|
|
386
|
+
connectionConfig.useServerProxy
|
|
387
|
+
);
|
|
388
|
+
const egressEndpoint = await sandboxes.getSandboxEndpoint(
|
|
389
|
+
opts.sandboxId,
|
|
390
|
+
DEFAULT_EGRESS_PORT,
|
|
391
|
+
connectionConfig.useServerProxy
|
|
329
392
|
);
|
|
330
393
|
const execdBaseUrl = `${connectionConfig.protocol}://${endpoint.endpoint}`;
|
|
394
|
+
const egressBaseUrl = `${connectionConfig.protocol}://${egressEndpoint.endpoint}`;
|
|
331
395
|
const { commands, files, health, metrics } =
|
|
332
396
|
adapterFactory.createExecdStack({
|
|
333
397
|
connectionConfig,
|
|
334
398
|
execdBaseUrl,
|
|
399
|
+
endpointHeaders: endpoint.headers,
|
|
335
400
|
});
|
|
401
|
+
const { egress } = adapterFactory.createEgressStack({
|
|
402
|
+
connectionConfig,
|
|
403
|
+
egressBaseUrl,
|
|
404
|
+
endpointHeaders: egressEndpoint.headers,
|
|
405
|
+
});
|
|
336
406
|
|
|
337
407
|
const sbx = new Sandbox({
|
|
338
408
|
id: opts.sandboxId,
|
|
@@ -345,6 +415,7 @@ export class Sandbox {
|
|
|
345
415
|
files,
|
|
346
416
|
health,
|
|
347
417
|
metrics,
|
|
418
|
+
egress,
|
|
348
419
|
});
|
|
349
420
|
|
|
350
421
|
if (!(opts.skipHealthCheck ?? false)) {
|
|
@@ -458,11 +529,23 @@ export class Sandbox {
|
|
|
458
529
|
return await this.sandboxes.renewSandboxExpiration(this.id, { expiresAt });
|
|
459
530
|
}
|
|
460
531
|
|
|
532
|
+
async getEgressPolicy(): Promise<NetworkPolicy> {
|
|
533
|
+
return await Sandbox._priv.get(this)!.egress.getPolicy();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async patchEgressRules(rules: NetworkRule[]): Promise<void> {
|
|
537
|
+
await Sandbox._priv.get(this)!.egress.patchRules(rules);
|
|
538
|
+
}
|
|
539
|
+
|
|
461
540
|
/**
|
|
462
541
|
* Get sandbox endpoint for a port (STRICT: no scheme), e.g. "localhost:44772" or "domain/route/.../44772".
|
|
463
542
|
*/
|
|
464
543
|
async getEndpoint(port: number): Promise<Endpoint> {
|
|
465
|
-
return await this.sandboxes.getSandboxEndpoint(
|
|
544
|
+
return await this.sandboxes.getSandboxEndpoint(
|
|
545
|
+
this.id,
|
|
546
|
+
port,
|
|
547
|
+
this.connectionConfig.useServerProxy
|
|
548
|
+
);
|
|
466
549
|
}
|
|
467
550
|
|
|
468
551
|
/**
|
|
@@ -479,26 +562,45 @@ export class Sandbox {
|
|
|
479
562
|
healthCheck?: (sbx: Sandbox) => boolean | Promise<boolean>;
|
|
480
563
|
}): Promise<void> {
|
|
481
564
|
const deadline = Date.now() + opts.readyTimeoutSeconds * 1000;
|
|
565
|
+
let attempt = 0;
|
|
566
|
+
let errorDetail = "Health check returned false continuously.";
|
|
567
|
+
|
|
568
|
+
const buildTimeoutMessage = () => {
|
|
569
|
+
const context = `domain=${this.connectionConfig.domain}, useServerProxy=${this.connectionConfig.useServerProxy}`;
|
|
570
|
+
let suggestion =
|
|
571
|
+
"If this sandbox runs in Docker bridge or remote-network mode, consider enabling useServerProxy=true.";
|
|
572
|
+
if (!this.connectionConfig.useServerProxy) {
|
|
573
|
+
suggestion += " You can also configure server-side [docker].host_ip for direct endpoint access.";
|
|
574
|
+
}
|
|
575
|
+
return `Sandbox health check timed out after ${opts.readyTimeoutSeconds}s (${attempt} attempts). ${errorDetail} Connection context: ${context}. ${suggestion}`;
|
|
576
|
+
};
|
|
482
577
|
|
|
483
578
|
// Wait until execd becomes reachable and passes health check.
|
|
484
579
|
while (true) {
|
|
485
580
|
if (Date.now() > deadline) {
|
|
486
581
|
throw new SandboxReadyTimeoutException({
|
|
487
|
-
message:
|
|
582
|
+
message: buildTimeoutMessage(),
|
|
488
583
|
});
|
|
489
584
|
}
|
|
585
|
+
attempt++;
|
|
490
586
|
try {
|
|
491
587
|
if (opts.healthCheck) {
|
|
492
588
|
const ok = await opts.healthCheck(this);
|
|
493
|
-
if (ok)
|
|
589
|
+
if (ok) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
494
592
|
} else {
|
|
495
593
|
const ok = await this.health.ping();
|
|
496
|
-
if (ok)
|
|
594
|
+
if (ok) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
497
597
|
}
|
|
498
|
-
|
|
499
|
-
|
|
598
|
+
errorDetail = "Health check returned false continuously.";
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
601
|
+
errorDetail = `Last health check error: ${message}`;
|
|
500
602
|
}
|
|
501
603
|
await sleep(opts.pollingIntervalMillis);
|
|
502
604
|
}
|
|
503
605
|
}
|
|
504
|
-
}
|
|
606
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright 2026 Alibaba Group Holding Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import type { NetworkPolicy, NetworkRule } from "../models/sandboxes.js";
|
|
16
|
+
|
|
17
|
+
export interface Egress {
|
|
18
|
+
getPolicy(): Promise<NetworkPolicy>;
|
|
19
|
+
/**
|
|
20
|
+
* Patch egress rules with sidecar merge semantics.
|
|
21
|
+
*
|
|
22
|
+
* Incoming rules take priority over existing rules with the same target.
|
|
23
|
+
* Existing rules for other targets remain unchanged. Within one patch payload,
|
|
24
|
+
* the first rule for a target wins. The current defaultAction is preserved.
|
|
25
|
+
*/
|
|
26
|
+
patchRules(rules: NetworkRule[]): Promise<void>;
|
|
27
|
+
}
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
15
|
import type { ExecutionHandlers } from "../models/execution.js";
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
CommandExecution,
|
|
18
|
+
CommandLogs,
|
|
19
|
+
CommandStatus,
|
|
20
|
+
RunCommandOpts,
|
|
21
|
+
ServerStreamEvent,
|
|
22
|
+
} from "../models/execd.js";
|
|
17
23
|
|
|
18
24
|
export interface ExecdCommands {
|
|
19
25
|
/**
|
|
@@ -32,4 +38,37 @@ export interface ExecdCommands {
|
|
|
32
38
|
* Note: Execd spec uses `DELETE /command?id=<sessionId>`.
|
|
33
39
|
*/
|
|
34
40
|
interrupt(sessionId: string): Promise<void>;
|
|
35
|
-
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the current running status for a command id.
|
|
44
|
+
*/
|
|
45
|
+
getCommandStatus(commandId: string): Promise<CommandStatus>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get background command logs (non-streamed).
|
|
49
|
+
*/
|
|
50
|
+
getBackgroundCommandLogs(commandId: string, cursor?: number): Promise<CommandLogs>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a bash session with optional working directory.
|
|
54
|
+
* Returns session ID for use with runInSession and deleteSession.
|
|
55
|
+
*/
|
|
56
|
+
createSession(options?: { workingDirectory?: string }): Promise<string>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run a shell command in an existing bash session (SSE stream, same event shape as run).
|
|
60
|
+
* Optional workingDirectory and timeout apply to this run only; session state (e.g. env) persists.
|
|
61
|
+
*/
|
|
62
|
+
runInSession(
|
|
63
|
+
sessionId: string,
|
|
64
|
+
command: string,
|
|
65
|
+
options?: { workingDirectory?: string; timeout?: number },
|
|
66
|
+
handlers?: ExecutionHandlers,
|
|
67
|
+
signal?: AbortSignal,
|
|
68
|
+
): Promise<CommandExecution>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Delete a bash session by ID. Frees resources; session ID must have been returned by createSession.
|
|
72
|
+
*/
|
|
73
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
74
|
+
}
|
|
@@ -38,5 +38,9 @@ export interface Sandboxes {
|
|
|
38
38
|
req: RenewSandboxExpirationRequest,
|
|
39
39
|
): Promise<RenewSandboxExpirationResponse>;
|
|
40
40
|
|
|
41
|
-
getSandboxEndpoint(
|
|
42
|
-
|
|
41
|
+
getSandboxEndpoint(
|
|
42
|
+
sandboxId: SandboxId,
|
|
43
|
+
port: number,
|
|
44
|
+
useServerProxy?: boolean
|
|
45
|
+
): Promise<Endpoint>;
|
|
46
|
+
}
|