7.6 KiB
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; - 首版唯一允许的
bearerscheme。
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
操作重新执行:
- 以 Project + Provider 解析 binding;
- 复验 exact binding 和 SecretRef Project;
- 解析一次 Secret material;
- 复制到短生命周期 bearer token buffer,并立即释放上游 material;
- 写入 content-free credential audit;
- 只在 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 在
fetchPromise 收敛后立即 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 当成生产审计已经完成。
被否决方案
- 进程级静态 API key:无法按 Project 隔离,也无法证明 Run 使用了哪一条授权。
- 把 token 放进 GenerateRequest 或 ModelInvocationContext:transport、Trace 和 replay record 会取得 plaintext authority。
- 缓存解密后的 token 并监听轮换:扩大常驻内存、后台任务和 stale credential 窗口,低配设备也要付费。
- 把 material contract 放进 runtime-core:即使只有
.d.ts,默认 Edge archive 也实测从 3,902,728 增至 3,903,377 bytes;该边界已撤回 AI package,默认基线恢复。 - 为 credential 新增 workspace package:没有独立部署收益,会再次产生过细包。
- 声称 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 完成。以下列表保留当时的演进上下文;仍未完成的是管理 ceremony、最终 KMS/Vault/HSM custody、真实 Kubernetes 纵切面与更广产品 route。
- 为 credential audit 提供双方言 durable repository,并把 binding revision/digest 与 ModelInvocation Start/Completion 查询关联;
- 本机产品 importer 通过
secret.usePolicy authority 注入EncryptedLocalSecretService,不得把 raw service 暴露给 transport; - Cluster 提供 PostgreSQL binding catalog 与 KMS/Vault material adapter;
- binding 配置、轮换、撤销、测试连接和审计查询 ceremony;
- durable usage/cost ledger、Project quota 和费用表;
- 上述门完成前继续关闭 HTTP/MCP/UI 产品 route。