Retour aux mises à jour
New releaseSep 3, 2026

vpod v0.8.1

Sandbox Linux légères et sécurisées pour les processus non fiables. Fonctionne dans le navigateur et sur le serveur.

Partager

Vpod

Qu'est-ce qu'un vpod ?

Un vpod est un sandbox léger et portable qui offre à un processus non fiable un environnement Linux instantané. Il utilise une architecture RISC‑V et s'exécute entièrement dans WebAssembly.

  • Démarrage rapide : Démarre en moins d'une seconde.
  • Portable : Fonctionne partout sans aucune configuration requise.
  • Isolé : Tout l'état d'exécution reste dans les sandboxes WASM.

Comment ça fonctionne

Un vpod exécute un système RISC‑V complet (RV64GC, un seul vCPU) compilé en WebAssembly. À l'intérieur, il démarre un vrai noyau Linux avec un véritable espace utilisateur, de sorte que les shells, outils et démons se comportent comme sur du matériel réel.

Instantanés. Au lieu de démarrer Linux à partir de zéro, un vpod restaure un instantané : un état machine sauvegardé (registres CPU, RAM, système de fichiers) capturé juste après le démarrage. Sa restauration prend bien moins d'une seconde. La suspension fonctionne de la même manière en sens inverse : seules les pages mémoire modifiées sont réécrites sur le disque, ce qui permet de mettre un sandbox en pause et de le reprendre plus tard, même depuis un autre processus.

Traduction à l'avance. L'émulation pure instruction par instruction est lente, et WebAssembly exclut un JIT à l'exécution. Ainsi, lors de la construction de l'instantané, les chemins de code invité les plus sollicités sont traduits du RISC‑V en code natif compilé dans le module WASM lui-même. À l'exécution, l'émulateur bascule vers ces blocs traduits lorsque le code invité correspond, et revient à l'interpréteur dans le cas contraire. Cela représente environ 5x de gain sur les tâches gourmandes en CPU, sans aucun effet sur l'isolation : le code traduit passe par les mêmes contrôles MMU et mémoire que le code interprété.

La frontière WASI. Le composant WASM communique avec l'hôte exclusivement via WASI 0.2. L'invité ne voit jamais les descripteurs de fichiers, sockets ou mémoire de l'hôte : l'accès au système de fichiers passe par des répertoires explicitement montés, et la mise en réseau passe par une pile réseau en mode utilisateur à l'intérieur du composant qui ne demande jamais à l'hôte que des sockets sortantes simples. Tout le reste (noyau invité, processus, mémoire) vit dans la mémoire linéaire WASM et meurt avec elle.

Spécification RV64GC

G (Extensions à usage général)

  • I : Jeu d'instructions entier de base 64 bits.
  • M : Multiplication et division matérielles, utiles pour le hachage et la cryptographie.
  • A : Opérations atomiques pour les programmes thread-safe.
  • F/D : Virgule flottante simple et double précision, adaptée au calcul scientifique et à l'inférence ML.

C (Instructions compressées) Réduit la taille du code de 30 %, améliorant la vitesse de récupération des instructions et l'efficacité mémoire. Cela compte lors de l'exécution d'un espace utilisateur Linux complet dans notre environnement WASM à mémoire contrainte.

[!NOTE] L'extension V (vectorielle) n'est pas implémentée. Les instructions RVV s'exécuteraient comme du RISC-V émulé ; il n'y a pas de passage SIMD vers le CPU hôte. L'ajout de V augmenterait la surcharge d'émulation sans aucun bénéfice de performance pour les charges vectorisées.

Pour commencer

SDK TypeScript

npm install @capsule-run/vpod
import { Sandbox } from "@capsule-run/vpod";

const sandbox = await Sandbox.create();

// L'état est préservé entre les appels
await sandbox.commands.run("export API_KEY=secret");
const key = await sandbox.commands.run("echo $API_KEY");
console.log(key.stdout); // secret

// REPL Python — les variables persistent
await sandbox.code.run("data = [1, 2, 3]");
const total = await sandbox.code.run("print(sum(data))");
console.log(total.text); // 6

