Skip to main content

Overview

Magpie supports two sandbox execution modes:
  1. Local — commands run via std::process::Command (default)
  2. Daytona — commands run in remote Daytona sandboxes via REST API (feature-gated)
Daytona sandboxes provide full isolation for concurrent pipeline runs, with support for pre-built snapshots and warm pools for fast acquisition.

Sandbox Trait

All command execution routes through the Sandbox trait:
#[async_trait]
pub trait Sandbox: Send + Sync {
    fn name(&self) -> &str;
    fn working_dir(&self) -> &str;
    async fn exec(&self, command: &str, args: &[&str]) -> Result<ExecOutput>;
    async fn exec_shell(&self, shell_cmd: &str) -> Result<ExecOutput>;
    async fn read_file(&self, path: &str) -> Result<Vec<u8>>;
    async fn write_file(&self, path: &str, content: &[u8]) -> Result<()>;
    async fn destroy(&self) -> Result<()>;
}
Source: crates/magpie-core/src/sandbox/mod.rs:62-90

DaytonaConfig

Configuration for creating Daytona sandboxes:
pub struct DaytonaConfig {
    pub api_key: String,
    pub base_url: String,
    pub organization_id: Option<String>,
    pub sandbox_class: String,
    pub snapshot_name: Option<String>,
    pub env_vars: HashMap<String, String>,
    pub volume_id: Option<String>,
    pub volume_mount_path: Option<String>,
}
Source: crates/magpie-core/src/sandbox/mod.rs:45-60

Environment Variables

Basic Settings

DAYTONA_API_KEY
string
required
Daytona API key for authentication.Example:
DAYTONA_API_KEY=daytona_abc123...
Note: When set, all pipeline commands execute inside a Daytona sandbox instead of locally.
DAYTONA_BASE_URL
string
default:"https://app.daytona.io/api"
Daytona API base URL.Example:
DAYTONA_BASE_URL=https://daytona.company.com/api
DAYTONA_ORGANIZATION_ID
string
Daytona organization ID (for multi-org accounts).Example:
DAYTONA_ORGANIZATION_ID=org-123456
DAYTONA_SANDBOX_CLASS
string
default:"small"
Sandbox size class. Available options depend on your Daytona plan:
  • small — 2 CPU, 4 GB RAM
  • medium — 4 CPU, 8 GB RAM
  • large — 8 CPU, 16 GB RAM
Example:
DAYTONA_SANDBOX_CLASS=large

Snapshots (Pre-built Images)

DAYTONA_SNAPSHOT_NAME
string
Daytona snapshot name to create sandboxes from. Snapshots are pre-built images with:
  • Repository cloned
  • Dependencies installed (e.g. cargo fetch, npm install)
  • Services configured
Example:
DAYTONA_SNAPSHOT_NAME=magpie-self
Performance:
  • Cold clone + build: 5-10 minutes
  • Snapshot-based creation: 5-10 seconds
Use Case: Production deployments where fast sandbox acquisition is critical.
DAYTONA_ENV
string
Comma-separated KEY=VALUE pairs to inject into sandboxes at creation time.Example:
DAYTONA_ENV="CLAUDE_CODE_OAUTH_TOKEN=sk-ant-...,GH_TOKEN=github_pat_...,DATABASE_URL=postgres://..."
Use Case: Pass API keys and secrets to sandboxes without baking them into snapshots.Parsing:
let env_str = std::env::var("DAYTONA_ENV")?;
let env: HashMap<String, String> = env_str
    .split(',')
    .filter_map(|pair| {
        let (k, v) = pair.split_once('=')?;
        Some((k.trim().to_string(), v.trim().to_string()))
    })
    .collect();
Source: crates/magpie-cli/src/main.rs (DAYTONA_ENV parsing)

Persistent Volumes (Build Cache)

DAYTONA_VOLUME_ID
string
Persistent volume UUID for build cache. Preserves compiled artifacts across sandbox instances:
  • Rust: target/
  • Node.js: node_modules/, .next/
  • Python: .venv/, __pycache__/
