@madarco/agentbox 0.4.1 → 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.
Files changed (38) hide show
  1. package/dist/{chunk-WR5FFGE5.js → chunk-6VTAPD4H.js} +123 -112
  2. package/dist/chunk-6VTAPD4H.js.map +1 -0
  3. package/dist/{chunk-J35IH7W5.js → chunk-7J5AJLWG.js} +61 -23
  4. package/dist/chunk-7J5AJLWG.js.map +1 -0
  5. package/dist/{chunk-FQD6ZWYW.js → chunk-FJNIFTWK.js} +66 -65
  6. package/dist/chunk-FJNIFTWK.js.map +1 -0
  7. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  8. package/dist/chunk-HPZMD5DE.js.map +1 -0
  9. package/dist/{chunk-NSIECUCS.js → chunk-PXUBE5KS.js} +365 -258
  10. package/dist/chunk-PXUBE5KS.js.map +1 -0
  11. package/dist/{chunk-SOMIKEN2.js → chunk-RFC5F5HR.js} +272 -214
  12. package/dist/chunk-RFC5F5HR.js.map +1 -0
  13. package/dist/create-AHZ3GVEZ-TGEDL7UX.js +15 -0
  14. package/dist/index.js +2757 -1854
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-LFOL6YFM-TCHDX3J5.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-Z4BVJODD-HEC4TMUZ.js +19 -0
  19. package/package.json +5 -4
  20. package/runtime/docker/Dockerfile.box +47 -19
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +39 -50
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +219 -148
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +42 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +26 -15
  25. package/runtime/relay/bin.cjs +288 -12
  26. package/share/agentbox-setup/SKILL.md +39 -50
  27. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  28. package/dist/chunk-IDR4HVIC.js.map +0 -1
  29. package/dist/chunk-J35IH7W5.js.map +0 -1
  30. package/dist/chunk-NSIECUCS.js.map +0 -1
  31. package/dist/chunk-SOMIKEN2.js.map +0 -1
  32. package/dist/chunk-WR5FFGE5.js.map +0 -1
  33. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  34. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  35. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
  36. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-LFOL6YFM-TCHDX3J5.js.map} +0 -0
  37. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  38. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-Z4BVJODD-HEC4TMUZ.js.map} +0 -0
@@ -1,26 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../packages/sandbox-docker/dist/chunk-GQRG227R.js
3
+ // ../../packages/sandbox-docker/dist/chunk-3MREFIME.js
4
4
  import { execa } from "execa";
