开发者文档/接口参考

Wire Protocol v1 详细参考

从分帧与握手到全部消息、身份、取消、订阅和退出,独立实现 Backend 所需的协议细节。

核对日期:2026 年 9 月 11 日

本页目录

适用范围与职责#

本页描述当前 macOS Host 的 Wire Protocol v1,足以作为其他语言实现 Backend 的接入参考。消息结构见JSON Schema,方法和事件的业务类型见Service Contract。Manifest v1、Wire v1、服务语义版本、SDK 0.3 是不同的版本维度。

Wire 是 Host ↔ Backend 的双向连接。WebView 使用 SDK 的 Bridge 适配层,不应自行读取 UDS 凭据。协议规定公开能力和 Host 生命周期连接;插件内部 UI ↔ Backend 的技术、变量命名、模块划分仍由作者决定,见内部通信

下文 JSON 均为帧的载荷,发送前还要添加长度前缀。示例 ID、时间戳和会话字段仅用于说明,应替换为本次连接的实际值。

进程环境与连接凭据#

Host 以插件根目录为工作目录启动受管 Backend,并提供:

变量含义
TROVE_SOCKET_PATHHost 的 Unix Domain Socket 路径,Backend 主动连接它
TROVE_SESSION_TOKEN本次进程会话的认证令牌;不是 Curio ID
TROVE_CURIO_ID / TROVE_CURIO_VERSION运行中的插件身份和包版本
TROVE_INSTANCE_ID可选,关联 UI 实例;不代表后端由该窗口拥有
TROVE_DATA_DIR / TROVE_CACHE_DIRHost 为该 Curio 分配的数据和缓存目录
TROVE_PROTOCOL_MIN / TROVE_PROTOCOL_MAX当前均为 1

手动附加开发进程时,Rust Kit 可从 TROVE_DEV_SESSION_FILE 指向的 .trove/dev-session.json 读取 socketPath/sessionToken;先由 trove dev 创建对应 external 会话。开发文件不是可复用的安装配置。不要把令牌写入日志、源码或安装包。

一份会话令牌只允许一个活动连接。Host 依据令牌绑定 Curio 身份,hello 中不允许自行选择其他插件。重启、禁用、卸载或开发会话结束后,旧会话不可继续使用。

分帧与字节规则#

text
[length: 4 bytes, unsigned big-endian][payload: length bytes, UTF-8 JSON]
00 00 00 16 7b 22 76 22 3a 31 2c 22 74 79 70 65 22 3a 22 72 65 61 64 79 22 7d
             {"v":1,"type":"ready"}

长度只统计 JSON 的 UTF-8 字节,不包含前缀,也不是字符串字符数。空帧无效,当前最大载荷 8,388,608 字节(8 MiB)。超限应立即拒绝,不要先按不可信长度分配内存。Node 编码示例:

js
function encode(message) {
  const payload = Buffer.from(JSON.stringify(message), "utf8");
  if (!payload.length || payload.length > 8 * 1024 * 1024) {
    throw new Error("Invalid frame length");
  }
  const header = Buffer.alloc(4);
  header.writeUInt32BE(payload.length);
  return Buffer.concat([header, payload]);
}

接收器必须累计读取:先收满 4 字节,再收满声明的载荷;一次 read 可能只有半个头,也可能包含多个完整帧。循环消费已完整的帧,把剩余部分留给下一次 read。UTF-8 JSON 必须解析为对象,并校验 v/type 和具体消息字段。帧中途 EOF 视为断连,不应把残片作为下一会话的输入。

协议不经过 stdout,不是按行 JSON,也没有 HTTP 头。写入时应串行排队,防止多个任务的头和载荷交错,并处理写入背压。

hello → welcome → ready#

连接成功后,Backend 必须首先发送 hello:

json
{
  "v": 1,
  "type": "hello",
  "client": "backend",
  "sessionToken": "REPLACE_WITH_HOST_SESSION_TOKEN",
  "protocol": {
    "min": 1,
    "max": 1
  },
  "features": [
    "call",
    "cancel",
    "subscribe"
  ],
  "implementation": {
    "name": "text-tools",
    "version": "1.0.0",
    "language": "rust"
  }
}
字段规则
v当前报文格式,固定 1
client必须为 backend
sessionToken使用 Host 颁发的值;至少 16 字符只是结构要求,不代表任意长字符串可认证
protocol.min/max支持的闭区间,正整数,min ≤ max,须包含 Host 支持的版本
features不重复的字符串数组;call 必需,cancel 和 subscribe 按实际支持声明
implementation可选实现元数据,用于诊断,不作为认证依据

Host 验证凭据与版本交集,返回 welcome:

