纯宿主侧改动,向后兼容。 - 契约(pkg/pluginapi/v1/plugin.proto,已重新生成):ListAccounts 增补 repeated AccountInfo accounts(保留 account_ids 向后兼容),新增 metadata_json 只读快照字段。HostServiceAPIVersion 1→2(InitHostServices 运行时协商,加法式, 旧调用方忽略新字段即可)。 - 权限边界:可见范围由清单能力经 pluginCapabilityAccountScopeGrants 推导, ListAccounts 与 ResolveOutboundIdentity 均以该范围为权威边界。 - metadata_json 采用 denylist:剥离 Credentials 与含反向引用的关联对象 Groups/AccountGroups(避免 encoding/json 循环引用),其余字段自动透传, 数据模型新增非机密字段无需改宿主。 测试:范围/元数据/无凭据泄漏用例、denylist 反射护栏 + 循环引用安全断言、纵深过滤用例; build/vet/gofmt、-tags unit 与 -race 全绿。
Sub2API 本地插件协议
本目录是插件开发者可以依赖的公开契约。v1/plugin.proto 和 v1/runtime.go 定义进程协议,v1/manifest.schema.json 定义包清单,docs/ 记录开发和发布规范。Provider 私有实现不应放入本目录。
开发文档
实体与运行方式
插件的交付实体是一个 .s2plugin 文件,本质上是带清单、签名、独立可执行文件和静态 UI 的 ZIP 包。管理员在独立的插件管理页手动上传,Sub2API 不从网络自动下载插件,也不要求 Docker。
启用后,Sub2API 以子进程方式拉起当前操作系统和 CPU 架构对应的二进制,通过本机 gRPC 流传递请求与响应。插件进程退出时会随 Sub2API 清理;停用时先停止接收新请求,再等待正在处理的请求结束。
多实例部署不要求共享插件目录。宿主会在数据库保存已验签的原始插件包,各实例缺少本地文件时会重新验签和解包,并周期性对齐启用状态、灰度比例和加密配置。所有实例必须连接同一数据库并使用相同的加密密钥。
独立进程是代码和发布边界,不是操作系统安全沙箱。插件拥有 Sub2API 服务用户所拥有的文件和网络权限,因此只应安装可信发布者的签名包。闭源二进制可提高源码分发门槛,但不能承诺无法反编译。
初期能力边界
当前只接受 openai.oauth.outbound_transport.v1:
- 仅匹配
platform=openai且account_type=oauth的上游 HTTP 请求。 - API Key 账号、其他 provider、OAuth 登录与 Token 刷新流程不进入插件。
- 插件建立真实的上游 HTTP/TLS 连接并返回原始 HTTP 响应。
- 命中插件的 OAuth WebSocket 账号会使用 Sub2API 现有 HTTP Bridge,不直接建立上游 WebSocket,避免绕过 v1 HTTP 插件协议。
- Sub2API 继续负责响应状态处理、SSE 解析、错误映射、用量统计、计费和下游输出。
- 灰度比例以账号 ID 稳定分桶,未命中的 OAuth 账号继续使用原有内置路径。
宿主服务(HostService)
HostService 是宿主经 go-plugin broker 反向暴露给插件的通用能力层,独立于具体插件类型,供有状态插件使用。它通过 TransportPlugin.InitHostServices 在运行时协商:宿主启动后把一个 broker 流 id 交给插件,插件用它拨号回宿主并获得 HostServiceClient。
- 可选且向后兼容:未实现
InitHostServices的旧插件返回Unimplemented,宿主静默跳过,转发能力不受影响。宿主服务有独立的HostServiceAPIVersion,新增能力不会改变传输契约版本,也不会使既有插件失效。 - 随进程回收:宿主服务实例与插件进程生命周期绑定,插件退出时 broker 关闭并自动
GracefulStop,无需插件手动清理。
当前提供的通用设施:
- 命名空间键值存储(KV):
KVGet/KVSet/KVDelete/KVList,为插件持久化跨请求、跨副本、跨重启的状态。存储由 Redis 支撑,因此多实例部署天然共享同一份状态。- 命名空间隔离:宿主根据服务该连接的运行时注入插件身份(
pluginKey),插件无法伪造,也无法读写其它插件的命名空间。 - 护栏:
namespace/key仅允许[A-Za-z0-9._-];单值上限 256 KiB;ttl_seconds为 0 表示不过期、正值有上限;KVList返回条数有上限。
- 命名空间隔离:宿主根据服务该连接的运行时注入插件身份(
新增宿主设施时,在 HostService 上追加 RPC 即可,无需改动传输契约或清单格式。
包结构
manifest.json
signature.json # 生产包必需
runtimes/linux-amd64/plugin
runtimes/linux-arm64/plugin
runtimes/windows-amd64/plugin.exe
ui/index.html
ui/assets/...
manifest.json 必须声明所有运行时和 UI 文件的 SHA-256。signature.json 使用受信任发布者的 Ed25519 私钥对 manifest.json 原始字节签名。官方 OpenAI Transport 公钥由宿主内置,第三方发布者公钥由部署者追加到 plugins.trusted_publishers。文件哈希由已签名清单保护。
插件默认保持停用。未签名包默认拒绝安装;plugins.allow_unsigned 只应用于开发者自己构建的本地调试包。
兼容性
清单必须同时声明:
requires.sub2api:允许的 Sub2API 语义化版本范围。requires.recommended_sub2api_version:建议使用的宿主版本。requires.tested_sub2api_versions:发布者实际验证过的宿主版本。plugin_protocol、transport_api、ui_bridge:三个独立协议版本。
宿主版本超出范围时,插件可以安装并查看,但保持“不兼容”状态且不能启用。版本在范围内但未列入已测试版本时,管理员必须再次确认才能启用。
UI 隔离与 Bridge
插件 UI 由包内静态文件实现,宿主使用只有 allow-scripts 权限的 sandbox iframe 加载。iframe 没有管理员 Token,也不能直接访问管理 API。宿主为每次打开配置页生成短时资源 URL 和独立 Bridge Token,并且同时校验消息来源窗口与 Token。
UI 可以发送以下消息。消息按语义分层,鉴权与副作用一致对应(读操作免二次验证,写/主动测试需要二次验证),任何插件都可复用,不针对具体插件定制:
| 消息 | 语义 | 二次验证 | 映射的插件 RPC |
|---|---|---|---|
config.load |
读取已保存配置 | 否 | 宿主数据库 |
config.save |
写入配置 | 是 | ValidateConfig + ApplyConfig |
config.test |
主动测试配置/连通性(可产生副作用) | 是 | TestConfig |
plugin.status |
读取运行时状态(无副作用) | 否 | Health(status_json) |
ui.resize / ui.notify |
仅 UI 交互 | — | — |
每个请求消息带 request_id,宿主以 <type>.result 返回结果。
- 配置整体使用 Sub2API 的密钥加密后存入数据库;运行中插件会先验证并应用新配置,数据库写入失败时恢复旧配置。
config.test的结果由插件 UI 自行展示(内联或经ui.notify),宿主不再对成功结果强制弹出提示,避免插件把它当作轻量状态轮询时刷屏。plugin.status是通用的只读状态通道:宿主经GET /admin/plugins/:id/status调用运行中插件的Health,返回{healthy, message, status_json}。status_json是插件自定义的不透明 JSON 快照(宿主不解析、不参与健康判定),插件必须以无副作用方式生成(不得应用配置、访问上游或触发探测),因此该端点只读、免二次验证。插件未运行时返回healthy=false且不含status_json。这样带状态面板的插件无需滥用config.test即可展示实时状态。
协议源码
v1/plugin.proto:稳定的进程间消息定义。v1/runtime.go:Go 插件进程启动入口和宿主客户端声明。v1/manifest.schema.json:manifest.json的 JSON Schema。
插件通过进程协议协作,不使用 Go 动态链接,也不要求插件与 Sub2API 使用相同编译器或共享内存 ABI。