샌드박스 환경에서 코드를 실행하는 다양한 방법을 수년간 실험해 왔는데, 최근 시도가 드디어 원하던 모든 조건을 갖춘 것 같다. micropython-wasm이라는 이름으로 알파 패키지를 공개했으며, Datasette Agent용 코드 실행 샌드박스 플러그인인 datasette-agent-micropython에도 이를 활용하고 있다.
내 주요 오픈소스 프로젝트인 Datasette, LLM, sqlite-utils는 모두 플러그인을 지원한다.
플러그인은 소프트웨어를 확장하는 메커니즘으로서 정말 마음에 든다. 잘 설계된 플러그인 시스템은 새로운 시도에 따르는 위험을 거의 없애준다. 아무리 파격적인 아이디어라도 핵심 애플리케이션 자체에 흔적을 남기지 않는다. 풀 리퀘스트 검토 없이도 하룻밤 사이에 새 기능이 생겨날 수 있다!
단, 큰 단점이 하나 있다. 내 플러그인 시스템은 모두 Python과 Pluggy를 사용하며, 플러그인 코드는 애플리케이션 내에서 전체 권한으로 실행된다. 버그가 있거나 악의적인 플러그인은 모든 것을 망가뜨리거나 개인 데이터를 유출할 수 있다.
승인되지 않은 파일 읽기, 네트워크 연결, 혹은 애플리케이션이나 사용자 컴퓨터에 위험을 초래할 수 있는 모든 동작을 차단하는 환경에서 플러그인 방식의 코드를 실행할 수 있다면 정말 좋겠다.
관심 범위는 플러그인에만 국한되지 않는다. 특히 Datasette에는 임의 코드 실행이 유용하게 쓰일 수 있는 기능이 많다. 이미 Datasette Enrichments에서 테이블에 저장된 값을 변환하는 코드 실행을 실험해봤다. 나아가, 정해진 위치에서 JSON을 가져와 딕셔너리 리스트로 재가공한 뒤 SQLite 데이터베이스 테이블에 행으로 삽입하는 코드를 스케줄에 따라 실행하는 메커니즘도 만들고 싶다.
목표는 내 Python 애플리케이션 안에서 코드를 안전하게 실행하는 것이다. 필요한 조건은 다음과 같다.
while True: s += "longer string"으로 애플리케이션이나 사용자 컴퓨터가 다운되는 일은 없어야 한다.웹 브라우저는 악성 코드 관점에서 상상할 수 있는 가장 적대적인 환경에서 동작한다. 거의 모든 페이지를 불러올 때마다 웹에서 신뢰할 수 없는 코드를 내려받아 실행하는 것이 브라우저의 역할이기 때문이다.
이런 점에서 JavaScript 엔진은 샌드박스로 훌륭한 후보처럼 보인다. 하지만 이 엔진들은 구조가 매우 복잡하고, 다른 프로젝트에 쉽게 내장하도록 설계되지 않았다. 내가 살펴본 Python용 v8 프로젝트 대부분은 유지보수가 뜸하고, 완전히 신뢰할 수 없는 코드에는 사용하지 말라는 경고가 붙어 있었다.
WebAssembly는 훨씬 나은 후보다. 처음부터 내가 원하는 모든 특성을 지원하도록 설계되었고, 브라우저에서 거의 10년 가까이 검증되어 왔다. Python 라이브러리 wasmtime은 활발히 유지보수되고 있으며 바이너리 휠도 제공된다.
wasmtime 같은 WebAssembly 엔진은 WebAssembly 바이너리를 실행한다. Rust처럼 WebAssembly로 직접 컴파일하기 쉬운 언어도 있지만, JavaScript나 Python 같은 동적 언어는 그렇지 않다. eval() 같은 언어 기본 요소를 지원하기 때문에, 런타임에 완전한 인터프리터가 필요하기 때문이다.
Python을 실행하려면 WebAssembly로 컴파일된 완전한 Python 인터프리터가 필요하다. 코드를 주입하고, 호스트 함수를 연결하고, 결과를 가져오기 쉽게 구성해야 한다.
Pyodide는 브라우저에서 WebAssembly로 Python을 실행하는 뛰어난 패키지를 제공하지만, 서버 사이드 Python에서 Pyodide를 사용하는 것은 지원되지 않는다. 내가 찾은 가장 최신 안내는 2024년 10월 자료로, "Pyodide는 Emscripten 툴체인으로 빌드되며 브라우저나 Node.js에서만 실행 가능하다"고 명시되어 있었다.
그러던 중 MicroPython을 대안으로 살펴보기로 했다. MicroPython 공식 사이트는 이렇게 설명한다.
MicroPython은 Python 3 프로그래밍 언어를 간결하고 효율적으로 구현한 것으로, Python 표준 라이브러리의 일부를 포함하며 마이크로컨트롤러와 제한된 환경에서 실행되도록 최적화되어 있습니다.
WebAssembly야말로 제한된 환경이 아닌가!
GPT-4.5 Pro에 관련 자료 조사를 맡겼더니, Yamamoto Takahashi가 올린 MicroPython 대상 PR이 나왔다. 제목은 "ports/unix를 위한 실험적 WASI 지원"이었다.
그 결과로 이 research.md 문서가 만들어졌고, Codex Desktop과 GPT-4.5 high에 맡겨 어떻게 되는지 지켜봤다.
read the research.md document and build this. You will probably need to write a script that compiles a custom WASM version of MicroPython as part of this project - fetch the MicroPython code to a /tmp directory for this as part of that script.
결과는 성공이었다. WebAssembly 샌드박스 안에서 Python 코드를 실행할 수 있는 프로토타입 Python 라이브러리가 탄생한 것이다!
가장 까다로운 문제는 인터프리터 상태 유지였다. 여기서 사용하는 WASM 빌드는 인터프리터를 시작하고, 코드를 실행하고, 마지막에 인터프리터를 종료하는 단일 진입점만 노출한다.
일회성 스크립트에는 문제없지만, Datasette Agent에서는 여러 번의 코드 실행 호출에 걸쳐 변수와 함수를 메모리에 유지해서 재사용해야 한다.
코딩 에이전트와 작업할 때의 장점은 아이디어에서 개념 증명(proof of concept)까지 빠르게 도달할 수 있다는 것이다. 다음과 같이 프롬프트를 입력했다.
For keeping variables resident: what if we ran code inside micropython itself which called a host function get_next_python_code() and then passed that to eval() - and that host function blocked until new code was available, maybe by running in a thread with a queue? Could that or a similar idea help here?
몇 차례 반복 작업 끝에 작동하는 버전을 얻었다! 이제 Python 코드에서 다음과 같이 사용할 수 있다.
from micropython_wasm import MicroPythonSession with MicroPythonSession() as session: print(session.run("x = 10\nprint(x)").stdout) print(session.run("x += 5\nprint(x)").stdout) print(session.run("print(x * 2)").stdout)
내부적으로는 스레드를 시작하고 요청 큐를 설정한 뒤, session.run() 명령에 해당하는 메시지를 해당 큐로 전송한다. 그리고 매번 실행 결과를 응답 큐에서 기다린다. WASM 내부에서는 MicroPython 인터프리터가 __session_next__() 호스트 함수가 다음 코드 라인을 반환할 때까지 대기하고, 반환된 코드를 eval()로 실행한 후 각 블록이 성공적으로 실행되면 __session_result__({"id": request_id, "ok": True})를 호출한다.
또 다른 복잡한 부분은 호스트 함수 지원이었다. 내 Python 라이브러리가 함수를 선택적으로 노출해 MicroPython에서 실행 중인 코드가 이를 호출할 수 있어야 했다.
Codex는 결국 78줄의 C 코드로 이 문제를 해결했고, 그 코드는 패키지와 함께 배포하는 362KB WebAssembly 바이너리에 컴파일되어 포함됐다.
C 프로그래머와는 거리가 멀지만, 해당 C 코드를 직접 읽고 두 가지 모델에 설명을 요청했으며(Claude의 설명 참고), 다양한 테스트도 진행했다.
WebAssembly로 작업할 때의 장점은, 설령 C 코드에 치명적인 결함이 있더라도 최악의 경우 WebAssembly 실행이 예외와 함께 실패하는 데 그친다는 것이다. 그 정도 위험은 감수할 수 있다.
메모리 제한은 wasmtime에서 직접 지원한다. CPU 제한은 조금 더 까다롭다. wasmtime은 WebAssembly 호출이 실행할 수 있는 연산 횟수를 제한하는 "연료(fuel)" 개념을 제공하는데, 이 문제에 딱 맞는 방식이지만 단위를 직관적으로 이해하기가 어렵다. 현재는 기본값을 2,000만으로 설정해 실험 중이지만, 이것이 가장 적절한 값인지는 아직 확신하지 못한다.
micropython-wasm 알파 버전이 PyPI에 공개되었다.
README에 설명된 대로 직접 Python 코드에서 사용해볼 수 있다. 버전 0.1a2에서는 간단한 CLI 모드도 추가했으며, 별도 설치 없이 uvx로 바로 시도해볼 수 있다.
uvx micropython-wasm -c 'print("Hello world")'
# To see it run out of fuel:
uvx micropython-wasm -c 's = ""; while True: s += "longer"'
# Outputs: micropython-wasm: guest exited with code 1Datasette Agent에서도 아래와 같이 사용해볼 수 있다.
uvx llm keys set openai
# Paste in an OpenAI key, then:
uvx --with datasette-agent \
--with datasette-agent-micropython \
--prerelease allow \
datasette --internal internal.db \
-s plugins.datasette-llm.default_model gpt-5.5 \
--root -o이후 http://127.0.0.1:8001/-/agent로 이동해 다음 프롬프트를 실행한다.
show me some micropython
![Screenshot of a chat application interface with a dark blue-grey header reading "home" on the left and "root" with a hamburger menu icon on the right. Below is a navigation row with "← Back" and "Chat" on the left and an "EXPORT" button on the right. A blue user message bubble reads "show me some micropython". Below it a collapsed thinking section reads "▸Thinking: … to show the result clearly. After that, I can wrap up with a brief explanation!" followed by a "▶ Tool: execute_micropython" label. A code block follows: "# A tiny MicroPython example: blink-style logic + Fibonacci" / "def fib(n):" / " a, b = 0, 1" / " out = []" / " for _ in range(n):" / " out.append(a)" / " a, b = b, a + b" / " return out" / 'print("Hello from MicroPython!")' / 'print("First 10 Fibonacci numbers:", fib(10))' / "# MicroPython often runs on microcontrollers, e.g.:" / "# from machine import Pin" / "# led = Pin(2, Pin.OUT)" / "# led.value(1) # turn LED on" / "# led.value(0) # turn LED off". Below a horizontal divider is the output: "Hello from MicroPython!" / "First 10 Fibonacci numbers: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]", followed by a "▶ Result: execute_micropython" label. At the bottom is a text input field with placeholder "Type a message..." and a blue "Send" button.](https://static.simonwillison.net/static/2026/micropython-in-datasette-agent.jpg)
유지보수가 부실한 샌드박스 라이브러리를 비판해 놓고 직접 만들어버렸으니, 참으로 아이러니한 상황이다!
의도적으로 알파 릴리스 버전을 붙였으며, 상당한 위험을 감수할 의향이 없는 분께는 아직 권장하기 어렵다.
충분한 테스트를 거쳐 직접 사용하는 데는 문제없다고 판단했다. 이를 활용한 첫 번째 플러그인 datasette-agent-micropython도 배포했다. 또한 해당 Datasette Agent 플러그인 안에서 GPT-4.5 xhigh를 붙잡아 두고 샌드박스를 탈출해 보도록 유도했지만, 지금까지는 성공하지 못했다.
이 구현이 전문 보안팀을 갖추고 높은 위험 부담을 안고 있는 기업들로 하여금 WebAssembly 기반 Python 샌드박싱 방식을 채택하고, 자체 솔루션을 오픈소스로 공개하는 계기가 되길 바란다.
이 글은 블로그의 장문 아티클만 포함된 피드에서 제공됩니다. 전체 포스트를 받아보려면 /atom/everything/을 구독하거나, 다른 구독 옵션을 확인하세요.