BoxAgnts 툴 시스템 (1) — 설계 동기 및 아키텍처 개요
출처: Dev.to
AI 에이전트 프레임워크 생태계는 어느 정도 포화 상태에 이르렀다고 말해도 과언이 아니다. 파이썬 생태계에는 LangChain, CrewAI, AutoGen이 있고, TypeScript에는 Vercel AI SDK가 있다. Go에는 langchaingo가 있다. 하지만 이 프레임워크들은 모두 같은 딜레마를 안고 있다. 도구를 신뢰할 수 없는 환경에서 실행되는 신뢰된 코드로 정의하고, 이후에 샌드박스로 틈을 메우는 방식이다.
BoxAgnts는 정반대의 접근 방식을 취한다. 더 어려운 길을 선택한다: 실행 시점부터 격리 경계를 구축하고, 도구 실행이 시작되기 전에 WebAssembly 샌드박스를 이용해 보안 제약을 강제한다. 즉, 사후에 시스템 콜을 가로채는 것이 아니라 실행 전부터 격리한다. 전체 시스템은 Rust로 처음부터 구현되었으며—12개의 크레이트, 외부 의존성이 전혀 없는 에이전트 런타임, 그리고 12개의 AI 모델 제공자와 직접 인터페이스하는 스트리밍 쿼리 엔진을 포함한다.
LangChain 스타일의 도구 시스템은 깊이 뿌리내린 전제를 가지고 있다: 도구 코드와 에이전트 런타임이 같은 프로세스에서 실행된다는 것이다. Python의 exec(), subprocess.run(), Node의 child_process.spawn() 등은 모두 도구 실행 시점에 권한 검사를 수행하고, 이를 “사후” 방식(위험한 시스템 콜을 가로채고, 특정 파일 경로를 차단 등)으로 처리한다.
이 모델에는 구조적인 문제가 있다. 새로운 “위험한 작업” 카테고리가 등장할 때마다 보안 인터셉터에 또 다른 규칙을 추가해야 한다—끝이 없는 무기 경쟁이다. rm -rf /가 차단되면 공격자는 dd if=/dev/zero of=/dev/sda를 시도한다. 이것도 차단되면 포크 폭탄을 시도한다. 알려진 위험 패턴을 모두 차단할 수는 있지만, 공격 표면은 여전히 운영 체제 전체 시스템 콜 테이블에 남아 있다.
보다 미묘한 문제는 도구 조합이다. file-read는 격리된 상태에서 읽기 전용이며, web-fetch도 읽기 전용이다. 하지만 AI가 먼저 file-read를 사용해 ~/.ssh/id_rsa를 탈취하고, 이어서 web-fetch를 이용해 외부 서버에 POST한다면, 단일 검증 포인트에서는 이 교차 도구 데이터 유출을 잡아내지 못한다.
BoxAgnts가 이 문제에 대응하는 방식: 도구가 호스트 운영 체제를 보지 못하게 한다.
이것이 WASM 샌드박스와 전통적인 샌드박스의 근본적인 차이점이다. 전통적인 샌드박스(seccomp, AppArmor, Docker seccomp 프로파일)는 OS 수준에서 권한을 “축소”한다. 반면 WASM 샌드박스는 권한 자체가 존재하지 않는 런타임 수준에서 동작한다. WASM 프로그램은 기본적으로 open()이 무엇인지조차 모른다—호스트가 WASI 인터페이스를 통해 명시적으로 주입한 디렉터리 핸들만 접근할 수 있다.
나는 이 설계를 “Capability‑based”와 “Permission‑reduction”이라고 부른다. 후자의 보안 경계는 “내가 금지하는 것을 제외한 모든 것”이다. 전자의 보안 경계는 “내가 명시적으로 허용한 것만—그 외는 존재하지 않는다”이다.
이 덕분에 비교적 낮은 엔지니어링 비용으로 강력한 보안 보장을 달성할 수 있다. 이제 아키텍처를 살펴보자.
BoxAgnts의 Rust 측 구성
BoxAgnts는 12개의 크레이트를 다섯 개 레이어로 구성한다(아래에서 하위 레이어부터 상위 레이어 순으로 설명).
레이어 1: Runtime (wasm-sandbox + wasm-tools)
wasm-sandbox는 Wasmtime의 컴포넌트 런타임을 감싸고, 위쪽에 통합된execute()인터페이스를 제공한다.wasm-tools는wasm32-wasip2타깃으로 컴파일된 WASM 도구들을 WASI 컴포넌트로 로드한다.- 각 호출 시 호스트는 작업 디렉터리를 Guest의 루트 파일 시스템으로 매핑하고, 허용된 외부 도메인 목록을 네트워크 레이어에 주입하며, 실행 시간 제한과 메모리 상한을 설정한 뒤 컴포넌트를 실행한다.
wasm-tools는 도구 메타데이터 추출을 담당한다. “설정 파일” 방식을 쓰지 않고, 대신 WASM 도구를 직접 실행해 --help 출력을 캡처하고 정규식으로 파라미터 목록, 타입, 설명을 파싱한다. 이 자체 기술 파싱 메커니즘이 BoxAgnts의 “무설정(zero‑configuration)” 등록의 기반이며, 도구 등록 문서에 자세히 설명돼 있다.
레이어 2: Core Abstractions (tools + tools‑manager + core)
core는 시스템 전반에서 사용되는 공유 데이터 타입을 정의한다:Message,ContentBlock,ToolDefinition,UsageInfo등. 몇 백 줄에 불과하지만, 그 정확성은 상위 모듈 전체에 직접적인 영향을 미친다.tools는Tool트레이트를 정의한다—도구 시스템 전체의 “보편 언어”다. Rust 내장 도구든 WASM 확장이든 모든 도구는 다음 메서드를 구현해야 한다.
pub trait Tool: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn source(&self) -> ToolSource;
fn permission_level(&self) -> PermissionLevel;
fn input_schema(&self) -> Value;
async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult;
}
tools‑manager는 도구 등록 및 탐색의 중심이다. 전역 도구 테이블을 유지하고, 확장 디렉터리의 파일 변화를 감시하며, 핫 리로드를 지원한다.
레이어 3: API Gateway (api + gateway)
api는 주요 AI 모델들과의 통합 로직을 캡슐화한다. 단일LlmProvider트레이트가 Anthropic Messages API, OpenAI Chat Completions, Google Gemini, Cohere, MiniMax 등 12개의 인터페이스를 통합한다. 양방향 메시지 포맷 변환을 위해 Transformer 패턴을 사용한다.gateway는 비즈니스 오케스트레이션을 제공한다—세션 관리, 도구 리스트 구성, 모델 선택, 권한 필터링, Cron 작업 스케줄링, 정적 사이트 배포 등을 담당한다.
레이어 4: Agent Loop (query)
query는 에이전트의 “심장박동”이다. run_query_loop() 함수가 전체 도구 호출 사이클을 구현한다:
- 메시지 히스토리와 시스템 프롬프트를 AI 모델에 전송
- SSE 스트림 이벤트를 파싱하고
ToolUse요청을 감지 - 도구 테이블에서 해당 도구 인스턴스를 찾아 권한 검사 수행
tool.execute()호출 후 결과를 캡처- 결과를 메시지 히스토리에 삽입하고 1단계로 돌아감
모델이 end_turn을 반환하거나, 컨텍스트가 소진되거나, 사용자가 취소하거나, 비용 한도를 초과할 때까지 반복한다.
레이어 5: Web Service (server)
최상위 레이어는 Axum 기반 HTTP/WebSocket 서버로, REST API와 실시간 WebSocket 채팅 채널을 제공한다. 프론트엔드는 Vue 3 + Vuetify 3 대시보드이며, 상태 관리는 Pinia, 코드 편집기는 CodeMirror 6을 사용한다.
아키텍처의 핵심 특징
- 명확한 책임 경계: 각 레이어는 암묵적인 교차 의존성이 없으며, 책임이 명확히 구분된다.
query는 도구가 Rust 내장인지 WASM 샌드박스인지 알지 못한다.gateway는 AI 모델이 Anthropic인지 OpenAI인지 구분하지 않는다.tools‑manager는 도구가 어떤 대화 흐름에서 호출될지 전혀 모른다.
Docker와의 비교
많은 사람이 “왜 Docker를 쓰지 않느냐”고 묻는다. 답은 시작 비용과 격리 세분화에 있다.
- 시작 비용: WASM 컴포넌트는 마이크로초 단위로 시작한다—사전 컴파일된 바이너리를 메모리에 로드하고 Wasmtime이 실행한다. Docker 컨테이너는 초 단위가 걸리며, 워밍 스타트조차도 수백 밀리초가 필요하다. 에이전트 대화에서 도구 호출은 턴당 여러 번, 빠른 응답이 요구되므로 Docker의 오버헤드는 감당할 수 없다.
- 격리 세분화: WASM은 컨테이너보다 더 미세한 제어가 가능하다. 컨테이너의 보안 경계는 Linux 네임스페이스와 cgroup(파일시스템, 네트워크, 프로세스)이다. WASM은 함수 호출 단위·메모리 접근 단위까지 경계를 설정할 수 있다.
wasm_fuel같은 “명령어 소모” 수준의 자원 제어는 컨테이너에서는 구현할 수 없다.
왜 Rust인가
Rust 생태계는 파이썬만큼 풍부하지는 않지만, BoxAgnts가 **고성능 WASM