
Ephemere microVM-Sandbox für KI-Agenten mit Network Allowlisting, Secret Injection via MITM-Proxy und VM-Isolation. Startet in unter einer Sekunde, unterstützt Go/Python/TypeScript-SDKs.
# Matchlock > **Experimentell:** Dieses Projekt befindet sich noch in aktiver Entwicklung und kann breaking changes unterliegen. Matchlock ist ein CLI-Tool zum Ausführen von KI-Agenten in flüchtigen Mikro-VMs – mit Netzwerk-Whitelisting, Secret-Injection via MITM-Proxy und VM-Isolation. Deine Secrets gelangen niemals in die VM. ## Warum Matchlock? KI-Agenten müssen Code ausführen, aber ihnen uneingeschränkten Zugriff auf deinen Rechner zu geben, ist riskant. Matchlock ermöglicht es dir, einem Agenten eine vollständige Linux-Umgebung zu geben, die in unter einer Sekunde startet – isoliert und wegwerfbar. Wenn du `--allow-host` oder `--secret` übergibst, versiegelt Matchlock das Netzwerk – nur Traffic zu explizit erlaubten Hosts kommt durch, alles andere wird blockiert. Wenn dein Agent eine API aufruft, werden die echten Anmeldedaten während der Übertragung vom Host eingefügt. Die Sandbox sieht nur einen Platzhalter. Selbst wenn der Agent dazu gebracht wird, etwas Bösartiges auszuführen, werden deine Schlüssel nicht preisgegeben und es gibt keinen Zielort für Daten. Im Inneren erhält der Agent eine vollständige Linux-Umgebung, um das zu tun, was er tun muss. Er kann Pakete installieren, Dateien schreiben und Chaos anrichten. Außen spürt dein Rechner nichts. Volume-Overlay-Mounts sind isolierte Snapshots, die verschwinden, wenn du fertig bist. Gleiches CLI und gleiches Verhalten, egal ob du auf einem Linux-Server oder einem MacBook arbeitest. ## Schnellstart ### Systemvoraussetzungen - **Linux** mit KVM-Unterstützung - **macOS** auf Apple Silicon ### Installation Siehe [`docs/install.md`](https://github.com/jingkaihe/matchlock/blob/main/docs/install.md) für vollständige Installationsdetails. **Schnellinstallation** Das folgende Skript erkennt das Betriebssystem und installiert Matchlock mit Homebrew auf macOS und mit rpm/deb auf Debian/RHEL-basierten Linux-Distributionen. ```bash curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash # Oder eine bestimmte Version installieren curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash -s -- --version 0.2.4 ``` **Homebrew** Die Installation über Homebrew wird sowohl auf macOS als auch auf Linux unterstützt: ```bash brew tap jingkaihe/essentials brew install matchlock ``` **Debian / Ubuntu (.deb)** ```bash sudo dpkg -i ./matchlock_<version>_linux_amd64.deb sudo apt-get install -f matchlock diagnose ``` **Fedora / RHEL / CentOS Stream (.rpm)** ```bash sudo dnf install ./matchlock_<version>_linux_amd64.rpm matchlock diagnose ``` Wenn `matchlock diagnose` eine fehlende Host-Einrichtung meldet, führe Folgendes aus: ```bash sudo matchlock setup linux ``` Um einen bestimmten Benutzer explizit zu registrieren, führe Folgendes aus: ```bash sudo matchlock setup user <name> ``` ### Verwendung ```bash # Basis 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' # Netzwerk-Whitelist matchlock run --image python:3.12-alpine \ --allow-host "api.openai.com" python agent.py # Halte die Abfangfunktion auch bei leerer Whitelist aktiviert, # damit Hosts zur Laufzeit hinzugefügt/entfernt werden können. 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 # Secret-Injection (gelangt niemals in die VM) export ANTHROPIC_API_KEY=sk-xxx matchlock run --image python:3.12-alpine \ --secret [email protected] python call_api.py # Langlebige Sandboxen matchlock run --image alpine:latest --rm=false # gibt VM-ID aus matchlock run --image nginx:latest -d # dasselbe wie oben, detached matchlock exec vm-abc12345 -it sh # daran anhängen matchlock port-forward vm-abc12345 8080:8080 # forward host:8080 -> guest:8080 # Ports beim Start veröffentlichen matchlock run --image alpine:latest --rm=false -p 8080:8080 # Lebenszyklus matchlock list | kill | rm | prune # Aus Dockerfile bauen (verwendet BuildKit-in-VM) matchlock build -f Dockerfile -t myapp:latest . # Rootfs aus Registry-Image vorab erstellen (cached für schnelleren Start) matchlock build alpine:latest # Image-Verwaltung matchlock image ls # Alle Images auflisten matchlock image rm myapp:latest # Ein lokales Image entfernen docker save myapp:latest | matchlock image import myapp:latest # Aus Tarball importieren ``` ## SDK Matchlock liefert Go-, Python- und TypeScript-SDKs zum Einbetten von Sandboxen direkt in deine Anwendung. Du kannst VMs starten, Befehle ausführen, Ausgaben streamen und Dateien programmatisch verwalten. **Go** ```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) } // The VM only ever sees a placeholder - the real key never enters the sandbox result, err := client.Exec(ctx, "echo $ANTHROPIC_API_KEY") if err != nil { panic(err) } fmt.Print(result.Stdout) // prints "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) } } ``` Go SDK Verhalten bei privaten IPs (`10/8`, `172.16/12`, `192.168/16`): - Standard (nicht gesetzt): Private IPs werden blockiert, sobald eine Netzwerkkonfiguration gesendet wird. - Explizite Blockierung: `.WithBlockPrivateIPs(true)` (oder `.BlockPrivateIPs()`) aufrufen. - Explizite Erlaubnis: `.AllowPrivateIPs()` oder `.WithBlockPrivateIPs(false)` aufrufen. ```go sandbox := sdk.New("alpine:latest"). AllowHost("api.openai.com"). AddHost("api.internal", "10.0.0.10"). WithNetworkMTU(1200). AllowPrivateIPs() // explizite Überschreibung: block_private_ips=false // SDK Netzwerkabfangung (Request/Response-Mutation, Body-Shaping, SSE-Datenzeilen-Transformation) 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"}, }, }, }, }) ``` Wenn du `client.Create(...)` direkt verwendest (ohne den Builder), setze: - `BlockPrivateIPsSet: true` - `BlockPrivateIPs: false` (oder `true`) Für vollständig offline Sandboxen (kein Gast-NIC / kein ausgehender Verkehr) verwende: - CLI: `--no-network` - Go SDK Builder: `.WithNoNetwork()` - Python SDK Builder: `.with_no_network()` - TypeScript SDK Builder: `.withNoNetwork()` **Python** ([PyPI](https://pypi.org/project/matchlock/)) ```bash pip install matchlock # or uv add matchlock ``` ```python 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** ```bash npm install matchlock-sdk ``` ```ts 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(); } ``` Weitere Beispiele im Verzeichnis [`examples/`](https://github.com/jingkaihe/matchlock/blob/main/examples): | Beschreibung | Beispiel | |---|---| | Streamt Anthropic-API-Antwort mit Secret-Injection (Go) | [`examples/go/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/basic) | | Interaktives Terminal mit PTY mittels ExecInteractive (Go) | [`examples/go/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/exec_modes) | | Injiziert API-Key über Network-Interception-Hook (Go) | [`examples/go/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/network_interception) | | VFS-Interception-Hooks für Dateioperations-Mutationen (Go) | [`examples/go/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/vfs_hooks) | | Streamt Anthropic-API-Antwort (Python) | [`examples/python/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/basic) | | Stream-, Pipe- und interaktive Ausführungsmodi (Python) | [`examples/python/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/exec_modes) | | Injiziert API-Key über Network-Interception-Hook (Python) | [`examples/python/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/network_interception) | | VFS-Interception-Hooks für Dateioperations-Mutationen (Python) | [`examples/python/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/vfs_hooks) | | Streamt Anthropic-API-Antwort (TypeScript) | [`examples/typescript/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/basic) | | Stream-, Pipe- und interaktive Ausführungsmodi (TypeScript) | [`examples/typescript/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/exec_modes) | | Injiziert API-Key über Network-Interception-Hook (TypeScript) | [`examples/typescript/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/network_interception) | | Claude Code CLI in Mikro-VM mit GitHub-Bootstrap | [`examples/claude-code/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code) | | Claude Code mit Docker in der Sandbox via SDK | [`examples/claude-code-with-docker/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code-with-docker) | | Claude Code mit Claude Pro/Max-Abonnement in der Sandbox | [`examples/claude-danger/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-danger) | | OpenAI Codex CLI in Mikro-VM mit GitHub-Bootstrap | [`examples/codex/`](https://github.com/jingkaihe/matchlock/blob/main/examples/codex) | | Docker-Daemon in der Sandbox mit systemd | [`examples/docker-in-sandbox/`](https://github.com/jingkaihe/matchlock/blob/main/examples/docker-in-sandbox) | | Streamlit-Chatbot mit Agent-Client-Protocol | [`examples/agent-client-protocol/`](https://github.com/jingkaihe/matchlock/blob/main/examples/agent-client-protocol) | | Browser-Automatisierung mit Kodelet und Playwright MCP | [`examples/playwright/`](https://github.com/jingkaihe/matchlock/blob/main/examples/playwright) | ## Architektur ```mermaid 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 ``` ### Netzwerk-Modi | Plattform | Modus | Mechanismus | |-----------|-------|-------------| | Linux | Transparenter Proxy | nftables DNAT auf Ports 80/443 | | macOS | NAT (Standard) | Virtualization.framework integrierter NAT | | macOS | Abfangung (mit `--allow-host`/`--secret`) | gVisor Userspace TCP/IP auf Layer 4 | ## Docs - [Runbook für Lebenszyklus und Bereinigung](https://github.com/jingkaihe/matchlock/blob/main/docs/lifecycle.md) - [Netzwerkabfangung](https://github.com/jingkaihe/matchlock/blob/main/docs/network-interception.md) - [VFS-Abfangung](https://github.com/jingkaihe/matchlock/blob/main/docs/vfs-interception.md) - [Entwickler-Referenz](https://github.com/jingkaihe/matchlock/blob/main/AGENTS.md) ## Lizenz MIT