await sandbox.close();

Le même paquet fonctionne dans un onglet de navigateur, où l'instantané est mis en cache dans le stockage privé d'origine plutôt que sur le disque.

[!IMPORTANT] Le premier appel à Sandbox.create() télécharge l'instantané par défaut (alpine) et le met en cache localement s'il n'est pas déjà présent.

SDK Python

pip install vpod
from vpod import Sandbox

# Exécuter une commande
sandbox = Sandbox.create()
result = sandbox.commands.run("whoami")
print(result.stdout)  # root
sandbox.close()

# Session persistante — l'état est préservé entre les appels
with Sandbox.create() as sandbox:
    sandbox.commands.run("export API_KEY=secret")
    result = sandbox.commands.run("echo $API_KEY")
    print(result.stdout)  # secret

# REPL Python — les variables persistent
with Sandbox.create() as sandbox:
    sandbox.code.run("import requests")
    sandbox.code.run("data = [1, 2, 3]")
    result = sandbox.code.run("print(sum(data))")
    print(result.text)  # 6

CLI

curl -fsSL https://install.vpod.sh | sh
Ou installer via PowerShell (Windows)
irm https://install.vpod.sh | iex
# Récupérer un instantané
vpod pull alpine:latest

# Démarrer un shell interactif
vpod

Documentation

Consultez la documentation Vpod.

Limitations

  • Surcharge d'émulation : Il n'y a pas de virtualisation matérielle dans WebAssembly, donc tout le code invité est émulé. La surcharge dépend entièrement de la charge de travail : les tâches liées aux E/S et au réseau s'exécutent presque à la vitesse native, tandis que les tâches lourdes en CPU s'exécutent nettement plus lentement même avec la traduction AOT. Si votre charge de travail consiste principalement à « exécuter un outil, lire un fichier, appeler une API », vous ne remarquerez rien.
  • Pas d'accès GPU : CUDA, Metal et les accélérateurs ML matériels ne sont pas disponibles. Un support pourrait être ajouté à l'avenir avec wasi-nn.

Contribuer

Les contributions sont les bienvenues, des rapports de bugs au support de nouveaux périphériques. Ouvrez une issue pour discuter de tout élément substantiel avant de le construire.

Prérequis

  • Rust (dernière version stable) avec la cible wasm32-wasip2 : rustup target add wasm32-wasip2
  • Python 3.10+ pour le SDK Python
  • Node 20+ pour le SDK TypeScript
  • Zig (0.16) et bsdtar, nécessaires uniquement si vous construisez vous-même des instantanés

Configuration de développement

# Ponctuel : générer le stub AOT (un nouveau clone n'a pas de blocs traduits)
./scripts/aot-stub.sh

# Construire le composant WASM (bibliothèque + CLI). Copie les deux niveaux dans sdks/python/vpod/
./scripts/build-wasm.sh

# Installer la CLI hôte
cargo install --path crates/vpod

# Installer le SDK Python en mode développement
pip install -e "sdks/python[dev]"

# Construire le SDK TypeScript. Récupère le composant depuis le répertoire du SDK Python
cd sdks/typescript && npm install && npm run build

npm run build utilise par défaut --tier aot ; CI fixe --tier base. La construction refuse un composant plus ancien que le fichier le plus récent sous crates/, donc relancez ./scripts/build-wasm.sh après avoir modifié l'émulateur. Une modification de l'émulateur ne se manifeste qu'à travers l'invité, donc un composant obsolète compile et réussit presque tout.

Exécution des tests

CI les exécute à chaque PR, donc exécutez-les avant de pousser :

cargo fmt --all -- --check                        # formatage
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all                                  # tests Rust

# Tests d'intégration du SDK Python (nécessite la bibliothèque WASM en place)
cp target/wasm32-wasip2/release/vpod_wasi_lib.wasm sdks/python/vpod/
pytest sdks/python/tests/ -v -m integration

# SDK TypeScript (depuis sdks/typescript)
npm run typecheck
npm test                  # unitaire
npm run test:all          # unitaire + intégration, nécessite un instantané local
npm run test:perf         # régressions du temps invité, constantes exactes

