Files
smartvm/readme.md
T

24 KiB

@push.rocks/smartvm

Run Firecracker microVMs from TypeScript without hand-rolling process management, Unix-socket HTTP calls, TAP devices, bridge setup, image caching, and cleanup. @push.rocks/smartvm gives you a typed orchestration layer for building tiny, fast, disk-light VM workflows on Linux/KVM.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

What It Does

smartvm wraps the operational parts of Amazon Firecracker:

  • Downloads and caches Firecracker binaries.
  • Resolves bootable Firecracker CI base-image bundles with latest and lts presets.
  • Supports project-owned hosted base-image manifests with SHA256 verification.
  • Creates and controls microVMs through Firecracker's HTTP-over-Unix-socket API.
  • Converts TypeScript camelCase config into Firecracker's snake_case payloads.
  • Creates TAP devices, a Linux bridge, static guest IP assignments, and NAT rules.
  • Defaults VM runtime artifacts to tmpfs via /dev/shm/.smartvm/runtime when available.
  • Stages writable drives into per-VM ephemeral storage by default so guest writes do not touch cached rootfs files.
  • Cleans up Firecracker processes, sockets, TAPs, bridges, NAT rules, and staged drive copies.

The design goal is close to a Cloudflare Workers/Deno-style filesystem model, adapted to Firecracker: immutable root image, explicit writable scratch, no accidental persistent state, and persistence only when you opt in.

Install

pnpm add @push.rocks/smartvm

Runtime Requirements

Firecracker is a Linux/KVM technology. The package is TypeScript, but the runtime host must provide the VM substrate.

Requirement Why it matters
Linux with /dev/kvm Firecracker needs KVM acceleration.
Firecracker binary Downloaded by ensureBinary() or supplied through firecrackerBinaryPath.
Root privileges for networking TAP devices, bridges, IP forwarding, and iptables NAT require elevated privileges.
Host tools: curl, tar, ip, sysctl, iptables Used for binary/image downloads and network setup.
Enough tmpfs memory Writable VM drives are copied into /dev/shm by default when available.

Quick Start

This is the happy path: let smartvm download Firecracker, resolve a known-good base image, boot it, and clean it up.

import { SmartVM } from '@push.rocks/smartvm';

const smartvm = new SmartVM({
  // Optional. Defaults are intentionally disk-light.
  dataDir: '/tmp/.smartvm',
  runtimeDir: '/dev/shm/.smartvm/runtime',
});

const baseImage = await smartvm.ensureBaseImage({ preset: 'latest' });

const vm = await smartvm.createVM({
  id: 'hello-firecracker',
  bootSource: {
    kernelImagePath: baseImage.kernelImagePath,
    bootArgs: baseImage.bootArgs,
  },
  machineConfig: {
    vcpuCount: 1,
    memSizeMib: 256,
  },
  drives: [
    {
      driveId: 'rootfs',
      pathOnHost: baseImage.rootfsPath,
      isRootDevice: true,
      isReadOnly: baseImage.rootfsIsReadOnly,
    },
  ],
});

try {
  await vm.start();
  console.log(vm.state); // "running"
  console.log(await vm.getVersion());
  console.log(await vm.getInfo());
} finally {
  if (vm.state === 'running' || vm.state === 'paused') {
    await vm.stop();
  }
  await vm.cleanup();
  await smartvm.cleanup();
}

Disk-Light Runtime Model

By default, smartvm treats VMs as ephemeral execution units.

