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