Les tests TypeScript importent le dist/ construit, pas src/, donc construisez avant de les exécuter. Ils recherchent un instantané dans le répertoire de cache partagé ; VPOD_TEST_SNAPSHOT=/path/to/x.snap les dirige ailleurs.

Pour exercer le navigateur de bout en bout, npm run dev sert la page avec COOP/COEP activés et node dev/run-network.mjs --browser chrome la pilote en mode headless.

Utilisation d'un instantané construit localement

Les SDK tirent par défaut depuis registry.vpod.sh. Pour exécuter un instantané que vous avez construit vous-même, fournissez-le directement au lieu d'un nom de registre :

// TypeScript : un fichier sur disque (Node), ou des octets (partout)
await Sandbox.create({ snapshot: { path: "./dist/alpine-3.23.0-256mb.snap" } });
await Sandbox.create({ snapshot: { bytes, name: "alpine-3.23.0-256mb.snap" } });
# Python : VPOD_SNAPSHOT=/path/to/x.snap

Conservez la taille de la RAM dans le nom de fichier dans tous les cas, car l'émulateur la lit à partir de là.

Construction d'instantanés

Le projet utilise des instantanés Alpine pré-construits depuis registry.vpod.sh, donc vous n'en avez normalement pas besoin. Pour en construire un localement :

./scripts/build-default-snapshot.sh   # dist/alpine-3.23.0-256mb.snap
./scripts/build-data-snapshot.sh      # variante 512 Mo avec numpy/pandas/scipy

[!TIP] Pour utiliser un instantané construit localement dans la CLI, décommentez les lignes dans resolve_snapshot() dans crates/vpod/src/main.rs.

Les constructions d'instantanés peuvent également exécuter le passage AOT (scripts/aot-snapshot.sh <snapshot>), qui trace une charge de travail représentative, traduit les blocs chauds et reconstruit l'émulateur avec ceux-ci intégrés. Cela prend du temps ; le stub de aot-stub.sh suffit pour le développement quotidien, tout fonctionne de la même manière, juste plus lentement.

À partir d'un Dockerfile (macOS et Linux)

Des instantanés personnalisés peuvent également être construits à partir d'un Dockerfile. Le constructeur utilise le CLI container d'Apple sur macOS et Docker Buildx sur Linux. Une nouvelle installation macOS nécessite une configuration unique de son runtime, sinon la construction attend un builder qui ne démarre jamais :

container system kernel set --recommended
container builder start

Sur Linux, installez Docker avec le plugin Buildx et enregistrez l'émulation riscv64 une fois si l'hôte n'est pas déjà configuré pour les constructions multiplateformes :

docker run --privileged --rm tonistiigi/binfmt --install riscv64
./scripts/build-custom-snapshot.sh -f Dockerfile -n my-image   # dist/my-image-256mb.snap
# optionnellement : --aot --trace-cmd '<la commande chaude de l'image>' pour intégrer les blocs AOT

Le Dockerfile est construit pour linux/riscv64 (BuildKit exécute les étapes RUN sous émulation), son rootfs aplati remplace le minirootfs Alpine, et le reste du pipeline est identique : overlay vpod, démarrage, --snapshot-save.

Seul le système de fichiers survit à l'exportation. ENV, CMD et ENTRYPOINT de la configuration de l'image sont ignorés, donc persistez l'environnement via /etc/profile.d/ dans une étape RUN. Le démarrage à chaud Python est appliqué automatiquement pour les images basées sur musl qui fournissent python3 dans /usr/bin ou /bin.

Pull requests

  • Gardez les PR ciblées : un changement par PR.
  • fmt, clippy et la suite de tests doivent réussir (CI impose les trois).
  • Si vous touchez aux chemins d'exécution ou de mémoire de l'émulateur, indiquez comment vous avez validé la correction (la suite de tests au minimum ; pour des changements subtils, un démarrage plus une charge de travail réelle dans l'invité constituent un bon test de cohérence).

Licence

Ce projet est sous licence Apache License 2.0. Consultez le fichier LICENSE pour plus de détails.

Catégories