
AIエージェント向けの一時的なmicroVMサンドボックス。ネットワークの許可リスト化、MITMプロキシによるシークレットインジェクション、VMレベルの分離を提供。1秒未満で起動し、Go/Python/TypeScript SDKをサポート。
# Matchlock > **実験段階:** このプロジェクトは現在活発に開発中であり、破壊的変更が行われる可能性があります。 Matchlock は、一時的なマイクロVM内でAIエージェントを実行するためのCLIツールです。ネットワークの許可リスト、MITMプロキシによるシークレットインジェクション、VMレベルの分離を備えています。シークレットがVMに入ることはありません。 ## なぜMatchlockか? AIエージェントはコードを実行する必要がありますが、無制限にマシンへのアクセスを与えることはリスクです。Matchlock では、エージェントに1秒以内で起動する完全なLinux環境を提供します。分離されており、使い捨てです。 `--allow-host` や `--secret` を渡すと、Matchlock はネットワークを封印します。明示的に許可されたホストへのトラフィックのみを通し、それ以外はすべてブロックします。エージェントがAPIを呼び出すと、実際の認証情報がホスト側でインフライトインジェクションされます。サンドボックスはプレースホルダのみを認識します。たとえエージェントが悪意のあるコードを実行するように騙されても、キーは漏洩せず、データが送信される場所もありません。内部ではエージェントは完全なLinux環境を自由に使えます。パッケージのインストール、ファイルの作成、何でもできます。外部ではマシンは何も感じません。ボリュームオーバーレイマウントは分離されたスナップショットで、使い終われば消えます。LinuxサーバでもMacBookでも同じCLI、同じ動作です。 ## クイックスタート ### システム要件 - **KVM対応のLinux** - **Apple Silicon搭載のmacOS** ### インストール 完全なインストール手順は [`docs/install.md`](https://github.com/jingkaihe/matchlock/blob/main/docs/install.md) を参照してください。 **クイックインストール** 以下のスクリプトはOSを検出し、macOSではHomebrew、Debian/RHEL系Linuxではrpm/debを使用してmatchlockをインストールします。 ```bash curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash # 特定のリリースをインストールする場合 curl -fsSL https://raw.githubusercontent.com/jingkaihe/matchlock/main/scripts/install.sh | bash -s -- --version 0.2.4 ``` **Homebrew** HomebrewベースのインストールはmacOSと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 ``` `matchlock diagnose` がホストのセットアップ不足を報告した場合は、以下を実行してください。 ```bash sudo matchlock setup linux ``` 特定のユーザーを明示的に登録するには、以下を実行します。 ```bash sudo matchlock setup user <name> ``` ### 使い方 ```bash # 基本 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' # ネットワーク許可リスト matchlock run --image python:3.12-alpine \ --allow-host "api.openai.com" python agent.py # 空の許可リストでもインターセプションを有効にしておくことで、 # 実行時にホストを追加/削除できるようにする。 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 # シークレットインジェクション(VMには決して入らない) export ANTHROPIC_API_KEY=sk-xxx matchlock run --image python:3.12-alpine \ --secret [email protected] python call_api.py # 長期存続するサンドボックス matchlock run --image alpine:latest --rm=false # VM IDを表示 matchlock run --image nginx:latest -d # 上と同様、デタッチ matchlock exec vm-abc12345 -it sh # アタッチ matchlock port-forward vm-abc12345 8080:8080 # ホスト:8080 -> ゲスト:8080 に転送 # 起動時にポートを公開 matchlock run --image alpine:latest --rm=false -p 8080:8080 # ライフサイクル matchlock list | kill | rm | prune # Dockerfileからビルド(VM内のBuildKitを使用) matchlock build -f Dockerfile -t myapp:latest . # レジストリイメージからrootfsを事前ビルド(キャッシュにより起動が高速化) matchlock build alpine:latest # イメージ管理 matchlock image ls # 全イメージ一覧 matchlock image rm myapp:latest # ローカルイメージの削除 docker save myapp:latest | matchlock image import myapp:latest # tarballからインポート ``` ## SDK Matchlock は Go、Python、TypeScript の SDK を提供しており、サンドボックスを直接アプリケーションに組み込むことができます。VMの起動、コマンドの実行、出力のストリーミング、ファイル管理をプログラムから行えます。 **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) } // VMはプレースホルダしか見ません。実際のキーはサンドボックスに入りません result, err := client.Exec(ctx, "echo $ANTHROPIC_API_KEY") if err != nil { panic(err) } fmt.Print(result.Stdout) // "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 のプライベートIP動作(`10/8`、`172.16/12`、`192.168/16`): - デフォルト(未設定):ネットワーク設定が送られた場合、プライベートIPはブロックされます。 - 明示的なブロック:`.WithBlockPrivateIPs(true)`(または `.BlockPrivateIPs()`)を呼び出します。 - 明示的な許可:`.AllowPrivateIPs()` または `.WithBlockPrivateIPs(false)` を呼び出します。 ```go sandbox := sdk.New("alpine:latest"). AllowHost("api.openai.com"). AddHost("api.internal", "10.0.0.10"). WithNetworkMTU(1200). AllowPrivateIPs() // 明示的なオーバーライド:block_private_ips=false // SDKネットワークインターセプション(リクエスト/レスポンスの変更、ボディ整形、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"}, }, }, }, }) ``` `client.Create(...)` を直接(ビルダーなしで)使用する場合は、以下を設定します。 - `BlockPrivateIPsSet: true` - `BlockPrivateIPs: false`(または `true`) 完全にオフラインのサンドボックス(ゲストNICなし/発信なし)の場合は、次のいずれかを使用します。 - CLI:`--no-network` - Go SDK ビルダー:`.WithNoNetwork()` - Python SDK ビルダー:`.with_no_network()` - TypeScript SDK ビルダー:`.withNoNetwork()` **Python** ([PyPI](https://pypi.org/project/matchlock/)) ```bash pip install matchlock # または 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(); } ``` その他の例は [`examples/`](https://github.com/jingkaihe/matchlock/blob/main/examples) ディレクトリにあります。 | 説明 | 例 | |---|---| | シークレットインジェクションを使用した Anthropic API レスポンスのストリーミング(Go) | [`examples/go/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/basic) | | ExecInteractive を使った PTY による対話型ターミナル(Go) | [`examples/go/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/exec_modes) | | ネットワークインターセプションフックによる API キーのインジェクション(Go) | [`examples/go/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/network_interception) | | ファイル操作の変更を行う VFS インターセプションフック(Go) | [`examples/go/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/go/vfs_hooks) | | Anthropic API レスポンスのストリーミング(Python) | [`examples/python/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/basic) | | ストリーム、パイプ、対話型実行モード(Python) | [`examples/python/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/exec_modes) | | ネットワークインターセプションフックによる API キーのインジェクション(Python) | [`examples/python/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/network_interception) | | ファイル操作の変更を行う VFS インターセプションフック(Python) | [`examples/python/vfs_hooks/`](https://github.com/jingkaihe/matchlock/blob/main/examples/python/vfs_hooks) | | Anthropic API レスポンスのストリーミング(TypeScript) | [`examples/typescript/basic/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/basic) | | ストリーム、パイプ、対話型実行モード(TypeScript) | [`examples/typescript/exec_modes/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/exec_modes) | | ネットワークインターセプションフックによる API キーのインジェクション(TypeScript) | [`examples/typescript/network_interception/`](https://github.com/jingkaihe/matchlock/blob/main/examples/typescript/network_interception) | | マイクロVM内の Claude Code CLI(GitHub ブートストラップ付き) | [`examples/claude-code/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code) | | SDK 経由でサンドボックス内に Docker を内包した Claude Code | [`examples/claude-code-with-docker/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-code-with-docker) | | サンドボックス内で Claude Pro/Max サブスクリプションを使用する Claude Code | [`examples/claude-danger/`](https://github.com/jingkaihe/matchlock/blob/main/examples/claude-danger) | | マイクロVM内の OpenAI Codex CLI(GitHub ブートストラップ付き) | [`examples/codex/`](https://github.com/jingkaihe/matchlock/blob/main/examples/codex) | | systemd を使用したサンドボックス内の Docker デーモン | [`examples/docker-in-sandbox/`](https://github.com/jingkaihe/matchlock/blob/main/examples/docker-in-sandbox) | | Agent Client Protocol を使用した Streamlit チャットボット | [`examples/agent-client-protocol/`](https://github.com/jingkaihe/matchlock/blob/main/examples/agent-client-protocol) | | Kodelet と Playwright MCP を使用したブラウザ自動化 | [`examples/playwright/`](https://github.com/jingkaihe/matchlock/blob/main/examples/playwright) | ## アーキテクチャ ```mermaid graph LR subgraph Host CLI["Matchlock CLI"] Policy["ポリシーエンジン"] Proxy["透過プロキシ + TLS MITM"] VFS["VFS サーバー"] CLI --> Policy CLI --> Proxy Policy --> Proxy end subgraph VM["マイクロVM(Firecracker / Virtualization.framework)"] Agent["ゲストエージェント"] FUSE["/workspace (FUSE)"] Image["任意の OCI イメージ(Alpine、Ubuntu など)"] Agent --- Image FUSE --- Image end Proxy -- "vsock :5000" --> Agent VFS -- "vsock :5001" --> FUSE ``` ### ネットワークモード | プラットフォーム | モード | 仕組み | |----------|------|-----------| | Linux | 透過プロキシ | nftables によるポート 80/443 の DNAT | | macOS | NAT(デフォルト) | Virtualization.framework 組み込み NAT | | macOS | インターセプション(`--allow-host`/`--secret` 使用時) | gVisor ユーザースペース TCP/IP(L4) | ## ドキュメント - [ライフサイクルとクリーンアップの Runbook](https://github.com/jingkaihe/matchlock/blob/main/docs/lifecycle.md) - [ネットワークインターセプション](https://github.com/jingkaihe/matchlock/blob/main/docs/network-interception.md) - [VFS インターセプション](https://github.com/jingkaihe/matchlock/blob/main/docs/vfs-interception.md) - [開発者向けリファレンス](https://github.com/jingkaihe/matchlock/blob/main/AGENTS.md) ## ライセンス MIT