
Deep Agents가 샌드박스 없이 신뢰할 수 없는 코드를 실행하는 방법
요약
Deep Agents의 동적 서브에이전트 도입에 따른 보안 문제를 해결하기 위한 설계 방식을 다룹니다. 프롬프트 인젝션 위험을 고려하여 실행 격리, 권한 격리, 지속 가능한 일시 중지를 핵심 요구사항으로 정의하고 WebAssembly를 활용한 해결책을 제시합니다.
핵심 포인트
- 에이전트가 작성한 코드는 신뢰할 수 없다는 가정하에 설계해야 함
- 실행 격리를 위해 WebAssembly(WASM)를 사용하여 메모리 경계를 보호함
- 권한 격리를 통해 에이전트의 데이터 및 작업 접근 범위를 제한함
- LangSmith Sandboxes와 코드 인터프리터라는 두 가지 접근 방식 제공

우리는 최근 Deep Agents에 동적 서브에이전트(dynamic subagents)를 도입했습니다. 서브에이전트를 한 번에 하나의 도구 호출(tool call)로 전달하는 대신, 에이전트가 이들을 조율하는 짧은 스크립트를 작성하도록 합니다. 그 스크립트는 에이전트가 코드를 작성하고 실행하는 코드 인터프리터(code interpreter)에서 실행됩니다.
이는 강력한 패턴이지만, 겉보기와 달리 매우 어려운 문제에 기반하고 있습니다:
신뢰할 수 없는 코드를 안전하고 안정적으로 실행하는 것은 어렵습니다.
신뢰할 수 없는 코드를 실행하는 것은 잘 연구된 문제입다. 하지만 신뢰할 수 없는 입력에 영향을 받는 에이전트가 작성한 코드를 실행하는 것은 그렇지 않습니다. 프롬프트 인젝션(prompt injection) 문제가 여전히 해결되지 않았기 때문에, 우리는 에이전트가 작성한 코드가 결국 허용되지 않는 행동을 할 것이라고 가정해야 합니다. 에이전트가 올바르게 행동할 것이라고 믿는 대신, 우리는 에이전트가 할 수 있는 일을 제한합니다. 이러한 방식으로 신뢰할 수 있는 에이전트를 만들기 위해서는 세 가지 설계 요구사항으로 요약됩니다:
- 실행 격리 (Execution isolation): 에이전트가 작성한 코드가 실행되는 호스트를 해칠 수 없어야 합니다.
- 권한 격리 (Capability isolation): 에이전트는 우리가 의도적으로 전달한 데이터와 작업에만 접근할 수 있어야 합니다.
- 지속 가능한 일시 중지 (Durable pauses): 실행이 인간의 입력을 위해 중단될 수 있어야 하며, 나중에 진행 상황을 잃지 않고 다시 재개될 수 있어야 합니다.
Interrupt 2026에서 우리는 이러한 요구사항을 바탕으로 구축된 두 가지 제품을 발표했습니다.
- LangSmith Sandboxes는 에이전트에게 완전한 원격 컨테이너를 제공합니다. 로컬 코딩 에이전트와 거의 동일한 자유를 갖지만, 다른 머신에서 격리되어 실행됩니다.
- Deep Agents를 위한 코드 인터프리터(Code Interpreters)는 반대의 접근 방식을 취합니다. 에이전트가 프로그램을 작성하고 실행할 수 있는 더 작은 런타임(runtime)을 제공하되, 우리가 제공하는 하네스(harness) 내부에서만 실행되도록 합니다.
우리는 이미 첫 번째 종류의 샌드박스에 대해 작성한 바 있습니다. 이 포스트는 두 번째 방식에 관한 것입니다. 즉, 왜 오케스트레이션 워크플로우(orchestration workflows)가 반드시 샌드박스화된 컴퓨터를 필요로 하지 않는지, 그리고 샌드박스를 매력적으로 만드는 격리 기능을 포기하지 않으면서 어떻게 더 작은 공격 표면(surface)을 유지하는지에 대해 다룹니다.
실행 격리 (Execution isolation)
신뢰할 수 없는 코드를 실행하는 모든 사람은 동일한 결론에 도달합니다. 즉, 코드와 그 외의 모든 것 사이에 엄격한 경계가 필요하다는 것입니다. 인터프리터 또한 프로세스를 벗어나지 않으면서 해당 경계가 필요하며, 이것이 바로 우리가 WebAssembly를 사용하는 이유입니다.
WebAssembly
WebAssembly (WASM)는 자체 메모리를 가진 샌드박스화된 프로세스 내 VM (Virtual Machine) 내부에서 실행되는 컴팩트한 바이너리 형식이며, 호스트가 제공하는 기능 (capabilities)을 통해서만 외부 세계와 상호작용할 수 있습니다. 분리된 선형 메모리 (linear memory)가 바로 경계의 핵심입니다. WASM 내부에서 실행되는 코드는 호스트 프로세스로의 포인터 역참조 (dereference)를 할 수 없으므로, 전달받지 않은 메모리를 읽거나 손상시킬 수 없습니다. WASM 런타임 (runtimes)은 엄격한 메모리 및 실행 제한을 쉽게 강제할 수 있게 해주며, 하네스 (harness)와 함께 실행되기 때문에 별도의 머신을 구축하지 않고도 인스트루멘테이션 (instrumentation)을 할 수 있습니다.
AWS, Shopify, Figma 모두 플랫폼에서 신뢰할 수 없는 코드를 실행하기 위해 WASM을 활용하고 있으며, 이는 WebContainers 및 wasmtime과 같은 도구들의 기반이 되는 것과 동일한 격리 모델입니다.
QuickJS
WASM은 우리에게 샌드박스를 제공하지만, 그 안에서 코드를 실행할 무언가는 여전히 필요합니다. 그것이 바로 QuickJS의 용도입니다. QuickJS는 순수 C 언어로 작성된 작고 빠르며 ECMA 표준을 준수하는 JavaScript 엔진입니다. 크기가 작기 때문에 경계 내부의 신뢰할 수 있는 표면 (trusted surface)을 작게 유지할 수 있으며, WASM으로 깔끔하게 컴파일되므로 엔진 자체가 경계 옆이 아닌 경계 뒤에 위치하게 됩니다. JavaScript는 또한 이 작업에 적합합니다. 컴파일 단계 없이 오케스트레이션 로직 (orchestration logic)을 작성할 수 있을 만큼 표현력이 풍부하며, 이는 에이전트가 생성하는 짧은 프로그램의 형태와 정확히 일치합니다.
기능 격리 (Capability isolation)
실행 경계는 에이전트가 호스트를 침해하는 것을 막아주지만, 에이전트가 무엇을 할 수 있는지에 대해서는 아무것도 말해주지 않습니다. 에이전트는 우리가 부여한 기능 (capabilities)만큼만 유용하고, 그만큼만 위험합니다.
결혼식을 계획하는 에이전트를 상상해 보세요. 유용하게 작동하려면 곳곳에서 민감한 데이터(계약서, RSVP, 가족 단체 채팅방)를 읽고 외부에서 그에 따른 조치(업체에 이메일 발송, 보증금 승인)를 취해야 합니다. 각각의 기능은 그 자체로는 합리적입니다. 하지만 이들을 하나의 자율 루프 (autonomous loop) 안에 결합하면, 단 하나의 악의적인 RSVP가 개인 예산을 읽고 업체에 "승인된" 변경 사항을 이메일로 보낼 수 있습니다.
Meta의 '2가지 규칙(rule of two)'은 이러한 제약 사항을 포착합니다. 프롬프트 인젝션 (Prompt Injection) 문제가 해결될 때까지, 에이전트는 다음 중 최대 두 가지만 수행할 수 있어야 합니다:
- 민감한 데이터에 접근
- 신뢰할 수 없는 콘텐츠에 노출
- 상태를 변경하거나 외부와 통신
이 지점이 인터프리터 (Interpreter)와 전통적인 샌드박스 (Sandbox)가 가장 크게 갈라지는 부분입니다. 샌드박스는 컴퓨터 형태(파일 시스템, 의존성, 셸)로 시작하므로, 보안 작업이 '감산적 (subtractive)'입니다. 즉, 광범위한 권한을 가진 상태에서 시작하여 이를 다시 회수하는 방식입니다. 반면 코드 인터프리터는 아무것도 없는 상태에서 시작합니다. 기본 상태에서는 파일을 읽거나, 네트워크 요청을 보내거나, 의존성을 설치할 수 없습니다. 에이전트가 가진 전부라고는 변수, 함수, 객체, 루프, 조건문 등과 같은 언어뿐입니다. 이보다 더 강력한 모든 기능은 하네스 (Harness)를 통해 의도적으로 브릿지 (Bridge)를 통해 연결됩니다.
.png)
브릿지로 연결된 기능의 가장 명확한 예는 코드 내에서 서브 에이전트 (Subagent)를 호출하는 것입니다. 프로세스 관리자나 네트워크 스택 대신, 에이전트는 좁은 계약 (Contract)을 가진 함수를 부여받고 하네스가 실행을 처리합니다. 우리가 그 브릿지를 소유하고 있기 때문에, 그 한계 또한 우리가 설정합니다. 즉, 한 번에 얼마나 많은 서브 에이전트를 실행할 수 있는지, 그리고 단일 호출이 얼마나 많은 서브 에이전트를 생성할 수 있는지를 결정합니다.
지속 가능한 일시 중지 (Durable pauses)
실행 격리 (Execution isolation)와 권한 제한은 실행 중인 프로그램을 안전하게 유지하지만, 마지막 요구 사항은 프로그램을 '살아있게' 유지하는 것입니다. 프로덕션 환경에 적합한 에이전트는 위험한 작업을 수행하기 전에 중단하고 인간의 승인을 기다려야 하며, 그 승인은 몇 초, 몇 시간, 또는 며칠 후에 돌아올 수 있습니다. 이는 종종 에이전트가 프로세스에서 퇴거된 이후에 발생합니다. 그렇다면 어떻게 반쯤 완료된 프로그램을 그토록 오랫동안 일시 중지하고, 정확히 중단된 지점부터 다시 시작할 수 있을까요?
QuickJS가 WASM 내부에서 실행되기 때문에, 프로그램을 재구축하는 대신 프로그램 자체를 일시 중지할 수 있습니다. 우리는 인터프리터의 선형 메모리 (Linear memory)를 LangGraph 상태로 직렬화 (Serialize)하며, 재개 시 하네스는 스냅샷을 복원하고 그 결과를 기다리고 있던 호출로 다시 전달합니다. 프로그램 입장에서는 단지 반환되는 데 시간이 오래 걸린 비동기 호출 (Async call)로 보일 뿐입니다.
시도해보기
이 기술의 기반이 되는 두 패키지는 현재 모두 실험적인 단계로 공개되어 있습니다:
quickjs-rs
— WASM을 통해 QuickJS를 실행하기 위한 런타임(runtime) 및 Python 바인딩(bindings).
langchain-quickjs
— quickjs-rs를 기반으로 구축된 Deep Agents 미들웨어(middleware).
우리는 몇몇 긴밀한 파트너들과 함께 이 기술을 프로덕션(production) 환경에 도입하기 위해 협력하고 있으며, 해당 배포 사례들로부터 얻은 피드백을 바탕으로 런타임을 더욱 견고하게 다듬고 있습니다. 이 인터프리터(interpreter)가 실제로 어떤 가능성을 열어주는지 확인하고 싶다면, Dynamic Subagents에 관한 저희 포스트를 읽어보시거나 직접 체험해 보세요!
uv add deepagents langchain-quickjs
from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware
agent = create_deep_agent(
...
AI 자동 생성 콘텐츠
본 콘텐츠는 LangChain Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기