@madarco/agentbox 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/{chunk-J35IH7W5.js → chunk-BBZMA2K6.js} +61 -23
  2. package/dist/chunk-BBZMA2K6.js.map +1 -0
  3. package/dist/{chunk-SOMIKEN2.js → chunk-HHMWQNLF.js} +272 -214
  4. package/dist/chunk-HHMWQNLF.js.map +1 -0
  5. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  6. package/dist/chunk-HPZMD5DE.js.map +1 -0
  7. package/dist/{chunk-NSIECUCS.js → chunk-HTTKML3C.js} +705 -289
  8. package/dist/chunk-HTTKML3C.js.map +1 -0
  9. package/dist/{chunk-WR5FFGE5.js → chunk-KJNZP6I3.js} +218 -128
  10. package/dist/chunk-KJNZP6I3.js.map +1 -0
  11. package/dist/{chunk-FQD6ZWYW.js → chunk-M7I247BK.js} +68 -65
  12. package/dist/chunk-M7I247BK.js.map +1 -0
  13. package/dist/create-6PWXI6HO-OWAMHBAK.js +15 -0
  14. package/dist/index.js +2394 -1283
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-EMXR46DI-DUVBXNTV.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-SZXOJE3D-N7OODCHW.js +19 -0
  19. package/package.json +3 -2
  20. package/runtime/docker/Dockerfile.box +65 -25
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +52 -55
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +272 -160
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +52 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  25. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  26. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -15
  27. package/runtime/relay/bin.cjs +407 -12
  28. package/share/agentbox-setup/SKILL.md +52 -55
  29. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  30. package/dist/chunk-IDR4HVIC.js.map +0 -1
  31. package/dist/chunk-J35IH7W5.js.map +0 -1
  32. package/dist/chunk-NSIECUCS.js.map +0 -1
  33. package/dist/chunk-SOMIKEN2.js.map +0 -1
  34. package/dist/chunk-WR5FFGE5.js.map +0 -1
  35. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  36. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  37. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
  38. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
  39. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  40. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
@@ -3,64 +3,132 @@ import {
3
3
  ConfigError,
4
4
  VNC_CONTAINER_PORT,
5
5
  WEB_CONTAINER_PORT,
6
+ bindWorktrees,
6
7
  buildClaudeMounts,
7
8
  buildIdeMounts,
8
- createBoxWorktree,
9
+ chownGitBindParents,
10
+ collectRepoCarryOver,
9
11
  createSnapshot,
10
12
  cursorServerVolumeName,
11
13
  detectGitRepos,
12
14
  dockerVolumeName,
13
15
  ensureClaudeVolume,
16
+ ensureHomeOwnedByVscode,
14
17
  ensureIdeVolumes,
15
18
  ensureRelay,
16
19
  generateRelayToken,
17
20
  generateVncPassword,
21
+ gitWorktreePathFor,
18
22
  launchCtlDaemon,
19
23
  launchDockerdDaemon,
20
24
  launchVncDaemon,
21
25
  loadConfig,
22
- mountOverlay,
26
+ pickFreshBranch,
23
27
  registerBoxWithRelay,
24
28
  rehydrateRelayRegistry,
25
29
  repairIdeOwnership,
26
30
  resolveClaudeVolume,
31
+ seedSetupSkillIntoVolume,
32
+ seedWorkspace,
33
+ seedWorkspaceFromDir,
27
34
  snapshotPathFor,
28
- verifyOverlay,
29
35
  vscodeServerVolumeName
30
- } from "./chunk-NSIECUCS.js";
36
+ } from "./chunk-HTTKML3C.js";
31
37
  import {
38
+ STATE_DIR,
32
39
  allocateProjectIndex,
33
40
  readState,
34
41
  recordBox
35
- } from "./chunk-IDR4HVIC.js";
42
+ } from "./chunk-HPZMD5DE.js";
36
43
  import {
37
- CHECKPOINT_MOUNT,
38
44
  CONTAINER_EXPORT_MERGED,
39
- CONTAINER_EXPORT_UPPER,
40
45
  DEFAULT_BOX_IMAGE,
41
46
  DEFAULT_ENV_PATTERNS,
42
47
  boxRunDirFor,
43
48
  containerExists,
44
49
  copyHostEnvFilesToBox,
50
+ copyHostFilesToBox,
45
51
  dockerInfo,
46
52
  dockerStorageDriver,
47
53
  ensureImage,
48
54
  ensureVolume,
49
55
  publishedHostPort,
50
- resolveCheckpointLower,
56
+ resolveCheckpoint,
51
57
  runBox
52
- } from "./chunk-SOMIKEN2.js";
58
+ } from "./chunk-HHMWQNLF.js";
53
59
 
