
Sandbox microVM éphémère pour agents d'IA avec liste blanche réseau, injection de secrets via proxy MITM, et isolation au niveau de la VM. Démarre en moins d'une seconde, prend en charge les SDK Go, Python et TypeScript.
Expérimental : Ce projet est encore en développement actif et peut subir des changements majeurs.
Matchlock est un outil en ligne de commande (CLI) pour exécuter des agents IA dans des micro-VM éphémères – avec une liste blanche réseau, une injection de secrets via un proxy MITM et un isolement au niveau de la VM. Vos secrets n’entrent jamais dans la VM.
Les agents IA ont besoin d’exécuter du code, mais leur donner un accès sans restriction à votre machine est risqué. Matchlock vous permet de fournir à un agent un environnement Linux complet qui démarre en moins d’une seconde – isolé et jetable.
Lorsque vous passez --allow-host ou --secret, Matchlock verrouille le réseau – seul le trafic vers les hôtes explicitement autorisés passe, tout le reste est bloqué. Quand votre agent appelle une API, les vraies identifiants sont injectés en vol par l’hôte. Le sandbox ne voit jamais qu’un espace réservé. Même si l’agent est trompé en exécutant quelque chose de malveillant, vos clés ne fuient pas et il n’y a nulle part où envoyer les données. À l’intérieur, l’agent dispose d’un environnement Linux complet pour faire ce dont il a besoin. Il peut installer des paquets, écrire des fichiers et faire du désordre. À l’extérieur, votre machine ne ressent rien. Les montages par overlay de volume sont des instantanés isolés qui disparaissent une fois terminés. Même CLI et même comportement, que vous soyez sur un serveur Linux ou un MacBook.
Voir docs/install.md pour les détails complets d’installation.
Installation rapide
Le script ci-dessous détecte le système d’exploitation et installe Matchlock avec Homebrew sur macOS et avec rpm/deb sur les distributions Linux Debian/RHEL.
curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash
# Ou installer une version spécifique
curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash -s -- --version 0.2.4
Homebrew
L’installation via Homebrew est prise en charge aussi bien sur macOS que sur Linux :
brew tap jingkaihe/essentials
brew install matchlock
Debian / Ubuntu (.deb)
sudo dpkg -i ./matchlock_<version>_linux_amd64.deb
sudo apt-get install -f
matchlock diagnose
Fedora / RHEL / CentOS Stream (.rpm)
sudo dnf install ./matchlock_<version>_linux_amd64.rpm
matchlock diagnose
Si matchlock diagnose signale une configuration hôte manquante, exécutez :
sudo matchlock setup linux
Pour enrôler explicitement un utilisateur spécifique, exécutez :
sudo matchlock setup user <name>
# Basique
matchlock run --image alpine:latest cat /etc/os-release
matchlock run --image alpine:latest -it sh
matchlock run --image alpine:latest --no-network -- sh -lc 'echo offline'
# Liste blanche réseau
matchlock run --image python:3.12-alpine \
--allow-host "api.openai.com" python agent.py
# Garder l’interception activée même avec une liste blanche vide,
# afin que les hôtes puissent être ajoutés/supprimés en cours d’exécution.
matchlock run --image alpine:latest --rm=false --network-intercept
matchlock allow-list add <vm-id> api.openai.com,api.anthropic.com
matchlock allow-list delete <vm-id> api.openai.com
# Injection de secrets (n’entre jamais dans la VM)
export ANTHROPIC_API_KEY=sk-xxx
matchlock run --image python:3.12-alpine \
--secret [email protected] python call_api.py
# Sandbox longue durée
matchlock run --image alpine:latest --rm=false # affiche l’ID de la VM
matchlock run --image nginx:latest -d # identique à ci-dessus, détaché
matchlock exec vm-abc12345 -it sh # s’y attacher
matchlock port-forward vm-abc12345 8080:8080 # forward hôte:8080 -> invité:8080
# Publier des ports au démarrage
matchlock run --image alpine:latest --rm=false -p 8080:8080
# Cycle de vie
matchlock list | kill | rm | prune
# Construire à partir d’un Dockerfile (utilise BuildKit-in-VM)
matchlock build -f Dockerfile -t myapp:latest .
# Pré-construire le rootfs à partir d’une image registre (met en cache pour un démarrage plus rapide)
matchlock build alpine:latest
# Gestion des images
matchlock image ls # Lister toutes les images
matchlock image rm myapp:latest # Supprimer une image locale
docker save myapp:latest | matchlock image import myapp:latest # Importer depuis une archive tar
Matchlock propose des SDK en Go, Python et TypeScript pour intégrer des sandbox directement dans votre application. Vous pouvez lancer des VM, exécuter des commandes, diffuser la sortie et gérer des fichiers par programme.
Go
package main
import (
"context"
"fmt"
"os"
"github.com/jingkaihe/matchlock/pkg/sdk"
)
func main() {
ctx := context.Background()
client, err := sdk.NewClient(sdk.DefaultConfig())
if err != nil {
panic(err)
}
defer client.Close(0)
defer client.Remove()
sandbox := sdk.New("alpine:latest").
AllowHost("dl-cdn.alpinelinux.org", "api.anthropic.com").
AddSecret("ANTHROPIC_API_KEY", os.Getenv("ANTHROPIC_API_KEY"), "api.anthropic.com")
if _, err := client.Launch(sandbox); err != nil {
panic(err)
}
if _, err := client.Exec(ctx, "apk add --no-cache curl"); err != nil {
panic(err)
}
// La VM ne voit jamais qu'un espace réservé - la vraie clé n'entre jamais dans le sandbox
result, err := client.Exec(ctx, "echo $ANTHROPIC_API_KEY")
if err != nil {
panic(err)
}
fmt.Print(result.Stdout) // affiche "SANDBOX_SECRET_a1b2c3d4..."
curlCmd := `curl -s --no-buffer https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":1024,"stream":true,
"messages":[{"role":"user","content":"Explain TCP to me"}]}'`
if _, err := client.ExecStream(ctx, curlCmd, os.Stdout, os.Stderr); err != nil {
panic(err)
}
}
Comportement des IP privées dans le SDK Go (10/8, 172.16/12, 192.168/16) :
.WithBlockPrivateIPs(true) (ou .BlockPrivateIPs())..AllowPrivateIPs() ou .WithBlockPrivateIPs(false).sandbox := sdk.New("alpine:latest").
AllowHost("api.openai.com").
AddHost("api.internal", "10.0.0.10").
WithNetworkMTU(1200).
AllowPrivateIPs() // surcharge explicite : block_private_ips=false
// Interception réseau SDK (mutation requête/réponse, mise en forme du corps, transformation des lignes de données SSE)
sandbox = sandbox.WithNetworkInterception(&sdk.NetworkInterceptionConfig{
Rules: []sdk.NetworkHookRule{
{
Phase: sdk.NetworkHookPhaseBefore,
Action: sdk.NetworkHookActionMutate,
Hosts: []string{"api.openai.com"},
SetHeaders: map[string]string{"X-Trace-Id": "trace-123"},
},
{
Phase: sdk.NetworkHookPhaseAfter,
Action: sdk.NetworkHookActionMutate,
Hosts: []string{"api.openai.com"},
BodyReplacements: []sdk.NetworkBodyTransform{
{Find: "internal-id", Replace: "redacted"},
},
},
},
})
Si vous utilisez client.Create(...) directement (sans le constructeur), définissez :
BlockPrivateIPsSet: trueBlockPrivateIPs: false (ou true)Pour des sandbox totalement hors ligne (pas de carte réseau invitée / pas de trafic sortant), utilisez :
--no-network.WithNoNetwork().with_no_network().withNoNetwork()Python (PyPI)
pip install matchlock
# ou
uv add matchlock
import os
import sys
from matchlock import Client, Sandbox
sandbox = (
Sandbox("python:3.12-alpine")
.allow_host(
"dl-cdn.alpinelinux.org",
"files.pythonhosted.org", "pypi.org",
"astral.sh", "github.com", "objects.githubusercontent.com",
"api.anthropic.com",
)
.add_secret(
"ANTHROPIC_API_KEY", os.environ["ANTHROPIC_API_KEY"], "api.anthropic.com"
)
)
SCRIPT = """\
# /// script
# requires-python = ">=3.12"
# dependencies = ["anthropic"]
# ///
import anthropic, os
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
with client.messages.stream(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain TCP/IP."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
print()
"""
with Client() as client:
client.launch(sandbox)
client.exec("pip install --quiet uv")
client.write_file("/workspace/ask.py", SCRIPT)
client.exec_stream("uv run /workspace/ask.py", stdout=sys.stdout, stderr=sys.stderr)
client.remove()
TypeScript
npm install matchlock-sdk
import { Client, Sandbox } from "matchlock-sdk";
const SCRIPT = `import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const stream = anthropic.messages
.stream({
model: "claude-haiku-4-5-20251001",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain TCP/IP." }],
})
.on("text", (text) => {
process.stdout.write(text);
});
await stream.finalMessage();
process.stdout.write("\\n");
`;
const client = new Client();
try {
const sandbox = new Sandbox("node:22-alpine")
.allowHost("registry.npmjs.org", "*.npmjs.org", "api.anthropic.com")
.addSecret("ANTHROPIC_API_KEY", process.env.ANTHROPIC_API_KEY ?? "", "api.anthropic.com");
await client.launch(sandbox);
await client.exec(
"npm init -y >/dev/null 2>&1 && npm install --quiet --no-bin-links @anthropic-ai/sdk",
{ workingDir: "/workspace" },
);
await client.writeFile("/workspace/ask.mjs", SCRIPT);
await client.execStream("node ask.mjs", {
workingDir: "/workspace",
stdout: process.stdout,
stderr: process.stderr,
});
} finally {
await client.close();
await client.remove();
}
Plus d’exemples dans le répertoire examples/ :
graph LR
subgraph Host
CLI["Matchlock CLI"]
Policy["Policy Engine"]
Proxy["Transparent Proxy + TLS MITM"]
VFS["VFS Server"]
CLI --> Policy
CLI --> Proxy
Policy --> Proxy
end
subgraph VM["Micro-VM (Firecracker / Virtualization.framework)"]
Agent["Guest Agent"]
FUSE["/workspace (FUSE)"]
Image["Any OCI Image (Alpine, Ubuntu, etc.)"]
Agent --- Image
FUSE --- Image
end
Proxy -- "vsock :5000" --> Agent
VFS -- "vsock :5001" --> FUSE| Plateforme |
|---|
MIT
| Description | Exemple |
|---|
| Diffuse la réponse de l’API Anthropic avec injection de secret (Go) | examples/go/basic/ |
| Terminal interactif avec PTY utilisant ExecInteractive (Go) | examples/go/exec_modes/ |
| Injecte la clé API via un hook d’interception réseau (Go) | examples/go/network_interception/ |
| Hooks d’interception VFS pour les mutations d’opérations sur fichiers (Go) | examples/go/vfs_hooks/ |
| Diffuse la réponse de l’API Anthropic (Python) | examples/python/basic/ |
| Modes d’exécution : flux, pipe et interactif (Python) | examples/python/exec_modes/ |
| Injecte la clé API via un hook d’interception réseau (Python) | examples/python/network_interception/ |
| Hooks d’interception VFS pour les mutations d’opérations sur fichiers (Python) | examples/python/vfs_hooks/ |
| Diffuse la réponse de l’API Anthropic (TypeScript) | examples/typescript/basic/ |
| Modes d’exécution : flux, pipe et interactif (TypeScript) | examples/typescript/exec_modes/ |
| Injecte la clé API via un hook d’interception réseau (TypeScript) | examples/typescript/network_interception/ |
| CLI Claude Code dans une micro-VM avec bootstrap GitHub | examples/claude-code/ |
| Claude Code avec Docker dans le sandbox via SDK | examples/claude-code-with-docker/ |
| Claude Code avec abonnement Claude Pro/Max dans le sandbox | examples/claude-danger/ |
| CLI OpenAI Codex dans une micro-VM avec bootstrap GitHub | examples/codex/ |
| Démon Docker dans un sandbox avec systemd | examples/docker-in-sandbox/ |
| Chatbot Streamlit utilisant le protocole Agent Client | examples/agent-client-protocol/ |
| Automatisation de navigateur avec Kodelet et Playwright MCP | examples/playwright/ |
| Mode |
|---|
| Mécanisme |
|---|
| Linux | Proxy transparent | DNAT nftables sur les ports 80/443 |
| macOS | NAT (par défaut) | NAT intégré de Virtualization.framework |
| macOS | Interception (avec --allow-host/--secret) | TCP/IP espace utilisateur gVisor au niveau L4 |