
Sandbox microVM effimero per agenti AI con allowlisting di rete, iniezione di segreti tramite proxy MITM e isolamento a livello di VM. Si avvia in meno di un secondo, supporta SDK Go/Python/TypeScript.
# Matchlock > **Sperimentale:** Questo progetto è ancora in fase di sviluppo attivo e soggetto a modifiche sostanziali. Matchlock è uno strumento CLI per eseguire agenti AI in microVM effimere - con allowlisting di rete, iniezione di segreti tramite proxy MITM e isolamento a livello di VM. I tuoi segreti non entrano mai nella VM. ## Perché Matchlock? Gli agenti AI devono eseguire codice, ma dare loro accesso illimitato alla tua macchina è un rischio. Matchlock ti permette di fornire a un agente un ambiente Linux completo che si avvia in meno di un secondo - isolato e usa e getta. Quando passi `--allow-host` o `--secret`, Matchlock sigilla la rete - solo il traffico verso host esplicitamente consentiti passa, tutto il resto viene bloccato. Quando il tuo agente chiama un'API, le credenziali reali vengono iniettate in volo dall'host. La sandbox vede solo un segnaposto. Anche se l'agente viene indotto a eseguire qualcosa di malevolo, le tue chiavi non vengono esposte e non c'è nessun posto dove i dati possano andare. All'interno l'agente riceve un ambiente Linux completo per fare tutto ciò di cui ha bisogno. Può installare pacchetti, scrivere file e fare confusione. All'esterno la tua macchina non sente nulla. I mount overlay del volume sono snapshot isolati che svaniscono quando hai finito. Stessa CLI e stesso comportamento, sia che tu sia su un server Linux che su un MacBook. ## Guida rapida ### Requisiti di sistema - **Linux** con supporto KVM - **macOS** su Apple Silicon ### Installazione Vedi [`docs/install.md`](https://github.com/jingkaihe/matchlock/blob/main/docs/install.md) per i dettagli completi sull'installazione. **Installazione rapida** Lo script seguente rileva il sistema operativo e installa matchlock usando Homebrew su macOS, e rpm/deb su distribuzioni Linux Debian/RHEL. ```bash curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash # Oppure installa una release specifica curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash -s -- --version 0.2.4 ``` **Homebrew** L'installazione basata su Homebrew è supportata sia su macOS che su Linux: ```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 ``` Se `matchlock diagnose` segnala una configurazione host mancante, esegui: ```bash sudo matchlock setup linux ``` Per registrare esplicitamente un utente specifico, esegui: ```bash sudo matchlock setup user <nome> ``` ### Utilizzo ```bash # Base 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' # Allowlist di rete matchlock run --image python:3.12-alpine \ --allow-host "api.openai.com" python agent.py # Mantieni l'intercettazione abilitata anche con una allowlist vuota, # per poter aggiungere/rimuovere host in fase di esecuzione. 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 # Iniezione di segreti (non entra mai nella VM) export ANTHROPIC_API_KEY=sk-xxx matchlock run --image python:3.12-alpine \ --secret [email protected] python call_api.py # Sandbox a lunga durata matchlock run --image alpine:latest --rm=false # stampa l'ID della VM matchlock run --image nginx:latest -d # stesso di sopra, in detached matchlock exec vm-abc12345 -it sh # si collega matchlock port-forward vm-abc12345 8080:8080 # forward host:8080 -> guest:8080 # Pubblica porte all'avvio matchlock run --image alpine:latest --rm=false -p 8080:8080 # Ciclo di vita matchlock list | kill | rm | prune # Build da Dockerfile (usa BuildKit-in-VM) matchlock build -f Dockerfile -t myapp:latest . # Pre-build rootfs da immagine registry (caching per avvii più veloci) matchlock build alpine:latest # Gestione immagini matchlock image ls # Elenca tutte le immagini matchlock image rm myapp:latest # Rimuove un'immagine locale docker save myapp:latest | matchlock image import myapp:latest # Importa da tarball ``` ## SDK Matchlock include SDK per Go, Python e TypeScript per incorporare sandbox direttamente nella tua applicazione. Puoi avviare VM, eseguire comandi, ottenere output in streaming e gestire file a livello di programmazione. **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) } // La VM vede solo un segnaposto - la chiave reale non entra mai nella sandbox result, err := client.Exec(ctx, "echo $ANTHROPIC_API_KEY") if err != nil { panic(err) } fmt.Print(result.Stdout) // stampa "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) } } ``` Comportamento degli IP privati del Go SDK (`10/8`, `172.16/12`, `192.168/16`): - Default (non impostato): gli IP privati vengono bloccati ogni volta che viene inviata una configurazione di rete. - Blocco esplicito: chiama `.WithBlockPrivateIPs(true)` (o `.BlockPrivateIPs()`). - Permesso esplicito: chiama `.AllowPrivateIPs()` o `.WithBlockPrivateIPs(false)`. ```go sandbox := sdk.New("alpine:latest"). AllowHost("api.openai.com"). AddHost("api.internal", "10.0.0.10"). WithNetworkMTU(1200). AllowPrivateIPs() // override esplicito: block_private_ips=false // Intercettazione di rete SDK (mutazione richiesta/risposta, shaping del corpo, trasformazione delle linee dati 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"}, }, }, }, }) ``` Se usi `client.Create(...)` direttamente (senza il builder), imposta: - `BlockPrivateIPsSet: true` - `BlockPrivateIPs: false` (o `true`) Per sandbox completamente offline (nessuna NIC guest / nessuna uscita), usa: - CLI: `--no-network` - Builder Go SDK: `.WithNoNetwork()` - Builder Python SDK: `.with_no_network()` - Builder TypeScript SDK: `.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(); } ``` Altri esempi nella directory [`examples/`](https://github.com/jingkaihe/matchlock/blob/main/examples): | Descrizione | Esempio | |---|---| | Stream della risposta API Anthropic con iniezione di segreti (Go) | [`examples/go/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/basic) | | Terminale interattivo con PTY usando ExecInteractive (Go) | [`examples/go/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/exec_modes) | | Inietta chiave API tramite hook di intercettazione di rete (Go) | [`examples/go/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/network_interception) | | Hook di intercettazione VFS per mutazioni di operazioni su file (Go) | [`examples/go/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/vfs_hooks) | | Stream della risposta API Anthropic (Python) | [`examples/python/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/basic) | | Modalità di esecuzione stream, pipe e interattiva (Python) | [`examples/python/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/exec_modes) | | Inietta chiave API tramite hook di intercettazione di rete (Python) | [`examples/python/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/network_interception) | | Hook di intercettazione VFS per mutazioni di operazioni su file (Python) | [`examples/python/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/vfs_hooks) | | Stream della risposta API Anthropic (TypeScript) | [`examples/typescript/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/basic) | | Modalità di esecuzione stream, pipe e interattiva (TypeScript) | [`examples/typescript/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/exec_modes) | | Inietta chiave API tramite hook di intercettazione di rete (TypeScript) | [`examples/typescript/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/network_interception) | | Claude Code CLI in micro-VM con bootstrap GitHub | [`examples/claude-code/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code) | | Claude Code con Docker all'interno della sandbox tramite SDK | [`examples/claude-code-with-docker/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code-with-docker) | | Claude Code con abbonamento Claude Pro/Max nella sandbox | [`examples/claude-danger/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-danger) | | OpenAI Codex CLI in micro-VM con bootstrap GitHub | [`examples/codex/`](https://github.com/jingkaihe/matchlock/blob/main/examples/codex) | | Demone Docker all'interno della sandbox con systemd | [`examples/docker-in-sandbox/`](https://github.com/jingkaihe/matchlock/blob/main/examples/docker-in-sandbox) | | Chatbot Streamlit che utilizza Agent Client Protocol | [`examples/agent-client-protocol/`](https://github.com/jingkaihe/matchlock/blob/main/examples/agent-client-protocol) | | Automazione del browser con Kodelet e Playwright MCP | [`examples/playwright/`](https://github.com/jingkaihe/matchlock/blob/main/examples/playwright) | ## Architettura ```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 ``` ### Modalità di rete | Piattaforma | Modalità | Meccanismo | |-------------|----------|------------| | Linux | Proxy trasparente | nftables DNAT sulle porte 80/443 | | macOS | NAT (predefinita) | NAT integrato di Virtualization.framework | | macOS | Intercettazione (con `--allow-host`/`--secret`) | gVisor userspace TCP/IP a livello L4 | ## Documentazione - [Runbook del ciclo di vita e pulizia](https://github.com/jingkaihe/matchlock/blob/main/docs/lifecycle.md) - [Intercettazione di rete](https://github.com/jingkaihe/matchlock/blob/main/docs/network-interception.md) - [Intercettazione VFS](https://github.com/jingkaihe/matchlock/blob/main/docs/vfs-interception.md) - [Riferimento per sviluppatori](https://github.com/jingkaihe/matchlock/blob/main/AGENTS.md) ## Licenza MIT