← All guides
AI agentsKVMsandboxingREST API

How to Give an AI Agent a Disposable Linux Sandbox

A tested lifecycle pattern for creating an isolated KVM VM, waiting for readiness, running a command, collecting output, and always destroying the VM.

Published · LiteVPS Engineering

An AI agent needs a disposable Linux sandbox when its task requires package installation, native binaries, background processes, or stronger isolation than a shared language runtime provides. The safe lifecycle is: create a VM with a short time-to-live, wait until it is ready, execute bounded work, capture the result, and destroy the VM in a cleanup handler. The time-to-live is a backstop, not a replacement for explicit deletion.

This guide shows that lifecycle against the current LiteVPS API. It deliberately handles the details that short examples often omit: selecting a live plan and OS template, wrapped API responses, readiness, command timeouts, and cleanup after failure.

What are we isolating?

The goal is not to make arbitrary hostile code magically safe. It is to put generated code in a short-lived virtual machine with its own kernel rather than execute it on the agent controller.

A useful trust boundary has at least four parts:

  1. The controller holds the API credential and decides what work is allowed.
  2. The worker VM receives only the input needed for one task.
  3. The command has a time limit and its output is treated as untrusted data.
  4. The worker is destroyed whether the command succeeds or fails.

KVM provides a separate guest kernel. That is a stronger default boundary than running another process or container on the controller, but it does not remove the need for network policy, credential scoping, input validation, and host patching. The Linux KVM documentation describes the virtualization subsystem beneath this model.

What do you need before running the example?

Create an API token in the LiteVPS dashboard and install curl and jq locally. Keep the token on the controller; do not place it inside the worker VM.

export LITEVPS_API="https://api.litevps.dev"
export LITEVPS_TOKEN="vtk_replace_with_your_token"

Discover the current plans and provisionable operating-system templates instead of copying identifiers from an old article:

curl --fail-with-body -sS "$LITEVPS_API/api/pricing" | jq '.plans[] | {
  id, name, vcpu, memory_mb, disk_gb, price_hourly, price_monthly
}'

curl --fail-with-body -sS \
  -H "Authorization: Bearer $LITEVPS_TOKEN" \
  "$LITEVPS_API/api/templates" | jq '.[] | {slug, name, description}'

Choose values returned by those calls:

export LITEVPS_PLAN_ID="replace-with-a-current-plan-id"
export LITEVPS_TEMPLATE="replace-with-a-current-template-slug"

The plan determines CPU, memory, disk, and price. The template selects the operating-system image and may provide initialization defaults. Both are required when creating a VM.

How do you create and always clean up the sandbox?

Save the following as run-sandbox.sh. The trap is installed immediately after the API returns a VM ID. Any later shell error or interruption triggers deletion.

#!/usr/bin/env bash
set -Eeuo pipefail

: "${LITEVPS_API:?Set LITEVPS_API}"
: "${LITEVPS_TOKEN:?Set LITEVPS_TOKEN}"
: "${LITEVPS_PLAN_ID:?Set LITEVPS_PLAN_ID}"
: "${LITEVPS_TEMPLATE:?Set LITEVPS_TEMPLATE}"

auth=(-H "Authorization: Bearer $LITEVPS_TOKEN")
vps_id=""

cleanup() {
  if [[ -n "$vps_id" ]]; then
    curl --fail-with-body -sS -X DELETE "${auth[@]}" \
      "$LITEVPS_API/api/vps/$vps_id" >/dev/null || \
      printf 'warning: deletion failed for VPS %s\n' "$vps_id" >&2
  fi
}
trap cleanup EXIT INT TERM

name="agent-task-$(date +%s)"
create_response="$({
  jq -n \
    --arg name "$name" \
    --arg plan "$LITEVPS_PLAN_ID" \
    --arg template "$LITEVPS_TEMPLATE" \
    '{name:$name, plan_id:$plan, template:$template, ttl_minutes:60}' |
  curl --fail-with-body -sS -X POST "${auth[@]}" \
    -H 'Content-Type: application/json' \
    --data-binary @- \
    "$LITEVPS_API/api/vps"
})"

vps_id="$(jq -er '.vps.id' <<<"$create_response")"
printf 'created %s\n' "$vps_id"

deadline=$((SECONDS + 600))
while (( SECONDS < deadline )); do
  status="$(curl --fail-with-body -sS "${auth[@]}" \
    "$LITEVPS_API/api/vps/$vps_id")"
  if [[ "$(jq -r '.vps.ready' <<<"$status")" == "true" ]]; then
    break
  fi
  sleep 5
done

if [[ "$(jq -r '.vps.ready' <<<"$status")" != "true" ]]; then
  printf 'VM did not become ready before the deadline\n' >&2
  exit 1
fi

jq -n '{command:"python3 -c \\"import platform; print(platform.platform())\\"", timeout_seconds:30}' |
  curl --fail-with-body -sS -X POST "${auth[@]}" \
    -H 'Content-Type: application/json' \
    --data-binary @- \
    "$LITEVPS_API/api/vps/$vps_id/exec" | jq '{stdout, stderr, exit_code, duration_ms}'

Run it with:

chmod +x run-sandbox.sh
./run-sandbox.sh

The create response is an object containing .vps; it is not the bare VM object. The detail response uses the same wrapper. A successful synchronous exec response contains stdout, stderr, exit_code, and duration_ms.

The create request includes ttl_minutes: 60. LiteVPS currently caps TTL according to a platform setting and checks for expired VMs periodically. Explicit deletion remains important because billing stops when the VM is destroyed, not merely when the controller finishes its task.

Why wait for ready instead of running?

running means the virtual machine domain has started. The ready field changes later, after the platform can connect to the guest. A controller that calls exec immediately after creation can race guest startup.

Use a bounded readiness loop. Ten minutes is intentionally conservative in the example; production controllers should choose a deadline based on their workload and report a clear timeout rather than poll forever.

What should you change for production work?

  • Give each task a unique VM name so operators can identify orphaned work.
  • Put a deadline around creation, readiness, and execution.
  • Limit the data and credentials copied into the worker.
  • Validate output before another agent or system consumes it.
  • Log the VM ID, task ID, timestamps, exit code, and cleanup outcome.
  • Reconcile periodically: list VMs with your task naming prefix and investigate anything older than its expected lifetime.
  • Treat TTL as a second cleanup mechanism, not the primary one.
  • Use snapshots only when retrying from a known checkpoint is worth retaining state and cost.

When should you use this pattern?

Use a disposable VM when an agent needs a normal Linux environment, system packages, process isolation, a writable filesystem, or a task-specific operating-system image. It is also useful when the controller must remain small and should never execute generated code itself.

When should you not use this pattern?

Do not create a VM for a pure API call, a database lookup, or a computation that already fits safely in a restricted in-process tool. Do not assume VM isolation permits malware analysis or hostile multi-tenant workloads without a dedicated threat model and additional controls. For millisecond-scale functions, VM startup may dominate total latency.

Use VPS, container, or serverless function for agent code execution? to choose the right boundary before building the worker pool. The LiteVPS platform guide explains the service model, and current pricing should be used for cost calculations rather than values copied into source code.

Methodology note

This guide was checked against the LiteVPS API routing, VM creation, readiness, execution, TTL, and deletion implementations on July 23, 2026. The example avoids fixed plan and template identifiers because those catalogs can change. Test it with non-sensitive work in your account before adapting it to production.