凭据、设置与权限策略
本页说明项目中由 CredentialProvider 暴露的凭据引用、凭据记录与安全配置边界:配置面只处理引用和非敏感状态,具体 provider 负责解析、存储及权限相关记录。
Purpose and Scope
本页覆盖 @deepseek-ai/dsh-credentials 的核心服务契约、凭据命名规则、解析与持久化语义,以及配置界面可以安全展示的状态。重点是“凭据、设置与权限策略”这一能力边界,而不是某个具体 provider 的文件格式或操作系统密钥链实现。
当前已核实的源代码主要是凭据服务定义;具体 provider 的注册、文件锁实现、设置 UI 和权限授权记录的完整实现细节未在本次有限源码读取中确认,因此不对这些部分的具体行为作推断。有关启动参数、插件组合和测试密钥策略,应分别参考 CLI、composition 及 testing 相关页面。
Overview
该能力将两种不同的寻址空间明确分开:
CredentialRef:表示一个环境变量式的引用名,例如DEEPSEEK_API_KEY。调用方通过引用解析当前值,值可能来自进程环境、provider 管理的存储或.env层。CredentialKey:表示某个插件拥有的凭据记录,格式为<scope>/<id>,例如插件作用域与其 provider route key 的组合。该空间只表达一个存储记录,不叠加环境层。
这种分离避免了配置文件直接携带 secret:配置只保存引用名;真正的值由 provider 在每次操作时解析。源码特别要求消费者不要跨操作缓存解析结果,从而使凭据更新可以在下一次操作生效,而不必重启插件。
另一个关键策略是“空值不算已配置”:空的 stored value 在 resolve 中视为缺失,在 describe 中也不能报告为已配置。与此同时,配置界面只能取得 presence、来源、记录种类和可写性等元数据,不能取得 secret 本身。
Architecture
Source: index.ts
架构中的 CredentialProvider 是 Cordis service:构造函数以 Context 为参数,并以 credentials 注册到 context。CredentialRef 的解析是分层读取;CredentialKey 的记录读取则没有环境层,记录是否存在就是事实本身。ResolvedCredential 携带值和来源,配置面使用 CredentialRecordInfo 等信息对象来避免泄露值。
核心设计与命名策略
引用名:CredentialRef
引用必须匹配 POSIX shell identifier 风格:首字符为字母或下划线,后续字符只能是字母、数字或下划线。credentialRef 在构造 branded value 前执行检查;非法输入会抛出 TypeError。isCredentialRefName 则提供非抛错检查,适合处理来自外部发现机制或 hook payload 的候选名称:不符合语法的字符串被视为“没有可解析的引用”,而不是异常。
1const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
2
3export function credentialRef(value: string): CredentialRef {
4 if (!isCredentialRefName(value)) {
5 throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`)
6 }
7 return brandString<CredentialRef>(value)
8}
9
10export function isCredentialRefName(value: string): boolean {
11 return REF_PATTERN.test(value)
12}Source: index.ts
记录键:CredentialKey
记录键由两个小写、连字符分隔的段组成:scope/id。credentialKey 会逐段验证;parseCredentialKey 还会要求输入恰好包含一个 /,避免多段字符串被误认为合法 key。credentialKeyScope 和 credentialKeyId 只负责读取已经 branded、因此已经经过构造校验的 key。
1const KEY_SEGMENT_PATTERN = /^[a-z][a-z0-9-]*$/
2
3export function credentialKey(scope: string, id: string): CredentialKey {
4 for (const segment of [scope, id]) {
5 if (!KEY_SEGMENT_PATTERN.test(segment)) {
6 throw new TypeError(`credential key segment "${segment}" must match ${String(KEY_SEGMENT_PATTERN)}`)
7 }
8 }
9 return brandString<CredentialKey>(`${scope}/${id}`)
10}
11
12export function parseCredentialKey(value: string): CredentialKey {
13 const segments = value.split('/')
14 const [scope, id] = segments
15 if (segments.length !== 2 || scope === undefined || id === undefined) {
16 throw new TypeError(`credential key "${value}" must be "<scope>/<id>"`)
17 }
18 return credentialKey(scope, id)
19}Core Flow
凭据使用的关键控制流是“每次操作解析一次、配置面只描述一次、写入受可见层约束”。源码明确规定消费者在每次 operation 调用 resolve,不要把解析结果缓存到后续 operation;这样 provider-managed value 的变更能在下一次调用中被观察到。
Source: index.ts
流程中的两个安全决策很重要:
resolve的结果是ResolvedCredential | undefined。未配置或空值不会伪装成有效 secret。describe返回的是配置状态、来源和可写性,而不是 value。这样设置界面可以显示“是否配置”而无需接触敏感内容。
服务契约与数据边界
ResolvedCredential
ResolvedCredential 包含两个字段:非空的 value,以及 provider 定义的 source。源码注释列出的本地 provider 来源标识包括 env、file、project-env 和 user-env;这些标识用于解释值来自哪一层,而不是向配置面暴露更多 secret 内容。
CredentialRecordInfo
CredentialRecordInfo 面向安全的配置 UI,包含:
| 字段 | 类型 | 语义 |
|---|---|---|
configured | boolean | 是否存在已存储记录;拥有 ambient authentication 的 ApiKeyRecord 即使没有 key 或环境值,也可以表达已配置。 |
kind | CredentialRecord['kind'],可选 | 已存储记录的 discriminator;没有记录时缺失。 |
writable | boolean | 当前 modifyRecord 是否可以成功。 |
这组字段刻意不包含凭据值。CredentialRecordEntry 同样只枚举 key 和记录 kind,不返回 record payload。
CredentialProvider
CredentialProvider 是抽象 Cordis Service,构造函数调用 super(ctx, 'credentials'),并通过 module augmentation 将 credentials: CredentialProvider 加到 Cordis Context。它定义了两类操作:
- 引用操作:
resolve、describe、set、unset。 - 记录操作:
readRecord,以及源码后续定义的记录描述/修改契约;本次读取窗口未包含这些后续方法的完整签名,因此不在此补写未经核实的参数和返回类型。
API Reference
credentialRef(value: string): CredentialRef
校验并 brand 一个引用名。value 必须匹配 ^[A-Za-z_][A-Za-z0-9_]*$;否则抛出 TypeError。该函数适合在应用内部把原始配置字符串转换为受约束的引用类型。
isCredentialRefName(value: string): boolean
执行同一引用语法检查但不抛出异常。适合对外部候选字符串进行探测。
credentialKey(scope: string, id: string): CredentialKey
校验两个小写连字符段,并以 scope/id 形式创建 branded key。任一段不符合 ^[a-z][a-z0-9-]*$ 时抛出 TypeError。
parseCredentialKey(value: string): CredentialKey
解析已经连接的 key。输入必须严格拆分为两个段;格式错误或任一段非法时抛出 TypeError。
CredentialProvider.resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
按调用时刻解析引用。返回当前值及其来源,未配置时返回 undefined。调用方不应跨 operation 缓存结果。
CredentialProvider.describe(ref: CredentialRef): Promise<CredentialInfo>
返回引用的配置状态、供应来源和可写性,但不暴露值;这是设置界面与诊断界面的安全入口。
CredentialProvider.set(ref: CredentialRef, value: string): Promise<void>
将非空值持久化到 provider 管理的可写来源。源码契约规定:当只读来源遮蔽该引用时必须拒绝写入,否则调用方会看到“写入成功”但之后解析仍返回遮蔽值;空值也必须拒绝,清除操作应使用 unset。
CredentialProvider.unset(ref: CredentialRef): Promise<void>
从 provider 管理的可写来源移除引用。移除不存在的引用是 no-op;只读来源遮蔽时与 set 一样拒绝操作。
CredentialProvider.readRecord(key: CredentialKey): Promise<CredentialRecord | undefined>
读取一个记录 key。没有记录时返回 undefined;已读出的 GrantRecord payload 不会在读取路径上被解释。
配置选项与设置边界
本次读取的凭据服务定义没有声明具体的配置文件键、默认 provider 名称或环境变量覆盖表,因此不能可靠列出具体配置 key。源码能够确认的配置边界如下:
| 配置面 | 可保存/展示的内容 | 不应暴露的内容 | 依据 |
|---|---|---|---|
| Composition/settings 文件 | CredentialRef 等环境变量名引用 | secret value | 服务注释明确区分 references 与 actual values。 |
| 凭据状态界面 | configured、kind、writable、来源信息 | secret value、record payload | CredentialRecordInfo 与 CredentialRecordEntry 只提供元数据。 |
| provider 写入入口 | 非空 value,写入 provider-managed writable source | 对只读遮蔽层执行假成功写入 | set/unset 契约明确要求拒绝。 |
具体 provider 的文件位置、优先级和加密方式未在已读取源码中确认;不要把上表解释为某个 provider 的完整存储实现。
失败模式、边界条件与并发语义
输入验证失败
- 引用名为空、以数字开头或含有连字符等不满足 shell identifier 规则时,
credentialRef抛出TypeError。 credentialKey的 scope 或 id 含大写字母、下划线开头、空字符串或其他不符合小写连字符规则的内容时,抛出TypeError。parseCredentialKey对缺少/、多于一个/或非法 segment 抛出TypeError。- 对不可信候选引用,优先使用
isCredentialRefName;源码特意提供该非抛错路径,避免把“没有可解析引用”误处理成系统故障。
空值与未配置
服务 seam-wide 规则将空 stored value 统一视为 absent:resolve 跳过它,describe 报告未配置。因而调用方不能仅以“存在一条记录”推断能拿到非空 secret;必须依赖解析结果或安全描述接口。
只读层遮蔽
set 与 unset 都会检查只读来源是否遮蔽目标引用。若遮蔽存在,写入或删除必须拒绝,因为 provider 仍会返回只读层的值。该策略牺牲了“命令看似成功”的便利,换取配置状态与实际生效值的一致性。
变化可见性与并发
源码规定解析按 operation 发生,而非在插件启动时缓存。因此凭据更新无需插件重启即可影响下一次 operation。对于记录写入,服务注释明确指出 token refresh 是“read-decide-replace under one lock”;这说明正确 provider 应将依赖当前值的修改作为受锁保护的原子决策。不过本次未读取具体 provider 的锁实现,不能进一步断言锁类型、跨进程一致性或崩溃恢复行为。
数据关系
Source: index.ts
性能与运维注意事项
当前契约只明确了按 operation 解析和 provider-owned source 的分层语义,没有提供缓存、超时、重试或批量 API。运维上应据此避免假设“启动时加载一次凭据”是正确行为;缓存策略若由具体 provider 引入,也必须保持下一次 operation 能观察到凭据变更这一契约。
诊断和配置输出应使用 describe 或记录信息接口,而不是打印 resolve 的 value。项目级安全材料还明确提醒凭据和敏感数据不应暴露;实际运行时应把原始 provider 错误、配置导出和日志视为潜在敏感输出处理。
扩展点
CredentialProvider 是抽象 service,provider 可以实现不同的存储后端和来源层,但必须保持以下不变量:
- 通过
ctx.credentials提供同一服务 seam。 - 只接受经过
CredentialRef/CredentialKey规则约束的寻址值。 resolve对空值返回未配置语义,不把空字符串当作有效 secret。describe、记录枚举和记录描述永不返回敏感值。set/unset不对被只读来源遮蔽的引用报告假成功。- 依赖当前记录值的修改在 provider 支持的锁语义下完成。
具体 provider 的注册方式和实现类未在本次读取范围内确认,故此处不指定实现名称或配置键。