json
{
  "v": 1,
  "type": "welcome",
  "protocol": 1,
  "host": {
    "name": "Trove",
    "version": "0.1.0"
  },
  "session": {
    "sessionId": "session-example",
    "curioId": "dev.example.text-tools",
    "curioVersion": "1.0.0",
    "instanceId": null
  },
  "features": [
    "call",
    "cancel",
    "subscribe"
  ],
  "limits": {
    "maxFrameBytes": 8388608,
    "maxInFlight": 256
  }
}

protocol 是选定版本。features 只保留双方支持的 call/cancel/subscribe;不要因为自己声明了某功能就假设已获支持。session 给出 Host 认证后的身份,instanceId 可为 null。limits 是该连接应遵守的上限。

Backend 初始化完成并能处理请求后发送:

json
{
  "v": 1,
  "type": "ready"
}

Host 收到 ready 后才发布连接并放行等待中的业务调用。不要把“进程已创建”当作“服务可用”。握手失败时 Host 尽力发送 protocolError 后断开;它可能来不及送达,EOF 也必须处理。

当前网关对整个 hello/welcome/ready 握手另有 10 秒上限;受管进程还有 Manifest 的 startupTimeoutMs(默认 10 秒)。仅增大 startupTimeoutMs 不会取消网关的 10 秒限制。

请求、响应与关联#

json
{
  "v": 1,
  "type": "request",
  "id": "backend-call-1",
  "service": "dev.example.text-tools",
  "method": "uppercase",
  "params": {
    "text": "hello"
  },
  "options": {
    "timeoutMs": 5000
  }
}

request 可由任一端发送。service 为 host.*、公开服务 ID,或仅指向调用者自己 Backend 的 @self。跨插件不能用 @self 访问另一个插件,也不查找 UI 方法。

字段规则
id非空字符串或有符号 64 位整数;建议字符串,避免 JS 数字精度问题
service / method非空字符串;提供者应拒绝未知服务或方法
params任意 JSON;省略时当前接收实现默认 {},推荐显式发送并按业务契约验证
options.timeoutMs可选非负整数毫秒;0 会立即超时,业务调用应给正值
context可选;由 Host 生成,下文说明其信任边界

成功与失败响应分别如下,result/error 必须恰有一个,result: null 是有效成功结果:

json
{
  "v": 1,
  "type": "response",
  "id": "backend-call-1",
  "result": {
    "text": "HELLO"
  }
}
json
{
  "v": 1,
  "type": "response",
  "id": "backend-call-1",
  "error": {
    "code": "request.invalid_params",
    "message": "text must be a string",
    "data": {
      "field": "text"
    },
    "retryable": false
  }
}

error.code、message 必需,data 可选,retryable 默认 false。Wire error 不保证有 traceId;使用 Host 给出的 context.traceId 关联诊断。SDK 可能额外提供 traceId。retryable 与 Contract 的 idempotent 都不会自动触发重试。

保留收到的 ID 和其类型。连接内各方向的未完成操作不能重复使用 ID;Host 转发时重写请求 ID,并在回程恢复调用方的 ID。响应允许乱序,因此必须用 pending map 关联,不能按到达顺序配对。调用已超时/取消后的迟到响应会被丢弃。

身份、父调用与双向请求#

以下示意消费者 A → 提供者 B → host.fs。B 收到 Host 转发的请求:

json
{
  "v": 1,
  "type": "request",
  "id": "host-provider-1",
  "service": "dev.example.text-tools",
  "method": "uppercaseFile",
  "params": {
    "path": "/tmp/input.txt"
  },
  "options": {
    "timeoutMs": 5000
  },
  "context": {
    "caller": {
      "curioId": "dev.example.consumer",
      "instanceId": "ui-example",
      "endpoint": "ui"
    },
    "traceId": "trace-example",
    "parentRequestId": "host-provider-1",
    "deadlineUnixMs": 1900000005000,
    "hop": 1
  }
}

B 使用新的请求 ID 反向请求 Host,携带收到的 context:

json
{
  "v": 1,
  "type": "request",
  "id": "reverse-1",
  "service": "host.fs",
  "method": "readText",
  "params": {
    "path": "/tmp/input.txt"
  },
  "context": {
    "caller": {
      "curioId": "dev.example.consumer",
      "instanceId": "ui-example",
      "endpoint": "ui"
    },
    "traceId": "trace-example",
    "parentRequestId": "host-provider-1",
    "deadlineUnixMs": 1900000005000,
    "hop": 1
  }
}
context 字段含义与边界
callerHost 在当前这一跳认证的调用者;curioId/instanceId 可为空,endpoint 表示 ui/backend/Host 入口
traceId同一父调用链的追踪 ID
parentRequestIdHost 发给当前 Backend 的活跃请求 ID,用来验证反向调用的父关系
deadlineUnixMsHost 控制的绝对截止时间,毫秒
hop已经过的路由跳数;超过 16 报 call.hop_limit

