@agentgrader/sandbox-docker 1.0.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.
@@ -0,0 +1,29 @@
1
+ import { SandboxHandle, PatchApplyResult, SandboxProvider } from '@agentgrader/core';
2
+ import Docker from 'dockerode';
3
+
4
+ declare class DockerSandboxHandle implements SandboxHandle {
5
+ private container;
6
+ constructor(container: Docker.Container);
7
+ exec(cmd: string): Promise<{
8
+ stdout: string;
9
+ stderr: string;
10
+ exitCode: number;
11
+ }>;
12
+ writeFile(path: string, content: string): Promise<void>;
13
+ readFile(path: string): Promise<string>;
14
+ gitDiff(): Promise<string>;
15
+ applyPatch(diff: string): Promise<PatchApplyResult>;
16
+ destroy(): Promise<void>;
17
+ }
18
+ declare class DockerSandboxProvider implements SandboxProvider {
19
+ readonly name = "docker";
20
+ private docker;
21
+ constructor();
22
+ create(opts: {
23
+ image?: string;
24
+ gitSnapshot?: string;
25
+ toolkits?: string[];
26
+ }): Promise<SandboxHandle>;
27
+ }
28
+
29
+ export { DockerSandboxHandle, DockerSandboxProvider };
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { Writable } from 'stream';
5
+ import Docker from 'dockerode';
6
+
7
+ // src/index.ts
8
+ var DockerSandboxHandle = class {
9
+ container;
10
+ constructor(container) {
11
+ this.container = container;
12
+ }
13
+ async exec(cmd) {
14
+ const exec = await this.container.exec({
15
+ Cmd: ["sh", "-c", cmd],
16
+ AttachStdout: true,
17
+ AttachStderr: true,
18
+ WorkingDir: "/app"
19
+ });
20
+ const stream = await exec.start({});
21
+ const stdoutBuffer = [];
22
+ const stderrBuffer = [];
23
+ const stdoutWritable = new Writable({
24
+ write(chunk, encoding, callback) {
25
+ stdoutBuffer.push(chunk.toString());
26
+ callback();
27
+ }
28
+ });
29
+ const stderrWritable = new Writable({
30
+ write(chunk, encoding, callback) {
31
+ stderrBuffer.push(chunk.toString());
32
+ callback();
33
+ }
34
+ });
35
+ this.container.modem.demuxStream(stream, stdoutWritable, stderrWritable);
36
+ let running = true;
37
+ let inspectData;
38
+ while (running) {
39
+ inspectData = await exec.inspect();
40
+ running = inspectData.Running;
41
+ if (running) {
42
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
43
+ }
44
+ }
45
+ return {
46
+ stdout: stdoutBuffer.join(""),
47
+ stderr: stderrBuffer.join(""),
48
+ exitCode: inspectData?.ExitCode ?? 0
49
+ };
50
+ }
51
+ async writeFile(path, content) {
52
+ await this.exec(`mkdir -p $(dirname "${path}")`);
53
+ const exec = await this.container.exec({
54
+ Cmd: ["sh", "-c", `cat > "${path}"`],
55
+ AttachStdin: true,
56
+ AttachStdout: false,
57
+ AttachStderr: false,
58
+ WorkingDir: "/app"
59
+ });
60
+ const stream = await exec.start({ hijack: true, stdin: true });
61
+ await new Promise((resolve2, reject) => {
62
+ stream.write(content, (err) => {
63
+ if (err) {
64
+ reject(err);
65
+ } else {
66
+ stream.end();
67
+ resolve2();
68
+ }
69
+ });
70
+ });
71
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
72
+ }
73
+ async readFile(path) {
74
+ const res = await this.exec(`cat "${path}"`);
75
+ if (res.exitCode !== 0) {
76
+ throw new Error(`Failed to read file ${path}: ${res.stderr}`);
77
+ }
78
+ return res.stdout;
79
+ }
80
+ async gitDiff() {
81
+ const res = await this.exec("git diff");
82
+ return res.stdout;
83
+ }
84
+ async applyPatch(diff) {
85
+ if (!diff || !diff.trim()) {
86
+ return { applied: true, repaired: false, output: "Empty patch - nothing to apply." };
87
+ }
88
+ const patchPath = `/tmp/agr-patch-${Date.now()}-${Math.random().toString(36).slice(2)}.diff`;
89
+ await this.writeFile(patchPath, diff.endsWith("\n") ? diff : `${diff}
90
+ `);
91
+ const attempts = [
92
+ { label: "git apply", cmd: `git apply --whitespace=nowarn "${patchPath}"`, repaired: false },
93
+ {
94
+ label: "git apply --3way",
95
+ cmd: `git apply --3way --whitespace=nowarn "${patchPath}"`,
96
+ repaired: true
97
+ },
98
+ {
99
+ label: "patch --fuzz=3",
100
+ cmd: `patch -p1 --fuzz=3 --batch < "${patchPath}"`,
101
+ repaired: true
102
+ }
103
+ ];
104
+ const log = [];
105
+ for (const attempt of attempts) {
106
+ const res = await this.exec(attempt.cmd);
107
+ log.push(`$ ${attempt.label}
108
+ ${res.stdout}${res.stderr}`.trim());
109
+ if (res.exitCode === 0) {
110
+ await this.exec(`rm -f "${patchPath}"`);
111
+ return { applied: true, repaired: attempt.repaired, output: log.join("\n\n") };
112
+ }
113
+ }
114
+ await this.exec(`rm -f "${patchPath}"`);
115
+ return { applied: false, repaired: false, output: log.join("\n\n") };
116
+ }
117
+ async destroy() {
118
+ try {
119
+ await this.container.stop();
120
+ } catch (e) {
121
+ }
122
+ try {
123
+ await this.container.remove({ force: true });
124
+ } catch (e) {
125
+ }
126
+ }
127
+ };
128
+ async function copyDirToContainer(container, localPath, containerPath, excludes = []) {
129
+ const args = ["--no-xattrs"];
130
+ for (const exclude of excludes) {
131
+ args.push("--exclude", exclude);
132
+ }
133
+ args.push("-cf", "-", "-C", localPath, ".");
134
+ const tarProcess = spawn("tar", args, {
135
+ env: {
136
+ ...process.env,
137
+ COPYFILE_DISABLE: "1"
138
+ }
139
+ });
140
+ await new Promise((resolve2, reject) => {
141
+ container.putArchive(tarProcess.stdout, { path: containerPath }, (err) => {
142
+ if (err) reject(err);
143
+ else resolve2();
144
+ });
145
+ });
146
+ await new Promise((resolve2, reject) => {
147
+ tarProcess.on("close", (code) => {
148
+ if (code === 0) resolve2();
149
+ else reject(new Error(`tar process exited with code ${code}`));
150
+ });
151
+ tarProcess.on("error", (err) => reject(err));
152
+ });
153
+ }
154
+ var DockerSandboxProvider = class {
155
+ name = "docker";
156
+ docker;
157
+ constructor() {
158
+ this.docker = new Docker();
159
+ }
160
+ async create(opts) {
161
+ const image = opts.image || "node:20";
162
+ let imageExists = false;
163
+ try {
164
+ await this.docker.getImage(image).inspect();
165
+ imageExists = true;
166
+ } catch (e) {
167
+ }
168
+ if (!imageExists) {
169
+ console.log(`Docker image "${image}" not found locally. Pulling...`);
170
+ const stream = await this.docker.pull(image);
171
+ await new Promise((resolve2, reject) => {
172
+ this.docker.modem.followProgress(stream, (err, res) => {
173
+ if (err) reject(err);
174
+ else resolve2();
175
+ });
176
+ });
177
+ console.log(`Docker image "${image}" successfully pulled.`);
178
+ }
179
+ const container = await this.docker.createContainer({
180
+ Image: image,
181
+ Cmd: ["tail", "-f", "/dev/null"],
182
+ Tty: true,
183
+ WorkingDir: "/app"
184
+ });
185
+ await container.start();
186
+ const handle = new DockerSandboxHandle(container);
187
+ await handle.exec("mkdir -p /app");
188
+ if (opts.gitSnapshot) {
189
+ const localPath = resolve(opts.gitSnapshot);
190
+ if (existsSync(localPath)) {
191
+ await copyDirToContainer(container, localPath, "/app");
192
+ }
193
+ }
194
+ await handle.exec(
195
+ "git init && git config user.email 'agent@agentgrader.local' && git config user.name 'Agentgrader' && git add -A && git commit -m 'initial' || true"
196
+ );
197
+ for (const toolkitOpt of opts.toolkits ?? []) {
198
+ const toolkitPath = resolve(toolkitOpt);
199
+ if (!existsSync(toolkitPath)) continue;
200
+ await copyDirToContainer(container, toolkitPath, "/app", ["bin"]);
201
+ const binPath = resolve(toolkitPath, "bin");
202
+ if (existsSync(binPath)) {
203
+ await handle.exec("mkdir -p /usr/local/bin");
204
+ await copyDirToContainer(container, binPath, "/usr/local/bin");
205
+ await handle.exec("chmod +x /usr/local/bin/* || true");
206
+ }
207
+ }
208
+ return handle;
209
+ }
210
+ };
211
+
212
+ export { DockerSandboxHandle, DockerSandboxProvider };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@agentgrader/sandbox-docker",
3
+ "version": "1.0.0",
4
+ "description": "Docker-based SandboxProvider implementation for the Agentgrader framework",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm --dts --clean --treeshake",
21
+ "build:watch": "tsup src/index.ts --format esm --dts --watch"
22
+ },
23
+ "dependencies": {
24
+ "@agentgrader/core": "workspace:*",
25
+ "dockerode": "^4.0.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/dockerode": "^3.3.31",
29
+ "tsup": "^8.5.1"
30
+ },
31
+ "peerDependencies": {
32
+ "@agentgrader/core": "^1.0.0"
33
+ }
34
+ }