Example:
DAYTONA_VOLUME_ID=12345678-1234-1234-1234-123456789abc
Performance: Speeds up builds by 3-10x (Rust incremental builds, npm/cargo cache hits).
DAYTONA_VOLUME_MOUNT_PATH
string
Mount point for the persistent volume inside the sandbox.Example:
DAYTONA_VOLUME_MOUNT_PATH=/workspace/target
Note: Must be set if DAYTONA_VOLUME_ID is set.

Creating Sandboxes

Cold Creation (Clone + Build)

let sandbox = DaytonaSandbox::create(&config, "my-org/my-repo").await?;
Process:
  1. Create sandbox via Daytona API
  2. Clone repo with gh repo clone my-org/my-repo /workspace/my-org-my-repo
  3. Sandbox is ready for command execution
Time: 2-5 minutes (depends on repo size and network speed) Source: crates/magpie-core/src/sandbox/daytona.rs:33-88

Snapshot-based Creation (Fast)

let sandbox = DaytonaSandbox::create_from_snapshot(
    &config,
    "magpie-self",
    "/workspace/magpie",
    env_vars,
    volumes,
).await?;
Process:
  1. Create sandbox from pre-built snapshot
  2. Wait for sandbox to reach Started state (large images: up to 5 minutes)
  3. Configure git (safe.directory, user identity, gh auth setup-git)
  4. Fix workspace permissions
Time: 5-10 seconds (after image pull) Source: crates/magpie-core/src/sandbox/daytona.rs:90-221

Warm Pool

The warm pool maintains pre-provisioned sandboxes for fast acquisition.

WarmPoolConfig

pub struct WarmPoolConfig {
    pub pool_size: usize,
    pub refresh_interval_secs: u64,
    pub daytona: DaytonaConfig,
    pub repos: Vec<RepoPoolConfig>,
}
pool_size
usize
How many sandboxes to keep per repo.Example: pool_size: 3 maintains 3 idle sandboxes for each configured repo.
refresh_interval_secs
u64
How often (seconds) to refresh idle workspaces with git fetch + build check.Example: refresh_interval_secs: 300 (5 minutes)
repos
Vec<RepoPoolConfig>
Per-repo configurations (see below).
Source: crates/magpie-core/src/sandbox/pool.rs:113-124

RepoPoolConfig

pub struct RepoPoolConfig {
    pub repo_name: String,
    pub repo_full_name: String,
    pub snapshot_name: String,
    pub sandbox_class: String,
    pub base_branch: String,
    pub build_check_cmd: Option<String>,
    pub volume_id: Option<String>,
    pub volume_mount_path: Option<String>,
    pub env_vars: HashMap<String, String>,
}
repo_name
String
Short repo name (e.g. "bnplx").
repo_full_name
String
Full repo name with org (e.g. "niteshballa/bnplx").
snapshot_name
String
Daytona snapshot to create sandboxes from.Example: "bnplx-snapshot"
sandbox_class
String
Sandbox size override (per-repo).Example: "large" for heavy builds, "small" for docs-only repos.
base_branch
String
Branch to track for refresh.Example: "main"
build_check_cmd
Option<String>
Command to verify build health during refresh.Example: Some("cargo check".to_string())
volume_id
Option<String>
Persistent volume UUID for this repo.
volume_mount_path
Option<String>
Volume mount path for this repo.
env_vars
HashMap<String, String>
Repo-specific environment variables (merged with pool-level env vars).
Source: crates/magpie-core/src/sandbox/pool.rs:86-108

Pool Lifecycle

