
에페머럴 마이크로VM 샌드박스로, AI 에이전트를 위한 네트워크 허용 목록, MITM 프록시를 통한 비밀 주입, VM 수준 격리를 제공합니다. 1초 미만으로 부팅되며, Go/Python/TypeScript SDK를 지원합니다.
실험적: 이 프로젝트는 아직 활발히 개발 중이며, 변경 사항이 발생할 수 있습니다.
Matchlock는 일시적인 마이크로VM에서 AI 에이전트를 실행하기 위한 CLI 도구입니다. 네트워크 허용 목록, MITM 프록시를 통한 비밀 주입, VM 수준 격리를 제공합니다. 사용자의 비밀은 VM에 절대 들어가지 않습니다.
AI 에이전트는 코드를 실행해야 하지만, 에이전트에게 머신에 대한 무제한 액세스를 허용하는 것은 위험합니다. Matchlock를 사용하면 1초 미만에 부팅되는 완전한 Linux 환경을 에이전트에 제공할 수 있습니다. 격리되어 있고 일회용입니다.
--allow-host 또는 --secret을 전달하면 Matchlock가 네트워크를 봉쇄합니다. 명시적으로 허용된 호스트로의 트래픽만 통과하고, 나머지는 모두 차단됩니다. 에이전트가 API를 호출할 때 실제 자격 증명은 호스트에 의해 전송 중에 주입됩니다. 샌드박스는 항상 자리 표시자만 볼 수 있습니다. 에이전트가 악성 코드를 실행하도록 속아도 키가 유출되지 않으며 데이터가 이동할 곳이 없습니다. 내부에서는 에이전트가 필요한 모든 작업을 수행할 수 있는 완전한 Linux 환경을 얻습니다. 패키지를 설치하고, 파일을 쓰고, 지저분하게 만들 수 있습니다. 외부에서는 사용자 머신이 아무것도 느끼지 못합니다. 볼륨 오버레이 마운트는 작업이 끝나면 사라지는 격리된 스냅샷입니다. Linux 서버든 MacBook이든 동일한 CLI와 동일한 동작을 제공합니다.
전체 설치 세부 사항은 docs/install.md를 참조하세요.
빠른 설치
아래 스크립트는 OS를 감지하고, macOS에서는 Homebrew를 사용하여 matchlock을 설치하고, Debian/RHEL 계열 Linux 배포판에서는 rpm/deb를 사용하여 설치합니다.
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
Homebrew 기반 설치는 macOS와 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
matchlock diagnose에서 호스트 설정이 누락되었다고 보고하면 다음을 실행하세요:
sudo matchlock setup linux
특정 사용자를 명시적으로 등록하려면 다음을 실행하세요:
sudo matchlock setup user <name>
# Basic
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'
# Network allowlist
matchlock run --image python:3.12-alpine \
--allow-host "api.openai.com" python agent.py
# Keep interception enabled even with an empty allowlist,
# so hosts can be added/removed at runtime.
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 (never enters the VM)
export ANTHROPIC_API_KEY=sk-xxx
matchlock run --image python:3.12-alpine \
--secret [email protected] python call_api.py
# Long-lived sandboxes
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
# Publish ports at startup
matchlock run --image alpine:latest --rm=false -p 8080:8080
# Lifecycle
matchlock list | kill | rm | prune
# Build from Dockerfile (uses BuildKit-in-VM)
matchlock build -f Dockerfile -t myapp:latest .
# Pre-build rootfs from registry image (caches for faster startup)
matchlock build alpine:latest
# Image management
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
Matchlock는 애플리케이션에 샌드박스를 직접 포함할 수 있는 Go, Python, TypeScript SDK를 제공합니다. VM을 실행하고, 명령을 실행하고, 출력을 스트리밍하고, 파일을 프로그래밍 방식으로 관리할 수 있습니다.
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 개인 IP 동작 (10/8, 172.16/12, 192.168/16):
.WithBlockPrivateIPs(true) (또는 .BlockPrivateIPs()) 호출.AllowPrivateIPs() 또는 .WithBlockPrivateIPs(false) 호출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
// SDK network interception (request/response mutation, body shaping, SSE data-line transform)
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: trueBlockPrivateIPs: false (또는 true)완전 오프라인 샌드박스(게스트 NIC 없음/송신 없음)의 경우 다음을 사용하세요:
--no-network.WithNoNetwork().with_no_network().withNoNetwork()Python (PyPI)
pip install matchlock
# or
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();
}
더 많은 예제는 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
| 플랫폼 | 모드 | 메커니즘 |
|---|
MIT
| 설명 | 예제 |
|---|
| 비밀 주입을 사용한 Anthropic API 응답 스트리밍 (Go) | examples/go/basic/ |
| ExecInteractive를 사용한 PTY 대화형 터미널 (Go) | examples/go/exec_modes/ |
| 네트워크 인터셉션 훅을 통한 API 키 주입 (Go) | examples/go/network_interception/ |
| 파일 작업 변형을 위한 VFS 인터셉션 훅 (Go) | examples/go/vfs_hooks/ |
| Anthropic API 응답 스트리밍 (Python) | examples/python/basic/ |
| 스트림, 파이프 및 대화형 실행 모드 (Python) | examples/python/exec_modes/ |
| 네트워크 인터셉션 훅을 통한 API 키 주입 (Python) | examples/python/network_interception/ |
| 파일 작업 변형을 위한 VFS 인터셉션 훅 (Python) | examples/python/vfs_hooks/ |
| Anthropic API 응답 스트리밍 (TypeScript) | examples/typescript/basic/ |
| 스트림, 파이프 및 대화형 실행 모드 (TypeScript) | examples/typescript/exec_modes/ |
| 네트워크 인터셉션 훅을 통한 API 키 주입 (TypeScript) | examples/typescript/network_interception/ |
| GitHub 부트스트랩을 사용한 마이크로VM의 Claude Code CLI | examples/claude-code/ |
| SDK를 통한 샌드박스 내 Docker를 사용한 Claude Code | examples/claude-code-with-docker/ |
| 샌드박스에서 Claude Pro/Max 구독을 사용한 Claude Code | examples/claude-danger/ |
| GitHub 부트스트랩을 사용한 마이크로VM의 OpenAI Codex CLI | examples/codex/ |
| systemd를 사용한 샌드박스 내 Docker 데몬 | examples/docker-in-sandbox/ |
| Agent Client Protocol을 사용한 Streamlit 챗봇 | examples/agent-client-protocol/ |
| Kodelet 및 Playwright MCP를 사용한 브라우저 자동화 | examples/playwright/ |
| Linux | 투명 프록시 | 포트 80/443의 nftables DNAT |
| macOS | NAT (기본값) | Virtualization.framework 내장 NAT |
| macOS | 인터셉션 (--allow-host/--secret 사용 시) | L4의 gVisor 사용자 공간 TCP/IP |