BoxAgnts 工具系统(5)——WASM 工具开发:从 Hello World 到生产部署

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

参数冲突处理inputfile_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 或配置格式,只需要遵循两条约定:

  1. --help 输出必须包含标准的 CLI 帮助块Usage:Options:Arguments:Commands:),供系统自动提取 Schema。
  2. stdout 输出 JSON 格式 {"error": bool, "content": "..."},可选的 metadata 字段用于向前端传递结构化渲染信息。

除此之外,工具代码完全是普通的 CLI 程序。这在开发者体验上是一个分水岭------传统的 Agent 框架要求开发者理解框架的 Tool 基类、Schema 声明格式、回调注册方式,BoxAgnts 把这些全部替换为"写好 --help 就行"。

跨语言支持是另一个独有的优势。Rust、Go、Python、C------任何能编译到 wasm32-wasi 的语言都可以用来开发 BoxAgnts 工具。编译后的 .wasm 文件放入扩展目录,热加载机制自动处理剩余的注册和缓存步骤。

参考资源

相关推荐
人工智能培训1 小时前
医疗行业的数字孪生革命
大数据·人工智能·重构·知识图谱·agent
zyk_computer1 小时前
AI Agent ,让循环收敛的那套闭环控制系统
人工智能·后端·python·ai·架构·agent·ai agent
niyongsheng2 小时前
如何用 Rust 写一个AI Agent:TUI 交互终端、CLI 子代理、飞书运维机器人
agent·deepseek
leeyi2 小时前
流式管道:Pipe、StreamReader、背压控制
agent·ai编程·领域驱动设计
佛系豪豪吖2 小时前
AtomCode 部署流程与使用经验
笔记·chatgpt·github·ai编程·gitcode
an317422 小时前
使用 LangGraph + DeepSeek 构建 AI 面试官:状态图设计与实践
前端·ai编程
星栈2 小时前
写 Makepad Demo 不难,难的是把它写成项目
前端·rust
HIT_Weston2 小时前
113、【Agent】【OpenCode】项目配置(package.json)
人工智能·agent·opencode
夜尽天明_2 小时前
告别 AI 乱写代码!一键生成项目“AI 说明书”,让 Cursor 和 Claude 乖乖守规矩
ai编程