pi-microsandbox 0.1.0
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/LICENSE +21 -0
- package/README.md +120 -0
- package/SECURITY.md +58 -0
- package/docs/commands.md +38 -0
- package/docs/configuration.md +80 -0
- package/docs/development.md +119 -0
- package/docs/getting-started.md +66 -0
- package/docs/images.md +190 -0
- package/docs/safety.md +39 -0
- package/docs/storage.md +57 -0
- package/docs/troubleshooting.md +20 -0
- package/extensions/pi-msb/command.ts +532 -0
- package/extensions/pi-msb/config.ts +771 -0
- package/extensions/pi-msb/control.ts +803 -0
- package/extensions/pi-msb/footer.ts +191 -0
- package/extensions/pi-msb/git.ts +256 -0
- package/extensions/pi-msb/index.ts +156 -0
- package/extensions/pi-msb/labels.ts +321 -0
- package/extensions/pi-msb/locks.ts +292 -0
- package/extensions/pi-msb/operations-exec.ts +434 -0
- package/extensions/pi-msb/operations.ts +321 -0
- package/extensions/pi-msb/prune.ts +232 -0
- package/extensions/pi-msb/sandbox-manager.ts +702 -0
- package/extensions/pi-msb/skill-access.ts +164 -0
- package/extensions/pi-msb/storage.ts +332 -0
- package/extensions/pi-msb/tools.ts +417 -0
- package/extensions/pi-msb/transport.ts +518 -0
- package/extensions/pi-msb/types.ts +436 -0
- package/package.json +74 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { posix as posixPath } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EntryKind,
|
|
5
|
+
ExecOptions,
|
|
6
|
+
ExecStreamOptions,
|
|
7
|
+
FsEntry,
|
|
8
|
+
SandboxTransport,
|
|
9
|
+
StatResult,
|
|
10
|
+
TransportErrorCode,
|
|
11
|
+
TransportExecResult,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
|
|
14
|
+
/** An SDK operation failed before it could produce a transport result. */
|
|
15
|
+
export class SandboxTransportError extends Error {
|
|
16
|
+
readonly code: TransportErrorCode;
|
|
17
|
+
override readonly cause?: unknown;
|
|
18
|
+
|
|
19
|
+
constructor(message: string, code: TransportErrorCode, cause?: unknown) {
|
|
20
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
21
|
+
this.name = "SandboxTransportError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type UnknownRecord = Record<string, unknown>;
|
|
28
|
+
type StreamEvent = {
|
|
29
|
+
kind?: unknown;
|
|
30
|
+
data?: unknown;
|
|
31
|
+
code?: unknown;
|
|
32
|
+
};
|
|
33
|
+
type ExecHandle = {
|
|
34
|
+
kill?: () => Promise<void> | void;
|
|
35
|
+
[Symbol.asyncIterator]?: () => AsyncIterator<StreamEvent>;
|
|
36
|
+
};
|
|
37
|
+
type ExecBuilder = {
|
|
38
|
+
args: (args: string[]) => ExecBuilder;
|
|
39
|
+
cwd?: (cwd: string) => ExecBuilder;
|
|
40
|
+
timeout?: (timeoutMs: number) => ExecBuilder;
|
|
41
|
+
};
|
|
42
|
+
type SandboxLike = {
|
|
43
|
+
fs?: () => UnknownRecord;
|
|
44
|
+
execWith?: (
|
|
45
|
+
command: string,
|
|
46
|
+
configure: (builder: ExecBuilder) => ExecBuilder,
|
|
47
|
+
) => Promise<UnknownRecord>;
|
|
48
|
+
execStreamWith?: (
|
|
49
|
+
command: string,
|
|
50
|
+
configure: (builder: ExecBuilder) => ExecBuilder,
|
|
51
|
+
) => Promise<ExecHandle>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ActiveStream = {
|
|
55
|
+
handle: ExecHandle;
|
|
56
|
+
kill: () => Promise<void>;
|
|
57
|
+
done: Promise<{ exitCode: number }>;
|
|
58
|
+
};
|
|
59
|
+
type PendingStreamCreation = {
|
|
60
|
+
done: Promise<void>;
|
|
61
|
+
resolve: () => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const TRANSPORT_CODES = new Set<TransportErrorCode>([
|
|
65
|
+
"NOT_FOUND",
|
|
66
|
+
"ACCESS",
|
|
67
|
+
"TIMEOUT",
|
|
68
|
+
"ABORTED",
|
|
69
|
+
"SANDBOX_DOWN",
|
|
70
|
+
"INVALID",
|
|
71
|
+
"IO",
|
|
72
|
+
"UNKNOWN",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
76
|
+
return typeof value === "object" && value !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function constructorName(error: unknown): string {
|
|
80
|
+
if (!isRecord(error)) return "";
|
|
81
|
+
const ctor = error.constructor;
|
|
82
|
+
if (typeof ctor === "function" && typeof ctor.name === "string" && ctor.name !== "Object") {
|
|
83
|
+
return ctor.name;
|
|
84
|
+
}
|
|
85
|
+
return typeof error.name === "string" ? error.name : "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sdkCode(error: unknown): string | undefined {
|
|
89
|
+
if (!isRecord(error) || typeof error.code !== "string") return undefined;
|
|
90
|
+
return error.code;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mapSdkError(error: unknown, operation: string): SandboxTransportError {
|
|
94
|
+
if (error instanceof SandboxTransportError) return error;
|
|
95
|
+
|
|
96
|
+
const name = constructorName(error);
|
|
97
|
+
const code = sdkCode(error);
|
|
98
|
+
let mapped: TransportErrorCode = "UNKNOWN";
|
|
99
|
+
|
|
100
|
+
// Prefer the SDK's typed error identity/code. Do not inspect arbitrary error
|
|
101
|
+
// messages: those can contain guest data or resolved configuration secrets.
|
|
102
|
+
if (
|
|
103
|
+
name === "SandboxNotFoundError" ||
|
|
104
|
+
name === "NotFoundError" ||
|
|
105
|
+
code === "NOT_FOUND" ||
|
|
106
|
+
code === "ENOENT" ||
|
|
107
|
+
code === "sandboxNotFound" ||
|
|
108
|
+
code === "volumeNotFound"
|
|
109
|
+
) {
|
|
110
|
+
mapped = "NOT_FOUND";
|
|
111
|
+
} else if (
|
|
112
|
+
name === "PermissionDeniedError" ||
|
|
113
|
+
name === "AccessDeniedError" ||
|
|
114
|
+
name === "SandboxPermissionError" ||
|
|
115
|
+
code === "ACCESS" ||
|
|
116
|
+
code === "EACCES" ||
|
|
117
|
+
code === "EPERM" ||
|
|
118
|
+
code === "permissionDenied"
|
|
119
|
+
) {
|
|
120
|
+
mapped = "ACCESS";
|
|
121
|
+
} else if (
|
|
122
|
+
name === "ExecTimeoutError" ||
|
|
123
|
+
name === "TimeoutError" ||
|
|
124
|
+
code === "TIMEOUT" ||
|
|
125
|
+
code === "ETIMEDOUT" ||
|
|
126
|
+
code === "execTimeout"
|
|
127
|
+
) {
|
|
128
|
+
mapped = "TIMEOUT";
|
|
129
|
+
} else if (
|
|
130
|
+
name === "AbortError" ||
|
|
131
|
+
name === "AbortExecutionError" ||
|
|
132
|
+
code === "ABORTED" ||
|
|
133
|
+
code === "ABORT_ERR"
|
|
134
|
+
) {
|
|
135
|
+
mapped = "ABORTED";
|
|
136
|
+
} else if (
|
|
137
|
+
name === "SandboxDownError" ||
|
|
138
|
+
name === "SandboxStoppedError" ||
|
|
139
|
+
name === "SandboxNotRunningError" ||
|
|
140
|
+
name === "ConnectionClosedError" ||
|
|
141
|
+
code === "SANDBOX_DOWN" ||
|
|
142
|
+
code === "CONNECTION_CLOSED" ||
|
|
143
|
+
code === "runtime"
|
|
144
|
+
) {
|
|
145
|
+
mapped = "SANDBOX_DOWN";
|
|
146
|
+
} else if (
|
|
147
|
+
name === "InvalidArgumentError" ||
|
|
148
|
+
name === "ValidationError" ||
|
|
149
|
+
name === "SandboxAlreadyExistsError" ||
|
|
150
|
+
name === "SandboxStillRunningError" ||
|
|
151
|
+
code === "INVALID" ||
|
|
152
|
+
code === "INVALID_ARGUMENT" ||
|
|
153
|
+
code === "VALIDATION_ERROR" ||
|
|
154
|
+
code === "invalidConfig" ||
|
|
155
|
+
code === "sandboxAlreadyExists" ||
|
|
156
|
+
code === "sandboxStillRunning" ||
|
|
157
|
+
code === "unsupportedOperation" ||
|
|
158
|
+
code === "unsupported" ||
|
|
159
|
+
code === "volumeAlreadyExists"
|
|
160
|
+
) {
|
|
161
|
+
mapped = "INVALID";
|
|
162
|
+
} else if (
|
|
163
|
+
name === "SandboxFsOpsError" ||
|
|
164
|
+
name === "IoError" ||
|
|
165
|
+
name === "IOError" ||
|
|
166
|
+
code === "IO" ||
|
|
167
|
+
code === "IO_ERROR" ||
|
|
168
|
+
code === "EIO" ||
|
|
169
|
+
code === "io" ||
|
|
170
|
+
code === "sandboxFsOps"
|
|
171
|
+
) {
|
|
172
|
+
mapped = "IO";
|
|
173
|
+
} else if (code && TRANSPORT_CODES.has(code as TransportErrorCode)) {
|
|
174
|
+
mapped = code as TransportErrorCode;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return new SandboxTransportError(`Sandbox transport ${operation} failed`, mapped, error);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function invalidSdk(operation: string): never {
|
|
181
|
+
throw new SandboxTransportError(
|
|
182
|
+
`Microsandbox SDK does not provide ${operation}`,
|
|
183
|
+
"INVALID",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function method<T extends (...args: any[]) => any>(
|
|
188
|
+
value: UnknownRecord | undefined,
|
|
189
|
+
name: string,
|
|
190
|
+
): T {
|
|
191
|
+
const candidate = value?.[name];
|
|
192
|
+
if (typeof candidate !== "function") invalidSdk(name);
|
|
193
|
+
return candidate.bind(value) as T;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function kindOf(value: unknown): EntryKind {
|
|
197
|
+
return value === "file" || value === "directory" ? value : "other";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dateMillis(value: unknown): number | null {
|
|
201
|
+
if (value === null || value === undefined) return null;
|
|
202
|
+
if (value instanceof Date) return value.getTime();
|
|
203
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function bytes(value: unknown, operation: string): Buffer {
|
|
207
|
+
if (value instanceof Uint8Array || typeof value === "string") return Buffer.from(value);
|
|
208
|
+
throw new SandboxTransportError(`Invalid ${operation} byte result`, "INVALID");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function configureExecution(
|
|
212
|
+
builder: ExecBuilder,
|
|
213
|
+
args: string[],
|
|
214
|
+
options?: ExecOptions,
|
|
215
|
+
): ExecBuilder {
|
|
216
|
+
let configured = builder.args(args);
|
|
217
|
+
if (options?.cwd !== undefined) {
|
|
218
|
+
if (typeof configured.cwd !== "function") invalidSdk("exec builder cwd");
|
|
219
|
+
configured = configured.cwd(options.cwd);
|
|
220
|
+
}
|
|
221
|
+
if (options?.timeoutMs !== undefined) {
|
|
222
|
+
if (typeof configured.timeout !== "function") invalidSdk("exec builder timeout");
|
|
223
|
+
configured = configured.timeout(options.timeoutMs);
|
|
224
|
+
}
|
|
225
|
+
return configured;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function eventData(value: unknown): Buffer {
|
|
229
|
+
return value instanceof Uint8Array || typeof value === "string"
|
|
230
|
+
? Buffer.from(value)
|
|
231
|
+
: Buffer.from([]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function killHandle(handle: ExecHandle): Promise<void> {
|
|
235
|
+
if (typeof handle.kill !== "function") return;
|
|
236
|
+
await handle.kill();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function drainHandle(handle: ExecHandle): Promise<void> {
|
|
240
|
+
if (typeof handle[Symbol.asyncIterator] !== "function") return;
|
|
241
|
+
const iterator = handle[Symbol.asyncIterator]!();
|
|
242
|
+
while (true) {
|
|
243
|
+
const next = await iterator.next();
|
|
244
|
+
if (next.done) return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Adapt a microsandbox Sandbox without importing the native SDK at
|
|
250
|
+
* extension load time. The caller supplies the connected Sandbox instance.
|
|
251
|
+
*/
|
|
252
|
+
export function createSdkTransport(sandbox: unknown): SandboxTransport {
|
|
253
|
+
const raw = sandbox as SandboxLike;
|
|
254
|
+
const active = new Set<ActiveStream>();
|
|
255
|
+
const pendingCreations = new Set<PendingStreamCreation>();
|
|
256
|
+
let disposed = false;
|
|
257
|
+
|
|
258
|
+
const rejectIfDisposed = (): void => {
|
|
259
|
+
if (disposed) {
|
|
260
|
+
throw new SandboxTransportError("Sandbox transport is disposed", "SANDBOX_DOWN");
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const call = async <T>(operation: string, fn: () => Promise<T>): Promise<T> => {
|
|
265
|
+
rejectIfDisposed();
|
|
266
|
+
try {
|
|
267
|
+
return await fn();
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw mapSdkError(error, operation);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const getFs = (): UnknownRecord => {
|
|
274
|
+
rejectIfDisposed();
|
|
275
|
+
if (typeof raw.fs !== "function") invalidSdk("fs");
|
|
276
|
+
const fs = raw.fs();
|
|
277
|
+
if (!isRecord(fs)) invalidSdk("fs");
|
|
278
|
+
return fs;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const readFile = (path: string): Promise<Buffer> =>
|
|
282
|
+
call("read", async () => {
|
|
283
|
+
const read = method<(path: string) => Promise<unknown>>(getFs(), "read");
|
|
284
|
+
return bytes(await read(path), "read");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const writeFile = (path: string, data: string | Buffer): Promise<void> =>
|
|
288
|
+
call("write", async () => {
|
|
289
|
+
const write = method<(path: string, data: string | Uint8Array) => Promise<void>>(
|
|
290
|
+
getFs(),
|
|
291
|
+
"write",
|
|
292
|
+
);
|
|
293
|
+
await write(path, data);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const exists = (path: string): Promise<boolean> =>
|
|
297
|
+
call("exists", async () => {
|
|
298
|
+
const check = method<(path: string) => Promise<boolean>>(getFs(), "exists");
|
|
299
|
+
return await check(path);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const stat = (path: string): Promise<StatResult> =>
|
|
303
|
+
call("stat", async () => {
|
|
304
|
+
const getStat = method<(path: string) => Promise<UnknownRecord>>(getFs(), "stat");
|
|
305
|
+
const result = await getStat(path);
|
|
306
|
+
return {
|
|
307
|
+
kind: kindOf(result.kind),
|
|
308
|
+
size: typeof result.size === "number" ? result.size : 0,
|
|
309
|
+
mode: typeof result.mode === "number" ? result.mode : 0,
|
|
310
|
+
readonly: result.readonly === true,
|
|
311
|
+
modifiedAt: dateMillis(result.modified ?? result.modifiedAt),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const list = (path: string): Promise<FsEntry[]> =>
|
|
316
|
+
call("list", async () => {
|
|
317
|
+
const listEntries = method<(path: string) => Promise<unknown[]>>(getFs(), "list");
|
|
318
|
+
const entries = await listEntries(path);
|
|
319
|
+
if (!Array.isArray(entries)) {
|
|
320
|
+
throw new SandboxTransportError("Invalid filesystem list result", "INVALID");
|
|
321
|
+
}
|
|
322
|
+
return entries.map((entry) => {
|
|
323
|
+
const value = isRecord(entry) ? entry : {};
|
|
324
|
+
const entryPath = typeof value.path === "string" ? value.path : "";
|
|
325
|
+
const name = posixPath.basename(entryPath) || (typeof value.name === "string" ? value.name : "");
|
|
326
|
+
return { name, kind: kindOf(value.kind) };
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const copyFromHost = (hostPath: string, guestPath: string): Promise<void> =>
|
|
331
|
+
call("copyFromHost", async () => {
|
|
332
|
+
const copy = method<
|
|
333
|
+
(hostPath: string, guestPath: string) => Promise<void>
|
|
334
|
+
>(getFs(), "copyFromHost");
|
|
335
|
+
await copy(hostPath, guestPath);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const copyToHost = (guestPath: string, hostPath: string): Promise<void> =>
|
|
339
|
+
call("copyToHost", async () => {
|
|
340
|
+
const copy = method<
|
|
341
|
+
(guestPath: string, hostPath: string) => Promise<void>
|
|
342
|
+
>(getFs(), "copyToHost");
|
|
343
|
+
await copy(guestPath, hostPath);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const exec = (
|
|
347
|
+
command: string,
|
|
348
|
+
args: string[],
|
|
349
|
+
options?: ExecOptions,
|
|
350
|
+
): Promise<TransportExecResult> =>
|
|
351
|
+
call("exec", async () => {
|
|
352
|
+
if (typeof raw.execWith !== "function") invalidSdk("execWith");
|
|
353
|
+
const output = await raw.execWith(command, (builder) =>
|
|
354
|
+
configureExecution(builder, args, options),
|
|
355
|
+
);
|
|
356
|
+
const stdoutBytes = method<() => Uint8Array>(output, "stdoutBytes");
|
|
357
|
+
const stderrBytes = method<() => Uint8Array>(output, "stderrBytes");
|
|
358
|
+
if (typeof output.code !== "number") {
|
|
359
|
+
throw new SandboxTransportError("Invalid execution exit code", "INVALID");
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
stdout: Buffer.from(stdoutBytes()),
|
|
363
|
+
stderr: Buffer.from(stderrBytes()),
|
|
364
|
+
exitCode: output.code,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const execStream = async (
|
|
369
|
+
command: string,
|
|
370
|
+
args: string[],
|
|
371
|
+
options?: ExecStreamOptions,
|
|
372
|
+
): Promise<{ exitCode: number }> => {
|
|
373
|
+
rejectIfDisposed();
|
|
374
|
+
if (typeof raw.execStreamWith !== "function") invalidSdk("execStreamWith");
|
|
375
|
+
|
|
376
|
+
let resolveCreation!: () => void;
|
|
377
|
+
const creation: PendingStreamCreation = {
|
|
378
|
+
done: new Promise<void>((resolve) => {
|
|
379
|
+
resolveCreation = resolve;
|
|
380
|
+
}),
|
|
381
|
+
resolve: () => resolveCreation(),
|
|
382
|
+
};
|
|
383
|
+
pendingCreations.add(creation);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
let handle: ExecHandle;
|
|
387
|
+
try {
|
|
388
|
+
handle = await raw.execStreamWith(command, (builder) =>
|
|
389
|
+
configureExecution(builder, args, options),
|
|
390
|
+
);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (disposed) {
|
|
393
|
+
throw new SandboxTransportError("Sandbox transport is disposed", "SANDBOX_DOWN", error);
|
|
394
|
+
}
|
|
395
|
+
throw mapSdkError(error, "execStream");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// dispose() can run while the SDK is still acquiring the handle. The
|
|
399
|
+
// late handle must be killed and drained before that dispose() settles.
|
|
400
|
+
if (disposed) {
|
|
401
|
+
try {
|
|
402
|
+
await killHandle(handle);
|
|
403
|
+
} catch {
|
|
404
|
+
// The transport remains fail-closed even if SDK cleanup reports an error.
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
await drainHandle(handle);
|
|
408
|
+
} catch {
|
|
409
|
+
// The transport remains fail-closed even if SDK draining reports an error.
|
|
410
|
+
}
|
|
411
|
+
throw new SandboxTransportError("Sandbox transport is disposed", "SANDBOX_DOWN");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (typeof handle?.[Symbol.asyncIterator] !== "function") {
|
|
415
|
+
throw new SandboxTransportError("Invalid execution stream handle", "INVALID");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let killPromise: Promise<void> | undefined;
|
|
419
|
+
const kill = (): Promise<void> => {
|
|
420
|
+
if (!killPromise) {
|
|
421
|
+
killPromise = killHandle(handle).catch((error) => {
|
|
422
|
+
throw mapSdkError(error, "kill");
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return killPromise;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
let aborted = options?.signal?.aborted === true;
|
|
429
|
+
let exitCode: number | undefined;
|
|
430
|
+
let entry!: ActiveStream;
|
|
431
|
+
const consume = async (): Promise<{ exitCode: number }> => {
|
|
432
|
+
const signal = options?.signal;
|
|
433
|
+
const onAbort = (): void => {
|
|
434
|
+
aborted = true;
|
|
435
|
+
void kill().catch(() => undefined);
|
|
436
|
+
};
|
|
437
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
438
|
+
try {
|
|
439
|
+
const iterator = handle[Symbol.asyncIterator]!();
|
|
440
|
+
while (true) {
|
|
441
|
+
const next = await iterator.next();
|
|
442
|
+
if (next.done) break;
|
|
443
|
+
const event = next.value as StreamEvent;
|
|
444
|
+
if (event.kind === "stdout") {
|
|
445
|
+
options?.onStdout?.(eventData(event.data));
|
|
446
|
+
} else if (event.kind === "stderr") {
|
|
447
|
+
options?.onStderr?.(eventData(event.data));
|
|
448
|
+
} else if (event.kind === "exited" && typeof event.code === "number") {
|
|
449
|
+
exitCode = event.code;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (aborted) {
|
|
453
|
+
throw new SandboxTransportError("Sandbox execution aborted", "ABORTED", signal?.reason);
|
|
454
|
+
}
|
|
455
|
+
if (disposed) {
|
|
456
|
+
throw new SandboxTransportError("Sandbox transport was disposed", "SANDBOX_DOWN");
|
|
457
|
+
}
|
|
458
|
+
return { exitCode: exitCode ?? 0 };
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (aborted) {
|
|
461
|
+
throw new SandboxTransportError("Sandbox execution aborted", "ABORTED", signal?.reason);
|
|
462
|
+
}
|
|
463
|
+
throw mapSdkError(error, "execStream");
|
|
464
|
+
} finally {
|
|
465
|
+
signal?.removeEventListener("abort", onAbort);
|
|
466
|
+
active.delete(entry);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
entry = {
|
|
471
|
+
handle,
|
|
472
|
+
kill,
|
|
473
|
+
done: Promise.resolve({ exitCode: 0 }),
|
|
474
|
+
};
|
|
475
|
+
active.add(entry);
|
|
476
|
+
if (aborted) void kill().catch(() => undefined);
|
|
477
|
+
entry.done = consume();
|
|
478
|
+
return entry.done;
|
|
479
|
+
} finally {
|
|
480
|
+
pendingCreations.delete(creation);
|
|
481
|
+
creation.resolve();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const dispose = async (): Promise<void> => {
|
|
486
|
+
if (disposed) return;
|
|
487
|
+
disposed = true;
|
|
488
|
+
|
|
489
|
+
// Wait for SDK acquisitions already in flight. A creation that resolves
|
|
490
|
+
// after disposal kills/drains its late handle before resolving this gate.
|
|
491
|
+
const pendingDrain = Promise.allSettled(
|
|
492
|
+
[...pendingCreations].map((creation) => creation.done),
|
|
493
|
+
);
|
|
494
|
+
const activeDrain = Promise.allSettled(
|
|
495
|
+
[...active].map(async (entry) => {
|
|
496
|
+
try {
|
|
497
|
+
await entry.kill();
|
|
498
|
+
} finally {
|
|
499
|
+
await entry.done;
|
|
500
|
+
}
|
|
501
|
+
}),
|
|
502
|
+
);
|
|
503
|
+
await Promise.all([pendingDrain, activeDrain]);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
readFile,
|
|
508
|
+
writeFile,
|
|
509
|
+
exists,
|
|
510
|
+
stat,
|
|
511
|
+
list,
|
|
512
|
+
copyFromHost,
|
|
513
|
+
copyToHost,
|
|
514
|
+
exec,
|
|
515
|
+
execStream,
|
|
516
|
+
dispose,
|
|
517
|
+
};
|
|
518
|
+
}
|