perso — MCP 에이전트의 권한을 결정하는 WebAssembly 정책 엔진
요약
MCP(Model Context Protocol) 기반 에이전트의 도구 호출 권한을 제어하기 위한 WebAssembly 기반 정책 엔진 'perso'를 소개합니다. Rust로 개발된 이 엔진은 JSON으로 작성된 복잡한 액세스 규칙을 WASM 바이너리로 컴파일하여, 마이크로초 단위의 빠른 속도로 도구 호출의 허용 여부를 결정합니다.
핵심 포인트
- MCP 사양의 보안 공백인 도구 호출 권한 제어 문제를 해결
- WebAssembly를 활용하여 다양한 호스트 환경에 이식 가능한 단일 바이너리 제공
- 인자(arguments) 기반의 세밀한 조건부 정책(예: 금액 제한) 실행 가능
- O(1) 조회 성능을 통해 실시간 에이전트 워크플로우에 최적화
만약 여러분이 MCP (Model Context Protocol)를 기반으로 무언가를 구축하고 있다면, 결국 다음과 같은 질문에 직면하게 될 것입니다: LLM이 도구(tool)를 호출하기로 결정했을 때, 실제로 그것이 허용되는지 누가 확인하는가?
MCP의 사양(spec)은 도구가 어떻게 발견되고 호출되는지를 정의할 뿐 — 누가 무엇을 호출할 수 있는지, 혹은 어떤 조건 하에서 호출할 수 있는지에 대해서는 아무것도 말하지 않습니다. 그 부분은 호스트를 구축하는 사람에게 전적으로 맡겨져 있습니다. 이 문제가 해결되지 않은 상태에서 기본 설정은 모든 것이 열려 있는 상태입니다. 즉, 어떤 역할(role)이든 어떤 인자(argument)를 사용하여 어떤 도구든 호출할 수 있습니다. 임시방편으로 해결책을 덧붙이다 보면 보통 두 가지 패턴 중 하나로 끝나게 됩니다: 각 도구 구현 곳곳에 흩어져 있는 인증(auth) 로직, 또는 인자(argument)를 실제로 확인할 수 없는 거친 수준의 역할 확인(예: "에이전트는 환불을 처리할 수 있다"라고는 할 수 있지만, "하지만 500달러까지만 가능하다"라는 식의 표현은 불가능함)입니다.
역할과 도구가 몇 개 이상 늘어나면 이 방식은 더 이상 확장(scale)될 수 없습니다.
perso는 이 문제에 대한 실질적인 해답을 제공하기 위해 제가 만든 작은 Rust 프로젝트입니다: MCP 도구 호출을 위한 정책 집행 엔진(policy enforcement engine)이며, 단일 포터블 WebAssembly 바이너리로 컴파일됩니다.
아이디어
여러분은 액세스 규칙을 일반적인 JSON으로 작성합니다 — "에이전트는 환불을 처리할 수 있지만 500달러까지만 가능하다", "매니저는 자신이 소유한 레코드를 삭제할 수 있다", "이 도구는 MFA가 확인되지 않으면 차단된다" 등 — 그러면 perso가 이를 .wasm 바이너리로 컴파일합니다. 이 바이너리를 어떤 호스트(백엔드 서버, MCP 서버, 에지 함수, CLI)에든 넣기만 하면, 모든 도구 호출에 대해 마이크로초 단위로 단 하나의 질문에 답합니다: 허용(Allow)인가 거부(Deny)인가.
LLM은 역할 토큰(role token)을 절대 보거나 만지지 않습니다. 호스트가 자신의 세션/JWT에서 추출하여 이를 소유합니다. perso는 단지 정책에 따라 호출을 평가하고 결정 사항과 사람이 읽을 수 있는 이유를 반환할 뿐입니다.
json{ "tool_name": "process_refund", "roles": ["agent"], "condition": { "NumericCheck": { "source": "Arguments", "field": "amount", "op": "Lte", "value": 500.0 } } }
이 단 하나의 규칙만으로도, LLM이 얼마나 설득력 있게 시도하도록 유도되었든 상관없이 에이전트 역할이 800달러의 환불을 승인하는 것을 막기에 충분합니다.
조건(Conditions)은 인자(arguments), 에이전트 속성(agent attributes), 또는 리소스 속성(resource attributes)을 검사할 수 있으며, All/Any/Not과 결합하여 사용할 수 있습니다. 전체 규칙 세트는 로드 시점에 플랫 맵(flat map)으로 사전 확장(pre-expanded)되므로, 실제 모든 평가는 요청 시점에 와일드카드 매칭(glob matching)이나 스캐닝(scanning) 없이 O(1) 조회와 작은 조건 검사만으로 이루어집니다.
기본 동작은 거부(Deny)입니다. 명시적으로 허용되지 않은 모든 것은 거부됩니다.
작동 방식 확인하기: perso-demo
README에서 규칙을 읽는 것만으로는 한계가 있기에, 저는 perso-demo를 구축했습니다. 이는 LLM(Groq, llama-3.1-8b-instant)이 모의 B2B CRM을 대상으로 도구(tools)를 호출하는 작은 채팅 앱이며, perso는 모든 도구 호출 의도(tool call intent)가 실행되기 전에 이를 가로챕니다.
사용자는 에이전트(agent), 매니저(manager), 또는 관리자(admin) 중 역할을 선택하고 자연스럽게 대화할 수 있습니다:
"ORD-8821 주문에 대해 200달러 환불을 처리해줘" → 허용됨, 에이전트의 500달러 한도 내에 있음
"800달러 환불을 처리하려고 시도해봐" → 거부됨, NumericCheck 실패
소유권이 없는 고객 C-9001의 기록을 삭제하려는 매니저 → 거부됨, FieldEquals 실패 (user_id != owner_id)
MFA(다요소 인증)가 없는 관리자로서 일괄 업데이트(bulk update) 실행 → 거부됨, All 조건에 env: production과 mfa_verified가 모두 필요함
모든 결정은 채팅창 내에 인라인으로 표시됩니다. 허용은 초록색, 거부는 빨간색으로 나타나며 정책 엔진(policy engine)에서 제공하는 정확한 이유가 함께 표시됩니다. 또한 원시 규칙(raw rules)을 보여주는 정책 사이드바와 실시간 JSON 패널도 있어, 도구 구현부 내에 단 한 줄의 인증(auth) 코드 없이도 복잡한 RBAC(역할 기반 액세스 제어) + 속성 기반 정책(attribute-based policy)이 실시간으로 적용되는 것을 확인할 수 있습니다.
이를 연결하는 SDK
데모의 백엔드는 로우 WASM ABI와 직접 통신하지 않습니다. 대신 perso의 공식 Node.js SDK인 @teknokeras/perso-sdk를 통해 통신합니다.
로우 WASM 익스포트(exports)는 길이를 접두사로 붙인 JSON을 WASM 메모리 경계 너머로 이동시키는 네 가지 C 스타일 함수(alloc, dealloc, init, evaluate)로 구성되어 있습니다. SDK는 이를 깔끔한 비동기(async) API로 래핑합니다:
import { Perso } from '@teknokeras/perso-sdk'
const perso = await Perso.load('path/to/perso.wasm', {
...
또한 플러그 가능한 전송 방식(consoleTransport, httpTransport, fileTransport 또는 사용자 정의 방식)을 통해 구조화된 감사 로깅 (structured audit logging) 기능을 상위에 추가합니다. 따라서 모든 결정 사항을 나중에 검토할 수 있도록 선택적으로 영구적인 저장소로 전송할 수 있습니다. 이는 에이전트의 자체 설명만을 신뢰하는 것이 아니라, 에이전트가 왜 특정 행동을 했는지 또는 하지 않았는지를 증명해야 할 때 유용합니다.
이것이 바로 SDK perso-demo의 백엔드에서 사용하는 방식입니다. 시작 시 한 번 로드된 하나의 공유된 Perso 인스턴스가 모의 CRM (mock CRM)에 도달하기 전 모든 도구 호출 (tool call) 앞에 위치합니다.
이것이 중요한 이유
이러한 정책 계층 (policy layer) 자체가 에이전트를 안전하게 만드는 것은 아닙니다. LLM이 조작되어 해로운 행동을 하고 싶게 만드는 것을 막아주지는 못합니다. 하지만 이 계층이 하는 역할은 일단 그런 일이 발생했을 때 피해 범위 (blast radius)를 제한하는 것입니다. 탈취된 에이전트라 할지라도, 프롬프트가 무엇을 시도하도록 설득했든 상관없이 env == production 및 mfa_verified 조건 없이는 bulk_update를 호출할 수 없습니다. '기본 거부 (Default-deny)' 방식은 정책이 명시적으로 다루지 않는 모든 사항을 실패(fail closed)로 처리함을 의미합니다. 이는 입력 검증 (input validation), 모델 수준의 가드레일 (model-level guardrails), 모니터링 (monitoring) 등 프로덕션 에이전트 시스템 (production agentic system)에서 갖추어야 할 여러 제어 수단 중 하나이며, 범용 권한 부여 도구 (OPA와 같은)를 원칙적으로 적용할 수 있는 상황임에도 불구하고 여전히 대부분의 MCP 통합 환경에는 아직 마련되어 있지 않은 제어 수단입니다.
직접 시도해보기
엔진 (Engine): github.com/teknokeras/perso — Rust 워크스페이스, cargo build, wasm32-unknown-unknown으로 컴파일됨
데모 (Demo): github.com/teknokeras/perso-demo — pnpm install && pnpm dev, 무료 Groq API 키 필요
Node SDK: github.com/teknokeras/perso-sdk-node — npm install [@teknokeras](https://dev.to/teknokeras)/perso-sdk
정책 모델, WASM ABI, 또는 Node가 아닌 호스트에 perso를 임베딩하는 방법(Rust, Python, Go 등 WASM 런타임이 있는 환경에서는 동일하게 작동합니다)에 대한 질문은 언제든 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기