Skip to content

dsh 中的凭证遵循 引用而非值 的模型:设置与组合文件携带对机密的引用(环境变量名),而提供方拥有实际值与它们的存储。消费表面只描述一个引用,永远不看到它的值。

角色
packages/credentials/credentialsctx.credentials 服务定义 + CredentialRef/ResolvedCredential
packages/credentials/credentials-local基于 $DSH_HOME/.credentials.yaml 的文件后端提供方
packages/llm/llmassertUsableApiKey——共享的密钥有效性诊断
packages/llm/llm-pi-ai每次请求解析 apiKeyEnv 的 pi-ai 适配器

凭证模型

CredentialRefpackages/credentials/credentials/src/types.ts)是一个带 brand 的 POSIX shell 标识符,例如 DEEPSEEK_API_KEY,由 credentialRef(value) 对照 /^[A-Za-z_][A-Za-z0-9_]*$/ 校验。服务定义(packages/credentials/credentials/src/index.ts)是注册为 ctx.credentials 的抽象 CredentialProvider,含四个操作:

操作签名含义
resolve(ref)Promise<ResolvedCredential | undefined>当前值 + 来源;未配置时为 undefined
describe(ref)Promise<CredentialInfo>configuredsourcewritable——绝不含值(对 UI 安全)
set(ref, value)Promise<void>持久化存储一个非空值
unset(ref)Promise<void>移除值;移除不存在的引用是 no-op

ResolvedCredential{ value, source };本地提供方的来源为 envfileproject-envuser-env 之一。一条 seam 全局规则约束所有提供方:空存储值处处视为缺席——resolve 跳过它,describe 报告未配置,因此空值不会伪装成已配置的机密。

值每次变化时,CredentialProvider.notifyUpdated(ref) 会向外发出 credentials/updated 事件并包含监听器失败——损坏的观察者永远无法让一次持久化写入看起来失败(INVARIANT 编码的失败仍会重新抛出)。

本地文件后端(credentials-local

LocalCredentialProviderpackages/credentials/credentials-local/src/index.ts)以 <harness home>/.credentials.yamlCREDENTIALS_FILENAME)为后端,并按信任度与环境分层:

text
继承的进程环境                    (只读,获胜)
> $DSH_HOME/.credentials.yaml     (提供方管理,可写)
> <调用 cwd>/.env                 (只读回退)
> $DSH_HOME/.env                  (只读回退)
  • 继承环境获胜,因为 DEEPSEEK_API_KEY=… dsh、CI 机密或容器 -e 是本次运行的明确意图,无法从内部编辑——因此它可见地只读(source: 'env'writable: false),而非静默遮蔽写入。
  • .env 回退低于受管存储,因此通过 Models 页写入的密钥会立即生效,即便用户 .env 里有更旧的密钥;项目 .env 高于用户那份(更具体的位置获胜)。
  • 该文档本身是一个严格的 CredentialRef 到字符串的 YAML 映射(不是 dotenv 文件):dsh 拥有且从不物化到环境中的存储,不能兼任用户的环境层。

存储安全

  • 权限:文件以 0600 创建/替换,目录以 0700assertOwnerOnly 在读取前拒绝任何已存在且带 group 或 other 位(0o077)的文档——在 Windows 上跳过,它没有可检查的 POSIX 模式。
  • 原子、保留注释的写入:每次写入在跨进程写锁(withFileLock)下重新读取文档,再只修补自己的键,因此注释与每个未触碰条目的格式都保留;写入用 writeFileAtomic
  • 热重载:一个 chokidar watcher(watch: true,默认开)重新读取文档;变化值通过 seam 作为 credentials/updated fan-out 传播。每次重载都会整体替换快照。
  • Fail-closed:启动时非法文档会令插件失败(一个存在但不可信的文档从不会被当作"未存储凭证");失败的重载保留最后一份好快照并告警。

插件配置:path(默认 <dshHome>/.credentials.yaml)、dshHome(默认 $DSH_HOME~/.dsh)、watch(默认 true)、debounceMs(默认 100)。

