| name | daytona-integration |
| description | Daytona SDK integration for sandbox lifecycle management. Use when working with SandboxManager, creating/destroying sandboxes, executing commands in sandboxes, or transferring files between sandboxes and local filesystem. |
Daytona Integration
Source: Daytona TypeScript SDK Docs
Installation
npm install @daytonaio/sdk
SDK Setup
import { Daytona } from '@daytonaio/sdk'
const daytona = new Daytona({
apiKey: process.env.DAYTONA_API_KEY,
apiUrl: 'https://app.daytona.io/api',
target: 'us',
})
DaytonaConfig Interface
| Property | Type | Required | Description |
|---|
apiKey | string | No* | API key authentication |
apiUrl | string | No | API endpoint (default: https://app.daytona.io/api) |
jwtToken | string | No* | JWT authentication (requires organizationId) |
organizationId | string | No | Required when using JWT |
target | string | No | Sandbox location preference |
*One of apiKey or jwtToken required.
Daytona Class Methods
create()
Create a new sandbox.
const sandbox = await daytona.create({
language: 'typescript',
envVars: {
CLAUDE_CODE_OAUTH_TOKEN: token,
NODE_ENV: 'development'
},
autoStopInterval: 15,
autoArchiveInterval: 10080,
autoDeleteInterval: 43200,
labels: { project: 'mvp' },
ephemeral: false
}, {
timeout: 60
})
Returns: Promise<Sandbox>
CreateSandboxParams
| Property | Type | Default | Description |
|---|
language | 'python' | 'typescript' | 'javascript' | 'python' | Runtime language |
envVars | Record<string, string> | {} | Environment variables |
autoStopInterval | number | 15 | Idle minutes before stop |
autoArchiveInterval | number | 10080 | Minutes before archive |
autoDeleteInterval | number | - | Minutes before deletion |
labels | Record<string, string> | - | Metadata tags |
ephemeral | boolean | false | Auto-cleanup on stop |
volumes | VolumeMount[] | - | Volume mounts |
image | string | Image | - | Custom Docker image |
resources | Resources | - | CPU/memory/disk allocation |
snapshot | string | - | Create from snapshot ID |
get()
Retrieve a sandbox by ID or name.
const sandbox = await daytona.get('sandbox-id-or-name')
Returns: Promise<Sandbox>
list()
List sandboxes with optional filtering.
const result = await daytona.list(
{ project: 'mvp' },
1,
10
)
Returns: Promise<PaginatedSandboxes>
findOne()
Find first sandbox matching filter.
const sandbox = await daytona.findOne({
id: 'sandbox-id',
name: 'sandbox-name',
labels: { project: 'mvp' }
})
Returns: Promise<Sandbox>
delete()
Delete a sandbox permanently.
await daytona.delete(sandbox, 60)
start() / stop()
await daytona.start(sandbox, 60)
await daytona.stop(sandbox)
Sandbox Class
Properties
| Property | Type | Description |
|---|
id | string | Unique sandbox identifier |
name | string | Sandbox name |
state | SandboxState | 'started' | 'stopped' | ... |
cpu | number | CPU count |
memory | number | Memory in GiB |
disk | number | Disk space in GiB |
gpu | number | GPU count |
env | Record<string, string> | Environment variables |
labels | Record<string, string> | Metadata |
process | Process | Process execution interface |
fs | FileSystem | File system interface |
git | Git | Git operations interface |
Methods
await sandbox.start(timeout?)
await sandbox.stop(timeout?)
await sandbox.delete(timeout)
await sandbox.archive()
await sandbox.recover(timeout?)
await sandbox.refreshData()
await sandbox.waitUntilStarted(timeout?)
await sandbox.waitUntilStopped(timeout?)
await sandbox.setLabels({ key: 'value' })
await sandbox.setAutostopInterval(minutes)
await sandbox.setAutoDeleteInterval(minutes)
await sandbox.setAutoArchiveInterval(minutes)
const workDir = await sandbox.getWorkDir()
const homeDir = await sandbox.getUserHomeDir()
const previewUrl = await sandbox.getPreviewLink(port)
const signedUrl = await sandbox.getSignedPreviewUrl(port, expiresInSeconds?)
Process Class (sandbox.process)
executeCommand()
Execute shell commands.
const response = await sandbox.process.executeCommand(
'npm install',
'/workspace',
{ NODE_ENV: 'prod' },
300
)
Returns: Promise<ExecuteResponse>
CRITICAL WARNING - Background Processes:
executeCommand() with timeout=0 does NOT run background servers properly. Despite documentation saying "0=indefinite", the Promise resolves immediately without spawning a tracked process. Use session-based execution for background processes instead (see Session-based Execution section below).
await sandbox.process.executeCommand('npm run dev', '/workspace', undefined, 0)
await sandbox.process.createSession('preview-server')
await sandbox.process.executeSessionCommand('preview-server', {
command: 'npm run dev',
async: true
}, 0)
ExecuteResponse Interface
| Property | Type | Description |
|---|
exitCode | number | Process exit status |
result | string | stdout content |
artifacts | ExecutionArtifacts | Additional data (stdout, charts) |
codeRun()
Execute code using appropriate runtime.
const response = await sandbox.process.codeRun(
'console.log("Hello")',
{ argv: [], env: {} },
60
)
Session-based Execution (for streaming)
await sandbox.process.createSession('my-session')
await sandbox.process.executeSessionCommand('my-session', {
command: 'npm start',
async: true
}, 300)
await sandbox.process.getSessionCommandLogs(
'my-session',
'command-id',
(stdout: string) => { },
(stderr: string) => { }
)
await sandbox.process.deleteSession('my-session')
PTY (Interactive Terminal)
const pty = await sandbox.process.createPty({ cols: 80, rows: 24 })
await sandbox.process.connectPty(pty.sessionId, {
onData: (data) => { },
onExit: (code) => { }
})
await sandbox.process.resizePtySession(pty.sessionId, 120, 40)
await sandbox.process.killPtySession(pty.sessionId)
File System (sandbox.fs)
const buffer = await sandbox.fs.downloadFile('/workspace/file.txt')
const content = buffer.toString()
await sandbox.fs.uploadFile(Buffer.from('content'), '/workspace/file.txt')
await sandbox.fs.uploadFile('/local/path.txt', '/workspace/file.txt')
const files = await sandbox.fs.listFiles('/workspace')
await sandbox.fs.createFolder('/workspace/new-dir', '755')
await sandbox.fs.deleteFile('/workspace/file.txt')
await sandbox.fs.deleteFile('/workspace/dir', true)
const info = await sandbox.fs.getFileDetails('/workspace/file.txt')
const results = await sandbox.fs.searchFiles('/workspace', '*.ts')
const matches = await sandbox.fs.findFiles('/workspace', 'pattern')
Parallel Sandbox Creation (4 agents)
const agentIds = ['agent-a', 'agent-b', 'agent-c', 'agent-d']
const sandboxes = await Promise.all(
agentIds.map(async (agentId) => {
const sandbox = await daytona.create({
language: 'typescript',
envVars: { CLAUDE_CODE_OAUTH_TOKEN: token },
labels: { agentId },
autoStopInterval: 60
})
return { agentId, sandbox }
})
)
File Transfer Patterns
Extract from sandbox (winner selection)
await sandbox.process.executeCommand(
'tar -czf /tmp/project.tar.gz -C /workspace .',
undefined,
undefined,
60
)
const tarBuffer = await sandbox.fs.downloadFile('/tmp/project.tar.gz')
import { writeFileSync } from 'fs'
writeFileSync('~/.multishot/runs/run-id/winner.tar.gz', tarBuffer)
Inject into sandbox (next round)
import { readFileSync } from 'fs'
const tarBuffer = readFileSync('~/.multishot/runs/run-id/winner.tar.gz')
await sandbox.fs.uploadFile(tarBuffer, '/tmp/project.tar.gz')
await sandbox.process.executeCommand(
'tar -xzf /tmp/project.tar.gz -C /workspace',
undefined,
undefined,
60
)
Error Handling
Network Error Retry Pattern
When executing commands that make API calls (Claude Code, npm installs, etc.), implement retry logic for transient network errors:
async function execWithRetry(
command: string,
maxRetries: number = 3
): Promise<ExecuteResponse> {
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await sandbox.process.executeCommand(
command,
workDir,
undefined,
900
)
if (response.exitCode !== 0) {
throw new Error(`Process exited with code ${response.exitCode}`)
}
return response
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err))
const isRetryable =
lastError.message.includes('ECONNRESET') ||
lastError.message.includes('ETIMEDOUT') ||
lastError.message.includes('ENOTFOUND') ||
lastError.message.includes('EAI_AGAIN') ||
lastError.message.includes('socket hang up')
if (!isRetryable) {
throw lastError
}
if (attempt < maxRetries) {
const delayMs = 5000 * Math.pow(2, attempt - 1)
console.warn(`Retry ${attempt}/${maxRetries} in ${delayMs / 1000}s...`)
await new Promise(resolve => setTimeout(resolve, delayMs))
} else {
throw new Error(`Network error after ${maxRetries} retries: ${lastError.message}`)
}
}
}
throw lastError || new Error('Unknown error')
}
Retryable errors:
ECONNRESET - Connection forcibly closed
ETIMEDOUT - Connection timed out
ENOTFOUND - DNS lookup failed
EAI_AGAIN - Temporary DNS failure
socket hang up - Connection dropped
Non-retryable errors (fail immediately):
- Authentication failures
- Invalid commands
- Permission errors
- Validation errors
Benefits:
- Prevents sandbox waste from transient network drops
- Critical for parallel agent execution (4 agents = higher network failure chance)
- Exponential backoff prevents overwhelming failing services
Sandbox Creation
try {
const sandbox = await daytona.create({ language: 'typescript' })
} catch (error) {
if (error.code === 'SANDBOX_CREATION_FAILED') {
await new Promise(r => setTimeout(r, 2000))
const sandbox = await daytona.create({ language: 'typescript' })
}
}
try {
} finally {
await sandbox.delete(30).catch(console.error)
}
MVP Implementation Notes
Current implementation uses executeCommand() with retry logic (not session-based streaming):
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await sandbox.process.executeCommand(
command,
workDir,
undefined,
900
)
if (response.result) onStdout?.(response.result)
return
} catch (err) {
}
}
Preview servers use session-based execution for background processes:
await sandbox.process.createSession(sessionId)
await sandbox.process.executeSessionCommand(sessionId, {
command: config.command,
async: true
}, 0)
Preview URL retrieval uses signed URLs:
const preview = await sandbox.getSignedPreviewUrl(port, 3600)
return preview.url
Preview Optimization Patterns
Config Caching
Reduce filesystem operations by caching project type detection results:
class SandboxManager {
private previewConfigs: Map<string, PreviewConfig> = new Map()
async detectProjectType(agentId: string): Promise<PreviewConfig> {
const cached = this.previewConfigs.get(agentId)
if (cached) {
console.log(`Using cached preview config: ${cached.type}`)
return cached
}
const config = await this.performDetection(agentId)
this.previewConfigs.set(agentId, config)
return config
}
clearPreviewCache(): void {
this.previewConfigs.clear()
}
async destroySandbox(agentId: string): Promise<void> {
this.previewConfigs.delete(agentId)
}
}
Benefits:
- Repeated preview requests reuse cached config (no filesystem access)
- Cleared on new run to detect fresh project structure
- Cleared per-agent on sandbox destruction
Session Reuse
Prevent duplicate preview servers by reusing existing sessions:
interface SandboxInfo {
sandbox: Sandbox
agentId: string
previewSessionId?: string
}
async startPreviewServer(
agentId: string,
config: PreviewConfig
): Promise<void> {
const info = this.sandboxes.get(agentId)
const sessionId = `preview-${agentId}`
if (info.previewSessionId === sessionId) {
console.log(`Reusing existing preview session: ${sessionId}`)
return
}
info.previewSessionId = sessionId
await sandbox.process.createSession(sessionId)
await sandbox.process.executeSessionCommand(sessionId, {
command: config.command,
async: true
}, 0)
}
Benefits:
- "Preview All" followed by single-agent preview reuses servers
- Single-agent preview can be clicked multiple times without restart
- Reduces server startup latency on repeated previews
Server Health Check
Poll server readiness instead of blind sleep:
async waitForServerReady(
agentId: string,
port: number,
maxAttempts: number = 30,
intervalMs: number = 1000
): Promise<boolean> {
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await sandbox.process.executeCommand(
`curl -f -s -o /dev/null -w "%{http_code}" http://localhost:${port} || echo "000"`,
'/workspace',
undefined,
5
)
const httpCode = response.result.trim()
if (httpCode !== "000" && httpCode !== "") {
console.log(`Server ready on port ${port} (HTTP ${httpCode})`)
return true
}
} catch {}
await new Promise(r => setTimeout(r, intervalMs))
}
console.warn(`Server not ready after ${maxAttempts} attempts`)
return false
}
Usage:
await startPreviewServer(agentId, config)
const isReady = await waitForServerReady(agentId, config.port, 30, 1000)
if (!isReady) {
throw new Error(`Server failed to start on port ${config.port}`)
}
const url = await getPreviewUrl(agentId, config.port)
CLI Installation Pattern
Robust CLI installation with verification, fallback, and retry logic:
async installClaudeCLI(agentId: string, maxRetries: number = 3): Promise<void> {
const installCmd = 'bash -c "set -o pipefail; curl -fsSL https://claude.ai/install.sh | bash"'
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await sandbox.process.executeCommand(installCmd)
if (response.exitCode !== 0) {
throw new Error(`Install failed: ${response.result}`)
}
const verifyResponse = await sandbox.process.executeCommand(
'claude --version', undefined, undefined, 30
)
if (verifyResponse.exitCode !== 0) {
throw new Error('Claude CLI not found after installation')
}
return
} catch (err) {
try {
const response = await sandbox.process.executeCommand(
'npm install -g @anthropic-ai/claude-code',
undefined, undefined,
300
)
if (response.exitCode !== 0) {
throw new Error(`NPM install failed: ${response.result}`)
}
const verifyResponse = await sandbox.process.executeCommand(
'claude --version', undefined, undefined, 30
)
if (verifyResponse.exitCode !== 0) {
throw new Error('Claude CLI not found after npm installation')
}
return
} catch (npmErr) {
lastError = npmErr instanceof Error ? npmErr : new Error(String(npmErr))
const errorMsg = lastError.message
const isRetryable =
errorMsg.includes('ETIMEDOUT') ||
errorMsg.includes('ECONNRESET') ||
errorMsg.includes('ENOTFOUND') ||
errorMsg.includes('EAI_AGAIN') ||
errorMsg.includes('socket hang up')
if (!isRetryable || attempt >= maxRetries) {
throw lastError
}
const delayMs = 5000 * Math.pow(2, attempt - 1)
await new Promise(r => setTimeout(r, delayMs))
}
}
}
throw lastError || new Error('Installation failed')
}
Key points:
- Use
set -o pipefail in bash to propagate curl failures
- Verify CLI is executable with
--version check after installation
- Set timeout on npm install (300s recommended) - network may be slow
- Retry up to 3 times on network errors (ETIMEDOUT, ECONNRESET, etc.)
- Exponential backoff prevents overwhelming failing services
- Daytona sandbox networking may need time to initialize after creation
Best Practices
- Timeouts: Set appropriate timeouts for long-running commands (15 min for Claude)
- Cleanup: Always destroy sandboxes in finally blocks
- Parallel creation: Use Promise.all() for creating multiple sandboxes
- Session streaming: Use session-based execution for real-time output (not in MVP)
- Ephemeral mode: Set
ephemeral: true for auto-cleanup scenarios
- CLI verification: Always verify CLI tools are installed after installation commands
- Error propagation: Throw exceptions on non-zero exit codes to ensure proper error handling