@madarco/agentbox 0.4.0 → 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/dist/{chunk-3NCUES35.js → chunk-6VTAPD4H.js} +123 -112
- package/dist/chunk-6VTAPD4H.js.map +1 -0
- package/dist/{chunk-J35IH7W5.js → chunk-7J5AJLWG.js} +61 -23
- package/dist/chunk-7J5AJLWG.js.map +1 -0
- package/dist/{chunk-3JKQNOXP.js → chunk-FJNIFTWK.js} +66 -65
- package/dist/chunk-FJNIFTWK.js.map +1 -0
- package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
- package/dist/chunk-HPZMD5DE.js.map +1 -0
- package/dist/{chunk-MOC54XL6.js → chunk-PXUBE5KS.js} +376 -245
- package/dist/chunk-PXUBE5KS.js.map +1 -0
- package/dist/{chunk-SOMIKEN2.js → chunk-RFC5F5HR.js} +272 -214
- package/dist/chunk-RFC5F5HR.js.map +1 -0
- package/dist/create-AHZ3GVEZ-TGEDL7UX.js +15 -0
- package/dist/index.js +2760 -1857
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-YTMZYKOE-TD5S5FTS.js → lifecycle-LFOL6YFM-TCHDX3J5.js} +5 -5
- package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js +19 -0
- package/package.json +3 -2
- package/runtime/docker/Dockerfile.box +53 -20
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +39 -50
- package/runtime/docker/packages/ctl/dist/bin.cjs +219 -148
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +42 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +26 -15
- package/runtime/relay/bin.cjs +288 -12
- package/share/agentbox-setup/SKILL.md +39 -50
- package/dist/chunk-3JKQNOXP.js.map +0 -1
- package/dist/chunk-3NCUES35.js.map +0 -1
- package/dist/chunk-IDR4HVIC.js.map +0 -1
- package/dist/chunk-J35IH7W5.js.map +0 -1
- package/dist/chunk-MOC54XL6.js.map +0 -1
- package/dist/chunk-SOMIKEN2.js.map +0 -1
- package/dist/create-SE6H4B5U-IWAZHJHV.js +0 -15
- package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
- /package/dist/{create-SE6H4B5U-IWAZHJHV.js.map → create-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
- /package/dist/{lifecycle-YTMZYKOE-TD5S5FTS.js.map → lifecycle-LFOL6YFM-TCHDX3J5.js.map} +0 -0
- /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
- /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-Z4BVJODD-HEC4TMUZ.js.map} +0 -0
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
ConfigError,
|
|
4
4
|
VNC_CONTAINER_PORT,
|
|
5
5
|
WEB_CONTAINER_PORT,
|
|
6
|
+
bindWorktrees,
|
|
6
7
|
buildClaudeMounts,
|
|
7
8
|
buildIdeMounts,
|
|
8
|
-
|
|
9
|
+
chownGitBindParents,
|
|
10
|
+
collectRepoCarryOver,
|
|
9
11
|
createSnapshot,
|
|
10
12
|
cursorServerVolumeName,
|
|
11
13
|
detectGitRepos,
|
|
@@ -15,43 +17,45 @@ import {
|
|
|
15
17
|
ensureRelay,
|
|
16
18
|
generateRelayToken,
|
|
17
19
|
generateVncPassword,
|
|
20
|
+
gitWorktreePathFor,
|
|
18
21
|
launchCtlDaemon,
|
|
19
22
|
launchDockerdDaemon,
|
|
20
23
|
launchVncDaemon,
|
|
21
24
|
loadConfig,
|
|
22
|
-
|
|
25
|
+
pickFreshBranch,
|
|
23
26
|
registerBoxWithRelay,
|
|
24
27
|
rehydrateRelayRegistry,
|
|
25
28
|
repairIdeOwnership,
|
|
26
29
|
resolveClaudeVolume,
|
|
30
|
+
seedSetupSkillIntoVolume,
|
|
31
|
+
seedWorkspace,
|
|
32
|
+
seedWorkspaceFromDir,
|
|
27
33
|
snapshotPathFor,
|
|
28
|
-
verifyOverlay,
|
|
29
34
|
vscodeServerVolumeName
|
|
30
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-PXUBE5KS.js";
|
|
31
36
|
import {
|
|
32
37
|
allocateProjectIndex,
|
|
33
38
|
readState,
|
|
34
39
|
recordBox
|
|
35
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-HPZMD5DE.js";
|
|
36
41
|
import {
|
|
37
|
-
CHECKPOINT_MOUNT,
|
|
38
42
|
CONTAINER_EXPORT_MERGED,
|
|
39
|
-
CONTAINER_EXPORT_UPPER,
|
|
40
43
|
DEFAULT_BOX_IMAGE,
|
|
41
44
|
DEFAULT_ENV_PATTERNS,
|
|
42
45
|
boxRunDirFor,
|
|
43
46
|
containerExists,
|
|
44
47
|
copyHostEnvFilesToBox,
|
|
48
|
+
copyHostFilesToBox,
|
|
45
49
|
dockerInfo,
|
|
46
50
|
dockerStorageDriver,
|
|
47
51
|
ensureImage,
|
|
48
52
|
ensureVolume,
|
|
49
53
|
publishedHostPort,
|
|
50
|
-
|
|
54
|
+
resolveCheckpoint,
|
|
51
55
|
runBox
|
|
52
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-RFC5F5HR.js";
|
|
53
57
|
|
|
54
|
-
// ../../packages/sandbox-docker/dist/chunk-
|
|
58
|
+
// ../../packages/sandbox-docker/dist/chunk-NCSJPHDB.js
|
|
55
59
|
import { randomBytes } from "crypto";
|
|
56
60
|
import { mkdir, stat } from "fs/promises";
|
|
57
61
|
import { homedir } from "os";
|
|
@@ -147,11 +151,29 @@ async function createBox(opts) {
|
|
|
147
151
|
}
|
|
148
152
|
await dockerInfo();
|
|
149
153
|
log("docker daemon reachable");
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
let checkpointImage;
|
|
155
|
+
let checkpointSource;
|
|
156
|
+
let restoredWorktrees;
|
|
157
|
+
if (opts.checkpointRef) {
|
|
158
|
+
const projectRootForCkpt = opts.projectRoot ?? workspace;
|
|
159
|
+
const head = await resolveCheckpoint(projectRootForCkpt, opts.checkpointRef);
|
|
160
|
+
if (!head) {
|
|
161
|
+
throw new Error(`checkpoint not found: ${opts.checkpointRef}`);
|
|
162
|
+
}
|
|
163
|
+
checkpointImage = head.manifest.image;
|
|
164
|
+
const chain = [head.name, ...head.manifest.parents];
|
|
165
|
+
checkpointSource = { ref: opts.checkpointRef, type: head.manifest.type, chain };
|
|
166
|
+
restoredWorktrees = head.manifest.worktrees;
|
|
167
|
+
log(
|
|
168
|
+
`starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;
|
|
172
|
+
const ensureRef = checkpointImage ? opts.image ?? DEFAULT_BOX_IMAGE : imageRef;
|
|
173
|
+
const { built } = await ensureImage(ensureRef, {
|
|
152
174
|
onProgress: (line) => log(`[image] ${line}`)
|
|
153
175
|
});
|
|
154
|
-
log(built ? `built image ${
|
|
176
|
+
log(built ? `built image ${ensureRef}` : `using cached image ${imageRef}`);
|
|
155
177
|
let relayUp = false;
|
|
156
178
|
try {
|
|
157
179
|
await ensureRelay({ onLog: log });
|
|
@@ -168,78 +190,54 @@ async function createBox(opts) {
|
|
|
168
190
|
if (await containerExists(containerName)) {
|
|
169
191
|
throw new Error(`container ${containerName} already exists; remove it first`);
|
|
170
192
|
}
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
let projectIndex;
|
|
194
|
+
if (opts.projectRoot) {
|
|
195
|
+
projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);
|
|
196
|
+
}
|
|
197
|
+
const repoCarryOvers = [];
|
|
173
198
|
const gitWorktreeRecords = [];
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (repos.length > 0) {
|
|
177
|
-
log(
|
|
178
|
-
`detected ${String(repos.length)} git repo(s): ` + repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? "@" + r.relPathFromWorkspace : ""}`).join(", ")
|
|
179
|
-
);
|
|
199
|
+
if (checkpointImage && restoredWorktrees && restoredWorktrees.length > 0) {
|
|
200
|
+
gitWorktreeRecords.push(...restoredWorktrees);
|
|
180
201
|
}
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
});
|
|
199
|
-
if (r.kind === "nested") {
|
|
200
|
-
nestedWorktreeBinds.push({
|
|
202
|
+
if (!checkpointImage) {
|
|
203
|
+
const repos = await detectGitRepos(workspace);
|
|
204
|
+
if (repos.length > 0) {
|
|
205
|
+
log(
|
|
206
|
+
`detected ${String(repos.length)} git repo(s): ` + repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? "@" + r.relPathFromWorkspace : ""}`).join(", ")
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
for (const r of repos) {
|
|
210
|
+
const branchBase = r.kind === "root" ? `agentbox/${name}` : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, "_")}`;
|
|
211
|
+
const branch = await pickFreshBranch(r.hostMainRepo, branchBase);
|
|
212
|
+
const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
|
|
213
|
+
const gitWorktreePath = gitWorktreePathFor(branch);
|
|
214
|
+
const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);
|
|
215
|
+
repoCarryOvers.push(carry);
|
|
216
|
+
gitWorktreeRecords.push({
|
|
217
|
+
kind: r.kind,
|
|
218
|
+
hostMainRepo: r.hostMainRepo,
|
|
201
219
|
containerPath,
|
|
202
|
-
|
|
220
|
+
gitWorktreePath,
|
|
221
|
+
branch,
|
|
222
|
+
relPathFromWorkspace: r.relPathFromWorkspace
|
|
203
223
|
});
|
|
204
224
|
}
|
|
205
225
|
}
|
|
206
|
-
let lowerPath = workspace;
|
|
207
|
-
const rootWorktree = gitWorktreeRecords.find((w) => w.kind === "root");
|
|
208
|
-
if (rootWorktree) {
|
|
209
|
-
lowerPath = rootWorktree.hostWorktreeDir;
|
|
210
|
-
log(`using worktree as overlay lower: ${lowerPath}`);
|
|
211
|
-
}
|
|
212
226
|
let snapshotDir = null;
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
const snapshotIsUseful = !checkpointImage && repoCarryOvers.length === 0;
|
|
228
|
+
if (opts.useSnapshot && snapshotIsUseful) {
|
|
229
|
+
snapshotDir = snapshotPathFor({ id, name, projectIndex });
|
|
215
230
|
log(`cloning workspace to ${snapshotDir} (APFS clone where available)`);
|
|
216
|
-
const snap = await createSnapshot({ source:
|
|
231
|
+
const snap = await createSnapshot({ source: workspace, destination: snapshotDir });
|
|
217
232
|
log(`pruned ${snap.prunedPaths.length} platform-dependent dirs from snapshot`);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
let lowerDirs;
|
|
221
|
-
let checkpointVolume;
|
|
222
|
-
let checkpointSource;
|
|
223
|
-
if (opts.checkpointRef) {
|
|
224
|
-
const projectRootForCkpt = opts.projectRoot ?? workspace;
|
|
225
|
-
const spec = await resolveCheckpointLower(projectRootForCkpt, opts.checkpointRef);
|
|
226
|
-
checkpointVolume = spec.volume;
|
|
227
|
-
const layerDirs = spec.subpaths.map((s) => `${CHECKPOINT_MOUNT}/${s}`);
|
|
228
|
-
lowerDirs = spec.type === "merged" ? layerDirs : [...layerDirs, "/host-src"];
|
|
229
|
-
checkpointSource = { ref: opts.checkpointRef, type: spec.type, chain: spec.chain };
|
|
230
|
-
log(
|
|
231
|
-
`starting from checkpoint ${opts.checkpointRef} (${spec.type}, ${String(spec.subpaths.length)} layer(s), volume ${spec.volume})`
|
|
232
|
-
);
|
|
233
|
+
} else if (opts.useSnapshot && !checkpointImage) {
|
|
234
|
+
log("skipping --host-snapshot: git worktree path reads content from .git, not from a workspace clone");
|
|
233
235
|
}
|
|
234
|
-
const upperVolume = `agentbox-upper-${id}`;
|
|
235
|
-
await ensureVolume(upperVolume);
|
|
236
236
|
await ensureIdeVolumes(id);
|
|
237
237
|
const dockerCacheShared = opts.docker?.sharedCache === true;
|
|
238
238
|
const dockerVolume = dockerVolumeName(id, dockerCacheShared);
|
|
239
239
|
await ensureVolume(dockerVolume);
|
|
240
|
-
log(
|
|
241
|
-
`prepared volumes ${upperVolume}, ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`
|
|
242
|
-
);
|
|
240
|
+
log(`prepared volumes ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`);
|
|
243
241
|
const ide = buildIdeMounts(id);
|
|
244
242
|
const claudeSpec = resolveClaudeVolume({
|
|
245
243
|
isolate: opts.claudeConfig?.isolate ?? false,
|
|
@@ -247,7 +245,7 @@ async function createBox(opts) {
|
|
|
247
245
|
});
|
|
248
246
|
const claudeEnsured = await ensureClaudeVolume(claudeSpec, {
|
|
249
247
|
syncFromHost: true,
|
|
250
|
-
image:
|
|
248
|
+
image: ensureRef,
|
|
251
249
|
hostWorkspace: workspace
|
|
252
250
|
});
|
|
253
251
|
if (claudeEnsured.synced) {
|
|
@@ -268,33 +266,24 @@ async function createBox(opts) {
|
|
|
268
266
|
} else {
|
|
269
267
|
log(`reusing volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
|
|
270
268
|
}
|
|
269
|
+
const seeded = await seedSetupSkillIntoVolume(claudeSpec.volume, ensureRef);
|
|
270
|
+
if (seeded.seeded) log(`seeded /agentbox-setup skill into ${claudeSpec.volume}`);
|
|
271
271
|
const claudeMounts = buildClaudeMounts(claudeSpec, process.env);
|
|
272
|
-
const boxDir = boxRunDirFor(id);
|
|
272
|
+
const boxDir = boxRunDirFor({ id, name, projectIndex });
|
|
273
273
|
const socketDir = join(boxDir, "run");
|
|
274
274
|
const socketPath = join(socketDir, "ctl.sock");
|
|
275
275
|
const mergedExportDir = join(boxDir, "workspace");
|
|
276
|
-
const upperExportDir = join(boxDir, "upper");
|
|
277
276
|
await mkdir(socketDir, { recursive: true });
|
|
278
277
|
await mkdir(mergedExportDir, { recursive: true });
|
|
279
|
-
await mkdir(upperExportDir, { recursive: true });
|
|
280
278
|
const extraVolumes = await buildIdentityMounts();
|
|
281
279
|
extraVolumes.push(...claudeMounts.extraVolumes);
|
|
282
280
|
extraVolumes.push(...ide.extraVolumes);
|
|
283
281
|
extraVolumes.push(`${socketDir}:/run/agentbox`);
|
|
284
282
|
extraVolumes.push(`${mergedExportDir}:${CONTAINER_EXPORT_MERGED}`);
|
|
285
|
-
extraVolumes.push(`${upperExportDir}:${CONTAINER_EXPORT_UPPER}`);
|
|
286
283
|
extraVolumes.push(`${dockerVolume}:/var/lib/docker`);
|
|
287
284
|
for (const w of gitWorktreeRecords) {
|
|
288
285
|
extraVolumes.push(`${w.hostMainRepo}/.git:${w.hostMainRepo}/.git`);
|
|
289
286
|
}
|
|
290
|
-
for (const w of gitWorktreeRecords) {
|
|
291
|
-
if (w.kind === "nested") {
|
|
292
|
-
extraVolumes.push(`${w.hostWorktreeDir}:/agentbox-worktrees/${w.relPathFromWorkspace}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (checkpointVolume) {
|
|
296
|
-
extraVolumes.push(`${checkpointVolume}:${CHECKPOINT_MOUNT}:ro`);
|
|
297
|
-
}
|
|
298
287
|
for (const v of extraVolumes) log(`mounting agent dir: ${v}`);
|
|
299
288
|
const relayToken = generateRelayToken();
|
|
300
289
|
if (relayUp) {
|
|
@@ -305,6 +294,7 @@ async function createBox(opts) {
|
|
|
305
294
|
name,
|
|
306
295
|
containerName,
|
|
307
296
|
createdAt,
|
|
297
|
+
projectIndex,
|
|
308
298
|
worktrees: gitWorktreeRecords
|
|
309
299
|
});
|
|
310
300
|
log(`registered box token with relay`);
|
|
@@ -326,10 +316,6 @@ async function createBox(opts) {
|
|
|
326
316
|
const webPortMappings = [
|
|
327
317
|
{ hostPort: 0, containerPort: WEB_CONTAINER_PORT, hostIp: "127.0.0.1" }
|
|
328
318
|
];
|
|
329
|
-
let projectIndex;
|
|
330
|
-
if (opts.projectRoot) {
|
|
331
|
-
projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);
|
|
332
|
-
}
|
|
333
319
|
const agentboxEnv = {
|
|
334
320
|
AGENTBOX: "1",
|
|
335
321
|
AGENTBOX_BOX_NAME: name,
|
|
@@ -355,8 +341,6 @@ async function createBox(opts) {
|
|
|
355
341
|
await runBox({
|
|
356
342
|
name: containerName,
|
|
357
343
|
image: imageRef,
|
|
358
|
-
lowerPath,
|
|
359
|
-
upperVolume,
|
|
360
344
|
extraVolumes,
|
|
361
345
|
limits: effectiveLimits,
|
|
362
346
|
portMappings: [...vncPortMappings, ...webPortMappings],
|
|
@@ -370,27 +354,45 @@ async function createBox(opts) {
|
|
|
370
354
|
}
|
|
371
355
|
});
|
|
372
356
|
log(`container ${containerName} started`);
|
|
357
|
+
if (gitWorktreeRecords.length > 0) {
|
|
358
|
+
await chownGitBindParents({
|
|
359
|
+
container: containerName,
|
|
360
|
+
hostMainRepos: gitWorktreeRecords.map((w) => w.hostMainRepo),
|
|
361
|
+
onLog: log
|
|
362
|
+
});
|
|
363
|
+
}
|
|
373
364
|
const boxEnv = await writeBoxEnvFile(containerName, boxEnvForFile);
|
|
374
365
|
if (boxEnv.ok) log("wrote /etc/agentbox/box.env");
|
|
375
366
|
else log(`writing /etc/agentbox/box.env failed: ${boxEnv.reason}`);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
367
|
+
if (!checkpointImage) {
|
|
368
|
+
if (repoCarryOvers.length > 0) {
|
|
369
|
+
try {
|
|
370
|
+
await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });
|
|
371
|
+
log("seeded /workspace from in-container git worktree(s)");
|
|
372
|
+
} catch (err) {
|
|
373
|
+
log(
|
|
374
|
+
`seedWorkspace failed; leaving ${containerName} running so you can inspect it`
|
|
375
|
+
);
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
const source = snapshotDir ?? workspace;
|
|
380
|
+
await seedWorkspaceFromDir({ container: containerName, hostSource: source, onLog: log });
|
|
381
381
|
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
382
|
+
} else if (restoredWorktrees && restoredWorktrees.length > 0) {
|
|
383
|
+
await bindWorktrees(
|
|
384
|
+
containerName,
|
|
385
|
+
restoredWorktrees.map((w) => ({
|
|
386
|
+
kind: w.kind,
|
|
387
|
+
containerPath: w.containerPath,
|
|
388
|
+
gitWorktreePath: w.gitWorktreePath
|
|
389
|
+
})),
|
|
390
|
+
log
|
|
391
|
+
);
|
|
392
|
+
log("re-bound /workspace from checkpoint image");
|
|
393
|
+
} else {
|
|
394
|
+
log("using /workspace from checkpoint image (no worktrees recorded; no rebind)");
|
|
392
395
|
}
|
|
393
|
-
log("overlay verified");
|
|
394
396
|
await repairIdeOwnership(containerName);
|
|
395
397
|
log(".vscode-server + .cursor-server ownership verified");
|
|
396
398
|
const ctl = await launchCtlDaemon(containerName, socketPath);
|
|
@@ -437,6 +439,18 @@ ${detail}`);
|
|
|
437
439
|
});
|
|
438
440
|
log(copied > 0 ? `copied ${String(copied)} env/config file(s)` : "no env/config files found");
|
|
439
441
|
}
|
|
442
|
+
if (opts.envFilesToImport && opts.envFilesToImport.length > 0) {
|
|
443
|
+
log(`copying ${String(opts.envFilesToImport.length)} selected env/config file(s) into /workspace`);
|
|
444
|
+
const { copied } = await copyHostFilesToBox({
|
|
445
|
+
container: containerName,
|
|
446
|
+
workspaceDir: workspace,
|
|
447
|
+
files: opts.envFilesToImport,
|
|
448
|
+
onLog: log
|
|
449
|
+
});
|
|
450
|
+
if (copied !== opts.envFilesToImport.length) {
|
|
451
|
+
log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
440
454
|
let vncHostPort = null;
|
|
441
455
|
if (vncEnabled) {
|
|
442
456
|
const vnc = await launchVncDaemon(containerName);
|
|
@@ -457,8 +471,6 @@ ${detail}`);
|
|
|
457
471
|
container: containerName,
|
|
458
472
|
image: imageRef,
|
|
459
473
|
workspacePath: workspace,
|
|
460
|
-
lowerPath,
|
|
461
|
-
upperVolume,
|
|
462
474
|
snapshotDir,
|
|
463
475
|
socketPath,
|
|
464
476
|
claudeConfigVolume: claudeSpec.volume,
|
|
@@ -478,14 +490,13 @@ ${detail}`);
|
|
|
478
490
|
dockerCacheShared: dockerCacheShared || void 0,
|
|
479
491
|
projectRoot: opts.projectRoot,
|
|
480
492
|
projectIndex,
|
|
481
|
-
|
|
482
|
-
checkpointVolume,
|
|
493
|
+
checkpointImage,
|
|
483
494
|
checkpointSource,
|
|
484
495
|
resourceLimits: persistableLimits(effectiveLimits),
|
|
485
496
|
createdAt
|
|
486
497
|
};
|
|
487
498
|
await recordBox(record);
|
|
488
|
-
return { record,
|
|
499
|
+
return { record, imageBuilt: built };
|
|
489
500
|
}
|
|
490
501
|
|
|
491
502
|
export {
|
|
@@ -493,4 +504,4 @@ export {
|
|
|
493
504
|
defaultBoxName,
|
|
494
505
|
createBox
|
|
495
506
|
};
|
|
496
|
-
//# sourceMappingURL=chunk-
|
|
507
|
+
//# sourceMappingURL=chunk-6VTAPD4H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../packages/sandbox-docker/src/create.ts","../../../packages/sandbox-docker/src/box-env.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { mkdir, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { execa } from 'execa';\nimport { ConfigError, loadConfig } from '@agentbox/ctl';\nimport {\n buildClaudeMounts,\n ensureClaudeVolume,\n resolveClaudeVolume,\n seedSetupSkillIntoVolume,\n} from './claude.js';\nimport {\n type BoxLimitSpec,\n containerExists,\n dockerInfo,\n dockerStorageDriver,\n ensureVolume,\n publishedHostPort,\n runBox,\n} from './docker.js';\nimport { dockerVolumeName, launchDockerdDaemon } from './dockerd.js';\nimport { generateVncPassword, launchVncDaemon, VNC_CONTAINER_PORT } from './vnc.js';\nimport { WEB_CONTAINER_PORT } from './web.js';\nimport { detectGitRepos, pickFreshBranch } from './git-worktree.js';\nimport {\n bindWorktrees,\n chownGitBindParents,\n collectRepoCarryOver,\n gitWorktreePathFor,\n seedWorkspace,\n seedWorkspaceFromDir,\n type RepoCarryOver,\n} from './in-box-git.js';\nimport {\n CONTAINER_EXPORT_MERGED,\n DEFAULT_ENV_PATTERNS,\n boxRunDirFor,\n copyHostEnvFilesToBox,\n copyHostFilesToBox,\n} from './host-export.js';\nimport { DEFAULT_BOX_IMAGE, ensureImage } from './image.js';\nimport {\n allocateProjectIndex,\n readState,\n recordBox,\n type BoxRecord,\n type GitWorktreeRecord,\n} from './state.js';\nimport { createSnapshot, snapshotPathFor } from './snapshot.js';\nimport { resolveCheckpoint } from './checkpoint.js';\nimport { launchCtlDaemon } from './ctl.js';\nimport { writeBoxEnvFile } from './box-env.js';\nimport {\n ensureRelay,\n generateRelayToken,\n registerBoxWithRelay,\n rehydrateRelayRegistry,\n} from './relay.js';\nimport {\n buildIdeMounts,\n cursorServerVolumeName,\n ensureIdeVolumes,\n repairIdeOwnership,\n vscodeServerVolumeName,\n} from './vscode.js';\n\nexport interface CreateBoxOptions {\n workspacePath: string;\n name?: string;\n /**\n * Take a `cp -c` APFS clone of the host workspace into\n * `~/.agentbox/snapshots/<id>/` before seeding `/workspace`. Stabilizes the\n * source of the tar pipe in the non-git case (and the untracked-file\n * pipe in the git case) against host edits during create. Effectively a\n * no-op when a git worktree is detected — the worktree's tracked content\n * comes from `.git`, not from a workspace copy.\n */\n useSnapshot: boolean;\n /**\n * Start the box from a project checkpoint (the `--snapshot <ref>` path).\n * Resolved against `projectRoot` (or `workspacePath` when unset). The\n * checkpoint is a local Docker *image* tag now; the box is created with\n * `docker run <ckpt-image>` and inherits a populated `/workspace`. No\n * `seedWorkspace` runs in this path.\n */\n checkpointRef?: string;\n image?: string;\n onLog?: (line: string) => void;\n /**\n * Claude Code config volume. When omitted, defaults to `{ isolate: false }` —\n * every box mounts the shared `agentbox-claude-config` volume at\n * /home/vscode/.claude so auth / skills / plugins persist across boxes.\n */\n claudeConfig?: { isolate: boolean };\n /** Extra env vars forwarded to the container (merged on top of claude env forwarding). */\n claudeEnv?: Record<string, string>;\n /**\n * When true, run `npm install -g @playwright/cli@latest` inside the box after\n * `/workspace` is seeded. agent-browser is always installed in the image;\n * this flag adds the Playwright CLI on top for boxes that need it.\n */\n withPlaywright?: boolean;\n /**\n * When true, copy the host's env/config files (DEFAULT_ENV_PATTERNS basename\n * globs — `.env*`, `secrets.toml`, `agentbox.yaml`, ...) into the box's\n * /workspace after seeding, bypassing gitignore. The reverse of `pull env`.\n * One-shot at create time; the files persist in the container's writable\n * layer across pause/stop/start.\n */\n withEnv?: boolean;\n /**\n * Explicit relative-path file list to copy from `workspacePath` into the\n * box's /workspace after seeding (no glob expansion, no scan — the list is\n * pre-vetted, e.g. picked by the wizard's multiselect). Independent of\n * `withEnv`: if both are set, both run (idempotent on overlapping files).\n * One-shot at create time; persists across pause/stop/start.\n */\n envFilesToImport?: string[];\n /**\n * VNC stack (Xvnc on :1 + websockify serving noVNC on container :6080).\n * Defaults to enabled. The CLI exposes `--no-vnc` for opt-out. Disabling\n * skips port mapping + password generation + the in-container supervisor\n * launch; the apt-installed binaries stay in the image but are unused.\n */\n vnc?: { enabled: boolean };\n /**\n * Docker-in-Docker. Always-on (the in-box dockerd is part of the box\n * surface). When `sharedCache` is true the per-box `agentbox-docker-<id>`\n * volume is replaced with the shared `agentbox-docker-cache` volume — image\n * layers persist across boxes (and `destroy`/`prune` won't remove it).\n */\n docker?: { sharedCache: boolean };\n /**\n * Absolute host path of the cwd's project at create time. When provided,\n * `createBox` stamps `projectRoot` + an allocated `projectIndex` on the\n * BoxRecord so the CLI can auto-pick / resolve by index. The CLI computes\n * this via `findProjectRoot(workspacePath)` from `@agentbox/config`; this\n * package stays free of the config dep. Omit for unowned boxes created\n * directly via the programmatic API.\n */\n projectRoot?: string;\n /**\n * Container resource ceilings (engine-agnostic: bytes / fractional cpus /\n * pid count / raw disk size string). Absent fields = unlimited. `disk` is\n * best-effort: dropped (with a warning via `onLog`) when the engine's\n * storage driver can't enforce `--storage-opt size=` (overlay2 / macOS).\n */\n limits?: BoxLimitSpec;\n}\n\nexport interface CreatedBox {\n record: BoxRecord;\n imageBuilt: boolean;\n}\n\n/**\n * Compact the engine-applied limits into the BoxRecord shape: only fields that\n * actually constrain the box (>0 / non-empty). Returns undefined when nothing\n * was applied so legacy/unlimited boxes stay free of the field.\n */\nfunction persistableLimits(\n lim: BoxLimitSpec | undefined,\n): BoxRecord['resourceLimits'] | undefined {\n if (!lim) return undefined;\n const out: NonNullable<BoxRecord['resourceLimits']> = {};\n if (lim.memoryBytes && lim.memoryBytes > 0) out.memoryBytes = Math.floor(lim.memoryBytes);\n if (lim.cpus && lim.cpus > 0) out.cpus = lim.cpus;\n if (lim.pidsLimit && lim.pidsLimit > 0) out.pidsLimit = Math.floor(lim.pidsLimit);\n if (lim.disk) out.disk = lim.disk;\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\nfunction generateBoxId(): string {\n return randomBytes(4).toString('hex');\n}\n\nexport function sanitizeBasename(workspacePath: string): string {\n const raw = basename(resolve(workspacePath));\n return raw\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^[-._]+|[-._]+$/g, '')\n .slice(0, 30)\n .replace(/[-._]+$/, '');\n}\n\nexport function defaultBoxName(workspacePath: string, id: string): string {\n const base = sanitizeBasename(workspacePath);\n return base.length > 0 ? `${base}-${id}` : id;\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\n// ~/.claude is intentionally NOT in this list: it lives in the named volume\n// `agentbox-claude-config` (see resolveClaudeVolume / ensureClaudeVolume) so\n// auth persists inside the container without leaking host state. Only\n// non-claude identity files are bind-mounted from the host.\nasync function buildIdentityMounts(): Promise<string[]> {\n const home = homedir();\n const candidates: Array<{ src: string; dst: string; readOnly: boolean }> = [\n { src: join(home, '.codex'), dst: '/home/vscode/.codex', readOnly: false },\n { src: join(home, '.gitconfig'), dst: '/home/vscode/.gitconfig', readOnly: true },\n ];\n const out: string[] = [];\n for (const c of candidates) {\n if (await pathExists(c.src)) {\n out.push(`${c.src}:${c.dst}${c.readOnly ? ':ro' : ''}`);\n }\n }\n return out;\n}\n\nexport async function createBox(opts: CreateBoxOptions): Promise<CreatedBox> {\n const log = opts.onLog ?? (() => {});\n const workspace = resolve(opts.workspacePath);\n if (!(await pathExists(workspace))) {\n throw new Error(`workspace does not exist: ${workspace}`);\n }\n\n // Pre-flight agentbox.yaml validation on the host so the user sees the real\n // ConfigError instead of an opaque \"socket did not appear\" timeout from the\n // detached daemon exec later. The daemon re-validates inside the box anyway\n // — defence in depth, and necessary because the file lives in the\n // container's writable layer and can be edited after create.\n const cfgPath = join(workspace, 'agentbox.yaml');\n if (await pathExists(cfgPath)) {\n try {\n const cfg = await loadConfig(cfgPath);\n log(`agentbox.yaml validated (${String(cfg.services.length)} service(s))`);\n } catch (err) {\n if (err instanceof ConfigError) {\n throw new Error(`agentbox.yaml validation failed:\\n ${err.message}`);\n }\n throw err;\n }\n }\n\n await dockerInfo();\n log('docker daemon reachable');\n\n // Checkpoint resolution happens *before* image ensure because a checkpoint\n // image replaces the base image as the docker-run base. resolveCheckpoint\n // returns null on miss; we error with the ref so the user can fix it.\n let checkpointImage: string | undefined;\n let checkpointSource: BoxRecord['checkpointSource'];\n let restoredWorktrees: GitWorktreeRecord[] | undefined;\n if (opts.checkpointRef) {\n const projectRootForCkpt = opts.projectRoot ?? workspace;\n const head = await resolveCheckpoint(projectRootForCkpt, opts.checkpointRef);\n if (!head) {\n throw new Error(`checkpoint not found: ${opts.checkpointRef}`);\n }\n checkpointImage = head.manifest.image;\n // Chain: head first then its parents (base-most last). For a flattened\n // checkpoint this collapses to a single-entry chain.\n const chain = [head.name, ...head.manifest.parents];\n checkpointSource = { ref: opts.checkpointRef, type: head.manifest.type, chain };\n // The source's per-worktree paths persisted on the manifest so we can\n // re-establish the /workspace bind mount(s) after `docker run` (docker\n // commit doesn't capture bind-mount content, so the image's /workspace\n // is empty until we re-bind).\n restoredWorktrees = head.manifest.worktrees;\n log(\n `starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`,\n );\n }\n\n const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;\n // ensureImage only acts on the base image; checkpoint images are local-only\n // and must already exist (they were created by `agentbox checkpoint`).\n const ensureRef = checkpointImage ? (opts.image ?? DEFAULT_BOX_IMAGE) : imageRef;\n const { built } = await ensureImage(ensureRef, {\n onProgress: (line) => log(`[image] ${line}`),\n });\n log(built ? `built image ${ensureRef}` : `using cached image ${imageRef}`);\n\n // Bring up the host relay before the box so the box can post events\n // immediately on boot. Best-effort — a relay outage shouldn't block create.\n // Always re-push known box tokens after ensure: the relay's registry is\n // in-memory, so a daemon restart or `docker restart agentbox-relay` between\n // CLI invocations leaves it empty. Repushing is idempotent and cheap.\n let relayUp = false;\n try {\n await ensureRelay({ onLog: log });\n const existing = await readState();\n await rehydrateRelayRegistry(existing.boxes);\n relayUp = true;\n } catch (err) {\n log(`relay unavailable: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n const id = generateBoxId();\n const name = opts.name ?? defaultBoxName(workspace, id);\n const containerName = `agentbox-${name}`;\n const createdAt = new Date().toISOString();\n if (await containerExists(containerName)) {\n throw new Error(`container ${containerName} already exists; remove it first`);\n }\n\n // Per-project monotonic index. Allocated *here* so it can flow into the\n // box / snapshot dir segments (`<id>-<n>-<mnemonic>`) and the\n // `AGENTBOX_PROJECT_INDEX` container env var. Pre-feature legacy boxes\n // never pass `projectRoot`; those records keep `projectIndex` undefined and\n // the dir segments fall back to `<id>-<mnemonic>`.\n let projectIndex: number | undefined;\n if (opts.projectRoot) {\n projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);\n }\n\n // Repo detection + host-side carry-over capture. Branches are picked here\n // (against the host main repos' refs) so they're recorded on the BoxRecord\n // regardless of whether the in-container `git worktree add` succeeds later.\n // When restoring from a checkpoint, the source's per-worktree records are\n // restored from the manifest *here* (not after `docker run`) so the\n // `.git/` bind-mounts in `extraVolumes` know which host main repos to\n // wire up — without those binds the in-container `/workspace/.git` would\n // resolve to a path that doesn't exist in the new container.\n const repoCarryOvers: RepoCarryOver[] = [];\n const gitWorktreeRecords: GitWorktreeRecord[] = [];\n if (checkpointImage && restoredWorktrees && restoredWorktrees.length > 0) {\n gitWorktreeRecords.push(...restoredWorktrees);\n }\n if (!checkpointImage) {\n const repos = await detectGitRepos(workspace);\n if (repos.length > 0) {\n log(\n `detected ${String(repos.length)} git repo(s): ` +\n repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? '@' + r.relPathFromWorkspace : ''}`).join(', '),\n );\n }\n for (const r of repos) {\n const branchBase =\n r.kind === 'root'\n ? `agentbox/${name}`\n : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, '_')}`;\n const branch = await pickFreshBranch(r.hostMainRepo, branchBase);\n const containerPath =\n r.kind === 'root' ? '/workspace' : `/workspace/${r.relPathFromWorkspace}`;\n const gitWorktreePath = gitWorktreePathFor(branch);\n const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);\n repoCarryOvers.push(carry);\n gitWorktreeRecords.push({\n kind: r.kind,\n hostMainRepo: r.hostMainRepo,\n containerPath,\n gitWorktreePath,\n branch,\n relPathFromWorkspace: r.relPathFromWorkspace,\n });\n }\n }\n\n // --host-snapshot: APFS clone the workspace into a per-box scratch dir.\n // Only the no-git, no-checkpoint path actually consumes the clone (as the\n // source of the tar pipe in seedWorkspaceFromDir). For the git path the\n // worktree content comes from `.git`'s object DB (bind-mounted) and the\n // untracked-file tar pipe reads from the live host main repo — neither\n // touches `snapshotDir`, so we skip it. For checkpoint restore there's no\n // seedWorkspace at all. Kept on the BoxRecord so destroyBox can clean it up.\n let snapshotDir: string | null = null;\n const snapshotIsUseful = !checkpointImage && repoCarryOvers.length === 0;\n if (opts.useSnapshot && snapshotIsUseful) {\n snapshotDir = snapshotPathFor({ id, name, projectIndex });\n log(`cloning workspace to ${snapshotDir} (APFS clone where available)`);\n const snap = await createSnapshot({ source: workspace, destination: snapshotDir });\n log(`pruned ${snap.prunedPaths.length} platform-dependent dirs from snapshot`);\n } else if (opts.useSnapshot && !checkpointImage) {\n log('skipping --host-snapshot: git worktree path reads content from .git, not from a workspace clone');\n }\n\n await ensureIdeVolumes(id);\n const dockerCacheShared = opts.docker?.sharedCache === true;\n const dockerVolume = dockerVolumeName(id, dockerCacheShared);\n await ensureVolume(dockerVolume);\n log(`prepared volumes ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`);\n const ide = buildIdeMounts(id);\n\n // Claude Code config volume. Shared by default so users sign in once across\n // every box; --isolate-claude-config opts into a per-box volume. Either way,\n // the host's ~/.claude is the authoritative source: we rsync host -> volume\n // on every create so updates on the host (new login, new skills, new MCP)\n // flow into the next box. Sync is additive — box-only state (session logs,\n // etc.) is preserved.\n const claudeSpec = resolveClaudeVolume({\n isolate: opts.claudeConfig?.isolate ?? false,\n boxId: id,\n });\n const claudeEnsured = await ensureClaudeVolume(claudeSpec, {\n syncFromHost: true,\n image: ensureRef,\n hostWorkspace: workspace,\n });\n if (claudeEnsured.synced) {\n log(`synced ${claudeSpec.volume} from ~/.claude`);\n if ((claudeEnsured.filteredHookCount ?? 0) > 0) {\n log(\n `filtered ${String(claudeEnsured.filteredHookCount)} host-path hook(s) (paths under ~/)`,\n );\n }\n if (claudeEnsured.clearedInstallMethod) {\n log(\"cleared host's installMethod from synced .claude.json (box uses the native installer)\");\n }\n if (claudeEnsured.aliasedProjectKey) {\n log(`aliased project state for ${workspace} -> /workspace in synced .claude.json`);\n }\n } else if (claudeEnsured.created) {\n log(`created empty volume ${claudeSpec.volume} (no host ~/.claude to sync)`);\n } else {\n log(`reusing volume ${claudeSpec.volume} (no host ~/.claude to sync)`);\n }\n // Box-only: seed /agentbox-setup into the volume from the image. Never\n // touches the host's ~/.claude. Skipped if a copy already exists.\n const seeded = await seedSetupSkillIntoVolume(claudeSpec.volume, ensureRef);\n if (seeded.seeded) log(`seeded /agentbox-setup skill into ${claudeSpec.volume}`);\n const claudeMounts = buildClaudeMounts(claudeSpec, process.env);\n\n const boxDir = boxRunDirFor({ id, name, projectIndex });\n const socketDir = join(boxDir, 'run');\n const socketPath = join(socketDir, 'ctl.sock');\n // Per-box host dir that `agentbox open` refreshes the merged /workspace\n // into. Bound in at create time so `docker exec rsync` can write straight\n // to the host filesystem — no container restart needed.\n const mergedExportDir = join(boxDir, 'workspace');\n await mkdir(socketDir, { recursive: true });\n await mkdir(mergedExportDir, { recursive: true });\n\n const extraVolumes = await buildIdentityMounts();\n extraVolumes.push(...claudeMounts.extraVolumes);\n extraVolumes.push(...ide.extraVolumes);\n extraVolumes.push(`${socketDir}:/run/agentbox`);\n extraVolumes.push(`${mergedExportDir}:${CONTAINER_EXPORT_MERGED}`);\n // In-box dockerd's data root. Per-box (`agentbox-docker-<id>`, wiped on\n // destroy) by default; shared (`agentbox-docker-cache`, preserved) when\n // `box.dockerCacheShared` is set.\n extraVolumes.push(`${dockerVolume}:/var/lib/docker`);\n // Bind-mount each main repo's `.git/` at its identical absolute host path,\n // RW. The in-container `git worktree add` writes to <main>/.git/worktrees/\n // and the agent's commits write to refs/objects; both have to hit the same\n // path on host and inside the container so `git push` from the host main\n // repo sees the new commits without further sync.\n for (const w of gitWorktreeRecords) {\n extraVolumes.push(`${w.hostMainRepo}/.git:${w.hostMainRepo}/.git`);\n }\n for (const v of extraVolumes) log(`mounting agent dir: ${v}`);\n\n // Per-box bearer token for the host relay. Register *before* runBox so the\n // box's supervisor can post on boot. Skip if the relay isn't reachable —\n // the box still works, it just won't deliver events to the host.\n const relayToken = generateRelayToken();\n if (relayUp) {\n try {\n await registerBoxWithRelay({\n boxId: id,\n token: relayToken,\n name,\n containerName,\n createdAt,\n projectIndex,\n worktrees: gitWorktreeRecords,\n });\n log(`registered box token with relay`);\n } catch (err) {\n log(`relay register failed: ${err instanceof Error ? err.message : String(err)}`);\n relayUp = false;\n }\n }\n const relayEnv: Record<string, string> = relayUp\n ? {\n // host.docker.internal resolves to the host (where the relay node\n // process is running). The matching `--add-host` is set in runBox.\n AGENTBOX_RELAY_URL: `http://host.docker.internal:8787`,\n AGENTBOX_RELAY_TOKEN: relayToken,\n }\n : {};\n\n // VNC stack defaults on; the CLI surfaces `--no-vnc` for opt-out. Generate\n // the password and the port mapping up front so they're baked into the\n // container's env + `-p` flags before `docker run` — both must be set at\n // create time (env survives stop/start; port mappings are immutable).\n const vncEnabled = opts.vnc?.enabled !== false;\n const vncPassword = vncEnabled ? generateVncPassword() : undefined;\n const vncEnv: Record<string, string> = vncEnabled && vncPassword\n ? { AGENTBOX_VNC_PASSWORD: vncPassword }\n : {};\n const vncPortMappings = vncEnabled\n ? [{ hostPort: 0, containerPort: VNC_CONTAINER_PORT, hostIp: '127.0.0.1' }]\n : [];\n\n // Reserve the web port unconditionally: `docker run -p` is immutable, but the\n // `expose:`-flagged service is usually only known after the in-box wizard\n // writes agentbox.yaml. The supervisor forwards :80 to it later; here we just\n // guarantee a published host port exists for whenever that happens.\n const webPortMappings = [\n { hostPort: 0, containerPort: WEB_CONTAINER_PORT, hostIp: '127.0.0.1' },\n ];\n\n // Identity vars that make the box self-aware. `projectIndex` was allocated\n // earlier (right after `id`/`name`) so dir-segment helpers could see it; we\n // just read the binding here.\n const agentboxEnv: Record<string, string> = {\n AGENTBOX: '1',\n AGENTBOX_BOX_NAME: name,\n AGENTBOX_HOST_WORKSPACE: workspace,\n ...(opts.projectRoot ? { AGENTBOX_PROJECT_ROOT: opts.projectRoot } : {}),\n ...(projectIndex !== undefined\n ? { AGENTBOX_PROJECT_INDEX: String(projectIndex) }\n : {}),\n };\n const boxEnvForFile: Record<string, string> = {\n AGENTBOX_BOX_ID: id,\n ...agentboxEnv,\n };\n\n // `--storage-opt size=` is only enforced by devicemapper/btrfs/zfs.\n const appliedLimits: BoxLimitSpec | undefined = opts.limits;\n let effectiveLimits = appliedLimits;\n if (appliedLimits?.disk) {\n const driver = await dockerStorageDriver();\n if (!/^(devicemapper|btrfs|zfs|windowsfilter)$/.test(driver)) {\n log(\n `warning: --disk/box.disk is a no-op on this engine (storage-driver=${driver || 'unknown'}); ignoring`,\n );\n effectiveLimits = { ...appliedLimits, disk: null };\n }\n }\n\n await runBox({\n name: containerName,\n image: imageRef,\n extraVolumes,\n limits: effectiveLimits,\n portMappings: [...vncPortMappings, ...webPortMappings],\n env: {\n AGENTBOX_BOX_ID: id,\n ...agentboxEnv,\n ...claudeMounts.env,\n ...relayEnv,\n ...vncEnv,\n ...(opts.claudeEnv ?? {}),\n },\n });\n log(`container ${containerName} started`);\n\n // Flip the in-container parent dir of each bind-mounted `.git` to\n // vscode-owned. Docker auto-creates the intermediates (e.g. the project root\n // path that contains `.git`) as root:root 755 in the writable layer; without\n // this chown the agent can't write siblings of `.git` (`.turbo/`, `.next/`,\n // build caches) at the project root. Non-recursive — the bind-mounted `.git`\n // itself stays untouched (recursive chown would propagate to the host).\n if (gitWorktreeRecords.length > 0) {\n await chownGitBindParents({\n container: containerName,\n hostMainRepos: gitWorktreeRecords.map((w) => w.hostMainRepo),\n onLog: log,\n });\n }\n\n // /etc/agentbox/box.env: sourced by /etc/profile.d/agentbox.sh in login\n // shells (the docker-run env doesn't reach `agentbox shell <box>` cleanly\n // without it). Best-effort — env vars on the container are the primary\n // path; this file is for shells launched via tools that strip env.\n const boxEnv = await writeBoxEnvFile(containerName, boxEnvForFile);\n if (boxEnv.ok) log('wrote /etc/agentbox/box.env');\n else log(`writing /etc/agentbox/box.env failed: ${boxEnv.reason}`);\n\n // Seed /workspace.\n // - Checkpoint restore: the image already has the source box's per-box\n // worktree dir populated; we only need to re-establish the bind mount\n // onto /workspace (docker commit doesn't capture bind-mount content).\n // - Git path: create in-container worktrees + bind + replay stash + untracked.\n // - No-git path: tar-pipe host workspace (or its APFS clone) into\n // /workspace (no bind — files live directly in the image's writable\n // layer at /workspace).\n if (!checkpointImage) {\n if (repoCarryOvers.length > 0) {\n try {\n await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });\n log('seeded /workspace from in-container git worktree(s)');\n } catch (err) {\n log(\n `seedWorkspace failed; leaving ${containerName} running so you can inspect it`,\n );\n throw err;\n }\n } else {\n const source = snapshotDir ?? workspace;\n await seedWorkspaceFromDir({ container: containerName, hostSource: source, onLog: log });\n }\n } else if (restoredWorktrees && restoredWorktrees.length > 0) {\n // gitWorktreeRecords was populated above (pre-`docker run`) so the .git\n // bind-mounts in extraVolumes are wired. The /workspace bind itself\n // can't be set up until the container is running, so we apply it here.\n await bindWorktrees(\n containerName,\n restoredWorktrees.map((w) => ({\n kind: w.kind,\n containerPath: w.containerPath,\n gitWorktreePath: w.gitWorktreePath,\n })),\n log,\n );\n log('re-bound /workspace from checkpoint image');\n } else {\n log('using /workspace from checkpoint image (no worktrees recorded; no rebind)');\n }\n\n await repairIdeOwnership(containerName);\n log('.vscode-server + .cursor-server ownership verified');\n\n const ctl = await launchCtlDaemon(containerName, socketPath);\n if (ctl.up) log('agentbox-ctl daemon up');\n else log(`agentbox-ctl daemon did not become reachable: ${ctl.reason}`);\n\n // dockerd: always-on, mirrors launchVncDaemon. Best-effort — a slow start\n // shouldn't fail box creation; `agentbox start` will relaunch on restart\n // (the daemon dies with the container). Storage driver is fuse-overlayfs,\n // pinned in /etc/docker/daemon.json baked into the image.\n const dockerd = await launchDockerdDaemon(containerName);\n if (dockerd.up) {\n log(`dockerd up (storage-driver=fuse-overlayfs, data root=${dockerVolume})`);\n } else {\n log(`dockerd did not become ready: ${dockerd.reason}`);\n }\n\n if (opts.withPlaywright) {\n log('installing @playwright/cli@latest (--with-playwright)');\n const result = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n 'root',\n containerName,\n 'bash',\n '-lc',\n 'npm install -g @playwright/cli@latest 2>&1',\n ],\n { reject: false },\n );\n for (const line of (result.stdout ?? '').split('\\n')) {\n if (line.trim().length > 0) log(`[playwright] ${line}`);\n }\n if (result.exitCode !== 0) {\n throw new Error(\n `failed to install @playwright/cli (exit ${String(result.exitCode)}): ${(result.stderr ?? '').toString().slice(0, 400)}`,\n );\n }\n log('@playwright/cli installed');\n }\n\n if (opts.withEnv) {\n log('copying host env/config files into /workspace (--with-env)');\n const { copied } = await copyHostEnvFilesToBox({\n container: containerName,\n workspaceDir: workspace,\n patterns: DEFAULT_ENV_PATTERNS,\n onLog: log,\n });\n log(copied > 0 ? `copied ${String(copied)} env/config file(s)` : 'no env/config files found');\n }\n\n if (opts.envFilesToImport && opts.envFilesToImport.length > 0) {\n log(`copying ${String(opts.envFilesToImport.length)} selected env/config file(s) into /workspace`);\n const { copied } = await copyHostFilesToBox({\n container: containerName,\n workspaceDir: workspace,\n files: opts.envFilesToImport,\n onLog: log,\n });\n if (copied !== opts.envFilesToImport.length) {\n log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);\n }\n }\n\n // VNC daemon (Xvnc + websockify). Best-effort, like launchCtlDaemon. The\n // host port mapping was wired into runBox above (hostPort=0 → random); we\n // resolve the assigned port here for storage. If the daemon fails to come\n // up we still record vncEnabled so `agentbox start` will retry the launch.\n let vncHostPort: number | null = null;\n if (vncEnabled) {\n const vnc = await launchVncDaemon(containerName);\n if (vnc.up) log('vnc stack up (Xvnc + websockify + noVNC)');\n else log(`vnc stack did not become reachable: ${vnc.reason}`);\n vncHostPort = await publishedHostPort(containerName, VNC_CONTAINER_PORT);\n if (vncHostPort) log(`vnc web on host 127.0.0.1:${String(vncHostPort)}`);\n }\n\n const webHostPort = await publishedHostPort(containerName, WEB_CONTAINER_PORT);\n if (webHostPort) {\n log(\n `web port reserved on host 127.0.0.1:${String(webHostPort)} ` +\n `(forwards to the web service once agentbox.yaml sets a service expose:)`,\n );\n }\n\n const record: BoxRecord = {\n id,\n name,\n container: containerName,\n image: imageRef,\n workspacePath: workspace,\n snapshotDir,\n socketPath,\n claudeConfigVolume: claudeSpec.volume,\n vscodeServerVolume: vscodeServerVolumeName(id),\n cursorServerVolume: cursorServerVolumeName(id),\n relayToken: relayUp ? relayToken : undefined,\n gitWorktrees: gitWorktreeRecords.length > 0 ? gitWorktreeRecords : undefined,\n withPlaywright: opts.withPlaywright ? true : undefined,\n withEnv: opts.withEnv ? true : undefined,\n vncEnabled: vncEnabled ? true : undefined,\n vncContainerPort: vncEnabled ? VNC_CONTAINER_PORT : undefined,\n vncHostPort: vncHostPort ?? undefined,\n vncPassword: vncPassword,\n webContainerPort: WEB_CONTAINER_PORT,\n webHostPort: webHostPort ?? undefined,\n dockerVolume,\n dockerCacheShared: dockerCacheShared || undefined,\n projectRoot: opts.projectRoot,\n projectIndex,\n checkpointImage,\n checkpointSource,\n resourceLimits: persistableLimits(effectiveLimits),\n createdAt,\n };\n await recordBox(record);\n\n return { record, imageBuilt: built };\n}\n","import { execa } from 'execa';\n\n/**\n * Writes /etc/agentbox/box.env inside the container as a POSIX-sourceable\n * key='value' file. Paired with /etc/profile.d/agentbox.sh (baked in the\n * image), which `set -a; . /etc/agentbox/box.env; set +a`s it on login.\n *\n * Best-effort: failure is logged by the caller; an unwritable file just\n * means interactive shells lose the AGENTBOX_* vars (the env vars baked\n * into docker run still survive).\n */\nexport async function writeBoxEnvFile(\n container: string,\n env: Record<string, string>,\n): Promise<{ ok: true } | { ok: false; reason: string }> {\n const body = formatBoxEnvBody(env);\n const result = await execa(\n 'docker',\n ['exec', '--user', 'root', '-i', container, 'sh', '-c', 'umask 022 && cat > /etc/agentbox/box.env'],\n { input: body, reject: false },\n );\n if (result.exitCode !== 0) {\n return {\n ok: false,\n reason: `docker exec failed (exit ${String(result.exitCode)}): ${(result.stderr ?? '').toString().slice(0, 400)}`,\n };\n }\n return { ok: true };\n}\n\n// Single-quote each value and escape embedded single quotes as '\\''. Avoids\n// double-quoted form because `. ` would expand $foo / `cmd` at source time.\nexport function formatBoxEnvBody(env: Record<string, string>): string {\n const lines: string[] = [];\n for (const [k, v] of Object.entries(env)) {\n lines.push(`${k}=${shellSingleQuote(v)}`);\n }\n return lines.join('\\n') + '\\n';\n}\n\nfunction shellSingleQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n"],"mappings":"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","names":["execa","execa"]}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
CHECKPOINT_IMAGE_PREFIX,
|
|
4
|
+
checkpointImageTag,
|
|
5
5
|
detectEngine,
|
|
6
6
|
inspectContainer,
|
|
7
7
|
inspectContainerStatus,
|
|
8
|
-
inspectVolumeMountpoint
|
|
9
|
-
|
|
10
|
-
} from "./chunk-SOMIKEN2.js";
|
|
8
|
+
inspectVolumeMountpoint
|
|
9
|
+
} from "./chunk-RFC5F5HR.js";
|
|
11
10
|
|
|
12
|
-
// ../../packages/sandbox-docker/dist/chunk-
|
|
11
|
+
// ../../packages/sandbox-docker/dist/chunk-HXGUOS4P.js
|
|
13
12
|
import { homedir } from "os";
|
|
14
13
|
import { join } from "path";
|
|
15
14
|
import { execa } from "execa";
|
|
@@ -81,17 +80,43 @@ async function volumeSizeBytes(name) {
|
|
|
81
80
|
}
|
|
82
81
|
return null;
|
|
83
82
|
}
|
|
84
|
-
async function
|
|
85
|
-
|
|
83
|
+
async function imageBytes(tag) {
|
|
84
|
+
const r = await execa("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
|
|
85
|
+
reject: false
|
|
86
|
+
});
|
|
87
|
+
if (r.exitCode !== 0) return null;
|
|
88
|
+
const n = Number.parseInt((r.stdout ?? "").trim(), 10);
|
|
89
|
+
return Number.isFinite(n) ? n : null;
|
|
90
|
+
}
|
|
91
|
+
async function projectCheckpointImageBytes(projectRoot, name) {
|
|
92
|
+
return imageBytes(checkpointImageTag(projectRoot, name));
|
|
86
93
|
}
|
|
87
|
-
async function
|
|
88
|
-
const
|
|
89
|
-
|
|
94
|
+
async function allCheckpointImagesBytes() {
|
|
95
|
+
const r = await execa(
|
|
96
|
+
"docker",
|
|
97
|
+
[
|
|
98
|
+
"image",
|
|
99
|
+
"ls",
|
|
100
|
+
"--format",
|
|
101
|
+
"{{.Repository}}:{{.Tag}} {{.Size}}",
|
|
102
|
+
`${CHECKPOINT_IMAGE_PREFIX}*`
|
|
103
|
+
],
|
|
104
|
+
{ reject: false }
|
|
90
105
|
);
|
|
91
|
-
if (
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
if (r.exitCode !== 0) return null;
|
|
107
|
+
const lines = (r.stdout ?? "").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
108
|
+
if (lines.length === 0) return null;
|
|
109
|
+
let total = 0;
|
|
110
|
+
let any = false;
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
const [, size] = line.split(" ");
|
|
113
|
+
const n = size ? parseDockerSize(size) : null;
|
|
114
|
+
if (n !== null) {
|
|
115
|
+
total += n;
|
|
116
|
+
any = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return any ? total : null;
|
|
95
120
|
}
|
|
96
121
|
async function agentboxHomeBytes() {
|
|
97
122
|
return duBytes(join(homedir(), ".agentbox"));
|
|
@@ -118,17 +143,30 @@ function reconcileLimits(persisted, dockerJson) {
|
|
|
118
143
|
disk: persisted.disk
|
|
119
144
|
};
|
|
120
145
|
}
|
|
146
|
+
async function containerWritableBytes(container) {
|
|
147
|
+
const r = await execa(
|
|
148
|
+
"docker",
|
|
149
|
+
["ps", "-a", "--filter", `name=^${container}$`, "--format", "{{.Size}}", "--size"],
|
|
150
|
+
{ reject: false }
|
|
151
|
+
);
|
|
152
|
+
if (r.exitCode !== 0) return null;
|
|
153
|
+
const first = (r.stdout ?? "").split("\n")[0]?.trim();
|
|
154
|
+
if (!first) return null;
|
|
155
|
+
const m = /^([^()]+?)(?:\s*\(.*\))?$/.exec(first);
|
|
156
|
+
const sz = m ? m[1].trim() : first;
|
|
157
|
+
return parseDockerSize(sz);
|
|
158
|
+
}
|
|
121
159
|
async function boxResourceStats(record) {
|
|
122
160
|
const warnings = [];
|
|
123
161
|
const dockerJson = await inspectContainer(record.container);
|
|
124
162
|
const limits = reconcileLimits(limitsFromRecord(record), dockerJson);
|
|
125
|
-
const [
|
|
126
|
-
|
|
163
|
+
const [diskContainer, diskDocker, snapshotDiskBytes, checkpointImageBytesValue] = await Promise.all([
|
|
164
|
+
containerWritableBytes(record.container),
|
|
127
165
|
record.dockerVolume ? volumeSizeBytes(record.dockerVolume) : Promise.resolve(null),
|
|
128
166
|
record.snapshotDir ? duBytes(record.snapshotDir) : Promise.resolve(null),
|
|
129
|
-
record.
|
|
167
|
+
record.checkpointImage ? imageBytes(record.checkpointImage) : Promise.resolve(null)
|
|
130
168
|
]);
|
|
131
|
-
const diskUsedBytes =
|
|
169
|
+
const diskUsedBytes = diskContainer === null && diskDocker === null ? null : (diskContainer ?? 0) + (diskDocker ?? 0);
|
|
132
170
|
if (diskUsedBytes === null) {
|
|
133
171
|
warnings.push("disk usage unavailable on this engine");
|
|
134
172
|
}
|
|
@@ -142,7 +180,7 @@ async function boxResourceStats(record) {
|
|
|
142
180
|
pids: null,
|
|
143
181
|
diskUsedBytes,
|
|
144
182
|
snapshotDiskBytes,
|
|
145
|
-
checkpointVolumeBytes,
|
|
183
|
+
checkpointVolumeBytes: checkpointImageBytesValue,
|
|
146
184
|
netRxBytes: null,
|
|
147
185
|
netTxBytes: null,
|
|
148
186
|
blockReadBytes: null,
|
|
@@ -192,9 +230,9 @@ async function boxResourceStats(record) {
|
|
|
192
230
|
export {
|
|
193
231
|
parseDockerSize,
|
|
194
232
|
volumeSizeBytes,
|
|
195
|
-
|
|
196
|
-
|
|
233
|
+
projectCheckpointImageBytes,
|
|
234
|
+
allCheckpointImagesBytes,
|
|
197
235
|
agentboxHomeBytes,
|
|
198
236
|
boxResourceStats
|
|
199
237
|
};
|
|
200
|
-
//# sourceMappingURL=chunk-
|
|
238
|
+
//# sourceMappingURL=chunk-7J5AJLWG.js.map
|