@agent-sandbox/cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/dist/index.js +191 -26
- 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
|
|
169
|
-
const {
|
|
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:
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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:
|
|
204
|
+
path: "/volumes/create",
|
|
205
|
+
body: {}
|
|
184
206
|
});
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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: () =>
|
|
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.0",
|
|
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;
|