@madarco/agentbox 0.5.0 → 0.7.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/_cloud-attach-DMVH6GWO.js +12 -0
- package/dist/chunk-7KOEFGN2.js +1162 -0
- package/dist/chunk-7KOEFGN2.js.map +1 -0
- package/dist/chunk-I24B6AXR.js +600 -0
- package/dist/chunk-I24B6AXR.js.map +1 -0
- package/dist/chunk-NAVL4R34.js +7546 -0
- package/dist/chunk-NAVL4R34.js.map +1 -0
- package/dist/chunk-NW5NYTQM.js +1366 -0
- package/dist/chunk-NW5NYTQM.js.map +1 -0
- package/dist/chunk-UK72UQ5U.js +237 -0
- package/dist/chunk-UK72UQ5U.js.map +1 -0
- package/dist/chunk-V5KZGB5V.js +722 -0
- package/dist/chunk-V5KZGB5V.js.map +1 -0
- package/dist/cloud-poller-ZIWSADJB-JXFRJUEM.js +10 -0
- package/dist/dist-ETCFRVPA.js +423 -0
- package/dist/dist-QZGJIBT5.js +1339 -0
- package/dist/dist-QZGJIBT5.js.map +1 -0
- package/dist/dist-R67WMLCF.js +183 -0
- package/dist/dist-R67WMLCF.js.map +1 -0
- package/dist/index.js +4088 -1451
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/runtime/docker/Dockerfile.box +115 -19
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +34 -19
- package/runtime/docker/packages/ctl/dist/bin.cjs +10246 -758
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +37 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
- package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
- package/runtime/hetzner/agentbox-codex-hooks.json +37 -0
- package/runtime/hetzner/agentbox-dockerd-start +132 -0
- package/runtime/hetzner/agentbox-open +28 -0
- package/runtime/hetzner/agentbox-setup-skill.md +196 -0
- package/runtime/hetzner/agentbox-vnc-start +77 -0
- package/runtime/hetzner/claude-managed-settings.json +54 -0
- package/runtime/hetzner/ctl.cjs +22350 -0
- package/runtime/hetzner/custom-system-CLAUDE.md +27 -0
- package/runtime/hetzner/scripts/install-box.sh +365 -0
- package/runtime/relay/bin.cjs +9182 -754
- package/share/agentbox-setup/SKILL.md +34 -19
- package/dist/chunk-6VTAPD4H.js +0 -507
- package/dist/chunk-6VTAPD4H.js.map +0 -1
- package/dist/chunk-7J5AJLWG.js +0 -238
- package/dist/chunk-7J5AJLWG.js.map +0 -1
- package/dist/chunk-FJNIFTWK.js +0 -523
- package/dist/chunk-FJNIFTWK.js.map +0 -1
- package/dist/chunk-HPZMD5DE.js +0 -106
- package/dist/chunk-HPZMD5DE.js.map +0 -1
- package/dist/chunk-PXUBE5KS.js +0 -2346
- package/dist/chunk-PXUBE5KS.js.map +0 -1
- package/dist/chunk-RFC5F5HR.js +0 -1709
- package/dist/chunk-RFC5F5HR.js.map +0 -1
- package/dist/create-AHZ3GVEZ-TGEDL7UX.js +0 -15
- package/dist/lifecycle-LFOL6YFM-TCHDX3J5.js +0 -38
- package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js +0 -19
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js.map +0 -1
- /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → _cloud-attach-DMVH6GWO.js.map} +0 -0
- /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → cloud-poller-ZIWSADJB-JXFRJUEM.js.map} +0 -0
- /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-ETCFRVPA.js.map} +0 -0
|
@@ -7,16 +7,22 @@ description: Generate an agentbox.yaml for the current AgentBox workspace. Invok
|
|
|
7
7
|
|
|
8
8
|
## Box layout (what you're configuring against)
|
|
9
9
|
|
|
10
|
-
Your user i `vscode` and you can use
|
|
10
|
+
Your user i `vscode` and you can use `sudo` to run commands as root.
|
|
11
11
|
|
|
12
|
-
`/workspace` is the
|
|
12
|
+
`/workspace` is where the user code lives, a per-box git worktree on a fresh `agentbox/<box-name>` branch (or a tar-piped copy of the host workspace for non-git projects).
|
|
13
|
+
Run `agentbox checkpoint --set-default` (similar to `docker commit`) to save any changes make to the system and workspace so that new boxes will start from a warm state. Everything is wiped on `agentbox destroy`.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Some special folders:
|
|
15
16
|
|
|
16
|
-
- **Host main repo's `.git/`** — bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`.
|
|
17
|
-
- **`~/.claude`** —
|
|
17
|
+
- **Host main repo's `.git/`** — If the box bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`.
|
|
18
|
+
- **`~/.claude`** — and similar home folders for coding agents are seeded from the host's `~/.claude` on each create so auth, skills, and plugins persist without leaking the host's home dir.
|
|
18
19
|
- **`agentbox.yaml`** — read by `agentbox-ctl` from `/workspace`. Tasks and services declared here are what the supervisor will run.
|
|
19
20
|
|
|
21
|
+
Exposed ports and services:
|
|
22
|
+
- **portless** - every port with `expose:` setting in agentbox.yaml, will be exposed not only as a local port but also as a special domain name `https://<name>.localhost` (so on https) using `portless` cli and proxy. This will be also mapped to the host where also `portless` proxy is running so users can access the same service on the same looking url.
|
|
23
|
+
- **vnc** - the webVNC server exposed on 6080 will be proxies to the host on a random port.
|
|
24
|
+
- **vscode** - the vscode server is proxied to the host on a random port.
|
|
25
|
+
|
|
20
26
|
## Goal
|
|
21
27
|
|
|
22
28
|
Produce a `/workspace/agentbox.yaml` that captures this project's services, tasks, and box defaults so the in-box supervisor (`agentbox-ctl`) can boot the workspace deterministically.
|
|
@@ -64,7 +70,7 @@ The box's primary web app (the dev server / Next.js / API the user opens in a br
|
|
|
64
70
|
as: 80 # must be 80 — the container port AgentBox publishes
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
At most **one** service may set `expose:`. AgentBox forwards container `:80` to `127.0.0.1:<port>` and publishes it on the host, so `agentbox list`/`status` show it as the box's main URL on every engine (no OrbStack dependency). Set this on the same service whose `ready_when:` you just wrote (a DB or worker should **not** get `expose:`).
|
|
73
|
+
At most **one** service may set `expose:`. AgentBox forwards container `:80` to `127.0.0.1:<port>` and publishes it on the host with `portless` proxy to a <boxname>.localhost url, so `agentbox list`/`status` show it as the box's main URL on every engine (no OrbStack dependency). Set this on the same service whose `ready_when:` you just wrote (a DB or worker should **not** get `expose:`).
|
|
68
74
|
|
|
69
75
|
## 4. Restart + backoff
|
|
70
76
|
|
|
@@ -158,24 +164,33 @@ services:
|
|
|
158
164
|
- A service with `restart: never` and an autostart dependency will block the dependent forever after one failed run — usually a mistake.
|
|
159
165
|
- `command:` is either a shell string (run via `bash -c`) or an argv array. Use the argv form if you need to avoid shell quoting.
|
|
160
166
|
|
|
161
|
-
## 8.
|
|
167
|
+
## 8. Checkpoint the warm state (do this at the very end)
|
|
168
|
+
|
|
169
|
+
Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --name setup --replace --set-default` so future boxes start ready.
|
|
170
|
+
Run this command exactly once. The `--name setup --replace` makes it idempotent — if it ever needs to run again it overwrites the existing `setup` checkpoint instead of stacking duplicates.
|
|
171
|
+
This doesn't need to be confirmed by the user.
|
|
172
|
+
It will pause the container for several seconds so warn the user about it and write Done when it's done.
|
|
173
|
+
|
|
174
|
+
## 9. Hand-off
|
|
162
175
|
|
|
163
|
-
|
|
164
|
-
2. **Apply it live**: from inside the box run `agentbox-ctl reload`. The already-running supervisor re-reads the config and immediately runs the declared tasks and autostarts the services — no box restart needed. It prints the `added` / `removed` / `changed` diff. If it errors because the daemon isn't running, the config is still valid: the next `agentbox start` (or `agentbox create` in this workspace) picks it up automatically.
|
|
165
|
-
3. Confirm with `agentbox-ctl status`: tasks should be `running` or `done`, autostart services `starting` or `ready`. If something failed, tail it with `agentbox-ctl logs <service>` and fix the config, then `agentbox-ctl reload` again.
|
|
166
|
-
4. Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready.
|
|
176
|
+
Tell the user (verbatim):
|
|
167
177
|
|
|
168
|
-
|
|
178
|
+
```
|
|
179
|
+
█████╗ ██████╗ ███████╗███╗ ██╗████████╗██████╗ ██████╗ ██╗ ██╗
|
|
180
|
+
██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██╔═══██╗╚██╗██╔╝
|
|
181
|
+
███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██████╔╝██║ ██║ ╚███╔╝
|
|
182
|
+
██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██╔══██╗██║ ██║ ██╔██╗
|
|
183
|
+
██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██████╔╝╚██████╔╝██╔╝ ██╗
|
|
184
|
+
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
185
|
+
```
|
|
169
186
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
> - commit it inside the box (`git add agentbox.yaml && git commit -m 'add agentbox config'`) — the box's `.git/` is bind-mounted, so the commit shows up on the host immediately; or
|
|
173
|
-
> - on the host, tell the user to run `agentbox download config` to update their original host workspace.
|
|
187
|
+
your box is ready, you can start more sessions with `agentbox claude`
|
|
188
|
+
you can access the web app at https://<boxname>.localhost
|
|
174
189
|
|
|
175
|
-
##
|
|
190
|
+
## 10. Known issues
|
|
176
191
|
|
|
177
192
|
- For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
|
|
178
193
|
|
|
179
|
-
-
|
|
194
|
+
- Service like flask, nextjs, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL should use the <boxname>.localhost url for the local development so that on the host it will use the same url as the box.
|
|
180
195
|
|
|
181
|
-
-
|
|
196
|
+
- The `install` task is intentionally a no-op once `node_modules/.agentbox-installed` exists. Do **not** remove the marker guard to "force a fresh install" — that reinstalls on every box start. To force a one-off rebuild, delete `node_modules` (or just the marker) then run `agentbox-ctl reload`.
|
package/dist/chunk-6VTAPD4H.js
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ConfigError,
|
|
4
|
-
VNC_CONTAINER_PORT,
|
|
5
|
-
WEB_CONTAINER_PORT,
|
|
6
|
-
bindWorktrees,
|
|
7
|
-
buildClaudeMounts,
|
|
8
|
-
buildIdeMounts,
|
|
9
|
-
chownGitBindParents,
|
|
10
|
-
collectRepoCarryOver,
|
|
11
|
-
createSnapshot,
|
|
12
|
-
cursorServerVolumeName,
|
|
13
|
-
detectGitRepos,
|
|
14
|
-
dockerVolumeName,
|
|
15
|
-
ensureClaudeVolume,
|
|
16
|
-
ensureIdeVolumes,
|
|
17
|
-
ensureRelay,
|
|
18
|
-
generateRelayToken,
|
|
19
|
-
generateVncPassword,
|
|
20
|
-
gitWorktreePathFor,
|
|
21
|
-
launchCtlDaemon,
|
|
22
|
-
launchDockerdDaemon,
|
|
23
|
-
launchVncDaemon,
|
|
24
|
-
loadConfig,
|
|
25
|
-
pickFreshBranch,
|
|
26
|
-
registerBoxWithRelay,
|
|
27
|
-
rehydrateRelayRegistry,
|
|
28
|
-
repairIdeOwnership,
|
|
29
|
-
resolveClaudeVolume,
|
|
30
|
-
seedSetupSkillIntoVolume,
|
|
31
|
-
seedWorkspace,
|
|
32
|
-
seedWorkspaceFromDir,
|
|
33
|
-
snapshotPathFor,
|
|
34
|
-
vscodeServerVolumeName
|
|
35
|
-
} from "./chunk-PXUBE5KS.js";
|
|
36
|
-
import {
|
|
37
|
-
allocateProjectIndex,
|
|
38
|
-
readState,
|
|
39
|
-
recordBox
|
|
40
|
-
} from "./chunk-HPZMD5DE.js";
|
|
41
|
-
import {
|
|
42
|
-
CONTAINER_EXPORT_MERGED,
|
|
43
|
-
DEFAULT_BOX_IMAGE,
|
|
44
|
-
DEFAULT_ENV_PATTERNS,
|
|
45
|
-
boxRunDirFor,
|
|
46
|
-
containerExists,
|
|
47
|
-
copyHostEnvFilesToBox,
|
|
48
|
-
copyHostFilesToBox,
|
|
49
|
-
dockerInfo,
|
|
50
|
-
dockerStorageDriver,
|
|
51
|
-
ensureImage,
|
|
52
|
-
ensureVolume,
|
|
53
|
-
publishedHostPort,
|
|
54
|
-
resolveCheckpoint,
|
|
55
|
-
runBox
|
|
56
|
-
} from "./chunk-RFC5F5HR.js";
|
|
57
|
-
|
|
58
|
-
// ../../packages/sandbox-docker/dist/chunk-NCSJPHDB.js
|
|
59
|
-
import { randomBytes } from "crypto";
|
|
60
|
-
import { mkdir, stat } from "fs/promises";
|
|
61
|
-
import { homedir } from "os";
|
|
62
|
-
import { basename, join, resolve } from "path";
|
|
63
|
-
import { execa as execa2 } from "execa";
|
|
64
|
-
import { execa } from "execa";
|
|
65
|
-
async function writeBoxEnvFile(container, env) {
|
|
66
|
-
const body = formatBoxEnvBody(env);
|
|
67
|
-
const result = await execa(
|
|
68
|
-
"docker",
|
|
69
|
-
["exec", "--user", "root", "-i", container, "sh", "-c", "umask 022 && cat > /etc/agentbox/box.env"],
|
|
70
|
-
{ input: body, reject: false }
|
|
71
|
-
);
|
|
72
|
-
if (result.exitCode !== 0) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
reason: `docker exec failed (exit ${String(result.exitCode)}): ${(result.stderr ?? "").toString().slice(0, 400)}`
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
return { ok: true };
|
|
79
|
-
}
|
|
80
|
-
function formatBoxEnvBody(env) {
|
|
81
|
-
const lines = [];
|
|
82
|
-
for (const [k, v] of Object.entries(env)) {
|
|
83
|
-
lines.push(`${k}=${shellSingleQuote(v)}`);
|
|
84
|
-
}
|
|
85
|
-
return lines.join("\n") + "\n";
|
|
86
|
-
}
|
|
87
|
-
function shellSingleQuote(s) {
|
|
88
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
89
|
-
}
|
|
90
|
-
function persistableLimits(lim) {
|
|
91
|
-
if (!lim) return void 0;
|
|
92
|
-
const out = {};
|
|
93
|
-
if (lim.memoryBytes && lim.memoryBytes > 0) out.memoryBytes = Math.floor(lim.memoryBytes);
|
|
94
|
-
if (lim.cpus && lim.cpus > 0) out.cpus = lim.cpus;
|
|
95
|
-
if (lim.pidsLimit && lim.pidsLimit > 0) out.pidsLimit = Math.floor(lim.pidsLimit);
|
|
96
|
-
if (lim.disk) out.disk = lim.disk;
|
|
97
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
98
|
-
}
|
|
99
|
-
function generateBoxId() {
|
|
100
|
-
return randomBytes(4).toString("hex");
|
|
101
|
-
}
|
|
102
|
-
function sanitizeBasename(workspacePath) {
|
|
103
|
-
const raw = basename(resolve(workspacePath));
|
|
104
|
-
return raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "").slice(0, 30).replace(/[-._]+$/, "");
|
|
105
|
-
}
|
|
106
|
-
function defaultBoxName(workspacePath, id) {
|
|
107
|
-
const base = sanitizeBasename(workspacePath);
|
|
108
|
-
return base.length > 0 ? `${base}-${id}` : id;
|
|
109
|
-
}
|
|
110
|
-
async function pathExists(p) {
|
|
111
|
-
try {
|
|
112
|
-
await stat(p);
|
|
113
|
-
return true;
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async function buildIdentityMounts() {
|
|
119
|
-
const home = homedir();
|
|
120
|
-
const candidates = [
|
|
121
|
-
{ src: join(home, ".codex"), dst: "/home/vscode/.codex", readOnly: false },
|
|
122
|
-
{ src: join(home, ".gitconfig"), dst: "/home/vscode/.gitconfig", readOnly: true }
|
|
123
|
-
];
|
|
124
|
-
const out = [];
|
|
125
|
-
for (const c of candidates) {
|
|
126
|
-
if (await pathExists(c.src)) {
|
|
127
|
-
out.push(`${c.src}:${c.dst}${c.readOnly ? ":ro" : ""}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
async function createBox(opts) {
|
|
133
|
-
const log = opts.onLog ?? (() => {
|
|
134
|
-
});
|
|
135
|
-
const workspace = resolve(opts.workspacePath);
|
|
136
|
-
if (!await pathExists(workspace)) {
|
|
137
|
-
throw new Error(`workspace does not exist: ${workspace}`);
|
|
138
|
-
}
|
|
139
|
-
const cfgPath = join(workspace, "agentbox.yaml");
|
|
140
|
-
if (await pathExists(cfgPath)) {
|
|
141
|
-
try {
|
|
142
|
-
const cfg = await loadConfig(cfgPath);
|
|
143
|
-
log(`agentbox.yaml validated (${String(cfg.services.length)} service(s))`);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
if (err instanceof ConfigError) {
|
|
146
|
-
throw new Error(`agentbox.yaml validation failed:
|
|
147
|
-
${err.message}`);
|
|
148
|
-
}
|
|
149
|
-
throw err;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
await dockerInfo();
|
|
153
|
-
log("docker daemon reachable");
|
|
154
|
-
let checkpointImage;
|
|
155
|
-
let checkpointSource;
|
|
156
|
-
let restoredWorktrees;
|
|
157
|
-
if (opts.checkpointRef) {
|
|
158
|
-
const projectRootForCkpt = opts.projectRoot ?? workspace;
|
|
159
|
-
const head = await resolveCheckpoint(projectRootForCkpt, opts.checkpointRef);
|
|
160
|
-
if (!head) {
|
|
161
|
-
throw new Error(`checkpoint not found: ${opts.checkpointRef}`);
|
|
162
|
-
}
|
|
163
|
-
checkpointImage = head.manifest.image;
|
|
164
|
-
const chain = [head.name, ...head.manifest.parents];
|
|
165
|
-
checkpointSource = { ref: opts.checkpointRef, type: head.manifest.type, chain };
|
|
166
|
-
restoredWorktrees = head.manifest.worktrees;
|
|
167
|
-
log(
|
|
168
|
-
`starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;
|
|
172
|
-
const ensureRef = checkpointImage ? opts.image ?? DEFAULT_BOX_IMAGE : imageRef;
|
|
173
|
-
const { built } = await ensureImage(ensureRef, {
|
|
174
|
-
onProgress: (line) => log(`[image] ${line}`)
|
|
175
|
-
});
|
|
176
|
-
log(built ? `built image ${ensureRef}` : `using cached image ${imageRef}`);
|
|
177
|
-
let relayUp = false;
|
|
178
|
-
try {
|
|
179
|
-
await ensureRelay({ onLog: log });
|
|
180
|
-
const existing = await readState();
|
|
181
|
-
await rehydrateRelayRegistry(existing.boxes);
|
|
182
|
-
relayUp = true;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
log(`relay unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
185
|
-
}
|
|
186
|
-
const id = generateBoxId();
|
|
187
|
-
const name = opts.name ?? defaultBoxName(workspace, id);
|
|
188
|
-
const containerName = `agentbox-${name}`;
|
|
189
|
-
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
190
|
-
if (await containerExists(containerName)) {
|
|
191
|
-
throw new Error(`container ${containerName} already exists; remove it first`);
|
|
192
|
-
}
|
|
193
|
-
let projectIndex;
|
|
194
|
-
if (opts.projectRoot) {
|
|
195
|
-
projectIndex = allocateProjectIndex(await readState(), opts.projectRoot);
|
|
196
|
-
}
|
|
197
|
-
const repoCarryOvers = [];
|
|
198
|
-
const gitWorktreeRecords = [];
|
|
199
|
-
if (checkpointImage && restoredWorktrees && restoredWorktrees.length > 0) {
|
|
200
|
-
gitWorktreeRecords.push(...restoredWorktrees);
|
|
201
|
-
}
|
|
202
|
-
if (!checkpointImage) {
|
|
203
|
-
const repos = await detectGitRepos(workspace);
|
|
204
|
-
if (repos.length > 0) {
|
|
205
|
-
log(
|
|
206
|
-
`detected ${String(repos.length)} git repo(s): ` + repos.map((r) => `${r.kind}${r.relPathFromWorkspace ? "@" + r.relPathFromWorkspace : ""}`).join(", ")
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
for (const r of repos) {
|
|
210
|
-
const branchBase = r.kind === "root" ? `agentbox/${name}` : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, "_")}`;
|
|
211
|
-
const branch = await pickFreshBranch(r.hostMainRepo, branchBase);
|
|
212
|
-
const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
|
|
213
|
-
const gitWorktreePath = gitWorktreePathFor(branch);
|
|
214
|
-
const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);
|
|
215
|
-
repoCarryOvers.push(carry);
|
|
216
|
-
gitWorktreeRecords.push({
|
|
217
|
-
kind: r.kind,
|
|
218
|
-
hostMainRepo: r.hostMainRepo,
|
|
219
|
-
containerPath,
|
|
220
|
-
gitWorktreePath,
|
|
221
|
-
branch,
|
|
222
|
-
relPathFromWorkspace: r.relPathFromWorkspace
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
let snapshotDir = null;
|
|
227
|
-
const snapshotIsUseful = !checkpointImage && repoCarryOvers.length === 0;
|
|
228
|
-
if (opts.useSnapshot && snapshotIsUseful) {
|
|
229
|
-
snapshotDir = snapshotPathFor({ id, name, projectIndex });
|
|
230
|
-
log(`cloning workspace to ${snapshotDir} (APFS clone where available)`);
|
|
231
|
-
const snap = await createSnapshot({ source: workspace, destination: snapshotDir });
|
|
232
|
-
log(`pruned ${snap.prunedPaths.length} platform-dependent dirs from snapshot`);
|
|
233
|
-
} else if (opts.useSnapshot && !checkpointImage) {
|
|
234
|
-
log("skipping --host-snapshot: git worktree path reads content from .git, not from a workspace clone");
|
|
235
|
-
}
|
|
236
|
-
await ensureIdeVolumes(id);
|
|
237
|
-
const dockerCacheShared = opts.docker?.sharedCache === true;
|
|
238
|
-
const dockerVolume = dockerVolumeName(id, dockerCacheShared);
|
|
239
|
-
await ensureVolume(dockerVolume);
|
|
240
|
-
log(`prepared volumes ${vscodeServerVolumeName(id)}, ${cursorServerVolumeName(id)}, ${dockerVolume}`);
|
|
241
|
-
const ide = buildIdeMounts(id);
|
|
242
|
-
const claudeSpec = resolveClaudeVolume({
|
|
243
|
-
isolate: opts.claudeConfig?.isolate ?? false,
|
|
244
|
-
boxId: id
|
|
245
|
-
});
|
|
246
|
-
const claudeEnsured = await ensureClaudeVolume(claudeSpec, {
|
|
247
|
-
syncFromHost: true,
|
|
248
|
-
image: ensureRef,
|
|
249
|
-
hostWorkspace: workspace
|
|
250
|
-
});
|
|
251
|
-
if (claudeEnsured.synced) {
|
|
252
|
-
log(`synced ${claudeSpec.volume} from ~/.claude`);
|
|
253
|
-
if ((claudeEnsured.filteredHookCount ?? 0) > 0) {
|
|
254
|
-
log(
|
|
255
|
-
`filtered ${String(claudeEnsured.filteredHookCount)} host-path hook(s) (paths under ~/)`
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
if (claudeEnsured.clearedInstallMethod) {
|
|
259
|
-
log("cleared host's installMethod from synced .claude.json (box uses the native installer)");
|
|
260
|
-
}
|
|
261
|
-
if (claudeEnsured.aliasedProjectKey) {
|
|
262
|
-
log(`aliased project state for ${workspace} -> /workspace in synced .claude.json`);
|
|
263
|
-
}
|
|
264
|
-
} else if (claudeEnsured.created) {
|
|
265
|
-
log(`created empty volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
|
|
266
|
-
} else {
|
|
267
|
-
log(`reusing volume ${claudeSpec.volume} (no host ~/.claude to sync)`);
|
|
268
|
-
}
|
|
269
|
-
const seeded = await seedSetupSkillIntoVolume(claudeSpec.volume, ensureRef);
|
|
270
|
-
if (seeded.seeded) log(`seeded /agentbox-setup skill into ${claudeSpec.volume}`);
|
|
271
|
-
const claudeMounts = buildClaudeMounts(claudeSpec, process.env);
|
|
272
|
-
const boxDir = boxRunDirFor({ id, name, projectIndex });
|
|
273
|
-
const socketDir = join(boxDir, "run");
|
|
274
|
-
const socketPath = join(socketDir, "ctl.sock");
|
|
275
|
-
const mergedExportDir = join(boxDir, "workspace");
|
|
276
|
-
await mkdir(socketDir, { recursive: true });
|
|
277
|
-
await mkdir(mergedExportDir, { recursive: true });
|
|
278
|
-
const extraVolumes = await buildIdentityMounts();
|
|
279
|
-
extraVolumes.push(...claudeMounts.extraVolumes);
|
|
280
|
-
extraVolumes.push(...ide.extraVolumes);
|
|
281
|
-
extraVolumes.push(`${socketDir}:/run/agentbox`);
|
|
282
|
-
extraVolumes.push(`${mergedExportDir}:${CONTAINER_EXPORT_MERGED}`);
|
|
283
|
-
extraVolumes.push(`${dockerVolume}:/var/lib/docker`);
|
|
284
|
-
for (const w of gitWorktreeRecords) {
|
|
285
|
-
extraVolumes.push(`${w.hostMainRepo}/.git:${w.hostMainRepo}/.git`);
|
|
286
|
-
}
|
|
287
|
-
for (const v of extraVolumes) log(`mounting agent dir: ${v}`);
|
|
288
|
-
const relayToken = generateRelayToken();
|
|
289
|
-
if (relayUp) {
|
|
290
|
-
try {
|
|
291
|
-
await registerBoxWithRelay({
|
|
292
|
-
boxId: id,
|
|
293
|
-
token: relayToken,
|
|
294
|
-
name,
|
|
295
|
-
containerName,
|
|
296
|
-
createdAt,
|
|
297
|
-
projectIndex,
|
|
298
|
-
worktrees: gitWorktreeRecords
|
|
299
|
-
});
|
|
300
|
-
log(`registered box token with relay`);
|
|
301
|
-
} catch (err) {
|
|
302
|
-
log(`relay register failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
303
|
-
relayUp = false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
const relayEnv = relayUp ? {
|
|
307
|
-
// host.docker.internal resolves to the host (where the relay node
|
|
308
|
-
// process is running). The matching `--add-host` is set in runBox.
|
|
309
|
-
AGENTBOX_RELAY_URL: `http://host.docker.internal:8787`,
|
|
310
|
-
AGENTBOX_RELAY_TOKEN: relayToken
|
|
311
|
-
} : {};
|
|
312
|
-
const vncEnabled = opts.vnc?.enabled !== false;
|
|
313
|
-
const vncPassword = vncEnabled ? generateVncPassword() : void 0;
|
|
314
|
-
const vncEnv = vncEnabled && vncPassword ? { AGENTBOX_VNC_PASSWORD: vncPassword } : {};
|
|
315
|
-
const vncPortMappings = vncEnabled ? [{ hostPort: 0, containerPort: VNC_CONTAINER_PORT, hostIp: "127.0.0.1" }] : [];
|
|
316
|
-
const webPortMappings = [
|
|
317
|
-
{ hostPort: 0, containerPort: WEB_CONTAINER_PORT, hostIp: "127.0.0.1" }
|
|
318
|
-
];
|
|
319
|
-
const agentboxEnv = {
|
|
320
|
-
AGENTBOX: "1",
|
|
321
|
-
AGENTBOX_BOX_NAME: name,
|
|
322
|
-
AGENTBOX_HOST_WORKSPACE: workspace,
|
|
323
|
-
...opts.projectRoot ? { AGENTBOX_PROJECT_ROOT: opts.projectRoot } : {},
|
|
324
|
-
...projectIndex !== void 0 ? { AGENTBOX_PROJECT_INDEX: String(projectIndex) } : {}
|
|
325
|
-
};
|
|
326
|
-
const boxEnvForFile = {
|
|
327
|
-
AGENTBOX_BOX_ID: id,
|
|
328
|
-
...agentboxEnv
|
|
329
|
-
};
|
|
330
|
-
const appliedLimits = opts.limits;
|
|
331
|
-
let effectiveLimits = appliedLimits;
|
|
332
|
-
if (appliedLimits?.disk) {
|
|
333
|
-
const driver = await dockerStorageDriver();
|
|
334
|
-
if (!/^(devicemapper|btrfs|zfs|windowsfilter)$/.test(driver)) {
|
|
335
|
-
log(
|
|
336
|
-
`warning: --disk/box.disk is a no-op on this engine (storage-driver=${driver || "unknown"}); ignoring`
|
|
337
|
-
);
|
|
338
|
-
effectiveLimits = { ...appliedLimits, disk: null };
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
await runBox({
|
|
342
|
-
name: containerName,
|
|
343
|
-
image: imageRef,
|
|
344
|
-
extraVolumes,
|
|
345
|
-
limits: effectiveLimits,
|
|
346
|
-
portMappings: [...vncPortMappings, ...webPortMappings],
|
|
347
|
-
env: {
|
|
348
|
-
AGENTBOX_BOX_ID: id,
|
|
349
|
-
...agentboxEnv,
|
|
350
|
-
...claudeMounts.env,
|
|
351
|
-
...relayEnv,
|
|
352
|
-
...vncEnv,
|
|
353
|
-
...opts.claudeEnv ?? {}
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
log(`container ${containerName} started`);
|
|
357
|
-
if (gitWorktreeRecords.length > 0) {
|
|
358
|
-
await chownGitBindParents({
|
|
359
|
-
container: containerName,
|
|
360
|
-
hostMainRepos: gitWorktreeRecords.map((w) => w.hostMainRepo),
|
|
361
|
-
onLog: log
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
const boxEnv = await writeBoxEnvFile(containerName, boxEnvForFile);
|
|
365
|
-
if (boxEnv.ok) log("wrote /etc/agentbox/box.env");
|
|
366
|
-
else log(`writing /etc/agentbox/box.env failed: ${boxEnv.reason}`);
|
|
367
|
-
if (!checkpointImage) {
|
|
368
|
-
if (repoCarryOvers.length > 0) {
|
|
369
|
-
try {
|
|
370
|
-
await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });
|
|
371
|
-
log("seeded /workspace from in-container git worktree(s)");
|
|
372
|
-
} catch (err) {
|
|
373
|
-
log(
|
|
374
|
-
`seedWorkspace failed; leaving ${containerName} running so you can inspect it`
|
|
375
|
-
);
|
|
376
|
-
throw err;
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
const source = snapshotDir ?? workspace;
|
|
380
|
-
await seedWorkspaceFromDir({ container: containerName, hostSource: source, onLog: log });
|
|
381
|
-
}
|
|
382
|
-
} else if (restoredWorktrees && restoredWorktrees.length > 0) {
|
|
383
|
-
await bindWorktrees(
|
|
384
|
-
containerName,
|
|
385
|
-
restoredWorktrees.map((w) => ({
|
|
386
|
-
kind: w.kind,
|
|
387
|
-
containerPath: w.containerPath,
|
|
388
|
-
gitWorktreePath: w.gitWorktreePath
|
|
389
|
-
})),
|
|
390
|
-
log
|
|
391
|
-
);
|
|
392
|
-
log("re-bound /workspace from checkpoint image");
|
|
393
|
-
} else {
|
|
394
|
-
log("using /workspace from checkpoint image (no worktrees recorded; no rebind)");
|
|
395
|
-
}
|
|
396
|
-
await repairIdeOwnership(containerName);
|
|
397
|
-
log(".vscode-server + .cursor-server ownership verified");
|
|
398
|
-
const ctl = await launchCtlDaemon(containerName, socketPath);
|
|
399
|
-
if (ctl.up) log("agentbox-ctl daemon up");
|
|
400
|
-
else log(`agentbox-ctl daemon did not become reachable: ${ctl.reason}`);
|
|
401
|
-
const dockerd = await launchDockerdDaemon(containerName);
|
|
402
|
-
if (dockerd.up) {
|
|
403
|
-
log(`dockerd up (storage-driver=fuse-overlayfs, data root=${dockerVolume})`);
|
|
404
|
-
} else {
|
|
405
|
-
log(`dockerd did not become ready: ${dockerd.reason}`);
|
|
406
|
-
}
|
|
407
|
-
if (opts.withPlaywright) {
|
|
408
|
-
log("installing @playwright/cli@latest (--with-playwright)");
|
|
409
|
-
const result = await execa2(
|
|
410
|
-
"docker",
|
|
411
|
-
[
|
|
412
|
-
"exec",
|
|
413
|
-
"--user",
|
|
414
|
-
"root",
|
|
415
|
-
containerName,
|
|
416
|
-
"bash",
|
|
417
|
-
"-lc",
|
|
418
|
-
"npm install -g @playwright/cli@latest 2>&1"
|
|
419
|
-
],
|
|
420
|
-
{ reject: false }
|
|
421
|
-
);
|
|
422
|
-
for (const line of (result.stdout ?? "").split("\n")) {
|
|
423
|
-
if (line.trim().length > 0) log(`[playwright] ${line}`);
|
|
424
|
-
}
|
|
425
|
-
if (result.exitCode !== 0) {
|
|
426
|
-
throw new Error(
|
|
427
|
-
`failed to install @playwright/cli (exit ${String(result.exitCode)}): ${(result.stderr ?? "").toString().slice(0, 400)}`
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
log("@playwright/cli installed");
|
|
431
|
-
}
|
|
432
|
-
if (opts.withEnv) {
|
|
433
|
-
log("copying host env/config files into /workspace (--with-env)");
|
|
434
|
-
const { copied } = await copyHostEnvFilesToBox({
|
|
435
|
-
container: containerName,
|
|
436
|
-
workspaceDir: workspace,
|
|
437
|
-
patterns: DEFAULT_ENV_PATTERNS,
|
|
438
|
-
onLog: log
|
|
439
|
-
});
|
|
440
|
-
log(copied > 0 ? `copied ${String(copied)} env/config file(s)` : "no env/config files found");
|
|
441
|
-
}
|
|
442
|
-
if (opts.envFilesToImport && opts.envFilesToImport.length > 0) {
|
|
443
|
-
log(`copying ${String(opts.envFilesToImport.length)} selected env/config file(s) into /workspace`);
|
|
444
|
-
const { copied } = await copyHostFilesToBox({
|
|
445
|
-
container: containerName,
|
|
446
|
-
workspaceDir: workspace,
|
|
447
|
-
files: opts.envFilesToImport,
|
|
448
|
-
onLog: log
|
|
449
|
-
});
|
|
450
|
-
if (copied !== opts.envFilesToImport.length) {
|
|
451
|
-
log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
let vncHostPort = null;
|
|
455
|
-
if (vncEnabled) {
|
|
456
|
-
const vnc = await launchVncDaemon(containerName);
|
|
457
|
-
if (vnc.up) log("vnc stack up (Xvnc + websockify + noVNC)");
|
|
458
|
-
else log(`vnc stack did not become reachable: ${vnc.reason}`);
|
|
459
|
-
vncHostPort = await publishedHostPort(containerName, VNC_CONTAINER_PORT);
|
|
460
|
-
if (vncHostPort) log(`vnc web on host 127.0.0.1:${String(vncHostPort)}`);
|
|
461
|
-
}
|
|
462
|
-
const webHostPort = await publishedHostPort(containerName, WEB_CONTAINER_PORT);
|
|
463
|
-
if (webHostPort) {
|
|
464
|
-
log(
|
|
465
|
-
`web port reserved on host 127.0.0.1:${String(webHostPort)} (forwards to the web service once agentbox.yaml sets a service expose:)`
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
const record = {
|
|
469
|
-
id,
|
|
470
|
-
name,
|
|
471
|
-
container: containerName,
|
|
472
|
-
image: imageRef,
|
|
473
|
-
workspacePath: workspace,
|
|
474
|
-
snapshotDir,
|
|
475
|
-
socketPath,
|
|
476
|
-
claudeConfigVolume: claudeSpec.volume,
|
|
477
|
-
vscodeServerVolume: vscodeServerVolumeName(id),
|
|
478
|
-
cursorServerVolume: cursorServerVolumeName(id),
|
|
479
|
-
relayToken: relayUp ? relayToken : void 0,
|
|
480
|
-
gitWorktrees: gitWorktreeRecords.length > 0 ? gitWorktreeRecords : void 0,
|
|
481
|
-
withPlaywright: opts.withPlaywright ? true : void 0,
|
|
482
|
-
withEnv: opts.withEnv ? true : void 0,
|
|
483
|
-
vncEnabled: vncEnabled ? true : void 0,
|
|
484
|
-
vncContainerPort: vncEnabled ? VNC_CONTAINER_PORT : void 0,
|
|
485
|
-
vncHostPort: vncHostPort ?? void 0,
|
|
486
|
-
vncPassword,
|
|
487
|
-
webContainerPort: WEB_CONTAINER_PORT,
|
|
488
|
-
webHostPort: webHostPort ?? void 0,
|
|
489
|
-
dockerVolume,
|
|
490
|
-
dockerCacheShared: dockerCacheShared || void 0,
|
|
491
|
-
projectRoot: opts.projectRoot,
|
|
492
|
-
projectIndex,
|
|
493
|
-
checkpointImage,
|
|
494
|
-
checkpointSource,
|
|
495
|
-
resourceLimits: persistableLimits(effectiveLimits),
|
|
496
|
-
createdAt
|
|
497
|
-
};
|
|
498
|
-
await recordBox(record);
|
|
499
|
-
return { record, imageBuilt: built };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export {
|
|
503
|
-
sanitizeBasename,
|
|
504
|
-
defaultBoxName,
|
|
505
|
-
createBox
|
|
506
|
-
};
|
|
507
|
-
//# sourceMappingURL=chunk-6VTAPD4H.js.map
|