用 WebAssembly 编写插件
沙箱可加载单元的通用 host-guest ABI 与内存约定、跨语言编写路径、Rust 实战示例,以及单一 .wasm 产物跨环境加载
本页是「沙箱可加载单元」的语言实现指南,给出一套与具体语言无关的 host-guest ABI 与内存约定:任何能编译到 WebAssembly(WASM)的语言,只要按本规范导出约定符号、按约定在边界两侧交换数据,就能写出可被灵渠平台加载的插件。本页主示例用 Rust,并在末尾指向 Go(经 TinyGo)与 TypeScript(经 AssemblyScript 兼容子集)两条同 ABI 的源语言路径。
本页定位。 本页是 自定义插件开发 在「沙箱可加载单元」形态上的落地参考——上层的插件模型、生命周期符号、执行时序、注册启停与失败处置以那一页为准;本页只补"如何用一种能编译到 WASM 的语言把那套契约写成真实可编译的 .wasm"。术语(前置 / 内建 / 后置、
order、短路、逐 chunk、fail-open / fail-closed)与 概念与编排、扩展点 SDK 参考 完全一致,不重复其内容。
1. 概述:沙箱可加载单元与跨语言编写
自定义插件开发 §4.1 给出了两种加载形态。本页只讲其中的沙箱可加载单元:
| 形态 | 一句话 | 本页是否覆盖 |
|---|---|---|
| 原生可加载单元 | 随平台运行环境、同进程内调用、性能最优、无跨边界开销;要求与平台运行环境构建参数对齐 | 否,见 用 Go 编写原生插件 |
| 沙箱可加载单元 | 跨边界以结构化数据(JSON)交换、隔离执行、内存受限、单一产物跨环境可加载;无构建对齐要求 | 是(本页) |
沙箱形态的关键特征,决定了它的编写方式与原生形态不同:
- (1)跨边界以 JSON 交换。插件与宿主运行时分处沙箱内外两侧,复杂数据一律编码为 JSON 字符串、经线性内存传递;插件不直接共享宿主的内部数据结构。
- (2)单一产物跨环境。一个
.wasm产物可在不同部署环境加载,没有运行环境家族、处理器架构、构建工具版本的对齐要求——这与原生形态相反。 - (3)语言无关。本页的 ABI 是 WASM 层面的符号与内存约定;任何能产出符合该约定
.wasm的语言都可据此编写。本页主用 Rust;TypeScript 是其语言特例(经 AssemblyScript 兼容子集,见 用 TypeScript 编写插件);Go 也可经 TinyGo 编译为沙箱单元(与 用 Go 编写原生插件 的原生路径相对照,见 §7)。
无论用哪种源语言,插件对外都呈现为 自定义插件开发 §2 约定的同一组生命周期符号(get_name / init / pre_hook / post_hook / cleanup);本页把这组抽象符号落到 WASM 导出函数与内存读写的具体规范上。
2. 适用场景与原生取舍
选沙箱还是原生,取决于对延迟、移植性、隔离性的权衡。下表与 自定义插件开发 §4.1 的取舍表口径一致:
| 维度 | 原生可加载单元 | 沙箱可加载单元(本页) |
|---|---|---|
| 执行开销 | 最低,同进程内直接调用、传引用 | 含跨边界的 JSON 序列化 / 反序列化开销 |
| 跨环境移植 | 需按目标环境分别构建 | 单一 .wasm 处处可加载 |
| 构建对齐要求 | 须与平台运行环境构建参数严格对齐 | 无对齐要求 |
| 隔离性 | 与平台同进程,无额外隔离 | 沙箱隔离、内存受限、无原生 IO |
选型建议(与上层文档一致):对延迟极敏感、且部署环境可控的治理逻辑,优先原生形态;需要跨多种部署环境分发一份产物、或希望以沙箱隔离限制插件影响面的逻辑,选沙箱形态。两种形态对外导出的符号语义一致,业务逻辑可在二者间平移——把同一套 Hook 处置逻辑换一条工具链构建即可。
适合沙箱形态的典型治理逻辑:入站校验与归属补全、为路由 / 护栏注入结构化提示、缓存判定辅助、出站脱敏与拦截判定、计量决策采样。这些都落在控制面 / 治理面,不需要原生 IO,正适合受限沙箱(能力边界见 §13)。
3. host-guest ABI 与内存约定
这是本页的核心。任何源语言只要满足本节的导出符号表与内存约定,产出的 .wasm 即可被平台加载。
3.1 导出符号表
沙箱单元须导出以下函数(精确小写名)。其中 malloc / free 服务于内存握手,其余对应 自定义插件开发 §2 的生命周期符号:
| 导出符号 | 签名 | 何时调用 | 职责 |
|---|---|---|---|
malloc | malloc(size:u32)->u32 | 宿主写入入参前 | 在 guest 线性内存中分配 size 字节,返回偏移指针 |
free | free(ptr:u32) 或 free(ptr:u32,size:u32) | 宿主读完出参后 | 释放 malloc 分配的缓冲 |
get_name | get_name()->u64 | 加载时 | 返回插件全局唯一标识;返回值为打包指针,指向标识字符串 |
init | init(config_ptr:u32,config_len:u32)->i32 | 加载后、首次处理请求前一次 | 接收配置 JSON,完成一次性初始化;成功返回 0 |
pre_hook | pre_hook(input_ptr:u32,input_len:u32)->u64 | 每次请求转发上游前 | 前置 Hook:读入参 JSON、处置、返回打包出参 JSON |
post_hook | post_hook(input_ptr:u32,input_len:u32)->u64 | 每次上游响应返回后(或短路后) | 后置 Hook:读入参 JSON、处置、返回打包出参 JSON |
cleanup | cleanup()->i32 | 卸载 / 平台停机时一次 | 释放资源;成功返回 0 |
约定要点:
- (1)复杂数据全量 JSON 跨边界。
pre_hook/post_hook/init/get_name涉及的结构化数据,一律编码为 UTF-8 的 JSON 字符串,经线性内存的指针 + 长度传递。 - (2)返回值用打包 u64。凡返回字符串缓冲的导出函数(
get_name/pre_hook/post_hook),返回一个 u64:高 32 位存放指针、低 32 位存放长度。宿主据此定位并读取出参缓冲。 - (3)返回码用 i32。
init/cleanup返回 i32 状态码,0表示成功,非0表示失败(失败处置见 §11)。 - (4)不另设流式专用导出。本平台流式按 扩展点 SDK §2.3 建模——后置 Hook 在流式下被多次调用(同一
post_hook,凭入参里的stream字段区分is_stream/chunk_index/is_last);不要为流式另写单独的 chunk 导出函数,以保持与既有契约一致。
3.2 内存生命周期与打包返回值
宿主与 guest 共享 guest 的线性内存,但分配与释放有明确的责任划分。一次 Hook 调用的内存握手如下:
宿主 沙箱单元(guest 线性内存)
│ malloc(in_len) ───────────────────▶ 分配 in_len 字节,返回偏移指针 in_ptr
│ 在 in_ptr 处写入入参 JSON 字节
│ pre_hook(in_ptr, in_len) ─────────▶ read_string(in_ptr,in_len) 解析 JSON → 处置
│ malloc(out_len) 分配出参缓冲 out_ptr
│ write_string 写出参 JSON
│ ◀───────── 返回打包 u64(高 32 位=out_ptr,低 32 位=out_len)
│ 按 out_ptr/out_len 读出参 JSON
│ free(in_ptr, in_len) ─────────────▶ 释放入参缓冲
│ free(out_ptr, out_len) ───────────▶ 释放出参缓冲要点:入参缓冲由宿主 malloc、写入、并在读完出参后 free;出参缓冲由 guest 在 Hook 内 malloc 并写入,由宿主读取后 free。打包返回值的拆解约定是固定的——宿主把 u64 右移 32 位得指针、对低 32 位取掩码得长度。
3.3 调用时序
把生命周期串起来,一个沙箱单元从加载到停机的完整调用时序如下(init 一次 → 每请求 pre_hook、内建上游、post_hook → 停机 cleanup):
流式响应下,
pre_hook仍只在请求转发前调用一次;post_hook中标记逐 chunk 的部分按 chunk 多次调用,request_id全程一致、末块is_last为true(见 扩展点 SDK §2.3)。init与cleanup各只在加载、卸载时调用一次,与单次请求无关。
4. Hook I/O 的沙箱视角
pre_hook / post_hook 的入参出参字段与 扩展点 SDK §2 完全一致——区别只在于:在沙箱里,这些字段就是你 malloc 读到、用 JSON 解析、处置后再 JSON 写回的那个字符串。动作枚举不变:前置为 continue / rewrite / short_circuit / reject,后置为 pass / redact / block。
下表为沙箱视角的字段速查(完整 JSON 形态见 扩展点 SDK §2.1 / §2.2):
| 方向 | 关键字段(嵌套以"/"展开) |
|---|---|
| 前置入参 | phase="pre"、request_id、api_key_id、request(model/messages/stream)、routing(complexity/candidate_model)、metadata(received_at/node_id)、context |
| 前置出参 | action(continue/rewrite/short_circuit/reject)、request(rewrite 时改写后的入站请求)、short_circuit_response、reject_reason(code/message)、context(向配对后置传递) |
| 后置入参 | phase="post"、request_id、api_key_id、response(model/content/finish_reason)、usage(input_tokens/output_tokens/cache_hit/thinking/price_tier)、stream(is_stream/chunk_index/is_last)、context(配对前置传来) |
| 后置出参 | action(pass/redact/block)、response(redact 时脱敏 / 改写后的出站内容)、block_reason(code/message) |
context是前置与后置之间的唯一对称通道:前置 Hook 写入context的值,会在配对的后置 Hook 入参里原样回传。沙箱内你不持有跨调用的全局状态(每次 Hook 都是一次干净的入参解析),因此跨阶段携带状态一律走context,而不是 guest 内的静态变量。
5. 完整最小示例(Rust)
下面给出一个"直通"沙箱单元的最小可编译骨架:含 memory 模块(malloc / free / read_string / write_string)、契约结构体(用 serde rename 对齐字段名)、以及 get_name / init / pre_hook / post_hook / cleanup 五个导出符号。它什么治理逻辑都不做(前置 continue、后置 pass),只用于先跑通"加载 → 挂载 → 调用 → 卸载"全链路。
5.1 Cargo.toml
[package]
name = "lingqu-plugin-passthrough"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"5.2 memory 模块(内存握手)
// src/lib.rs —— 沙箱可加载单元(passthrough 直通骨架)
use serde::{Deserialize, Serialize};
// ===== memory:宿主与 guest 的内存握手 =====
mod memory {
use std::alloc::{alloc, dealloc, Layout};
// 宿主调用:在 guest 线性内存中分配 size 字节,返回偏移指针
#[no_mangle]
pub extern "C" fn malloc(size: u32) -> u32 {
if size == 0 {
return 0;
}
let layout = Layout::from_size_align(size as usize, 1).unwrap();
unsafe { alloc(layout) as u32 }
}
// 宿主调用:释放 malloc 分配的缓冲(采用带 size 的形态)
#[no_mangle]
pub extern "C" fn free(ptr: u32, size: u32) {
if ptr == 0 || size == 0 {
return;
}
let layout = Layout::from_size_align(size as usize, 1).unwrap();
unsafe { dealloc(ptr as *mut u8, layout) }
}
// guest 内部:把 ptr/len 处的字节读成入参 JSON 字符串
pub fn read_string(ptr: u32, len: u32) -> String {
let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) };
String::from_utf8_lossy(slice).into_owned()
}
// guest 内部:把出参 JSON 写入新缓冲,返回打包 u64(高 32 位=指针,低 32 位=长度)
pub fn write_string(s: &str) -> u64 {
let bytes = s.as_bytes();
let len = bytes.len() as u32;
let ptr = malloc(len);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, len as usize);
}
((ptr as u64) << 32) | (len as u64)
}
}5.3 契约结构体与导出符号
serde 的 rename / 字段名直接对齐 Hook 契约;未列出的入参字段被 serde 忽略,按需取用即可。
// ===== 入参出参结构体(字段名对齐 Hook 契约)=====
#[derive(Deserialize)]
struct PreInput {
phase: String,
request_id: String,
// 其余字段(api_key_id / request / routing / metadata / context)按需补充
}
#[derive(Serialize)]
struct PreResult {
action: String, // continue | rewrite | short_circuit | reject
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<serde_json::Value>,
}
#[derive(Deserialize)]
struct PostInput {
phase: String,
request_id: String,
#[serde(default)]
context: serde_json::Value,
}
#[derive(Serialize)]
struct PostResult {
action: String, // pass | redact | block
}
// ===== 生命周期与 Hook 导出符号 =====
#[no_mangle]
pub extern "C" fn get_name() -> u64 {
memory::write_string("出站直通插件")
}
#[no_mangle]
pub extern "C" fn init(config_ptr: u32, config_len: u32) -> i32 {
let _config = memory::read_string(config_ptr, config_len);
// 解析配置、建立一次性状态;成功返回 0
0
}
#[no_mangle]
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
let raw = memory::read_string(input_ptr, input_len);
let _input: PreInput = match serde_json::from_str(&raw) {
Ok(v) => v,
Err(_) => return memory::write_string("{\"action\":\"continue\"}"),
};
let result = PreResult {
action: "continue".to_string(),
context: Some(serde_json::json!({ "marked_by": "出站直通插件" })),
};
memory::write_string(&serde_json::to_string(&result).unwrap())
}
#[no_mangle]
pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 {
let raw = memory::read_string(input_ptr, input_len);
let _input: PostInput = match serde_json::from_str(&raw) {
Ok(v) => v,
Err(_) => return memory::write_string("{\"action\":\"pass\"}"),
};
let result = PostResult { action: "pass".to_string() };
memory::write_string(&serde_json::to_string(&result).unwrap())
}
#[no_mangle]
pub extern "C" fn cleanup() -> i32 {
0
}跑通直通骨架后,再逐步把
action从continue/pass改为rewrite/short_circuit/redact/block,填入真实治理逻辑。各action取值的完整语义见 扩展点 SDK §2;填入逻辑时务必恪守统一能力边界(见 §13)。
6. 实战示例(Rust):请求复杂度路由提示增强插件
下面给出一个真实治理场景的沙箱单元,挂在前置段(pre)、挂载到路由中间件节点:
pre_hook:读routing.complexity。对复杂请求,向request注入一条结构化路由提示(action="rewrite"),并把决策写入context。提示仅供路由中间件消费、不触碰messages的用户语义——属治理面改写。post_hook:读前置传来的context,做一次计量采样后action="pass",不接触响应内容。
memory 模块与导出符号的注册方式同 §5(此处省略 memory、get_name / init / cleanup),只替换两个 Hook 与相关结构体:
use serde::{Deserialize, Serialize};
// ===== 入参出参结构体 =====
#[derive(Deserialize)]
struct PreInput {
request_id: String,
request: RequestPayload,
routing: Routing,
}
#[derive(Deserialize, Serialize, Clone)]
struct RequestPayload {
model: String,
messages: serde_json::Value, // 原样保留,不改用户语义
#[serde(default)]
stream: bool,
// 治理面附加:结构化路由提示,仅供路由中间件消费
#[serde(skip_serializing_if = "Option::is_none")]
routing_hint: Option<RoutingHint>,
}
#[derive(Deserialize)]
struct Routing {
complexity: String, // 简单 | 复杂
candidate_model: String,
}
#[derive(Deserialize, Serialize, Clone)]
struct RoutingHint {
prefer_tier: String,
reason: String,
}
#[derive(Serialize)]
struct PreResult {
action: String,
#[serde(skip_serializing_if = "Option::is_none")]
request: Option<RequestPayload>,
context: serde_json::Value,
}
#[derive(Deserialize)]
struct PostInput {
#[serde(default)]
context: serde_json::Value,
}
// ===== 前置 Hook:复杂请求注入路由提示,决策写入 context =====
#[no_mangle]
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
let raw = memory::read_string(input_ptr, input_len);
let input: PreInput = match serde_json::from_str(&raw) {
Ok(v) => v,
Err(_) => return memory::write_string("{\"action\":\"continue\",\"context\":{}}"),
};
// 仅读 routing.complexity 做治理面研判
if input.routing.complexity != "复杂" {
let result = PreResult {
action: "continue".to_string(),
request: None,
context: serde_json::json!({ "hint_applied": false }),
};
return memory::write_string(&serde_json::to_string(&result).unwrap());
}
// 复杂请求:注入结构化路由提示(不触碰 messages)
let mut rewritten = input.request.clone();
rewritten.routing_hint = Some(RoutingHint {
prefer_tier: "high_capability".to_string(),
reason: "complexity=复杂,建议路由至能力更强档".to_string(),
});
let result = PreResult {
action: "rewrite".to_string(),
request: Some(rewritten),
context: serde_json::json!({
"hint_applied": true,
"candidate_model": input.routing.candidate_model,
}),
};
memory::write_string(&serde_json::to_string(&result).unwrap())
}
// ===== 后置 Hook:读 context 做计量采样后放行 =====
#[no_mangle]
pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 {
let raw = memory::read_string(input_ptr, input_len);
let input: PostInput = serde_json::from_str(&raw)
.unwrap_or(PostInput { context: serde_json::json!({}) });
// 读取前置写入的 context 做一次计量采样(仅采集,不改内容)
let hint_applied = input
.context
.get("hint_applied")
.and_then(|v| v.as_bool())
.unwrap_or(false);
sample_metric("routing_hint_applied", hint_applied);
memory::write_string("{\"action\":\"pass\"}")
}
// 计量采样:沙箱内无原生 IO,采样结果经平台计量底座归集
fn sample_metric(_key: &str, _value: bool) {
// 仅采集决策标记,不接触响应内容(治理面)
}这一例突出两点:(1)
context的跨阶段传递——前置把hint_applied/candidate_model写入context,配对后置原样读回用于计量采样,无需任何 guest 全局状态;(2)能力边界——rewrite只在request上附加结构化路由提示、messages原样保留,后置只采样不改response,全程落在治理面,不对结果内容做实质性生成、加工或合成(见 §13)。
7. 其他源语言(TinyGo / TypeScript)
本页 ABI 是 WASM 层面的约定,三种源语言产出的 .wasm 共享同一符号表与内存约定,业务逻辑可平移:
| 源语言 | 工具链 | 构建命令(要点) |
|---|---|---|
| Rust(本页主示例) | cargo + wasm32 目标 | cargo build --release --target wasm32-unknown-unknown |
| Go | TinyGo | tinygo build -o plugin.wasm -target=wasi |
| TypeScript | AssemblyScript(asc) | 见 用 TypeScript 编写插件 |
TinyGo 路径:用 TinyGo 把 Go 源编译为沙箱 .wasm:
tinygo build -o plugin.wasm -target=wasi注意:TinyGo 需以导出注解(
//export malloc等)显式导出本节符号表中的每个函数,并自行实现与本 ABI 一致的内存读写(malloc/free配套、打包 u64 返回值、JSON 跨边界)。同一套 Go 治理逻辑既可经 TinyGo 编译为沙箱单元(本页 ABI),也可经标准 Go 工具链编译为原生单元(见 用 Go 编写原生插件)——前者单一产物跨环境、无对齐要求,后者性能最优、须与平台运行环境构建参数对齐。
TypeScript 路径:TypeScript 经 AssemblyScript 兼容子集由 asc 编译为 .wasm,可用 JSON 序列化库处理跨边界字符串;细节、最小示例与坑位见 用 TypeScript 编写插件。
8. 构建
Rust 沙箱单元的构建:
# 一次性:添加 wasm32 目标
rustup target add wasm32-unknown-unknown
# 构建沙箱单元
cargo build --release --target wasm32-unknown-unknown
# 产物(包名中的连字符在产物名里转为下划线)
# target/wasm32-unknown-unknown/release/lingqu_plugin_passthrough.wasm
# 可选:压缩体积(标准 WASM 优化工具)
wasm-opt -Os target/wasm32-unknown-unknown/release/lingqu_plugin_passthrough.wasm \
-o plugin.wasm沙箱形态无平台对齐要求:上面产出的单一
.wasm可跨部署环境加载,不需要按运行环境家族 / 处理器架构 / 构建工具版本分别构建。这正是沙箱形态相对原生形态的核心优势。wasm-opt仅压缩体积、不改变 ABI,是可选步骤。
9. 注册与加载
注册流程沿用 自定义插件开发 §4 的统一口径。
第一步,确认目标实例支持动态加载——读部署状态、看 capabilities.dynamic_plugins:
curl https://<平台入口地址>/api/v1/deployment \
-H "Cookie: <控制台会话凭证>"{
"deploy_mode": "private",
"deploy_status": "ready",
"capabilities": {
"dynamic_plugins": true
}
}第二步,注册沙箱单元——artifact_ref 指向 .wasm 产物,本例的路由提示增强插件挂前置段:
curl https://<平台入口地址>/api/v1/pipeline/extensions \
-X POST \
-H "Content-Type: application/json" \
-H "Cookie: <控制台会话凭证>" \
-d '{
"name": "请求复杂度路由提示增强插件",
"artifact_ref": "<.wasm 构建产物引用(加载位置/版本标识)>",
"node_id": "<挂载的路由中间件节点>",
"hook_type": "pre",
"placement": "pre",
"order": 20,
"enabled": true,
"config": {
"<插件自定义配置项>": "<值>"
},
"fail_policy": "fail_open"
}'| 字段 | 本例取值与说明 |
|---|---|
name | 须与 get_name 返回一致 |
artifact_ref | 指向 .wasm 产物的加载位置 / 版本标识 |
node_id | 挂载到的内建路由中间件节点 |
hook_type / placement | 均为 pre,二者须匹配 |
order | 段位内执行位次,值小者先 |
fail_policy | 本例为路由提示增强、非安全护栏,选 fail_open(提示失败仍放行请求);安全护栏类插件默认 fail_closed(见 §11) |
读取已注册清单用 GET /api/v1/pipeline/extensions(见 扩展点 SDK §3)。
若
dynamic_plugins为false,私有化部署需先联系部署方切换为「支持动态加载」的构建参数,再注册;SaaS 部署由平台运营侧统一受理。
10. 限制
沙箱形态以隔离换性能与移植性,相应有以下硬性限制:
- (1)内存受限。guest 在受限的线性内存上运行,宿主对单插件的内存上限有约束;入参出参体量大时须注意,避免在 Hook 内构造超大缓冲。
- (2)沙箱隔离。插件运行在沙箱内,不能直接访问宿主内部数据结构或全局状态;跨调用状态一律走
context,跨边界数据一律走 JSON。 - (3)无原生 IO。沙箱单元不持有原生文件 / 网络 IO;外部连接、落盘、上报等不能在 guest 内直接发起——计量采集等结果经出参 /
context交回宿主,由平台计量底座归集(见 §6 的sample_metric注释)。 - (4)有序列化开销。每次 Hook 都要做一轮 JSON 编解码;这是沙箱形态相对原生形态延迟更高的根因(见 §12)。
若某治理逻辑强依赖原生 IO 或对单次延迟极敏感,应改用原生形态(见 用 Go 编写原生插件),而非在沙箱里绕开限制。
11. 失败处置与启停
沙箱单元的失败处置与启停,与 自定义插件开发 §5、扩展点 SDK §5 的统一口径一致,不另设规则:
| 情形 | 处置 |
|---|---|
加载 / init 返回非 0 或报错 | 该插件不纳入编排;其余插件与内建管道照常运行;「中间件编排」页标注为加载失败 |
前置 Hook 返回 reject | 请求被结构化拒答,不再转发上游;逐条记录护栏处置结果为对应类别 |
| 前置 Hook 执行报错 | 按 fail_policy:fail_open 放行、fail_closed 拒绝;安全护栏类默认 fail_closed |
后置 Hook 返回 block | 出站响应被拦截、结构化返回;已发生的计量按实落账 |
| 后置 Hook 执行报错 | 不影响已完成的上游调用与计量落账;按 fail_policy 决定是否放行出站内容,护栏类默认拦截 |
| 短路路径上的后置异常 | 逆序收尾继续执行其余后置 Hook,单个插件异常不阻断整条收尾链 |
启停沿用统一口径:
- 启用 / 停用:
enabled控制单个插件执行状态;停用的插件保留在编排清单中、运行时被跳过,便于灰度与回退。 - 热替换:更新
artifact_ref的版本标识即触发平台对该.wasm的重新加载——先以新产物完成init,再切换执行,旧实例cleanup收尾,不打断在途请求的整体管道。 - 位次调整:段位内位次经
PUT /api/v1/pipeline/order重排(见 概念与编排 §2.2)。
涉及安全合规的护栏类沙箱单元一律默认
fail_closed——"宁可拒绝,不放行未经复核的内容"。单个沙箱单元的异常被隔离在该插件范围内,不会让整条管道连带失败。
12. 常见问题
问:哪些语言能编译到本 ABI?
答:任何能编译为 wasm32 目标、能导出本节符号表(malloc / free / get_name / init / pre_hook / post_hook / cleanup)、并以 JSON 字符串跨边界的语言都可以。本页以 Rust 为主,Go(经 TinyGo)与 TypeScript(经 AssemblyScript 兼容子集)均已覆盖。关键不在语言本身,而在能否产出符合符号表与内存约定的 .wasm。
问:沙箱比原生慢的量级来自哪里? 答:主要来自跨边界的 JSON 序列化 / 反序列化(每次 Hook 把入参出参在 JSON 字符串与线性内存之间编解码),以及沙箱隔离边界的调用成本——而非指令执行本身。Hook 入参出参体量越大,序列化占比越高;无大对象往返时差距很小。原生形态同进程直接传引用、无这层编解码,故延迟最低。绝对差值随入参大小变化,不做承诺值;可按"多一层 JSON 编解码"的量级理解。
问:.wasm 跨环境是否真的免重编?
答:是。沙箱单元以单一 .wasm 产物跨环境加载,没有运行环境家族 / 处理器架构 / 构建工具版本的对齐要求(这点与原生形态相反)。同一 .wasm 可在不同部署环境加载,业务逻辑无需按环境分别构建。前提是目标实例 capabilities.dynamic_plugins 为 true。
13. 能力边界
沙箱单元与所有扩展点 / 插件共享同一条能力边界(见 扩展点 SDK §6、自定义插件开发 §6):
沙箱单元只在控制面 / 治理面对请求与响应施加处置——校验、路由提示、缓存判定、脱敏、拦截、计量采集——不对结果内容做实质性的生成、加工或合成。内容生成始终由路由选定的上游模型完成。前置 Hook 的
rewrite与后置 Hook 的redact都限于治理面改写(如补充结构化路由提示、去除敏感片段),不构成对生成内容的实质性创作。
这条边界既是设计原则,也是注册时的硬性约束:试图在沙箱单元里替代上游做内容生成,不符合扩展点 SDK 的能力定义。§6 实战示例正是恪守此边界的范式——rewrite 只附加路由提示、不改用户消息,后置只采样、不改响应。
相关链接
- 概念与编排:三类治理中间件、执行位次、条件分支、短路语义与流式逐 chunk 处理。
- 扩展点 SDK 参考:前置 / 后置对称 Hook 模型、Hook 签名与生命周期、注册启停、编排约束与失败处置、能力边界声明。
- 自定义插件开发:插件模型、生命周期符号、执行时序、两种加载形态、注册与启停。
- 用 Go 编写原生插件:原生可加载单元的编写路径,与本页沙箱形态相对照。
- 用 TypeScript 编写插件:经 AssemblyScript 兼容子集产出同 ABI 的
.wasm。 - API 参考:管道编排与扩展点接口的完整端点契约。