WASM 沙箱为 BoxAgnts 提供了指令级的安全隔离,工具注册链路则实现了零配置的自动发现。在这两个基础设施之上,开发者只需要关注一件事:编写符合 CLI 惯例的程序。这篇直接上手,从一个 base64 编码工具的完整开发过程开始,到编译、部署、测试,再到一些容易踩坑的地方。
为什么选 base64 作为示例
base64 编码/解码是一个理想的示例工具:逻辑足够简单(不会分散注意力),但覆盖了 AI Agent 工具的典型特征------有多个输入参数(模式、输入来源、输出目标)、有错误处理(非法 base64 字符串)、有文件 I/O、有严格的输出格式要求。理解了 base64 工具的开发模式,就理解了所有 WASM 工具的开发模式。
完整的示例代码位于 BoxAgnts 仓库的 examples/tool-sample-base64-component/。
Cargo.toml 配置
ini
[package]
name = "tool-sample-base64-component"
version = "1.0.0"
edition = "2021"
[[bin]]
name = "base64"
path = "src/main.rs"
[dependencies]
clap = { version = "4", features = ["derive", "string"] }
base64 = "0.22"
serde_json = "1"
依赖非常轻:clap 处理 CLI 参数解析,base64 处理编解码逻辑,serde_json 做结构化输出。没有 WASM 特定的依赖------Wasmtime 在宿主侧提供运行环境,WASM 工具本身不需要知道自己跑在沙箱里。
WASM 编译目标需要在 .cargo/config.toml 中指定(或者通过命令行 --target):
ini
[build]
target = "wasm32-wasip2"
核心代码
主函数结构如下(完整代码见仓库):
rust
use clap::{Parser, ValueEnum};
use base64::{engine::general_purpose, Engine as _};
use serde_json::json;
#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
enum Mode { Encode, Decode }
#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
enum Alphabet { Standard, UrlSafe }
#[derive(Parser, Debug)]
#[command(name = "base64")]
#[command(version)]
#[command(about = "Strict Base64 encode/decode tool")]
struct Args {
#[arg(long, value_enum, required = true)]
mode: Mode,
#[arg(long, conflicts_with = "file_path")]
input: Option<String>,
#[arg(long, conflicts_with = "input")]
file_path: Option<String>,
#[arg(long)]
output_file: Option<String>,
#[arg(long, value_enum, default_value = "standard")]
alphabet: Alphabet,
#[arg(long, default_value_t = false)]
no_padding: bool,
}
fn main() {
let args = Args::parse();
if let Err(e) = validate_args(&args) {
eprintln!(r#"{{"error":true,"content":"{}"}}"#, e);
std::process::exit(1);
}
let input_bytes = match read_input(&args) {
Ok(b) => b,
Err(e) => {
eprintln!(r#"{{"error":true,"content":"{}"}}"#, e);
std::process::exit(1);
}
};
let engine: &dyn Engine = match (&args.alphabet, args.no_padding) {
(Alphabet::Standard, false) => &general_purpose::STANDARD,
(Alphabet::Standard, true) => &general_purpose::STANDARD_NO_PAD,
(Alphabet::UrlSafe, false) => &general_purpose::URL_SAFE,
(Alphabet::UrlSafe, true) => &general_purpose::URL_SAFE_NO_PAD,
};
let result = match args.mode {
Mode::Encode => engine.encode(&input_bytes),
Mode::Decode => {
let input_str = std::str::from_utf8(&input_bytes)
.unwrap_or_else(|_| "");
match engine.decode(input_str.trim()) {
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
Err(e) => {
eprintln!(r#"{{"error":true,"content":"Invalid base64: {}"}}"#, e);
std::process::exit(1);
}
}
}
};
if let Some(output_file) = &args.output_file {
std::fs::write(output_file, &result).unwrap_or_else(|e| {
eprintln!(r#"{{"error":true,"content":"Write failed: {}"}}"#, e);
std::process::exit(1);
});
println!(r#"{{"error":false,"content":"Written to {}"}}"#, output_file);
} else {
println!(r#"{{"error":false,"content":"{}"}}"#, result);
}
}
几个实现细节值得说明。
JSON 输出格式 。WASM 工具通过 stdout 返回 JSON 对象,格式约定为 {"error": bool, "content": "..."}。BoxAgnts 的 WasmTool::execute() 会自动解析这个 JSON 并映射到 ToolResult。如果 stdout 不是合法 JSON,整段文本被视为成功结果的 content。
参数冲突处理 。input 和 file_path 互斥------conflicts_with 让 clap 在解析阶段就拒绝同时出现的情况,而不是等到业务代码里再检查。
错误输出到 stderr。WASM 安全失败时应输出到 stderr 而非 stdout。BoxAgnts 分别捕获两个流,stderr 内容用于错误报告,stdout 用于工具结果。
编译与部署
bash
# 编译
cargo build --target wasm32-wasip2 --release
# 产物位置
ls target/wasm32-wasip2/release/base64.wasm
编译完成后直接复制到扩展目录:
bash
cp target/wasm32-wasip2/release/base64.wasm \
app/extensions/tools/base64-component.wasm
文件系统的变化被 notify 事件监听器捕获,触发热加载流程:沙箱执行 --help、解析输出、生成 ToolSpec、注册到全局工具表。从文件复制到工具可用的总延迟通常在 100 毫秒以内,其中主要耗时是 Wasmtime 编译 WASM 为 .cwasm 缓存。
跨语言开发
虽然示例用了 Rust,但 WASM 工具可以用任何支持 wasm32-wasi 的语言。以下是用 Go 写一个简单的 file-read 工具的伪代码对比:
lua
// Go 版本 file-read(使用 TinyGo 编译)
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, `{"error":true,"content":"Missing file path"}`)
os.Exit(1)
}
data, err := os.ReadFile(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, `{"error":true,"content":"%s"}`, err)
os.Exit(1)
}
fmt.Printf(`{"error":false,"content":"%s"}`, string(data))
}
arduino
# 编译
tinygo build -target wasm-wasi -o file-read.wasm main.go
Go 版本和 Rust 版本的 file-read 行为完全一致------它们输出相同格式的 JSON,在相同的沙箱约束下运行,被相同的 WasmTool::execute() 调用。这是 WASM 作为工具分发格式的核心价值:定义一个简单的输出约定,不同语言的实现自动兼容。
常见问题
文件 I/O 的路径
WASM 工具看到的文件系统不是宿主机的完整文件系统。如果 RunOption.work_dir 被设为 /home/user/project,WASM 工具内部用 ./src/main.rs 访问的就是宿主机的 /home/user/project/src/main.rs。如果试图访问 /etc/passwd,会因为不在映射的目录范围内而失败。
stdout 缓冲区
WASM 的 stdout 是行缓冲还是全缓冲取决于 WASI 实现。如果工具在写 JSON 后没有显式 flush 就退出,最后一块输出可能丢失。对于单次输出少量 JSON 的场景通常不会出问题,但如果工具产生大量输出(比如 file-read 读取 100MB 的文件),建议分段输出或使用流式协议。
编码问题
println! 在 WASI 环境下默认输出 UTF-8。如果工具需要输出非 UTF-8 编码的文本(比如读取 GBK 编码文件),需要手动控制编码并在结果的 content 字段中做 Base64 包装。
测试工具
开发过程中可以用 BoxAgnts 的 CLI 直接测试 WASM 工具,不需要通过 AI 对话:
makefile
# 模拟工具注册------查看系统解析出的 ToolSpec
boxagnts tool:validate path/to/tool.wasm
# 模拟工具执行------传入 JSON 参数
boxagnts tool:execute path/to/tool.wasm '{"mode":"encode","input":"hello"}'
这比通过 AI 对话测试快得多,而且能够直接看到 Wasmtime 层面的错误信息(如果沙箱启动失败)。
工具 vs 技能
WASM 工具适合处理有确定性的计算型任务:编解码、文件操作、数据库查询、正则匹配。但如果一个任务的核心不是"计算"而是"指导 AI 的思维过程"------比如代码审查、架构建议、写作指导------就不适合用 WASM 工具实现。这类场景应该用 Skill(技能),它是纯 Markdown 提示词模板,由系统加载后注入到 AI 的上下文中,AI 据此自主决策和执行操作。
总结
BoxAgnts 的 WASM 工具开发流程在简洁性上做了减法------开发者不需要学习任何 BoxAgnts 特有的 API 或配置格式,只需要遵循两条约定:
--help输出必须包含标准的 CLI 帮助块 (Usage:、Options:、Arguments:或Commands:),供系统自动提取 Schema。- stdout 输出 JSON 格式
{"error": bool, "content": "..."},可选的metadata字段用于向前端传递结构化渲染信息。
除此之外,工具代码完全是普通的 CLI 程序。这在开发者体验上是一个分水岭------传统的 Agent 框架要求开发者理解框架的 Tool 基类、Schema 声明格式、回调注册方式,BoxAgnts 把这些全部替换为"写好 --help 就行"。
跨语言支持是另一个独有的优势。Rust、Go、Python、C------任何能编译到 wasm32-wasi 的语言都可以用来开发 BoxAgnts 工具。编译后的 .wasm 文件放入扩展目录,热加载机制自动处理剩余的注册和缓存步骤。
参考资源
- BoxAgnts 源代码:github.com/guyoung/box...
- base64 工具示例:github.com/guyoung/box...
- Cargo WASM 编译指南:rustwasm.github.io/docs/book/
- TinyGo WASM 编译:tinygo.org/docs/guides...
- WASI Preview2 组件模型:component-model.bytecodealliance.org/