@anvia/sandbox 0.3.7 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +75 -3
- package/dist/index.js +918 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
// src/capabilities.ts
|
|
2
|
+
function isSandboxPortSession(session) {
|
|
3
|
+
const candidate = session;
|
|
4
|
+
return Array.isArray(candidate.publishedPorts) && typeof candidate.waitForPort === "function";
|
|
5
|
+
}
|
|
6
|
+
function isSandboxProcessSession(session) {
|
|
7
|
+
const candidate = session;
|
|
8
|
+
return typeof candidate.startProcess === "function" && typeof candidate.listProcesses === "function" && typeof candidate.readProcessLogs === "function" && typeof candidate.stopProcess === "function";
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// src/docker-sandbox.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
12
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3
13
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
4
14
|
import os from "os";
|
|
5
15
|
import path2 from "path";
|
|
@@ -35,6 +45,10 @@ var SandboxFileSizeError = class extends SandboxError {
|
|
|
35
45
|
};
|
|
36
46
|
var SandboxToolPolicyError = class extends SandboxError {
|
|
37
47
|
};
|
|
48
|
+
var SandboxPortError = class extends SandboxError {
|
|
49
|
+
};
|
|
50
|
+
var SandboxProcessError = class extends SandboxError {
|
|
51
|
+
};
|
|
38
52
|
|
|
39
53
|
// src/docker-cli.ts
|
|
40
54
|
var defaultMaxOutputBytes = 1024 * 1024;
|
|
@@ -138,6 +152,10 @@ function createOutputCollector(maxBytes, onChunk) {
|
|
|
138
152
|
};
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
// src/docker-process.ts
|
|
156
|
+
import { spawn as spawn2 } from "child_process";
|
|
157
|
+
import { randomUUID } from "crypto";
|
|
158
|
+
|
|
141
159
|
// src/path.ts
|
|
142
160
|
import path from "path";
|
|
143
161
|
function normalizeSandboxPath(input, options = {}) {
|
|
@@ -172,11 +190,499 @@ function parentSandboxPath(relativePath) {
|
|
|
172
190
|
return parent === "." ? "." : parent;
|
|
173
191
|
}
|
|
174
192
|
|
|
193
|
+
// src/docker-process.ts
|
|
194
|
+
var processMarkerPrefix = "ANVIA_PROCESS";
|
|
195
|
+
var processWrapper = [
|
|
196
|
+
'marker="$1"',
|
|
197
|
+
"shift",
|
|
198
|
+
"if command -v setsid >/dev/null 2>&1; then",
|
|
199
|
+
' setsid "$@" &',
|
|
200
|
+
" child=$!",
|
|
201
|
+
" group=$child",
|
|
202
|
+
"else",
|
|
203
|
+
' "$@" &',
|
|
204
|
+
" child=$!",
|
|
205
|
+
" group=$$",
|
|
206
|
+
"fi",
|
|
207
|
+
`printf '\\036%s:%s:%s:%s\\036' "$marker" "$$" "$child" "$group"`,
|
|
208
|
+
"terminate() {",
|
|
209
|
+
' wait "$child" 2>/dev/null || true',
|
|
210
|
+
" exit 143",
|
|
211
|
+
"}",
|
|
212
|
+
"trap terminate TERM INT",
|
|
213
|
+
'wait "$child"',
|
|
214
|
+
"exit $?"
|
|
215
|
+
].join("\n");
|
|
216
|
+
var DockerProcessManager = class {
|
|
217
|
+
constructor(options) {
|
|
218
|
+
this.options = options;
|
|
219
|
+
if (!Number.isInteger(options.maxProcesses) || options.maxProcesses < 0) {
|
|
220
|
+
throw new SandboxProcessError("Sandbox maxProcesses must be a non-negative integer.");
|
|
221
|
+
}
|
|
222
|
+
if (!Number.isInteger(options.maxOutputBytes) || options.maxOutputBytes < 0) {
|
|
223
|
+
throw new SandboxProcessError("Sandbox maxOutputBytes must be a non-negative integer.");
|
|
224
|
+
}
|
|
225
|
+
if (!Number.isInteger(options.startupTimeoutMs) || options.startupTimeoutMs <= 0) {
|
|
226
|
+
throw new SandboxProcessError("Sandbox process startup timeout must be a positive integer.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
options;
|
|
230
|
+
records = /* @__PURE__ */ new Map();
|
|
231
|
+
disposed = false;
|
|
232
|
+
async start(options) {
|
|
233
|
+
this.assertActive();
|
|
234
|
+
assertStartOptions(options);
|
|
235
|
+
await this.pruneCompletedRecords();
|
|
236
|
+
const trackedCount = this.records.size;
|
|
237
|
+
if (trackedCount >= this.options.maxProcesses) {
|
|
238
|
+
throw new SandboxProcessError(
|
|
239
|
+
`Sandbox process limit reached (${trackedCount} >= ${this.options.maxProcesses}).`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const id = randomUUID();
|
|
243
|
+
const marker = `${processMarkerPrefix}:${id}`;
|
|
244
|
+
const dockerArgs = this.createExecArgs(options, marker);
|
|
245
|
+
const child = spawn2(this.options.dockerPath, dockerArgs, {
|
|
246
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
247
|
+
});
|
|
248
|
+
child.stdin.end();
|
|
249
|
+
let resolveStarted;
|
|
250
|
+
let rejectStarted;
|
|
251
|
+
const started = new Promise((resolve, reject) => {
|
|
252
|
+
resolveStarted = resolve;
|
|
253
|
+
rejectStarted = reject;
|
|
254
|
+
});
|
|
255
|
+
let resolveClosed;
|
|
256
|
+
const closed = new Promise((resolve) => {
|
|
257
|
+
resolveClosed = resolve;
|
|
258
|
+
});
|
|
259
|
+
const info = {
|
|
260
|
+
id,
|
|
261
|
+
command: options.command,
|
|
262
|
+
args: [...options.args ?? []],
|
|
263
|
+
status: "running",
|
|
264
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
265
|
+
};
|
|
266
|
+
if (options.cwd !== void 0) info.cwd = options.cwd;
|
|
267
|
+
const record = {
|
|
268
|
+
info,
|
|
269
|
+
startedAtMs: Date.now(),
|
|
270
|
+
child,
|
|
271
|
+
stdout: new TailOutputCollector(this.options.maxOutputBytes),
|
|
272
|
+
stderr: new TailOutputCollector(this.options.maxOutputBytes),
|
|
273
|
+
markerStart: Buffer.from(`${marker}:`),
|
|
274
|
+
markerBuffer: Buffer.alloc(0),
|
|
275
|
+
spawnFailed: false,
|
|
276
|
+
stopRequested: false,
|
|
277
|
+
startResolved: false,
|
|
278
|
+
exitNotified: false,
|
|
279
|
+
resolveStarted,
|
|
280
|
+
rejectStarted,
|
|
281
|
+
started,
|
|
282
|
+
resolveClosed,
|
|
283
|
+
closed
|
|
284
|
+
};
|
|
285
|
+
this.records.set(id, record);
|
|
286
|
+
this.observe(record);
|
|
287
|
+
try {
|
|
288
|
+
await this.waitForStart(record);
|
|
289
|
+
await this.options.onStart?.(copyProcessInfo(record.info));
|
|
290
|
+
record.startResolved = true;
|
|
291
|
+
if (record.info.status !== "running") this.notifyExit(record);
|
|
292
|
+
return copyProcessInfo(record.info);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (await this.cleanupFailedStart(record)) this.records.delete(id);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
list() {
|
|
299
|
+
this.assertActive();
|
|
300
|
+
return [...this.records.values()].map((record) => copyProcessInfo(record.info));
|
|
301
|
+
}
|
|
302
|
+
logs(processId, options = {}) {
|
|
303
|
+
this.assertActive();
|
|
304
|
+
const record = this.getRecord(processId);
|
|
305
|
+
const stdout = record.stdout.snapshot(options.tailBytes);
|
|
306
|
+
const stderr = record.stderr.snapshot(options.tailBytes);
|
|
307
|
+
return {
|
|
308
|
+
stdout: stdout.text,
|
|
309
|
+
stderr: stderr.text,
|
|
310
|
+
stdoutTruncated: stdout.truncated,
|
|
311
|
+
stderrTruncated: stderr.truncated
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
async stop(processId, options = {}) {
|
|
315
|
+
this.assertActive();
|
|
316
|
+
const record = this.getRecord(processId);
|
|
317
|
+
const gracePeriodMs = options.gracePeriodMs ?? 5e3;
|
|
318
|
+
if (!Number.isInteger(gracePeriodMs) || gracePeriodMs < 0) {
|
|
319
|
+
throw new SandboxProcessError("Process gracePeriodMs must be a non-negative integer.");
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
if (!await this.terminateRecord(record, gracePeriodMs)) {
|
|
323
|
+
throw new SandboxProcessError(`Sandbox process did not stop: ${processId}`);
|
|
324
|
+
}
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (record.info.status === "running") record.stopRequested = false;
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
return copyProcessInfo(record.info);
|
|
330
|
+
}
|
|
331
|
+
async dispose() {
|
|
332
|
+
if (this.disposed) return;
|
|
333
|
+
this.disposed = true;
|
|
334
|
+
await Promise.all(
|
|
335
|
+
[...this.records.values()].map(async (record) => {
|
|
336
|
+
await this.terminateRecord(record, 1e3).catch(() => false);
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
createExecArgs(options, marker) {
|
|
341
|
+
const args = ["exec", "-w", containerPath(this.options.workdir, options.cwd ?? ".")];
|
|
342
|
+
for (const [key, value] of Object.entries({ ...this.options.env, ...options.env })) {
|
|
343
|
+
args.push("-e", `${key}=${value}`);
|
|
344
|
+
}
|
|
345
|
+
args.push(
|
|
346
|
+
this.options.containerName,
|
|
347
|
+
"sh",
|
|
348
|
+
"-c",
|
|
349
|
+
processWrapper,
|
|
350
|
+
"anvia-managed-process",
|
|
351
|
+
marker,
|
|
352
|
+
options.command,
|
|
353
|
+
...options.args ?? []
|
|
354
|
+
);
|
|
355
|
+
return args;
|
|
356
|
+
}
|
|
357
|
+
observe(record) {
|
|
358
|
+
record.child.stdout.on("data", (chunk) => this.acceptStdout(record, chunk));
|
|
359
|
+
record.child.stderr.on("data", (chunk) => record.stderr.accept(chunk));
|
|
360
|
+
record.child.on("error", (error) => {
|
|
361
|
+
record.spawnFailed = true;
|
|
362
|
+
const normalized = error.code === "ENOENT" ? new SandboxDockerUnavailableError("Docker CLI was not found.", error) : error;
|
|
363
|
+
record.rejectStarted(normalized);
|
|
364
|
+
});
|
|
365
|
+
record.child.on("close", (code) => {
|
|
366
|
+
if (record.markerBuffer.length > 0) {
|
|
367
|
+
record.stdout.accept(record.markerBuffer);
|
|
368
|
+
record.markerBuffer = Buffer.alloc(0);
|
|
369
|
+
}
|
|
370
|
+
record.info.status = record.stopRequested ? "stopped" : "exited";
|
|
371
|
+
record.info.exitCode = code ?? 1;
|
|
372
|
+
record.info.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
373
|
+
record.rejectStarted(
|
|
374
|
+
new SandboxProcessError(
|
|
375
|
+
`Sandbox process exited before startup completed: ${record.info.id}`
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
record.resolveClosed();
|
|
379
|
+
if (record.startResolved) this.notifyExit(record);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
acceptStdout(record, chunk) {
|
|
383
|
+
if (record.supervisorPid !== void 0) {
|
|
384
|
+
record.stdout.accept(chunk);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
record.markerBuffer = Buffer.concat([record.markerBuffer, chunk]);
|
|
388
|
+
const start = record.markerBuffer.indexOf(record.markerStart);
|
|
389
|
+
if (start < 0) {
|
|
390
|
+
const retainedBytes = Math.max(0, record.markerStart.length - 1);
|
|
391
|
+
if (record.markerBuffer.length > retainedBytes) {
|
|
392
|
+
const split = record.markerBuffer.length - retainedBytes;
|
|
393
|
+
record.stdout.accept(record.markerBuffer.subarray(0, split));
|
|
394
|
+
record.markerBuffer = record.markerBuffer.subarray(split);
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const end = record.markerBuffer.indexOf(30, start + record.markerStart.length);
|
|
399
|
+
if (end < 0) {
|
|
400
|
+
if (start > 0) {
|
|
401
|
+
record.stdout.accept(record.markerBuffer.subarray(0, start));
|
|
402
|
+
record.markerBuffer = record.markerBuffer.subarray(start);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const rawPids = record.markerBuffer.subarray(start + record.markerStart.length, end).toString("utf8").split(":");
|
|
407
|
+
const supervisorPid = Number(rawPids[0]);
|
|
408
|
+
const childPid = Number(rawPids[1]);
|
|
409
|
+
const processGroupId = Number(rawPids[2]);
|
|
410
|
+
if (!isProcessId(supervisorPid) || !isProcessId(childPid) || !isProcessId(processGroupId)) {
|
|
411
|
+
record.rejectStarted(
|
|
412
|
+
new SandboxProcessError("Sandbox process returned invalid process IDs.")
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (start > 0) record.stdout.accept(record.markerBuffer.subarray(0, start));
|
|
417
|
+
if (end + 1 < record.markerBuffer.length) {
|
|
418
|
+
record.stdout.accept(record.markerBuffer.subarray(end + 1));
|
|
419
|
+
}
|
|
420
|
+
record.markerBuffer = Buffer.alloc(0);
|
|
421
|
+
record.supervisorPid = supervisorPid;
|
|
422
|
+
record.childPid = childPid;
|
|
423
|
+
record.processGroupId = processGroupId;
|
|
424
|
+
record.resolveStarted();
|
|
425
|
+
}
|
|
426
|
+
async waitForStart(record) {
|
|
427
|
+
let timeout;
|
|
428
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
429
|
+
timeout = setTimeout(() => {
|
|
430
|
+
reject(new SandboxTimeoutError("Starting sandbox process timed out."));
|
|
431
|
+
}, this.options.startupTimeoutMs);
|
|
432
|
+
timeout.unref?.();
|
|
433
|
+
});
|
|
434
|
+
try {
|
|
435
|
+
await Promise.race([record.started, timeoutPromise]);
|
|
436
|
+
} finally {
|
|
437
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async cleanupFailedStart(record) {
|
|
441
|
+
record.stopRequested = true;
|
|
442
|
+
if (record.processGroupId === void 0 && record.info.status === "running") {
|
|
443
|
+
await waitForPromise(record.closed, 250);
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
return await this.terminateRecord(record, 1e3);
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async terminateRecord(record, gracePeriodMs) {
|
|
452
|
+
record.stopRequested = true;
|
|
453
|
+
if (record.processGroupId === void 0) {
|
|
454
|
+
if (record.info.status === "running") {
|
|
455
|
+
await waitForPromise(record.closed, Math.min(gracePeriodMs, 250));
|
|
456
|
+
}
|
|
457
|
+
if (record.processGroupId === void 0) {
|
|
458
|
+
return record.spawnFailed && record.info.status !== "running";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!await this.isProcessGroupRunning(record)) {
|
|
462
|
+
return this.finishAfterGroupExit(record);
|
|
463
|
+
}
|
|
464
|
+
await this.signal(record, "TERM");
|
|
465
|
+
if (await this.waitForRecordExit(record, gracePeriodMs)) return true;
|
|
466
|
+
await this.signal(record, "KILL");
|
|
467
|
+
if (await this.waitForRecordExit(record, 1e3)) return true;
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
async waitForRecordExit(record, timeoutMs) {
|
|
471
|
+
const deadline = Date.now() + timeoutMs;
|
|
472
|
+
while (true) {
|
|
473
|
+
if (!await this.isProcessGroupRunning(record)) {
|
|
474
|
+
return this.finishAfterGroupExit(record);
|
|
475
|
+
}
|
|
476
|
+
const remainingMs = deadline - Date.now();
|
|
477
|
+
if (remainingMs <= 0) return false;
|
|
478
|
+
const intervalMs = Math.min(50, remainingMs);
|
|
479
|
+
if (record.info.status === "running") {
|
|
480
|
+
await waitForPromise(record.closed, intervalMs);
|
|
481
|
+
} else {
|
|
482
|
+
await waitForDelay(intervalMs);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async finishAfterGroupExit(record) {
|
|
487
|
+
if (record.info.status !== "running") return true;
|
|
488
|
+
record.child.kill("SIGKILL");
|
|
489
|
+
return waitForPromise(record.closed, 1e3);
|
|
490
|
+
}
|
|
491
|
+
async isProcessGroupRunning(record) {
|
|
492
|
+
if (record.processGroupId === void 0) return false;
|
|
493
|
+
const result = await runDockerCli(
|
|
494
|
+
[
|
|
495
|
+
"exec",
|
|
496
|
+
this.options.containerName,
|
|
497
|
+
"sh",
|
|
498
|
+
"-c",
|
|
499
|
+
'kill -0 "-$1" 2>/dev/null',
|
|
500
|
+
"anvia-process-group-check",
|
|
501
|
+
String(record.processGroupId)
|
|
502
|
+
],
|
|
503
|
+
{
|
|
504
|
+
dockerPath: this.options.dockerPath,
|
|
505
|
+
timeoutMs: 5e3,
|
|
506
|
+
maxOutputBytes: this.options.maxOutputBytes
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
return result.exitCode === 0;
|
|
510
|
+
}
|
|
511
|
+
async signal(record, signal) {
|
|
512
|
+
if (record.processGroupId === void 0) return;
|
|
513
|
+
const result = await runDockerCli(
|
|
514
|
+
[
|
|
515
|
+
"exec",
|
|
516
|
+
this.options.containerName,
|
|
517
|
+
"sh",
|
|
518
|
+
"-c",
|
|
519
|
+
'kill "-$2" "-$1" 2>/dev/null || ! kill -0 "-$1" 2>/dev/null',
|
|
520
|
+
"anvia-process-signal",
|
|
521
|
+
String(record.processGroupId),
|
|
522
|
+
signal
|
|
523
|
+
],
|
|
524
|
+
{
|
|
525
|
+
dockerPath: this.options.dockerPath,
|
|
526
|
+
timeoutMs: 5e3,
|
|
527
|
+
maxOutputBytes: this.options.maxOutputBytes
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
if (result.exitCode !== 0) {
|
|
531
|
+
throw new SandboxDockerCommandError(
|
|
532
|
+
`Unable to stop sandbox process: ${record.info.id}`,
|
|
533
|
+
result
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
getRecord(processId) {
|
|
538
|
+
const record = this.records.get(processId);
|
|
539
|
+
if (record === void 0) {
|
|
540
|
+
throw new SandboxProcessError(`Unknown sandbox process: ${processId}`);
|
|
541
|
+
}
|
|
542
|
+
return record;
|
|
543
|
+
}
|
|
544
|
+
async pruneCompletedRecords() {
|
|
545
|
+
for (const [id, record] of this.records) {
|
|
546
|
+
if (this.records.size < this.options.maxProcesses) return;
|
|
547
|
+
const cleanupConfirmed = record.processGroupId === void 0 ? record.spawnFailed : !await this.isProcessGroupRunning(record);
|
|
548
|
+
if (record.info.status !== "running" && cleanupConfirmed) {
|
|
549
|
+
this.records.delete(id);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
logsUnsafe(record) {
|
|
554
|
+
const stdout = record.stdout.snapshot();
|
|
555
|
+
const stderr = record.stderr.snapshot();
|
|
556
|
+
return {
|
|
557
|
+
stdout: stdout.text,
|
|
558
|
+
stderr: stderr.text,
|
|
559
|
+
stdoutTruncated: stdout.truncated,
|
|
560
|
+
stderrTruncated: stderr.truncated
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
notifyExit(record) {
|
|
564
|
+
if (record.exitNotified) return;
|
|
565
|
+
record.exitNotified = true;
|
|
566
|
+
const durationMs = Date.now() - record.startedAtMs;
|
|
567
|
+
const notify = async () => this.options.onExit?.(copyProcessInfo(record.info), this.logsUnsafe(record), durationMs);
|
|
568
|
+
void notify().catch(() => void 0);
|
|
569
|
+
}
|
|
570
|
+
assertActive() {
|
|
571
|
+
if (this.disposed) {
|
|
572
|
+
throw new SandboxProcessError("Sandbox process manager has been disposed.");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
var TailOutputCollector = class {
|
|
577
|
+
constructor(maxBytes) {
|
|
578
|
+
this.maxBytes = maxBytes;
|
|
579
|
+
}
|
|
580
|
+
maxBytes;
|
|
581
|
+
chunks = [];
|
|
582
|
+
length = 0;
|
|
583
|
+
didTruncate = false;
|
|
584
|
+
accept(chunk) {
|
|
585
|
+
if (chunk.length === 0) return;
|
|
586
|
+
if (this.maxBytes <= 0) {
|
|
587
|
+
this.didTruncate = true;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (chunk.length >= this.maxBytes) {
|
|
591
|
+
this.chunks = [chunk.subarray(chunk.length - this.maxBytes)];
|
|
592
|
+
this.length = this.maxBytes;
|
|
593
|
+
this.didTruncate = true;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.chunks.push(chunk);
|
|
597
|
+
this.length += chunk.length;
|
|
598
|
+
while (this.length > this.maxBytes) {
|
|
599
|
+
const first = this.chunks[0];
|
|
600
|
+
if (first === void 0) break;
|
|
601
|
+
const overflow = this.length - this.maxBytes;
|
|
602
|
+
if (first.length <= overflow) {
|
|
603
|
+
this.chunks.shift();
|
|
604
|
+
this.length -= first.length;
|
|
605
|
+
} else {
|
|
606
|
+
this.chunks[0] = first.subarray(overflow);
|
|
607
|
+
this.length -= overflow;
|
|
608
|
+
}
|
|
609
|
+
this.didTruncate = true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
snapshot(tailBytes) {
|
|
613
|
+
if (tailBytes !== void 0 && (!Number.isInteger(tailBytes) || tailBytes < 0)) {
|
|
614
|
+
throw new SandboxProcessError("Process tailBytes must be a non-negative integer.");
|
|
615
|
+
}
|
|
616
|
+
const bytes = Buffer.concat(this.chunks, this.length);
|
|
617
|
+
const selected = tailBytes === 0 ? Buffer.alloc(0) : tailBytes === void 0 || bytes.length <= tailBytes ? bytes : bytes.subarray(bytes.length - tailBytes);
|
|
618
|
+
return {
|
|
619
|
+
text: selected.toString("utf8"),
|
|
620
|
+
truncated: this.didTruncate || selected.length < bytes.length
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
function assertStartOptions(options) {
|
|
625
|
+
if (options.command.trim().length === 0) {
|
|
626
|
+
throw new SandboxProcessError("Sandbox process command cannot be empty.");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function isProcessId(value) {
|
|
630
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
631
|
+
}
|
|
632
|
+
function copyProcessInfo(info) {
|
|
633
|
+
const copy = {
|
|
634
|
+
id: info.id,
|
|
635
|
+
command: info.command,
|
|
636
|
+
args: [...info.args],
|
|
637
|
+
status: info.status,
|
|
638
|
+
startedAt: info.startedAt
|
|
639
|
+
};
|
|
640
|
+
if (info.cwd !== void 0) copy.cwd = info.cwd;
|
|
641
|
+
if (info.exitCode !== void 0) copy.exitCode = info.exitCode;
|
|
642
|
+
if (info.endedAt !== void 0) copy.endedAt = info.endedAt;
|
|
643
|
+
return copy;
|
|
644
|
+
}
|
|
645
|
+
async function waitForPromise(promise, timeoutMs) {
|
|
646
|
+
let timeout;
|
|
647
|
+
try {
|
|
648
|
+
return await Promise.race([
|
|
649
|
+
promise.then(() => true),
|
|
650
|
+
new Promise((resolve) => {
|
|
651
|
+
timeout = setTimeout(() => resolve(false), timeoutMs);
|
|
652
|
+
timeout.unref?.();
|
|
653
|
+
})
|
|
654
|
+
]);
|
|
655
|
+
} finally {
|
|
656
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function waitForDelay(timeoutMs) {
|
|
660
|
+
await new Promise((resolve) => {
|
|
661
|
+
const timeout = setTimeout(resolve, timeoutMs);
|
|
662
|
+
timeout.unref?.();
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
175
666
|
// src/docker-sandbox.ts
|
|
176
667
|
var defaultImage = "node:22-bookworm";
|
|
177
668
|
var defaultWorkdir = "/workspace";
|
|
178
669
|
var defaultTimeoutMs = 3e4;
|
|
179
670
|
var defaultMaxOutputBytes2 = 1024 * 1024;
|
|
671
|
+
var defaultMaxProcesses = 4;
|
|
672
|
+
var portProbeScript = [
|
|
673
|
+
`port="$(printf '%04X' "$1")"`,
|
|
674
|
+
"for table in /proc/net/tcp /proc/net/tcp6; do",
|
|
675
|
+
' [ -r "$table" ] || continue',
|
|
676
|
+
" while read -r _ local _ state _; do",
|
|
677
|
+
' case "$local" in',
|
|
678
|
+
' "00000000:$port"|"00000000000000000000000000000000:$port")',
|
|
679
|
+
' [ "$state" = "0A" ] && exit 0',
|
|
680
|
+
" ;;",
|
|
681
|
+
" esac",
|
|
682
|
+
' done < "$table"',
|
|
683
|
+
"done",
|
|
684
|
+
"exit 1"
|
|
685
|
+
].join("\n");
|
|
180
686
|
var DockerSandbox = class _DockerSandbox {
|
|
181
687
|
provider = "docker";
|
|
182
688
|
image;
|
|
@@ -226,8 +732,10 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
226
732
|
return new _DockerSandbox({ ...options, image: options.image ?? "denoland/deno:debian" });
|
|
227
733
|
}
|
|
228
734
|
async createSession(options = {}) {
|
|
735
|
+
const ports = validatePublishedPorts(options.ports ?? []);
|
|
736
|
+
this.assertPortNetworkCompatible(ports);
|
|
229
737
|
await this.ensureImage();
|
|
230
|
-
const id = sanitizeResourceId(options.id ??
|
|
738
|
+
const id = sanitizeResourceId(options.id ?? randomUUID2());
|
|
231
739
|
const workspace = options.workspace ?? this.workspace;
|
|
232
740
|
const workspaceId = getWorkspaceId(workspace, id);
|
|
233
741
|
const containerName = `anvia-sandbox-${id}`;
|
|
@@ -236,13 +744,14 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
236
744
|
await assertDockerCli(["volume", "create", volumeName], this.cliOptions());
|
|
237
745
|
try {
|
|
238
746
|
await assertDockerCli(
|
|
239
|
-
this.createRunArgs(containerName, volumeName, workspace, options.metadata),
|
|
747
|
+
this.createRunArgs(containerName, volumeName, workspace, options.metadata, ports),
|
|
240
748
|
{
|
|
241
749
|
...this.cliOptions(),
|
|
242
750
|
timeoutMs: this.limits.timeoutMs ?? defaultTimeoutMs
|
|
243
751
|
}
|
|
244
752
|
);
|
|
245
|
-
const
|
|
753
|
+
const publishedPorts = await this.inspectPublishedPorts(containerName, ports);
|
|
754
|
+
const session = new DockerSandboxSessionImpl({
|
|
246
755
|
id,
|
|
247
756
|
containerName,
|
|
248
757
|
volumeName,
|
|
@@ -252,7 +761,8 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
252
761
|
lifecycle: this.lifecycle,
|
|
253
762
|
removeVolumeOnDestroy,
|
|
254
763
|
env: options.manifest?.env ?? {},
|
|
255
|
-
hooks: this.hooks
|
|
764
|
+
hooks: this.hooks,
|
|
765
|
+
publishedPorts
|
|
256
766
|
});
|
|
257
767
|
await session.applyManifest(options.manifest);
|
|
258
768
|
await this.hooks.onSessionCreate?.(session.event());
|
|
@@ -274,7 +784,7 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
274
784
|
}
|
|
275
785
|
}
|
|
276
786
|
}
|
|
277
|
-
createRunArgs(containerName, volumeName, workspace, metadata) {
|
|
787
|
+
createRunArgs(containerName, volumeName, workspace, metadata, ports) {
|
|
278
788
|
const args = [
|
|
279
789
|
"run",
|
|
280
790
|
"-d",
|
|
@@ -300,6 +810,9 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
300
810
|
}
|
|
301
811
|
}
|
|
302
812
|
this.appendNetworkArgs(args);
|
|
813
|
+
for (const port of ports) {
|
|
814
|
+
args.push("--publish", `127.0.0.1::${port}/tcp`);
|
|
815
|
+
}
|
|
303
816
|
this.appendLimitArgs(args);
|
|
304
817
|
this.appendSecurityArgs(args);
|
|
305
818
|
if (this.user !== void 0) {
|
|
@@ -323,6 +836,52 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
323
836
|
args.push("--network", mode);
|
|
324
837
|
}
|
|
325
838
|
}
|
|
839
|
+
assertPortNetworkCompatible(ports) {
|
|
840
|
+
if (ports.length === 0) return;
|
|
841
|
+
const mode = typeof this.network === "object" ? this.network.mode : this.network;
|
|
842
|
+
if (mode === false || mode === "none" || mode === "host" || typeof mode === "string" && mode.startsWith("container:")) {
|
|
843
|
+
throw new SandboxPortError(
|
|
844
|
+
"Published sandbox ports require network: true or a bridge-capable Docker network."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async inspectPublishedPorts(containerName, ports) {
|
|
849
|
+
if (ports.length === 0) return [];
|
|
850
|
+
const raw = await assertDockerCli(
|
|
851
|
+
["container", "inspect", "--format", "{{json .NetworkSettings.Ports}}", containerName],
|
|
852
|
+
this.cliOptions()
|
|
853
|
+
);
|
|
854
|
+
let mappings;
|
|
855
|
+
try {
|
|
856
|
+
mappings = JSON.parse(raw);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
throw new SandboxPortError("Docker returned invalid published port metadata.", error);
|
|
859
|
+
}
|
|
860
|
+
if (!isRecord(mappings)) {
|
|
861
|
+
throw new SandboxPortError("Docker returned invalid published port metadata.");
|
|
862
|
+
}
|
|
863
|
+
return ports.map((containerPort) => {
|
|
864
|
+
const entries = mappings[`${containerPort}/tcp`];
|
|
865
|
+
if (!Array.isArray(entries)) {
|
|
866
|
+
throw new SandboxPortError(`Docker did not publish sandbox port ${containerPort}/tcp.`);
|
|
867
|
+
}
|
|
868
|
+
const entry = entries.find(
|
|
869
|
+
(candidate) => isRecord(candidate) && candidate.HostIp === "127.0.0.1" && typeof candidate.HostPort === "string"
|
|
870
|
+
);
|
|
871
|
+
if (!isRecord(entry) || typeof entry.HostPort !== "string") {
|
|
872
|
+
throw new SandboxPortError(
|
|
873
|
+
`Docker did not bind sandbox port ${containerPort}/tcp to 127.0.0.1.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const hostPort = Number(entry.HostPort);
|
|
877
|
+
if (!isValidPort(hostPort)) {
|
|
878
|
+
throw new SandboxPortError(
|
|
879
|
+
`Docker returned an invalid host port for ${containerPort}/tcp.`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
return { containerPort, host: "127.0.0.1", hostPort, protocol: "tcp" };
|
|
883
|
+
});
|
|
884
|
+
}
|
|
326
885
|
appendLimitArgs(args) {
|
|
327
886
|
if (this.limits.memoryMb !== void 0) {
|
|
328
887
|
args.push("--memory", `${this.limits.memoryMb}m`);
|
|
@@ -360,10 +919,11 @@ var DockerSandbox = class _DockerSandbox {
|
|
|
360
919
|
}
|
|
361
920
|
}
|
|
362
921
|
};
|
|
363
|
-
var
|
|
922
|
+
var DockerSandboxSessionImpl = class {
|
|
364
923
|
provider = "docker";
|
|
365
924
|
id;
|
|
366
925
|
workdir;
|
|
926
|
+
publishedPorts;
|
|
367
927
|
containerName;
|
|
368
928
|
volumeName;
|
|
369
929
|
dockerPath;
|
|
@@ -372,6 +932,7 @@ var DockerSandboxSession = class {
|
|
|
372
932
|
removeVolumeOnDestroy;
|
|
373
933
|
env;
|
|
374
934
|
hooks;
|
|
935
|
+
processManager;
|
|
375
936
|
ttlTimer;
|
|
376
937
|
idleTimer;
|
|
377
938
|
activeOperations = 0;
|
|
@@ -387,6 +948,44 @@ var DockerSandboxSession = class {
|
|
|
387
948
|
this.removeVolumeOnDestroy = options.removeVolumeOnDestroy;
|
|
388
949
|
this.env = options.env;
|
|
389
950
|
this.hooks = options.hooks;
|
|
951
|
+
this.publishedPorts = options.publishedPorts;
|
|
952
|
+
this.processManager = new DockerProcessManager({
|
|
953
|
+
containerName: this.containerName,
|
|
954
|
+
dockerPath: this.dockerPath,
|
|
955
|
+
workdir: this.workdir,
|
|
956
|
+
env: this.env,
|
|
957
|
+
maxOutputBytes: this.limits.maxOutputBytes ?? defaultMaxOutputBytes2,
|
|
958
|
+
maxProcesses: this.limits.maxProcesses ?? defaultMaxProcesses,
|
|
959
|
+
startupTimeoutMs: this.limits.timeoutMs ?? defaultTimeoutMs,
|
|
960
|
+
onStart: async (process) => {
|
|
961
|
+
const event = {
|
|
962
|
+
...this.event(),
|
|
963
|
+
command: process.command,
|
|
964
|
+
args: process.args
|
|
965
|
+
};
|
|
966
|
+
if (process.cwd !== void 0) event.cwd = process.cwd;
|
|
967
|
+
await this.hooks.onExecStart?.(event);
|
|
968
|
+
},
|
|
969
|
+
onExit: async (process, logs, durationMs) => {
|
|
970
|
+
const event = {
|
|
971
|
+
...this.event(),
|
|
972
|
+
command: process.command,
|
|
973
|
+
args: process.args,
|
|
974
|
+
result: {
|
|
975
|
+
stdout: logs.stdout,
|
|
976
|
+
stderr: logs.stderr,
|
|
977
|
+
exitCode: process.exitCode ?? 1,
|
|
978
|
+
durationMs,
|
|
979
|
+
timedOut: false,
|
|
980
|
+
aborted: process.status === "stopped",
|
|
981
|
+
stdoutTruncated: logs.stdoutTruncated,
|
|
982
|
+
stderrTruncated: logs.stderrTruncated
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
if (process.cwd !== void 0) event.cwd = process.cwd;
|
|
986
|
+
await this.hooks.onExecEnd?.(event);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
390
989
|
this.startLifecycleTimers();
|
|
391
990
|
}
|
|
392
991
|
async applyManifest(manifest) {
|
|
@@ -468,6 +1067,49 @@ var DockerSandboxSession = class {
|
|
|
468
1067
|
await run;
|
|
469
1068
|
}
|
|
470
1069
|
}
|
|
1070
|
+
async startProcess(options) {
|
|
1071
|
+
return this.runOperation(async () => this.processManager.start(options));
|
|
1072
|
+
}
|
|
1073
|
+
async listProcesses() {
|
|
1074
|
+
return this.runOperation(async () => this.processManager.list());
|
|
1075
|
+
}
|
|
1076
|
+
async readProcessLogs(processId, options) {
|
|
1077
|
+
return this.runOperation(async () => this.processManager.logs(processId, options));
|
|
1078
|
+
}
|
|
1079
|
+
async stopProcess(processId, options) {
|
|
1080
|
+
return this.runOperation(async () => this.processManager.stop(processId, options));
|
|
1081
|
+
}
|
|
1082
|
+
async waitForPort(containerPort, options = {}) {
|
|
1083
|
+
return this.runOperation(async () => {
|
|
1084
|
+
const publishedPort = this.publishedPorts.find(
|
|
1085
|
+
(candidate) => candidate.containerPort === containerPort
|
|
1086
|
+
);
|
|
1087
|
+
if (publishedPort === void 0) {
|
|
1088
|
+
throw new SandboxPortError(`Sandbox port is not published: ${containerPort}/tcp`);
|
|
1089
|
+
}
|
|
1090
|
+
const timeoutMs = options.timeoutMs ?? this.limits.timeoutMs ?? defaultTimeoutMs;
|
|
1091
|
+
const intervalMs = options.intervalMs ?? 250;
|
|
1092
|
+
assertWaitOptions(timeoutMs, intervalMs);
|
|
1093
|
+
const deadline = Date.now() + timeoutMs;
|
|
1094
|
+
while (true) {
|
|
1095
|
+
this.assertActive();
|
|
1096
|
+
if (options.signal?.aborted === true) throw abortReason(options.signal);
|
|
1097
|
+
const remainingMs = deadline - Date.now();
|
|
1098
|
+
if (remainingMs <= 0) {
|
|
1099
|
+
throw new SandboxTimeoutError(`Waiting for sandbox port ${containerPort}/tcp timed out.`);
|
|
1100
|
+
}
|
|
1101
|
+
const probeTimeoutMs = Math.min(1e3, remainingMs);
|
|
1102
|
+
if (await this.isPortListening(containerPort, probeTimeoutMs)) {
|
|
1103
|
+
return publishedPort;
|
|
1104
|
+
}
|
|
1105
|
+
const remainingAfterProbeMs = deadline - Date.now();
|
|
1106
|
+
if (remainingAfterProbeMs <= 0) {
|
|
1107
|
+
throw new SandboxTimeoutError(`Waiting for sandbox port ${containerPort}/tcp timed out.`);
|
|
1108
|
+
}
|
|
1109
|
+
await waitWithSignal(Math.min(intervalMs, remainingAfterProbeMs), options.signal);
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
471
1113
|
async readFile(filePath) {
|
|
472
1114
|
return this.runOperation(async () => {
|
|
473
1115
|
const normalized = normalizeSandboxPath(filePath);
|
|
@@ -537,6 +1179,7 @@ var DockerSandboxSession = class {
|
|
|
537
1179
|
}
|
|
538
1180
|
this.destroyed = true;
|
|
539
1181
|
this.clearLifecycleTimers();
|
|
1182
|
+
await this.processManager.dispose();
|
|
540
1183
|
await runDockerCli(["rm", "-f", this.containerName], this.cliOptions()).catch(() => void 0);
|
|
541
1184
|
if (this.removeVolumeOnDestroy) {
|
|
542
1185
|
await runDockerCli(["volume", "rm", "-f", this.volumeName], this.cliOptions()).catch(
|
|
@@ -578,6 +1221,24 @@ var DockerSandboxSession = class {
|
|
|
578
1221
|
maxOutputBytes: this.limits.maxOutputBytes ?? defaultMaxOutputBytes2
|
|
579
1222
|
};
|
|
580
1223
|
}
|
|
1224
|
+
async isPortListening(containerPort, timeoutMs) {
|
|
1225
|
+
const result = await runDockerCli(
|
|
1226
|
+
[
|
|
1227
|
+
"exec",
|
|
1228
|
+
this.containerName,
|
|
1229
|
+
"sh",
|
|
1230
|
+
"-c",
|
|
1231
|
+
portProbeScript,
|
|
1232
|
+
"anvia-port-probe",
|
|
1233
|
+
String(containerPort)
|
|
1234
|
+
],
|
|
1235
|
+
{
|
|
1236
|
+
...this.cliOptions(),
|
|
1237
|
+
timeoutMs: Math.max(1, timeoutMs)
|
|
1238
|
+
}
|
|
1239
|
+
);
|
|
1240
|
+
return result.exitCode === 0;
|
|
1241
|
+
}
|
|
581
1242
|
async execCommand(options) {
|
|
582
1243
|
if (options.command.trim().length === 0) {
|
|
583
1244
|
throw new SandboxDockerCommandError("Sandbox command cannot be empty.", {
|
|
@@ -697,7 +1358,7 @@ function shouldDestroyWorkspace(workspace) {
|
|
|
697
1358
|
}
|
|
698
1359
|
function sanitizeResourceId(id) {
|
|
699
1360
|
const sanitized = id.toLowerCase().replaceAll(/[^a-z0-9_.-]/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
700
|
-
return sanitized.length > 0 ? sanitized :
|
|
1361
|
+
return sanitized.length > 0 ? sanitized : randomUUID2();
|
|
701
1362
|
}
|
|
702
1363
|
function byteLength(data) {
|
|
703
1364
|
return typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
@@ -714,6 +1375,50 @@ function mapFindType(type) {
|
|
|
714
1375
|
}
|
|
715
1376
|
return "other";
|
|
716
1377
|
}
|
|
1378
|
+
function validatePublishedPorts(ports) {
|
|
1379
|
+
const unique = /* @__PURE__ */ new Set();
|
|
1380
|
+
for (const port of ports) {
|
|
1381
|
+
if (!isValidPort(port)) {
|
|
1382
|
+
throw new SandboxPortError(`Sandbox port must be an integer from 1 to 65535: ${port}`);
|
|
1383
|
+
}
|
|
1384
|
+
if (unique.has(port)) {
|
|
1385
|
+
throw new SandboxPortError(`Sandbox port is duplicated: ${port}`);
|
|
1386
|
+
}
|
|
1387
|
+
unique.add(port);
|
|
1388
|
+
}
|
|
1389
|
+
return [...unique];
|
|
1390
|
+
}
|
|
1391
|
+
function isValidPort(port) {
|
|
1392
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
1393
|
+
}
|
|
1394
|
+
function isRecord(value) {
|
|
1395
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1396
|
+
}
|
|
1397
|
+
function assertWaitOptions(timeoutMs, intervalMs) {
|
|
1398
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
1399
|
+
throw new SandboxPortError("Port wait timeoutMs must be a positive integer.");
|
|
1400
|
+
}
|
|
1401
|
+
if (!Number.isInteger(intervalMs) || intervalMs <= 0) {
|
|
1402
|
+
throw new SandboxPortError("Port wait intervalMs must be a positive integer.");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async function waitWithSignal(timeoutMs, signal) {
|
|
1406
|
+
if (signal?.aborted === true) throw abortReason(signal);
|
|
1407
|
+
await new Promise((resolve, reject) => {
|
|
1408
|
+
const timeout = setTimeout(() => {
|
|
1409
|
+
signal?.removeEventListener("abort", abort);
|
|
1410
|
+
resolve();
|
|
1411
|
+
}, timeoutMs);
|
|
1412
|
+
const abort = () => {
|
|
1413
|
+
clearTimeout(timeout);
|
|
1414
|
+
reject(abortReason(signal));
|
|
1415
|
+
};
|
|
1416
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
function abortReason(signal) {
|
|
1420
|
+
return signal?.reason ?? new SandboxPortError("Waiting for sandbox port was aborted.");
|
|
1421
|
+
}
|
|
717
1422
|
|
|
718
1423
|
// src/tools.ts
|
|
719
1424
|
import { createTool } from "@anvia/core/tool";
|
|
@@ -736,7 +1441,26 @@ var writeFileInput = z.object({
|
|
|
736
1441
|
var listFilesInput = z.object({
|
|
737
1442
|
path: z.string().optional().describe("Relative directory path inside the sandbox. Defaults to root.")
|
|
738
1443
|
});
|
|
1444
|
+
var emptyInput = z.object({});
|
|
1445
|
+
var startProcessInput = z.object({
|
|
1446
|
+
command: z.string().min(1).describe("Executable to run as a managed sandbox process."),
|
|
1447
|
+
args: z.array(z.string()).optional().describe("Command arguments."),
|
|
1448
|
+
cwd: z.string().optional().describe("Relative working directory inside the sandbox."),
|
|
1449
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables for this process.")
|
|
1450
|
+
});
|
|
1451
|
+
var processIdInput = z.object({
|
|
1452
|
+
processId: z.string().min(1).describe("Managed process identifier.")
|
|
1453
|
+
});
|
|
1454
|
+
var readProcessLogsInput = processIdInput.extend({
|
|
1455
|
+
tailBytes: z.number().int().nonnegative().max(1024 * 1024).optional().describe("Maximum trailing bytes to return from each output stream.")
|
|
1456
|
+
});
|
|
1457
|
+
var waitForPortInput = z.object({
|
|
1458
|
+
containerPort: z.number().int().min(1).max(65535).describe("Pre-authorized TCP port inside the sandbox container."),
|
|
1459
|
+
timeoutMs: z.number().int().positive().max(3e5).optional().describe("Maximum time to wait for the port to accept connections.")
|
|
1460
|
+
});
|
|
739
1461
|
var textOutput = z.string();
|
|
1462
|
+
var maxToolLogBytes = 1024 * 1024;
|
|
1463
|
+
var sandboxToolMetadataKey = /* @__PURE__ */ Symbol.for("anvia.sandbox.tool.metadata");
|
|
740
1464
|
function createSandboxTools(session, options = {}) {
|
|
741
1465
|
const include = new Set(
|
|
742
1466
|
options.allow ?? options.include ?? ["exec_command", "read_file", "write_file", "list_files"]
|
|
@@ -754,6 +1478,35 @@ function createSandboxTools(session, options = {}) {
|
|
|
754
1478
|
if (include.has("list_files")) {
|
|
755
1479
|
tools.push(createListFilesTool(session));
|
|
756
1480
|
}
|
|
1481
|
+
const portToolsRequested = include.has("list_ports") || include.has("wait_for_port");
|
|
1482
|
+
const portSession = portToolsRequested ? requirePortSession(session) : void 0;
|
|
1483
|
+
if (include.has("list_ports") && portSession !== void 0) {
|
|
1484
|
+
tools.push(createListPortsTool(portSession));
|
|
1485
|
+
}
|
|
1486
|
+
const processToolsRequested = include.has("start_process") || include.has("list_processes") || include.has("read_process_logs") || include.has("stop_process");
|
|
1487
|
+
if (include.has("wait_for_port") || processToolsRequested) assertProcessToolPolicy(options);
|
|
1488
|
+
const processSession = processToolsRequested ? requireProcessSession(session) : void 0;
|
|
1489
|
+
if (include.has("start_process") && processSession !== void 0) {
|
|
1490
|
+
tools.push(createStartProcessTool(processSession, options));
|
|
1491
|
+
}
|
|
1492
|
+
if (include.has("list_processes") && processSession !== void 0) {
|
|
1493
|
+
tools.push(createListProcessesTool(processSession));
|
|
1494
|
+
}
|
|
1495
|
+
if (include.has("read_process_logs") && processSession !== void 0) {
|
|
1496
|
+
tools.push(createReadProcessLogsTool(processSession, options));
|
|
1497
|
+
}
|
|
1498
|
+
if (include.has("stop_process") && processSession !== void 0) {
|
|
1499
|
+
tools.push(createStopProcessTool(processSession, options));
|
|
1500
|
+
}
|
|
1501
|
+
if (include.has("wait_for_port") && portSession !== void 0) {
|
|
1502
|
+
tools.push(createWaitForPortTool(portSession, options));
|
|
1503
|
+
}
|
|
1504
|
+
for (const tool of tools) {
|
|
1505
|
+
Object.defineProperty(tool, sandboxToolMetadataKey, {
|
|
1506
|
+
value: { session },
|
|
1507
|
+
enumerable: false
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
757
1510
|
return tools;
|
|
758
1511
|
}
|
|
759
1512
|
function createExecCommandTool(session, options) {
|
|
@@ -834,6 +1587,114 @@ function createListFilesTool(session) {
|
|
|
834
1587
|
}
|
|
835
1588
|
});
|
|
836
1589
|
}
|
|
1590
|
+
function createListPortsTool(session) {
|
|
1591
|
+
return createTool({
|
|
1592
|
+
name: "list_ports",
|
|
1593
|
+
description: "List pre-authorized sandbox preview ports. Servers must bind to 0.0.0.0 on a listed container port.",
|
|
1594
|
+
input: emptyInput,
|
|
1595
|
+
output: textOutput,
|
|
1596
|
+
execute: async () => {
|
|
1597
|
+
if (session.publishedPorts.length === 0) {
|
|
1598
|
+
return "No sandbox ports are published.";
|
|
1599
|
+
}
|
|
1600
|
+
return session.publishedPorts.map((port) => `${port.containerPort}/${port.protocol} ${port.host}:${port.hostPort}`).join("\n");
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
function createStartProcessTool(session, options) {
|
|
1605
|
+
return createTool({
|
|
1606
|
+
name: "start_process",
|
|
1607
|
+
description: "Start a managed long-running process inside the sandbox. Use list_ports first and bind web servers to 0.0.0.0.",
|
|
1608
|
+
input: startProcessInput,
|
|
1609
|
+
output: textOutput,
|
|
1610
|
+
execute: async ({ command, args, cwd, env }) => {
|
|
1611
|
+
assertCommandAllowed(command, options);
|
|
1612
|
+
const processOptions = { command };
|
|
1613
|
+
if (args !== void 0) processOptions.args = args;
|
|
1614
|
+
if (cwd !== void 0) processOptions.cwd = cwd;
|
|
1615
|
+
if (env !== void 0) processOptions.env = env;
|
|
1616
|
+
return formatProcessInfo(await session.startProcess(processOptions));
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
function createListProcessesTool(session) {
|
|
1621
|
+
return createTool({
|
|
1622
|
+
name: "list_processes",
|
|
1623
|
+
description: "List managed sandbox processes and their current status.",
|
|
1624
|
+
input: emptyInput,
|
|
1625
|
+
output: textOutput,
|
|
1626
|
+
execute: async () => {
|
|
1627
|
+
const processes = await session.listProcesses();
|
|
1628
|
+
if (processes.length === 0) return "No managed processes.";
|
|
1629
|
+
return processes.map(formatProcessInfo).join("\n\n");
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
function createReadProcessLogsTool(session, options) {
|
|
1634
|
+
return createTool({
|
|
1635
|
+
name: "read_process_logs",
|
|
1636
|
+
description: "Read recent stdout and stderr from a managed sandbox process.",
|
|
1637
|
+
input: readProcessLogsInput,
|
|
1638
|
+
output: textOutput,
|
|
1639
|
+
execute: async ({ processId, tailBytes }) => {
|
|
1640
|
+
const configuredMaxLogBytes = options.process?.maxLogBytes ?? 64 * 1024;
|
|
1641
|
+
if (!Number.isInteger(configuredMaxLogBytes) || configuredMaxLogBytes < 0) {
|
|
1642
|
+
throw new SandboxToolPolicyError("Process maxLogBytes must be a non-negative integer.");
|
|
1643
|
+
}
|
|
1644
|
+
const maxLogBytes = Math.min(configuredMaxLogBytes, maxToolLogBytes);
|
|
1645
|
+
const effectiveTailBytes = tailBytes ?? maxLogBytes;
|
|
1646
|
+
if (effectiveTailBytes > maxLogBytes) {
|
|
1647
|
+
throw new SandboxToolPolicyError(
|
|
1648
|
+
`Process log request exceeds sandbox tool policy (${effectiveTailBytes} > ${maxLogBytes}).`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
const logs = await session.readProcessLogs(processId, {
|
|
1652
|
+
tailBytes: effectiveTailBytes
|
|
1653
|
+
});
|
|
1654
|
+
const parts = [];
|
|
1655
|
+
if (logs.stdout.length > 0) parts.push(`stdout:
|
|
1656
|
+
${logs.stdout.trimEnd()}`);
|
|
1657
|
+
if (logs.stderr.length > 0) parts.push(`stderr:
|
|
1658
|
+
${logs.stderr.trimEnd()}`);
|
|
1659
|
+
if (logs.stdoutTruncated || logs.stderrTruncated) parts.push("output_truncated: true");
|
|
1660
|
+
return parts.length > 0 ? parts.join("\n\n") : "No process output.";
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
function createStopProcessTool(session, options) {
|
|
1665
|
+
return createTool({
|
|
1666
|
+
name: "stop_process",
|
|
1667
|
+
description: "Stop a managed sandbox process.",
|
|
1668
|
+
input: processIdInput,
|
|
1669
|
+
output: textOutput,
|
|
1670
|
+
execute: async ({ processId }) => formatProcessInfo(
|
|
1671
|
+
await session.stopProcess(processId, {
|
|
1672
|
+
gracePeriodMs: options.process?.stopGracePeriodMs ?? 5e3
|
|
1673
|
+
})
|
|
1674
|
+
)
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
function createWaitForPortTool(session, options) {
|
|
1678
|
+
return createTool({
|
|
1679
|
+
name: "wait_for_port",
|
|
1680
|
+
description: "Wait until a pre-authorized sandbox TCP port is accepting connections.",
|
|
1681
|
+
input: waitForPortInput,
|
|
1682
|
+
output: textOutput,
|
|
1683
|
+
execute: async ({ containerPort, timeoutMs }) => {
|
|
1684
|
+
const effectiveTimeoutMs = timeoutMs ?? options.process?.defaultWaitTimeoutMs ?? 3e4;
|
|
1685
|
+
const maxWaitTimeoutMs = options.process?.maxWaitTimeoutMs ?? 3e5;
|
|
1686
|
+
if (effectiveTimeoutMs > maxWaitTimeoutMs) {
|
|
1687
|
+
throw new SandboxToolPolicyError(
|
|
1688
|
+
`Port wait timeout exceeds sandbox tool policy (${effectiveTimeoutMs} > ${maxWaitTimeoutMs}).`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
const port = await session.waitForPort(containerPort, {
|
|
1692
|
+
timeoutMs: effectiveTimeoutMs
|
|
1693
|
+
});
|
|
1694
|
+
return `ready: ${port.containerPort}/${port.protocol} -> ${port.host}:${port.hostPort}`;
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
837
1698
|
function formatExecResult(result) {
|
|
838
1699
|
const parts = [`exit_code: ${result.exitCode}`];
|
|
839
1700
|
if (result.timedOut) {
|
|
@@ -855,6 +1716,50 @@ ${result.stderr.trimEnd()}`);
|
|
|
855
1716
|
}
|
|
856
1717
|
return parts.join("\n\n");
|
|
857
1718
|
}
|
|
1719
|
+
function formatProcessInfo(process) {
|
|
1720
|
+
const parts = [
|
|
1721
|
+
`process_id: ${process.id}`,
|
|
1722
|
+
`status: ${process.status}`,
|
|
1723
|
+
`command: ${[process.command, ...process.args].join(" ")}`
|
|
1724
|
+
];
|
|
1725
|
+
if (process.exitCode !== void 0) parts.push(`exit_code: ${process.exitCode}`);
|
|
1726
|
+
return parts.join("\n");
|
|
1727
|
+
}
|
|
1728
|
+
function requirePortSession(session) {
|
|
1729
|
+
if (!isSandboxPortSession(session)) {
|
|
1730
|
+
throw new SandboxToolPolicyError("The sandbox session does not support published port tools.");
|
|
1731
|
+
}
|
|
1732
|
+
return session;
|
|
1733
|
+
}
|
|
1734
|
+
function requireProcessSession(session) {
|
|
1735
|
+
if (!isSandboxProcessSession(session)) {
|
|
1736
|
+
throw new SandboxToolPolicyError("The sandbox session does not support managed process tools.");
|
|
1737
|
+
}
|
|
1738
|
+
return session;
|
|
1739
|
+
}
|
|
1740
|
+
function assertProcessToolPolicy(options) {
|
|
1741
|
+
const policy = options.process;
|
|
1742
|
+
if (policy === void 0) return;
|
|
1743
|
+
if (policy.maxLogBytes !== void 0 && (!Number.isInteger(policy.maxLogBytes) || policy.maxLogBytes < 0 || policy.maxLogBytes > maxToolLogBytes)) {
|
|
1744
|
+
throw new SandboxToolPolicyError(
|
|
1745
|
+
`Process maxLogBytes must be an integer from 0 to ${maxToolLogBytes}.`
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
const maxWaitTimeoutMs = policy.maxWaitTimeoutMs ?? 3e5;
|
|
1749
|
+
if (!Number.isInteger(maxWaitTimeoutMs) || maxWaitTimeoutMs <= 0 || maxWaitTimeoutMs > 3e5) {
|
|
1750
|
+
throw new SandboxToolPolicyError(
|
|
1751
|
+
"Process maxWaitTimeoutMs must be an integer from 1 to 300000."
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
if (policy.defaultWaitTimeoutMs !== void 0 && (!Number.isInteger(policy.defaultWaitTimeoutMs) || policy.defaultWaitTimeoutMs <= 0 || policy.defaultWaitTimeoutMs > maxWaitTimeoutMs)) {
|
|
1755
|
+
throw new SandboxToolPolicyError(
|
|
1756
|
+
"Process defaultWaitTimeoutMs must be positive and no greater than maxWaitTimeoutMs."
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
if (policy.stopGracePeriodMs !== void 0 && (!Number.isInteger(policy.stopGracePeriodMs) || policy.stopGracePeriodMs < 0)) {
|
|
1760
|
+
throw new SandboxToolPolicyError("Process stopGracePeriodMs must be a non-negative integer.");
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
858
1763
|
function assertCommandAllowed(command, options) {
|
|
859
1764
|
const policy = options.exec;
|
|
860
1765
|
if (policy?.blockedCommands?.includes(command)) {
|
|
@@ -891,9 +1796,13 @@ export {
|
|
|
891
1796
|
SandboxError,
|
|
892
1797
|
SandboxFileSizeError,
|
|
893
1798
|
SandboxPathError,
|
|
1799
|
+
SandboxPortError,
|
|
1800
|
+
SandboxProcessError,
|
|
894
1801
|
SandboxSessionDestroyedError,
|
|
895
1802
|
SandboxTimeoutError,
|
|
896
1803
|
SandboxToolPolicyError,
|
|
897
|
-
createSandboxTools
|
|
1804
|
+
createSandboxTools,
|
|
1805
|
+
isSandboxPortSession,
|
|
1806
|
+
isSandboxProcessSession
|
|
898
1807
|
};
|
|
899
1808
|
//# sourceMappingURL=index.js.map
|