@snappedly-tools/shipyard 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -146
- package/dist/chunk-56TDSWFU.js +255 -0
- package/dist/chunk-56TDSWFU.js.map +1 -0
- package/dist/chunk-ACD46ZM4.js +136 -0
- package/dist/chunk-ACD46ZM4.js.map +1 -0
- package/dist/{chunk-2GT3OPO5.js → chunk-FDYOTN55.js} +196 -6
- package/dist/chunk-FDYOTN55.js.map +1 -0
- package/dist/chunk-SOJTAJTF.js +78 -0
- package/dist/chunk-SOJTAJTF.js.map +1 -0
- package/dist/chunk-WAXEMZUV.js +877 -0
- package/dist/chunk-WAXEMZUV.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -118
- package/dist/index.js.map +1 -1
- package/dist/main.js +1831 -143
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.js +3 -3
- package/dist/sandboxes/no-sandbox.js +2 -2
- package/dist/sandboxes/vercel.js +15 -35
- package/dist/sandboxes/vercel.js.map +1 -1
- package/dist/templates/parallel-planner/main.mts +11 -8
- package/dist/templates/parallel-planner-with-review/main.mts +23 -13
- package/package.json +3 -2
- package/dist/chunk-2GT3OPO5.js.map +0 -1
- package/dist/chunk-OCJKONZ7.js +0 -475
- package/dist/chunk-OCJKONZ7.js.map +0 -1
- package/dist/chunk-VDHZO5PP.js +0 -113
- package/dist/chunk-VDHZO5PP.js.map +0 -1
- package/dist/chunk-VW4MI5GJ.js +0 -126
- package/dist/chunk-VW4MI5GJ.js.map +0 -1
- package/dist/chunk-WA7QPAXO.js +0 -50
- package/dist/chunk-WA7QPAXO.js.map +0 -1
package/dist/chunk-OCJKONZ7.js
DELETED
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import { resolveUserMounts, assertIsolatedWorkspaceMounts, processFileMountParents, defaultImageName, registerShutdown, DockerError, formatVolumeMount } from './chunk-2GT3OPO5.js';
|
|
2
|
-
import { createIsolatedSandboxProvider } from './chunk-KMGNFXKN.js';
|
|
3
|
-
import { MAX_TAIL_CHARS, OutputByteCounter, BoundedTail } from './chunk-WA7QPAXO.js';
|
|
4
|
-
import { execFile, execFileSync, spawn } from 'child_process';
|
|
5
|
-
import { resolve, posix } from 'path';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import { randomUUID } from 'crypto';
|
|
8
|
-
import { createInterface } from 'readline';
|
|
9
|
-
import { Effect } from 'effect';
|
|
10
|
-
|
|
11
|
-
var exec = promisify(execFile);
|
|
12
|
-
var copyIntoContainer = async (engine, container, owner, hostPath, sandboxPath) => {
|
|
13
|
-
try {
|
|
14
|
-
await exec(engine, [
|
|
15
|
-
"exec",
|
|
16
|
-
container,
|
|
17
|
-
"mkdir",
|
|
18
|
-
"-p",
|
|
19
|
-
"--",
|
|
20
|
-
posix.dirname(sandboxPath)
|
|
21
|
-
]);
|
|
22
|
-
await exec(engine, ["cp", hostPath, `${container}:${sandboxPath}`]);
|
|
23
|
-
await exec(engine, [
|
|
24
|
-
"exec",
|
|
25
|
-
"--user",
|
|
26
|
-
"0:0",
|
|
27
|
-
container,
|
|
28
|
-
"chown",
|
|
29
|
-
"-hR",
|
|
30
|
-
"--",
|
|
31
|
-
owner,
|
|
32
|
-
sandboxPath
|
|
33
|
-
]);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`${engine} cp (in) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
var dockerExec = (args) => Effect.async((resume) => {
|
|
41
|
-
execFile(
|
|
42
|
-
"docker",
|
|
43
|
-
args,
|
|
44
|
-
{ maxBuffer: 10 * 1024 * 1024 },
|
|
45
|
-
(error, stdout, stderr) => {
|
|
46
|
-
if (error) {
|
|
47
|
-
resume(
|
|
48
|
-
Effect.fail(
|
|
49
|
-
new DockerError({
|
|
50
|
-
message: `docker ${args[0]} failed: ${stderr?.toString() || error.message}`
|
|
51
|
-
})
|
|
52
|
-
)
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
resume(Effect.succeed(stdout.toString()));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
var buildImage = (imageName, dockerfileDir, options) => Effect.gen(function* () {
|
|
61
|
-
const buildArgFlags = Object.entries(options?.buildArgs ?? {}).flatMap(
|
|
62
|
-
([k, v]) => ["--build-arg", `${k}=${v}`]
|
|
63
|
-
);
|
|
64
|
-
if (options?.dockerfile) {
|
|
65
|
-
yield* dockerExec([
|
|
66
|
-
"build",
|
|
67
|
-
"-t",
|
|
68
|
-
imageName,
|
|
69
|
-
...buildArgFlags,
|
|
70
|
-
"-f",
|
|
71
|
-
resolve(options.dockerfile),
|
|
72
|
-
process.cwd()
|
|
73
|
-
]);
|
|
74
|
-
} else {
|
|
75
|
-
yield* dockerExec([
|
|
76
|
-
"build",
|
|
77
|
-
"-t",
|
|
78
|
-
imageName,
|
|
79
|
-
...buildArgFlags,
|
|
80
|
-
resolve(dockerfileDir)
|
|
81
|
-
]);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
var startContainer = (containerName, imageName, env, options) => Effect.gen(function* () {
|
|
85
|
-
const existing = yield* dockerExec([
|
|
86
|
-
"ps",
|
|
87
|
-
"-a",
|
|
88
|
-
"--filter",
|
|
89
|
-
`name=^${containerName}$`,
|
|
90
|
-
"--format",
|
|
91
|
-
"{{.Names}}"
|
|
92
|
-
]);
|
|
93
|
-
if (existing.trim() === containerName) {
|
|
94
|
-
yield* Effect.fail(
|
|
95
|
-
new DockerError({
|
|
96
|
-
message: `Container '${containerName}' already exists. Run cleanup first.`
|
|
97
|
-
})
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
const envFlags = Object.entries(env).flatMap(([k, v]) => [
|
|
101
|
-
"-e",
|
|
102
|
-
`${k}=${v}`
|
|
103
|
-
]);
|
|
104
|
-
const selinuxLabel = options?.selinuxLabel ?? "z";
|
|
105
|
-
const volumeFlags = (options?.volumeMounts ?? []).flatMap((mount) => [
|
|
106
|
-
"-v",
|
|
107
|
-
formatVolumeMount(mount, selinuxLabel)
|
|
108
|
-
]);
|
|
109
|
-
const workdirFlags = options?.workdir ? ["-w", options.workdir] : [];
|
|
110
|
-
const userFlags = options?.user ? ["--user", options.user] : [];
|
|
111
|
-
const networks = options?.network ? Array.isArray(options.network) ? options.network : [options.network] : [];
|
|
112
|
-
const networkFlags = networks.flatMap((n) => ["--network", n]);
|
|
113
|
-
const groupAddFlags = (options?.groups ?? []).flatMap((g) => [
|
|
114
|
-
"--group-add",
|
|
115
|
-
String(g)
|
|
116
|
-
]);
|
|
117
|
-
const deviceFlags = (options?.devices ?? []).flatMap((d) => [
|
|
118
|
-
"--device",
|
|
119
|
-
d
|
|
120
|
-
]);
|
|
121
|
-
const cpusFlags = options?.cpus !== void 0 ? ["--cpus", String(options.cpus)] : [];
|
|
122
|
-
yield* dockerExec([
|
|
123
|
-
"run",
|
|
124
|
-
"-d",
|
|
125
|
-
"--init",
|
|
126
|
-
"--name",
|
|
127
|
-
containerName,
|
|
128
|
-
...envFlags,
|
|
129
|
-
...volumeFlags,
|
|
130
|
-
...workdirFlags,
|
|
131
|
-
...userFlags,
|
|
132
|
-
...networkFlags,
|
|
133
|
-
...groupAddFlags,
|
|
134
|
-
...deviceFlags,
|
|
135
|
-
...cpusFlags,
|
|
136
|
-
imageName
|
|
137
|
-
]);
|
|
138
|
-
});
|
|
139
|
-
var removeContainer = (containerName) => Effect.gen(function* () {
|
|
140
|
-
yield* Effect.ignore(dockerExec(["stop", containerName]));
|
|
141
|
-
yield* Effect.ignore(dockerExec(["rm", containerName]));
|
|
142
|
-
});
|
|
143
|
-
var removeImage = (imageName) => Effect.gen(function* () {
|
|
144
|
-
yield* dockerExec(["rmi", imageName]);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// src/sandboxes/docker.ts
|
|
148
|
-
var docker = (options) => {
|
|
149
|
-
const configuredImageName = options?.imageName;
|
|
150
|
-
const selinuxLabel = options?.selinuxLabel ?? "z";
|
|
151
|
-
const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
|
|
152
|
-
const sandboxHomedir = "/home/agent";
|
|
153
|
-
const userMounts = options?.mounts ? resolveUserMounts(options.mounts, sandboxHomedir) : [];
|
|
154
|
-
assertIsolatedWorkspaceMounts(userMounts);
|
|
155
|
-
const parentDirsToCreate = processFileMountParents(
|
|
156
|
-
userMounts,
|
|
157
|
-
sandboxHomedir
|
|
158
|
-
);
|
|
159
|
-
return createIsolatedSandboxProvider({
|
|
160
|
-
name: "docker",
|
|
161
|
-
env: options?.env,
|
|
162
|
-
create: async (createOptions) => {
|
|
163
|
-
const containerName = `shipyard-${randomUUID()}`;
|
|
164
|
-
const worktreePath = "/home/agent/workspace";
|
|
165
|
-
const allMounts = userMounts;
|
|
166
|
-
const volumeMounts = allMounts.map((m) => ({
|
|
167
|
-
hostPath: m.hostPath,
|
|
168
|
-
sandboxPath: m.sandboxPath,
|
|
169
|
-
readonly: m.readonly
|
|
170
|
-
}));
|
|
171
|
-
const imageName = configuredImageName ?? defaultImageName(createOptions.hostRepoPath ?? process.cwd());
|
|
172
|
-
const containerUid = options?.containerUid ?? process.getuid?.() ?? 1e3;
|
|
173
|
-
const containerGid = options?.containerGid ?? process.getgid?.() ?? 1e3;
|
|
174
|
-
await checkImageUid(imageName, containerUid);
|
|
175
|
-
await Effect.runPromise(
|
|
176
|
-
startContainer(
|
|
177
|
-
containerName,
|
|
178
|
-
imageName,
|
|
179
|
-
{
|
|
180
|
-
...createOptions.env,
|
|
181
|
-
HOME: "/home/agent"
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
volumeMounts,
|
|
185
|
-
workdir: worktreePath,
|
|
186
|
-
user: `${containerUid}:${containerGid}`,
|
|
187
|
-
network: options?.network,
|
|
188
|
-
groups: options?.groups,
|
|
189
|
-
devices: options?.devices,
|
|
190
|
-
cpus: options?.cpus,
|
|
191
|
-
selinuxLabel
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
);
|
|
195
|
-
for (const dir of parentDirsToCreate) {
|
|
196
|
-
await new Promise((resolve2, reject) => {
|
|
197
|
-
execFile(
|
|
198
|
-
"docker",
|
|
199
|
-
[
|
|
200
|
-
"exec",
|
|
201
|
-
"--user",
|
|
202
|
-
"0:0",
|
|
203
|
-
containerName,
|
|
204
|
-
"sh",
|
|
205
|
-
"-c",
|
|
206
|
-
`mkdir -p "$1" && chown "$2" "$1"`,
|
|
207
|
-
"sh",
|
|
208
|
-
dir,
|
|
209
|
-
`${containerUid}:${containerGid}`
|
|
210
|
-
],
|
|
211
|
-
(error) => {
|
|
212
|
-
if (error) {
|
|
213
|
-
reject(
|
|
214
|
-
new Error(
|
|
215
|
-
`Failed to create parent directory '${dir}' in container: ${error.message}`
|
|
216
|
-
)
|
|
217
|
-
);
|
|
218
|
-
} else {
|
|
219
|
-
resolve2();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
const removeContainerSync = () => {
|
|
226
|
-
try {
|
|
227
|
-
execFileSync("docker", ["rm", "-f", containerName], {
|
|
228
|
-
stdio: "ignore"
|
|
229
|
-
});
|
|
230
|
-
} catch {
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
const unregisterShutdown = registerShutdown(removeContainerSync);
|
|
234
|
-
const handle = {
|
|
235
|
-
worktreePath,
|
|
236
|
-
exec: (command, opts) => {
|
|
237
|
-
const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
|
|
238
|
-
const args = ["exec"];
|
|
239
|
-
if (opts?.stdin !== void 0) args.push("-i");
|
|
240
|
-
if (opts?.cwd) args.push("-w", opts.cwd);
|
|
241
|
-
args.push(containerName, "sh", "-c", effectiveCommand);
|
|
242
|
-
return new Promise((resolve2, reject) => {
|
|
243
|
-
const proc = spawn("docker", args, {
|
|
244
|
-
signal: opts?.signal,
|
|
245
|
-
stdio: [
|
|
246
|
-
opts?.stdin !== void 0 ? "pipe" : "ignore",
|
|
247
|
-
"pipe",
|
|
248
|
-
"pipe"
|
|
249
|
-
]
|
|
250
|
-
});
|
|
251
|
-
if (opts?.stdin !== void 0) {
|
|
252
|
-
proc.stdin.write(opts.stdin);
|
|
253
|
-
proc.stdin.end();
|
|
254
|
-
}
|
|
255
|
-
proc.on("error", (error) => {
|
|
256
|
-
reject(new Error(`docker exec failed: ${error.message}`));
|
|
257
|
-
});
|
|
258
|
-
if (opts?.onLine || opts?.maxOutputBytes !== void 0) {
|
|
259
|
-
const onLine = opts?.onLine ?? (() => {
|
|
260
|
-
});
|
|
261
|
-
const outputLimit = opts?.maxOutputBytes === void 0 ? void 0 : new OutputByteCounter(opts.maxOutputBytes);
|
|
262
|
-
const tailChars = opts?.maxOutputBytes ?? maxOutputTailChars;
|
|
263
|
-
const stdoutTail = new BoundedTail(tailChars, "\n");
|
|
264
|
-
const stderrTail = new BoundedTail(tailChars, "");
|
|
265
|
-
let limitError;
|
|
266
|
-
const checkOutputLimit = (chunk) => {
|
|
267
|
-
if (outputLimit === void 0 || limitError !== void 0) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
outputLimit.add(chunk);
|
|
271
|
-
if (outputLimit.exceeded) {
|
|
272
|
-
limitError = new Error(
|
|
273
|
-
`Sandbox command output exceeded ${opts.maxOutputBytes} bytes`
|
|
274
|
-
);
|
|
275
|
-
proc.kill("SIGKILL");
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
proc.stdout.on("data", checkOutputLimit);
|
|
279
|
-
proc.stderr.on("data", checkOutputLimit);
|
|
280
|
-
const rl = createInterface({ input: proc.stdout });
|
|
281
|
-
rl.on("line", (line) => {
|
|
282
|
-
stdoutTail.push(line);
|
|
283
|
-
onLine(line);
|
|
284
|
-
});
|
|
285
|
-
proc.stderr.on("data", (chunk) => {
|
|
286
|
-
stderrTail.push(chunk.toString());
|
|
287
|
-
});
|
|
288
|
-
proc.on("close", (code) => {
|
|
289
|
-
if (limitError !== void 0) {
|
|
290
|
-
reject(limitError);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
resolve2({
|
|
294
|
-
stdout: stdoutTail.toString(),
|
|
295
|
-
stderr: stderrTail.toString(),
|
|
296
|
-
exitCode: code ?? 0
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
} else {
|
|
300
|
-
const stdoutChunks = [];
|
|
301
|
-
const stderrChunks = [];
|
|
302
|
-
proc.stdout.on("data", (chunk) => {
|
|
303
|
-
stdoutChunks.push(chunk.toString());
|
|
304
|
-
});
|
|
305
|
-
proc.stderr.on("data", (chunk) => {
|
|
306
|
-
stderrChunks.push(chunk.toString());
|
|
307
|
-
});
|
|
308
|
-
proc.on("close", (code) => {
|
|
309
|
-
resolve2({
|
|
310
|
-
stdout: stdoutChunks.join(""),
|
|
311
|
-
stderr: stderrChunks.join(""),
|
|
312
|
-
exitCode: code ?? 0
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
},
|
|
318
|
-
interactiveExec: (args, opts) => {
|
|
319
|
-
return new Promise((resolve2, reject) => {
|
|
320
|
-
opts.signal?.throwIfAborted();
|
|
321
|
-
const executionId = randomUUID();
|
|
322
|
-
const pidFile = `/tmp/shipyard-interactive-${executionId}.pid`;
|
|
323
|
-
const cancelFile = `/tmp/shipyard-interactive-${executionId}.cancel`;
|
|
324
|
-
const dockerArgs = ["exec"];
|
|
325
|
-
if ("isTTY" in opts.stdin && opts.stdin.isTTY) {
|
|
326
|
-
dockerArgs.push("-it");
|
|
327
|
-
} else {
|
|
328
|
-
dockerArgs.push("-i");
|
|
329
|
-
}
|
|
330
|
-
if (opts.cwd) dockerArgs.push("-w", opts.cwd);
|
|
331
|
-
dockerArgs.push(
|
|
332
|
-
containerName,
|
|
333
|
-
"sh",
|
|
334
|
-
"-c",
|
|
335
|
-
'pid_file=$1; cancel_file=$2; shift 2; command -v setsid >/dev/null 2>&1 && command -v ps >/dev/null 2>&1 && command -v awk >/dev/null 2>&1 || { printf "%s\\n" "setsid, ps, and awk are required for cancellable interactive execution" >&2; exit 127; }; if [ -e "$cancel_file" ]; then rm -f "$pid_file" "$cancel_file"; exit 130; fi; setsid "$@" & child=$!; printf "%s\\n" "$child" > "$pid_file"; wait "$child"; status=$?; rm -f "$pid_file" "$cancel_file"; exit "$status"',
|
|
336
|
-
"shipyard-interactive",
|
|
337
|
-
pidFile,
|
|
338
|
-
cancelFile,
|
|
339
|
-
...args
|
|
340
|
-
);
|
|
341
|
-
const proc = spawn("docker", dockerArgs, {
|
|
342
|
-
stdio: [opts.stdin, opts.stdout, opts.stderr]
|
|
343
|
-
});
|
|
344
|
-
let settled = false;
|
|
345
|
-
let cancelling = false;
|
|
346
|
-
const finish = (result) => {
|
|
347
|
-
if (settled) return;
|
|
348
|
-
settled = true;
|
|
349
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
350
|
-
if ("error" in result) reject(result.error);
|
|
351
|
-
else resolve2(result);
|
|
352
|
-
};
|
|
353
|
-
const onAbort = () => {
|
|
354
|
-
if (cancelling || settled) return;
|
|
355
|
-
cancelling = true;
|
|
356
|
-
const cancellationScript = `touch "$1"; attempts=0; while [ ! -s "$2" ] && [ "$attempts" -lt 50 ]; do sleep 0.1; attempts=$((attempts + 1)); done; if [ -s "$2" ]; then pid=$(cat "$2"); group_running() { ps -eo pgid=,stat= | awk -v pg="$pid" '$1 == pg && $2 !~ /^Z/ { found=1 } END { exit found ? 0 : 1 }'; }; kill -TERM "-$pid" 2>/dev/null || true; attempts=0; while group_running && [ "$attempts" -lt 20 ]; do sleep 0.1; attempts=$((attempts + 1)); done; kill -KILL "-$pid" 2>/dev/null || true; attempts=0; while group_running && [ "$attempts" -lt 20 ]; do sleep 0.1; attempts=$((attempts + 1)); done; if group_running; then exit 1; fi; rm -f "$1" "$2"; fi`;
|
|
357
|
-
execFile(
|
|
358
|
-
"docker",
|
|
359
|
-
[
|
|
360
|
-
"exec",
|
|
361
|
-
containerName,
|
|
362
|
-
"sh",
|
|
363
|
-
"-c",
|
|
364
|
-
cancellationScript,
|
|
365
|
-
"shipyard-cancel",
|
|
366
|
-
cancelFile,
|
|
367
|
-
pidFile
|
|
368
|
-
],
|
|
369
|
-
{ timeout: 7e3 },
|
|
370
|
-
(error) => {
|
|
371
|
-
proc.kill("SIGKILL");
|
|
372
|
-
if (error) {
|
|
373
|
-
finish({
|
|
374
|
-
error: new Error(
|
|
375
|
-
`Failed to terminate interactive process in Docker sandbox: ${error.message}`
|
|
376
|
-
)
|
|
377
|
-
});
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
finish({
|
|
381
|
-
error: opts.signal?.reason ?? new Error("Interactive execution aborted")
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
};
|
|
386
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
387
|
-
if (opts.signal?.aborted) onAbort();
|
|
388
|
-
proc.on("error", (error) => {
|
|
389
|
-
if (!cancelling) {
|
|
390
|
-
finish({
|
|
391
|
-
error: new Error(`docker exec failed: ${error.message}`)
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
proc.on("close", (code) => {
|
|
396
|
-
if (cancelling) return;
|
|
397
|
-
execFile(
|
|
398
|
-
"docker",
|
|
399
|
-
["exec", containerName, "rm", "-f", pidFile, cancelFile],
|
|
400
|
-
() => {
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
finish({ exitCode: code ?? 0 });
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
},
|
|
407
|
-
copyIn: (hostPath, sandboxPath) => copyIntoContainer(
|
|
408
|
-
"docker",
|
|
409
|
-
containerName,
|
|
410
|
-
`${containerUid}:${containerGid}`,
|
|
411
|
-
hostPath,
|
|
412
|
-
sandboxPath
|
|
413
|
-
),
|
|
414
|
-
copyFileOut: (sandboxPath, hostPath) => new Promise((resolve2, reject) => {
|
|
415
|
-
execFile(
|
|
416
|
-
"docker",
|
|
417
|
-
["cp", `${containerName}:${sandboxPath}`, hostPath],
|
|
418
|
-
(error) => {
|
|
419
|
-
if (error) {
|
|
420
|
-
reject(new Error(`docker cp (out) failed: ${error.message}`));
|
|
421
|
-
} else {
|
|
422
|
-
resolve2();
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
);
|
|
426
|
-
}),
|
|
427
|
-
close: async () => {
|
|
428
|
-
unregisterShutdown();
|
|
429
|
-
await Effect.runPromise(removeContainer(containerName));
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
return handle;
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
};
|
|
436
|
-
var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) => {
|
|
437
|
-
execFile(
|
|
438
|
-
"docker",
|
|
439
|
-
["image", "inspect", imageName, "--format", "{{.Config.User}}"],
|
|
440
|
-
(error, stdout) => {
|
|
441
|
-
if (error) {
|
|
442
|
-
reject(
|
|
443
|
-
new Error(
|
|
444
|
-
`Image '${imageName}' not found locally. Build it first with 'shipyard docker build-image'.`
|
|
445
|
-
)
|
|
446
|
-
);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
const imageUser = (stdout ?? "").toString().trim();
|
|
450
|
-
if (!imageUser) {
|
|
451
|
-
resolve2();
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const uidPart = imageUser.split(":")[0];
|
|
455
|
-
const imageUid = parseInt(uidPart, 10);
|
|
456
|
-
if (isNaN(imageUid)) {
|
|
457
|
-
resolve2();
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
if (imageUid !== expectedUid) {
|
|
461
|
-
reject(
|
|
462
|
-
new Error(
|
|
463
|
-
`UID mismatch: image '${imageName}' was built with UID ${imageUid}, but the expected UID is ${expectedUid}. Rebuild the image with 'shipyard docker build-image', or pass containerUid: ${imageUid} to docker() to match the image.`
|
|
464
|
-
)
|
|
465
|
-
);
|
|
466
|
-
} else {
|
|
467
|
-
resolve2();
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
export { buildImage, docker, removeImage };
|
|
474
|
-
//# sourceMappingURL=chunk-OCJKONZ7.js.map
|
|
475
|
-
//# sourceMappingURL=chunk-OCJKONZ7.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sandboxes/copyIntoContainer.ts","../src/DockerLifecycle.ts","../src/sandboxes/docker.ts"],"names":["execFile","Effect","resolve"],"mappings":"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","file":"chunk-OCJKONZ7.js","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { posix } from \"node:path\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\n/** Container cp creates root-owned files; session files may have mode 0600. */\nexport const copyIntoContainer = async (\n engine: \"docker\",\n container: string,\n owner: string,\n hostPath: string,\n sandboxPath: string,\n): Promise<void> => {\n try {\n await exec(engine, [\n \"exec\",\n container,\n \"mkdir\",\n \"-p\",\n \"--\",\n posix.dirname(sandboxPath),\n ]);\n await exec(engine, [\"cp\", hostPath, `${container}:${sandboxPath}`]);\n await exec(engine, [\n \"exec\",\n \"--user\",\n \"0:0\",\n container,\n \"chown\",\n \"-hR\",\n \"--\",\n owner,\n sandboxPath,\n ]);\n } catch (error) {\n throw new Error(\n `${engine} cp (in) failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n};\n","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 shipyard 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 .shipyard/ 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 \"--init\",\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","import { copyIntoContainer } from \"./copyIntoContainer.js\";\n/**\n * Docker sandbox provider — wraps DockerLifecycle into a SandboxProvider.\n *\n * Usage:\n * import { docker } from \"@snappedly-tools/shipyard/sandboxes/docker\";\n * await run({ agent: codex(CODEX_MODELS.routine), 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 createIsolatedSandboxProvider,\n type IsolatedSandboxProvider,\n type IsolatedCreateOptions,\n type IsolatedSandboxHandle,\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 assertIsolatedWorkspaceMounts,\n processFileMountParents,\n} from \"../mountUtils.js\";\nimport {\n BoundedTail,\n MAX_TAIL_CHARS,\n OutputByteCounter,\n} 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 isolated repository and Git storage.\n * Changes are transferred through git bundle and validated sync-out.\n */\nexport const docker = (options?: DockerOptions): IsolatedSandboxProvider => {\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 assertIsolatedWorkspaceMounts(userMounts);\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 createIsolatedSandboxProvider({\n name: \"docker\",\n env: options?.env,\n create: async (\n createOptions: IsolatedCreateOptions,\n ): Promise<IsolatedSandboxHandle> => {\n const containerName = `shipyard-${randomUUID()}`;\n\n const worktreePath = \"/home/agent/workspace\";\n\n // Repository and Git storage are container-owned. Only explicitly\n // configured mounts cross the host boundary.\n const allMounts = 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 ??\n defaultImageName(createOptions.hostRepoPath ?? process.cwd());\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: IsolatedSandboxHandle = {\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 signal?: AbortSignal;\n maxOutputBytes?: number;\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 signal: opts?.signal,\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 || opts?.maxOutputBytes !== undefined) {\n const onLine = opts?.onLine ?? (() => {});\n const outputLimit =\n opts?.maxOutputBytes === undefined\n ? undefined\n : new OutputByteCounter(opts.maxOutputBytes);\n const tailChars = opts?.maxOutputBytes ?? maxOutputTailChars;\n const stdoutTail = new BoundedTail(tailChars, \"\\n\");\n const stderrTail = new BoundedTail(tailChars, \"\");\n let limitError: Error | undefined;\n const checkOutputLimit = (chunk: Buffer): void => {\n if (outputLimit === undefined || limitError !== undefined) {\n return;\n }\n outputLimit.add(chunk);\n if (outputLimit.exceeded) {\n limitError = new Error(\n `Sandbox command output exceeded ${opts.maxOutputBytes} bytes`,\n );\n proc.kill(\"SIGKILL\");\n }\n };\n proc.stdout!.on(\"data\", checkOutputLimit);\n proc.stderr!.on(\"data\", checkOutputLimit);\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 if (limitError !== undefined) {\n reject(limitError);\n return;\n }\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 opts.signal?.throwIfAborted();\n const executionId = randomUUID();\n const pidFile = `/tmp/shipyard-interactive-${executionId}.pid`;\n const cancelFile = `/tmp/shipyard-interactive-${executionId}.cancel`;\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(\n containerName,\n \"sh\",\n \"-c\",\n 'pid_file=$1; cancel_file=$2; shift 2; command -v setsid >/dev/null 2>&1 && command -v ps >/dev/null 2>&1 && command -v awk >/dev/null 2>&1 || { printf \"%s\\\\n\" \"setsid, ps, and awk are required for cancellable interactive execution\" >&2; exit 127; }; if [ -e \"$cancel_file\" ]; then rm -f \"$pid_file\" \"$cancel_file\"; exit 130; fi; setsid \"$@\" & child=$!; printf \"%s\\\\n\" \"$child\" > \"$pid_file\"; wait \"$child\"; status=$?; rm -f \"$pid_file\" \"$cancel_file\"; exit \"$status\"',\n \"shipyard-interactive\",\n pidFile,\n cancelFile,\n ...args,\n );\n\n const proc = spawn(\"docker\", dockerArgs, {\n stdio: [opts.stdin, opts.stdout, opts.stderr] as StdioOptions,\n });\n\n let settled = false;\n let cancelling = false;\n const finish = (\n result:\n | { readonly exitCode: number }\n | { readonly error: unknown },\n ): void => {\n if (settled) return;\n settled = true;\n opts.signal?.removeEventListener(\"abort\", onAbort);\n if (\"error\" in result) reject(result.error);\n else resolve(result);\n };\n\n const onAbort = (): void => {\n if (cancelling || settled) return;\n cancelling = true;\n const cancellationScript =\n 'touch \"$1\"; attempts=0; while [ ! -s \"$2\" ] && [ \"$attempts\" -lt 50 ]; do sleep 0.1; attempts=$((attempts + 1)); done; if [ -s \"$2\" ]; then pid=$(cat \"$2\"); group_running() { ps -eo pgid=,stat= | awk -v pg=\"$pid\" \\'$1 == pg && $2 !~ /^Z/ { found=1 } END { exit found ? 0 : 1 }\\'; }; kill -TERM \"-$pid\" 2>/dev/null || true; attempts=0; while group_running && [ \"$attempts\" -lt 20 ]; do sleep 0.1; attempts=$((attempts + 1)); done; kill -KILL \"-$pid\" 2>/dev/null || true; attempts=0; while group_running && [ \"$attempts\" -lt 20 ]; do sleep 0.1; attempts=$((attempts + 1)); done; if group_running; then exit 1; fi; rm -f \"$1\" \"$2\"; fi';\n execFile(\n \"docker\",\n [\n \"exec\",\n containerName,\n \"sh\",\n \"-c\",\n cancellationScript,\n \"shipyard-cancel\",\n cancelFile,\n pidFile,\n ],\n { timeout: 7_000 },\n (error) => {\n proc.kill(\"SIGKILL\");\n if (error) {\n finish({\n error: new Error(\n `Failed to terminate interactive process in Docker sandbox: ${error.message}`,\n ),\n });\n return;\n }\n finish({\n error:\n opts.signal?.reason ??\n new Error(\"Interactive execution aborted\"),\n });\n },\n );\n };\n\n opts.signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (opts.signal?.aborted) onAbort();\n\n proc.on(\"error\", (error: Error) => {\n if (!cancelling) {\n finish({\n error: new Error(`docker exec failed: ${error.message}`),\n });\n }\n });\n\n proc.on(\"close\", (code: number | null) => {\n if (cancelling) return;\n execFile(\n \"docker\",\n [\"exec\", containerName, \"rm\", \"-f\", pidFile, cancelFile],\n () => {},\n );\n finish({ exitCode: code ?? 0 });\n });\n });\n },\n\n copyIn: (hostPath, sandboxPath) =>\n copyIntoContainer(\n \"docker\",\n containerName,\n `${containerUid}:${containerGid}`,\n hostPath,\n sandboxPath,\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 'shipyard 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 'shipyard 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"]}
|