5
- import { mkdir as mkdir2, readFile as readFile3, stat as stat3 } from "fs/promises";
5
+ import { mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
6
6
  import { homedir as homedir2 } from "os";
7
7
  import { join as join3 } from "path";
8
8
  import { execa as execa2 } from "execa";
9
- import { execa as execa3 } from "execa";
10
- import { existsSync } from "fs";
11
- import { fileURLToPath } from "url";
12
- import { dirname as dirname3, resolve as resolve2 } from "path";
13
- import { mkdir as mkdir22, readFile as readFile22, readdir as readdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
14
- import { homedir as homedir22 } from "os";
15
- import { join as join22 } from "path";
16
- import { execa as execa4 } from "execa";
17
9
 
18
10
  // ../../packages/config/dist/index.js
19
11
  import { parse as parseYaml } from "yaml";
20
12
  import { createHash } from "crypto";
21
13
  import { realpath, stat } from "fs/promises";
22
14
  import { homedir } from "os";
23
- import { dirname, join, resolve } from "path";
15
+ import { basename, dirname, join, resolve } from "path";
24
16
  import { readFile } from "fs/promises";
25
17
  import { parse as parseYaml2 } from "yaml";
26
18
  import { mkdir, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "fs/promises";
@@ -442,8 +434,14 @@ function hashProjectPath(absPath) {
442
434
  const normalised = absPath.length > 1 && absPath.endsWith("/") ? absPath.slice(0, -1) : absPath;
443
435
  return createHash("sha1").update(normalised).digest("hex").slice(0, 16);
444
436
  }
437
+ function sanitizeMnemonic(raw) {
438
+ return raw.toLowerCase().replace(/-/g, "_").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "unnamed";
439
+ }
440
+ function projectDirSegment(absPath) {
441
+ return `${hashProjectPath(absPath)}-${sanitizeMnemonic(basename(absPath))}`;
442
+ }
445
443
  function projectConfigDir(absPath) {
446
- return join(PROJECTS_DIR, hashProjectPath(absPath));
444
+ return join(PROJECTS_DIR, projectDirSegment(absPath));
447
445
  }
448
446
  function projectConfigFile(absPath) {
449
447
  return join(projectConfigDir(absPath), "config.yaml");
@@ -603,14 +601,17 @@ async function listProjectsConfigured() {
603
601
  throw err;
604
602
  }
605
603
  const out = [];
606
- for (const hash of entries) {
607
- if (!/^[0-9a-f]{16}$/.test(hash)) continue;
608
- const meta = await readMeta(hash);
604
+ for (const dirName of entries) {
605
+ const m = /^([0-9a-f]{16})(?:-.+)?$/.exec(dirName);
606
+ if (!m) continue;
607
+ const hash = m[1];
608
+ const meta = await readMeta(dirName);
609
609
  if (!meta) continue;
610
610
  const cfgPath = projectConfigFile(meta.originalPath);
611
611
  const hasConfig = await fileExists2(cfgPath);
612
612
  out.push({
613
613
  hash,
614
+ dirName,
614
615
  originalPath: meta.originalPath,
615
616
  createdAt: meta.createdAt,
616
617
  lastSeenAt: meta.lastSeenAt,
@@ -637,7 +638,7 @@ async function pruneOrphanProjectConfigs(opts = {}) {
637
638
  removed.push({ hash: entry.hash, originalPath: entry.originalPath });
638
639
  if (!dryRun) {
639
640
  try {
640
- await rm(join2(PROJECTS_DIR, entry.hash), { recursive: true, force: true });
641
+ await rm(join2(PROJECTS_DIR, entry.dirName), { recursive: true, force: true });
641
642
  } catch {
642
643
  }
643
644
  }
@@ -661,8 +662,8 @@ async function bumpProjectGcCounter() {
661
662
  await rename(tmp, PROJECT_GC_COUNTER_FILE);
662
663
  return next;
663
664
  }
664
- async function readMeta(hash) {
665
- const metaPath = `${PROJECTS_DIR}/${hash}/meta.json`;
665
+ async function readMeta(dirName) {
666
+ const metaPath = `${PROJECTS_DIR}/${dirName}/meta.json`;
666
667
  try {
667
668
  const text = await readFile2(metaPath, "utf8");
668
669
  const parsed = JSON.parse(text);
@@ -745,7 +746,15 @@ async function touchProjectMeta(absPath) {
745
746
  await rename(tmp, metaPath);
746
747
  }
747
748
 
748
- // ../../packages/sandbox-docker/dist/chunk-GQRG227R.js
749
+ // ../../packages/sandbox-docker/dist/chunk-3MREFIME.js
750
+ import { execa as execa3 } from "execa";
751
+ import { existsSync } from "fs";
752
+ import { fileURLToPath } from "url";
753
+ import { dirname as dirname3, resolve as resolve2 } from "path";
754
+ import { mkdir as mkdir22, mkdtemp, readFile as readFile22, readdir as readdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
755
+ import { homedir as homedir22, tmpdir } from "os";
756
+ import { basename as basename2, join as join22 } from "path";
757
+ import { execa as execa4 } from "execa";
749
758
  async function dockerInfo() {
750
759
  const result = await execa("docker", ["info"], { reject: false });
751
760
  if (result.exitCode !== 0) {
@@ -771,6 +780,13 @@ async function runBox(spec) {
771
780
  // need. Both are scoped to the outer box's namespaces — inner containers
772
781
  // can't escape it. We still avoid --privileged for cloud portability.
773
782
  "--cap-add=NET_ADMIN",
783
+ // /dev/fuse + SYS_ADMIN + apparmor:unconfined used to be required for the
784
+ // outer /workspace FUSE overlay. That overlay is gone, but they're still
785
+ // load-bearing for the *inner* dockerd: it runs with
786
+ // storage-driver=fuse-overlayfs (set in /etc/docker/daemon.json in the
787
+ // image) because the kernel `overlay` driver isn't usable from an
788
+ // unprivileged outer container, and fuse-overlayfs needs the fuse device
789
+ // + SYS_ADMIN to mount layers for inner containers.
774
790
  "--device=/dev/fuse",
775
791
  "--security-opt=apparmor:unconfined",
776
792
  "--security-opt=seccomp=unconfined",
@@ -785,11 +801,7 @@ async function runBox(spec) {
785
801
  // name host.docker.internal. Docker Desktop / OrbStack ship this alias by
786
802
  // default; on Linux native Docker it requires this explicit flag (no-op
787
803
  // on the macOS engines). Boxes use it to reach the host relay process.
788
- "--add-host=host.docker.internal:host-gateway",
789
- "-v",
790
- `${spec.lowerPath}:/host-src:ro`,
791
- "-v",
792
- `${spec.upperVolume}:/upper`
804
+ "--add-host=host.docker.internal:host-gateway"
793
805
  ];
794
806
  const lim = spec.limits;
795
807
  if (lim) {
@@ -950,7 +962,6 @@ async function listAgentboxVolumes() {
950
962
  return (result.stdout ?? "").split("\n").map((s) => s.trim()).filter((s) => s.startsWith(AGENTBOX_PREFIX));
951
963
  }
952
964
  var CONTAINER_EXPORT_MERGED = "/host-export";
953
- var CONTAINER_EXPORT_UPPER = "/host-export-upper";
954
965
  var cachedEngine = null;
955
966
  async function detectEngine() {
956
967
  if (cachedEngine !== null) return cachedEngine;
@@ -967,15 +978,23 @@ function setEngineOverride(engine) {
967
978
  cachedEngine = engine;
968
979
  }
969
980
  var BOXES_ROOT = join3(homedir2(), ".agentbox", "boxes");
970
- function boxRunDirFor(id) {
971
- return join3(BOXES_ROOT, id);
981
+ function boxDirSegment(box) {
982
+ const mnemonic = sanitizeMnemonic(box.name);
983
+ const n = box.projectIndex;
984
+ if (typeof n === "number" && Number.isFinite(n) && n > 0) {
985
+ return `${box.id}-${String(n)}-${mnemonic}`;
986
+ }
987
+ return `${box.id}-${mnemonic}`;
972
988
  }
973
- function boxStatusPathFor(id) {
974
- return join3(boxRunDirFor(id), "status.json");
989
+ function boxRunDirFor(box) {
990
+ return join3(BOXES_ROOT, boxDirSegment(box));
975
991
  }
976
- async function readBoxStatus(id) {
992
+ function boxStatusPathFor(box) {
993
+ return join3(boxRunDirFor(box), "status.json");
994
+ }
995
+ async function readBoxStatus(box) {
977
996
  try {
978
- const raw = await readFile3(boxStatusPathFor(id), "utf8");
997
+ const raw = await readFile3(boxStatusPathFor(box), "utf8");
979
998
  const parsed = JSON.parse(raw);
980
999
  if (parsed.schema !== 1) return null;
981
1000
  return parsed;
@@ -983,95 +1002,56 @@ async function readBoxStatus(id) {
983
1002
  return null;
984
1003
  }
985
1004
  }
986
- async function pathExists(p) {
987
- try {
988
- await stat3(p);
989
- return true;
990
- } catch {
991
- return false;
992
- }
993
- }
994
1005
  function orbstackVolumePath(volume, ...sub) {
995
1006
  return join3(homedir2(), "OrbStack", "docker", "volumes", volume, ...sub);
996
1007
  }
997
- async function resolveUpperLiveOnHost(upperVolume, engine) {
998
- if (engine !== "orbstack") return null;
999
- const orbPath = orbstackVolumePath(upperVolume, "upper");
1000
- if (await pathExists(orbPath)) return orbPath;
1001
- const mp = await inspectVolumeMountpoint(upperVolume);
1002
- if (mp && !mp.startsWith("/var/lib/docker")) {
1003
- const candidate = join3(mp, "upper");
1004
- if (await pathExists(candidate)) return candidate;
1005
- }
1006
- return null;
1007
- }
1008
- async function getHostPaths(record, engine) {
1009
- const eng = engine ?? await detectEngine();
1010
- const boxDir = boxRunDirFor(record.id);
1008
+ async function getHostPaths(record) {
1009
+ const boxDir = boxRunDirFor(record);
1011
1010
  return {
1012
1011
  boxDir,
1013
- mergedExport: join3(boxDir, "workspace"),
1014
- upperExport: join3(boxDir, "upper"),
1015
- upperLiveOnHost: await resolveUpperLiveOnHost(record.upperVolume, eng)
1012
+ mergedExport: join3(boxDir, "workspace")
1016
1013
  };
1017
1014
  }
1018
1015
  async function hasContainerPath(container, path) {
1019
1016
  const probe = await execInBox(container, ["test", "-d", path], { user: "root" });
1020
1017
  return probe.exitCode === 0;
1021
1018
  }
1022
- async function refreshExport(record, opts) {
1023
- const engine = await detectEngine();
1024
- const paths = await getHostPaths(record, engine);
1025
- if (opts.layer === "upper" && engine === "orbstack" && paths.upperLiveOnHost) {
1026
- await mkdir2(paths.boxDir, { recursive: true });
1027
- return { hostPath: paths.upperLiveOnHost, copied: false, usedFallback: false };
1028
- }
1029
- const ctx = opts.layer === "merged" ? {
1030
- hostBoxDir: paths.boxDir,
1031
- hostTarget: paths.mergedExport,
1032
- containerSource: "/workspace",
1033
- containerBind: CONTAINER_EXPORT_MERGED,
1034
- excludeNodeModules: !opts.includeNodeModules
1035
- } : {
1036
- hostBoxDir: paths.boxDir,
1037
- hostTarget: paths.upperExport,
1038
- containerSource: "/upper/upper",
1039
- containerBind: CONTAINER_EXPORT_UPPER,
1040
- excludeNodeModules: false
1041
- };
1042
- await mkdir2(ctx.hostTarget, { recursive: true });
1043
- const bindAvailable = await hasContainerPath(record.container, ctx.containerBind);
1019
+ async function refreshExport(record, opts = {}) {
1020
+ const paths = await getHostPaths(record);
1021
+ const excludeNodeModules = !opts.includeNodeModules;
1022
+ await mkdir2(paths.mergedExport, { recursive: true });
1023
+ const bindAvailable = await hasContainerPath(record.container, CONTAINER_EXPORT_MERGED);
1044
1024
  if (bindAvailable) {
1045
1025
  const args = ["rsync", "-a", "--delete"];
1046
- if (ctx.excludeNodeModules) args.push("--exclude=node_modules");
1047
- args.push(`${ctx.containerSource}/`, `${ctx.containerBind}/`);
1026
+ if (excludeNodeModules) args.push("--exclude=node_modules");
1027
+ args.push("/workspace/", `${CONTAINER_EXPORT_MERGED}/`);
1048
1028
  const r = await execInBox(record.container, args, { user: "root" });
1049
1029
  if (r.exitCode !== 0) {
1050
- throw new ExportError(`rsync into ${ctx.containerBind} failed`, r.stdout, r.stderr);
1030
+ throw new ExportError(`rsync into ${CONTAINER_EXPORT_MERGED} failed`, r.stdout, r.stderr);
1051
1031
  }
1052
- return { hostPath: ctx.hostTarget, copied: true, usedFallback: false };
1032
+ return { hostPath: paths.mergedExport, copied: true, usedFallback: false };
1053
1033
  }
1054
- const excludes = ctx.excludeNodeModules ? ["--exclude=node_modules"] : [];
1034
+ const excludes = excludeNodeModules ? ["--exclude=node_modules"] : [];
1055
1035
  const result = await execa2(
1056
1036
  "docker",
1057
- ["exec", "--user", "root", record.container, "tar", "-cf", "-", ...excludes, "-C", ctx.containerSource, "."],
1037
+ ["exec", "--user", "root", record.container, "tar", "-cf", "-", ...excludes, "-C", "/workspace", "."],
1058
1038
  { reject: false, encoding: "buffer" }
1059
1039
  );
1060
1040
  if (result.exitCode !== 0) {
1061
1041
  throw new ExportError(
1062
- `tar from ${ctx.containerSource} failed`,
1042
+ `tar from /workspace failed`,
1063
1043
  "",
1064
1044
  typeof result.stderr === "string" ? result.stderr : result.stderr.toString("utf8")
1065
1045
  );
1066
1046
  }
1067
- const extract = await execa2("tar", ["-xf", "-", "-C", ctx.hostTarget], {
1047
+ const extract = await execa2("tar", ["-xf", "-", "-C", paths.mergedExport], {
1068
1048
  input: result.stdout,
1069
1049
  reject: false
1070
1050
  });
1071
1051
  if (extract.exitCode !== 0) {
1072
1052
  throw new ExportError("tar extract on host failed", extract.stdout, extract.stderr);
1073
1053
  }
1074
- return { hostPath: ctx.hostTarget, copied: true, usedFallback: true };
1054
+ return { hostPath: paths.mergedExport, copied: true, usedFallback: true };
1075
1055
  }
1076
1056
  var DEFAULT_ENV_PATTERNS = [
1077
1057
  ".env",
@@ -1189,6 +1169,40 @@ async function copyHostEnvFilesToBox(opts) {
1189
1169
  }
1190
1170
  return { copied: list.length };
1191
1171
  }
1172
+ async function scanHostEnvFiles(workspaceDir, patterns) {
1173
+ if (patterns.length === 0) return [];
1174
+ const found = await execa2("find", buildHostEnvFindArgs(patterns).slice(1), {
1175
+ cwd: workspaceDir,
1176
+ reject: false
1177
+ });
1178
+ if (found.exitCode !== 0) return [];
1179
+ return String(found.stdout).split("\0").map((p) => p.replace(/^\.\//, "")).filter((p) => p.length > 0);
1180
+ }
1181
+ async function copyHostFilesToBox(opts) {
1182
+ const log = opts.onLog ?? (() => {
1183
+ });
1184
+ const list = opts.files.map((p) => p.replace(/^\.\//, "")).filter((p) => p.length > 0);
1185
+ if (list.length === 0) return { copied: 0 };
1186
+ const packed = await execa2("tar", ["-C", opts.workspaceDir, "--null", "-T", "-", "-cf", "-"], {
1187
+ input: list.join("\0"),
1188
+ encoding: "buffer",
1189
+ reject: false
1190
+ });
1191
+ if (packed.exitCode !== 0) {
1192
+ log(`warning: env-file tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
1193
+ return { copied: 0 };
1194
+ }
1195
+ const extract = await execa2(
1196
+ "docker",
1197
+ ["exec", "-i", "--user", "1000:1000", opts.container, "tar", "-xf", "-", "-C", "/workspace"],
1198
+ { input: packed.stdout, reject: false }
1199
+ );
1200
+ if (extract.exitCode !== 0) {
1201
+ log(`warning: env-file copy into box failed: ${String(extract.stderr).slice(0, 300)}`);
1202
+ return { copied: 0 };
1203
+ }
1204
+ return { copied: list.length };
1205
+ }
1192
1206
  function parseItemizedChanges(stdout) {
1193
1207
  return stdout.split("\n").map((l) => l.trimEnd()).filter((l) => l.length > 0).filter((l) => {
1194
1208
  const code = l[0];
@@ -1197,15 +1211,13 @@ function parseItemizedChanges(stdout) {
1197
1211
  });
1198
1212
  }
1199
1213
  async function pullToHost(record, opts = {}) {
1200
- const engine = await detectEngine();
1201
- const paths = await getHostPaths(record, engine);
1214
+ const paths = await getHostPaths(record);
1202
1215
  let scratchDir;
1203
1216
  if (opts.noRefresh) {
1204
1217
  scratchDir = paths.mergedExport;
1205
1218
  await mkdir2(scratchDir, { recursive: true });
1206
1219
  } else {
1207
1220
  const refreshed = await refreshExport(record, {
1208
- layer: "merged",
1209
1221
  includeNodeModules: opts.includeNodeModules
1210
1222
  });
1211
1223
  scratchDir = refreshed.hostPath;
@@ -1280,13 +1292,9 @@ async function openInFinder(record, opts) {
1280
1292
  let copied = false;
1281
1293
  let usedFallback = false;
1282
1294
  if (opts.noRefresh) {
1283
- const paths = await getHostPaths(record, engine);
1284
- if (opts.layer === "upper" && engine === "orbstack" && paths.upperLiveOnHost) {
1285
- hostPath = paths.upperLiveOnHost;
1286
- } else {
1287
- hostPath = opts.layer === "merged" ? paths.mergedExport : paths.upperExport;
1288
- await mkdir2(hostPath, { recursive: true });
1289
- }
1295
+ const paths = await getHostPaths(record);
1296
+ hostPath = paths.mergedExport;
1297
+ await mkdir2(hostPath, { recursive: true });
1290
1298
  } else {
1291
1299
  const refreshed = await refreshExport(record, opts);
1292
1300
  hostPath = refreshed.hostPath;
@@ -1369,13 +1377,13 @@ async function ensureImage(ref = DEFAULT_BOX_IMAGE, opts = {}) {
1369
1377
  return { ref, built: true };
1370
1378
  }
1371
1379
  var CHECKPOINTS_ROOT = join22(homedir22(), ".agentbox", "checkpoints");
1372
- var CHECKPOINT_VOLUME_PREFIX = "agentbox-ckpt-";
1373
- var CHECKPOINT_MOUNT = "/agentbox-checkpoints";
1374
- function checkpointVolumeName(projectRoot) {
1375
- return `${CHECKPOINT_VOLUME_PREFIX}${hashProjectPath(projectRoot)}`;
1380
+ var CHECKPOINT_IMAGE_PREFIX = "agentbox-ckpt-";
1381
+ function checkpointImageTag(projectRoot, name) {
1382
+ const mnemonic = sanitizeMnemonic(basename2(projectRoot));
1383
+ return `${CHECKPOINT_IMAGE_PREFIX}${hashProjectPath(projectRoot)}_${mnemonic}:${name}`;
1376
1384
  }
1377
1385
  function projectCheckpointsDir(projectRoot) {
1378
- return join22(CHECKPOINTS_ROOT, hashProjectPath(projectRoot));
1386
+ return join22(CHECKPOINTS_ROOT, projectDirSegment(projectRoot));
1379
1387
  }
1380
1388
  function checkpointDir(projectRoot, name) {
1381
1389
  return join22(projectCheckpointsDir(projectRoot), name);
@@ -1384,7 +1392,7 @@ async function readManifest(dir) {
1384
1392
  try {
1385
1393
  const raw = await readFile22(join22(dir, "manifest.json"), "utf8");
1386
1394
  const m = JSON.parse(raw);
1387
- if (m.schema !== 1) return null;
1395
+ if (m.schema !== 2) return null;
1388
1396
  return m;
1389
1397
  } catch {
1390
1398
  return null;
@@ -1413,17 +1421,35 @@ async function resolveCheckpoint(projectRoot, ref) {
1413
1421
  if (!manifest) return null;
1414
1422
  return { name: ref, dir, manifest };
1415
1423
  }
1424
+ async function listAllCheckpointImages() {
1425
+ let projectDirs;
1426
+ try {
1427
+ projectDirs = (await readdir2(CHECKPOINTS_ROOT, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
1428
+ } catch {
1429
+ return [];
1430
+ }
1431
+ const out = /* @__PURE__ */ new Set();
1432
+ for (const proj of projectDirs) {
1433
+ const projPath = join22(CHECKPOINTS_ROOT, proj);
1434
+ let names;
1435
+ try {
1436
+ names = (await readdir2(projPath, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
1437
+ } catch {
1438
+ continue;
1439
+ }
1440
+ for (const name of names) {
1441
+ const manifest = await readManifest(join22(projPath, name));
1442
+ if (manifest) out.add(manifest.image);
1443
+ }
1444
+ }
1445
+ return Array.from(out);
1446
+ }
1416
1447
  async function removeCheckpoint(projectRoot, ref) {
1417
1448
  const dir = checkpointDir(projectRoot, ref);
1418
1449
  const manifest = await readManifest(dir);
1419
1450
  if (!manifest) return false;
1420
1451
  await rm2(dir, { recursive: true, force: true });
1421
- const volume = manifest.volume || checkpointVolumeName(projectRoot);
1422
- await execa4(
1423
- "docker",
1424
- ["run", "--rm", "--user", "0:0", "-v", `${volume}:/dst`, DEFAULT_BOX_IMAGE, "rm", "-rf", `/dst/${ref}`],
1425
- { reject: false }
1426
- );
1452
+ await removeImage(manifest.image, { force: true });
1427
1453
  return true;
1428
1454
  }
1429
1455
  function computeNextCheckpointName(existingNames, boxName) {
@@ -1445,102 +1471,122 @@ async function nextCheckpointName(projectRoot, boxName) {
1445
1471
  function chainDepth(box) {
1446
1472
  return box.checkpointSource?.chain.length ?? 0;
1447
1473
  }
1448
- function shq(s) {
1449
- return `'${s.replace(/'/g, `'\\''`)}'`;
1474
+ async function runCleanup(container, log) {
1475
+ const r = await execInBox(container, ["/usr/local/bin/agentbox-checkpoint-cleanup"], {
1476
+ user: "root"
1477
+ });
1478
+ if (r.exitCode !== 0) {
1479
+ log(`warning: checkpoint cleanup exited ${String(r.exitCode)}: ${r.stderr.slice(0, 200)}`);
1480
+ }
1481
+ }
1482
+ async function inspectImageConfig(imageRef) {
1483
+ const r = await execa4("docker", ["image", "inspect", imageRef], { reject: false });
1484
+ if (r.exitCode !== 0) {
1485
+ throw new CheckpointError(`docker image inspect ${imageRef} failed`, r.stdout, r.stderr);
1486
+ }
1487
+ const parsed = JSON.parse(r.stdout);
1488
+ if (!Array.isArray(parsed) || parsed.length === 0 || !parsed[0]?.Config) {
1489
+ throw new CheckpointError(`unexpected docker image inspect shape for ${imageRef}`, r.stdout, "");
1490
+ }
1491
+ return parsed[0].Config;
1492
+ }
1493
+ var RUNTIME_ENV_BLOCKLIST = /* @__PURE__ */ new Set([
1494
+ "AGENTBOX",
1495
+ "AGENTBOX_BOX_ID",
1496
+ "AGENTBOX_BOX_NAME",
1497
+ "AGENTBOX_HOST_WORKSPACE",
1498
+ "AGENTBOX_PROJECT_ROOT",
1499
+ "AGENTBOX_PROJECT_INDEX",
1500
+ "AGENTBOX_RELAY_URL",
1501
+ "AGENTBOX_RELAY_TOKEN",
1502
+ "AGENTBOX_VNC_PASSWORD",
1503
+ "CLAUDE_EFFORT",
1504
+ "CLAUDE_CODE_OAUTH_TOKEN",
1505
+ "ANTHROPIC_API_KEY",
1506
+ "ANTHROPIC_MODEL"
1507
+ ]);
1508
+ function renderConfigDirectives(cfg) {
1509
+ const lines = [];
1510
+ for (const kv of cfg.Env ?? []) {
1511
+ const eq = kv.indexOf("=");
1512
+ if (eq <= 0) continue;
1513
+ const k = kv.slice(0, eq);
1514
+ if (RUNTIME_ENV_BLOCKLIST.has(k)) continue;
1515
+ const v = kv.slice(eq + 1);
1516
+ lines.push(`ENV ${k}=${dockerfileQuote(v)}`);
1517
+ }
1518
+ if (cfg.WorkingDir) lines.push(`WORKDIR ${cfg.WorkingDir}`);
1519
+ if (cfg.User) lines.push(`USER ${cfg.User}`);
1520
+ for (const p of Object.keys(cfg.ExposedPorts ?? {})) lines.push(`EXPOSE ${p.replace("/tcp", "")}`);
1521
+ if (cfg.Entrypoint && cfg.Entrypoint.length > 0) {
1522
+ lines.push(`ENTRYPOINT ${JSON.stringify(cfg.Entrypoint)}`);
1523
+ }
1524
+ if (cfg.Cmd && cfg.Cmd.length > 0) {
1525
+ lines.push(`CMD ${JSON.stringify(cfg.Cmd)}`);
1526
+ }
1527
+ return lines;
1528
+ }
1529
+ function dockerfileQuote(v) {
1530
+ if (/^[A-Za-z0-9._/:+@,=-]+$/.test(v)) return v;
1531
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1450
1532
  }
1451
1533
  async function createCheckpoint(opts) {
1452
1534
  const log = opts.onLog ?? (() => {
1453
1535
  });
1454
1536
  const { box } = opts;
1455
- const type = opts.merged === true || chainDepth(box) >= opts.maxLayers ? "merged" : "layered";
1537
+ const type = opts.merged === true || chainDepth(box) >= opts.maxLayers ? "flattened" : "layered";
1456
1538
  const name = opts.name ?? await nextCheckpointName(opts.projectRoot, box.name);
1457
1539
  const dir = checkpointDir(opts.projectRoot, name);
1458
- if (await readManifest(dir)) {
1459
- throw new CheckpointError(`checkpoint ${name} already exists (rm it first)`, "", "");
1540
+ const existing = await readManifest(dir);
1541
+ if (existing) {
1542
+ if (opts.replace) {
1543
+ log(`replacing existing checkpoint ${name} (created ${existing.createdAt})`);
1544
+ await removeCheckpoint(opts.projectRoot, name);
1545
+ } else {
1546
+ throw new CheckpointError(
1547
+ `checkpoint ${name} already exists (created ${existing.createdAt}; rm it or pass --replace to recapture)`,
1548
+ "",
1549
+ ""
1550
+ );
1551
+ }
1460
1552
  }
1461
- const volume = checkpointVolumeName(opts.projectRoot);
1462
- await ensureVolume(volume);
1553
+ const tag = checkpointImageTag(opts.projectRoot, name);
1463
1554
  await mkdir22(dir, { recursive: true });
1464
- const qn = shq(name);
1555
+ log(`running pre-commit cleanup in ${box.container}`);
1556
+ await runCleanup(box.container, log);
1465
1557
  if (type === "layered") {
1466
- log(`capturing upper delta of ${box.container} -> ${volume}/${name} (layered)`);
1467
- const script = [
1468
- "set -u",
1469
- `rm -rf /dst/${qn}`,
1470
- `mkdir -p /dst/${qn}`,
1471
- `cp -a /src/upper/. /dst/${qn}/ 2>/dev/null || true`,
1472
- `ls -A /dst/${qn} >/dev/null`
1473
- ].join("\n");
1474
- const r = await execa4(
1475
- "docker",
1476
- [
1477
- "run",
1478
- "--rm",
1479
- "--user",
1480
- "0:0",
1481
- "-v",
1482
- `${box.upperVolume}:/src:ro`,
1483
- "-v",
1484
- `${volume}:/dst`,
1485
- box.image,
1486
- "bash",
1487
- "-lc",
1488
- script
1489
- ],
1490
- { reject: false }
1491
- );
1558
+ log(`docker commit ${box.container} -> ${tag} (layered)`);
1559
+ const r = await execa4("docker", ["commit", box.container, tag], { reject: false });
1492
1560
  if (r.exitCode !== 0) {
1493
- throw new CheckpointError(`failed to copy upper layer for ${box.name}`, r.stdout, r.stderr);
1561
+ throw new CheckpointError(`docker commit failed for ${box.container}`, r.stdout, r.stderr);
1494
1562
  }
1495
1563
  } else {
1496
- log(`capturing merged /workspace of ${box.container} -> ${volume}/${name} (merged)`);
1497
- const packed = await execa4(
1498
- "docker",
1499
- ["exec", "--user", "root", box.container, "tar", "-C", "/workspace", "-cf", "-", "."],
1500
- { reject: false, encoding: "buffer" }
1501
- );
1502
- if (packed.exitCode !== 0) {
1503
- throw new CheckpointError(
1504
- `failed to tar merged /workspace for ${box.name} (is the box running?)`,
1505
- "",
1506
- typeof packed.stderr === "string" ? packed.stderr : packed.stderr.toString("utf8")
1507
- );
1564
+ log(`docker commit ${box.container} -> <intermediate> (flattened path)`);
1565
+ const intermediate = `${tag}-intermediate`;
1566
+ const commit = await execa4("docker", ["commit", box.container, intermediate], {
1567
+ reject: false
1568
+ });
1569
+ if (commit.exitCode !== 0) {
1570
+ throw new CheckpointError(`docker commit (intermediate) failed`, commit.stdout, commit.stderr);
1508
1571
  }
1509
- const extract = await execa4(
1510
- "docker",
1511
- [
1512
- "run",
1513
- "-i",
1514
- "--rm",
1515
- "--user",
1516
- "0:0",
1517
- "-v",
1518
- `${volume}:/dst`,
1519
- box.image,
1520
- "bash",
1521
- "-lc",
1522
- `set -u; rm -rf /dst/${qn}; mkdir -p /dst/${qn}; tar -xf - -C /dst/${qn}`
1523
- ],
1524
- { input: packed.stdout, reject: false }
1525
- );
1526
- if (extract.exitCode !== 0) {
1527
- throw new CheckpointError(
1528
- "tar extract into checkpoint volume failed",
1529
- extract.stdout,
1530
- extract.stderr
1531
- );
1572
+ try {
1573
+ await flattenImage(intermediate, tag, log);
1574
+ } finally {
1575
+ await removeImage(intermediate, { force: true });
1532
1576
  }
1533
1577
  }
1534
1578
  const base = (box.gitWorktrees ?? []).some((w) => w.kind === "root") ? "worktree" : "workspace";
1535
1579
  const manifest = {
1536
- schema: 1,
1580
+ schema: 2,
1537
1581
  name,
1538
1582
  type,
1583
+ image: tag,
1584
+ // Layered carries lineage forward; flattened is self-contained.
1539
1585
  parents: type === "layered" ? box.checkpointSource?.chain ?? [] : [],
1540
1586
  base,
1541
1587
  sourceBoxId: box.id,
1542
1588
  sourceBoxName: box.name,
1543
- volume,
1589
+ worktrees: box.gitWorktrees,
1544
1590
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1545
1591
  };
1546
1592
  await writeFile2(join22(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
@@ -1550,35 +1596,45 @@ async function createCheckpoint(opts) {
1550
1596
  }
1551
1597
  return { name, dir, manifest };
1552
1598
  }
1553
- async function resolveCheckpointLower(projectRoot, ref) {
1554
- const head = await resolveCheckpoint(projectRoot, ref);
1555
- if (!head) throw new CheckpointError(`checkpoint not found: ${ref}`, "", "");
1556
- if (!head.manifest.volume) {
1557
- throw new CheckpointError(
1558
- `checkpoint ${ref} is a legacy host-dir checkpoint; recreate it`,
1559
- "",
1560
- ""
1561
- );
1562
- }
1563
- const volume = head.manifest.volume;
1564
- if (head.manifest.type === "merged") {
1565
- return { type: "merged", volume, subpaths: [head.name], chain: [head.name] };
1599
+ async function flattenImage(sourceTag, destTag, log) {
1600
+ const tmpName = `agentbox-flatten-${Date.now().toString(36)}`;
1601
+ const create = await execa4(
1602
+ "docker",
1603
+ ["create", "--name", tmpName, sourceTag, "sleep", "0"],
1604
+ { reject: false }
1605
+ );
1606
+ if (create.exitCode !== 0) {
1607
+ throw new CheckpointError(`docker create for flatten failed`, create.stdout, create.stderr);
1566
1608
  }
1567
- const subpaths = [head.name];
1568
- const chain = [head.name];
1569
- for (const parentRef of head.manifest.parents) {
1570
- const p = await resolveCheckpoint(projectRoot, parentRef);
1571
- if (!p) {
1572
- throw new CheckpointError(
1573
- `checkpoint ${ref} references missing parent ${parentRef}`,
1574
- "",
1575
- ""
1576
- );
1609
+ const scratch = await mkdtemp(join22(tmpdir(), "agentbox-flatten-"));
1610
+ try {
1611
+ const rootfsPath = join22(scratch, "rootfs.tar");
1612
+ log(`exporting rootfs of ${sourceTag} to ${rootfsPath}`);
1613
+ const exp = await execa4("docker", ["export", "-o", rootfsPath, tmpName], { reject: false });
1614
+ if (exp.exitCode !== 0) {
1615
+ throw new CheckpointError(`docker export failed`, exp.stdout, exp.stderr);
1616
+ }
1617
+ const cfg = await inspectImageConfig(sourceTag);
1618
+ const lines = [
1619
+ "FROM scratch",
1620
+ // ADD untars during build (Docker's documented behavior for local tars).
1621
+ "ADD rootfs.tar /",
1622
+ ...renderConfigDirectives(cfg)
1623
+ ];
1624
+ await writeFile2(join22(scratch, "Dockerfile"), lines.join("\n") + "\n", "utf8");
1625
+ log(`building flattened ${destTag} from rootfs.tar (FROM scratch)`);
1626
+ const build = await execa4(
1627
+ "docker",
1628
+ ["build", "-t", destTag, "-f", join22(scratch, "Dockerfile"), scratch],
1629
+ { reject: false }
1630
+ );
1631
+ if (build.exitCode !== 0) {
1632
+ throw new CheckpointError(`flatten docker build failed`, build.stdout, build.stderr);
1577
1633
  }
1578
- subpaths.push(p.name);
1579
- chain.push(p.name);
1634
+ } finally {
1635
+ await execa4("docker", ["rm", "-f", tmpName], { reject: false });
1636
+ await rm2(scratch, { recursive: true, force: true });
1580
1637
  }
1581
- return { type: "layered", volume, subpaths, chain };
1582
1638
  }
1583
1639
  var CheckpointError = class extends Error {
1584
1640
  constructor(message, stdout, stderr) {
@@ -1596,6 +1652,7 @@ export {
1596
1652
  lookupKey,
1597
1653
  UserConfigError,
1598
1654
  findProjectRoot,
1655
+ sanitizeMnemonic,
1599
1656
  configPathFor,
1600
1657
  loadEffectiveConfig,
1601
1658
  setConfigValue,
@@ -1625,7 +1682,6 @@ export {
1625
1682
  publishedHostPort,
1626
1683
  listAgentboxVolumes,
1627
1684
  CONTAINER_EXPORT_MERGED,
1628
- CONTAINER_EXPORT_UPPER,
1629
1685
  detectEngine,
1630
1686
  setEngineOverride,
1631
1687
  BOXES_ROOT,
@@ -1636,16 +1692,18 @@ export {
1636
1692
  refreshExport,
1637
1693
  DEFAULT_ENV_PATTERNS,
1638
1694
  copyHostEnvFilesToBox,
1695
+ scanHostEnvFiles,
1696
+ copyHostFilesToBox,
1639
1697
  pullToHost,
1640
1698
  openInFinder,
1641
1699
  DEFAULT_BOX_IMAGE,
1642
1700
  ensureImage,
1643
- CHECKPOINT_VOLUME_PREFIX,
1644
- CHECKPOINT_MOUNT,
1645
- checkpointVolumeName,
1701
+ CHECKPOINT_IMAGE_PREFIX,
1702
+ checkpointImageTag,
1646
1703
  listCheckpoints,
1704
+ resolveCheckpoint,
1705
+ listAllCheckpointImages,
1647
1706
  removeCheckpoint,
1648
- createCheckpoint,
1649
- resolveCheckpointLower
1707
+ createCheckpoint
1650
1708
  };
1651
- //# sourceMappingURL=chunk-SOMIKEN2.js.map
1709
+ //# sourceMappingURL=chunk-RFC5F5HR.js.map