@luxorai/agentbox-provider 0.0.0-stage → 0.1.1

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/index.js ADDED
@@ -0,0 +1,1603 @@
1
+ // src/index.ts
2
+ import {
3
+ createCloudProvider,
4
+ currentCloudBaseFingerprint,
5
+ listCloudCheckpoints,
6
+ readCliStamp as readCliStamp2,
7
+ removeCloudCheckpointDir,
8
+ resolveCloudCheckpoint,
9
+ SDK_API_VERSION,
10
+ writeCloudCheckpointManifest
11
+ } from "@madarco/agentbox-provider-sdk";
12
+
13
+ // src/backend.ts
14
+ import { createReadStream, createWriteStream, rmSync } from "fs";
15
+ import { basename, posix } from "path";
16
+ import { Readable } from "stream";
17
+ import { pipeline } from "stream/promises";
18
+
19
+ // src/sdk.ts
20
+ import { TenkiSandbox } from "@tenkicloud/sandbox";
21
+ import { UserFacingError } from "@madarco/agentbox-provider-sdk";
22
+
23
+ // src/env-loader.ts
24
+ import { existsSync, readFileSync } from "fs";
25
+ import { homedir } from "os";
26
+ import { resolve } from "path";
27
+ var TENKI_KEYS = [
28
+ "TENKI_AUTH_TOKEN",
29
+ "TENKI_API_TOKEN",
30
+ "TENKI_BASE_URL",
31
+ "TENKI_GATEWAY_ADDRESS"
32
+ ];
33
+ var loaded = false;
34
+ function ensureTenkiEnvLoaded() {
35
+ if (loaded) return;
36
+ loaded = true;
37
+ importTenkiFromFile(resolve(homedir(), ".agentbox", "secrets.env"), TENKI_KEYS);
38
+ if (!process.env.TENKI_AUTH_TOKEN && process.env.TENKI_API_TOKEN) {
39
+ process.env.TENKI_AUTH_TOKEN = process.env.TENKI_API_TOKEN;
40
+ }
41
+ }
42
+ function reloadTenkiEnv() {
43
+ loaded = false;
44
+ ensureTenkiEnvLoaded();
45
+ }
46
+ function importTenkiFromFile(path, keys) {
47
+ if (!existsSync(path)) return;
48
+ let body;
49
+ try {
50
+ body = readFileSync(path, "utf8");
51
+ } catch {
52
+ return;
53
+ }
54
+ const parsed = parseEnvFile(body);
55
+ for (const key of keys) {
56
+ if (process.env[key] !== void 0) continue;
57
+ const value = parsed[key];
58
+ if (typeof value === "string") {
59
+ process.env[key] = value;
60
+ }
61
+ }
62
+ }
63
+ function parseEnvFile(body) {
64
+ const out = {};
65
+ for (const rawLine of body.split(/\r?\n/)) {
66
+ const line = rawLine.trim();
67
+ if (line.length === 0 || line.startsWith("#")) continue;
68
+ const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
69
+ const eq = stripped.indexOf("=");
70
+ if (eq <= 0) continue;
71
+ const key = stripped.slice(0, eq).trim();
72
+ let value = stripped.slice(eq + 1).trim();
73
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
74
+ value = value.slice(1, -1);
75
+ }
76
+ out[key] = value;
77
+ }
78
+ return out;
79
+ }
80
+
81
+ // src/sdk.ts
82
+ import {
83
+ SandboxError,
84
+ MissingAuthTokenError,
85
+ SessionNotFoundError,
86
+ SessionTerminatedError,
87
+ SessionExpiredError,
88
+ SnapshotNotFoundError,
89
+ SnapshotFailedError,
90
+ UnauthorizedError,
91
+ PermissionDeniedError,
92
+ QuotaExceededError,
93
+ CapacityUnavailableError,
94
+ RateLimitedError,
95
+ InvalidStateError,
96
+ FileNotFoundError
97
+ } from "@tenkicloud/sandbox";
98
+ function resolveAuthToken() {
99
+ ensureTenkiEnvLoaded();
100
+ const t = process.env.TENKI_AUTH_TOKEN;
101
+ if (!t) {
102
+ throw new UserFacingError(
103
+ "Tenki credentials not configured.\nSet TENKI_AUTH_TOKEN (your auth token from https://tenki.cloud) in the environment or in ~/.agentbox/secrets.env. Running `agentbox create --provider tenki` on a terminal also prompts for it."
104
+ );
105
+ }
106
+ return t;
107
+ }
108
+ function hasUsableCredentials() {
109
+ ensureTenkiEnvLoaded();
110
+ return Boolean(process.env.TENKI_AUTH_TOKEN);
111
+ }
112
+ function getTenkiClient() {
113
+ const opts = { authToken: resolveAuthToken() };
114
+ const baseUrl = process.env.TENKI_BASE_URL;
115
+ if (baseUrl) opts.baseUrl = baseUrl;
116
+ const gatewayAddress = process.env.TENKI_GATEWAY_ADDRESS;
117
+ if (gatewayAddress) opts.gatewayAddress = gatewayAddress;
118
+ return new TenkiSandbox(opts);
119
+ }
120
+
121
+ // src/retry.ts
122
+ var DEFAULT_BACKOFF = [1e3, 2e3, 4e3];
123
+ var DEFAULT_ATTEMPT_TIMEOUT_MS = 3e4;
124
+ var AttemptTimeoutError = class extends Error {
125
+ constructor(method, ms) {
126
+ super(`tenki ${method}: per-attempt timeout after ${String(ms)}ms`);
127
+ this.name = "AttemptTimeoutError";
128
+ }
129
+ };
130
+ function connectCodeOf(err) {
131
+ if (!err || typeof err !== "object") return void 0;
132
+ const code = err.code;
133
+ return typeof code === "string" ? code : void 0;
134
+ }
135
+ function statusCodeOf(err) {
136
+ if (!err || typeof err !== "object") return void 0;
137
+ for (const key of ["statusCode", "status"]) {
138
+ const v = err[key];
139
+ if (typeof v === "number") return v;
140
+ }
141
+ return void 0;
142
+ }
143
+ var RETRIABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
144
+ "unavailable",
145
+ "resource_exhausted",
146
+ "deadline_exceeded",
147
+ "aborted"
148
+ ]);
149
+ function isRetriable(err, allowAmbiguous) {
150
+ if (err instanceof AttemptTimeoutError) return allowAmbiguous;
151
+ const name = err instanceof Error ? err.name : void 0;
152
+ if (name === "RateLimitedError" || name === "CapacityUnavailableError") return true;
153
+ if (name === "UnauthorizedError" || name === "PermissionDeniedError" || name === "MissingAuthTokenError" || name === "SessionNotFoundError" || name === "SnapshotNotFoundError" || name === "QuotaExceededError" || name === "InvalidStateError" || name === "FileNotFoundError") {
154
+ return false;
155
+ }
156
+ const connectCode = connectCodeOf(err);
157
+ if (connectCode !== void 0 && RETRIABLE_CONNECT_CODES.has(connectCode)) {
158
+ return connectCode === "unavailable" || connectCode === "resource_exhausted" ? true : allowAmbiguous;
159
+ }
160
+ const status = statusCodeOf(err);
161
+ if (status !== void 0) {
162
+ if (status === 429) return true;
163
+ if (status >= 500 && status <= 599) return allowAmbiguous;
164
+ return false;
165
+ }
166
+ if (err && typeof err === "object") {
167
+ const candidates = [err, err.cause];
168
+ for (const c of candidates) {
169
+ if (!c || typeof c !== "object") continue;
170
+ const code = c.code;
171
+ if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNABORTED" || code === "EAI_AGAIN" || code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EPIPE" || code === "UND_ERR_SOCKET" || code === "UND_ERR_CONNECT_TIMEOUT") {
172
+ return allowAmbiguous;
173
+ }
174
+ }
175
+ }
176
+ return false;
177
+ }
178
+ async function withTenkiRetry(opts, fn) {
179
+ const backoff = opts.backoffMs ?? DEFAULT_BACKOFF;
180
+ const maxAttempts = backoff.length + 1;
181
+ const timeoutMs = opts.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
182
+ const log2 = opts.onRetry ?? defaultRetryLog;
183
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
184
+ try {
185
+ return await raceTimeout(fn(), timeoutMs, opts.method);
186
+ } catch (err) {
187
+ const last = attempt === maxAttempts;
188
+ if (last || !isRetriable(err, opts.retryOnAmbiguous)) throw err;
189
+ const delay = backoff[attempt - 1] ?? backoff[backoff.length - 1] ?? 4e3;
190
+ log2(
191
+ `tenki ${opts.method}: attempt ${String(attempt)} failed (${errorSummary(err)}); retrying in ${String(delay)}ms`
192
+ );
193
+ await sleep(delay);
194
+ }
195
+ }
196
+ throw new Error(`withTenkiRetry: exhausted attempts for ${opts.method}`);
197
+ }
198
+ function defaultRetryLog(line) {
199
+ process.stderr.write(`
200
+ [tenki-retry] ${line}
201
+ `);
202
+ }
203
+ function sleep(ms) {
204
+ return new Promise((r) => setTimeout(r, ms));
205
+ }
206
+ async function raceTimeout(p, ms, method) {
207
+ let timer;
208
+ try {
209
+ return await Promise.race([
210
+ p,
211
+ new Promise((_resolve, reject) => {
212
+ timer = setTimeout(() => reject(new AttemptTimeoutError(method, ms)), ms);
213
+ })
214
+ ]);
215
+ } finally {
216
+ if (timer !== void 0) clearTimeout(timer);
217
+ }
218
+ }
219
+ function errorSummary(err) {
220
+ if (err instanceof Error) {
221
+ const code = connectCodeOf(err) ?? statusCodeOf(err);
222
+ return code !== void 0 ? `${err.name}(${String(code)}): ${truncate(err.message)}` : `${err.name}: ${truncate(err.message)}`;
223
+ }
224
+ return truncate(String(err));
225
+ }
226
+ function truncate(s, max = 160) {
227
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
228
+ }
229
+
230
+ // src/prepared-state.ts
231
+ import {
232
+ preparedStatePathFor,
233
+ readPreparedStateRaw,
234
+ UserFacingError as UserFacingError2,
235
+ writePreparedStateRaw
236
+ } from "@madarco/agentbox-provider-sdk";
237
+ var SCHEMA = 2;
238
+ function preparedStatePath() {
239
+ return preparedStatePathFor("tenki");
240
+ }
241
+ function readPreparedState() {
242
+ const raw = readPreparedStateRaw("tenki");
243
+ if (raw === null || typeof raw !== "object") return { schema: SCHEMA };
244
+ const parsed = raw;
245
+ if (parsed.schema !== SCHEMA) {
246
+ return { schema: SCHEMA };
247
+ }
248
+ return { schema: SCHEMA, base: parsed.base };
249
+ }
250
+ function writePreparedState(state) {
251
+ writePreparedStateRaw("tenki", state);
252
+ }
253
+ function updatePreparedState(mutate) {
254
+ const s = readPreparedState();
255
+ mutate(s);
256
+ writePreparedState(s);
257
+ }
258
+ async function currentTenkiBaseFingerprintLive() {
259
+ return void 0;
260
+ }
261
+ function ensureTenkiBaseImage() {
262
+ const state = readPreparedState();
263
+ if (state.base !== void 0) return;
264
+ throw new UserFacingError2(
265
+ "no Tenki base image found.\nRun `agentbox prepare --provider tenki` first \u2014 it bakes a snapshot carrying the agentbox runtime (agentbox-ctl, claude/codex/opencode, tmux) in your Tenki workspace so per-box `create` boots ready in seconds."
266
+ );
267
+ }
268
+
269
+ // src/build-attach.ts
270
+ import { execFile } from "child_process";
271
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
272
+ import { homedir as homedir2 } from "os";
273
+ import { dirname, join, resolve as resolve2 } from "path";
274
+ import { fileURLToPath } from "url";
275
+ import { promisify } from "util";
276
+ import {
277
+ hostTermForCloud,
278
+ renderInnerCommand,
279
+ UserFacingError as UserFacingError3
280
+ } from "@madarco/agentbox-provider-sdk";
281
+ var execFileAsync = promisify(execFile);
282
+ var SELF = dirname(fileURLToPath(import.meta.url));
283
+ var SSH_USER = process.env.AGENTBOX_TENKI_SSH_USER?.trim() || "tenki";
284
+ var CERT_TTL_MS = 60 * 6e4;
285
+ function resolveAttachHelperPath() {
286
+ const candidates = [
287
+ resolve2(SELF, "attach-helper.cjs"),
288
+ resolve2(SELF, "..", "dist", "attach-helper.cjs")
289
+ ];
290
+ for (const p of candidates) {
291
+ if (existsSync2(p)) return p;
292
+ }
293
+ return candidates[0];
294
+ }
295
+ function sshKeyDir(sandboxId) {
296
+ return resolve2(homedir2(), ".agentbox", "boxes", sandboxId, "ssh");
297
+ }
298
+ async function ensureKeypair(sandboxId) {
299
+ const dir = sshKeyDir(sandboxId);
300
+ mkdirSync(dir, { recursive: true, mode: 448 });
301
+ const keyPath = join(dir, "id_ed25519");
302
+ if (!existsSync2(keyPath)) {
303
+ try {
304
+ await execFileAsync("ssh-keygen", [
305
+ "-t",
306
+ "ed25519",
307
+ "-N",
308
+ "",
309
+ "-C",
310
+ `agentbox-tenki-${sandboxId}`,
311
+ "-q",
312
+ "-f",
313
+ keyPath
314
+ ]);
315
+ } catch (err) {
316
+ throw new UserFacingError3(
317
+ "tenki attach: could not generate an SSH key \u2014 `ssh-keygen` is required (it ships with OpenSSH, alongside the `ssh` client this attach also needs).\n" + (err instanceof Error ? err.message : String(err))
318
+ );
319
+ }
320
+ chmodSync(keyPath, 384);
321
+ }
322
+ return { keyPath, publicKey: readFileSync2(`${keyPath}.pub`, "utf8").trim() };
323
+ }
324
+ async function issueCert(sandboxId, keyPath, publicKey) {
325
+ const cert = await getTenkiClient().issueSandboxSSHCert(sandboxId, publicKey, {
326
+ ttlMs: CERT_TTL_MS
327
+ });
328
+ const certPath = `${keyPath}-cert.pub`;
329
+ writeFileSync(certPath, cert.sshCert.trim() + "\n", { mode: 384 });
330
+ return certPath;
331
+ }
332
+ function shq(s) {
333
+ return `'${s.replace(/'/g, `'\\''`)}'`;
334
+ }
335
+ async function buildTenkiAttach(box, kind, opts) {
336
+ const sandboxId = box.cloud?.sandboxId;
337
+ if (!sandboxId) {
338
+ throw new Error(`tenki box ${box.name} has no sandboxId \u2014 record is malformed`);
339
+ }
340
+ const helper = resolveAttachHelperPath();
341
+ if (!existsSync2(helper)) {
342
+ throw new Error(
343
+ `tenki attach helper not found at ${helper} \u2014 the installed @tenkicloud/agentbox-provider is missing dist/attach-helper.cjs; reinstall the package.`
344
+ );
345
+ }
346
+ const authToken = resolveAuthToken();
347
+ const inner = renderInnerCommand(kind, opts);
348
+ const hostTerm = hostTermForCloud();
349
+ const env = {
350
+ // The ProxyCommand child opens the gateway WebSocket, so it needs the token.
351
+ TENKI_AUTH_TOKEN: authToken,
352
+ AGENTBOX_HOST_TERM: hostTerm,
353
+ // `ssh -t` propagates the local TERM to the remote PTY.
354
+ TERM: hostTerm
355
+ };
356
+ if (process.env.TENKI_BASE_URL) env.TENKI_BASE_URL = process.env.TENKI_BASE_URL;
357
+ if (process.env.TENKI_GATEWAY_ADDRESS)
358
+ env.TENKI_GATEWAY_ADDRESS = process.env.TENKI_GATEWAY_ADDRESS;
359
+ if (opts?.detached) {
360
+ return {
361
+ argv: [process.execPath, helper, "--detached", "--session-id", sandboxId],
362
+ env: { ...env, AGENTBOX_TENKI_INNER_CMD: inner }
363
+ };
364
+ }
365
+ const { keyPath, publicKey } = await ensureKeypair(sandboxId);
366
+ const certPath = await issueCert(sandboxId, keyPath, publicKey);
367
+ const proxyCommand = `${shq(process.execPath)} ${shq(helper)} --proxy --session-id ${shq(sandboxId)}`;
368
+ const argv = [
369
+ "ssh",
370
+ "-o",
371
+ `ProxyCommand=${proxyCommand}`,
372
+ // Only ever offer the key we just minted a cert for; without this ssh may
373
+ // walk the user's agent/default identities first and exhaust auth attempts.
374
+ "-o",
375
+ "IdentitiesOnly=yes",
376
+ "-i",
377
+ keyPath,
378
+ "-o",
379
+ `CertificateFile=${certPath}`,
380
+ // The gateway terminates the connection, and the "host" is a session id
381
+ // rather than a stable endpoint — so there is no host key worth pinning, and
382
+ // recording one would just churn the user's known_hosts.
383
+ "-o",
384
+ "StrictHostKeyChecking=no",
385
+ "-o",
386
+ "UserKnownHostsFile=/dev/null",
387
+ // Suppresses the "Permanently added … to the list of known hosts" banner
388
+ // that would otherwise print over the agent's first screen.
389
+ "-o",
390
+ "LogLevel=ERROR",
391
+ // Force a PTY: the remote command is `tmux attach`, which needs one.
392
+ "-t",
393
+ `${SSH_USER}@${sandboxId}`,
394
+ inner
395
+ ];
396
+ return { argv, env };
397
+ }
398
+
399
+ // src/backend.ts
400
+ var TENKI_WEB_PORT = 8080;
401
+ var DEFAULT_MAX_DURATION_MS = 45 * 6e4;
402
+ var PROVISION_TIMEOUT_MS = 3e5;
403
+ var decoder = new TextDecoder();
404
+ function mapState(s) {
405
+ switch (s) {
406
+ case "RUNNING":
407
+ case "CREATING":
408
+ case "RESUMING":
409
+ return "running";
410
+ case "PAUSED":
411
+ case "PAUSING":
412
+ // USER_SHUTDOWN is a stopped-but-not-terminated VM (0.4.0+). The API groups
413
+ // it with PAUSED as an existing, non-active state, so treat it as paused so
414
+ // an op that needs the box live resumes it rather than agentbox concluding
415
+ // the box is gone and recreating it.
416
+ case "USER_SHUTDOWN":
417
+ return "paused";
418
+ default:
419
+ return "missing";
420
+ }
421
+ }
422
+ function isGone(err) {
423
+ const name = err instanceof Error ? err.name : "";
424
+ return name === "SessionNotFoundError" || name === "SessionTerminatedError" || name === "SessionExpiredError";
425
+ }
426
+ function safeName(name) {
427
+ return name.replace(/[\u0000-\u001f]/g, "").slice(0, 200);
428
+ }
429
+ function previewSlug(sandboxId, port) {
430
+ const id = sandboxId.replace(/[^a-z0-9]/gi, "").slice(-12).toLowerCase() || "box";
431
+ return `ab-${id}-${String(port)}`;
432
+ }
433
+ var REFRESH_MIN_TTL_MS = 6e4;
434
+ function findPermanentShare(shares, port) {
435
+ return shares.find((p) => p.port === port && p.expiresAt === void 0);
436
+ }
437
+ function findExpiringShare(shares, port, minRemainingMs, nowMs) {
438
+ return shares.find(
439
+ (p) => p.port === port && p.expiresAt !== void 0 && p.expiresAt.getTime() - nowMs >= minRemainingMs
440
+ );
441
+ }
442
+ async function resolveSession(handle) {
443
+ return getTenkiClient().get(handle.sandboxId);
444
+ }
445
+ function resolveOwnerScope() {
446
+ const envWorkspace = process.env.AGENTBOX_TENKI_WORKSPACE_ID?.trim();
447
+ return envWorkspace ? { workspaceId: envWorkspace } : {};
448
+ }
449
+ async function ensureLive(session) {
450
+ if (session.state === "PAUSED" || session.state === "PAUSING" || session.state === "USER_SHUTDOWN") {
451
+ await session.resume();
452
+ await session.waitReady(PROVISION_TIMEOUT_MS);
453
+ }
454
+ }
455
+ function shq2(s) {
456
+ return `'${s.replace(/'/g, `'\\''`)}'`;
457
+ }
458
+ var stageSeq = 0;
459
+ function stageName(kind) {
460
+ stageSeq += 1;
461
+ return `.agentbox-${kind}-${String(process.pid)}-${String(stageSeq)}.tmp`;
462
+ }
463
+ var workdirCache = /* @__PURE__ */ new Map();
464
+ async function resolveGuestWorkdir(session) {
465
+ const cached = workdirCache.get(session.id);
466
+ if (cached !== void 0) return cached;
467
+ const r = await session.run(["bash", "-c", "pwd"]);
468
+ const wd = decoder.decode(r.stdout).trim() || "/";
469
+ workdirCache.set(session.id, wd);
470
+ return wd;
471
+ }
472
+ function isUnderWorkdir(path, workdir) {
473
+ if (!posix.isAbsolute(path)) return true;
474
+ const p = posix.normalize(path);
475
+ const d = posix.normalize(workdir);
476
+ return p === d || p.startsWith(d.endsWith("/") ? d : `${d}/`);
477
+ }
478
+ async function moveStagedTo(session, relStage, remotePath) {
479
+ const script = [
480
+ "set -eu",
481
+ "if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi",
482
+ `src="$(pwd)/${relStage}"`,
483
+ `$SUDO mkdir -p ${shq2(posix.dirname(remotePath))}`,
484
+ `$SUDO mv -f "$src" ${shq2(remotePath)}`
485
+ ].join("\n");
486
+ const r = await session.run(["bash", "-c", script]);
487
+ if (r.exitCode !== 0) {
488
+ try {
489
+ await session.run(["bash", "-c", `rm -f ${shq2(relStage)}`]);
490
+ } catch {
491
+ }
492
+ throw new Error(
493
+ `tenki uploadFile: staging move to ${remotePath} failed (exit ${String(r.exitCode)}): ${decoder.decode(r.stderr).trim()}`
494
+ );
495
+ }
496
+ }
497
+ async function copyIntoStage(session, remotePath, relStage) {
498
+ const script = [
499
+ "set -eu",
500
+ "if command -v sudo >/dev/null 2>&1; then SUDO='sudo -n'; else SUDO=''; fi",
501
+ `dst="$(pwd)/${relStage}"`,
502
+ `$SUDO cp -f ${shq2(remotePath)} "$dst"`,
503
+ // The RPC reads as the guest user; ensure it owns the staged copy.
504
+ `$SUDO chown "$(id -u):$(id -g)" "$dst" 2>/dev/null || true`
505
+ ].join("\n");
506
+ const r = await session.run(["bash", "-c", script]);
507
+ if (r.exitCode !== 0) {
508
+ throw new Error(
509
+ `tenki downloadFile: staging copy from ${remotePath} failed (exit ${String(r.exitCode)}): ${decoder.decode(r.stderr).trim()}`
510
+ );
511
+ }
512
+ }
513
+ function parseTenkiSize(spec) {
514
+ if (!spec) return void 0;
515
+ const parts = spec.trim().split("-");
516
+ if (parts.length !== 2 && parts.length !== 3) return void 0;
517
+ const nums = parts.map((p) => Number(p));
518
+ if (nums.some((n) => !Number.isInteger(n) || n <= 0)) return void 0;
519
+ return { cpu: nums[0], memoryGb: nums[1], diskGb: nums[2] };
520
+ }
521
+ var tenkiBackend = {
522
+ name: "tenki",
523
+ webProxyPort: TENKI_WEB_PORT,
524
+ async provision(req) {
525
+ const log2 = req.onLog ?? (() => {
526
+ });
527
+ if (req.snapshot === void 0) ensureTenkiBaseImage();
528
+ const snapshotId = req.snapshot ?? readPreparedState().base?.snapshotId;
529
+ if (!snapshotId) {
530
+ throw new Error(
531
+ "tenki provision: no base snapshot available \u2014 `agentbox prepare --provider tenki` must run first"
532
+ );
533
+ }
534
+ let cpuCores = req.resources?.cpu ?? 2;
535
+ let memoryMb = (req.resources?.memory ?? 4) * 1024;
536
+ let diskSizeGb = req.resources?.disk ?? 8;
537
+ if (req.size && req.size.trim().length > 0) {
538
+ const parsed = parseTenkiSize(req.size);
539
+ if (parsed) {
540
+ cpuCores = parsed.cpu;
541
+ memoryMb = parsed.memoryGb * 1024;
542
+ if (parsed.diskGb !== void 0) diskSizeGb = parsed.diskGb;
543
+ } else {
544
+ (req.onLog ?? (() => {
545
+ }))(
546
+ `tenki: ignoring invalid size '${req.size}' (expected 'cpu-memory' or 'cpu-memory-disk' GB, e.g. '4-8')`
547
+ );
548
+ }
549
+ }
550
+ const maxDurationMs = req.timeoutMs ?? DEFAULT_MAX_DURATION_MS;
551
+ const client = getTenkiClient();
552
+ const { workspaceId } = resolveOwnerScope();
553
+ const session = await withTenkiRetry(
554
+ { method: "provision", retryOnAmbiguous: false, attemptTimeoutMs: 12e4, backoffMs: [] },
555
+ () => client.create({
556
+ name: safeName(req.name),
557
+ snapshotId,
558
+ ...workspaceId ? { workspaceId } : {},
559
+ env: req.env,
560
+ cpuCores,
561
+ memoryMb,
562
+ diskSizeGb,
563
+ // Inbound is required for preview URLs (exposePort); outbound for the
564
+ // agents' network. Tenki has no domain-allowlist primitive, so
565
+ // req.networkPolicy 'deny-all' maps to allowOutbound:false, else allow.
566
+ allowInbound: true,
567
+ allowOutbound: req.networkPolicy !== "deny-all",
568
+ maxDurationMs,
569
+ // Marker so list()/prune can filter to agentbox-managed sessions.
570
+ metadata: { agentbox: "true", "agentbox.name": safeName(req.name) },
571
+ tags: ["agentbox"],
572
+ // Return as soon as the session exists; we own the wait below so a
573
+ // readiness timeout can't lose the id.
574
+ waitReady: false
575
+ })
576
+ );
577
+ try {
578
+ await session.waitReady(PROVISION_TIMEOUT_MS);
579
+ } catch (err) {
580
+ await session.close().catch(() => void 0);
581
+ throw err;
582
+ }
583
+ log2(`tenki: created session ${session.id} (${req.snapshot ? "snapshot" : "image"})`);
584
+ return { sandboxId: session.id };
585
+ },
586
+ async get(sandboxId) {
587
+ return withTenkiRetry({ method: "get", retryOnAmbiguous: true }, async () => {
588
+ try {
589
+ await getTenkiClient().get(sandboxId);
590
+ return { sandboxId };
591
+ } catch (err) {
592
+ if (isGone(err)) return null;
593
+ throw err;
594
+ }
595
+ });
596
+ },
597
+ async list() {
598
+ return withTenkiRetry({ method: "list", retryOnAmbiguous: true }, async () => {
599
+ const sessions = await getTenkiClient().list();
600
+ const out = [];
601
+ for (const s of sessions) {
602
+ if (s.metadata?.["agentbox"] !== "true") continue;
603
+ const summary = { sandboxId: s.id, state: mapState(s.state) };
604
+ const friendly = s.metadata?.["agentbox.name"] ?? s.name;
605
+ if (friendly) summary.name = friendly;
606
+ out.push(summary);
607
+ }
608
+ return out;
609
+ });
610
+ },
611
+ async start(h) {
612
+ await withTenkiRetry(
613
+ { method: "start", retryOnAmbiguous: true, attemptTimeoutMs: PROVISION_TIMEOUT_MS },
614
+ async () => {
615
+ const session = await resolveSession(h);
616
+ await ensureLive(session);
617
+ }
618
+ );
619
+ },
620
+ // No separate "stop VM, keep record" in the high-level SDK — pause IS the
621
+ // cold-storage state (it captures a pause snapshot). stop ≡ pause.
622
+ async stop(h) {
623
+ await this.pause(h);
624
+ },
625
+ async pause(h) {
626
+ await withTenkiRetry(
627
+ { method: "pause", retryOnAmbiguous: true, attemptTimeoutMs: 12e4 },
628
+ async () => {
629
+ const session = await resolveSession(h);
630
+ if (session.state === "PAUSED" || session.state === "PAUSING" || session.state === "USER_SHUTDOWN")
631
+ return;
632
+ await session.pause();
633
+ }
634
+ );
635
+ },
636
+ async resume(h) {
637
+ await this.start(h);
638
+ },
639
+ async destroy(h) {
640
+ await withTenkiRetry(
641
+ { method: "destroy", retryOnAmbiguous: true, attemptTimeoutMs: 12e4 },
642
+ async () => {
643
+ try {
644
+ const session = await resolveSession(h);
645
+ await session.close();
646
+ } catch (err) {
647
+ if (isGone(err)) return;
648
+ throw err;
649
+ }
650
+ }
651
+ );
652
+ try {
653
+ rmSync(sshKeyDir(h.sandboxId), { recursive: true, force: true });
654
+ } catch {
655
+ }
656
+ },
657
+ async state(h) {
658
+ return withTenkiRetry({ method: "state", retryOnAmbiguous: true }, async () => {
659
+ try {
660
+ const session = await resolveSession(h);
661
+ return mapState(session.state);
662
+ } catch (err) {
663
+ if (isGone(err)) return "missing";
664
+ throw err;
665
+ }
666
+ });
667
+ },
668
+ // The host keepalive loop calls this while the agent is active. `extend` is
669
+ // ADDITIVE (extends by N ms), and the SDK doesn't expose the remaining time
670
+ // readably, so we extend by `target - current` (the host owns the deadline
671
+ // bookkeeping, same as vercel). The host only calls when target > current.
672
+ async renewTimeout(h, targetDeadlineEpochMs, currentDeadlineEpochMs) {
673
+ const deltaMs = Math.max(0, targetDeadlineEpochMs - currentDeadlineEpochMs);
674
+ if (deltaMs === 0) return;
675
+ await withTenkiRetry({ method: "renewTimeout", retryOnAmbiguous: true }, async () => {
676
+ const session = await resolveSession(h);
677
+ await session.extend(deltaMs);
678
+ });
679
+ },
680
+ async exec(h, cmd, opts) {
681
+ const timeoutMs = opts?.attemptTimeoutMs ?? 3e5;
682
+ return withTenkiRetry(
683
+ {
684
+ method: "exec",
685
+ retryOnAmbiguous: opts?.noRetry ? false : true,
686
+ attemptTimeoutMs: timeoutMs,
687
+ backoffMs: opts?.noRetry ? [] : void 0
688
+ },
689
+ async () => {
690
+ const session = await resolveSession(h);
691
+ await ensureLive(session);
692
+ const r = await session.run(["bash", "-c", cmd], {
693
+ ...opts?.cwd !== void 0 ? { cwd: opts.cwd } : {},
694
+ ...opts?.env !== void 0 ? { env: opts.env } : {},
695
+ ...opts?.user === "root" ? { privileged: true } : {}
696
+ });
697
+ return {
698
+ exitCode: r.exitCode,
699
+ stdout: decoder.decode(r.stdout),
700
+ stderr: decoder.decode(r.stderr)
701
+ };
702
+ }
703
+ );
704
+ },
705
+ async uploadFile(h, localPath, remotePath) {
706
+ await withTenkiRetry(
707
+ { method: "uploadFile", retryOnAmbiguous: true, attemptTimeoutMs: 3e5 },
708
+ async () => {
709
+ const session = await resolveSession(h);
710
+ await ensureLive(session);
711
+ const workdir = await resolveGuestWorkdir(session);
712
+ const web = () => Readable.toWeb(createReadStream(localPath));
713
+ if (isUnderWorkdir(remotePath, workdir)) {
714
+ await session.writeFileStream(remotePath, web(), { mode: 420 });
715
+ return;
716
+ }
717
+ const stage = stageName("upload");
718
+ await session.writeFileStream(stage, web(), { mode: 420 });
719
+ await moveStagedTo(session, stage, remotePath);
720
+ }
721
+ );
722
+ },
723
+ async downloadFile(h, remotePath, localPath) {
724
+ await withTenkiRetry(
725
+ { method: "downloadFile", retryOnAmbiguous: true, attemptTimeoutMs: 3e5 },
726
+ async () => {
727
+ const session = await resolveSession(h);
728
+ await ensureLive(session);
729
+ const workdir = await resolveGuestWorkdir(session);
730
+ const sink = (web) => pipeline(Readable.fromWeb(web), createWriteStream(localPath));
731
+ if (isUnderWorkdir(remotePath, workdir)) {
732
+ const web = await session.readFileStream(
733
+ remotePath
734
+ );
735
+ await sink(web);
736
+ return;
737
+ }
738
+ const stage = stageName("download");
739
+ await copyIntoStage(session, remotePath, stage);
740
+ try {
741
+ const web = await session.readFileStream(
742
+ stage
743
+ );
744
+ await sink(web);
745
+ } finally {
746
+ await session.remove(stage).catch(() => void 0);
747
+ }
748
+ }
749
+ );
750
+ },
751
+ async listFiles(h, remoteDir) {
752
+ return withTenkiRetry({ method: "listFiles", retryOnAmbiguous: true }, async () => {
753
+ const session = await resolveSession(h);
754
+ await ensureLive(session);
755
+ const entries = await session.list(remoteDir);
756
+ return entries.map((e) => ({ name: basename(e.path), isDir: e.isDir }));
757
+ });
758
+ },
759
+ async previewUrl(h, port) {
760
+ return withTenkiRetry({ method: "previewUrl", retryOnAmbiguous: true }, async () => {
761
+ const session = await resolveSession(h);
762
+ const shares = await session.listExposedPorts();
763
+ const existing = findPermanentShare(shares, port);
764
+ if (existing) return { url: existing.previewUrl };
765
+ if (shares.some((p) => p.port === port)) {
766
+ await session.unexposePort(port).catch(() => void 0);
767
+ }
768
+ const exposed = await session.exposePort(port, { slug: previewSlug(h.sandboxId, port) });
769
+ return { url: exposed.previewUrl };
770
+ });
771
+ },
772
+ // Tenki preview URLs are public + browser-usable; expiry maps to ttlMs.
773
+ // NOTE: the API rejects `slug` + `ttlMs` together (`expires_at is not
774
+ // supported when slug is set`), so an expiring share can't carry our stable
775
+ // slug — we mint a server-assigned URL instead. Re-exposing therefore churns
776
+ // the URL, which is inherent to an expiring (signed) share.
777
+ async signedPreviewUrl(h, port, expiresInSeconds) {
778
+ return withTenkiRetry({ method: "signedPreviewUrl", retryOnAmbiguous: true }, async () => {
779
+ const session = await resolveSession(h);
780
+ const ttlMs = Math.max(1, expiresInSeconds) * 1e3;
781
+ const shares = await session.listExposedPorts();
782
+ const existing = findExpiringShare(shares, port, ttlMs, Date.now());
783
+ if (existing) return { url: existing.previewUrl };
784
+ if (shares.some((p) => p.port === port)) {
785
+ await session.unexposePort(port).catch(() => void 0);
786
+ }
787
+ const exposed = await session.exposePort(port, { ttlMs });
788
+ return { url: exposed.previewUrl };
789
+ });
790
+ },
791
+ // Re-mint a share when the cached URL stops responding (host poller).
792
+ async refreshPreviewUrl(h, port) {
793
+ return withTenkiRetry({ method: "refreshPreviewUrl", retryOnAmbiguous: true }, async () => {
794
+ const session = await resolveSession(h);
795
+ const prior = (await session.listExposedPorts()).find((p) => p.port === port);
796
+ const priorRemainingMs = prior?.expiresAt === void 0 ? void 0 : prior.expiresAt.getTime() - Date.now();
797
+ try {
798
+ await session.unexposePort(port);
799
+ } catch {
800
+ }
801
+ if (priorRemainingMs !== void 0) {
802
+ const exposed2 = await session.exposePort(port, {
803
+ ttlMs: Math.max(REFRESH_MIN_TTL_MS, priorRemainingMs)
804
+ });
805
+ return { url: exposed2.previewUrl };
806
+ }
807
+ const exposed = await session.exposePort(port, { slug: previewSlug(h.sandboxId, port) });
808
+ return { url: exposed.previewUrl };
809
+ });
810
+ },
811
+ /**
812
+ * Probe whether a snapshot id is still bootable. Returns false on any lookup
813
+ * failure (treated by the cloud-provider as "gone" so it falls back to a
814
+ * from-base boot rather than erroring the user).
815
+ */
816
+ async snapshotExists(snapshotName) {
817
+ return withTenkiRetry({ method: "snapshotExists", retryOnAmbiguous: true }, async () => {
818
+ try {
819
+ const snap = await getTenkiClient().getSnapshot(snapshotName);
820
+ return snap.state === "READY";
821
+ } catch {
822
+ return false;
823
+ }
824
+ });
825
+ }
826
+ };
827
+
828
+ // src/prepare.ts
829
+ import {
830
+ lstatSync,
831
+ mkdirSync as mkdirSync3,
832
+ mkdtempSync,
833
+ readdirSync,
834
+ rmSync as rmSync2,
835
+ statSync,
836
+ symlinkSync
837
+ } from "fs";
838
+ import { homedir as homedir5, tmpdir } from "os";
839
+ import { join as join2 } from "path";
840
+ import {
841
+ readCliStamp,
842
+ resolveSharedRuntimeAsset,
843
+ stageClaudeStaticForUpload,
844
+ stageCodexStaticForUpload,
845
+ stageOpencodeStaticForUpload,
846
+ UserFacingError as UserFacingError4
847
+ } from "@madarco/agentbox-provider-sdk";
848
+
849
+ // src/credentials.ts
850
+ import { homedir as homedir4 } from "os";
851
+ import { resolve as resolve4 } from "path";
852
+ import { hostOpenCommand } from "@madarco/agentbox-provider-sdk";
853
+ import { cancel, confirm, intro, isCancel, log, note, outro, password } from "@clack/prompts";
854
+
855
+ // src/secrets.ts
856
+ import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
857
+ import { homedir as homedir3 } from "os";
858
+ import { dirname as dirname2, resolve as resolve3 } from "path";
859
+ function secretsEnvPath() {
860
+ return resolve3(homedir3(), ".agentbox", "secrets.env");
861
+ }
862
+ function writeManagedSecrets(managedKeys, record) {
863
+ for (const k of managedKeys) delete process.env[k];
864
+ for (const [k, v] of Object.entries(record)) process.env[k] = v;
865
+ const path = secretsEnvPath();
866
+ mkdirSync2(dirname2(path), { recursive: true });
867
+ let existing = "";
868
+ if (existsSync3(path)) {
869
+ try {
870
+ existing = readFileSync3(path, "utf8");
871
+ } catch {
872
+ existing = "";
873
+ }
874
+ }
875
+ const kept = existing.split(/\r?\n/).filter((line) => {
876
+ const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
877
+ const eq = stripped.indexOf("=");
878
+ if (eq <= 0) return true;
879
+ return !managedKeys.includes(stripped.slice(0, eq).trim());
880
+ }).join("\n").replace(/\s+$/u, "");
881
+ const lines = Object.entries(record).map(([k, v]) => `${k}=${v}`);
882
+ const body = (kept ? `${kept}
883
+ ` : "") + lines.join("\n") + "\n";
884
+ const tmp = `${path}.tmp`;
885
+ writeFileSync2(tmp, body, { mode: 384 });
886
+ try {
887
+ chmodSync2(tmp, 384);
888
+ } catch {
889
+ }
890
+ renameSync(tmp, path);
891
+ try {
892
+ chmodSync2(path, 384);
893
+ } catch {
894
+ }
895
+ }
896
+
897
+ // src/credentials.ts
898
+ function exitOnCancel(v) {
899
+ if (isCancel(v)) {
900
+ cancel("Cancelled.");
901
+ process.exit(130);
902
+ }
903
+ return v;
904
+ }
905
+ var DASHBOARD_URL = "https://tenki.cloud";
906
+ var DOCS_URL = "https://tenki.cloud/docs";
907
+ var MANAGED_KEYS = ["TENKI_AUTH_TOKEN"];
908
+ async function ensureTenkiCredentials(opts = {}) {
909
+ ensureTenkiEnvLoaded();
910
+ if (!opts.force && hasUsableCredentials()) return;
911
+ if (!process.stdin.isTTY) return;
912
+ intro("Tenki setup");
913
+ note(
914
+ `AgentBox needs a Tenki workspace auth token to provision sandboxes.
915
+ Create one in your Tenki dashboard (${DASHBOARD_URL}; see ${DOCS_URL}), then paste it below.
916
+ The token is stored in \`~/.agentbox/secrets.env\` (mode 0600) \u2014 no .env.local harvesting.`,
917
+ "Credentials required"
918
+ );
919
+ const openIt = exitOnCancel(
920
+ await confirm({
921
+ message: `Open ${DASHBOARD_URL} to create a token?`,
922
+ initialValue: true
923
+ })
924
+ );
925
+ if (openIt) openDashboard();
926
+ const token = exitOnCancel(
927
+ await password({
928
+ message: "Paste your Tenki auth token (tk_\u2026)",
929
+ validate: (v) => v && v.trim().length > 0 ? void 0 : "Cannot be empty"
930
+ })
931
+ );
932
+ persistCredentials({ authToken: token.trim() });
933
+ reloadTenkiEnv();
934
+ log.success(`Tenki credentials saved to ${secretsPath()}`);
935
+ outro("Setup complete.");
936
+ }
937
+ function persistCredentials(creds) {
938
+ writeManagedSecrets(MANAGED_KEYS, { TENKI_AUTH_TOKEN: creds.authToken });
939
+ }
940
+ function setTenkiCredentials(fields) {
941
+ const token = (fields.token ?? "").trim();
942
+ if (!token) {
943
+ return { ok: false, error: "token is required", status: { configured: false } };
944
+ }
945
+ persistCredentials({ authToken: token });
946
+ const cred = readTenkiCredStatus();
947
+ return { ok: true, status: { configured: cred.auth !== "none", label: cred.auth } };
948
+ }
949
+ function openDashboard() {
950
+ import("child_process").then(({ spawnSync }) => {
951
+ const r = spawnSync(hostOpenCommand(), [DASHBOARD_URL], { stdio: "ignore" });
952
+ if (r.status !== 0) {
953
+ log.warn(`Could not auto-open the browser \u2014 visit ${DASHBOARD_URL} manually.`);
954
+ }
955
+ }).catch(() => {
956
+ log.warn(`Could not auto-open the browser \u2014 visit ${DASHBOARD_URL} manually.`);
957
+ });
958
+ }
959
+ function secretsPath() {
960
+ return resolve4(homedir4(), ".agentbox", "secrets.env");
961
+ }
962
+ function readTenkiCredStatus() {
963
+ const shellHad = process.env.TENKI_AUTH_TOKEN !== void 0;
964
+ ensureTenkiEnvLoaded();
965
+ const token = process.env.TENKI_AUTH_TOKEN;
966
+ if (!token) return { auth: "none", source: "none" };
967
+ return { auth: "token", token, source: shellHad ? "env" : "secrets.env" };
968
+ }
969
+ function maskKey(value) {
970
+ if (value.length <= 8) return "*".repeat(value.length);
971
+ return `${value.slice(0, 4)}\u2026${"*".repeat(8)}${value.slice(-4)}`;
972
+ }
973
+
974
+ // src/prepare.ts
975
+ var BUILDER_CPU = 2;
976
+ var BUILDER_MEMORY_MB = 4096;
977
+ var BUILDER_DISK_GB = 20;
978
+ var BUILDER_MAX_DURATION_MS = 45 * 6e4;
979
+ var BUILDER_READY_TIMEOUT_MS = 24e4;
980
+ var BOX_USER = "tenki";
981
+ var BOX_HOME = `/home/${BOX_USER}`;
982
+ var BOX_DIRS = [
983
+ "/workspace",
984
+ "/run/agentbox",
985
+ "/var/log/agentbox",
986
+ "/var/lib/agentbox",
987
+ "/etc/agentbox",
988
+ "/etc/claude-code",
989
+ "/usr/local/share/agentbox"
990
+ ];
991
+ var OWNED_PATHS = [
992
+ BOX_HOME,
993
+ "/workspace",
994
+ "/run/agentbox",
995
+ "/var/log/agentbox",
996
+ "/var/lib/agentbox"
997
+ ];
998
+ var PREPARE_ASSETS = [
999
+ { shared: "ctl.cjs", dest: "/usr/local/bin/agentbox-ctl", mode: "0755" },
1000
+ { shared: "agentbox-vnc-start", dest: "/usr/local/bin/agentbox-vnc-start", mode: "0755" },
1001
+ { shared: "agentbox-dockerd-start", dest: "/usr/local/bin/agentbox-dockerd-start", mode: "0755" },
1002
+ {
1003
+ shared: "agentbox-portless-trust",
1004
+ dest: "/usr/local/bin/agentbox-portless-trust",
1005
+ mode: "0755"
1006
+ },
1007
+ {
1008
+ shared: "agentbox-checkpoint-cleanup",
1009
+ dest: "/usr/local/bin/agentbox-checkpoint-cleanup",
1010
+ mode: "0755"
1011
+ },
1012
+ { shared: "agentbox-open", dest: "/usr/local/bin/agentbox-open", mode: "0755" },
1013
+ { shared: "gh-shim", dest: "/usr/local/bin/gh", mode: "0755" },
1014
+ { shared: "git-shim", dest: "/usr/local/bin/git", mode: "0755" },
1015
+ { shared: "ntn-shim", dest: "/usr/local/bin/ntn", mode: "0755" },
1016
+ { shared: "linear-shim", dest: "/usr/local/bin/linear", mode: "0755" },
1017
+ {
1018
+ shared: "claude-managed-settings.json",
1019
+ dest: "/etc/claude-code/managed-settings.json",
1020
+ mode: "0644"
1021
+ },
1022
+ {
1023
+ shared: "agentbox-codex-hooks.json",
1024
+ dest: "/usr/local/share/agentbox/codex-hooks.json",
1025
+ mode: "0644"
1026
+ },
1027
+ {
1028
+ shared: "agentbox-setup-skill.md",
1029
+ dest: "/usr/local/share/agentbox/setup-guide.md",
1030
+ mode: "0644"
1031
+ },
1032
+ {
1033
+ shared: "opencode-agentbox-plugin.js",
1034
+ dest: "/usr/local/share/agentbox/opencode-agentbox-plugin.js",
1035
+ mode: "0644"
1036
+ }
1037
+ ];
1038
+ function stagedAssetPath(shared) {
1039
+ return `/tmp/agentbox-stage-${shared}`;
1040
+ }
1041
+ var AGENT_PACKAGES = ["@anthropic-ai/claude-code", "@openai/codex", "opencode-ai"];
1042
+ var PROFILE_SH = `# Auto-loaded by login shells; box.env is written at create time.
1043
+ if [ -r /etc/agentbox/box.env ]; then
1044
+ set -a
1045
+ . /etc/agentbox/box.env
1046
+ set +a
1047
+ fi
1048
+ case ":$PATH:" in
1049
+ *:${BOX_HOME}/.local/bin:*) : ;;
1050
+ *) PATH=${BOX_HOME}/.local/bin:$PATH ;;
1051
+ esac
1052
+ export PATH
1053
+ export COLORTERM=\${COLORTERM:-truecolor}
1054
+ export DISABLE_AUTOUPDATER=\${DISABLE_AUTOUPDATER:-1}
1055
+ export BROWSER=\${BROWSER:-/usr/local/bin/agentbox-open}
1056
+ `;
1057
+ function buildInstallPlan() {
1058
+ const quoted = (paths) => paths.map((p) => `"${p}"`).join(" ");
1059
+ return [
1060
+ {
1061
+ name: "apt-packages",
1062
+ root: true,
1063
+ timeoutMs: 6e5,
1064
+ // tmux: the one hard dependency the base image lacks, and every
1065
+ // interactive attach runs inside a tmux session.
1066
+ //
1067
+ // docker.io: in-box Docker. Verified working on a Tenki microVM — cgroup2,
1068
+ // native overlay2 (not the slow vfs fallback), and the shared
1069
+ // `agentbox-dockerd-start` launcher brings the daemon up. Containers do
1070
+ // not need nested virtualization, so the absence of /dev/kvm is irrelevant.
1071
+ command: [
1072
+ "set -euo pipefail",
1073
+ "export DEBIAN_FRONTEND=noninteractive",
1074
+ "apt-get update -qq",
1075
+ "apt-get install -y --no-install-recommends tmux docker.io",
1076
+ // The agent runs unprivileged. Login shells (attach, tmux) reach the
1077
+ // daemon through this group; Tenki's guest agent starts its exec
1078
+ // processes without supplementary groups, so those rely instead on the
1079
+ // socket mode the dockerd launcher sets. Both paths are covered.
1080
+ `usermod -aG docker ${BOX_USER}`
1081
+ ].join("\n")
1082
+ },
1083
+ {
1084
+ name: "box-dirs",
1085
+ root: true,
1086
+ command: `set -euo pipefail
1087
+ mkdir -p ${quoted(BOX_DIRS)}
1088
+ chmod 755 /workspace`
1089
+ },
1090
+ {
1091
+ name: "vscode-user",
1092
+ root: true,
1093
+ // AgentBox's credential seed (as inlined in the published provider SDK)
1094
+ // extracts with an in-shell `sudo -u vscode`, so the box needs a real
1095
+ // `vscode` account or the seed dies with "sudo: unknown user vscode" and
1096
+ // every box starts logged out. We add it as an ALIAS of the box user
1097
+ // (same uid/gid, same home, `-o` to permit the duplicate uid) rather than
1098
+ // a separate account: files then land owned by the user the agents
1099
+ // actually run as, and Tenki's own guest agent keeps its identity.
1100
+ //
1101
+ // The CLI has since switched to `backend.exec(…, { user })` with no
1102
+ // in-shell sudo; this step stays harmless once the SDK catches up.
1103
+ command: [
1104
+ "set -euo pipefail",
1105
+ "if ! id vscode >/dev/null 2>&1; then",
1106
+ ` useradd -o -u "$(id -u ${BOX_USER})" -g "$(id -g ${BOX_USER})" -d ${BOX_HOME} -s /bin/bash vscode`,
1107
+ "fi",
1108
+ "id vscode >/dev/null"
1109
+ ].join("\n")
1110
+ },
1111
+ {
1112
+ name: "vscode-home-link",
1113
+ root: true,
1114
+ // agentbox-ctl hardcodes /home/vscode/... credential paths and the profile
1115
+ // shim puts /home/vscode/.local/bin on PATH. A symlink satisfies both
1116
+ // without renaming the platform's own user.
1117
+ command: `set -euo pipefail
1118
+ ln -sfn ${BOX_HOME} /home/vscode`
1119
+ },
1120
+ {
1121
+ name: "runtime-assets",
1122
+ root: true,
1123
+ command: [
1124
+ "set -euo pipefail",
1125
+ ...PREPARE_ASSETS.map(
1126
+ ({ shared, dest, mode }) => `install -D -m ${mode} "${stagedAssetPath(shared)}" "${dest}"`
1127
+ ),
1128
+ // `xdg-open` is what agents and browsers actually invoke; route it to the
1129
+ // relay-backed opener so a link opens on the *host*, not in the box.
1130
+ "ln -sf /usr/local/bin/agentbox-open /usr/local/bin/xdg-open",
1131
+ `rm -f ${quoted(PREPARE_ASSETS.map((a) => stagedAssetPath(a.shared)))}`
1132
+ ].join("\n")
1133
+ },
1134
+ {
1135
+ name: "agent-dirs",
1136
+ // Runs as the box user so its own home comes out correctly owned.
1137
+ //
1138
+ // The credential PIVOT is the point of this step. AgentBox's cloud scaffold
1139
+ // seeds per-agent credentials at CREATE time — for a backend with no volume
1140
+ // primitive (us, like vercel/e2b/hetzner) it uploads them straight into
1141
+ // `~/.agentbox-creds/<agent>/`. Those symlinks are what make the agents
1142
+ // actually read them; without the layout baked here, the seed lands in a
1143
+ // directory nothing consults and every box starts logged out.
1144
+ command: [
1145
+ "set -euo pipefail",
1146
+ `mkdir -p ${BOX_HOME}/.claude/skills/agentbox-setup ${BOX_HOME}/.codex ${BOX_HOME}/.local/share/opencode ${BOX_HOME}/.agentbox-creds/claude ${BOX_HOME}/.agentbox-creds/codex ${BOX_HOME}/.agentbox-creds/opencode`,
1147
+ `ln -sf ${BOX_HOME}/.agentbox-creds/claude/.credentials.json ${BOX_HOME}/.claude/.credentials.json`,
1148
+ `ln -sf ${BOX_HOME}/.agentbox-creds/codex/auth.json ${BOX_HOME}/.codex/auth.json`,
1149
+ `ln -sf ${BOX_HOME}/.agentbox-creds/opencode/auth.json ${BOX_HOME}/.local/share/opencode/auth.json`,
1150
+ `ln -sf ${BOX_HOME}/.claude/_claude.json ${BOX_HOME}/.claude.json`,
1151
+ // The in-box first-run wizard the setup prompt refers to.
1152
+ `cp /usr/local/share/agentbox/setup-guide.md ${BOX_HOME}/.claude/skills/agentbox-setup/SKILL.md`
1153
+ ].join("\n")
1154
+ },
1155
+ {
1156
+ name: "coding-agents",
1157
+ // npm's global prefix is writable by the box user in Tenki's base image, so
1158
+ // this needs no elevation — and installing as the box user keeps the
1159
+ // resulting file ownership right.
1160
+ timeoutMs: 18e5,
1161
+ command: `set -euo pipefail
1162
+ npm install -g ${AGENT_PACKAGES.join(" ")}`
1163
+ },
1164
+ {
1165
+ name: "login-shell-env",
1166
+ root: true,
1167
+ command: [
1168
+ "set -euo pipefail",
1169
+ "cat > /etc/profile.d/agentbox.sh <<'AGENTBOX_PROFILE'",
1170
+ PROFILE_SH.trimEnd(),
1171
+ "AGENTBOX_PROFILE",
1172
+ "chmod 0644 /etc/profile.d/agentbox.sh"
1173
+ ].join("\n")
1174
+ },
1175
+ {
1176
+ name: "ownership",
1177
+ root: true,
1178
+ // Last: the steps above wrote as root, and the box runs unprivileged.
1179
+ command: `set -euo pipefail
1180
+ chown -R ${BOX_USER}:${BOX_USER} ${quoted(OWNED_PATHS)}`
1181
+ },
1182
+ {
1183
+ name: "verify",
1184
+ // Fail the bake here rather than pinning a snapshot that cannot supervise a
1185
+ // box. `agentbox-ctl` is the one that really matters.
1186
+ command: [
1187
+ "set -euo pipefail",
1188
+ "command -v tmux >/dev/null",
1189
+ "command -v agentbox-ctl >/dev/null",
1190
+ // In-box docker: the binaries plus the box user's group membership, so a
1191
+ // login shell in the box reaches the daemon without sudo.
1192
+ "command -v docker >/dev/null",
1193
+ "command -v dockerd >/dev/null",
1194
+ `id -nG ${BOX_USER} | tr ' ' '\\n' | grep -qx docker`,
1195
+ "test -x /usr/local/bin/agentbox-ctl",
1196
+ "test -f /etc/claude-code/managed-settings.json",
1197
+ 'for a in claude codex opencode; do command -v "$a" >/dev/null || { echo "agent $a missing" >&2; exit 1; }; done',
1198
+ // The credential pivot must be symlinks pointing into ~/.agentbox-creds,
1199
+ // or the create-time credential seed lands where nothing reads it. A
1200
+ // dangling link is expected and fine here: the target file only appears
1201
+ // when a box is created.
1202
+ `for l in "${BOX_HOME}/.claude/.credentials.json" "${BOX_HOME}/.codex/auth.json" "${BOX_HOME}/.local/share/opencode/auth.json" "${BOX_HOME}/.claude.json"; do test -L "$l" || { echo "credential pivot missing: $l" >&2; exit 1; }; done`,
1203
+ `test -d "${BOX_HOME}/.agentbox-creds/claude"`,
1204
+ // The credential seed runs `sudo -u vscode`; without this the seed fails
1205
+ // silently-ish at create and boxes start unauthenticated.
1206
+ "id vscode >/dev/null",
1207
+ `test "$(id -u vscode)" = "$(id -u ${BOX_USER})"`,
1208
+ `test -f "${BOX_HOME}/.claude/skills/agentbox-setup/SKILL.md"`
1209
+ ].join("\n")
1210
+ }
1211
+ ];
1212
+ }
1213
+ function shadowCodexHome(hostHome) {
1214
+ const source = join2(hostHome, ".codex");
1215
+ try {
1216
+ if (!lstatSync(source).isDirectory()) return null;
1217
+ } catch {
1218
+ return null;
1219
+ }
1220
+ const root = mkdtempSync(join2(tmpdir(), "agentbox-tenki-codex-shadow-"));
1221
+ const mirror = (from, to, depth) => {
1222
+ if (depth > 12) return;
1223
+ mkdirSync3(to, { recursive: true });
1224
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
1225
+ if (entry.name === "auth.json") continue;
1226
+ const src = join2(from, entry.name);
1227
+ const dest = join2(to, entry.name);
1228
+ let stat;
1229
+ try {
1230
+ stat = statSync(src);
1231
+ } catch {
1232
+ continue;
1233
+ }
1234
+ if (stat.isDirectory()) {
1235
+ mirror(src, dest, depth + 1);
1236
+ } else if (stat.isFile()) {
1237
+ try {
1238
+ symlinkSync(src, dest);
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ }
1243
+ };
1244
+ try {
1245
+ mirror(source, join2(root, ".codex"), 0);
1246
+ } catch {
1247
+ rmSync2(root, { recursive: true, force: true });
1248
+ return null;
1249
+ }
1250
+ return { home: root, cleanup: () => rmSync2(root, { recursive: true, force: true }) };
1251
+ }
1252
+ async function bakeAgentStaticConfig(handle, hostWorkspace, progress) {
1253
+ progress("staging host agent static config");
1254
+ const stagings = [];
1255
+ const stageOne = async (kind, stage, dest) => {
1256
+ let tar;
1257
+ try {
1258
+ tar = await stage();
1259
+ } catch (err) {
1260
+ progress(
1261
+ `WARNING: could not stage ${kind} config (${err instanceof Error ? err.message.split("\n")[0] : String(err)}) \u2014 continuing without it`
1262
+ );
1263
+ return;
1264
+ }
1265
+ for (const w of tar.warnings) progress(w);
1266
+ if (tar.tarballPath) stagings.push({ kind, tar, dest });
1267
+ else await tar.cleanup();
1268
+ };
1269
+ try {
1270
+ await stageOne(
1271
+ "claude",
1272
+ () => stageClaudeStaticForUpload(hostWorkspace ? { hostWorkspace } : {}),
1273
+ `${BOX_HOME}/.claude`
1274
+ );
1275
+ const codexShadow = shadowCodexHome(homedir5());
1276
+ try {
1277
+ await stageOne(
1278
+ "codex",
1279
+ () => codexShadow ? stageCodexStaticForUpload({ hostHome: codexShadow.home }) : stageCodexStaticForUpload(),
1280
+ `${BOX_HOME}/.codex`
1281
+ );
1282
+ } finally {
1283
+ codexShadow?.cleanup();
1284
+ }
1285
+ await stageOne(
1286
+ "opencode",
1287
+ () => stageOpencodeStaticForUpload(),
1288
+ `${BOX_HOME}/.local/share/opencode`
1289
+ );
1290
+ if (stagings.length === 0) {
1291
+ progress("no host agent config to bake (boxes will start unconfigured)");
1292
+ return;
1293
+ }
1294
+ for (const st of stagings) {
1295
+ const remote = `/tmp/agentbox-${st.kind}-static.tar.gz`;
1296
+ progress(`baking ${st.kind} static config`);
1297
+ try {
1298
+ await tenkiBackend.uploadFile(handle, st.tar.tarballPath, remote);
1299
+ const extract = [
1300
+ "set -euo pipefail",
1301
+ `mkdir -p "${st.dest}"`,
1302
+ `tar -xzf "${remote}" -C "${st.dest}" --no-same-permissions --no-same-owner -m`,
1303
+ `rm -f "${remote}"`
1304
+ ].join("\n");
1305
+ const r = await tenkiBackend.exec(handle, extract, { attemptTimeoutMs: 3e5 });
1306
+ if (r.exitCode !== 0) {
1307
+ progress(
1308
+ `WARNING: ${st.kind} static config did not extract (exit ${String(r.exitCode)}) \u2014 continuing`
1309
+ );
1310
+ }
1311
+ } catch (err) {
1312
+ progress(
1313
+ `WARNING: ${st.kind} static config failed to bake (${err instanceof Error ? err.message.split("\n")[0] : String(err)}) \u2014 continuing`
1314
+ );
1315
+ }
1316
+ }
1317
+ } finally {
1318
+ for (const st of stagings) await st.tar.cleanup();
1319
+ }
1320
+ }
1321
+ async function snapshotExists(snapshotId) {
1322
+ try {
1323
+ const snap = await getTenkiClient().getSnapshot(snapshotId);
1324
+ return snap.state === void 0 || snap.state === "READY";
1325
+ } catch {
1326
+ return false;
1327
+ }
1328
+ }
1329
+ function snapshotLabel() {
1330
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").replace("Z", "");
1331
+ return `agentbox-base-${stamp}`.toLowerCase();
1332
+ }
1333
+ async function prepareTenki(opts = {}) {
1334
+ await ensureTenkiCredentials();
1335
+ resolveAuthToken();
1336
+ const client = getTenkiClient();
1337
+ const log2 = opts.onLog ?? (() => {
1338
+ });
1339
+ const progress = (s) => log2(`prepare-tenki: ${s}`);
1340
+ const existing = readPreparedState();
1341
+ if (!opts.force && existing.base) {
1342
+ if (await snapshotExists(existing.base.snapshotId)) {
1343
+ progress(
1344
+ `base snapshot ${existing.base.snapshotId} already prepared; skipping (pass --force to rebake)`
1345
+ );
1346
+ return { snapshotName: existing.base.snapshotId };
1347
+ }
1348
+ progress(
1349
+ `recorded base snapshot ${existing.base.snapshotId} no longer exists in this workspace; re-baking`
1350
+ );
1351
+ }
1352
+ const assets = PREPARE_ASSETS.map((asset) => ({
1353
+ ...asset,
1354
+ localPath: resolveSharedRuntimeAsset(asset.shared)
1355
+ }));
1356
+ const baseImage = opts.baseImage ?? process.env.AGENTBOX_TENKI_BASE_IMAGE;
1357
+ const workspaceId = opts.workspaceId ?? process.env.AGENTBOX_TENKI_WORKSPACE_ID;
1358
+ progress("booting builder sandbox");
1359
+ const builder = await client.create({
1360
+ name: "agentbox-prepare",
1361
+ ...baseImage ? { image: baseImage } : {},
1362
+ ...workspaceId ? { workspaceId } : {},
1363
+ cpuCores: opts.cpuCount ?? BUILDER_CPU,
1364
+ memoryMb: opts.memoryMB ?? BUILDER_MEMORY_MB,
1365
+ diskSizeGb: opts.diskSizeGB ?? BUILDER_DISK_GB,
1366
+ maxDurationMs: BUILDER_MAX_DURATION_MS,
1367
+ allowOutbound: true,
1368
+ // The builder serves no traffic; it only needs egress for apt and npm.
1369
+ allowInbound: false,
1370
+ waitReady: true,
1371
+ waitTimeoutMs: BUILDER_READY_TIMEOUT_MS,
1372
+ // Marker so `list`/prune can tell an in-flight bake from a real box.
1373
+ metadata: { agentbox: "true", "agentbox.role": "prepare" }
1374
+ });
1375
+ const handle = { sandboxId: builder.id };
1376
+ progress(`builder ${builder.id} ready`);
1377
+ try {
1378
+ progress(`uploading ${String(assets.length)} runtime assets`);
1379
+ for (const asset of assets) {
1380
+ await tenkiBackend.uploadFile(handle, asset.localPath, stagedAssetPath(asset.shared));
1381
+ }
1382
+ const plan = buildInstallPlan();
1383
+ for (const [index, step] of plan.entries()) {
1384
+ progress(`[${String(index + 1)}/${String(plan.length)}] ${step.name}`);
1385
+ const result = await tenkiBackend.exec(handle, step.command, {
1386
+ ...step.root ? { user: "root" } : {},
1387
+ ...step.timeoutMs ? { attemptTimeoutMs: step.timeoutMs } : {}
1388
+ });
1389
+ if (result.exitCode !== 0) {
1390
+ const detail = (result.stderr || result.stdout || "").trim().split("\n").slice(-12).join("\n");
1391
+ throw new UserFacingError4(
1392
+ `tenki prepare: step '${step.name}' failed (exit ${String(result.exitCode)})` + (detail ? `:
1393
+ ${detail}` : "")
1394
+ );
1395
+ }
1396
+ }
1397
+ await bakeAgentStaticConfig(handle, opts.hostWorkspace, progress);
1398
+ progress("snapshotting the builder (this takes a minute)");
1399
+ const label = snapshotLabel();
1400
+ const snapshot = await client.createSnapshotAndWait(builder.id, { name: label });
1401
+ const cliStamp = readCliStamp();
1402
+ writePreparedState({
1403
+ schema: 2,
1404
+ base: {
1405
+ snapshotId: snapshot.id,
1406
+ snapshotName: label,
1407
+ cliVersion: cliStamp.cliVersion,
1408
+ cliCommit: cliStamp.cliCommit,
1409
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1410
+ }
1411
+ });
1412
+ progress(`wrote ${preparedStatePath()}`);
1413
+ progress(`prepare complete \u2014 base snapshot ${snapshot.id}`);
1414
+ return { snapshotName: snapshot.id };
1415
+ } finally {
1416
+ try {
1417
+ const session = await client.get(builder.id);
1418
+ await session.close();
1419
+ progress(`builder ${builder.id} destroyed`);
1420
+ } catch (err) {
1421
+ progress(
1422
+ `WARNING: could not destroy builder ${builder.id} \u2014 terminate it manually: ` + (err instanceof Error ? err.message : String(err))
1423
+ );
1424
+ }
1425
+ }
1426
+ }
1427
+ var prepareTenkiProvider = (req) => prepareTenki({
1428
+ name: req.name,
1429
+ hostWorkspace: req.hostWorkspace ?? process.cwd(),
1430
+ force: req.force,
1431
+ // `--size` sizes the BOX at create time (the backend applies cpu/memory/disk
1432
+ // per session), so it deliberately does not size the builder here.
1433
+ onLog: req.onLog
1434
+ });
1435
+
1436
+ // src/provider-module.ts
1437
+ import {
1438
+ errSummary
1439
+ } from "@madarco/agentbox-provider-sdk";
1440
+ function readCredStatusSummary() {
1441
+ const cred = readTenkiCredStatus();
1442
+ return { configured: cred.auth !== "none", label: cred.auth };
1443
+ }
1444
+ async function doctorChecks() {
1445
+ try {
1446
+ const cred = readTenkiCredStatus();
1447
+ const credRes = cred.auth === "none" ? {
1448
+ label: "credentials",
1449
+ status: "warn",
1450
+ detail: "not configured",
1451
+ // No `tenki login` command exists for a plugin provider: point at
1452
+ // the two places the token is actually read from. An interactive
1453
+ // `create` also prompts for it.
1454
+ hint: `set TENKI_AUTH_TOKEN in the environment or in ${secretsPath()}`
1455
+ } : {
1456
+ label: "credentials",
1457
+ status: "ok",
1458
+ detail: `${cred.token ? maskKey(cred.token) : "token"} (${cred.source})`
1459
+ };
1460
+ const prepared = readPreparedState();
1461
+ const baseRes = prepared.base?.snapshotId ? {
1462
+ label: "base image",
1463
+ status: "ok",
1464
+ // Snapshot ids are long opaque UUIDs; show a short prefix plus the CLI
1465
+ // version that baked it, which is what actually tells you if it's stale.
1466
+ detail: `${prepared.base.snapshotId.slice(0, 18)}\u2026 (${prepared.base.cliVersion ?? "\u2014"})`
1467
+ } : {
1468
+ label: "base image",
1469
+ status: "warn",
1470
+ detail: "not prepared",
1471
+ hint: "`agentbox prepare --provider tenki`"
1472
+ };
1473
+ return [credRes, baseRes];
1474
+ } catch (err) {
1475
+ return [{ label: "credentials", status: "warn", detail: errSummary(err) }];
1476
+ }
1477
+ }
1478
+
1479
+ // src/index.ts
1480
+ var PROVIDER_NAME = "tenki";
1481
+ var BACKEND_NAME = PROVIDER_NAME;
1482
+ var cloudProvider = createCloudProvider(tenkiBackend, {
1483
+ defaultResources: { cpu: 2, memory: 4, disk: 8 },
1484
+ launchDockerd: true
1485
+ });
1486
+ function envTimeoutMs() {
1487
+ const raw = process.env.AGENTBOX_TENKI_TIMEOUT_MS?.trim();
1488
+ if (!raw) return void 0;
1489
+ const n = Number(raw);
1490
+ return Number.isFinite(n) && n > 0 ? n : void 0;
1491
+ }
1492
+ function createTenkiBox(req) {
1493
+ const requested = req.providerOptions?.["timeoutMs"];
1494
+ const timeoutMs = typeof requested === "number" && requested > 0 ? requested : envTimeoutMs() ?? DEFAULT_MAX_DURATION_MS;
1495
+ return cloudProvider.create({
1496
+ ...req,
1497
+ providerOptions: { ...req.providerOptions, timeoutMs }
1498
+ });
1499
+ }
1500
+ async function createTenkiSnapshot(sessionId, name) {
1501
+ return withTenkiRetry(
1502
+ { method: "createSnapshot", retryOnAmbiguous: false, attemptTimeoutMs: 9e5, backoffMs: [] },
1503
+ async () => {
1504
+ const snap = await getTenkiClient().createSnapshotAndWait(sessionId, { name });
1505
+ return snap.id;
1506
+ }
1507
+ );
1508
+ }
1509
+ async function deleteTenkiSnapshot(snapshotId) {
1510
+ await withTenkiRetry({ method: "deleteSnapshot", retryOnAmbiguous: true }, async () => {
1511
+ try {
1512
+ await getTenkiClient().deleteSnapshot(snapshotId);
1513
+ } catch (err) {
1514
+ const name = err instanceof Error ? err.name : "";
1515
+ const msg = err instanceof Error ? err.message : String(err);
1516
+ if (name === "SnapshotNotFoundError" || /not.?found|404/i.test(msg)) return;
1517
+ throw err;
1518
+ }
1519
+ });
1520
+ }
1521
+ function snapshotLabel2(boxName, checkpointName) {
1522
+ const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
1523
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").replace("Z", "");
1524
+ return `agentbox-${sanitize(boxName)}-${sanitize(checkpointName)}-${sanitize(ts)}`;
1525
+ }
1526
+ var tenkiCheckpoint = {
1527
+ async create(box, name) {
1528
+ if (!box.projectRoot) {
1529
+ throw new Error(
1530
+ "cloud checkpoint requires the box to have a project root (run `agentbox checkpoint` from inside the project)"
1531
+ );
1532
+ }
1533
+ if (!box.cloud?.sandboxId) {
1534
+ throw new Error(`tenki box ${box.name} has no sandboxId \u2014 record is malformed`);
1535
+ }
1536
+ const label = snapshotLabel2(box.name, name);
1537
+ const snapshotId = await createTenkiSnapshot(box.cloud.sandboxId, label);
1538
+ const info = await writeCloudCheckpointManifest(box.projectRoot, BACKEND_NAME, name, {
1539
+ snapshotName: snapshotId,
1540
+ sourceBoxId: box.id,
1541
+ sourceBoxName: box.name,
1542
+ baseProvider: BACKEND_NAME,
1543
+ baseFingerprint: currentCloudBaseFingerprint(BACKEND_NAME),
1544
+ cliVersion: readCliStamp2().cliVersion
1545
+ });
1546
+ return { ref: info.name };
1547
+ },
1548
+ async list(projectRoot) {
1549
+ const entries = await listCloudCheckpoints(projectRoot, BACKEND_NAME);
1550
+ return entries.map((e) => ({ ref: e.name, createdAt: e.manifest.createdAt }));
1551
+ },
1552
+ async remove(projectRoot, ref) {
1553
+ const entry = await resolveCloudCheckpoint(projectRoot, BACKEND_NAME, ref);
1554
+ if (!entry) return;
1555
+ await deleteTenkiSnapshot(entry.manifest.snapshotName);
1556
+ await removeCloudCheckpointDir(projectRoot, BACKEND_NAME, ref);
1557
+ }
1558
+ };
1559
+ var tenkiProvider = {
1560
+ ...cloudProvider,
1561
+ create: createTenkiBox,
1562
+ prepare: prepareTenkiProvider,
1563
+ buildAttach: buildTenkiAttach,
1564
+ checkpoint: tenkiCheckpoint,
1565
+ baseFingerprint: () => currentTenkiBaseFingerprintLive()
1566
+ };
1567
+ var providerModule = {
1568
+ provider: tenkiProvider,
1569
+ backend: tenkiBackend,
1570
+ ensureCredentials: ensureTenkiCredentials,
1571
+ readCredStatus: readCredStatusSummary,
1572
+ setCredentials: (fields) => Promise.resolve(setTenkiCredentials(fields)),
1573
+ currentBaseFingerprintLive: () => currentTenkiBaseFingerprintLive(),
1574
+ doctorChecks
1575
+ };
1576
+ export {
1577
+ PREPARE_ASSETS,
1578
+ PROVIDER_NAME,
1579
+ SDK_API_VERSION,
1580
+ buildInstallPlan,
1581
+ buildTenkiAttach,
1582
+ currentTenkiBaseFingerprintLive,
1583
+ ensureTenkiBaseImage,
1584
+ ensureTenkiCredentials,
1585
+ ensureTenkiEnvLoaded,
1586
+ maskKey,
1587
+ prepareTenki,
1588
+ prepareTenkiProvider,
1589
+ preparedStatePath,
1590
+ providerModule,
1591
+ readPreparedState,
1592
+ readTenkiCredStatus,
1593
+ reloadTenkiEnv,
1594
+ secretsPath,
1595
+ setTenkiCredentials,
1596
+ shadowCodexHome,
1597
+ stagedAssetPath,
1598
+ tenkiBackend,
1599
+ tenkiProvider,
1600
+ updatePreparedState,
1601
+ writePreparedState
1602
+ };
1603
+ //# sourceMappingURL=index.js.map