@anvia/sandbox 0.6.0 → 1.0.0-rc.10
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 +56 -0
- package/dist/{chunk-FTNNCT6S.js → chunk-4D2GWLEH.js} +34 -51
- package/dist/chunk-4D2GWLEH.js.map +1 -0
- package/dist/cli.js +35 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +229 -251
- package/dist/index.js +1735 -1100
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-FTNNCT6S.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,31 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
SandboxDockerUnavailableError,
|
|
4
|
-
SandboxError,
|
|
5
|
-
SandboxFileSizeError,
|
|
6
|
-
SandboxPathError,
|
|
7
|
-
SandboxPortError,
|
|
8
|
-
SandboxProcessError,
|
|
9
|
-
SandboxSessionDestroyedError,
|
|
10
|
-
SandboxTimeoutError,
|
|
11
|
-
SandboxToolPolicyError,
|
|
2
|
+
DockerSandboxError,
|
|
12
3
|
assertDockerCli,
|
|
4
|
+
decodeUtf8,
|
|
13
5
|
runDockerCli
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
|
|
16
|
-
// src/capabilities.ts
|
|
17
|
-
function isSandboxPortSession(session) {
|
|
18
|
-
const candidate = session;
|
|
19
|
-
return Array.isArray(candidate.publishedPorts) && typeof candidate.waitForPort === "function";
|
|
20
|
-
}
|
|
21
|
-
function isSandboxProcessSession(session) {
|
|
22
|
-
const candidate = session;
|
|
23
|
-
return typeof candidate.startProcess === "function" && typeof candidate.listProcesses === "function" && typeof candidate.readProcessLogs === "function" && typeof candidate.stopProcess === "function";
|
|
24
|
-
}
|
|
6
|
+
} from "./chunk-4D2GWLEH.js";
|
|
25
7
|
|
|
26
8
|
// src/docker-sandbox.ts
|
|
27
9
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
28
|
-
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
10
|
+
import { lstat, mkdtemp, readFile, realpath, rm, writeFile } from "fs/promises";
|
|
29
11
|
import os from "os";
|
|
30
12
|
import path2 from "path";
|
|
31
13
|
|
|
@@ -37,21 +19,25 @@ import { randomUUID } from "crypto";
|
|
|
37
19
|
import path from "path";
|
|
38
20
|
function normalizeSandboxPath(input, options = {}) {
|
|
39
21
|
if (input.length === 0) {
|
|
40
|
-
throw new
|
|
22
|
+
throw new DockerSandboxError("Sandbox path cannot be empty.", "invalid_path");
|
|
41
23
|
}
|
|
42
24
|
if (input.includes("\0")) {
|
|
43
|
-
throw new
|
|
25
|
+
throw new DockerSandboxError("Sandbox path cannot contain null bytes.", "invalid_path");
|
|
44
26
|
}
|
|
45
27
|
const normalized = path.posix.normalize(input.replaceAll("\\", "/"));
|
|
46
28
|
if (path.posix.isAbsolute(normalized)) {
|
|
47
|
-
throw new
|
|
29
|
+
throw new DockerSandboxError(`Sandbox path must be relative: ${input}`, "invalid_path");
|
|
48
30
|
}
|
|
49
31
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
50
|
-
throw new
|
|
32
|
+
throw new DockerSandboxError(
|
|
33
|
+
`Sandbox path cannot leave the workspace: ${input}`,
|
|
34
|
+
"invalid_path"
|
|
35
|
+
);
|
|
51
36
|
}
|
|
52
37
|
if (normalized === "." && options.allowRoot !== true) {
|
|
53
|
-
throw new
|
|
54
|
-
"Sandbox path must refer to a file or directory inside the workspace."
|
|
38
|
+
throw new DockerSandboxError(
|
|
39
|
+
"Sandbox path must refer to a file or directory inside the workspace.",
|
|
40
|
+
"invalid_path"
|
|
55
41
|
);
|
|
56
42
|
}
|
|
57
43
|
return normalized;
|
|
@@ -69,19 +55,25 @@ function parentSandboxPath(relativePath) {
|
|
|
69
55
|
|
|
70
56
|
// src/docker-process.ts
|
|
71
57
|
var processMarkerPrefix = "ANVIA_PROCESS";
|
|
58
|
+
var processLauncher = [
|
|
59
|
+
'wrapper="$1"',
|
|
60
|
+
"shift",
|
|
61
|
+
// GNU setsid forks when it is already a process-group leader. `-w` keeps that launcher alive
|
|
62
|
+
// until the new session exits, so Docker does not detach from a still-running managed process.
|
|
63
|
+
"if command -v setsid >/dev/null 2>&1 && setsid -w true >/dev/null 2>&1; then",
|
|
64
|
+
' exec setsid -w sh -c "$wrapper" "$@"',
|
|
65
|
+
"fi",
|
|
66
|
+
'exec sh -c "$wrapper" "$@"'
|
|
67
|
+
].join("\n");
|
|
72
68
|
var processWrapper = [
|
|
73
69
|
'marker="$1"',
|
|
74
70
|
"shift",
|
|
75
|
-
"
|
|
76
|
-
'
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
" child=$!",
|
|
82
|
-
" group=$$",
|
|
83
|
-
"fi",
|
|
84
|
-
`printf '\\036%s:%s:%s:%s\\036' "$marker" "$$" "$child" "$group"`,
|
|
71
|
+
"group=$$",
|
|
72
|
+
`printf '\\036%s:%s:%s\\036' "$marker" "$$" "$group"`,
|
|
73
|
+
"IFS= read -r ready || exit 125",
|
|
74
|
+
'[ "$ready" = "start" ] || exit 125',
|
|
75
|
+
'"$@" &',
|
|
76
|
+
"child=$!",
|
|
85
77
|
"terminate() {",
|
|
86
78
|
' wait "$child" 2>/dev/null || true',
|
|
87
79
|
" exit 143",
|
|
@@ -94,13 +86,13 @@ var DockerProcessManager = class {
|
|
|
94
86
|
constructor(options) {
|
|
95
87
|
this.options = options;
|
|
96
88
|
if (!Number.isInteger(options.maxProcesses) || options.maxProcesses < 0) {
|
|
97
|
-
throw
|
|
89
|
+
throw processError("Sandbox maxProcesses must be a non-negative integer.");
|
|
98
90
|
}
|
|
99
91
|
if (!Number.isInteger(options.maxOutputBytes) || options.maxOutputBytes < 0) {
|
|
100
|
-
throw
|
|
92
|
+
throw processError("Sandbox maxOutputBytes must be a non-negative integer.");
|
|
101
93
|
}
|
|
102
94
|
if (!Number.isInteger(options.startupTimeoutMs) || options.startupTimeoutMs <= 0) {
|
|
103
|
-
throw
|
|
95
|
+
throw processError("Sandbox process startup timeout must be a positive integer.");
|
|
104
96
|
}
|
|
105
97
|
}
|
|
106
98
|
options;
|
|
@@ -109,10 +101,11 @@ var DockerProcessManager = class {
|
|
|
109
101
|
async start(options) {
|
|
110
102
|
this.assertActive();
|
|
111
103
|
assertStartOptions(options);
|
|
112
|
-
|
|
104
|
+
options.abortSignal?.throwIfAborted();
|
|
105
|
+
await this.pruneCompletedRecords(options.abortSignal);
|
|
113
106
|
const trackedCount = this.records.size;
|
|
114
107
|
if (trackedCount >= this.options.maxProcesses) {
|
|
115
|
-
throw
|
|
108
|
+
throw processError(
|
|
116
109
|
`Sandbox process limit reached (${trackedCount} >= ${this.options.maxProcesses}).`
|
|
117
110
|
);
|
|
118
111
|
}
|
|
@@ -122,7 +115,6 @@ var DockerProcessManager = class {
|
|
|
122
115
|
const child = spawn(this.options.dockerPath, dockerArgs, {
|
|
123
116
|
stdio: ["pipe", "pipe", "pipe"]
|
|
124
117
|
});
|
|
125
|
-
child.stdin.end();
|
|
126
118
|
let resolveStarted;
|
|
127
119
|
let rejectStarted;
|
|
128
120
|
const started = new Promise((resolve, reject) => {
|
|
@@ -143,7 +135,6 @@ var DockerProcessManager = class {
|
|
|
143
135
|
if (options.cwd !== void 0) info.cwd = options.cwd;
|
|
144
136
|
const record = {
|
|
145
137
|
info,
|
|
146
|
-
startedAtMs: Date.now(),
|
|
147
138
|
child,
|
|
148
139
|
stdout: new TailOutputCollector(this.options.maxOutputBytes),
|
|
149
140
|
stderr: new TailOutputCollector(this.options.maxOutputBytes),
|
|
@@ -152,7 +143,6 @@ var DockerProcessManager = class {
|
|
|
152
143
|
spawnFailed: false,
|
|
153
144
|
stopRequested: false,
|
|
154
145
|
startResolved: false,
|
|
155
|
-
exitNotified: false,
|
|
156
146
|
resolveStarted,
|
|
157
147
|
rejectStarted,
|
|
158
148
|
started,
|
|
@@ -162,10 +152,8 @@ var DockerProcessManager = class {
|
|
|
162
152
|
this.records.set(id, record);
|
|
163
153
|
this.observe(record);
|
|
164
154
|
try {
|
|
165
|
-
await this.waitForStart(record);
|
|
166
|
-
await this.options.onStart?.(copyProcessInfo(record.info));
|
|
155
|
+
await this.waitForStart(record, options.abortSignal);
|
|
167
156
|
record.startResolved = true;
|
|
168
|
-
if (record.info.status !== "running") this.notifyExit(record);
|
|
169
157
|
return copyProcessInfo(record.info);
|
|
170
158
|
} catch (error) {
|
|
171
159
|
if (await this.cleanupFailedStart(record)) this.records.delete(id);
|
|
@@ -176,28 +164,27 @@ var DockerProcessManager = class {
|
|
|
176
164
|
this.assertActive();
|
|
177
165
|
return [...this.records.values()].map((record) => copyProcessInfo(record.info));
|
|
178
166
|
}
|
|
179
|
-
logs(processId,
|
|
167
|
+
logs(processId, tailBytes) {
|
|
180
168
|
this.assertActive();
|
|
181
169
|
const record = this.getRecord(processId);
|
|
182
|
-
const stdout = record.stdout.snapshot(
|
|
183
|
-
const stderr = record.stderr.snapshot(
|
|
170
|
+
const stdout = record.stdout.snapshot(tailBytes);
|
|
171
|
+
const stderr = record.stderr.snapshot(tailBytes);
|
|
184
172
|
return {
|
|
185
|
-
stdout: stdout.
|
|
186
|
-
stderr: stderr.
|
|
173
|
+
stdout: stdout.bytes,
|
|
174
|
+
stderr: stderr.bytes,
|
|
187
175
|
stdoutTruncated: stdout.truncated,
|
|
188
176
|
stderrTruncated: stderr.truncated
|
|
189
177
|
};
|
|
190
178
|
}
|
|
191
|
-
async stop(processId,
|
|
179
|
+
async stop(processId, gracePeriodMs = 5e3, abortSignal) {
|
|
192
180
|
this.assertActive();
|
|
193
181
|
const record = this.getRecord(processId);
|
|
194
|
-
const gracePeriodMs = options.gracePeriodMs ?? 5e3;
|
|
195
182
|
if (!Number.isInteger(gracePeriodMs) || gracePeriodMs < 0) {
|
|
196
|
-
throw
|
|
183
|
+
throw processError("Process gracePeriodMs must be a non-negative integer.");
|
|
197
184
|
}
|
|
198
185
|
try {
|
|
199
|
-
if (!await this.terminateRecord(record, gracePeriodMs)) {
|
|
200
|
-
throw
|
|
186
|
+
if (!await this.terminateRecord(record, gracePeriodMs, abortSignal)) {
|
|
187
|
+
throw processError(`Sandbox process did not stop: ${processId}`);
|
|
201
188
|
}
|
|
202
189
|
} catch (error) {
|
|
203
190
|
if (record.info.status === "running") record.stopRequested = false;
|
|
@@ -205,17 +192,20 @@ var DockerProcessManager = class {
|
|
|
205
192
|
}
|
|
206
193
|
return copyProcessInfo(record.info);
|
|
207
194
|
}
|
|
208
|
-
async dispose() {
|
|
195
|
+
async dispose(abortSignal) {
|
|
209
196
|
if (this.disposed) return;
|
|
210
|
-
|
|
197
|
+
abortSignal?.throwIfAborted();
|
|
211
198
|
await Promise.all(
|
|
212
199
|
[...this.records.values()].map(async (record) => {
|
|
213
|
-
await this.terminateRecord(record, 1e3)
|
|
200
|
+
if (!await this.terminateRecord(record, 1e3, abortSignal)) {
|
|
201
|
+
throw processError(`Sandbox process did not stop: ${record.info.id}`);
|
|
202
|
+
}
|
|
214
203
|
})
|
|
215
204
|
);
|
|
205
|
+
this.disposed = true;
|
|
216
206
|
}
|
|
217
207
|
createExecArgs(options, marker) {
|
|
218
|
-
const args = ["exec", "-w", containerPath(this.options.workdir, options.cwd ?? ".")];
|
|
208
|
+
const args = ["exec", "-i", "-w", containerPath(this.options.workdir, options.cwd ?? ".")];
|
|
219
209
|
for (const [key, value] of Object.entries({ ...this.options.env, ...options.env })) {
|
|
220
210
|
args.push("-e", `${key}=${value}`);
|
|
221
211
|
}
|
|
@@ -223,6 +213,8 @@ var DockerProcessManager = class {
|
|
|
223
213
|
this.options.containerName,
|
|
224
214
|
"sh",
|
|
225
215
|
"-c",
|
|
216
|
+
processLauncher,
|
|
217
|
+
"anvia-process-launcher",
|
|
226
218
|
processWrapper,
|
|
227
219
|
"anvia-managed-process",
|
|
228
220
|
marker,
|
|
@@ -236,7 +228,9 @@ var DockerProcessManager = class {
|
|
|
236
228
|
record.child.stderr.on("data", (chunk) => record.stderr.accept(chunk));
|
|
237
229
|
record.child.on("error", (error) => {
|
|
238
230
|
record.spawnFailed = true;
|
|
239
|
-
const normalized = error.code === "ENOENT" ? new
|
|
231
|
+
const normalized = error.code === "ENOENT" ? new DockerSandboxError("Docker CLI was not found.", "docker_unavailable", void 0, {
|
|
232
|
+
cause: error
|
|
233
|
+
}) : error;
|
|
240
234
|
record.rejectStarted(normalized);
|
|
241
235
|
});
|
|
242
236
|
record.child.on("close", (code) => {
|
|
@@ -248,12 +242,9 @@ var DockerProcessManager = class {
|
|
|
248
242
|
record.info.exitCode = code ?? 1;
|
|
249
243
|
record.info.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
244
|
record.rejectStarted(
|
|
251
|
-
|
|
252
|
-
`Sandbox process exited before startup completed: ${record.info.id}`
|
|
253
|
-
)
|
|
245
|
+
processError(`Sandbox process exited before startup completed: ${record.info.id}`)
|
|
254
246
|
);
|
|
255
247
|
record.resolveClosed();
|
|
256
|
-
if (record.startResolved) this.notifyExit(record);
|
|
257
248
|
});
|
|
258
249
|
}
|
|
259
250
|
acceptStdout(record, chunk) {
|
|
@@ -280,14 +271,13 @@ var DockerProcessManager = class {
|
|
|
280
271
|
}
|
|
281
272
|
return;
|
|
282
273
|
}
|
|
283
|
-
const rawPids =
|
|
274
|
+
const rawPids = decodeUtf8(
|
|
275
|
+
record.markerBuffer.subarray(start + record.markerStart.length, end)
|
|
276
|
+
).split(":");
|
|
284
277
|
const supervisorPid = Number(rawPids[0]);
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
record.rejectStarted(
|
|
289
|
-
new SandboxProcessError("Sandbox process returned invalid process IDs.")
|
|
290
|
-
);
|
|
278
|
+
const processGroupId = Number(rawPids[1]);
|
|
279
|
+
if (!isProcessId(supervisorPid) || !isProcessId(processGroupId) || rawPids.length !== 2) {
|
|
280
|
+
record.rejectStarted(processError("Sandbox process returned invalid process IDs."));
|
|
291
281
|
return;
|
|
292
282
|
}
|
|
293
283
|
if (start > 0) record.stdout.accept(record.markerBuffer.subarray(0, start));
|
|
@@ -296,28 +286,38 @@ var DockerProcessManager = class {
|
|
|
296
286
|
}
|
|
297
287
|
record.markerBuffer = Buffer.alloc(0);
|
|
298
288
|
record.supervisorPid = supervisorPid;
|
|
299
|
-
record.childPid = childPid;
|
|
300
289
|
record.processGroupId = processGroupId;
|
|
290
|
+
record.child.stdin.write("start\n");
|
|
301
291
|
record.resolveStarted();
|
|
302
292
|
}
|
|
303
|
-
async waitForStart(record) {
|
|
293
|
+
async waitForStart(record, abortSignal) {
|
|
304
294
|
let timeout;
|
|
295
|
+
let abort;
|
|
305
296
|
const timeoutPromise = new Promise((_, reject) => {
|
|
306
297
|
timeout = setTimeout(() => {
|
|
307
|
-
reject(new
|
|
298
|
+
reject(new DockerSandboxError("Starting sandbox process timed out.", "timeout"));
|
|
308
299
|
}, this.options.startupTimeoutMs);
|
|
309
300
|
timeout.unref?.();
|
|
310
301
|
});
|
|
311
302
|
try {
|
|
312
|
-
|
|
303
|
+
const abortPromise = new Promise((_, reject) => {
|
|
304
|
+
if (abortSignal === void 0) return;
|
|
305
|
+
abort = () => reject(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
306
|
+
if (abortSignal.aborted) abort();
|
|
307
|
+
else abortSignal.addEventListener("abort", abort, { once: true });
|
|
308
|
+
});
|
|
309
|
+
await Promise.race([record.started, timeoutPromise, abortPromise]);
|
|
313
310
|
} finally {
|
|
314
311
|
if (timeout !== void 0) clearTimeout(timeout);
|
|
312
|
+
if (abort !== void 0) abortSignal?.removeEventListener("abort", abort);
|
|
315
313
|
}
|
|
316
314
|
}
|
|
317
315
|
async cleanupFailedStart(record) {
|
|
318
316
|
record.stopRequested = true;
|
|
319
317
|
if (record.processGroupId === void 0 && record.info.status === "running") {
|
|
320
|
-
|
|
318
|
+
record.child.stdin.destroy();
|
|
319
|
+
record.child.kill("SIGKILL");
|
|
320
|
+
await waitForPromise(record.closed, 1e3);
|
|
321
321
|
}
|
|
322
322
|
try {
|
|
323
323
|
return await this.terminateRecord(record, 1e3);
|
|
@@ -325,47 +325,49 @@ var DockerProcessManager = class {
|
|
|
325
325
|
return false;
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
|
-
async terminateRecord(record, gracePeriodMs) {
|
|
328
|
+
async terminateRecord(record, gracePeriodMs, abortSignal) {
|
|
329
|
+
abortSignal?.throwIfAborted();
|
|
329
330
|
record.stopRequested = true;
|
|
330
331
|
if (record.processGroupId === void 0) {
|
|
331
332
|
if (record.info.status === "running") {
|
|
332
|
-
await waitForPromise(record.closed, Math.min(gracePeriodMs, 250));
|
|
333
|
+
await waitForPromise(record.closed, Math.min(gracePeriodMs, 250), abortSignal);
|
|
333
334
|
}
|
|
334
335
|
if (record.processGroupId === void 0) {
|
|
335
|
-
return record.
|
|
336
|
+
return record.info.status !== "running";
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
|
-
if (!await this.isProcessGroupRunning(record)) {
|
|
339
|
-
return this.finishAfterGroupExit(record);
|
|
339
|
+
if (!await this.isProcessGroupRunning(record, abortSignal)) {
|
|
340
|
+
return this.finishAfterGroupExit(record, abortSignal);
|
|
340
341
|
}
|
|
341
|
-
await this.signal(record, "TERM");
|
|
342
|
-
if (await this.waitForRecordExit(record, gracePeriodMs)) return true;
|
|
343
|
-
await this.signal(record, "KILL");
|
|
344
|
-
if (await this.waitForRecordExit(record, 1e3)) return true;
|
|
342
|
+
await this.signal(record, "TERM", abortSignal);
|
|
343
|
+
if (await this.waitForRecordExit(record, gracePeriodMs, abortSignal)) return true;
|
|
344
|
+
await this.signal(record, "KILL", abortSignal);
|
|
345
|
+
if (await this.waitForRecordExit(record, 1e3, abortSignal)) return true;
|
|
345
346
|
return false;
|
|
346
347
|
}
|
|
347
|
-
async waitForRecordExit(record, timeoutMs) {
|
|
348
|
+
async waitForRecordExit(record, timeoutMs, abortSignal) {
|
|
348
349
|
const deadline = Date.now() + timeoutMs;
|
|
349
350
|
while (true) {
|
|
350
|
-
|
|
351
|
-
|
|
351
|
+
abortSignal?.throwIfAborted();
|
|
352
|
+
if (!await this.isProcessGroupRunning(record, abortSignal)) {
|
|
353
|
+
return this.finishAfterGroupExit(record, abortSignal);
|
|
352
354
|
}
|
|
353
355
|
const remainingMs = deadline - Date.now();
|
|
354
356
|
if (remainingMs <= 0) return false;
|
|
355
357
|
const intervalMs = Math.min(50, remainingMs);
|
|
356
358
|
if (record.info.status === "running") {
|
|
357
|
-
await waitForPromise(record.closed, intervalMs);
|
|
359
|
+
await waitForPromise(record.closed, intervalMs, abortSignal);
|
|
358
360
|
} else {
|
|
359
|
-
await waitForDelay(intervalMs);
|
|
361
|
+
await waitForDelay(intervalMs, abortSignal);
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
|
-
async finishAfterGroupExit(record) {
|
|
365
|
+
async finishAfterGroupExit(record, abortSignal) {
|
|
364
366
|
if (record.info.status !== "running") return true;
|
|
365
367
|
record.child.kill("SIGKILL");
|
|
366
|
-
return waitForPromise(record.closed, 1e3);
|
|
368
|
+
return waitForPromise(record.closed, 1e3, abortSignal);
|
|
367
369
|
}
|
|
368
|
-
async isProcessGroupRunning(record) {
|
|
370
|
+
async isProcessGroupRunning(record, abortSignal) {
|
|
369
371
|
if (record.processGroupId === void 0) return false;
|
|
370
372
|
const result = await runDockerCli(
|
|
371
373
|
[
|
|
@@ -375,17 +377,18 @@ var DockerProcessManager = class {
|
|
|
375
377
|
"-c",
|
|
376
378
|
'kill -0 "-$1" 2>/dev/null',
|
|
377
379
|
"anvia-process-group-check",
|
|
378
|
-
|
|
380
|
+
`${record.processGroupId}`
|
|
379
381
|
],
|
|
380
382
|
{
|
|
381
383
|
dockerPath: this.options.dockerPath,
|
|
382
384
|
timeoutMs: 5e3,
|
|
383
|
-
maxOutputBytes: this.options.maxOutputBytes
|
|
385
|
+
maxOutputBytes: this.options.maxOutputBytes,
|
|
386
|
+
signal: abortSignal
|
|
384
387
|
}
|
|
385
388
|
);
|
|
386
389
|
return result.exitCode === 0;
|
|
387
390
|
}
|
|
388
|
-
async signal(record,
|
|
391
|
+
async signal(record, dockerSignal, abortSignal) {
|
|
389
392
|
if (record.processGroupId === void 0) return;
|
|
390
393
|
const result = await runDockerCli(
|
|
391
394
|
[
|
|
@@ -395,18 +398,20 @@ var DockerProcessManager = class {
|
|
|
395
398
|
"-c",
|
|
396
399
|
'kill "-$2" "-$1" 2>/dev/null || ! kill -0 "-$1" 2>/dev/null',
|
|
397
400
|
"anvia-process-signal",
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
`${record.processGroupId}`,
|
|
402
|
+
dockerSignal
|
|
400
403
|
],
|
|
401
404
|
{
|
|
402
405
|
dockerPath: this.options.dockerPath,
|
|
403
406
|
timeoutMs: 5e3,
|
|
404
|
-
maxOutputBytes: this.options.maxOutputBytes
|
|
407
|
+
maxOutputBytes: this.options.maxOutputBytes,
|
|
408
|
+
signal: abortSignal
|
|
405
409
|
}
|
|
406
410
|
);
|
|
407
411
|
if (result.exitCode !== 0) {
|
|
408
|
-
throw new
|
|
412
|
+
throw new DockerSandboxError(
|
|
409
413
|
`Unable to stop sandbox process: ${record.info.id}`,
|
|
414
|
+
"docker_command_failed",
|
|
410
415
|
result
|
|
411
416
|
);
|
|
412
417
|
}
|
|
@@ -414,39 +419,22 @@ var DockerProcessManager = class {
|
|
|
414
419
|
getRecord(processId) {
|
|
415
420
|
const record = this.records.get(processId);
|
|
416
421
|
if (record === void 0) {
|
|
417
|
-
throw
|
|
422
|
+
throw processError(`Unknown sandbox process: ${processId}`);
|
|
418
423
|
}
|
|
419
424
|
return record;
|
|
420
425
|
}
|
|
421
|
-
async pruneCompletedRecords() {
|
|
426
|
+
async pruneCompletedRecords(abortSignal) {
|
|
422
427
|
for (const [id, record] of this.records) {
|
|
423
428
|
if (this.records.size < this.options.maxProcesses) return;
|
|
424
|
-
const cleanupConfirmed = record.processGroupId === void 0 ? record.spawnFailed : !await this.isProcessGroupRunning(record);
|
|
429
|
+
const cleanupConfirmed = record.processGroupId === void 0 ? record.spawnFailed : !await this.isProcessGroupRunning(record, abortSignal);
|
|
425
430
|
if (record.info.status !== "running" && cleanupConfirmed) {
|
|
426
431
|
this.records.delete(id);
|
|
427
432
|
}
|
|
428
433
|
}
|
|
429
434
|
}
|
|
430
|
-
logsUnsafe(record) {
|
|
431
|
-
const stdout = record.stdout.snapshot();
|
|
432
|
-
const stderr = record.stderr.snapshot();
|
|
433
|
-
return {
|
|
434
|
-
stdout: stdout.text,
|
|
435
|
-
stderr: stderr.text,
|
|
436
|
-
stdoutTruncated: stdout.truncated,
|
|
437
|
-
stderrTruncated: stderr.truncated
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
notifyExit(record) {
|
|
441
|
-
if (record.exitNotified) return;
|
|
442
|
-
record.exitNotified = true;
|
|
443
|
-
const durationMs = Date.now() - record.startedAtMs;
|
|
444
|
-
const notify = async () => this.options.onExit?.(copyProcessInfo(record.info), this.logsUnsafe(record), durationMs);
|
|
445
|
-
void notify().catch(() => void 0);
|
|
446
|
-
}
|
|
447
435
|
assertActive() {
|
|
448
436
|
if (this.disposed) {
|
|
449
|
-
throw
|
|
437
|
+
throw processError("Sandbox process manager has been disposed.");
|
|
450
438
|
}
|
|
451
439
|
}
|
|
452
440
|
};
|
|
@@ -488,56 +476,72 @@ var TailOutputCollector = class {
|
|
|
488
476
|
}
|
|
489
477
|
snapshot(tailBytes) {
|
|
490
478
|
if (tailBytes !== void 0 && (!Number.isInteger(tailBytes) || tailBytes < 0)) {
|
|
491
|
-
throw
|
|
479
|
+
throw processError("Process tailBytes must be a non-negative integer.");
|
|
492
480
|
}
|
|
493
481
|
const bytes = Buffer.concat(this.chunks, this.length);
|
|
494
482
|
const selected = tailBytes === 0 ? Buffer.alloc(0) : tailBytes === void 0 || bytes.length <= tailBytes ? bytes : bytes.subarray(bytes.length - tailBytes);
|
|
495
483
|
return {
|
|
496
|
-
|
|
484
|
+
bytes: new Uint8Array(selected.buffer, selected.byteOffset, selected.byteLength).slice(),
|
|
497
485
|
truncated: this.didTruncate || selected.length < bytes.length
|
|
498
486
|
};
|
|
499
487
|
}
|
|
500
488
|
};
|
|
489
|
+
function processError(message) {
|
|
490
|
+
return new DockerSandboxError(message, "process");
|
|
491
|
+
}
|
|
501
492
|
function assertStartOptions(options) {
|
|
502
493
|
if (options.command.trim().length === 0) {
|
|
503
|
-
throw
|
|
494
|
+
throw processError("Sandbox process command cannot be empty.");
|
|
504
495
|
}
|
|
505
496
|
}
|
|
506
497
|
function isProcessId(value) {
|
|
507
498
|
return Number.isSafeInteger(value) && value > 0;
|
|
508
499
|
}
|
|
509
500
|
function copyProcessInfo(info) {
|
|
510
|
-
|
|
501
|
+
let copy = {
|
|
511
502
|
id: info.id,
|
|
512
503
|
command: info.command,
|
|
513
504
|
args: [...info.args],
|
|
514
505
|
status: info.status,
|
|
515
506
|
startedAt: info.startedAt
|
|
516
507
|
};
|
|
517
|
-
if (info.cwd !== void 0) copy
|
|
518
|
-
if (info.exitCode !== void 0) copy
|
|
519
|
-
if (info.endedAt !== void 0) copy
|
|
508
|
+
if (info.cwd !== void 0) copy = { ...copy, cwd: info.cwd };
|
|
509
|
+
if (info.exitCode !== void 0) copy = { ...copy, exitCode: info.exitCode };
|
|
510
|
+
if (info.endedAt !== void 0) copy = { ...copy, endedAt: info.endedAt };
|
|
520
511
|
return copy;
|
|
521
512
|
}
|
|
522
|
-
async function waitForPromise(promise, timeoutMs) {
|
|
513
|
+
async function waitForPromise(promise, timeoutMs, abortSignal) {
|
|
514
|
+
abortSignal?.throwIfAborted();
|
|
523
515
|
let timeout;
|
|
516
|
+
let abort;
|
|
524
517
|
try {
|
|
525
518
|
return await Promise.race([
|
|
526
519
|
promise.then(() => true),
|
|
527
520
|
new Promise((resolve) => {
|
|
528
521
|
timeout = setTimeout(() => resolve(false), timeoutMs);
|
|
529
522
|
timeout.unref?.();
|
|
523
|
+
}),
|
|
524
|
+
new Promise((_, reject) => {
|
|
525
|
+
if (abortSignal === void 0) return;
|
|
526
|
+
abort = () => reject(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
527
|
+
if (abortSignal.aborted) abort();
|
|
528
|
+
else abortSignal.addEventListener("abort", abort, { once: true });
|
|
530
529
|
})
|
|
531
530
|
]);
|
|
532
531
|
} finally {
|
|
533
532
|
if (timeout !== void 0) clearTimeout(timeout);
|
|
533
|
+
if (abort !== void 0) abortSignal?.removeEventListener("abort", abort);
|
|
534
534
|
}
|
|
535
535
|
}
|
|
536
|
-
async function waitForDelay(timeoutMs) {
|
|
537
|
-
await
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
536
|
+
async function waitForDelay(timeoutMs, abortSignal) {
|
|
537
|
+
await waitForPromise(
|
|
538
|
+
new Promise((resolve) => {
|
|
539
|
+
const timeout = setTimeout(resolve, timeoutMs);
|
|
540
|
+
timeout.unref?.();
|
|
541
|
+
}),
|
|
542
|
+
timeoutMs,
|
|
543
|
+
abortSignal
|
|
544
|
+
);
|
|
541
545
|
}
|
|
542
546
|
|
|
543
547
|
// src/text-file.ts
|
|
@@ -586,13 +590,31 @@ function decodeCompleteUtf8(bytes) {
|
|
|
586
590
|
}
|
|
587
591
|
|
|
588
592
|
// src/docker-sandbox.ts
|
|
589
|
-
var
|
|
593
|
+
var schemaVersion = "1";
|
|
594
|
+
var labelPrefix = "anvia.sandbox.";
|
|
590
595
|
var defaultWorkdir = "/workspace";
|
|
591
|
-
var
|
|
596
|
+
var defaultCommandTimeoutMs = 3e4;
|
|
592
597
|
var defaultMaxOutputBytes = 1024 * 1024;
|
|
598
|
+
var defaultMaxFileBytes = 10 * 1024 * 1024;
|
|
593
599
|
var defaultMaxProcesses = 4;
|
|
594
600
|
var defaultTextFilePageLines = 500;
|
|
595
601
|
var defaultTextFilePageBytes = 64 * 1024;
|
|
602
|
+
var defaultPortWaitTimeoutMs = 3e4;
|
|
603
|
+
var defaultPortWaitIntervalMs = 100;
|
|
604
|
+
var idPattern = /^[a-z0-9](?:[a-z0-9_.-]{0,62})$/;
|
|
605
|
+
var envKeyPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
606
|
+
var labels = {
|
|
607
|
+
schema: `${labelPrefix}schema`,
|
|
608
|
+
id: `${labelPrefix}id`,
|
|
609
|
+
workdir: `${labelPrefix}workdir`,
|
|
610
|
+
workspaceType: `${labelPrefix}workspace.type`,
|
|
611
|
+
workspaceVolume: `${labelPrefix}workspace.volume`,
|
|
612
|
+
networkMode: `${labelPrefix}network.mode`,
|
|
613
|
+
commandTimeoutMs: `${labelPrefix}runtime.command-timeout-ms`,
|
|
614
|
+
maxOutputBytes: `${labelPrefix}runtime.max-output-bytes`,
|
|
615
|
+
maxFileBytes: `${labelPrefix}runtime.max-file-bytes`,
|
|
616
|
+
maxProcesses: `${labelPrefix}runtime.max-processes`
|
|
617
|
+
};
|
|
596
618
|
var portProbeScript = [
|
|
597
619
|
`port="$(printf '%04X' "$1")"`,
|
|
598
620
|
"for table in /proc/net/tcp /proc/net/tcp6; do",
|
|
@@ -607,1238 +629,1851 @@ var portProbeScript = [
|
|
|
607
629
|
"done",
|
|
608
630
|
"exit 1"
|
|
609
631
|
].join("\n");
|
|
610
|
-
var
|
|
611
|
-
'start="$1"',
|
|
612
|
-
'count="$2"',
|
|
613
|
-
'max_bytes="$3"',
|
|
614
|
-
'file="$4"',
|
|
615
|
-
'end="$((start + count))"',
|
|
616
|
-
'[ -f "$file" ] || { echo "Not a readable file: $file" >&2; exit 66; }',
|
|
617
|
-
'sed -n "$start,$end p;$end q" "$file" | head -c "$max_bytes"'
|
|
618
|
-
].join("\n");
|
|
619
|
-
var DockerSandbox = class _DockerSandbox {
|
|
620
|
-
provider = "docker";
|
|
621
|
-
image;
|
|
622
|
-
pull;
|
|
623
|
-
workdir;
|
|
624
|
-
workspace;
|
|
625
|
-
lifecycle;
|
|
626
|
-
network;
|
|
632
|
+
var DockerSandboxClient = class {
|
|
627
633
|
dockerPath;
|
|
628
|
-
labels;
|
|
629
|
-
limits;
|
|
630
|
-
security;
|
|
631
|
-
hooks;
|
|
632
|
-
user;
|
|
633
634
|
constructor(options = {}) {
|
|
634
|
-
|
|
635
|
-
this.pull = options.pull ?? "missing";
|
|
636
|
-
this.workdir = options.workdir ?? defaultWorkdir;
|
|
637
|
-
this.workspace = options.workspace ?? { mode: "ephemeral" };
|
|
638
|
-
const lifecycle = {
|
|
639
|
-
autoDestroy: options.lifecycle?.autoDestroy ?? true
|
|
640
|
-
};
|
|
641
|
-
if (options.lifecycle?.ttlMs !== void 0) lifecycle.ttlMs = options.lifecycle.ttlMs;
|
|
642
|
-
if (options.lifecycle?.idleTimeoutMs !== void 0) {
|
|
643
|
-
lifecycle.idleTimeoutMs = options.lifecycle.idleTimeoutMs;
|
|
644
|
-
}
|
|
645
|
-
this.lifecycle = lifecycle;
|
|
646
|
-
this.network = options.network ?? false;
|
|
635
|
+
if (!isRecord(options)) throw new TypeError("options must be an object.");
|
|
647
636
|
this.dockerPath = options.dockerPath ?? "docker";
|
|
648
|
-
this.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
637
|
+
assertNonEmptyString(this.dockerPath, "dockerPath");
|
|
638
|
+
}
|
|
639
|
+
async pullImage(options) {
|
|
640
|
+
if (!isRecord(options)) throw new TypeError("options must be an object.");
|
|
641
|
+
assertNonEmptyString(options.image, "image");
|
|
642
|
+
options.abortSignal?.throwIfAborted();
|
|
643
|
+
await assertDockerCli(["pull", options.image], this.cliOptions(options.abortSignal));
|
|
644
|
+
}
|
|
645
|
+
async createSandbox(options) {
|
|
646
|
+
validateCreateOptions(options);
|
|
647
|
+
options = snapshotCreateOptions(options);
|
|
648
|
+
options.abortSignal?.throwIfAborted();
|
|
649
|
+
const id = options.id ?? randomUUID2();
|
|
650
|
+
assertSandboxId(id);
|
|
651
|
+
const containerName = containerNameFor(id);
|
|
652
|
+
const workdir = options.workdir ?? defaultWorkdir;
|
|
653
|
+
const runtime = resolveRuntimeLimits(options.runtime);
|
|
654
|
+
const workspace = copyWorkspace(options.workspace);
|
|
655
|
+
const network = copyNetwork(options.network);
|
|
656
|
+
const env = copyStringRecord(options.env, "env");
|
|
657
|
+
const userLabels = copyStringRecord(options.labels, "labels");
|
|
658
|
+
const volumeName = workspace.type === "ephemeral" ? `${containerName}-workspace-${randomUUID2()}` : workspace.name;
|
|
659
|
+
const ownsVolume = workspace.type === "ephemeral";
|
|
660
|
+
await this.assertContainerDoesNotExist(containerName, options.abortSignal);
|
|
661
|
+
await this.assertImageExists(options.image, options.abortSignal);
|
|
662
|
+
if (workspace.type === "docker-volume") {
|
|
663
|
+
await this.assertVolumeExists(workspace.name, options.abortSignal);
|
|
664
|
+
}
|
|
665
|
+
let containerCreated = false;
|
|
666
|
+
let volumeCreated = false;
|
|
678
667
|
try {
|
|
668
|
+
if (workspace.type === "ephemeral") {
|
|
669
|
+
await assertDockerCli(
|
|
670
|
+
["volume", "create", "--label", `${labels.id}=${id}`, volumeName],
|
|
671
|
+
this.cliOptions(options.abortSignal)
|
|
672
|
+
);
|
|
673
|
+
volumeCreated = true;
|
|
674
|
+
}
|
|
679
675
|
await assertDockerCli(
|
|
680
|
-
|
|
676
|
+
createRunArgs({
|
|
677
|
+
id,
|
|
678
|
+
containerName,
|
|
679
|
+
image: options.image,
|
|
680
|
+
workdir,
|
|
681
|
+
workspace,
|
|
682
|
+
volumeName,
|
|
683
|
+
env,
|
|
684
|
+
user: options.user,
|
|
685
|
+
userLabels,
|
|
686
|
+
resources: options.resources,
|
|
687
|
+
runtime,
|
|
688
|
+
security: options.security,
|
|
689
|
+
network
|
|
690
|
+
}),
|
|
681
691
|
{
|
|
682
|
-
...this.cliOptions(),
|
|
683
|
-
timeoutMs:
|
|
692
|
+
...this.cliOptions(options.abortSignal),
|
|
693
|
+
timeoutMs: runtime.commandTimeoutMs
|
|
684
694
|
}
|
|
685
695
|
);
|
|
686
|
-
|
|
687
|
-
const
|
|
696
|
+
containerCreated = true;
|
|
697
|
+
const publishedPorts = await inspectPublishedPorts({
|
|
698
|
+
dockerPath: this.dockerPath,
|
|
699
|
+
containerName,
|
|
700
|
+
ports: network.mode === "bridge" ? [...network.ports ?? []] : [],
|
|
701
|
+
abortSignal: options.abortSignal
|
|
702
|
+
});
|
|
703
|
+
const sandbox = this.createHandle({
|
|
688
704
|
id,
|
|
689
705
|
containerName,
|
|
706
|
+
workdir,
|
|
707
|
+
workspace,
|
|
690
708
|
volumeName,
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
lifecycle: this.lifecycle,
|
|
695
|
-
removeVolumeOnDestroy,
|
|
696
|
-
env: options.manifest?.env ?? {},
|
|
697
|
-
hooks: this.hooks,
|
|
709
|
+
ownsVolume,
|
|
710
|
+
env,
|
|
711
|
+
runtime,
|
|
698
712
|
publishedPorts
|
|
699
713
|
});
|
|
700
|
-
await
|
|
701
|
-
|
|
702
|
-
return session;
|
|
714
|
+
await applyInitialContent(sandbox.runtime, options);
|
|
715
|
+
return sandbox;
|
|
703
716
|
} catch (error) {
|
|
704
|
-
|
|
717
|
+
const cleanupErrors = [];
|
|
718
|
+
if (containerCreated) {
|
|
719
|
+
try {
|
|
720
|
+
await removeContainer(this.dockerPath, containerName);
|
|
721
|
+
} catch (cleanupError) {
|
|
722
|
+
cleanupErrors.push(cleanupError);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (ownsVolume && volumeCreated) {
|
|
726
|
+
try {
|
|
727
|
+
await removeVolume(this.dockerPath, volumeName);
|
|
728
|
+
} catch (cleanupError) {
|
|
729
|
+
cleanupErrors.push(cleanupError);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (cleanupErrors.length > 0) {
|
|
733
|
+
throw new AggregateError([error, ...cleanupErrors], "Sandbox creation and rollback failed");
|
|
734
|
+
}
|
|
705
735
|
throw error;
|
|
706
736
|
}
|
|
707
737
|
}
|
|
708
|
-
async
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
738
|
+
async resumeSandbox(options) {
|
|
739
|
+
if (!isRecord(options)) throw new TypeError("options must be an object.");
|
|
740
|
+
assertSandboxId(options.id);
|
|
741
|
+
options.abortSignal?.throwIfAborted();
|
|
742
|
+
const containerName = containerNameFor(options.id);
|
|
743
|
+
const inspection = await inspectContainer(
|
|
744
|
+
this.dockerPath,
|
|
745
|
+
containerName,
|
|
746
|
+
options.abortSignal,
|
|
747
|
+
true
|
|
748
|
+
);
|
|
749
|
+
const configuration = configurationFromInspection(options.id, containerName, inspection);
|
|
750
|
+
if (inspection.State.Paused === true || inspection.State.Dead === true) {
|
|
751
|
+
throw new DockerSandboxError(
|
|
752
|
+
`Sandbox cannot be resumed from Docker state: ${inspection.State.Status ?? "unknown"}`,
|
|
753
|
+
"invalid_state"
|
|
754
|
+
);
|
|
712
755
|
}
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
if (inspect.exitCode !== 0) {
|
|
716
|
-
await assertDockerCli(["pull", this.image], this.cliOptions());
|
|
717
|
-
}
|
|
756
|
+
if (inspection.State.Running === true) {
|
|
757
|
+
await assertDockerCli(["stop", containerName], this.cliOptions(options.abortSignal));
|
|
718
758
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
"run",
|
|
723
|
-
"-d",
|
|
724
|
-
"--name",
|
|
759
|
+
await assertDockerCli(["start", containerName], this.cliOptions(options.abortSignal));
|
|
760
|
+
configuration.publishedPorts = await inspectPublishedPorts({
|
|
761
|
+
dockerPath: this.dockerPath,
|
|
725
762
|
containerName,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
for (const port of ports) {
|
|
747
|
-
args.push("--publish", `127.0.0.1::${port}/tcp`);
|
|
763
|
+
ports: configuredContainerPorts(inspection),
|
|
764
|
+
abortSignal: options.abortSignal
|
|
765
|
+
});
|
|
766
|
+
return this.createHandle(configuration);
|
|
767
|
+
}
|
|
768
|
+
createHandle(configuration) {
|
|
769
|
+
return new DockerSandboxHandle({
|
|
770
|
+
dockerPath: this.dockerPath,
|
|
771
|
+
configuration
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
async assertImageExists(image, abortSignal) {
|
|
775
|
+
const result = await runDockerCli(["image", "inspect", image], this.cliOptions(abortSignal));
|
|
776
|
+
if (result.exitCode === 0) return;
|
|
777
|
+
const message = safeDecode(result.stderr);
|
|
778
|
+
if (message.toLowerCase().includes("no such image")) {
|
|
779
|
+
throw new DockerSandboxError(
|
|
780
|
+
`Docker image is not available locally: ${image}`,
|
|
781
|
+
"image_not_found"
|
|
782
|
+
);
|
|
748
783
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
784
|
+
throw new DockerSandboxError(
|
|
785
|
+
"Unable to inspect Docker image.",
|
|
786
|
+
"docker_command_failed",
|
|
787
|
+
result
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
async assertContainerDoesNotExist(containerName, abortSignal) {
|
|
791
|
+
const result = await runDockerCli(["container", "inspect", containerName], {
|
|
792
|
+
...this.cliOptions(abortSignal),
|
|
793
|
+
maxOutputBytes: defaultMaxOutputBytes
|
|
794
|
+
});
|
|
795
|
+
if (result.exitCode === 0) {
|
|
796
|
+
throw new DockerSandboxError(
|
|
797
|
+
`A Docker container already exists for sandbox: ${containerName}`,
|
|
798
|
+
"invalid_state"
|
|
799
|
+
);
|
|
753
800
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
"
|
|
757
|
-
"
|
|
758
|
-
|
|
801
|
+
if (safeDecode(result.stderr).toLowerCase().includes("no such")) return;
|
|
802
|
+
throw new DockerSandboxError(
|
|
803
|
+
"Unable to check whether the Docker sandbox already exists.",
|
|
804
|
+
"docker_command_failed",
|
|
805
|
+
result
|
|
759
806
|
);
|
|
760
|
-
return args;
|
|
761
807
|
}
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
|
|
808
|
+
async assertVolumeExists(name, abortSignal) {
|
|
809
|
+
const result = await runDockerCli(["volume", "inspect", name], this.cliOptions(abortSignal));
|
|
810
|
+
if (result.exitCode === 0) return;
|
|
811
|
+
const message = safeDecode(result.stderr);
|
|
812
|
+
if (message.toLowerCase().includes("no such volume")) {
|
|
813
|
+
throw new DockerSandboxError(`Docker volume does not exist: ${name}`, "volume_not_found");
|
|
814
|
+
}
|
|
815
|
+
throw new DockerSandboxError(
|
|
816
|
+
"Unable to inspect Docker volume.",
|
|
817
|
+
"docker_command_failed",
|
|
818
|
+
result
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
cliOptions(abortSignal) {
|
|
822
|
+
return { dockerPath: this.dockerPath, signal: abortSignal };
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
var DockerSandboxHandle = class {
|
|
826
|
+
id;
|
|
827
|
+
runtime;
|
|
828
|
+
currentState = "running";
|
|
829
|
+
dockerPath;
|
|
830
|
+
configuration;
|
|
831
|
+
runtimeImpl;
|
|
832
|
+
stopPromise;
|
|
833
|
+
destroyPromise;
|
|
834
|
+
constructor(options) {
|
|
835
|
+
this.dockerPath = options.dockerPath;
|
|
836
|
+
this.configuration = options.configuration;
|
|
837
|
+
this.id = options.configuration.id;
|
|
838
|
+
this.runtimeImpl = new DockerSandboxRuntimeImpl({
|
|
839
|
+
configuration: options.configuration,
|
|
840
|
+
dockerPath: options.dockerPath,
|
|
841
|
+
state: () => this.currentState
|
|
842
|
+
});
|
|
843
|
+
this.runtime = Object.freeze(this.runtimeImpl.publicRuntime());
|
|
844
|
+
}
|
|
845
|
+
get state() {
|
|
846
|
+
return this.currentState;
|
|
847
|
+
}
|
|
848
|
+
inspector(options) {
|
|
849
|
+
if (!isRecord(options)) throw new TypeError("options must be an object.");
|
|
850
|
+
if (options.files !== true && options.ports !== true && options.processes !== true) {
|
|
851
|
+
throw new TypeError("Sandbox inspector must enable at least one capability.");
|
|
852
|
+
}
|
|
853
|
+
let inspector = {
|
|
854
|
+
id: this.id,
|
|
855
|
+
provider: "docker",
|
|
856
|
+
workdir: this.configuration.workdir
|
|
857
|
+
};
|
|
858
|
+
if (options.files === true) {
|
|
859
|
+
inspector = {
|
|
860
|
+
...inspector,
|
|
861
|
+
listFiles: this.runtime.listFiles.bind(this.runtime),
|
|
862
|
+
readFile: this.runtime.readFile.bind(this.runtime)
|
|
863
|
+
};
|
|
767
864
|
}
|
|
768
|
-
if (
|
|
769
|
-
|
|
865
|
+
if (options.ports === true) {
|
|
866
|
+
inspector = { ...inspector, publishedPorts: this.runtime.publishedPorts };
|
|
770
867
|
}
|
|
868
|
+
if (options.processes === true) {
|
|
869
|
+
inspector = {
|
|
870
|
+
...inspector,
|
|
871
|
+
listProcesses: this.runtime.listProcesses.bind(this.runtime),
|
|
872
|
+
readProcessLogs: this.runtime.readProcessLogs.bind(this.runtime)
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
return Object.freeze(inspector);
|
|
771
876
|
}
|
|
772
|
-
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
if (
|
|
776
|
-
throw
|
|
777
|
-
|
|
778
|
-
|
|
877
|
+
async stop(options = {}) {
|
|
878
|
+
if (!isRecord(options)) throw new TypeError("options must be an object.");
|
|
879
|
+
if (this.currentState === "stopped") return;
|
|
880
|
+
if (this.currentState === "destroyed" || this.currentState === "destroying") {
|
|
881
|
+
throw invalidState(this.id, this.currentState);
|
|
882
|
+
}
|
|
883
|
+
if (this.stopPromise !== void 0) return this.stopPromise;
|
|
884
|
+
this.stopPromise = this.performStop(options.abortSignal);
|
|
885
|
+
try {
|
|
886
|
+
await this.stopPromise;
|
|
887
|
+
} finally {
|
|
888
|
+
this.stopPromise = void 0;
|
|
779
889
|
}
|
|
780
890
|
}
|
|
781
|
-
async
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
["container", "inspect", "--format", "{{json .NetworkSettings.Ports}}", containerName],
|
|
785
|
-
this.cliOptions()
|
|
786
|
-
);
|
|
787
|
-
let mappings;
|
|
891
|
+
async performStop(abortSignal) {
|
|
892
|
+
abortSignal?.throwIfAborted();
|
|
893
|
+
this.currentState = "stopping";
|
|
788
894
|
try {
|
|
789
|
-
|
|
895
|
+
await this.runtimeImpl.closeActiveOperations("Sandbox is stopping.");
|
|
896
|
+
await this.runtimeImpl.disposeProcesses(abortSignal);
|
|
897
|
+
await assertDockerCli(["stop", this.configuration.containerName], {
|
|
898
|
+
dockerPath: this.dockerPath,
|
|
899
|
+
signal: abortSignal
|
|
900
|
+
});
|
|
901
|
+
this.currentState = "stopped";
|
|
790
902
|
} catch (error) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (!isRecord(mappings)) {
|
|
794
|
-
throw new SandboxPortError("Docker returned invalid published port metadata.");
|
|
903
|
+
this.currentState = "error";
|
|
904
|
+
throw error;
|
|
795
905
|
}
|
|
796
|
-
return ports.map((containerPort) => {
|
|
797
|
-
const entries = mappings[`${containerPort}/tcp`];
|
|
798
|
-
if (!Array.isArray(entries)) {
|
|
799
|
-
throw new SandboxPortError(`Docker did not publish sandbox port ${containerPort}/tcp.`);
|
|
800
|
-
}
|
|
801
|
-
const entry = entries.find(
|
|
802
|
-
(candidate) => isRecord(candidate) && candidate.HostIp === "127.0.0.1" && typeof candidate.HostPort === "string"
|
|
803
|
-
);
|
|
804
|
-
if (!isRecord(entry) || typeof entry.HostPort !== "string") {
|
|
805
|
-
throw new SandboxPortError(
|
|
806
|
-
`Docker did not bind sandbox port ${containerPort}/tcp to 127.0.0.1.`
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
const hostPort = Number(entry.HostPort);
|
|
810
|
-
if (!isValidPort(hostPort)) {
|
|
811
|
-
throw new SandboxPortError(
|
|
812
|
-
`Docker returned an invalid host port for ${containerPort}/tcp.`
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
return { containerPort, host: "127.0.0.1", hostPort, protocol: "tcp" };
|
|
816
|
-
});
|
|
817
906
|
}
|
|
818
|
-
|
|
819
|
-
if (this.
|
|
820
|
-
|
|
907
|
+
async destroy() {
|
|
908
|
+
if (this.currentState === "destroyed") return;
|
|
909
|
+
if (this.destroyPromise !== void 0) return this.destroyPromise;
|
|
910
|
+
this.destroyPromise = this.performDestroy();
|
|
911
|
+
try {
|
|
912
|
+
await this.destroyPromise;
|
|
913
|
+
} finally {
|
|
914
|
+
this.destroyPromise = void 0;
|
|
821
915
|
}
|
|
822
|
-
|
|
823
|
-
|
|
916
|
+
}
|
|
917
|
+
async performDestroy() {
|
|
918
|
+
if (this.stopPromise !== void 0) await this.stopPromise.catch(() => void 0);
|
|
919
|
+
this.currentState = "destroying";
|
|
920
|
+
const failures = [];
|
|
921
|
+
try {
|
|
922
|
+
await this.runtimeImpl.closeActiveOperations("Sandbox is being destroyed.");
|
|
923
|
+
} catch (error) {
|
|
924
|
+
failures.push(error);
|
|
824
925
|
}
|
|
825
|
-
|
|
826
|
-
|
|
926
|
+
try {
|
|
927
|
+
await this.runtimeImpl.disposeProcesses();
|
|
928
|
+
} catch (error) {
|
|
929
|
+
failures.push(error);
|
|
827
930
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
931
|
+
try {
|
|
932
|
+
await removeContainer(this.dockerPath, this.configuration.containerName);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
failures.push(error);
|
|
832
935
|
}
|
|
833
|
-
if (this.
|
|
834
|
-
|
|
936
|
+
if (this.configuration.ownsVolume) {
|
|
937
|
+
try {
|
|
938
|
+
await removeVolume(this.dockerPath, this.configuration.volumeName);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
failures.push(error);
|
|
941
|
+
}
|
|
835
942
|
}
|
|
836
|
-
|
|
837
|
-
|
|
943
|
+
if (failures.length > 0) {
|
|
944
|
+
this.currentState = "error";
|
|
945
|
+
throw new AggregateError(failures, `Unable to destroy sandbox: ${this.id}`);
|
|
838
946
|
}
|
|
947
|
+
this.currentState = "destroyed";
|
|
839
948
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
dockerPath: this.dockerPath,
|
|
843
|
-
maxOutputBytes: this.limits.maxOutputBytes ?? defaultMaxOutputBytes
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
async cleanup(containerName, volumeName) {
|
|
847
|
-
await runDockerCli(["rm", "-f", containerName], this.cliOptions()).catch(() => void 0);
|
|
848
|
-
if (volumeName !== void 0) {
|
|
849
|
-
await runDockerCli(["volume", "rm", "-f", volumeName], this.cliOptions()).catch(
|
|
850
|
-
() => void 0
|
|
851
|
-
);
|
|
852
|
-
}
|
|
949
|
+
async [Symbol.asyncDispose]() {
|
|
950
|
+
await this.destroy();
|
|
853
951
|
}
|
|
854
952
|
};
|
|
855
|
-
var
|
|
856
|
-
|
|
857
|
-
id;
|
|
858
|
-
workdir;
|
|
859
|
-
publishedPorts;
|
|
860
|
-
containerName;
|
|
861
|
-
volumeName;
|
|
953
|
+
var DockerSandboxRuntimeImpl = class {
|
|
954
|
+
configuration;
|
|
862
955
|
dockerPath;
|
|
863
|
-
|
|
864
|
-
lifecycle;
|
|
865
|
-
removeVolumeOnDestroy;
|
|
866
|
-
env;
|
|
867
|
-
hooks;
|
|
956
|
+
state;
|
|
868
957
|
processManager;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
destroyed = false;
|
|
958
|
+
activeControllers = /* @__PURE__ */ new Set();
|
|
959
|
+
activeOperations = /* @__PURE__ */ new Set();
|
|
960
|
+
processDisposed = false;
|
|
873
961
|
constructor(options) {
|
|
874
|
-
this.
|
|
875
|
-
this.containerName = options.containerName;
|
|
876
|
-
this.volumeName = options.volumeName;
|
|
877
|
-
this.workdir = options.workdir;
|
|
962
|
+
this.configuration = options.configuration;
|
|
878
963
|
this.dockerPath = options.dockerPath;
|
|
879
|
-
this.
|
|
880
|
-
this.lifecycle = options.lifecycle;
|
|
881
|
-
this.removeVolumeOnDestroy = options.removeVolumeOnDestroy;
|
|
882
|
-
this.env = options.env;
|
|
883
|
-
this.hooks = options.hooks;
|
|
884
|
-
this.publishedPorts = options.publishedPorts;
|
|
964
|
+
this.state = options.state;
|
|
885
965
|
this.processManager = new DockerProcessManager({
|
|
886
|
-
containerName:
|
|
887
|
-
dockerPath:
|
|
888
|
-
workdir:
|
|
889
|
-
env:
|
|
890
|
-
maxOutputBytes:
|
|
891
|
-
maxProcesses:
|
|
892
|
-
startupTimeoutMs:
|
|
893
|
-
onStart: async (process) => {
|
|
894
|
-
const event = {
|
|
895
|
-
...this.event(),
|
|
896
|
-
command: process.command,
|
|
897
|
-
args: process.args
|
|
898
|
-
};
|
|
899
|
-
if (process.cwd !== void 0) event.cwd = process.cwd;
|
|
900
|
-
await this.hooks.onExecStart?.(event);
|
|
901
|
-
},
|
|
902
|
-
onExit: async (process, logs, durationMs) => {
|
|
903
|
-
const event = {
|
|
904
|
-
...this.event(),
|
|
905
|
-
command: process.command,
|
|
906
|
-
args: process.args,
|
|
907
|
-
result: {
|
|
908
|
-
stdout: logs.stdout,
|
|
909
|
-
stderr: logs.stderr,
|
|
910
|
-
exitCode: process.exitCode ?? 1,
|
|
911
|
-
durationMs,
|
|
912
|
-
timedOut: false,
|
|
913
|
-
aborted: process.status === "stopped",
|
|
914
|
-
stdoutTruncated: logs.stdoutTruncated,
|
|
915
|
-
stderrTruncated: logs.stderrTruncated
|
|
916
|
-
}
|
|
917
|
-
};
|
|
918
|
-
if (process.cwd !== void 0) event.cwd = process.cwd;
|
|
919
|
-
await this.hooks.onExecEnd?.(event);
|
|
920
|
-
}
|
|
966
|
+
containerName: options.configuration.containerName,
|
|
967
|
+
dockerPath: options.dockerPath,
|
|
968
|
+
workdir: options.configuration.workdir,
|
|
969
|
+
env: options.configuration.env,
|
|
970
|
+
maxOutputBytes: options.configuration.runtime.maxOutputBytes,
|
|
971
|
+
maxProcesses: options.configuration.runtime.maxProcesses,
|
|
972
|
+
startupTimeoutMs: options.configuration.runtime.commandTimeoutMs
|
|
921
973
|
});
|
|
922
|
-
this.startLifecycleTimers();
|
|
923
974
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
975
|
+
publicRuntime() {
|
|
976
|
+
return {
|
|
977
|
+
id: this.configuration.id,
|
|
978
|
+
provider: "docker",
|
|
979
|
+
workdir: this.configuration.workdir,
|
|
980
|
+
publishedPorts: Object.freeze(
|
|
981
|
+
this.configuration.publishedPorts.map((port) => Object.freeze({ ...port }))
|
|
982
|
+
),
|
|
983
|
+
exec: (options) => this.exec(options),
|
|
984
|
+
execStream: (options) => this.execStream(options),
|
|
985
|
+
readFile: (options) => this.readFile(options),
|
|
986
|
+
readTextFile: (options) => this.readTextFile(options),
|
|
987
|
+
readTextFilePage: (options) => this.readTextFilePage(options),
|
|
988
|
+
writeFile: (options) => this.writeFile(options),
|
|
989
|
+
writeTextFile: (options) => this.writeTextFile(options),
|
|
990
|
+
listFiles: (options) => this.listFiles(options),
|
|
991
|
+
startProcess: (options) => this.startProcess(options),
|
|
992
|
+
listProcesses: (options) => this.listProcesses(options),
|
|
993
|
+
readProcessLogs: (options) => this.readProcessLogs(options),
|
|
994
|
+
stopProcess: (options) => this.stopProcess(options),
|
|
995
|
+
waitForPort: (options) => this.waitForPort(options)
|
|
996
|
+
};
|
|
933
997
|
}
|
|
934
998
|
async exec(options) {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
...this.event(),
|
|
946
|
-
command: options.command,
|
|
947
|
-
args: options.args ?? [],
|
|
948
|
-
result: normalizedResult
|
|
949
|
-
};
|
|
950
|
-
if (options.cwd !== void 0) endEvent.cwd = options.cwd;
|
|
951
|
-
await this.hooks.onExecEnd?.(endEvent);
|
|
952
|
-
return normalizedResult;
|
|
999
|
+
validateExecOptions(options);
|
|
1000
|
+
return this.runOperation(options.abortSignal, async (abortSignal) => {
|
|
1001
|
+
const result = await runDockerCli(this.execArgs(options), {
|
|
1002
|
+
dockerPath: this.dockerPath,
|
|
1003
|
+
timeoutMs: options.timeoutMs ?? this.configuration.runtime.commandTimeoutMs,
|
|
1004
|
+
maxOutputBytes: this.configuration.runtime.maxOutputBytes,
|
|
1005
|
+
input: options.input,
|
|
1006
|
+
signal: abortSignal
|
|
1007
|
+
});
|
|
1008
|
+
return toExecResult(result);
|
|
953
1009
|
});
|
|
954
1010
|
}
|
|
955
1011
|
async *execStream(options) {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1012
|
+
validateExecOptions(options);
|
|
1013
|
+
this.assertRunning();
|
|
1014
|
+
const streamAbort = new AbortController();
|
|
1015
|
+
const combined = combineSignals(options.abortSignal, streamAbort.signal);
|
|
1016
|
+
const queue = [];
|
|
1017
|
+
let queuedBytes = 0;
|
|
1018
|
+
let wake;
|
|
1019
|
+
let complete = false;
|
|
1020
|
+
let failure;
|
|
960
1021
|
const push = (event) => {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1022
|
+
if (event.type !== "result") {
|
|
1023
|
+
queuedBytes += event.data.byteLength;
|
|
1024
|
+
if (queuedBytes > this.configuration.runtime.maxOutputBytes) {
|
|
1025
|
+
streamAbort.abort(
|
|
1026
|
+
new DockerSandboxError("Sandbox stream consumer is too slow.", "docker_command_failed")
|
|
1027
|
+
);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
queue.push(event);
|
|
1032
|
+
wake?.();
|
|
1033
|
+
wake = void 0;
|
|
964
1034
|
};
|
|
965
|
-
const
|
|
966
|
-
|
|
1035
|
+
const run = this.runOperation(combined, async (abortSignal) => {
|
|
1036
|
+
const result = await runDockerCli(this.execArgs(options), {
|
|
1037
|
+
dockerPath: this.dockerPath,
|
|
1038
|
+
timeoutMs: options.timeoutMs ?? this.configuration.runtime.commandTimeoutMs,
|
|
1039
|
+
maxOutputBytes: this.configuration.runtime.maxOutputBytes,
|
|
1040
|
+
input: options.input,
|
|
1041
|
+
signal: abortSignal,
|
|
1042
|
+
onStdout: (data) => push({ type: "stdout", data: data.slice() }),
|
|
1043
|
+
onStderr: (data) => push({ type: "stderr", data: data.slice() })
|
|
1044
|
+
});
|
|
1045
|
+
push({ type: "result", result: toExecResult(result) });
|
|
967
1046
|
});
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
push({ type: "stdout", chunk, text: Buffer.from(chunk).toString("utf8") });
|
|
1047
|
+
void run.then(
|
|
1048
|
+
() => {
|
|
1049
|
+
complete = true;
|
|
1050
|
+
wake?.();
|
|
973
1051
|
},
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1052
|
+
(error) => {
|
|
1053
|
+
failure = error;
|
|
1054
|
+
complete = true;
|
|
1055
|
+
wake?.();
|
|
977
1056
|
}
|
|
978
|
-
|
|
979
|
-
push({ type: "exit", result });
|
|
980
|
-
}).catch((caught) => {
|
|
981
|
-
error = caught;
|
|
982
|
-
}).finally(() => {
|
|
983
|
-
done = true;
|
|
984
|
-
notify?.();
|
|
985
|
-
notify = void 0;
|
|
986
|
-
});
|
|
1057
|
+
);
|
|
987
1058
|
try {
|
|
988
|
-
while (!
|
|
989
|
-
const event =
|
|
1059
|
+
while (!complete || queue.length > 0) {
|
|
1060
|
+
const event = queue.shift();
|
|
990
1061
|
if (event !== void 0) {
|
|
1062
|
+
if (event.type !== "result") queuedBytes -= event.data.byteLength;
|
|
991
1063
|
yield event;
|
|
992
1064
|
continue;
|
|
993
1065
|
}
|
|
994
|
-
await
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
throw error;
|
|
1066
|
+
await new Promise((resolve) => {
|
|
1067
|
+
wake = resolve;
|
|
1068
|
+
});
|
|
998
1069
|
}
|
|
1070
|
+
if (failure !== void 0) throw failure;
|
|
999
1071
|
} finally {
|
|
1000
|
-
|
|
1072
|
+
if (!complete) streamAbort.abort(new DOMException("Stream closed", "AbortError"));
|
|
1073
|
+
await run.catch(() => void 0);
|
|
1001
1074
|
}
|
|
1002
1075
|
}
|
|
1003
|
-
async
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
return this.runOperation(async () => this.processManager.list());
|
|
1008
|
-
}
|
|
1009
|
-
async readProcessLogs(processId, options) {
|
|
1010
|
-
return this.runOperation(async () => this.processManager.logs(processId, options));
|
|
1011
|
-
}
|
|
1012
|
-
async stopProcess(processId, options) {
|
|
1013
|
-
return this.runOperation(async () => this.processManager.stop(processId, options));
|
|
1014
|
-
}
|
|
1015
|
-
async waitForPort(containerPort, options = {}) {
|
|
1016
|
-
return this.runOperation(async () => {
|
|
1017
|
-
const publishedPort = this.publishedPorts.find(
|
|
1018
|
-
(candidate) => candidate.containerPort === containerPort
|
|
1019
|
-
);
|
|
1020
|
-
if (publishedPort === void 0) {
|
|
1021
|
-
throw new SandboxPortError(`Sandbox port is not published: ${containerPort}/tcp`);
|
|
1022
|
-
}
|
|
1023
|
-
const timeoutMs = options.timeoutMs ?? this.limits.timeoutMs ?? defaultTimeoutMs;
|
|
1024
|
-
const intervalMs = options.intervalMs ?? 250;
|
|
1025
|
-
assertWaitOptions(timeoutMs, intervalMs);
|
|
1026
|
-
const deadline = Date.now() + timeoutMs;
|
|
1027
|
-
while (true) {
|
|
1028
|
-
this.assertActive();
|
|
1029
|
-
if (options.signal?.aborted === true) throw abortReason(options.signal);
|
|
1030
|
-
const remainingMs = deadline - Date.now();
|
|
1031
|
-
if (remainingMs <= 0) {
|
|
1032
|
-
throw new SandboxTimeoutError(`Waiting for sandbox port ${containerPort}/tcp timed out.`);
|
|
1033
|
-
}
|
|
1034
|
-
const probeTimeoutMs = Math.min(1e3, remainingMs);
|
|
1035
|
-
if (await this.isPortListening(containerPort, probeTimeoutMs)) {
|
|
1036
|
-
return publishedPort;
|
|
1037
|
-
}
|
|
1038
|
-
const remainingAfterProbeMs = deadline - Date.now();
|
|
1039
|
-
if (remainingAfterProbeMs <= 0) {
|
|
1040
|
-
throw new SandboxTimeoutError(`Waiting for sandbox port ${containerPort}/tcp timed out.`);
|
|
1041
|
-
}
|
|
1042
|
-
await waitWithSignal(Math.min(intervalMs, remainingAfterProbeMs), options.signal);
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
async readFile(filePath) {
|
|
1047
|
-
return this.runOperation(async () => {
|
|
1048
|
-
const normalized = normalizeSandboxPath(filePath);
|
|
1076
|
+
async readFile(options) {
|
|
1077
|
+
assertReadOptions(options);
|
|
1078
|
+
return this.runOperation(options.abortSignal, async (abortSignal) => {
|
|
1079
|
+
const normalized = normalizeSandboxPath(options.path);
|
|
1049
1080
|
const tempDir = await mkdtemp(path2.join(os.tmpdir(), "anvia-sandbox-read-"));
|
|
1050
1081
|
const target = path2.join(tempDir, path2.basename(normalized));
|
|
1051
1082
|
try {
|
|
1052
1083
|
await assertDockerCli(
|
|
1053
|
-
[
|
|
1054
|
-
|
|
1084
|
+
[
|
|
1085
|
+
"cp",
|
|
1086
|
+
`${this.configuration.containerName}:${containerPath(this.configuration.workdir, normalized)}`,
|
|
1087
|
+
target
|
|
1088
|
+
],
|
|
1089
|
+
{ dockerPath: this.dockerPath, signal: abortSignal }
|
|
1055
1090
|
);
|
|
1056
|
-
|
|
1057
|
-
const bytes = await readFile(target);
|
|
1058
|
-
this.assertFileSize(bytes.byteLength,
|
|
1059
|
-
return bytes;
|
|
1091
|
+
await assertCopiedRegularFile(tempDir, target, normalized);
|
|
1092
|
+
const bytes = await readFile(target, { signal: abortSignal });
|
|
1093
|
+
this.assertFileSize(bytes.byteLength, normalized);
|
|
1094
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength).slice();
|
|
1060
1095
|
} finally {
|
|
1061
1096
|
await rm(tempDir, { recursive: true, force: true });
|
|
1062
1097
|
}
|
|
1063
1098
|
});
|
|
1064
1099
|
}
|
|
1065
|
-
async readTextFile(
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
`Unable to read sandbox text file: ${filePath}`,
|
|
1100
|
-
result
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
|
-
return createTextFilePage(result.stdout, {
|
|
1104
|
-
startLine,
|
|
1105
|
-
lineCount,
|
|
1106
|
-
maxBytes,
|
|
1107
|
-
contentStartLine: startLine
|
|
1108
|
-
});
|
|
1109
|
-
});
|
|
1100
|
+
async readTextFile(options) {
|
|
1101
|
+
return decodeUtf8(await this.readFile(options));
|
|
1102
|
+
}
|
|
1103
|
+
async readTextFilePage(options) {
|
|
1104
|
+
assertReadOptions(options);
|
|
1105
|
+
const startLine = options.startLine ?? 1;
|
|
1106
|
+
const lineCount = options.lineCount ?? defaultTextFilePageLines;
|
|
1107
|
+
const maxBytes = options.maxBytes ?? defaultTextFilePageBytes;
|
|
1108
|
+
assertPositiveSafeInteger(startLine, "startLine");
|
|
1109
|
+
assertPositiveSafeInteger(lineCount, "lineCount");
|
|
1110
|
+
assertPositiveSafeInteger(maxBytes, "maxBytes");
|
|
1111
|
+
let readOptions = {
|
|
1112
|
+
path: options.path
|
|
1113
|
+
};
|
|
1114
|
+
if (options.abortSignal !== void 0) {
|
|
1115
|
+
readOptions = { ...readOptions, abortSignal: options.abortSignal };
|
|
1116
|
+
}
|
|
1117
|
+
const text = await this.readTextFile(readOptions);
|
|
1118
|
+
return createTextFilePage(text, { startLine, lineCount, maxBytes });
|
|
1119
|
+
}
|
|
1120
|
+
async writeFile(options) {
|
|
1121
|
+
assertOptionsObject(options);
|
|
1122
|
+
if (!(options.data instanceof Uint8Array)) throw new TypeError("data must be a Uint8Array.");
|
|
1123
|
+
const data = options.data.slice();
|
|
1124
|
+
await this.writeBytes(options.path, data, options.abortSignal);
|
|
1125
|
+
}
|
|
1126
|
+
async writeTextFile(options) {
|
|
1127
|
+
assertOptionsObject(options);
|
|
1128
|
+
if (typeof options.text !== "string") throw new TypeError("text must be a string.");
|
|
1129
|
+
await this.writeBytes(
|
|
1130
|
+
options.path,
|
|
1131
|
+
new TextEncoder().encode(options.text),
|
|
1132
|
+
options.abortSignal
|
|
1133
|
+
);
|
|
1110
1134
|
}
|
|
1111
|
-
async
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
await this.mkdir(parentSandboxPath(normalized));
|
|
1135
|
+
async writeBytes(filePath, data, abortSignal) {
|
|
1136
|
+
const normalized = normalizeSandboxPath(filePath);
|
|
1137
|
+
this.assertFileSize(data.byteLength, normalized);
|
|
1138
|
+
await this.runOperation(abortSignal, async (effectiveSignal) => {
|
|
1139
|
+
await this.mkdir(parentSandboxPath(normalized), effectiveSignal);
|
|
1117
1140
|
const tempDir = await mkdtemp(path2.join(os.tmpdir(), "anvia-sandbox-write-"));
|
|
1118
1141
|
const source = path2.join(tempDir, path2.basename(normalized));
|
|
1119
1142
|
try {
|
|
1120
|
-
await writeFile(source, data);
|
|
1143
|
+
await writeFile(source, data, { signal: effectiveSignal });
|
|
1121
1144
|
await assertDockerCli(
|
|
1122
|
-
[
|
|
1123
|
-
|
|
1145
|
+
[
|
|
1146
|
+
"cp",
|
|
1147
|
+
source,
|
|
1148
|
+
`${this.configuration.containerName}:${containerPath(this.configuration.workdir, normalized)}`
|
|
1149
|
+
],
|
|
1150
|
+
{ dockerPath: this.dockerPath, signal: effectiveSignal }
|
|
1124
1151
|
);
|
|
1125
|
-
await this.hooks.onFileWrite?.({ ...this.event(), path: normalized, size });
|
|
1126
1152
|
} finally {
|
|
1127
1153
|
await rm(tempDir, { recursive: true, force: true });
|
|
1128
1154
|
}
|
|
1129
1155
|
});
|
|
1130
1156
|
}
|
|
1131
|
-
async
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
return this.runOperation(async () => {
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1157
|
+
async listFiles(options = {}) {
|
|
1158
|
+
assertOptionsObject(options);
|
|
1159
|
+
const requestedPath = options.path ?? ".";
|
|
1160
|
+
const normalized = normalizeSandboxPath(requestedPath, { allowRoot: true });
|
|
1161
|
+
return this.runOperation(options.abortSignal, async (abortSignal) => {
|
|
1162
|
+
const result = await this.execCommand(
|
|
1163
|
+
{
|
|
1164
|
+
command: "find",
|
|
1165
|
+
args: [
|
|
1166
|
+
containerPath(this.configuration.workdir, normalized),
|
|
1167
|
+
"-mindepth",
|
|
1168
|
+
"1",
|
|
1169
|
+
"-maxdepth",
|
|
1170
|
+
"1",
|
|
1171
|
+
"-printf",
|
|
1172
|
+
"%p %y %s\n"
|
|
1173
|
+
]
|
|
1174
|
+
},
|
|
1175
|
+
abortSignal
|
|
1176
|
+
);
|
|
1177
|
+
if (result.status !== "exited" || result.exitCode !== 0) {
|
|
1178
|
+
throw new DockerSandboxError(
|
|
1179
|
+
"Unable to list sandbox files.",
|
|
1180
|
+
"docker_command_failed",
|
|
1181
|
+
result
|
|
1182
|
+
);
|
|
1147
1183
|
}
|
|
1148
|
-
|
|
1184
|
+
const output = decodeUtf8(result.stdout);
|
|
1185
|
+
return output.split("\n").filter((line) => line.length > 0).map((line) => parseFindEntry(line, this.configuration.workdir));
|
|
1149
1186
|
});
|
|
1150
1187
|
}
|
|
1151
|
-
async
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1188
|
+
async startProcess(options) {
|
|
1189
|
+
validateProcessStartOptions(options);
|
|
1190
|
+
return this.runOperation(
|
|
1191
|
+
options.abortSignal,
|
|
1192
|
+
async (abortSignal) => this.processManager.start({ ...options, abortSignal })
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
async listProcesses(options = {}) {
|
|
1196
|
+
assertOptionsObject(options);
|
|
1197
|
+
return this.runOperation(options.abortSignal, async () => this.processManager.list());
|
|
1198
|
+
}
|
|
1199
|
+
async readProcessLogs(options) {
|
|
1200
|
+
assertOptionsObject(options);
|
|
1201
|
+
assertNonEmptyString(options.processId, "processId");
|
|
1202
|
+
if (options.tailBytes !== void 0)
|
|
1203
|
+
assertNonNegativeSafeInteger(options.tailBytes, "tailBytes");
|
|
1204
|
+
return this.runOperation(
|
|
1205
|
+
options.abortSignal,
|
|
1206
|
+
async () => this.processManager.logs(options.processId, options.tailBytes)
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
async stopProcess(options) {
|
|
1210
|
+
assertOptionsObject(options);
|
|
1211
|
+
assertNonEmptyString(options.processId, "processId");
|
|
1212
|
+
if (options.gracePeriodMs !== void 0) {
|
|
1213
|
+
assertNonNegativeSafeInteger(options.gracePeriodMs, "gracePeriodMs");
|
|
1163
1214
|
}
|
|
1164
|
-
|
|
1215
|
+
return this.runOperation(
|
|
1216
|
+
options.abortSignal,
|
|
1217
|
+
async (abortSignal) => this.processManager.stop(options.processId, options.gracePeriodMs, abortSignal)
|
|
1218
|
+
);
|
|
1165
1219
|
}
|
|
1166
|
-
async
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
throw new
|
|
1174
|
-
`
|
|
1175
|
-
|
|
1220
|
+
async waitForPort(options) {
|
|
1221
|
+
assertOptionsObject(options);
|
|
1222
|
+
assertPort(options.containerPort);
|
|
1223
|
+
const publishedPort = this.configuration.publishedPorts.find(
|
|
1224
|
+
(candidate) => candidate.containerPort === options.containerPort
|
|
1225
|
+
);
|
|
1226
|
+
if (publishedPort === void 0) {
|
|
1227
|
+
throw new DockerSandboxError(
|
|
1228
|
+
`Sandbox port is not published: ${options.containerPort}/tcp`,
|
|
1229
|
+
"port"
|
|
1176
1230
|
);
|
|
1177
1231
|
}
|
|
1232
|
+
const timeoutMs = options.timeoutMs ?? defaultPortWaitTimeoutMs;
|
|
1233
|
+
const intervalMs = options.intervalMs ?? defaultPortWaitIntervalMs;
|
|
1234
|
+
assertPositiveSafeInteger(timeoutMs, "timeoutMs");
|
|
1235
|
+
assertPositiveSafeInteger(intervalMs, "intervalMs");
|
|
1236
|
+
return this.runOperation(options.abortSignal, async (abortSignal) => {
|
|
1237
|
+
const deadline = Date.now() + timeoutMs;
|
|
1238
|
+
while (true) {
|
|
1239
|
+
const result = await this.execCommand(
|
|
1240
|
+
{
|
|
1241
|
+
command: "sh",
|
|
1242
|
+
args: ["-c", portProbeScript, "anvia-port-probe", `${options.containerPort}`],
|
|
1243
|
+
timeoutMs: Math.min(5e3, timeoutMs)
|
|
1244
|
+
},
|
|
1245
|
+
abortSignal
|
|
1246
|
+
);
|
|
1247
|
+
if (result.status === "exited" && result.exitCode === 0) return { ...publishedPort };
|
|
1248
|
+
if (Date.now() >= deadline) {
|
|
1249
|
+
throw new DockerSandboxError(
|
|
1250
|
+
`Waiting for sandbox port timed out: ${options.containerPort}`,
|
|
1251
|
+
"timeout"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
await waitWithSignal(Math.min(intervalMs, deadline - Date.now()), abortSignal);
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1178
1257
|
}
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1258
|
+
async closeActiveOperations(message) {
|
|
1259
|
+
const reason = new DockerSandboxError(message, "invalid_state");
|
|
1260
|
+
for (const controller of this.activeControllers) controller.abort(reason);
|
|
1261
|
+
await Promise.allSettled([...this.activeOperations]);
|
|
1262
|
+
}
|
|
1263
|
+
async disposeProcesses(abortSignal) {
|
|
1264
|
+
if (this.processDisposed) return;
|
|
1265
|
+
await this.processManager.dispose(abortSignal);
|
|
1266
|
+
this.processDisposed = true;
|
|
1267
|
+
}
|
|
1268
|
+
async runOperation(abortSignal, operation) {
|
|
1269
|
+
this.assertRunning();
|
|
1270
|
+
abortSignal?.throwIfAborted();
|
|
1271
|
+
const controller = new AbortController();
|
|
1272
|
+
const effectiveSignal = combineSignals(abortSignal, controller.signal);
|
|
1273
|
+
const execution = operation(effectiveSignal);
|
|
1274
|
+
this.activeControllers.add(controller);
|
|
1275
|
+
this.activeOperations.add(execution);
|
|
1276
|
+
try {
|
|
1277
|
+
return await execution;
|
|
1278
|
+
} finally {
|
|
1279
|
+
this.activeControllers.delete(controller);
|
|
1280
|
+
this.activeOperations.delete(execution);
|
|
1190
1281
|
}
|
|
1191
|
-
return entry;
|
|
1192
1282
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1283
|
+
assertRunning() {
|
|
1284
|
+
const current = this.state();
|
|
1285
|
+
if (current !== "running") throw invalidState(this.configuration.id, current);
|
|
1286
|
+
}
|
|
1287
|
+
execArgs(options) {
|
|
1288
|
+
const args = ["exec"];
|
|
1289
|
+
if (options.input !== void 0) args.push("-i");
|
|
1290
|
+
args.push("-w", containerPath(this.configuration.workdir, options.cwd ?? "."));
|
|
1291
|
+
for (const [key, value] of Object.entries(options.env ?? {}))
|
|
1292
|
+
args.push("-e", `${key}=${value}`);
|
|
1293
|
+
args.push(this.configuration.containerName, options.command, ...options.args ?? []);
|
|
1294
|
+
return args;
|
|
1295
|
+
}
|
|
1296
|
+
async execCommand(options, abortSignal) {
|
|
1297
|
+
const result = await runDockerCli(this.execArgs(options), {
|
|
1195
1298
|
dockerPath: this.dockerPath,
|
|
1196
|
-
|
|
1197
|
-
|
|
1299
|
+
timeoutMs: options.timeoutMs ?? this.configuration.runtime.commandTimeoutMs,
|
|
1300
|
+
maxOutputBytes: this.configuration.runtime.maxOutputBytes,
|
|
1301
|
+
input: options.input,
|
|
1302
|
+
signal: abortSignal
|
|
1303
|
+
});
|
|
1304
|
+
return toExecResult(result);
|
|
1198
1305
|
}
|
|
1199
|
-
async
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
"sh",
|
|
1205
|
-
"-c",
|
|
1206
|
-
portProbeScript,
|
|
1207
|
-
"anvia-port-probe",
|
|
1208
|
-
String(containerPort)
|
|
1209
|
-
],
|
|
1210
|
-
{
|
|
1211
|
-
...this.cliOptions(),
|
|
1212
|
-
timeoutMs: Math.max(1, timeoutMs)
|
|
1213
|
-
}
|
|
1306
|
+
async mkdir(directory, abortSignal) {
|
|
1307
|
+
if (directory === ".") return;
|
|
1308
|
+
const result = await this.execCommand(
|
|
1309
|
+
{ command: "mkdir", args: ["-p", containerPath(this.configuration.workdir, directory)] },
|
|
1310
|
+
abortSignal
|
|
1214
1311
|
);
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
stderr: "",
|
|
1222
|
-
exitCode: 1
|
|
1223
|
-
});
|
|
1312
|
+
if (result.status !== "exited" || result.exitCode !== 0) {
|
|
1313
|
+
throw new DockerSandboxError(
|
|
1314
|
+
"Unable to create sandbox directory.",
|
|
1315
|
+
"docker_command_failed",
|
|
1316
|
+
result
|
|
1317
|
+
);
|
|
1224
1318
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1319
|
+
}
|
|
1320
|
+
assertFileSize(size, filePath) {
|
|
1321
|
+
if (size > this.configuration.runtime.maxFileBytes) {
|
|
1322
|
+
throw new DockerSandboxError(
|
|
1323
|
+
`Sandbox file exceeds maxFileBytes (${size} > ${this.configuration.runtime.maxFileBytes}): ${filePath}`,
|
|
1324
|
+
"file_too_large"
|
|
1325
|
+
);
|
|
1228
1326
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
async function applyInitialContent(runtime, options) {
|
|
1330
|
+
for (const directory of options.directories ?? []) {
|
|
1331
|
+
const marker = path2.posix.join(normalizeSandboxPath(directory), ".anvia-keep");
|
|
1332
|
+
const abort = options.abortSignal === void 0 ? {} : { abortSignal: options.abortSignal };
|
|
1333
|
+
await runtime.writeFile({ path: marker, data: new Uint8Array(), ...abort });
|
|
1334
|
+
const removed = await runtime.exec({ command: "rm", args: [marker], ...abort });
|
|
1335
|
+
if (removed.status !== "exited" || removed.exitCode !== 0) {
|
|
1336
|
+
throw new DockerSandboxError(
|
|
1337
|
+
`Unable to create initial sandbox directory: ${directory}`,
|
|
1338
|
+
"docker_command_failed",
|
|
1339
|
+
removed
|
|
1340
|
+
);
|
|
1233
1341
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1342
|
+
}
|
|
1343
|
+
for (const [filePath, content] of Object.entries(options.files ?? {})) {
|
|
1344
|
+
if (typeof content === "string") {
|
|
1345
|
+
let writeOptions = {
|
|
1346
|
+
path: filePath,
|
|
1347
|
+
text: content
|
|
1348
|
+
};
|
|
1349
|
+
if (options.abortSignal !== void 0) {
|
|
1350
|
+
writeOptions = { ...writeOptions, abortSignal: options.abortSignal };
|
|
1351
|
+
}
|
|
1352
|
+
await runtime.writeTextFile(writeOptions);
|
|
1353
|
+
} else {
|
|
1354
|
+
let writeOptions = {
|
|
1355
|
+
path: filePath,
|
|
1356
|
+
data: content
|
|
1249
1357
|
};
|
|
1358
|
+
if (options.abortSignal !== void 0) {
|
|
1359
|
+
writeOptions = { ...writeOptions, abortSignal: options.abortSignal };
|
|
1360
|
+
}
|
|
1361
|
+
await runtime.writeFile(writeOptions);
|
|
1250
1362
|
}
|
|
1251
|
-
return result;
|
|
1252
1363
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1364
|
+
}
|
|
1365
|
+
function createRunArgs(options) {
|
|
1366
|
+
const runtimeLabels = {
|
|
1367
|
+
[labels.schema]: schemaVersion,
|
|
1368
|
+
[labels.id]: options.id,
|
|
1369
|
+
[labels.workdir]: options.workdir,
|
|
1370
|
+
[labels.workspaceType]: options.workspace.type,
|
|
1371
|
+
[labels.workspaceVolume]: options.volumeName,
|
|
1372
|
+
[labels.networkMode]: options.network.mode,
|
|
1373
|
+
[labels.commandTimeoutMs]: `${options.runtime.commandTimeoutMs}`,
|
|
1374
|
+
[labels.maxOutputBytes]: `${options.runtime.maxOutputBytes}`,
|
|
1375
|
+
[labels.maxFileBytes]: `${options.runtime.maxFileBytes}`,
|
|
1376
|
+
[labels.maxProcesses]: `${options.runtime.maxProcesses}`
|
|
1377
|
+
};
|
|
1378
|
+
const args = [
|
|
1379
|
+
"run",
|
|
1380
|
+
"-d",
|
|
1381
|
+
"--name",
|
|
1382
|
+
options.containerName,
|
|
1383
|
+
"--mount",
|
|
1384
|
+
`type=volume,src=${options.volumeName},dst=${options.workdir}`,
|
|
1385
|
+
"-w",
|
|
1386
|
+
options.workdir
|
|
1387
|
+
];
|
|
1388
|
+
for (const [key, value] of Object.entries({ ...options.userLabels, ...runtimeLabels })) {
|
|
1389
|
+
args.push("--label", `${key}=${value}`);
|
|
1390
|
+
}
|
|
1391
|
+
for (const [key, value] of Object.entries(options.env)) args.push("--env", `${key}=${value}`);
|
|
1392
|
+
if (options.user !== void 0) args.push("--user", options.user);
|
|
1393
|
+
if (options.network.mode === "none") {
|
|
1394
|
+
args.push("--network", "none");
|
|
1395
|
+
} else {
|
|
1396
|
+
for (const port of options.network.ports ?? [])
|
|
1397
|
+
args.push("--publish", `127.0.0.1::${port}/tcp`);
|
|
1257
1398
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
};
|
|
1399
|
+
if (options.resources?.memoryMb !== void 0)
|
|
1400
|
+
args.push("--memory", `${options.resources.memoryMb}m`);
|
|
1401
|
+
if (options.resources?.cpus !== void 0) args.push("--cpus", `${options.resources.cpus}`);
|
|
1402
|
+
if (options.resources?.pidsLimit !== void 0) {
|
|
1403
|
+
args.push("--pids-limit", `${options.resources.pidsLimit}`);
|
|
1264
1404
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
this.activeOperations += 1;
|
|
1268
|
-
this.clearIdleTimer();
|
|
1269
|
-
try {
|
|
1270
|
-
return await operation();
|
|
1271
|
-
} finally {
|
|
1272
|
-
this.activeOperations -= 1;
|
|
1273
|
-
this.scheduleIdleTimer();
|
|
1274
|
-
}
|
|
1405
|
+
if (options.resources?.sharedMemoryMb !== void 0) {
|
|
1406
|
+
args.push("--shm-size", `${options.resources.sharedMemoryMb}m`);
|
|
1275
1407
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1408
|
+
if (options.security?.readonlyRootfs === true) args.push("--read-only");
|
|
1409
|
+
if (options.security?.noNewPrivileges ?? true)
|
|
1410
|
+
args.push("--security-opt", "no-new-privileges:true");
|
|
1411
|
+
if (options.security?.seccompProfile !== void 0) {
|
|
1412
|
+
args.push("--security-opt", `seccomp=${options.security.seccompProfile.path}`);
|
|
1413
|
+
}
|
|
1414
|
+
for (const capability of options.security?.dropCapabilities ?? ["ALL"]) {
|
|
1415
|
+
args.push("--cap-drop", capability);
|
|
1416
|
+
}
|
|
1417
|
+
for (const capability of options.security?.addCapabilities ?? []) {
|
|
1418
|
+
args.push("--cap-add", capability);
|
|
1419
|
+
}
|
|
1420
|
+
args.push(
|
|
1421
|
+
options.image,
|
|
1422
|
+
"sh",
|
|
1423
|
+
"-c",
|
|
1424
|
+
"trap 'exit 0' TERM INT; while :; do sleep 3600 & wait $!; done"
|
|
1425
|
+
);
|
|
1426
|
+
return args;
|
|
1427
|
+
}
|
|
1428
|
+
function validateCreateOptions(options) {
|
|
1429
|
+
if (options === null || typeof options !== "object")
|
|
1430
|
+
throw new TypeError("options must be an object.");
|
|
1431
|
+
assertNonEmptyString(options.image, "image");
|
|
1432
|
+
if (options.id !== void 0) assertSandboxId(options.id);
|
|
1433
|
+
const workdir = options.workdir ?? defaultWorkdir;
|
|
1434
|
+
if (!path2.posix.isAbsolute(workdir) || workdir.includes("\0")) {
|
|
1435
|
+
throw new TypeError("workdir must be an absolute POSIX path.");
|
|
1436
|
+
}
|
|
1437
|
+
if (!isRecord(options.workspace)) throw new TypeError("workspace must be an object.");
|
|
1438
|
+
if (options.workspace.type === "docker-volume") assertDockerVolumeName(options.workspace.name);
|
|
1439
|
+
if (options.workspace.type !== "ephemeral" && options.workspace.type !== "docker-volume") {
|
|
1440
|
+
throw new TypeError("workspace must use type ephemeral or docker-volume.");
|
|
1441
|
+
}
|
|
1442
|
+
if (options.workspace.type === "ephemeral" && "name" in options.workspace) {
|
|
1443
|
+
throw new TypeError("An ephemeral workspace cannot specify a Docker volume name.");
|
|
1444
|
+
}
|
|
1445
|
+
if (!isRecord(options.network)) throw new TypeError("network must be an object.");
|
|
1446
|
+
if (options.network.mode !== "none" && options.network.mode !== "bridge") {
|
|
1447
|
+
throw new TypeError("network must use mode none or bridge.");
|
|
1448
|
+
}
|
|
1449
|
+
if (options.network.mode === "none" && "ports" in options.network) {
|
|
1450
|
+
throw new TypeError("Network mode none cannot publish ports.");
|
|
1451
|
+
}
|
|
1452
|
+
if (options.network.mode === "bridge") {
|
|
1453
|
+
if (options.network.ports !== void 0 && !Array.isArray(options.network.ports)) {
|
|
1454
|
+
throw new TypeError("network.ports must be an array.");
|
|
1455
|
+
}
|
|
1456
|
+
validatePorts(options.network.ports ?? []);
|
|
1457
|
+
}
|
|
1458
|
+
validateRuntimeLimits(options.runtime);
|
|
1459
|
+
validateResources(options.resources);
|
|
1460
|
+
validateSecurity(options.security);
|
|
1461
|
+
copyStringRecord(options.env, "env");
|
|
1462
|
+
const userLabels = copyStringRecord(options.labels, "labels");
|
|
1463
|
+
for (const key of Object.keys(userLabels)) {
|
|
1464
|
+
if (key.startsWith(labelPrefix)) throw new TypeError(`Docker label is reserved: ${key}`);
|
|
1465
|
+
}
|
|
1466
|
+
if (options.user !== void 0) assertNonEmptyString(options.user, "user");
|
|
1467
|
+
if (options.directories !== void 0 && !Array.isArray(options.directories)) {
|
|
1468
|
+
throw new TypeError("directories must be an array.");
|
|
1469
|
+
}
|
|
1470
|
+
for (const directory of options.directories ?? []) {
|
|
1471
|
+
if (typeof directory !== "string") throw new TypeError("directories must contain strings.");
|
|
1472
|
+
normalizeSandboxPath(directory);
|
|
1473
|
+
}
|
|
1474
|
+
if (options.files !== void 0 && !isRecord(options.files)) {
|
|
1475
|
+
throw new TypeError("files must be an object.");
|
|
1476
|
+
}
|
|
1477
|
+
for (const [filePath, content] of Object.entries(options.files ?? {})) {
|
|
1478
|
+
normalizeSandboxPath(filePath);
|
|
1479
|
+
if (typeof content !== "string" && !(content instanceof Uint8Array)) {
|
|
1480
|
+
throw new TypeError(`Initial file must be a string or Uint8Array: ${filePath}`);
|
|
1481
|
+
}
|
|
1482
|
+
const byteLength = typeof content === "string" ? new TextEncoder().encode(content).byteLength : content.byteLength;
|
|
1483
|
+
const maxFileBytes = options.runtime?.maxFileBytes ?? defaultMaxFileBytes;
|
|
1484
|
+
if (byteLength > maxFileBytes) {
|
|
1485
|
+
throw new DockerSandboxError(
|
|
1486
|
+
`Initial file exceeds maxFileBytes (${byteLength} > ${maxFileBytes}): ${filePath}`,
|
|
1487
|
+
"file_too_large"
|
|
1280
1488
|
);
|
|
1281
1489
|
}
|
|
1282
1490
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1491
|
+
}
|
|
1492
|
+
function validateRuntimeLimits(runtime) {
|
|
1493
|
+
if (runtime !== void 0 && !isRecord(runtime)) {
|
|
1494
|
+
throw new TypeError("runtime must be an object.");
|
|
1495
|
+
}
|
|
1496
|
+
if (runtime?.commandTimeoutMs !== void 0)
|
|
1497
|
+
assertPositiveSafeInteger(runtime.commandTimeoutMs, "commandTimeoutMs");
|
|
1498
|
+
if (runtime?.maxOutputBytes !== void 0)
|
|
1499
|
+
assertNonNegativeSafeInteger(runtime.maxOutputBytes, "maxOutputBytes");
|
|
1500
|
+
if (runtime?.maxFileBytes !== void 0)
|
|
1501
|
+
assertNonNegativeSafeInteger(runtime.maxFileBytes, "maxFileBytes");
|
|
1502
|
+
if (runtime?.maxProcesses !== void 0)
|
|
1503
|
+
assertNonNegativeSafeInteger(runtime.maxProcesses, "maxProcesses");
|
|
1504
|
+
}
|
|
1505
|
+
function validateResources(resources) {
|
|
1506
|
+
if (resources !== void 0 && !isRecord(resources)) {
|
|
1507
|
+
throw new TypeError("resources must be an object.");
|
|
1508
|
+
}
|
|
1509
|
+
if (resources?.memoryMb !== void 0) assertPositiveSafeInteger(resources.memoryMb, "memoryMb");
|
|
1510
|
+
if (resources?.pidsLimit !== void 0)
|
|
1511
|
+
assertPositiveSafeInteger(resources.pidsLimit, "pidsLimit");
|
|
1512
|
+
if (resources?.sharedMemoryMb !== void 0)
|
|
1513
|
+
assertPositiveSafeInteger(resources.sharedMemoryMb, "sharedMemoryMb");
|
|
1514
|
+
if (resources?.cpus !== void 0 && (!Number.isFinite(resources.cpus) || resources.cpus <= 0)) {
|
|
1515
|
+
throw new RangeError("cpus must be a positive finite number.");
|
|
1294
1516
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
if (this.destroyed || this.activeOperations > 0) {
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
this.clearIdleTimer();
|
|
1303
|
-
this.idleTimer = setTimeout(() => {
|
|
1304
|
-
void this.destroy().catch(() => void 0);
|
|
1305
|
-
}, this.lifecycle.idleTimeoutMs);
|
|
1306
|
-
this.idleTimer.unref?.();
|
|
1517
|
+
}
|
|
1518
|
+
function validateSecurity(security) {
|
|
1519
|
+
if (security !== void 0 && !isRecord(security)) {
|
|
1520
|
+
throw new TypeError("security must be an object.");
|
|
1307
1521
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
this.clearIdleTimer();
|
|
1522
|
+
if (security?.dropCapabilities !== void 0 && !Array.isArray(security.dropCapabilities)) {
|
|
1523
|
+
throw new TypeError("dropCapabilities must be an array.");
|
|
1524
|
+
}
|
|
1525
|
+
if (security?.addCapabilities !== void 0 && !Array.isArray(security.addCapabilities)) {
|
|
1526
|
+
throw new TypeError("addCapabilities must be an array.");
|
|
1314
1527
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1528
|
+
if (security?.readonlyRootfs !== void 0 && typeof security.readonlyRootfs !== "boolean") {
|
|
1529
|
+
throw new TypeError("readonlyRootfs must be a boolean.");
|
|
1530
|
+
}
|
|
1531
|
+
if (security?.noNewPrivileges !== void 0 && typeof security.noNewPrivileges !== "boolean") {
|
|
1532
|
+
throw new TypeError("noNewPrivileges must be a boolean.");
|
|
1533
|
+
}
|
|
1534
|
+
if (security?.seccompProfile !== void 0) {
|
|
1535
|
+
if (!isRecord(security.seccompProfile) || security.seccompProfile.type !== "path") {
|
|
1536
|
+
throw new TypeError('seccompProfile must be a { type: "path", path } object.');
|
|
1537
|
+
}
|
|
1538
|
+
assertNonEmptyString(security.seccompProfile.path, "seccompProfile.path");
|
|
1539
|
+
if (!path2.isAbsolute(security.seccompProfile.path) || security.seccompProfile.path.includes("\0")) {
|
|
1540
|
+
throw new TypeError("seccompProfile.path must be an absolute host path.");
|
|
1319
1541
|
}
|
|
1320
1542
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1543
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1544
|
+
for (const capability of security?.dropCapabilities ?? []) {
|
|
1545
|
+
assertNonEmptyString(capability, "dropCapabilities value");
|
|
1546
|
+
if (seen.has(capability))
|
|
1547
|
+
throw new TypeError(`dropCapabilities contains a duplicate: ${capability}`);
|
|
1548
|
+
seen.add(capability);
|
|
1549
|
+
}
|
|
1550
|
+
seen.clear();
|
|
1551
|
+
for (const capability of security?.addCapabilities ?? []) {
|
|
1552
|
+
assertNonEmptyString(capability, "addCapabilities value");
|
|
1553
|
+
if (seen.has(capability))
|
|
1554
|
+
throw new TypeError(`addCapabilities contains a duplicate: ${capability}`);
|
|
1555
|
+
seen.add(capability);
|
|
1325
1556
|
}
|
|
1326
|
-
return sessionId;
|
|
1327
1557
|
}
|
|
1328
|
-
function
|
|
1329
|
-
|
|
1330
|
-
|
|
1558
|
+
function validateExecOptions(options) {
|
|
1559
|
+
assertOptionsObject(options);
|
|
1560
|
+
assertNonEmptyString(options.command, "command");
|
|
1561
|
+
if (options.args !== void 0 && (!Array.isArray(options.args) || !options.args.every((value) => typeof value === "string"))) {
|
|
1562
|
+
throw new TypeError("args must contain only strings.");
|
|
1563
|
+
}
|
|
1564
|
+
if (options.cwd !== void 0) normalizeSandboxPath(options.cwd, { allowRoot: true });
|
|
1565
|
+
copyStringRecord(options.env, "env");
|
|
1566
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger(options.timeoutMs, "timeoutMs");
|
|
1567
|
+
if (options.input !== void 0 && typeof options.input !== "string" && !(options.input instanceof Uint8Array)) {
|
|
1568
|
+
throw new TypeError("input must be a string or Uint8Array.");
|
|
1331
1569
|
}
|
|
1332
|
-
return true;
|
|
1333
1570
|
}
|
|
1334
|
-
function
|
|
1335
|
-
|
|
1336
|
-
|
|
1571
|
+
function validateProcessStartOptions(options) {
|
|
1572
|
+
assertOptionsObject(options);
|
|
1573
|
+
assertNonEmptyString(options.command, "command");
|
|
1574
|
+
if (options.args !== void 0 && (!Array.isArray(options.args) || !options.args.every((value) => typeof value === "string"))) {
|
|
1575
|
+
throw new TypeError("args must contain only strings.");
|
|
1576
|
+
}
|
|
1577
|
+
if (options.cwd !== void 0) normalizeSandboxPath(options.cwd, { allowRoot: true });
|
|
1578
|
+
copyStringRecord(options.env, "env");
|
|
1337
1579
|
}
|
|
1338
|
-
function
|
|
1339
|
-
|
|
1580
|
+
function assertReadOptions(options) {
|
|
1581
|
+
assertOptionsObject(options);
|
|
1582
|
+
normalizeSandboxPath(options.path);
|
|
1340
1583
|
}
|
|
1341
|
-
function
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
1584
|
+
function assertOptionsObject(value) {
|
|
1585
|
+
if (!isRecord(value)) throw new TypeError("options must be an object.");
|
|
1586
|
+
}
|
|
1587
|
+
function resolveRuntimeLimits(runtime) {
|
|
1588
|
+
return {
|
|
1589
|
+
commandTimeoutMs: runtime?.commandTimeoutMs ?? defaultCommandTimeoutMs,
|
|
1590
|
+
maxOutputBytes: runtime?.maxOutputBytes ?? defaultMaxOutputBytes,
|
|
1591
|
+
maxFileBytes: runtime?.maxFileBytes ?? defaultMaxFileBytes,
|
|
1592
|
+
maxProcesses: runtime?.maxProcesses ?? defaultMaxProcesses
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
function snapshotCreateOptions(options) {
|
|
1596
|
+
const files = {};
|
|
1597
|
+
for (const [filePath, content] of Object.entries(options.files ?? {})) {
|
|
1598
|
+
files[filePath] = typeof content === "string" ? content : content.slice();
|
|
1599
|
+
}
|
|
1600
|
+
const workspace = copyWorkspace(options.workspace);
|
|
1601
|
+
const network = copyNetwork(options.network);
|
|
1602
|
+
let snapshot = {
|
|
1603
|
+
image: options.image,
|
|
1604
|
+
workspace,
|
|
1605
|
+
network
|
|
1606
|
+
};
|
|
1607
|
+
if (options.id !== void 0) snapshot = { ...snapshot, id: options.id };
|
|
1608
|
+
if (options.workdir !== void 0) snapshot = { ...snapshot, workdir: options.workdir };
|
|
1609
|
+
if (options.files !== void 0) snapshot = { ...snapshot, files: Object.freeze(files) };
|
|
1610
|
+
if (options.directories !== void 0) {
|
|
1611
|
+
snapshot = { ...snapshot, directories: Object.freeze([...options.directories]) };
|
|
1344
1612
|
}
|
|
1345
|
-
if (
|
|
1346
|
-
|
|
1613
|
+
if (options.env !== void 0) {
|
|
1614
|
+
snapshot = { ...snapshot, env: Object.freeze({ ...options.env }) };
|
|
1347
1615
|
}
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
1616
|
+
if (options.user !== void 0) snapshot = { ...snapshot, user: options.user };
|
|
1617
|
+
if (options.labels !== void 0) {
|
|
1618
|
+
snapshot = { ...snapshot, labels: Object.freeze({ ...options.labels }) };
|
|
1350
1619
|
}
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1620
|
+
if (options.resources !== void 0) {
|
|
1621
|
+
snapshot = { ...snapshot, resources: Object.freeze({ ...options.resources }) };
|
|
1622
|
+
}
|
|
1623
|
+
if (options.runtime !== void 0) {
|
|
1624
|
+
snapshot = { ...snapshot, runtime: Object.freeze({ ...options.runtime }) };
|
|
1625
|
+
}
|
|
1626
|
+
if (options.security !== void 0) {
|
|
1627
|
+
let security = {
|
|
1628
|
+
...options.security
|
|
1629
|
+
};
|
|
1630
|
+
if (options.security.seccompProfile !== void 0) {
|
|
1631
|
+
security = {
|
|
1632
|
+
...security,
|
|
1633
|
+
seccompProfile: Object.freeze({ ...options.security.seccompProfile })
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (options.security.dropCapabilities !== void 0) {
|
|
1637
|
+
security = {
|
|
1638
|
+
...security,
|
|
1639
|
+
dropCapabilities: Object.freeze([...options.security.dropCapabilities])
|
|
1640
|
+
};
|
|
1358
1641
|
}
|
|
1359
|
-
if (
|
|
1360
|
-
|
|
1642
|
+
if (options.security.addCapabilities !== void 0) {
|
|
1643
|
+
security = {
|
|
1644
|
+
...security,
|
|
1645
|
+
addCapabilities: Object.freeze([...options.security.addCapabilities])
|
|
1646
|
+
};
|
|
1361
1647
|
}
|
|
1362
|
-
|
|
1648
|
+
snapshot = { ...snapshot, security: Object.freeze(security) };
|
|
1363
1649
|
}
|
|
1364
|
-
|
|
1650
|
+
if (options.abortSignal !== void 0) {
|
|
1651
|
+
snapshot = { ...snapshot, abortSignal: options.abortSignal };
|
|
1652
|
+
}
|
|
1653
|
+
return Object.freeze(snapshot);
|
|
1365
1654
|
}
|
|
1366
|
-
function
|
|
1367
|
-
|
|
1655
|
+
function configurationFromInspection(id, containerName, inspection) {
|
|
1656
|
+
const containerLabels = inspection.Config.Labels ?? {};
|
|
1657
|
+
if (containerLabels[labels.schema] !== schemaVersion || containerLabels[labels.id] !== id) {
|
|
1658
|
+
throw new DockerSandboxError(
|
|
1659
|
+
`Container is not an Anvia sandbox: ${containerName}`,
|
|
1660
|
+
"sandbox_not_found"
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
const workdir = requiredLabel(containerLabels, labels.workdir);
|
|
1664
|
+
const workspaceType = requiredLabel(containerLabels, labels.workspaceType);
|
|
1665
|
+
const volumeName = requiredLabel(containerLabels, labels.workspaceVolume);
|
|
1666
|
+
const networkMode = requiredLabel(containerLabels, labels.networkMode);
|
|
1667
|
+
if (networkMode !== "none" && networkMode !== "bridge") invalidInspection("network mode");
|
|
1668
|
+
const workspace = workspaceType === "ephemeral" ? { type: "ephemeral" } : workspaceType === "docker-volume" ? { type: "docker-volume", name: volumeName } : invalidInspection("workspace type");
|
|
1669
|
+
const runtime = {
|
|
1670
|
+
commandTimeoutMs: parseLabelInteger(containerLabels, labels.commandTimeoutMs, true),
|
|
1671
|
+
maxOutputBytes: parseLabelInteger(containerLabels, labels.maxOutputBytes, false),
|
|
1672
|
+
maxFileBytes: parseLabelInteger(containerLabels, labels.maxFileBytes, false),
|
|
1673
|
+
maxProcesses: parseLabelInteger(containerLabels, labels.maxProcesses, false)
|
|
1674
|
+
};
|
|
1675
|
+
return {
|
|
1676
|
+
id,
|
|
1677
|
+
containerName,
|
|
1678
|
+
workdir,
|
|
1679
|
+
workspace,
|
|
1680
|
+
volumeName,
|
|
1681
|
+
ownsVolume: workspace.type === "ephemeral",
|
|
1682
|
+
env: {},
|
|
1683
|
+
runtime,
|
|
1684
|
+
publishedPorts: []
|
|
1685
|
+
};
|
|
1368
1686
|
}
|
|
1369
|
-
function
|
|
1370
|
-
|
|
1687
|
+
async function inspectContainer(dockerPath, containerName, abortSignal, notFoundAsSandboxError) {
|
|
1688
|
+
const result = await runDockerCli(["container", "inspect", containerName], {
|
|
1689
|
+
dockerPath,
|
|
1690
|
+
signal: abortSignal
|
|
1691
|
+
});
|
|
1692
|
+
if (result.exitCode !== 0) {
|
|
1693
|
+
const message = safeDecode(result.stderr);
|
|
1694
|
+
if (notFoundAsSandboxError && message.toLowerCase().includes("no such")) {
|
|
1695
|
+
throw new DockerSandboxError(`Sandbox does not exist: ${containerName}`, "sandbox_not_found");
|
|
1696
|
+
}
|
|
1697
|
+
throw new DockerSandboxError(
|
|
1698
|
+
"Unable to inspect Docker sandbox.",
|
|
1699
|
+
"docker_command_failed",
|
|
1700
|
+
result
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
let value;
|
|
1704
|
+
try {
|
|
1705
|
+
value = JSON.parse(decodeUtf8(result.stdout));
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
throw new DockerSandboxError(
|
|
1708
|
+
"Docker returned invalid container metadata.",
|
|
1709
|
+
"docker_command_failed",
|
|
1710
|
+
void 0,
|
|
1711
|
+
{ cause: error }
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
if (!Array.isArray(value) || value.length !== 1 || !isInspection(value[0])) {
|
|
1715
|
+
throw new DockerSandboxError(
|
|
1716
|
+
"Docker returned invalid container metadata.",
|
|
1717
|
+
"docker_command_failed"
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
return value[0];
|
|
1371
1721
|
}
|
|
1372
|
-
function
|
|
1373
|
-
|
|
1374
|
-
|
|
1722
|
+
function isInspection(value) {
|
|
1723
|
+
return isRecord(value) && isRecord(value.Config) && isRecord(value.State);
|
|
1724
|
+
}
|
|
1725
|
+
async function inspectPublishedPorts(options) {
|
|
1726
|
+
if (options.ports.length === 0) return [];
|
|
1727
|
+
const inspection = await inspectContainer(
|
|
1728
|
+
options.dockerPath,
|
|
1729
|
+
options.containerName,
|
|
1730
|
+
options.abortSignal,
|
|
1731
|
+
false
|
|
1732
|
+
);
|
|
1733
|
+
const mappings = inspection.NetworkSettings?.Ports ?? {};
|
|
1734
|
+
return options.ports.map((containerPort) => {
|
|
1735
|
+
const entries = mappings[`${containerPort}/tcp`];
|
|
1736
|
+
const entry = entries?.find((candidate) => candidate.HostIp === "127.0.0.1");
|
|
1737
|
+
const hostPort = Number(entry?.HostPort);
|
|
1738
|
+
if (!isPort(hostPort)) {
|
|
1739
|
+
throw new DockerSandboxError(`Docker did not publish port: ${containerPort}`, "port");
|
|
1740
|
+
}
|
|
1741
|
+
return { containerPort, host: "127.0.0.1", hostPort, protocol: "tcp" };
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
function configuredContainerPorts(inspection) {
|
|
1745
|
+
return Object.keys(inspection.HostConfig?.PortBindings ?? {}).flatMap((key) => {
|
|
1746
|
+
const match = /^(\d+)\/tcp$/.exec(key);
|
|
1747
|
+
if (match?.[1] === void 0) return [];
|
|
1748
|
+
const value = Number(match[1]);
|
|
1749
|
+
return isPort(value) ? [value] : [];
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
async function assertCopiedRegularFile(temporaryDirectory, target, sandboxPath) {
|
|
1753
|
+
const metadata = await lstat(target);
|
|
1754
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
1755
|
+
throw new DockerSandboxError(
|
|
1756
|
+
`Sandbox path is not a regular file: ${sandboxPath}`,
|
|
1757
|
+
"invalid_path"
|
|
1758
|
+
);
|
|
1375
1759
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1760
|
+
const resolvedTarget = await realpath(target);
|
|
1761
|
+
const relativeTarget = path2.relative(temporaryDirectory, resolvedTarget);
|
|
1762
|
+
if (relativeTarget === "" || relativeTarget === ".." || relativeTarget.startsWith(`..${path2.sep}`) || path2.isAbsolute(relativeTarget)) {
|
|
1763
|
+
throw new DockerSandboxError(
|
|
1764
|
+
`Sandbox file escaped its temporary read boundary: ${sandboxPath}`,
|
|
1765
|
+
"invalid_path"
|
|
1766
|
+
);
|
|
1378
1767
|
}
|
|
1379
1768
|
}
|
|
1380
|
-
function
|
|
1381
|
-
|
|
1382
|
-
|
|
1769
|
+
async function removeContainer(dockerPath, containerName) {
|
|
1770
|
+
const result = await runDockerCli(["rm", "-f", containerName], { dockerPath });
|
|
1771
|
+
if (result.exitCode === 0 || safeDecode(result.stderr).toLowerCase().includes("no such")) return;
|
|
1772
|
+
throw new DockerSandboxError("Unable to remove Docker sandbox.", "docker_command_failed", result);
|
|
1773
|
+
}
|
|
1774
|
+
async function removeVolume(dockerPath, volumeName) {
|
|
1775
|
+
const result = await runDockerCli(["volume", "rm", volumeName], { dockerPath });
|
|
1776
|
+
if (result.exitCode === 0 || safeDecode(result.stderr).toLowerCase().includes("no such")) return;
|
|
1777
|
+
throw new DockerSandboxError(
|
|
1778
|
+
"Unable to remove Docker sandbox volume.",
|
|
1779
|
+
"docker_command_failed",
|
|
1780
|
+
result
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
function parseFindEntry(line, workdir) {
|
|
1784
|
+
const [absolutePath, rawType, rawSize] = line.split(" ");
|
|
1785
|
+
if (absolutePath === void 0 || rawType === void 0 || rawSize === void 0) {
|
|
1786
|
+
throw new DockerSandboxError("Docker returned invalid file metadata.", "docker_command_failed");
|
|
1787
|
+
}
|
|
1788
|
+
const relativePath = path2.posix.relative(workdir, absolutePath);
|
|
1789
|
+
const size = Number(rawSize);
|
|
1790
|
+
const type = mapFindType(rawType);
|
|
1791
|
+
let entry = {
|
|
1792
|
+
path: normalizeSandboxPath(relativePath),
|
|
1793
|
+
type
|
|
1794
|
+
};
|
|
1795
|
+
if (type === "file" && Number.isSafeInteger(size) && size >= 0) {
|
|
1796
|
+
entry = { ...entry, size };
|
|
1383
1797
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1798
|
+
return entry;
|
|
1799
|
+
}
|
|
1800
|
+
function mapFindType(value) {
|
|
1801
|
+
if (value === "f") return "file";
|
|
1802
|
+
if (value === "d") return "directory";
|
|
1803
|
+
if (value === "l") return "symlink";
|
|
1804
|
+
return "other";
|
|
1805
|
+
}
|
|
1806
|
+
function toExecResult(result) {
|
|
1807
|
+
const common = {
|
|
1808
|
+
stdout: result.stdout.slice(),
|
|
1809
|
+
stderr: result.stderr.slice(),
|
|
1810
|
+
durationMs: result.durationMs,
|
|
1811
|
+
stdoutTruncated: result.stdoutTruncated,
|
|
1812
|
+
stderrTruncated: result.stderrTruncated
|
|
1813
|
+
};
|
|
1814
|
+
return result.timedOut ? { ...common, status: "timed_out" } : { ...common, status: "exited", exitCode: result.exitCode };
|
|
1815
|
+
}
|
|
1816
|
+
function copyWorkspace(workspace) {
|
|
1817
|
+
return workspace.type === "ephemeral" ? Object.freeze({ type: "ephemeral" }) : Object.freeze({ type: "docker-volume", name: workspace.name });
|
|
1818
|
+
}
|
|
1819
|
+
function copyNetwork(network) {
|
|
1820
|
+
return network.mode === "none" ? Object.freeze({ mode: "none" }) : Object.freeze({ mode: "bridge", ports: Object.freeze([...network.ports ?? []]) });
|
|
1821
|
+
}
|
|
1822
|
+
function copyStringRecord(value, name) {
|
|
1823
|
+
if (value === void 0) return {};
|
|
1824
|
+
if (!isRecord(value)) throw new TypeError(`${name} must be an object.`);
|
|
1825
|
+
const copy = {};
|
|
1826
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1827
|
+
if (typeof item !== "string") throw new TypeError(`${name}.${key} must be a string.`);
|
|
1828
|
+
if (name === "env" && !envKeyPattern.test(key))
|
|
1829
|
+
throw new TypeError(`Invalid environment variable name: ${key}`);
|
|
1830
|
+
copy[key] = item;
|
|
1386
1831
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1832
|
+
return copy;
|
|
1833
|
+
}
|
|
1834
|
+
function requiredLabel(containerLabels, name) {
|
|
1835
|
+
const value = containerLabels[name];
|
|
1836
|
+
if (value === void 0 || value.length === 0) invalidInspection(name);
|
|
1837
|
+
return value;
|
|
1838
|
+
}
|
|
1839
|
+
function parseLabelInteger(containerLabels, name, positive) {
|
|
1840
|
+
const value = Number(requiredLabel(containerLabels, name));
|
|
1841
|
+
if (!Number.isSafeInteger(value) || (positive ? value <= 0 : value < 0)) invalidInspection(name);
|
|
1842
|
+
return value;
|
|
1843
|
+
}
|
|
1844
|
+
function invalidInspection(field) {
|
|
1845
|
+
throw new DockerSandboxError(`Sandbox metadata is invalid: ${field}`, "invalid_state");
|
|
1846
|
+
}
|
|
1847
|
+
function validatePorts(ports) {
|
|
1848
|
+
if (!Array.isArray(ports)) throw new TypeError("ports must be an array.");
|
|
1849
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1850
|
+
for (const port of ports) {
|
|
1851
|
+
assertPort(port);
|
|
1852
|
+
if (seen.has(port)) throw new DockerSandboxError(`Sandbox port is duplicated: ${port}`, "port");
|
|
1853
|
+
seen.add(port);
|
|
1389
1854
|
}
|
|
1390
1855
|
}
|
|
1391
|
-
|
|
1392
|
-
if (
|
|
1856
|
+
function assertPort(port) {
|
|
1857
|
+
if (!isPort(port))
|
|
1858
|
+
throw new DockerSandboxError(
|
|
1859
|
+
`Sandbox port must be an integer from 1 to 65535: ${port}`,
|
|
1860
|
+
"port"
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
function isPort(port) {
|
|
1864
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
1865
|
+
}
|
|
1866
|
+
function assertSandboxId(id) {
|
|
1867
|
+
if (typeof id !== "string" || !idPattern.test(id)) {
|
|
1868
|
+
throw new TypeError(
|
|
1869
|
+
"Sandbox id must be 1-63 lowercase letters, numbers, dots, underscores, or hyphens."
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function assertDockerVolumeName(name) {
|
|
1874
|
+
assertNonEmptyString(name, "workspace.name");
|
|
1875
|
+
if (name.includes(",") || name.includes("\0"))
|
|
1876
|
+
throw new TypeError("workspace.name is not a valid Docker volume name.");
|
|
1877
|
+
}
|
|
1878
|
+
function assertNonEmptyString(value, name) {
|
|
1879
|
+
if (typeof value !== "string" || value.length === 0)
|
|
1880
|
+
throw new TypeError(`${name} must be a non-empty string.`);
|
|
1881
|
+
}
|
|
1882
|
+
function assertPositiveSafeInteger(value, name) {
|
|
1883
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
1884
|
+
throw new RangeError(`${name} must be a positive safe integer.`);
|
|
1885
|
+
}
|
|
1886
|
+
function assertNonNegativeSafeInteger(value, name) {
|
|
1887
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
1888
|
+
throw new RangeError(`${name} must be a non-negative safe integer.`);
|
|
1889
|
+
}
|
|
1890
|
+
function invalidState(id, state) {
|
|
1891
|
+
return new DockerSandboxError(`Sandbox ${id} is not running (${state}).`, "invalid_state", {
|
|
1892
|
+
state
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
function containerNameFor(id) {
|
|
1896
|
+
return `anvia-sandbox-${id}`;
|
|
1897
|
+
}
|
|
1898
|
+
function combineSignals(first, second) {
|
|
1899
|
+
return first === void 0 ? second : AbortSignal.any([first, second]);
|
|
1900
|
+
}
|
|
1901
|
+
async function waitWithSignal(timeoutMs, abortSignal) {
|
|
1902
|
+
abortSignal.throwIfAborted();
|
|
1393
1903
|
await new Promise((resolve, reject) => {
|
|
1394
1904
|
const timeout = setTimeout(() => {
|
|
1395
|
-
|
|
1905
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1396
1906
|
resolve();
|
|
1397
1907
|
}, timeoutMs);
|
|
1398
1908
|
const abort = () => {
|
|
1399
1909
|
clearTimeout(timeout);
|
|
1400
|
-
reject(
|
|
1910
|
+
reject(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
1401
1911
|
};
|
|
1402
|
-
|
|
1912
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1403
1913
|
});
|
|
1404
1914
|
}
|
|
1405
|
-
function
|
|
1406
|
-
|
|
1915
|
+
function safeDecode(bytes) {
|
|
1916
|
+
try {
|
|
1917
|
+
return decodeUtf8(bytes);
|
|
1918
|
+
} catch {
|
|
1919
|
+
return "";
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function isRecord(value) {
|
|
1923
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1407
1924
|
}
|
|
1408
1925
|
|
|
1409
1926
|
// src/tools.ts
|
|
1410
1927
|
import { createTool } from "@anvia/core/tool";
|
|
1411
1928
|
import { z } from "zod";
|
|
1929
|
+
var allToolNames = [
|
|
1930
|
+
"exec_command",
|
|
1931
|
+
"read_file",
|
|
1932
|
+
"write_file",
|
|
1933
|
+
"list_files",
|
|
1934
|
+
"list_ports",
|
|
1935
|
+
"start_process",
|
|
1936
|
+
"list_processes",
|
|
1937
|
+
"read_process_logs",
|
|
1938
|
+
"stop_process",
|
|
1939
|
+
"wait_for_port"
|
|
1940
|
+
];
|
|
1412
1941
|
var execCommandInput = z.object({
|
|
1413
|
-
command: z.string().min(1)
|
|
1414
|
-
args: z.array(z.string()).optional()
|
|
1415
|
-
cwd: z.string().optional()
|
|
1416
|
-
env: z.record(z.string(), z.string()).optional()
|
|
1417
|
-
timeoutMs: z.number().int().positive().max(3e5).optional()
|
|
1418
|
-
input: z.string().optional()
|
|
1942
|
+
command: z.string().min(1),
|
|
1943
|
+
args: z.array(z.string()).optional(),
|
|
1944
|
+
cwd: z.string().optional(),
|
|
1945
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
1946
|
+
timeoutMs: z.number().int().positive().max(3e5).optional(),
|
|
1947
|
+
input: z.string().optional()
|
|
1419
1948
|
});
|
|
1949
|
+
var execResultOutput = z.discriminatedUnion("status", [
|
|
1950
|
+
z.object({
|
|
1951
|
+
status: z.literal("exited"),
|
|
1952
|
+
exitCode: z.number().int(),
|
|
1953
|
+
stdout: z.string(),
|
|
1954
|
+
stderr: z.string(),
|
|
1955
|
+
durationMs: z.number().nonnegative(),
|
|
1956
|
+
stdoutTruncated: z.boolean(),
|
|
1957
|
+
stderrTruncated: z.boolean()
|
|
1958
|
+
}),
|
|
1959
|
+
z.object({
|
|
1960
|
+
status: z.literal("timed_out"),
|
|
1961
|
+
stdout: z.string(),
|
|
1962
|
+
stderr: z.string(),
|
|
1963
|
+
durationMs: z.number().nonnegative(),
|
|
1964
|
+
stdoutTruncated: z.boolean(),
|
|
1965
|
+
stderrTruncated: z.boolean()
|
|
1966
|
+
})
|
|
1967
|
+
]);
|
|
1420
1968
|
var readFileInput = z.object({
|
|
1421
|
-
path: z.string().min(1)
|
|
1422
|
-
startLine: z.number().int().positive().optional()
|
|
1423
|
-
lineCount: z.number().int().positive().max(1e4).optional()
|
|
1969
|
+
path: z.string().min(1),
|
|
1970
|
+
startLine: z.number().int().positive().optional(),
|
|
1971
|
+
lineCount: z.number().int().positive().max(1e4).optional()
|
|
1972
|
+
});
|
|
1973
|
+
var readFileOutput = z.object({
|
|
1974
|
+
content: z.string(),
|
|
1975
|
+
startLine: z.number().int().positive(),
|
|
1976
|
+
endLine: z.number().int().positive().nullable(),
|
|
1977
|
+
nextStartLine: z.number().int().positive().nullable(),
|
|
1978
|
+
truncated: z.boolean(),
|
|
1979
|
+
truncatedBy: z.enum(["lines", "bytes"]).nullable()
|
|
1424
1980
|
});
|
|
1425
1981
|
var writeFileInput = z.object({
|
|
1426
|
-
path: z.string().min(1)
|
|
1427
|
-
content: z.string()
|
|
1982
|
+
path: z.string().min(1),
|
|
1983
|
+
content: z.string()
|
|
1984
|
+
});
|
|
1985
|
+
var writeFileOutput = z.object({
|
|
1986
|
+
path: z.string(),
|
|
1987
|
+
bytesWritten: z.number().int().nonnegative()
|
|
1988
|
+
});
|
|
1989
|
+
var listFilesInput = z.object({ path: z.string().optional() });
|
|
1990
|
+
var fileEntryOutput = z.object({
|
|
1991
|
+
path: z.string(),
|
|
1992
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
1993
|
+
size: z.number().int().nonnegative().optional()
|
|
1428
1994
|
});
|
|
1429
|
-
var
|
|
1430
|
-
path: z.string()
|
|
1995
|
+
var listFilesOutput = z.object({
|
|
1996
|
+
path: z.string(),
|
|
1997
|
+
entries: z.array(fileEntryOutput)
|
|
1431
1998
|
});
|
|
1432
1999
|
var emptyInput = z.object({});
|
|
2000
|
+
var publishedPortOutput = z.object({
|
|
2001
|
+
containerPort: z.number().int(),
|
|
2002
|
+
host: z.literal("127.0.0.1"),
|
|
2003
|
+
hostPort: z.number().int(),
|
|
2004
|
+
protocol: z.literal("tcp")
|
|
2005
|
+
});
|
|
2006
|
+
var listPortsOutput = z.object({ ports: z.array(publishedPortOutput) });
|
|
1433
2007
|
var startProcessInput = z.object({
|
|
1434
|
-
command: z.string().min(1)
|
|
1435
|
-
args: z.array(z.string()).optional()
|
|
1436
|
-
cwd: z.string().optional()
|
|
1437
|
-
env: z.record(z.string(), z.string()).optional()
|
|
2008
|
+
command: z.string().min(1),
|
|
2009
|
+
args: z.array(z.string()).optional(),
|
|
2010
|
+
cwd: z.string().optional(),
|
|
2011
|
+
env: z.record(z.string(), z.string()).optional()
|
|
1438
2012
|
});
|
|
1439
|
-
var
|
|
1440
|
-
|
|
2013
|
+
var processInfoOutput = z.object({
|
|
2014
|
+
id: z.string(),
|
|
2015
|
+
command: z.string(),
|
|
2016
|
+
args: z.array(z.string()),
|
|
2017
|
+
cwd: z.string().optional(),
|
|
2018
|
+
status: z.enum(["running", "exited", "stopped"]),
|
|
2019
|
+
exitCode: z.number().int().optional(),
|
|
2020
|
+
startedAt: z.string(),
|
|
2021
|
+
endedAt: z.string().optional()
|
|
1441
2022
|
});
|
|
2023
|
+
var listProcessesOutput = z.object({ processes: z.array(processInfoOutput) });
|
|
2024
|
+
var processIdInput = z.object({ processId: z.string().min(1) });
|
|
1442
2025
|
var readProcessLogsInput = processIdInput.extend({
|
|
1443
|
-
tailBytes: z.number().int().nonnegative().max(1024 * 1024).optional()
|
|
2026
|
+
tailBytes: z.number().int().nonnegative().max(1024 * 1024).optional()
|
|
2027
|
+
});
|
|
2028
|
+
var processLogsOutput = z.object({
|
|
2029
|
+
processId: z.string(),
|
|
2030
|
+
stdout: z.string(),
|
|
2031
|
+
stderr: z.string(),
|
|
2032
|
+
stdoutTruncated: z.boolean(),
|
|
2033
|
+
stderrTruncated: z.boolean()
|
|
1444
2034
|
});
|
|
1445
2035
|
var waitForPortInput = z.object({
|
|
1446
|
-
containerPort: z.number().int().min(1).max(65535)
|
|
1447
|
-
timeoutMs: z.number().int().positive().max(3e5).optional()
|
|
2036
|
+
containerPort: z.number().int().min(1).max(65535),
|
|
2037
|
+
timeoutMs: z.number().int().positive().max(3e5).optional()
|
|
1448
2038
|
});
|
|
1449
|
-
var
|
|
1450
|
-
var maxToolLogBytes = 1024 * 1024;
|
|
2039
|
+
var waitForPortOutput = z.object({ port: publishedPortOutput });
|
|
1451
2040
|
var defaultReadFileLineCount = 500;
|
|
1452
2041
|
var defaultReadFileMaxLineCount = 2e3;
|
|
1453
2042
|
var defaultReadFileMaxBytes = 64 * 1024;
|
|
1454
|
-
var maxToolReadFileBytes = 1024 * 1024;
|
|
1455
2043
|
var maxToolReadFileLines = 1e4;
|
|
1456
|
-
var
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
truncatedBy: z.enum(["lines", "bytes"]).nullable()
|
|
1463
|
-
});
|
|
1464
|
-
var sandboxToolMetadataKey = /* @__PURE__ */ Symbol.for("anvia.sandbox.tool.metadata");
|
|
1465
|
-
function createSandboxTools(session, options = {}) {
|
|
1466
|
-
const include = new Set(
|
|
1467
|
-
options.allow ?? options.include ?? ["exec_command", "read_file", "write_file", "list_files"]
|
|
1468
|
-
);
|
|
2044
|
+
var maxToolBytes = 1024 * 1024;
|
|
2045
|
+
var maxToolTimeoutMs = 3e5;
|
|
2046
|
+
function createDockerSandboxTools(options) {
|
|
2047
|
+
validateFactoryOptions(options);
|
|
2048
|
+
options = snapshotFactoryOptions(options);
|
|
2049
|
+
const selected = new Set(options.tools);
|
|
1469
2050
|
const tools = [];
|
|
1470
|
-
|
|
1471
|
-
tools.push(createExecCommandTool(
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
tools.push(
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
tools.push(
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
tools.push(
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
}
|
|
1487
|
-
const processToolsRequested = include.has("start_process") || include.has("list_processes") || include.has("read_process_logs") || include.has("stop_process");
|
|
1488
|
-
if (include.has("wait_for_port") || processToolsRequested) assertProcessToolPolicy(options);
|
|
1489
|
-
const processSession = processToolsRequested ? requireProcessSession(session) : void 0;
|
|
1490
|
-
if (include.has("start_process") && processSession !== void 0) {
|
|
1491
|
-
tools.push(createStartProcessTool(processSession, options));
|
|
1492
|
-
}
|
|
1493
|
-
if (include.has("list_processes") && processSession !== void 0) {
|
|
1494
|
-
tools.push(createListProcessesTool(processSession));
|
|
1495
|
-
}
|
|
1496
|
-
if (include.has("read_process_logs") && processSession !== void 0) {
|
|
1497
|
-
tools.push(createReadProcessLogsTool(processSession, options));
|
|
1498
|
-
}
|
|
1499
|
-
if (include.has("stop_process") && processSession !== void 0) {
|
|
1500
|
-
tools.push(createStopProcessTool(processSession, options));
|
|
1501
|
-
}
|
|
1502
|
-
if (include.has("wait_for_port") && portSession !== void 0) {
|
|
1503
|
-
tools.push(createWaitForPortTool(portSession, options));
|
|
1504
|
-
}
|
|
1505
|
-
for (const tool of tools) {
|
|
1506
|
-
Object.defineProperty(tool, sandboxToolMetadataKey, {
|
|
1507
|
-
value: { session },
|
|
1508
|
-
enumerable: false
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
return tools;
|
|
2051
|
+
for (const name of options.tools) {
|
|
2052
|
+
if (name === "exec_command") tools.push(createExecCommandTool(options));
|
|
2053
|
+
else if (name === "read_file") tools.push(createReadFileTool(options));
|
|
2054
|
+
else if (name === "write_file") tools.push(createWriteFileTool(options));
|
|
2055
|
+
else if (name === "list_files") tools.push(createListFilesTool(options.sandbox));
|
|
2056
|
+
else if (name === "list_ports") tools.push(createListPortsTool(options.sandbox));
|
|
2057
|
+
else if (name === "start_process") tools.push(createStartProcessTool(options));
|
|
2058
|
+
else if (name === "list_processes") tools.push(createListProcessesTool(options.sandbox));
|
|
2059
|
+
else if (name === "read_process_logs") tools.push(createReadProcessLogsTool(options));
|
|
2060
|
+
else if (name === "stop_process") tools.push(createStopProcessTool(options));
|
|
2061
|
+
else if (name === "wait_for_port") tools.push(createWaitForPortTool(options));
|
|
2062
|
+
}
|
|
2063
|
+
if (tools.length !== selected.size) {
|
|
2064
|
+
throw toolPolicyError("Sandbox tool selection contains an unsupported tool name.");
|
|
2065
|
+
}
|
|
2066
|
+
return Object.freeze(tools);
|
|
1512
2067
|
}
|
|
1513
|
-
function createExecCommandTool(
|
|
1514
|
-
const policy = options.exec ?? {};
|
|
2068
|
+
function createExecCommandTool(options) {
|
|
1515
2069
|
return createTool({
|
|
1516
2070
|
name: "exec_command",
|
|
1517
|
-
description: "Run
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
execute: async ({ command, args, cwd, env, timeoutMs, input }) => {
|
|
1521
|
-
assertCommandAllowed(command, options);
|
|
1522
|
-
const
|
|
2071
|
+
description: "Run one executable inside the sandbox with structured arguments.",
|
|
2072
|
+
inputSchema: execCommandInput,
|
|
2073
|
+
outputSchema: execResultOutput,
|
|
2074
|
+
execute: async ({ command, args, cwd, env, timeoutMs, input }, context) => {
|
|
2075
|
+
assertCommandAllowed(command, options.exec?.commands);
|
|
2076
|
+
const effectiveTimeoutMs = timeoutMs ?? options.exec?.defaultTimeoutMs;
|
|
2077
|
+
assertTimeoutAllowed(effectiveTimeoutMs, options.exec?.maxTimeoutMs);
|
|
2078
|
+
let execOptions = {
|
|
1523
2079
|
command
|
|
1524
2080
|
};
|
|
1525
|
-
if (args !== void 0) {
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
if (cwd !== void 0) {
|
|
1529
|
-
execOptions.cwd = cwd;
|
|
1530
|
-
}
|
|
1531
|
-
if (env !== void 0) {
|
|
1532
|
-
execOptions.env = env;
|
|
1533
|
-
}
|
|
1534
|
-
const effectiveTimeoutMs = timeoutMs ?? policy.defaultTimeoutMs ?? options.execTimeoutMs;
|
|
2081
|
+
if (args !== void 0) execOptions = { ...execOptions, args };
|
|
2082
|
+
if (cwd !== void 0) execOptions = { ...execOptions, cwd };
|
|
2083
|
+
if (env !== void 0) execOptions = { ...execOptions, env };
|
|
1535
2084
|
if (effectiveTimeoutMs !== void 0) {
|
|
1536
|
-
|
|
1537
|
-
execOptions.timeoutMs = effectiveTimeoutMs;
|
|
2085
|
+
execOptions = { ...execOptions, timeoutMs: effectiveTimeoutMs };
|
|
1538
2086
|
}
|
|
1539
|
-
if (input !== void 0) {
|
|
1540
|
-
|
|
2087
|
+
if (input !== void 0) execOptions = { ...execOptions, input };
|
|
2088
|
+
if (context.abortSignal !== void 0) {
|
|
2089
|
+
execOptions = { ...execOptions, abortSignal: context.abortSignal };
|
|
1541
2090
|
}
|
|
1542
|
-
|
|
1543
|
-
return formatExecResult(result);
|
|
2091
|
+
return serializeExecResult(await options.sandbox.exec(execOptions));
|
|
1544
2092
|
}
|
|
1545
2093
|
});
|
|
1546
2094
|
}
|
|
1547
|
-
function createReadFileTool(
|
|
2095
|
+
function createReadFileTool(options) {
|
|
2096
|
+
const limits = resolveReadFileLimits(options);
|
|
1548
2097
|
return createTool({
|
|
1549
2098
|
name: "read_file",
|
|
1550
|
-
description: "Read a bounded page of a text file from the sandbox workspace.
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
execute: async ({ path: path3, startLine, lineCount }) => {
|
|
1554
|
-
const limits = resolveReadFileLimits(options);
|
|
1555
|
-
const effectiveStartLine = startLine ?? 1;
|
|
2099
|
+
description: "Read a bounded page of a UTF-8 text file from the sandbox workspace.",
|
|
2100
|
+
inputSchema: readFileInput,
|
|
2101
|
+
outputSchema: readFileOutput,
|
|
2102
|
+
execute: async ({ path: path3, startLine, lineCount }, context) => {
|
|
1556
2103
|
const effectiveLineCount = lineCount ?? limits.defaultLineCount;
|
|
1557
2104
|
if (effectiveLineCount > limits.maxLineCount) {
|
|
1558
|
-
throw
|
|
1559
|
-
`File read line count exceeds
|
|
2105
|
+
throw toolPolicyError(
|
|
2106
|
+
`File read line count exceeds policy (${effectiveLineCount} > ${limits.maxLineCount}).`
|
|
1560
2107
|
);
|
|
1561
2108
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
lineCount: effectiveLineCount,
|
|
1566
|
-
maxBytes: limits.maxBytes
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
return createTextFilePage(await session.readTextFile(path3), {
|
|
1570
|
-
startLine: effectiveStartLine,
|
|
2109
|
+
let readOptions = {
|
|
2110
|
+
path: path3,
|
|
2111
|
+
startLine: startLine ?? 1,
|
|
1571
2112
|
lineCount: effectiveLineCount,
|
|
1572
2113
|
maxBytes: limits.maxBytes
|
|
1573
|
-
}
|
|
2114
|
+
};
|
|
2115
|
+
if (context.abortSignal !== void 0) {
|
|
2116
|
+
readOptions = { ...readOptions, abortSignal: context.abortSignal };
|
|
2117
|
+
}
|
|
2118
|
+
return options.sandbox.readTextFilePage(readOptions);
|
|
1574
2119
|
}
|
|
1575
2120
|
});
|
|
1576
2121
|
}
|
|
1577
|
-
function createWriteFileTool(
|
|
2122
|
+
function createWriteFileTool(options) {
|
|
1578
2123
|
return createTool({
|
|
1579
2124
|
name: "write_file",
|
|
1580
|
-
description: "Write a text file inside the sandbox workspace.
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
execute: async ({ path: path3, content }) => {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2125
|
+
description: "Write a complete UTF-8 text file inside the sandbox workspace.",
|
|
2126
|
+
inputSchema: writeFileInput,
|
|
2127
|
+
outputSchema: writeFileOutput,
|
|
2128
|
+
execute: async ({ path: path3, content }, context) => {
|
|
2129
|
+
const bytesWritten = new TextEncoder().encode(content).byteLength;
|
|
2130
|
+
const maxBytes = options.writeFile?.maxBytes;
|
|
2131
|
+
if (maxBytes !== void 0 && bytesWritten > maxBytes) {
|
|
2132
|
+
throw toolPolicyError(`File content exceeds policy (${bytesWritten} > ${maxBytes}).`);
|
|
2133
|
+
}
|
|
2134
|
+
let writeOptions = {
|
|
2135
|
+
path: path3,
|
|
2136
|
+
text: content
|
|
2137
|
+
};
|
|
2138
|
+
if (context.abortSignal !== void 0) {
|
|
2139
|
+
writeOptions = { ...writeOptions, abortSignal: context.abortSignal };
|
|
2140
|
+
}
|
|
2141
|
+
await options.sandbox.writeTextFile(writeOptions);
|
|
2142
|
+
return { path: path3, bytesWritten };
|
|
1587
2143
|
}
|
|
1588
2144
|
});
|
|
1589
2145
|
}
|
|
1590
|
-
function createListFilesTool(
|
|
2146
|
+
function createListFilesTool(sandbox) {
|
|
1591
2147
|
return createTool({
|
|
1592
2148
|
name: "list_files",
|
|
1593
|
-
description: "List
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
execute: async ({ path: path3 }) => {
|
|
1597
|
-
|
|
1598
|
-
if (
|
|
1599
|
-
|
|
2149
|
+
description: "List direct children of a directory in the sandbox workspace.",
|
|
2150
|
+
inputSchema: listFilesInput,
|
|
2151
|
+
outputSchema: listFilesOutput,
|
|
2152
|
+
execute: async ({ path: path3 }, context) => {
|
|
2153
|
+
let listOptions = {};
|
|
2154
|
+
if (path3 !== void 0) listOptions = { ...listOptions, path: path3 };
|
|
2155
|
+
if (context.abortSignal !== void 0) {
|
|
2156
|
+
listOptions = { ...listOptions, abortSignal: context.abortSignal };
|
|
1600
2157
|
}
|
|
1601
|
-
return entries.
|
|
1602
|
-
const size = entry.size === void 0 ? "" : ` ${entry.size}b`;
|
|
1603
|
-
return `${entry.type}${size} ${entry.path}`;
|
|
1604
|
-
}).join("\n");
|
|
2158
|
+
return { path: path3 ?? ".", entries: [...await sandbox.listFiles(listOptions)] };
|
|
1605
2159
|
}
|
|
1606
2160
|
});
|
|
1607
2161
|
}
|
|
1608
|
-
function createListPortsTool(
|
|
2162
|
+
function createListPortsTool(sandbox) {
|
|
1609
2163
|
return createTool({
|
|
1610
2164
|
name: "list_ports",
|
|
1611
|
-
description: "List
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
execute: async () => {
|
|
1615
|
-
if (session.publishedPorts.length === 0) {
|
|
1616
|
-
return "No sandbox ports are published.";
|
|
1617
|
-
}
|
|
1618
|
-
return session.publishedPorts.map((port) => `${port.containerPort}/${port.protocol} ${port.host}:${port.hostPort}`).join("\n");
|
|
1619
|
-
}
|
|
2165
|
+
description: "List explicitly published localhost TCP ports for the sandbox.",
|
|
2166
|
+
inputSchema: emptyInput,
|
|
2167
|
+
outputSchema: listPortsOutput,
|
|
2168
|
+
execute: async () => ({ ports: [...sandbox.publishedPorts] })
|
|
1620
2169
|
});
|
|
1621
2170
|
}
|
|
1622
|
-
function createStartProcessTool(
|
|
2171
|
+
function createStartProcessTool(options) {
|
|
1623
2172
|
return createTool({
|
|
1624
2173
|
name: "start_process",
|
|
1625
|
-
description: "Start a managed long-running process inside the sandbox.
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
execute: async ({ command, args, cwd, env }) => {
|
|
1629
|
-
assertCommandAllowed(command, options);
|
|
1630
|
-
|
|
1631
|
-
if (args !== void 0)
|
|
1632
|
-
if (cwd !== void 0)
|
|
1633
|
-
if (env !== void 0)
|
|
1634
|
-
|
|
2174
|
+
description: "Start a managed long-running process inside the sandbox.",
|
|
2175
|
+
inputSchema: startProcessInput,
|
|
2176
|
+
outputSchema: processInfoOutput,
|
|
2177
|
+
execute: async ({ command, args, cwd, env }, context) => {
|
|
2178
|
+
assertCommandAllowed(command, options.exec?.commands);
|
|
2179
|
+
let startOptions = { command };
|
|
2180
|
+
if (args !== void 0) startOptions = { ...startOptions, args };
|
|
2181
|
+
if (cwd !== void 0) startOptions = { ...startOptions, cwd };
|
|
2182
|
+
if (env !== void 0) startOptions = { ...startOptions, env };
|
|
2183
|
+
if (context.abortSignal !== void 0) {
|
|
2184
|
+
startOptions = { ...startOptions, abortSignal: context.abortSignal };
|
|
2185
|
+
}
|
|
2186
|
+
return serializeProcessInfo(await options.sandbox.startProcess(startOptions));
|
|
1635
2187
|
}
|
|
1636
2188
|
});
|
|
1637
2189
|
}
|
|
1638
|
-
function createListProcessesTool(
|
|
2190
|
+
function createListProcessesTool(sandbox) {
|
|
1639
2191
|
return createTool({
|
|
1640
2192
|
name: "list_processes",
|
|
1641
|
-
description: "List managed
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
execute: async () => {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}
|
|
2193
|
+
description: "List processes managed by this live sandbox handle.",
|
|
2194
|
+
inputSchema: emptyInput,
|
|
2195
|
+
outputSchema: listProcessesOutput,
|
|
2196
|
+
execute: async (_, context) => ({
|
|
2197
|
+
processes: (await sandbox.listProcesses(
|
|
2198
|
+
context.abortSignal === void 0 ? {} : { abortSignal: context.abortSignal }
|
|
2199
|
+
)).map(serializeProcessInfo)
|
|
2200
|
+
})
|
|
1649
2201
|
});
|
|
1650
2202
|
}
|
|
1651
|
-
function createReadProcessLogsTool(
|
|
2203
|
+
function createReadProcessLogsTool(options) {
|
|
2204
|
+
const maxLogBytes = options.process?.maxLogBytes ?? 64 * 1024;
|
|
1652
2205
|
return createTool({
|
|
1653
2206
|
name: "read_process_logs",
|
|
1654
|
-
description: "Read
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
execute: async ({ processId, tailBytes }) => {
|
|
1658
|
-
const configuredMaxLogBytes = options.process?.maxLogBytes ?? 64 * 1024;
|
|
1659
|
-
if (!Number.isInteger(configuredMaxLogBytes) || configuredMaxLogBytes < 0) {
|
|
1660
|
-
throw new SandboxToolPolicyError("Process maxLogBytes must be a non-negative integer.");
|
|
1661
|
-
}
|
|
1662
|
-
const maxLogBytes = Math.min(configuredMaxLogBytes, maxToolLogBytes);
|
|
2207
|
+
description: "Read bounded UTF-8 output from a managed sandbox process.",
|
|
2208
|
+
inputSchema: readProcessLogsInput,
|
|
2209
|
+
outputSchema: processLogsOutput,
|
|
2210
|
+
execute: async ({ processId, tailBytes }, context) => {
|
|
1663
2211
|
const effectiveTailBytes = tailBytes ?? maxLogBytes;
|
|
1664
2212
|
if (effectiveTailBytes > maxLogBytes) {
|
|
1665
|
-
throw
|
|
1666
|
-
`Process log request exceeds
|
|
2213
|
+
throw toolPolicyError(
|
|
2214
|
+
`Process log request exceeds policy (${effectiveTailBytes} > ${maxLogBytes}).`
|
|
1667
2215
|
);
|
|
1668
2216
|
}
|
|
1669
|
-
|
|
2217
|
+
let readOptions = {
|
|
2218
|
+
processId,
|
|
1670
2219
|
tailBytes: effectiveTailBytes
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2220
|
+
};
|
|
2221
|
+
if (context.abortSignal !== void 0) {
|
|
2222
|
+
readOptions = { ...readOptions, abortSignal: context.abortSignal };
|
|
2223
|
+
}
|
|
2224
|
+
const logs = await options.sandbox.readProcessLogs(readOptions);
|
|
2225
|
+
return {
|
|
2226
|
+
processId,
|
|
2227
|
+
stdout: decodeUtf8(logs.stdout),
|
|
2228
|
+
stderr: decodeUtf8(logs.stderr),
|
|
2229
|
+
stdoutTruncated: logs.stdoutTruncated,
|
|
2230
|
+
stderrTruncated: logs.stderrTruncated
|
|
2231
|
+
};
|
|
1679
2232
|
}
|
|
1680
2233
|
});
|
|
1681
2234
|
}
|
|
1682
|
-
function createStopProcessTool(
|
|
2235
|
+
function createStopProcessTool(options) {
|
|
1683
2236
|
return createTool({
|
|
1684
2237
|
name: "stop_process",
|
|
1685
|
-
description: "Stop a managed sandbox
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
execute: async ({ processId }) =>
|
|
1689
|
-
|
|
2238
|
+
description: "Stop a process managed by this live sandbox handle.",
|
|
2239
|
+
inputSchema: processIdInput,
|
|
2240
|
+
outputSchema: processInfoOutput,
|
|
2241
|
+
execute: async ({ processId }, context) => {
|
|
2242
|
+
let stopOptions = {
|
|
2243
|
+
processId,
|
|
1690
2244
|
gracePeriodMs: options.process?.stopGracePeriodMs ?? 5e3
|
|
1691
|
-
}
|
|
1692
|
-
|
|
2245
|
+
};
|
|
2246
|
+
if (context.abortSignal !== void 0) {
|
|
2247
|
+
stopOptions = { ...stopOptions, abortSignal: context.abortSignal };
|
|
2248
|
+
}
|
|
2249
|
+
return serializeProcessInfo(await options.sandbox.stopProcess(stopOptions));
|
|
2250
|
+
}
|
|
1693
2251
|
});
|
|
1694
2252
|
}
|
|
1695
|
-
function createWaitForPortTool(
|
|
2253
|
+
function createWaitForPortTool(options) {
|
|
1696
2254
|
return createTool({
|
|
1697
2255
|
name: "wait_for_port",
|
|
1698
|
-
description: "Wait until
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
execute: async ({ containerPort, timeoutMs }) => {
|
|
2256
|
+
description: "Wait until an explicitly published sandbox TCP port is listening.",
|
|
2257
|
+
inputSchema: waitForPortInput,
|
|
2258
|
+
outputSchema: waitForPortOutput,
|
|
2259
|
+
execute: async ({ containerPort, timeoutMs }, context) => {
|
|
1702
2260
|
const effectiveTimeoutMs = timeoutMs ?? options.process?.defaultWaitTimeoutMs ?? 3e4;
|
|
1703
|
-
const maxWaitTimeoutMs = options.process?.maxWaitTimeoutMs ??
|
|
2261
|
+
const maxWaitTimeoutMs = options.process?.maxWaitTimeoutMs ?? maxToolTimeoutMs;
|
|
1704
2262
|
if (effectiveTimeoutMs > maxWaitTimeoutMs) {
|
|
1705
|
-
throw
|
|
1706
|
-
`Port wait timeout exceeds
|
|
2263
|
+
throw toolPolicyError(
|
|
2264
|
+
`Port wait timeout exceeds policy (${effectiveTimeoutMs} > ${maxWaitTimeoutMs}).`
|
|
1707
2265
|
);
|
|
1708
2266
|
}
|
|
1709
|
-
|
|
2267
|
+
let waitOptions = {
|
|
2268
|
+
containerPort,
|
|
1710
2269
|
timeoutMs: effectiveTimeoutMs
|
|
1711
|
-
}
|
|
1712
|
-
|
|
2270
|
+
};
|
|
2271
|
+
if (context.abortSignal !== void 0) {
|
|
2272
|
+
waitOptions = { ...waitOptions, abortSignal: context.abortSignal };
|
|
2273
|
+
}
|
|
2274
|
+
return { port: await options.sandbox.waitForPort(waitOptions) };
|
|
1713
2275
|
}
|
|
1714
2276
|
});
|
|
1715
2277
|
}
|
|
1716
|
-
function
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
${result.stdout.trimEnd()}`);
|
|
1727
|
-
}
|
|
1728
|
-
if (result.stderr.length > 0) {
|
|
1729
|
-
parts.push(`stderr:
|
|
1730
|
-
${result.stderr.trimEnd()}`);
|
|
1731
|
-
}
|
|
1732
|
-
if (result.stdoutTruncated || result.stderrTruncated) {
|
|
1733
|
-
parts.push("output_truncated: true");
|
|
1734
|
-
}
|
|
1735
|
-
return parts.join("\n\n");
|
|
2278
|
+
function serializeExecResult(result) {
|
|
2279
|
+
const output = {
|
|
2280
|
+
status: result.status,
|
|
2281
|
+
stdout: decodeUtf8(result.stdout),
|
|
2282
|
+
stderr: decodeUtf8(result.stderr),
|
|
2283
|
+
durationMs: result.durationMs,
|
|
2284
|
+
stdoutTruncated: result.stdoutTruncated,
|
|
2285
|
+
stderrTruncated: result.stderrTruncated
|
|
2286
|
+
};
|
|
2287
|
+
return result.status === "exited" ? { ...output, status: "exited", exitCode: result.exitCode } : { ...output, status: "timed_out" };
|
|
1736
2288
|
}
|
|
1737
|
-
function
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2289
|
+
function serializeProcessInfo(process) {
|
|
2290
|
+
let serialized = {
|
|
2291
|
+
id: process.id,
|
|
2292
|
+
command: process.command,
|
|
2293
|
+
args: [...process.args],
|
|
2294
|
+
status: process.status,
|
|
2295
|
+
startedAt: process.startedAt
|
|
2296
|
+
};
|
|
2297
|
+
if (process.cwd !== void 0) serialized = { ...serialized, cwd: process.cwd };
|
|
2298
|
+
if (process.exitCode !== void 0) serialized = { ...serialized, exitCode: process.exitCode };
|
|
2299
|
+
if (process.endedAt !== void 0) serialized = { ...serialized, endedAt: process.endedAt };
|
|
2300
|
+
return serialized;
|
|
1745
2301
|
}
|
|
1746
|
-
function
|
|
1747
|
-
if (!
|
|
1748
|
-
throw new
|
|
2302
|
+
function validateFactoryOptions(options) {
|
|
2303
|
+
if (!isRecord2(options)) {
|
|
2304
|
+
throw new TypeError("options must be an object.");
|
|
2305
|
+
}
|
|
2306
|
+
if (!isDockerSandboxRuntime(options.sandbox)) {
|
|
2307
|
+
throw new TypeError("sandbox must be a DockerSandboxRuntime.");
|
|
2308
|
+
}
|
|
2309
|
+
if (!Array.isArray(options.tools) || options.tools.length === 0) {
|
|
2310
|
+
throw new TypeError("tools must be a non-empty array.");
|
|
2311
|
+
}
|
|
2312
|
+
const known = new Set(allToolNames);
|
|
2313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2314
|
+
for (const name of options.tools) {
|
|
2315
|
+
if (typeof name !== "string" || !known.has(name)) {
|
|
2316
|
+
throw new TypeError("tools contains an unsupported sandbox tool name.");
|
|
2317
|
+
}
|
|
2318
|
+
if (seen.has(name)) throw new TypeError(`tools contains a duplicate: ${name}`);
|
|
2319
|
+
seen.add(name);
|
|
2320
|
+
}
|
|
2321
|
+
for (const [name, policy] of [
|
|
2322
|
+
["exec", options.exec],
|
|
2323
|
+
["readFile", options.readFile],
|
|
2324
|
+
["writeFile", options.writeFile],
|
|
2325
|
+
["process", options.process]
|
|
2326
|
+
]) {
|
|
2327
|
+
if (policy !== void 0 && !isRecord2(policy)) {
|
|
2328
|
+
throw new TypeError(`${name} must be an object.`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
validateCommandPolicy(options.exec?.commands);
|
|
2332
|
+
assertOptionalPositiveInteger(options.exec?.defaultTimeoutMs, "exec.defaultTimeoutMs");
|
|
2333
|
+
assertOptionalPositiveInteger(options.exec?.maxTimeoutMs, "exec.maxTimeoutMs");
|
|
2334
|
+
if ((options.exec?.defaultTimeoutMs ?? 0) > maxToolTimeoutMs || (options.exec?.maxTimeoutMs ?? 0) > maxToolTimeoutMs) {
|
|
2335
|
+
throw toolPolicyError(`Sandbox tool command timeouts cannot exceed ${maxToolTimeoutMs}.`);
|
|
2336
|
+
}
|
|
2337
|
+
if (options.exec?.defaultTimeoutMs !== void 0 && options.exec.maxTimeoutMs !== void 0 && options.exec.defaultTimeoutMs > options.exec.maxTimeoutMs) {
|
|
2338
|
+
throw toolPolicyError("exec.defaultTimeoutMs cannot exceed exec.maxTimeoutMs.");
|
|
2339
|
+
}
|
|
2340
|
+
assertOptionalBoundedPositiveInteger(options.readFile?.maxBytes, "readFile.maxBytes");
|
|
2341
|
+
assertOptionalPositiveInteger(options.readFile?.defaultLineCount, "readFile.defaultLineCount");
|
|
2342
|
+
assertOptionalPositiveInteger(options.readFile?.maxLineCount, "readFile.maxLineCount");
|
|
2343
|
+
assertOptionalBoundedNonNegativeInteger(options.writeFile?.maxBytes, "writeFile.maxBytes");
|
|
2344
|
+
assertOptionalBoundedNonNegativeInteger(options.process?.maxLogBytes, "process.maxLogBytes");
|
|
2345
|
+
assertOptionalPositiveInteger(
|
|
2346
|
+
options.process?.defaultWaitTimeoutMs,
|
|
2347
|
+
"process.defaultWaitTimeoutMs"
|
|
2348
|
+
);
|
|
2349
|
+
assertOptionalPositiveInteger(options.process?.maxWaitTimeoutMs, "process.maxWaitTimeoutMs");
|
|
2350
|
+
assertOptionalNonNegativeInteger(options.process?.stopGracePeriodMs, "process.stopGracePeriodMs");
|
|
2351
|
+
const defaultWaitTimeoutMs = options.process?.defaultWaitTimeoutMs;
|
|
2352
|
+
const maxWaitTimeoutMs = options.process?.maxWaitTimeoutMs ?? maxToolTimeoutMs;
|
|
2353
|
+
if (maxWaitTimeoutMs > maxToolTimeoutMs) {
|
|
2354
|
+
throw toolPolicyError(`process.maxWaitTimeoutMs cannot exceed ${maxToolTimeoutMs}.`);
|
|
1749
2355
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
function requireProcessSession(session) {
|
|
1753
|
-
if (!isSandboxProcessSession(session)) {
|
|
1754
|
-
throw new SandboxToolPolicyError("The sandbox session does not support managed process tools.");
|
|
2356
|
+
if (defaultWaitTimeoutMs !== void 0 && defaultWaitTimeoutMs > maxWaitTimeoutMs) {
|
|
2357
|
+
throw toolPolicyError("process.defaultWaitTimeoutMs cannot exceed process.maxWaitTimeoutMs.");
|
|
1755
2358
|
}
|
|
1756
|
-
return session;
|
|
1757
2359
|
}
|
|
1758
|
-
function
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2360
|
+
function snapshotFactoryOptions(options) {
|
|
2361
|
+
const toolNames = Object.freeze([...options.tools]);
|
|
2362
|
+
const commands = options.exec?.commands === void 0 ? void 0 : Object.freeze({
|
|
2363
|
+
mode: options.exec.commands.mode,
|
|
2364
|
+
values: Object.freeze([...options.exec.commands.values])
|
|
2365
|
+
});
|
|
2366
|
+
let snapshot = {
|
|
2367
|
+
sandbox: options.sandbox,
|
|
2368
|
+
tools: toolNames
|
|
2369
|
+
};
|
|
2370
|
+
if (options.exec !== void 0) {
|
|
2371
|
+
let exec = { ...options.exec };
|
|
2372
|
+
if (commands !== void 0) exec = { ...exec, commands };
|
|
2373
|
+
snapshot = { ...snapshot, exec: Object.freeze(exec) };
|
|
1765
2374
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
throw new SandboxToolPolicyError(
|
|
1769
|
-
"Process maxWaitTimeoutMs must be an integer from 1 to 300000."
|
|
1770
|
-
);
|
|
2375
|
+
if (options.readFile !== void 0) {
|
|
2376
|
+
snapshot = { ...snapshot, readFile: Object.freeze({ ...options.readFile }) };
|
|
1771
2377
|
}
|
|
1772
|
-
if (
|
|
1773
|
-
|
|
1774
|
-
"Process defaultWaitTimeoutMs must be positive and no greater than maxWaitTimeoutMs."
|
|
1775
|
-
);
|
|
2378
|
+
if (options.writeFile !== void 0) {
|
|
2379
|
+
snapshot = { ...snapshot, writeFile: Object.freeze({ ...options.writeFile }) };
|
|
1776
2380
|
}
|
|
1777
|
-
if (
|
|
1778
|
-
|
|
2381
|
+
if (options.process !== void 0) {
|
|
2382
|
+
snapshot = { ...snapshot, process: Object.freeze({ ...options.process }) };
|
|
1779
2383
|
}
|
|
2384
|
+
return Object.freeze(snapshot);
|
|
1780
2385
|
}
|
|
1781
|
-
function
|
|
1782
|
-
|
|
1783
|
-
if (policy
|
|
1784
|
-
|
|
2386
|
+
function validateCommandPolicy(policy) {
|
|
2387
|
+
if (policy === void 0) return;
|
|
2388
|
+
if (!isRecord2(policy)) throw toolPolicyError("exec.commands must be an object.");
|
|
2389
|
+
if (policy.mode !== "allow" && policy.mode !== "block") {
|
|
2390
|
+
throw toolPolicyError("exec.commands must use mode allow or block.");
|
|
1785
2391
|
}
|
|
1786
|
-
if (
|
|
1787
|
-
throw
|
|
2392
|
+
if (!Array.isArray(policy.values))
|
|
2393
|
+
throw toolPolicyError("exec.commands.values must be an array.");
|
|
2394
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2395
|
+
for (const value of policy.values) {
|
|
2396
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2397
|
+
throw toolPolicyError("exec.commands.values must contain non-empty strings.");
|
|
2398
|
+
}
|
|
2399
|
+
if (seen.has(value))
|
|
2400
|
+
throw toolPolicyError(`exec.commands.values contains a duplicate: ${value}`);
|
|
2401
|
+
seen.add(value);
|
|
1788
2402
|
}
|
|
1789
2403
|
}
|
|
1790
|
-
function
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
);
|
|
2404
|
+
function assertCommandAllowed(command, policy) {
|
|
2405
|
+
if (policy === void 0) return;
|
|
2406
|
+
const included = policy.values.includes(command);
|
|
2407
|
+
if (policy.mode === "allow" && !included || policy.mode === "block" && included) {
|
|
2408
|
+
throw toolPolicyError(`Command is rejected by sandbox tool policy: ${command}`);
|
|
1796
2409
|
}
|
|
1797
2410
|
}
|
|
1798
|
-
function
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
throw new SandboxToolPolicyError("File content exceeds sandbox tool policy.");
|
|
2411
|
+
function assertTimeoutAllowed(timeoutMs, maxTimeoutMs) {
|
|
2412
|
+
if (timeoutMs !== void 0 && maxTimeoutMs !== void 0 && timeoutMs > maxTimeoutMs) {
|
|
2413
|
+
throw toolPolicyError(`Command timeout exceeds policy (${timeoutMs} > ${maxTimeoutMs}).`);
|
|
1802
2414
|
}
|
|
1803
2415
|
}
|
|
1804
2416
|
function resolveReadFileLimits(options) {
|
|
1805
2417
|
const defaultLineCount = options.readFile?.defaultLineCount ?? defaultReadFileLineCount;
|
|
1806
2418
|
const maxLineCount = options.readFile?.maxLineCount ?? defaultReadFileMaxLineCount;
|
|
1807
2419
|
const maxBytes = options.readFile?.maxBytes ?? defaultReadFileMaxBytes;
|
|
1808
|
-
if (!Number.isInteger(defaultLineCount) || defaultLineCount <= 0) {
|
|
1809
|
-
throw new SandboxToolPolicyError("File defaultLineCount must be a positive integer.");
|
|
1810
|
-
}
|
|
1811
|
-
if (!Number.isInteger(maxLineCount) || maxLineCount <= 0 || maxLineCount > maxToolReadFileLines) {
|
|
1812
|
-
throw new SandboxToolPolicyError(
|
|
1813
|
-
`File maxLineCount must be a positive integer no greater than ${maxToolReadFileLines}.`
|
|
1814
|
-
);
|
|
1815
|
-
}
|
|
1816
2420
|
if (defaultLineCount > maxLineCount) {
|
|
1817
|
-
throw
|
|
1818
|
-
`File defaultLineCount exceeds maxLineCount (${defaultLineCount} > ${maxLineCount}).`
|
|
1819
|
-
);
|
|
2421
|
+
throw toolPolicyError("readFile.defaultLineCount cannot exceed readFile.maxLineCount.");
|
|
1820
2422
|
}
|
|
1821
|
-
if (
|
|
1822
|
-
throw
|
|
1823
|
-
`File maxBytes must be a positive integer no greater than ${maxToolReadFileBytes}.`
|
|
1824
|
-
);
|
|
2423
|
+
if (maxLineCount > maxToolReadFileLines) {
|
|
2424
|
+
throw toolPolicyError(`readFile.maxLineCount cannot exceed ${maxToolReadFileLines}.`);
|
|
1825
2425
|
}
|
|
1826
2426
|
return { defaultLineCount, maxLineCount, maxBytes };
|
|
1827
2427
|
}
|
|
2428
|
+
function assertOptionalPositiveInteger(value, name) {
|
|
2429
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
2430
|
+
throw toolPolicyError(`${name} must be a positive safe integer.`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function assertOptionalNonNegativeInteger(value, name) {
|
|
2434
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) {
|
|
2435
|
+
throw toolPolicyError(`${name} must be a non-negative safe integer.`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function assertOptionalBoundedNonNegativeInteger(value, name) {
|
|
2439
|
+
assertOptionalNonNegativeInteger(value, name);
|
|
2440
|
+
if (value !== void 0 && value > maxToolBytes) {
|
|
2441
|
+
throw toolPolicyError(`${name} cannot exceed ${maxToolBytes}.`);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function assertOptionalBoundedPositiveInteger(value, name) {
|
|
2445
|
+
assertOptionalPositiveInteger(value, name);
|
|
2446
|
+
if (value !== void 0 && value > maxToolBytes) {
|
|
2447
|
+
throw toolPolicyError(`${name} cannot exceed ${maxToolBytes}.`);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
function isDockerSandboxRuntime(value) {
|
|
2451
|
+
if (!isRecord2(value)) return false;
|
|
2452
|
+
return typeof value.id === "string" && value.provider === "docker" && typeof value.workdir === "string" && Array.isArray(value.publishedPorts) && [
|
|
2453
|
+
"exec",
|
|
2454
|
+
"execStream",
|
|
2455
|
+
"readFile",
|
|
2456
|
+
"readTextFile",
|
|
2457
|
+
"readTextFilePage",
|
|
2458
|
+
"writeFile",
|
|
2459
|
+
"writeTextFile",
|
|
2460
|
+
"listFiles",
|
|
2461
|
+
"startProcess",
|
|
2462
|
+
"listProcesses",
|
|
2463
|
+
"readProcessLogs",
|
|
2464
|
+
"stopProcess",
|
|
2465
|
+
"waitForPort"
|
|
2466
|
+
].every((name) => typeof value[name] === "function");
|
|
2467
|
+
}
|
|
2468
|
+
function isRecord2(value) {
|
|
2469
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2470
|
+
}
|
|
2471
|
+
function toolPolicyError(message) {
|
|
2472
|
+
return new DockerSandboxError(message, "tool_policy");
|
|
2473
|
+
}
|
|
1828
2474
|
export {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
SandboxError,
|
|
1833
|
-
SandboxFileSizeError,
|
|
1834
|
-
SandboxPathError,
|
|
1835
|
-
SandboxPortError,
|
|
1836
|
-
SandboxProcessError,
|
|
1837
|
-
SandboxSessionDestroyedError,
|
|
1838
|
-
SandboxTimeoutError,
|
|
1839
|
-
SandboxToolPolicyError,
|
|
1840
|
-
createSandboxTools,
|
|
1841
|
-
isSandboxPortSession,
|
|
1842
|
-
isSandboxProcessSession
|
|
2475
|
+
DockerSandboxClient,
|
|
2476
|
+
DockerSandboxError,
|
|
2477
|
+
createDockerSandboxTools
|
|
1843
2478
|
};
|
|
1844
2479
|
//# sourceMappingURL=index.js.map
|