Path Default Persistence model
Firecracker binaries /tmp/.smartvm/bin Cached for reuse.
Base images /tmp/.smartvm/base-images Cached, retention-limited, verified before reuse.
VM sockets /dev/shm/.smartvm/runtime/<vmId>/firecracker.sock Per-VM tmpfs, deleted on cleanup.
Writable drives /dev/shm/.smartvm/runtime/<vmId>/drives/* Per-VM tmpfs copy, deleted on cleanup.
Read-only drives Original path Not copied unless ephemeral: true.

Writable drives are staged into the VM runtime directory before boot. Firecracker receives the staged path, so guest writes do not modify cached base images or source rootfs files.

const vm = await smartvm.createVM({
  bootSource: { kernelImagePath: baseImage.kernelImagePath, bootArgs: baseImage.bootArgs },
  machineConfig: { vcpuCount: 1, memSizeMib: 256 },
  drives: [
    {
      driveId: 'rootfs',
      pathOnHost: baseImage.rootfsPath,
      isRootDevice: true,
      isReadOnly: false,
      // Default for writable drives: true
      ephemeral: true,
    },
  ],
});

Opt into persistence only when that is the point:

const persistentVm = await smartvm.createVM({
  bootSource: { kernelImagePath: '/images/vmlinux', bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off' },
  machineConfig: { vcpuCount: 2, memSizeMib: 512 },
  drives: [
    {
      driveId: 'state',
      pathOnHost: '/var/lib/my-vm/state.ext4',
      isRootDevice: true,
      isReadOnly: false,
      ephemeral: false,
    },
  ],
});

You can also disable writable-drive staging globally:

const smartvm = new SmartVM({
  ephemeralWritableDrives: false,
});

Best practice for high-volume VM starts:

  • Prefer squashfs or another read-only root filesystem.
  • Put mutable scratch data on tmpfs-backed writable drives.
  • Keep shared assets read-only by default.
  • Use external services, object storage, databases, or explicit persistent drives for durable state.
  • Use a dedicated runtimeDir on a real tmpfs if /dev/shm is too small or unavailable.

Architecture

SmartVM
  ImageManager          downloads/caches Firecracker binaries and manual images
  BaseImageManager      resolves known-good base-image bundles
  NetworkManager        creates TAP devices, bridge, NAT, and static guest network data
  MicroVM
    FirecrackerProcess  starts/stops the VMM process
    SocketClient        talks HTTP over the Firecracker Unix socket
    VMConfig            validates and transforms TypeScript config

Firecracker exposes a REST API over a Unix domain socket. smartvm starts the child process, waits for readiness, sends pre-boot config in the right order, starts the instance, and tears down host resources when you are done.

SmartVM

SmartVM is the top-level orchestrator.

import { SmartVM } from '@push.rocks/smartvm';
import type { ISmartVMOptions } from '@push.rocks/smartvm';

const options: ISmartVMOptions = {
  dataDir: '/tmp/.smartvm',
  runtimeDir: '/dev/shm/.smartvm/runtime',
  ephemeralWritableDrives: true,
  firecrackerVersion: 'v1.7.0',
  arch: 'x86_64',
  firecrackerBinaryPath: '/usr/bin/firecracker',
  bridgeName: 'svbr0',
  subnet: '172.30.0.0/24',
  baseImageCacheDir: '/tmp/.smartvm/base-images',
  maxStoredBaseImages: 2,
  baseImageManifestUrl: 'https://assets.example.com/smartvm/manifest.json',
  baseImageManifestPath: './assets/base-images/local.manifest.json',
};

const smartvm = new SmartVM(options);
API Description
ensureBinary() Ensures the Firecracker binary exists and returns its path.
ensureBaseImage(options) Resolves/downloads a base-image bundle and returns kernel/rootfs paths plus boot args.
createVM(config) Creates a MicroVM instance. It does not boot until vm.start().
getRuntimeDir() Returns the active runtime directory used for per-VM tmpfs artifacts.
getVM(id) Looks up an active VM by ID.
listVMs() Lists active VM IDs.
vmCount Number of tracked VMs.
removeVM(id) Removes a VM from the internal tracking map.
stopAll() Stops every running or paused VM.
cleanup() Cleans up all tracked VMs and networking resources.

Base Images

BaseImageManager gives you fast bootable image discovery without committing giant rootfs files to git.

const baseImage = await smartvm.ensureBaseImage(); // preset: "latest"
const ltsBaseImage = await smartvm.ensureBaseImage({ preset: 'lts' });
const freshBaseImage = await smartvm.ensureBaseImage({ preset: 'latest', forceDownload: true });

Presets:

Preset Behavior
latest Resolves the latest Firecracker release and matching CI demo artifacts.
lts Uses the pinned Firecracker CI train v1.7 / Firecracker v1.7.0.
hosted Uses a project-owned manifest. Requires manifestUrl, manifestPath, or manager-level hosted manifest options.

The resolver prefers read-only squashfs rootfs artifacts when Firecracker CI exposes them, falling back to ext4 when needed.

import { BaseImageManager } from '@push.rocks/smartvm';

const baseImageManager = new BaseImageManager({
  arch: 'x86_64',
  cacheDir: '/tmp/.smartvm/base-images',
  maxStoredBaseImages: 4,
  hostedManifestPath: './assets/base-images/smartvm-minimal.manifest.json',
});

console.log(baseImageManager.getCacheDir());
console.log(baseImageManager.getMaxStoredBaseImages());

const hosted = await baseImageManager.ensureBaseImage({ preset: 'hosted' });
const evictedBundleIds = await baseImageManager.pruneBaseImageCache(hosted.bundleId);

IBaseImageBundle contains:

  • kernelImagePath
  • rootfsPath
  • rootfsType
  • rootfsIsReadOnly
  • bootArgs
  • firecrackerVersion
  • checksums
  • sizes
  • source metadata

Cache behavior:

  • Default cache directory: /tmp/.smartvm/base-images
  • Default retention: 2 bundles
  • Older bundles are evicted with a console.warn when retention is exceeded
  • Cached artifacts are checked for size and SHA256 before reuse
  • Hosted URL artifacts require SHA256 hashes
  • Hosted local-path artifacts may omit SHA256, but hashes are still recorded in the cached manifest

Hosted manifest example:

The repository ships an example at assets/base-images/smartvm-minimal.manifest.example.json.

{
  "schemaVersion": 1,
  "bundleId": "smartvm-minimal-v1-x86_64",
  "name": "SmartVM minimal x86_64 bundle",
  "arch": "x86_64",
  "firecrackerVersion": "v1.15.1",
  "rootfsType": "squashfs",
  "rootfsIsReadOnly": true,
  "bootArgs": "console=ttyS0 reboot=k panic=1 pci=off ro rootfstype=squashfs",
  "kernel": {
    "url": "https://assets.example.com/smartvm/vmlinux",
    "fileName": "vmlinux",
    "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
    "sizeBytes": 12345678
  },
  "rootfs": {
    "url": "https://assets.example.com/smartvm/rootfs.squashfs",
    "fileName": "rootfs.squashfs",
    "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
    "sizeBytes": 12345678
  }
}

MicroVM Lifecycle

MicroVM is a single Firecracker instance with a strict state machine:

created -> configuring -> running -> paused -> stopped
                         \-> error
const vm = await smartvm.createVM({
  id: 'api-worker-1',
  bootSource: {
    kernelImagePath: baseImage.kernelImagePath,
    bootArgs: baseImage.bootArgs,
  },
  machineConfig: {
    vcpuCount: 2,
    memSizeMib: 512,
    smt: false,
    cpuTemplate: 'T2',
    trackDirtyPages: true,
  },
  drives: [
    {
      driveId: 'rootfs',
      pathOnHost: baseImage.rootfsPath,
      isRootDevice: true,
      isReadOnly: baseImage.rootfsIsReadOnly,
      cacheType: 'Unsafe',
      ephemeral: true,
      rateLimiter: {
        bandwidth: { size: 100_000_000, refillTime: 1_000_000_000 },
        ops: { size: 1000, refillTime: 1_000_000_000 },
      },
    },
  ],
  networkInterfaces: [{ ifaceId: 'eth0' }],
  vsock: {
    guestCid: 3,
    udsPath: '/dev/shm/api-worker-1.vsock',
  },
  balloon: {
    amountMib: 128,
    deflateOnOom: true,
    statsPollingIntervalS: 5,
  },
  mmds: {
    version: 'V2',
    networkInterfaces: ['eth0'],
  },
});

await vm.start();
await vm.pause();
await vm.resume();
await vm.stop();
await vm.cleanup();
API Valid state Description
start() created Stages ephemeral drives, starts Firecracker, applies config, boots the VM.
pause() running Pauses execution.
resume() paused Resumes execution.
stop() running, paused Sends Ctrl+Alt+Del, waits briefly, then stops the process.
cleanup() any Stops process, deletes sockets/runtime dir, removes auto-created TAPs.
getInfo() after start Returns Firecracker instance info.
getVersion() after start Returns Firecracker version info.
setMetadata(data) running, paused Writes MMDS metadata.
getMetadata() running, paused Reads MMDS metadata.
updateDrive(id, path) running, paused Hot-updates a drive path.
updateNetworkInterface(id, update) running, paused Updates network interface config such as rate limiters.
updateBalloon(mib) running, paused Resizes the balloon device.
createSnapshot(params) paused Creates a Firecracker snapshot.
loadSnapshot(params) created, configuring Low-level Firecracker snapshot-load call; requires an initialized socket client.
getTapDevices() any Returns TAP devices created automatically by this VM.
getVMConfig() any Returns the internal VMConfig instance.
getRuntimeDir() any Returns the per-VM runtime directory after it has been created.

Snapshot caveat: diff snapshots require dirty-page tracking to be enabled before boot through machineConfig.trackDirtyPages. Snapshot restore is currently exposed as a low-level API; full restore orchestration should be built around the Firecracker process/config lifecycle intentionally.

Networking

NetworkManager creates host-side networking primitives. It does not run DHCP inside the guest. Your guest image must configure its interface itself, or you must pass static ip= kernel boot arguments.

Automatic mode:

const vm = await smartvm.createVM({
  bootSource: { kernelImagePath: baseImage.kernelImagePath, bootArgs: baseImage.bootArgs },
  machineConfig: { vcpuCount: 1, memSizeMib: 256 },
  drives: [{ driveId: 'rootfs', pathOnHost: baseImage.rootfsPath, isRootDevice: true, isReadOnly: baseImage.rootfsIsReadOnly }],
  networkInterfaces: [{ ifaceId: 'eth0' }],
});

Static-kernel-args mode:

const tap = await smartvm.networkManager.createTapDevice('net-vm', 'eth0');

const vm = await smartvm.createVM({
  id: 'net-vm',
  bootSource: {
    kernelImagePath: baseImage.kernelImagePath,
    bootArgs: `${baseImage.bootArgs} ${smartvm.networkManager.getGuestNetworkBootArgs(tap)}`,
  },
  machineConfig: { vcpuCount: 1, memSizeMib: 256 },
  drives: [{ driveId: 'rootfs', pathOnHost: baseImage.rootfsPath, isRootDevice: true, isReadOnly: baseImage.rootfsIsReadOnly }],
  networkInterfaces: [
    {
      ifaceId: 'eth0',
      hostDevName: tap.tapName,
      guestMac: tap.mac,
    },
  ],
});

Networking behavior:

  • Default bridge: svbr0
  • Default subnet: 172.30.0.0/24
  • Subnet input is normalized to the network address
  • Prefix length must be 1-30
  • Gateway uses the first usable address
  • Guest IP allocation starts at the second usable address
  • Allocation is sequential and not reused within the same NetworkManager instance
  • MAC addresses are deterministic and locally administered (02:xx:xx:xx:xx:xx)
  • TAP names are capped to Linux's 15-character IFNAMSIZ limit
  • NAT masquerade uses the host default route interface
  • Use a dedicated bridge name; cleanup() tears down the bridge configured by this manager

ImageManager

ImageManager is the lower-level helper for Firecracker binaries and manually managed kernel/rootfs files.

const imageManager = smartvm.imageManager;

await imageManager.ensureDirectories();

const latest = await imageManager.getLatestVersion();
const firecrackerPath = await imageManager.downloadFirecracker(latest);

const kernelPath = await imageManager.downloadKernel(
  'https://example.com/vmlinux',
  'vmlinux',
);

const rootfsPath = await imageManager.downloadRootfs(
  'https://example.com/rootfs.ext4',
  'rootfs.ext4',
);

const blankRootfs = await imageManager.createBlankRootfs('scratch.ext4', 1024);
const clonedRootfs = await imageManager.cloneRootfs(rootfsPath, 'vm-rootfs.ext4');

Useful path helpers:

  • getBinDir()
  • getKernelsDir()
  • getRootfsDir()
  • getSocketsDir()
  • getFirecrackerPath(version)
  • getJailerPath(version)
  • getSocketPath(vmId)

Note: SmartVM.createVM() uses runtimeDir/<vmId>/firecracker.sock for new VM sockets by default. ImageManager.getSocketPath() remains available for lower-level/custom flows.

VMConfig

VMConfig validates IMicroVMConfig and transforms it into Firecracker API payloads.

import { VMConfig } from '@push.rocks/smartvm';

const vmConfig = new VMConfig({
  bootSource: { kernelImagePath: '/images/vmlinux', bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off' },
  machineConfig: { vcpuCount: 2, memSizeMib: 256 },
  drives: [{ driveId: 'rootfs', pathOnHost: '/images/rootfs.ext4', isRootDevice: true }],
});

const validation = vmConfig.validate();
if (!validation.valid) {
  throw new Error(validation.errors.join('; '));
}

console.log(vmConfig.toBootSourcePayload());
console.log(vmConfig.toMachineConfigPayload());
console.log(vmConfig.toDrivePayload(vmConfig.config.drives![0]));

The constructor clones caller-provided config, so internal normalization does not mutate your original object.

SocketClient

SocketClient is the raw Firecracker API client. Most users should go through MicroVM, but the low-level client is exported for tooling and diagnostics.

import { SocketClient } from '@push.rocks/smartvm';

const client = new SocketClient({ socketPath: '/dev/shm/.smartvm/runtime/vm/firecracker.sock' });

const version = await client.get('/version');
const machineConfig = await client.put('/machine-config', {
  vcpu_count: 1,
  mem_size_mib: 256,
});
const paused = await client.patch('/vm', { state: 'Paused' });

console.log(version.ok, version.statusCode, version.body);
console.log(machineConfig.ok, machineConfig.statusCode, machineConfig.body);
console.log(paused.ok, paused.statusCode, paused.body);

SocketClient returns { ok, statusCode, body }. Non-2xx responses do not become API_ERROR until higher-level MicroVM helpers validate them.

Metadata Service

Firecracker MMDS lets the host pass structured metadata to a running VM.

const vm = await smartvm.createVM({
  bootSource: { kernelImagePath: baseImage.kernelImagePath, bootArgs: baseImage.bootArgs },
  machineConfig: { vcpuCount: 1, memSizeMib: 256 },
  drives: [{ driveId: 'rootfs', pathOnHost: baseImage.rootfsPath, isRootDevice: true, isReadOnly: baseImage.rootfsIsReadOnly }],
  networkInterfaces: [{ ifaceId: 'eth0' }],
  mmds: {
    version: 'V2',
    networkInterfaces: ['eth0'],
  },
});

await vm.start();

await vm.setMetadata({
  instance: { id: 'api-worker-1', region: 'local' },
  config: { mode: 'ephemeral' },
});

console.log(await vm.getMetadata());

Error Handling

All package-level failures use SmartVMError with structured codes.

import { SmartVMError } from '@push.rocks/smartvm';

try {
  await vm.start();
} catch (err) {
  if (err instanceof SmartVMError) {
    console.error(err.code);
    console.error(err.statusCode);
    console.error(err.details);
  }
  throw err;
}
Code Meaning
INVALID_STATE Operation is invalid for the current VM state.
INVALID_CONFIG VM configuration failed validation.
SOCKET_TIMEOUT Firecracker did not create its socket in time.
API_TIMEOUT Firecracker API readiness check timed out.
SOCKET_REQUEST_FAILED Unix-socket HTTP request failed.
API_ERROR Firecracker returned a non-2xx response through a high-level VM call.
BINARY_NOT_FOUND Custom Firecracker binary path does not exist.
DOWNLOAD_FAILED Binary, kernel, or rootfs download failed.
VERSION_FETCH_FAILED Latest Firecracker version lookup failed.
BASE_IMAGE_RESOLVE_FAILED Firecracker CI base-image artifact resolution failed.
BASE_IMAGE_MANIFEST_FAILED Hosted manifest could not be loaded or used.
BASE_IMAGE_PREPARE_FAILED Base-image download/copy/verification failed.
INVALID_BASE_IMAGE_MANIFEST Hosted manifest schema or artifact metadata is invalid.
INVALID_BASE_IMAGE_CACHE_LIMIT Base-image retention limit is invalid.
INVALID_SUBNET Subnet is not a supported IPv4 CIDR.
INVALID_INTERFACE_NAME Bridge or TAP name is invalid.
IP_EXHAUSTED No guest IPs remain in the configured subnet.
BRIDGE_SETUP_FAILED Bridge/NAT setup failed.
TAP_CREATE_FAILED TAP creation failed.
ROOTFS_CREATE_FAILED Blank rootfs creation failed.
ROOTFS_CLONE_FAILED Rootfs clone failed.
START_FAILED VM start sequence failed.
NO_CLIENT Socket client is not initialized.

Testing

Default tests are safe on machines without KVM or root privileges:

pnpm test
pnpm run build

The default suite covers config validation, payload generation, lifecycle guards, base-image cache behavior, hosted manifest validation, VM tracking, ephemeral drive staging, and subnet/IP behavior.

Opt into real Firecracker boot tests on a Linux/KVM host:

SMARTVM_RUN_INTEGRATION=true pnpm test

Accepted truthy values for SMARTVM_RUN_INTEGRATION: 1, true, yes.

Useful integration-test environment variables:

Variable Purpose
SMARTVM_BASE_IMAGE_PRESET latest or lts; default is latest.
SMARTVM_BASE_IMAGE_MANIFEST_URL Hosted/project-owned base-image manifest URL.
SMARTVM_BASE_IMAGE_MANIFEST_PATH Local hosted manifest path.
SMARTVM_BASE_IMAGE_CACHE_DIR Override /tmp/.smartvm/base-images.
SMARTVM_MAX_STORED_BASE_IMAGES Override default retention of 2.
SMARTVM_FIRECRACKER_VERSION Override the Firecracker binary version.
SMARTVM_ARCH x86_64 or aarch64; defaults from host architecture.
SMARTVM_INTEGRATION_DATA_DIR Override the Firecracker binary data directory used by integration tests.

TypeScript Surface

Main exports:

export {
  SmartVM,
  MicroVM,
  NetworkManager,
  FirecrackerProcess,
  BaseImageManager,
  ImageManager,
  SocketClient,
  VMConfig,
};

Important exported types:

import type {
  ISmartVMOptions,
  IMicroVMRuntimeOptions,
  IMicroVMConfig,
  IBootSource,
  IMachineConfig,
  IDriveConfig,
  INetworkInterfaceConfig,
  IVsockConfig,
  IBalloonConfig,
  IMmdsConfig,
  ILoggerConfig,
  IMetricsConfig,
  IBaseImageManagerOptions,
  IEnsureBaseImageOptions,
  IBaseImageBundle,
  IBaseImageHostedManifest,
  IBaseImageArtifactManifest,
  ISnapshotCreateParams,
  ISnapshotLoadParams,
  IRateLimiter,
  INetworkManagerOptions,
  ITapDevice,
  ISocketClientOptions,
  IApiResponse,
  TVMState,
  TFirecrackerArch,
  TCacheType,
  TSnapshotType,
  TLogLevel,
  TBaseImagePreset,
  TBaseImageRootfsType,
} from '@push.rocks/smartvm';

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the LICENSE file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.