
Sandbox efêmero de microVM para agentes de IA com listagem de permissões de rede, injeção de segredos via proxy MITM e isolamento em nível de VM. Inicializa em menos de um segundo, suporta SDKs Go/Python/TypeScript.
# Matchlock > **Experimental:** Este projeto ainda está em desenvolvimento ativo e sujeito a mudanças significativas. Matchlock é uma ferramenta CLI para executar agentes de IA em microVMs efêmeras - com lista de permissões de rede, injeção de segredos via proxy MITM e isolamento em nível de VM. Seus segredos nunca entram na VM. ## Por que Matchlock? Agentes de IA precisam executar código, mas dar-lhes acesso irrestrito à sua máquina é um risco. Matchlock permite que você entregue a um agente um ambiente Linux completo que inicializa em menos de um segundo - isolado e descartável. Quando você passa `--allow-host` ou `--secret`, Matchlock sela a rede - apenas tráfego para hosts explicitamente permitidos passa, e todo o resto é bloqueado. Quando seu agente chama uma API, as credenciais reais são injetadas em voo pelo host. O sandbox só vê um espaço reservado. Mesmo que o agente seja enganado para executar algo malicioso, suas chaves não vazam e não há para onde os dados irem. Dentro, o agente obtém um ambiente Linux completo para fazer o que precisar. Ele pode instalar pacotes, escrever arquivos e fazer bagunça. Lá fora, sua máquina não sente nada. Montagens de sobreposição de volume são snapshots isolados que desaparecem quando você termina. Mesmo CLI e mesmo comportamento, seja em um servidor Linux ou em um MacBook. ## Início Rápido ### Requisitos de Sistema - **Linux** com suporte a KVM - **macOS** em Apple Silicon ### Instalar Veja [`docs/install.md`](https://github.com/jingkaihe/matchlock/blob/main/docs/install.md) para detalhes completos de instalação. **Instalação Rápida** O script abaixo detecta o sistema operacional e instala o matchlock usando Homebrew no macOS e rpm/deb em distribuições Linux Debian/RHEL. ```bash curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash # Or install a specific release curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash -s -- --version 0.2.4 ``` **Homebrew** A instalação baseada em Homebrew é suportada tanto no macOS quanto no 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` relatar configuração de host ausente, execute: ```bash sudo matchlock setup linux ``` Para inscrever um usuário específico explicitamente, execute: ```bash sudo matchlock setup user <name> ``` ### Uso ```bash # Básico 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' # Lista de permissões de rede matchlock run --image python:3.12-alpine \ --allow-host "api.openai.com" python agent.py # Mantenha a interceptação ativada mesmo com uma lista de permissões vazia, # para que hosts possam ser adicionados/removidos em tempo de execução. 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 # Injeção de segredos (nunca entra na VM) export ANTHROPIC_API_KEY=sk-xxx matchlock run --image python:3.12-alpine \ --secret [email protected] python call_api.py # Sandboxes de longa duração matchlock run --image alpine:latest --rm=false # prints VM ID matchlock run --image nginx:latest -d # same as above, detached matchlock exec vm-abc12345 -it sh # attach to it matchlock port-forward vm-abc12345 8080:8080 # forward host:8080 -> guest:8080 # Publicar portas na inicialização matchlock run --image alpine:latest --rm=false -p 8080:8080 # Ciclo de vida matchlock list | kill | rm | prune # Construir a partir de Dockerfile (usa BuildKit na VM) matchlock build -f Dockerfile -t myapp:latest . # Pré-construir rootfs a partir de imagem de registro (armazena em cache para inicialização mais rápida) matchlock build alpine:latest # Gerenciamento de imagens matchlock image ls # List all images matchlock image rm myapp:latest # Remove a local image docker save myapp:latest | matchlock image import myapp:latest # Import from tarball ``` ## SDK Matchlock oferece SDKs em Go, Python e TypeScript para incorporar sandboxes diretamente em sua aplicação. Você pode iniciar VMs, executar comandos, transmitir saída e gerenciar arquivos programaticamente. **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) } } ``` Comportamento de IP privado do SDK Go (`10/8`, `172.16/12`, `192.168/16`): - Padrão (não definido): IPs privados são bloqueados sempre que uma configuração de rede é enviada. - Bloqueio explícito: chame `.WithBlockPrivateIPs(true)` (ou `.BlockPrivateIPs()`). - Permissão explícita: chame `.AllowPrivateIPs()` ou `.WithBlockPrivateIPs(false)`. ```go sandbox := sdk.New("alpine:latest"). AllowHost("api.openai.com"). AddHost("api.internal", "10.0.0.10"). WithNetworkMTU(1200). AllowPrivateIPs() // explicit override: block_private_ips=false // Interceptação de rede do SDK (mutação de requisição/resposta, formatação de corpo, transformação de linha de dados 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 você usar `client.Create(...)` diretamente (sem o construtor), defina: - `BlockPrivateIPsSet: true` - `BlockPrivateIPs: false` (ou `true`) Para sandboxes totalmente offline (sem NIC convidado / sem egresso), use: - CLI: `--no-network` - Construtor Go SDK: `.WithNoNetwork()` - Construtor Python SDK: `.with_no_network()` - Construtor 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(); } ``` Mais exemplos no diretório [`examples/`](https://github.com/jingkaihe/matchlock/blob/main/examples): | Descrição | Exemplo | |---|---| | Transmite resposta da API Anthropic com injeção de segredo (Go) | [`examples/go/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/basic) | | Terminal interativo com PTY usando ExecInteractive (Go) | [`examples/go/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/exec_modes) | | Injeta chave de API via hook de interceptação de rede (Go) | [`examples/go/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/network_interception) | | Hooks de interceptação VFS para mutações de operações de arquivo (Go) | [`examples/go/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/vfs_hooks) | | Transmite resposta da API Anthropic (Python) | [`examples/python/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/basic) | | Modos de execução stream, pipe e interativo (Python) | [`examples/python/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/exec_modes) | | Injeta chave de API via hook de interceptação de rede (Python) | [`examples/python/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/network_interception) | | Hooks de interceptação VFS para mutações de operações de arquivo (Python) | [`examples/python/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/vfs_hooks) | | Transmite resposta da API Anthropic (TypeScript) | [`examples/typescript/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/basic) | | Modos de execução stream, pipe e interativo (TypeScript) | [`examples/typescript/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/exec_modes) | | Injeta chave de API via hook de interceptação de rede (TypeScript) | [`examples/typescript/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/network_interception) | | CLI do Claude Code em micro-VM com bootstrap do GitHub | [`examples/claude-code/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code) | | Claude Code com Docker dentro do sandbox via SDK | [`examples/claude-code-with-docker/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code-with-docker) | | Claude Code com assinatura Claude Pro/Max no sandbox | [`examples/claude-danger/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-danger) | | CLI do OpenAI Codex em micro-VM com bootstrap do GitHub | [`examples/codex/`](https://github.com/jingkaihe/matchlock/blob/main/examples/codex) | | Daemon Docker dentro do sandbox com systemd | [`examples/docker-in-sandbox/`](https://github.com/jingkaihe/matchlock/blob/main/examples/docker-in-sandbox) | | Chatbot Streamlit usando Agent Client Protocol | [`examples/agent-client-protocol/`](https://github.com/jingkaihe/matchlock/blob/main/examples/agent-client-protocol) | | Automação de navegador com Kodelet e Playwright MCP | [`examples/playwright/`](https://github.com/jingkaihe/matchlock/blob/main/examples/playwright) | ## Arquitetura ```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 ``` ### Modos de Rede | Plataforma | Modo | Mecanismo | |------------|------|-----------| | Linux | Proxy transparente | nftables DNAT nas portas 80/443 | | macOS | NAT (padrão) | NAT embutido do Virtualization.framework | | macOS | Interceptação (com `--allow-host`/`--secret`) | TCP/IP em espaço de usuário do gVisor na camada 4 | ## Documentação - [Manual de ciclo de vida e limpeza](https://github.com/jingkaihe/matchlock/blob/main/docs/lifecycle.md) - [Interceptação de Rede](https://github.com/jingkaihe/matchlock/blob/main/docs/network-interception.md) - [Interceptação VFS](https://github.com/jingkaihe/matchlock/blob/main/docs/vfs-interception.md) - [Referência do Desenvolvedor](https://github.com/jingkaihe/matchlock/blob/main/AGENTS.md) ## Licença MIT