LLM 提供方如何获取凭证

LLM 包每次请求都通过 seam 解析,绝不在操作间缓存。

llmpackages/llm/llm/src/index.ts)贡献 assertUsableApiKey(raw, pkg, ref)——与 LlmError 并存于 LlmError 旁的共享有效性诊断。它静默修剪周围空白(存储的密钥可能来自凭证 seam、.env 行或 shell 导出),并以 INVALID_CREDENTIAL_CODE 拒绝空或无法放入 header 的密钥,点名要修复的 ref绝不回显密钥。密钥绝不出现在消息或 UI 中。

llm-pi-aipackages/llm/llm-pi-ai/src/index.ts)在 resolveApiKey(provider, profile) 中展示了实际获取路径:

ts
const ref = profile.apiKeyEnv
if (ref === undefined) return undefined  // defer to provider-native discovery
const credentials = ctx.get('credentials')
const hit = credentials !== undefined
  ? (await credentials.resolve(ref))?.value
  : launchEnvironmentOf(ctx).get(ref)?.value   // no seam ⇒ environment is the whole plane
if (hit !== undefined && hit.length > 0) return assertUsableApiKey(hit, 'llm-pi-ai', ref)
throw new LlmError(/* … */, 'MISSING_CREDENTIAL')

每个提供方 profile 声明一个 apiKeyEnv(例如 OPENAI_API_KEY)。只有不声明任何凭证的 profile 才交给 pi-ai 提供方原生发现——一旦声明,miss 会大声失败,而非让 pi-ai 捡起无关的环境密钥(OPENAI_API_KEY 及其同侪),那会给别家租户计费。适配器还暴露 storedApiKey(provider) 用于探查草稿端点,其中请求方表面编辑的是一份脱敏描述符,从不持有已存储机密。

API 代理是否注入凭证?

Web 聚合代理packages/host/apiproxy)暴露一个 credentials RPC 域(src/api/credentials.ts,schema 在 src/api/credentials.schema.ts),完全建立在 credentials.describe / credentials.set / credentials.unset 之上——值只在这个链路上恰好跨过一次,入站、在 set,而且永远不再回传。schema 镜像 seam 的 credentialRef 守卫(坏名字在到达服务前就以 bad-request 失败),describe 视图只携带 { configured, source, writable }。代理把凭证注入出站请求——那是 LLM 适配器通过 ctx.credentials 的职责。(代理配置表面确实为其自身 settings schema 引用 apiKeyEnv/凭证形态的名字,但解析与脱敏都发生在提供方层。)

被脱敏的内容

  • 链路上:凭证值只出现在 credentials.set 的请求载荷中;describe 与不含 set 的 resolve 表面从不发出它。
  • 日志中assertUsableApiKey 从不回显密钥;YAML 解析错误以 代码+位置 报告,绝不报告有问题的源码行(describeYamlError 只引用代码与行列,因为那行藏有机密);packages/subprocess/subprocess/src/index.ts 里的子进程环境清洗会从 scrubbedParentEnv 丢弃 SENSITIVE_ENV_PATTERNKEY|PASSWORD|SECRET|TOKEN)与所有 DSH_* 名,因此 harness 机密不会隐式泄漏到派生的子进程中。
  • 模型转录中:凭证事件是会话日志事实,不是面向模型的内容;提供方描述的是配置状态,而非值。

延伸阅读

  • 权限与审批——与凭证 seam 并行的审批/沙箱管线;两者都上同一个 ctx.approval/ctx.credentials 服务模型。
  • 守卫:超时与重复提醒——credentialsnotifyUpdated fan-out 也使用的不变量安全网模式。
  • packages/credentials/credentials/src/index.ts——抽象 CredentialProvider 与引用模型。
  • packages/credentials/credentials-local/src/index.ts——分层提供方及其 assertOwnerOnly 存储检查。
  • packages/llm/llm-pi-ai/src/index.ts——resolveApiKey,逐请求的凭证获取。
  • packages/host/apiproxy/src/api/credentials.schema.ts——凭证值单向链路。