Files
cpa-plugin/docs/modules/dev.md
T

611 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件运行时与 CPA 集成开发文档
## 1. 定位
本模块是 `cpa-ext` 与 CLIProxyAPI(CPA)之间的工程适配层。它负责:
- native dynamic library ABI
- JSON RPC envelope 和方法分发;
- capability 注册与 schema 协商;
- CPA callback 到 Core/采集模块的转换;
- 配置注册、热重配、运行时切换和 shutdown;
- Management API 与静态资源接入;
- 构建、动态库检查、安装和宿主集成测试。
它必须保持很薄。任何计价公式、余额规则、SQL、聚合算法或路由策略都不应写进 C ABI 入口或 RPC dispatcher。
生产安全前提见 [security.md](security.md),安装/升级/恢复见 [operations.md](operations.md),所有 ABI、故障、性能和 soak 证据见 [test-plan.md](test-plan.md)。
## 2. 当前兼容基线
设计时核对的源码版本:
| 项目 | revision |
| --- | --- |
| `CLIProxyAPI` | `f43aad7637ad813745bf7d341acb5663617570c5` |
| `cpa-plugin-key-billing` | `25b534ae386f830f537cca9215cff5586e630b3a` |
| `cpa-usage-keeper` | `d62cad3f345ae574089a14a4ac75cca023c7ead6` |
该 CPA revision 的契约:
- minimum host baselineCLIProxyAPI `v7.2.130`(上述 exact tag/revision);
- Native ABI`1`
- RPC schema:最高 `3`
- 宿主实际调用的 RPC lifecycle`plugin.register``plugin.reconfigure``plugin.shutdown` 目前只存在方法常量,没有宿主调用点;
- CPA module 声明 Go `1.26.0`,当前 cpa-ext module 声明 Go `1.24`;构建矩阵必须固定实际 Go/toolchain 版本并纳入双 runtime soak,不能只比较 `go.mod` 最低版本;
- stream schema 3 的 payload chunk 不再重复携带 request body,必须在 header-init 或请求 hook 缓存关联所需的最小信息。
源码 checkout 才是最终规范。每次升级 CPA 都必须重新核对:
- `CLIProxyAPI/sdk/pluginabi/types.go`
- `CLIProxyAPI/sdk/pluginapi/types.go`
- `CLIProxyAPI/internal/pluginhost/rpc_schema.go`
- `CLIProxyAPI/internal/pluginhost/rpc_client.go`
- 相同 capability 的官方 example 和测试。
ABI 版本与 RPC schema 版本相互独立,不能因为两者当前分别是 1 和 3 就混成一个“插件版本”。
## 3. 目标 capability
完整产品需要以下 capability,但开发时只声明已经实现并测试的方法:
| Capability | RPC 方法 | 适配到内部能力 |
| --- | --- | --- |
| `frontend_auth_provider` | `frontend_auth.identifier``frontend_auth.authenticate` | 插件自管下游 Key 认证 |
| `frontend_auth_provider_exclusive` | 注册字段,无独立方法 | 生产模式阻止其他认证 provider 绕过 Core |
| `request_interceptor` | `request.intercept_before``request.intercept_after` | 准入、Request 建立、Execution attempt |
| `request_lifecycle_plugin` | `request.complete` | 成功/失败/拒绝/取消终态与清理 |
| `response_before_translator` | `response.normalize_before` | 读取接近 provider 原始语义的 Usage |
| `response_interceptor` | `response.intercept_after` | 非流式响应与 RequestID 关联 |
| `response_stream_interceptor` | `response.intercept_stream_chunk` | 流式 response ID/Usage 与 RequestID 关联 |
| `usage_plugin` | `usage.handle` | CPA 标准 Usage、身份、TTFT、Latency、失败交叉校验 |
| `scheduler` | `scheduler.pick` | Key/账户绑定到候选 AuthID |
| `management_api` | `management.register``management.handle` | 管理 JSON API、静态 shell、用户只读资源 |
不需要声明:model registrar/provider、auth provider、executor、translator、model router 等。`cpa-ext` 初期不替代 CPA 的 Codex 执行器或协议翻译器。
### 3.1 分阶段声明
建议 capability 开通顺序:
1. 当前骨架:`usage_plugin`
2. 采集闭环:request interceptor + lifecycle + response hooks
3. 自管 Keyfrontend auth,并在生产测试完成后打开 exclusive;
4. 账户绑定:scheduler
5. 管理与 UImanagement API。
未实现的方法不能返回空成功来假装支持。注册字段为 true 后,该 capability 隐含的所有方法都必须有结构化响应和测试。
## 4. Native ABI 入口
Go c-shared 入口放在 `cmd/cpa-ext/main.go`,使用 `//go:build cshared`。普通测试使用 `main_stub.go``//go:build !cshared`,确保 `go test ./...` 不要求 CGO。
`cliproxy_plugin_init` 必须严格按当前 CPA C 声明填充:
```text
abi_version
call
free_buffer
shutdown (optional but cpa-ext 必须实现)
```
硬规则:
- `plugin == nil` 时返回非零;
- 每次 call 开始先把 response ptr/len 初始化为零;
- 请求字节若要离开当前函数必须先复制到 Go-owned memory
- 返回值使用 C allocator 分配,且只由插件自己的 `free_buffer` 释放;
- host callback 返回的 buffer 必须由 host 的 free function 释放;
- 不向 C 返回 Go heap pointer
- 不在 ABI 层使用 `os.Exit``log.Fatal` 或故意 panic
- recovered panic 转换成脱敏 error envelope;宿主自身也会 fuse panic 插件,但插件仍应保护 Core 边界。
入口只做:byte copy、envelope marshalling、App 指针读取、C buffer ownership。业务逻辑进入普通 Go dispatcher/Core。
## 5. RPC envelope 与本地 DTO
所有 RPC 使用:
```json
{
"ok": true,
"result": {}
}
```
或:
```json
{
"ok": false,
"error": {
"code": "stable_code",
"message": "safe message",
"http_status": 400,
"retryable": false
}
}
```
未知方法返回 `unknown_method`,不能 panic 或返回裸字符串。
支持多个 CPA release 时可以维护插件自己的 wire DTO,避免编译期强耦合宿主 module;但每个字段名、JSON tag、零值和新增字段都必须逐项对照当前 `pluginapi/types.go``rpc_schema.go`。CPA capability payload 多数嵌入无 JSON tag 的 exported structwire 字段通常是 PascalCase;生命周期与 registration wrapper 使用明确 snake_case,不能统一猜测命名规则。
## 6. 注册与热重配
### 6.1 `plugin.register`
宿主传入:
- `config_yaml`
- host 支持的 `schema_version`
插件执行:
1. 解析 lifecycle request
2. 协商 `min(host_schema, plugin_max_schema)`
3. 严格解析和校验配置;
4. 初始化或取得 Core Runtime
5. 返回 metadata、协商 schema 和已实现 capability
6. metadata 的 Name、Version、Author、GitHubRepository 必须非空且稳定。
### 6.2 `plugin.reconfigure`
`plugin.reconfigure` 返回的 metadata/capability shape 必须与 register 一致。配置更新流程:
```text
parse → validate → build immutable candidate → open/check dependencies
→ atomic swap Runtime pointer → drain old Runtime
```
首次 `plugin.register` 配置无效时返回错误,不进入 active capabilities。**已经注册后的 `plugin.reconfigure` 不能直接返回错误**:当前 CPA 会因此把该插件从本轮 active records 移除、清除 exclusive provider,可能恢复原生认证。安全做法是保留最后一个有效 Runtime,记录 `reconfigure_rejected` 诊断,并仍以 success envelope 返回上一次有效 metadata/schema/capability shape。只有先修改 CPA 使 reconfigure 失败保留旧 active record 后,插件才可以对无效热配置返回 error。
配置分两类:
- 热配置:UI 选项、限流阈值、未绑定策略、统计刷新参数;
- 冷配置:database path、Key HMAC secret 来源、结算币种、插件 identity。
冷配置变化默认以上述 LKG 方式拒绝热切换并在诊断/API 提示重启;除非实现了完整的双 Runtime 打开、迁移和原子切换。当前 CPA 替换二进制时会把旧 native 实例放入 retired 集合直到宿主整体 shutdown,不能假定热替换时旧实例已排空。
### 6.3 建议配置
```yaml
plugins:
enabled: true
dir: plugins
configs:
cpa-ext:
enabled: true
priority: 100
codex_only: true
data_dir: data/cpa-ext
database_file: cpa-ext.db
settlement_currency: USD
frontend_auth_exclusive: true
unbound_key_policy: deny
unpriced_usage_policy: deny_new_requests
```
实例 Key 校验 secret 使用环境变量、secret file 或操作系统 secret store,不直接写进该 YAML,也不放进管理 metadata。
### 6.4 Schema 协商也是性能契约
生命周期 DTO 必须同时读取 `config_yaml` 和宿主传入的 `schema_version`,注册结果使用:
```text
negotiated_schema = min(host_schema, plugin_max_schema)
```
不能把 schema 固定成编译期常量后忽略宿主版本。当前 `cpa-plugin-key-billing v0.3.1` 正是一个反例:
- `internal/plugin/types.go` 固定 `SchemaVersion = 2`
- 它自己的 `LifecycleRequest` 只定义 `config_yaml`,没有读取 host schema
- `registration()` 每次都返回固定 schema 2
- README 同时需要兼容最低 CPA `7.2.103`,而其计费闭环使用 schema 2 已具备的 request lifecycle/terminate 能力。
因此可以确认“固定 2 是旧契约/兼容实现且尚未适配新性能契约”;无法仅凭源码断言作者的主观动机。当前 CPA `v7.2.130` 的 schema 3 专门解决一项流式放大:payload chunk 不再重复携带 `OriginalRequest``RequestBody`,只在 `ChunkIndex=-1` 的 header-init 发送一次。由于 Go JSON 会把 `[]byte` 编成 base64schema 2 在长 Prompt 下会把复制量和编码量放大到“请求体大小 × chunk 数”。
本项目规则:
- cpa-ext 的生产流式计费最低要求 RPC schema 3;宿主低于 3 时拒绝启用流式计费,不静默回退到高开销 schema 2;
- register 与 reconfigure 都返回实际协商版本,并用真机测试确认宿主记录的 schema;
- schema 3 只解决 `response.intercept_stream_chunk` 的重复请求体,**不解决全部流式开销**;
- 当前 `response.normalize_before` 在每个上游流式帧仍携带 `OriginalRequest``TranslatedRequest``Body`stream interceptor 还会携带最多 64 个、合计 1 MiB 的 `HistoryChunks`
- 插件本地 DTO 即使忽略这些字段,宿主侧 JSON/base64 编码、内存复制和 C ABI 调用已经发生,不能把“没有读取字段”当成零成本。
所以 schema 3 是必须的立即缓解,不是完整解决。完整解决需要缩小宿主 callback DTO 或消除逐 chunk callback,见 8.4 和 13.4。
## 7. Runtime 与模块装配
只保留一个有明确所有权的全局 App 入口:
```text
atomic App/Runtime pointer
├─ Core facade
├─ CPA adapters
├─ repositories
├─ callback-driven maintenance
├─ optional sidecar client
└─ immutable config/catalog snapshots
```
建议目录:
```text
cmd/cpa-ext/
main.go # cshared ABI
main_stub.go
internal/plugin/
dispatcher.go
registration.go
envelope.go
wire_*.go
internal/cpaadapter/
frontend_auth.go
interceptor.go
lifecycle.go
responses.go
usage.go
scheduler.go
management.go
internal/core/
internal/domain/
internal/repository/
internal/statistics/
web/ # React/Vite source
internal/webui/ # go:embed built assets
```
`internal/plugin` 只理解 RPC`internal/cpaadapter` 只把 CPA DTO 转成内部 command/observation`internal/core` 不 import `C` 或 CPA wire DTO。
### 7.1 P0:双 Go runtime 可行性门禁
`cpa-plugin-key-billing/internal/billing/store.go` 记录过实际事故:Go c-shared 插件在 Go 宿主进程中运行自己的 timer/GC/preemption 后,整个 CPA 因 `fatal error: bad flushGen` 崩溃,因此该插件刻意不保留 goroutine。当前 CPA 在 config disable 或二进制热替换时也不会立刻 shutdown 旧 native 实例,旧 goroutine 可能长期存活。
所以在实现业务前必须完成 runtime spike/soak,不能先把“动态库内 SQLite + 多个长期 worker”当成已成立:
1. 最小 c-shared 插件零长期 goroutine,验证 ABI、并发 callback、GC 压力和反复 register/reconfigure
2. 加入选定 SQLite driver 的同步短事务,确认 driver/`database/sql` 是否暗启长期 goroutine/timer
3. 分别测试 callback-driven 维护、一个受控 worker、完整 worker 集合;
4. 覆盖 config disable、无效 reconfigure、binary replacement、plugin fuse、CPA shutdown
5. 在目标 Windows DLL 与 Linux/WSL `.so` 上执行并发压力和至少 24 小时 soak,任何宿主 crash、旧实例活动或不可解释 goroutine 增长都判失败。
门禁通过前的 MVP 采用 callback/management-request 驱动:事实同步短事务落库,聚合小批追赶、清理和 checkpoint 只在安全宿主调用中有预算地执行;备份和长任务交给外部 sidecar/运维命令。门禁失败时长期聚合、归档、备份、provider refresh 必须永久移到 sidecar,动态库只保留认证/准入/采集/结算的薄同步路径。
## 8. CPA 生命周期映射
### 8.1 Frontend auth
`frontend_auth.authenticate` 收到 method/path/headers/query/body。适配器应:
- 在读取/验证 Key 前应用 [access-routing.md](access-routing.md) 的 method + path allowlist,未知入口默认 `Authenticated=false`
- 只读取允许的认证 Header
- 限制 body/headers 的处理量;
- 不记录完整请求或 Key
- 调用 Core credential authentication
- 返回稳定 Credential ID 作为 Principal
- 失败返回 `Authenticated=false`,不泄露“Key 是否存在”等枚举信息。
生产打开 exclusive 时,当前 CPA 会在多个 exclusive provider 中选择最高 prioritypriority 相同按 plugin ID 决定。必须在真实宿主中确认 `cpa-ext` 是唯一生效的认证路径。
当前 CPA frontend-auth contract 不能携带插件自定义认证错误。`internal/pluginhost/adapters_auth.go` 会将插件 RPC 错误或 `Authenticated=false` 都转换为 access manager 的 `NotHandled`;如果所有生效 provider 都是 `NotHandled`manager 返回宿主 `401 no_credentials`。由此得到三条实现约束:
- 第一版不要在 API 文档中承诺 `invalid_credential`、disabled/expired 的独立错误码;
- credential lookup 数据库故障仍然 fail closed,但外部也会看到 `401 no_credentials`,内部必须保留可告警的诊断分类;
- 如需向客户端准确返回认证服务 `503`,必须升级 CPA frontend-auth wire response/adapter,并更新兼容性矩阵。
Core readiness(数据库、HMAC keyring、账本、当前价格政策)不健康时,frontend auth 也返回 `Authenticated=false`,作为 interceptor 之前的第二道 fail-closed;外部仍只看到 401,真实原因进入诊断。
当前宿主在调用 frontend auth 前会 `ReadAll` 完整请求 Body 并将 headers/query/body 复制进 RPC,即使插件只检查 Header 也无法在内存分配前拒绝;Management handler 同样先被宿主完整读取。反向代理/CPA HTTP 层必须配置全局请求体上限,frontend-auth 对 Codex 大 Prompt 还要做容量基准。长期应推动 CPA 提供 header-only auth DTO 和宿主级 `MaxBytesReader`;插件自己的长度校验只减少后续解析,不能宣称保护了宿主内存。
更关键的是:exclusive 只在插件 active 时存在。当前 CPA 没有 `required plugin` 或“零认证 provider 默认拒绝”的配置;插件缺失/加载失败/被 fuse 时会清除 exclusive,恢复其他 provider,甚至在零 provider 时走 legacy 放行。生产部署必须同时满足:
1. CPA 配置一个不分发给用户的 256-bit 以上随机 native sentinel key,确保插件缺失时不是零 provider;
2. `cpa-ext` 注册 management readiness 路径,报告数据库、HMAC keyring、价格、exclusive/capability 自检结果;
3. 外部启动器/反向代理在 readiness 成功前不开放用户端口,并在插件丢失/fuse 后撤流量;
4. 最终推动 CPA 增加 required-plugin + authentication-default-deny;在此之前发布说明必须标注这是部署安全前提,而非动态库自身能力。
sentinel 只由部署运维保管,不能交给用户、写进前端或作为日常调用 Key;否则它本身就是计费旁路。
### 8.2 Request interceptors
- before-upstream-authCPA 方法名 before-auth):同步耐久化 Request pending、金额准入、占用并发;
- after-auth:每次上游尝试记录 selected auth/model/format
- metadata 视为只读 JSON-like snapshot
- interceptor response 不能任意给后续阶段增加 metadata;
- nested `plugin_host_model_callback` 必须识别,避免重复计费。
当前 CPA 对 interceptor RPC error 或 host-boundary panic 的处理是记录后继续请求。所有预期拒绝以及 DB、账本、价格故障必须返回正常 RPC success envelope,结果为 `Terminate=true`、合法状态码和脱敏错误体;不得 `return error` 表示拒绝。dispatcher 在方法边界 recover,并把 interceptor panic 转为上述 `503` termination。若 panic 已越过插件边界并导致宿主 fuse,当前请求无法由纯插件保证 fail closed,因此必须依赖上一节的部署门禁。
### 8.3 Scheduler
`scheduler.pick` 只能从宿主提供的 candidate 列表选择 AuthID,或明确 delegate 内置 scheduler。候选已经经过 provider/model/disabled/cooldown/tried 等过滤,插件不能选列表之外的 Auth。
宿主把 `Handled=false`、空响应、unknown AuthID 和 scheduler host-boundary panic 当作 unhandled,然后回退内置选择。strict 绑定的拒绝必须由插件内部 recover 后返回 scheduler RPC errorafter-auth interceptor 在真正执行前还要复核 selected AuthID。不应承诺 scheduler 自定义 HTTP error body:当前契约通常只能向调用方形成 generic 5xx。
当前宿主只会让最高优先级的有效 scheduler 策略实际主导选择,因此所有 Key binding、strict/preferred/pool 和 fallback 规则应在 `cpa-ext` 一个 scheduler 内组合。CPA `home.enabled` 会跳过 plugin scheduler,并使 plugin Management/resource routes 返回 404cpa-ext 第一版整体不支持 Home,生产启动检查必须拒绝该模式。
### 8.4 Response 与 Usage
- 翻译前 response 用来解析 provider 权威 Usage
- 非流式/流式 response interceptor 用 RequestID 绑定 response ID
- schema 3 stream payload chunk 不含重复 request body,关联数据在 header-init/请求阶段缓存;
- `usage.handle` 当前没有 RequestID,不能单独作为逐请求扣费提交点;
- 多来源观察保留 provenance 和 quality,不一致时不静默覆盖;
- callback 可能并发、重复、乱序或迟到。
为什么参考插件会同时使用两个同步 response hook
- 翻译前 hook 能看到 provider 权威 Usage,但当前没有 RequestID
- 翻译后的 response/stream hook 有 RequestID,可以用 response ID 完成归属;
- Codex WebSocket 同协议透传时可能不经过翻译前 hook,`cpa-plugin-key-billing v0.3.1` 因而又从下游 chunk 读取 response ID/Usage。
这个关联思路可以借鉴,但 capability 组合不能原样复制。当前两个 hook 都在下游交付关键路径同步执行:每个 frame/chunk 要经过宿主结构复制、JSON/base64、C ABI、插件 JSON 解码和返回 envelope。快速判断“这个事件没 Usage”只能节省插件内部解析,无法消除前面的传输成本。
采集方案按以下优先级选择:
1. 最优:扩展 CPA `UsageRecord`,直接提供 RequestID、Execution/AttemptID、ResponseID、Usage revision/source;插件不注册逐 chunk hook
2. 次优:新增只读 usage observer/final-frame callback,只发送 response ID、RequestID 和 Usage,不携带 Prompt、HistoryChunks 或可修改响应的能力;
3. 过渡:必须使用双 hook 时协商 schema 3,在 header-init 缓存最小关联,并推动 response-before 增加同类“payload frame 省略 request bodies”契约以及 stream capability 的 `needs_history=false`
4. 禁止:schema 2 + 双 hook 作为生产默认,或通过时间/AuthID 猜配 Usage 来换取速度。
在宿主契约尚未补齐时,是否保留 stream hook 由 13.4 的性能/正确性联合门禁决定,不能只因功能测试能扣到钱就发布。
每个 Execution 维护 canonical cumulative usage vector、source rank、response ID 和 hash。只有事务内 CAS 确认 vector 改变时才增加本地 `usage_revision`;相同 callback 重放不产生新 revision,多来源不能相加。response hook 取得新的可靠 canonical Usage 后,必须在返回 CPA 前同步写 SQLite observation/inbox;至少最终 Usage 不能只进入内存 channel。
若上游已经产生 Usage 后 SQLite 持久化失败,纯插件无法撤销该上游成本。adapter 记录高严重度诊断、把 Core readiness 置为 unhealthy,并让后续 frontend auth/interceptor 拒绝新请求;不得返回一个会被宿主忽略的 response-hook RPC error并假装已经可靠落库。该故障是管理员对账中的 `persistence_gap`,恢复后需要显式核对,不能猜费。
### 8.5 Request completion
当前 CPA 定义:succeeded、failed、rejected、canceled。completion 包含 RequestID、TraceID、模型、时间、状态和 metadata,但不包含 Token。
宿主以异步、单次且无 durable retry 的方式通知 lifecycle plugin,业务不能依赖调用方 context 仍然存活。适配器应快速完成持久化命令或投递到**已经持久化的 inbox**;不能只放进可能丢失的内存 channel。completion 只负责终态、释放并发和触发结算,不是 Request/Usage 的唯一落库点。启动恢复必须扫描 pending、已保存 Usage 但未结算的 Execution 和未发布 projection/outbox。
取消处理按 `billing.md`/`core.md`:释放并发与完成金额核算分离;有 Usage 结算,无 Usage 进入 awaiting,迟到 Usage 追加补记。
## 9. Management API 与用户页面限制
CPA 当前提供两类 plugin route
### 9.1 Management routes
- 挂载于 `/v0/management/...`
- 由 CPA Management Key 认证;
- 支持声明的精确 HTTP method/path
- 用于管理员 CRUD、账本、价格、路由、统计和诊断;
- handler 必须在 DTO 层拒绝过大 body、错误 content type,并限制分页和响应大小;但宿主已先完整读取 body,真正的内存上限必须设置在反向代理/CPA HTTP 层。
### 9.2 Resource routes
- 挂载于 `/v0/resource/plugins/<pluginID>/...`
- 当前只允许 browser GET resource
- 不经过 CPA Management Key
- 只能声明精确路径,不支持 `:param``*``..`
- 适合静态 HTML/JS/CSS shell,但天然不是安全的管理员 API。
因此:
- 静态 shell 可以公开;
- 管理员数据只从 Management routes 取得;
- 不能把 CPA Management Key 写入 HTML、JS、URL、localStorage 或插件 session
- 用户自助页不能直接调用管理员接口。
MVP 用户只读数据可以注册少量精确 resource GET JSON 路径,并由插件 handler 自行校验 `Authorization: Bearer <downstream-key>`,只返回该 Credential 的金额投影。Key 只保存在页面内存,不放 URL、cookie 日志或 localStorage,刷新后重新输入。若需要 HttpOnly session、POST 操作、稳定登录和完整 CSRF 模型,应增加经过明确设计的外部 sidecar/public API,或推动 CPA 增加 authenticated user plugin routes;不能假装当前 Management capability 已经提供这类接口。
Home 模式下 Management 与 resource route 都不可用;不是只有 scheduler 失效。第一版 readiness 遇到 `home.enabled` 必须失败。
### 9.3 Adapter 数据最小化与脱敏
CPA wire payload 的秘密面比业务契约大。每个 adapter 在进入 Core 前执行 allowlist/drop
| 入口 | 可能含秘密 | adapter 规则 |
| --- | --- | --- |
| frontend auth | Authorization、query、完整 Prompt Body | 只提取允许 Header 和 method/path;验证后丢弃原值,禁止日志/持久化 |
| scheduler | `Options.Headers` 当前未保证已脱敏 | 路由只读取 host metadata 中 caller_scopeheaders 默认全部丢弃 |
| Management | CPA Management Key、Cookie、完整 body | 进入 Core 前删除 Authorization/Cookiebody 严格 DTO 解码、字段/大小校验 |
| compatibility usage | `UsageRecord.APIKey` 可能是原始 CPA Key | 在 adapter 内立即映射/HMAC,之后只传 credential/scope;原值不得落库 |
| failure/response hooks | 错误 body、request/response 正文 | 只提取标准错误类别、response ID、Usage;原始正文默认丢弃 |
日志 API 默认只接收 stable IDs、preview、长度和分类,不接受通用 wire DTO。任何调试开关也不得输出 Authorization、Management Key、OAuth/provider token、Prompt 或完整响应。
## 10. 后台任务
只有通过 7.1 的目标平台 soak gate 后,插件才允许启用有界后台 worker;否则以下任务由 callback-driven runner 或 sidecar 承担:
- statistics aggregation
- awaiting usage reconciliation
- archive/cleanup
- backup
- catalog/quota refresh。
规则:
- 每个 worker 接收 Runtime context
- queue 有界且可观测,关键事实先落 SQLite;
- 不持锁执行 host callback、网络或慢 SQL
- 单个可选 worker 失败不杀死 CPA
- migration/账本等核心失败进入 fail-closed health
- reconfigure 不得重复启动同一 worker
- shutdown cancel 后有界等待,超时记录错误并继续释放资源。
CPA config disable/热替换不会立即调用旧 native shutdown,因此“worker 属于 Runtime”仍不足以保证停止;启用 worker 的版本还必须有 host generation/lease,使失去 active 身份的旧 Runtime 在下一次 lease 检查时自行停机。纯插件拿不到可靠 active lease 时,不得启用长期 worker。
## 11. Shutdown
当前 CPA revision 只会通过 native function table 的 `shutdown` 真正通知关闭;虽然 ABI 常量中定义了 `plugin.shutdown`,宿主没有调用点,不能依赖它。native shutdown 必须进入一个幂等 `sync.Once`/状态机:
1. 拒绝新管理写入与新准入;
2. cancel worker context
3. 有界等待 worker
4. 提交已在事务中的必要账本写入;
5. checkpoint/关闭 SQLite reader 和 writer
6. 清除 host callback context 引用;
7. 重复 shutdown 返回成功。
不能无限等待外部网络、不能调用 `os.Exit`、不能在动态库卸载后留下仍访问插件状态的 goroutine。
不能依赖 shutdown 处理 config disable 或 binary hot replacement:当前 CPA 可能只摘除 capability/retire 旧库,直至整个 plugin host 关闭才调用旧实例 shutdown。生产更新动态库采用排空并重启 CPA,不支持带长期资源的原地热替换。
## 12. 构建、发现与安装
Go c-shared 需要目标平台 C toolchain。仅设置 `GOOS/GOARCH` 通常不足以交叉编译 CGO,应在目标系统或可靠对应 toolchain 中分别构建。
Linux/WSL
```bash
gofmt -w cmd internal
go test ./...
go test -race ./...
CGO_ENABLED=1 go build -tags cshared -buildmode=c-shared \
-o bin/cpa-ext.so ./cmd/cpa-ext
file bin/cpa-ext.so
nm -D bin/cpa-ext.so | grep cliproxy_plugin_init
```
WSL 产出的 `.so` 只能给 Linux/WSL CPA 使用,不能放进 Windows CPA。
Windows
```powershell
$env:CGO_ENABLED='1'
go build -tags cshared -buildmode=c-shared -o bin/cpa-ext.dll ./cmd/cpa-ext
# 使用 Visual Studio dumpbin /exports 或 llvm-nm 验证 cliproxy_plugin_init
```
CPA discovery 支持:
```text
plugins/<goos>/<goarch>/cpa-ext.so
plugins/cpa-ext.so
plugins/cpa-ext-v<version>.so
```
Windows 使用 `.dll`macOS 使用 `.dylib`。插件 ID 必须与配置 key、文件名 ID 和 management/resource path 一致。版本化文件名不带前导 `v` 的版本字段部分,例如 `cpa-ext-v0.1.0.so`
发布按 OS/architecture 分包,包含 checksum、插件版本、目标 CPA revision/minimum version、ABI/RPC schema、固定构建工具链和 migration 说明;不包含数据库、secret、auth 文件或生成的 `.h`(除非确有消费者)。版本化文件名、Metadata.Version 和 release metadata 必须一致。第一版升级要求排空并重启 CPA,不宣传 live binary hot reload。
## 13. 测试顺序
### 13.1 普通 Go 测试
- RPC envelope、未知方法、畸形 JSON
- register/reconfigure 一致性;已注册后的无效 reconfigure 返回 LKG registration、保留旧 Runtime并记录诊断;
- Core 领域/用例;
- SQLite transaction、migration、幂等、恢复;
- 并发认证、准入、终态、迟到 Usage;
- 各 capability dispatcher
- shutdown/reconfigure race。
- adapter redactionAuthorization/Management Key/Prompt/UsageRecord.APIKey 不进入 Core、日志或数据库。
### 13.2 动态库测试
- 实际 c-shared build
- `cliproxy_plugin_init` export
- 文件名/目录 discovery
- ABI 不匹配拒绝;
- response buffer 能被正确释放;
- shutdown 可重复。
- 零常驻 goroutine基线、SQLite driver goroutine/timer 清单和双 runtime 压力/soak。
### 13.3 CPA 真机端到端
按 capability 逐项验证:
1. 宿主日志显示 plugin registered、schema 和 capability 正确;
2. reconfigure 保留数据库且不重复 worker;
3. 自管 Key 成功,未知/禁用 Key 失败,原生 Key 无法绕过 exclusive
4. 非流式与流式 Codex 请求;
5. strict/preferred/pool Auth 路由;
6. 成功、上游失败、本地拒绝、客户端取消;
7. 取消前有 Usage 扣费、无 Usage 标 unmeasured、迟到 Usage 补记;
8. 重试和重复 callback 不重复扣;
9. 额度耗尽返回 429
10. 管理路由受 Management Key 保护,resource 不泄密;
11. 重启后余额、账本、pending recovery 和统计 checkpoint 一致。
12. 写入畸形热配置后 exclusive/LKG 仍 active;插件缺失、fuse 和 Home 模式被 gateway/readiness 阻断;
13. 枚举全部 authenticated endpointallowlist 外入口拒绝;
14. DB error、interceptor/scheduler panic/invalid response 故障注入不形成可控边界内的免费请求或串号路由;
15. 大 Prompt/Management body 的代理上限和 frontend-auth 复制成本达到容量目标;
16. `A & B <x>` Management JSON 往返和 CSV 公式注入防护正确。
### 13.4 流式性能回归门禁
基准必须在**同一个 CPA、同一个 OAuth 账户、同一模型、同一 effective service tier、同一 transport**下做 A/B,避免把账号、Fast 或网络差异误判成插件开销。固定四组:
| 组别 | 目的 |
| --- | --- |
| 不加载任何目标插件 | CPA/OAuth 基线 |
| schema 3 空 hook 插件 | 测量宿主 callback/ABI 固有成本 |
| 仅采集与关联 | 测量 response parsing、锁和持久化成本 |
| 完整 cpa-ext | 最终性能与计费正确性 |
覆盖:
- SSE 与 Codex WebSocket
- 1 KiB、128 KiB、1 MiB Prompt
- 短输出以及 32、256、1024 chunks
- 并发 1、8、32
- standard 与 priority/Fast
- 成功、上游失败和客户端取消。
采集指标:TTFT P50/P95、总时长、tokens/s 或 chunks/s、chunk 间隔、CPU、alloc/GC、插件 RPC 次数/总字节/最大 payload、锁等待,以及最终 canonical Usage/金额。
结构性断言:
- schema 3 payload chunk 的 `OriginalRequest`/`RequestBody` 为空,完整请求只允许出现在 header-init
- 若仍启用 response-before,必须单独统计它重复携带请求体的字节数,不能混入“schema 3 已优化”的结论;
- `HistoryChunks` 未被业务使用时不得长期作为生产 payload;当前宿主无法关闭时,必须以基准证明成本可接受或先修改宿主;
- 单个普通 chunk 的目标大小为 `O(chunk)`,不得随 Prompt 或累计历史线性增长;
- 优化前后响应字节、Usage、金额和取消结算结果一致。
默认发布阈值:常规负载吞吐下降不超过 5%,压力负载不超过 10%,P95 TTFT 增量不超过 `max(20ms, 5%)`。任一结构性断言失败,或正确性依赖猜配,均直接判定不通过,百分比阈值不能豁免。
## 14. 源码导航
CPA 权威源码:
| 问题 | 路径 |
| --- | --- |
| ABI/schema/method 常量 | `CLIProxyAPI/sdk/pluginabi/types.go` |
| capability DTO | `CLIProxyAPI/sdk/pluginapi/types.go` |
| registration wire schema | `CLIProxyAPI/internal/pluginhost/rpc_schema.go` |
| schema negotiation/RPC adapter | `internal/pluginhost/rpc_client.go` |
| 加载、配置和 lifecycle | `internal/pluginhost/host.go``config.go` |
| 动态库命名/发现 | `internal/pluginhost/platform.go` |
| C loading/ownership | `internal/pluginhost/loader_windows.go``loader_unix.go` |
| frontend auth/exclusive | `internal/pluginhost/adapters_auth.go` |
| scheduler | `internal/pluginhost/scheduler.go` |
| intercept/lifecycle adapters | `internal/pluginhost/adapters_interceptors.go` |
| usage/response adapters | `internal/pluginhost/adapters_usage_translation.go` |
| management/resource routes | `internal/pluginhost/management.go` |
| Principal → caller_scope | `CLIProxyAPI/sdk/api/handlers/handlers.go``sdk/cliproxy/session/identity.go` |
优先官方 examples
- `examples/plugin/frontend-auth-exclusive/`
- `examples/plugin/request-lifecycle/`
- `examples/plugin/scheduler/`
- `examples/plugin/usage/`
- `examples/plugin/management-api/`
- `examples/plugin/codex-service-tier/`
生产结构参考 `cpa-plugin-key-billing/cmd/cpa-key-billing/main.go``internal/plugin/`,但协议冲突时永远以目标 CPA 源码和同 revision 官方测试为准。
## 15. 验收标准
- C ABI 入口保持薄且内存所有权正确;
- register/reconfigure 协商不高于宿主 schema,并返回相同 capability shape
- 只声明已实现能力,所有隐含方法均有测试;
- Core、领域和 Repository 可以在无 CGO 情况下测试;
- callback 并发、重复、乱序、迟到不会造成状态泄漏或重复账单;
- production exclusive auth 已通过真实 CPA 验证,不存在认证旁路;
- 全部 CPA 已认证 endpoint 已枚举,MVP allowlist 外入口用用户 Key 一律失败;
- 插件缺失、加载失败、host fuse 时 sentinel + readiness gate 能阻止用户流量;
- interceptor/scheduler 的 error、invalid response 和 panic 已验证不会在插件可控边界内放行;
- 部署检查拒绝 `home.enabled`,因为 scheduler 与全部插件 UI/API 均不可用;
- Management API、resource 和用户只读路径权限分离;
- build 产物导出正确 init symbol,并能被目标 CPA revision 加载;
- shutdown/reconfigure 不泄漏 worker、数据库连接或 host callback context
- Windows/Linux 目标工具链下双 Go runtime/SQLite spike 与 24h soak 通过,或所有长期任务已移入 sidecar;
- 发布说明准确报告 CPA revision、ABI、schema、capability、构建命令、产物和未执行检查。