Files
shaw 29eead917f feat(pluginapi): HostService 账号目录返回结构化只读元数据
纯宿主侧改动,向后兼容。

- 契约(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 全绿。
2026-09-20 11:26:19 +08:00
..

Sub2API 本地插件协议

本目录是插件开发者可以依赖的公开契约。v1/plugin.proto 和 v1/runtime.go 定义进程协议,v1/manifest.schema.json 定义包清单,docs/ 记录开发和发布规范。Provider 私有实现不应放入本目录。

开发文档

  • 开发指南:从运行时、配置到集成测试的完整流程。
  • UI Bridge:沙箱配置 UI 的消息结构和安全要求。
  • 包格式:清单、文件哈希、签名和版本规则。
  • 安全边界:进程权限、敏感数据和故障策略。

实体与运行方式

插件的交付实体是一个 .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。