Documentation
¶
Overview ¶
Package wire 执行沙箱的**跨进程调用协议**。
它解决什么 ¶
沙箱档位(Level)答的是「能力面多强」,预算支持面(Capable)答的是「资源兜底 兑现哪几项」。但这两者都还缺一块:**程序一旦出宿主进程,工具回调就够不着了**。
进程内执行(goja VM 跑在宿主进程里)时,程序里的 `host.<tool>()` 是一次普通 Go 函数调用;程序出宿主进程后,它变成一次**跨进程 IPC**。这段 IPC 若由每个 后端各写一遍,就会有一堆互不兼容的私有协议——而协议恰恰是最不该私有的东西 (它决定了「子进程里的程序怎么跟父侧说话」,换个后端就得换一套)。
故本包把协议**收成一份**:帧格式、请求/响应语义、父侧服务端循环、子侧客户端。 后端只实现「怎么起这个进程」与「怎么执行这个程序」。
帧格式 ¶
长度前缀 + JSON:
┌───────────────┬──────────────────────────────┐ │ 4B 大端长度 N │ N 字节 JSON 负载 │ └───────────────┴──────────────────────────────┘
**不用行分隔**——工具输出可能含换行,行分隔会被内容切断(一条多行的日志就能 把后面的帧全毁掉)。长度前缀对负载内容零假设。
六条不变量 ¶
- **版本响亮失败**:`v` 不匹配即拒,不静默错配。错配的症状是「字段读成了 另一个意思」——那是最难查的一类,故不静默。
- **失败分类跨进程同源**:`kind` 取 `guardx/toolfail` 的分类,**不是**自由 字符串。分类一旦退化成字符串,父侧就分不清「可重试」与「永久拒绝」, 重试策略随之瞎掉。`toolfail` 与 `sandbox` 同在 guardx——**同层引用, 无跨仓依赖**。
- **预算的权威在父侧**:调用数与输出字节数由**父侧**记账。子进程只是执行者 ——若子进程自己记账,它可以谎报自己没超预算。
- **在途调用有硬上限**:总量预算(MaxCalls)拦不住「瞬间发起十万次并发 调用」——那时它们都还没返回,父侧已被 goroutine 淹没。故父侧另设**在途 并发上限**,超出即回「超限」。这是**可用性**约束,与 MaxCalls 的**配额** 约束是两件事。
- **取消传播**:父侧 ctx 取消 → 取消在途调用 + 通知子进程终止(否则子进程 继续跑,父侧却已经返回了)。
- **响应按 `id` 关联,不按到达顺序**:程序里多个 await 是并发的,返回顺序与 请求顺序无关。
谁该用它 ¶
后端实现方(如 agent-harness 的 ptc 子进程后端):起进程、连管道、跑程序; **协议本身用本包的 Transport / Server / Client**。
宿主:只声明「这次执行要哪一档 + 哪些预算项」,不碰协议。
Index ¶
Constants ¶
const DefaultMaxInflight = 64
DefaultMaxInflight 在途调用数默认硬上限。
取 64:既能吃住「程序内并发几十次工具调用」的常见形态,又不至于让单个失控 程序把父侧的 goroutine 堆爆。宿主可调。
const MaxFrameBytes = 16 << 20
MaxFrameBytes 单帧负载上限(字节)。
长度前缀是 4 字节,理论上限 4GB——但那是**攻击面**:一个坏的长度前缀就能让 接收方预分配几 GB。故设上限,超限即拒。
默认 16MB 覆盖了「工具返回一个大结果」的常见情形(宿主侧另有 MaxBytes 预算 逐次把关,见不变量 3)。
const Version = 1
Version 协议版本。
**不匹配即响亮失败**(不变量 1):静默错配的症状是「字段读成了另一个意思」—— 那是最难查的一类错误,故版本对不上直接拒,不试图「兼容」。
Variables ¶
var ErrInflightLimit = errors.New("wire: 在途调用数超过硬上限")
ErrInflightLimit 在途调用数超过硬上限(不变量 4)。
与「配额超限」(MaxCalls)是**两件事**:配额是「总共能调多少次」,在途上限是 「同时能挂多少个」。后者拦的是「瞬间发起十万次并发调用」——那时它们都还没 返回,父侧已被 goroutine 淹没。
Functions ¶
Types ¶
type Budget ¶
type Budget struct {
// MaxCalls 总调用数上限。0 = 不限。
MaxCalls int
// MaxBytes 累计输出字节上限。0 = 不限。
MaxBytes int
// contains filtered or unexported fields
}
Budget 父侧记账的预算(不变量 3)。
与 `sandbox.Budget` 的区别:那是「宿主对**后端**的要求」(哪些项要兜住), 这里是「父侧**实际记着**的账」(已用掉多少)。两者不是一回事——要求的是 上限,实际的是消耗。
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client 子侧客户端:程序在子进程里用它向父侧请求工具调用。
与 Transport 的方向相反 ¶
Transport 是**父侧**用的(父侧发起调用、收响应);Client 是**子侧**用的 (子侧发起调用、收响应)。两者共用同一套帧格式与「按 id 关联」语义—— 协议只有一份,两个方向只是角色不同。
取消传播(不变量 5) ¶
父侧 ctx 取消 → 父侧关闭传输端 → 子侧读到 EOF → 本客户端的在途调用全部失败。 子侧不需要额外的取消通道:连接断本身就是最强的取消信号。
func (*Client) CallWithID ¶
CallWithID 用指定 id 发起调用。
type Handler ¶
Handler 父侧处理一次工具调用。
返回的 Response 若 OK=false,**Kind 必须是 toolfail 的分类**(不变量 2)—— 分类一旦退化成自由字符串,子进程侧就分不清「可重试」与「永久拒绝」。
Handler 返回的 error 会被转成「分类 = unknown」的失败帧;能归类的失败请自己填 Response.Kind(宿主最清楚那次失败属于哪一类)。
type Request ¶
type Request struct {
// V 协议版本。
V int `json:"v"`
// ID 调用标识。响应按它关联(不变量 6),故**必须非空且在一次执行内唯一**。
ID string `json:"id"`
// Tool 工具名。
Tool string `json:"tool"`
// Args 工具参数原文(宿主侧按自己的工具面解析)。
Args string `json:"args"`
// DeadlineMS 本次调用的期限(毫秒)。0 = 不限期。
//
// 由**子进程**声明、**父侧**执行:父侧是记账与取消的权威(不变量 3/5),
// 子进程说了不算,父侧必须自己兜住。
DeadlineMS int64 `json:"deadline_ms,omitempty"`
}
Request 一次工具调用请求帧(子进程 → 父侧)。
Args 是**原始参数串**(通常是 JSON 文本),不解析成结构体——协议不该替宿主 决定工具参数长什么样(那是宿主的工具面知识)。
type Response ¶
type Response struct {
// V 协议版本。
V int `json:"v"`
// ID 对应的请求标识(不变量 6)。
ID string `json:"id"`
// OK 本次调用是否成功。
OK bool `json:"ok"`
// Value 成功时的结果文本(编码由宿主工具面定)。
Value string `json:"value,omitempty"`
// Kind 失败时的分类(**toolfail.Class**,不变量 2)。
//
// 空串只允许在「不是工具失败」的场合(如协议层错误、父侧拒绝)——那类错误由
// 父侧直接终止执行域,不走正常响应路径。
Kind string `json:"kind,omitempty"`
// Error 失败时的可读原因(给人看,不参与判定)。
Error string `json:"error,omitempty"`
}
Response 一次调用响应帧(父侧 → 子进程)。
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server 父侧服务端:读子进程发来的调用请求,执行后回帧。
父侧是**应答方**(谁发起、谁应答) ¶
程序在子进程里(`host.<tool>()` 由程序发起),故**子侧是发起方**(Client), 父侧是应答方(Server)。方向搞反会导致记账记在错误的一侧—— 配额该在**接受调用**时扣,不是谁发起谁扣。
它同时是**预算的权威**(不变量 3) ¶
调用数与输出字节数由**父侧**记账。子进程只是执行者——若子进程自己记账, 它可以谎报自己没超预算。故 Budget 挂在 Server 上,由父侧逐次扣减。
并发处理 + 在途硬上限(不变量 4) ¶
每个请求在独立 goroutine 里处理,故一次慢调用不阻塞其他调用。总量预算 (MaxCalls)拦不住「瞬间发起十万次并发调用」——那时它们都还没返回,父侧已被 goroutine 淹没。故另设**在途并发上限**,超出即回「超限」帧,**不排队等**。
这是**可用性**约束,与 MaxCalls 的**配额**约束是两件事。
取消传播(不变量 5) ¶
父侧 ctx 取消 → 关闭连接 → 子侧读到 EOF → 子侧全部在途调用失败。 不需要单独的取消帧:**管道关闭本身就是最强的取消信号**。
func NewServer ¶
func NewServer(rw io.ReadWriter, h Handler, opts ...ServerOption) *Server
NewServer 构造父侧服务端。
type ServerOption ¶
type ServerOption func(*Server)
ServerOption Server 的构造选项。
func WithMaxInflight ¶
func WithMaxInflight(n int) ServerOption
WithMaxInflight 设在途调用硬上限(不变量 4)。<=0 用默认值。