Repository Wiki
deepseek-ai/deepseek-harness

凭据、设置与权限策略

本页说明项目中由 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

Loading diagram...

Source: index.ts

架构中的 CredentialProvider 是 Cordis service:构造函数以 Context 为参数,并以 credentials 注册到 context。CredentialRef 的解析是分层读取;CredentialKey 的记录读取则没有环境层,记录是否存在就是事实本身。ResolvedCredential 携带值和来源,配置面使用 CredentialRecordInfo 等信息对象来避免泄露值。

核心设计与命名策略

引用名:CredentialRef

引用必须匹配 POSIX shell identifier 风格:首字符为字母或下划线,后续字符只能是字母、数字或下划线。credentialRef 在构造 branded value 前执行检查;非法输入会抛出 TypeError。isCredentialRefName 则提供非抛错检查,适合处理来自外部发现机制或 hook payload 的候选名称:不符合语法的字符串被视为“没有可解析的引用”,而不是异常。

typescript
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。

typescript
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}

Source: index.ts Source: index.ts

Core Flow

凭据使用的关键控制流是“每次操作解析一次、配置面只描述一次、写入受可见层约束”。源码明确规定消费者在每次 operation 调用 resolve,不要把解析结果缓存到后续 operation;这样 provider-managed value 的变更能在下一次调用中被观察到。

Loading diagram...

Source: index.ts

流程中的两个安全决策很重要:

  1. resolve 的结果是 ResolvedCredential | undefined。未配置或空值不会伪装成有效 secret。
  2. describe 返回的是配置状态、来源和可写性,而不是 value。这样设置界面可以显示“是否配置”而无需接触敏感内容。

服务契约与数据边界

ResolvedCredential

ResolvedCredential 包含两个字段:非空的 value,以及 provider 定义的 source。源码注释列出的本地 provider 来源标识包括 env、file、project-env 和 user-env;这些标识用于解释值来自哪一层,而不是向配置面暴露更多 secret 内容。

CredentialRecordInfo

CredentialRecordInfo 面向安全的配置 UI,包含:

字段类型语义
configuredboolean是否存在已存储记录;拥有 ambient authentication 的 ApiKeyRecord 即使没有 key 或环境值,也可以表达已配置。
kindCredentialRecord['kind'],可选已存储记录的 discriminator;没有记录时缺失。
writableboolean当前 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 payloadCredentialRecordInfo 与 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 的锁实现,不能进一步断言锁类型、跨进程一致性或崩溃恢复行为。

数据关系

Loading diagram...

Source: index.ts

性能与运维注意事项

当前契约只明确了按 operation 解析和 provider-owned source 的分层语义,没有提供缓存、超时、重试或批量 API。运维上应据此避免假设“启动时加载一次凭据”是正确行为;缓存策略若由具体 provider 引入,也必须保持下一次 operation 能观察到凭据变更这一契约。

诊断和配置输出应使用 describe 或记录信息接口,而不是打印 resolve 的 value。项目级安全材料还明确提醒凭据和敏感数据不应暴露;实际运行时应把原始 provider 错误、配置导出和日志视为潜在敏感输出处理。

扩展点

CredentialProvider 是抽象 service,provider 可以实现不同的存储后端和来源层,但必须保持以下不变量:

  1. 通过 ctx.credentials 提供同一服务 seam。
  2. 只接受经过 CredentialRef/CredentialKey 规则约束的寻址值。
  3. resolve 对空值返回未配置语义,不把空字符串当作有效 secret。
  4. describe、记录枚举和记录描述永不返回敏感值。
  5. set/unset 不对被只读来源遮蔽的引用报告假成功。
  6. 依赖当前记录值的修改在 provider 支持的锁语义下完成。

具体 provider 的注册方式和实现类未在本次读取范围内确认,故此处不指定实现名称或配置键。

Sources

(1 files)