@rockclaver/sandcastle 0.7.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1355 -0
  3. package/dist/MountConfig-CmXclHA5.d.ts +26 -0
  4. package/dist/SandboxProvider-EkSMuBp8.d.ts +243 -0
  5. package/dist/chunk-72UVAC7B.js +99 -0
  6. package/dist/chunk-72UVAC7B.js.map +1 -0
  7. package/dist/chunk-BIWNFKGV.js +22 -0
  8. package/dist/chunk-BIWNFKGV.js.map +1 -0
  9. package/dist/chunk-FKX3DRTL.js +362 -0
  10. package/dist/chunk-FKX3DRTL.js.map +1 -0
  11. package/dist/chunk-NGBM7T3E.js +76 -0
  12. package/dist/chunk-NGBM7T3E.js.map +1 -0
  13. package/dist/chunk-QCLZLPJ7.js +26431 -0
  14. package/dist/chunk-QCLZLPJ7.js.map +1 -0
  15. package/dist/chunk-VAKEM3U2.js +26997 -0
  16. package/dist/chunk-VAKEM3U2.js.map +1 -0
  17. package/dist/index.d.ts +943 -0
  18. package/dist/index.js +2393 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/main.d.ts +1 -0
  21. package/dist/main.js +19268 -0
  22. package/dist/main.js.map +1 -0
  23. package/dist/mountUtils-CCA-bbpK.d.ts +25 -0
  24. package/dist/sandboxes/daytona.d.ts +60 -0
  25. package/dist/sandboxes/daytona.js +122 -0
  26. package/dist/sandboxes/daytona.js.map +1 -0
  27. package/dist/sandboxes/docker.d.ts +110 -0
  28. package/dist/sandboxes/docker.js +9 -0
  29. package/dist/sandboxes/docker.js.map +1 -0
  30. package/dist/sandboxes/no-sandbox.d.ts +38 -0
  31. package/dist/sandboxes/no-sandbox.js +7 -0
  32. package/dist/sandboxes/no-sandbox.js.map +1 -0
  33. package/dist/sandboxes/podman.d.ts +124 -0
  34. package/dist/sandboxes/podman.js +299 -0
  35. package/dist/sandboxes/podman.js.map +1 -0
  36. package/dist/sandboxes/vercel.d.ts +104 -0
  37. package/dist/sandboxes/vercel.js +148 -0
  38. package/dist/sandboxes/vercel.js.map +1 -0
  39. package/dist/templates/blank/main.mts +14 -0
  40. package/dist/templates/blank/prompt.md +12 -0
  41. package/dist/templates/blank/template.json +4 -0
  42. package/dist/templates/parallel-planner/implement-prompt.md +62 -0
  43. package/dist/templates/parallel-planner/main.mts +204 -0
  44. package/dist/templates/parallel-planner/merge-prompt.md +26 -0
  45. package/dist/templates/parallel-planner/plan-prompt.md +37 -0
  46. package/dist/templates/parallel-planner/template.json +4 -0
  47. package/dist/templates/parallel-planner-with-review/CODING_STANDARDS.md +27 -0
  48. package/dist/templates/parallel-planner-with-review/implement-prompt.md +62 -0
  49. package/dist/templates/parallel-planner-with-review/main.mts +226 -0
  50. package/dist/templates/parallel-planner-with-review/merge-prompt.md +26 -0
  51. package/dist/templates/parallel-planner-with-review/plan-prompt.md +37 -0
  52. package/dist/templates/parallel-planner-with-review/review-prompt.md +55 -0
  53. package/dist/templates/parallel-planner-with-review/template.json +4 -0
  54. package/dist/templates/sequential-reviewer/CODING_STANDARDS.md +27 -0
  55. package/dist/templates/sequential-reviewer/implement-prompt.md +53 -0
  56. package/dist/templates/sequential-reviewer/main.mts +119 -0
  57. package/dist/templates/sequential-reviewer/review-prompt.md +55 -0
  58. package/dist/templates/sequential-reviewer/template.json +4 -0
  59. package/dist/templates/simple-loop/main.mts +49 -0
  60. package/dist/templates/simple-loop/prompt.md +53 -0
  61. package/dist/templates/simple-loop/template.json +4 -0
  62. package/package.json +104 -0
