Skip to main content
← back to blog

Introducing Kalahari: MicroVM Isolation for Agent Code

Kalahari is an agent sandbox SDK that runs OCI images in a microVM on Linux (KVM) and macOS (Hypervisor.framework), with no remote service required.

kalahari release developer-tools virtualization security

Any agent task that executes untrusted code, generated code, or unreviewed dependencies should run behind a real machine boundary. The reason most do not is simple: real isolation has historically been infrastructure, not a library.

Containers are fast and convenient, but they share the host kernel. For untrusted agent code, that is the wrong boundary.

Remote sandbox services offer stronger isolation, but they put a remote control path in front of sandbox operations. They do not work offline, they complicate CI, and they require sending your code and state to a vendor.

Kalahari is the option that has been missing: microVM isolation as an SDK you import. It runs locally on macOS through Hypervisor.framework and on Linux through KVM, with the same SDK and code path from laptop to production.

import { KalahariClient } from '@amlalabs/kalahari';

const client = new KalahariClient({ image: 'node:22-alpine' });
const sandbox = await client.createSandbox();

await sandbox.writeFile('/workspace/app.js', 'console.log("hello")\n');

const result = await sandbox.run('node', {
  args: ['/workspace/app.js'],
});

console.log(result.stdout);
await sandbox.destroy();

Each createSandbox() returns a microVM with its own guest kernel, filesystem, and process tree. The workload does not share the host kernel; its path to the host is limited to the VM boundary and the explicit devices and policies Kalahari exposes. Traditional container-escape techniques target a shared host kernel; Kalahari moves the workload behind a VM boundary instead. It is not a replacement for least privilege, patching, or host hardening; it gives each task a VM boundary instead of a shared-kernel container boundary.

Kalahari pulls standard OCI images directly from public registries such as Docker Hub and GHCR, without requiring a local Docker daemon. Sandboxes are created and controlled from your process, with no daemon, remote service, or sandbox server to operate.

Boot Is Not the Cost. Preparation Is

For a cached image, VM startup can be small enough not to dominate many agent workloads. That is not what makes per-task isolation expensive.

The cost that actually dominates is preparing the environment: npm install, pulling test fixtures, warming a cache, restoring a snapshot of project state. That work takes seconds to minutes, and re-doing it for every agent task is what makes “every task in its own VM” sound impractical.

Kalahari’s answer is the zygote. Prepare a sandbox once. Install dependencies, pull data, configure state. Freeze the prepared sandbox into a zygote. Spawn isolated children from it on demand.

await sandbox.writeFile('/workspace/state.txt', 'base\n');

const zygote = await sandbox.zygote();
const [first, second, third] = await Promise.all([zygote.spawn(), zygote.spawn(), zygote.spawn()]);

Each child is a separate microVM cloned from the prepared state, not a process sharing the zygote runtime. Writes in one child are invisible to the others. The zygote is a template, not a shared mutable environment: each child receives the prepared state and then diverges independently. The expensive work happens once, the isolation happens every time.

That is the practical version of the isolation argument: real boundaries, fast enough to make per-task isolation practical in normal workflows, on the same machine you develop on.

A small benchmark on an AMD Ryzen 9 9900X with KVM, running node:22-alpine in a 512 MiB / 1 vCPU microVM with the image already cached. These are single-machine measurements, not a portability promise.

Pathp50p90
Fresh sandbox boot117 ms123 ms
Convert prepared sandbox to zygote45 ms45 ms
Zygote child spawn84 ms101 ms

The bigger win in real agent workloads is what’s not in the table: dependency install and cache warming happen once during zygote preparation, not once per task.

Getting Started

npm install @amlalabs/kalahari
import { KalahariClient } from '@amlalabs/kalahari';

const client = new KalahariClient({ image: 'node:22-alpine' });
const sandbox = await client.createSandbox();

const result = await sandbox.run('node', {
  args: ['-e', 'console.log("hello from kalahari")'],
});

console.log(result.stdout);
await sandbox.destroy();

If you want to know how the snapshot stays correct, how processes survive the parked-VM scheduler, or how filesystem and network policy hold up under pressure, the design notes are here: