@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
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// ../../packages/sandbox-docker/dist/chunk-
|
|
3
|
+
// ../../packages/sandbox-docker/dist/chunk-3MREFIME.js
|
|
4
4
|
import { execa } from "execa";
|
|
5
|
-
import { mkdir as mkdir2, readFile as readFile3
|
|
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,
|
|
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
|
|
607
|
-
|
|
608
|
-
|
|
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.
|
|
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(
|
|
665
|
-
const metaPath = `${PROJECTS_DIR}/${
|
|
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-
|
|
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
|
|
971
|
-
|
|
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
|
|
974
|
-
return join3(
|
|
989
|
+
function boxRunDirFor(box) {
|
|
990
|
+
return join3(BOXES_ROOT, boxDirSegment(box));
|
|
975
991
|
}
|
|
976
|
-
|
|
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(
|
|
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
|
|
998
|
-
|
|
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
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
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 (
|
|
1047
|
-
args.push(
|
|
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 ${
|
|
1030
|
+
throw new ExportError(`rsync into ${CONTAINER_EXPORT_MERGED} failed`, r.stdout, r.stderr);
|
|
1051
1031
|
}
|
|
1052
|
-
return { hostPath:
|
|
1032
|
+
return { hostPath: paths.mergedExport, copied: true, usedFallback: false };
|
|
1053
1033
|
}
|
|
1054
|
-
const excludes =
|
|
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",
|
|
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
|
|
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",
|
|
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:
|
|
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
|
|
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
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
return `${
|
|
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,
|
|
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 !==
|
|
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
|
-
|
|
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
|
|
1449
|
-
|
|
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 ? "
|
|
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
|
-
|
|
1459
|
-
|
|
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
|
|
1462
|
-
await ensureVolume(volume);
|
|
1553
|
+
const tag = checkpointImageTag(opts.projectRoot, name);
|
|
1463
1554
|
await mkdir22(dir, { recursive: true });
|
|
1464
|
-
|
|
1555
|
+
log(`running pre-commit cleanup in ${box.container}`);
|
|
1556
|
+
await runCleanup(box.container, log);
|
|
1465
1557
|
if (type === "layered") {
|
|
1466
|
-
log(`
|
|
1467
|
-
const
|
|
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(`
|
|
1561
|
+
throw new CheckpointError(`docker commit failed for ${box.container}`, r.stdout, r.stderr);
|
|
1494
1562
|
}
|
|
1495
1563
|
} else {
|
|
1496
|
-
log(`
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
)
|
|
1502
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
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-
|
|
1709
|
+
//# sourceMappingURL=chunk-RFC5F5HR.js.map
|