@@ -0,0 +1,362 @@
1
+ import { createRequire } from 'node:module';
2
+ import { Effect_exports, resolveUserMounts, processFileMountParents, defaultImageName, registerShutdown, DockerError, formatVolumeMount } from './chunk-VAKEM3U2.js';
3
+ import { createBindMountSandboxProvider } from './chunk-BIWNFKGV.js';
4
+ import { MAX_TAIL_CHARS, BoundedTail } from './chunk-NGBM7T3E.js';
5
+ import { execFile, execFileSync, spawn } from 'child_process';
6
+ import { randomUUID } from 'crypto';
7
+ import { createInterface } from 'readline';
8
+ import { resolve } from 'path';
9
+
10
+ createRequire(import.meta.url);
11
+ var dockerExec = (args) => Effect_exports.async((resume) => {
12
+ execFile(
13
+ "docker",
14
+ args,
15
+ { maxBuffer: 10 * 1024 * 1024 },
16
+ (error, stdout, stderr) => {
17
+ if (error) {
18
+ resume(
19
+ Effect_exports.fail(
20
+ new DockerError({
21
+ message: `docker ${args[0]} failed: ${stderr?.toString() || error.message}`
22
+ })
23
+ )
24
+ );
25
+ } else {
26
+ resume(Effect_exports.succeed(stdout.toString()));
27
+ }
28
+ }
29
+ );
30
+ });
31
+ var buildImage = (imageName, dockerfileDir, options) => Effect_exports.gen(function* () {
32
+ const buildArgFlags = Object.entries(options?.buildArgs ?? {}).flatMap(
33
+ ([k, v]) => ["--build-arg", `${k}=${v}`]
34
+ );
35
+ if (options?.dockerfile) {
36
+ yield* dockerExec([
37
+ "build",
38
+ "-t",
39
+ imageName,
40
+ ...buildArgFlags,
41
+ "-f",
42
+ resolve(options.dockerfile),
43
+ process.cwd()
44
+ ]);
45
+ } else {
46
+ yield* dockerExec([
47
+ "build",
48
+ "-t",
49
+ imageName,
50
+ ...buildArgFlags,
51
+ resolve(dockerfileDir)
52
+ ]);
53
+ }
54
+ });
55
+ var startContainer = (containerName, imageName, env, options) => Effect_exports.gen(function* () {
56
+ const existing = yield* dockerExec([
57
+ "ps",
58
+ "-a",
59
+ "--filter",
60
+ `name=^${containerName}$`,
61
+ "--format",
62
+ "{{.Names}}"
63
+ ]);
64
+ if (existing.trim() === containerName) {
65
+ yield* Effect_exports.fail(
66
+ new DockerError({
67
+ message: `Container '${containerName}' already exists. Run cleanup first.`
68
+ })
69
+ );
70
+ }
71
+ const envFlags = Object.entries(env).flatMap(([k, v]) => [
72
+ "-e",
73
+ `${k}=${v}`
74
+ ]);
75
+ const selinuxLabel = options?.selinuxLabel ?? "z";
76
+ const volumeFlags = (options?.volumeMounts ?? []).flatMap((mount) => [
77
+ "-v",
78
+ formatVolumeMount(mount, selinuxLabel)
79
+ ]);
80
+ const workdirFlags = options?.workdir ? ["-w", options.workdir] : [];
81
+ const userFlags = options?.user ? ["--user", options.user] : [];
82
+ const networks = options?.network ? Array.isArray(options.network) ? options.network : [options.network] : [];
83
+ const networkFlags = networks.flatMap((n) => ["--network", n]);
84
+ const groupAddFlags = (options?.groups ?? []).flatMap((g) => [
85
+ "--group-add",
86
+ String(g)
87
+ ]);
88
+ const deviceFlags = (options?.devices ?? []).flatMap((d) => [
89
+ "--device",
90
+ d
91
+ ]);
92
+ const cpusFlags = options?.cpus !== void 0 ? ["--cpus", String(options.cpus)] : [];
93
+ yield* dockerExec([
94
+ "run",
95
+ "-d",
96
+ "--name",
97
+ containerName,
98
+ ...envFlags,
99
+ ...volumeFlags,
100
+ ...workdirFlags,
101
+ ...userFlags,
102
+ ...networkFlags,
103
+ ...groupAddFlags,
104
+ ...deviceFlags,
105
+ ...cpusFlags,
106
+ imageName
107
+ ]);
108
+ });
109
+ var removeContainer = (containerName) => Effect_exports.gen(function* () {
110
+ yield* Effect_exports.ignore(dockerExec(["stop", containerName]));
111
+ yield* Effect_exports.ignore(dockerExec(["rm", containerName]));
112
+ });
113
+ var removeImage = (imageName) => Effect_exports.gen(function* () {
114
+ yield* dockerExec(["rmi", imageName]);
115
+ });
116
+
117
+ // src/sandboxes/docker.ts
118
+ var docker = (options) => {
119
+ const configuredImageName = options?.imageName;
120
+ const selinuxLabel = options?.selinuxLabel ?? "z";
121
+ const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
122
+ const sandboxHomedir = "/home/agent";
123
+ const userMounts = options?.mounts ? resolveUserMounts(options.mounts, sandboxHomedir) : [];
124
+ const parentDirsToCreate = processFileMountParents(
125
+ userMounts,
126
+ sandboxHomedir
127
+ );
128
+ return createBindMountSandboxProvider({
129
+ name: "docker",
130
+ env: options?.env,
131
+ sandboxHomedir,
132
+ create: async (createOptions) => {
133
+ const containerName = `sandcastle-${randomUUID()}`;
134
+ const worktreePath = createOptions.mounts.find(
135
+ (m) => m.hostPath === createOptions.worktreePath
136
+ )?.sandboxPath ?? "/home/agent/workspace";
137
+ const allMounts = [...createOptions.mounts, ...userMounts];
138
+ const volumeMounts = allMounts.map((m) => ({
139
+ hostPath: m.hostPath,
140
+ sandboxPath: m.sandboxPath,
141
+ readonly: m.readonly
142
+ }));
143
+ const imageName = configuredImageName ?? defaultImageName(createOptions.hostRepoPath);
144
+ const containerUid = options?.containerUid ?? process.getuid?.() ?? 1e3;
145
+ const containerGid = options?.containerGid ?? process.getgid?.() ?? 1e3;
146
+ await checkImageUid(imageName, containerUid);
147
+ await Effect_exports.runPromise(
148
+ startContainer(
149
+ containerName,
150
+ imageName,
151
+ {
152
+ ...createOptions.env,
153
+ HOME: "/home/agent"
154
+ },
155
+ {
156
+ volumeMounts,
157
+ workdir: worktreePath,
158
+ user: `${containerUid}:${containerGid}`,
159
+ network: options?.network,
160
+ groups: options?.groups,
161
+ devices: options?.devices,
162
+ cpus: options?.cpus,
163
+ selinuxLabel
164
+ }
165
+ )
166
+ );
167
+ for (const dir of parentDirsToCreate) {
168
+ await new Promise((resolve2, reject) => {
169
+ execFile(
170
+ "docker",
171
+ [
172
+ "exec",
173
+ "--user",
174
+ "0:0",
175
+ containerName,
176
+ "sh",
177
+ "-c",
178
+ `mkdir -p "$1" && chown "$2" "$1"`,
179
+ "sh",
180
+ dir,
181
+ `${containerUid}:${containerGid}`
182
+ ],
183
+ (error) => {
184
+ if (error) {
185
+ reject(
186
+ new Error(
187
+ `Failed to create parent directory '${dir}' in container: ${error.message}`
188
+ )
189
+ );
190
+ } else {
191
+ resolve2();
192
+ }
193
+ }
194
+ );
195
+ });
196
+ }
197
+ const removeContainerSync = () => {
198
+ try {
199
+ execFileSync("docker", ["rm", "-f", containerName], {
200
+ stdio: "ignore"
201
+ });
202
+ } catch {
203
+ }
204
+ };
205
+ const unregisterShutdown = registerShutdown(removeContainerSync);
206
+ const handle = {
207
+ worktreePath,
208
+ exec: (command, opts) => {
209
+ const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
210
+ const args = ["exec"];
211
+ if (opts?.stdin !== void 0) args.push("-i");
212
+ if (opts?.cwd) args.push("-w", opts.cwd);
213
+ args.push(containerName, "sh", "-c", effectiveCommand);
214
+ return new Promise((resolve2, reject) => {
215
+ const proc = spawn("docker", args, {
216
+ stdio: [
217
+ opts?.stdin !== void 0 ? "pipe" : "ignore",
218
+ "pipe",
219
+ "pipe"
220
+ ]
221
+ });
222
+ if (opts?.stdin !== void 0) {
223
+ proc.stdin.write(opts.stdin);
224
+ proc.stdin.end();
225
+ }
226
+ proc.on("error", (error) => {
227
+ reject(new Error(`docker exec failed: ${error.message}`));
228
+ });
229
+ if (opts?.onLine) {
230
+ const onLine = opts.onLine;
231
+ const stdoutTail = new BoundedTail(maxOutputTailChars, "\n");
232
+ const stderrTail = new BoundedTail(maxOutputTailChars, "");
233
+ const rl = createInterface({ input: proc.stdout });
234
+ rl.on("line", (line) => {
235
+ stdoutTail.push(line);
236
+ onLine(line);
237
+ });
238
+ proc.stderr.on("data", (chunk) => {
239
+ stderrTail.push(chunk.toString());
240
+ });
241
+ proc.on("close", (code) => {
242
+ resolve2({
243
+ stdout: stdoutTail.toString(),
244
+ stderr: stderrTail.toString(),
245
+ exitCode: code ?? 0
246
+ });
247
+ });
248
+ } else {
249
+ const stdoutChunks = [];
250
+ const stderrChunks = [];
251
+ proc.stdout.on("data", (chunk) => {
252
+ stdoutChunks.push(chunk.toString());
253
+ });
254
+ proc.stderr.on("data", (chunk) => {
255
+ stderrChunks.push(chunk.toString());
256
+ });
257
+ proc.on("close", (code) => {
258
+ resolve2({
259
+ stdout: stdoutChunks.join(""),
260
+ stderr: stderrChunks.join(""),
261
+ exitCode: code ?? 0
262
+ });
263
+ });
264
+ }
265
+ });
266
+ },
267
+ interactiveExec: (args, opts) => {
268
+ return new Promise((resolve2, reject) => {
269
+ const dockerArgs = ["exec"];
270
+ if ("isTTY" in opts.stdin && opts.stdin.isTTY) {
271
+ dockerArgs.push("-it");
272
+ } else {
273
+ dockerArgs.push("-i");
274
+ }
275
+ if (opts.cwd) dockerArgs.push("-w", opts.cwd);
276
+ dockerArgs.push(containerName, ...args);
277
+ const proc = spawn("docker", dockerArgs, {
278
+ stdio: [opts.stdin, opts.stdout, opts.stderr]
279
+ });
280
+ proc.on("error", (error) => {
281
+ reject(new Error(`docker exec failed: ${error.message}`));
282
+ });
283
+ proc.on("close", (code) => {
284
+ resolve2({ exitCode: code ?? 0 });
285
+ });
286
+ });
287
+ },
288
+ copyFileIn: (hostPath, sandboxPath) => new Promise((resolve2, reject) => {
289
+ execFile(
290
+ "docker",
291
+ ["cp", hostPath, `${containerName}:${sandboxPath}`],
292
+ (error) => {
293
+ if (error) {
294
+ reject(new Error(`docker cp (in) failed: ${error.message}`));
295
+ } else {
296
+ resolve2();
297
+ }
298
+ }
299
+ );
300
+ }),
301
+ copyFileOut: (sandboxPath, hostPath) => new Promise((resolve2, reject) => {
302
+ execFile(
303
+ "docker",
304
+ ["cp", `${containerName}:${sandboxPath}`, hostPath],
305
+ (error) => {
306
+ if (error) {
307
+ reject(new Error(`docker cp (out) failed: ${error.message}`));
308
+ } else {
309
+ resolve2();
310
+ }
311
+ }
312
+ );
313
+ }),
314
+ close: async () => {
315
+ unregisterShutdown();
316
+ await Effect_exports.runPromise(removeContainer(containerName));
317
+ }
318
+ };
319
+ return handle;
320
+ }
321
+ });
322
+ };
323
+ var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) => {
324
+ execFile(
325
+ "docker",
326
+ ["image", "inspect", imageName, "--format", "{{.Config.User}}"],
327
+ (error, stdout) => {
328
+ if (error) {
329
+ reject(
330
+ new Error(
331
+ `Image '${imageName}' not found locally. Build it first with 'sandcastle docker build-image'.`
332
+ )
333
+ );
334
+ return;
335
+ }
336
+ const imageUser = (stdout ?? "").toString().trim();
337
+ if (!imageUser) {
338
+ resolve2();
339
+ return;
340
+ }
341
+ const uidPart = imageUser.split(":")[0];
342
+ const imageUid = parseInt(uidPart, 10);
343
+ if (isNaN(imageUid)) {
344
+ resolve2();
345
+ return;
346
+ }
347
+ if (imageUid !== expectedUid) {
348
+ reject(
349
+ new Error(
350
+ `UID mismatch: image '${imageName}' was built with UID ${imageUid}, but the expected UID is ${expectedUid}. Rebuild the image with 'sandcastle docker build-image', or pass containerUid: ${imageUid} to docker() to match the image.`
351
+ )
352
+ );
353
+ } else {
354
+ resolve2();
355
+ }
356
+ }
357
+ );
358
+ });
359
+
360
+ export { buildImage, docker, removeImage };
361
+ //# sourceMappingURL=chunk-FKX3DRTL.js.map
362
+ //# sourceMappingURL=chunk-FKX3DRTL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/DockerLifecycle.ts","../src/sandboxes/docker.ts"],"names":["resolve","execFile"],"mappings":"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","file":"chunk-FKX3DRTL.js","sourcesContent":["import { Effect } from \"effect\";\nimport { execFile } from \"node:child_process\";\nimport { resolve } from \"node:path\";\nimport { DockerError } from \"./errors.js\";\nimport { formatVolumeMount, type SelinuxLabel } from \"./mountUtils.js\";\n\nconst dockerExec = (args: string[]): Effect.Effect<string, DockerError> =>\n Effect.async((resume) => {\n execFile(\n \"docker\",\n args,\n { maxBuffer: 10 * 1024 * 1024 },\n (error, stdout, stderr) => {\n if (error) {\n resume(\n Effect.fail(\n new DockerError({\n message: `docker ${args[0]} failed: ${stderr?.toString() || error.message}`,\n }),\n ),\n );\n } else {\n resume(Effect.succeed(stdout.toString()));\n }\n },\n );\n });\n\n/**\n * Build the sandcastle Docker image.\n *\n * When `dockerfile` is provided, uses `docker build -f <dockerfile> <cwd>`\n * so COPY instructions resolve relative to the current working directory.\n * Otherwise, uses `docker build <dockerfileDir>` (the default .sandcastle/ directory).\n */\nexport const buildImage = (\n imageName: string,\n dockerfileDir: string,\n options?: {\n readonly dockerfile?: string;\n readonly buildArgs?: Record<string, string>;\n },\n): Effect.Effect<void, DockerError> =>\n Effect.gen(function* () {\n const buildArgFlags = Object.entries(options?.buildArgs ?? {}).flatMap(\n ([k, v]) => [\"--build-arg\", `${k}=${v}`],\n );\n if (options?.dockerfile) {\n yield* dockerExec([\n \"build\",\n \"-t\",\n imageName,\n ...buildArgFlags,\n \"-f\",\n resolve(options.dockerfile),\n process.cwd(),\n ]);\n } else {\n yield* dockerExec([\n \"build\",\n \"-t\",\n imageName,\n ...buildArgFlags,\n resolve(dockerfileDir),\n ]);\n }\n });\n\nexport interface VolumeMount {\n readonly hostPath: string;\n readonly sandboxPath: string;\n readonly readonly?: boolean;\n}\n\nexport interface StartContainerOptions {\n readonly volumeMounts?: readonly VolumeMount[];\n readonly workdir?: string;\n /** Run the container as this uid:gid instead of the Dockerfile's USER. */\n readonly user?: string;\n /** Docker network(s) to attach the container to. Passed as `--network` flags. */\n readonly network?: string | readonly string[];\n /** Supplementary groups to add the container user to. Passed as `--group-add` flags. */\n readonly groups?: readonly (string | number)[];\n /** Host devices to expose to the container. Passed as `--device` flags. */\n readonly devices?: readonly string[];\n /** Limit CPU resources via `--cpus` (e.g. `1.5`). Fractional values allowed. */\n readonly cpus?: number;\n /**\n * SELinux volume label suffix applied to bind mounts (default `\"z\"`).\n *\n * - `\"z\"` — shared label. No-op on non-SELinux systems.\n * - `\"Z\"` — private label; only this container can access the mount.\n * - `false` — disable labeling entirely.\n */\n readonly selinuxLabel?: SelinuxLabel;\n}\n\n/**\n * Start a new container with environment variables injected.\n */\nexport const startContainer = (\n containerName: string,\n imageName: string,\n env: Record<string, string>,\n options?: StartContainerOptions,\n): Effect.Effect<void, DockerError> =>\n Effect.gen(function* () {\n // Check if container already exists\n const existing = yield* dockerExec([\n \"ps\",\n \"-a\",\n \"--filter\",\n `name=^${containerName}$`,\n \"--format\",\n \"{{.Names}}\",\n ]);\n\n if (existing.trim() === containerName) {\n yield* Effect.fail(\n new DockerError({\n message: `Container '${containerName}' already exists. Run cleanup first.`,\n }),\n );\n }\n\n const envFlags = Object.entries(env).flatMap(([k, v]) => [\n \"-e\",\n `${k}=${v}`,\n ]);\n\n const selinuxLabel = options?.selinuxLabel ?? \"z\";\n const volumeFlags = (options?.volumeMounts ?? []).flatMap((mount) => [\n \"-v\",\n formatVolumeMount(mount, selinuxLabel),\n ]);\n\n const workdirFlags = options?.workdir ? [\"-w\", options.workdir] : [];\n const userFlags = options?.user ? [\"--user\", options.user] : [];\n const networks = options?.network\n ? Array.isArray(options.network)\n ? options.network\n : [options.network]\n : [];\n const networkFlags = networks.flatMap((n) => [\"--network\", n]);\n const groupAddFlags = (options?.groups ?? []).flatMap((g) => [\n \"--group-add\",\n String(g),\n ]);\n const deviceFlags = (options?.devices ?? []).flatMap((d) => [\n \"--device\",\n d,\n ]);\n const cpusFlags =\n options?.cpus !== undefined ? [\"--cpus\", String(options.cpus)] : [];\n\n yield* dockerExec([\n \"run\",\n \"-d\",\n \"--name\",\n containerName,\n ...envFlags,\n ...volumeFlags,\n ...workdirFlags,\n ...userFlags,\n ...networkFlags,\n ...groupAddFlags,\n ...deviceFlags,\n ...cpusFlags,\n imageName,\n ]);\n });\n\n/**\n * Stop and remove a container without removing the image.\n */\nexport const removeContainer = (\n containerName: string,\n): Effect.Effect<void, DockerError> =>\n Effect.gen(function* () {\n // Stop container (ignore errors if already stopped)\n yield* Effect.ignore(dockerExec([\"stop\", containerName]));\n // Remove container (ignore errors if not found)\n yield* Effect.ignore(dockerExec([\"rm\", containerName]));\n });\n\n/**\n * Remove a Docker image.\n */\nexport const removeImage = (\n imageName: string,\n): Effect.Effect<void, DockerError> =>\n Effect.gen(function* () {\n yield* dockerExec([\"rmi\", imageName]);\n });\n","/**\n * Docker sandbox provider — wraps DockerLifecycle into a SandboxProvider.\n *\n * Usage:\n * import { docker } from \"sandcastle/sandboxes/docker\";\n * await run({ agent: claudeCode(\"claude-opus-4-7\"), sandbox: docker() });\n */\n\nimport {\n execFile,\n execFileSync,\n spawn,\n type StdioOptions,\n} from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { createInterface } from \"node:readline\";\nimport { Effect } from \"effect\";\nimport { startContainer, removeContainer } from \"../DockerLifecycle.js\";\nimport {\n createBindMountSandboxProvider,\n type SandboxProvider,\n type BindMountCreateOptions,\n type BindMountSandboxHandle,\n type ExecResult,\n type InteractiveExecOptions,\n} from \"../SandboxProvider.js\";\nimport type { MountConfig } from \"../MountConfig.js\";\nimport type { SelinuxLabel } from \"../mountUtils.js\";\nimport {\n defaultImageName,\n resolveUserMounts,\n processFileMountParents,\n} from \"../mountUtils.js\";\nimport { BoundedTail, MAX_TAIL_CHARS } from \"../boundedTail.js\";\nimport { registerShutdown } from \"../shutdownRegistry.js\";\n\nexport interface DockerOptions {\n /** Docker image name (default: derived from repo directory name). */\n readonly imageName?: string;\n /**\n * The UID of the `agent` user inside the container image (default: host UID via `process.getuid()`, or 1000).\n *\n * Must match the UID baked into the image at build time. Used as the `--user` flag value\n * and checked against the image's configured UID in the pre-flight diagnostic.\n */\n readonly containerUid?: number;\n /**\n * The GID of the `agent` user inside the container image (default: host GID via `process.getgid()`, or 1000).\n *\n * Must match the GID baked into the image at build time. Used as the `--user` flag value.\n */\n readonly containerGid?: number;\n /**\n * SELinux volume label suffix applied to bind mounts.\n *\n * - `\"z\"` — shared label (default). No-op on non-SELinux systems.\n * - `\"Z\"` — private label; only this container can access the mount.\n * - `false` — disable labeling entirely.\n */\n readonly selinuxLabel?: SelinuxLabel;\n /**\n * Additional host directories to bind-mount into the sandbox.\n *\n * Each entry specifies a `hostPath` (tilde-expanded) and `sandboxPath`.\n * If `hostPath` does not exist, sandbox creation fails with a clear error.\n */\n readonly mounts?: readonly MountConfig[];\n /** Environment variables injected by this provider. Merged at launch time with env resolver and agent provider env. */\n readonly env?: Record<string, string>;\n /**\n * Docker network(s) to attach the container to.\n *\n * - `\"my-network\"` → `--network my-network`\n * - `[\"net1\", \"net2\"]` → `--network net1 --network net2`\n *\n * When omitted, Docker's default bridge network is used.\n */\n readonly network?: string | readonly string[];\n /**\n * Supplementary groups to add the container user to, via `--group-add`.\n *\n * Accepts group names or numeric GIDs:\n *\n * - `[\"docker\"]` → `--group-add docker`\n * - `[999]` → `--group-add 999`\n * - `[\"docker\", 999]` → `--group-add docker --group-add 999`\n *\n * Useful for granting access to a bind-mounted Docker socket (Docker-outside-of-Docker).\n * When omitted, no `--group-add` flags are added.\n */\n readonly groups?: readonly (string | number)[];\n /**\n * Host devices to expose to the container, via `--device`.\n *\n * Each entry is a full device spec in `host[:container[:permissions]]` form:\n *\n * - `[\"/dev/kvm\"]` → `--device /dev/kvm`\n * - `[\"/dev/sda:/dev/xvda:rwm\"]` → `--device /dev/sda:/dev/xvda:rwm`\n * - `[\"/dev/kvm\", \"/dev/fuse\"]` → `--device /dev/kvm --device /dev/fuse`\n *\n * When omitted, no `--device` flags are added.\n */\n readonly devices?: readonly string[];\n /**\n * Maximum number of characters of streamed `exec` output retained per stream\n * (stdout and stderr) when an `onLine` callback is supplied (default: 64KiB).\n *\n * Output is delivered live to `onLine` regardless; this only bounds the tail\n * returned in `ExecResult`, preventing a long-running agent's output from\n * overflowing V8's max string length and crashing the run.\n */\n readonly maxOutputTailChars?: number;\n /**\n * Limit the CPU resources available to the container, via `--cpus`.\n *\n * Maps directly to `docker run --cpus`. Accepts fractional values:\n *\n * - `2` → `--cpus 2` (at most 2 CPUs)\n * - `1.5` → `--cpus 1.5` (at most 1.5 CPUs)\n *\n * When omitted, no `--cpus` flag is added and the container is unconstrained.\n */\n readonly cpus?: number;\n}\n\n/**\n * Create a Docker sandbox provider.\n *\n * The returned provider creates Docker containers with bind-mounts\n * for the worktree and git directories.\n */\nexport const docker = (options?: DockerOptions): SandboxProvider => {\n const configuredImageName = options?.imageName;\n const selinuxLabel = options?.selinuxLabel ?? \"z\";\n const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;\n const sandboxHomedir = \"/home/agent\";\n const userMounts = options?.mounts\n ? resolveUserMounts(options.mounts, sandboxHomedir)\n : [];\n // Validate file mounts and collect parent dirs to create at container start.\n // Throws at construction time if any file mount parent is outside sandboxHomedir.\n const parentDirsToCreate = processFileMountParents(\n userMounts,\n sandboxHomedir,\n );\n\n return createBindMountSandboxProvider({\n name: \"docker\",\n env: options?.env,\n sandboxHomedir,\n create: async (\n createOptions: BindMountCreateOptions,\n ): Promise<BindMountSandboxHandle> => {\n const containerName = `sandcastle-${randomUUID()}`;\n\n const worktreePath =\n createOptions.mounts.find(\n (m) => m.hostPath === createOptions.worktreePath,\n )?.sandboxPath ?? \"/home/agent/workspace\";\n\n // Build volume mount list (internal mounts + user-provided mounts)\n const allMounts = [...createOptions.mounts, ...userMounts];\n const volumeMounts = allMounts.map((m) => ({\n hostPath: m.hostPath,\n sandboxPath: m.sandboxPath,\n readonly: m.readonly,\n }));\n\n // Resolve image name\n const imageName =\n configuredImageName ?? defaultImageName(createOptions.hostRepoPath);\n\n const containerUid = options?.containerUid ?? process.getuid?.() ?? 1000;\n const containerGid = options?.containerGid ?? process.getgid?.() ?? 1000;\n\n // Pre-flight: verify image exists and UID matches\n await checkImageUid(imageName, containerUid);\n\n // Start container\n await Effect.runPromise(\n startContainer(\n containerName,\n imageName,\n {\n ...createOptions.env,\n HOME: \"/home/agent\",\n },\n {\n volumeMounts,\n workdir: worktreePath,\n user: `${containerUid}:${containerGid}`,\n network: options?.network,\n groups: options?.groups,\n devices: options?.devices,\n cpus: options?.cpus,\n selinuxLabel,\n },\n ),\n );\n\n // Create parent directories for file mounts and chown to the container user\n for (const dir of parentDirsToCreate) {\n await new Promise<void>((resolve, reject) => {\n execFile(\n \"docker\",\n [\n \"exec\",\n \"--user\",\n \"0:0\",\n containerName,\n \"sh\",\n \"-c\",\n `mkdir -p \"$1\" && chown \"$2\" \"$1\"`,\n \"sh\",\n dir,\n `${containerUid}:${containerGid}`,\n ],\n (error) => {\n if (error) {\n reject(\n new Error(\n `Failed to create parent directory '${dir}' in container: ${error.message}`,\n ),\n );\n } else {\n resolve();\n }\n },\n );\n });\n }\n\n // Register synchronous container cleanup via the shared shutdown registry\n // so concurrent sandboxes share a single exit/SIGINT/SIGTERM listener\n // instead of tripping Node's MaxListenersExceededWarning.\n const removeContainerSync = () => {\n try {\n execFileSync(\"docker\", [\"rm\", \"-f\", containerName], {\n stdio: \"ignore\",\n });\n } catch {\n /* best-effort */\n }\n };\n const unregisterShutdown = registerShutdown(removeContainerSync);\n\n const handle: BindMountSandboxHandle = {\n worktreePath,\n\n exec: (\n command: string,\n opts?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n },\n ): Promise<ExecResult> => {\n const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;\n const args = [\"exec\"];\n if (opts?.stdin !== undefined) args.push(\"-i\");\n if (opts?.cwd) args.push(\"-w\", opts.cwd);\n args.push(containerName, \"sh\", \"-c\", effectiveCommand);\n\n return new Promise((resolve, reject) => {\n const proc = spawn(\"docker\", args, {\n stdio: [\n opts?.stdin !== undefined ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ],\n });\n\n if (opts?.stdin !== undefined) {\n proc.stdin!.write(opts.stdin);\n proc.stdin!.end();\n }\n\n proc.on(\"error\", (error) => {\n reject(new Error(`docker exec failed: ${error.message}`));\n });\n\n if (opts?.onLine) {\n const onLine = opts.onLine;\n const stdoutTail = new BoundedTail(maxOutputTailChars, \"\\n\");\n const stderrTail = new BoundedTail(maxOutputTailChars, \"\");\n const rl = createInterface({ input: proc.stdout! });\n rl.on(\"line\", (line) => {\n stdoutTail.push(line);\n onLine(line);\n });\n proc.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrTail.push(chunk.toString());\n });\n proc.on(\"close\", (code) => {\n resolve({\n stdout: stdoutTail.toString(),\n stderr: stderrTail.toString(),\n exitCode: code ?? 0,\n });\n });\n } else {\n const stdoutChunks: string[] = [];\n const stderrChunks: string[] = [];\n proc.stdout!.on(\"data\", (chunk: Buffer) => {\n stdoutChunks.push(chunk.toString());\n });\n proc.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrChunks.push(chunk.toString());\n });\n proc.on(\"close\", (code) => {\n resolve({\n stdout: stdoutChunks.join(\"\"),\n stderr: stderrChunks.join(\"\"),\n exitCode: code ?? 0,\n });\n });\n }\n });\n },\n\n interactiveExec: (\n args: string[],\n opts: InteractiveExecOptions,\n ): Promise<{ exitCode: number }> => {\n return new Promise((resolve, reject) => {\n const dockerArgs = [\"exec\"];\n // Allocate a pseudo-terminal when stdin looks like a TTY\n if (\n \"isTTY\" in opts.stdin &&\n (opts.stdin as { isTTY?: boolean }).isTTY\n ) {\n dockerArgs.push(\"-it\");\n } else {\n dockerArgs.push(\"-i\");\n }\n if (opts.cwd) dockerArgs.push(\"-w\", opts.cwd);\n dockerArgs.push(containerName, ...args);\n\n const proc = spawn(\"docker\", dockerArgs, {\n stdio: [opts.stdin, opts.stdout, opts.stderr] as StdioOptions,\n });\n\n proc.on(\"error\", (error: Error) => {\n reject(new Error(`docker exec failed: ${error.message}`));\n });\n\n proc.on(\"close\", (code: number | null) => {\n resolve({ exitCode: code ?? 0 });\n });\n });\n },\n\n copyFileIn: (hostPath: string, sandboxPath: string): Promise<void> =>\n new Promise((resolve, reject) => {\n execFile(\n \"docker\",\n [\"cp\", hostPath, `${containerName}:${sandboxPath}`],\n (error) => {\n if (error) {\n reject(new Error(`docker cp (in) failed: ${error.message}`));\n } else {\n resolve();\n }\n },\n );\n }),\n\n copyFileOut: (sandboxPath: string, hostPath: string): Promise<void> =>\n new Promise((resolve, reject) => {\n execFile(\n \"docker\",\n [\"cp\", `${containerName}:${sandboxPath}`, hostPath],\n (error) => {\n if (error) {\n reject(new Error(`docker cp (out) failed: ${error.message}`));\n } else {\n resolve();\n }\n },\n );\n }),\n\n close: async (): Promise<void> => {\n unregisterShutdown();\n await Effect.runPromise(removeContainer(containerName));\n },\n };\n\n return handle;\n },\n });\n};\n\n// Re-export for backwards compatibility\nexport { defaultImageName };\n\nconst checkImageUid = (imageName: string, expectedUid: number): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n execFile(\n \"docker\",\n [\"image\", \"inspect\", imageName, \"--format\", \"{{.Config.User}}\"],\n (error, stdout) => {\n if (error) {\n reject(\n new Error(\n `Image '${imageName}' not found locally. Build it first with 'sandcastle docker build-image'.`,\n ),\n );\n return;\n }\n const imageUser = (stdout ?? \"\").toString().trim();\n if (!imageUser) {\n // No USER directive in image — skip check\n resolve();\n return;\n }\n const uidPart = imageUser.split(\":\")[0]!;\n const imageUid = parseInt(uidPart, 10);\n if (isNaN(imageUid)) {\n // Non-numeric user (e.g. \"agent\") — can't compare, skip check\n resolve();\n return;\n }\n if (imageUid !== expectedUid) {\n reject(\n new Error(\n `UID mismatch: image '${imageName}' was built with UID ${imageUid}, ` +\n `but the expected UID is ${expectedUid}. ` +\n `Rebuild the image with 'sandcastle docker build-image', ` +\n `or pass containerUid: ${imageUid} to docker() to match the image.`,\n ),\n );\n } else {\n resolve();\n }\n },\n );\n });\n"]}
@@ -0,0 +1,76 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require$1 = createRequire(import.meta.url);
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __require = /* @__PURE__ */ ((x) => typeof require$1 !== "undefined" ? require$1 : typeof Proxy !== "undefined" ? new Proxy(x, {
11
+ get: (a, b) => (typeof require$1 !== "undefined" ? require$1 : a)[b]
12
+ }) : x)(function(x) {
13
+ if (typeof require$1 !== "undefined") return require$1.apply(this, arguments);
14
+ throw Error('Dynamic require of "' + x + '" is not supported');
15
+ });
16
+ var __commonJS = (cb, mod) => function __require2() {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
32
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
33
+ // If the importer is in node compatibility mode or this is not an ESM
34
+ // file that has been converted to a CommonJS file using a Babel-
35
+ // compatible transform (i.e. "__esModule" has not been set), then set
36
+ // "default" to the CommonJS "module.exports" for node compatibility.
37
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
38
+ mod
39
+ ));
40
+
41
+ // src/boundedTail.ts
42
+ var MAX_TAIL_CHARS = 64 * 1024;
43
+ var BoundedTail = class {
44
+ items = [];
45
+ totalChars = 0;
46
+ maxChars;
47
+ separator;
48
+ /**
49
+ * @param maxChars Maximum length of the joined tail. Defaults to {@link MAX_TAIL_CHARS}.
50
+ * @param separator String placed between items by {@link toString}. Must match
51
+ * how the caller would otherwise have joined the accumulated chunks (e.g.
52
+ * `"\n"` for line streams, `""` for raw chunk streams).
53
+ */
54
+ constructor(maxChars = MAX_TAIL_CHARS, separator = "") {
55
+ this.maxChars = maxChars;
56
+ this.separator = separator;
57
+ }
58
+ /** Append one item to the tail, evicting oldest items to stay within budget. */
59
+ push(item) {
60
+ const bounded = item.length > this.maxChars ? item.slice(item.length - this.maxChars) : item;
61
+ this.totalChars += bounded.length + (this.items.length > 0 ? this.separator.length : 0);
62
+ this.items.push(bounded);
63
+ while (this.totalChars > this.maxChars && this.items.length > 1) {
64
+ const dropped = this.items.shift();
65
+ this.totalChars -= dropped.length + this.separator.length;
66
+ }
67
+ }
68
+ /** Join the retained tail into a single string (length ≤ `maxChars`). */
69
+ toString() {
70
+ return this.items.join(this.separator);
71
+ }
72
+ };
73
+
74
+ export { BoundedTail, MAX_TAIL_CHARS, __commonJS, __export, __reExport, __require, __toESM };
75
+ //# sourceMappingURL=chunk-NGBM7T3E.js.map
76
+ //# sourceMappingURL=chunk-NGBM7T3E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/boundedTail.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBO,IAAM,iBAAiB,EAAA,GAAK;AAa5B,IAAM,cAAN,MAAkB;AAAA,EACN,QAAkB,EAAC;AAAA,EAC5B,UAAA,GAAa,CAAA;AAAA,EACJ,QAAA;AAAA,EACA,SAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,WAAA,CAAY,QAAA,GAAmB,cAAA,EAAgB,SAAA,GAAY,EAAA,EAAI;AAC7D,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AAAA,EACnB;AAAA;AAAA,EAGA,KAAK,IAAA,EAAoB;AACvB,IAAA,MAAM,OAAA,GACJ,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,QAAA,GACf,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,QAAQ,CAAA,GACtC,IAAA;AACN,IAAA,IAAA,CAAK,UAAA,IACH,QAAQ,MAAA,IAAU,IAAA,CAAK,MAAM,MAAA,GAAS,CAAA,GAAI,IAAA,CAAK,SAAA,CAAU,MAAA,GAAS,CAAA,CAAA;AACpE,IAAA,IAAA,CAAK,KAAA,CAAM,KAAK,OAAO,CAAA;AACvB,IAAA,OAAO,KAAK,UAAA,GAAa,IAAA,CAAK,YAAY,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA,EAAG;AAC/D,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,KAAA,EAAM;AACjC,MAAA,IAAA,CAAK,UAAA,IAAc,OAAA,CAAQ,MAAA,GAAS,IAAA,CAAK,SAAA,CAAU,MAAA;AAAA,IACrD;AAAA,EACF;AAAA;AAAA,EAGA,QAAA,GAAmB;AACjB,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,SAAS,CAAA;AAAA,EACvC;AACF","file":"chunk-NGBM7T3E.js","sourcesContent":["/**\n * A bounded, rolling tail of streamed output — a pure, provider-agnostic\n * utility shared by every sandbox provider that streams `exec` output.\n *\n * When a provider streams output line-by-line, it accumulates the stream only\n * to build the returned `ExecResult.stdout`/`stderr`. Consumers read just the\n * tail of that value (e.g. the last lines of an error, or a fallback for the\n * agent's final result), so retaining the whole stream is unnecessary — and,\n * once the accumulated string passes V8's ~512MB max string length, fatal: a\n * naive `chunks.join()` throws `RangeError: Invalid string length`, which on a\n * long agent run crashes the whole orchestration.\n */\n\n/**\n * Default maximum number of characters retained in a bounded output tail.\n *\n * 64KiB sits comfortably above any agent completion signal or structured-output\n * payload while staying far below V8's max string length.\n */\nexport const MAX_TAIL_CHARS = 64 * 1024;\n\n/**\n * A fixed-size rolling tail of strings, bounded by total character length.\n *\n * `push` appends to the tail; once the joined length would exceed `maxChars`,\n * the oldest items are dropped from the front. A single item longer than\n * `maxChars` is truncated to its own tail, so a newline-free blob can't\n * overflow on one push. `toString` joins the retained items, and its length is\n * always at most `maxChars`.\n *\n * The running length counter is encapsulated so callers can't desync it.\n */\nexport class BoundedTail {\n private readonly items: string[] = [];\n private totalChars = 0;\n private readonly maxChars: number;\n private readonly separator: string;\n\n /**\n * @param maxChars Maximum length of the joined tail. Defaults to {@link MAX_TAIL_CHARS}.\n * @param separator String placed between items by {@link toString}. Must match\n * how the caller would otherwise have joined the accumulated chunks (e.g.\n * `\"\\n\"` for line streams, `\"\"` for raw chunk streams).\n */\n constructor(maxChars: number = MAX_TAIL_CHARS, separator = \"\") {\n this.maxChars = maxChars;\n this.separator = separator;\n }\n\n /** Append one item to the tail, evicting oldest items to stay within budget. */\n push(item: string): void {\n const bounded =\n item.length > this.maxChars\n ? item.slice(item.length - this.maxChars)\n : item;\n this.totalChars +=\n bounded.length + (this.items.length > 0 ? this.separator.length : 0);\n this.items.push(bounded);\n while (this.totalChars > this.maxChars && this.items.length > 1) {\n const dropped = this.items.shift()!;\n this.totalChars -= dropped.length + this.separator.length;\n }\n }\n\n /** Join the retained tail into a single string (length ≤ `maxChars`). */\n toString(): string {\n return this.items.join(this.separator);\n }\n}\n"]}