@hasna/sandboxes 0.1.7 → 0.1.9

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/mcp/index.js CHANGED
@@ -147,10 +147,11 @@ class E2BProvider {
147
147
  onStderr: opts?.onStderr ? (data) => opts.onStderr(data) : undefined,
148
148
  envs: opts?.env,
149
149
  cwd: opts?.cwd,
150
- timeoutMs: opts?.timeout ? opts.timeout * 1000 : undefined
150
+ timeoutMs: opts?.timeout ? opts.timeout * 1000 : undefined,
151
+ ...opts?.stdin !== undefined ? { stdin: opts.stdin } : {}
151
152
  });
152
153
  return {
153
- exit_code: result.exitCode,
154
+ exit_code: result.exitCode ?? 0,
154
155
  stdout: result.stdout,
155
156
  stderr: result.stderr
156
157
  };
@@ -158,10 +159,28 @@ class E2BProvider {
158
159
  throw new ProviderError("e2b", `Failed to exec command: ${err.message}`);
159
160
  }
160
161
  }
161
- async readFile(sandboxId, path) {
162
+ async readFile(sandboxId, path, opts) {
162
163
  const sandbox = await this.getInstance(sandboxId);
163
164
  try {
164
- return await sandbox.files.read(path, { format: "text" });
165
+ if (opts?.encoding === "base64") {
166
+ const bytes = await sandbox.files.read(path, { format: "bytes" });
167
+ const sliced = opts.offset !== undefined || opts.limit !== undefined ? bytes.slice(opts.offset ?? 0, opts.limit !== undefined ? (opts.offset ?? 0) + opts.limit : undefined) : bytes;
168
+ return Buffer.from(sliced).toString("base64");
169
+ } else if (opts?.encoding === "hex") {
170
+ const bytes = await sandbox.files.read(path, { format: "bytes" });
171
+ const sliced = opts.offset !== undefined || opts.limit !== undefined ? bytes.slice(opts.offset ?? 0, opts.limit !== undefined ? (opts.offset ?? 0) + opts.limit : undefined) : bytes;
172
+ return Buffer.from(sliced).toString("hex");
173
+ } else {
174
+ const content = await sandbox.files.read(path, { format: "text" });
175
+ if (opts?.offset !== undefined || opts?.limit !== undefined) {
176
+ const lines = content.split(`
177
+ `);
178
+ const sliced = lines.slice(opts.offset ?? 0, opts.limit !== undefined ? (opts.offset ?? 0) + opts.limit : undefined);
179
+ return sliced.join(`
180
+ `);
181
+ }
182
+ return content;
183
+ }
165
184
  } catch (err) {
166
185
  throw new ProviderError("e2b", `Failed to read file ${path}: ${err.message}`);
167
186
  }
@@ -174,9 +193,21 @@ class E2BProvider {
174
193
  throw new ProviderError("e2b", `Failed to write file ${path}: ${err.message}`);
175
194
  }
176
195
  }
177
- async listFiles(sandboxId, path) {
196
+ async listFiles(sandboxId, path, opts) {
178
197
  const sandbox = await this.getInstance(sandboxId);
179
198
  try {
199
+ if (opts?.recursive || opts?.glob) {
200
+ const pattern = opts.glob ? opts.glob : "*";
201
+ const cmd = opts.recursive ? `find ${JSON.stringify(path)} -name ${JSON.stringify(pattern)} 2>/dev/null | head -500` : `ls -la ${JSON.stringify(path)}/${pattern} 2>/dev/null`;
202
+ const result = await sandbox.commands.run(cmd);
203
+ return result.stdout.trim().split(`
204
+ `).filter(Boolean).map((p) => ({
205
+ path: p.trim(),
206
+ name: p.trim().split("/").pop() || p.trim(),
207
+ is_dir: false,
208
+ size: 0
209
+ }));
210
+ }
180
211
  const entries = await sandbox.files.list(path);
181
212
  return entries.map((e) => ({
182
213
  path: e.path,
@@ -217,6 +248,15 @@ class E2BProvider {
217
248
  throw new ProviderError("e2b", `Failed to resume sandbox: ${err.message}`);
218
249
  }
219
250
  }
251
+ async getPublicUrl(sandboxId, port, _protocol) {
252
+ const sandbox = await this.getInstance(sandboxId);
253
+ try {
254
+ const host = sandbox.getHost(port);
255
+ return `https://${host}`;
256
+ } catch (err) {
257
+ throw new ProviderError("e2b", `Failed to get public URL for port ${port}: ${err.message}`);
258
+ }
259
+ }
220
260
  async keepAlive(sandboxId, durationMs) {
221
261
  const sandbox = await this.getInstance(sandboxId);
222
262
  try {
@@ -377,6 +417,9 @@ class DaytonaProvider {
377
417
  throw new ProviderError("daytona", `Failed to delete sandbox: ${err.message}`);
378
418
  }
379
419
  }
420
+ async getPublicUrl(_sandboxId, _port, _protocol) {
421
+ throw new ProviderError("daytona", "Port forwarding not supported by Daytona provider");
422
+ }
380
423
  async pause(_sandboxId) {
381
424
  throw new ProviderError("daytona", "Pause/resume not supported by Daytona provider");
382
425
  }
@@ -605,6 +648,9 @@ class ModalProvider {
605
648
  async delete(sandboxId) {
606
649
  await this.stop(sandboxId);
607
650
  }
651
+ async getPublicUrl(_sandboxId, _port, _protocol) {
652
+ throw new ProviderError("modal", "Port forwarding not supported by Modal provider");
653
+ }
608
654
  async pause(_sandboxId) {
609
655
  throw new ProviderError("modal", "Pause/resume not supported by Modal provider");
610
656
  }
@@ -4795,6 +4841,24 @@ ALTER TABLE sandbox_sessions_new RENAME TO sandbox_sessions;
4795
4841
  CREATE INDEX IF NOT EXISTS idx_sessions_sandbox ON sandbox_sessions(sandbox_id);
4796
4842
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sandbox_sessions(status);
4797
4843
  INSERT OR IGNORE INTO _migrations (id) VALUES (3);
4844
+ `,
4845
+ `
4846
+ CREATE TABLE IF NOT EXISTS snapshots (
4847
+ id TEXT PRIMARY KEY,
4848
+ sandbox_id TEXT NOT NULL,
4849
+ provider_sandbox_id TEXT NOT NULL,
4850
+ provider TEXT NOT NULL,
4851
+ name TEXT,
4852
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
4853
+ );
4854
+ CREATE INDEX IF NOT EXISTS idx_snapshots_sandbox ON snapshots(sandbox_id);
4855
+ INSERT OR IGNORE INTO _migrations (id) VALUES (4);
4856
+ `,
4857
+ `
4858
+ ALTER TABLE sandboxes ADD COLUMN budget_limit_usd REAL;
4859
+ ALTER TABLE sandboxes ADD COLUMN on_budget_exceeded TEXT NOT NULL DEFAULT 'terminate' CHECK(on_budget_exceeded IN ('terminate', 'pause', 'notify'));
4860
+ ALTER TABLE sandboxes ADD COLUMN started_at TEXT;
4861
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
4798
4862
  `
4799
4863
  ];
4800
4864
  var db = null;
@@ -4855,6 +4919,9 @@ function rowToSandbox(row) {
4855
4919
  project_id: row.project_id,
4856
4920
  on_timeout: row.on_timeout ?? "terminate",
4857
4921
  auto_resume: row.auto_resume === 1,
4922
+ budget_limit_usd: row.budget_limit_usd ?? null,
4923
+ on_budget_exceeded: row.on_budget_exceeded ?? "terminate",
4924
+ started_at: row.started_at ?? null,
4858
4925
  created_at: row.created_at,
4859
4926
  updated_at: row.updated_at
4860
4927
  };
@@ -4872,8 +4939,10 @@ function createSandbox(input) {
4872
4939
  const project_id = input.project_id ?? null;
4873
4940
  const on_timeout = input.on_timeout ?? "terminate";
4874
4941
  const auto_resume = input.auto_resume ? 1 : 0;
4875
- db2.query(`INSERT INTO sandboxes (id, provider, name, status, image, timeout, config, env_vars, project_id, on_timeout, auto_resume, created_at, updated_at)
4876
- VALUES (?, ?, ?, 'creating', ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider, name, image, timeout, config, env_vars, project_id, on_timeout, auto_resume, timestamp, timestamp);
4942
+ const budget_limit_usd = input.budget_limit_usd ?? null;
4943
+ const on_budget_exceeded = input.on_budget_exceeded ?? "terminate";
4944
+ db2.query(`INSERT INTO sandboxes (id, provider, name, status, image, timeout, config, env_vars, project_id, on_timeout, auto_resume, budget_limit_usd, on_budget_exceeded, created_at, updated_at)
4945
+ VALUES (?, ?, ?, 'creating', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider, name, image, timeout, config, env_vars, project_id, on_timeout, auto_resume, budget_limit_usd, on_budget_exceeded, timestamp, timestamp);
4877
4946
  return getSandbox(id);
4878
4947
  }
4879
4948
  function getSandbox(id) {
@@ -4945,6 +5014,10 @@ function updateSandbox(id, updates) {
4945
5014
  setClauses.push("keep_alive_until = ?");
4946
5015
  params.push(updates.keep_alive_until);
4947
5016
  }
5017
+ if (updates.started_at !== undefined) {
5018
+ setClauses.push("started_at = ?");
5019
+ params.push(updates.started_at);
5020
+ }
4948
5021
  if (setClauses.length === 0) {
4949
5022
  return getSandbox(resolvedId);
4950
5023
  }
@@ -5183,6 +5256,46 @@ function deleteTemplate(id) {
5183
5256
  db2.query("DELETE FROM templates WHERE id = ?").run(resolvedId);
5184
5257
  }
5185
5258
 
5259
+ // src/db/snapshots.ts
5260
+ class SnapshotNotFoundError extends Error {
5261
+ constructor(id) {
5262
+ super(`Snapshot not found: ${id}`);
5263
+ this.name = "SnapshotNotFoundError";
5264
+ }
5265
+ }
5266
+ function createSnapshot(input) {
5267
+ const db2 = getDatabase();
5268
+ const id = uuid();
5269
+ const timestamp = now();
5270
+ db2.query(`INSERT INTO snapshots (id, sandbox_id, provider_sandbox_id, provider, name, created_at)
5271
+ VALUES (?, ?, ?, ?, ?, ?)`).run(id, input.sandbox_id, input.provider_sandbox_id, input.provider, input.name ?? null, timestamp);
5272
+ return getSnapshot(id);
5273
+ }
5274
+ function getSnapshot(id) {
5275
+ const db2 = getDatabase();
5276
+ const resolvedId = resolvePartialId("snapshots", id);
5277
+ if (!resolvedId)
5278
+ throw new SnapshotNotFoundError(id);
5279
+ const row = db2.query("SELECT * FROM snapshots WHERE id = ?").get(resolvedId);
5280
+ if (!row)
5281
+ throw new SnapshotNotFoundError(id);
5282
+ return row;
5283
+ }
5284
+ function listSnapshots(sandboxId) {
5285
+ const db2 = getDatabase();
5286
+ if (sandboxId) {
5287
+ return db2.query("SELECT * FROM snapshots WHERE sandbox_id = ? ORDER BY created_at DESC").all(sandboxId);
5288
+ }
5289
+ return db2.query("SELECT * FROM snapshots ORDER BY created_at DESC").all();
5290
+ }
5291
+ function deleteSnapshot(id) {
5292
+ const db2 = getDatabase();
5293
+ const resolvedId = resolvePartialId("snapshots", id);
5294
+ if (!resolvedId)
5295
+ throw new SnapshotNotFoundError(id);
5296
+ db2.query("DELETE FROM snapshots WHERE id = ?").run(resolvedId);
5297
+ }
5298
+
5186
5299
  // src/providers/index.ts
5187
5300
  init_types();
5188
5301
 
@@ -5426,13 +5539,23 @@ function getAgentDriver(name) {
5426
5539
  }
5427
5540
 
5428
5541
  // src/lib/agent-runner.ts
5542
+ async function fireWebhook(url, payload) {
5543
+ try {
5544
+ await fetch(url, {
5545
+ method: "POST",
5546
+ headers: { "Content-Type": "application/json" },
5547
+ body: JSON.stringify(payload)
5548
+ });
5549
+ } catch {}
5550
+ }
5429
5551
  async function runAgent(sandboxId, opts) {
5430
5552
  const sandbox = getSandbox(sandboxId);
5431
5553
  if (!sandbox.provider_sandbox_id) {
5432
5554
  throw new Error("Sandbox has no provider instance");
5433
5555
  }
5434
5556
  const provider = await getProvider(sandbox.provider);
5435
- const env = Object.keys(sandbox.env_vars ?? {}).length > 0 ? sandbox.env_vars : undefined;
5557
+ const mergedEnv = { ...sandbox.env_vars, ...opts.callEnvVars };
5558
+ const env = Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined;
5436
5559
  let cmd;
5437
5560
  const driver = opts.agentType !== "custom" ? getAgentDriver(opts.agentType) : undefined;
5438
5561
  if (opts.command) {
@@ -5451,6 +5574,18 @@ async function runAgent(sandboxId, opts) {
5451
5574
  command: cmd
5452
5575
  });
5453
5576
  emitLifecycleEvent(sandbox.id, `Agent ${opts.agentType} started: ${opts.prompt.slice(0, 100)}`);
5577
+ const startedAt = Date.now();
5578
+ const webhookEvents = opts.webhookEvents ?? ["start", "complete", "error"];
5579
+ if (opts.webhookUrl && webhookEvents.includes("start")) {
5580
+ fireWebhook(opts.webhookUrl, {
5581
+ event: "start",
5582
+ session_id: session.id,
5583
+ sandbox_id: sandbox.id,
5584
+ agent_type: opts.agentType,
5585
+ status: "running",
5586
+ timestamp: new Date().toISOString()
5587
+ });
5588
+ }
5454
5589
  const collector = createStreamCollector(sandbox.id, session.id);
5455
5590
  provider.exec(sandbox.provider_sandbox_id, cmd, {
5456
5591
  onStdout: (data) => {
@@ -5467,9 +5602,32 @@ async function runAgent(sandboxId, opts) {
5467
5602
  const status = exitResult.exit_code === 0 ? "completed" : "failed";
5468
5603
  endSession(session.id, exitResult.exit_code ?? 0, status);
5469
5604
  emitLifecycleEvent(sandbox.id, `Agent ${opts.agentType} finished with exit code ${exitResult.exit_code}`);
5605
+ if (opts.webhookUrl && webhookEvents.includes("complete")) {
5606
+ fireWebhook(opts.webhookUrl, {
5607
+ event: "complete",
5608
+ session_id: session.id,
5609
+ sandbox_id: sandbox.id,
5610
+ agent_type: opts.agentType,
5611
+ status,
5612
+ exit_code: exitResult.exit_code,
5613
+ duration_ms: Date.now() - startedAt,
5614
+ timestamp: new Date().toISOString()
5615
+ });
5616
+ }
5470
5617
  }).catch((err) => {
5471
5618
  endSession(session.id, 1, "failed");
5472
5619
  emitLifecycleEvent(sandbox.id, `Agent ${opts.agentType} failed: ${err.message}`);
5620
+ if (opts.webhookUrl && webhookEvents.includes("error")) {
5621
+ fireWebhook(opts.webhookUrl, {
5622
+ event: "error",
5623
+ session_id: session.id,
5624
+ sandbox_id: sandbox.id,
5625
+ agent_type: opts.agentType,
5626
+ status: "failed",
5627
+ duration_ms: Date.now() - startedAt,
5628
+ timestamp: new Date().toISOString()
5629
+ });
5630
+ }
5473
5631
  });
5474
5632
  return session;
5475
5633
  }
@@ -5484,7 +5642,62 @@ async function stopAgent(sandboxId) {
5484
5642
  emitLifecycleEvent(sandbox.id, "Agent stopped by user");
5485
5643
  }
5486
5644
 
5645
+ // src/lib/images.ts
5646
+ var BUILTIN_IMAGES = {
5647
+ node20: {
5648
+ e2b: "e2bdev/base:latest",
5649
+ description: "Node 20 + npm + pnpm + yarn",
5650
+ setup_script: "curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs && npm install -g pnpm yarn"
5651
+ },
5652
+ "node20-claude": {
5653
+ e2b: "e2bdev/base:latest",
5654
+ description: "Node 20 + Claude Code CLI pre-installed",
5655
+ setup_script: `curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs && npm install -g @anthropic-ai/claude-code && mkdir -p ~/.claude && echo '{"hasCompletedOnboarding":true,"hasTrustDialogAccepted":true,"hasAcknowledgedCostThreshold":true}' > ~/.claude.json`
5656
+ },
5657
+ "node20-codex": {
5658
+ e2b: "e2bdev/base:latest",
5659
+ description: "Node 20 + Codex CLI pre-installed",
5660
+ setup_script: `curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs && npm install -g @openai/codex && mkdir -p ~/.codex && echo '[core]
5661
+ approvalMode = "full-auto"
5662
+ ' > ~/.codex/config.toml`
5663
+ },
5664
+ python312: {
5665
+ e2b: "e2bdev/base:latest",
5666
+ description: "Python 3.12 + uv + pip",
5667
+ setup_script: "apt-get update && apt-get install -y python3.12 python3-pip && pip3 install uv"
5668
+ },
5669
+ "python312-agents": {
5670
+ e2b: "e2bdev/base:latest",
5671
+ description: "Python 3.12 + uv + common AI libs",
5672
+ setup_script: "apt-get update && apt-get install -y python3.12 python3-pip && pip3 install uv anthropic openai langchain"
5673
+ },
5674
+ fullstack: {
5675
+ e2b: "e2bdev/base:latest",
5676
+ description: "Node 20 + Python 3.12 + git + build tools",
5677
+ setup_script: "apt-get update && apt-get install -y git build-essential python3.12 python3-pip && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs && npm install -g pnpm"
5678
+ }
5679
+ };
5680
+ function resolveImage(image) {
5681
+ return BUILTIN_IMAGES[image]?.e2b ?? image;
5682
+ }
5683
+ function getBuiltinImageSetupScript(image) {
5684
+ return BUILTIN_IMAGES[image]?.setup_script;
5685
+ }
5686
+
5487
5687
  // src/mcp/index.ts
5688
+ var E2B_COST_PER_SECOND = 0.000014;
5689
+ var DAYTONA_COST_PER_SECOND = 0.00001;
5690
+ function estimateCost(providerName, startedAt) {
5691
+ if (!startedAt)
5692
+ return { compute_seconds: 0, cost_usd: 0 };
5693
+ const seconds = (Date.now() - new Date(startedAt).getTime()) / 1000;
5694
+ const rate = providerName === "daytona" ? DAYTONA_COST_PER_SECOND : E2B_COST_PER_SECOND;
5695
+ return {
5696
+ compute_seconds: Math.round(seconds),
5697
+ cost_usd: Math.round(seconds * rate * 1e6) / 1e6
5698
+ };
5699
+ }
5700
+ var exposedPorts = new Map;
5488
5701
  function ok(data) {
5489
5702
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
5490
5703
  }
@@ -5518,7 +5731,16 @@ var TOOL_CATALOG = [
5518
5731
  { name: "list_templates", description: "List all sandbox templates" },
5519
5732
  { name: "get_template", description: "Get a sandbox template by ID" },
5520
5733
  { name: "delete_template", description: "Delete a sandbox template" },
5521
- { name: "get_sandbox_status", description: "Get running processes, disk usage and uptime in a sandbox" }
5734
+ { name: "get_sandbox_status", description: "Get running processes, disk usage and uptime in a sandbox" },
5735
+ { name: "snapshot_sandbox", description: "Capture sandbox filesystem state as a snapshot" },
5736
+ { name: "list_snapshots", description: "List filesystem snapshots" },
5737
+ { name: "delete_snapshot", description: "Delete a snapshot" },
5738
+ { name: "expose_port", description: "Forward a sandbox port and get a public URL" },
5739
+ { name: "list_exposed_ports", description: "List all forwarded ports for a sandbox" },
5740
+ { name: "close_port", description: "Stop forwarding a sandbox port" },
5741
+ { name: "get_network_log", description: "Get outbound network connections from a sandbox" },
5742
+ { name: "watch_file", description: "Get new content from a file since a previous read (tail -f equivalent)" },
5743
+ { name: "list_images", description: "List available pre-warmed sandbox image aliases" }
5522
5744
  ];
5523
5745
  var server = new McpServer({
5524
5746
  name: "sandboxes",
@@ -5532,7 +5754,11 @@ server.tool("create_sandbox", "Create a new sandbox", {
5532
5754
  env_vars: exports_external.record(exports_external.string()).optional().describe("Environment variables"),
5533
5755
  template_id: exports_external.string().optional().describe("Template ID to base this sandbox on"),
5534
5756
  on_timeout: exports_external.enum(["pause", "terminate"]).optional().describe("What to do on timeout: pause (saves state) or terminate"),
5535
- auto_resume: exports_external.boolean().optional().describe("Auto-resume paused sandbox on next connect")
5757
+ auto_resume: exports_external.boolean().optional().describe("Auto-resume paused sandbox on next connect"),
5758
+ snapshot_id: exports_external.string().optional().describe("Snapshot ID to restore from"),
5759
+ network: exports_external.enum(["full", "restricted", "none"]).optional().describe("Network access policy for the sandbox"),
5760
+ budget_limit_usd: exports_external.number().optional().describe("Auto-terminate sandbox if compute cost exceeds this USD amount"),
5761
+ on_budget_exceeded: exports_external.enum(["terminate", "pause", "notify"]).optional().describe("Action when budget limit is reached (default: terminate)")
5536
5762
  }, async (params) => {
5537
5763
  try {
5538
5764
  const providerName = params.provider ?? getDefaultProvider();
@@ -5542,23 +5768,38 @@ server.tool("create_sandbox", "Create a new sandbox", {
5542
5768
  const tmpl = getTemplate(params.template_id);
5543
5769
  templateData = { image: tmpl.image ?? undefined, env_vars: tmpl.env_vars, setup_script: tmpl.setup_script };
5544
5770
  }
5545
- const image = params.image ?? templateData.image;
5771
+ const rawImage = params.image ?? templateData.image;
5772
+ const resolvedImage = rawImage ? resolveImage(rawImage) : rawImage;
5773
+ const builtinSetupScript = rawImage ? getBuiltinImageSetupScript(rawImage) : undefined;
5546
5774
  const envVars = { ...templateData.env_vars, ...params.env_vars };
5547
5775
  const onTimeout = params.on_timeout ?? "terminate";
5548
5776
  const autoResume = params.auto_resume ?? false;
5549
5777
  const sandbox = createSandbox({
5550
5778
  provider: providerName,
5551
- image,
5779
+ image: resolvedImage,
5552
5780
  timeout,
5553
5781
  name: params.name,
5554
5782
  env_vars: envVars,
5555
5783
  on_timeout: onTimeout,
5556
5784
  auto_resume: autoResume,
5557
- template_id: params.template_id
5785
+ template_id: params.template_id,
5786
+ config: { network: params.network ?? "full" },
5787
+ budget_limit_usd: params.budget_limit_usd,
5788
+ on_budget_exceeded: params.on_budget_exceeded
5558
5789
  });
5559
5790
  const provider = await getProvider(providerName);
5791
+ if (params.snapshot_id) {
5792
+ const snapshot = getSnapshot(params.snapshot_id);
5793
+ await provider.resume(snapshot.provider_sandbox_id);
5794
+ const updated2 = updateSandbox(sandbox.id, {
5795
+ provider_sandbox_id: snapshot.provider_sandbox_id,
5796
+ status: "running"
5797
+ });
5798
+ emitLifecycleEvent(sandbox.id, `Sandbox restored from snapshot ${snapshot.id}`);
5799
+ return ok(updated2);
5800
+ }
5560
5801
  const result = await provider.create({
5561
- image,
5802
+ image: resolvedImage,
5562
5803
  timeout,
5563
5804
  envVars,
5564
5805
  onTimeout,
@@ -5566,7 +5807,8 @@ server.tool("create_sandbox", "Create a new sandbox", {
5566
5807
  });
5567
5808
  const updated = updateSandbox(sandbox.id, {
5568
5809
  provider_sandbox_id: result.id,
5569
- status: "running"
5810
+ status: "running",
5811
+ started_at: new Date().toISOString()
5570
5812
  });
5571
5813
  emitLifecycleEvent(sandbox.id, "sandbox created");
5572
5814
  if (templateData.setup_script && result.id) {
@@ -5574,6 +5816,11 @@ server.tool("create_sandbox", "Create a new sandbox", {
5574
5816
  await provider.exec(result.id, templateData.setup_script);
5575
5817
  } catch {}
5576
5818
  }
5819
+ if (builtinSetupScript && result.id) {
5820
+ try {
5821
+ await provider.exec(result.id, builtinSetupScript);
5822
+ } catch {}
5823
+ }
5577
5824
  return ok(updated);
5578
5825
  } catch (e) {
5579
5826
  return err(e);
@@ -5583,7 +5830,9 @@ server.tool("get_sandbox", "Get sandbox details by ID", {
5583
5830
  id: exports_external.string().describe("Sandbox ID or partial ID")
5584
5831
  }, async (params) => {
5585
5832
  try {
5586
- return ok(getSandbox(params.id));
5833
+ const sandbox = getSandbox(params.id);
5834
+ const cost = estimateCost(sandbox.provider, sandbox.started_at);
5835
+ return ok({ ...sandbox, ...cost });
5587
5836
  } catch (e) {
5588
5837
  return err(e);
5589
5838
  }
@@ -5593,10 +5842,11 @@ server.tool("list_sandboxes", "List sandboxes with filters", {
5593
5842
  provider: exports_external.string().optional().describe("Filter by provider")
5594
5843
  }, async (params) => {
5595
5844
  try {
5596
- return ok(listSandboxes({
5845
+ const sandboxes = listSandboxes({
5597
5846
  status: params.status,
5598
5847
  provider: params.provider
5599
- }));
5848
+ });
5849
+ return ok(sandboxes.map((s) => ({ ...s, ...estimateCost(s.provider, s.started_at) })));
5600
5850
  } catch (e) {
5601
5851
  return err(e);
5602
5852
  }
@@ -5652,7 +5902,10 @@ server.tool("keep_alive", "Extend sandbox lifetime", {
5652
5902
  server.tool("exec_command", "Execute a command in a sandbox", {
5653
5903
  sandbox_id: exports_external.string().describe("Sandbox ID or partial ID"),
5654
5904
  command: exports_external.string().describe("Command to execute"),
5655
- background: exports_external.boolean().optional().describe("Run in background")
5905
+ background: exports_external.boolean().optional().describe("Run in background"),
5906
+ env_vars: exports_external.record(exports_external.string()).optional().describe("Per-call environment variables (merged with sandbox env_vars, not persisted)"),
5907
+ stdin: exports_external.string().optional().describe("String to pipe as stdin to the command"),
5908
+ tty: exports_external.boolean().optional().describe("Allocate a TTY for the session (best-effort)")
5656
5909
  }, async (params) => {
5657
5910
  try {
5658
5911
  const sandbox = getSandbox(params.sandbox_id);
@@ -5664,12 +5917,15 @@ server.tool("exec_command", "Execute a command in a sandbox", {
5664
5917
  });
5665
5918
  const collector = createStreamCollector(sandbox.id, session.id);
5666
5919
  const provider = await getProvider(sandbox.provider);
5667
- const env = Object.keys(sandbox.env_vars ?? {}).length > 0 ? sandbox.env_vars : undefined;
5920
+ const callEnv = { ...sandbox.env_vars, ...params.env_vars };
5921
+ const env = Object.keys(callEnv).length > 0 ? callEnv : undefined;
5668
5922
  if (params.background) {
5669
5923
  provider.exec(sandbox.provider_sandbox_id, params.command, {
5670
5924
  onStdout: collector.onStdout,
5671
5925
  onStderr: collector.onStderr,
5672
- env
5926
+ env,
5927
+ stdin: params.stdin,
5928
+ tty: params.tty
5673
5929
  }).then((res) => {
5674
5930
  const r = res;
5675
5931
  endSession(session.id, r.exit_code ?? 0);
@@ -5681,7 +5937,9 @@ server.tool("exec_command", "Execute a command in a sandbox", {
5681
5937
  const result = await provider.exec(sandbox.provider_sandbox_id, params.command, {
5682
5938
  onStdout: collector.onStdout,
5683
5939
  onStderr: collector.onStderr,
5684
- env
5940
+ env,
5941
+ stdin: params.stdin,
5942
+ tty: params.tty
5685
5943
  });
5686
5944
  const execResult = result;
5687
5945
  endSession(session.id, execResult.exit_code);
@@ -5697,15 +5955,22 @@ server.tool("exec_command", "Execute a command in a sandbox", {
5697
5955
  });
5698
5956
  server.tool("read_file", "Read a file from a sandbox", {
5699
5957
  sandbox_id: exports_external.string().describe("Sandbox ID or partial ID"),
5700
- path: exports_external.string().describe("File path")
5958
+ path: exports_external.string().describe("File path"),
5959
+ offset: exports_external.number().optional().describe("Line or byte offset to start reading from"),
5960
+ limit: exports_external.number().optional().describe("Max lines or bytes to return"),
5961
+ encoding: exports_external.enum(["utf8", "base64", "hex"]).optional().describe("Output encoding (default: utf8)")
5701
5962
  }, async (params) => {
5702
5963
  try {
5703
5964
  const sandbox = getSandbox(params.sandbox_id);
5704
5965
  if (!sandbox.provider_sandbox_id)
5705
5966
  throw new Error("Sandbox has no provider ID");
5706
5967
  const provider = await getProvider(sandbox.provider);
5707
- const content = await provider.readFile(sandbox.provider_sandbox_id, params.path);
5708
- return ok({ path: params.path, content });
5968
+ const content = await provider.readFile(sandbox.provider_sandbox_id, params.path, {
5969
+ encoding: params.encoding,
5970
+ offset: params.offset,
5971
+ limit: params.limit
5972
+ });
5973
+ return ok({ path: params.path, content, encoding: params.encoding ?? "utf8" });
5709
5974
  } catch (e) {
5710
5975
  return err(e);
5711
5976
  }
@@ -5728,14 +5993,19 @@ server.tool("write_file", "Write a file to a sandbox", {
5728
5993
  });
5729
5994
  server.tool("list_files", "List files in a sandbox directory", {
5730
5995
  sandbox_id: exports_external.string().describe("Sandbox ID or partial ID"),
5731
- path: exports_external.string().describe("Directory path")
5996
+ path: exports_external.string().describe("Directory path"),
5997
+ recursive: exports_external.boolean().optional().describe("List files recursively"),
5998
+ glob: exports_external.string().optional().describe("Glob pattern to filter files")
5732
5999
  }, async (params) => {
5733
6000
  try {
5734
6001
  const sandbox = getSandbox(params.sandbox_id);
5735
6002
  if (!sandbox.provider_sandbox_id)
5736
6003
  throw new Error("Sandbox has no provider ID");
5737
6004
  const provider = await getProvider(sandbox.provider);
5738
- const files = await provider.listFiles(sandbox.provider_sandbox_id, params.path);
6005
+ const files = await provider.listFiles(sandbox.provider_sandbox_id, params.path, {
6006
+ recursive: params.recursive,
6007
+ glob: params.glob
6008
+ });
5739
6009
  return ok(files);
5740
6010
  } catch (e) {
5741
6011
  return err(e);
@@ -5815,14 +6085,20 @@ server.tool("run_agent", "Run an AI agent inside a sandbox", {
5815
6085
  agent_type: exports_external.enum(["claude", "codex", "gemini", "opencode", "pi", "custom"]).describe("Agent type"),
5816
6086
  prompt: exports_external.string().describe("Prompt for the agent"),
5817
6087
  agent_name: exports_external.string().optional().describe("Agent name"),
5818
- command: exports_external.string().optional().describe("Custom command (for 'custom' type)")
6088
+ command: exports_external.string().optional().describe("Custom command (for 'custom' type)"),
6089
+ env_vars: exports_external.record(exports_external.string()).optional().describe("Per-call environment variables (merged with sandbox env_vars, not persisted)"),
6090
+ webhook_url: exports_external.string().optional().describe("URL to POST result to when agent finishes"),
6091
+ webhook_events: exports_external.array(exports_external.enum(["start", "complete", "error"])).optional().describe("Which events to notify on (default: all)")
5819
6092
  }, async (params) => {
5820
6093
  try {
5821
6094
  const session = await runAgent(params.sandbox_id, {
5822
6095
  agentType: params.agent_type,
5823
6096
  prompt: params.prompt,
5824
6097
  agentName: params.agent_name,
5825
- command: params.command
6098
+ command: params.command,
6099
+ callEnvVars: params.env_vars,
6100
+ webhookUrl: params.webhook_url,
6101
+ webhookEvents: params.webhook_events
5826
6102
  });
5827
6103
  return ok({ session_id: session.id, status: session.status });
5828
6104
  } catch (e) {
@@ -5842,17 +6118,19 @@ server.tool("stop_agent", "Stop a running agent in a sandbox", {
5842
6118
  server.tool("get_agent_output", "Get output from an agent session", {
5843
6119
  sandbox_id: exports_external.string().describe("Sandbox ID"),
5844
6120
  session_id: exports_external.string().optional().describe("Session ID"),
5845
- limit: exports_external.number().optional().describe("Max events")
6121
+ limit: exports_external.number().optional().describe("Max events"),
6122
+ offset: exports_external.number().optional().describe("Skip first N events (for incremental polling)")
5846
6123
  }, async (params) => {
5847
6124
  try {
5848
6125
  const events = listEvents({
5849
6126
  sandbox_id: params.sandbox_id,
5850
6127
  session_id: params.session_id,
5851
- limit: params.limit || 100
6128
+ limit: params.limit || 100,
6129
+ offset: params.offset
5852
6130
  });
5853
6131
  const stdout = events.filter((e) => e.type === "stdout").map((e) => e.data).join("");
5854
6132
  const stderr = events.filter((e) => e.type === "stderr").map((e) => e.data).join("");
5855
- return ok({ stdout, stderr, event_count: events.length });
6133
+ return ok({ stdout, stderr, event_count: events.length, next_offset: (params.offset ?? 0) + events.length });
5856
6134
  } catch (e) {
5857
6135
  return err(e);
5858
6136
  }
@@ -5963,5 +6241,142 @@ server.tool("get_sandbox_status", "Get running processes, disk usage and uptime
5963
6241
  return err(e);
5964
6242
  }
5965
6243
  });
6244
+ server.tool("snapshot_sandbox", "Capture sandbox filesystem state as a snapshot", {
6245
+ id: exports_external.string().describe("Sandbox ID or partial ID"),
6246
+ name: exports_external.string().optional().describe("Snapshot name")
6247
+ }, async (params) => {
6248
+ try {
6249
+ const sandbox = getSandbox(params.id);
6250
+ if (!sandbox.provider_sandbox_id)
6251
+ throw new Error("Sandbox has no provider ID");
6252
+ const provider = await getProvider(sandbox.provider);
6253
+ await provider.pause(sandbox.provider_sandbox_id);
6254
+ updateSandbox(sandbox.id, { status: "paused" });
6255
+ const snapshot = createSnapshot({
6256
+ sandbox_id: sandbox.id,
6257
+ provider_sandbox_id: sandbox.provider_sandbox_id,
6258
+ provider: sandbox.provider,
6259
+ name: params.name
6260
+ });
6261
+ emitLifecycleEvent(sandbox.id, `Snapshot created: ${snapshot.id}`);
6262
+ return ok(snapshot);
6263
+ } catch (e) {
6264
+ return err(e);
6265
+ }
6266
+ });
6267
+ server.tool("list_snapshots", "List filesystem snapshots", {
6268
+ sandbox_id: exports_external.string().optional().describe("Filter by sandbox ID")
6269
+ }, async (params) => {
6270
+ try {
6271
+ return ok(listSnapshots(params.sandbox_id));
6272
+ } catch (e) {
6273
+ return err(e);
6274
+ }
6275
+ });
6276
+ server.tool("delete_snapshot", "Delete a snapshot", {
6277
+ id: exports_external.string().describe("Snapshot ID or partial ID")
6278
+ }, async (params) => {
6279
+ try {
6280
+ deleteSnapshot(params.id);
6281
+ return ok({ deleted: params.id });
6282
+ } catch (e) {
6283
+ return err(e);
6284
+ }
6285
+ });
6286
+ server.tool("expose_port", "Forward a sandbox port and get a public URL", {
6287
+ sandbox_id: exports_external.string().describe("Sandbox ID or partial ID"),
6288
+ port: exports_external.number().describe("Port number to expose"),
6289
+ protocol: exports_external.string().optional().describe("Protocol: http or ws (default: http)")
6290
+ }, async (params) => {
6291
+ try {
6292
+ const sandbox = getSandbox(params.sandbox_id);
6293
+ if (!sandbox.provider_sandbox_id)
6294
+ throw new Error("Sandbox has no provider ID");
6295
+ const provider = await getProvider(sandbox.provider);
6296
+ const url = await provider.getPublicUrl(sandbox.provider_sandbox_id, params.port, params.protocol);
6297
+ if (!exposedPorts.has(sandbox.id))
6298
+ exposedPorts.set(sandbox.id, new Map);
6299
+ exposedPorts.get(sandbox.id).set(params.port, url);
6300
+ return ok({ sandbox_id: sandbox.id, port: params.port, url });
6301
+ } catch (e) {
6302
+ return err(e);
6303
+ }
6304
+ });
6305
+ server.tool("list_exposed_ports", "List all forwarded ports for a sandbox", {
6306
+ sandbox_id: exports_external.string().describe("Sandbox ID or partial ID")
6307
+ }, async (params) => {
6308
+ try {
6309
+ const sandbox = getSandbox(params.sandbox_id);
6310
+ const ports = exposedPorts.get(sandbox.id) ?? new Map;
6311
+ const result = Array.from(ports.entries()).map(([port, url]) => ({ port, url }));
6312
+ return ok(result);
6313
+ } catch (e) {
6314
+ return err(e);
6315
+ }
6316
+ });
6317
+ server.tool("close_port", "Stop forwarding a sandbox port", {
6318
+ sandbox_id: exports_external.string().describe("Sandbox ID or partial ID"),
6319
+ port: exports_external.number().describe("Port number to close")
6320
+ }, async (params) => {
6321
+ try {
6322
+ const sandbox = getSandbox(params.sandbox_id);
6323
+ exposedPorts.get(sandbox.id)?.delete(params.port);
6324
+ return ok({ sandbox_id: sandbox.id, port: params.port, closed: true });
6325
+ } catch (e) {
6326
+ return err(e);
6327
+ }
6328
+ });
6329
+ server.tool("get_network_log", "Get outbound network connections from a sandbox", {
6330
+ sandbox_id: exports_external.string().describe("Sandbox ID or partial ID")
6331
+ }, async (params) => {
6332
+ try {
6333
+ const sandbox = getSandbox(params.sandbox_id);
6334
+ if (!sandbox.provider_sandbox_id)
6335
+ throw new Error("Sandbox has no provider ID");
6336
+ const provider = await getProvider(sandbox.provider);
6337
+ const result = await provider.exec(sandbox.provider_sandbox_id, "ss -tnp 2>/dev/null || netstat -tnp 2>/dev/null || echo 'Network log not available'");
6338
+ return ok({ sandbox_id: sandbox.id, connections: (result.stdout || "").trim() });
6339
+ } catch (e) {
6340
+ return err(e);
6341
+ }
6342
+ });
6343
+ server.tool("watch_file", "Get new content from a file since a previous read (tail -f equivalent)", {
6344
+ sandbox_id: exports_external.string().describe("Sandbox ID or partial ID"),
6345
+ path: exports_external.string().describe("File path to watch"),
6346
+ offset: exports_external.number().optional().describe("Line offset to read from (use next_offset from previous call)"),
6347
+ limit: exports_external.number().optional().describe("Max lines to return (default: 100)")
6348
+ }, async (params) => {
6349
+ try {
6350
+ const sandbox = getSandbox(params.sandbox_id);
6351
+ if (!sandbox.provider_sandbox_id)
6352
+ throw new Error("Sandbox has no provider ID");
6353
+ const provider = await getProvider(sandbox.provider);
6354
+ const content = await provider.readFile(sandbox.provider_sandbox_id, params.path, {
6355
+ offset: params.offset,
6356
+ limit: params.limit ?? 100
6357
+ });
6358
+ const lines = content.split(`
6359
+ `);
6360
+ return ok({
6361
+ path: params.path,
6362
+ content,
6363
+ lines_read: lines.length,
6364
+ next_offset: (params.offset ?? 0) + lines.length
6365
+ });
6366
+ } catch (e) {
6367
+ return err(e);
6368
+ }
6369
+ });
6370
+ server.tool("list_images", "List available pre-warmed sandbox image aliases", {}, async () => {
6371
+ try {
6372
+ return ok(Object.entries(BUILTIN_IMAGES).map(([name, info]) => ({
6373
+ name,
6374
+ description: info.description,
6375
+ has_setup_script: !!info.setup_script
6376
+ })));
6377
+ } catch (e) {
6378
+ return err(e);
6379
+ }
6380
+ });
5966
6381
  var transport = new StdioServerTransport;
5967
6382
  await server.connect(transport);