凭证与连接统一#
六种 payload 类合并为一个 CredentialData(共享字段和一个 data 字典),统一的 resolve_connection() 向通信层提供单次请求需要的全部信息:认证值、Base URL、请求头和类型。因此,每个 key 可以携带自己的 Base URL,已存储凭证会一次性迁移至新结构。
设计来源为
../providers/auth/credential-connection-unification.md.
数据模型#
此前 Credential.payload 存放 ApiKeyPayload、OAuthPayload、DeviceCodePayload、CliDelegatedPayload、ExternalProcessPayload 或 SsoPayload,现在统一存放 CredentialData:
@dataclass
class CredentialData:
kind: str
auth_value: str = ""
base_url: str = ""
headers: dict = field(default_factory=dict)
data: dict = field(default_factory=dict)
公共字段统一描述每种凭证“发送什么”。data 存放特定类型的数据,例如 refresh token、外部文件路径和 device-code 流程 ID。
序列化采用扁平结构 {kind, auth_value, base_url, headers, data},不含 __type__ 判别字段,因为 kind 现在是真实字段,而不是类身份。_payload_from_dict 仅接受此结构;运行时不兼容旧的六类 JSON。
Credential.metadata 及显示信息(email、name、org)保留原位置,既不属于 CredentialData,也不属于 ResolvedConnection。
用 kind 匹配替代 isinstance#
统一类后,类型检查变为值检查:
| 旧写法 | 新写法 |
|---|---|
isinstance(payload, ApiKeyPayload) |
payload.kind == "api_key" |
isinstance(payload, OAuthPayload) |
payload.kind == "oauth" |
isinstance(payload, (OAuthPayload, DeviceCodePayload)) |
payload.kind in ("oauth", "device_code") |
isinstance(payload, DeviceCodePayload) |
payload.kind == "device_code" |
isinstance(payload, CliDelegatedPayload) |
payload.kind == "cli_delegated" |
isinstance(payload, ExternalProcessPayload) |
payload.kind == "credential_process" |
isinstance(payload, SsoPayload) |
payload.kind == "sso" |
payload.access_token / payload.api_key |
payload.auth_value |
payload.refresh_token |
payload.data.get("refresh_token", "") |
payload.expires_at_ms |
payload.data.get("expires_at_ms", 0) |
其他旧字段(store_path、client_id 等) |
payload.data.get(...) |
构造映射#
| 旧构造方式 | 新构造方式 |
|---|---|
ApiKeyPayload(api_key=K) |
CredentialData(kind="api_key", auth_value=K) |
OAuthPayload(access_token=A, refresh_token=R, expires_at_ms=E, scope=S, client_id=C, token_endpoint=T, id_token=I, extra=X) |
CredentialData(kind="oauth", auth_value=A, data={"refresh_token":R,"expires_at_ms":E,"scope":S,"client_id":C,"token_endpoint":T,"id_token":I,"extra":X}) |
DeviceCodePayload(access_token=A, refresh_token=R, expires_at_ms=E, device_code_flow_id=F, extra=X) |
CredentialData(kind="device_code", auth_value=A, data={"refresh_token":R,"expires_at_ms":E,"device_code_flow_id":F,"extra":X}) |
CliDelegatedPayload(store_path=P, access_key_path=A, refresh_key_path=R, expires_key_path=E) |
CredentialData(kind="cli_delegated", data={"store_path":P,"access_key_path":A,"refresh_key_path":R,"expires_key_path":E}) |
ExternalProcessPayload(command=C, parses=Pa, json_key_path=J, cache_seconds=S) |
CredentialData(kind="credential_process", data={"command":C,"parses":Pa,"json_key_path":J,"cache_seconds":S}) |
解析:统一读取出口#
返回单独 str 的 _extract_token 被 resolve_connection 替代,后者返回请求需要的全部信息:
@dataclass
class ResolvedConnection:
kind: str
auth_value: str
base_url: str | None
headers: dict
def resolve_connection(cred: Credential) -> ResolvedConnection | None:
...
解析器执行以下规则:
cli_delegated在解析时读取外部文件,因此 token 始终是磁盘上的最新值。credential_process和sso尚未接入请求流程,没有认证值的凭证也无法发起请求;这些情况均返回None,由调用方回退。- 空
base_url转为None,使通信层区分“此凭证未指定端点”和“此凭证指定了端点”,并据此回退。
acquire_pooled(provider, profile=None) 返回 tuple[ResolvedConnection, str, str] | None,不再返回 token 三元组,使连接信息完整传至通信层。
凭证优先,目录回退#
通信层优先使用凭证信息,由模型目录补充缺失值:
base_url = (conn.base_url if conn and conn.base_url else None) or model.base_url
headers = {**(opts.headers or {}), **(conn.headers if conn else {})}
这样,一个 API key 可以使用自己的端点,同一 provider 的其他 key 仍使用目录默认值。OAuth 检测采用相同方式:通信层不再检查 token 字符串,而是接收根据 conn.kind in ("oauth", "device_code") 计算的 is_oauth。
已存储凭证迁移#
运行时不支持旧格式,因此通过独立流程进行一次性转换。migrate_payload_dict(old) -> dict 将携带 __type__ 的旧 payload 字典映射为新的扁平结构,并保持幂等:已经采用新结构的 payload 原样返回。migrate_store(root) -> int 遍历 <root>/auth/<provider>/<profile>.json,原子重写各文件,并返回修改数量。管理文件(_rotation、_active、_disabled、_order.json)没有 credentials 列表,因此跳过。
迁移在存储首次加载、读取任何凭证池之前自动运行,同时提供 openprogram auth migrate 命令。迁移失败不得阻止存储启动;真正损坏的文件随后通过 from_dict 的 AuthCorruptCredentialError 显示。
本次结构调整递增 CREDENTIAL_SCHEMA_VERSION。旧 v 值触发迁移,而非损坏错误。
附录:实现状态#
当前认证路径已实现。CredentialData 和 schema 版本 3 定义于 openprogram/auth/types.py;ResolvedConnection 与 resolve_connection 位于 openprogram/auth/resolver.py;payload 转换和存储迁移位于 openprogram/auth/_migrate_payload.py;openprogram/auth/store.py 接入首次加载迁移;openprogram/auth/cli.py 暴露 openprogram auth migrate。Provider 凭证池现在接收已解析连接,通信层可以优先使用凭证的端点和请求头。原始构造/匹配表仍作为设计契约。尚需验收真实凭证存储副本的迁移及常规 auth/provider 回归套件;本页不声称这些检查已在此执行。