1. Provision
let pool = Arc::new(WarmPool::new(config));
pool.provision().await?;
Creates pool_size sandboxes per repo from snapshots, then runs post-provision commands (service start, migrations, etc.). Source: crates/magpie-core/src/sandbox/pool.rs:161-198 2. Acquire
let workspace = pool.acquire("my-org/my-repo").await?;
Returns an idle sandbox for the given repo, or None if the pool is exhausted. Source: crates/magpie-core/src/sandbox/pool.rs:295-309 3. Release
pool.release(&workspace, "main").await?;
Resets git state (checkout base, clean) while preserving build artifacts, then marks the workspace as idle. Source: crates/magpie-core/src/sandbox/pool.rs:314-336 4. Refresh Loop
pool.start_refresh_loop().await;
Background task that periodically runs git fetch + build check on all idle workspaces. Source: crates/magpie-core/src/sandbox/pool.rs:341-428 5. Shutdown
pool.shutdown().await;
Stops the refresh loop and destroys all sandboxes. Source: crates/magpie-core/src/sandbox/pool.rs:431-457

Example Configurations

Local Execution (Default)

# No Daytona env vars → commands run locally
MAGPIE_REPO_DIR=/home/user/my-project

Daytona Cold Clone

DAYTONA_API_KEY=daytona_abc123...
DAYTONA_BASE_URL=https://app.daytona.io/api
DAYTONA_SANDBOX_CLASS=small

MAGPIE_GITHUB_ORG=my-company

Daytona Snapshot (Fast)

DAYTONA_API_KEY=daytona_abc123...
DAYTONA_BASE_URL=https://app.daytona.io/api
DAYTONA_SANDBOX_CLASS=large
DAYTONA_SNAPSHOT_NAME=magpie-self
DAYTONA_ENV="CLAUDE_CODE_OAUTH_TOKEN=sk-ant-...,GH_TOKEN=github_pat_..."

MAGPIE_GITHUB_ORG=my-company

Daytona with Persistent Volume

DAYTONA_API_KEY=daytona_abc123...
DAYTONA_SANDBOX_CLASS=large
DAYTONA_SNAPSHOT_NAME=magpie-self
DAYTONA_VOLUME_ID=12345678-1234-1234-1234-123456789abc
DAYTONA_VOLUME_MOUNT_PATH=/workspace/target
DAYTONA_ENV="CLAUDE_CODE_OAUTH_TOKEN=sk-ant-...,GH_TOKEN=github_pat_..."

Warm Pool (Code)

let pool_config = WarmPoolConfig {
    pool_size: 2,
    refresh_interval_secs: 300,
    daytona: daytona_config,
    repos: vec![
        RepoPoolConfig {
            repo_name: "bnplx".to_string(),
            repo_full_name: "my-org/bnplx".to_string(),
            snapshot_name: "bnplx-snapshot".to_string(),
            sandbox_class: "large".to_string(),
            base_branch: "main".to_string(),
            build_check_cmd: Some("cargo check".to_string()),
            volume_id: Some("12345678-1234-1234-1234-123456789abc".to_string()),
            volume_mount_path: Some("/workspace/target".to_string()),
            env_vars: HashMap::new(),
        },
    ],
};

let pool = Arc::new(WarmPool::new(pool_config));
pool.provision().await?;
pool.start_refresh_loop().await;

// Use in PipelineConfig
let config = PipelineConfig {
    pool: Some(pool),
    ..Default::default()
};

Troubleshooting

Sandbox Creation Timeout

Cause: Large snapshot images (4+ GB) can take up to 5 minutes to start. Fix: The pipeline waits up to 300 seconds. If this isn’t enough, check Daytona API logs.

Git Operations Fail

Cause: Snapshot-based sandboxes need safe.directory configured. Fix: DaytonaSandbox::create_from_snapshot handles this automatically via setup commands. Source: crates/magpie-core/src/sandbox/daytona.rs:172-194

Permission Denied

Cause: Snapshot image was built as root but sandbox runs as daytona user. Fix: Setup commands run sudo chmod -R 777 /workspace/magpie to fix ownership. Important: Only chmod the top-level directory, NOT recursive on large dirs (triggers copy-on-write, fills disk). Source: crates/magpie-core/src/sandbox/daytona.rs:160-182

Build Cache Miss

Cause: Persistent volume not attached. Fix: Verify DAYTONA_VOLUME_ID and DAYTONA_VOLUME_MOUNT_PATH are set correctly.

Build docs developers (and LLMs) love