From sandbox-blueprint
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.
npx claudepluginhub tangle-network/skills --plugin sandbox-blueprintThis skill uses the workspace's default tool permissions.
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.
Triggers research for existing libraries, tools, and patterns before coding new features. Searches npm, PyPI, MCP/skills, GitHub; evaluates matches and decides adopt/extend/build.
Audits cross-stack repos (C++/Android/iOS/Web), classifies files as project/third-party/artifacts, detects embedded libraries, assigns module verdicts, generates interactive HTML reports.
Reorganizes X and LinkedIn networks: review-first pruning of low-value follows, priority-based add/follow recommendations, and drafts warm outreach in user's voice.
Share bugs, ideas, or general feedback.
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());
tokio::spawn(server);
req-{counter:016x}, propagates via task-local/api/auth/challenge POST — request EIP-191 challenge
/api/auth/verify POST — exchange signature for session token
/api/sandboxes GET — list sandboxes (owner-filtered)
/api/sandboxes/{id} GET — sandbox details
/api/sandboxes/{id}/secrets POST — inject secrets (phase 2)
/api/sandboxes/{id}/stop POST — stop sandbox
/api/sandboxes/{id}/resume POST — resume from any tier
/api/provisions/{call_id} GET — provision progress
/api/health GET — operator health
Two background loops:
Enforces timeouts on running sandboxes:
idle_timeout_seconds → soft stopmax_lifetime_seconds → hard deleteTiered demotion of stopped sandboxes:
| Transition | Retention | Action |
|---|---|---|
| Hot → Warm | 1 day | docker commit (preserve filesystem) |
| Warm → Cold | 2 days | TAR upload to S3 |
| Cold → Gone | 7 days | S3 object delete |
User BYOS3 snapshots are never deleted (distinguished via SANDBOX_SNAPSHOT_DESTINATION_PREFIX).
Cleans expired PASETO challenges and session tokens.
Three-state circuit breaker per sandbox for sidecar health:
Closed (healthy) → Open (failed, cooldown) → Half-Open (probe) → Closed
CIRCUIT_BREAKER_COOLDOWN_SECS (default 30s)pub struct PersistentStore<V> {
db: LocalDatabase,
// File-based JSON at $BLUEPRINT_STATE_DIR/ (default: ./blueprint-state)
// RwLock-protected for concurrent tokio task access
}
// Operations:
store.get(id) -> Option<V>
store.find(predicate) -> Vec<V>
store.values() -> Vec<V>
store.insert(id, value)
store.remove(id)
store.update(id, |v| { /* mutate */ })
Records encrypted at rest via ChaCha20-Poly1305 (key from SESSION_AUTH_SECRET).
Atomic counters for telemetry:
// Core
total_jobs, total_duration_ms, total_input_tokens, total_output_tokens
// Resources
active_sandboxes, peak_sandboxes, allocated_cpu_cores, allocated_memory_mb
// Lifecycle
reaped_idle, reaped_lifetime, garbage_collected, snapshot_count
Optional QoS integration: periodic snapshot + on-chain submission (gated by qos feature flag).
contract MyBlueprint is BlueprintServiceManagerBase {
// Mode flags
bool public immutable instanceMode;
bool public immutable teeRequired;
// State
mapping(address => uint32) public operatorCapacity;
mapping(bytes32 => address) public sandboxOperator;
// Pricing multipliers per job
uint256 constant CREATE_MULTIPLIER = 50;
uint256 constant DELETE_MULTIPLIER = 1;
// Events
event SandboxCreated(bytes32 indexed sandboxId, address indexed operator);
event OperatorProvisioned(uint64 indexed serviceId, address indexed operator);
}
Deploy the same contract 3x for cloud/instance/TEE-instance modes with different constructor flags.
#[tokio::main]
async fn main() -> Result<()> {
// 1. Logging
setup_tracing();
// 2. Auth validation
session_auth::validate_required_config()?;
// 3. Optional QoS
let qos = init_qos_if_enabled().await;
// 4. Optional TEE backend
let tee = backend_factory::backend_from_env().await?;
// 5. Blueprint environment
let env = BlueprintEnvironment::load()?;
// 6. Tangle client
let client = env.tangle_client().await?;
// 7. BPM bridge
let bpm = connect_bpm(&env).await?;
// 8. Operator API (spawned)
let router = operator_api_router();
let listener = TcpListener::bind((bind_addr, api_port)).await?;
tokio::spawn(axum::serve(listener, router).with_graceful_shutdown(shutdown.clone()));
// 9. Reconciliation
reconcile_on_startup(&store).await?;
// 10. Background services (spawned)
spawn_reaper(store.clone(), interval);
spawn_gc(store.clone(), interval);
spawn_session_gc(interval);
// 11. BlueprintRunner
BlueprintRunner::new(env)
.router(router())
.producer(TangleProducer::new(client.clone()))
.consumer(TangleConsumer::new(client))
.run()
.await?;
}
| Variable | Default | Purpose |
|---|---|---|
SIDECAR_IMAGE | ghcr.io/tangle-network/sidecar:latest | Container image |
SIDECAR_PUBLIC_HOST | 127.0.0.1 | Sidecar URL hostname |
SIDECAR_HTTP_PORT | 8080 | Container internal port |
SIDECAR_PULL_IMAGE | true | Pull image on first use |
SANDBOX_DEFAULT_IDLE_TIMEOUT | 1800 (30m) | Default idle timeout |
SANDBOX_DEFAULT_MAX_LIFETIME | 86400 (1d) | Default max lifetime |
SANDBOX_MAX_IDLE_TIMEOUT | 7200 (2h) | Operator cap on idle |
SANDBOX_MAX_MAX_LIFETIME | 172800 (2d) | Operator cap on lifetime |
SANDBOX_REAPER_INTERVAL | 30 | Reaper tick (seconds) |
SANDBOX_GC_INTERVAL | 3600 | GC tick (seconds) |
SANDBOX_GC_HOT_RETENTION | 86400 | Hot → Warm (seconds) |
SANDBOX_GC_WARM_RETENTION | 172800 | Warm → Cold (seconds) |
SANDBOX_GC_COLD_RETENTION | 604800 | Cold → Gone (seconds) |
SANDBOX_SNAPSHOT_AUTO_COMMIT | true | Docker commit on stop |
SANDBOX_SNAPSHOT_DESTINATION_PREFIX | (none) | Operator S3 prefix |
SESSION_AUTH_SECRET | (required) | 32-byte hex for PASETO + encryption |
OPERATOR_API_PORT | 9090 | Operator API bind port |
TEE_BACKEND | (none) | phala/nitro/gcp/azure/direct |
ALLOW_STANDALONE | false | Dev-only: bypass BPM connection |
OPERATOR_MAX_CAPACITY | (none) | Advertised operator capacity |
BLUEPRINT_STATE_DIR | ./blueprint-state | Persistent store directory |
metadata_json.runtime_backend, cannot change.FIRECRACKER_SIDECAR_AUTH_DISABLED must be set.@tangle-network/agent-ui provides sandbox-specific frontend components for agent chat, terminal, and sidecar auth. This is distinct from @tangle-network/blueprint-ui (general blueprint frontend — see blueprint-frontend skill).
Source: packages/agent-ui
Entry points:
@tangle-network/agent-ui — components, hooks, types@tangle-network/agent-ui/primitives — small helpers@tangle-network/agent-ui/terminal — lazy xterm.js terminal@tangle-network/agent-ui/styles — stylesheetimport { useSidecarAuth, useWagmiSidecarAuth } from '@tangle-network/agent-ui';
// Generic (any signing method):
const { token, isAuthenticated, authenticate } = useSidecarAuth(sidecarUrl, signMessage);
// Wagmi adapter:
const auth = useWagmiSidecarAuth(sidecarUrl);
import { ChatContainer, useSessionStream } from '@tangle-network/agent-ui';
const { messages, partMap, isStreaming, send, abort } = useSessionStream({
sidecarUrl,
sessionId,
token,
});
<ChatContainer
messages={messages}
partMap={partMap}
isStreaming={isStreaming}
onSend={send}
branding={{ name: 'My Agent', icon: '...' }}
/>
import { TerminalView } from '@tangle-network/agent-ui/terminal';
import { usePtySession } from '@tangle-network/agent-ui';
const pty = usePtySession(sidecarUrl, token);
<TerminalView session={pty} />
// Lazy-loads xterm.js (~333KB)
Apps should support both direct sidecar access (local dev) and proxied operator API (production):
// Direct mode (local testing):
const client = createDirectClient('http://localhost:32768', authToken);
await client.prompt('hello'); // POST /agent/prompt
// Proxied mode (production, through operator):
const client = createProxiedClient('sandbox-id', pasetoToken, 'http://operator:9090');
await client.prompt('hello'); // POST /api/sandboxes/sandbox-id/prompt
Pattern from ui/src/lib/api/sandboxClient.ts.
For blueprints that serve UI from the operator binary (Rust):
// In operator_api.rs:
use include_dir::{include_dir, Dir};
static CONTROL_PLANE_UI_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../control-plane-ui");
// Serve at root:
// GET / → index.html
// GET /app.js, /styles.css, /assets/* → static files
Build: cd ui && pnpm run build:embedded compiles React app into control-plane-ui/ directory, which gets embedded at cargo build time.
src/index.ts — public APIsrc/hooks/useSidecarAuth.ts — EIP-191 + PASETO authsrc/hooks/useSessionStream.ts — SSE message streamingsrc/hooks/usePtySession.ts — PTY terminalsrc/components/chat/ChatContainer.tsx — full chat UI| Blueprint | Location | Notes |
|---|---|---|
| ai-agent-sandbox-blueprint | tangle-network/ai-agent-sandbox-blueprint | Production reference. 5 jobs, Docker/Firecracker/TEE. |
| ai-trading-blueprints | tangle-network/ai-trading-blueprint | Specialized DeFi variant. 12 jobs, adds validator committee + protocol adapters. Shares sandbox-runtime. |
| openclaw-sandbox-blueprint | tangle-network/openclaw-sandbox-blueprint | Embedded UI variant. Serves React app from operator binary via include_dir!. |
| microvm-blueprint | tangle-network/microvm-blueprint | Minimal reference. 5 lifecycle jobs + Axum query service. No sidecar SDK. |
src/runtime.rs — container lifecycle, CreateSandboxParams, SandboxRecordsrc/operator_api.rs — Axum router, request ID, security headers, rate limitingsrc/session_auth.rs — EIP-191 + PASETOsrc/scoped_session_auth.rs — sandbox/instance scope enforcementsrc/auth.rs — sidecar bearer token validationsrc/store.rs — PersistentStore (JSON + RwLock)src/reaper.rs — idle/lifetime enforcement, tiered GCsrc/circuit_breaker.rs — three-state circuit breakersrc/metrics.rs — atomic counterssrc/provision_progress.rs — multi-phase progress trackingsrc/secret_provisioning.rs — two-phase secret injectionsrc/contracts.rs — SandboxProvider + RuntimeAdapter traitssrc/http.rs — sidecar HTTP clientsrc/tee/mod.rs — TeeBackend traitsrc/firecracker.rs — host-agent integrationsrc/error.rs — SandboxError enumsrc/lib.rs — Router, ABI types, job constantssrc/jobs/sandbox.rs — create/delete handlerssrc/main.rs — BlueprintRunner wiring, background servicescontracts/src/AgentSandboxBlueprint.sol — BSM with mode flags