Remove Daytona cloud provider from codebase (#2261)

Simplify the cloud matrix by removing Daytona. All Daytona-specific code,
scripts, tests, and configuration have been removed. Daytona has been moved
to "Previously Considered" in the Cloud Provider Wishlist (#1183) and can
be revived on community demand.

Closes #2260

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-03-06 15:53:08 -08:00 committed by GitHub
parent 50397f19a3
commit 035e4bf830
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 21 additions and 1578 deletions

View file

@ -193,44 +193,6 @@ describe("DigitalOcean --custom prompts", () => {
});
});
describe("Daytona --custom prompts", () => {
const savedCustom = process.env.SPAWN_CUSTOM;
const savedCpu = process.env.DAYTONA_CPU;
const savedMemory = process.env.DAYTONA_MEMORY;
const savedDisk = process.env.DAYTONA_DISK;
afterEach(() => {
restoreEnv("SPAWN_CUSTOM", savedCustom);
restoreEnv("DAYTONA_CPU", savedCpu);
restoreEnv("DAYTONA_MEMORY", savedMemory);
restoreEnv("DAYTONA_DISK", savedDisk);
});
it("promptSandboxSize should return default without --custom", async () => {
delete process.env.DAYTONA_CPU;
delete process.env.DAYTONA_MEMORY;
delete process.env.DAYTONA_DISK;
delete process.env.SPAWN_CUSTOM;
const { promptSandboxSize, DEFAULT_SANDBOX_SIZE } = await import("../daytona/daytona");
const result = await promptSandboxSize();
expect(result.cpu).toBe(DEFAULT_SANDBOX_SIZE.cpu);
expect(result.memory).toBe(DEFAULT_SANDBOX_SIZE.memory);
expect(result.disk).toBe(DEFAULT_SANDBOX_SIZE.disk);
});
it("promptSandboxSize should respect env vars", async () => {
process.env.DAYTONA_CPU = "4";
process.env.DAYTONA_MEMORY = "8";
process.env.DAYTONA_DISK = "50";
process.env.SPAWN_CUSTOM = "1";
const { promptSandboxSize } = await import("../daytona/daytona");
const result = await promptSandboxSize();
expect(result.cpu).toBe(4);
expect(result.memory).toBe(8);
expect(result.disk).toBe(50);
});
});
/** Helper to restore or delete an env var */
function restoreEnv(key: string, savedValue: string | undefined): void {
if (savedValue !== undefined) {

View file

@ -24,12 +24,10 @@ describe("validateConnectionIP", () => {
it("should accept special sentinel values", () => {
expect(() => validateConnectionIP("sprite-console")).not.toThrow();
expect(() => validateConnectionIP("daytona-sandbox")).not.toThrow();
expect(() => validateConnectionIP("localhost")).not.toThrow();
});
it("should accept valid hostnames", () => {
expect(() => validateConnectionIP("ssh.app.daytona.io")).not.toThrow();
expect(() => validateConnectionIP("example.com")).not.toThrow();
expect(() => validateConnectionIP("sub.domain.example.com")).not.toThrow();
});

View file

@ -68,20 +68,6 @@ export async function cmdConnect(connection: VMConnection): Promise<void> {
);
}
// Handle Daytona sandbox connections
if (connection.ip === "daytona-sandbox" && connection.server_id) {
p.log.step(`Connecting to Daytona sandbox ${pc.bold(connection.server_id)}...`);
return runInteractiveCommand(
"daytona",
[
"ssh",
connection.server_id,
],
"Daytona sandbox connection failed",
`daytona ssh ${connection.server_id}`,
);
}
// Handle SSH connections
p.log.step(`Connecting to ${pc.bold(connection.ip)}...`);
const sshCmd = `ssh ${connection.user}@${connection.ip}`;
@ -175,24 +161,6 @@ export async function cmdEnterAgent(
);
}
// Handle Daytona sandbox connections
if (connection.ip === "daytona-sandbox" && connection.server_id) {
p.log.step(`Entering ${pc.bold(agentName)} on Daytona sandbox ${pc.bold(connection.server_id)}...`);
return runInteractiveCommand(
"daytona",
[
"ssh",
connection.server_id,
"--",
"bash",
"-lc",
remoteCmd,
],
`Failed to enter ${agentName}`,
`daytona ssh ${connection.server_id} -- bash -lc '${remoteCmd}'`,
);
}
// Standard SSH connection with agent launch
p.log.step(`Entering ${pc.bold(agentName)} on ${pc.bold(connection.ip)}...`);
const escapedRemoteCmd = remoteCmd.replace(/'/g, "'\\''");

View file

@ -4,7 +4,6 @@ import type { Manifest } from "../manifest.js";
import * as p from "@clack/prompts";
import pc from "picocolors";
import { authenticate as awsAuthenticate, destroyServer as awsDestroyServer, ensureAwsCli } from "../aws/aws.js";
import { destroyServer as daytonaDestroyServer, ensureDaytonaToken } from "../daytona/daytona.js";
import { destroyServer as doDestroyServer, ensureDoToken } from "../digitalocean/digitalocean.js";
import {
authenticate as gcpAuthenticate,
@ -57,9 +56,6 @@ async function ensureDeleteCredentials(record: SpawnRecord): Promise<void> {
await ensureAwsCli();
await awsAuthenticate();
break;
case "daytona":
await ensureDaytonaToken();
break;
case "sprite":
await ensureSpriteCli();
await ensureSpriteAuthenticated();
@ -163,12 +159,6 @@ async function execDeleteServer(record: SpawnRecord): Promise<boolean> {
await awsDestroyServer(id);
});
case "daytona":
return tryDelete(async () => {
await ensureDaytonaToken();
await daytonaDestroyServer(id);
});
case "sprite":
return tryDelete(async () => {
await ensureSpriteCli();

View file

@ -284,12 +284,7 @@ export async function handleRecordAction(selected: SpawnRecord, manifest: Manife
options.push({
value: "reconnect",
label: "SSH into VM",
hint:
conn.ip === "sprite-console"
? `sprite console -s ${conn.server_name}`
: conn.ip === "daytona-sandbox"
? `daytona ssh ${conn.server_id}`
: `ssh ${conn.user}@${conn.ip}`,
hint: conn.ip === "sprite-console" ? `sprite console -s ${conn.server_name}` : `ssh ${conn.user}@${conn.ip}`,
});
}

View file

@ -1,9 +0,0 @@
// daytona/agents.ts — Daytona agent configs (thin wrapper over shared)
import { createCloudAgents } from "../shared/agent-setup";
import { runServer, uploadFile } from "./daytona";
export const { agents, resolveAgent } = createCloudAgents({
runServer,
uploadFile,
});

View file

@ -1,670 +0,0 @@
// daytona/daytona.ts — Core Daytona provider: API, SSH, provisioning, execution
import type { CloudInitTier } from "../shared/agents";
import { mkdirSync, readFileSync } from "node:fs";
import { saveVmConnection } from "../history.js";
import { getPackagesForTier, NODE_INSTALL_CMD, needsBun, needsNode } from "../shared/cloud-init";
import { parseJsonObj } from "../shared/parse";
import { killWithTimeout, sleep, spawnInteractive } from "../shared/ssh";
import { isString } from "../shared/type-guards";
import {
defaultSpawnName,
getSpawnCloudConfigPath,
jsonEscape,
loadApiToken,
logError,
logInfo,
logStep,
logStepDone,
logStepInline,
logWarn,
prompt,
sanitizeTermValue,
selectFromList,
toKebabCase,
validateServerName,
} from "../shared/ui";
const DAYTONA_API_BASE = "https://app.daytona.io/api";
const DAYTONA_DASHBOARD_URL = "https://app.daytona.io/";
// ─── State ───────────────────────────────────────────────────────────────────
export interface DaytonaState {
apiKey: string;
sandboxId: string;
sshToken: string;
sshHost: string;
sshPort: string;
}
let _state: DaytonaState = {
apiKey: "",
sandboxId: "",
sshToken: "",
sshHost: "",
sshPort: "",
};
/** Reset session state — used in tests for isolation. */
export function resetDaytonaState(): void {
_state = {
apiKey: "",
sandboxId: "",
sshToken: "",
sshHost: "",
sshPort: "",
};
}
// ─── API Client ──────────────────────────────────────────────────────────────
async function daytonaApi(method: string, endpoint: string, body?: string, maxRetries = 3): Promise<string> {
const url = `${DAYTONA_API_BASE}${endpoint}`;
let interval = 2;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${_state.apiKey}`,
};
const opts: RequestInit = {
method,
headers,
};
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
opts.body = body;
}
const resp = await fetch(url, {
...opts,
signal: AbortSignal.timeout(30_000),
});
const text = await resp.text();
if ((resp.status === 429 || resp.status >= 500) && attempt < maxRetries) {
logWarn(`API ${resp.status} (attempt ${attempt}/${maxRetries}), retrying in ${interval}s...`);
await sleep(interval * 1000);
interval = Math.min(interval * 2, 30);
continue;
}
if (!resp.ok) {
throw new Error(`Daytona API error ${resp.status}: ${extractApiError(text)}`);
}
return text;
} catch (err) {
if (attempt >= maxRetries) {
throw err;
}
logWarn(`API request failed (attempt ${attempt}/${maxRetries}), retrying...`);
await sleep(interval * 1000);
interval = Math.min(interval * 2, 30);
}
}
throw new Error("daytonaApi: unreachable");
}
function extractApiError(text: string, fallback = "Unknown error"): string {
const data = parseJsonObj(text);
if (!data) {
return fallback;
}
const msg = data.message || data.error || data.detail;
return isString(msg) ? msg : fallback;
}
// ─── Token Management ────────────────────────────────────────────────────────
async function saveTokenToConfig(token: string): Promise<void> {
const configPath = getSpawnCloudConfigPath("daytona");
const dir = configPath.replace(/\/[^/]+$/, "");
mkdirSync(dir, {
recursive: true,
mode: 0o700,
});
const escaped = jsonEscape(token);
await Bun.write(configPath, `{\n "api_key": ${escaped},\n "token": ${escaped}\n}\n`, {
mode: 0o600,
});
}
async function testDaytonaToken(): Promise<boolean> {
if (!_state.apiKey) {
return false;
}
try {
await daytonaApi("GET", "/sandbox?page=1&limit=1", undefined, 1);
return true;
} catch {
return false;
}
}
export async function ensureDaytonaToken(): Promise<void> {
// 1. Env var
if (process.env.DAYTONA_API_KEY) {
_state.apiKey = process.env.DAYTONA_API_KEY.trim();
if (await testDaytonaToken()) {
logInfo("Using Daytona API key from environment");
await saveTokenToConfig(_state.apiKey);
return;
}
logWarn("DAYTONA_API_KEY from environment is invalid");
_state.apiKey = "";
}
// 2. Saved config
const saved = loadApiToken("daytona");
if (saved) {
_state.apiKey = saved;
if (await testDaytonaToken()) {
logInfo("Using saved Daytona API key");
return;
}
logWarn("Saved Daytona token is invalid or expired");
_state.apiKey = "";
}
// 3. Manual token entry
logStep("Manual token entry");
logWarn("Get your API key from: https://app.daytona.io/dashboard/keys");
const token = await prompt("Enter your Daytona API key: ");
if (!token) {
throw new Error("No token provided");
}
_state.apiKey = token.trim();
if (!(await testDaytonaToken())) {
logError("Token is invalid");
_state.apiKey = "";
throw new Error("Invalid Daytona token");
}
await saveTokenToConfig(_state.apiKey);
logInfo("Using manually entered Daytona API key");
}
// ─── Connection Tracking ─────────────────────────────────────────────────────
// ─── SSH Helpers ─────────────────────────────────────────────────────────────
/** Build SSH args common to all SSH operations. */
function sshBaseArgs(): string[] {
const args = [
"ssh",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-o",
"LogLevel=ERROR",
"-o",
"ServerAliveInterval=15",
"-o",
"ServerAliveCountMax=3",
"-o",
"ConnectTimeout=10",
"-o",
"PubkeyAuthentication=no",
];
if (_state.sshPort) {
args.push("-o", `Port=${_state.sshPort}`);
}
return args;
}
// ─── Sandbox Size Options ────────────────────────────────────────────────────
export interface SandboxSize {
id: string;
cpu: number;
memory: number;
disk: number;
label: string;
}
export const SANDBOX_SIZES: SandboxSize[] = [
{
id: "small",
cpu: 2,
memory: 4,
disk: 30,
label: "2 vCPU \u00b7 4 GiB RAM \u00b7 30 GiB disk",
},
{
id: "medium",
cpu: 4,
memory: 8,
disk: 50,
label: "4 vCPU \u00b7 8 GiB RAM \u00b7 50 GiB disk",
},
{
id: "large",
cpu: 8,
memory: 16,
disk: 100,
label: "8 vCPU \u00b7 16 GiB RAM \u00b7 100 GiB disk",
},
];
export const DEFAULT_SANDBOX_SIZE = SANDBOX_SIZES[0];
export async function promptSandboxSize(): Promise<SandboxSize> {
if (process.env.DAYTONA_CPU || process.env.DAYTONA_MEMORY) {
const cpu = Number.parseInt(process.env.DAYTONA_CPU || "2", 10);
const memory = Number.parseInt(process.env.DAYTONA_MEMORY || "4", 10);
const disk = Number.parseInt(process.env.DAYTONA_DISK || "30", 10);
return {
id: "env",
cpu,
memory,
disk,
label: `${cpu} vCPU \u00b7 ${memory} GiB RAM \u00b7 ${disk} GiB disk`,
};
}
if (process.env.SPAWN_CUSTOM !== "1") {
return DEFAULT_SANDBOX_SIZE;
}
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return DEFAULT_SANDBOX_SIZE;
}
process.stderr.write("\n");
const items = SANDBOX_SIZES.map((s) => `${s.id}|${s.label}`);
const selectedId = await selectFromList(items, "Daytona sandbox size", DEFAULT_SANDBOX_SIZE.id);
return SANDBOX_SIZES.find((s) => s.id === selectedId) || DEFAULT_SANDBOX_SIZE;
}
// ─── Provisioning ────────────────────────────────────────────────────────────
async function setupSshAccess(): Promise<void> {
logStep("Setting up SSH access...");
const sshResp = await daytonaApi("POST", `/sandbox/${_state.sandboxId}/ssh-access?expiresInMinutes=480`);
const data = parseJsonObj(sshResp);
if (!data) {
logError("Failed to parse SSH access response");
throw new Error("SSH access parse failure");
}
_state.sshToken = isString(data.token) ? data.token : "";
const sshCommand = isString(data.sshCommand) ? data.sshCommand : "";
if (!_state.sshToken) {
logError(`Failed to get SSH access: ${extractApiError(sshResp)}`);
throw new Error("SSH access failed");
}
// Parse host from sshCommand (e.g., "ssh -p 2222 TOKEN@HOST" or "ssh TOKEN@HOST")
const hostMatch = sshCommand.match(/[^@ ]+$/);
_state.sshHost = hostMatch ? hostMatch[0] : "ssh.app.daytona.io";
// Parse port if present
const portMatch = sshCommand.match(/-p\s+(\d+)/);
_state.sshPort = portMatch ? portMatch[1] : "";
logInfo("SSH access ready");
}
export async function createServer(name: string, sandboxSize?: SandboxSize): Promise<void> {
const cpu = sandboxSize?.cpu ?? Number.parseInt(process.env.DAYTONA_CPU || "2", 10);
const memory = sandboxSize?.memory ?? Number.parseInt(process.env.DAYTONA_MEMORY || "4", 10);
const disk = sandboxSize?.disk ?? Number.parseInt(process.env.DAYTONA_DISK || "30", 10);
logStep(`Creating Daytona sandbox '${name}' (${cpu} vCPU, ${memory} GiB RAM, ${disk} GiB disk)...`);
const image = process.env.DAYTONA_IMAGE || "daytonaio/sandbox:latest";
if (/[^a-zA-Z0-9./:_-]/.test(image)) {
logError(`Invalid image name: ${image}`);
throw new Error("Invalid image");
}
const dockerfile = `FROM ${image}`;
const body = JSON.stringify({
name,
buildInfo: {
dockerfileContent: dockerfile,
},
cpu,
memory,
disk,
autoStopInterval: 0,
autoArchiveInterval: 0,
});
const response = await daytonaApi("POST", "/sandbox", body);
const data = parseJsonObj(response);
_state.sandboxId = isString(data?.id) ? data.id : "";
if (!_state.sandboxId) {
logError(`Failed to create sandbox: ${extractApiError(response)}`);
throw new Error("Sandbox creation failed");
}
logInfo(`Sandbox created: ${_state.sandboxId}`);
// Wait for sandbox to reach started state
logStep("Waiting for sandbox to start...");
const maxWait = 120;
let waited = 0;
while (waited < maxWait) {
const statusResp = await daytonaApi("GET", `/sandbox/${_state.sandboxId}`);
const statusData = parseJsonObj(statusResp);
const state = isString(statusData?.state) ? statusData.state : "";
if (state === "started" || state === "running") {
break;
}
if (state === "error" || state === "failed") {
const reason = isString(statusData?.errorReason) ? statusData.errorReason : "unknown";
logError(`Sandbox entered error state: ${reason}`);
throw new Error("Sandbox error state");
}
await sleep(3000);
waited += 3;
}
if (waited >= maxWait) {
logError(`Sandbox did not start within ${maxWait}s`);
logWarn(`Check sandbox status at: ${DAYTONA_DASHBOARD_URL}`);
throw new Error("Sandbox start timeout");
}
// Set up SSH access
await setupSshAccess();
saveVmConnection(
"daytona-sandbox",
"daytona",
_state.sandboxId,
name,
"daytona",
undefined,
undefined,
process.env.SPAWN_ID || undefined,
);
}
// ─── Execution ───────────────────────────────────────────────────────────────
/**
* Run a command on the remote sandbox via SSH.
* Adds a brief sleep after each call to let Daytona's gateway release the connection slot.
*/
export async function runServer(cmd: string, timeoutSecs?: number): Promise<void> {
const fullCmd = `export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && ${cmd}`;
const args = [
...sshBaseArgs(),
"-o",
"BatchMode=yes",
`${_state.sshToken}@${_state.sshHost}`,
"--",
fullCmd,
];
const proc = Bun.spawn(args, {
stdio: [
"pipe",
"inherit",
"inherit",
],
});
// Close stdin but keep process alive (Daytona gateway doesn't propagate stdin EOF)
try {
proc.stdin!.end();
} catch {
/* already closed */
}
const timeout = (timeoutSecs || 300) * 1000;
const timer = setTimeout(() => killWithTimeout(proc), timeout);
try {
const exitCode = await proc.exited;
// Brief sleep to let gateway release connection slot
await sleep(1000);
if (exitCode !== 0) {
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
}
} finally {
clearTimeout(timer);
}
}
/** Run a command and capture stdout. */
export async function runServerCapture(cmd: string, timeoutSecs?: number): Promise<string> {
const fullCmd = `export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && ${cmd}`;
const args = [
...sshBaseArgs(),
"-o",
"BatchMode=yes",
`${_state.sshToken}@${_state.sshHost}`,
"--",
fullCmd,
];
const proc = Bun.spawn(args, {
stdio: [
"pipe",
"pipe",
"pipe",
],
});
try {
proc.stdin!.end();
} catch {
/* already closed */
}
const timeout = (timeoutSecs || 300) * 1000;
const timer = setTimeout(() => killWithTimeout(proc), timeout);
try {
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
const [stdout] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const exitCode = await proc.exited;
await sleep(1000);
if (exitCode !== 0) {
throw new Error(`run_server_capture failed (exit ${exitCode})`);
}
return stdout.trim();
} finally {
clearTimeout(timer);
}
}
/**
* Upload a file to the remote sandbox via base64-encoded SSH command channel.
* Daytona's SSH gateway doesn't support SCP/SFTP.
*/
export async function uploadFile(localPath: string, remotePath: string): Promise<void> {
if (
!/^[a-zA-Z0-9/_.~-]+$/.test(remotePath) ||
remotePath.includes("..") ||
remotePath.split("/").some((s) => s.startsWith("-"))
) {
logError(`Invalid remote path: ${remotePath}`);
throw new Error("Invalid remote path");
}
const content: Buffer = readFileSync(localPath);
const b64 = content.toString("base64");
const args = [
...sshBaseArgs(),
"-o",
"BatchMode=yes",
`${_state.sshToken}@${_state.sshHost}`,
"--",
`base64 -d > '${remotePath}'`,
];
const proc = Bun.spawn(args, {
stdio: [
"pipe",
"ignore",
"ignore",
],
});
try {
const stdin = proc.stdin;
if (stdin) {
stdin.write(b64 + "\n");
stdin.end();
}
} catch {
/* stdin already closed */
}
const exitCode = await proc.exited;
await sleep(1000);
if (exitCode !== 0) {
throw new Error(`upload_file failed for ${remotePath}`);
}
}
export async function interactiveSession(cmd: string): Promise<number> {
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
// Single-quote escaping prevents shell expansion ($(), ${}, backticks) unlike JSON.stringify double-quoting
const shellEscapedCmd = cmd.replace(/'/g, "'\\''");
const fullCmd = `export TERM=${term} PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c '${shellEscapedCmd}'`;
// Interactive mode — drop BatchMode so the PTY works
const args = [
...sshBaseArgs(),
"-t", // Force PTY allocation
`${_state.sshToken}@${_state.sshHost}`,
"--",
fullCmd,
];
const exitCode = spawnInteractive(args);
// Post-session summary
process.stderr.write("\n");
logWarn(`Session ended. Your sandbox '${_state.sandboxId}' may still be running.`);
logWarn("Remember to delete it when you're done to avoid ongoing charges.");
logWarn("");
logWarn("Manage or delete it in your dashboard:");
logWarn(` ${DAYTONA_DASHBOARD_URL}`);
logWarn("");
logInfo("To delete from CLI:");
logInfo(" spawn delete");
return exitCode;
}
// ─── Cloud Init ──────────────────────────────────────────────────────────────
async function waitForSsh(maxAttempts = 20): Promise<void> {
logStep("Waiting for SSH connectivity...");
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const output = await runServerCapture("echo ok");
if (output.includes("ok")) {
logStepDone();
logInfo("SSH is ready");
return;
}
} catch {
// ignore
}
logStepInline(`SSH not ready yet (${attempt}/${maxAttempts})`);
await sleep(5000);
}
logStepDone();
logError(`SSH connectivity failed after ${maxAttempts} attempts`);
throw new Error("SSH wait timeout");
}
export async function waitForCloudInit(tier: CloudInitTier = "full"): Promise<void> {
await waitForSsh();
const packages = getPackagesForTier(tier);
logStep("Installing base tools in sandbox...");
const parts = [
"export DEBIAN_FRONTEND=noninteractive",
"apt-get update -y",
`apt-get install -y --no-install-recommends ${packages.join(" ")}`,
];
if (needsNode(tier)) {
parts.push(NODE_INSTALL_CMD);
}
if (needsBun(tier)) {
parts.push("curl --proto '=https' -fsSL https://bun.sh/install | bash");
}
parts.push(
`echo 'export PATH="\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}"' >> ~/.bashrc`,
`echo 'export PATH="\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}"' >> ~/.zshrc`,
);
try {
await runServer(parts.join(" && "));
} catch {
logWarn("Base tools install had errors, continuing...");
}
logInfo("Base tools installed");
}
// ─── Server Name ─────────────────────────────────────────────────────────────
export async function getServerName(): Promise<string> {
if (process.env.DAYTONA_SANDBOX_NAME) {
const name = process.env.DAYTONA_SANDBOX_NAME;
if (!validateServerName(name)) {
logError(`Invalid DAYTONA_SANDBOX_NAME: '${name}'`);
throw new Error("Invalid server name");
}
logInfo(`Using sandbox name from environment: ${name}`);
return name;
}
const kebab = process.env.SPAWN_NAME_KEBAB || (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
return kebab || defaultSpawnName();
}
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) {
return;
}
let kebab: string;
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
kebab = (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "") || defaultSpawnName();
} else {
const derived = process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "";
const fallback = derived || defaultSpawnName();
process.stderr.write("\n");
const answer = await prompt(`Daytona workspace name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}
// ─── Lifecycle ───────────────────────────────────────────────────────────────
export async function destroyServer(id?: string): Promise<void> {
const targetId = id || _state.sandboxId;
if (!targetId) {
logWarn("No sandbox ID to destroy");
return;
}
logStep(`Destroying sandbox ${targetId}...`);
try {
await daytonaApi("DELETE", `/sandbox/${targetId}`);
} catch (err) {
logError(`Failed to destroy sandbox ${targetId}`);
logError(err instanceof Error ? err.message : "Unknown error");
logWarn("The sandbox may still be running and incurring charges.");
logWarn(`Delete it manually at: ${DAYTONA_DASHBOARD_URL}`);
throw new Error("Sandbox deletion failed");
}
logInfo("Sandbox destroyed");
}

View file

@ -1,68 +0,0 @@
#!/usr/bin/env bun
// daytona/main.ts — Orchestrator: deploys an agent on Daytona
import type { CloudOrchestrator } from "../shared/orchestrate";
import type { SandboxSize } from "./daytona";
import { saveLaunchCmd } from "../history.js";
import { runOrchestration } from "../shared/orchestrate";
import { agents, resolveAgent } from "./agents";
import {
createServer as createDaytonaServer,
ensureDaytonaToken,
getServerName,
interactiveSession,
promptSandboxSize,
promptSpawnName,
runServer,
uploadFile,
waitForCloudInit,
} from "./daytona";
async function main() {
const agentName = process.argv[2];
if (!agentName) {
console.error("Usage: bun run daytona/main.ts <agent>");
console.error(`Agents: ${Object.keys(agents).join(", ")}`);
process.exit(1);
}
const agent = resolveAgent(agentName);
let sandboxSize: SandboxSize | undefined;
const cloud: CloudOrchestrator = {
cloudName: "daytona",
cloudLabel: "Daytona",
runner: {
runServer,
uploadFile,
},
async authenticate() {
await promptSpawnName();
await ensureDaytonaToken();
},
async promptSize() {
sandboxSize = await promptSandboxSize();
},
async createServer(name: string, spawnId?: string) {
process.env.SPAWN_ID = spawnId || "";
await createDaytonaServer(name, sandboxSize);
},
getServerName,
async waitForReady() {
await waitForCloudInit(agent.cloudInitTier);
},
interactiveSession,
saveLaunchCmd: (cmd: string, sid?: string) => saveLaunchCmd(cmd, sid),
};
await runOrchestration(cloud, agent, agentName);
}
main().catch((err) => {
const msg = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
process.stderr.write(`\x1b[0;31mFatal: ${msg}\x1b[0m\n`);
process.exit(1);
});

View file

@ -15,7 +15,7 @@ const IPV4_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/;
// IPv6 address pattern (simplified - catches most valid IPv6 addresses)
const IPV6_PATTERN = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
// Hostname pattern: valid DNS hostnames (e.g., ssh.app.daytona.io)
// Hostname pattern: valid DNS hostnames (e.g., compute.amazonaws.com)
// Only allows safe characters: lowercase alphanumeric, hyphens, dots
// Must have at least two labels (e.g., "host.domain")
const HOSTNAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
@ -27,7 +27,6 @@ const USERNAME_PATTERN = /^[a-z_][a-z0-9_-]*\$?$/;
// Special connection sentinel values (not actual IPs)
const CONNECTION_SENTINELS = [
"sprite-console",
"daytona-sandbox",
"localhost",
];
@ -171,8 +170,8 @@ export function validateScriptContent(script: string): void {
* Allows:
* - Valid IPv4 addresses (e.g., "192.168.1.1")
* - Valid IPv6 addresses (e.g., "::1", "2001:db8::1")
* - Valid hostnames (e.g., "ssh.app.daytona.io")
* - Special sentinel values ("sprite-console", "daytona-sandbox", "localhost")
* - Valid hostnames (e.g., "compute.amazonaws.com")
* - Special sentinel values ("sprite-console", "localhost")
*
* @param ip - The IP address or sentinel to validate
* @throws Error if validation fails
@ -213,7 +212,7 @@ export function validateConnectionIP(ip: string): void {
return;
}
// Validate as hostname (e.g., ssh.app.daytona.io)
// Validate as hostname (e.g., compute.amazonaws.com)
if (HOSTNAME_PATTERN.test(ip)) {
return;
}