@anvia/sandbox 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -196,11 +702,14 @@ var DockerSandbox = class _DockerSandbox {
196
702
  this.pull = options.pull ?? "missing";
197
703
  this.workdir = options.workdir ?? defaultWorkdir;
198
704
  this.workspace = options.workspace ?? { mode: "ephemeral" };
199
- this.lifecycle = {
200
- autoDestroy: options.lifecycle?.autoDestroy ?? true,
201
- ...options.lifecycle?.ttlMs === void 0 ? {} : { ttlMs: options.lifecycle.ttlMs },
202
- ...options.lifecycle?.idleTimeoutMs === void 0 ? {} : { idleTimeoutMs: options.lifecycle.idleTimeoutMs }
705
+ const lifecycle = {
706
+ autoDestroy: options.lifecycle?.autoDestroy ?? true
203
707
  };
708
+ if (options.lifecycle?.ttlMs !== void 0) lifecycle.ttlMs = options.lifecycle.ttlMs;
709
+ if (options.lifecycle?.idleTimeoutMs !== void 0) {
710
+ lifecycle.idleTimeoutMs = options.lifecycle.idleTimeoutMs;
711
+ }
712
+ this.lifecycle = lifecycle;
204
713
  this.network = options.network ?? false;
205
714
  this.dockerPath = options.dockerPath ?? "docker";
206
715
  this.labels = options.labels ?? {};
@@ -223,8 +732,10 @@ var DockerSandbox = class _DockerSandbox {
223
732
  return new _DockerSandbox({ ...options, image: options.image ?? "denoland/deno:debian" });
224
733
  }
225
734
  async createSession(options = {}) {
735
+ const ports = validatePublishedPorts(options.ports ?? []);
736
+ this.assertPortNetworkCompatible(ports);
226
737
  await this.ensureImage();
227
- const id = sanitizeResourceId(options.id ?? randomUUID());
738
+ const id = sanitizeResourceId(options.id ?? randomUUID2());
228
739
  const workspace = options.workspace ?? this.workspace;
229
740
  const workspaceId = getWorkspaceId(workspace, id);
230
741
  const containerName = `anvia-sandbox-${id}`;
@@ -233,13 +744,14 @@ var DockerSandbox = class _DockerSandbox {
233
744
  await assertDockerCli(["volume", "create", volumeName], this.cliOptions());
234
745
  try {
235
746
  await assertDockerCli(
236
- this.createRunArgs(containerName, volumeName, workspace, options.metadata),
747
+ this.createRunArgs(containerName, volumeName, workspace, options.metadata, ports),
237
748
  {
238
749
  ...this.cliOptions(),
239
750
  timeoutMs: this.limits.timeoutMs ?? defaultTimeoutMs
240
751
  }
241
752
  );
242
- const session = new DockerSandboxSession({
753
+ const publishedPorts = await this.inspectPublishedPorts(containerName, ports);
754
+ const session = new DockerSandboxSessionImpl({
243
755
  id,
244
756
  containerName,
245
757
  volumeName,
@@ -249,7 +761,8 @@ var DockerSandbox = class _DockerSandbox {
249
761
  lifecycle: this.lifecycle,
250
762
  removeVolumeOnDestroy,
251
763
  env: options.manifest?.env ?? {},
252
- hooks: this.hooks
764
+ hooks: this.hooks,
765
+ publishedPorts
253
766
  });
254
767
  await session.applyManifest(options.manifest);
255
768
  await this.hooks.onSessionCreate?.(session.event());
@@ -271,7 +784,7 @@ var DockerSandbox = class _DockerSandbox {
271
784
  }
272
785
  }
273
786
  }
274
- createRunArgs(containerName, volumeName, workspace, metadata) {
787
+ createRunArgs(containerName, volumeName, workspace, metadata, ports) {
275
788
  const args = [
276
789
  "run",
277
790
  "-d",
@@ -297,6 +810,9 @@ var DockerSandbox = class _DockerSandbox {
297
810
  }
298
811
  }
299
812
  this.appendNetworkArgs(args);
813
+ for (const port of ports) {
814
+ args.push("--publish", `127.0.0.1::${port}/tcp`);
815
+ }
300
816
  this.appendLimitArgs(args);
301
817
  this.appendSecurityArgs(args);
302
818
  if (this.user !== void 0) {
@@ -320,6 +836,52 @@ var DockerSandbox = class _DockerSandbox {
320
836
  args.push("--network", mode);
321
837
  }
322
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
+ }
323
885
  appendLimitArgs(args) {
324
886
  if (this.limits.memoryMb !== void 0) {
325
887
  args.push("--memory", `${this.limits.memoryMb}m`);
@@ -357,10 +919,11 @@ var DockerSandbox = class _DockerSandbox {
357
919
  }
358
920
  }
359
921
  };
360
- var DockerSandboxSession = class {
922
+ var DockerSandboxSessionImpl = class {
361
923
  provider = "docker";
362
924
  id;
363
925
  workdir;
926
+ publishedPorts;
364
927
  containerName;
365
928
  volumeName;
366
929
  dockerPath;
@@ -369,6 +932,7 @@ var DockerSandboxSession = class {
369
932
  removeVolumeOnDestroy;
370
933
  env;
371
934
  hooks;
935
+ processManager;
372
936
  ttlTimer;
373
937
  idleTimer;
374
938
  activeOperations = 0;
@@ -384,6 +948,44 @@ var DockerSandboxSession = class {
384
948
  this.removeVolumeOnDestroy = options.removeVolumeOnDestroy;
385
949
  this.env = options.env;
386
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
+ });
387
989
  this.startLifecycleTimers();
388
990
  }
389
991
  async applyManifest(manifest) {
@@ -398,20 +1000,22 @@ var DockerSandboxSession = class {
398
1000
  }
399
1001
  async exec(options) {
400
1002
  return this.runOperation(async () => {
401
- await this.hooks.onExecStart?.({
1003
+ const startEvent = {
402
1004
  ...this.event(),
403
1005
  command: options.command,
404
- args: options.args ?? [],
405
- ...options.cwd === void 0 ? {} : { cwd: options.cwd }
406
- });
1006
+ args: options.args ?? []
1007
+ };
1008
+ if (options.cwd !== void 0) startEvent.cwd = options.cwd;
1009
+ await this.hooks.onExecStart?.(startEvent);
407
1010
  const normalizedResult = await this.execCommand(options);
408
- await this.hooks.onExecEnd?.({
1011
+ const endEvent = {
409
1012
  ...this.event(),
410
1013
  command: options.command,
411
1014
  args: options.args ?? [],
412
- ...options.cwd === void 0 ? {} : { cwd: options.cwd },
413
1015
  result: normalizedResult
414
- });
1016
+ };
1017
+ if (options.cwd !== void 0) endEvent.cwd = options.cwd;
1018
+ await this.hooks.onExecEnd?.(endEvent);
415
1019
  return normalizedResult;
416
1020
  });
417
1021
  }
@@ -463,6 +1067,49 @@ var DockerSandboxSession = class {
463
1067
  await run;
464
1068
  }
465
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
+ }
466
1113
  async readFile(filePath) {
467
1114
  return this.runOperation(async () => {
468
1115
  const normalized = normalizeSandboxPath(filePath);
@@ -532,6 +1179,7 @@ var DockerSandboxSession = class {
532
1179
  }
533
1180
  this.destroyed = true;
534
1181
  this.clearLifecycleTimers();
1182
+ await this.processManager.dispose();
535
1183
  await runDockerCli(["rm", "-f", this.containerName], this.cliOptions()).catch(() => void 0);
536
1184
  if (this.removeVolumeOnDestroy) {
537
1185
  await runDockerCli(["volume", "rm", "-f", this.volumeName], this.cliOptions()).catch(
@@ -573,6 +1221,24 @@ var DockerSandboxSession = class {
573
1221
  maxOutputBytes: this.limits.maxOutputBytes ?? defaultMaxOutputBytes2
574
1222
  };
575
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
+ }
576
1242
  async execCommand(options) {
577
1243
  if (options.command.trim().length === 0) {
578
1244
  throw new SandboxDockerCommandError("Sandbox command cannot be empty.", {
@@ -594,12 +1260,12 @@ var DockerSandboxSession = class {
594
1260
  const cliOptions = {
595
1261
  dockerPath: this.dockerPath,
596
1262
  timeoutMs: options.timeoutMs ?? this.limits.timeoutMs ?? defaultTimeoutMs,
597
- maxOutputBytes: this.limits.maxOutputBytes ?? defaultMaxOutputBytes2,
598
- ...options.input === void 0 ? {} : { input: options.input },
599
- ...options.signal === void 0 ? {} : { signal: options.signal },
600
- ...options.onStdout === void 0 ? {} : { onStdout: options.onStdout },
601
- ...options.onStderr === void 0 ? {} : { onStderr: options.onStderr }
1263
+ maxOutputBytes: this.limits.maxOutputBytes ?? defaultMaxOutputBytes2
602
1264
  };
1265
+ if (options.input !== void 0) cliOptions.input = options.input;
1266
+ if (options.signal !== void 0) cliOptions.signal = options.signal;
1267
+ if (options.onStdout !== void 0) cliOptions.onStdout = options.onStdout;
1268
+ if (options.onStderr !== void 0) cliOptions.onStderr = options.onStderr;
603
1269
  const result = await runDockerCli(args, cliOptions);
604
1270
  if (result.timedOut) {
605
1271
  return {
@@ -692,7 +1358,7 @@ function shouldDestroyWorkspace(workspace) {
692
1358
  }
693
1359
  function sanitizeResourceId(id) {
694
1360
  const sanitized = id.toLowerCase().replaceAll(/[^a-z0-9_.-]/g, "-").replaceAll(/^-+|-+$/g, "");
695
- return sanitized.length > 0 ? sanitized : randomUUID();
1361
+ return sanitized.length > 0 ? sanitized : randomUUID2();
696
1362
  }
697
1363
  function byteLength(data) {
698
1364
  return typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
@@ -709,6 +1375,50 @@ function mapFindType(type) {
709
1375
  }
710
1376
  return "other";
711
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
+ }
712
1422
 
713
1423
  // src/tools.ts
714
1424
  import { createTool } from "@anvia/core/tool";
@@ -731,7 +1441,25 @@ var writeFileInput = z.object({
731
1441
  var listFilesInput = z.object({
732
1442
  path: z.string().optional().describe("Relative directory path inside the sandbox. Defaults to root.")
733
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
+ });
734
1461
  var textOutput = z.string();
1462
+ var maxToolLogBytes = 1024 * 1024;
735
1463
  function createSandboxTools(session, options = {}) {
736
1464
  const include = new Set(
737
1465
  options.allow ?? options.include ?? ["exec_command", "read_file", "write_file", "list_files"]
@@ -749,6 +1477,29 @@ function createSandboxTools(session, options = {}) {
749
1477
  if (include.has("list_files")) {
750
1478
  tools.push(createListFilesTool(session));
751
1479
  }
1480
+ const portToolsRequested = include.has("list_ports") || include.has("wait_for_port");
1481
+ const portSession = portToolsRequested ? requirePortSession(session) : void 0;
1482
+ if (include.has("list_ports") && portSession !== void 0) {
1483
+ tools.push(createListPortsTool(portSession));
1484
+ }
1485
+ const processToolsRequested = include.has("start_process") || include.has("list_processes") || include.has("read_process_logs") || include.has("stop_process");
1486
+ if (include.has("wait_for_port") || processToolsRequested) assertProcessToolPolicy(options);
1487
+ const processSession = processToolsRequested ? requireProcessSession(session) : void 0;
1488
+ if (include.has("start_process") && processSession !== void 0) {
1489
+ tools.push(createStartProcessTool(processSession, options));
1490
+ }
1491
+ if (include.has("list_processes") && processSession !== void 0) {
1492
+ tools.push(createListProcessesTool(processSession));
1493
+ }
1494
+ if (include.has("read_process_logs") && processSession !== void 0) {
1495
+ tools.push(createReadProcessLogsTool(processSession, options));
1496
+ }
1497
+ if (include.has("stop_process") && processSession !== void 0) {
1498
+ tools.push(createStopProcessTool(processSession, options));
1499
+ }
1500
+ if (include.has("wait_for_port") && portSession !== void 0) {
1501
+ tools.push(createWaitForPortTool(portSession, options));
1502
+ }
752
1503
  return tools;
753
1504
  }
754
1505
  function createExecCommandTool(session, options) {
@@ -829,6 +1580,114 @@ function createListFilesTool(session) {
829
1580
  }
830
1581
  });
831
1582
  }
1583
+ function createListPortsTool(session) {
1584
+ return createTool({
1585
+ name: "list_ports",
1586
+ description: "List pre-authorized sandbox preview ports. Servers must bind to 0.0.0.0 on a listed container port.",
1587
+ input: emptyInput,
1588
+ output: textOutput,
1589
+ execute: async () => {
1590
+ if (session.publishedPorts.length === 0) {
1591
+ return "No sandbox ports are published.";
1592
+ }
1593
+ return session.publishedPorts.map((port) => `${port.containerPort}/${port.protocol} ${port.host}:${port.hostPort}`).join("\n");
1594
+ }
1595
+ });
1596
+ }
1597
+ function createStartProcessTool(session, options) {
1598
+ return createTool({
1599
+ name: "start_process",
1600
+ description: "Start a managed long-running process inside the sandbox. Use list_ports first and bind web servers to 0.0.0.0.",
1601
+ input: startProcessInput,
1602
+ output: textOutput,
1603
+ execute: async ({ command, args, cwd, env }) => {
1604
+ assertCommandAllowed(command, options);
1605
+ const processOptions = { command };
1606
+ if (args !== void 0) processOptions.args = args;
1607
+ if (cwd !== void 0) processOptions.cwd = cwd;
1608
+ if (env !== void 0) processOptions.env = env;
1609
+ return formatProcessInfo(await session.startProcess(processOptions));
1610
+ }
1611
+ });
1612
+ }
1613
+ function createListProcessesTool(session) {
1614
+ return createTool({
1615
+ name: "list_processes",
1616
+ description: "List managed sandbox processes and their current status.",
1617
+ input: emptyInput,
1618
+ output: textOutput,
1619
+ execute: async () => {
1620
+ const processes = await session.listProcesses();
1621
+ if (processes.length === 0) return "No managed processes.";
1622
+ return processes.map(formatProcessInfo).join("\n\n");
1623
+ }
1624
+ });
1625
+ }
1626
+ function createReadProcessLogsTool(session, options) {
1627
+ return createTool({
1628
+ name: "read_process_logs",
1629
+ description: "Read recent stdout and stderr from a managed sandbox process.",
1630
+ input: readProcessLogsInput,
1631
+ output: textOutput,
1632
+ execute: async ({ processId, tailBytes }) => {
1633
+ const configuredMaxLogBytes = options.process?.maxLogBytes ?? 64 * 1024;
1634
+ if (!Number.isInteger(configuredMaxLogBytes) || configuredMaxLogBytes < 0) {
1635
+ throw new SandboxToolPolicyError("Process maxLogBytes must be a non-negative integer.");
1636
+ }
1637
+ const maxLogBytes = Math.min(configuredMaxLogBytes, maxToolLogBytes);
1638
+ const effectiveTailBytes = tailBytes ?? maxLogBytes;
1639
+ if (effectiveTailBytes > maxLogBytes) {
1640
+ throw new SandboxToolPolicyError(
1641
+ `Process log request exceeds sandbox tool policy (${effectiveTailBytes} > ${maxLogBytes}).`
1642
+ );
1643
+ }
1644
+ const logs = await session.readProcessLogs(processId, {
1645
+ tailBytes: effectiveTailBytes
1646
+ });
1647
+ const parts = [];
1648
+ if (logs.stdout.length > 0) parts.push(`stdout:
1649
+ ${logs.stdout.trimEnd()}`);
1650
+ if (logs.stderr.length > 0) parts.push(`stderr:
1651
+ ${logs.stderr.trimEnd()}`);
1652
+ if (logs.stdoutTruncated || logs.stderrTruncated) parts.push("output_truncated: true");
1653
+ return parts.length > 0 ? parts.join("\n\n") : "No process output.";
1654
+ }
1655
+ });
1656
+ }
1657
+ function createStopProcessTool(session, options) {
1658
+ return createTool({
1659
+ name: "stop_process",
1660
+ description: "Stop a managed sandbox process.",
1661
+ input: processIdInput,
1662
+ output: textOutput,
1663
+ execute: async ({ processId }) => formatProcessInfo(
1664
+ await session.stopProcess(processId, {
1665
+ gracePeriodMs: options.process?.stopGracePeriodMs ?? 5e3
1666
+ })
1667
+ )
1668
+ });
1669
+ }
1670
+ function createWaitForPortTool(session, options) {
1671
+ return createTool({
1672
+ name: "wait_for_port",
1673
+ description: "Wait until a pre-authorized sandbox TCP port is accepting connections.",
1674
+ input: waitForPortInput,
1675
+ output: textOutput,
1676
+ execute: async ({ containerPort, timeoutMs }) => {
1677
+ const effectiveTimeoutMs = timeoutMs ?? options.process?.defaultWaitTimeoutMs ?? 3e4;
1678
+ const maxWaitTimeoutMs = options.process?.maxWaitTimeoutMs ?? 3e5;
1679
+ if (effectiveTimeoutMs > maxWaitTimeoutMs) {
1680
+ throw new SandboxToolPolicyError(
1681
+ `Port wait timeout exceeds sandbox tool policy (${effectiveTimeoutMs} > ${maxWaitTimeoutMs}).`
1682
+ );
1683
+ }
1684
+ const port = await session.waitForPort(containerPort, {
1685
+ timeoutMs: effectiveTimeoutMs
1686
+ });
1687
+ return `ready: ${port.containerPort}/${port.protocol} -> ${port.host}:${port.hostPort}`;
1688
+ }
1689
+ });
1690
+ }
832
1691
  function formatExecResult(result) {
833
1692
  const parts = [`exit_code: ${result.exitCode}`];
834
1693
  if (result.timedOut) {
@@ -850,6 +1709,50 @@ ${result.stderr.trimEnd()}`);
850
1709
  }
851
1710
  return parts.join("\n\n");
852
1711
  }
1712
+ function formatProcessInfo(process) {
1713
+ const parts = [
1714
+ `process_id: ${process.id}`,
1715
+ `status: ${process.status}`,
1716
+ `command: ${[process.command, ...process.args].join(" ")}`
1717
+ ];
1718
+ if (process.exitCode !== void 0) parts.push(`exit_code: ${process.exitCode}`);
1719
+ return parts.join("\n");
1720
+ }
1721
+ function requirePortSession(session) {
1722
+ if (!isSandboxPortSession(session)) {
1723
+ throw new SandboxToolPolicyError("The sandbox session does not support published port tools.");
1724
+ }
1725
+ return session;
1726
+ }
1727
+ function requireProcessSession(session) {
1728
+ if (!isSandboxProcessSession(session)) {
1729
+ throw new SandboxToolPolicyError("The sandbox session does not support managed process tools.");
1730
+ }
1731
+ return session;
1732
+ }
1733
+ function assertProcessToolPolicy(options) {
1734
+ const policy = options.process;
1735
+ if (policy === void 0) return;
1736
+ if (policy.maxLogBytes !== void 0 && (!Number.isInteger(policy.maxLogBytes) || policy.maxLogBytes < 0 || policy.maxLogBytes > maxToolLogBytes)) {
1737
+ throw new SandboxToolPolicyError(
1738
+ `Process maxLogBytes must be an integer from 0 to ${maxToolLogBytes}.`
1739
+ );
1740
+ }
1741
+ const maxWaitTimeoutMs = policy.maxWaitTimeoutMs ?? 3e5;
1742
+ if (!Number.isInteger(maxWaitTimeoutMs) || maxWaitTimeoutMs <= 0 || maxWaitTimeoutMs > 3e5) {
1743
+ throw new SandboxToolPolicyError(
1744
+ "Process maxWaitTimeoutMs must be an integer from 1 to 300000."
1745
+ );
1746
+ }
1747
+ if (policy.defaultWaitTimeoutMs !== void 0 && (!Number.isInteger(policy.defaultWaitTimeoutMs) || policy.defaultWaitTimeoutMs <= 0 || policy.defaultWaitTimeoutMs > maxWaitTimeoutMs)) {
1748
+ throw new SandboxToolPolicyError(
1749
+ "Process defaultWaitTimeoutMs must be positive and no greater than maxWaitTimeoutMs."
1750
+ );
1751
+ }
1752
+ if (policy.stopGracePeriodMs !== void 0 && (!Number.isInteger(policy.stopGracePeriodMs) || policy.stopGracePeriodMs < 0)) {
1753
+ throw new SandboxToolPolicyError("Process stopGracePeriodMs must be a non-negative integer.");
1754
+ }
1755
+ }
853
1756
  function assertCommandAllowed(command, options) {
854
1757
  const policy = options.exec;
855
1758
  if (policy?.blockedCommands?.includes(command)) {
@@ -886,9 +1789,13 @@ export {
886
1789
  SandboxError,
887
1790
  SandboxFileSizeError,
888
1791
  SandboxPathError,
1792
+ SandboxPortError,
1793
+ SandboxProcessError,
889
1794
  SandboxSessionDestroyedError,
890
1795
  SandboxTimeoutError,
891
1796
  SandboxToolPolicyError,
892
- createSandboxTools
1797
+ createSandboxTools,
1798
+ isSandboxPortSession,
1799
+ isSandboxProcessSession
893
1800
  };
894
1801
  //# sourceMappingURL=index.js.map