灵渠 LingQu
中间件管道与扩展点 SDK

用 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 的生命周期符号:

导出符号签名何时调用职责
mallocmalloc(size:u32)->u32宿主写入入参前在 guest 线性内存中分配 size 字节,返回偏移指针
freefree(ptr:u32)free(ptr:u32,size:u32)宿主读完出参后释放 malloc 分配的缓冲
get_nameget_name()->u64加载时返回插件全局唯一标识;返回值为打包指针,指向标识字符串
initinit(config_ptr:u32,config_len:u32)->i32加载后、首次处理请求前一次接收配置 JSON,完成一次性初始化;成功返回 0
pre_hookpre_hook(input_ptr:u32,input_len:u32)->u64每次请求转发上游前前置 Hook:读入参 JSON、处置、返回打包出参 JSON
post_hookpost_hook(input_ptr:u32,input_len:u32)->u64每次上游响应返回后(或短路后)后置 Hook:读入参 JSON、处置、返回打包出参 JSON
cleanupcleanup()->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)返回码用 i32init / 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):

加载 get_name 读取插件标识; 加载 init 传入配置 返回 0; malloc 分配入参缓冲 写入前置入参 JSON; 调用 pre_hook in_ptr in_len; 返回打包 u64 指针与长度; 读取出参后 free 释放缓冲; malloc 写入后置入参 JSON 调用 post_hook; 返回打包 u64 指针与长度; 读取出参后 free 释放缓冲; 调用 cleanup 返回 0; 每请求 前置段; 内建段 上游推理; 每请求 后置段 流式下多次调用; 停机; 宿主 运行时; 沙箱单元 guest;

流式响应下,pre_hook 仍只在请求转发前调用一次;post_hook 中标记逐 chunk 的部分按 chunk 多次调用,request_id 全程一致、末块 is_lasttrue(见 扩展点 SDK §2.3)。initcleanup 各只在加载、卸载时调用一次,与单次请求无关。

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_idapi_key_idrequest(model/messages/stream)、routing(complexity/candidate_model)、metadata(received_at/node_id)、context
前置出参action(continue/rewrite/short_circuit/reject)、request(rewrite 时改写后的入站请求)、short_circuit_responsereject_reason(code/message)、context(向配对后置传递)
后置入参phase="post"、request_idapi_key_idresponse(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
}

跑通直通骨架后,再逐步把 actioncontinue / 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(此处省略 memoryget_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
GoTinyGotinygo build -o plugin.wasm -target=wasi
TypeScriptAssemblyScript(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_pluginsfalse,私有化部署需先联系部署方切换为「支持动态加载」的构建参数,再注册;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_policyfail_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_pluginstrue

13. 能力边界

沙箱单元与所有扩展点 / 插件共享同一条能力边界(见 扩展点 SDK §6自定义插件开发 §6):

沙箱单元只在控制面 / 治理面对请求与响应施加处置——校验、路由提示、缓存判定、脱敏、拦截、计量采集——不对结果内容做实质性的生成、加工或合成。内容生成始终由路由选定的上游模型完成。前置 Hook 的 rewrite 与后置 Hook 的 redact 都限于治理面改写(如补充结构化路由提示、去除敏感片段),不构成对生成内容的实质性创作。

这条边界既是设计原则,也是注册时的硬性约束:试图在沙箱单元里替代上游做内容生成,不符合扩展点 SDK 的能力定义。§6 实战示例正是恪守此边界的范式——rewrite 只附加路由提示、不改用户消息,后置只采样、不改响应。

相关链接

  • 概念与编排:三类治理中间件、执行位次、条件分支、短路语义与流式逐 chunk 处理。
  • 扩展点 SDK 参考:前置 / 后置对称 Hook 模型、Hook 签名与生命周期、注册启停、编排约束与失败处置、能力边界声明。
  • 自定义插件开发:插件模型、生命周期符号、执行时序、两种加载形态、注册与启停。
  • 用 Go 编写原生插件:原生可加载单元的编写路径,与本页沙箱形态相对照。
  • 用 TypeScript 编写插件:经 AssemblyScript 兼容子集产出同 ABI 的 .wasm
  • API 参考:管道编排与扩展点接口的完整端点契约。

On this page