@snappedly-tools/shipyard 0.7.0 → 0.9.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 +46 -43
- package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
- package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
- package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
- package/dist/chunk-57EEKW3R.js.map +1 -0
- package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
- package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
- package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
- package/dist/chunk-JZUBT4WG.js.map +1 -0
- package/dist/chunk-NQRFVKCU.js +1840 -0
- package/dist/chunk-NQRFVKCU.js.map +1 -0
- package/dist/chunk-Z5C4LHVP.js +137 -0
- package/dist/chunk-Z5C4LHVP.js.map +1 -0
- package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
- package/dist/index.d.ts +99 -2606
- package/dist/index.js +412 -7487
- package/dist/index.js.map +1 -1
- package/dist/integrations/github.d.ts +52 -0
- package/dist/integrations/github.js +1049 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/releases.d.ts +136 -0
- package/dist/integrations/releases.js +500 -0
- package/dist/integrations/releases.js.map +1 -0
- package/dist/main.js +826 -472
- package/dist/main.js.map +1 -1
- package/dist/publication-BPoy_M9M.d.ts +1200 -0
- package/dist/sandboxes/docker.d.ts +2 -3
- package/dist/sandboxes/docker.js +2 -4
- package/dist/templates/parallel-planner/main.mts +78 -16
- package/dist/templates/parallel-planner/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner/setup.sh +1 -0
- package/dist/templates/parallel-planner-with-review/main.mts +81 -19
- package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
- package/dist/templates/sequential-reviewer/main.mts +59 -6
- package/dist/templates/sequential-reviewer/setup.sh +1 -0
- package/dist/templates/shared/setup.sh +1 -0
- package/dist/templates/simple-loop/main.mts +58 -5
- package/dist/templates/simple-loop/setup.sh +1 -0
- package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
- package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
- package/dist/workflow.d.ts +472 -0
- package/dist/workflow.js +4345 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +11 -15
- package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
- package/dist/chunk-ACD46ZM4.js +0 -136
- package/dist/chunk-ACD46ZM4.js.map +0 -1
- package/dist/chunk-FDYOTN55.js.map +0 -1
- package/dist/chunk-KMGNFXKN.js +0 -38
- package/dist/chunk-KMGNFXKN.js.map +0 -1
- package/dist/chunk-SOJTAJTF.js +0 -78
- package/dist/chunk-SOJTAJTF.js.map +0 -1
- package/dist/chunk-WAXEMZUV.js.map +0 -1
- package/dist/sandboxes/no-sandbox.d.ts +0 -37
- package/dist/sandboxes/no-sandbox.js +0 -4
- package/dist/sandboxes/no-sandbox.js.map +0 -1
- package/dist/sandboxes/vercel.d.ts +0 -104
- package/dist/sandboxes/vercel.js +0 -166
- package/dist/sandboxes/vercel.js.map +0 -1
- package/dist/templates/blank/main.mts +0 -13
- package/dist/templates/blank/prompt.md +0 -12
- package/dist/templates/blank/template.json +0 -4
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { mkdtemp, writeFile, chmod, mkdir, stat, copyFile, rm, readdir, lstat, readFile, open } from 'fs/promises';
|
|
1
|
+
import { existsSync, constants } from 'fs';
|
|
2
|
+
import { writeFile, chmod, mkdir, stat, copyFile, rm, readdir, mkdtemp, lstat, readFile, open } from 'fs/promises';
|
|
4
3
|
import * as clack from '@clack/prompts';
|
|
5
4
|
import { FileSystem } from '@effect/platform';
|
|
6
|
-
import { join, basename,
|
|
5
|
+
import { posix, join, basename, dirname, resolve, relative, sep, isAbsolute, normalize } from 'path';
|
|
7
6
|
import { Context, Layer, Effect, Data, Exit, Duration, Option, Ref, Deferred, Schedule } from 'effect';
|
|
8
7
|
import { styleText, promisify } from 'util';
|
|
9
|
-
import { tmpdir, homedir } from 'os';
|
|
10
8
|
import { randomBytes } from 'crypto';
|
|
11
9
|
import { exec, execFile } from 'child_process';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
12
11
|
|
|
12
|
+
// src/Display.ts
|
|
13
13
|
var Display = class extends Context.Tag("Display")() {
|
|
14
14
|
};
|
|
15
15
|
var SilentDisplay = {
|
|
@@ -31,12 +31,21 @@ var SilentDisplay = {
|
|
|
31
31
|
),
|
|
32
32
|
progress: (title, effect) => Effect.gen(function* () {
|
|
33
33
|
const updates = [];
|
|
34
|
-
const
|
|
34
|
+
const report = Object.assign(
|
|
35
|
+
(update) => {
|
|
36
|
+
updates.push(update);
|
|
37
|
+
},
|
|
38
|
+
{ pause: () => {
|
|
39
|
+
}, resume: () => {
|
|
40
|
+
} }
|
|
41
|
+
);
|
|
42
|
+
const exit = yield* Effect.exit(effect(report));
|
|
35
43
|
yield* Ref.update(ref, (entries) => [
|
|
36
44
|
...entries,
|
|
37
45
|
{ _tag: "progress", title, updates: [...updates] }
|
|
38
46
|
]);
|
|
39
|
-
return
|
|
47
|
+
if (Exit.isFailure(exit)) return yield* Effect.failCause(exit.cause);
|
|
48
|
+
return exit.value;
|
|
40
49
|
}),
|
|
41
50
|
summary: (title, rows) => Ref.update(ref, (entries) => [
|
|
42
51
|
...entries,
|
|
@@ -140,7 +149,15 @@ var FileDisplay = {
|
|
|
140
149
|
yield* appendToLog(`${title}...`);
|
|
141
150
|
const start = Date.now();
|
|
142
151
|
const updates = [];
|
|
143
|
-
const
|
|
152
|
+
const report = Object.assign(
|
|
153
|
+
(update) => {
|
|
154
|
+
updates.push(update);
|
|
155
|
+
},
|
|
156
|
+
{ pause: () => {
|
|
157
|
+
}, resume: () => {
|
|
158
|
+
} }
|
|
159
|
+
);
|
|
160
|
+
const result = yield* effect(report);
|
|
144
161
|
for (const update of updates) {
|
|
145
162
|
yield* appendToLog(
|
|
146
163
|
` ${update.message} (${update.current}/${update.total})`
|
|
@@ -211,25 +228,39 @@ var ClackDisplay = {
|
|
|
211
228
|
),
|
|
212
229
|
progress: (title, effect) => Effect.acquireUseRelease(
|
|
213
230
|
Effect.sync(() => {
|
|
214
|
-
const progress2 = clack.progress({ max: 100 });
|
|
231
|
+
const progress2 = clack.progress({ max: 100, withGuide: false });
|
|
215
232
|
let percent = 0;
|
|
233
|
+
let currentMessage = title;
|
|
234
|
+
let paused = false;
|
|
216
235
|
progress2.start(title);
|
|
217
|
-
const report = (update) => {
|
|
236
|
+
const report = ((update) => {
|
|
218
237
|
const total = Math.max(1, update.total);
|
|
219
238
|
const current = Math.min(total, Math.max(0, update.current));
|
|
220
239
|
const nextPercent = Math.round(current / total * 100);
|
|
221
240
|
const message = `${update.message} (${current}/${total})`;
|
|
241
|
+
currentMessage = message;
|
|
222
242
|
if (nextPercent > percent) {
|
|
223
243
|
progress2.advance(nextPercent - percent, message);
|
|
224
244
|
} else {
|
|
225
245
|
progress2.message(message);
|
|
226
246
|
}
|
|
227
247
|
percent = nextPercent;
|
|
248
|
+
});
|
|
249
|
+
report.pause = () => {
|
|
250
|
+
if (paused) return;
|
|
251
|
+
progress2.clear();
|
|
252
|
+
paused = true;
|
|
253
|
+
};
|
|
254
|
+
report.resume = () => {
|
|
255
|
+
if (!paused) return;
|
|
256
|
+
progress2.start(currentMessage);
|
|
257
|
+
paused = false;
|
|
228
258
|
};
|
|
229
259
|
return { progress: progress2, report };
|
|
230
260
|
}),
|
|
231
261
|
({ report }) => effect(report),
|
|
232
|
-
({ progress: progress2 }, exit) => Effect.sync(() => {
|
|
262
|
+
({ progress: progress2, report }, exit) => Effect.sync(() => {
|
|
263
|
+
report.resume();
|
|
233
264
|
if (exit._tag === "Success") {
|
|
234
265
|
progress2.stop(`${title} complete`);
|
|
235
266
|
} else {
|
|
@@ -259,6 +290,67 @@ var ClackDisplay = {
|
|
|
259
290
|
)
|
|
260
291
|
})
|
|
261
292
|
};
|
|
293
|
+
|
|
294
|
+
// src/SandboxProvider.ts
|
|
295
|
+
var createIsolatedSandboxProvider = (config) => ({
|
|
296
|
+
tag: "isolated",
|
|
297
|
+
name: config.name,
|
|
298
|
+
env: config.env ?? {},
|
|
299
|
+
create: config.create
|
|
300
|
+
});
|
|
301
|
+
var toSessionTransferHandle = (handle) => handle === void 0 ? void 0 : "copyFileIn" in handle ? handle : {
|
|
302
|
+
worktreePath: handle.worktreePath,
|
|
303
|
+
exec: (command, options) => handle.exec(command, options),
|
|
304
|
+
interactiveExec: handle.interactiveExec?.bind(handle),
|
|
305
|
+
copyFileIn: (source, destination) => handle.copyIn(source, destination),
|
|
306
|
+
copyFileOut: (source, destination) => handle.copyFileOut(source, destination),
|
|
307
|
+
close: () => handle.close()
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/shutdownRegistry.ts
|
|
311
|
+
var teardownCallbacks = /* @__PURE__ */ new Set();
|
|
312
|
+
var listenersInstalled = false;
|
|
313
|
+
var runTeardowns = () => {
|
|
314
|
+
for (const teardown of teardownCallbacks) {
|
|
315
|
+
try {
|
|
316
|
+
teardown();
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
var handleExit = () => {
|
|
322
|
+
runTeardowns();
|
|
323
|
+
};
|
|
324
|
+
var handleSignal = () => {
|
|
325
|
+
detachListeners();
|
|
326
|
+
runTeardowns();
|
|
327
|
+
process.exit(1);
|
|
328
|
+
};
|
|
329
|
+
var attachListeners = () => {
|
|
330
|
+
if (listenersInstalled) return;
|
|
331
|
+
listenersInstalled = true;
|
|
332
|
+
process.on("exit", handleExit);
|
|
333
|
+
process.on("SIGINT", handleSignal);
|
|
334
|
+
process.on("SIGTERM", handleSignal);
|
|
335
|
+
};
|
|
336
|
+
var detachListeners = () => {
|
|
337
|
+
if (!listenersInstalled) return;
|
|
338
|
+
listenersInstalled = false;
|
|
339
|
+
process.removeListener("exit", handleExit);
|
|
340
|
+
process.removeListener("SIGINT", handleSignal);
|
|
341
|
+
process.removeListener("SIGTERM", handleSignal);
|
|
342
|
+
};
|
|
343
|
+
var registerShutdown = (teardown) => {
|
|
344
|
+
teardownCallbacks.add(teardown);
|
|
345
|
+
attachListeners();
|
|
346
|
+
let active = true;
|
|
347
|
+
return () => {
|
|
348
|
+
if (!active) return;
|
|
349
|
+
active = false;
|
|
350
|
+
teardownCallbacks.delete(teardown);
|
|
351
|
+
if (teardownCallbacks.size === 0) detachListeners();
|
|
352
|
+
};
|
|
353
|
+
};
|
|
262
354
|
var CwdError = class extends Data.TaggedError("CwdError") {
|
|
263
355
|
};
|
|
264
356
|
var resolveCwd = (cwd) => Effect.gen(function* () {
|
|
@@ -280,8 +372,6 @@ var resolveCwd = (cwd) => Effect.gen(function* () {
|
|
|
280
372
|
}
|
|
281
373
|
return resolved;
|
|
282
374
|
});
|
|
283
|
-
|
|
284
|
-
// src/errors.ts
|
|
285
375
|
var ExecError = class extends Data.TaggedError("ExecError") {
|
|
286
376
|
};
|
|
287
377
|
var ExecHostError = class extends Data.TaggedError("ExecHostError") {
|
|
@@ -364,10 +454,7 @@ var WORKTREES_DIR = "worktrees";
|
|
|
364
454
|
var PATCHES_DIR = "patches";
|
|
365
455
|
var LOCKS_DIR = "locks";
|
|
366
456
|
var RUNNER_DIR = "runner";
|
|
367
|
-
var RUNNER_SANDBOX_MASK_DIR = "runner-sandbox-mask";
|
|
368
|
-
var PARENT_GIT_SANDBOX_DIR = "/.shipyard-parent-git";
|
|
369
457
|
var SYNC_BASE_REF = "refs/shipyard/sync-base";
|
|
370
|
-
var CONTAINER_NAME_PREFIX = "shipyard-";
|
|
371
458
|
var WINDOWS_DRIVE_PATH = /^[A-Za-z]:/;
|
|
372
459
|
var assertSafeRelativePath = (relativePath, label = "relative path") => {
|
|
373
460
|
if (relativePath.length === 0 || relativePath.includes("\0") || isAbsolute(relativePath) || WINDOWS_DRIVE_PATH.test(relativePath) || relativePath.startsWith("\\\\") || relativePath.split(/[\\/]/).some((part) => part === "..")) {
|
|
@@ -440,7 +527,7 @@ var SAFE_CONFIG = [
|
|
|
440
527
|
["checkout.recurseSubmodules", "false"]
|
|
441
528
|
];
|
|
442
529
|
var COMMAND_CONFIG_PATTERN = "^(filter\\..*\\.(clean|smudge|process|required)|merge\\..*\\.driver|diff\\..*\\.(command|textconv)|difftool\\..*\\.cmd|mergetool\\..*\\.cmd)$";
|
|
443
|
-
var runGit = (args, cwd, options = {}) => new Promise((
|
|
530
|
+
var runGit = (args, cwd, options = {}) => new Promise((resolve4, reject) => {
|
|
444
531
|
execFile(
|
|
445
532
|
"git",
|
|
446
533
|
[...args],
|
|
@@ -458,11 +545,11 @@ var runGit = (args, cwd, options = {}) => new Promise((resolve6, reject) => {
|
|
|
458
545
|
},
|
|
459
546
|
(error, stdout, stderr) => {
|
|
460
547
|
if (!error) {
|
|
461
|
-
|
|
548
|
+
resolve4({ stdout, stderr, exitCode: 0 });
|
|
462
549
|
return;
|
|
463
550
|
}
|
|
464
551
|
if (typeof error.code === "number") {
|
|
465
|
-
|
|
552
|
+
resolve4({ stdout, stderr, exitCode: error.code });
|
|
466
553
|
return;
|
|
467
554
|
}
|
|
468
555
|
reject(error);
|
|
@@ -869,6 +956,14 @@ var pruneStale = (repoDir) => Effect.gen(function* () {
|
|
|
869
956
|
})
|
|
870
957
|
)
|
|
871
958
|
);
|
|
959
|
+
|
|
960
|
+
// src/shellQuote.ts
|
|
961
|
+
var shellQuote = (value) => {
|
|
962
|
+
if (value.includes("\0")) {
|
|
963
|
+
throw new Error("Cannot quote a string containing a NUL byte");
|
|
964
|
+
}
|
|
965
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
966
|
+
};
|
|
872
967
|
var PROTECTED_RUNNER_PATH = `${CONFIG_DIR}/${RUNNER_DIR}`;
|
|
873
968
|
var pathKey = (value) => value.normalize("NFC").toLocaleLowerCase("en-US");
|
|
874
969
|
var SAFE_RUNNER_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -900,121 +995,14 @@ var assertExcludesRepositoryRunner = (relativePath) => {
|
|
|
900
995
|
);
|
|
901
996
|
}
|
|
902
997
|
};
|
|
903
|
-
var
|
|
904
|
-
const runnerDir = join(repoDir, CONFIG_DIR, RUNNER_DIR);
|
|
905
|
-
const maskDir = join(repoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR);
|
|
906
|
-
for (const [name, path] of [
|
|
907
|
-
["repository runner", runnerDir],
|
|
908
|
-
["repository runner sandbox mask", maskDir]
|
|
909
|
-
]) {
|
|
910
|
-
const stat2 = lstatSync(path);
|
|
911
|
-
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
912
|
-
throw new Error(
|
|
913
|
-
`The protected ${name} path must be a real directory: ${path}`
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
if (pathKey(realpathSync(runnerDir)) === pathKey(realpathSync(maskDir))) {
|
|
918
|
-
throw new Error(
|
|
919
|
-
"The protected repository runner and sandbox mask paths must be distinct directories."
|
|
920
|
-
);
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
var assertProtectedDirectoryIdentities = async (runnerDir, maskDir, inspect) => {
|
|
998
|
+
var assertProtectedDirectoryIdentities = async (runnerDir, inspect) => {
|
|
924
999
|
if (!inspect) return;
|
|
925
1000
|
const runner = await inspect(runnerDir);
|
|
926
|
-
const mask = await inspect(maskDir);
|
|
927
1001
|
if (!runner.directory || runner.symbolicLink) {
|
|
928
1002
|
throw new Error(
|
|
929
1003
|
`The protected repository runner path must be a real directory: ${runnerDir}`
|
|
930
1004
|
);
|
|
931
1005
|
}
|
|
932
|
-
if (!mask.directory || mask.symbolicLink) {
|
|
933
|
-
throw new Error(
|
|
934
|
-
`The protected repository runner sandbox mask path must be a real directory: ${maskDir}`
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
if (pathKey(runner.realPath) === pathKey(mask.realPath)) {
|
|
938
|
-
throw new Error(
|
|
939
|
-
"The protected repository runner and sandbox mask paths must be distinct directories."
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
|
|
944
|
-
// src/CopyToWorktree.ts
|
|
945
|
-
var COPY_TO_WORKTREE_TIMEOUT_MS = 6e4;
|
|
946
|
-
var getCopyOnWriteFlags = (platform) => platform === "darwin" ? ["-cR"] : ["-R", "--reflink=auto"];
|
|
947
|
-
var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
|
|
948
|
-
const effectiveTimeout = timeoutMs ?? COPY_TO_WORKTREE_TIMEOUT_MS;
|
|
949
|
-
return Effect.gen(function* () {
|
|
950
|
-
const cowFlags = getCopyOnWriteFlags(process.platform);
|
|
951
|
-
for (const relativePath of paths) {
|
|
952
|
-
let src;
|
|
953
|
-
let dest;
|
|
954
|
-
try {
|
|
955
|
-
assertExcludesRepositoryRunner(relativePath);
|
|
956
|
-
src = resolveSafeRelativePath(hostRepoDir, relativePath, "copy path");
|
|
957
|
-
dest = resolveSafeRelativePath(worktreePath, relativePath, "copy path");
|
|
958
|
-
} catch (error) {
|
|
959
|
-
return yield* Effect.fail(
|
|
960
|
-
new CopyToWorktreeError({
|
|
961
|
-
message: `Refusing unsafe copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
962
|
-
path: relativePath,
|
|
963
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
964
|
-
exitCode: null
|
|
965
|
-
})
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
if (!existsSync(src)) {
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
yield* Effect.tryPromise({
|
|
972
|
-
try: async () => {
|
|
973
|
-
await assertNoSymlinkComponents(hostRepoDir, src, "copy source");
|
|
974
|
-
await assertNoSymlinkComponents(worktreePath, dest, "copy target");
|
|
975
|
-
},
|
|
976
|
-
catch: (error) => new CopyToWorktreeError({
|
|
977
|
-
message: `Refusing symlinked copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
978
|
-
path: relativePath,
|
|
979
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
980
|
-
exitCode: null
|
|
981
|
-
})
|
|
982
|
-
});
|
|
983
|
-
yield* Effect.async((resume) => {
|
|
984
|
-
execFile("cp", [...cowFlags, src, dest], (error) => {
|
|
985
|
-
if (error) {
|
|
986
|
-
execFile("cp", ["-R", src, dest], (fallbackError, _, stderr) => {
|
|
987
|
-
if (fallbackError) {
|
|
988
|
-
resume(
|
|
989
|
-
Effect.fail(
|
|
990
|
-
new CopyToWorktreeError({
|
|
991
|
-
message: `Failed to copy ${relativePath} to worktree: ${stderr || fallbackError.message}`,
|
|
992
|
-
path: relativePath,
|
|
993
|
-
stderr: stderr || fallbackError.message,
|
|
994
|
-
exitCode: typeof fallbackError.code === "number" ? fallbackError.code : null
|
|
995
|
-
})
|
|
996
|
-
)
|
|
997
|
-
);
|
|
998
|
-
} else {
|
|
999
|
-
resume(Effect.succeed(void 0));
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
} else {
|
|
1003
|
-
resume(Effect.succeed(void 0));
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
}).pipe(
|
|
1009
|
-
withTimeout(
|
|
1010
|
-
effectiveTimeout,
|
|
1011
|
-
() => new CopyToWorktreeTimeoutError({
|
|
1012
|
-
message: `Copying files to worktree timed out after ${effectiveTimeout}ms`,
|
|
1013
|
-
timeoutMs: effectiveTimeout,
|
|
1014
|
-
paths
|
|
1015
|
-
})
|
|
1016
|
-
)
|
|
1017
|
-
);
|
|
1018
1006
|
};
|
|
1019
1007
|
|
|
1020
1008
|
// src/RecoveryMessage.ts
|
|
@@ -1096,22 +1084,21 @@ var formatCommandBlock = (commands) => {
|
|
|
1096
1084
|
}
|
|
1097
1085
|
return commands.map((cmd, i) => i < commands.length - 1 ? ` ${cmd} && \\` : ` ${cmd}`).join("\n");
|
|
1098
1086
|
};
|
|
1099
|
-
|
|
1100
|
-
// src/syncOut.ts
|
|
1101
1087
|
var execHostGit2 = (args, cwd) => Effect.tryPromise({
|
|
1102
1088
|
try: () => execHostGit(args, cwd),
|
|
1103
|
-
catch: (
|
|
1089
|
+
catch: (error) => new SyncError({
|
|
1104
1090
|
message: `Host command failed: git ${args.join(" ")}
|
|
1105
|
-
${
|
|
1091
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
1106
1092
|
})
|
|
1107
1093
|
});
|
|
1108
|
-
var
|
|
1094
|
+
var execSandbox = (handle, command, options) => Effect.tryPromise({
|
|
1109
1095
|
try: () => handle.exec(command, options),
|
|
1110
|
-
catch: (
|
|
1096
|
+
catch: (error) => new SyncError({
|
|
1111
1097
|
message: `Sandbox exec failed: ${command}
|
|
1112
|
-
${
|
|
1098
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
1113
1099
|
})
|
|
1114
|
-
})
|
|
1100
|
+
});
|
|
1101
|
+
var execOk = (handle, command, options) => execSandbox(handle, command, options).pipe(
|
|
1115
1102
|
Effect.flatMap(
|
|
1116
1103
|
(result) => result.exitCode !== 0 ? Effect.fail(
|
|
1117
1104
|
new SyncError({
|
|
@@ -1121,13 +1108,8 @@ ${result.stderr}`
|
|
|
1121
1108
|
) : Effect.succeed(result)
|
|
1122
1109
|
)
|
|
1123
1110
|
);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
catch: (e) => new SyncError({
|
|
1127
|
-
message: `Sandbox exec failed: ${command}
|
|
1128
|
-
${e instanceof Error ? e.message : String(e)}`
|
|
1129
|
-
})
|
|
1130
|
-
});
|
|
1111
|
+
|
|
1112
|
+
// src/syncOut.ts
|
|
1131
1113
|
var isEmptyPatch = (patchPath) => Effect.tryPromise({
|
|
1132
1114
|
try: async () => {
|
|
1133
1115
|
const info = await stat(patchPath);
|
|
@@ -1524,8 +1506,6 @@ ${msg}`);
|
|
|
1524
1506
|
});
|
|
1525
1507
|
}
|
|
1526
1508
|
});
|
|
1527
|
-
|
|
1528
|
-
// src/SandboxLifecycle.ts
|
|
1529
1509
|
var GIT_SETUP_TIMEOUT_MS = 1e4;
|
|
1530
1510
|
var HOOK_TIMEOUT_MS = 6e4;
|
|
1531
1511
|
var COMMIT_COLLECTION_TIMEOUT_MS = 3e4;
|
|
@@ -1859,401 +1839,75 @@ ${err instanceof Error ? err.message : String(err)}`
|
|
|
1859
1839
|
}
|
|
1860
1840
|
return { result, branch: finalBranch, commits };
|
|
1861
1841
|
});
|
|
1862
|
-
var
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1842
|
+
var makeSandboxFromHandle = (handle) => ({
|
|
1843
|
+
exec: (command, options) => Effect.tryPromise({
|
|
1844
|
+
try: () => handle.exec(command, options),
|
|
1845
|
+
catch: (e) => new ExecError({
|
|
1846
|
+
command,
|
|
1847
|
+
message: `exec failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1848
|
+
})
|
|
1849
|
+
}),
|
|
1850
|
+
copyIn: (hostPath, sandboxPath) => Effect.tryPromise({
|
|
1851
|
+
try: () => handle.copyIn(hostPath, sandboxPath),
|
|
1852
|
+
catch: (e) => new CopyError({
|
|
1853
|
+
message: `copyIn failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1854
|
+
})
|
|
1855
|
+
}),
|
|
1856
|
+
copyFileOut: (sandboxPath, hostPath) => Effect.tryPromise({
|
|
1857
|
+
try: () => handle.copyFileOut(sandboxPath, hostPath),
|
|
1858
|
+
catch: (e) => new CopyError({
|
|
1859
|
+
message: `copyFileOut failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1860
|
+
})
|
|
1867
1861
|
})
|
|
1868
1862
|
});
|
|
1869
|
-
var
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1863
|
+
var SANDBOX_REPO_DIR = "/home/agent/workspace";
|
|
1864
|
+
var SandboxFactory = class extends Context.Tag("SandboxFactory")() {
|
|
1865
|
+
};
|
|
1866
|
+
var SandboxConfig = class extends Context.Tag("SandboxConfig")() {
|
|
1867
|
+
};
|
|
1868
|
+
var printWorktreePreservedMessage = (worktreePath, reason) => {
|
|
1869
|
+
console.error(`
|
|
1870
|
+
${reason}`);
|
|
1871
|
+
console.error(` To review: cd ${worktreePath}`);
|
|
1872
|
+
console.error(` To clean up: git worktree remove --force ${worktreePath}`);
|
|
1873
|
+
};
|
|
1874
|
+
var cleanupWorktree = (worktreePath, exit) => hasUncommittedChanges(worktreePath).pipe(
|
|
1875
|
+
Effect.catchAll(() => Effect.succeed(false)),
|
|
1876
|
+
Effect.flatMap((isDirty) => {
|
|
1877
|
+
if (isDirty) {
|
|
1878
|
+
printWorktreePreservedMessage(
|
|
1879
|
+
worktreePath,
|
|
1880
|
+
Exit.isSuccess(exit) ? `Run succeeded but worktree has uncommitted changes at ${worktreePath}` : `Worktree preserved at ${worktreePath}`
|
|
1881
|
+
);
|
|
1882
|
+
return Effect.succeed(worktreePath);
|
|
1883
|
+
}
|
|
1884
|
+
if (!Exit.isSuccess(exit)) {
|
|
1885
|
+
console.error(`
|
|
1886
|
+
Worktree removed (no uncommitted changes)`);
|
|
1887
|
+
}
|
|
1888
|
+
return remove(worktreePath).pipe(
|
|
1889
|
+
Effect.map(() => void 0)
|
|
1890
|
+
);
|
|
1874
1891
|
})
|
|
1875
|
-
}).pipe(
|
|
1876
|
-
Effect.flatMap(
|
|
1877
|
-
(result) => result.exitCode !== 0 ? Effect.fail(
|
|
1878
|
-
new SyncError({
|
|
1879
|
-
message: `Sandbox command failed (exit ${result.exitCode}): ${command}
|
|
1880
|
-
${result.stderr}`
|
|
1881
|
-
})
|
|
1882
|
-
) : Effect.succeed(result)
|
|
1883
|
-
)
|
|
1884
1892
|
);
|
|
1885
|
-
var
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
return yield* Effect.fail(
|
|
1893
|
-
new SyncError({
|
|
1894
|
-
message: `Refusing to copy repository history into the sandbox because it contains protected repository runner files under ${protectedRunnerPath}. Remove those files from all refs before retrying.`
|
|
1895
|
-
})
|
|
1896
|
-
);
|
|
1897
|
-
}
|
|
1898
|
-
const branch = (yield* execHostGit3(
|
|
1899
|
-
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
1900
|
-
hostRepoDir
|
|
1901
|
-
)).trim();
|
|
1902
|
-
const bundleDir = yield* Effect.tryPromise({
|
|
1903
|
-
try: () => mkdtemp(join(tmpdir(), `${RUNTIME_NAMESPACE}-bundle-`)),
|
|
1904
|
-
catch: (e) => new SyncError({
|
|
1905
|
-
message: `Failed to create temp dir: ${e instanceof Error ? e.message : String(e)}`
|
|
1906
|
-
})
|
|
1907
|
-
});
|
|
1908
|
-
const bundleHostPath = join(bundleDir, "repo.bundle");
|
|
1909
|
-
yield* Effect.ensuring(
|
|
1910
|
-
Effect.gen(function* () {
|
|
1911
|
-
yield* execHostGit3(
|
|
1912
|
-
["bundle", "create", bundleHostPath, "--all"],
|
|
1913
|
-
hostRepoDir
|
|
1914
|
-
);
|
|
1915
|
-
const mkTempResult = yield* execOk3(
|
|
1916
|
-
handle,
|
|
1917
|
-
`mktemp -d -t ${RUNTIME_NAMESPACE}-XXXXXX`
|
|
1918
|
-
);
|
|
1919
|
-
const sandboxTmpDir = mkTempResult.stdout.trim();
|
|
1920
|
-
const bundleSandboxPath = `${sandboxTmpDir}/repo.bundle`;
|
|
1921
|
-
yield* Effect.tryPromise({
|
|
1922
|
-
try: () => handle.copyIn(bundleHostPath, bundleSandboxPath),
|
|
1923
|
-
catch: (e) => new SyncError({
|
|
1924
|
-
message: `Failed to copy bundle into sandbox: ${e instanceof Error ? e.message : String(e)}`
|
|
1925
|
-
})
|
|
1926
|
-
});
|
|
1927
|
-
const worktreePath = handle.worktreePath;
|
|
1928
|
-
yield* execOk3(
|
|
1929
|
-
handle,
|
|
1930
|
-
`git clone ${shellQuote(bundleSandboxPath)} ${shellQuote(`${worktreePath}_clone`)}`
|
|
1931
|
-
);
|
|
1932
|
-
yield* execOk3(
|
|
1933
|
-
handle,
|
|
1934
|
-
`rm -rf ${shellQuote(worktreePath)} && mv ${shellQuote(`${worktreePath}_clone`)} ${shellQuote(worktreePath)}`
|
|
1935
|
-
);
|
|
1936
|
-
yield* execOk3(handle, `git checkout ${shellQuote(branch)}`, {
|
|
1937
|
-
cwd: worktreePath
|
|
1893
|
+
var attachPreservedPath = (path, e) => {
|
|
1894
|
+
if (path !== void 0) {
|
|
1895
|
+
if (e instanceof AgentIdleTimeoutError) {
|
|
1896
|
+
return new AgentIdleTimeoutError({
|
|
1897
|
+
message: e.message,
|
|
1898
|
+
timeoutMs: e.timeoutMs,
|
|
1899
|
+
preservedWorktreePath: path
|
|
1938
1900
|
});
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1901
|
+
}
|
|
1902
|
+
if (e instanceof AgentError) {
|
|
1903
|
+
return new AgentError({
|
|
1904
|
+
message: e.message,
|
|
1905
|
+
preservedWorktreePath: path
|
|
1942
1906
|
});
|
|
1943
|
-
|
|
1944
|
-
["rev-parse", "HEAD"],
|
|
1945
|
-
hostRepoDir
|
|
1946
|
-
)).trim();
|
|
1947
|
-
const sandboxHead = (yield* execOk3(handle, "git rev-parse HEAD", {
|
|
1948
|
-
cwd: worktreePath
|
|
1949
|
-
})).stdout.trim();
|
|
1950
|
-
if (hostHead !== sandboxHead) {
|
|
1951
|
-
yield* Effect.fail(
|
|
1952
|
-
new SyncError({
|
|
1953
|
-
message: `HEAD mismatch after sync-in: host=${hostHead} sandbox=${sandboxHead}`
|
|
1954
|
-
})
|
|
1955
|
-
);
|
|
1956
|
-
}
|
|
1957
|
-
}),
|
|
1958
|
-
// Clean up host-side bundle temp dir (runs regardless of success/failure)
|
|
1959
|
-
Effect.promise(() => rm(bundleDir, { recursive: true, force: true }))
|
|
1960
|
-
);
|
|
1961
|
-
return { branch };
|
|
1962
|
-
});
|
|
1963
|
-
|
|
1964
|
-
// src/startSandbox.ts
|
|
1965
|
-
var CONTAINER_START_TIMEOUT_MS = 12e4;
|
|
1966
|
-
var SYNC_IN_TIMEOUT_MS = 12e4;
|
|
1967
|
-
var COPY_PATHS_TIMEOUT_MS = 12e4;
|
|
1968
|
-
var startSandbox = (options) => {
|
|
1969
|
-
if (options.provider.tag === "bind-mount") {
|
|
1970
|
-
return startBindMountSandbox(options);
|
|
1971
|
-
}
|
|
1972
|
-
if (options.provider.tag === "none") {
|
|
1973
|
-
return startNoSandbox(options);
|
|
1974
|
-
}
|
|
1975
|
-
return startIsolatedSandbox(options);
|
|
1976
|
-
};
|
|
1977
|
-
var startNoSandbox = (options) => Effect.tryPromise({
|
|
1978
|
-
try: () => options.provider.create({
|
|
1979
|
-
worktreePath: options.worktreeOrRepoPath,
|
|
1980
|
-
env: options.env
|
|
1981
|
-
}),
|
|
1982
|
-
catch: (e) => new WorktreeError({
|
|
1983
|
-
message: `Provider '${options.provider.name}' create failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1984
|
-
})
|
|
1985
|
-
}).pipe(
|
|
1986
|
-
Effect.map((handle) => ({
|
|
1987
|
-
handle,
|
|
1988
|
-
sandbox: makeSandboxFromHandle(handle),
|
|
1989
|
-
worktreePath: handle.worktreePath
|
|
1990
|
-
}))
|
|
1991
|
-
);
|
|
1992
|
-
var startBindMountSandbox = (options) => Effect.tryPromise({
|
|
1993
|
-
try: () => {
|
|
1994
|
-
const usesRunnerMask = options.worktreeOrRepoPath === options.hostRepoDir && existsSync(join(options.hostRepoDir, CONFIG_DIR, RUNNER_DIR)) && existsSync(
|
|
1995
|
-
join(options.hostRepoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR)
|
|
1996
|
-
);
|
|
1997
|
-
if (usesRunnerMask) {
|
|
1998
|
-
assertSafeRepositoryRunnerDirectories(options.hostRepoDir);
|
|
1999
|
-
}
|
|
2000
|
-
const rawMounts = [
|
|
2001
|
-
{
|
|
2002
|
-
hostPath: options.worktreeOrRepoPath,
|
|
2003
|
-
sandboxPath: options.repoDir
|
|
2004
|
-
},
|
|
2005
|
-
...options.gitMounts,
|
|
2006
|
-
...usesRunnerMask ? [
|
|
2007
|
-
{
|
|
2008
|
-
hostPath: join(
|
|
2009
|
-
options.hostRepoDir,
|
|
2010
|
-
CONFIG_DIR,
|
|
2011
|
-
RUNNER_SANDBOX_MASK_DIR
|
|
2012
|
-
),
|
|
2013
|
-
sandboxPath: posix.join(
|
|
2014
|
-
options.repoDir,
|
|
2015
|
-
CONFIG_DIR,
|
|
2016
|
-
RUNNER_DIR
|
|
2017
|
-
),
|
|
2018
|
-
readonly: true
|
|
2019
|
-
}
|
|
2020
|
-
] : []
|
|
2021
|
-
];
|
|
2022
|
-
const mounts = normalizeMounts(
|
|
2023
|
-
rawMounts,
|
|
2024
|
-
options.worktreeOrRepoPath,
|
|
2025
|
-
SANDBOX_REPO_DIR
|
|
2026
|
-
);
|
|
2027
|
-
const worktreePath = process.platform === "win32" ? options.worktreeOrRepoPath.replace(/\\/g, "/") : options.worktreeOrRepoPath;
|
|
2028
|
-
return options.provider.create({
|
|
2029
|
-
worktreePath,
|
|
2030
|
-
hostRepoPath: options.hostRepoDir,
|
|
2031
|
-
mounts,
|
|
2032
|
-
env: options.env
|
|
2033
|
-
});
|
|
2034
|
-
},
|
|
2035
|
-
catch: (e) => new WorktreeError({
|
|
2036
|
-
message: `Provider '${options.provider.name}' create failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2037
|
-
})
|
|
2038
|
-
}).pipe(
|
|
2039
|
-
Effect.map((handle) => ({
|
|
2040
|
-
handle,
|
|
2041
|
-
sandbox: makeSandboxFromHandle(handle),
|
|
2042
|
-
worktreePath: handle.worktreePath
|
|
2043
|
-
})),
|
|
2044
|
-
withTimeout(
|
|
2045
|
-
CONTAINER_START_TIMEOUT_MS,
|
|
2046
|
-
() => new ContainerStartTimeoutError({
|
|
2047
|
-
message: `Sandbox container start timed out after ${CONTAINER_START_TIMEOUT_MS}ms`,
|
|
2048
|
-
timeoutMs: CONTAINER_START_TIMEOUT_MS
|
|
2049
|
-
})
|
|
2050
|
-
)
|
|
2051
|
-
);
|
|
2052
|
-
var startIsolatedSandbox = (options) => Effect.gen(function* () {
|
|
2053
|
-
const handle = yield* Effect.tryPromise({
|
|
2054
|
-
try: () => options.provider.create({
|
|
2055
|
-
env: options.env,
|
|
2056
|
-
hostRepoPath: options.sourceRepoDir ?? options.hostRepoDir
|
|
2057
|
-
}),
|
|
2058
|
-
catch: (e) => new WorktreeError({
|
|
2059
|
-
message: `Isolated provider '${options.provider.name}' setup failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2060
|
-
})
|
|
2061
|
-
}).pipe(
|
|
2062
|
-
withTimeout(
|
|
2063
|
-
CONTAINER_START_TIMEOUT_MS,
|
|
2064
|
-
() => new ContainerStartTimeoutError({
|
|
2065
|
-
message: `Isolated sandbox container start timed out after ${CONTAINER_START_TIMEOUT_MS}ms`,
|
|
2066
|
-
timeoutMs: CONTAINER_START_TIMEOUT_MS
|
|
2067
|
-
})
|
|
2068
|
-
)
|
|
2069
|
-
);
|
|
2070
|
-
return yield* Effect.gen(function* () {
|
|
2071
|
-
yield* syncIn(options.hostRepoDir, handle).pipe(
|
|
2072
|
-
withTimeout(
|
|
2073
|
-
SYNC_IN_TIMEOUT_MS,
|
|
2074
|
-
() => new SyncInTimeoutError({
|
|
2075
|
-
message: `Sync-in timed out after ${SYNC_IN_TIMEOUT_MS}ms`,
|
|
2076
|
-
timeoutMs: SYNC_IN_TIMEOUT_MS
|
|
2077
|
-
})
|
|
2078
|
-
)
|
|
2079
|
-
);
|
|
2080
|
-
if (options.copyPaths && options.copyPaths.length > 0) {
|
|
2081
|
-
const pathsToCopy = options.copyPaths;
|
|
2082
|
-
yield* Effect.gen(function* () {
|
|
2083
|
-
for (const relativePath of pathsToCopy) {
|
|
2084
|
-
let hostPath;
|
|
2085
|
-
let sandboxPath;
|
|
2086
|
-
try {
|
|
2087
|
-
assertExcludesRepositoryRunner(relativePath);
|
|
2088
|
-
hostPath = resolveSafeRelativePath(
|
|
2089
|
-
options.sourceRepoDir ?? options.hostRepoDir,
|
|
2090
|
-
relativePath,
|
|
2091
|
-
"copy path"
|
|
2092
|
-
);
|
|
2093
|
-
resolveSafeRelativePath(
|
|
2094
|
-
handle.worktreePath,
|
|
2095
|
-
relativePath,
|
|
2096
|
-
"copy path"
|
|
2097
|
-
);
|
|
2098
|
-
sandboxPath = posix.join(handle.worktreePath, relativePath);
|
|
2099
|
-
} catch (error) {
|
|
2100
|
-
return yield* Effect.fail(
|
|
2101
|
-
new WorktreeError({
|
|
2102
|
-
message: `Refusing unsafe copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2103
|
-
})
|
|
2104
|
-
);
|
|
2105
|
-
}
|
|
2106
|
-
if (!existsSync(hostPath)) {
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
yield* Effect.tryPromise({
|
|
2110
|
-
try: async () => {
|
|
2111
|
-
await assertNoSymlinkComponents(
|
|
2112
|
-
options.sourceRepoDir ?? options.hostRepoDir,
|
|
2113
|
-
hostPath,
|
|
2114
|
-
"copy source"
|
|
2115
|
-
);
|
|
2116
|
-
},
|
|
2117
|
-
catch: (error) => new WorktreeError({
|
|
2118
|
-
message: `Refusing symlinked copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2119
|
-
})
|
|
2120
|
-
});
|
|
2121
|
-
yield* Effect.tryPromise({
|
|
2122
|
-
try: () => handle.copyIn(hostPath, sandboxPath),
|
|
2123
|
-
catch: (e) => new WorktreeError({
|
|
2124
|
-
message: `Failed to copy ${relativePath} into sandbox: ${e instanceof Error ? e.message : String(e)}`
|
|
2125
|
-
})
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
}).pipe(
|
|
2129
|
-
withTimeout(
|
|
2130
|
-
COPY_PATHS_TIMEOUT_MS,
|
|
2131
|
-
() => new CopyToWorktreeTimeoutError({
|
|
2132
|
-
message: `Copying paths to worktree timed out after ${COPY_PATHS_TIMEOUT_MS}ms`,
|
|
2133
|
-
timeoutMs: COPY_PATHS_TIMEOUT_MS,
|
|
2134
|
-
paths: pathsToCopy
|
|
2135
|
-
})
|
|
2136
|
-
)
|
|
2137
|
-
);
|
|
2138
|
-
}
|
|
2139
|
-
return {
|
|
2140
|
-
handle,
|
|
2141
|
-
sandbox: makeSandboxFromHandle(handle),
|
|
2142
|
-
worktreePath: handle.worktreePath
|
|
2143
|
-
};
|
|
2144
|
-
}).pipe(Effect.onError(() => Effect.promise(() => handle.close())));
|
|
2145
|
-
});
|
|
2146
|
-
|
|
2147
|
-
// src/SandboxFactory.ts
|
|
2148
|
-
var getCopyIn = (handle) => {
|
|
2149
|
-
if ("copyIn" in handle) {
|
|
2150
|
-
return (hostPath, sandboxPath) => Effect.tryPromise({
|
|
2151
|
-
try: () => handle.copyIn(hostPath, sandboxPath),
|
|
2152
|
-
catch: (e) => new CopyError({
|
|
2153
|
-
message: `copyIn failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2154
|
-
})
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
if ("copyFileIn" in handle) {
|
|
2158
|
-
return (hostPath, sandboxPath) => Effect.tryPromise({
|
|
2159
|
-
try: () => handle.copyFileIn(hostPath, sandboxPath),
|
|
2160
|
-
catch: (e) => new CopyError({
|
|
2161
|
-
message: `copyFileIn failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2162
|
-
})
|
|
2163
|
-
});
|
|
2164
|
-
}
|
|
2165
|
-
return () => Effect.fail(
|
|
2166
|
-
new CopyError({
|
|
2167
|
-
message: "copyIn is not supported for this sandbox provider"
|
|
2168
|
-
})
|
|
2169
|
-
);
|
|
2170
|
-
};
|
|
2171
|
-
var makeSandboxFromHandle = (handle) => ({
|
|
2172
|
-
exec: (command, options) => Effect.tryPromise({
|
|
2173
|
-
try: () => handle.exec(command, options),
|
|
2174
|
-
catch: (e) => new ExecError({
|
|
2175
|
-
command,
|
|
2176
|
-
message: `exec failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2177
|
-
})
|
|
2178
|
-
}),
|
|
2179
|
-
copyIn: getCopyIn(handle),
|
|
2180
|
-
copyFileOut: "copyFileOut" in handle ? (sandboxPath, hostPath) => Effect.tryPromise({
|
|
2181
|
-
try: () => handle.copyFileOut(sandboxPath, hostPath),
|
|
2182
|
-
catch: (e) => new CopyError({
|
|
2183
|
-
message: `copyFileOut failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2184
|
-
})
|
|
2185
|
-
}) : () => Effect.fail(
|
|
2186
|
-
new CopyError({
|
|
2187
|
-
message: "copyFileOut is not supported for this sandbox provider"
|
|
2188
|
-
})
|
|
2189
|
-
)
|
|
2190
|
-
});
|
|
2191
|
-
var SANDBOX_REPO_DIR = "/home/agent/workspace";
|
|
2192
|
-
var SandboxFactory = class extends Context.Tag("SandboxFactory")() {
|
|
2193
|
-
};
|
|
2194
|
-
var SandboxConfig = class extends Context.Tag("SandboxConfig")() {
|
|
2195
|
-
};
|
|
2196
|
-
var printWorktreePreservedMessage = (worktreePath, reason) => {
|
|
2197
|
-
console.error(`
|
|
2198
|
-
${reason}`);
|
|
2199
|
-
console.error(` To review: cd ${worktreePath}`);
|
|
2200
|
-
console.error(` To clean up: git worktree remove --force ${worktreePath}`);
|
|
2201
|
-
};
|
|
2202
|
-
var cleanupWorktree = (worktreePath, exit) => hasUncommittedChanges(worktreePath).pipe(
|
|
2203
|
-
Effect.catchAll(() => Effect.succeed(false)),
|
|
2204
|
-
Effect.flatMap((isDirty) => {
|
|
2205
|
-
if (isDirty) {
|
|
2206
|
-
printWorktreePreservedMessage(
|
|
2207
|
-
worktreePath,
|
|
2208
|
-
Exit.isSuccess(exit) ? `Run succeeded but worktree has uncommitted changes at ${worktreePath}` : `Worktree preserved at ${worktreePath}`
|
|
2209
|
-
);
|
|
2210
|
-
return Effect.succeed(worktreePath);
|
|
2211
|
-
}
|
|
2212
|
-
if (!Exit.isSuccess(exit)) {
|
|
2213
|
-
console.error(`
|
|
2214
|
-
Worktree removed (no uncommitted changes)`);
|
|
2215
|
-
}
|
|
2216
|
-
return remove(worktreePath).pipe(
|
|
2217
|
-
Effect.map(() => void 0)
|
|
2218
|
-
);
|
|
2219
|
-
})
|
|
2220
|
-
);
|
|
2221
|
-
var attachPreservedPath = (path, e) => {
|
|
2222
|
-
if (path !== void 0) {
|
|
2223
|
-
if (e instanceof AgentIdleTimeoutError) {
|
|
2224
|
-
return new AgentIdleTimeoutError({
|
|
2225
|
-
message: e.message,
|
|
2226
|
-
timeoutMs: e.timeoutMs,
|
|
2227
|
-
preservedWorktreePath: path
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
|
-
if (e instanceof AgentError) {
|
|
2231
|
-
return new AgentError({
|
|
2232
|
-
message: e.message,
|
|
2233
|
-
preservedWorktreePath: path
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
1907
|
+
}
|
|
2236
1908
|
}
|
|
2237
1909
|
return e;
|
|
2238
1910
|
};
|
|
2239
|
-
var resolveGitMounts = (gitPath) => Effect.gen(function* () {
|
|
2240
|
-
const fs = yield* FileSystem.FileSystem;
|
|
2241
|
-
const stat2 = yield* fs.stat(gitPath);
|
|
2242
|
-
if (stat2.type === "Directory") {
|
|
2243
|
-
return [{ hostPath: gitPath, sandboxPath: gitPath }];
|
|
2244
|
-
}
|
|
2245
|
-
const content = (yield* fs.readFileString(gitPath)).trim();
|
|
2246
|
-
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
2247
|
-
if (!match) {
|
|
2248
|
-
return [{ hostPath: gitPath, sandboxPath: gitPath }];
|
|
2249
|
-
}
|
|
2250
|
-
const gitdirPath = match[1];
|
|
2251
|
-
const parentGitDir = resolve(gitdirPath, "..", "..");
|
|
2252
|
-
return [
|
|
2253
|
-
{ hostPath: gitPath, sandboxPath: gitPath },
|
|
2254
|
-
{ hostPath: parentGitDir, sandboxPath: parentGitDir }
|
|
2255
|
-
];
|
|
2256
|
-
});
|
|
2257
1911
|
var WorktreeDockerSandboxFactory = {
|
|
2258
1912
|
layer: Layer.effect(
|
|
2259
1913
|
SandboxFactory,
|
|
@@ -2269,11 +1923,9 @@ var WorktreeDockerSandboxFactory = {
|
|
|
2269
1923
|
signal,
|
|
2270
1924
|
timeouts
|
|
2271
1925
|
} = yield* SandboxConfig;
|
|
2272
|
-
const isHeadMode = branchStrategy.type === "head";
|
|
2273
1926
|
const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
|
|
2274
1927
|
const baseBranch = branchStrategy.type === "branch" ? branchStrategy.baseBranch : void 0;
|
|
2275
1928
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
2276
|
-
const display = yield* Display;
|
|
2277
1929
|
const pruneAndCreate = () => pruneStale(hostRepoDir).pipe(
|
|
2278
1930
|
Effect.catchAll(
|
|
2279
1931
|
(e) => Effect.sync(() => {
|
|
@@ -2290,100 +1942,7 @@ var WorktreeDockerSandboxFactory = {
|
|
|
2290
1942
|
);
|
|
2291
1943
|
return {
|
|
2292
1944
|
withSandbox: (makeEffect) => {
|
|
2293
|
-
|
|
2294
|
-
let preservedPath;
|
|
2295
|
-
if (isHeadMode) {
|
|
2296
|
-
return (hooks?.host?.onWorktreeReady?.length ? runHostHooks(
|
|
2297
|
-
hooks.host.onWorktreeReady,
|
|
2298
|
-
hostRepoDir,
|
|
2299
|
-
signal
|
|
2300
|
-
) : Effect.void).pipe(
|
|
2301
|
-
Effect.andThen(
|
|
2302
|
-
Effect.acquireUseRelease(
|
|
2303
|
-
startSandbox({
|
|
2304
|
-
provider: sandboxProvider,
|
|
2305
|
-
hostRepoDir,
|
|
2306
|
-
env,
|
|
2307
|
-
worktreeOrRepoPath: hostRepoDir
|
|
2308
|
-
}),
|
|
2309
|
-
({ sandbox, worktreePath }) => makeEffect(
|
|
2310
|
-
{
|
|
2311
|
-
hostWorktreePath: hostRepoDir,
|
|
2312
|
-
sandboxRepoPath: worktreePath
|
|
2313
|
-
},
|
|
2314
|
-
sandbox
|
|
2315
|
-
),
|
|
2316
|
-
({ handle }) => Effect.tryPromise({
|
|
2317
|
-
try: () => handle.close(),
|
|
2318
|
-
catch: () => void 0
|
|
2319
|
-
}).pipe(Effect.orDie)
|
|
2320
|
-
).pipe(
|
|
2321
|
-
Effect.map((value) => ({
|
|
2322
|
-
value,
|
|
2323
|
-
preservedWorktreePath: void 0
|
|
2324
|
-
}))
|
|
2325
|
-
)
|
|
2326
|
-
)
|
|
2327
|
-
);
|
|
2328
|
-
}
|
|
2329
|
-
return Effect.acquireUseRelease(
|
|
2330
|
-
pruneAndCreate(),
|
|
2331
|
-
(worktreeInfo) => (copyPaths && copyPaths.length > 0 ? display.spinner(
|
|
2332
|
-
"Copying to worktree",
|
|
2333
|
-
copyToWorktree(
|
|
2334
|
-
copyPaths,
|
|
2335
|
-
hostRepoDir,
|
|
2336
|
-
worktreeInfo.path,
|
|
2337
|
-
timeouts?.copyToWorktreeMs
|
|
2338
|
-
)
|
|
2339
|
-
) : Effect.succeed(void 0)).pipe(
|
|
2340
|
-
Effect.andThen(
|
|
2341
|
-
hooks?.host?.onWorktreeReady?.length ? runHostHooks(
|
|
2342
|
-
hooks.host.onWorktreeReady,
|
|
2343
|
-
worktreeInfo.path,
|
|
2344
|
-
signal
|
|
2345
|
-
) : Effect.void
|
|
2346
|
-
),
|
|
2347
|
-
Effect.andThen(
|
|
2348
|
-
Effect.acquireUseRelease(
|
|
2349
|
-
startSandbox({
|
|
2350
|
-
provider: sandboxProvider,
|
|
2351
|
-
hostRepoDir,
|
|
2352
|
-
env,
|
|
2353
|
-
worktreeOrRepoPath: worktreeInfo.path
|
|
2354
|
-
}),
|
|
2355
|
-
({ sandbox, worktreePath }) => makeEffect(
|
|
2356
|
-
{
|
|
2357
|
-
hostWorktreePath: worktreeInfo.path,
|
|
2358
|
-
sandboxRepoPath: worktreePath
|
|
2359
|
-
},
|
|
2360
|
-
sandbox
|
|
2361
|
-
),
|
|
2362
|
-
({ handle }) => Effect.tryPromise({
|
|
2363
|
-
try: () => handle.close(),
|
|
2364
|
-
catch: () => void 0
|
|
2365
|
-
}).pipe(Effect.orDie)
|
|
2366
|
-
)
|
|
2367
|
-
)
|
|
2368
|
-
),
|
|
2369
|
-
(worktreeInfo, exit) => cleanupWorktree(worktreeInfo.path, exit).pipe(
|
|
2370
|
-
Effect.tap((p) => {
|
|
2371
|
-
preservedPath = p;
|
|
2372
|
-
}),
|
|
2373
|
-
Effect.asVoid,
|
|
2374
|
-
Effect.orDie
|
|
2375
|
-
)
|
|
2376
|
-
).pipe(
|
|
2377
|
-
Effect.map((value) => ({
|
|
2378
|
-
value,
|
|
2379
|
-
preservedWorktreePath: preservedPath
|
|
2380
|
-
})),
|
|
2381
|
-
Effect.mapError(
|
|
2382
|
-
(e) => attachPreservedPath(preservedPath, e)
|
|
2383
|
-
)
|
|
2384
|
-
);
|
|
2385
|
-
}
|
|
2386
|
-
if (sandboxProvider.tag === "isolated") {
|
|
1945
|
+
{
|
|
2387
1946
|
let preservedPath;
|
|
2388
1947
|
return Effect.acquireUseRelease(
|
|
2389
1948
|
pruneAndCreate(),
|
|
@@ -2399,13 +1958,14 @@ var WorktreeDockerSandboxFactory = {
|
|
|
2399
1958
|
hostRepoDir: worktreeInfo.path,
|
|
2400
1959
|
sourceRepoDir: hostRepoDir,
|
|
2401
1960
|
env,
|
|
2402
|
-
copyPaths
|
|
1961
|
+
copyPaths,
|
|
1962
|
+
copyTimeoutMs: timeouts?.copyToWorktreeMs
|
|
2403
1963
|
}),
|
|
2404
1964
|
({ sandbox, worktreePath, handle }) => makeEffect(
|
|
2405
1965
|
{
|
|
2406
1966
|
hostWorktreePath: worktreeInfo.path,
|
|
2407
1967
|
sandboxRepoPath: worktreePath,
|
|
2408
|
-
|
|
1968
|
+
sessionTransferHandle: toSessionTransferHandle(handle),
|
|
2409
1969
|
applyToHost: () => syncOut(
|
|
2410
1970
|
worktreeInfo.path,
|
|
2411
1971
|
handle
|
|
@@ -2437,374 +1997,191 @@ var WorktreeDockerSandboxFactory = {
|
|
|
2437
1997
|
)
|
|
2438
1998
|
);
|
|
2439
1999
|
}
|
|
2440
|
-
if (isHeadMode) {
|
|
2441
|
-
const gitPath = join(hostRepoDir, ".git");
|
|
2442
|
-
return (hooks?.host?.onWorktreeReady?.length ? runHostHooks(hooks.host.onWorktreeReady, hostRepoDir, signal) : Effect.void).pipe(
|
|
2443
|
-
Effect.andThen(resolveGitMounts(gitPath)),
|
|
2444
|
-
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
2445
|
-
Effect.mapError(
|
|
2446
|
-
(e) => new WorktreeError({
|
|
2447
|
-
message: `Failed to resolve git mounts: ${e}`
|
|
2448
|
-
})
|
|
2449
|
-
),
|
|
2450
|
-
Effect.flatMap(
|
|
2451
|
-
(gitMounts) => (
|
|
2452
|
-
// Patch git mounts for Windows worktree compatibility (ADR-0006)
|
|
2453
|
-
patchGitMountsForWindows(
|
|
2454
|
-
gitMounts,
|
|
2455
|
-
hostRepoDir,
|
|
2456
|
-
SANDBOX_REPO_DIR
|
|
2457
|
-
)
|
|
2458
|
-
)
|
|
2459
|
-
),
|
|
2460
|
-
Effect.flatMap(
|
|
2461
|
-
(gitMounts) => Effect.acquireUseRelease(
|
|
2462
|
-
startSandbox({
|
|
2463
|
-
provider: sandboxProvider,
|
|
2464
|
-
hostRepoDir,
|
|
2465
|
-
env,
|
|
2466
|
-
worktreeOrRepoPath: hostRepoDir,
|
|
2467
|
-
gitMounts,
|
|
2468
|
-
repoDir: SANDBOX_REPO_DIR
|
|
2469
|
-
}),
|
|
2470
|
-
// Use
|
|
2471
|
-
({ sandbox, worktreePath, handle }) => makeEffect(
|
|
2472
|
-
{
|
|
2473
|
-
hostWorktreePath: hostRepoDir,
|
|
2474
|
-
sandboxRepoPath: worktreePath,
|
|
2475
|
-
bindMountHandle: handle
|
|
2476
|
-
},
|
|
2477
|
-
sandbox
|
|
2478
|
-
),
|
|
2479
|
-
// Release
|
|
2480
|
-
({ handle }) => Effect.tryPromise({
|
|
2481
|
-
try: () => handle.close(),
|
|
2482
|
-
catch: () => void 0
|
|
2483
|
-
}).pipe(Effect.orDie)
|
|
2484
|
-
).pipe(
|
|
2485
|
-
Effect.map((value) => ({
|
|
2486
|
-
value,
|
|
2487
|
-
preservedWorktreePath: void 0
|
|
2488
|
-
}))
|
|
2489
|
-
)
|
|
2490
|
-
)
|
|
2491
|
-
);
|
|
2492
|
-
}
|
|
2493
|
-
let preservedWorktreePath;
|
|
2494
|
-
return Effect.acquireUseRelease(
|
|
2495
|
-
// Acquire: prune stale worktrees (best-effort), then create the worktree.
|
|
2496
|
-
pruneAndCreate(),
|
|
2497
|
-
// Use: copy files, run host hooks, resolve+patch git mounts, then start
|
|
2498
|
-
// the sandbox under a nested acquireUseRelease.
|
|
2499
|
-
(worktreeInfo) => (copyPaths && copyPaths.length > 0 ? display.spinner(
|
|
2500
|
-
"Copying to worktree",
|
|
2501
|
-
copyToWorktree(
|
|
2502
|
-
copyPaths,
|
|
2503
|
-
hostRepoDir,
|
|
2504
|
-
worktreeInfo.path,
|
|
2505
|
-
timeouts?.copyToWorktreeMs
|
|
2506
|
-
)
|
|
2507
|
-
) : Effect.succeed(void 0)).pipe(
|
|
2508
|
-
Effect.andThen(
|
|
2509
|
-
hooks?.host?.onWorktreeReady?.length ? runHostHooks(
|
|
2510
|
-
hooks.host.onWorktreeReady,
|
|
2511
|
-
worktreeInfo.path,
|
|
2512
|
-
signal
|
|
2513
|
-
) : Effect.void
|
|
2514
|
-
),
|
|
2515
|
-
Effect.andThen(
|
|
2516
|
-
resolveGitMounts(join(hostRepoDir, ".git")).pipe(
|
|
2517
|
-
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
2518
|
-
Effect.mapError(
|
|
2519
|
-
(e) => new WorktreeError({
|
|
2520
|
-
message: `Failed to resolve git mounts: ${e}`
|
|
2521
|
-
})
|
|
2522
|
-
)
|
|
2523
|
-
)
|
|
2524
|
-
),
|
|
2525
|
-
// Patch git mounts for Windows worktree compatibility (ADR-0006)
|
|
2526
|
-
Effect.flatMap(
|
|
2527
|
-
(gitMounts) => patchGitMountsForWindows(
|
|
2528
|
-
gitMounts,
|
|
2529
|
-
worktreeInfo.path,
|
|
2530
|
-
SANDBOX_REPO_DIR
|
|
2531
|
-
)
|
|
2532
|
-
),
|
|
2533
|
-
Effect.flatMap(
|
|
2534
|
-
(gitMounts) => Effect.acquireUseRelease(
|
|
2535
|
-
// sandboxProvider is guaranteed bind-mount here
|
|
2536
|
-
// (isolated providers return early above)
|
|
2537
|
-
startSandbox({
|
|
2538
|
-
provider: sandboxProvider,
|
|
2539
|
-
hostRepoDir,
|
|
2540
|
-
env,
|
|
2541
|
-
worktreeOrRepoPath: worktreeInfo.path,
|
|
2542
|
-
gitMounts,
|
|
2543
|
-
repoDir: SANDBOX_REPO_DIR
|
|
2544
|
-
}),
|
|
2545
|
-
({ sandbox, worktreePath, handle }) => makeEffect(
|
|
2546
|
-
{
|
|
2547
|
-
hostWorktreePath: worktreeInfo.path,
|
|
2548
|
-
sandboxRepoPath: worktreePath,
|
|
2549
|
-
bindMountHandle: handle
|
|
2550
|
-
},
|
|
2551
|
-
sandbox
|
|
2552
|
-
),
|
|
2553
|
-
({ handle }) => Effect.tryPromise({
|
|
2554
|
-
try: () => handle.close(),
|
|
2555
|
-
catch: () => void 0
|
|
2556
|
-
}).pipe(Effect.orDie)
|
|
2557
|
-
)
|
|
2558
|
-
)
|
|
2559
|
-
),
|
|
2560
|
-
// Release: remove or preserve the worktree based on dirty state.
|
|
2561
|
-
(worktreeInfo, exit) => cleanupWorktree(worktreeInfo.path, exit).pipe(
|
|
2562
|
-
Effect.tap((p) => {
|
|
2563
|
-
preservedWorktreePath = p;
|
|
2564
|
-
}),
|
|
2565
|
-
Effect.asVoid,
|
|
2566
|
-
Effect.orDie
|
|
2567
|
-
)
|
|
2568
|
-
).pipe(
|
|
2569
|
-
Effect.map((value) => ({
|
|
2570
|
-
value,
|
|
2571
|
-
preservedWorktreePath
|
|
2572
|
-
})),
|
|
2573
|
-
Effect.mapError(
|
|
2574
|
-
(e) => attachPreservedPath(preservedWorktreePath, e)
|
|
2575
|
-
)
|
|
2576
|
-
);
|
|
2577
2000
|
}
|
|
2578
2001
|
};
|
|
2579
2002
|
})
|
|
2580
2003
|
)
|
|
2581
2004
|
};
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
return p;
|
|
2594
|
-
};
|
|
2595
|
-
var resolveHostPath = (hostPath) => {
|
|
2596
|
-
const expanded = expandTilde(hostPath);
|
|
2597
|
-
return isAbsolute(expanded) ? expanded : resolve(process.cwd(), expanded);
|
|
2598
|
-
};
|
|
2599
|
-
var resolveSandboxPath = (sandboxPath, sandboxHomedir) => {
|
|
2600
|
-
const hasTilde = sandboxPath === "~" || sandboxPath.startsWith("~/") || sandboxPath.startsWith("~\\");
|
|
2601
|
-
if (hasTilde && sandboxHomedir === void 0) {
|
|
2602
|
-
throw new Error(
|
|
2603
|
-
`sandboxPath "${sandboxPath}" contains a tilde but the provider has no sandboxHomedir set`
|
|
2604
|
-
);
|
|
2605
|
-
}
|
|
2606
|
-
const expanded = hasTilde ? expandTilde(sandboxPath, sandboxHomedir) : sandboxPath;
|
|
2607
|
-
return isAbsolute(expanded) ? resolve(expanded) : resolve(SANDBOX_REPO_DIR, expanded);
|
|
2608
|
-
};
|
|
2609
|
-
var resolveUserMounts = (mounts, sandboxHomedir) => mounts.map((m) => {
|
|
2610
|
-
const resolvedHostPath = resolveHostPath(m.hostPath);
|
|
2611
|
-
if (!existsSync(resolvedHostPath)) {
|
|
2612
|
-
throw new Error(
|
|
2613
|
-
`Mount hostPath does not exist: ${m.hostPath}` + (m.hostPath !== resolvedHostPath ? ` (resolved to ${resolvedHostPath})` : "")
|
|
2005
|
+
var syncIn = (hostRepoDir, handle) => Effect.gen(function* () {
|
|
2006
|
+
const protectedRunnerPath = `${CONFIG_DIR}/${RUNNER_DIR}`;
|
|
2007
|
+
const runnerObjects = yield* execHostGit2(
|
|
2008
|
+
["rev-list", "--objects", "--all", "--", protectedRunnerPath],
|
|
2009
|
+
hostRepoDir
|
|
2010
|
+
);
|
|
2011
|
+
if (runnerObjects.trim().length > 0) {
|
|
2012
|
+
return yield* Effect.fail(
|
|
2013
|
+
new SyncError({
|
|
2014
|
+
message: `Refusing to copy repository history into the sandbox because it contains protected repository runner files under ${protectedRunnerPath}. Remove those files from all refs before retrying.`
|
|
2015
|
+
})
|
|
2614
2016
|
);
|
|
2615
2017
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
})
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
const normalizedWorktree = worktreeHostPath.replace(/\\/g, "/");
|
|
2625
|
-
return mounts.map((m) => {
|
|
2626
|
-
const hostPath = m.hostPath.replace(/\\/g, "/");
|
|
2627
|
-
let sandboxPath = m.sandboxPath;
|
|
2628
|
-
if (/^[A-Za-z]:[/\\]/.test(sandboxPath) || sandboxPath.includes("\\")) {
|
|
2629
|
-
const normalizedSandboxPath = sandboxPath.replace(/\\/g, "/");
|
|
2630
|
-
if (normalizedSandboxPath.startsWith(normalizedWorktree + "/")) {
|
|
2631
|
-
const relativeSuffix = normalizedSandboxPath.slice(
|
|
2632
|
-
normalizedWorktree.length
|
|
2633
|
-
);
|
|
2634
|
-
sandboxPath = sandboxRepoDir + relativeSuffix;
|
|
2635
|
-
} else if (normalizedSandboxPath === normalizedWorktree) {
|
|
2636
|
-
sandboxPath = sandboxRepoDir;
|
|
2637
|
-
} else {
|
|
2638
|
-
sandboxPath = normalizedSandboxPath;
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
return { ...m, hostPath, sandboxPath };
|
|
2642
|
-
});
|
|
2643
|
-
};
|
|
2644
|
-
var parseGitdirPath = (gitdirPath) => {
|
|
2645
|
-
const normalized = gitdirPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2646
|
-
const segments = normalized.split("/");
|
|
2647
|
-
const worktreeName = segments.pop();
|
|
2648
|
-
segments.pop();
|
|
2649
|
-
const parentGitDir = segments.join("/");
|
|
2650
|
-
return { worktreeName, parentGitDir };
|
|
2651
|
-
};
|
|
2652
|
-
var patchGitMountsForWindows = (gitMounts, worktreeHostPath, sandboxRepoDir, readFile2, statFile, platform = process.platform) => Effect.gen(function* () {
|
|
2653
|
-
if (platform !== "win32") return gitMounts;
|
|
2654
|
-
const _readFile = readFile2 ?? (async (p) => {
|
|
2655
|
-
const { readFile: rf } = await import('fs/promises');
|
|
2656
|
-
return rf(p, "utf-8");
|
|
2657
|
-
});
|
|
2658
|
-
const _statFile = statFile ?? (async (p) => {
|
|
2659
|
-
const { stat: stat2 } = await import('fs/promises');
|
|
2660
|
-
const s = await stat2(p);
|
|
2661
|
-
return s.isDirectory() ? "directory" : "file";
|
|
2662
|
-
});
|
|
2663
|
-
const gitEntryPath = join(worktreeHostPath, ".git");
|
|
2664
|
-
const gitEntryType = yield* Effect.tryPromise({
|
|
2665
|
-
try: () => _statFile(gitEntryPath),
|
|
2666
|
-
catch: () => null
|
|
2667
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
2668
|
-
if (gitEntryType === null) return gitMounts;
|
|
2669
|
-
if (gitEntryType === "directory") return gitMounts;
|
|
2670
|
-
const content = yield* Effect.tryPromise({
|
|
2671
|
-
try: () => _readFile(gitEntryPath),
|
|
2672
|
-
catch: () => null
|
|
2673
|
-
}).pipe(
|
|
2674
|
-
Effect.map((s) => s.trim()),
|
|
2675
|
-
Effect.catchAll(() => Effect.succeed(null))
|
|
2676
|
-
);
|
|
2677
|
-
if (content === null) return gitMounts;
|
|
2678
|
-
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
2679
|
-
if (!match) return gitMounts;
|
|
2680
|
-
const gitdirPath = match[1];
|
|
2681
|
-
const { parentGitDir, worktreeName } = parseGitdirPath(gitdirPath);
|
|
2682
|
-
const correctedGitdir = `${PARENT_GIT_SANDBOX_DIR}/worktrees/${worktreeName}`;
|
|
2683
|
-
const tempDir = yield* Effect.tryPromise({
|
|
2684
|
-
try: () => mkdtemp(join(tmpdir(), `${CONTAINER_NAME_PREFIX}git-`)),
|
|
2685
|
-
catch: (e) => new WorktreeError({
|
|
2686
|
-
message: `Failed to create temp dir for git override: ${e instanceof Error ? e.message : String(e)}`
|
|
2687
|
-
})
|
|
2688
|
-
});
|
|
2689
|
-
const tempGitFile = join(tempDir, "git-override");
|
|
2690
|
-
yield* Effect.tryPromise({
|
|
2691
|
-
try: () => writeFile(tempGitFile, `gitdir: ${correctedGitdir}
|
|
2692
|
-
`),
|
|
2693
|
-
catch: (e) => new WorktreeError({
|
|
2694
|
-
message: `Failed to write git override file: ${e instanceof Error ? e.message : String(e)}`
|
|
2018
|
+
const branch = (yield* execHostGit2(
|
|
2019
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
2020
|
+
hostRepoDir
|
|
2021
|
+
)).trim();
|
|
2022
|
+
const bundleDir = yield* Effect.tryPromise({
|
|
2023
|
+
try: () => mkdtemp(join(tmpdir(), `${RUNTIME_NAMESPACE}-bundle-`)),
|
|
2024
|
+
catch: (e) => new SyncError({
|
|
2025
|
+
message: `Failed to create temp dir: ${e instanceof Error ? e.message : String(e)}`
|
|
2695
2026
|
})
|
|
2696
2027
|
});
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2028
|
+
const bundleHostPath = join(bundleDir, "repo.bundle");
|
|
2029
|
+
yield* Effect.ensuring(
|
|
2030
|
+
Effect.gen(function* () {
|
|
2031
|
+
yield* execHostGit2(
|
|
2032
|
+
["bundle", "create", bundleHostPath, "--all"],
|
|
2033
|
+
hostRepoDir
|
|
2034
|
+
);
|
|
2035
|
+
const mkTempResult = yield* execOk(
|
|
2036
|
+
handle,
|
|
2037
|
+
`mktemp -d -t ${RUNTIME_NAMESPACE}-XXXXXX`
|
|
2038
|
+
);
|
|
2039
|
+
const sandboxTmpDir = mkTempResult.stdout.trim();
|
|
2040
|
+
const bundleSandboxPath = `${sandboxTmpDir}/repo.bundle`;
|
|
2041
|
+
yield* Effect.tryPromise({
|
|
2042
|
+
try: () => handle.copyIn(bundleHostPath, bundleSandboxPath),
|
|
2043
|
+
catch: (e) => new SyncError({
|
|
2044
|
+
message: `Failed to copy bundle into sandbox: ${e instanceof Error ? e.message : String(e)}`
|
|
2045
|
+
})
|
|
2710
2046
|
});
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
}
|
|
2716
|
-
if (!replacedGitFile) {
|
|
2717
|
-
correctedMounts.push({
|
|
2718
|
-
hostPath: tempGitFile,
|
|
2719
|
-
sandboxPath: `${sandboxRepoDir}/.git`
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2722
|
-
return correctedMounts;
|
|
2723
|
-
});
|
|
2724
|
-
var formatVolumeMount = (mount, selinuxLabel) => {
|
|
2725
|
-
const base = `${mount.hostPath}:${mount.sandboxPath}`;
|
|
2726
|
-
const options = [mount.readonly ? "ro" : void 0, selinuxLabel || void 0].filter((option) => option !== void 0).join(",");
|
|
2727
|
-
return options ? `${base}:${options}` : base;
|
|
2728
|
-
};
|
|
2729
|
-
var processFileMountParents = (mounts, sandboxHomedir, statFn = statSync) => {
|
|
2730
|
-
const parentDirs = /* @__PURE__ */ new Set();
|
|
2731
|
-
for (const mount of mounts) {
|
|
2732
|
-
let isFile;
|
|
2733
|
-
try {
|
|
2734
|
-
isFile = statFn(mount.hostPath).isFile();
|
|
2735
|
-
} catch {
|
|
2736
|
-
continue;
|
|
2737
|
-
}
|
|
2738
|
-
if (!isFile) continue;
|
|
2739
|
-
const parentDir = resolve(dirname(mount.sandboxPath));
|
|
2740
|
-
const homeDir = resolve(sandboxHomedir);
|
|
2741
|
-
if (parentDir === homeDir) continue;
|
|
2742
|
-
const fromHome = relative(homeDir, parentDir);
|
|
2743
|
-
if (fromHome === ".." || fromHome.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(fromHome)) {
|
|
2744
|
-
throw new Error(
|
|
2745
|
-
`Cannot mount file to '${mount.sandboxPath}': parent directory '${parentDir}' is outside the sandbox home directory ('${homeDir}'). Mount the parent directory instead, or rebuild the image with '${parentDir}' pre-created.`
|
|
2047
|
+
const worktreePath = handle.worktreePath;
|
|
2048
|
+
yield* execOk(
|
|
2049
|
+
handle,
|
|
2050
|
+
`git clone ${shellQuote(bundleSandboxPath)} ${shellQuote(`${worktreePath}_clone`)}`
|
|
2746
2051
|
);
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
return [...parentDirs];
|
|
2751
|
-
};
|
|
2752
|
-
var assertIsolatedWorkspaceMounts = (mounts) => {
|
|
2753
|
-
for (const mount of mounts) {
|
|
2754
|
-
const path = posix.resolve(mount.sandboxPath);
|
|
2755
|
-
if (path === "/" || path === SANDBOX_REPO_DIR || path.startsWith(`${SANDBOX_REPO_DIR}/`) || SANDBOX_REPO_DIR.startsWith(`${path}/`)) {
|
|
2756
|
-
throw new Error(
|
|
2757
|
-
`Mount overlaps the isolated workspace: ${mount.sandboxPath}. Mount outside ${SANDBOX_REPO_DIR} or use copyToWorktree.`
|
|
2052
|
+
yield* execOk(
|
|
2053
|
+
handle,
|
|
2054
|
+
`rm -rf ${shellQuote(worktreePath)} && mv ${shellQuote(`${worktreePath}_clone`)} ${shellQuote(worktreePath)}`
|
|
2758
2055
|
);
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
};
|
|
2056
|
+
yield* execOk(handle, `git checkout ${shellQuote(branch)}`, {
|
|
2057
|
+
cwd: worktreePath
|
|
2058
|
+
});
|
|
2059
|
+
yield* Effect.tryPromise({
|
|
2060
|
+
try: () => handle.exec(`rm -rf ${shellQuote(sandboxTmpDir)}`),
|
|
2061
|
+
catch: () => new SyncError({ message: "Failed to clean up sandbox temp dir" })
|
|
2062
|
+
});
|
|
2063
|
+
const hostHead = (yield* execHostGit2(
|
|
2064
|
+
["rev-parse", "HEAD"],
|
|
2065
|
+
hostRepoDir
|
|
2066
|
+
)).trim();
|
|
2067
|
+
const sandboxHead = (yield* execOk(handle, "git rev-parse HEAD", {
|
|
2068
|
+
cwd: worktreePath
|
|
2069
|
+
})).stdout.trim();
|
|
2070
|
+
if (hostHead !== sandboxHead) {
|
|
2071
|
+
yield* Effect.fail(
|
|
2072
|
+
new SyncError({
|
|
2073
|
+
message: `HEAD mismatch after sync-in: host=${hostHead} sandbox=${sandboxHead}`
|
|
2074
|
+
})
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
}),
|
|
2078
|
+
// Clean up host-side bundle temp dir (runs regardless of success/failure)
|
|
2079
|
+
Effect.promise(() => rm(bundleDir, { recursive: true, force: true }))
|
|
2080
|
+
);
|
|
2081
|
+
return { branch };
|
|
2082
|
+
});
|
|
2762
2083
|
|
|
2763
|
-
// src/
|
|
2764
|
-
var
|
|
2765
|
-
var
|
|
2766
|
-
var
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2084
|
+
// src/startSandbox.ts
|
|
2085
|
+
var CONTAINER_START_TIMEOUT_MS = 12e4;
|
|
2086
|
+
var SYNC_IN_TIMEOUT_MS = 12e4;
|
|
2087
|
+
var COPY_PATHS_TIMEOUT_MS = 6e4;
|
|
2088
|
+
var startSandbox = (options) => Effect.gen(function* () {
|
|
2089
|
+
const handle = yield* Effect.tryPromise({
|
|
2090
|
+
try: () => options.provider.create({
|
|
2091
|
+
env: options.env,
|
|
2092
|
+
hostRepoPath: options.sourceRepoDir ?? options.hostRepoDir
|
|
2093
|
+
}),
|
|
2094
|
+
catch: (e) => new WorktreeError({
|
|
2095
|
+
message: `Isolated provider '${options.provider.name}' setup failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2096
|
+
})
|
|
2097
|
+
}).pipe(
|
|
2098
|
+
withTimeout(
|
|
2099
|
+
CONTAINER_START_TIMEOUT_MS,
|
|
2100
|
+
() => new ContainerStartTimeoutError({
|
|
2101
|
+
message: `Isolated sandbox container start timed out after ${CONTAINER_START_TIMEOUT_MS}ms`,
|
|
2102
|
+
timeoutMs: CONTAINER_START_TIMEOUT_MS
|
|
2103
|
+
})
|
|
2104
|
+
)
|
|
2105
|
+
);
|
|
2106
|
+
return yield* Effect.gen(function* () {
|
|
2107
|
+
yield* syncIn(options.hostRepoDir, handle).pipe(
|
|
2108
|
+
withTimeout(
|
|
2109
|
+
SYNC_IN_TIMEOUT_MS,
|
|
2110
|
+
() => new SyncInTimeoutError({
|
|
2111
|
+
message: `Sync-in timed out after ${SYNC_IN_TIMEOUT_MS}ms`,
|
|
2112
|
+
timeoutMs: SYNC_IN_TIMEOUT_MS
|
|
2113
|
+
})
|
|
2114
|
+
)
|
|
2115
|
+
);
|
|
2116
|
+
if (options.copyPaths && options.copyPaths.length > 0) {
|
|
2117
|
+
const pathsToCopy = options.copyPaths;
|
|
2118
|
+
const copySourceDir = options.copySourceDir ?? options.sourceRepoDir ?? options.hostRepoDir;
|
|
2119
|
+
const copyTimeoutMs = options.copyTimeoutMs ?? COPY_PATHS_TIMEOUT_MS;
|
|
2120
|
+
yield* Effect.gen(function* () {
|
|
2121
|
+
for (const relativePath of pathsToCopy) {
|
|
2122
|
+
let hostPath;
|
|
2123
|
+
let sandboxPath;
|
|
2124
|
+
try {
|
|
2125
|
+
assertExcludesRepositoryRunner(relativePath);
|
|
2126
|
+
hostPath = resolveSafeRelativePath(
|
|
2127
|
+
copySourceDir,
|
|
2128
|
+
relativePath,
|
|
2129
|
+
"copy path"
|
|
2130
|
+
);
|
|
2131
|
+
resolveSafeRelativePath(
|
|
2132
|
+
handle.worktreePath,
|
|
2133
|
+
relativePath,
|
|
2134
|
+
"copy path"
|
|
2135
|
+
);
|
|
2136
|
+
sandboxPath = posix.join(handle.worktreePath, relativePath);
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
return yield* Effect.fail(
|
|
2139
|
+
new WorktreeError({
|
|
2140
|
+
message: `Refusing unsafe copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2141
|
+
})
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
if (!existsSync(hostPath)) {
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
yield* Effect.tryPromise({
|
|
2148
|
+
try: async () => {
|
|
2149
|
+
await assertNoSymlinkComponents(
|
|
2150
|
+
copySourceDir,
|
|
2151
|
+
hostPath,
|
|
2152
|
+
"copy source"
|
|
2153
|
+
);
|
|
2154
|
+
},
|
|
2155
|
+
catch: (error) => new WorktreeError({
|
|
2156
|
+
message: `Refusing symlinked copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2157
|
+
})
|
|
2158
|
+
});
|
|
2159
|
+
yield* Effect.tryPromise({
|
|
2160
|
+
try: () => handle.copyIn(hostPath, sandboxPath),
|
|
2161
|
+
catch: (e) => new WorktreeError({
|
|
2162
|
+
message: `Failed to copy ${relativePath} into sandbox: ${e instanceof Error ? e.message : String(e)}`
|
|
2163
|
+
})
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
}).pipe(
|
|
2167
|
+
withTimeout(
|
|
2168
|
+
copyTimeoutMs,
|
|
2169
|
+
() => new CopyToWorktreeTimeoutError({
|
|
2170
|
+
message: `Copying paths to sandbox timed out after ${copyTimeoutMs}ms`,
|
|
2171
|
+
timeoutMs: copyTimeoutMs,
|
|
2172
|
+
paths: pathsToCopy
|
|
2173
|
+
})
|
|
2174
|
+
)
|
|
2175
|
+
);
|
|
2771
2176
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
};
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
runTeardowns();
|
|
2780
|
-
process.exit(1);
|
|
2781
|
-
};
|
|
2782
|
-
var attachListeners = () => {
|
|
2783
|
-
if (listenersInstalled) return;
|
|
2784
|
-
listenersInstalled = true;
|
|
2785
|
-
process.on("exit", handleExit);
|
|
2786
|
-
process.on("SIGINT", handleSignal);
|
|
2787
|
-
process.on("SIGTERM", handleSignal);
|
|
2788
|
-
};
|
|
2789
|
-
var detachListeners = () => {
|
|
2790
|
-
if (!listenersInstalled) return;
|
|
2791
|
-
listenersInstalled = false;
|
|
2792
|
-
process.removeListener("exit", handleExit);
|
|
2793
|
-
process.removeListener("SIGINT", handleSignal);
|
|
2794
|
-
process.removeListener("SIGTERM", handleSignal);
|
|
2795
|
-
};
|
|
2796
|
-
var registerShutdown = (teardown) => {
|
|
2797
|
-
teardownCallbacks.add(teardown);
|
|
2798
|
-
attachListeners();
|
|
2799
|
-
let active = true;
|
|
2800
|
-
return () => {
|
|
2801
|
-
if (!active) return;
|
|
2802
|
-
active = false;
|
|
2803
|
-
teardownCallbacks.delete(teardown);
|
|
2804
|
-
if (teardownCallbacks.size === 0) detachListeners();
|
|
2805
|
-
};
|
|
2806
|
-
};
|
|
2177
|
+
return {
|
|
2178
|
+
handle,
|
|
2179
|
+
sandbox: makeSandboxFromHandle(handle),
|
|
2180
|
+
worktreePath: handle.worktreePath
|
|
2181
|
+
};
|
|
2182
|
+
}).pipe(Effect.onError(() => Effect.promise(() => handle.close())));
|
|
2183
|
+
});
|
|
2807
2184
|
|
|
2808
|
-
export { ACTIVATION_LABEL, AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, RUNNER_DIR,
|
|
2809
|
-
//# sourceMappingURL=chunk-
|
|
2810
|
-
//# sourceMappingURL=chunk-
|
|
2185
|
+
export { ACTIVATION_LABEL, AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CopyToWorktreeError, CopyToWorktreeTimeoutError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, RUNNER_DIR, RUNTIME_NAMESPACE, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertExcludesRepositoryRunner, assertNoSymlinkComponents, assertProtectedDirectoryIdentities, create, createIsolatedSandboxProvider, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, pruneStale, registerShutdown, remove, repositoryRunnerEnvironment, resolveCwd, resolveSafeRelativePath, runHostHooks, shellQuote, startSandbox, syncOut, toSessionTransferHandle, withSandboxLifecycle, withTimeout };
|
|
2186
|
+
//# sourceMappingURL=chunk-57EEKW3R.js.map
|
|
2187
|
+
//# sourceMappingURL=chunk-57EEKW3R.js.map
|