@agent-sandbox/cli 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -9
  2. package/dist/index.js +191 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,6 +4,8 @@ Command-line interface for Agent Sandbox.
4
4
 
5
5
  ## Install
6
6
 
7
+ Install or upgrade the CLI:
8
+
7
9
  ```bash
8
10
  npm install -g @agent-sandbox/cli
9
11
  ```
@@ -22,17 +24,24 @@ npx skills add https://github.com/usamaasfar/agent-sandbox/tree/main/skills/agen
22
24
 
23
25
  ```bash
24
26
  agent-sandbox create
25
- agent-sandbox write <sandboxId> "printf 'hello\n' > /proc/1/fd/1"
26
- agent-sandbox read <sandboxId>
27
- agent-sandbox delete <sandboxId>
27
+ agent-sandbox write <sandboxId|name> "printf 'hello\n' > /proc/1/fd/1"
28
+ agent-sandbox read <sandboxId|name>
29
+ agent-sandbox delete <sandboxId|name>
28
30
  ```
29
31
 
30
32
  ## Command Reference
31
33
 
34
+ ### `--version`
35
+
36
+ ```bash
37
+ agent-sandbox --version
38
+ agent-sandbox -v
39
+ ```
40
+
32
41
  ### `create`
33
42
 
34
43
  ```bash
35
- agent-sandbox create [--name <name>] [--image <image>]
44
+ agent-sandbox create [--name <name>] [--image <image>] [--clone <sandboxId|name>]
36
45
  ```
37
46
 
38
47
  Prints the `sandboxId`.
@@ -40,7 +49,7 @@ Prints the `sandboxId`.
40
49
  ### `delete`
41
50
 
42
51
  ```bash
43
- agent-sandbox delete <sandboxId>
52
+ agent-sandbox delete <sandboxId|name>
44
53
  ```
45
54
 
46
55
  ### `list`
@@ -54,7 +63,7 @@ Prints a JSON array of managed sandboxes.
54
63
  ### `read`
55
64
 
56
65
  ```bash
57
- agent-sandbox read <sandboxId> [--tail <n>]
66
+ agent-sandbox read <sandboxId|name> [--tail <n>]
58
67
  ```
59
68
 
60
69
  Returns container logs. `--tail` limits to the last N lines.
@@ -62,7 +71,7 @@ Returns container logs. `--tail` limits to the last N lines.
62
71
  ### `write`
63
72
 
64
73
  ```bash
65
- agent-sandbox write <sandboxId> <input> [--detach]
74
+ agent-sandbox write <sandboxId|name> <input> [--detach]
66
75
  ```
67
76
 
68
77
  Runs the command and returns its output and exit code. Use `--detach` to fire-and-forget
@@ -71,7 +80,7 @@ Runs the command and returns its output and exit code. Use `--detach` to fire-an
71
80
  ### `upload`
72
81
 
73
82
  ```bash
74
- agent-sandbox upload <sandboxId> <localPath> <remotePath>
83
+ agent-sandbox upload <sandboxId|name> <localPath> <remotePath>
75
84
  ```
76
85
 
77
86
  Both paths must be absolute.
@@ -79,7 +88,7 @@ Both paths must be absolute.
79
88
  ### `download`
80
89
 
81
90
  ```bash
82
- agent-sandbox download <sandboxId> <remotePath> <localPath>
91
+ agent-sandbox download <sandboxId|name> <remotePath> <localPath>
83
92
  ```
84
93
 
85
94
  `remotePath` may be a file or directory. Both paths must be absolute.
package/dist/index.js CHANGED
@@ -165,26 +165,71 @@ async function dockerJSON(options) {
165
165
  }
166
166
 
167
167
  // ../../packages/core/src/primitives/create.ts
