@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/cli/index.js +126 -8
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/sandboxes.d.ts +1 -1
- package/dist/db/sandboxes.d.ts.map +1 -1
- package/dist/db/snapshots.d.ts +29 -0
- package/dist/db/snapshots.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +168 -7
- package/dist/lib/agent-runner.d.ts +3 -0
- package/dist/lib/agent-runner.d.ts.map +1 -1
- package/dist/lib/images.d.ts +13 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/mcp/index.js +447 -32
- package/dist/providers/daytona.d.ts +1 -0
- package/dist/providers/daytona.d.ts.map +1 -1
- package/dist/providers/e2b.d.ts +10 -2
- package/dist/providers/e2b.d.ts.map +1 -1
- package/dist/providers/modal.d.ts +1 -0
- package/dist/providers/modal.d.ts.map +1 -1
- package/dist/providers/types.d.ts +12 -2
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/server/index.js +80 -7
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
4876
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|