microsandbox-mcp 0.5.7 → 0.6.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.
Files changed (63) hide show
  1. package/README.md +142 -27
  2. package/dist/config.d.ts +10 -0
  3. package/dist/config.js +42 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/index.js +13 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/resources.d.ts +2 -0
  8. package/dist/resources.js +71 -0
  9. package/dist/resources.js.map +1 -0
  10. package/dist/schemas/common.d.ts +7 -0
  11. package/dist/schemas/common.js +8 -0
  12. package/dist/schemas/common.js.map +1 -0
  13. package/dist/state/exec-sessions.d.ts +19 -0
  14. package/dist/state/exec-sessions.js +151 -0
  15. package/dist/state/exec-sessions.js.map +1 -0
  16. package/dist/tools/exec.js +283 -56
  17. package/dist/tools/exec.js.map +1 -1
  18. package/dist/tools/filesystem.js +195 -59
  19. package/dist/tools/filesystem.js.map +1 -1
  20. package/dist/tools/image.d.ts +2 -0
  21. package/dist/tools/image.js +99 -0
  22. package/dist/tools/image.js.map +1 -0
  23. package/dist/tools/logs.d.ts +2 -0
  24. package/dist/tools/logs.js +128 -0
  25. package/dist/tools/logs.js.map +1 -0
  26. package/dist/tools/metrics.js +48 -65
  27. package/dist/tools/metrics.js.map +1 -1
  28. package/dist/tools/runtime.d.ts +2 -0
  29. package/dist/tools/runtime.js +61 -0
  30. package/dist/tools/runtime.js.map +1 -0
  31. package/dist/tools/sandbox.d.ts +216 -0
  32. package/dist/tools/sandbox.js +619 -202
  33. package/dist/tools/sandbox.js.map +1 -1
  34. package/dist/tools/snapshot.d.ts +2 -0
  35. package/dist/tools/snapshot.js +234 -0
  36. package/dist/tools/snapshot.js.map +1 -0
  37. package/dist/tools/ssh.d.ts +2 -0
  38. package/dist/tools/ssh.js +323 -0
  39. package/dist/tools/ssh.js.map +1 -0
  40. package/dist/tools/volume.js +287 -23
  41. package/dist/tools/volume.js.map +1 -1
  42. package/dist/utils/errors.d.ts +2 -7
  43. package/dist/utils/errors.js +6 -5
  44. package/dist/utils/errors.js.map +1 -1
  45. package/dist/utils/exec-output.d.ts +20 -0
  46. package/dist/utils/exec-output.js +15 -0
  47. package/dist/utils/exec-output.js.map +1 -0
  48. package/dist/utils/output.d.ts +11 -0
  49. package/dist/utils/output.js +21 -0
  50. package/dist/utils/output.js.map +1 -0
  51. package/dist/utils/policy.d.ts +8 -0
  52. package/dist/utils/policy.js +47 -0
  53. package/dist/utils/policy.js.map +1 -0
  54. package/dist/utils/redaction.d.ts +1 -0
  55. package/dist/utils/redaction.js +15 -0
  56. package/dist/utils/redaction.js.map +1 -0
  57. package/dist/utils/response.d.ts +34 -0
  58. package/dist/utils/response.js +38 -0
  59. package/dist/utils/response.js.map +1 -0
  60. package/dist/utils/serialization.d.ts +15 -0
  61. package/dist/utils/serialization.js +164 -0
  62. package/dist/utils/serialization.js.map +1 -0
  63. package/package.json +5 -3
@@ -1,60 +1,175 @@
1
1
  import { z } from "zod";
2
2
  import { Sandbox } from "microsandbox";
3
3
  import { formatError } from "../utils/errors.js";