Host 仅在同一 Backend 连接上找到仍活跃的父请求时,继承其追踪、期限、跳数和取消链;不信任 Backend 自填的 caller、deadline 或 hop。找不到父请求时视为独立调用,不能伪造一条已结束的链。

A 调用 B 后,B 再调用 Host 的身份是 B。 父关系延续不等于身份冒充;例如 Host storage 按 B 的命名空间执行,B 的 @self 也指向 B。B 收到第一跳时可检查 A 的身份并自行实施业务授权。

读取循环必须在处理请求期间继续接收反向响应和 cancel。不要在唯一读取线程 await 一个依赖 Host 响应的业务处理函数,否则会死锁。Rust Context::request 已处理响应关联与父上下文转发。

总期限、取消与重试#

调用期限覆盖 SDK 连接、提供者冷启动和执行,默认按方法 Contract,未配置时通常 30 秒;文件选择对话框为 5 分钟。SDK 的显式 timeoutMs 范围为 1–2,147,483,647 毫秒;Wire 的整数范围更宽,不代表任意长请求都可用。子调用只可缩短父调用剩余期限。

超时、AbortSignal、调用方断连会结束等待,并尽力向正在执行的 Backend 发送:

json
{
  "v": 1,
  "type": "cancel",
  "id": "host-provider-1"
}

cancel 的 id 对应这一条连接上原请求的 id,不是 traceId。cancel 没有单独确认响应。提供者应通知工作任务停止;任务若仍要返回响应,可使用原请求 ID 和 request.cancelled。Host 已结束的等待不因迟到结果而恢复。

取消是协作式的,不会回滚文件写入等已完成副作用,不保证强行终止阻塞 FFI。把 CPU/FFI 放到合适的工作线程,设置可中断点并清理资源。单次调用取消也不应停止其他调用共享的提供者启动。

断连后的在途调用失败,不自动重放;重新订阅和业务重试由调用方决定。避免对有副作用的方法无条件重试。

订阅、事件与释放#

协商 subscribe 后,订阅使用独立的请求 ID,topic 对应契约中的 events 键:

json
{
  "v": 1,
  "type": "subscribe",
  "id": "subscribe-1",
  "service": "dev.example.text-tools",
  "topic": "progress",
  "params": {
    "taskId": "preview"
  },
  "options": {
    "timeoutMs": 5000
  }
}

提供者分配当前连接内唯一的 subscriptionId 并确认:

json
{
  "v": 1,
  "type": "response",
  "id": "subscribe-1",
  "result": {
    "subscriptionId": "provider-sub-1"
  }
}

随后发送事件:

json
{
  "v": 1,
  "type": "event",
  "subscriptionId": "provider-sub-1",
  "event": "progress",
  "seq": 1,
  "data": {
    "progress": 0.5
  }
}
字段规则
subscriptionId已确认的订阅 ID,不是 subscribe 的请求 ID
event业务主题名称,与该订阅语义一致
seq正整数;从 1 开始持续递增,重复或倒退会导致协议错误
data按事件 payload Schema 的 JSON 值

Host 在消费者和提供者之间转换 subscriptionId,向消费者维护自己的连续 seq。提供者应先发送确认,再发送事件。Host 在确认转发期间最多缓冲 128 个事件,不能依赖无界早到事件队列。

params 是订阅筛选条件,实现筛选是提供者的职责。Rust emit_for 按调用者、服务、主题和 JSON 参数完全相等匹配。coalesce: latest 目前只是声明,Host 不保证自动合并。事件没有历史回放或断线续传。

释放订阅使用新的请求 ID:

json
{
  "v": 1,
  "type": "unsubscribe",
  "id": "unsubscribe-1",
  "subscriptionId": "provider-sub-1"
}
json
{
  "v": 1,
  "type": "response",
  "id": "unsubscribe-1",
  "result": {
    "closed": true
  }
}

Host 检查订阅归属后释放路由并通知提供者;消费者收到 closed 不表示远端清理确认已被 Host 等待。关闭必须幂等,未知或已释放订阅可返回 closed: false。调用方断连也会释放其订阅。

提供者断连时,Host 向消费者发送保留的 subscription.closed 结束通知(data.code 为 provider.disconnected),再移除订阅。SDK 将其呈现为 subscription.closed Promise 的结束原因,不作为普通业务事件。

进程结束与窗口独立性#

Host 停止提供者时发送:

json
{
  "v": 1,
  "type": "shutdown",
  "reason": "host.quit",
  "gracePeriodMs": 2000
}

