@agent-sandbox/api 0.1.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 +112 -0
- package/dist/index.js +556 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @agent-sandbox/api
|
|
2
|
+
|
|
3
|
+
Programmatic wrapper for Agent Sandbox.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agent-sandbox/api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Docker running locally
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { AgentSandbox } from "@agent-sandbox/api";
|
|
19
|
+
|
|
20
|
+
const sandbox = new AgentSandbox();
|
|
21
|
+
const { sandboxId } = await sandbox.create();
|
|
22
|
+
|
|
23
|
+
const { output } = await sandbox.write({ sandboxId, input: "echo hello" });
|
|
24
|
+
console.log(output);
|
|
25
|
+
|
|
26
|
+
await sandbox.delete({ sandboxId });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API Reference
|
|
30
|
+
|
|
31
|
+
### `create(options?)`
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
sandbox.create(options?: {
|
|
35
|
+
name?: string;
|
|
36
|
+
image?: string;
|
|
37
|
+
volume?: string;
|
|
38
|
+
}): Promise<{ sandboxId: string }>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `delete(options)`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
sandbox.delete(options: {
|
|
45
|
+
sandboxId: string;
|
|
46
|
+
}): Promise<{ ok: boolean }>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `list()`
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
sandbox.list(): Promise<{
|
|
53
|
+
sandboxes: Array<{
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
status: string;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
}>;
|
|
59
|
+
}>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `read(options)`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
sandbox.read(options: {
|
|
66
|
+
sandboxId: string;
|
|
67
|
+
tail?: number;
|
|
68
|
+
}): Promise<{ output: string }>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Returns container logs. `tail` limits to the last N lines.
|
|
72
|
+
|
|
73
|
+
### `write(options)`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
sandbox.write(options: {
|
|
77
|
+
sandboxId: string;
|
|
78
|
+
input: string;
|
|
79
|
+
detach?: boolean;
|
|
80
|
+
}): Promise<{ ok: boolean; output?: string; exitCode?: number }>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Runs the command and returns its output and exit code. Set `detach: true` to fire-and-forget
|
|
84
|
+
(returns immediately with no output, useful for starting background processes).
|
|
85
|
+
|
|
86
|
+
### `upload(options)`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
sandbox.upload(options: {
|
|
90
|
+
sandboxId: string;
|
|
91
|
+
localPath: string;
|
|
92
|
+
remotePath: string;
|
|
93
|
+
}): Promise<{ ok: boolean }>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Both paths must be absolute.
|
|
97
|
+
|
|
98
|
+
### `download(options)`
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
sandbox.download(options: {
|
|
102
|
+
sandboxId: string;
|
|
103
|
+
remotePath: string;
|
|
104
|
+
localPath: string;
|
|
105
|
+
}): Promise<{ ok: boolean }>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`remotePath` may be a file or directory. Both paths must be absolute.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../packages/core/src/errors.ts
|
|
3
|
+
class SandboxError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "SandboxError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class ContainerNotFoundError extends SandboxError {
|
|
11
|
+
constructor(sandboxId) {
|
|
12
|
+
super(`Container not found: ${sandboxId}`);
|
|
13
|
+
this.name = "ContainerNotFoundError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class DockerError extends SandboxError {
|
|
18
|
+
status;
|
|
19
|
+
constructor(status, message) {
|
|
20
|
+
super(`Docker API error ${status}: ${message}`);
|
|
21
|
+
this.name = "DockerError";
|
|
22
|
+
this.status = status;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ExecError extends SandboxError {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(`Exec failed: ${message}`);
|
|
29
|
+
this.name = "ExecError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ../../packages/core/src/docker/client.ts
|
|
33
|
+
var DOCKER_HOST = "http://localhost";
|
|
34
|
+
var CLIENT_API_VERSION = "1.53";
|
|
35
|
+
var DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
36
|
+
var cachedApiVersion = null;
|
|
37
|
+
var resolvingApiVersion = null;
|
|
38
|
+
function isRawBody(body) {
|
|
39
|
+
return typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || body instanceof FormData || body instanceof URLSearchParams || body instanceof ReadableStream || ArrayBuffer.isView(body);
|
|
40
|
+
}
|
|
41
|
+
function parseApiVersion(version) {
|
|
42
|
+
const match = version.match(/^(\d+)\.(\d+)$/);
|
|
43
|
+
if (!match) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return [Number(match[1]), Number(match[2])];
|
|
47
|
+
}
|
|
48
|
+
function selectApiVersion(serverVersion) {
|
|
49
|
+
const client = parseApiVersion(CLIENT_API_VERSION);
|
|
50
|
+
const server = parseApiVersion(serverVersion);
|
|
51
|
+
if (!client || !server) {
|
|
52
|
+
return CLIENT_API_VERSION;
|
|
53
|
+
}
|
|
54
|
+
if (server[0] < client[0]) {
|
|
55
|
+
return serverVersion;
|
|
56
|
+
}
|
|
57
|
+
if (server[0] > client[0]) {
|
|
58
|
+
return CLIENT_API_VERSION;
|
|
59
|
+
}
|
|
60
|
+
return server[1] < client[1] ? serverVersion : CLIENT_API_VERSION;
|
|
61
|
+
}
|
|
62
|
+
function buildUrl(path, query, apiVersion = CLIENT_API_VERSION) {
|
|
63
|
+
const url = new URL(`${DOCKER_HOST}/v${apiVersion}${path}`);
|
|
64
|
+
if (query) {
|
|
65
|
+
for (const [key, value] of Object.entries(query)) {
|
|
66
|
+
url.searchParams.set(key, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return url.toString();
|
|
70
|
+
}
|
|
71
|
+
async function dockerFetch(input, init) {
|
|
72
|
+
return fetch(input, {
|
|
73
|
+
...init,
|
|
74
|
+
unix: DOCKER_SOCKET_PATH
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function readDockerErrorMessage(response) {
|
|
78
|
+
let text = "";
|
|
79
|
+
try {
|
|
80
|
+
text = await response.text();
|
|
81
|
+
} catch {
|
|
82
|
+
return response.statusText || "Request failed";
|
|
83
|
+
}
|
|
84
|
+
if (!text) {
|
|
85
|
+
return response.statusText || "Request failed";
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(text);
|
|
89
|
+
if (typeof parsed.message === "string" && parsed.message.trim()) {
|
|
90
|
+
return parsed.message;
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
async function probeApiVersion() {
|
|
96
|
+
try {
|
|
97
|
+
const response = await dockerFetch(`${DOCKER_HOST}/version`, {
|
|
98
|
+
method: "GET"
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const payload = await response.json();
|
|
104
|
+
if (typeof payload.ApiVersion !== "string" || !payload.ApiVersion.trim()) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return selectApiVersion(payload.ApiVersion);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function resolveApiVersion() {
|
|
113
|
+
if (cachedApiVersion) {
|
|
114
|
+
return cachedApiVersion;
|
|
115
|
+
}
|
|
116
|
+
if (!resolvingApiVersion) {
|
|
117
|
+
resolvingApiVersion = probeApiVersion().then((apiVersion) => {
|
|
118
|
+
if (apiVersion) {
|
|
119
|
+
cachedApiVersion = apiVersion;
|
|
120
|
+
return apiVersion;
|
|
121
|
+
}
|
|
122
|
+
return CLIENT_API_VERSION;
|
|
123
|
+
}).finally(() => {
|
|
124
|
+
resolvingApiVersion = null;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return resolvingApiVersion;
|
|
128
|
+
}
|
|
129
|
+
async function dockerRequest(options) {
|
|
130
|
+
const { method = "GET", path, query, body, headers = {}, allowStatus = [] } = options;
|
|
131
|
+
const requestHeaders = { ...headers };
|
|
132
|
+
let requestBody;
|
|
133
|
+
if (body !== undefined) {
|
|
134
|
+
if (isRawBody(body)) {
|
|
135
|
+
requestBody = body;
|
|
136
|
+
} else {
|
|
137
|
+
requestBody = JSON.stringify(body);
|
|
138
|
+
if (!Object.keys(requestHeaders).some((key) => key.toLowerCase() === "content-type")) {
|
|
139
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
const apiVersion = await resolveApiVersion();
|
|
146
|
+
response = await dockerFetch(buildUrl(path, query, apiVersion), {
|
|
147
|
+
method,
|
|
148
|
+
headers: requestHeaders,
|
|
149
|
+
body: requestBody
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : "Unknown Docker transport error";
|
|
153
|
+
throw new DockerError(0, message);
|
|
154
|
+
}
|
|
155
|
+
if (!response.ok && !allowStatus.includes(response.status)) {
|
|
156
|
+
throw new DockerError(response.status, await readDockerErrorMessage(response));
|
|
157
|
+
}
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
async function dockerJSON(options) {
|
|
161
|
+
const response = await dockerRequest(options);
|
|
162
|
+
return await response.json();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ../../packages/core/src/primitives/create.ts
|
|
166
|
+
async function create(options = {}) {
|
|
167
|
+
const { name, image = "agent-sandbox", volume } = options;
|
|
168
|
+
const hostConfig = volume ? { Binds: [`${volume}:/home/sandbox`] } : undefined;
|
|
169
|
+
const createResponse = await dockerJSON({
|
|
170
|
+
method: "POST",
|
|
171
|
+
path: "/containers/create",
|
|
172
|
+
query: name ? { name } : undefined,
|
|
173
|
+
body: {
|
|
174
|
+
Image: image,
|
|
175
|
+
Labels: {
|
|
176
|
+
"agent-sandbox": "true"
|
|
177
|
+
},
|
|
178
|
+
...hostConfig ? { HostConfig: hostConfig } : {}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
await dockerRequest({
|
|
182
|
+
method: "POST",
|
|
183
|
+
path: `/containers/${createResponse.Id}/start`
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
sandboxId: createResponse.Id.slice(0, 12)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// ../../packages/core/src/primitives/delete.ts
|
|
190
|
+
function remapContainerNotFound(error, sandboxId) {
|
|
191
|
+
if (error instanceof DockerError && error.status === 404) {
|
|
192
|
+
throw new ContainerNotFoundError(sandboxId);
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
async function deleteContainer(options) {
|
|
197
|
+
const { sandboxId } = options;
|
|
198
|
+
try {
|
|
199
|
+
await dockerRequest({
|
|
200
|
+
method: "POST",
|
|
201
|
+
path: `/containers/${sandboxId}/stop`,
|
|
202
|
+
allowStatus: [304]
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
remapContainerNotFound(error, sandboxId);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await dockerRequest({
|
|
209
|
+
method: "DELETE",
|
|
210
|
+
path: `/containers/${sandboxId}`
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
remapContainerNotFound(error, sandboxId);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
ok: true
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// ../../packages/core/src/primitives/download.ts
|
|
220
|
+
import path2 from "path";
|
|
221
|
+
|
|
222
|
+
// ../../packages/core/src/archive.ts
|
|
223
|
+
import { tmpdir } from "os";
|
|
224
|
+
import path from "path";
|
|
225
|
+
import { mkdir, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
|
|
226
|
+
function ensureAbsoluteHostPath(targetPath) {
|
|
227
|
+
if (!path.isAbsolute(targetPath)) {
|
|
228
|
+
throw new SandboxError(`Host path must be absolute: ${targetPath}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function ensurePathExists(targetPath) {
|
|
232
|
+
try {
|
|
233
|
+
await stat(targetPath);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
236
|
+
throw new SandboxError(`Host path does not exist: ${targetPath}`);
|
|
237
|
+
}
|
|
238
|
+
if (error instanceof Error) {
|
|
239
|
+
throw new SandboxError(`Failed to access host path ${targetPath}: ${error.message}`);
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function ensureTarSuccess(process, action) {
|
|
245
|
+
const exitCode = await process.exited;
|
|
246
|
+
if (exitCode === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const stderr = process.stderr;
|
|
250
|
+
const detail = stderr ? (await new Response(stderr).text()).trim() : "";
|
|
251
|
+
const suffix = detail ? `: ${detail}` : "";
|
|
252
|
+
throw new SandboxError(`${action} failed${suffix}`);
|
|
253
|
+
}
|
|
254
|
+
function spawnTar(command, options) {
|
|
255
|
+
try {
|
|
256
|
+
return Bun.spawn(command, options);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : "Unknown tar error";
|
|
259
|
+
throw new SandboxError(`Failed to start tar: ${message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function createTarStream(localPath) {
|
|
263
|
+
ensureAbsoluteHostPath(localPath);
|
|
264
|
+
await ensurePathExists(localPath);
|
|
265
|
+
const directory = path.dirname(localPath);
|
|
266
|
+
const name = path.basename(localPath);
|
|
267
|
+
const process = spawnTar(["tar", "-C", directory, "-cf", "-", name], {
|
|
268
|
+
stdout: "pipe",
|
|
269
|
+
stderr: "pipe"
|
|
270
|
+
});
|
|
271
|
+
const stream = process.stdout;
|
|
272
|
+
if (!stream) {
|
|
273
|
+
throw new SandboxError("Tar create failed: tar did not produce an archive stream");
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
stream,
|
|
277
|
+
complete: () => ensureTarSuccess(process, "Tar create"),
|
|
278
|
+
abort: () => {
|
|
279
|
+
try {
|
|
280
|
+
process.kill();
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async function extractTarStream(stream, localPath) {
|
|
286
|
+
ensureAbsoluteHostPath(localPath);
|
|
287
|
+
const targetDirectory = path.dirname(localPath);
|
|
288
|
+
await mkdir(targetDirectory, { recursive: true });
|
|
289
|
+
const extractDirectory = await mkdtemp(path.join(tmpdir(), "agent-sandbox-download-"));
|
|
290
|
+
try {
|
|
291
|
+
const process = spawnTar(["tar", "-C", extractDirectory, "-xf", "-"], {
|
|
292
|
+
stdin: stream,
|
|
293
|
+
stdout: "ignore",
|
|
294
|
+
stderr: "pipe"
|
|
295
|
+
});
|
|
296
|
+
await ensureTarSuccess(process, "Tar extract");
|
|
297
|
+
const extractedEntries = await readdir(extractDirectory);
|
|
298
|
+
const [firstEntry] = extractedEntries;
|
|
299
|
+
if (extractedEntries.length !== 1 || !firstEntry) {
|
|
300
|
+
throw new SandboxError("Tar extract failed: archive did not contain exactly one top-level entry");
|
|
301
|
+
}
|
|
302
|
+
const extractedPath = path.join(extractDirectory, firstEntry);
|
|
303
|
+
const extractedStats = await stat(extractedPath);
|
|
304
|
+
if (!extractedStats.isFile() && !extractedStats.isDirectory()) {
|
|
305
|
+
throw new SandboxError("Tar extract failed: archive did not contain a regular file or directory");
|
|
306
|
+
}
|
|
307
|
+
await rm(localPath, { recursive: true, force: true });
|
|
308
|
+
await rename(extractedPath, localPath);
|
|
309
|
+
} finally {
|
|
310
|
+
await rm(extractDirectory, { recursive: true, force: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ../../packages/core/src/primitives/download.ts
|
|
315
|
+
function ensureAbsoluteContainerPath(targetPath) {
|
|
316
|
+
if (!path2.posix.isAbsolute(targetPath)) {
|
|
317
|
+
throw new SandboxError(`Container path must be absolute: ${targetPath}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function download(options) {
|
|
321
|
+
ensureAbsoluteContainerPath(options.remotePath);
|
|
322
|
+
if (!path2.isAbsolute(options.localPath)) {
|
|
323
|
+
throw new SandboxError(`Host path must be absolute: ${options.localPath}`);
|
|
324
|
+
}
|
|
325
|
+
let response;
|
|
326
|
+
try {
|
|
327
|
+
response = await dockerRequest({
|
|
328
|
+
path: `/containers/${options.sandboxId}/archive`,
|
|
329
|
+
query: {
|
|
330
|
+
path: options.remotePath
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error instanceof DockerError && error.status === 404 && error.message.includes("No such container")) {
|
|
335
|
+
throw new ContainerNotFoundError(options.sandboxId);
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
if (!response.body) {
|
|
340
|
+
throw new SandboxError("Docker archive response was empty");
|
|
341
|
+
}
|
|
342
|
+
await extractTarStream(response.body, options.localPath);
|
|
343
|
+
return {
|
|
344
|
+
ok: true
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// ../../packages/core/src/primitives/list.ts
|
|
348
|
+
async function list() {
|
|
349
|
+
const containers = await dockerJSON({
|
|
350
|
+
path: "/containers/json",
|
|
351
|
+
query: {
|
|
352
|
+
filters: JSON.stringify({
|
|
353
|
+
label: ["agent-sandbox=true"]
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
const sandboxes = containers.map((container) => {
|
|
358
|
+
const name = (container.Names[0] ?? "").replace(/^\//, "");
|
|
359
|
+
return {
|
|
360
|
+
id: container.Id.slice(0, 12),
|
|
361
|
+
name,
|
|
362
|
+
status: container.State,
|
|
363
|
+
createdAt: new Date(container.Created * 1000).toISOString()
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
return { sandboxes };
|
|
367
|
+
}
|
|
368
|
+
// ../../packages/core/src/docker/parse-log.ts
|
|
369
|
+
function parseLogOutput(bytes) {
|
|
370
|
+
const chunks = [];
|
|
371
|
+
let totalLength = 0;
|
|
372
|
+
let offset = 0;
|
|
373
|
+
while (offset < bytes.length) {
|
|
374
|
+
if (bytes.length - offset < 8) {
|
|
375
|
+
throw new SandboxError("Malformed Docker log stream");
|
|
376
|
+
}
|
|
377
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset + 4, 4);
|
|
378
|
+
const payloadLength = view.getUint32(0);
|
|
379
|
+
const payloadStart = offset + 8;
|
|
380
|
+
const payloadEnd = payloadStart + payloadLength;
|
|
381
|
+
if (payloadEnd > bytes.length) {
|
|
382
|
+
throw new SandboxError("Malformed Docker log stream");
|
|
383
|
+
}
|
|
384
|
+
const chunk = bytes.slice(payloadStart, payloadEnd);
|
|
385
|
+
chunks.push(chunk);
|
|
386
|
+
totalLength += chunk.length;
|
|
387
|
+
offset = payloadEnd;
|
|
388
|
+
}
|
|
389
|
+
const output = new Uint8Array(totalLength);
|
|
390
|
+
let writeOffset = 0;
|
|
391
|
+
for (const chunk of chunks) {
|
|
392
|
+
output.set(chunk, writeOffset);
|
|
393
|
+
writeOffset += chunk.length;
|
|
394
|
+
}
|
|
395
|
+
return new TextDecoder().decode(output);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ../../packages/core/src/primitives/read.ts
|
|
399
|
+
async function read(options) {
|
|
400
|
+
const query = {
|
|
401
|
+
stdout: "1",
|
|
402
|
+
stderr: "1"
|
|
403
|
+
};
|
|
404
|
+
if (options.tail !== undefined) {
|
|
405
|
+
query.tail = String(options.tail);
|
|
406
|
+
}
|
|
407
|
+
let response;
|
|
408
|
+
try {
|
|
409
|
+
response = await dockerRequest({
|
|
410
|
+
path: `/containers/${options.sandboxId}/logs`,
|
|
411
|
+
query
|
|
412
|
+
});
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (error instanceof DockerError && error.status === 404) {
|
|
415
|
+
throw new ContainerNotFoundError(options.sandboxId);
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
420
|
+
return {
|
|
421
|
+
output: parseLogOutput(bytes)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
// ../../packages/core/src/primitives/upload.ts
|
|
425
|
+
import path3 from "path";
|
|
426
|
+
function ensureAbsoluteContainerPath2(targetPath) {
|
|
427
|
+
if (!path3.posix.isAbsolute(targetPath)) {
|
|
428
|
+
throw new SandboxError(`Container path must be absolute: ${targetPath}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function remapUploadError(error, sandboxId) {
|
|
432
|
+
if (error instanceof DockerError && error.status === 404) {
|
|
433
|
+
throw new ContainerNotFoundError(sandboxId);
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
async function upload(options) {
|
|
438
|
+
ensureAbsoluteContainerPath2(options.remotePath);
|
|
439
|
+
const archive = await createTarStream(options.localPath);
|
|
440
|
+
try {
|
|
441
|
+
await dockerRequest({
|
|
442
|
+
method: "PUT",
|
|
443
|
+
path: `/containers/${options.sandboxId}/archive`,
|
|
444
|
+
query: {
|
|
445
|
+
path: path3.posix.dirname(options.remotePath)
|
|
446
|
+
},
|
|
447
|
+
body: archive.stream,
|
|
448
|
+
headers: {
|
|
449
|
+
"Content-Type": "application/x-tar"
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
} catch (error) {
|
|
453
|
+
archive.abort();
|
|
454
|
+
remapUploadError(error, options.sandboxId);
|
|
455
|
+
}
|
|
456
|
+
await archive.complete();
|
|
457
|
+
return {
|
|
458
|
+
ok: true
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// ../../packages/core/src/primitives/write.ts
|
|
462
|
+
function getDockerMessage(error) {
|
|
463
|
+
return error.message.replace(/^Docker API error \d+: /, "");
|
|
464
|
+
}
|
|
465
|
+
async function write(options) {
|
|
466
|
+
const detach = options.detach ?? false;
|
|
467
|
+
let execId;
|
|
468
|
+
try {
|
|
469
|
+
const response = await dockerJSON({
|
|
470
|
+
method: "POST",
|
|
471
|
+
path: `/containers/${options.sandboxId}/exec`,
|
|
472
|
+
body: {
|
|
473
|
+
Cmd: ["/bin/sh", "-c", options.input],
|
|
474
|
+
AttachStdout: !detach,
|
|
475
|
+
AttachStderr: !detach
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
execId = response.Id;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (error instanceof DockerError && error.status === 404) {
|
|
481
|
+
throw new ContainerNotFoundError(options.sandboxId);
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
if (detach) {
|
|
486
|
+
try {
|
|
487
|
+
await dockerRequest({
|
|
488
|
+
method: "POST",
|
|
489
|
+
path: `/exec/${execId}/start`,
|
|
490
|
+
body: {
|
|
491
|
+
Detach: true
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (error instanceof DockerError) {
|
|
496
|
+
throw new ExecError(getDockerMessage(error));
|
|
497
|
+
}
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
return { ok: true };
|
|
501
|
+
}
|
|
502
|
+
let startResponse;
|
|
503
|
+
try {
|
|
504
|
+
startResponse = await dockerRequest({
|
|
505
|
+
method: "POST",
|
|
506
|
+
path: `/exec/${execId}/start`,
|
|
507
|
+
body: {
|
|
508
|
+
Detach: false,
|
|
509
|
+
Tty: false
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error instanceof DockerError) {
|
|
514
|
+
throw new ExecError(getDockerMessage(error));
|
|
515
|
+
}
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
const bytes = new Uint8Array(await startResponse.arrayBuffer());
|
|
519
|
+
const output = parseLogOutput(bytes);
|
|
520
|
+
const inspect = await dockerJSON({
|
|
521
|
+
method: "GET",
|
|
522
|
+
path: `/exec/${execId}/json`
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
ok: true,
|
|
526
|
+
output,
|
|
527
|
+
exitCode: inspect.ExitCode
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
// src/agent-sandbox.ts
|
|
531
|
+
class AgentSandbox {
|
|
532
|
+
async create(options = {}) {
|
|
533
|
+
return create(options);
|
|
534
|
+
}
|
|
535
|
+
async delete(options) {
|
|
536
|
+
return deleteContainer(options);
|
|
537
|
+
}
|
|
538
|
+
async list() {
|
|
539
|
+
return list();
|
|
540
|
+
}
|
|
541
|
+
async read(options) {
|
|
542
|
+
return read(options);
|
|
543
|
+
}
|
|
544
|
+
async write(options) {
|
|
545
|
+
return write(options);
|
|
546
|
+
}
|
|
547
|
+
async upload(options) {
|
|
548
|
+
return upload(options);
|
|
549
|
+
}
|
|
550
|
+
async download(options) {
|
|
551
|
+
return download(options);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
export {
|
|
555
|
+
AgentSandbox
|
|
556
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-sandbox/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Programmatic API wrapper for Agent Sandbox",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build ./index.ts --outfile dist/index.js --target bun",
|
|
16
|
+
"check-types": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@agent-sandbox/core": "*",
|
|
20
|
+
"@types/bun": "latest",
|
|
21
|
+
"typescript": "^5"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"bun": ">=1.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|