# 插件运行时与 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 baseline:CLIProxyAPI `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. 自管 Key:frontend auth,并在生产测试完成后打开 exclusive; 4. 账户绑定:scheduler; 5. 管理与 UI:management 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 struct,wire 字段通常是 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` 编成 base64,schema 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 中选择最高 priority,priority 相同按 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-auth(CPA 方法名 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 error;after-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 返回 404;cpa-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//...`; - 当前只允许 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 `,只返回该 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_scope;headers 默认全部丢弃 | | Management | CPA Management Key、Cookie、完整 body | 进入 Core 前删除 Authorization/Cookie;body 严格 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///cpa-ext.so plugins/cpa-ext.so plugins/cpa-ext-v.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 redaction:Authorization/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 endpoint,allowlist 外入口拒绝; 14. DB error、interceptor/scheduler panic/invalid response 故障注入不形成可控边界内的免费请求或串号路由; 15. 大 Prompt/Management body 的代理上限和 frontend-auth 复制成本达到容量目标; 16. `A & B ` 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、构建命令、产物和未执行检查。