使用 MoonBit 和 Wassette 构建安全的 WebAssembly 工具

欢迎来到 MoonBit 和 Wassette 的世界!本教程将带您一步步构建一个基于 WebAssembly 组件模型的安全工具。通过一个实用的天气查询应用示例,您将学习如何利用 MoonBit 的高效性和 wassette 的安全特性,创建功能强大的 AI 工具。
wassette 和 MCP 简介
MCP(Model Completion Protocol)是 AI 模型与外部工具交互的协议。当 AI 需要执行特定任务(如网络访问或数据查询)时,会通过 MCP 调用相应工具。这种机制扩展了 AI 的能力,但也带来安全挑战。
wassette 是微软开发的一个基于 WebAssembly 组件模型的运行时,为 AI 系统提供安全执行外部工具的环境。它通过沙箱隔离和精确的权限控制,解决了 AI 工具可能带来的安全风险。
wassette 让工具运行在隔离环境中,权限受策略文件严格限制,接口通过 WIT(WebAssembly Interface Type)清晰定义。同时,也利用 WIT 接口来生成工具交互的数据格式。
总体流程
在开始之前,让我们先了解一下整体流程:
让我们开始这段奇妙的旅程吧!
第1步:安装必要工具
首先,我们需要安装三个工具(我们假设已经安装 MoonBit 工具链):
- wasm-tools:WebAssembly 工具集,用于处理和操作 Wasm 文件
- wit-deps:WebAssembly 接口类型依赖管理器
- wit-bindgen:WebAssembly 接口类型绑定生成器,用于生成语言绑定
- wassette:基于 Wasm 组件模型的运行时,用于执行我们的工具
其中,wasm-tools wit-deps wit-bindgen 可通过 cargo 安装(需安装 Rust):
cargo install wasm-tools
cargo install wit-deps
cargo install wit-bindgen-cli
或从 GitHub Release 下载:
- wit-bindgen: https://github.com/bytecodealliance/wit-bindgen/releases/tag/v0.45.0
- wasm-tools: https://github.com/bytecodealliance/wasm-tools/releases/tag/v1.238.0
- wit-deps: https://github.com/bytecodealliance/wit-deps/releases/tag/v0.5.0
wassette 需从 GitHub Release 下载:
第2步:定义接口
接口定义是整个工作流程的核心。我们使用 WebAssembly 接口类型 (WIT) 格式来定义组件的接口。
首先,创建项目目录和必要的子目录:
mkdir -p weather-app/wit
cd weather-app
创建 wit/deps.toml
在 wit 目录下创建 deps.toml 文件,定义项目依赖:
cli = "https://github.com/WebAssembly/wasi-cli/archive/refs/tags/v0.2.7.tar.gz"
http = "https://github.com/WebAssembly/wasi-http/archive/refs/tags/v0.2.7.tar.gz"
这些依赖项指定了我们将使用的 WASI(WebAssembly 系统接口)组件:
cli:提供命令行接口功能。在这个例子中未使用。http:提供 HTTP 客户端和服务器功能。在这个例子中使用客户端功能。
然后,运行 wit-deps update。这个命令会获取依赖,并在 wit/deps/ 目录下展开。
创建 wit/world.wit
接下来,创建 wit/world.wit 文件来定义我们的组件接口。 WIT
是一种声明式接口描述语言,专为 WebAssembly
组件模型设计。它允许我们定义组件之间如何交互,而不需要关心具体的实现细节。
具体详情可以查看 组件模型
手册。
package peter-jerry-ye:weather@0.1.0;
world w {
import wasi:http/outgoing-handler@0.2.7;
export get-weather: func(city: string) -> result<string, string>;
}
这个 WIT 文件定义了:
- 一个名为
peter-jerry-ye:weather的包,版本为 0.1.0 - 一个名为
w的世界(world),它是组件的主要接口 - 导入 WASI HTTP 的对外请求接口
- 导出一个名为
get-weather的函数,它接受一个城市名称字符串,返回一个结果(成功时为天气信息字符串,失败时为错误信息字符串)
第3步:生成代码
现在我们已经定义了接口,下一步是生成相应的代码骨架。我们使用 wit-bindgen
工具来为 MoonBit 生成绑定代码:
# 确保您在项目根目录下
wit-bindgen moonbit --derive-eq --derive-show --derive-error wit
这个命令会读取 wit 目录中的文件,并生成相应的 MoonBit 代码。生成的文件将放在
gen 目录下。
注:当前生成版本存在部分警告,之后会进行 修复。
生成的目录结构应该如下:
.
├── ffi/
├── gen/
│ ├── ffi.mbt
│ ├── moon.pkg.json
│ ├── world
│ │ └── w
│ │ ├── moon.pkg.json
│ │ └── stub.mbt
│ └── world_w_export.mbt
├── interface/
├── moon.mod.json
├── Tutorial.md
├── wit/
└── world/
这些生成的文件包含了:
- 基础的 FFI(外部函数接口)代码(
ffi/) - 生成的导入函数(
world/interface/ - 导出函数的包装器(
gen/) - 待实现的
stub.mbt文件
第4步:修改生成的代码
现在我们需要修改生成的存根文件,实现我们的天气查询功能。主要需要编辑的是
gen/world/w/stub.mbt 文件以及同目录下的
moon.pkg.json。在此之前,先让我们添加一下依赖,方便后续实现:
moon update
moon add moonbitlang/x
{
"import": [
"peter-jerry-ye/weather/interface/wasi/http/types",
"peter-jerry-ye/weather/interface/wasi/http/outgoingHandler",
"peter-jerry-ye/weather/interface/wasi/io/poll",
"peter-jerry-ye/weather/interface/wasi/io/streams",
"peter-jerry-ye/weather/interface/wasi/io/error",
"moonbitlang/x/encoding"
]
}
让我们看一下生成的存根代码:
// Generated by `wit-bindgen` 0.44.0.
///|
pub fn (city : String) -> Result[String, String]
get_weather(String
city : String
String) -> enum Result[A, B] {
Err(B)
Ok(A)
}
Result[String
String, String
String] {
... // 这里是我们需要实现的部分
}
现在,我们需要添加实现代码,使用 HTTP 客户端请求天气信息。编辑
gen/world/w/stub.mbt 文件,编辑如下:
///|
pub fn (city : String) -> Result[String, String]
get_weather(String
city : String
String) -> enum Result[A, B] {
Err(B)
Ok(A)
}
Result[String
String, String
String] {
(try? (city : String) -> String raise
利用 MoonBit 错误处理机制,简化实现
get_weather_(String
city)).(self : Result[String, Error], f : (Error) -> String) -> Result[String, String]
Maps the value of a Result if it is Err into another, otherwise returns the Ok value unchanged.
Example
let x: Result[Int, String] = Err("error")
let y = x.map_err((v : String) => { v + "!" })
assert_eq(y, Err("error!"))
map_err(_.(self : Error) -> String
to_string())
}
///| 利用 MoonBit 错误处理机制,简化实现
fn (city : String) -> String raise
利用 MoonBit 错误处理机制,简化实现
get_weather_(String
city : String
String) -> String
String raise {
// 创建请求
let Unit
request = (Unit) -> Unit
@types.OutgoingRequest::outgoing_request(
() -> Unit
@types.Fields::fields(),
)
// 为了天气,我们访问 wttr.in 来获取
if Unit
request.(Unit) -> Unit
set_authority(Unit
Some("wttr.in")) is (_/0) -> Unit
Err(_) {
(msg : String, loc~ : SourceLoc = _) -> Unit raise Failure
Raises a Failure error with a given message and source location.
Parameters:
message : A string containing the error message to be included in the
failure.
location : The source code location where the failure occurred.
Automatically provided by the compiler when not specified.
Returns a value of type T wrapped in a Failure error type.
Throws an error of type Failure with a message that includes both the
source location and the provided error message.
fail("Invalid Authority")
}
// 我们采用最简单的格式
if Unit
request.(Unit) -> Unit
set_path_with_query(Unit
Some("/\{String
city}?format=3")) is (_/0) -> Unit
Err(_) {
(msg : String, loc~ : SourceLoc = _) -> Unit raise Failure
Raises a Failure error with a given message and source location.
Parameters:
message : A string containing the error message to be included in the
failure.
location : The source code location where the failure occurred.
Automatically provided by the compiler when not specified.
Returns a value of type T wrapped in a Failure error type.
Throws an error of type Failure with a message that includes both the
source location and the provided error message.
fail("Invalid path with query")
}
if Unit
request.(Unit) -> Unit
set_method(Unit
Get) is (_/0) -> Unit
Err(_) {
(msg : String, loc~ : SourceLoc = _) -> Unit raise Failure
Raises a Failure error with a given message and source location.
Parameters:
message : A string containing the error message to be included in the
failure.
location : The source code location where the failure occurred.
Automatically provided by the compiler when not specified.
Returns a value of type T wrapped in a Failure error type.
Throws an error of type Failure with a message that includes both the
source location and the provided error message.
fail("Invalid Method")
}
// 发出请求
let Unit
future_response = (Unit, Unit) -> Unit
@outgoingHandler.handle(Unit
request, Unit
None).() -> Unit
unwrap_or_error()
defer Unit
future_response.() -> Unit
drop()
// 在这里,我们采用同步实现,等待请求返回
let Unit
pollable = Unit
future_response.() -> Unit
subscribe()
defer Unit
pollable.() -> Unit
drop()
Unit
pollable.() -> Unit
block()
// 在请求返回后,我们获取结果
let Unit
response = Unit
future_response.() -> Unit
get().() -> Unit
unwrap().() -> Unit
unwrap().() -> Unit
unwrap_or_error()
defer Unit
response.() -> Unit
drop()
let Unit
body = Unit
response.() -> Unit
consume().() -> Unit
unwrap()
defer Unit
body.() -> Unit
drop()
let Unit
stream = Unit
body.() -> Unit
stream().() -> Unit
unwrap()
defer Unit
stream.() -> Unit
drop()
// 将数据流解码为字符串
let Unit
decoder = (Unit) -> Unit
@encoding.decoder(Unit
UTF8)
let StringBuilder
builder = type StringBuilder
StringBuilder::(size_hint? : Int) -> StringBuilder
Creates a new string builder with an optional initial capacity hint.
Parameters:
size_hint : An optional initial capacity hint for the internal buffer. If
less than 1, a minimum capacity of 1 is used. Defaults to 0. It is the size of bytes,
not the size of characters. size_hint may be ignored on some platforms, JS for example.
Returns a new StringBuilder instance with the specified initial capacity.
new()
loop Unit
stream.(Int) -> Unit
blocking_read(1024) {
(Unit) -> Unit
Ok(Unit
bytes) => {
Unit
decoder.(Unit, StringBuilder, Bool) -> Unit
decode_to(
Unit
bytes.() -> Unit
unsafe_reinterpret_as_bytes()[:],
StringBuilder
builder,
Bool
stream=true,
)
continue Unit
stream.(Int) -> Unit
blocking_read(1024)
}
// 如果流被关闭,则视为 EOF,正常结束
(_/0) -> Unit
Err(_/0
Closed) => Unit
decoder.(String, StringBuilder, Bool) -> Unit
decode_to("", StringBuilder
builder, Bool
stream=false)
// 如果出错,我们获取错误信息
(_/0) -> Unit
Err((Unit) -> _/0
LastOperationFailed(Unit
e)) => {
defer Unit
e.() -> Unit
drop()
(msg : String, loc~ : SourceLoc = _) -> Unit raise Failure
Raises a Failure error with a given message and source location.
Parameters:
message : A string containing the error message to be included in the
failure.
location : The source code location where the failure occurred.
Automatically provided by the compiler when not specified.
Returns a value of type T wrapped in a Failure error type.
Throws an error of type Failure with a message that includes both the
source location and the provided error message.
fail(Unit
e.() -> String
to_debug_string())
}
}
StringBuilder
builder.(self : StringBuilder) -> String
Returns the current content of the StringBuilder as a string.
to_string()
}
这段代码实现了以下功能:
- 创建一个 HTTP 请求,目标是
wttr.in天气服务 - 设置请求路径,包含城市名称和格式参数
- 发送请求并等待响应
- 从响应中提取内容
- 解码内容并返回天气信息字符串
这段代码使用了 WASI HTTP 接口来发送请求,以同步 API 进行交互。其中,defer
关键字确保资源在使用后被正确释放。
第5步:构建项目
现在我们已经实现了功能,下一步是构建项目。
# 编译 MoonBit 代码,生成核心 WebAssembly 模块
moon build --target wasm
# 嵌入 WIT 接口信息,指定字符串编码
wasm-tools component embed wit target/wasm/release/build/gen/gen.wasm -o core.wasm --encoding utf16
# 将核心 Wasm 模块转化为 Wasm 组件模块
wasm-tools component new core.wasm -o weather.wasm
构建成功后,会在项目根目录生成 weather.wasm 文件,这就是我们的 WebAssembly
组件。
之后,我们将它加载到 wassette 的路径中。当然,也可以选择通过对话,让 AI 来进行动态加载,不仅可以加载本地文件,也可以加载远程服务器上的文件。
wassette component load file://$(pwd)/component.wasm