168
- async function create(options = {}) {
169
- const { name, image = "agent-sandbox" } = options;
170
- const createResponse = await dockerJSON({
168
+ async function copyVolume(sourceVolume, destVolume) {
169
+ const { Id } = await dockerJSON({
171
170
  method: "POST",
172
171
  path: "/containers/create",
173
- query: name ? { name } : undefined,
174
172
  body: {
175
- Image: image,
176
- Labels: {
177
- "agent-sandbox": "true"
173
+ Image: "ghcr.io/usamaasfar/agent-sandbox:latest",
174
+ Cmd: ["sh", "-c", "cp -a /from/. /to/"],
175
+ HostConfig: {
176
+ Binds: [`${sourceVolume}:/from:ro`, `${destVolume}:/to`]
178
177
  }
179
178
  }
180
179
  });
181
- await dockerRequest({
180
+ try {
181
+ await dockerRequest({ method: "POST", path: `/containers/${Id}/start` });
182
+ const { StatusCode } = await dockerJSON({
183
+ method: "POST",
184
+ path: `/containers/${Id}/wait`
185
+ });
186
+ if (StatusCode !== 0) {
187
+ throw new SandboxError(`Volume copy failed with exit code ${StatusCode}`);
188
+ }
189
+ } finally {
190
+ await dockerRequest({ method: "DELETE", path: `/containers/${Id}`, allowStatus: [404] });
191
+ }
192
+ }
193
+ async function getVolumeForSandbox(sandboxId) {
194
+ const info = await dockerJSON({
195
+ method: "GET",
196
+ path: `/containers/${sandboxId}/json`
197
+ });
198
+ return info.Mounts.find((m) => m.Destination === "/data" && m.Type === "volume")?.Name;
199
+ }
200
+ async function create(options = {}) {
201
+ const { name, image = "ghcr.io/usamaasfar/agent-sandbox:latest", clone } = options;
202
+ const { Name: volume } = await dockerJSON({
182
203
  method: "POST",
183
- path: `/containers/${createResponse.Id}/start`
204
+ path: "/volumes/create",
205
+ body: {}
184
206
  });
185
- return {
186
- sandboxId: createResponse.Id.slice(0, 12)
187
- };
207
+ try {
208
+ if (clone) {
209
+ const sourceVolume = await getVolumeForSandbox(clone);
210
+ if (!sourceVolume) {
211
+ throw new SandboxError(`No volume found for sandbox ${clone}`);
212
+ }
213
+ await copyVolume(sourceVolume, volume);
214
+ }
215
+ const { Id } = await dockerJSON({
216
+ method: "POST",
217
+ path: "/containers/create",
218
+ query: name ? { name } : undefined,
219
+ body: {
220
+ Image: image,
221
+ Labels: { "agent-sandbox": "true" },
222
+ HostConfig: {
223
+ Binds: [`${volume}:/data`]
224
+ }
225
+ }
226
+ });
227
+ await dockerRequest({ method: "POST", path: `/containers/${Id}/start` });
228
+ return { sandboxId: Id.slice(0, 12) };
229
+ } catch (error) {
230
+ await dockerRequest({ method: "DELETE", path: `/volumes/${volume}`, allowStatus: [404] });
231
+ throw error;
232
+ }
188
233
  }
189
234
  // ../../packages/core/src/primitives/delete.ts
190
235
  function remapContainerNotFound(error, sandboxId) {
@@ -193,8 +238,20 @@ function remapContainerNotFound(error, sandboxId) {
193
238
  }
194
239
  throw error;
195
240
  }
241
+ async function getVolumeForSandbox2(sandboxId) {
242
+ try {
243
+ const info = await dockerJSON({
244
+ method: "GET",
245
+ path: `/containers/${sandboxId}/json`
246
+ });
247
+ return info.Mounts?.find((m) => m.Destination === "/data" && m.Type === "volume")?.Name ?? null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
196
252
  async function deleteContainer(options) {
197
253
  const { sandboxId } = options;
254
+ const volume = await getVolumeForSandbox2(sandboxId);
198
255
  try {
199
256
  await dockerRequest({
200
257
  method: "POST",
@@ -212,9 +269,14 @@ async function deleteContainer(options) {
212
269
  } catch (error) {
213
270
  remapContainerNotFound(error, sandboxId);
214
271
  }
215
- return {
216
- ok: true
217
- };
272
+ if (volume) {
273
+ await dockerRequest({
274
+ method: "DELETE",
275
+ path: `/volumes/${volume}`,
276
+ allowStatus: [404]
277
+ });
278
+ }
279
+ return { ok: true };
218
280
  }
219
281
  // ../../packages/core/src/primitives/download.ts
220
282
  import path2 from "path";
@@ -222,7 +284,12 @@ import path2 from "path";
222
284
  // ../../packages/core/src/archive.ts
223
285
  import { tmpdir } from "os";
224
286
  import path from "path";
225
- import { mkdir, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
287
+ import { cp, mkdir, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
288
+ function ensureSafeTarEntryName(name) {
289
+ if (name === "." || name === ".." || path.basename(name) !== name) {
290
+ throw new SandboxError(`Invalid remote path name: ${name}`);
291
+ }
292
+ }
226
293
  function ensureAbsoluteHostPath(targetPath) {
227
294
  if (!path.isAbsolute(targetPath)) {
228
295
  throw new SandboxError(`Host path must be absolute: ${targetPath}`);
@@ -259,26 +326,70 @@ function spawnTar(command, options) {
259
326
  throw new SandboxError(`Failed to start tar: ${message}`);
260
327
  }
261
328
  }
262
- async function createTarStream(localPath) {
263
- ensureAbsoluteHostPath(localPath);
264
- await ensurePathExists(localPath);
329
+ async function createTarSource(localPath, remoteName) {
265
330
  const directory = path.dirname(localPath);
266
331
  const name = path.basename(localPath);
267
- const process2 = spawnTar(["tar", "-C", directory, "-cf", "-", name], {
268
- stdout: "pipe",
269
- stderr: "pipe"
270
- });
332
+ if (!remoteName || remoteName === name) {
333
+ return {
334
+ cleanup: async () => {},
335
+ directory,
336
+ name
337
+ };
338
+ }
339
+ ensureSafeTarEntryName(remoteName);
340
+ const stagingDirectory = await mkdtemp(path.join(tmpdir(), "agent-sandbox-upload-"));
341
+ const stagingPath = path.join(stagingDirectory, remoteName);
342
+ try {
343
+ await cp(localPath, stagingPath, { force: true, recursive: true });
344
+ } catch (error) {
345
+ await rm(stagingDirectory, { recursive: true, force: true });
346
+ throw error;
347
+ }
348
+ return {
349
+ cleanup: async () => {
350
+ await rm(stagingDirectory, { recursive: true, force: true });
351
+ },
352
+ directory: stagingDirectory,
353
+ name: remoteName
354
+ };
355
+ }
356
+ async function createTarStream(localPath, remoteName) {
357
+ ensureAbsoluteHostPath(localPath);
358
+ await ensurePathExists(localPath);
359
+ const source = await createTarSource(localPath, remoteName);
360
+ let process2;
361
+ try {
362
+ process2 = spawnTar(["tar", "-C", source.directory, "-cf", "-", "--", source.name], {
363
+ env: {
364
+ ...globalThis.process.env,
365
+ COPYFILE_DISABLE: "1"
366
+ },
367
+ stdout: "pipe",
368
+ stderr: "pipe"
369
+ });
370
+ } catch (error) {
371
+ await source.cleanup();
372
+ throw error;
373
+ }
271
374
  const stream = process2.stdout;
272
375
  if (!stream) {
376
+ await source.cleanup();
273
377
  throw new SandboxError("Tar create failed: tar did not produce an archive stream");
274
378
  }
275
379
  return {
276
380
  stream,
277
- complete: () => ensureTarSuccess(process2, "Tar create"),
381
+ complete: async () => {
382
+ try {
383
+ await ensureTarSuccess(process2, "Tar create");
384
+ } finally {
385
+ await source.cleanup();
386
+ }
387
+ },
278
388
  abort: () => {
279
389
  try {
280
390
  process2.kill();
281
391
  } catch {}
392
+ source.cleanup().catch(() => {});
282
393
  }
283
394
  };
284
395
  }
@@ -436,7 +547,7 @@ function remapUploadError(error, sandboxId) {
436
547
  }
437
548
  async function upload(options) {
438
549
  ensureAbsoluteContainerPath2(options.remotePath);
439
- const archive = await createTarStream(options.localPath);
550
+ const archive = await createTarStream(options.localPath, path3.posix.basename(options.remotePath));
440
551
  try {
441
552
  await dockerRequest({
442
553
  method: "PUT",
@@ -546,6 +657,7 @@ function readOption(args, index, name) {
546
657
  async function runCreateCommand(args) {
547
658
  let name;
548
659
  let image;
660
+ let clone;
549
661
  for (let i = 0;i < args.length; ) {
550
662
  const arg = args[i];
551
663
  switch (arg) {
@@ -561,11 +673,17 @@ async function runCreateCommand(args) {
561
673
  i = option.nextIndex;
562
674
  break;
563
675
  }
676
+ case "--clone": {
677
+ const option = readOption(args, i, "--clone");
678
+ clone = option.value;
679
+ i = option.nextIndex;
680
+ break;
681
+ }
564
682
  default:
565
683
  throw new Error(`Unknown argument: ${arg}`);
566
684
  }
567
685
  }
568
- const options = { name, image };
686
+ const options = { name, image, clone };
569
687
  const result = await create(options);
570
688
  console.log(result.sandboxId);
571
689
  }
@@ -670,6 +788,49 @@ async function runWriteCommand(args) {
670
788
  }
671
789
  }
672
790
  }
791
+ // package.json
792
+ var package_default = {
793
+ name: "@agent-sandbox/cli",
794
+ version: "0.2.1",
795
+ description: "CLI to spin up isolated terminal sandboxes for AI agents",
796
+ type: "module",
797
+ license: "MIT",
798
+ bin: {
799
+ "agent-sandbox": "./dist/index.js"
800
+ },
801
+ files: [
802
+ "dist"
803
+ ],
804
+ scripts: {
805
+ dev: "bun run index.ts",
806
+ build: "bun build ./index.ts --outfile dist/index.js --target bun",
807
+ "build:binary": "bun build ./index.ts --compile --minify --bytecode --outfile dist/agent-sandbox"
808
+ },
809
+ devDependencies: {
810
+ "@agent-sandbox/core": "*",
811
+ "@types/bun": "^1.3.9"
812
+ },
813
+ engines: {
814
+ bun: ">=1.0.0"
815
+ },
816
+ repository: {
817
+ type: "git",
818
+ url: "git+https://github.com/usamaasfar/agent-sandbox.git",
819
+ directory: "apps/cli"
820
+ },
821
+ homepage: "https://github.com/usamaasfar/agent-sandbox#readme",
822
+ bugs: {
823
+ url: "https://github.com/usamaasfar/agent-sandbox/issues"
824
+ },
825
+ keywords: [
826
+ "sandbox",
827
+ "agent",
828
+ "ai",
829
+ "terminal",
830
+ "docker",
831
+ "cli"
832
+ ]
833
+ };
673
834
 
674
835
  // index.ts
675
836
  var USAGE = "Usage: agent-sandbox <create|delete|list|read|write|upload|download> ...";
@@ -678,6 +839,10 @@ function printHelp() {
678
839
  }
679
840
  async function main() {
680
841
  const [command, ...rest] = process.argv.slice(2);
842
+ if (command === "--version" || command === "-v") {
843
+ console.log(package_default.version);
844
+ return;
845
+ }
681
846
  if (command === undefined || command === "--help" || command === "-h" || command === "help") {
682
847
  printHelp();
683
848
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-sandbox/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "CLI to spin up isolated terminal sandboxes for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",