4
- async function waitForStopped(name, timeoutMs = 10000) {
5
- const start = Date.now();
6
- while (Date.now() - start < timeoutMs) {
7
- try {
8
- const h = await Sandbox.get(name);
9
- if (h.status !== "running")
10
- return;
11
- }
12
- catch {
13
- return; // Sandbox gone, that's fine.
14
- }
15
- await new Promise((r) => setTimeout(r, 200));
16
- }
17
- }
4
+ import { formatExecOutput } from "../utils/exec-output.js";
5
+ import { assertHostPathAllowed } from "../utils/policy.js";
6
+ import { ok } from "../utils/response.js";
7
+ import { sandboxHandleData, sandboxSummaryData } from "../utils/serialization.js";
8
+ const statusSchema = z.enum(["running", "stopped", "crashed", "draining", "all"]);
9
+ const selectorSchema = z.object({
10
+ name: z.string().optional().describe("Sandbox name"),
11
+ names: z.array(z.string()).optional().describe("Sandbox names"),
12
+ labels: z.record(z.string(), z.string()).optional().describe("AND-matched labels"),
13
+ status: statusSchema.optional().describe("Status filter"),
14
+ });
15
+ const mountSchema = z.object({
16
+ kind: z.enum(["bind", "named", "tmpfs", "disk"]),
17
+ guestPath: z.string(),
18
+ hostPath: z.string().optional(),
19
+ name: z.string().optional(),
20
+ source: z.string().optional(),
21
+ readonly: z.boolean().optional(),
22
+ noexec: z.boolean().optional(),
23
+ nosuid: z.boolean().optional(),
24
+ nodev: z.boolean().optional(),
25
+ sizeMib: z.number().int().positive().optional(),
26
+ format: z.enum(["raw", "qcow2", "vmdk"]).optional(),
27
+ fstype: z.string().optional(),
28
+ statVirtualization: z.enum(["strict", "relaxed", "off"]).optional(),
29
+ hostPermissions: z.enum(["private", "mirror"]).optional(),
30
+ });
31
+ const patchSchema = z.object({
32
+ kind: z.enum(["text", "file", "copyFile", "copyDir", "mkdir", "append", "remove", "symlink"]),
33
+ type: z.enum(["text", "file", "copyFile", "copyDir", "mkdir", "append", "remove", "symlink"]).optional(),
34
+ path: z.string().optional(),
35
+ guestPath: z.string().optional(),
36
+ hostPath: z.string().optional(),
37
+ content: z.string().optional(),
38
+ contentBase64: z.string().optional(),
39
+ target: z.string().optional(),
40
+ linkPath: z.string().optional(),
41
+ mode: z.number().int().optional(),
42
+ replace: z.boolean().optional(),
43
+ });
44
+ const rootfsSchema = z.discriminatedUnion("kind", [
45
+ z.object({
46
+ kind: z.literal("oci"),
47
+ reference: z.string(),
48
+ pullPolicy: z.enum(["always", "if-missing", "never"]).optional(),
49
+ upperSizeMib: z.number().int().positive().optional(),
50
+ registry: z.object({
51
+ insecure: z.boolean().optional(),
52
+ caCertsPath: z.string().optional(),
53
+ auth: z.object({
54
+ kind: z.enum(["anonymous", "basic"]),
55
+ username: z.string().optional(),
56
+ password: z.string().optional(),
57
+ }).optional(),
58
+ }).optional(),
59
+ }),
60
+ z.object({ kind: z.literal("bind"), path: z.string() }),
61
+ z.object({
62
+ kind: z.literal("disk"),
63
+ path: z.string(),
64
+ fstype: z.string().optional(),
65
+ }),
66
+ z.object({ kind: z.literal("snapshot"), pathOrName: z.string() }),
67
+ ]);
68
+ export const sandboxCreateSchema = z.object({
69
+ name: z.string().optional(),
70
+ image: z.string().optional().describe("Convenience OCI image or local rootfs path"),
71
+ rootfs: rootfsSchema.optional(),
72
+ cpus: z.number().int().min(1).max(16).optional(),
73
+ memoryMib: z.number().int().min(128).optional(),
74
+ workdir: z.string().optional(),
75
+ env: z.record(z.string(), z.string()).optional(),
76
+ volumes: z.array(mountSchema).optional(),
77
+ mounts: z.array(mountSchema).optional(),
78
+ patches: z.array(patchSchema).optional(),
79
+ entrypoint: z.array(z.string()).optional(),
80
+ hostname: z.string().optional(),
81
+ maxDuration: z.number().int().positive().optional(),
82
+ idleTimeout: z.number().int().positive().optional(),
83
+ resources: z.object({
84
+ cpus: z.number().int().min(1).max(16).optional(),
85
+ memoryMib: z.number().int().min(128).optional(),
86
+ }).optional(),
87
+ process: z.object({
88
+ workdir: z.string().optional(),
89
+ shell: z.string().optional(),
90
+ env: z.record(z.string(), z.string()).optional(),
91
+ labels: z.record(z.string(), z.string()).optional(),
92
+ entrypoint: z.array(z.string()).optional(),
93
+ user: z.string().optional(),
94
+ scripts: z.record(z.string(), z.string()).optional(),
95
+ rlimits: z.array(z.object({
96
+ resource: z.string(),
97
+ soft: z.number().int().nonnegative(),
98
+ hard: z.number().int().nonnegative().optional(),
99
+ })).optional(),
100
+ init: z.object({
101
+ path: z.string(),
102
+ args: z.array(z.string()).optional(),
103
+ env: z.record(z.string(), z.string()).optional(),
104
+ }).optional(),
105
+ }).optional(),
106
+ lifecycle: z.object({
107
+ detached: z.boolean().optional(),
108
+ replace: z.boolean().optional(),
109
+ replaceTimeoutMs: z.number().int().nonnegative().optional(),
110
+ maxDurationSecs: z.number().int().positive().optional(),
111
+ idleTimeoutSecs: z.number().int().positive().optional(),
112
+ logLevel: z.enum(["error", "warn", "info", "debug", "trace"]).optional(),
113
+ metricsSampleIntervalMs: z.number().int().nonnegative().optional(),
114
+ disableMetricsSample: z.boolean().optional(),
115
+ quietLogs: z.boolean().optional(),
116
+ }).optional(),
117
+ network: z.object({
118
+ disabled: z.boolean().optional(),
119
+ ports: z.array(z.object({
120
+ hostPort: z.number().int().min(1).max(65535),
121
+ guestPort: z.number().int().min(1).max(65535),
122
+ bindAddress: z.string().optional(),
123
+ protocol: z.enum(["tcp", "udp"]).optional(),
124
+ })).optional(),
125
+ maxConnections: z.number().int().positive().optional(),
126
+ trustHostCas: z.boolean().optional(),
127
+ ipv4Pool: z.string().optional(),
128
+ ipv6Pool: z.string().optional(),
129
+ dns: z.object({
130
+ allowRebind: z.boolean().optional(),
131
+ nameservers: z.array(z.string()).optional(),
132
+ queryTimeoutMs: z.number().int().positive().optional(),
133
+ }).optional(),
134
+ tls: z.object({
135
+ ports: z.array(z.number().int().min(1).max(65535)).optional(),
136
+ bypassDomains: z.array(z.string()).optional(),
137
+ blockQuic: z.boolean().optional(),
138
+ caCertPath: z.string().optional(),
139
+ caKeyPath: z.string().optional(),
140
+ upstreamCaCertPaths: z.array(z.string()).optional(),
141
+ trustHostCas: z.boolean().optional(),
142
+ }).optional(),
143
+ }).optional(),
144
+ secrets: z.array(z.object({
145
+ envVar: z.string(),
146
+ value: z.string().optional(),
147
+ valueEnv: z.string().optional(),
148
+ allowedHost: z.string(),
149
+ })).optional(),
150
+ });
151
+ const createSchema = sandboxCreateSchema;
18
152
  export function registerSandboxTools(server) {
19
153
  server.registerTool("sandbox_run", {
20
154
  title: "Run Command in Ephemeral Sandbox",
21
- description: "Create an ephemeral sandbox, run a shell command, return the output, and destroy the sandbox. " +
22
- "Best for quick one-off tasks like running a script, checking a command, or testing code.",
23
- inputSchema: z.object({
24
- image: z.string().describe('OCI image (e.g. "python:3.12", "node:22", "alpine")'),
155
+ description: "Create an ephemeral sandbox, run a shell command, return output, and destroy it.",
156
+ inputSchema: createSchema.extend({
25
157
  command: z.string().describe("Shell command to execute"),
26
- memoryMib: z.number().int().min(128).optional().describe("Memory in MiB (default: 512)"),
27
- cpus: z.number().int().min(1).max(16).optional().describe("Number of vCPUs (default: 1)"),
28
- env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
29
158
  }),
30
159
  annotations: {
31
160
  readOnlyHint: false,
32
161
  destructiveHint: false,
33
162
  idempotentHint: false,
34
163
  },
35
- }, async ({ image, command, memoryMib, cpus, env }) => {
36
- const name = `mcp-run-${Date.now()}`;
164
+ }, async (args) => {
165
+ const name = args.name ?? `mcp-run-${Date.now()}`;
37
166
  let sandbox;
38
167
  try {
39
- let builder = Sandbox.builder(name)
40
- .image(image)
41
- .memory(memoryMib ?? 512)
42
- .cpus(cpus ?? 1);
43
- if (env)
44
- builder = builder.envs(env);
168
+ const builder = buildSandbox(name, args, false);
45
169
  sandbox = await builder.create();
46
- const output = await sandbox.shell(command);
47
- return {
48
- content: [{
49
- type: "text",
50
- text: JSON.stringify({
51
- stdout: output.stdout(),
52
- stderr: output.stderr(),
53
- exitCode: output.code,
54
- success: output.success,
55
- }, null, 2),
56
- }],
57
- };
170
+ const output = await sandbox.shell(args.command);
171
+ const result = formatExecOutput(output);
172
+ return ok({ name, ...result.data }, { truncated: result.truncated });
58
173
  }
59
174
  catch (error) {
60
175
  return formatError(error);
@@ -74,117 +189,51 @@ export function registerSandboxTools(server) {
74
189
  });
75
190
  server.registerTool("sandbox_create", {
76
191
  title: "Create Sandbox",
77
- description: "Create and boot a persistent named sandbox with full configuration. " +
78
- "The sandbox stays running until explicitly stopped.",
79
- inputSchema: z.object({
192
+ description: "Create and boot a persistent named sandbox.",
193
+ inputSchema: createSchema.extend({
80
194
  name: z.string().describe("Unique sandbox name"),
81
- image: z.string().describe('OCI image reference (e.g. "python:3.12", "ubuntu:24.04")'),
82
- cpus: z.number().int().min(1).max(16).optional().describe("Virtual CPUs (default: 1)"),
83
- memoryMib: z.number().int().min(128).optional().describe("Memory in MiB (default: 512)"),
84
- workdir: z.string().optional().describe("Default working directory inside the sandbox"),
85
- env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
86
- volumes: z.array(z.object({
87
- guestPath: z.string().describe("Mount point inside sandbox"),
88
- type: z.enum(["bind", "named", "tmpfs"]).describe("Volume type"),
89
- source: z.string().optional().describe("Host path (bind) or volume name (named)"),
90
- readonly: z.boolean().optional().describe("Mount as read-only"),
91
- sizeMib: z.number().optional().describe("Size for tmpfs volumes"),
92
- })).optional().describe("Volume mounts"),
93
- patches: z.array(z.object({
94
- type: z.enum(["text", "mkdir", "append", "remove", "symlink"]).describe("Patch type"),
95
- path: z.string().describe("Target path inside sandbox"),
96
- content: z.string().optional().describe("File content (text, append)"),
97
- target: z.string().optional().describe("Symlink target"),
98
- mode: z.number().optional().describe("Unix permissions"),
99
- })).optional().describe("Rootfs modifications applied before boot"),
100
- entrypoint: z.array(z.string()).optional().describe("Override image entrypoint"),
101
- hostname: z.string().optional().describe("Guest hostname"),
102
- maxDuration: z.number().optional().describe("Auto-stop after N seconds"),
103
- idleTimeout: z.number().optional().describe("Auto-stop after N seconds of inactivity"),
104
195
  }),
105
196
  annotations: {
106
197
  readOnlyHint: false,
107
198
  destructiveHint: false,
108
199
  idempotentHint: false,
109
200
  },
110
- }, async ({ name, image, cpus, memoryMib, workdir, env, volumes, patches, entrypoint, hostname, maxDuration, idleTimeout }) => {
201
+ }, async (args) => {
111
202
  try {
112
- let builder = Sandbox.builder(name)
113
- .image(image)
114
- .cpus(cpus ?? 1)
115
- .memory(memoryMib ?? 512);
116
- if (workdir)
117
- builder = builder.workdir(workdir);
118
- if (env)
119
- builder = builder.envs(env);
120
- if (entrypoint)
121
- builder = builder.entrypoint(entrypoint);
122
- if (hostname)
123
- builder = builder.hostname(hostname);
124
- if (maxDuration)
125
- builder = builder.maxDuration(maxDuration);
126
- if (idleTimeout)
127
- builder = builder.idleTimeout(idleTimeout);
128
- if (volumes) {
129
- for (const v of volumes) {
130
- builder = builder.volume(v.guestPath, (m) => {
131
- switch (v.type) {
132
- case "bind": {
133
- let b = m.bind(v.source);
134
- if (v.readonly)
135
- b = b.readonly();
136
- return b;
137
- }
138
- case "named": {
139
- let b = m.named(v.source);
140
- if (v.readonly)
141
- b = b.readonly();
142
- return b;
143
- }
144
- case "tmpfs": {
145
- let b = m.tmpfs();
146
- if (typeof v.sizeMib === "number")
147
- b = b.size(v.sizeMib);
148
- if (v.readonly)
149
- b = b.readonly();
150
- return b;
151
- }
152
- }
153
- });
154
- }
203
+ const builder = buildSandbox(args.name, args, true);
204
+ const sandbox = await builder.create();
205
+ if (args.lifecycle?.detached !== false) {
206
+ await sandbox.detach();
155
207
  }
156
- if (patches && patches.length > 0) {
157
- builder = builder.patch((p) => {
158
- let acc = p;
159
- for (const patch of patches) {
160
- switch (patch.type) {
161
- case "text":
162
- acc = acc.text(patch.path, patch.content, { mode: patch.mode });
163
- break;
164
- case "mkdir":
165
- acc = acc.mkdir(patch.path, { mode: patch.mode });
166
- break;
167
- case "append":
168
- acc = acc.append(patch.path, patch.content);
169
- break;
170
- case "remove":
171
- acc = acc.remove(patch.path);
172
- break;
173
- case "symlink":
174
- acc = acc.symlink(patch.target, patch.path);
175
- break;
176
- }
177
- }
178
- return acc;
179
- });
208
+ return ok({ name: args.name, status: "running" });
209
+ }
210
+ catch (error) {
211
+ return formatError(error);
212
+ }
213
+ });
214
+ server.registerTool("sandbox_start", {
215
+ title: "Start Sandboxes",
216
+ description: "Start stopped sandboxes by name or selector.",
217
+ inputSchema: selectorSchema,
218
+ annotations: {
219
+ readOnlyHint: false,
220
+ destructiveHint: false,
221
+ idempotentHint: true,
222
+ },
223
+ }, async (selector) => {
224
+ try {
225
+ const handles = await resolveSandboxHandles(selector, { requireSelector: true });
226
+ const results = [];
227
+ for (const handle of handles) {
228
+ if (handle.status === "running") {
229
+ results.push({ name: handle.name, started: false, alreadyRunning: true });
230
+ continue;
231
+ }
232
+ const sandbox = await handle.startDetached();
233
+ await sandbox.detach();
234
+ results.push({ name: handle.name, started: true });
180
235
  }
181
- await builder.create();
182
- return {
183
- content: [{
184
- type: "text",
185
- text: JSON.stringify({ name, status: "running", image }, null, 2),
186
- }],
187
- };
236
+ return ok(results);
188
237
  }
189
238
  catch (error) {
190
239
  return formatError(error);
@@ -192,24 +241,36 @@ export function registerSandboxTools(server) {
192
241
  });
193
242
  server.registerTool("sandbox_list", {
194
243
  title: "List Sandboxes",
195
- description: "List all sandboxes with their status.",
196
- inputSchema: z.object({}),
244
+ description: "List sandboxes with status and summary metadata.",
245
+ inputSchema: selectorSchema,
246
+ annotations: {
247
+ readOnlyHint: true,
248
+ destructiveHint: false,
249
+ idempotentHint: true,
250
+ },
251
+ }, async (selector) => {
252
+ try {
253
+ const handles = await resolveSandboxHandles(selector, { defaultAll: true });
254
+ return ok(handles.map(sandboxSummaryData));
255
+ }
256
+ catch (error) {
257
+ return formatError(error);
258
+ }
259
+ });
260
+ server.registerTool("sandbox_status", {
261
+ title: "Sandbox Status",
262
+ description: "Show status for one sandbox or a filtered sandbox set.",
263
+ inputSchema: selectorSchema,
197
264
  annotations: {
198
265
  readOnlyHint: true,
199
266
  destructiveHint: false,
200
267
  idempotentHint: true,
201
268
  },
202
- }, async () => {
269
+ }, async (selector) => {
203
270
  try {
204
- const handles = await Sandbox.list();
205
- const results = handles.map((h) => ({
206
- name: h.name,
207
- status: h.status,
208
- createdAt: h.createdAt,
209
- }));
210
- return {
211
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
212
- };
271
+ const status = selector.status ?? (selector.name || selector.names ? "all" : "running");
272
+ const handles = await resolveSandboxHandles({ ...selector, status }, { defaultAll: true });
273
+ return ok(handles.map(sandboxSummaryData));
213
274
  }
214
275
  catch (error) {
215
276
  return formatError(error);
@@ -229,87 +290,443 @@ export function registerSandboxTools(server) {
229
290
  }, async ({ name }) => {
230
291
  try {
231
292
  const handle = await Sandbox.get(name);
232
- return {
233
- content: [{
234
- type: "text",
235
- text: JSON.stringify({
236
- name: handle.name,
237
- status: handle.status,
238
- config: handle.configJson,
239
- createdAt: handle.createdAt,
240
- updatedAt: handle.updatedAt,
241
- }, null, 2),
242
- }],
243
- };
293
+ return ok(sandboxHandleData(handle));
244
294
  }
245
295
  catch (error) {
246
296
  return formatError(error);
247
297
  }
248
298
  });
249
299
  server.registerTool("sandbox_stop", {
250
- title: "Stop Sandbox",
251
- description: "Stop a running sandbox. Use force to kill with SIGKILL instead of graceful SIGTERM.",
252
- inputSchema: z.object({
253
- name: z.string().describe("Sandbox name"),
254
- force: z.boolean().optional().describe("Force kill with SIGKILL instead of graceful SIGTERM"),
300
+ title: "Stop Sandboxes",
301
+ description: "Stop running sandboxes gracefully or forcefully.",
302
+ inputSchema: selectorSchema.extend({
303
+ force: z.boolean().optional(),
304
+ timeoutMs: z.number().int().nonnegative().optional(),
255
305
  }),
256
306
  annotations: {
257
307
  readOnlyHint: false,
258
308
  destructiveHint: true,
259
- idempotentHint: false,
309
+ idempotentHint: true,
260
310
  },
261
- }, async ({ name, force }) => {
311
+ }, async ({ force, timeoutMs, ...selector }) => {
262
312
  try {
263
- const handle = await Sandbox.get(name);
264
- if (force) {
265
- await handle.kill();
313
+ const handles = await resolveSandboxHandles(selector, { requireSelector: true });
314
+ const results = [];
315
+ for (const handle of handles) {
316
+ if (handle.status !== "running") {
317
+ results.push({ name: handle.name, stopped: false, alreadyStopped: true, status: handle.status });
318
+ continue;
319
+ }
320
+ if (force) {
321
+ if (typeof timeoutMs === "number")
322
+ await handle.killWithTimeout(timeoutMs);
323
+ else
324
+ await handle.kill();
325
+ }
326
+ else if (typeof timeoutMs === "number") {
327
+ await handle.stopWithTimeout(timeoutMs);
328
+ }
329
+ else {
330
+ await handle.stop();
331
+ }
332
+ results.push({ name: handle.name, stopped: true });
266
333
  }
267
- else {
268
- await handle.stop();
334
+ return ok(results);
335
+ }
336
+ catch (error) {
337
+ return formatError(error);
338
+ }
339
+ });
340
+ server.registerTool("sandbox_drain", {
341
+ title: "Drain Sandboxes",
342
+ description: "Request graceful drain for running sandboxes.",
343
+ inputSchema: selectorSchema,
344
+ annotations: {
345
+ readOnlyHint: false,
346
+ destructiveHint: false,
347
+ idempotentHint: true,
348
+ },
349
+ }, async (selector) => {
350
+ try {
351
+ const handles = await resolveSandboxHandles(selector, { requireSelector: true });
352
+ const results = [];
353
+ for (const handle of handles) {
354
+ if (handle.status !== "running") {
355
+ results.push({ name: handle.name, drainRequested: false, status: handle.status });
356
+ continue;
357
+ }
358
+ await handle.requestDrain();
359
+ results.push({ name: handle.name, drainRequested: true });
269
360
  }
270
- await waitForStopped(name);
271
- return {
272
- content: [{ type: "text", text: JSON.stringify({ name, status: "stopped" }, null, 2) }],
273
- };
361
+ return ok(results);
362
+ }
363
+ catch (error) {
364
+ return formatError(error);
365
+ }
366
+ });
367
+ server.registerTool("sandbox_wait", {
368
+ title: "Wait For Sandboxes",
369
+ description: "Wait until selected sandboxes reach a terminal state.",
370
+ inputSchema: selectorSchema,
371
+ annotations: {
372
+ readOnlyHint: true,
373
+ destructiveHint: false,
374
+ idempotentHint: true,
375
+ },
376
+ }, async (selector) => {
377
+ try {
378
+ const handles = await resolveSandboxHandles(selector, { requireSelector: true });
379
+ const results = [];
380
+ for (const handle of handles) {
381
+ results.push(await handle.waitUntilStopped());
382
+ }
383
+ return ok(results);
274
384
  }
275
385
  catch (error) {
276
386
  return formatError(error);
277
387
  }
278
388
  });
279
389
  server.registerTool("sandbox_remove", {
280
- title: "Remove Sandbox",
281
- description: "Remove a sandbox. Must be stopped unless force is true.",
282
- inputSchema: z.object({
283
- name: z.string().describe("Sandbox name"),
284
- force: z.boolean().optional().describe("Stop and remove in one step if still running"),
390
+ title: "Remove Sandboxes",
391
+ description: "Remove stopped sandboxes. Use force to stop running sandboxes first.",
392
+ inputSchema: selectorSchema.extend({
393
+ force: z.boolean().optional(),
285
394
  }),
286
395
  annotations: {
287
396
  readOnlyHint: false,
288
397
  destructiveHint: true,
289
398
  idempotentHint: true,
290
399
  },
291
- }, async ({ name, force }) => {
400
+ }, async ({ force, ...selector }) => {
292
401
  try {
293
- if (force) {
294
- try {
295
- const handle = await Sandbox.get(name);
296
- if (handle.status === "running") {
297
- await handle.kill();
298
- await waitForStopped(name);
299
- }
300
- }
301
- catch {
302
- // Sandbox may already be stopped or not exist.
402
+ const handles = await resolveSandboxHandles(selector, { requireSelector: true });
403
+ const results = [];
404
+ for (const handle of handles) {
405
+ if (force && handle.status === "running") {
406
+ await handle.kill();
303
407
  }
408
+ await Sandbox.remove(handle.name);
409
+ results.push({ name: handle.name, removed: true });
304
410
  }
305
- await Sandbox.remove(name);
306
- return {
307
- content: [{ type: "text", text: JSON.stringify({ name, removed: true }, null, 2) }],
308
- };
411
+ return ok(results);
309
412
  }
310
413
  catch (error) {
311
414
  return formatError(error);
312
415
  }
313
416
  });
314
417
  }
418
+ async function waitForStopped(name, timeoutMs = 10000) {
419
+ const start = Date.now();
420
+ while (Date.now() - start < timeoutMs) {
421
+ try {
422
+ const h = await Sandbox.get(name);
423
+ if (h.status !== "running")
424
+ return;
425
+ }
426
+ catch {
427
+ return;
428
+ }
429
+ await new Promise((r) => setTimeout(r, 200));
430
+ }
431
+ }
432
+ function buildSandbox(name, args, persistent) {
433
+ let builder = Sandbox.builder(name);
434
+ builder = applyRootfs(builder, args);
435
+ const cpus = args.resources?.cpus ?? args.cpus;
436
+ const memoryMib = args.resources?.memoryMib ?? args.memoryMib;
437
+ const workdir = args.process?.workdir ?? args.workdir;
438
+ const env = { ...(args.env ?? {}), ...(args.process?.env ?? {}) };
439
+ const entrypoint = args.process?.entrypoint ?? args.entrypoint;
440
+ const maxDuration = args.lifecycle?.maxDurationSecs ?? args.maxDuration;
441
+ const idleTimeout = args.lifecycle?.idleTimeoutSecs ?? args.idleTimeout;
442
+ if (cpus)
443
+ builder = builder.cpus(cpus);
444
+ if (memoryMib)
445
+ builder = builder.memory(memoryMib);
446
+ if (workdir)
447
+ builder = builder.workdir(workdir);
448
+ if (args.process?.shell)
449
+ builder = builder.shell(args.process.shell);
450
+ if (Object.keys(env).length > 0)
451
+ builder = builder.envs(env);
452
+ if (args.process?.labels)
453
+ builder = builder.labels(args.process.labels);
454
+ if (entrypoint)
455
+ builder = builder.entrypoint(entrypoint);
456
+ if (args.hostname)
457
+ builder = builder.hostname(args.hostname);
458
+ if (args.process?.user)
459
+ builder = builder.user(args.process.user);
460
+ if (args.process?.scripts)
461
+ builder = builder.scripts(args.process.scripts);
462
+ if (maxDuration)
463
+ builder = builder.maxDuration(maxDuration);
464
+ if (idleTimeout)
465
+ builder = builder.idleTimeout(idleTimeout);
466
+ if (args.lifecycle?.replaceTimeoutMs !== undefined)
467
+ builder = builder.replaceWithTimeout(args.lifecycle.replaceTimeoutMs);
468
+ else if (args.lifecycle?.replace)
469
+ builder = builder.replace();
470
+ if (args.lifecycle?.logLevel)
471
+ builder = builder.logLevel(args.lifecycle.logLevel);
472
+ if (args.lifecycle?.metricsSampleIntervalMs !== undefined)
473
+ builder = builder.metricsSampleIntervalMs(args.lifecycle.metricsSampleIntervalMs);
474
+ if (args.lifecycle?.disableMetricsSample)
475
+ builder = builder.disableMetricsSample();
476
+ if (args.lifecycle?.quietLogs)
477
+ builder = builder.quietLogs();
478
+ builder = builder.detached(args.lifecycle?.detached ?? persistent);
479
+ if (args.process?.init) {
480
+ const init = args.process.init;
481
+ if (init.env && Object.keys(init.env).length > 0) {
482
+ builder = builder.initWith(init.path, (b) => {
483
+ let acc = init.args ? b.args(init.args) : b;
484
+ acc = acc.envs(init.env);
485
+ return acc;
486
+ });
487
+ }
488
+ else {
489
+ builder = builder.init(init.path, init.args);
490
+ }
491
+ }
492
+ for (const rlimit of args.process?.rlimits ?? []) {
493
+ builder = builder.rlimitRange(rlimit.resource, rlimit.soft, rlimit.hard ?? rlimit.soft);
494
+ }
495
+ for (const mount of [...(args.volumes ?? []), ...(args.mounts ?? [])]) {
496
+ builder = applyMount(builder, mount);
497
+ }
498
+ if (args.patches && args.patches.length > 0) {
499
+ builder = builder.patch((p) => {
500
+ let acc = p;
501
+ for (const patch of args.patches ?? []) {
502
+ const kind = patch.kind ?? patch.type;
503
+ switch (kind) {
504
+ case "text":
505
+ acc = acc.text(requiredPath(patch), patch.content ?? "", { mode: patch.mode, replace: patch.replace });
506
+ break;
507
+ case "file":
508
+ acc = acc.file(requiredPath(patch), Buffer.from(patch.contentBase64 ?? "", "base64"), { mode: patch.mode, replace: patch.replace });
509
+ break;
510
+ case "copyFile":
511
+ acc = acc.copyFile(assertHostPathAllowed(requiredHostPath(patch)), requiredGuestPath(patch), { mode: patch.mode, replace: patch.replace });
512
+ break;
513
+ case "copyDir":
514
+ acc = acc.copyDir(assertHostPathAllowed(requiredHostPath(patch)), requiredGuestPath(patch), { replace: patch.replace });
515
+ break;
516
+ case "mkdir":
517
+ acc = acc.mkdir(requiredPath(patch), { mode: patch.mode });
518
+ break;
519
+ case "append":
520
+ acc = acc.append(requiredPath(patch), patch.content ?? "");
521
+ break;
522
+ case "remove":
523
+ acc = acc.remove(requiredPath(patch));
524
+ break;
525
+ case "symlink":
526
+ acc = acc.symlink(patch.target ?? "", patch.linkPath ?? requiredPath(patch), { replace: patch.replace });
527
+ break;
528
+ }
529
+ }
530
+ return acc;
531
+ });
532
+ }
533
+ if (args.network || args.secrets) {
534
+ builder = builder.network((n) => {
535
+ let acc = n;
536
+ if (args.network?.disabled)
537
+ acc = acc.enabled(false);
538
+ for (const port of args.network?.ports ?? []) {
539
+ const protocol = port.protocol ?? "tcp";
540
+ if (protocol === "udp" && port.bindAddress)
541
+ acc = acc.portUdpBind(port.bindAddress, port.hostPort, port.guestPort);
542
+ else if (protocol === "udp")
543
+ acc = acc.portUdp(port.hostPort, port.guestPort);
544
+ else if (port.bindAddress)
545
+ acc = acc.portBind(port.bindAddress, port.hostPort, port.guestPort);
546
+ else
547
+ acc = acc.port(port.hostPort, port.guestPort);
548
+ }
549
+ if (args.network?.maxConnections)
550
+ acc = acc.maxConnections(args.network.maxConnections);
551
+ if (args.network?.trustHostCas || args.network?.tls?.trustHostCas)
552
+ acc = acc.trustHostCAs(true);
553
+ if (args.network?.ipv4Pool)
554
+ acc = acc.ipv4Pool(args.network.ipv4Pool);
555
+ if (args.network?.ipv6Pool)
556
+ acc = acc.ipv6Pool(args.network.ipv6Pool);
557
+ if (args.network?.dns) {
558
+ const dns = args.network.dns;
559
+ acc = acc.dns((b) => {
560
+ let next = b;
561
+ if (dns.allowRebind !== undefined)
562
+ next = next.rebindProtection(!dns.allowRebind);
563
+ if (dns.nameservers)
564
+ next = next.nameservers(dns.nameservers);
565
+ if (dns.queryTimeoutMs)
566
+ next = next.queryTimeoutMs(dns.queryTimeoutMs);
567
+ return next;
568
+ });
569
+ }
570
+ if (args.network?.tls) {
571
+ const tls = args.network.tls;
572
+ acc = acc.tls((b) => {
573
+ let next = b;
574
+ for (const bypass of tls.bypassDomains ?? [])
575
+ next = next.bypass(bypass);
576
+ if (tls.ports)
577
+ next = next.interceptedPorts(tls.ports);
578
+ if (tls.blockQuic !== undefined)
579
+ next = next.blockQuic(tls.blockQuic);
580
+ if (tls.caCertPath)
581
+ next = next.interceptCaCert(assertHostPathAllowed(tls.caCertPath));
582
+ if (tls.caKeyPath)
583
+ next = next.interceptCaKey(assertHostPathAllowed(tls.caKeyPath));
584
+ for (const ca of tls.upstreamCaCertPaths ?? [])
585
+ next = next.upstreamCaCert(assertHostPathAllowed(ca));
586
+ return next;
587
+ });
588
+ }
589
+ for (const secret of args.secrets ?? []) {
590
+ const value = secret.value ?? (secret.valueEnv ? process.env[secret.valueEnv] : undefined);
591
+ if (value === undefined)
592
+ throw new Error(`missing secret value for ${secret.envVar}`);
593
+ acc = acc.secretEnvSimple(secret.envVar, value, secret.allowedHost);
594
+ }
595
+ return acc;
596
+ });
597
+ }
598
+ return builder;
599
+ }
600
+ function applyRootfs(builder, args) {
601
+ const rootfs = args.rootfs;
602
+ if (rootfs) {
603
+ switch (rootfs.kind) {
604
+ case "oci":
605
+ builder = builder.imageWith((image) => {
606
+ let acc = image.oci(rootfs.reference);
607
+ if (rootfs.upperSizeMib)
608
+ acc = acc.upperSize(rootfs.upperSizeMib);
609
+ return acc;
610
+ });
611
+ if (rootfs.pullPolicy)
612
+ builder = builder.pullPolicy(rootfs.pullPolicy);
613
+ if (rootfs.registry) {
614
+ const registry = rootfs.registry;
615
+ builder = builder.registry((r) => {
616
+ let acc = r;
617
+ if (registry.insecure)
618
+ acc = acc.insecure();
619
+ if (registry.caCertsPath)
620
+ acc = acc.caCertsPath(assertHostPathAllowed(registry.caCertsPath));
621
+ if (registry.auth)
622
+ acc = acc.auth(registry.auth);
623
+ return acc;
624
+ });
625
+ }
626
+ return builder;
627
+ case "bind":
628
+ return builder.image(assertHostPathAllowed(rootfs.path));
629
+ case "disk":
630
+ return builder.imageWith((image) => {
631
+ let acc = image.disk(assertHostPathAllowed(rootfs.path));
632
+ if (rootfs.fstype)
633
+ acc = acc.fstype(rootfs.fstype);
634
+ return acc;
635
+ });
636
+ case "snapshot":
637
+ return builder.fromSnapshot(resolvePathOrName(rootfs.pathOrName));
638
+ }
639
+ }
640
+ if (!args.image)
641
+ throw new Error("sandbox_create requires rootfs or image");
642
+ return builder.image(resolvePathOrName(args.image));
643
+ }
644
+ function applyMount(builder, mount) {
645
+ return builder.volume(mount.guestPath, (m) => {
646
+ let acc = m;
647
+ switch (mount.kind) {
648
+ case "bind":
649
+ acc = acc.bind(assertHostPathAllowed(mount.hostPath ?? mount.source ?? ""));
650
+ break;
651
+ case "named":
652
+ acc = acc.named(mount.name ?? mount.source ?? "");
653
+ break;
654
+ case "tmpfs":
655
+ acc = acc.tmpfs();
656
+ if (mount.sizeMib)
657
+ acc = acc.size(mount.sizeMib);
658
+ break;
659
+ case "disk":
660
+ acc = acc.disk(assertHostPathAllowed(mount.hostPath ?? mount.source ?? ""));
661
+ if (mount.format)
662
+ acc = acc.format(mount.format);
663
+ if (mount.fstype)
664
+ acc = acc.fstype(mount.fstype);
665
+ break;
666
+ }
667
+ if (mount.readonly)
668
+ acc = acc.readonly();
669
+ if (mount.noexec)
670
+ acc = acc.noexec();
671
+ if (mount.nosuid)
672
+ acc = acc.nosuid();
673
+ if (mount.nodev)
674
+ acc = acc.nodev();
675
+ if (mount.statVirtualization)
676
+ acc = acc.statVirtualization(mount.statVirtualization);
677
+ if (mount.hostPermissions)
678
+ acc = acc.hostPermissions(mount.hostPermissions);
679
+ return acc;
680
+ });
681
+ }
682
+ async function resolveSandboxHandles(selector, options = {}) {
683
+ const names = [...(selector.name ? [selector.name] : []), ...(selector.names ?? [])];
684
+ const hasSelector = names.length > 0 || Object.keys(selector.labels ?? {}).length > 0 || selector.status !== undefined;
685
+ if (options.requireSelector && !hasSelector)
686
+ throw new Error("at least one selector is required");
687
+ const map = new Map();
688
+ for (const name of names) {
689
+ const handle = await Sandbox.get(name);
690
+ map.set(handle.name, handle);
691
+ }
692
+ if (Object.keys(selector.labels ?? {}).length > 0) {
693
+ for (const handle of await Sandbox.listWith({ labels: selector.labels })) {
694
+ map.set(handle.name, handle);
695
+ }
696
+ }
697
+ else if (names.length === 0 && (selector.status !== undefined || options.defaultAll)) {
698
+ for (const handle of await Sandbox.list()) {
699
+ map.set(handle.name, handle);
700
+ }
701
+ }
702
+ let handles = [...map.values()];
703
+ if (selector.status && selector.status !== "all") {
704
+ handles = handles.filter((handle) => handle.status === selector.status);
705
+ }
706
+ handles.sort((left, right) => left.name.localeCompare(right.name));
707
+ return handles;
708
+ }
709
+ function resolvePathOrName(value) {
710
+ return looksLikePath(value) ? assertHostPathAllowed(value) : value;
711
+ }
712
+ function looksLikePath(value) {
713
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~");
714
+ }
715
+ function requiredPath(patch) {
716
+ const path = patch.path ?? patch.guestPath;
717
+ if (!path)
718
+ throw new Error(`${patch.kind} patch requires path`);
719
+ return path;
720
+ }
721
+ function requiredGuestPath(patch) {
722
+ const path = patch.guestPath ?? patch.path;
723
+ if (!path)
724
+ throw new Error(`${patch.kind} patch requires guestPath`);
725
+ return path;
726
+ }
727
+ function requiredHostPath(patch) {
728
+ if (!patch.hostPath)
729
+ throw new Error(`${patch.kind} patch requires hostPath`);
730
+ return patch.hostPath;
731
+ }
315
732
  //# sourceMappingURL=sandbox.js.map