daytona-integration について
Daytona SDKでリモートサンドボックスを管理する開発者向けのTypeScript統合ガイドです。サンドボックスのライフサイクル、プロセス実行、ファイル転送、プレビューサーバーのパターンに加え、設定とエラー処理を説明します。
主な機能
- 01サンドボックスの作成と管理
- 02ランタイム、リソース、環境の設定
- 03コマンド、コード、対話セッションの実行
- 04サンドボックスのファイルシステムで転送
- 05リトライとプレビューサーバーの処理
活用シーン
- 01サンドボックスで開発コマンドを実行
- 02並列エージェント用の個別サンドボックスを作成
- 03プロジェクトファイルをアップロードして出力を取得
- 04プレビューサーバーを起動してURLを取得
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ seanchiuai/ multishot --skill daytona-integrationインストール後、Claude Code で/daytona-integrationを入力して読み込まれたか確認します。
詳細
Daytona 統合
インストール
npm install @daytonaio/sdk
SDK のセットアップ
import { Daytona } from '@daytonaio/sdk'
const daytona = new Daytona({
apiKey: process.env.DAYTONA_API_KEY, // Required for auth
apiUrl: 'https://app.daytona.io/api', // Default API endpoint
target: 'us', // Optional: region preference
// Alternative auth:
// jwtToken: 'jwt-token', // Requires organizationId
// organizationId: 'org-id'
})
DaytonaConfig インターフェース
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
apiKey | string | いいえ* | API キー認証 |
apiUrl | string | いいえ | API エンドポイント(デフォルト:https://app.daytona.io/api) |
jwtToken | string | いいえ* | JWT 認証(organizationId が必要) |
organizationId | string | いいえ | JWT 使用時に必須 |
target | string | いいえ | サンドボックスの場所の優先設定 |
*apiKey または jwtToken のいずれかが必要です。
Daytona クラスのメソッド
create()
新しいサンドボックスを作成します。
const sandbox = await daytona.create({
language: 'typescript', // 'python' | 'typescript' | 'javascript'
envVars: { // Environment variables
CLAUDE_CODE_OAUTH_TOKEN: token,
NODE_ENV: 'development'
},
autoStopInterval: 15, // Minutes idle before stop (default: 15)
autoArchiveInterval: 10080, // Minutes before archive (default: 7 days)
autoDeleteInterval: 43200, // Minutes before deletion
labels: { project: 'mvp' }, // Metadata tags
ephemeral: false // Auto-cleanup on stop
}, {
timeout: 60 // Creation timeout in seconds
})
戻り値: Promise<Sandbox>
CreateSandboxParams
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
language | 'python' | 'typescript' | 'javascript' | 'python' | ランタイム言語 |
envVars | Record<string, string> | {} | 環境変数 |
autoStopInterval | number | 15 | 停止するまでのアイドル時間(分) |
autoArchiveInterval | number | 10080 | アーカイブまでの時間(分) |
autoDeleteInterval | number | - | 削除までの時間(分) |
labels | Record<string, string> | - | メタデータタグ |
ephemeral | boolean | false | 停止時に自動クリーンアップ |
volumes | VolumeMount[] | - | ボリュームのマウント |
image | string | Image | - | カスタム Docker イメージ |
resources | Resources | - | CPU/メモリ/ディスクの割り当て |
snapshot | string | - | スナップショット ID から作成 |
get()
ID または名前でサンドボックスを取得します。
const sandbox = await daytona.get('sandbox-id-or-name')
戻り値: Promise<Sandbox>
list()
オプションのフィルターを指定してサンドボックスを一覧表示します。
const result = await daytona.list(
{ project: 'mvp' }, // Filter by labels
1, // Page number
10 // Items per page
)
// result.sandboxes: Sandbox[]
// result.total: number
戻り値: Promise<PaginatedSandboxes>
findOne()
フィルターに一致する最初のサンドボックスを検索します。
const sandbox = await daytona.findOne({
id: 'sandbox-id',
name: 'sandbox-name',
labels: { project: 'mvp' }
})
戻り値: Promise<Sandbox>
delete()
サンドボックスを完全に削除します。
await daytona.delete(sandbox, 60) // sandbox object, timeout seconds
start() / stop()
await daytona.start(sandbox, 60) // Start and await ready, timeout seconds
await daytona.stop(sandbox) // Stop execution
Sandbox クラス
プロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
id | string | 一意のサンドボックス識別子 |
name | string | サンドボックス名 |
state | SandboxState | 'started' | 'stopped' | ... |
cpu | number | CPU 数 |
memory | number | メモリ容量(GiB) |
disk | number | ディスク容量(GiB) |
gpu | number | GPU 数 |
env | Record<string, string> | 環境変数 |
labels | Record<string, string> | メタデータ |
process | Process | プロセス実行インターフェース |
fs | FileSystem | ファイルシステムインターフェース |
git | Git | Git 操作インターフェース |
メソッド
// Lifecycle
await sandbox.start(timeout?)
await sandbox.stop(timeout?)
await sandbox.delete(timeout)
await sandbox.archive()
await sandbox.recover(timeout?)
// State
await sandbox.refreshData()
await sandbox.waitUntilStarted(timeout?)
await sandbox.waitUntilStopped(timeout?)
// Configuration
await sandbox.setLabels({ key: 'value' })
await sandbox.setAutostopInterval(minutes)
await sandbox.setAutoDeleteInterval(minutes)
await sandbox.setAutoArchiveInterval(minutes)
// Paths
const workDir = await sandbox.getWorkDir()
const homeDir = await sandbox.getUserHomeDir()
// Access
const previewUrl = await sandbox.getPreviewLink(port)
const signedUrl = await sandbox.getSignedPreviewUrl(port, expiresInSeconds?)
Process クラス(sandbox.process)
executeCommand()
シェルコマンドを実行します。
const response = await sandbox.process.executeCommand(
'npm install', // command
'/workspace', // cwd (optional)
{ NODE_ENV: 'prod' }, // env (optional)
300 // timeout in seconds (optional, 0=indefinite)
)
戻り値: Promise<ExecuteResponse>
重大な警告 — バックグラウンドプロセス:
timeout=0 を指定した executeCommand() では、バックグラウンドサーバーは適切に実行されません。ドキュメントには「0=無期限」と記載されていますが、追跡対象のプロセスを起動せずに Promise が直ちに解決されます。代わりに、バックグラウンドプロセスにはセッションベースの実行を使用してください(下記の「セッションベースの実行」セクションを参照)。
// ❌ WRONG - Server won't actually run
await sandbox.process.executeCommand('npm run dev', '/workspace', undefined, 0)
// ✅ CORRECT - Use sessions for background processes
await sandbox.process.createSession('preview-server')
await sandbox.process.executeSessionCommand('preview-server', {
command: 'npm run dev',
async: true
}, 0)
ExecuteResponse インターフェース
| プロパティ | 型 | 説明 |
|---|---|---|
exitCode | number | プロセスの終了ステータス |
result | string | stdout の内容 |
artifacts | ExecutionArtifacts | 追加データ(stdout、チャート) |
codeRun()
適切なランタイムを使用してコードを実行します。
const response = await sandbox.process.codeRun(
'console.log("Hello")', // code
{ argv: [], env: {} }, // params (optional)
60 // timeout in seconds (optional)
)
セッションベースの実行(ストリーミング用)
// Create a persistent session
await sandbox.process.createSession('my-session')
// Execute with async log callbacks
await sandbox.process.executeSessionCommand('my-session', {
command: 'npm start',
async: true
}, 300)
// Get logs with streaming callbacks
await sandbox.process.getSessionCommandLogs(
'my-session',
'command-id',
(stdout: string) => { /* handle stdout chunk */ },
(stderr: string) => { /* handle stderr chunk */ }
)
// Cleanup
await sandbox.process.deleteSession('my-session')
PTY(対話型ターミナル)
// Create PTY session
const pty = await sandbox.process.createPty({ cols: 80, rows: 24 })
// Connect via WebSocket
await sandbox.process.connectPty(pty.sessionId, {
onData: (data) => { /* terminal output */ },
onExit: (code) => { /* process exited */ }
})
// Resize
await sandbox.process.resizePtySession(pty.sessionId, 120, 40)
// Kill
await sandbox.process.killPtySession(pty.sessionId)
ファイルシステム(sandbox.fs)
// Download file (returns Buffer)
const buffer = await sandbox.fs.downloadFile('/workspace/file.txt')
const content = buffer.toString()
// Upload file (Buffer or local path)
await sandbox.fs.uploadFile(Buffer.from('content'), '/workspace/file.txt')
await sandbox.fs.uploadFile('/local/path.txt', '/workspace/file.txt')
// List directory (returns FileInfo[])
const files = await sandbox.fs.listFiles('/workspace')
// files[i].name, files[i].size, files[i].isDir
// Create directory
await sandbox.fs.createFolder('/workspace/new-dir', '755')
// Delete file/directory
await sandbox.fs.deleteFile('/workspace/file.txt')
await sandbox.fs.deleteFile('/workspace/dir', true) // recursive
// File details
const info = await sandbox.fs.getFileDetails('/workspace/file.txt')
// Search
const results = await sandbox.fs.searchFiles('/workspace', '*.ts')
const matches = await sandbox.fs.findFiles('/workspace', 'pattern')
サンドボックスの並列作成(4 エージェント)
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 }
})
)
ファイル転送のパターン
サンドボックスから抽出(勝者の選定)
// Create tar in sandbox
await sandbox.process.executeCommand(
'tar -czf /tmp/project.tar.gz -C /workspace .',
undefined,
undefined,
60
)
// Download
const tarBuffer = await sandbox.fs.downloadFile('/tmp/project.tar.gz')
// Save locally
import { writeFileSync } from 'fs'
writeFileSync('~/.multishot/runs/run-id/winner.tar.gz', tarBuffer)
サンドボックスに注入(次のラウンド)
import { readFileSync } from 'fs'
// Upload tar
const tarBuffer = readFileSync('~/.multishot/runs/run-id/winner.tar.gz')
await sandbox.fs.uploadFile(tarBuffer, '/tmp/project.tar.gz')
// Extract
await sandbox.process.executeCommand(
'tar -xzf /tmp/project.tar.gz -C /workspace',
undefined,
undefined,
60
)
エラー処理
ネットワークエラーの再試行パターン
API 呼び出しを行うコマンド(Claude Code、npm インストールなど)を実行する際は、一時的なネットワークエラーに対する再試行ロジックを実装します。
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 // Success
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err))
// Check if error is retryable (network issues)
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 // Non-network error - fail immediately
}
if (attempt < maxRetries) {
// Exponential backoff: 5s, 10s, 20s
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')
}
再試行可能なエラー:
ECONNRESET- 接続が強制的に切断されたETIMEDOUT- 接続がタイムアウトしたENOTFOUND- DNS ルックアップに失敗したEAI_AGAIN- 一時的な DNS エラーsocket hang up- 接続が切断された
再試行不可能なエラー(即座に失敗):
- 認証エラー
- 無効なコマンド
- 権限エラー
- 検証エラー
利点:
- 一時的なネットワーク切断によるサンドボックスの浪費を防ぎます
- 並列エージェント実行では不可欠です(エージェントが4つの場合、ネットワーク障害の発生確率が高くなります)
- 指数バックオフにより、障害が発生しているサービスへの過剰な負荷を防ぎます
サンドボックスの作成
try {
const sandbox = await daytona.create({ language: 'typescript' })
} catch (error) {
if (error.code === 'SANDBOX_CREATION_FAILED') {
// Retry once
await new Promise(r => setTimeout(r, 2000))
const sandbox = await daytona.create({ language: 'typescript' })
}
}
// Always cleanup in finally
try {
// ... work with sandbox
} finally {
await sandbox.delete(30).catch(console.error)
}
MVP 実装に関する注意事項
現在の実装では、セッションベースのストリーミングではなく、再試行ロジックを備えた executeCommand() を使用しています。
// SandboxManager.execClaudeCommand() pattern with retries
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await sandbox.process.executeCommand(
command,
workDir,
undefined,
900 // 15 minute timeout
)
if (response.result) onStdout?.(response.result)
return // Success
} catch (err) {
// Retry logic for ECONNRESET, ETIMEDOUT, etc.
// Exponential backoff: 5s, 10s, 20s
}
}
プレビューサーバーでは、バックグラウンドプロセスにセッションベースの実行を使用します。
await sandbox.process.createSession(sessionId)
await sandbox.process.executeSessionCommand(sessionId, {
command: config.command,
async: true
}, 0)
プレビュー URL の取得には署名付き URL を使用します。
const preview = await sandbox.getSignedPreviewUrl(port, 3600)
return preview.url
プレビュー最適化パターン
設定のキャッシュ
プロジェクトタイプの検出結果をキャッシュして、ファイルシステム操作を減らします。
class SandboxManager {
private previewConfigs: Map<string, PreviewConfig> = new Map()
async detectProjectType(agentId: string): Promise<PreviewConfig> {
// Check cache first
const cached = this.previewConfigs.get(agentId)
if (cached) {
console.log(`Using cached preview config: ${cached.type}`)
return cached
}
// Detect project type (filesystem operations)
const config = await this.performDetection(agentId)
// Cache result
this.previewConfigs.set(agentId, config)
return config
}
clearPreviewCache(): void {
this.previewConfigs.clear()
}
async destroySandbox(agentId: string): Promise<void> {
// Clear cached config when sandbox destroyed
this.previewConfigs.delete(agentId)
// ... destroy sandbox
}
}
利点:
- プレビューのリクエストを繰り返しても、キャッシュされた設定が再利用されます(ファイルシステムへのアクセスはありません)
- 新しい実行時にキャッシュをクリアし、新しいプロジェクト構造を検出します
- サンドボックスの破棄時に、エージェントごとにキャッシュをクリアします
セッションの再利用
既存のセッションを再利用して、プレビューサーバーの重複を防ぎます。
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}`
// Reuse existing session if available
if (info.previewSessionId === sessionId) {
console.log(`Reusing existing preview session: ${sessionId}`)
return
}
// Create new session
info.previewSessionId = sessionId
await sandbox.process.createSession(sessionId)
await sandbox.process.executeSessionCommand(sessionId, {
command: config.command,
async: true
}, 0)
}
利点:
- 「すべてプレビュー」の後に単一エージェントのプレビューを実行しても、サーバーが再利用されます
- 単一エージェントのプレビューは、再起動せずに何度でもクリックできます
- プレビューを繰り返す際のサーバー起動遅延を減らします
サーバーのヘルスチェック
待機時間を固定せず、サーバーの準備状況をポーリングします。
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()
// Any HTTP response (200, 404, etc.) indicates server is ready
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
}
使用方法:
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 インストールパターン
検証、フォールバック、再試行ロジックを備えた堅牢な CLI インストールです。
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 {
// Try curl install first
const response = await sandbox.process.executeCommand(installCmd)
if (response.exitCode !== 0) {
throw new Error(`Install failed: ${response.result}`)
}
// Verify installation
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 // Success
} catch (err) {
// Fallback: try npm install
try {
const response = await sandbox.process.executeCommand(
'npm install -g @anthropic-ai/claude-code',
undefined, undefined,
300 // 5 min timeout for npm
)
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 // Success
} catch (npmErr) {
lastError = npmErr instanceof Error ? npmErr : new Error(String(npmErr))
const errorMsg = lastError.message
// Check if retryable (network errors)
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
}
// Exponential backoff: 5s, 10s, 20s
const delayMs = 5000 * Math.pow(2, attempt - 1)
await new Promise(r => setTimeout(r, delayMs))
}
}
}
throw lastError || new Error('Installation failed')
}
要点:
- curl の失敗を伝播させるため、bash では
set -o pipefailを使用します - インストール後に
--versionで確認し、CLI が実行可能であることを検証します - npm インストールにはタイムアウトを設定します(300秒を推奨)。ネットワークが遅い場合があります
- ネットワークエラー(ETIMEDOUT、ECONNRESET など)が発生した場合は、最大3回再試行します
- 指数バックオフにより、障害が発生しているサービスへの過剰な負荷を防ぎます
- Daytona サンドボックスでは、作成後にネットワークの初期化に時間がかかる場合があります
ベストプラクティス
- タイムアウト:長時間実行されるコマンドには適切なタイムアウトを設定します(Claude の場合は15分)
- クリーンアップ:必ず finally ブロックでサンドボックスを破棄します
- 並列作成:複数のサンドボックスを作成するには Promise.all() を使用します
- セッションストリーミング:リアルタイム出力にはセッションベースの実行を使用します(MVP では未対応)
- エフェメラルモード:自動クリーンアップが必要な場合は
ephemeral: trueを設定します - CLI の検証:インストールコマンドの実行後は、必ず CLI ツールがインストールされていることを検証します
- エラーの伝播:適切なエラー処理を確実に行うため、終了コードが0以外の場合は例外をスローします