54
- // ../../packages/sandbox-docker/dist/chunk-OHAASUGY.js
60
+ // ../../packages/sandbox-docker/dist/chunk-A4F6SVSK.js
55
61
  import { randomBytes } from "crypto";
56
- import { mkdir, stat } from "fs/promises";
62
+ import { mkdir as mkdir2, stat } from "fs/promises";
57
63
  import { homedir } from "os";
58
- import { basename, join, resolve } from "path";
59
- import { execa as execa2 } from "execa";
64
+ import { basename, join as join2, resolve } from "path";
65
+ import { execa as execa3 } from "execa";
66
+ import { chmod, mkdir, readFile } from "fs/promises";
67
+ import { join } from "path";
60
68
  import { execa } from "execa";
69
+ import { execa as execa2 } from "execa";
70
+ var CREDENTIALS_BACKUP_FILE = join(STATE_DIR, "claude-credentials.json");
71
+ async function hostBackupHasCredentials(path = CREDENTIALS_BACKUP_FILE) {
72
+ try {
73
+ const parsed = JSON.parse(await readFile(path, "utf8"));
74
+ const rt = parsed?.claudeAiOauth?.refreshToken;
75
+ return typeof rt === "string" && rt.length > 0;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ function parseSyncResult(stdout) {
81
+ const volumeHasCredentials = /\bVOLREAL=yes\b/.test(stdout);
82
+ if (/\bEXTRACTED=yes\b/.test(stdout)) return { direction: "extracted", volumeHasCredentials };
83
+ if (/\bSEEDED=yes\b/.test(stdout)) return { direction: "seeded", volumeHasCredentials };
84
+ return { direction: "noop", volumeHasCredentials };
85
+ }
86
+ var SYNC_SCRIPT = `
87
+ EXTRACTED=no
88
+ SEEDED=no
89
+ VOL=/dst/.credentials.json
90
+ HOST=/host-state/claude-credentials.json
91
+ if [ -f "$VOL" ] && jq -e '(.claudeAiOauth.refreshToken // "") | length > 0' "$VOL" >/dev/null 2>&1; then VOL_REAL=yes; else VOL_REAL=no; fi
92
+ if [ -f "$HOST" ] && jq -e '(.claudeAiOauth.refreshToken // "") | length > 0' "$HOST" >/dev/null 2>&1; then HOST_REAL=yes; else HOST_REAL=no; fi
93
+ if [ "$VOL_REAL" = yes ] && [ "$ISOLATE" != yes ]; then
94
+ cp -a "$VOL" "$HOST" && chmod 600 "$HOST" && EXTRACTED=yes
95
+ elif [ "$VOL_REAL" = no ] && [ "$HOST_REAL" = yes ]; then
96
+ cp -a "$HOST" "$VOL" && chown 1000:1000 "$VOL" && chmod 600 "$VOL" && SEEDED=yes && VOL_REAL=yes
97
+ fi
98
+ echo "EXTRACTED=$EXTRACTED SEEDED=$SEEDED VOLREAL=$VOL_REAL"
99
+ `;
100
+ async function syncClaudeCredentials(spec, opts) {
101
+ try {
102
+ await mkdir(STATE_DIR, { recursive: true });
103
+ const { stdout } = await execa("docker", [
104
+ "run",
105
+ "--rm",
106
+ "--user",
107
+ "0",
108
+ "-v",
109
+ `${spec.volume}:/dst`,
110
+ "-v",
111
+ `${STATE_DIR}:/host-state`,
112
+ "-e",
113
+ `ISOLATE=${opts.isolate ? "yes" : "no"}`,
114
+ opts.image,
115
+ "sh",
116
+ "-c",
117
+ SYNC_SCRIPT
118
+ ]);
119
+ const result = parseSyncResult(stdout);
120
+ if (result.direction === "extracted") {
121
+ await chmod(CREDENTIALS_BACKUP_FILE, 384).catch(() => {
122
+ });
123
+ }
124
+ return result;
125
+ } catch {
126
+ return { direction: "noop", volumeHasCredentials: false };
127
+ }
128
+ }
61
129
  async function writeBoxEnvFile(container, env) {
62
130
  const body = formatBoxEnvBody(env);
63
- const result = await execa(
131
+ const result = await execa2(
64
132
  "docker",
65
133
  ["exec", "--user", "root", "-i", container, "sh", "-c", "umask 022 && cat > /etc/agentbox/box.env"],
66
134
  { input: body, reject: false }
@@ -114,8 +182,8 @@ async function pathExists(p) {
114
182
  async function buildIdentityMounts() {
115
183
  const home = homedir();
116
184
  const candidates = [
117
- { src: join(home, ".codex"), dst: "/home/vscode/.codex", readOnly: false },
118
- { src: join(home, ".gitconfig"), dst: "/home/vscode/.gitconfig", readOnly: true }
185
+ { src: join2(home, ".codex"), dst: "/home/vscode/.codex", readOnly: false },
186
+ { src: join2(home, ".gitconfig"), dst: "/home/vscode/.gitconfig", readOnly: true }
119
187
  ];
120
188
  const out = [];
121
189
  for (const c of candidates) {
@@ -132,7 +200,7 @@ async function createBox(opts) {
132
200
  if (!await pathExists(workspace)) {
133
201
  throw new Error(`workspace does not exist: ${workspace}`);
134
202
  }
135
- const cfgPath = join(workspace, "agentbox.yaml");
203
+ const cfgPath = join2(workspace, "agentbox.yaml");
136
204
  if (await pathExists(cfgPath)) {
137
205
  try {
138
206
  const cfg = await loadConfig(cfgPath);
@@ -147,11 +215,29 @@ async function createBox(opts) {
147
215
  }
148
216
  await dockerInfo();
149
217
  log("docker daemon reachable");
150
- const imageRef = opts.image ?? DEFAULT_BOX_IMAGE;
151
- const { built } = await ensureImage(imageRef, {
218
+ let checkpointImage;
219
+ let checkpointSource;
220
+ let restoredWorktrees;
221
+ if (opts.checkpointRef) {
222
+ const projectRootForCkpt = opts.projectRoot ?? workspace;
223
+ const head = await resolveCheckpoint(projectRootForCkpt, opts.checkpointRef);
224
+ if (!head) {
225
+ throw new Error(`checkpoint not found: ${opts.checkpointRef}`);
226
+ }
227
+ checkpointImage = head.manifest.image;
228
+ const chain = [head.name, ...head.manifest.parents];
229
+ checkpointSource = { ref: opts.checkpointRef, type: head.manifest.type, chain };
230
+ restoredWorktrees = head.manifest.worktrees;
231
+ log(
232
+ `starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`
233
+ );
234
+ }
235
+ const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;
236
+ const ensureRef = checkpointImage ? opts.image ?? DEFAULT_BOX_IMAGE : imageRef;
237
+ const { built } = await ensureImage(ensureRef, {
152
238
  onProgress: (line) => log(`[image] ${line}`)
153
239
  });
154
- log(built ? `built image ${imageRef}` : `using cached image ${imageRef}`);
240
+ log(built ? `built image ${ensureRef}` : `using cached image ${imageRef}`);
155
241
  let relayUp = false;
156
242
  try {
157
243
  await ensureRelay({ onLog: log });
@@ -168,78 +254,54 @@ async function createBox(opts) {
168
254
  if (await containerExists(containerName)) {
169
255
  throw new Error(`container ${containerName} already exists; remove it first`);
170
256
  }
171
- const worktreesRoot = join(boxRunDirFor(id), "worktrees");
172
- await mkdir(worktreesRoot, { recursive: true });
257
+ let projectIndex;
258
+ if (opts.projectRoot) {
259
+ projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);
260
+ }
261
+ const repoCarryOvers = [];
173
262
  const gitWorktreeRecords = [];
174
- const nestedWorktreeBinds = [];
175
- const repos = await detectGitRepos(workspace);
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
- );
263
+ if (checkpointImage && restoredWorktrees && restoredWorktrees.length > 0) {
264
+ gitWorktreeRecords.push(...restoredWorktrees);
180
265
  }
181
- for (const r of repos) {
182
- const worktreeDir = join(worktreesRoot, r.relPathFromWorkspace || "root");
183
- const branchBase = r.kind === "root" ? `agentbox/${name}` : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, "_")}`;
184
- const result = await createBoxWorktree({
185
- hostMainRepo: r.hostMainRepo,
186
- branchName: branchBase,
187
- worktreeDir,
188
- onLog: log
189
- });
190
- const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
191
- gitWorktreeRecords.push({
192
- kind: r.kind,
193
- hostMainRepo: r.hostMainRepo,
194
- hostWorktreeDir: worktreeDir,
195
- containerPath,
196
- branch: result.branchName,
197
- relPathFromWorkspace: r.relPathFromWorkspace
198
- });
199
- if (r.kind === "nested") {
200
- nestedWorktreeBinds.push({
266
+ if (!checkpointImage) {
267
+ const repos = await detectGitRepos(workspace);
268
+ if (repos.length > 0) {
269
+ log(
270
+ `detected ${String(repos.length)} git repo(s): ` + repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? "@" + r.relPathFromWorkspace : ""}`).join(", ")
271
+ );
272
+ }
273
+ for (const r of repos) {
274
+ const branchBase = r.kind === "root" ? `agentbox/${name}` : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, "_")}`;
275
+ const branch = await pickFreshBranch(r.hostMainRepo, branchBase);
276
+ const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
277
+ const gitWorktreePath = gitWorktreePathFor(branch);
278
+ const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);
279
+ repoCarryOvers.push(carry);
280
+ gitWorktreeRecords.push({
281
+ kind: r.kind,
282
+ hostMainRepo: r.hostMainRepo,
201
283
  containerPath,
202
- mountFromPath: `/agentbox-worktrees/${r.relPathFromWorkspace}`
284
+ gitWorktreePath,
285
+ branch,
286
+ relPathFromWorkspace: r.relPathFromWorkspace
203
287
  });
204
288
  }
205
289
  }
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
290
  let snapshotDir = null;
213
- if (opts.useSnapshot) {
214
- snapshotDir = snapshotPathFor(id);
291
+ const snapshotIsUseful = !checkpointImage && repoCarryOvers.length === 0;
292
+ if (opts.useSnapshot && snapshotIsUseful) {
293
+ snapshotDir = snapshotPathFor({ id, name, projectIndex });
215
294
  log(`cloning workspace to ${snapshotDir} (APFS clone where available)`);
216
- const snap = await createSnapshot({ source: lowerPath, destination: snapshotDir });
295
+ const snap = await createSnapshot({ source: workspace, destination: snapshotDir });
217
296
  log(`pruned ${snap.prunedPaths.length} platform-dependent dirs from snapshot`);
218
- lowerPath = snapshotDir;
297
+ } else if (opts.useSnapshot && !checkpointImage) {
298
+ log("skipping --host-snapshot: git worktree path reads content from .git, not from a workspace clone");
219
299
  }
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
- }
234
- const upperVolume = `agentbox-upper-${id}`;
235
- await ensureVolume(upperVolume);
236
300
  await ensureIdeVolumes(id);
237
301
  const dockerCacheShared = opts.docker?.sharedCache === true;
238
302
  const dockerVolume = dockerVolumeName(id, dockerCacheShared);
239
303
  await ensureVolume(dockerVolume);
240
- log(
241
- `prepared volumes ${upperVolume}, ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`
242
- );
304
+ log(`prepared volumes ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`);
243
305
  const ide = buildIdeMounts(id);
244
306
  const claudeSpec = resolveClaudeVolume({
245
307
  isolate: opts.claudeConfig?.isolate ?? false,
@@ -247,7 +309,7 @@ async function createBox(opts) {
247
309
  });
248
310
  const claudeEnsured = await ensureClaudeVolume(claudeSpec, {
249
311
  syncFromHost: true,
250
- image: imageRef,
312
+ image: ensureRef,
251
313
  hostWorkspace: workspace
252
314
  });
253
315
  if (claudeEnsured.synced) {
@@ -257,44 +319,47 @@ async function createBox(opts) {
257
319
  `filtered ${String(claudeEnsured.filteredHookCount)} host-path hook(s) (paths under ~/)`
258
320
  );
259
321
  }
260
- if (claudeEnsured.clearedInstallMethod) {
261
- log("cleared host's installMethod from synced .claude.json (box uses the native installer)");
322
+ if (claudeEnsured.installMethodFixed) {
323
+ log("set installMethod=native in synced .claude.json (matches box native install)");
262
324
  }
263
325
  if (claudeEnsured.aliasedProjectKey) {
264
326
  log(`aliased project state for ${workspace} -> /workspace in synced .claude.json`);
265
327
  }
328
+ if (claudeEnsured.workspaceTrusted) {
329
+ log("pre-trusted /workspace in synced .claude.json (skips the trust dialog)");
330
+ }
266
331
  } else if (claudeEnsured.created) {
267
332
  log(`created empty volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
268
333
  } else {
269
334
  log(`reusing volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
270
335
  }
336
+ const seeded = await seedSetupSkillIntoVolume(claudeSpec.volume, ensureRef);
337
+ if (seeded.seeded) log(`refreshed /agentbox-setup skill into ${claudeSpec.volume}`);
338
+ const credSync = await syncClaudeCredentials(claudeSpec, {
339
+ image: ensureRef,
340
+ isolate: opts.claudeConfig?.isolate ?? false
341
+ });
342
+ if (credSync.direction === "extracted") {
343
+ log("extracted box claude credentials to host backup");
344
+ } else if (credSync.direction === "seeded") {
345
+ log(`seeded claude credentials into ${claudeSpec.volume} from host backup`);
346
+ }
271
347
  const claudeMounts = buildClaudeMounts(claudeSpec, process.env);
272
- const boxDir = boxRunDirFor(id);
273
- const socketDir = join(boxDir, "run");
274
- const socketPath = join(socketDir, "ctl.sock");
275
- const mergedExportDir = join(boxDir, "workspace");
276
- const upperExportDir = join(boxDir, "upper");
277
- await mkdir(socketDir, { recursive: true });
278
- await mkdir(mergedExportDir, { recursive: true });
279
- await mkdir(upperExportDir, { recursive: true });
348
+ const boxDir = boxRunDirFor({ id, name, projectIndex });
349
+ const socketDir = join2(boxDir, "run");
350
+ const socketPath = join2(socketDir, "ctl.sock");
351
+ const mergedExportDir = join2(boxDir, "workspace");
352
+ await mkdir2(socketDir, { recursive: true });
353
+ await mkdir2(mergedExportDir, { recursive: true });
280
354
  const extraVolumes = await buildIdentityMounts();
281
355
  extraVolumes.push(...claudeMounts.extraVolumes);
282
356
  extraVolumes.push(...ide.extraVolumes);
283
357
  extraVolumes.push(`${socketDir}:/run/agentbox`);
284
358
  extraVolumes.push(`${mergedExportDir}:${CONTAINER_EXPORT_MERGED}`);
285
- extraVolumes.push(`${upperExportDir}:${CONTAINER_EXPORT_UPPER}`);
286
359
  extraVolumes.push(`${dockerVolume}:/var/lib/docker`);
287
360
  for (const w of gitWorktreeRecords) {
288
361
  extraVolumes.push(`${w.hostMainRepo}/.git:${w.hostMainRepo}/.git`);
289
362
  }
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
363
  for (const v of extraVolumes) log(`mounting agent dir: ${v}`);
299
364
  const relayToken = generateRelayToken();
300
365
  if (relayUp) {
@@ -305,6 +370,7 @@ async function createBox(opts) {
305
370
  name,
306
371
  containerName,
307
372
  createdAt,
373
+ projectIndex,
308
374
  worktrees: gitWorktreeRecords
309
375
  });
310
376
  log(`registered box token with relay`);
@@ -326,10 +392,6 @@ async function createBox(opts) {
326
392
  const webPortMappings = [
327
393
  { hostPort: 0, containerPort: WEB_CONTAINER_PORT, hostIp: "127.0.0.1" }
328
394
  ];
329
- let projectIndex;
330
- if (opts.projectRoot) {
331
- projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);
332
- }
333
395
  const agentboxEnv = {
334
396
  AGENTBOX: "1",
335
397
  AGENTBOX_BOX_NAME: name,
@@ -355,8 +417,6 @@ async function createBox(opts) {
355
417
  await runBox({
356
418
  name: containerName,
357
419
  image: imageRef,
358
- lowerPath,
359
- upperVolume,
360
420
  extraVolumes,
361
421
  limits: effectiveLimits,
362
422
  portMappings: [...vncPortMappings, ...webPortMappings],
@@ -370,27 +430,46 @@ async function createBox(opts) {
370
430
  }
371
431
  });
372
432
  log(`container ${containerName} started`);
433
+ if (gitWorktreeRecords.length > 0) {
434
+ await chownGitBindParents({
435
+ container: containerName,
436
+ hostMainRepos: gitWorktreeRecords.map((w) => w.hostMainRepo),
437
+ onLog: log
438
+ });
439
+ }
373
440
  const boxEnv = await writeBoxEnvFile(containerName, boxEnvForFile);
374
441
  if (boxEnv.ok) log("wrote /etc/agentbox/box.env");
375
442
  else log(`writing /etc/agentbox/box.env failed: ${boxEnv.reason}`);
376
- try {
377
- await mountOverlay(containerName, { lowerDirs, nestedWorktrees: nestedWorktreeBinds });
378
- log("fuse-overlayfs mounted at /workspace");
379
- if (nestedWorktreeBinds.length > 0) {
380
- log(`bind-mounted ${String(nestedWorktreeBinds.length)} nested worktree(s) over /workspace`);
443
+ await ensureHomeOwnedByVscode(containerName);
444
+ if (!checkpointImage) {
445
+ if (repoCarryOvers.length > 0) {
446
+ try {
447
+ await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });
448
+ log("seeded /workspace from in-container git worktree(s)");
449
+ } catch (err) {
450
+ log(
451
+ `seedWorkspace failed; leaving ${containerName} running so you can inspect it`
452
+ );
453
+ throw err;
454
+ }
455
+ } else {
456
+ const source = snapshotDir ?? workspace;
457
+ await seedWorkspaceFromDir({ container: containerName, hostSource: source, onLog: log });
381
458
  }
382
- } catch (err) {
383
- log(`overlay mount failed; leaving container ${containerName} running so you can inspect it`);
384
- throw err;
385
- }
386
- const overlayChecks = await verifyOverlay(containerName, lowerDirs ?? ["/host-src"]);
387
- const failed = overlayChecks.filter((c) => !c.ok);
388
- if (failed.length > 0) {
389
- const detail = failed.map((c) => ` - ${c.name}: ${c.detail}`).join("\n");
390
- throw new Error(`overlay verification failed:
391
- ${detail}`);
459
+ } else if (restoredWorktrees && restoredWorktrees.length > 0) {
460
+ await bindWorktrees(
461
+ containerName,
462
+ restoredWorktrees.map((w) => ({
463
+ kind: w.kind,
464
+ containerPath: w.containerPath,
465
+ gitWorktreePath: w.gitWorktreePath
466
+ })),
467
+ log
468
+ );
469
+ log("re-bound /workspace from checkpoint image");
470
+ } else {
471
+ log("using /workspace from checkpoint image (no worktrees recorded; no rebind)");
392
472
  }
393
- log("overlay verified");
394
473
  await repairIdeOwnership(containerName);
395
474
  log(".vscode-server + .cursor-server ownership verified");
396
475
  const ctl = await launchCtlDaemon(containerName, socketPath);
@@ -398,13 +477,13 @@ ${detail}`);
398
477
  else log(`agentbox-ctl daemon did not become reachable: ${ctl.reason}`);
399
478
  const dockerd = await launchDockerdDaemon(containerName);
400
479
  if (dockerd.up) {
401
- log(`dockerd up (storage-driver=fuse-overlayfs, data root=${dockerVolume})`);
480
+ log(`dockerd up (data root=${dockerVolume})`);
402
481
  } else {
403
482
  log(`dockerd did not become ready: ${dockerd.reason}`);
404
483
  }
405
484
  if (opts.withPlaywright) {
406
485
  log("installing @playwright/cli@latest (--with-playwright)");
407
- const result = await execa2(
486
+ const result = await execa3(
408
487
  "docker",
409
488
  [
410
489
  "exec",
@@ -437,6 +516,18 @@ ${detail}`);
437
516
  });
438
517
  log(copied > 0 ? `copied ${String(copied)} env/config file(s)` : "no env/config files found");
439
518
  }
519
+ if (opts.envFilesToImport && opts.envFilesToImport.length > 0) {
520
+ log(`copying ${String(opts.envFilesToImport.length)} selected env/config file(s) into /workspace`);
521
+ const { copied } = await copyHostFilesToBox({
522
+ container: containerName,
523
+ workspaceDir: workspace,
524
+ files: opts.envFilesToImport,
525
+ onLog: log
526
+ });
527
+ if (copied !== opts.envFilesToImport.length) {
528
+ log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);
529
+ }
530
+ }
440
531
  let vncHostPort = null;
441
532
  if (vncEnabled) {
442
533
  const vnc = await launchVncDaemon(containerName);
@@ -457,8 +548,6 @@ ${detail}`);
457
548
  container: containerName,
458
549
  image: imageRef,
459
550
  workspacePath: workspace,
460
- lowerPath,
461
- upperVolume,
462
551
  snapshotDir,
463
552
  socketPath,
464
553
  claudeConfigVolume: claudeSpec.volume,
@@ -478,19 +567,20 @@ ${detail}`);
478
567
  dockerCacheShared: dockerCacheShared || void 0,
479
568
  projectRoot: opts.projectRoot,
480
569
  projectIndex,
481
- lowerDirs,
482
- checkpointVolume,
570
+ checkpointImage,
483
571
  checkpointSource,
484
572
  resourceLimits: persistableLimits(effectiveLimits),
485
573
  createdAt
486
574
  };
487
575
  await recordBox(record);
488
- return { record, overlayChecks, imageBuilt: built };
576
+ return { record, imageBuilt: built };
489
577
  }
490
578
 
491
579
  export {
580
+ hostBackupHasCredentials,
581
+ syncClaudeCredentials,
492
582
  sanitizeBasename,
493
583
  defaultBoxName,
494
584
  createBox
495
585
  };
496
- //# sourceMappingURL=chunk-WR5FFGE5.js.map
586
+ //# sourceMappingURL=chunk-KJNZP6I3.js.map