OpenProgram Docs

凭证与连接统一#

六种 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 回归套件;本页不声称这些检查已在此执行。

Last updated · 2026-10-10