@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,299 @@
1
+ import { createRequire } from 'node:module';
2
+ import { resolveUserMounts, processFileMountParents, formatVolumeMount, defaultImageName, registerShutdown } from '../chunk-VAKEM3U2.js';
3
+ export { defaultImageName } from '../chunk-VAKEM3U2.js';
4
+ import { createBindMountSandboxProvider } from '../chunk-BIWNFKGV.js';
5
+ import { MAX_TAIL_CHARS, BoundedTail } from '../chunk-NGBM7T3E.js';
6
+ import { execFile, execFileSync, spawn } from 'child_process';
7
+ import { randomUUID } from 'crypto';
8
+ import { createInterface } from 'readline';
9
+
10
+ createRequire(import.meta.url);
11
+ var podman = (options) => {
12
+ const configuredImageName = options?.imageName;
13
+ const selinuxLabel = options?.selinuxLabel ?? "z";
14
+ const userns = options?.userns ?? "keep-id";
15
+ const containerUid = options?.containerUid ?? 1e3;
16
+ const containerGid = options?.containerGid ?? 1e3;
17
+ const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
18
+ const sandboxHomedir = "/home/agent";
19
+ const userMounts = options?.mounts ? resolveUserMounts(options.mounts, sandboxHomedir) : [];
20
+ const parentDirsToCreate = processFileMountParents(
21
+ userMounts,
22
+ sandboxHomedir
23
+ );
24
+ return createBindMountSandboxProvider({
25
+ name: "podman",
26
+ env: options?.env,
27
+ sandboxHomedir,
28
+ create: async (createOptions) => {
29
+ const containerName = `sandcastle-${randomUUID()}`;
30
+ const worktreePath = createOptions.mounts.find(
31
+ (m) => m.hostPath === createOptions.worktreePath
32
+ )?.sandboxPath ?? "/home/agent/workspace";
33
+ const allMounts = [...createOptions.mounts, ...userMounts];
34
+ const volumeMounts = allMounts.map(
35
+ (m) => formatVolumeMount(m, selinuxLabel)
36
+ );
37
+ const imageName = configuredImageName ?? defaultImageName(createOptions.hostRepoPath);
38
+ if (process.platform === "darwin" || process.platform === "win32") {
39
+ await checkPodmanMachine();
40
+ }
41
+ await checkImageExists(imageName);
42
+ const env = { ...createOptions.env, HOME: "/home/agent" };
43
+ const envArgs = Object.entries(env).flatMap(([key, value]) => [
44
+ "-e",
45
+ `${key}=${value}`
46
+ ]);
47
+ const volumeArgs = volumeMounts.flatMap((v) => ["-v", v]);
48
+ const usernsArgs = userns ? [`--userns=keep-id:uid=${containerUid},gid=${containerGid}`] : [];
49
+ const userArgs = ["--user", `${containerUid}:${containerGid}`];
50
+ const networks = options?.network ? Array.isArray(options.network) ? options.network : [options.network] : [];
51
+ const networkArgs = networks.flatMap((n) => ["--network", n]);
52
+ const groupArgs = (options?.groups ?? []).flatMap((g) => [
53
+ "--group-add",
54
+ String(g)
55
+ ]);
56
+ const deviceArgs = (options?.devices ?? []).flatMap((d) => [
57
+ "--device",
58
+ d
59
+ ]);
60
+ const cpusArgs = options?.cpus !== void 0 ? ["--cpus", String(options.cpus)] : [];
61
+ await new Promise((resolve, reject) => {
62
+ execFile(
63
+ "podman",
64
+ [
65
+ "run",
66
+ "-d",
67
+ "--name",
68
+ containerName,
69
+ ...userArgs,
70
+ ...usernsArgs,
71
+ ...networkArgs,
72
+ ...groupArgs,
73
+ ...deviceArgs,
74
+ ...cpusArgs,
75
+ "-w",
76
+ worktreePath,
77
+ ...envArgs,
78
+ ...volumeArgs,
79
+ "--entrypoint",
80
+ "sleep",
81
+ imageName,
82
+ "infinity"
83
+ ],
84
+ (error) => {
85
+ if (error) {
86
+ reject(new Error(`podman run failed: ${error.message}`));
87
+ } else {
88
+ resolve();
89
+ }
90
+ }
91
+ );
92
+ });
93
+ for (const dir of parentDirsToCreate) {
94
+ await new Promise((resolve, reject) => {
95
+ execFile(
96
+ "podman",
97
+ [
98
+ "exec",
99
+ "--user",
100
+ "0:0",
101
+ containerName,
102
+ "sh",
103
+ "-c",
104
+ `mkdir -p "$1" && chown "$2" "$1"`,
105
+ "sh",
106
+ dir,
107
+ `${containerUid}:${containerGid}`
108
+ ],
109
+ (error) => {
110
+ if (error) {
111
+ reject(
112
+ new Error(
113
+ `Failed to create parent directory '${dir}' in container: ${error.message}`
114
+ )
115
+ );
116
+ } else {
117
+ resolve();
118
+ }
119
+ }
120
+ );
121
+ });
122
+ }
123
+ const removeContainerSync = () => {
124
+ try {
125
+ execFileSync("podman", ["rm", "-f", containerName], {
126
+ stdio: "ignore",
127
+ timeout: 5e3
128
+ });
129
+ } catch {
130
+ }
131
+ };
132
+ const unregisterShutdown = registerShutdown(removeContainerSync);
133
+ const handle = {
134
+ worktreePath,
135
+ exec: (command, opts) => {
136
+ const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
137
+ const args = ["exec"];
138
+ if (opts?.stdin !== void 0) args.push("-i");
139
+ if (opts?.cwd) args.push("-w", opts.cwd);
140
+ args.push(containerName, "sh", "-c", effectiveCommand);
141
+ return new Promise((resolve, reject) => {
142
+ const proc = spawn("podman", args, {
143
+ stdio: [
144
+ opts?.stdin !== void 0 ? "pipe" : "ignore",
145
+ "pipe",
146
+ "pipe"
147
+ ]
148
+ });
149
+ if (opts?.stdin !== void 0) {
150
+ proc.stdin.write(opts.stdin);
151
+ proc.stdin.end();
152
+ }
153
+ proc.on("error", (error) => {
154
+ reject(new Error(`podman exec failed: ${error.message}`));
155
+ });
156
+ if (opts?.onLine) {
157
+ const onLine = opts.onLine;
158
+ const stdoutTail = new BoundedTail(maxOutputTailChars, "\n");
159
+ const stderrTail = new BoundedTail(maxOutputTailChars, "");
160
+ const rl = createInterface({ input: proc.stdout });
161
+ rl.on("line", (line) => {
162
+ stdoutTail.push(line);
163
+ onLine(line);
164
+ });
165
+ proc.stderr.on("data", (chunk) => {
166
+ stderrTail.push(chunk.toString());
167
+ });
168
+ proc.on("close", (code) => {
169
+ resolve({
170
+ stdout: stdoutTail.toString(),
171
+ stderr: stderrTail.toString(),
172
+ exitCode: code ?? 0
173
+ });
174
+ });
175
+ } else {
176
+ const stdoutChunks = [];
177
+ const stderrChunks = [];
178
+ proc.stdout.on("data", (chunk) => {
179
+ stdoutChunks.push(chunk.toString());
180
+ });
181
+ proc.stderr.on("data", (chunk) => {
182
+ stderrChunks.push(chunk.toString());
183
+ });
184
+ proc.on("close", (code) => {
185
+ resolve({
186
+ stdout: stdoutChunks.join(""),
187
+ stderr: stderrChunks.join(""),
188
+ exitCode: code ?? 0
189
+ });
190
+ });
191
+ }
192
+ });
193
+ },
194
+ interactiveExec: (args, opts) => {
195
+ return new Promise((resolve, reject) => {
196
+ const podmanArgs = ["exec"];
197
+ if ("isTTY" in opts.stdin && opts.stdin.isTTY) {
198
+ podmanArgs.push("-it");
199
+ } else {
200
+ podmanArgs.push("-i");
201
+ }
202
+ if (opts.cwd) podmanArgs.push("-w", opts.cwd);
203
+ podmanArgs.push(containerName, ...args);
204
+ const proc = spawn("podman", podmanArgs, {
205
+ stdio: [opts.stdin, opts.stdout, opts.stderr]
206
+ });
207
+ proc.on("error", (error) => {
208
+ reject(new Error(`podman exec failed: ${error.message}`));
209
+ });
210
+ proc.on("close", (code) => {
211
+ resolve({ exitCode: code ?? 0 });
212
+ });
213
+ });
214
+ },
215
+ copyFileIn: (hostPath, sandboxPath) => new Promise((resolve, reject) => {
216
+ execFile(
217
+ "podman",
218
+ ["cp", hostPath, `${containerName}:${sandboxPath}`],
219
+ (error) => {
220
+ if (error) {
221
+ reject(new Error(`podman cp (in) failed: ${error.message}`));
222
+ } else {
223
+ resolve();
224
+ }
225
+ }
226
+ );
227
+ }),
228
+ copyFileOut: (sandboxPath, hostPath) => new Promise((resolve, reject) => {
229
+ execFile(
230
+ "podman",
231
+ ["cp", `${containerName}:${sandboxPath}`, hostPath],
232
+ (error) => {
233
+ if (error) {
234
+ reject(new Error(`podman cp (out) failed: ${error.message}`));
235
+ } else {
236
+ resolve();
237
+ }
238
+ }
239
+ );
240
+ }),
241
+ close: async () => {
242
+ unregisterShutdown();
243
+ await new Promise((resolve, reject) => {
244
+ execFile("podman", ["rm", "-f", containerName], (error) => {
245
+ if (error) {
246
+ reject(new Error(`podman rm failed: ${error.message}`));
247
+ } else {
248
+ resolve();
249
+ }
250
+ });
251
+ });
252
+ }
253
+ };
254
+ return handle;
255
+ }
256
+ });
257
+ };
258
+ var checkImageExists = (imageName) => new Promise((resolve, reject) => {
259
+ execFile("podman", ["image", "inspect", imageName], (error) => {
260
+ if (error) {
261
+ reject(
262
+ new Error(
263
+ `Image '${imageName}' not found locally. Build it first with 'podman build -t ${imageName} .'`
264
+ )
265
+ );
266
+ } else {
267
+ resolve();
268
+ }
269
+ });
270
+ });
271
+ var podmanMachineError = () => new Error(
272
+ "Podman Machine is not running. Run 'podman machine init && podman machine start' first."
273
+ );
274
+ var checkPodmanMachine = () => new Promise((resolve, reject) => {
275
+ execFile(
276
+ "podman",
277
+ ["machine", "list", "--format", "json"],
278
+ (error, stdout) => {
279
+ if (error) {
280
+ reject(podmanMachineError());
281
+ return;
282
+ }
283
+ try {
284
+ const machines = JSON.parse(stdout.toString());
285
+ if (machines.some((m) => m.Running)) {
286
+ resolve();
287
+ } else {
288
+ reject(podmanMachineError());
289
+ }
290
+ } catch {
291
+ reject(podmanMachineError());
292
+ }
293
+ }
294
+ );
295
+ });
296
+
297
+ export { podman };
298
+ //# sourceMappingURL=podman.js.map
299
+ //# sourceMappingURL=podman.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sandboxes/podman.ts"],"names":[],"mappings":";;;;;;;;;;AAgJO,IAAM,MAAA,GAAS,CAAC,OAAA,KAA6C;AAClE,EAAA,MAAM,sBAAsB,OAAA,EAAS,SAAA;AACrC,EAAA,MAAM,YAAA,GAAe,SAAS,YAAA,IAAgB,GAAA;AAC9C,EAAA,MAAM,MAAA,GAAS,SAAS,MAAA,IAAU,SAAA;AAClC,EAAA,MAAM,YAAA,GAAe,SAAS,YAAA,IAAgB,GAAA;AAC9C,EAAA,MAAM,YAAA,GAAe,SAAS,YAAA,IAAgB,GAAA;AAC9C,EAAA,MAAM,kBAAA,GAAqB,SAAS,kBAAA,IAAsB,cAAA;AAC1D,EAAA,MAAM,cAAA,GAAiB,aAAA;AACvB,EAAA,MAAM,UAAA,GAAa,SAAS,MAAA,GACxB,iBAAA,CAAkB,QAAQ,MAAA,EAAQ,cAAc,IAChD,EAAC;AAGL,EAAA,MAAM,kBAAA,GAAqB,uBAAA;AAAA,IACzB,UAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,OAAO,8BAAA,CAA+B;AAAA,IACpC,IAAA,EAAM,QAAA;AAAA,IACN,KAAK,OAAA,EAAS,GAAA;AAAA,IACd,cAAA;AAAA,IACA,MAAA,EAAQ,OACN,aAAA,KACoC;AACpC,MAAA,MAAM,aAAA,GAAgB,CAAA,WAAA,EAAc,UAAA,EAAY,CAAA,CAAA;AAEhD,MAAA,MAAM,YAAA,GACJ,cAAc,MAAA,CAAO,IAAA;AAAA,QACnB,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,KAAa,aAAA,CAAc;AAAA,SACnC,WAAA,IAAe,uBAAA;AAGpB,MAAA,MAAM,YAAY,CAAC,GAAG,aAAA,CAAc,MAAA,EAAQ,GAAG,UAAU,CAAA;AACzD,MAAA,MAAM,eAAe,SAAA,CAAU,GAAA;AAAA,QAAI,CAAC,CAAA,KAClC,iBAAA,CAAkB,CAAA,EAAG,YAAY;AAAA,OACnC;AAGA,MAAA,MAAM,SAAA,GACJ,mBAAA,IAAuB,gBAAA,CAAiB,aAAA,CAAc,YAAY,CAAA;AAGpE,MAAA,IAAI,OAAA,CAAQ,QAAA,KAAa,QAAA,IAAY,OAAA,CAAQ,aAAa,OAAA,EAAS;AACjE,QAAA,MAAM,kBAAA,EAAmB;AAAA,MAC3B;AAGA,MAAA,MAAM,iBAAiB,SAAS,CAAA;AAEhC,MAAA,MAAM,MAAM,EAAE,GAAG,aAAA,CAAc,GAAA,EAAK,MAAM,aAAA,EAAc;AACxD,MAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,CAAE,QAAQ,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,KAAM;AAAA,QAC5D,IAAA;AAAA,QACA,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,KAAK,CAAA;AAAA,OAChB,CAAA;AACD,MAAA,MAAM,UAAA,GAAa,aAAa,OAAA,CAAQ,CAAC,MAAM,CAAC,IAAA,EAAM,CAAC,CAAC,CAAA;AACxD,MAAA,MAAM,UAAA,GAAa,SACf,CAAC,CAAA,qBAAA,EAAwB,YAAY,CAAA,KAAA,EAAQ,YAAY,CAAA,CAAE,CAAA,GAC3D,EAAC;AACL,MAAA,MAAM,WAAW,CAAC,QAAA,EAAU,GAAG,YAAY,CAAA,CAAA,EAAI,YAAY,CAAA,CAAE,CAAA;AAC7D,MAAA,MAAM,QAAA,GAAW,OAAA,EAAS,OAAA,GACtB,KAAA,CAAM,QAAQ,OAAA,CAAQ,OAAO,CAAA,GAC3B,OAAA,CAAQ,OAAA,GACR,CAAC,OAAA,CAAQ,OAAO,IAClB,EAAC;AACL,MAAA,MAAM,WAAA,GAAc,SAAS,OAAA,CAAQ,CAAC,MAAM,CAAC,WAAA,EAAa,CAAC,CAAC,CAAA;AAC5D,MAAA,MAAM,aAAa,OAAA,EAAS,MAAA,IAAU,EAAC,EAAG,OAAA,CAAQ,CAAC,CAAA,KAAM;AAAA,QACvD,aAAA;AAAA,QACA,OAAO,CAAC;AAAA,OACT,CAAA;AACD,MAAA,MAAM,cAAc,OAAA,EAAS,OAAA,IAAW,EAAC,EAAG,OAAA,CAAQ,CAAC,CAAA,KAAM;AAAA,QACzD,UAAA;AAAA,QACA;AAAA,OACD,CAAA;AACD,MAAA,MAAM,QAAA,GACJ,OAAA,EAAS,IAAA,KAAS,MAAA,GAAY,CAAC,QAAA,EAAU,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAC,CAAA,GAAI,EAAC;AAGpE,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,QAAA,QAAA;AAAA,UACE,QAAA;AAAA,UACA;AAAA,YACE,KAAA;AAAA,YACA,IAAA;AAAA,YACA,QAAA;AAAA,YACA,aAAA;AAAA,YACA,GAAG,QAAA;AAAA,YACH,GAAG,UAAA;AAAA,YACH,GAAG,WAAA;AAAA,YACH,GAAG,SAAA;AAAA,YACH,GAAG,UAAA;AAAA,YACH,GAAG,QAAA;AAAA,YACH,IAAA;AAAA,YACA,YAAA;AAAA,YACA,GAAG,OAAA;AAAA,YACH,GAAG,UAAA;AAAA,YACH,cAAA;AAAA,YACA,OAAA;AAAA,YACA,SAAA;AAAA,YACA;AAAA,WACF;AAAA,UACA,CAAC,KAAA,KAAU;AACT,YAAA,IAAI,KAAA,EAAO;AACT,cAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,YACzD,CAAA,MAAO;AACL,cAAA,OAAA,EAAQ;AAAA,YACV;AAAA,UACF;AAAA,SACF;AAAA,MACF,CAAC,CAAA;AAGD,MAAA,KAAA,MAAW,OAAO,kBAAA,EAAoB;AACpC,QAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,UAAA,QAAA;AAAA,YACE,QAAA;AAAA,YACA;AAAA,cACE,MAAA;AAAA,cACA,QAAA;AAAA,cACA,KAAA;AAAA,cACA,aAAA;AAAA,cACA,IAAA;AAAA,cACA,IAAA;AAAA,cACA,CAAA,gCAAA,CAAA;AAAA,cACA,IAAA;AAAA,cACA,GAAA;AAAA,cACA,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,YAAY,CAAA;AAAA,aACjC;AAAA,YACA,CAAC,KAAA,KAAU;AACT,cAAA,IAAI,KAAA,EAAO;AACT,gBAAA,MAAA;AAAA,kBACE,IAAI,KAAA;AAAA,oBACF,CAAA,mCAAA,EAAsC,GAAG,CAAA,gBAAA,EAAmB,KAAA,CAAM,OAAO,CAAA;AAAA;AAC3E,iBACF;AAAA,cACF,CAAA,MAAO;AACL,gBAAA,OAAA,EAAQ;AAAA,cACV;AAAA,YACF;AAAA,WACF;AAAA,QACF,CAAC,CAAA;AAAA,MACH;AAKA,MAAA,MAAM,sBAAsB,MAAM;AAChC,QAAA,IAAI;AACF,UAAA,YAAA,CAAa,QAAA,EAAU,CAAC,IAAA,EAAM,IAAA,EAAM,aAAa,CAAA,EAAG;AAAA,YAClD,KAAA,EAAO,QAAA;AAAA,YACP,OAAA,EAAS;AAAA,WACV,CAAA;AAAA,QACH,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF,CAAA;AACA,MAAA,MAAM,kBAAA,GAAqB,iBAAiB,mBAAmB,CAAA;AAE/D,MAAA,MAAM,MAAA,GAAiC;AAAA,QACrC,YAAA;AAAA,QAEA,IAAA,EAAM,CACJ,OAAA,EACA,IAAA,KAMwB;AACxB,UAAA,MAAM,gBAAA,GAAmB,IAAA,EAAM,IAAA,GAAO,CAAA,KAAA,EAAQ,OAAO,CAAA,CAAA,GAAK,OAAA;AAC1D,UAAA,MAAM,IAAA,GAAO,CAAC,MAAM,CAAA;AACpB,UAAA,IAAI,IAAA,EAAM,KAAA,KAAU,MAAA,EAAW,IAAA,CAAK,KAAK,IAAI,CAAA;AAC7C,UAAA,IAAI,MAAM,GAAA,EAAK,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,KAAK,GAAG,CAAA;AACvC,UAAA,IAAA,CAAK,IAAA,CAAK,aAAA,EAAe,IAAA,EAAM,IAAA,EAAM,gBAAgB,CAAA;AAErD,UAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,YAAA,MAAM,IAAA,GAAO,KAAA,CAAM,QAAA,EAAU,IAAA,EAAM;AAAA,cACjC,KAAA,EAAO;AAAA,gBACL,IAAA,EAAM,KAAA,KAAU,MAAA,GAAY,MAAA,GAAS,QAAA;AAAA,gBACrC,MAAA;AAAA,gBACA;AAAA;AACF,aACD,CAAA;AAED,YAAA,IAAI,IAAA,EAAM,UAAU,MAAA,EAAW;AAC7B,cAAA,IAAA,CAAK,KAAA,CAAO,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA;AAC5B,cAAA,IAAA,CAAK,MAAO,GAAA,EAAI;AAAA,YAClB;AAEA,YAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,KAAA,KAAU;AAC1B,cAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,YAC1D,CAAC,CAAA;AAED,YAAA,IAAI,MAAM,MAAA,EAAQ;AAChB,cAAA,MAAM,SAAS,IAAA,CAAK,MAAA;AACpB,cAAA,MAAM,UAAA,GAAa,IAAI,WAAA,CAAY,kBAAA,EAAoB,IAAI,CAAA;AAC3D,cAAA,MAAM,UAAA,GAAa,IAAI,WAAA,CAAY,kBAAA,EAAoB,EAAE,CAAA;AACzD,cAAA,MAAM,KAAK,eAAA,CAAgB,EAAE,KAAA,EAAO,IAAA,CAAK,QAAS,CAAA;AAClD,cAAA,EAAA,CAAG,EAAA,CAAG,MAAA,EAAQ,CAAC,IAAA,KAAS;AACtB,gBAAA,UAAA,CAAW,KAAK,IAAI,CAAA;AACpB,gBAAA,MAAA,CAAO,IAAI,CAAA;AAAA,cACb,CAAC,CAAA;AACD,cAAA,IAAA,CAAK,MAAA,CAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AACzC,gBAAA,UAAA,CAAW,IAAA,CAAK,KAAA,CAAM,QAAA,EAAU,CAAA;AAAA,cAClC,CAAC,CAAA;AACD,cAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAS;AACzB,gBAAA,OAAA,CAAQ;AAAA,kBACN,MAAA,EAAQ,WAAW,QAAA,EAAS;AAAA,kBAC5B,MAAA,EAAQ,WAAW,QAAA,EAAS;AAAA,kBAC5B,UAAU,IAAA,IAAQ;AAAA,iBACnB,CAAA;AAAA,cACH,CAAC,CAAA;AAAA,YACH,CAAA,MAAO;AACL,cAAA,MAAM,eAAyB,EAAC;AAChC,cAAA,MAAM,eAAyB,EAAC;AAChC,cAAA,IAAA,CAAK,MAAA,CAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AACzC,gBAAA,YAAA,CAAa,IAAA,CAAK,KAAA,CAAM,QAAA,EAAU,CAAA;AAAA,cACpC,CAAC,CAAA;AACD,cAAA,IAAA,CAAK,MAAA,CAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AACzC,gBAAA,YAAA,CAAa,IAAA,CAAK,KAAA,CAAM,QAAA,EAAU,CAAA;AAAA,cACpC,CAAC,CAAA;AACD,cAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAS;AACzB,gBAAA,OAAA,CAAQ;AAAA,kBACN,MAAA,EAAQ,YAAA,CAAa,IAAA,CAAK,EAAE,CAAA;AAAA,kBAC5B,MAAA,EAAQ,YAAA,CAAa,IAAA,CAAK,EAAE,CAAA;AAAA,kBAC5B,UAAU,IAAA,IAAQ;AAAA,iBACnB,CAAA;AAAA,cACH,CAAC,CAAA;AAAA,YACH;AAAA,UACF,CAAC,CAAA;AAAA,QACH,CAAA;AAAA,QAEA,eAAA,EAAiB,CACf,IAAA,EACA,IAAA,KACkC;AAClC,UAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,YAAA,MAAM,UAAA,GAAa,CAAC,MAAM,CAAA;AAE1B,YAAA,IACE,OAAA,IAAW,IAAA,CAAK,KAAA,IACf,IAAA,CAAK,MAA8B,KAAA,EACpC;AACA,cAAA,UAAA,CAAW,KAAK,KAAK,CAAA;AAAA,YACvB,CAAA,MAAO;AACL,cAAA,UAAA,CAAW,KAAK,IAAI,CAAA;AAAA,YACtB;AACA,YAAA,IAAI,KAAK,GAAA,EAAK,UAAA,CAAW,IAAA,CAAK,IAAA,EAAM,KAAK,GAAG,CAAA;AAC5C,YAAA,UAAA,CAAW,IAAA,CAAK,aAAA,EAAe,GAAG,IAAI,CAAA;AAEtC,YAAA,MAAM,IAAA,GAAO,KAAA,CAAM,QAAA,EAAU,UAAA,EAAY;AAAA,cACvC,OAAO,CAAC,IAAA,CAAK,OAAO,IAAA,CAAK,MAAA,EAAQ,KAAK,MAAM;AAAA,aAC7C,CAAA;AAED,YAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,KAAA,KAAiB;AACjC,cAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,YAC1D,CAAC,CAAA;AAED,YAAA,IAAA,CAAK,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAwB;AACxC,cAAA,OAAA,CAAQ,EAAE,QAAA,EAAU,IAAA,IAAQ,CAAA,EAAG,CAAA;AAAA,YACjC,CAAC,CAAA;AAAA,UACH,CAAC,CAAA;AAAA,QACH,CAAA;AAAA,QAEA,UAAA,EAAY,CAAC,QAAA,EAAkB,WAAA,KAC7B,IAAI,OAAA,CAAQ,CAAC,SAAS,MAAA,KAAW;AAC/B,UAAA,QAAA;AAAA,YACE,QAAA;AAAA,YACA,CAAC,IAAA,EAAM,QAAA,EAAU,GAAG,aAAa,CAAA,CAAA,EAAI,WAAW,CAAA,CAAE,CAAA;AAAA,YAClD,CAAC,KAAA,KAAU;AACT,cAAA,IAAI,KAAA,EAAO;AACT,gBAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,cAC7D,CAAA,MAAO;AACL,gBAAA,OAAA,EAAQ;AAAA,cACV;AAAA,YACF;AAAA,WACF;AAAA,QACF,CAAC,CAAA;AAAA,QAEH,WAAA,EAAa,CAAC,WAAA,EAAqB,QAAA,KACjC,IAAI,OAAA,CAAQ,CAAC,SAAS,MAAA,KAAW;AAC/B,UAAA,QAAA;AAAA,YACE,QAAA;AAAA,YACA,CAAC,IAAA,EAAM,CAAA,EAAG,aAAa,CAAA,CAAA,EAAI,WAAW,IAAI,QAAQ,CAAA;AAAA,YAClD,CAAC,KAAA,KAAU;AACT,cAAA,IAAI,KAAA,EAAO;AACT,gBAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,cAC9D,CAAA,MAAO;AACL,gBAAA,OAAA,EAAQ;AAAA,cACV;AAAA,YACF;AAAA,WACF;AAAA,QACF,CAAC,CAAA;AAAA,QAEH,OAAO,YAA2B;AAChC,UAAA,kBAAA,EAAmB;AACnB,UAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,YAAA,QAAA,CAAS,UAAU,CAAC,IAAA,EAAM,MAAM,aAAa,CAAA,EAAG,CAAC,KAAA,KAAU;AACzD,cAAA,IAAI,KAAA,EAAO;AACT,gBAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,cACxD,CAAA,MAAO;AACL,gBAAA,OAAA,EAAQ;AAAA,cACV;AAAA,YACF,CAAC,CAAA;AAAA,UACH,CAAC,CAAA;AAAA,QACH;AAAA,OACF;AAEA,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,GACD,CAAA;AACH;AAKA,IAAM,mBAAmB,CAAC,SAAA,KACxB,IAAI,OAAA,CAAc,CAAC,SAAS,MAAA,KAAW;AACrC,EAAA,QAAA,CAAS,UAAU,CAAC,OAAA,EAAS,WAAW,SAAS,CAAA,EAAG,CAAC,KAAA,KAAU;AAC7D,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAA;AAAA,QACE,IAAI,KAAA;AAAA,UACF,CAAA,OAAA,EAAU,SAAS,CAAA,0DAAA,EAA6D,SAAS,CAAA,GAAA;AAAA;AAC3F,OACF;AAAA,IACF,CAAA,MAAO;AACL,MAAA,OAAA,EAAQ;AAAA,IACV;AAAA,EACF,CAAC,CAAA;AACH,CAAC,CAAA;AAEH,IAAM,kBAAA,GAAqB,MACzB,IAAI,KAAA;AAAA,EACF;AACF,CAAA;AAEF,IAAM,qBAAqB,MACzB,IAAI,OAAA,CAAc,CAAC,SAAS,MAAA,KAAW;AACrC,EAAA,QAAA;AAAA,IACE,QAAA;AAAA,IACA,CAAC,SAAA,EAAW,MAAA,EAAQ,UAAA,EAAY,MAAM,CAAA;AAAA,IACtC,CAAC,OAAO,MAAA,KAAW;AACjB,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,MAAA,CAAO,oBAAoB,CAAA;AAC3B,QAAA;AAAA,MACF;AACA,MAAA,IAAI;AACF,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,UAAU,CAAA;AAG7C,QAAA,IAAI,SAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,EAAG;AACnC,UAAA,OAAA,EAAQ;AAAA,QACV,CAAA,MAAO;AACL,UAAA,MAAA,CAAO,oBAAoB,CAAA;AAAA,QAC7B;AAAA,MACF,CAAA,CAAA,MAAQ;AACN,QAAA,MAAA,CAAO,oBAAoB,CAAA;AAAA,MAC7B;AAAA,IACF;AAAA,GACF;AACF,CAAC,CAAA","file":"podman.js","sourcesContent":["/**\n * Podman sandbox provider — creates Podman containers with bind-mounts.\n *\n * Usage:\n * import { podman } from \"sandcastle/sandboxes/podman\";\n * await run({ agent: claudeCode(\"claude-opus-4-7\"), sandbox: podman() });\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 {\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 formatVolumeMount,\n processFileMountParents,\n} from \"../mountUtils.js\";\nimport { BoundedTail, MAX_TAIL_CHARS } from \"../boundedTail.js\";\nimport { registerShutdown } from \"../shutdownRegistry.js\";\n\nexport interface PodmanOptions {\n /** Podman image name (default: derived from repo directory name). */\n readonly imageName?: string;\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 * User namespace mode for rootless Podman.\n *\n * - `\"keep-id\"` (default) — maps host UID to `containerUid` inside the\n * container via `--userns=keep-id:uid=N,gid=N`, so both bind-mounted\n * files and image-built files have correct ownership without chown.\n * - `false` — disable; use for rootful Podman setups.\n */\n readonly userns?: \"keep-id\" | false;\n /**\n * The UID of the `agent` user inside the container image (default: 1000).\n *\n * Must match the UID set in the Containerfile. Used with `--userns=keep-id`\n * to map the host user to this UID inside the container.\n */\n readonly containerUid?: number;\n /**\n * The GID of the `agent` user inside the container image (default: 1000).\n *\n * Must match the GID set in the Containerfile. Used with `--userns=keep-id`\n * to map the host group to this GID inside the container.\n */\n readonly containerGid?: number;\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 * Podman 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, Podman's default 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 * Under rootless Podman, exposing a host device often requires host-side\n * group/permission setup and may interact with `--userns=keep-id`.\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 `podman 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 Podman sandbox provider.\n *\n * The returned provider creates Podman containers with bind-mounts\n * for the worktree and git directories. Calls the `podman` binary\n * on PATH directly. On macOS/Windows, verifies that a Podman Machine\n * is running before container creation.\n */\nexport const podman = (options?: PodmanOptions): SandboxProvider => {\n const configuredImageName = options?.imageName;\n const selinuxLabel = options?.selinuxLabel ?? \"z\";\n const userns = options?.userns ?? \"keep-id\";\n const containerUid = options?.containerUid ?? 1000;\n const containerGid = options?.containerGid ?? 1000;\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: \"podman\",\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 strings with optional SELinux label (internal + user mounts)\n const allMounts = [...createOptions.mounts, ...userMounts];\n const volumeMounts = allMounts.map((m) =>\n formatVolumeMount(m, selinuxLabel),\n );\n\n // Resolve image name\n const imageName =\n configuredImageName ?? defaultImageName(createOptions.hostRepoPath);\n\n // Pre-flight: check Podman Machine on macOS/Windows\n if (process.platform === \"darwin\" || process.platform === \"win32\") {\n await checkPodmanMachine();\n }\n\n // Pre-flight: verify image exists locally\n await checkImageExists(imageName);\n\n const env = { ...createOptions.env, HOME: \"/home/agent\" };\n const envArgs = Object.entries(env).flatMap(([key, value]) => [\n \"-e\",\n `${key}=${value}`,\n ]);\n const volumeArgs = volumeMounts.flatMap((v) => [\"-v\", v]);\n const usernsArgs = userns\n ? [`--userns=keep-id:uid=${containerUid},gid=${containerGid}`]\n : [];\n const userArgs = [\"--user\", `${containerUid}:${containerGid}`];\n const networks = options?.network\n ? Array.isArray(options.network)\n ? options.network\n : [options.network]\n : [];\n const networkArgs = networks.flatMap((n) => [\"--network\", n]);\n const groupArgs = (options?.groups ?? []).flatMap((g) => [\n \"--group-add\",\n String(g),\n ]);\n const deviceArgs = (options?.devices ?? []).flatMap((d) => [\n \"--device\",\n d,\n ]);\n const cpusArgs =\n options?.cpus !== undefined ? [\"--cpus\", String(options.cpus)] : [];\n\n // Start container via podman run\n await new Promise<void>((resolve, reject) => {\n execFile(\n \"podman\",\n [\n \"run\",\n \"-d\",\n \"--name\",\n containerName,\n ...userArgs,\n ...usernsArgs,\n ...networkArgs,\n ...groupArgs,\n ...deviceArgs,\n ...cpusArgs,\n \"-w\",\n worktreePath,\n ...envArgs,\n ...volumeArgs,\n \"--entrypoint\",\n \"sleep\",\n imageName,\n \"infinity\",\n ],\n (error) => {\n if (error) {\n reject(new Error(`podman run failed: ${error.message}`));\n } else {\n resolve();\n }\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 \"podman\",\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(\"podman\", [\"rm\", \"-f\", containerName], {\n stdio: \"ignore\",\n timeout: 5000,\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(\"podman\", 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(`podman 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 podmanArgs = [\"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 podmanArgs.push(\"-it\");\n } else {\n podmanArgs.push(\"-i\");\n }\n if (opts.cwd) podmanArgs.push(\"-w\", opts.cwd);\n podmanArgs.push(containerName, ...args);\n\n const proc = spawn(\"podman\", podmanArgs, {\n stdio: [opts.stdin, opts.stdout, opts.stderr] as StdioOptions,\n });\n\n proc.on(\"error\", (error: Error) => {\n reject(new Error(`podman 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 \"podman\",\n [\"cp\", hostPath, `${containerName}:${sandboxPath}`],\n (error) => {\n if (error) {\n reject(new Error(`podman 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 \"podman\",\n [\"cp\", `${containerName}:${sandboxPath}`, hostPath],\n (error) => {\n if (error) {\n reject(new Error(`podman cp (out) failed: ${error.message}`));\n } else {\n resolve();\n }\n },\n );\n }),\n\n close: async (): Promise<void> => {\n unregisterShutdown();\n await new Promise<void>((resolve, reject) => {\n execFile(\"podman\", [\"rm\", \"-f\", containerName], (error) => {\n if (error) {\n reject(new Error(`podman rm failed: ${error.message}`));\n } else {\n resolve();\n }\n });\n });\n },\n };\n\n return handle;\n },\n });\n};\n\n// Re-export for backwards compatibility\nexport { defaultImageName };\n\nconst checkImageExists = (imageName: string): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n execFile(\"podman\", [\"image\", \"inspect\", imageName], (error) => {\n if (error) {\n reject(\n new Error(\n `Image '${imageName}' not found locally. Build it first with 'podman build -t ${imageName} .'`,\n ),\n );\n } else {\n resolve();\n }\n });\n });\n\nconst podmanMachineError = () =>\n new Error(\n \"Podman Machine is not running. Run 'podman machine init && podman machine start' first.\",\n );\n\nconst checkPodmanMachine = (): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n execFile(\n \"podman\",\n [\"machine\", \"list\", \"--format\", \"json\"],\n (error, stdout) => {\n if (error) {\n reject(podmanMachineError());\n return;\n }\n try {\n const machines = JSON.parse(stdout.toString()) as Array<{\n Running?: boolean;\n }>;\n if (machines.some((m) => m.Running)) {\n resolve();\n } else {\n reject(podmanMachineError());\n }\n } catch {\n reject(podmanMachineError());\n }\n },\n );\n });\n"]}
@@ -0,0 +1,104 @@
1
+ import { I as IsolatedSandboxProvider } from '../SandboxProvider-EkSMuBp8.js';
2
+
3
+ /**
4
+ * Vercel isolated sandbox provider — wraps `@vercel/sandbox` into a SandboxProvider.
5
+ *
6
+ * Usage:
7
+ * import { vercel } from "sandcastle/sandboxes/vercel";
8
+ * await run({ agent: claudeCode("claude-opus-4-7"), sandbox: vercel() });
9
+ */
10
+
11
+ /**
12
+ * Options for creating a Vercel sandbox provider.
13
+ *
14
+ * All `@vercel/sandbox` `Sandbox.create()` options are accepted as pass-through,
15
+ * plus Sandcastle-specific options for auth and branch strategy.
16
+ */
17
+ interface VercelOptions {
18
+ /**
19
+ * Vercel access token.
20
+ *
21
+ * Falls back to the SDK's default auth behavior, which reads
22
+ * `VERCEL_OIDC_TOKEN` (recommended for Vercel-hosted environments) or
23
+ * `VERCEL_TOKEN` from the environment.
24
+ */
25
+ readonly token?: string;
26
+ /**
27
+ * The source of the sandbox (git repo, tarball, or snapshot).
28
+ * Omit to start an empty sandbox.
29
+ */
30
+ readonly source?: {
31
+ type: "git";
32
+ url: string;
33
+ depth?: number;
34
+ revision?: string;
35
+ username?: string;
36
+ password?: string;
37
+ } | {
38
+ type: "tarball";
39
+ url: string;
40
+ } | {
41
+ type: "snapshot";
42
+ snapshotId: string;
43
+ };
44
+ /** Array of port numbers to expose from the sandbox (up to 4). */
45
+ readonly ports?: number[];
46
+ /** Timeout in milliseconds before the sandbox auto-terminates. */
47
+ readonly timeout?: number;
48
+ /**
49
+ * Resources to allocate to the sandbox.
50
+ * Each vCPU gets 2048 MB of memory.
51
+ */
52
+ readonly resources?: {
53
+ vcpus: number;
54
+ };
55
+ /**
56
+ * The runtime of the sandbox (e.g. `"node24"`, `"node22"`, `"python3.13"`).
57
+ * Defaults to `"node24"`.
58
+ */
59
+ readonly runtime?: string;
60
+ /**
61
+ * Network policy for the sandbox.
62
+ * Defaults to full internet access if not specified.
63
+ */
64
+ readonly networkPolicy?: Record<string, unknown>;
65
+ /**
66
+ * Vercel project ID to associate sandbox operations with.
67
+ */
68
+ readonly projectId?: string;
69
+ /**
70
+ * Vercel team ID to associate sandbox operations with.
71
+ */
72
+ readonly teamId?: string;
73
+ /**
74
+ * Timeout in milliseconds (alias for `timeout`, kept for discoverability).
75
+ */
76
+ readonly timeoutMs?: number;
77
+ /**
78
+ * Sandbox template shorthand (e.g. `"node-22"`).
79
+ * Maps to the `runtime` option.
80
+ */
81
+ readonly template?: string;
82
+ /** Environment variables injected by this provider. Merged at launch time with env resolver and agent provider env. */
83
+ readonly env?: Record<string, string>;
84
+ /**
85
+ * Maximum number of characters of streamed `exec` output retained per stream
86
+ * (stdout and stderr) when an `onLine` callback is supplied (default: 64KiB).
87
+ *
88
+ * Output is delivered live to `onLine` regardless; this only bounds the tail
89
+ * returned in `ExecResult`, preventing a long-running agent's output from
90
+ * overflowing V8's max string length and crashing the run.
91
+ */
92
+ readonly maxOutputTailChars?: number;
93
+ }
94
+ /**
95
+ * Create a Vercel isolated sandbox provider.
96
+ *
97
+ * The returned provider creates Vercel Firecracker microVM sandboxes via
98
+ * the `@vercel/sandbox` SDK. Each sandbox is ephemeral — one sandbox per run.
99
+ *
100
+ * Requires `@vercel/sandbox` to be installed as a peer dependency.
101
+ */
102
+ declare const vercel: (options?: VercelOptions) => IsolatedSandboxProvider;
103
+
104
+ export { type VercelOptions, vercel };
@@ -0,0 +1,148 @@
1
+ import { createRequire } from 'node:module';
2
+ import { createIsolatedSandboxProvider } from '../chunk-BIWNFKGV.js';
3
+ import { MAX_TAIL_CHARS, BoundedTail } from '../chunk-NGBM7T3E.js';
4
+ import { execSync } from 'child_process';
5
+ import { mkdir, writeFile, stat, readFile, unlink } from 'fs/promises';
6
+ import { tmpdir } from 'os';
7
+ import { dirname, join } from 'path';
8
+ import { Writable } from 'stream';
9
+
10
+ createRequire(import.meta.url);
11
+ var VERCEL_REPO_PATH = "/vercel/sandbox/workspace";
12
+ var vercel = (options) => createIsolatedSandboxProvider({
13
+ name: "vercel",
14
+ env: options?.env,
15
+ create: async (createOptions) => {
16
+ const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
17
+ const { Sandbox } = await import('@vercel/sandbox');
18
+ const createParams = {};
19
+ if (options?.source) createParams.source = options.source;
20
+ if (options?.ports) createParams.ports = options.ports;
21
+ if (options?.resources) createParams.resources = options.resources;
22
+ if (options?.networkPolicy)
23
+ createParams.networkPolicy = options.networkPolicy;
24
+ const resolvedRuntime = options?.runtime ?? options?.template;
25
+ if (resolvedRuntime) createParams.runtime = resolvedRuntime;
26
+ const timeoutValue = options?.timeout ?? options?.timeoutMs;
27
+ if (timeoutValue !== void 0) createParams.timeout = timeoutValue;
28
+ createParams.env = createOptions.env;
29
+ if (options?.token) createParams.token = options.token;
30
+ if (options?.projectId) createParams.projectId = options.projectId;
31
+ if (options?.teamId) createParams.teamId = options.teamId;
32
+ const sandbox = await Sandbox.create(
33
+ createParams
34
+ );
35
+ await sandbox.mkDir(VERCEL_REPO_PATH);
36
+ const handle = {
37
+ worktreePath: VERCEL_REPO_PATH,
38
+ exec: async (command, opts) => {
39
+ if (opts?.onLine) {
40
+ const onLine = opts.onLine;
41
+ const stdoutTail = new BoundedTail(maxOutputTailChars, "\n");
42
+ const stderrTail = new BoundedTail(maxOutputTailChars, "");
43
+ let partial = "";
44
+ const stdoutWritable = new Writable({
45
+ write(chunk, _encoding, callback) {
46
+ const text = partial + chunk.toString();
47
+ const lines = text.split("\n");
48
+ partial = lines.pop() ?? "";
49
+ for (const line of lines) {
50
+ stdoutTail.push(line);
51
+ onLine(line);
52
+ }
53
+ callback();
54
+ },
55
+ final(callback) {
56
+ if (partial) {
57
+ stdoutTail.push(partial);
58
+ onLine(partial);
59
+ partial = "";
60
+ }
61
+ callback();
62
+ }
63
+ });
64
+ const stderrWritable = new Writable({
65
+ write(chunk, _encoding, callback) {
66
+ stderrTail.push(chunk.toString());
67
+ callback();
68
+ }
69
+ });
70
+ const result2 = await sandbox.runCommand({
71
+ cmd: "sh",
72
+ args: ["-c", command],
73
+ cwd: opts?.cwd ?? VERCEL_REPO_PATH,
74
+ stdout: stdoutWritable,
75
+ stderr: stderrWritable,
76
+ ...opts?.sudo ? { sudo: true } : {}
77
+ });
78
+ return {
79
+ stdout: stdoutTail.toString(),
80
+ stderr: stderrTail.toString(),
81
+ exitCode: result2.exitCode
82
+ };
83
+ }
84
+ const result = await sandbox.runCommand({
85
+ cmd: "sh",
86
+ args: ["-c", command],
87
+ cwd: opts?.cwd ?? VERCEL_REPO_PATH,
88
+ ...opts?.sudo ? { sudo: true } : {}
89
+ });
90
+ const stdout = await result.stdout();
91
+ const stderr = await result.stderr();
92
+ return {
93
+ stdout,
94
+ stderr,
95
+ exitCode: result.exitCode
96
+ };
97
+ },
98
+ copyIn: async (hostPath, sandboxPath) => {
99
+ const info = await stat(hostPath);
100
+ if (info.isDirectory()) {
101
+ const tarPath = join(
102
+ tmpdir(),
103
+ `sandcastle-copyin-${Date.now()}.tar.gz`
104
+ );
105
+ execSync(`tar -czf "${tarPath}" -C "${hostPath}" .`);
106
+ try {
107
+ const tarContent = await readFile(tarPath);
108
+ const sandboxTarPath = `/tmp/sandcastle-copyin-${Date.now()}.tar.gz`;
109
+ await sandbox.writeFiles([
110
+ { path: sandboxTarPath, content: tarContent }
111
+ ]);
112
+ await sandbox.runCommand({
113
+ cmd: "sh",
114
+ args: [
115
+ "-c",
116
+ `mkdir -p "${sandboxPath}" && tar -xzf "${sandboxTarPath}" -C "${sandboxPath}" && rm -f "${sandboxTarPath}"`
117
+ ]
118
+ });
119
+ } finally {
120
+ await unlink(tarPath).catch(() => {
121
+ });
122
+ }
123
+ } else {
124
+ const content = await readFile(hostPath);
125
+ await sandbox.writeFiles([{ path: sandboxPath, content }]);
126
+ }
127
+ },
128
+ copyFileOut: async (sandboxPath, hostPath) => {
129
+ const buffer = await sandbox.readFileToBuffer({
130
+ path: sandboxPath
131
+ });
132
+ if (!buffer) {
133
+ throw new Error(`File not found in Vercel sandbox: ${sandboxPath}`);
134
+ }
135
+ await mkdir(dirname(hostPath), { recursive: true });
136
+ await writeFile(hostPath, buffer);
137
+ },
138
+ close: async () => {
139
+ await sandbox.stop();
140
+ }
141
+ };
142
+ return handle;
143
+ }
144
+ });
145
+
146
+ export { vercel };
147
+ //# sourceMappingURL=vercel.js.map
148
+ //# sourceMappingURL=vercel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sandboxes/vercel.ts"],"names":["result"],"mappings":"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","file":"vercel.js","sourcesContent":["/**\n * Vercel isolated sandbox provider — wraps `@vercel/sandbox` into a SandboxProvider.\n *\n * Usage:\n * import { vercel } from \"sandcastle/sandboxes/vercel\";\n * await run({ agent: claudeCode(\"claude-opus-4-7\"), sandbox: vercel() });\n */\n\nimport { execSync } from \"node:child_process\";\nimport { readFile, unlink, writeFile, mkdir, stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { Writable } from \"node:stream\";\nimport {\n createIsolatedSandboxProvider,\n type ExecResult,\n type IsolatedSandboxHandle,\n type IsolatedSandboxProvider,\n} from \"../SandboxProvider.js\";\nimport { BoundedTail, MAX_TAIL_CHARS } from \"../boundedTail.js\";\n\n/** Worktree path inside the Vercel sandbox. */\nconst VERCEL_REPO_PATH = \"/vercel/sandbox/workspace\";\n\n/**\n * Options for creating a Vercel sandbox provider.\n *\n * All `@vercel/sandbox` `Sandbox.create()` options are accepted as pass-through,\n * plus Sandcastle-specific options for auth and branch strategy.\n */\nexport interface VercelOptions {\n /**\n * Vercel access token.\n *\n * Falls back to the SDK's default auth behavior, which reads\n * `VERCEL_OIDC_TOKEN` (recommended for Vercel-hosted environments) or\n * `VERCEL_TOKEN` from the environment.\n */\n readonly token?: string;\n\n // ---- Pass-through @vercel/sandbox Sandbox.create() options ----\n\n /**\n * The source of the sandbox (git repo, tarball, or snapshot).\n * Omit to start an empty sandbox.\n */\n readonly source?:\n | {\n type: \"git\";\n url: string;\n depth?: number;\n revision?: string;\n username?: string;\n password?: string;\n }\n | {\n type: \"tarball\";\n url: string;\n }\n | {\n type: \"snapshot\";\n snapshotId: string;\n };\n\n /** Array of port numbers to expose from the sandbox (up to 4). */\n readonly ports?: number[];\n\n /** Timeout in milliseconds before the sandbox auto-terminates. */\n readonly timeout?: number;\n\n /**\n * Resources to allocate to the sandbox.\n * Each vCPU gets 2048 MB of memory.\n */\n readonly resources?: {\n vcpus: number;\n };\n\n /**\n * The runtime of the sandbox (e.g. `\"node24\"`, `\"node22\"`, `\"python3.13\"`).\n * Defaults to `\"node24\"`.\n */\n readonly runtime?: string;\n\n /**\n * Network policy for the sandbox.\n * Defaults to full internet access if not specified.\n */\n readonly networkPolicy?: Record<string, unknown>;\n\n /**\n * Vercel project ID to associate sandbox operations with.\n */\n readonly projectId?: string;\n\n /**\n * Vercel team ID to associate sandbox operations with.\n */\n readonly teamId?: string;\n\n /**\n * Timeout in milliseconds (alias for `timeout`, kept for discoverability).\n */\n readonly timeoutMs?: number;\n\n /**\n * Sandbox template shorthand (e.g. `\"node-22\"`).\n * Maps to the `runtime` option.\n */\n readonly template?: string;\n\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 /**\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\n/**\n * Create a Vercel isolated sandbox provider.\n *\n * The returned provider creates Vercel Firecracker microVM sandboxes via\n * the `@vercel/sandbox` SDK. Each sandbox is ephemeral — one sandbox per run.\n *\n * Requires `@vercel/sandbox` to be installed as a peer dependency.\n */\nexport const vercel = (options?: VercelOptions): IsolatedSandboxProvider =>\n createIsolatedSandboxProvider({\n name: \"vercel\",\n env: options?.env,\n create: async (createOptions): Promise<IsolatedSandboxHandle> => {\n const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;\n // Dynamic import so the peer dependency is only loaded at runtime\n const { Sandbox } = await import(\"@vercel/sandbox\");\n\n const createParams: Record<string, unknown> = {};\n\n // Pass through SDK options\n if (options?.source) createParams.source = options.source;\n if (options?.ports) createParams.ports = options.ports;\n if (options?.resources) createParams.resources = options.resources;\n if (options?.networkPolicy)\n createParams.networkPolicy = options.networkPolicy;\n // runtime takes precedence over the template convenience alias\n const resolvedRuntime = options?.runtime ?? options?.template;\n if (resolvedRuntime) createParams.runtime = resolvedRuntime;\n\n // Timeout: prefer explicit timeout, fall back to timeoutMs alias\n const timeoutValue = options?.timeout ?? options?.timeoutMs;\n if (timeoutValue !== undefined) createParams.timeout = timeoutValue;\n\n // Merge provider env with Sandcastle env\n createParams.env = createOptions.env;\n\n // Auth: pass token and team/project IDs if provided\n if (options?.token) createParams.token = options.token;\n if (options?.projectId) createParams.projectId = options.projectId;\n if (options?.teamId) createParams.teamId = options.teamId;\n\n const sandbox = await Sandbox.create(\n createParams as Parameters<typeof Sandbox.create>[0],\n );\n\n // Ensure worktree directory exists\n await sandbox.mkDir(VERCEL_REPO_PATH);\n\n const handle: IsolatedSandboxHandle = {\n worktreePath: VERCEL_REPO_PATH,\n\n exec: async (\n command: string,\n opts?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n },\n ): Promise<ExecResult> => {\n if (opts?.onLine) {\n const onLine = opts.onLine;\n const stdoutTail = new BoundedTail(maxOutputTailChars, \"\\n\");\n const stderrTail = new BoundedTail(maxOutputTailChars, \"\");\n let partial = \"\";\n\n const stdoutWritable = new Writable({\n write(chunk, _encoding, callback) {\n const text = partial + chunk.toString();\n const lines = text.split(\"\\n\");\n partial = lines.pop() ?? \"\";\n for (const line of lines) {\n stdoutTail.push(line);\n onLine(line);\n }\n callback();\n },\n final(callback) {\n if (partial) {\n stdoutTail.push(partial);\n onLine(partial);\n partial = \"\";\n }\n callback();\n },\n });\n\n const stderrWritable = new Writable({\n write(chunk, _encoding, callback) {\n stderrTail.push(chunk.toString());\n callback();\n },\n });\n\n const result = await sandbox.runCommand({\n cmd: \"sh\",\n args: [\"-c\", command],\n cwd: opts?.cwd ?? VERCEL_REPO_PATH,\n stdout: stdoutWritable,\n stderr: stderrWritable,\n ...(opts?.sudo ? { sudo: true } : {}),\n });\n\n return {\n stdout: stdoutTail.toString(),\n stderr: stderrTail.toString(),\n exitCode: result.exitCode,\n };\n }\n\n const result = await sandbox.runCommand({\n cmd: \"sh\",\n args: [\"-c\", command],\n cwd: opts?.cwd ?? VERCEL_REPO_PATH,\n ...(opts?.sudo ? { sudo: true } : {}),\n });\n\n const stdout = await result.stdout();\n const stderr = await result.stderr();\n\n return {\n stdout,\n stderr,\n exitCode: result.exitCode,\n };\n },\n\n copyIn: async (\n hostPath: string,\n sandboxPath: string,\n ): Promise<void> => {\n const info = await stat(hostPath);\n if (info.isDirectory()) {\n const tarPath = join(\n tmpdir(),\n `sandcastle-copyin-${Date.now()}.tar.gz`,\n );\n execSync(`tar -czf \"${tarPath}\" -C \"${hostPath}\" .`);\n try {\n const tarContent = await readFile(tarPath);\n const sandboxTarPath = `/tmp/sandcastle-copyin-${Date.now()}.tar.gz`;\n await sandbox.writeFiles([\n { path: sandboxTarPath, content: tarContent },\n ]);\n await sandbox.runCommand({\n cmd: \"sh\",\n args: [\n \"-c\",\n `mkdir -p \"${sandboxPath}\" && tar -xzf \"${sandboxTarPath}\" -C \"${sandboxPath}\" && rm -f \"${sandboxTarPath}\"`,\n ],\n });\n } finally {\n await unlink(tarPath).catch(() => {});\n }\n } else {\n const content = await readFile(hostPath);\n await sandbox.writeFiles([{ path: sandboxPath, content }]);\n }\n },\n\n copyFileOut: async (\n sandboxPath: string,\n hostPath: string,\n ): Promise<void> => {\n const buffer = await sandbox.readFileToBuffer({\n path: sandboxPath,\n });\n if (!buffer) {\n throw new Error(`File not found in Vercel sandbox: ${sandboxPath}`);\n }\n await mkdir(dirname(hostPath), { recursive: true });\n await writeFile(hostPath, buffer);\n },\n\n close: async (): Promise<void> => {\n await sandbox.stop();\n },\n };\n\n return handle;\n },\n });\n"]}
@@ -0,0 +1,14 @@
1
+ import { run, agent } from "@ai-hero/sandcastle";
2
+ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
3
+
4
+ // Blank template: customize this to build your own orchestration.
5
+ // Run this with: npx tsx .sandcastle/main.mts
6
+ // Or add to package.json scripts: "sandcastle": "npx tsx .sandcastle/main.mts"
7
+
8
+ await run({
9
+ // The agent provider is resolved at runtime by agent(): the AGENT env var
10
+ // (or this baked default) picks the provider, AGENT_MODEL picks the model.
11
+ agent: agent({ default: "claude-code" }),
12
+ sandbox: docker(),
13
+ promptFile: "./.sandcastle/prompt.md",
14
+ });