Use when building a sandbox-style Tangle Blueprint — container/VM provisioning, lifecycle management, operator API, session auth, secret provisioning, sidecar integration, tiered GC, and BSM contracts. Based on the production ai-agent-sandbox-blueprint.
Use this skill when building a Tangle Blueprint that provisions and manages sandbox containers or VMs. This captures the production-proven patterns from ai-agent-sandbox-blueprint and ai-trading-blueprints — the architecture for any blueprint that manages compute instances with sidecars.
For the Rust SDK primitives (Router, TangleLayer, BlueprintRunner), see tangle-blueprint-expert.
For the general blueprint frontend (job submission, operator discovery, blueprint-ui), see blueprint-frontend.
For the sandbox SDK packages (providers, sessions, streaming), see sandbox-sdk.
Sandbox blueprints use a three-layer crate hierarchy:
{name}-runtime/ (L1: stable runtime contracts, reusable across blueprints)
├── runtime.rs — container lifecycle, CreateSandboxParams, SandboxRecord
├── operator_api.rs — Axum HTTP router, middleware, rate limiting
├── session_auth.rs — EIP-191 + PASETO session management
├── scoped_session_auth.rs — sandbox/instance scope enforcement
├── auth.rs — sidecar bearer token validation
├── store.rs — PersistentStore (JSON filesystem, RwLock)
├── reaper.rs — idle/lifetime enforcement, tiered GC
├── circuit_breaker.rs — three-state circuit breaker
├── metrics.rs — atomic counters for telemetry
├── provision_progress.rs — multi-phase progress tracking
├── secret_provisioning.rs — two-phase secret injection
├── contracts.rs — SandboxProvider + RuntimeAdapter traits
├── http.rs — sidecar HTTP client with auth headers
├── tee/ — TEE backend trait + implementations
├── firecracker.rs — Firecracker host-agent integration
└── error.rs — typed error taxonomy
{name}-blueprint-lib/ (L2: product-specific job handlers)
├── lib.rs — Router setup, ABI types, job ID constants
├── jobs/ — per-job handler functions
└── state.rs — product-specific state helpers
{name}-blueprint-bin/ (L3: binary entry point)
└── main.rs — BlueprintRunner wiring, background services, startup
Variant pattern: A single runtime crate can support multiple deployment modes:
Each mode gets its own lib + bin crate pair sharing the same runtime.
On-chain jobs (state-changing mutations only):
Operator API (everything else):
Rule: Jobs mutate state. Reads and operational I/O go through the operator HTTP API. Never put secrets or large payloads in on-chain job calldata.
use blueprint_sdk::Router;
use blueprint_sdk::tangle::layers::TangleLayer;
pub const JOB_CREATE: u32 = 0;
pub const JOB_DELETE: u32 = 1;
pub fn router() -> Router {
Router::new()
.route(JOB_CREATE, create_instance.layer(TangleLayer))
.route(JOB_DELETE, delete_instance.layer(TangleLayer))
}
Job handlers extract on-chain arguments via TangleArg<T> and return results via TangleResult<T>:
use blueprint_sdk::tangle::extract::{TangleArg, TangleResult, Caller, CallId};
use blueprint_sdk::tangle::layers::TangleLayer;
pub async fn create_instance(
TangleArg(request): TangleArg<CreateRequest>,
caller: Caller,
call_id: CallId,
context: Context<RuntimeState>,
) -> TangleResult<CreateOutput> {
// Validate caller is service owner
// Create container via runtime adapter
// Track provision progress
// Return result
Ok(TangleResult(output))
}
ABI types use sol! macro for on-chain encoding:
use alloy_sol_types::sol;
sol! {
struct CreateRequest {
string name;
string image;
uint64 cpu_cores;
uint64 memory_mb;
uint64 disk_gb;
string metadata_json;
}
struct CreateOutput {
string sandbox_id;
string sidecar_url;
string token;
}
}
Multi-phase provisioning with progress tracking:
SIDECAR_PULL_IMAGE=true and image not cached// Phases: Queued → ImagePull → ContainerCreate → ContainerStart → HealthCheck → Ready | Failed
// Queryable via operator API: GET /api/provisions/{call_id}
// Polls every 2s from frontend via useProvisionProgress()
Two primary states with tiered storage transitions:
┌──────────┐
create → │ Running │ ← resume (from any tier)
└────┬─────┘
│ stop (idle timeout / manual / max lifetime)
▼
┌──────────┐
│ Stopped │
└────┬─────┘
│ GC tiers (automatic)
▼
Hot (container) ──1d──→ Warm (committed image)
Warm ──2d──→ Cold (S3 snapshot)
Cold ──7d──→ Gone (deleted)
docker commit preserves filesystem statepub struct SandboxRecord {
pub id: String,
pub owner: String, // immutable after creation
pub sidecar_url: Option<String>,
pub token: String,
pub state: SandboxState, // Running | Stopped
pub created_at: i64,
pub last_activity_at: i64,
pub max_lifetime_seconds: u64,
pub idle_timeout_seconds: u64,
pub cpu_cores: u64,
pub memory_mb: u64,
pub disk_gb: u64,
pub snapshot_image_id: Option<String>, // Warm tier
pub snapshot_s3_url: Option<String>, // Cold tier
pub tee_deployment_id: Option<String>,
pub base_env_json: Option<String>,
pub user_env_json: Option<String>,
}
Authorization: Bearer {token} headersubtle::ConstantTimeEq)x-request-id header (unique per request)const SIDECAR_EXEC_TIMEOUT: Duration = Duration::from_secs(30);
const SIDECAR_AGENT_TIMEOUT: Duration = Duration::from_secs(90); // LLM inference
const SIDECAR_DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
Three-tier auth model:
Client: POST /api/auth/challenge → { challenge, expires_at }
Client: Signs challenge with wallet (personal_sign)
Client: POST /api/auth/verify → { signature, challenge } → { token, expires_at }
SESSION_AUTH_SECRET (32-byte hex, required in production){ address, scope, issued_at, expires_at }sandbox:{id} (cloud mode) or instance:{id} (instance mode)Secrets never appear in on-chain calldata:
Phase 1 (on-chain): JOB_CREATE with base_env_json only (non-sensitive config)
Phase 2 (off-chain): Secrets injected via operator API
POST /api/sandboxes/{id}/secrets → container recreated with merged envPOST /api/sandboxes/{id}/tee/sealed-secrets → client encrypts to TEE public keyPhase 1: On-chain create → base config only
Phase 2: Off-chain inject → secrets merged, container recreated
Key functions: inject_secrets(), wipe_secrets(), merge_env_json()
Invariant: Sandbox identity (ID, token) is preserved across secret injection — the container is recreated but the record is the same.
pub trait TeeBackend: Send + Sync {
async fn deploy(&self, params: TeeDeployParams) -> Result<TeeDeployment>;
async fn attestation(&self, deployment_id: &str) -> Result<TeeAttestation>;
async fn stop(&self, deployment_id: &str) -> Result<()>;
async fn destroy(&self, deployment_id: &str) -> Result<()>;
fn tee_type(&self) -> TeeType;
// Optional: sealed secrets support
}
Backends: phala (dstack), aws_nitro, gcp, azure, direct (local TDX/SEV)
Selected via TEE_BACKEND env var. Backend factory in tee/backend_factory.rs.
Axum-based HTTP server running alongside the BlueprintRunner:
// In main.rs:
let router = operator_api_router();
let listener = TcpListener::bind((bind_addr, api_port)).await?;
let server = axum::serve(listener, router).with_graceful_shutdown(shutdown_signal());