# ADR-0169:Project-bound AI Provider Credential Binding 与可清零 Secret Material - 状态:Accepted - 日期:2026-07-26 - 关联:RFC D-08、D-12、D-156、D-159;ADR-0073、ADR-0167、ADR-0168 ## 背景 Model Gateway 已能在 durable `StepRun.kind=model` fence 下调用远程 Provider,但原有 OpenAI-compatible credential port 只返回一个无上下文的 authorization string: - Provider 无法证明 credential 属于当前 Project; - `listModels`、`generate` 和 `stream` 可能意外共用进程级 token; - string 没有明确 material owner 或释放时点; - Secret 轮换容易引入 cache、watcher 或重启要求; - credential 获取失败和审计失败是否发生在网络请求前没有契约保证。 QingLong 3.0 同时面向低配路由设备与多副本 Cluster。解决方案不能把 AI contract 放入 `runtime-core`,也不能为 credential 再拆一个 workspace package。 ## 决策 ### 1. Binding 留在同一个 `@qinglong/ai` 能力包 新增显式 subpath `@qinglong/ai/provider-credential`,不新增 workspace package。 `ModelProviderCredentialBinding` 使用 exact-shape v1 contract,固定包含: - `projectId`; - Provider identity; - binding revision; - canonical Project-bound `SecretRef`; - 首版唯一允许的 `bearer` scheme。 Binding 不保存 Secret 明文、token digest、authorization header、URL 或模型内容。 `SecretRef.projectId` 必须与 binding Project 完全相同。binding 的 domain-separated SHA-256 digest 可进入低敏审计,但不能据此恢复 Secret 名称或值。 ### 2. Secret material 必须有单一 owner 和显式 dispose AI package 只定义 structural `ModelProviderSecretMaterialProvider`: - 输入是 exact Project 与 SecretRef,可携带 AbortSignal; - 输出必须回显 exact SecretRef; - plaintext 使用 consumer-owned `Uint8Array`; - Provider 必须提供 `dispose()`; - 缺失、身份漂移、形状漂移和 disposal 失败全部 fail closed。 `@qinglong/local-secret` 的 `EncryptedLocalSecretService` 提供结构兼容的 `resolveProjectSecretMaterial`: - 复用既有 AES-256-GCM envelope、外置 keyring 和 canonical SecretRef; - pinned ref 解析指定 version,unpinned ref 解析当前 version; - 解密 key 在返回前清零; - 返回的 plaintext bytes 由调用方 lease 最终清零; - Project 不匹配、已取消、缺失 key/envelope 和 storage 错误均不返回明文。 Cluster 不复用本机 keyring;未来 KMS/Vault adapter 实现同一 structural port。 ### 3. 每次调用重新解析,不缓存 credential `BoundModelProviderCredentialProvider` 对每个 `list_models`、`generate` 或 `stream` 操作重新执行: 1. 以 Project + Provider 解析 binding; 2. 复验 exact binding 和 SecretRef Project; 3. 解析一次 Secret material; 4. 复制到短生命周期 bearer token buffer,并立即释放上游 material; 5. 写入 content-free credential audit; 6. 只在 audit 成功后交付 authorization lease。 未固定 version 的 SecretRef 因而在下一次调用自然取得当前 Secret version,不需要 watcher、timer、cache invalidation 或进程重启。每次调用最多解析一次,不自动 retry。 ### 4. Authorization lease 只覆盖请求建立阶段 首版只接受 ASCII bearer token,完整 authorization header 不得超过 4 KiB。CR/LF、 空值、非 ASCII、空格、未知 scheme 和超限 material 都在网络前拒绝。 OpenAI-compatible adapter 向 credential provider 传递 operation、Provider、 Project、requestId 和 AbortSignal: - credentialed `listModels` 缺少 Project 或 requestId 时 fail closed; - adapter 在 `fetch` Promise 收敛后立即 dispose authorization lease; - fetch 失败、HTTP 失败和 malformed lease 同样执行 dispose; - disposal 失败时丢弃 response,不能把结果交给调用方; - disposed lease 不再返回原 authorization string。 JavaScript/Fetch 内部不可证明所有 string copy 被原地清零,因此这里不声称拥有该能力。 受控边界是:原始 Secret 与 bearer buffer 可清零,authorization string 不持久化、不 进入 audit、repository 或日志,并只存活到请求建立完成。 ### 5. Credential audit 必须先于外部网络 credential audit 只包含: - schema、operation; - Project、Provider、request identity; - binding revision 与 binding digest; - occurrence time。 不包含 SecretRef、Secret name、token、header、Prompt、模型输出或 raw error。 Audit sink 失败时,material 和 token buffer 都必须释放,OpenAI-compatible fetch 调用次数必须为零。 该 port 要求产品 composition 注入 durable sink;当前没有产品 importer,因此不能把 测试 sink 当成生产审计已经完成。 ## 被否决方案 1. **进程级静态 API key**:无法按 Project 隔离,也无法证明 Run 使用了哪一条授权。 2. **把 token 放进 GenerateRequest 或 ModelInvocationContext**:transport、Trace 和 replay record 会取得 plaintext authority。 3. **缓存解密后的 token 并监听轮换**:扩大常驻内存、后台任务和 stale credential 窗口,低配设备也要付费。 4. **把 material contract 放进 runtime-core**:即使只有 `.d.ts`,默认 Edge archive 也实测从 3,902,728 增至 3,903,377 bytes;该边界已撤回 AI package,默认基线恢复。 5. **为 credential 新增 workspace package**:没有独立部署收益,会再次产生过细包。 6. **声称 JavaScript string 可可靠清零**:不符合运行时事实;只约束可拥有的 byte buffer 和 string 生命周期。 ## 验证 - `@qinglong/ai`:50 pass、1 条 PostgreSQL 条件 skip; - `@qinglong/local-secret`:6 pass; - binding normalization、Project fence、stable digest、exact subpath 通过; - unpinned SecretRef 连续两次解析取得两个版本,没有 cache 或 watcher; - missing/drifted binding、非法/非 ASCII/超限 material 全部 fail closed; - audit failure 时 Secret material 已 dispose,OpenAI-compatible fetch 为 0; - fetch failure 和 malformed authorization lease 均执行 dispose; - cluster dependency audit 覆盖 22 importers、AI 14 个 TypeScript source, `findings=[]`; - disabled Edge/Standalone/Cluster 仍只加载 1 个 AI module,storage/provider loader 为 0;8 MiB RSS/50 ms 门通过; - 默认 Edge archive 恢复并保持 3,902,728 bytes、478 files、40 modules; - edge-ai 为 4,212,508 bytes、508 files、41 modules; - standalone-ai 为 4,212,580 bytes、508 files、41 modules; - edge/standalone application 为 4,547,398/4,547,530 bytes、589 files、87 modules,仍低于 5 MiB/640 files 门。 ## 后续门禁 > 2026-08-02 更新:第 1 项的 PostgreSQL durable catalog/audit、第 3 项的 projected > Cluster material adapter,以及显式 Cluster AI 产品组合和独立供应链 artifact 已由 > [ADR-0263](./ADR-0263-explicit-cluster-ai-composition-and-provider-credential-authority.md) > 完成。以下列表保留当时的演进上下文;仍未完成的是管理 ceremony、最终 KMS/Vault/HSM > custody、真实 Kubernetes 纵切面与更广产品 route。 1. 为 credential audit 提供双方言 durable repository,并把 binding revision/digest 与 ModelInvocation Start/Completion 查询关联; 2. 本机产品 importer 通过 `secret.use` Policy authority 注入 `EncryptedLocalSecretService`,不得把 raw service 暴露给 transport; 3. Cluster 提供 PostgreSQL binding catalog 与 KMS/Vault material adapter; 4. binding 配置、轮换、撤销、测试连接和审计查询 ceremony; 5. durable usage/cost ledger、Project quota 和费用表; 6. 上述门完成前继续关闭 HTTP/MCP/UI 产品 route。