跳到主要内容
路径文档

凭据管理

一句话版:DSH 的凭据系统 ctx.credentials 三条铁律:配置只存引用、不存密钥;按操作逐次解析;空存储值等于"未配置"。密钥真正存哪由 provider 决定(默认 ~/.dsh 下)。

这一篇讲清楚"密钥在哪、怎么被读取、怎么轮换"。

一、三条原则(源码 README)

  1. 配置携带对密钥的引用,从不携带密钥本身apiKeyEnv: DEEPSEEK_API_KEY 是引用;值活在 credential provider 里。所以 settings/cordis 文档可安全同步、可渲染,describe() 能答"配没配、来自哪、能不能写",而从不持有值;轮换密钥不碰任何配置文件。
  2. 消费者按操作解析resolve(ref) 在每次操作开始时调用(LLM adapter 每次模型请求解析一次),跨操作不缓存:这让改了的密钥立刻对下一次请求生效,不用重启插件
  3. 空存储值 = 缺失resolve 跳过它,describe 报未配置。一个空串永远不能伪装成已配置的密钥。

二、表面 API

import type { Context } from '@deepseek-ai/cordis'
import { credentialRef } from '@deepseek-ai/dsh-credentials'

declare const ctx: Context

const ref = credentialRef('DEEPSEEK_API_KEY') // POSIX shell 标识,branded
const hit = await ctx.credentials.resolve(ref) // { value, source } | undefined
const info = await ctx.credentials.describe(ref)// { configured, source?, writable } — 从不是值
await ctx.credentials.set(ref, 'sk-…') // 只读源遮蔽时拒绝
await ctx.credentials.unset(ref) // 缺失 no-op;同上遮蔽规则

credentials/updated (ref) 在 committed 改变后触发(用于配置 UI 刷新"已配置"徽标)。消费者不需要它:因为他们按操作 re-resolve。

三、解析流程与优先级

本地 provider(dsh-credentials-local)把四层来源按一条诚实优先级叠起来:

来源 id可写谁赢
继承的进程环境env永远
$DSH_HOME/.credentials.yaml 文档file是(set/unset压过两个 .env
<调用目录>/.envproject-env此处不可写压过用户 .env
$DSH_HOME/.envuser-env此处不可写兜底

逐行读:

  • 启动环境永远赢DEEPSEEK_API_KEY=… dsh、CI secret、容器 -e 是本次运行的算子意图;又因为它无法从内部改写,必须可见地只读——describe()source: 'env', writable: falseset/unset 直接拒绝。
  • 受管存储压过 .env 兜底:Models 页写进的 key 立即生效,哪怕 .env 里还躺着旧 key;两个 .env 层只在"什么都没存"时才解析,存一个 key 就把它们替换成实际生效来源。
  • 快照,不是 process.env:产品 CLI 下解析读的是 launcher 冻结的环境快照,只有它能说清一个值来自启动 shell 还是某个文件。

四、遮蔽规则(fail-loud)

set/unset故意的 fail-loud:当一个只读源(本地 provider 里就是活动进程环境)当前供应这个 ref,写入会"看起来成功"但解析仍返回遮蔽值:seam 直接拒绝describe().writable 让 UI 预先把这个 ref 渲染成只读。

场景结果
进程环境里有 DEEPSEEK_API_KEY,代码再 set拒绝;解析仍是环境里的旧值
.env 里有旧 key,set 写进 store成功;store 立即覆盖 .env 成为生效来源
set(ref, '')(空串)拒绝;空存储值 = 缺失,unset 才是删 key

五、Provider

dsh-credentials-local 把继承的进程环境叠加在它管理的 $DSH_HOME/.credentials.yaml 文档上,launcher 的 project/user .env 层作为 fallback。

llm-pi-ai 的 provider-native discovery 是另一条路径:无 credential ref 的 provider 直接读取进程环境,看不到 Harness 托管 store。AWS 场景需显式导出 AWS_PROFILE 或访问密钥变量;只有 ~/.aws/credentials 文件还不够。

配置项默认含义
path<harness home>/.credentials.yaml凭据文档位置
dshHome$DSH_HOME~/.dshpath 省略时使用的 Harness home
watchtrue热发布外部编辑
debounceMs100watcher 写静默窗口

文档是"凭据引用 → 值"的 YAML 映射、仅此而已;非映射根、非 POSIX 标识的 key、非字符串值、空串、重复 key、坏 YAML 都整份拒绝(启动时响亮失败,热重载时 warn 并保留最后一份好快照)。写入补丁式改解析后的文档(注释与未触碰项的排版保留),并在 dsh-atomic-write 的跨进程写锁下先重读合并、再以 0600(目录 0700)原子提交。外部编辑在快照整体替换后按变化的 ref 发布 credentials/updated,磁盘上删掉的条目绝不在内存残留。

seam 的形状为 keyring / helper-command / KMS-backed provider 留了空间;远程 settings provider 永远不需要携带密钥。

六、安全边界与已知限制

边界只到"别的 OS 用户"为止,不挡模型0600 + 0700 挡住的是其它用户;工具进程(bash、文件系统工具)以同一用户运行,workspace-write 文件策略限制的是而非。harness 实际守住的更窄:从不把文档路径交给模型、也从不载入进程环境($DSH_HOME/.env 才是普通环境层)——这是克制,不是边界。

限制说明
同 ref 并发写last-write-wins:写锁 + read-modify-write 防丢条目,但两写者改同一 ref 仍后者胜
同 UID 进程可读file-effect 沙箱不拒读;真正隔离需 OS-keychain provider(模型进程读不到的存储,deferred)
环境变更不可见快照启动时冻结,运行中导出变量不进解析;改环境来源凭据需重启
原子而非 crash-durable继承 dsh-atomic-write;store 在启动时重读

七、轮换演练

密钥换新不必改任何配置文件——因为配置只存引用。按"它现在从哪来"分三条路:

当前来源(describe().source轮换步骤
env改启动环境(新 shell / CI secret / 容器 -e),重启生效
fileset(ref, 新值) 原子写 0600 立即生效;或直接编辑文件,watcher 热发布
project-env / user-env直接 set 写进 store 覆盖 .env;或先清 .env 旧值再 set

完成后用 describe(ref) 核对 configured: truesource 指向预期层;旧值仍躺在 .env 里也没关系——只要 store 有值,.env 就退回兜底。

八、对模型的影响

间接的,经消费它的 LLM adapter:解析出的值授权 provider 请求,adapter 拥有所有模型可见面。

九、验证

# 看凭据文件(0600)
ls -la ~/.dsh/.credentials.yaml
# 组合里看 credentials 是否装载
dsh web --dump-config | grep -i credential

下一步