Files
qinglong/docs/adr/ADR-0169-project-bound-ai-provider-credential-binding.md
T

156 lines
7.6 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.
# ADR-0169Project-bound AI Provider Credential Binding 与可清零 Secret Material
- 状态:Accepted
- 日期:2026-07-26
- 关联:RFC D-08、D-12、D-156、D-159ADR-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 解析指定 versionunpinned 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 已 disposeOpenAI-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 modulestorage/provider loader
为 08 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。