@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
|
@@ -0,0 +1,1366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CLAUDE_FORWARDED_ENV_KEYS,
|
|
4
|
+
CODEX_FORWARDED_ENV_KEYS,
|
|
5
|
+
OPENCODE_FORWARDED_ENV_KEYS,
|
|
6
|
+
allocateProjectIndex,
|
|
7
|
+
buildHostEnvFindArgs,
|
|
8
|
+
detectGitRepos,
|
|
9
|
+
ensureRelay,
|
|
10
|
+
forgetBoxFromRelay,
|
|
11
|
+
generateRelayToken,
|
|
12
|
+
generateVncPassword,
|
|
13
|
+
hashProjectPath,
|
|
14
|
+
portlessAlias,
|
|
15
|
+
portlessGetUrl,
|
|
16
|
+
portlessUnalias,
|
|
17
|
+
projectDirSegment,
|
|
18
|
+
readState,
|
|
19
|
+
recordBox,
|
|
20
|
+
registerBoxWithRelay,
|
|
21
|
+
removeBoxRecord,
|
|
22
|
+
sanitizeMnemonic,
|
|
23
|
+
stageClaudeCredentialsForUpload,
|
|
24
|
+
stageClaudeStaticForUpload,
|
|
25
|
+
stageCodexCredentialsForUpload,
|
|
26
|
+
stageCodexStaticForUpload,
|
|
27
|
+
stageOpencodeCredentialsForUpload,
|
|
28
|
+
stageOpencodeStaticForUpload
|
|
29
|
+
} from "./chunk-NAVL4R34.js";
|
|
30
|
+
|
|
31
|
+
// ../../packages/sandbox-cloud/dist/index.js
|
|
32
|
+
import { randomBytes } from "crypto";
|
|
33
|
+
import { basename as basename2 } from "path";
|
|
34
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
|
|
35
|
+
import { homedir } from "os";
|
|
36
|
+
import { basename, join } from "path";
|
|
37
|
+
import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
38
|
+
import { tmpdir } from "os";
|
|
39
|
+
import { join as join2 } from "path";
|
|
40
|
+
import { execa } from "execa";
|
|
41
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
42
|
+
import { join as join3 } from "path";
|
|
43
|
+
import { parse as parseYaml } from "yaml";
|
|
44
|
+
import { execa as execa2 } from "execa";
|
|
45
|
+
import { existsSync, mkdirSync, renameSync, statSync } from "fs";
|
|
46
|
+
import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
|
|
47
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
48
|
+
import {
|
|
49
|
+
basename as hostBasename,
|
|
50
|
+
dirname as hostDirname,
|
|
51
|
+
join as hostJoin,
|
|
52
|
+
resolve as hostResolve
|
|
53
|
+
} from "path";
|
|
54
|
+
import { posix } from "path";
|
|
55
|
+
import { execa as execa3 } from "execa";
|
|
56
|
+
import { mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
|
|
57
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
58
|
+
import { join as join4 } from "path";
|
|
59
|
+
var CREDENTIALS_VOLUME = "agentbox-credentials";
|
|
60
|
+
var AGENT_SPECS = [
|
|
61
|
+
{
|
|
62
|
+
kind: "claude",
|
|
63
|
+
staticMountPath: "/home/vscode/.claude",
|
|
64
|
+
credentialsMountPath: "/home/vscode/.agentbox-creds/claude",
|
|
65
|
+
credentialsSubpath: "claude/",
|
|
66
|
+
stageStatic: (opts) => stageClaudeStaticForUpload({ hostWorkspace: opts.hostWorkspace }),
|
|
67
|
+
stageCredentials: () => stageClaudeCredentialsForUpload()
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: "codex",
|
|
71
|
+
staticMountPath: "/home/vscode/.codex",
|
|
72
|
+
credentialsMountPath: "/home/vscode/.agentbox-creds/codex",
|
|
73
|
+
credentialsSubpath: "codex/",
|
|
74
|
+
stageStatic: () => stageCodexStaticForUpload(),
|
|
75
|
+
stageCredentials: () => stageCodexCredentialsForUpload()
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: "opencode",
|
|
79
|
+
staticMountPath: "/home/vscode/.local/share/opencode",
|
|
80
|
+
credentialsMountPath: "/home/vscode/.agentbox-creds/opencode",
|
|
81
|
+
credentialsSubpath: "opencode/",
|
|
82
|
+
stageStatic: () => stageOpencodeStaticForUpload(),
|
|
83
|
+
stageCredentials: () => stageOpencodeCredentialsForUpload()
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
var SEED_MARKER = ".agentbox-seeded-at";
|
|
87
|
+
async function ensureAgentVolumesForCloud(backend, opts = {}) {
|
|
88
|
+
const log = opts.onLog ?? (() => {
|
|
89
|
+
});
|
|
90
|
+
if (typeof backend.ensureVolume !== "function") {
|
|
91
|
+
log(
|
|
92
|
+
`cloud backend '${backend.name}' has no volume primitive \u2014 agent credentials will not persist across boxes`
|
|
93
|
+
);
|
|
94
|
+
return { mounts: [], env: buildForwardedEnv([]), agents: [] };
|
|
95
|
+
}
|
|
96
|
+
let volumeId;
|
|
97
|
+
try {
|
|
98
|
+
const ensured = await backend.ensureVolume(CREDENTIALS_VOLUME);
|
|
99
|
+
volumeId = ensured.volumeId;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
log(`ensureVolume(${CREDENTIALS_VOLUME}) failed (skipping credentials seed): ${msg}`);
|
|
103
|
+
return { mounts: [], env: buildForwardedEnv([]), agents: [] };
|
|
104
|
+
}
|
|
105
|
+
const mounts = AGENT_SPECS.map((spec) => ({
|
|
106
|
+
volumeId,
|
|
107
|
+
mountPath: spec.credentialsMountPath,
|
|
108
|
+
subpath: spec.credentialsSubpath
|
|
109
|
+
}));
|
|
110
|
+
const agents = AGENT_SPECS.map((s) => s.kind);
|
|
111
|
+
return { mounts, env: buildForwardedEnv(agents), agents };
|
|
112
|
+
}
|
|
113
|
+
function buildForwardedEnv(agents) {
|
|
114
|
+
const env = {};
|
|
115
|
+
if (agents.includes("opencode")) {
|
|
116
|
+
env["OPENCODE_CONFIG_DIR"] = "/home/vscode/.local/share/opencode/config";
|
|
117
|
+
}
|
|
118
|
+
const forwardedKeys = /* @__PURE__ */ new Set([
|
|
119
|
+
...CLAUDE_FORWARDED_ENV_KEYS,
|
|
120
|
+
...CODEX_FORWARDED_ENV_KEYS,
|
|
121
|
+
...OPENCODE_FORWARDED_ENV_KEYS
|
|
122
|
+
]);
|
|
123
|
+
for (const k of forwardedKeys) {
|
|
124
|
+
const v = process.env[k];
|
|
125
|
+
if (typeof v === "string" && v.length > 0) env[k] = v;
|
|
126
|
+
}
|
|
127
|
+
return env;
|
|
128
|
+
}
|
|
129
|
+
async function seedAgentVolumesIfFresh(backend, handle, opts = {}) {
|
|
130
|
+
const wanted = new Set(opts.agents ?? AGENT_SPECS.map((s) => s.kind));
|
|
131
|
+
const specs = AGENT_SPECS.filter((s) => wanted.has(s.kind));
|
|
132
|
+
await Promise.all(specs.map((spec) => seedCredentialsOne(backend, handle, spec, opts)));
|
|
133
|
+
}
|
|
134
|
+
async function seedCredentialsOne(backend, handle, spec, opts) {
|
|
135
|
+
const log = opts.onLog ?? (() => {
|
|
136
|
+
});
|
|
137
|
+
if (!opts.force) {
|
|
138
|
+
const probe = await backend.exec(
|
|
139
|
+
handle,
|
|
140
|
+
`test -f ${spec.credentialsMountPath}/${SEED_MARKER}`
|
|
141
|
+
);
|
|
142
|
+
if (probe.exitCode === 0) {
|
|
143
|
+
log(`${spec.kind}: credentials already seeded \u2014 mounting only`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
log(`${spec.kind}: staging host credentials`);
|
|
148
|
+
const staged = await spec.stageCredentials();
|
|
149
|
+
for (const w of staged.warnings) log(w);
|
|
150
|
+
try {
|
|
151
|
+
if (staged.tarballPath === null) {
|
|
152
|
+
log(`${spec.kind}: no credentials to seed`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let tarSize = 0;
|
|
156
|
+
try {
|
|
157
|
+
const { statSync: statSync2 } = await import("fs");
|
|
158
|
+
tarSize = statSync2(staged.tarballPath).size;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
const sizeKB = (tarSize / 1024).toFixed(1);
|
|
162
|
+
log(`${spec.kind}: uploading ${sizeKB} KB credentials tarball`);
|
|
163
|
+
process.stderr.write(`[agent-creds] ${spec.kind}: uploading ${sizeKB} KB...
|
|
164
|
+
`);
|
|
165
|
+
const t0 = Date.now();
|
|
166
|
+
const remoteTar = `/tmp/agentbox-${spec.kind}-creds.tar.gz`;
|
|
167
|
+
await backend.uploadFile(handle, staged.tarballPath, remoteTar);
|
|
168
|
+
const upDt = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
169
|
+
process.stderr.write(`[agent-creds] ${spec.kind}: upload done in ${upDt}s
|
|
170
|
+
`);
|
|
171
|
+
const stageDir = `/tmp/agentbox-creds-stage-${spec.kind}`;
|
|
172
|
+
const extractCmd = `set -e; rm -rf ${stageDir}; mkdir -p ${stageDir}; tar -xzf ${remoteTar} -C ${stageDir}; cp -r ${stageDir}/. ${spec.credentialsMountPath}/; rm -rf ${stageDir}; date -u +%FT%TZ > ${spec.credentialsMountPath}/${SEED_MARKER}; rm -f ${remoteTar}`;
|
|
173
|
+
const extract = await backend.exec(handle, extractCmd);
|
|
174
|
+
if (extract.exitCode !== 0) {
|
|
175
|
+
const msg = `${spec.kind}: credentials extract failed (exit ${String(extract.exitCode)}); agent falls back to interactive login. stdout: ${extract.stdout.slice(-200)} stderr: ${extract.stderr.slice(-200)}`;
|
|
176
|
+
log(msg);
|
|
177
|
+
process.stderr.write(`[agent-creds] ${msg}
|
|
178
|
+
`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
log(`${spec.kind}: credentials seeded \u2713`);
|
|
182
|
+
process.stderr.write(`[agent-creds] ${spec.kind}: credentials seeded
|
|
183
|
+
`);
|
|
184
|
+
} finally {
|
|
185
|
+
await staged.cleanup();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function agentSpecsForCloud() {
|
|
189
|
+
return AGENT_SPECS.map((s) => ({
|
|
190
|
+
kind: s.kind,
|
|
191
|
+
staticMountPath: s.staticMountPath,
|
|
192
|
+
credentialsMountPath: s.credentialsMountPath,
|
|
193
|
+
credentialsSubpath: s.credentialsSubpath
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
var CLOUD_CHECKPOINTS_ROOT = join(homedir(), ".agentbox", "cloud-checkpoints");
|
|
197
|
+
var CLOUD_SNAPSHOT_NAME_PREFIX = "agentbox-ckpt-";
|
|
198
|
+
function cloudSnapshotName(projectRoot, name) {
|
|
199
|
+
const mnemonic = sanitizeMnemonic(basename(projectRoot));
|
|
200
|
+
return `${CLOUD_SNAPSHOT_NAME_PREFIX}${hashProjectPath(projectRoot)}_${mnemonic}-${name}`;
|
|
201
|
+
}
|
|
202
|
+
function backendDir(backend, projectRoot) {
|
|
203
|
+
return join(CLOUD_CHECKPOINTS_ROOT, backend, projectDirSegment(projectRoot));
|
|
204
|
+
}
|
|
205
|
+
function checkpointDir(backend, projectRoot, name) {
|
|
206
|
+
return join(backendDir(backend, projectRoot), name);
|
|
207
|
+
}
|
|
208
|
+
async function readManifest(dir) {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await readFile(join(dir, "manifest.json"), "utf8");
|
|
211
|
+
const m = JSON.parse(raw);
|
|
212
|
+
if (m.schema !== 1) return null;
|
|
213
|
+
return m;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function listCloudCheckpoints(projectRoot, backend) {
|
|
219
|
+
const root = backendDir(backend, projectRoot);
|
|
220
|
+
let entries;
|
|
221
|
+
try {
|
|
222
|
+
entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const name of entries) {
|
|
228
|
+
const dir = join(root, name);
|
|
229
|
+
const manifest = await readManifest(dir);
|
|
230
|
+
if (manifest) out.push({ name, dir, manifest });
|
|
231
|
+
}
|
|
232
|
+
out.sort((a, b) => a.manifest.createdAt.localeCompare(b.manifest.createdAt));
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
async function resolveCloudCheckpoint(projectRoot, backend, ref) {
|
|
236
|
+
const dir = checkpointDir(backend, projectRoot, ref);
|
|
237
|
+
const manifest = await readManifest(dir);
|
|
238
|
+
if (!manifest) return null;
|
|
239
|
+
return { name: ref, dir, manifest };
|
|
240
|
+
}
|
|
241
|
+
async function writeCloudCheckpointManifest(projectRoot, backend, name, fields) {
|
|
242
|
+
const dir = checkpointDir(backend, projectRoot, name);
|
|
243
|
+
await mkdir(dir, { recursive: true });
|
|
244
|
+
const manifest = {
|
|
245
|
+
schema: 1,
|
|
246
|
+
name,
|
|
247
|
+
backend,
|
|
248
|
+
snapshotName: fields.snapshotName,
|
|
249
|
+
sourceBoxId: fields.sourceBoxId,
|
|
250
|
+
sourceBoxName: fields.sourceBoxName,
|
|
251
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
252
|
+
};
|
|
253
|
+
await writeFile(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
254
|
+
return { name, dir, manifest };
|
|
255
|
+
}
|
|
256
|
+
async function removeCloudCheckpointDir(projectRoot, backend, name) {
|
|
257
|
+
const dir = checkpointDir(backend, projectRoot, name);
|
|
258
|
+
const existed = await readManifest(dir) !== null;
|
|
259
|
+
if (!existed) return false;
|
|
260
|
+
await rm(dir, { recursive: true, force: true });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
var WORKSPACE_DIR_DEFAULT = "/workspace";
|
|
264
|
+
var REMOTE_TAR_PATH = "/tmp/agentbox-envfiles.tar";
|
|
265
|
+
async function uploadEnvFiles(args) {
|
|
266
|
+
const log = args.onLog ?? (() => {
|
|
267
|
+
});
|
|
268
|
+
if (args.files.length === 0) return { copied: 0 };
|
|
269
|
+
const workspaceDir = args.workspaceDir ?? WORKSPACE_DIR_DEFAULT;
|
|
270
|
+
const found = await execa("find", buildHostEnvFindArgs(args.files).slice(1), {
|
|
271
|
+
cwd: args.workspacePath,
|
|
272
|
+
reject: false
|
|
273
|
+
});
|
|
274
|
+
if (found.exitCode !== 0) {
|
|
275
|
+
log(`env-file scan failed: ${String(found.stderr).slice(0, 300)}`);
|
|
276
|
+
return { copied: 0 };
|
|
277
|
+
}
|
|
278
|
+
const list = String(found.stdout).split("\0").filter((p) => p.length > 0);
|
|
279
|
+
if (list.length === 0) return { copied: 0 };
|
|
280
|
+
const stage = await mkdtemp(join2(tmpdir(), "agentbox-envfiles-"));
|
|
281
|
+
const localTar = join2(stage, "envfiles.tar");
|
|
282
|
+
try {
|
|
283
|
+
const packed = await execa(
|
|
284
|
+
"tar",
|
|
285
|
+
["-C", args.workspacePath, "--null", "-T", "-", "-cf", localTar],
|
|
286
|
+
{ input: list.join("\0"), reject: false }
|
|
287
|
+
);
|
|
288
|
+
if (packed.exitCode !== 0) {
|
|
289
|
+
log(`env-file tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
|
|
290
|
+
return { copied: 0 };
|
|
291
|
+
}
|
|
292
|
+
await writeFile2(join2(stage, ".marker"), "").catch(() => {
|
|
293
|
+
});
|
|
294
|
+
await args.backend.uploadFile(args.handle, localTar, REMOTE_TAR_PATH);
|
|
295
|
+
const extract = await args.backend.exec(
|
|
296
|
+
args.handle,
|
|
297
|
+
`tar -xf ${REMOTE_TAR_PATH} -C ${workspaceDir} --no-same-permissions --no-same-owner -m && rm -f ${REMOTE_TAR_PATH}`
|
|
298
|
+
);
|
|
299
|
+
if (extract.exitCode !== 0) {
|
|
300
|
+
log(
|
|
301
|
+
`env-file extract failed (exit ${String(extract.exitCode)}); stdout: ${extract.stdout.slice(-200)} stderr: ${extract.stderr.slice(-200)}`
|
|
302
|
+
);
|
|
303
|
+
return { copied: 0 };
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
await rm2(stage, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
return { copied: list.length };
|
|
309
|
+
}
|
|
310
|
+
async function readExposedServicePorts(workspacePath) {
|
|
311
|
+
let text;
|
|
312
|
+
try {
|
|
313
|
+
text = await readFile2(join3(workspacePath, "agentbox.yaml"), "utf8");
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
let doc;
|
|
318
|
+
try {
|
|
319
|
+
doc = parseYaml(text);
|
|
320
|
+
} catch {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
if (!isPlainObject(doc)) return [];
|
|
324
|
+
const services = doc["services"];
|
|
325
|
+
if (!isPlainObject(services)) return [];
|
|
326
|
+
const out = /* @__PURE__ */ new Set();
|
|
327
|
+
for (const value of Object.values(services)) {
|
|
328
|
+
if (!isPlainObject(value)) continue;
|
|
329
|
+
const expose = value["expose"];
|
|
330
|
+
if (!isPlainObject(expose)) continue;
|
|
331
|
+
const port = expose["port"];
|
|
332
|
+
if (typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536) {
|
|
333
|
+
out.add(port);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return [...out].sort((a, b) => a - b);
|
|
337
|
+
}
|
|
338
|
+
function isPlainObject(v) {
|
|
339
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
340
|
+
}
|
|
341
|
+
function quoteShellArgv(argv) {
|
|
342
|
+
return argv.map(quoteShellArg).join(" ");
|
|
343
|
+
}
|
|
344
|
+
function quoteShellArg(arg) {
|
|
345
|
+
if (arg.length === 0) return "''";
|
|
346
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg)) return arg;
|
|
347
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
348
|
+
}
|
|
349
|
+
function bashScript(body) {
|
|
350
|
+
return `bash -c ${quoteShellArg(body)}`;
|
|
351
|
+
}
|
|
352
|
+
var REMOTE_UP_TAR = "/tmp/agentbox-cp-up.tar.gz";
|
|
353
|
+
var REMOTE_DOWN_TAR = "/tmp/agentbox-cp-down.tar.gz";
|
|
354
|
+
async function uploadToCloudBox(backend, handle, hostSrc, boxDst) {
|
|
355
|
+
const srcAbs = hostResolve(hostSrc);
|
|
356
|
+
if (!existsSync(srcAbs)) throw new Error(`source not found: ${hostSrc}`);
|
|
357
|
+
const srcBasename = hostBasename(srcAbs);
|
|
358
|
+
const srcParent = hostDirname(srcAbs);
|
|
359
|
+
let boxParent;
|
|
360
|
+
let finalName;
|
|
361
|
+
if (boxDst.endsWith("/")) {
|
|
362
|
+
boxParent = boxDst.replace(/\/+$/, "") || "/";
|
|
363
|
+
finalName = srcBasename;
|
|
364
|
+
} else {
|
|
365
|
+
boxParent = posix.dirname(boxDst);
|
|
366
|
+
finalName = posix.basename(boxDst);
|
|
367
|
+
}
|
|
368
|
+
const finalPath = boxParent === "/" ? `/${finalName}` : `${boxParent}/${finalName}`;
|
|
369
|
+
const stage = await mkdtemp2(hostJoin(tmpdir2(), "agentbox-cp-up-"));
|
|
370
|
+
const localTar = hostJoin(stage, "payload.tar.gz");
|
|
371
|
+
try {
|
|
372
|
+
await execa2("tar", ["-C", srcParent, "-czf", localTar, srcBasename], {
|
|
373
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
374
|
+
});
|
|
375
|
+
await backend.uploadFile(handle, localTar, REMOTE_UP_TAR);
|
|
376
|
+
const initialPath = boxParent === "/" ? `/${srcBasename}` : `${boxParent}/${srcBasename}`;
|
|
377
|
+
const renameStep = finalName !== srcBasename ? `$SUDO cp -f ${quoteShellArg(initialPath)} ${quoteShellArg(finalPath)} && $SUDO rm -f ${quoteShellArg(initialPath)}` : ": # no rename";
|
|
378
|
+
const script = [
|
|
379
|
+
`set -euo pipefail`,
|
|
380
|
+
`if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`,
|
|
381
|
+
`$SUDO mkdir -p ${quoteShellArg(boxParent)}`,
|
|
382
|
+
// --no-same-permissions / --no-same-owner / -m: Daytona's S3-backed
|
|
383
|
+
// FUSE volumes reject chmod/utime/chown; skipping them lets the extract
|
|
384
|
+
// complete on a mounted-volume destination. Harmless no-op on the
|
|
385
|
+
// sandbox's regular disk. Same flags as the credential-seed extract.
|
|
386
|
+
`$SUDO tar -xzf ${quoteShellArg(REMOTE_UP_TAR)} -C ${quoteShellArg(boxParent)} --no-same-permissions --no-same-owner -m`,
|
|
387
|
+
renameStep,
|
|
388
|
+
// chown only the landed path — anything we mkdir'd through stays at
|
|
389
|
+
// its existing ownership. Tolerate failure (chown bad on read-only mounts).
|
|
390
|
+
`$SUDO chown -R "$(id -un):$(id -gn)" ${quoteShellArg(finalPath)} || true`,
|
|
391
|
+
`rm -f ${quoteShellArg(REMOTE_UP_TAR)}`
|
|
392
|
+
].join("\n");
|
|
393
|
+
const r = await backend.exec(handle, bashScript(script));
|
|
394
|
+
if (r.exitCode !== 0) {
|
|
395
|
+
throw new Error(`cloud upload extract failed: ${r.stderr || r.stdout}`);
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
await rm3(stage, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
return { finalPath };
|
|
401
|
+
}
|
|
402
|
+
async function pullCloudDirContents(backend, handle, boxSrcDir, hostDstDir) {
|
|
403
|
+
const dstAbs = hostResolve(hostDstDir);
|
|
404
|
+
mkdirSync(dstAbs, { recursive: true });
|
|
405
|
+
const stage = await mkdtemp2(hostJoin(tmpdir2(), "agentbox-pull-"));
|
|
406
|
+
const localTar = hostJoin(stage, "payload.tar.gz");
|
|
407
|
+
try {
|
|
408
|
+
const packScript = [
|
|
409
|
+
`set -euo pipefail`,
|
|
410
|
+
`cd ${quoteShellArg(boxSrcDir)}`,
|
|
411
|
+
`tar -czf ${quoteShellArg(REMOTE_DOWN_TAR)} .`
|
|
412
|
+
].join("\n");
|
|
413
|
+
const r = await backend.exec(handle, bashScript(packScript));
|
|
414
|
+
if (r.exitCode !== 0) {
|
|
415
|
+
throw new Error(`cloud workspace pack failed: ${r.stderr || r.stdout}`);
|
|
416
|
+
}
|
|
417
|
+
await backend.downloadFile(handle, REMOTE_DOWN_TAR, localTar);
|
|
418
|
+
await execa2("tar", ["-xzf", localTar, "-C", dstAbs]);
|
|
419
|
+
await backend.exec(handle, `rm -f ${quoteShellArg(REMOTE_DOWN_TAR)}`).catch(() => {
|
|
420
|
+
});
|
|
421
|
+
} finally {
|
|
422
|
+
await rm3(stage, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
return { finalPath: dstAbs };
|
|
425
|
+
}
|
|
426
|
+
async function downloadFromCloudBox(backend, handle, boxSrc, hostDst) {
|
|
427
|
+
const srcBasename = posix.basename(boxSrc);
|
|
428
|
+
const srcParent = posix.dirname(boxSrc);
|
|
429
|
+
const dstAbs = hostResolve(hostDst);
|
|
430
|
+
let hostParent;
|
|
431
|
+
let finalName;
|
|
432
|
+
const dstExists = existsSync(dstAbs);
|
|
433
|
+
if (hostDst.endsWith("/") || dstExists && statSync(dstAbs).isDirectory()) {
|
|
434
|
+
hostParent = dstAbs;
|
|
435
|
+
finalName = srcBasename;
|
|
436
|
+
} else {
|
|
437
|
+
hostParent = hostDirname(dstAbs);
|
|
438
|
+
finalName = hostBasename(dstAbs);
|
|
439
|
+
}
|
|
440
|
+
mkdirSync(hostParent, { recursive: true });
|
|
441
|
+
const finalPath = hostJoin(hostParent, finalName);
|
|
442
|
+
const stage = await mkdtemp2(hostJoin(tmpdir2(), "agentbox-cp-down-"));
|
|
443
|
+
const localTar = hostJoin(stage, "payload.tar.gz");
|
|
444
|
+
try {
|
|
445
|
+
const packScript = [
|
|
446
|
+
`set -euo pipefail`,
|
|
447
|
+
`cd ${quoteShellArg(srcParent)}`,
|
|
448
|
+
`tar -czf ${quoteShellArg(REMOTE_DOWN_TAR)} ${quoteShellArg(srcBasename)}`
|
|
449
|
+
].join("\n");
|
|
450
|
+
const r = await backend.exec(handle, bashScript(packScript));
|
|
451
|
+
if (r.exitCode !== 0) {
|
|
452
|
+
throw new Error(`cloud download pack failed: ${r.stderr || r.stdout}`);
|
|
453
|
+
}
|
|
454
|
+
await backend.downloadFile(handle, REMOTE_DOWN_TAR, localTar);
|
|
455
|
+
await execa2("tar", ["-xzf", localTar, "-C", hostParent]);
|
|
456
|
+
if (finalName !== srcBasename) {
|
|
457
|
+
renameSync(hostJoin(hostParent, srcBasename), finalPath);
|
|
458
|
+
}
|
|
459
|
+
await backend.exec(handle, `rm -f ${quoteShellArg(REMOTE_DOWN_TAR)}`).catch(() => {
|
|
460
|
+
});
|
|
461
|
+
} finally {
|
|
462
|
+
await rm3(stage, { recursive: true, force: true });
|
|
463
|
+
}
|
|
464
|
+
return { finalPath };
|
|
465
|
+
}
|
|
466
|
+
async function launchCloudCtlDaemon(args) {
|
|
467
|
+
const env = [
|
|
468
|
+
`AGENTBOX_BOX_ID=${quoteShellArgv([args.boxId])}`,
|
|
469
|
+
`AGENTBOX_BOX_NAME=${quoteShellArgv([args.boxName])}`,
|
|
470
|
+
`AGENTBOX_BOX_KIND=cloud`
|
|
471
|
+
];
|
|
472
|
+
if (args.relayUrl) env.push(`AGENTBOX_RELAY_URL=${quoteShellArgv([args.relayUrl])}`);
|
|
473
|
+
if (args.relayToken) env.push(`AGENTBOX_RELAY_TOKEN=${quoteShellArgv([args.relayToken])}`);
|
|
474
|
+
if (args.bridgeToken) env.push(`AGENTBOX_BRIDGE_TOKEN=${quoteShellArgv([args.bridgeToken])}`);
|
|
475
|
+
const script = [
|
|
476
|
+
`set -e`,
|
|
477
|
+
`if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`,
|
|
478
|
+
`$SUDO mkdir -p /run/agentbox /var/log/agentbox`,
|
|
479
|
+
`$SUDO chown "$(id -un):$(id -gn)" /run/agentbox /var/log/agentbox`,
|
|
480
|
+
`export ${env.join(" ")}`,
|
|
481
|
+
`nohup /usr/local/bin/agentbox-ctl daemon >> /var/log/agentbox/ctl-daemon.log 2>&1 &`,
|
|
482
|
+
`disown`,
|
|
483
|
+
`echo started`
|
|
484
|
+
].join("\n");
|
|
485
|
+
const r = await args.backend.exec(args.handle, bashScript(script));
|
|
486
|
+
if (r.exitCode !== 0) {
|
|
487
|
+
throw new Error(`agentbox-ctl daemon launch failed: ${r.stderr || r.stdout}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function launchCloudDockerdDaemon(args) {
|
|
491
|
+
const timeoutMs = args.timeoutMs ?? 6e4;
|
|
492
|
+
const startScript = [
|
|
493
|
+
`set -euo pipefail`,
|
|
494
|
+
`mkdir -p /var/log/agentbox`,
|
|
495
|
+
`nohup sudo -n /usr/local/bin/agentbox-dockerd-start >> /var/log/agentbox/dockerd.log 2>&1 &`,
|
|
496
|
+
`echo "spawned dockerd"`
|
|
497
|
+
].join("\n");
|
|
498
|
+
const launch = await args.backend.exec(args.handle, bashScript(startScript));
|
|
499
|
+
if (launch.exitCode !== 0) {
|
|
500
|
+
return {
|
|
501
|
+
up: false,
|
|
502
|
+
reason: `dockerd launch failed: ${launch.stderr || launch.stdout}`
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const probeCmd = "[ -S /var/run/docker.sock ] && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1";
|
|
506
|
+
const deadline = Date.now() + timeoutMs;
|
|
507
|
+
while (Date.now() < deadline) {
|
|
508
|
+
const probe = await args.backend.exec(args.handle, probeCmd);
|
|
509
|
+
if (probe.exitCode === 0) return { up: true };
|
|
510
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
up: false,
|
|
514
|
+
reason: `dockerd did not become ready within ${String(timeoutMs)}ms`
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async function launchCloudVncDaemon(args) {
|
|
518
|
+
const script = [
|
|
519
|
+
`set -e`,
|
|
520
|
+
`cd /home/vscode`,
|
|
521
|
+
`export AGENTBOX_VNC_PASSWORD=${quoteShellArg(args.vncPassword)}`,
|
|
522
|
+
`mkdir -p /var/log/agentbox 2>/dev/null || true`,
|
|
523
|
+
`nohup /usr/local/bin/agentbox-vnc-start >> /var/log/agentbox/vnc-start.log 2>&1 &`,
|
|
524
|
+
`disown`,
|
|
525
|
+
// Match the host-side probe in packages/sandbox-docker/src/vnc.ts: poll for
|
|
526
|
+
// ~5s, then give up. The script itself waits for Xvnc internally; this
|
|
527
|
+
// outer probe confirms websockify's public port came up too.
|
|
528
|
+
`for _ in $(seq 1 50); do`,
|
|
529
|
+
` if (echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null; then echo ready; exit 0; fi`,
|
|
530
|
+
` sleep 0.1`,
|
|
531
|
+
`done`,
|
|
532
|
+
`echo "websockify did not bind 6080 within 5s" >&2`,
|
|
533
|
+
`exit 1`
|
|
534
|
+
].join("\n");
|
|
535
|
+
const r = await args.backend.exec(args.handle, bashScript(script));
|
|
536
|
+
if (r.exitCode !== 0) {
|
|
537
|
+
throw new Error(`agentbox-vnc-start failed: ${r.stderr || r.stdout}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
var WORKSPACE_DIR_DEFAULT2 = "/workspace";
|
|
541
|
+
async function seedCloudWorkspace(args) {
|
|
542
|
+
const workspaceDir = args.workspaceDir ?? WORKSPACE_DIR_DEFAULT2;
|
|
543
|
+
const log = args.onLog ?? (() => {
|
|
544
|
+
});
|
|
545
|
+
const repos = await detectGitRepos(args.workspacePath);
|
|
546
|
+
const root = repos.find((r) => r.kind === "root");
|
|
547
|
+
const nested = repos.filter((r) => r.kind === "nested");
|
|
548
|
+
if (root) {
|
|
549
|
+
log(
|
|
550
|
+
nested.length > 0 ? `seeding /workspace from git bundle (+${String(nested.length)} nested repo${nested.length === 1 ? "" : "s"})` : "seeding /workspace from git bundle"
|
|
551
|
+
);
|
|
552
|
+
await seedFromGitBundle({
|
|
553
|
+
backend: args.backend,
|
|
554
|
+
handle: args.handle,
|
|
555
|
+
hostRepo: root.hostMainRepo,
|
|
556
|
+
branch: args.branch,
|
|
557
|
+
workspaceDir
|
|
558
|
+
});
|
|
559
|
+
for (const r of nested) {
|
|
560
|
+
const sub = `${workspaceDir}/${r.relPathFromWorkspace}`;
|
|
561
|
+
log(`seeding nested repo ${r.relPathFromWorkspace} from git bundle`);
|
|
562
|
+
await seedFromGitBundle({
|
|
563
|
+
backend: args.backend,
|
|
564
|
+
handle: args.handle,
|
|
565
|
+
hostRepo: r.hostMainRepo,
|
|
566
|
+
branch: args.branch,
|
|
567
|
+
workspaceDir: sub
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return { fromGit: true, branch: args.branch };
|
|
571
|
+
}
|
|
572
|
+
log("seeding /workspace from workspace tarball (no git detected)");
|
|
573
|
+
await seedFromTar({
|
|
574
|
+
backend: args.backend,
|
|
575
|
+
handle: args.handle,
|
|
576
|
+
hostDir: args.workspacePath,
|
|
577
|
+
workspaceDir
|
|
578
|
+
});
|
|
579
|
+
return { fromGit: false, branch: args.branch };
|
|
580
|
+
}
|
|
581
|
+
var STASH_CARRYOVER_REF = "refs/agentbox-carryover/stash";
|
|
582
|
+
var REMOTE_UNTRACKED_TAR = "/tmp/agentbox-carryover-untracked.tar.gz";
|
|
583
|
+
async function seedFromGitBundle(args) {
|
|
584
|
+
const stage = await mkdtemp3(join4(tmpdir3(), "agentbox-bundle-"));
|
|
585
|
+
const bundlePath = join4(stage, "workspace.bundle");
|
|
586
|
+
const untrackedTarPath = join4(stage, "untracked.tar.gz");
|
|
587
|
+
const stashSha = await safeStashCreate(args.hostRepo);
|
|
588
|
+
const untrackedSize = await maybeBuildUntrackedTar(args.hostRepo, untrackedTarPath);
|
|
589
|
+
let stashRefCreated = false;
|
|
590
|
+
try {
|
|
591
|
+
if (stashSha) {
|
|
592
|
+
const ref = await execa3(
|
|
593
|
+
"git",
|
|
594
|
+
["-C", args.hostRepo, "update-ref", STASH_CARRYOVER_REF, stashSha],
|
|
595
|
+
{ reject: false }
|
|
596
|
+
);
|
|
597
|
+
stashRefCreated = ref.exitCode === 0;
|
|
598
|
+
}
|
|
599
|
+
const depthRaw = process.env["AGENTBOX_BUNDLE_DEPTH"];
|
|
600
|
+
const depth = depthRaw ? Number.parseInt(depthRaw, 10) : NaN;
|
|
601
|
+
const bundleArgs = ["-C", args.hostRepo, "bundle", "create", bundlePath];
|
|
602
|
+
if (Number.isFinite(depth) && depth > 0) {
|
|
603
|
+
bundleArgs.push(`--depth=${String(depth)}`, "HEAD");
|
|
604
|
+
} else {
|
|
605
|
+
bundleArgs.push("--all");
|
|
606
|
+
}
|
|
607
|
+
if (stashRefCreated) bundleArgs.push(STASH_CARRYOVER_REF);
|
|
608
|
+
await execa3("git", bundleArgs);
|
|
609
|
+
if (stashRefCreated) {
|
|
610
|
+
await execa3("git", ["-C", args.hostRepo, "update-ref", "-d", STASH_CARRYOVER_REF], {
|
|
611
|
+
reject: false
|
|
612
|
+
});
|
|
613
|
+
stashRefCreated = false;
|
|
614
|
+
}
|
|
615
|
+
const remoteUrl = await readOriginUrl(args.hostRepo);
|
|
616
|
+
const remoteBundle = "/tmp/agentbox-workspace.bundle";
|
|
617
|
+
await args.backend.uploadFile(args.handle, bundlePath, remoteBundle);
|
|
618
|
+
if (untrackedSize > 0) {
|
|
619
|
+
await args.backend.uploadFile(args.handle, untrackedTarPath, REMOTE_UNTRACKED_TAR);
|
|
620
|
+
}
|
|
621
|
+
const setOrigin = remoteUrl ? `git -C ${quoteShellArgv([args.workspaceDir])} remote set-url origin ${quoteShellArgv([remoteUrl])}` : ": # no host origin to copy";
|
|
622
|
+
const SUDO = `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`;
|
|
623
|
+
const carryOverSteps = stashSha ? [
|
|
624
|
+
`if git -C ${quoteShellArgv([args.workspaceDir])} rev-parse --verify ${quoteShellArgv([`refs/remotes/origin/agentbox-carryover/stash`])} >/dev/null 2>&1; then git -C ${quoteShellArgv([args.workspaceDir])} stash apply ${quoteShellArgv([`refs/remotes/origin/agentbox-carryover/stash`])} || echo "agentbox: stash apply soft-failed; carry-over may be incomplete" >&2 ; git -C ${quoteShellArgv([args.workspaceDir])} update-ref -d ${quoteShellArgv([`refs/remotes/origin/agentbox-carryover/stash`])} || true ; fi`
|
|
625
|
+
] : [];
|
|
626
|
+
if (untrackedSize > 0) {
|
|
627
|
+
carryOverSteps.push(
|
|
628
|
+
`if [ -f ${quoteShellArgv([REMOTE_UNTRACKED_TAR])} ]; then tar -C ${quoteShellArgv([args.workspaceDir])} -xzf ${quoteShellArgv([REMOTE_UNTRACKED_TAR])} && rm -f ${quoteShellArgv([REMOTE_UNTRACKED_TAR])} ; fi`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const script = [
|
|
632
|
+
`set -euo pipefail`,
|
|
633
|
+
// Move out of any cwd we might inherit from Daytona's executeCommand
|
|
634
|
+
// before we delete /workspace. The agentbox image bakes WORKDIR
|
|
635
|
+
// /workspace; if the shell's cwd is /workspace when we `rm -rf` it,
|
|
636
|
+
// the next process inherits a stale cwd FD and git-clone's child
|
|
637
|
+
// (index-pack) fails with "Unable to read current working directory".
|
|
638
|
+
`cd /tmp`,
|
|
639
|
+
SUDO,
|
|
640
|
+
// rm -rf only the directory we're about to clone into — for nested
|
|
641
|
+
// repos this is just `/workspace/<rel>`, so the root clone (already
|
|
642
|
+
// at `/workspace`) is preserved.
|
|
643
|
+
`$SUDO rm -rf ${quoteShellArgv([args.workspaceDir])}`,
|
|
644
|
+
`$SUDO mkdir -p ${quoteShellArgv([args.workspaceDir])}`,
|
|
645
|
+
`$SUDO chown "$(id -un):$(id -gn)" ${quoteShellArgv([args.workspaceDir])}`,
|
|
646
|
+
`git clone ${quoteShellArgv([remoteBundle, args.workspaceDir])}`,
|
|
647
|
+
setOrigin,
|
|
648
|
+
`git -C ${quoteShellArgv([args.workspaceDir])} fetch ${quoteShellArgv([remoteBundle])} --tags '+refs/heads/*:refs/remotes/bundle/*' || true`,
|
|
649
|
+
`git -C ${quoteShellArgv([args.workspaceDir])} checkout -B ${quoteShellArgv([args.branch])}`,
|
|
650
|
+
...carryOverSteps,
|
|
651
|
+
`rm -f ${quoteShellArgv([remoteBundle])}`
|
|
652
|
+
].join("\n");
|
|
653
|
+
const r = await args.backend.exec(args.handle, bashScript(script));
|
|
654
|
+
if (r.exitCode !== 0) {
|
|
655
|
+
throw new Error(`workspace seed (bundle) failed: ${r.stderr || r.stdout}`);
|
|
656
|
+
}
|
|
657
|
+
} finally {
|
|
658
|
+
if (stashRefCreated) {
|
|
659
|
+
await execa3("git", ["-C", args.hostRepo, "update-ref", "-d", STASH_CARRYOVER_REF], {
|
|
660
|
+
reject: false
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
await rm4(stage, { recursive: true, force: true });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function safeStashCreate(hostRepo) {
|
|
667
|
+
const r = await execa3("git", ["-C", hostRepo, "stash", "create"], { reject: false });
|
|
668
|
+
if (r.exitCode !== 0) return null;
|
|
669
|
+
const sha = r.stdout.trim();
|
|
670
|
+
return sha.length > 0 ? sha : null;
|
|
671
|
+
}
|
|
672
|
+
async function maybeBuildUntrackedTar(hostRepo, outPath) {
|
|
673
|
+
const list = await execa3(
|
|
674
|
+
"git",
|
|
675
|
+
["-C", hostRepo, "ls-files", "--others", "--exclude-standard", "-z"],
|
|
676
|
+
{ reject: false }
|
|
677
|
+
);
|
|
678
|
+
if (list.exitCode !== 0 || list.stdout.length === 0) return 0;
|
|
679
|
+
const tar = await execa3(
|
|
680
|
+
"tar",
|
|
681
|
+
["-C", hostRepo, "--null", "-T", "-", "-czf", outPath],
|
|
682
|
+
{
|
|
683
|
+
input: list.stdout,
|
|
684
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
685
|
+
reject: false
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
if (tar.exitCode !== 0) return 0;
|
|
689
|
+
try {
|
|
690
|
+
const { stat } = await import("fs/promises");
|
|
691
|
+
const s = await stat(outPath);
|
|
692
|
+
return s.size;
|
|
693
|
+
} catch {
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function readOriginUrl(hostRepo) {
|
|
698
|
+
const r = await execa3("git", ["-C", hostRepo, "remote", "get-url", "origin"], { reject: false });
|
|
699
|
+
if (r.exitCode !== 0) return null;
|
|
700
|
+
const out = (r.stdout ?? "").trim();
|
|
701
|
+
return out.length > 0 ? out : null;
|
|
702
|
+
}
|
|
703
|
+
async function seedFromTar(args) {
|
|
704
|
+
const stage = await mkdtemp3(join4(tmpdir3(), "agentbox-tar-"));
|
|
705
|
+
const tarPath = join4(stage, "workspace.tar.gz");
|
|
706
|
+
try {
|
|
707
|
+
await execa3("tar", ["-C", args.hostDir, "-czf", tarPath, "."]);
|
|
708
|
+
const remoteTar = "/tmp/agentbox-workspace.tar.gz";
|
|
709
|
+
await args.backend.uploadFile(args.handle, tarPath, remoteTar);
|
|
710
|
+
const SUDO = `if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi`;
|
|
711
|
+
const script = [
|
|
712
|
+
`set -euo pipefail`,
|
|
713
|
+
// Move out of any cwd we might inherit from Daytona's executeCommand
|
|
714
|
+
// before we delete /workspace. The agentbox image bakes WORKDIR
|
|
715
|
+
// /workspace; if the shell's cwd is /workspace when we `rm -rf` it,
|
|
716
|
+
// the next process inherits a stale cwd FD and git-clone's child
|
|
717
|
+
// (index-pack) fails with "Unable to read current working directory".
|
|
718
|
+
`cd /tmp`,
|
|
719
|
+
SUDO,
|
|
720
|
+
`$SUDO rm -rf ${quoteShellArgv([args.workspaceDir])}`,
|
|
721
|
+
`$SUDO mkdir -p ${quoteShellArgv([args.workspaceDir])}`,
|
|
722
|
+
`$SUDO chown "$(id -un):$(id -gn)" ${quoteShellArgv([args.workspaceDir])}`,
|
|
723
|
+
`tar -C ${quoteShellArgv([args.workspaceDir])} -xzf ${quoteShellArgv([remoteTar])}`,
|
|
724
|
+
`rm -f ${quoteShellArgv([remoteTar])}`
|
|
725
|
+
].join("\n");
|
|
726
|
+
const r = await args.backend.exec(args.handle, bashScript(script));
|
|
727
|
+
if (r.exitCode !== 0) {
|
|
728
|
+
throw new Error(`workspace seed (tar) failed: ${r.stderr || r.stdout}`);
|
|
729
|
+
}
|
|
730
|
+
} finally {
|
|
731
|
+
await rm4(stage, { recursive: true, force: true });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
var CLOUD_WORKSPACE_DIR = "/workspace";
|
|
735
|
+
var CLOUD_WEB_PROXY_PORT = 80;
|
|
736
|
+
var CLOUD_VNC_PORT = 6080;
|
|
737
|
+
var DEFAULT_SIGNED_URL_TTL_SECONDS = 3600;
|
|
738
|
+
var FALLBACK_IMAGE = "agentbox/box:dev";
|
|
739
|
+
var DEFAULT_PORTLESS_PROXY_PORT = 1355;
|
|
740
|
+
function parsePortlessUrl(url) {
|
|
741
|
+
try {
|
|
742
|
+
const u = new URL(url);
|
|
743
|
+
if (!u.hostname.endsWith(".localhost")) return void 0;
|
|
744
|
+
const tls = u.protocol === "https:";
|
|
745
|
+
const proxyPort = u.port ? Number.parseInt(u.port, 10) : tls ? 443 : 80;
|
|
746
|
+
if (!Number.isFinite(proxyPort)) return void 0;
|
|
747
|
+
return { proxyPort, tls };
|
|
748
|
+
} catch {
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function parseLoopbackPort(url) {
|
|
753
|
+
try {
|
|
754
|
+
const u = new URL(url);
|
|
755
|
+
if (u.hostname !== "127.0.0.1" && u.hostname !== "localhost") return void 0;
|
|
756
|
+
const port = Number.parseInt(u.port, 10);
|
|
757
|
+
return Number.isFinite(port) ? port : void 0;
|
|
758
|
+
} catch {
|
|
759
|
+
return void 0;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function bootstrapPortlessForCloudBox(backend, handle, args) {
|
|
763
|
+
const localPort = parseLoopbackPort(args.webPreviewUrl);
|
|
764
|
+
if (localPort === void 0) return void 0;
|
|
765
|
+
const ok = await portlessAlias(args.boxName, localPort);
|
|
766
|
+
if (!ok) {
|
|
767
|
+
args.onLog(
|
|
768
|
+
`portless: alias not registered (portless CLI missing or not running) \u2014 host URL stays http://127.0.0.1:${String(localPort)}`
|
|
769
|
+
);
|
|
770
|
+
return void 0;
|
|
771
|
+
}
|
|
772
|
+
const url = await portlessGetUrl(args.boxName);
|
|
773
|
+
args.onLog(`portless alias ${url} -> 127.0.0.1:${String(localPort)}`);
|
|
774
|
+
if (backend.startInBoxPortless) {
|
|
775
|
+
const mode = parsePortlessUrl(url) ?? { proxyPort: DEFAULT_PORTLESS_PROXY_PORT, tls: false };
|
|
776
|
+
try {
|
|
777
|
+
await backend.startInBoxPortless(handle, {
|
|
778
|
+
boxName: args.boxName,
|
|
779
|
+
proxyPort: mode.proxyPort,
|
|
780
|
+
tls: mode.tls,
|
|
781
|
+
webPort: args.webPort
|
|
782
|
+
});
|
|
783
|
+
args.onLog(
|
|
784
|
+
`portless: in-box mirror up on 127.0.0.1:${String(mode.proxyPort)} (${mode.tls ? "https" : "http"})`
|
|
785
|
+
);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
args.onLog(
|
|
788
|
+
`portless: in-box mirror failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return { alias: args.boxName, url };
|
|
793
|
+
}
|
|
794
|
+
function createCloudProvider(backend, opts = {}) {
|
|
795
|
+
const providerName = backend.name;
|
|
796
|
+
function handleFor(box) {
|
|
797
|
+
const sandboxId = box.cloud?.sandboxId;
|
|
798
|
+
if (!sandboxId) {
|
|
799
|
+
throw new Error(`cloud box ${box.name} has no sandboxId \u2014 record is malformed`);
|
|
800
|
+
}
|
|
801
|
+
return { sandboxId };
|
|
802
|
+
}
|
|
803
|
+
function mintBox(req) {
|
|
804
|
+
const id = randomBytes(4).toString("hex");
|
|
805
|
+
const name = req.name ?? `${basename2(req.workspacePath)}-${id}`;
|
|
806
|
+
return {
|
|
807
|
+
id,
|
|
808
|
+
name,
|
|
809
|
+
branch: `agentbox/${name}`
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
async function probe(box) {
|
|
813
|
+
try {
|
|
814
|
+
const h = handleFor(box);
|
|
815
|
+
const state = await backend.state(h);
|
|
816
|
+
return state;
|
|
817
|
+
} catch {
|
|
818
|
+
return "missing";
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
name: providerName,
|
|
823
|
+
async create(req) {
|
|
824
|
+
const log = req.onLog ?? (() => {
|
|
825
|
+
});
|
|
826
|
+
const { id, name, branch } = mintBox(req);
|
|
827
|
+
const image = opts.provisionImage ? await opts.provisionImage(req) : req.image ?? FALLBACK_IMAGE;
|
|
828
|
+
const resources = opts.defaultResources ?? { cpu: 2, memory: 4, disk: 8 };
|
|
829
|
+
const relayToken = generateRelayToken();
|
|
830
|
+
const bridgeToken = generateRelayToken();
|
|
831
|
+
try {
|
|
832
|
+
await ensureRelay({ onLog: log });
|
|
833
|
+
} catch (err) {
|
|
834
|
+
log(`relay ensure failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
835
|
+
}
|
|
836
|
+
let snapshotName;
|
|
837
|
+
let resolvedCheckpointRef;
|
|
838
|
+
if (req.checkpointRef && req.projectRoot) {
|
|
839
|
+
const found = await resolveCloudCheckpoint(req.projectRoot, backend.name, req.checkpointRef);
|
|
840
|
+
if (found) {
|
|
841
|
+
snapshotName = found.manifest.snapshotName;
|
|
842
|
+
resolvedCheckpointRef = found.name;
|
|
843
|
+
log(`provisioning from cloud checkpoint '${found.name}' (snapshot ${snapshotName})`);
|
|
844
|
+
} else {
|
|
845
|
+
log(
|
|
846
|
+
`cloud checkpoint '${req.checkpointRef}' not found for ${backend.name}; provisioning from base image`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const agentVolumes = await ensureAgentVolumesForCloud(backend, { onLog: log });
|
|
851
|
+
log(
|
|
852
|
+
snapshotName ? `provisioning ${providerName} sandbox from snapshot` : `provisioning ${providerName} sandbox`
|
|
853
|
+
);
|
|
854
|
+
const handle = await backend.provision({
|
|
855
|
+
name,
|
|
856
|
+
image,
|
|
857
|
+
snapshot: snapshotName,
|
|
858
|
+
resources,
|
|
859
|
+
env: {
|
|
860
|
+
AGENTBOX_BOX_ID: id,
|
|
861
|
+
AGENTBOX_BOX_NAME: name,
|
|
862
|
+
AGENTBOX_BOX_KIND: "cloud",
|
|
863
|
+
// In-sandbox relay is on the box's loopback at the default port.
|
|
864
|
+
AGENTBOX_RELAY_URL: `http://127.0.0.1:${String(8787)}`,
|
|
865
|
+
AGENTBOX_RELAY_TOKEN: relayToken,
|
|
866
|
+
AGENTBOX_BRIDGE_TOKEN: bridgeToken,
|
|
867
|
+
...agentVolumes.env
|
|
868
|
+
},
|
|
869
|
+
volumes: agentVolumes.mounts,
|
|
870
|
+
onLog: log
|
|
871
|
+
});
|
|
872
|
+
try {
|
|
873
|
+
if (snapshotName) {
|
|
874
|
+
log("skipping workspace seed \u2014 snapshot already contains /workspace");
|
|
875
|
+
} else {
|
|
876
|
+
await seedCloudWorkspace({
|
|
877
|
+
backend,
|
|
878
|
+
handle,
|
|
879
|
+
workspacePath: req.workspacePath,
|
|
880
|
+
branch,
|
|
881
|
+
workspaceDir: CLOUD_WORKSPACE_DIR,
|
|
882
|
+
onLog: log
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (agentVolumes.agents.length > 0) {
|
|
886
|
+
await seedAgentVolumesIfFresh(backend, handle, {
|
|
887
|
+
agents: agentVolumes.agents,
|
|
888
|
+
hostWorkspace: req.workspacePath,
|
|
889
|
+
onLog: log
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
if (req.envFilesToImport && req.envFilesToImport.length > 0) {
|
|
893
|
+
const { copied } = await uploadEnvFiles({
|
|
894
|
+
backend,
|
|
895
|
+
handle,
|
|
896
|
+
workspacePath: req.workspacePath,
|
|
897
|
+
files: req.envFilesToImport,
|
|
898
|
+
workspaceDir: CLOUD_WORKSPACE_DIR,
|
|
899
|
+
onLog: log
|
|
900
|
+
});
|
|
901
|
+
if (copied > 0) log(`copied ${String(copied)} env/config file(s) into /workspace`);
|
|
902
|
+
}
|
|
903
|
+
log("launching agentbox-ctl daemon");
|
|
904
|
+
await launchCloudCtlDaemon({
|
|
905
|
+
backend,
|
|
906
|
+
handle,
|
|
907
|
+
boxId: id,
|
|
908
|
+
boxName: name,
|
|
909
|
+
relayUrl: `http://127.0.0.1:${String(8787)}`,
|
|
910
|
+
relayToken,
|
|
911
|
+
bridgeToken
|
|
912
|
+
});
|
|
913
|
+
log("launching in-box dockerd");
|
|
914
|
+
try {
|
|
915
|
+
const dockerd = await launchCloudDockerdDaemon({ backend, handle, timeoutMs: 6e4 });
|
|
916
|
+
if (!dockerd.up) log(`dockerd did not become ready (continuing): ${dockerd.reason ?? "unknown"}`);
|
|
917
|
+
} catch (err) {
|
|
918
|
+
log(`dockerd daemon launch failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
919
|
+
}
|
|
920
|
+
const vncEnabled = req.vnc?.enabled !== false;
|
|
921
|
+
const vncPassword = vncEnabled ? generateVncPassword() : void 0;
|
|
922
|
+
if (vncEnabled && vncPassword) {
|
|
923
|
+
log("launching VNC stack (Xvnc + websockify + noVNC)");
|
|
924
|
+
try {
|
|
925
|
+
await launchCloudVncDaemon({ backend, handle, vncPassword });
|
|
926
|
+
} catch (err) {
|
|
927
|
+
log(
|
|
928
|
+
`VNC daemon launch failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
let webPreview;
|
|
933
|
+
try {
|
|
934
|
+
webPreview = await backend.previewUrl(handle, CLOUD_WEB_PROXY_PORT);
|
|
935
|
+
} catch {
|
|
936
|
+
webPreview = void 0;
|
|
937
|
+
}
|
|
938
|
+
const portlessOpt = req.providerOptions?.["portless"] ?? true;
|
|
939
|
+
let portlessAliasName;
|
|
940
|
+
let portlessUrlResolved;
|
|
941
|
+
if (portlessOpt && webPreview) {
|
|
942
|
+
const r = await bootstrapPortlessForCloudBox(backend, handle, {
|
|
943
|
+
boxName: name,
|
|
944
|
+
webPreviewUrl: webPreview.url,
|
|
945
|
+
webPort: CLOUD_WEB_PROXY_PORT,
|
|
946
|
+
onLog: log
|
|
947
|
+
});
|
|
948
|
+
if (r) {
|
|
949
|
+
portlessAliasName = r.alias;
|
|
950
|
+
portlessUrlResolved = r.url;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const servicePorts = await readExposedServicePorts(req.workspacePath);
|
|
954
|
+
const servicePreviews = {};
|
|
955
|
+
for (const port of servicePorts) {
|
|
956
|
+
if (port === CLOUD_WEB_PROXY_PORT) continue;
|
|
957
|
+
try {
|
|
958
|
+
const p = await backend.previewUrl(handle, port);
|
|
959
|
+
servicePreviews[port] = p.url;
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
let relayPreview;
|
|
964
|
+
try {
|
|
965
|
+
relayPreview = await backend.previewUrl(handle, 8787);
|
|
966
|
+
} catch {
|
|
967
|
+
relayPreview = void 0;
|
|
968
|
+
}
|
|
969
|
+
if (relayPreview) {
|
|
970
|
+
try {
|
|
971
|
+
await registerBoxWithRelay({
|
|
972
|
+
boxId: id,
|
|
973
|
+
token: relayToken,
|
|
974
|
+
name,
|
|
975
|
+
kind: "cloud",
|
|
976
|
+
backend: backend.name,
|
|
977
|
+
previewUrl: relayPreview.url,
|
|
978
|
+
previewToken: relayPreview.token,
|
|
979
|
+
bridgeToken,
|
|
980
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
981
|
+
});
|
|
982
|
+
} catch (err) {
|
|
983
|
+
log(
|
|
984
|
+
`register with host relay failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const state = await readState();
|
|
989
|
+
const projectIndex = req.projectRoot ? allocateProjectIndex(state, req.projectRoot) : void 0;
|
|
990
|
+
const record = {
|
|
991
|
+
id,
|
|
992
|
+
name,
|
|
993
|
+
provider: providerName,
|
|
994
|
+
// `container` carries the sandbox id with a `cloud:` prefix —
|
|
995
|
+
// unique within state, never collides with a real docker
|
|
996
|
+
// container, and grepping for `agentbox-cloud-*` (the old
|
|
997
|
+
// synthetic value) finds nothing now. `image` mirrors the
|
|
998
|
+
// resolved cloud image so `BoxRecord.image: string` stays
|
|
999
|
+
// required without docker-internal readers seeing `undefined`.
|
|
1000
|
+
container: `cloud:${handle.sandboxId}`,
|
|
1001
|
+
image,
|
|
1002
|
+
workspacePath: req.workspacePath,
|
|
1003
|
+
projectRoot: req.projectRoot,
|
|
1004
|
+
projectIndex,
|
|
1005
|
+
relayToken,
|
|
1006
|
+
withPlaywright: req.withPlaywright,
|
|
1007
|
+
withEnv: req.withEnv,
|
|
1008
|
+
portlessAlias: portlessAliasName,
|
|
1009
|
+
portlessUrl: portlessUrlResolved,
|
|
1010
|
+
vncEnabled,
|
|
1011
|
+
vncPassword,
|
|
1012
|
+
vncContainerPort: vncEnabled ? CLOUD_VNC_PORT : void 0,
|
|
1013
|
+
resourceLimits: req.limits ? {
|
|
1014
|
+
memoryBytes: req.limits.memoryBytes ?? void 0,
|
|
1015
|
+
cpus: req.limits.cpus ?? void 0,
|
|
1016
|
+
pidsLimit: req.limits.pidsLimit ?? void 0,
|
|
1017
|
+
disk: req.limits.disk ?? void 0
|
|
1018
|
+
} : void 0,
|
|
1019
|
+
cloud: {
|
|
1020
|
+
backend: backend.name,
|
|
1021
|
+
sandboxId: handle.sandboxId,
|
|
1022
|
+
image,
|
|
1023
|
+
webPort: CLOUD_WEB_PROXY_PORT,
|
|
1024
|
+
previewUrls: (() => {
|
|
1025
|
+
const m = { ...servicePreviews };
|
|
1026
|
+
if (webPreview) m[CLOUD_WEB_PROXY_PORT] = webPreview.url;
|
|
1027
|
+
return Object.keys(m).length > 0 ? m : void 0;
|
|
1028
|
+
})(),
|
|
1029
|
+
relayPreviewUrl: relayPreview?.url,
|
|
1030
|
+
relayPreviewToken: relayPreview?.token,
|
|
1031
|
+
bridgeToken,
|
|
1032
|
+
snapshotRef: resolvedCheckpointRef
|
|
1033
|
+
},
|
|
1034
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1035
|
+
};
|
|
1036
|
+
await recordBox(record);
|
|
1037
|
+
return { record, imageBuilt: false };
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
try {
|
|
1040
|
+
await backend.destroy(handle);
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
throw err;
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
async start(box) {
|
|
1047
|
+
const h = handleFor(box);
|
|
1048
|
+
await backend.start(h);
|
|
1049
|
+
const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
|
|
1050
|
+
let webPreview;
|
|
1051
|
+
try {
|
|
1052
|
+
webPreview = await backend.previewUrl(h, webPort);
|
|
1053
|
+
} catch {
|
|
1054
|
+
const cached = box.cloud?.previewUrls?.[webPort];
|
|
1055
|
+
webPreview = cached ? { url: cached } : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
const servicePreviews = {};
|
|
1058
|
+
try {
|
|
1059
|
+
const ports = await readExposedServicePorts(box.workspacePath);
|
|
1060
|
+
for (const port of ports) {
|
|
1061
|
+
if (port === webPort) continue;
|
|
1062
|
+
try {
|
|
1063
|
+
const p = await backend.previewUrl(h, port);
|
|
1064
|
+
servicePreviews[port] = p.url;
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
let relayPreview;
|
|
1071
|
+
try {
|
|
1072
|
+
relayPreview = await backend.previewUrl(h, 8787);
|
|
1073
|
+
} catch {
|
|
1074
|
+
relayPreview = box.cloud?.relayPreviewUrl ? { url: box.cloud.relayPreviewUrl, token: box.cloud.relayPreviewToken } : void 0;
|
|
1075
|
+
}
|
|
1076
|
+
const mergedPreviews = {
|
|
1077
|
+
...box.cloud?.previewUrls ?? {},
|
|
1078
|
+
...servicePreviews
|
|
1079
|
+
};
|
|
1080
|
+
if (webPreview !== void 0) mergedPreviews[webPort] = webPreview.url;
|
|
1081
|
+
let portlessAliasName = box.portlessAlias;
|
|
1082
|
+
let portlessUrlResolved = box.portlessUrl;
|
|
1083
|
+
if (box.portlessAlias && webPreview) {
|
|
1084
|
+
const r = await bootstrapPortlessForCloudBox(backend, h, {
|
|
1085
|
+
boxName: box.name,
|
|
1086
|
+
webPreviewUrl: webPreview.url,
|
|
1087
|
+
webPort,
|
|
1088
|
+
onLog: () => {
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
if (r) {
|
|
1092
|
+
portlessAliasName = r.alias;
|
|
1093
|
+
portlessUrlResolved = r.url;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const next = {
|
|
1097
|
+
...box,
|
|
1098
|
+
portlessAlias: portlessAliasName,
|
|
1099
|
+
portlessUrl: portlessUrlResolved,
|
|
1100
|
+
cloud: {
|
|
1101
|
+
...box.cloud ?? { backend: providerName, sandboxId: h.sandboxId },
|
|
1102
|
+
webPort,
|
|
1103
|
+
previewUrls: Object.keys(mergedPreviews).length > 0 ? mergedPreviews : void 0,
|
|
1104
|
+
relayPreviewUrl: relayPreview?.url ?? box.cloud?.relayPreviewUrl,
|
|
1105
|
+
relayPreviewToken: relayPreview?.token ?? box.cloud?.relayPreviewToken
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
await recordBox(next);
|
|
1109
|
+
await launchCloudCtlDaemon({
|
|
1110
|
+
backend,
|
|
1111
|
+
handle: h,
|
|
1112
|
+
boxId: box.id,
|
|
1113
|
+
boxName: box.name,
|
|
1114
|
+
relayUrl: `http://127.0.0.1:${String(8787)}`,
|
|
1115
|
+
relayToken: box.relayToken ?? "",
|
|
1116
|
+
bridgeToken: box.cloud?.bridgeToken
|
|
1117
|
+
});
|
|
1118
|
+
try {
|
|
1119
|
+
const dockerd = await launchCloudDockerdDaemon({ backend, handle: h, timeoutMs: 6e4 });
|
|
1120
|
+
if (!dockerd.up) {
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
if (box.vncEnabled && box.vncPassword) {
|
|
1125
|
+
try {
|
|
1126
|
+
await launchCloudVncDaemon({ backend, handle: h, vncPassword: box.vncPassword });
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (relayPreview && box.relayToken && box.cloud?.bridgeToken) {
|
|
1131
|
+
try {
|
|
1132
|
+
await registerBoxWithRelay({
|
|
1133
|
+
boxId: box.id,
|
|
1134
|
+
token: box.relayToken,
|
|
1135
|
+
name: box.name,
|
|
1136
|
+
kind: "cloud",
|
|
1137
|
+
backend: backend.name,
|
|
1138
|
+
previewUrl: relayPreview.url,
|
|
1139
|
+
previewToken: relayPreview.token,
|
|
1140
|
+
bridgeToken: box.cloud.bridgeToken,
|
|
1141
|
+
createdAt: box.createdAt,
|
|
1142
|
+
projectIndex: box.projectIndex
|
|
1143
|
+
});
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return next;
|
|
1148
|
+
},
|
|
1149
|
+
async pause(box) {
|
|
1150
|
+
await backend.pause(handleFor(box));
|
|
1151
|
+
},
|
|
1152
|
+
async resume(box) {
|
|
1153
|
+
await backend.resume(handleFor(box));
|
|
1154
|
+
},
|
|
1155
|
+
async stop(box) {
|
|
1156
|
+
await backend.stop(handleFor(box));
|
|
1157
|
+
},
|
|
1158
|
+
async destroy(box) {
|
|
1159
|
+
try {
|
|
1160
|
+
await backend.destroy(handleFor(box));
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1163
|
+
if (!/not.?found|missing/i.test(msg)) throw err;
|
|
1164
|
+
}
|
|
1165
|
+
if (box.portlessAlias) {
|
|
1166
|
+
try {
|
|
1167
|
+
await portlessUnalias(box.portlessAlias);
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
await forgetBoxFromRelay(box.id);
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
await removeBoxRecord(box.id);
|
|
1176
|
+
},
|
|
1177
|
+
async probeState(box) {
|
|
1178
|
+
return probe(box);
|
|
1179
|
+
},
|
|
1180
|
+
async inspect(box) {
|
|
1181
|
+
const state = await probe(box);
|
|
1182
|
+
const webPort = box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
|
|
1183
|
+
const portlessWebUrl = box.portlessAlias !== void 0 ? box.portlessUrl ?? `https://${box.portlessAlias}.localhost` : void 0;
|
|
1184
|
+
const cachedWebUrl = box.cloud?.previewUrls?.[webPort];
|
|
1185
|
+
const webUrl = portlessWebUrl ?? cachedWebUrl;
|
|
1186
|
+
const endpoints = [];
|
|
1187
|
+
if (webUrl) {
|
|
1188
|
+
endpoints.push({
|
|
1189
|
+
kind: "web",
|
|
1190
|
+
name: "web",
|
|
1191
|
+
containerPort: webPort,
|
|
1192
|
+
url: webUrl,
|
|
1193
|
+
reachable: true
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
for (const [portStr, url] of Object.entries(box.cloud?.previewUrls ?? {})) {
|
|
1197
|
+
const port = Number.parseInt(portStr, 10);
|
|
1198
|
+
if (!Number.isFinite(port) || port === webPort) continue;
|
|
1199
|
+
endpoints.push({
|
|
1200
|
+
kind: "web",
|
|
1201
|
+
name: `service-${String(port)}`,
|
|
1202
|
+
containerPort: port,
|
|
1203
|
+
url,
|
|
1204
|
+
reachable: true
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
record: box,
|
|
1209
|
+
state,
|
|
1210
|
+
endpoints: {
|
|
1211
|
+
domain: webUrl ? new URL(webUrl).host : "",
|
|
1212
|
+
domainIsOrb: false,
|
|
1213
|
+
endpoints
|
|
1214
|
+
},
|
|
1215
|
+
raw: void 0
|
|
1216
|
+
};
|
|
1217
|
+
},
|
|
1218
|
+
async exec(box, argv, opts2) {
|
|
1219
|
+
const r = await backend.exec(handleFor(box), quoteShellArgv(argv), {
|
|
1220
|
+
cwd: opts2?.cwd,
|
|
1221
|
+
env: opts2?.env,
|
|
1222
|
+
user: opts2?.user
|
|
1223
|
+
});
|
|
1224
|
+
return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
|
|
1225
|
+
},
|
|
1226
|
+
async buildAttach(box, kind, opts2) {
|
|
1227
|
+
if (!backend.attachArgv) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`cloud backend '${backend.name}' does not implement attachArgv \u2014 interactive attach not supported`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
const handle = handleFor(box);
|
|
1233
|
+
const baseArgv = await backend.attachArgv(handle);
|
|
1234
|
+
const inner = renderInnerCommand(kind, opts2);
|
|
1235
|
+
const argv = [...baseArgv.slice(1), "-t", inner];
|
|
1236
|
+
const fullArgv = [baseArgv[0], ...argv];
|
|
1237
|
+
const cleanup = backend.revokeAttachToken ? async () => {
|
|
1238
|
+
await backend.revokeAttachToken(handle, baseArgv);
|
|
1239
|
+
} : void 0;
|
|
1240
|
+
return { argv: fullArgv, cleanup };
|
|
1241
|
+
},
|
|
1242
|
+
async uploadPath(box, hostSrc, boxDst) {
|
|
1243
|
+
return uploadToCloudBox(backend, handleFor(box), hostSrc, boxDst);
|
|
1244
|
+
},
|
|
1245
|
+
async downloadPath(box, boxSrc, hostDst) {
|
|
1246
|
+
return downloadFromCloudBox(backend, handleFor(box), boxSrc, hostDst);
|
|
1247
|
+
},
|
|
1248
|
+
async downloadDirContents(box, boxSrc, hostDst) {
|
|
1249
|
+
return pullCloudDirContents(backend, handleFor(box), boxSrc, hostDst);
|
|
1250
|
+
},
|
|
1251
|
+
async resolveUrl(box, opts2) {
|
|
1252
|
+
const h = handleFor(box);
|
|
1253
|
+
const kind = opts2?.kind ?? "web";
|
|
1254
|
+
const port = kind === "vnc" ? CLOUD_VNC_PORT : box.cloud?.webPort ?? CLOUD_WEB_PROXY_PORT;
|
|
1255
|
+
if (backend.signedPreviewUrl) {
|
|
1256
|
+
const ttl = opts2?.ttl ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
|
|
1257
|
+
const signed = await backend.signedPreviewUrl(h, port, ttl);
|
|
1258
|
+
return signed.url;
|
|
1259
|
+
}
|
|
1260
|
+
const p = await backend.previewUrl(h, port);
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`cloud backend '${backend.name}' does not support signed preview URLs; the standard URL (${p.url}) requires a header token (e.g. x-daytona-preview-token: ${p.token ?? "<unset>"}) that browsers can't attach from a click. Use a programmatic client or wait for backend support.`
|
|
1263
|
+
);
|
|
1264
|
+
},
|
|
1265
|
+
// Cloud checkpoint capability. Backends without `createSnapshot` get a
|
|
1266
|
+
// capability stub whose methods throw — the CLI's `agentbox checkpoint
|
|
1267
|
+
// create` then surfaces a clean "not supported" error rather than a
|
|
1268
|
+
// silent no-op.
|
|
1269
|
+
checkpoint: makeCloudCheckpoint(backend)
|
|
1270
|
+
// stats is provider-optional; cloud backends without a metrics API just
|
|
1271
|
+
// omit it. Backends that have one can decorate the returned provider.
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function makeCloudCheckpoint(backend) {
|
|
1275
|
+
return {
|
|
1276
|
+
async create(box, name) {
|
|
1277
|
+
if (!backend.createSnapshot) {
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
`cloud backend '${backend.name}' doesn't support snapshots \u2014 \`agentbox checkpoint\` unavailable`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
if (!box.projectRoot) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`cloud checkpoint requires the box to have a project root (run \`agentbox checkpoint\` from inside the project)`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (!box.cloud?.sandboxId) {
|
|
1288
|
+
throw new Error(`cloud box ${box.name} has no sandboxId \u2014 record is malformed`);
|
|
1289
|
+
}
|
|
1290
|
+
const snapshotName = cloudSnapshotName(box.projectRoot, name);
|
|
1291
|
+
await backend.createSnapshot({ sandboxId: box.cloud.sandboxId }, snapshotName);
|
|
1292
|
+
const info = await writeCloudCheckpointManifest(box.projectRoot, backend.name, name, {
|
|
1293
|
+
snapshotName,
|
|
1294
|
+
sourceBoxId: box.id,
|
|
1295
|
+
sourceBoxName: box.name
|
|
1296
|
+
});
|
|
1297
|
+
return { ref: info.name };
|
|
1298
|
+
},
|
|
1299
|
+
async list(projectRoot) {
|
|
1300
|
+
const entries = await listCloudCheckpoints(projectRoot, backend.name);
|
|
1301
|
+
return entries.map((e) => ({ ref: e.name, createdAt: e.manifest.createdAt }));
|
|
1302
|
+
},
|
|
1303
|
+
async remove(projectRoot, ref) {
|
|
1304
|
+
const entry = await resolveCloudCheckpoint(projectRoot, backend.name, ref);
|
|
1305
|
+
if (!entry) return;
|
|
1306
|
+
if (backend.deleteSnapshot) {
|
|
1307
|
+
try {
|
|
1308
|
+
await backend.deleteSnapshot(entry.manifest.snapshotName);
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
await removeCloudCheckpointDir(projectRoot, backend.name, ref);
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function renderInnerCommand(kind, opts) {
|
|
1317
|
+
const sessionName = opts?.sessionName ?? defaultSessionName(kind);
|
|
1318
|
+
const fallback = opts?.command ?? defaultCommand(kind, opts);
|
|
1319
|
+
if (kind === "logs") {
|
|
1320
|
+
return fallback;
|
|
1321
|
+
}
|
|
1322
|
+
if (opts?.noTmux) {
|
|
1323
|
+
return fallback;
|
|
1324
|
+
}
|
|
1325
|
+
return `command -v tmux >/dev/null || { echo "tmux not installed in sandbox"; exit 127; }; exec tmux new-session -A -c ${shellSingle(CLOUD_WORKSPACE_DIR)} -s ${shellSingle(sessionName)} ${shellSingle(fallback)}`;
|
|
1326
|
+
}
|
|
1327
|
+
function defaultSessionName(kind) {
|
|
1328
|
+
switch (kind) {
|
|
1329
|
+
case "shell":
|
|
1330
|
+
return "shell";
|
|
1331
|
+
case "agent":
|
|
1332
|
+
return "agent";
|
|
1333
|
+
case "logs":
|
|
1334
|
+
return "logs";
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function defaultCommand(kind, opts) {
|
|
1338
|
+
switch (kind) {
|
|
1339
|
+
case "shell":
|
|
1340
|
+
return "bash -l";
|
|
1341
|
+
case "agent":
|
|
1342
|
+
return "bash -l";
|
|
1343
|
+
case "logs": {
|
|
1344
|
+
if (!opts?.service) {
|
|
1345
|
+
return 'echo "no service specified \u2014 set BuildAttachOptions.service"';
|
|
1346
|
+
}
|
|
1347
|
+
const tail = opts.tail !== void 0 ? String(opts.tail) : "200";
|
|
1348
|
+
const args = [`--tail ${shellSingle(tail)}`];
|
|
1349
|
+
if (opts.follow !== false) args.push("--follow");
|
|
1350
|
+
return `/usr/local/bin/agentbox-ctl logs ${shellSingle(opts.service)} ${args.join(" ")}`;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function shellSingle(s) {
|
|
1355
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
export {
|
|
1359
|
+
ensureAgentVolumesForCloud,
|
|
1360
|
+
seedAgentVolumesIfFresh,
|
|
1361
|
+
agentSpecsForCloud,
|
|
1362
|
+
listCloudCheckpoints,
|
|
1363
|
+
resolveCloudCheckpoint,
|
|
1364
|
+
createCloudProvider
|
|
1365
|
+
};
|
|
1366
|
+
//# sourceMappingURL=chunk-NW5NYTQM.js.map
|