@alibaba-group/opensandbox 0.1.0-dev4 → 0.1.1-dev0
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/dist/adapters/commandsAdapter.js +1 -0
- package/dist/adapters/commandsAdapter.js.map +1 -0
- package/dist/adapters/filesystemAdapter.js +1 -0
- package/dist/adapters/filesystemAdapter.js.map +1 -0
- package/dist/adapters/healthAdapter.js +1 -0
- package/dist/adapters/healthAdapter.js.map +1 -0
- package/dist/adapters/metricsAdapter.js +1 -0
- package/dist/adapters/metricsAdapter.js.map +1 -0
- package/dist/adapters/openapiError.js +1 -0
- package/dist/adapters/openapiError.js.map +1 -0
- package/dist/adapters/sandboxesAdapter.js +1 -0
- package/dist/adapters/sandboxesAdapter.js.map +1 -0
- package/dist/adapters/sse.js +1 -0
- package/dist/adapters/sse.js.map +1 -0
- package/dist/api/execd.js +1 -0
- package/dist/api/execd.js.map +1 -0
- package/dist/api/lifecycle.js +1 -0
- package/dist/api/lifecycle.js.map +1 -0
- package/dist/config/connection.d.ts.map +1 -1
- package/dist/config/connection.js +5 -2
- package/dist/config/connection.js.map +1 -0
- package/dist/core/constants.js +1 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/exceptions.js +1 -0
- package/dist/core/exceptions.js.map +1 -0
- package/dist/factory/adapterFactory.js +1 -0
- package/dist/factory/adapterFactory.js.map +1 -0
- package/dist/factory/defaultAdapterFactory.js +1 -0
- package/dist/factory/defaultAdapterFactory.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -0
- package/dist/manager.js +1 -0
- package/dist/manager.js.map +1 -0
- package/dist/models/execd.js +1 -0
- package/dist/models/execd.js.map +1 -0
- package/dist/models/execution.js +1 -0
- package/dist/models/execution.js.map +1 -0
- package/dist/models/executionEventDispatcher.js +1 -0
- package/dist/models/executionEventDispatcher.js.map +1 -0
- package/dist/models/filesystem.js +1 -0
- package/dist/models/filesystem.js.map +1 -0
- package/dist/models/sandboxes.js +1 -0
- package/dist/models/sandboxes.js.map +1 -0
- package/dist/openapi/execdClient.js +1 -0
- package/dist/openapi/execdClient.js.map +1 -0
- package/dist/openapi/lifecycleClient.js +1 -0
- package/dist/openapi/lifecycleClient.js.map +1 -0
- package/dist/sandbox.js +1 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/services/execdCommands.js +1 -0
- package/dist/services/execdCommands.js.map +1 -0
- package/dist/services/execdHealth.js +1 -0
- package/dist/services/execdHealth.js.map +1 -0
- package/dist/services/execdMetrics.js +1 -0
- package/dist/services/execdMetrics.js.map +1 -0
- package/dist/services/filesystem.js +1 -0
- package/dist/services/filesystem.js.map +1 -0
- package/dist/services/sandboxes.js +1 -0
- package/dist/services/sandboxes.js.map +1 -0
- package/package.json +3 -2
- package/src/adapters/commandsAdapter.ts +112 -0
- package/src/adapters/filesystemAdapter.ts +575 -0
- package/src/adapters/healthAdapter.ts +27 -0
- package/src/adapters/metricsAdapter.ts +51 -0
- package/src/adapters/openapiError.ts +42 -0
- package/src/adapters/sandboxesAdapter.ts +187 -0
- package/src/adapters/sse.ts +95 -0
- package/src/api/execd.ts +1569 -0
- package/src/api/lifecycle.ts +801 -0
- package/src/config/connection.ts +377 -0
- package/src/core/constants.ts +29 -0
- package/src/core/exceptions.ts +134 -0
- package/src/factory/adapterFactory.ts +51 -0
- package/src/factory/defaultAdapterFactory.ts +69 -0
- package/src/index.ts +108 -0
- package/src/internal.ts +39 -0
- package/src/manager.ts +111 -0
- package/src/models/execd.ts +90 -0
- package/src/models/execution.ts +71 -0
- package/src/models/executionEventDispatcher.ts +97 -0
- package/src/models/filesystem.ts +103 -0
- package/src/models/sandboxes.ts +142 -0
- package/src/openapi/execdClient.ts +49 -0
- package/src/openapi/lifecycleClient.ts +70 -0
- package/src/sandbox.ts +459 -0
- package/src/services/execdCommands.ts +35 -0
- package/src/services/execdHealth.ts +17 -0
- package/src/services/execdMetrics.ts +19 -0
- package/src/services/filesystem.ts +47 -0
- package/src/services/sandboxes.ts +42 -0
|
@@ -0,0 +1,575 @@
|
|
|
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 { ExecdClient } from "../openapi/execdClient.js";
|
|
16
|
+
import { throwOnOpenApiFetchError } from "./openapiError.js";
|
|
17
|
+
import type { SandboxFiles } from "../services/filesystem.js";
|
|
18
|
+
import type { paths as ExecdPaths } from "../api/execd.js";
|
|
19
|
+
import type {
|
|
20
|
+
ContentReplaceEntry,
|
|
21
|
+
FileInfo,
|
|
22
|
+
FileMetadata,
|
|
23
|
+
FilesInfoResponse,
|
|
24
|
+
MoveEntry,
|
|
25
|
+
Permission,
|
|
26
|
+
RenameFileItem,
|
|
27
|
+
ReplaceFileContentItem,
|
|
28
|
+
SearchEntry,
|
|
29
|
+
SearchFilesResponse,
|
|
30
|
+
SetPermissionEntry,
|
|
31
|
+
WriteEntry,
|
|
32
|
+
} from "../models/filesystem.js";
|
|
33
|
+
import { SandboxApiException, SandboxError } from "../core/exceptions.js";
|
|
34
|
+
|
|
35
|
+
function joinUrl(baseUrl: string, pathname: string): string {
|
|
36
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
37
|
+
const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
38
|
+
return `${base}${path}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toUploadBlob(data: Blob | Uint8Array | ArrayBuffer | string): Blob {
|
|
42
|
+
if (typeof data === "string") return new Blob([data]);
|
|
43
|
+
if (data instanceof Blob) return data;
|
|
44
|
+
if (data instanceof ArrayBuffer) return new Blob([data]);
|
|
45
|
+
// Copy into a new Uint8Array backed by ArrayBuffer (not SharedArrayBuffer)
|
|
46
|
+
const copied = Uint8Array.from(data);
|
|
47
|
+
return new Blob([copied.buffer]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isReadableStream(v: unknown): v is ReadableStream<Uint8Array> {
|
|
51
|
+
return !!v && typeof (v as any).getReader === "function";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isAsyncIterable(v: unknown): v is AsyncIterable<Uint8Array> {
|
|
55
|
+
return !!v && typeof (v as any)[Symbol.asyncIterator] === "function";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isNodeRuntime(): boolean {
|
|
59
|
+
const p = (globalThis as any)?.process;
|
|
60
|
+
return !!(p?.versions?.node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function collectBytes(
|
|
64
|
+
source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>
|
|
65
|
+
): Promise<Uint8Array> {
|
|
66
|
+
const chunks: Uint8Array[] = [];
|
|
67
|
+
let total = 0;
|
|
68
|
+
|
|
69
|
+
if (isReadableStream(source)) {
|
|
70
|
+
const reader = source.getReader();
|
|
71
|
+
try {
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) break;
|
|
75
|
+
if (value) {
|
|
76
|
+
chunks.push(value);
|
|
77
|
+
total += value.length;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
reader.releaseLock();
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
for await (const chunk of source) {
|
|
85
|
+
chunks.push(chunk);
|
|
86
|
+
total += chunk.length;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const out = new Uint8Array(total);
|
|
91
|
+
let offset = 0;
|
|
92
|
+
for (const chunk of chunks) {
|
|
93
|
+
out.set(chunk, offset);
|
|
94
|
+
offset += chunk.length;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toReadableStream(
|
|
100
|
+
it: AsyncIterable<Uint8Array>
|
|
101
|
+
): ReadableStream<Uint8Array> {
|
|
102
|
+
const RS: any = ReadableStream as any;
|
|
103
|
+
if (typeof RS?.from === "function") return RS.from(it);
|
|
104
|
+
const iterator = it[Symbol.asyncIterator]();
|
|
105
|
+
return new ReadableStream<Uint8Array>({
|
|
106
|
+
async pull(controller) {
|
|
107
|
+
const r = await iterator.next();
|
|
108
|
+
if (r.done) {
|
|
109
|
+
controller.close();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
controller.enqueue(r.value);
|
|
113
|
+
},
|
|
114
|
+
async cancel() {
|
|
115
|
+
await iterator.return?.();
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function basename(p: string): string {
|
|
121
|
+
const parts = p.split("/").filter(Boolean);
|
|
122
|
+
return parts.length ? parts[parts.length - 1] : "file";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function encodeUtf8(s: string): Uint8Array {
|
|
126
|
+
return new TextEncoder().encode(s);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function* multipartUploadBody(opts: {
|
|
130
|
+
boundary: string;
|
|
131
|
+
metadataJson: string;
|
|
132
|
+
fileName: string;
|
|
133
|
+
fileContentType: string;
|
|
134
|
+
file: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
|
|
135
|
+
}): AsyncIterable<Uint8Array> {
|
|
136
|
+
const b = opts.boundary;
|
|
137
|
+
|
|
138
|
+
// Part 1: metadata (application/json)
|
|
139
|
+
yield encodeUtf8(`--${b}\r\n`);
|
|
140
|
+
yield encodeUtf8(
|
|
141
|
+
`Content-Disposition: form-data; name="metadata"; filename="metadata"\r\n`
|
|
142
|
+
);
|
|
143
|
+
yield encodeUtf8(`Content-Type: application/json\r\n\r\n`);
|
|
144
|
+
yield encodeUtf8(opts.metadataJson);
|
|
145
|
+
yield encodeUtf8(`\r\n`);
|
|
146
|
+
|
|
147
|
+
// Part 2: file
|
|
148
|
+
yield encodeUtf8(`--${b}\r\n`);
|
|
149
|
+
yield encodeUtf8(
|
|
150
|
+
`Content-Disposition: form-data; name="file"; filename="${opts.fileName}"\r\n`
|
|
151
|
+
);
|
|
152
|
+
yield encodeUtf8(`Content-Type: ${opts.fileContentType}\r\n\r\n`);
|
|
153
|
+
|
|
154
|
+
if (isReadableStream(opts.file)) {
|
|
155
|
+
const reader = opts.file.getReader();
|
|
156
|
+
try {
|
|
157
|
+
while (true) {
|
|
158
|
+
const { done, value } = await reader.read();
|
|
159
|
+
if (done) break;
|
|
160
|
+
if (value) yield value;
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
reader.releaseLock();
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
for await (const chunk of opts.file) {
|
|
167
|
+
yield chunk;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
yield encodeUtf8(`\r\n--${b}--\r\n`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface FilesystemAdapterOptions {
|
|
175
|
+
/**
|
|
176
|
+
* Must match the baseUrl used by the ExecdClient, used for binary endpoints
|
|
177
|
+
* like download/upload where we bypass JSON parsing.
|
|
178
|
+
*/
|
|
179
|
+
baseUrl: string;
|
|
180
|
+
fetch?: typeof fetch;
|
|
181
|
+
headers?: Record<string, string>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toPermission(e: {
|
|
185
|
+
mode?: number;
|
|
186
|
+
owner?: string;
|
|
187
|
+
group?: string;
|
|
188
|
+
}): Permission {
|
|
189
|
+
return {
|
|
190
|
+
mode: e.mode ?? 755,
|
|
191
|
+
owner: e.owner,
|
|
192
|
+
group: e.group,
|
|
193
|
+
} as Permission;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Filesystem adapter that exposes user-facing file APIs (`sandbox.files`).
|
|
198
|
+
*
|
|
199
|
+
* This adapter owns all request/response conversions:
|
|
200
|
+
* - Maps friendly method shapes to API payloads
|
|
201
|
+
* - Parses timestamps into `Date`
|
|
202
|
+
* - Implements streaming upload/download helpers
|
|
203
|
+
*/
|
|
204
|
+
export class FilesystemAdapter implements SandboxFiles {
|
|
205
|
+
private readonly fetch: typeof fetch;
|
|
206
|
+
|
|
207
|
+
private static readonly Api = {
|
|
208
|
+
// This is intentionally derived from OpenAPI schema types so API changes surface quickly.
|
|
209
|
+
SearchFilesOk:
|
|
210
|
+
null as unknown as ExecdPaths["/files/search"]["get"]["responses"][200]["content"]["application/json"],
|
|
211
|
+
FilesInfoOk:
|
|
212
|
+
null as unknown as ExecdPaths["/files/info"]["get"]["responses"][200]["content"]["application/json"],
|
|
213
|
+
MakeDirsRequest:
|
|
214
|
+
null as unknown as ExecdPaths["/directories"]["post"]["requestBody"]["content"]["application/json"],
|
|
215
|
+
SetPermissionsRequest:
|
|
216
|
+
null as unknown as ExecdPaths["/files/permissions"]["post"]["requestBody"]["content"]["application/json"],
|
|
217
|
+
MoveFilesRequest:
|
|
218
|
+
null as unknown as ExecdPaths["/files/mv"]["post"]["requestBody"]["content"]["application/json"],
|
|
219
|
+
ReplaceContentsRequest:
|
|
220
|
+
null as unknown as ExecdPaths["/files/replace"]["post"]["requestBody"]["content"]["application/json"],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
constructor(
|
|
224
|
+
private readonly client: ExecdClient,
|
|
225
|
+
private readonly opts: FilesystemAdapterOptions
|
|
226
|
+
) {
|
|
227
|
+
this.fetch = opts.fetch ?? fetch;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private parseIsoDate(field: string, v: unknown): Date {
|
|
231
|
+
if (typeof v !== "string" || !v) {
|
|
232
|
+
throw new Error(`Invalid ${field}: expected ISO string, got ${typeof v}`);
|
|
233
|
+
}
|
|
234
|
+
const d = new Date(v);
|
|
235
|
+
if (Number.isNaN(d.getTime())) {
|
|
236
|
+
throw new Error(`Invalid ${field}: ${v}`);
|
|
237
|
+
}
|
|
238
|
+
return d;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static readonly _ApiFileInfo =
|
|
242
|
+
null as unknown as (typeof FilesystemAdapter.Api.SearchFilesOk)[number];
|
|
243
|
+
|
|
244
|
+
private mapApiFileInfo(raw: typeof FilesystemAdapter._ApiFileInfo): FileInfo {
|
|
245
|
+
const { path, size, created_at, modified_at, mode, owner, group, ...rest } =
|
|
246
|
+
raw;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...rest,
|
|
250
|
+
path,
|
|
251
|
+
size,
|
|
252
|
+
mode,
|
|
253
|
+
owner,
|
|
254
|
+
group,
|
|
255
|
+
createdAt: created_at
|
|
256
|
+
? this.parseIsoDate("createdAt", created_at)
|
|
257
|
+
: undefined,
|
|
258
|
+
modifiedAt: modified_at
|
|
259
|
+
? this.parseIsoDate("modifiedAt", modified_at)
|
|
260
|
+
: undefined,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async getFileInfo(paths: string[]): Promise<Record<string, FileInfo>> {
|
|
265
|
+
const { data, error, response } = await this.client.GET("/files/info", {
|
|
266
|
+
params: { query: { path: paths } },
|
|
267
|
+
});
|
|
268
|
+
throwOnOpenApiFetchError({ error, response }, "Get file info failed");
|
|
269
|
+
const raw = data as typeof FilesystemAdapter.Api.FilesInfoOk | undefined;
|
|
270
|
+
if (!raw) return {} as FilesInfoResponse;
|
|
271
|
+
if (typeof raw !== "object") {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Get file info failed: unexpected response shape (got ${typeof raw})`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const out: Record<string, FileInfo> = {};
|
|
277
|
+
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
278
|
+
if (!v || typeof v !== "object") {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Get file info failed: invalid file info for path=${k}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
out[k] = this.mapApiFileInfo(v as typeof FilesystemAdapter._ApiFileInfo);
|
|
284
|
+
}
|
|
285
|
+
return out as FilesInfoResponse;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async deleteFiles(paths: string[]): Promise<void> {
|
|
289
|
+
const { error, response } = await this.client.DELETE("/files", {
|
|
290
|
+
params: { query: { path: paths } },
|
|
291
|
+
});
|
|
292
|
+
throwOnOpenApiFetchError({ error, response }, "Delete files failed");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async createDirectories(
|
|
296
|
+
entries: Pick<WriteEntry, "path" | "mode" | "owner" | "group">[]
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
const map: Record<string, Permission> = {};
|
|
299
|
+
for (const e of entries) {
|
|
300
|
+
map[e.path] = toPermission(e);
|
|
301
|
+
}
|
|
302
|
+
const body = map as unknown as typeof FilesystemAdapter.Api.MakeDirsRequest;
|
|
303
|
+
const { error, response } = await this.client.POST("/directories", {
|
|
304
|
+
body,
|
|
305
|
+
});
|
|
306
|
+
throwOnOpenApiFetchError({ error, response }, "Create directories failed");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async deleteDirectories(paths: string[]): Promise<void> {
|
|
310
|
+
const { error, response } = await this.client.DELETE("/directories", {
|
|
311
|
+
params: { query: { path: paths } },
|
|
312
|
+
});
|
|
313
|
+
throwOnOpenApiFetchError({ error, response }, "Delete directories failed");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async setPermissions(entries: SetPermissionEntry[]): Promise<void> {
|
|
317
|
+
const req: Record<string, Permission> = {};
|
|
318
|
+
for (const e of entries) {
|
|
319
|
+
req[e.path] = toPermission(e);
|
|
320
|
+
}
|
|
321
|
+
const body =
|
|
322
|
+
req as unknown as typeof FilesystemAdapter.Api.SetPermissionsRequest;
|
|
323
|
+
const { error, response } = await this.client.POST("/files/permissions", {
|
|
324
|
+
body,
|
|
325
|
+
});
|
|
326
|
+
throwOnOpenApiFetchError({ error, response }, "Set permissions failed");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async moveFiles(entries: MoveEntry[]): Promise<void> {
|
|
330
|
+
const req: RenameFileItem[] = entries.map((e) => ({
|
|
331
|
+
src: e.src,
|
|
332
|
+
dest: e.dest,
|
|
333
|
+
}));
|
|
334
|
+
const body =
|
|
335
|
+
req as unknown as typeof FilesystemAdapter.Api.MoveFilesRequest;
|
|
336
|
+
const { error, response } = await this.client.POST("/files/mv", {
|
|
337
|
+
body,
|
|
338
|
+
});
|
|
339
|
+
throwOnOpenApiFetchError({ error, response }, "Move files failed");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async replaceContents(entries: ContentReplaceEntry[]): Promise<void> {
|
|
343
|
+
const req: Record<string, ReplaceFileContentItem> = {};
|
|
344
|
+
for (const e of entries) {
|
|
345
|
+
req[e.path] = { old: e.oldContent, new: e.newContent };
|
|
346
|
+
}
|
|
347
|
+
const body =
|
|
348
|
+
req as unknown as typeof FilesystemAdapter.Api.ReplaceContentsRequest;
|
|
349
|
+
const { error, response } = await this.client.POST("/files/replace", {
|
|
350
|
+
body,
|
|
351
|
+
});
|
|
352
|
+
throwOnOpenApiFetchError({ error, response }, "Replace contents failed");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async search(entry: SearchEntry): Promise<SearchFilesResponse> {
|
|
356
|
+
const { data, error, response } = await this.client.GET("/files/search", {
|
|
357
|
+
params: { query: { path: entry.path, pattern: entry.pattern } },
|
|
358
|
+
});
|
|
359
|
+
throwOnOpenApiFetchError({ error, response }, "Search files failed");
|
|
360
|
+
|
|
361
|
+
// Make the OpenAPI contract explicit (and fail loudly on unexpected shapes).
|
|
362
|
+
const ok = data as typeof FilesystemAdapter.Api.SearchFilesOk | undefined;
|
|
363
|
+
if (!ok) return [];
|
|
364
|
+
if (!Array.isArray(ok)) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Search files failed: unexpected response shape (expected array, got ${typeof ok})`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return ok.map((x) => this.mapApiFileInfo(x));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async uploadFile(
|
|
373
|
+
meta: FileMetadata,
|
|
374
|
+
data:
|
|
375
|
+
| Blob
|
|
376
|
+
| Uint8Array
|
|
377
|
+
| ArrayBuffer
|
|
378
|
+
| string
|
|
379
|
+
| AsyncIterable<Uint8Array>
|
|
380
|
+
| ReadableStream<Uint8Array>
|
|
381
|
+
): Promise<void> {
|
|
382
|
+
const url = joinUrl(this.opts.baseUrl, "/files/upload");
|
|
383
|
+
const fileName = basename(meta.path);
|
|
384
|
+
const metadataJson = JSON.stringify(meta);
|
|
385
|
+
|
|
386
|
+
// Streaming path (large files): build multipart body manually to avoid buffering.
|
|
387
|
+
if (isReadableStream(data) || isAsyncIterable(data)) {
|
|
388
|
+
// Browsers do not allow streaming multipart requests with custom boundaries.
|
|
389
|
+
// Fall back to in-memory uploads when streaming is unavailable.
|
|
390
|
+
if (!isNodeRuntime()) {
|
|
391
|
+
const bytes = await collectBytes(data);
|
|
392
|
+
return await this.uploadFile(meta, bytes);
|
|
393
|
+
}
|
|
394
|
+
const boundary = `opensandbox_${Math.random()
|
|
395
|
+
.toString(16)
|
|
396
|
+
.slice(2)}_${Date.now()}`;
|
|
397
|
+
const bodyIt = multipartUploadBody({
|
|
398
|
+
boundary,
|
|
399
|
+
metadataJson,
|
|
400
|
+
fileName,
|
|
401
|
+
fileContentType: "application/octet-stream",
|
|
402
|
+
file: data,
|
|
403
|
+
});
|
|
404
|
+
const stream = toReadableStream(bodyIt);
|
|
405
|
+
|
|
406
|
+
const res = await this.fetch(url, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: {
|
|
409
|
+
"content-type": `multipart/form-data; boundary=${boundary}`,
|
|
410
|
+
...(this.opts.headers ?? {}),
|
|
411
|
+
},
|
|
412
|
+
body: stream as any,
|
|
413
|
+
// Node fetch (undici) requires duplex for streaming request bodies.
|
|
414
|
+
duplex: "half" as any,
|
|
415
|
+
} as any);
|
|
416
|
+
|
|
417
|
+
if (!res.ok) {
|
|
418
|
+
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
419
|
+
const rawBody = await res.text().catch(() => undefined);
|
|
420
|
+
throw new SandboxApiException({
|
|
421
|
+
message: `Upload failed (status=${res.status})`,
|
|
422
|
+
statusCode: res.status,
|
|
423
|
+
requestId,
|
|
424
|
+
error: new SandboxError(
|
|
425
|
+
SandboxError.UNEXPECTED_RESPONSE,
|
|
426
|
+
"Upload failed"
|
|
427
|
+
),
|
|
428
|
+
rawBody,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// In-memory path (small files): use FormData.
|
|
435
|
+
const form = new FormData();
|
|
436
|
+
form.append(
|
|
437
|
+
"metadata",
|
|
438
|
+
new Blob([metadataJson], { type: "application/json" }),
|
|
439
|
+
"metadata"
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (typeof data === "string") {
|
|
443
|
+
const textBlob = new Blob([data], { type: "text/plain; charset=utf-8" });
|
|
444
|
+
form.append("file", textBlob, fileName);
|
|
445
|
+
} else {
|
|
446
|
+
const blob = toUploadBlob(data);
|
|
447
|
+
const fileBlob = blob.type
|
|
448
|
+
? blob
|
|
449
|
+
: new Blob([blob], { type: "application/octet-stream" });
|
|
450
|
+
form.append("file", fileBlob, fileName);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const res = await this.fetch(url, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: {
|
|
456
|
+
...(this.opts.headers ?? {}),
|
|
457
|
+
},
|
|
458
|
+
body: form,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (!res.ok) {
|
|
462
|
+
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
463
|
+
const rawBody = await res.text().catch(() => undefined);
|
|
464
|
+
throw new SandboxApiException({
|
|
465
|
+
message: `Upload failed (status=${res.status})`,
|
|
466
|
+
statusCode: res.status,
|
|
467
|
+
requestId,
|
|
468
|
+
error: new SandboxError(
|
|
469
|
+
SandboxError.UNEXPECTED_RESPONSE,
|
|
470
|
+
"Upload failed"
|
|
471
|
+
),
|
|
472
|
+
rawBody,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async readBytes(
|
|
478
|
+
path: string,
|
|
479
|
+
opts?: { range?: string }
|
|
480
|
+
): Promise<Uint8Array> {
|
|
481
|
+
const url =
|
|
482
|
+
joinUrl(this.opts.baseUrl, "/files/download") +
|
|
483
|
+
`?path=${encodeURIComponent(path)}`;
|
|
484
|
+
const res = await this.fetch(url, {
|
|
485
|
+
method: "GET",
|
|
486
|
+
headers: {
|
|
487
|
+
...(this.opts.headers ?? {}),
|
|
488
|
+
...(opts?.range ? { Range: opts.range } : {}),
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
if (!res.ok) {
|
|
492
|
+
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
493
|
+
const rawBody = await res.text().catch(() => undefined);
|
|
494
|
+
throw new SandboxApiException({
|
|
495
|
+
message: "Download failed",
|
|
496
|
+
statusCode: res.status,
|
|
497
|
+
requestId,
|
|
498
|
+
error: new SandboxError(
|
|
499
|
+
SandboxError.UNEXPECTED_RESPONSE,
|
|
500
|
+
"Download failed"
|
|
501
|
+
),
|
|
502
|
+
rawBody,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const ab = await res.arrayBuffer();
|
|
506
|
+
return new Uint8Array(ab);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
readBytesStream(
|
|
510
|
+
path: string,
|
|
511
|
+
opts?: { range?: string }
|
|
512
|
+
): AsyncIterable<Uint8Array> {
|
|
513
|
+
return this.downloadStream(path, opts);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private async *downloadStream(
|
|
517
|
+
path: string,
|
|
518
|
+
opts?: { range?: string }
|
|
519
|
+
): AsyncIterable<Uint8Array> {
|
|
520
|
+
const url =
|
|
521
|
+
joinUrl(this.opts.baseUrl, "/files/download") +
|
|
522
|
+
`?path=${encodeURIComponent(path)}`;
|
|
523
|
+
const res = await this.fetch(url, {
|
|
524
|
+
method: "GET",
|
|
525
|
+
headers: {
|
|
526
|
+
...(this.opts.headers ?? {}),
|
|
527
|
+
...(opts?.range ? { Range: opts.range } : {}),
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
if (!res.ok) {
|
|
531
|
+
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
532
|
+
const rawBody = await res.text().catch(() => undefined);
|
|
533
|
+
throw new SandboxApiException({
|
|
534
|
+
message: "Download stream failed",
|
|
535
|
+
statusCode: res.status,
|
|
536
|
+
requestId,
|
|
537
|
+
error: new SandboxError(
|
|
538
|
+
SandboxError.UNEXPECTED_RESPONSE,
|
|
539
|
+
"Download stream failed"
|
|
540
|
+
),
|
|
541
|
+
rawBody,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const body = res.body as ReadableStream<Uint8Array> | null;
|
|
546
|
+
if (!body) return;
|
|
547
|
+
const reader = body.getReader();
|
|
548
|
+
while (true) {
|
|
549
|
+
const { done, value } = await reader.read();
|
|
550
|
+
if (done) return;
|
|
551
|
+
if (value) yield value;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async readFile(
|
|
556
|
+
path: string,
|
|
557
|
+
opts?: { encoding?: string; range?: string }
|
|
558
|
+
): Promise<string> {
|
|
559
|
+
const bytes = await this.readBytes(path, { range: opts?.range });
|
|
560
|
+
const encoding = opts?.encoding ?? "utf-8";
|
|
561
|
+
return new TextDecoder(encoding).decode(bytes);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async writeFiles(entries: WriteEntry[]): Promise<void> {
|
|
565
|
+
for (const e of entries) {
|
|
566
|
+
const meta: FileMetadata = {
|
|
567
|
+
path: e.path,
|
|
568
|
+
owner: e.owner,
|
|
569
|
+
group: e.group,
|
|
570
|
+
mode: e.mode,
|
|
571
|
+
};
|
|
572
|
+
await this.uploadFile(meta, e.data ?? "");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -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 { ExecdClient } from "../openapi/execdClient.js";
|
|
16
|
+
import { throwOnOpenApiFetchError } from "./openapiError.js";
|
|
17
|
+
import type { ExecdHealth } from "../services/execdHealth.js";
|
|
18
|
+
|
|
19
|
+
export class HealthAdapter implements ExecdHealth {
|
|
20
|
+
constructor(private readonly client: ExecdClient) {}
|
|
21
|
+
|
|
22
|
+
async ping(): Promise<boolean> {
|
|
23
|
+
const { error, response } = await this.client.GET("/ping");
|
|
24
|
+
throwOnOpenApiFetchError({ error, response }, "Execd ping failed");
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { ExecdClient } from "../openapi/execdClient.js";
|
|
16
|
+
import { throwOnOpenApiFetchError } from "./openapiError.js";
|
|
17
|
+
import type { paths as ExecdPaths } from "../api/execd.js";
|
|
18
|
+
import type { SandboxMetrics } from "../models/execd.js";
|
|
19
|
+
import type { ExecdMetrics } from "../services/execdMetrics.js";
|
|
20
|
+
|
|
21
|
+
type ApiMetricsOk =
|
|
22
|
+
ExecdPaths["/metrics"]["get"]["responses"][200]["content"]["application/json"];
|
|
23
|
+
|
|
24
|
+
function normalizeMetrics(m: ApiMetricsOk): SandboxMetrics {
|
|
25
|
+
const cpuCount = m.cpu_count ?? 0;
|
|
26
|
+
const cpuUsedPercentage = m.cpu_used_pct ?? 0;
|
|
27
|
+
const memoryTotalMiB = m.mem_total_mib ?? 0;
|
|
28
|
+
const memoryUsedMiB = m.mem_used_mib ?? 0;
|
|
29
|
+
const timestamp = m.timestamp ?? 0;
|
|
30
|
+
return {
|
|
31
|
+
cpuCount: Number(cpuCount),
|
|
32
|
+
cpuUsedPercentage: Number(cpuUsedPercentage),
|
|
33
|
+
memoryTotalMiB: Number(memoryTotalMiB),
|
|
34
|
+
memoryUsedMiB: Number(memoryUsedMiB),
|
|
35
|
+
timestamp: Number(timestamp),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class MetricsAdapter implements ExecdMetrics {
|
|
40
|
+
constructor(private readonly client: ExecdClient) {}
|
|
41
|
+
|
|
42
|
+
async getMetrics(): Promise<SandboxMetrics> {
|
|
43
|
+
const { data, error, response } = await this.client.GET("/metrics");
|
|
44
|
+
throwOnOpenApiFetchError({ error, response }, "Get execd metrics failed");
|
|
45
|
+
const ok = data as ApiMetricsOk | undefined;
|
|
46
|
+
if (!ok || typeof ok !== "object") {
|
|
47
|
+
throw new Error("Get execd metrics failed: unexpected response shape");
|
|
48
|
+
}
|
|
49
|
+
return normalizeMetrics(ok);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { SandboxApiException, SandboxError } from "../core/exceptions.js";
|
|
16
|
+
|
|
17
|
+
export function throwOnOpenApiFetchError(
|
|
18
|
+
result: { error?: unknown; response: Response },
|
|
19
|
+
fallbackMessage: string,
|
|
20
|
+
): void {
|
|
21
|
+
if (!result.error) return;
|
|
22
|
+
|
|
23
|
+
const requestId = result.response.headers.get("x-request-id") ?? undefined;
|
|
24
|
+
const status = (result.response as any).status ?? 0;
|
|
25
|
+
|
|
26
|
+
const err = result.error as any;
|
|
27
|
+
const message =
|
|
28
|
+
err?.message ??
|
|
29
|
+
err?.error?.message ??
|
|
30
|
+
fallbackMessage;
|
|
31
|
+
|
|
32
|
+
const code = err?.code ?? err?.error?.code;
|
|
33
|
+
const msg = err?.message ?? err?.error?.message ?? message;
|
|
34
|
+
|
|
35
|
+
throw new SandboxApiException({
|
|
36
|
+
message: msg,
|
|
37
|
+
statusCode: status,
|
|
38
|
+
requestId,
|
|
39
|
+
error: code ? new SandboxError(String(code), String(msg ?? "")) : new SandboxError(SandboxError.UNEXPECTED_RESPONSE, String(msg ?? "")),
|
|
40
|
+
rawBody: result.error,
|
|
41
|
+
});
|
|
42
|
+
}
|