mcpField
D
daytona-integration

daytona-integration

github.com/seanchiuaiクラウド・DevOps038 回インストール40 週間前に更新
リポジトリ

daytona-integration について

Daytona SDKでリモートサンドボックスを管理する開発者向けのTypeScript統合ガイドです。サンドボックスのライフサイクル、プロセス実行、ファイル転送、プレビューサーバーのパターンに加え、設定とエラー処理を説明します。

主な機能

  1. 01サンドボックスの作成と管理
  2. 02ランタイム、リソース、環境の設定
  3. 03コマンド、コード、対話セッションの実行
  4. 04サンドボックスのファイルシステムで転送
  5. 05リトライとプレビューサーバーの処理

活用シーン

  1. 01サンドボックスで開発コマンドを実行
  2. 02並列エージェント用の個別サンドボックスを作成
  3. 03プロジェクトファイルをアップロードして出力を取得
  4. 04プレビューサーバーを起動してURLを取得

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/seanchiuai/multishot --skill daytona-integration

インストール後、Claude Code で/daytona-integrationを入力して読み込まれたか確認します。

詳細

Daytona 統合

出典:Daytona TypeScript SDK Docs

インストール

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 インターフェース
プロパティ型必須説明
apiKeystringいいえ*API キー認証
apiUrlstringいいえAPI エンドポイント(デフォルト:https://app.daytona.io/api)
jwtTokenstringいいえ*JWT 認証(organizationId が必要)
organizationIdstringいいえJWT 使用時に必須
targetstringいいえサンドボックスの場所の優先設定

*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'ランタイム言語
envVarsRecord<string, string>{}環境変数
autoStopIntervalnumber15停止するまでのアイドル時間(分)
autoArchiveIntervalnumber10080アーカイブまでの時間(分)
autoDeleteIntervalnumber-削除までの時間(分)
labelsRecord<string, string>-メタデータタグ
ephemeralbooleanfalse停止時に自動クリーンアップ
volumesVolumeMount[]-ボリュームのマウント
imagestring | Image-カスタム Docker イメージ
resourcesResources-CPU/メモリ/ディスクの割り当て
snapshotstring-スナップショット 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 クラス

プロパティ
プロパティ型説明
idstring一意のサンドボックス識別子
namestringサンドボックス名
stateSandboxState'started' | 'stopped' | ...
cpunumberCPU 数
memorynumberメモリ容量(GiB)
disknumberディスク容量(GiB)
gpunumberGPU 数
envRecord<string, string>環境変数
labelsRecord<string, string>メタデータ
processProcessプロセス実行インターフェース
fsFileSystemファイルシステムインターフェース
gitGitGit 操作インターフェース
メソッド
// 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 インターフェース
プロパティ型説明
exitCodenumberプロセスの終了ステータス
resultstringstdout の内容
artifactsExecutionArtifacts追加データ(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 サンドボックスでは、作成後にネットワークの初期化に時間がかかる場合があります

ベストプラクティス

  1. タイムアウト:長時間実行されるコマンドには適切なタイムアウトを設定します(Claude の場合は15分)
  2. クリーンアップ:必ず finally ブロックでサンドボックスを破棄します
  3. 並列作成:複数のサンドボックスを作成するには Promise.all() を使用します
  4. セッションストリーミング:リアルタイム出力にはセッションベースの実行を使用します(MVP では未対応)
  5. エフェメラルモード:自動クリーンアップが必要な場合は ephemeral: true を設定します
  6. CLI の検証:インストールコマンドの実行後は、必ず CLI ツールがインストールされていることを検証します
  7. エラーの伝播:適切なエラー処理を確実に行うため、終了コードが0以外の場合は例外をスローします

コメント