@crewhaus/sandbox 0.1.4 → 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/dist/index.d.ts +105 -0
- package/dist/index.js +308 -0
- package/package.json +9 -6
- package/src/index.test.ts +0 -553
- package/src/index.ts +0 -433
package/src/index.ts
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import { CrewhausError } from "@crewhaus/errors";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Catalog R8 `sandbox` — containerised exec environment.
|
|
5
|
-
*
|
|
6
|
-
* Backends:
|
|
7
|
-
* docker — production default; assumes `docker` daemon reachable.
|
|
8
|
-
* podman — drop-in replacement that swaps the CLI binary.
|
|
9
|
-
* noop — in-process exec (NOT a security boundary). Test-only;
|
|
10
|
-
* must be opted in via `CREWHAUS_SANDBOX=noop`. The
|
|
11
|
-
* permission engine refuses to satisfy `requiresSandbox`
|
|
12
|
-
* tools when this backend is active.
|
|
13
|
-
*
|
|
14
|
-
* Defaults applied to every container:
|
|
15
|
-
* --network none
|
|
16
|
-
* --memory 512m
|
|
17
|
-
* --cpus 1.0
|
|
18
|
-
* --read-only
|
|
19
|
-
* --tmpfs /tmp:rw,size=64m,mode=1777,exec
|
|
20
|
-
* 60 second wall-clock timeout
|
|
21
|
-
*
|
|
22
|
-
* Image allowlist: any image string requested by `exec()` must appear
|
|
23
|
-
* in the constructor's `allowedImages` set OR in
|
|
24
|
-
* `CREWHAUS_SANDBOX_ALLOWED_IMAGES` (comma-separated). If neither is
|
|
25
|
-
* set, only the curated default list is allowed:
|
|
26
|
-
* - python:3.13-slim
|
|
27
|
-
* - node:22-alpine
|
|
28
|
-
* - alpine:3.19
|
|
29
|
-
*
|
|
30
|
-
* Mount whitelist: callers pass `mounts: ReadonlyArray<{src,dst,readonly?}>`,
|
|
31
|
-
* but only `src` paths inside `mountWhitelist` (or under `process.cwd()`
|
|
32
|
-
* by default) are accepted. Path-traversal attempts (`..` segments,
|
|
33
|
-
* non-absolute `src`) throw before docker is invoked.
|
|
34
|
-
*
|
|
35
|
-
* SECURITY: image strings and command strings are passed as separate
|
|
36
|
-
* `Bun.spawn` argv elements, so shell metacharacters (`;`, `&&`, `$()`)
|
|
37
|
-
* cannot escape the docker run invocation. Image and mount values are
|
|
38
|
-
* additionally screened for line-feed and dash-prefix tampering before
|
|
39
|
-
* the spawn so an attacker cannot smuggle CLI flags via input.
|
|
40
|
-
*
|
|
41
|
-
* Layer R8 (production safety floor). Pairs with `tool-code-execution`
|
|
42
|
-
* (R4) and `permission-engine` (R8 — the `requiresSandbox` floor).
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
export type SandboxBackend = "docker" | "podman" | "noop";
|
|
46
|
-
|
|
47
|
-
export type SandboxMount = {
|
|
48
|
-
/** Absolute host path. Must be inside `mountWhitelist` or cwd. */
|
|
49
|
-
readonly src: string;
|
|
50
|
-
/** Absolute container path. */
|
|
51
|
-
readonly dst: string;
|
|
52
|
-
/** Defaults to true (read-only mount). */
|
|
53
|
-
readonly readonly?: boolean;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export type SandboxOptions = {
|
|
57
|
-
/** Defaults to env `CREWHAUS_SANDBOX` (then "docker"). */
|
|
58
|
-
readonly backend?: SandboxBackend;
|
|
59
|
-
/** Non-empty subset of permitted images. Empty = use defaults+env. */
|
|
60
|
-
readonly allowedImages?: ReadonlyArray<string>;
|
|
61
|
-
/** Absolute paths under which `mounts.src` may live. Defaults to [cwd]. */
|
|
62
|
-
readonly mountWhitelist?: ReadonlyArray<string>;
|
|
63
|
-
/** Default exec timeout. Per-call timeout overrides this. */
|
|
64
|
-
readonly defaultTimeoutMs?: number;
|
|
65
|
-
/** Memory cap, e.g. "512m". */
|
|
66
|
-
readonly memory?: string;
|
|
67
|
-
/** CPU cap, e.g. "1.0". */
|
|
68
|
-
readonly cpus?: string;
|
|
69
|
-
/** When true, network is allowed (default false). Smoke checks rely on this default. */
|
|
70
|
-
readonly network?: boolean;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type SandboxExecOptions = {
|
|
74
|
-
/** Container image (e.g. "python:3.13-slim"). Must be in allowlist. */
|
|
75
|
-
readonly image: string;
|
|
76
|
-
/** Argv form — passed as separate args to the interpreter. */
|
|
77
|
-
readonly argv: ReadonlyArray<string>;
|
|
78
|
-
/** Optional: piped to the container's stdin. */
|
|
79
|
-
readonly stdin?: string;
|
|
80
|
-
/** Optional: extra env vars (key/value, no shell interpolation). */
|
|
81
|
-
readonly env?: Readonly<Record<string, string>>;
|
|
82
|
-
/** Per-call mount additions; src must pass the whitelist. */
|
|
83
|
-
readonly mounts?: ReadonlyArray<SandboxMount>;
|
|
84
|
-
/** Override the sandbox's default timeout. */
|
|
85
|
-
readonly timeoutMs?: number;
|
|
86
|
-
/** Optional cooperative cancellation. */
|
|
87
|
-
readonly signal?: AbortSignal;
|
|
88
|
-
/** Forwarded line-by-line for streaming consumers. */
|
|
89
|
-
readonly onStdoutChunk?: (chunk: string) => void;
|
|
90
|
-
readonly onStderrChunk?: (chunk: string) => void;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export type SandboxExecResult = {
|
|
94
|
-
readonly stdout: string;
|
|
95
|
-
readonly stderr: string;
|
|
96
|
-
readonly exitCode: number;
|
|
97
|
-
readonly timedOut: boolean;
|
|
98
|
-
readonly durationMs: number;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export class SandboxError extends CrewhausError {
|
|
102
|
-
override readonly name = "SandboxError";
|
|
103
|
-
constructor(message: string, cause?: unknown) {
|
|
104
|
-
super("config", message, cause);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface Sandbox {
|
|
109
|
-
readonly backend: SandboxBackend;
|
|
110
|
-
exec(opts: SandboxExecOptions): Promise<SandboxExecResult>;
|
|
111
|
-
/** Idempotent. */
|
|
112
|
-
close(): Promise<void>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const DEFAULT_ALLOWED_IMAGES: ReadonlyArray<string> = [
|
|
116
|
-
"python:3.13-slim",
|
|
117
|
-
"node:22-alpine",
|
|
118
|
-
"alpine:3.19",
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
122
|
-
const DEFAULT_MEMORY = "512m";
|
|
123
|
-
const DEFAULT_CPUS = "1.0";
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Image strings must be `repository[:tag][@digest]`. We disallow leading
|
|
127
|
-
* dashes (CLI flag injection), whitespace (newline-injection), and shell
|
|
128
|
-
* metacharacters even though we never pass them to a shell — defense in
|
|
129
|
-
* depth.
|
|
130
|
-
*/
|
|
131
|
-
const IMAGE_RE = /^[a-z0-9][a-z0-9._\-/]*(?::[a-zA-Z0-9._\-]+)?(?:@sha256:[a-f0-9]{64})?$/;
|
|
132
|
-
|
|
133
|
-
function readEnvBackend(): SandboxBackend | undefined {
|
|
134
|
-
const raw = (process.env["CREWHAUS_SANDBOX"] ?? "").trim().toLowerCase();
|
|
135
|
-
if (raw === "") return undefined;
|
|
136
|
-
if (raw === "docker" || raw === "podman" || raw === "noop") return raw;
|
|
137
|
-
throw new SandboxError(`CREWHAUS_SANDBOX=${raw} is not one of docker|podman|noop`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function readEnvAllowedImages(): ReadonlyArray<string> {
|
|
141
|
-
const raw = process.env["CREWHAUS_SANDBOX_ALLOWED_IMAGES"];
|
|
142
|
-
if (raw === undefined || raw.trim() === "") return [];
|
|
143
|
-
return raw
|
|
144
|
-
.split(",")
|
|
145
|
-
.map((s) => s.trim())
|
|
146
|
-
.filter((s) => s.length > 0);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function validateImage(image: string, allow: ReadonlySet<string>): void {
|
|
150
|
-
if (image.length === 0) throw new SandboxError("image is required");
|
|
151
|
-
if (image.startsWith("-")) {
|
|
152
|
-
throw new SandboxError(`image "${image}" looks like a CLI flag — refused`);
|
|
153
|
-
}
|
|
154
|
-
if (image.includes("\n") || image.includes(" ") || image.includes("\t")) {
|
|
155
|
-
throw new SandboxError(`image "${image}" contains whitespace — refused`);
|
|
156
|
-
}
|
|
157
|
-
if (!IMAGE_RE.test(image)) {
|
|
158
|
-
throw new SandboxError(`image "${image}" is not a valid registry reference`);
|
|
159
|
-
}
|
|
160
|
-
if (!allow.has(image)) {
|
|
161
|
-
const list = [...allow].sort().join(", ");
|
|
162
|
-
throw new SandboxError(
|
|
163
|
-
`image "${image}" is not on the allowlist — allowed: ${list || "(empty)"}`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function validateMount(m: SandboxMount, whitelist: ReadonlyArray<string>): void {
|
|
169
|
-
if (!m.src.startsWith("/")) {
|
|
170
|
-
throw new SandboxError(`mount src "${m.src}" must be absolute`);
|
|
171
|
-
}
|
|
172
|
-
if (!m.dst.startsWith("/")) {
|
|
173
|
-
throw new SandboxError(`mount dst "${m.dst}" must be absolute`);
|
|
174
|
-
}
|
|
175
|
-
if (m.src.includes("..") || m.dst.includes("..")) {
|
|
176
|
-
throw new SandboxError(`mount path may not contain ".." (src="${m.src}", dst="${m.dst}")`);
|
|
177
|
-
}
|
|
178
|
-
if (m.src.includes("\n") || m.dst.includes("\n")) {
|
|
179
|
-
throw new SandboxError("mount path may not contain newlines");
|
|
180
|
-
}
|
|
181
|
-
const ok = whitelist.some((root) => m.src === root || m.src.startsWith(`${root}/`));
|
|
182
|
-
if (!ok) {
|
|
183
|
-
const roots = whitelist.join(", ");
|
|
184
|
-
throw new SandboxError(`mount src "${m.src}" is not under any whitelisted root (${roots})`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function validateEnvKey(key: string): void {
|
|
189
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
190
|
-
throw new SandboxError(`env key "${key}" is not a valid identifier`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
class DockerLikeSandbox implements Sandbox {
|
|
195
|
-
readonly backend: SandboxBackend;
|
|
196
|
-
private readonly cli: string;
|
|
197
|
-
private readonly allowedImages: ReadonlySet<string>;
|
|
198
|
-
private readonly mountWhitelist: ReadonlyArray<string>;
|
|
199
|
-
private readonly defaultTimeoutMs: number;
|
|
200
|
-
private readonly memory: string;
|
|
201
|
-
private readonly cpus: string;
|
|
202
|
-
private readonly network: boolean;
|
|
203
|
-
private closed = false;
|
|
204
|
-
|
|
205
|
-
constructor(backend: "docker" | "podman", opts: SandboxOptions) {
|
|
206
|
-
this.backend = backend;
|
|
207
|
-
this.cli = backend;
|
|
208
|
-
const ownAllowed = (opts.allowedImages ?? []).filter((s) => s.length > 0);
|
|
209
|
-
const envAllowed = readEnvAllowedImages();
|
|
210
|
-
const merged = new Set<string>(
|
|
211
|
-
ownAllowed.length > 0 || envAllowed.length > 0
|
|
212
|
-
? [...ownAllowed, ...envAllowed]
|
|
213
|
-
: DEFAULT_ALLOWED_IMAGES,
|
|
214
|
-
);
|
|
215
|
-
this.allowedImages = merged;
|
|
216
|
-
this.mountWhitelist = (opts.mountWhitelist ?? [process.cwd()]).map((p) => {
|
|
217
|
-
if (!p.startsWith("/")) {
|
|
218
|
-
throw new SandboxError(`mountWhitelist entry "${p}" must be absolute`);
|
|
219
|
-
}
|
|
220
|
-
return p;
|
|
221
|
-
});
|
|
222
|
-
this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
223
|
-
this.memory = opts.memory ?? DEFAULT_MEMORY;
|
|
224
|
-
this.cpus = opts.cpus ?? DEFAULT_CPUS;
|
|
225
|
-
this.network = opts.network === true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async exec(opts: SandboxExecOptions): Promise<SandboxExecResult> {
|
|
229
|
-
if (this.closed) throw new SandboxError("sandbox is closed");
|
|
230
|
-
validateImage(opts.image, this.allowedImages);
|
|
231
|
-
const mounts = opts.mounts ?? [];
|
|
232
|
-
for (const m of mounts) validateMount(m, this.mountWhitelist);
|
|
233
|
-
|
|
234
|
-
const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
|
|
235
|
-
const cliArgs: string[] = [
|
|
236
|
-
"run",
|
|
237
|
-
"--rm",
|
|
238
|
-
"-i",
|
|
239
|
-
this.network ? "--network=bridge" : "--network=none",
|
|
240
|
-
`--memory=${this.memory}`,
|
|
241
|
-
`--cpus=${this.cpus}`,
|
|
242
|
-
"--read-only",
|
|
243
|
-
"--tmpfs",
|
|
244
|
-
"/tmp:rw,size=64m,mode=1777,exec",
|
|
245
|
-
"--security-opt",
|
|
246
|
-
"no-new-privileges",
|
|
247
|
-
];
|
|
248
|
-
for (const m of mounts) {
|
|
249
|
-
const ro = m.readonly !== false;
|
|
250
|
-
cliArgs.push("-v", `${m.src}:${m.dst}${ro ? ":ro" : ""}`);
|
|
251
|
-
}
|
|
252
|
-
if (opts.env !== undefined) {
|
|
253
|
-
for (const [k, v] of Object.entries(opts.env)) {
|
|
254
|
-
validateEnvKey(k);
|
|
255
|
-
cliArgs.push("-e", `${k}=${v}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
cliArgs.push(opts.image, ...opts.argv);
|
|
259
|
-
|
|
260
|
-
const t0 = performance.now();
|
|
261
|
-
const proc = Bun.spawn([this.cli, ...cliArgs], {
|
|
262
|
-
stdin: "pipe",
|
|
263
|
-
stdout: "pipe",
|
|
264
|
-
stderr: "pipe",
|
|
265
|
-
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
if (opts.stdin !== undefined) {
|
|
269
|
-
proc.stdin.write(opts.stdin);
|
|
270
|
-
}
|
|
271
|
-
proc.stdin.end();
|
|
272
|
-
|
|
273
|
-
let timedOut = false;
|
|
274
|
-
const timer = setTimeout(() => {
|
|
275
|
-
timedOut = true;
|
|
276
|
-
try {
|
|
277
|
-
proc.kill("SIGKILL");
|
|
278
|
-
} catch {
|
|
279
|
-
// already exited
|
|
280
|
-
}
|
|
281
|
-
}, timeoutMs);
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const stdoutP = collectStream(proc.stdout, opts.onStdoutChunk);
|
|
285
|
-
const stderrP = collectStream(proc.stderr, opts.onStderrChunk);
|
|
286
|
-
const exitCode = await proc.exited;
|
|
287
|
-
const stdout = await stdoutP;
|
|
288
|
-
const stderr = await stderrP;
|
|
289
|
-
return {
|
|
290
|
-
stdout,
|
|
291
|
-
stderr,
|
|
292
|
-
exitCode,
|
|
293
|
-
timedOut,
|
|
294
|
-
durationMs: performance.now() - t0,
|
|
295
|
-
};
|
|
296
|
-
} finally {
|
|
297
|
-
clearTimeout(timer);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async close(): Promise<void> {
|
|
302
|
-
this.closed = true;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
class NoopSandbox implements Sandbox {
|
|
307
|
-
readonly backend: SandboxBackend = "noop";
|
|
308
|
-
private readonly allowedImages: ReadonlySet<string>;
|
|
309
|
-
private readonly mountWhitelist: ReadonlyArray<string>;
|
|
310
|
-
private readonly defaultTimeoutMs: number;
|
|
311
|
-
private closed = false;
|
|
312
|
-
|
|
313
|
-
constructor(opts: SandboxOptions = {}) {
|
|
314
|
-
const ownAllowed = (opts.allowedImages ?? []).filter((s) => s.length > 0);
|
|
315
|
-
const envAllowed = readEnvAllowedImages();
|
|
316
|
-
const merged = new Set<string>(
|
|
317
|
-
ownAllowed.length > 0 || envAllowed.length > 0
|
|
318
|
-
? [...ownAllowed, ...envAllowed]
|
|
319
|
-
: DEFAULT_ALLOWED_IMAGES,
|
|
320
|
-
);
|
|
321
|
-
this.allowedImages = merged;
|
|
322
|
-
this.mountWhitelist = (opts.mountWhitelist ?? [process.cwd()]).map((p) => {
|
|
323
|
-
if (!p.startsWith("/")) {
|
|
324
|
-
throw new SandboxError(`mountWhitelist entry "${p}" must be absolute`);
|
|
325
|
-
}
|
|
326
|
-
return p;
|
|
327
|
-
});
|
|
328
|
-
this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async exec(opts: SandboxExecOptions): Promise<SandboxExecResult> {
|
|
332
|
-
if (this.closed) throw new SandboxError("sandbox is closed");
|
|
333
|
-
validateImage(opts.image, this.allowedImages);
|
|
334
|
-
const mounts = opts.mounts ?? [];
|
|
335
|
-
for (const m of mounts) validateMount(m, this.mountWhitelist);
|
|
336
|
-
if (opts.env !== undefined) {
|
|
337
|
-
for (const k of Object.keys(opts.env)) validateEnvKey(k);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// The noop backend runs the requested argv directly via Bun.spawn —
|
|
341
|
-
// there is NO isolation. It exists to make unit tests deterministic
|
|
342
|
-
// without a docker daemon. Production paths reject this backend at
|
|
343
|
-
// the permission layer (`requiresSandbox` denial).
|
|
344
|
-
const t0 = performance.now();
|
|
345
|
-
const argv = [...opts.argv];
|
|
346
|
-
const env =
|
|
347
|
-
opts.env !== undefined
|
|
348
|
-
? ({ ...process.env, ...opts.env } as Record<string, string>)
|
|
349
|
-
: undefined;
|
|
350
|
-
const proc = Bun.spawn(argv, {
|
|
351
|
-
stdin: "pipe",
|
|
352
|
-
stdout: "pipe",
|
|
353
|
-
stderr: "pipe",
|
|
354
|
-
...(env !== undefined ? { env } : {}),
|
|
355
|
-
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
if (opts.stdin !== undefined) {
|
|
359
|
-
proc.stdin.write(opts.stdin);
|
|
360
|
-
}
|
|
361
|
-
proc.stdin.end();
|
|
362
|
-
|
|
363
|
-
let timedOut = false;
|
|
364
|
-
const timer = setTimeout(() => {
|
|
365
|
-
timedOut = true;
|
|
366
|
-
try {
|
|
367
|
-
proc.kill("SIGKILL");
|
|
368
|
-
} catch {
|
|
369
|
-
// already exited
|
|
370
|
-
}
|
|
371
|
-
}, opts.timeoutMs ?? this.defaultTimeoutMs);
|
|
372
|
-
try {
|
|
373
|
-
const stdoutP = collectStream(proc.stdout, opts.onStdoutChunk);
|
|
374
|
-
const stderrP = collectStream(proc.stderr, opts.onStderrChunk);
|
|
375
|
-
const exitCode = await proc.exited;
|
|
376
|
-
const stdout = await stdoutP;
|
|
377
|
-
const stderr = await stderrP;
|
|
378
|
-
return {
|
|
379
|
-
stdout,
|
|
380
|
-
stderr,
|
|
381
|
-
exitCode,
|
|
382
|
-
timedOut,
|
|
383
|
-
durationMs: performance.now() - t0,
|
|
384
|
-
};
|
|
385
|
-
} finally {
|
|
386
|
-
clearTimeout(timer);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async close(): Promise<void> {
|
|
391
|
-
this.closed = true;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
async function collectStream(
|
|
396
|
-
stream: ReadableStream<Uint8Array> | undefined,
|
|
397
|
-
onChunk?: (chunk: string) => void,
|
|
398
|
-
): Promise<string> {
|
|
399
|
-
if (stream === undefined) return "";
|
|
400
|
-
// When no streaming consumer is attached, take the fast path through
|
|
401
|
-
// Response.text() which Bun has tuned for spawned-process pipes.
|
|
402
|
-
if (onChunk === undefined) {
|
|
403
|
-
return await new Response(stream).text();
|
|
404
|
-
}
|
|
405
|
-
const decoder = new TextDecoder();
|
|
406
|
-
const reader = stream.getReader();
|
|
407
|
-
let acc = "";
|
|
408
|
-
try {
|
|
409
|
-
while (true) {
|
|
410
|
-
const { value, done } = await reader.read();
|
|
411
|
-
if (done) break;
|
|
412
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
413
|
-
acc += chunk;
|
|
414
|
-
if (chunk.length > 0) onChunk(chunk);
|
|
415
|
-
}
|
|
416
|
-
const tail = decoder.decode();
|
|
417
|
-
if (tail.length > 0) {
|
|
418
|
-
acc += tail;
|
|
419
|
-
onChunk(tail);
|
|
420
|
-
}
|
|
421
|
-
} finally {
|
|
422
|
-
reader.releaseLock();
|
|
423
|
-
}
|
|
424
|
-
return acc;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export function createSandbox(opts: SandboxOptions = {}): Sandbox {
|
|
428
|
-
const backend = opts.backend ?? readEnvBackend() ?? "docker";
|
|
429
|
-
if (backend === "noop") return new NoopSandbox(opts);
|
|
430
|
-
return new DockerLikeSandbox(backend, opts);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export const SANDBOX_DEFAULT_ALLOWED_IMAGES = DEFAULT_ALLOWED_IMAGES;
|