reason 是诊断字符串,不应硬编码少数值才允许退出;gracePeriodMs 为允许的清理时间。停止接受新业务工作,取消任务、释放订阅与资源、刷新日志,最后发送:

json
{
  "v": 1,
  "type": "goodbye"
}

随后关闭连接并退出。超过宽限期时,Host 可终止受管进程组及其子进程。手动启动的 external 开发 Backend 也收到 shutdown,但 Host 不强行杀死它;它必须自行遵守退出协议。连接 EOF 也应触发清理。

公开 Backend 的生命周期独立于已安装 UI:关闭窗口不发送停止公开提供者的 shutdown;默认可继续服务和输出日志。无公开服务的 on-demand 私有后端仍随窗口关闭而停止。开发窗口关闭会结束开发会话及该次 Backend。Host 退出、禁用、卸载或启用的空闲回收会结束公开提供者。

默认 idleTimeoutMs 为 0,不做空闲回收;调用和订阅计入活动。重复获取引用不会新建进程,也不赋予调用方销毁共享进程的权力。

错误与容量限制#

json
{
  "v": 1,
  "type": "protocolError",
  "error": {
    "code": "protocol.invalid_message",
    "message": "Response requires exactly one of result or error",
    "retryable": false
  }
}

protocolError 不含请求 ID,表示连接级协议失败;业务失败应返回 response.error。连接发生致命错误时应清理所有 pending/订阅并断开。错误帧不保证送达,必须处理直接 EOF。

代码/限制处理方式
protocol.invalid_json / invalid_message检查编码、必需字段、版本和消息顺序
protocol.unsupported_version对齐握手版本范围
protocol.frame_too_large缩小载荷;应用层自行分页或分块
provider.start_timeout / provider.not_ready检查启动、握手和 ready 时机
provider.disconnected拒绝在途操作,释放订阅;后续请求可由 Host 重新启动
service.not_found / method_not_found / version_mismatch核对注册契约、方法和 requires 兼容范围
request.invalid_params / request.timeout / request.cancelled分别处理业务输入、期限和取消
request.too_many_in_flight / transport.backpressure限制并发及生产速率,不积累无限队列
帧上限8 MiB JSON 载荷
在途上限每提供者 256;协商值见 welcome.limits
网关连接上限当前 128
输出队列Host:512 条且 16 MiB;Rust Kit:256 条且 16 MiB,双重上限
订阅初始化缓冲Host 每订阅最多 128 事件

当前默认 Host 校验报文、已注册的方法/主题及依赖版本,但没有默认开启任意业务 params/result/event 的完整 JSON Schema 校验。不要把 TS 类型或 Host 路由当作输入验证。Rust Kit 依据类型反序列化输入,其他语言应提供同等业务验证。

日志不是协议消息#

stdout/stderr 保持普通文本输出,由 Host 收集到本次 Run 的有界内存日志。开发时可在终端和调试控制台查看,安装后在同一个调试控制台查看;不持久化、不提供历史查询。UI console、Backend stdout/stderr 和 Host 生命周期消息分流展示,见调试与日志

Host 只能读取进程已写出的字节。C/C++ stdout 的块缓冲可能导致操作时没有输出、退出时集中出现;需要在源端开启行缓冲或显式 flush。关闭后的尾部日志不意味着操作刚发生。不要为得到日志而往 Wire 发送未定义消息。

实现与互操作验收#

可下载完整 Rust fixture测试用例,放入独立 Rust starter 验证。fixture 使用模板已有依赖,覆盖成功、结构化失败、取消、反向调用和事件。它的 host.test 仅由测试器提供,不能作为产品 Host 能力使用。

sh
trove test
trove test --cases protocol-cases.json

基本模式验证握手、分片、八个并发未知方法错误以及 shutdown/goodbye。完整 cases 才验证给定业务操作、独立并发、执行取消、父上下文回传、订阅事件与释放;实际 Host 的身份路由还需集成测试。验证通过不等于任意方法都正确,也不替代业务测试。

实现新的语言适配时,按以下顺序建立可观察的行为:

  1. 有界帧解码器,处理分片、粘包、非法 JSON、超大帧和 EOF。
  2. hello/welcome/ready 状态机,ready 前不接收业务请求。
  3. 独立读取循环、并发处理函数、双向 pending map、唯一请求 ID。
  4. 截止时间和协作取消,迟到响应与断连资源清理。
  5. 需要事件时实现订阅确认、筛选、递增序号及幂等释放。
  6. shutdown 宽限清理、goodbye、子进程退出和日志 flush。

最后用真实 Host 测试:从另一 Curio 获取引用,确认冷启动成功且没有窗口;关闭提供者窗口后再次调用;在开发和安装两种运行方式查看即时日志。