14 KiB
description, kind
| description | kind |
|---|---|
| 面向用户与维护者的文件型凭据提供方:选择、配置或排查本地凭据存储及其环境分层。 | package-reference |
@deepseek-ai/dsh-credentials-local
English | 中文
概述
dsh-credentials-local 是产品默认的本机凭据存储:harness home 下的一个私有文件,存放 API 密钥与其他机密,可由配置界面写入,你自己编辑该文件时也会自动重载。该文件是带版本的文档,含一个存放密钥值的 refs 分节和一个存放持久化按插件记录的 records 分节,因此授权 grant 或提供方环境值能与密钥一起跨重启保留。密钥来自四个位置,顺序固定:你启动时的环境优先,其次是存储文件,再次是项目和主目录的 .env 文件。你保存的密钥会立即生效,即使某个 .env 里还留着更旧的密钥。只有你的 OS 用户能读取该文件,而且产品绝不把文件路径交给 agent(智能体)。
目录
使用本包
本包为组合提供本地凭据存储:API 密钥与其他机密只需保存一次,之后每个按名引用它们的请求都会用到。常用路径是显式的:加载存储、通过配置界面或 ctx.credentials 保存密钥,然后在需要时由产品解析。
何时使用
把它作为默认本地存储:产品的 base 组合会加载它,你通过配置界面保存的密钥会立即生效。当部署必须让提供方密钥远离自身 agent 时选择其他存储——文件权限做不到这一点,因为 agent 的工具进程以你的 OS 用户身份运行(见「谁能读取该文件」)。
设置
- name: '@deepseek-ai/dsh-credentials-local'
config:
path: /absolute/path/to/.credentials.yaml
| 字段 | 默认值 | 含义 |
|---|---|---|
path |
<harness home>/.credentials.yaml |
凭据文件所在位置 |
dshHome |
$DSH_HOME 或 ~/.dsh |
path 缺省时使用的 harness home |
watch |
true |
文件在磁盘上变化时自动重载 |
debounceMs |
100 |
变化后等待这么久再重载,单位为毫秒 |
生成的配置目录是每个受支持字段及其 JSDoc 的穷尽式真源。
存储与移除密钥
用 set 保存密钥、用 unset 移除、用 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')
await ctx.credentials.set(ref, 'sk-…') // save
await ctx.credentials.describe(ref) // { configured, source?, writable } — never the value
await ctx.credentials.unset(ref) // remove
你保存的密钥会被下一个按名引用它的请求使用;describe 报告它是否已设置、来自哪里、能否写入——绝不返回值本身。记录也持久化在同一文件中:插件按 <owner>/<id> 寻址一条记录,并用 seam 的记录操作(readRecord、describeRecord、listRecords、modifyRecord、deleteRecord)管理它。
密钥从哪里来
密钥按一个固定顺序解析——先有值的位置胜出:
| 位置 | 可写? | 优先于 |
|---|---|---|
你启动时的环境(DEEPSEEK_API_KEY=… dsh) |
否 | 一切 |
| 存储文件 | 是(set/unset) |
两个 .env 文件 |
项目的 .env(<invocation cwd>/.env) |
不在此处 | 主目录 .env |
主目录的 .env($DSH_HOME/.env) |
不在此处 | 无 |
启动环境优先,因为按次覆盖——DEEPSEEK_API_KEY=… dsh、CI 机密、容器 -e——代表本次运行的明确意图;它无法从产品内部修改,因此被报告为只读,写入会被拒绝。其他一切来源都输给存储文件,这正是你保存的密钥会立即生效的原因,即使某个 .env 里还留着更旧的密钥;没有存储任何东西时,那两个 .env 层会参与解析。环境层是启动时拍摄的启动器环境快照,因此启动之后才导出的变量不会被看到。
凭据文件本身
带版本的 YAML 文档,每个键空间一个分节,除此之外别无他物:
version: 1
refs:
DEEPSEEK_API_KEY: sk-…
OPENAI_API_KEY: sk-…
records:
llm-pi-ai/openai-codex:
kind: grant
payload: # written verbatim; this provider does not interpret it
type: oauth
access: eyJhbGciOi…
refresh: rft_9f8e7d…
expires: 1786000000000
llm-pi-ai/amazon-bedrock:
kind: api-key # environment values, no key: this route uses an AWS profile
env:
AWS_PROFILE: prod
llm-pi-ai/amazon-bedrock-dev:
kind: api-key # neither: the owner confirmed the ambient credential chain
你可以直接编辑该文件——存储会自动重载并接收变更,包括你删除的密钥或记录。refs 按环境变量名存放密钥值;records 按 <owner>/<id> 存放按插件凭据,每条都带 api-key 或 grant 标签,其中 grant 的 payload 由存储逐字保留,因为只有它的拥有者能解释。产品写入时会保留注释与未触及条目的排版;直接位于某条目上方的注释属于该条目的注解,会随它一起删除。文件只存放凭据,因此任何其他内容都会被明确拒绝,而不是被静默忽略:非 mapping 的根、未知的顶层键、在其键空间内不可寻址的键、类型错误或空的值、未知的记录标签或字段、重复键以及格式错误的 YAML 都会在启动时失败;运行期热重载时则保留最后可用内容并告警。
密钥的值可以是任意文本,包括多行值——不需要任何引号技巧。空值等于「没有密钥」,这正是文件中的空字符串被拒绝的原因:移除密钥是删除它,而不是把它置空。grant 的 payload 必须经受 JSON 往返,进出两个方向都会强制这一点,因此存储会拒绝无法逐字读回的值。如果磁盘上的文件已无法解析,保存会失败,而不是覆盖产品读不懂的内容。
谁能读取该文件
只有你的 OS 用户能读取该文件:产品以仅属主可访问的权限创建它,在 POSIX 上还会拒绝加载任何其他用户可读的文件——错误会提示你运行 chmod 600。Windows 没有可检查的 mode,因此在那里跳过该检查而不是伪造它。agent 不是另一个用户:它的工具进程以你的身份运行,因此它们读这个文件与读你拥有的任何其他文件毫无二致。产品绝不把文件路径交给 agent,也绝不把文件载入环境,因此要拿到某个值,需要刻意去读一条并未交给 agent 的路径。这是审慎,不是边界:必须让提供方密钥远离自身 agent 的部署无法靠文件权限做到。
可能出错的地方
- 启动环境提供的密钥是只读的——
DEEPSEEK_API_KEY=… dsh在本轮运行中优先,保存或移除它都会被拒绝。请先在启动 shell 中清除该变量。 - 空值无法保存——存储空字符串会被拒绝;请改为移除密钥。
- 存储拒绝加载它无法信任的文件——任何其他用户可读的文件、格式错误的 YAML 或无法到达的路径都会在启动时失败;运行期热重载则保留最后可用内容并告警。
- 同一时刻的修改都会被保留——如果你在产品写入的同时编辑文件,你的变更会被并入,而不是被覆盖。
理解实现
实现细节——点击展开
本节解释提供方背后的设计决策,并指出实现它们的代码位置;可观察行为已在使用本包中完整说明。
设计理念
- 一套明确的优先级。 继承环境优先,因为它是本次运行的明确意图且无法从进程内部修改;它之下的所有来源都输给受管存储,因此已存密钥永远不会被陈旧的
.env挤掉。 - 文档只存放凭据。 它是带
refs与records分节的版本化文档,而不是 dotenv 文件:一个 harness 拥有、且绝不物化进环境的存储,不能同时充当用户的环境层,否则会以自己的优先级遮蔽非机密条目。 - 写入打补丁,重载整体替换。 行编辑在跨进程写锁下保留注释与未触及条目;重载整体交换解析后的快照,已删除条目绝不在内存滞留。
- 信任攸关处明确报错。 启动与重载都会拒绝不可读、无效或可被属主之外读取的文档;失败的活动重载保留最后可用快照并告警,而不是拖垮进程。
源码地图
| 文件 | 职责 |
|---|---|
src/index.ts |
提供方:层解析、严格文档解析、写锁下的引用与记录写路径、watcher 生命周期、权限检查 |
src/invariant.ts |
不变式伴生插件(无运行时不变式;事件生命周期约定归 seam 伴生插件) |
解析与写入路径
resolve 与 describe 按优先级顺序读取继承环境快照、已解析文档快照与 .env 后备层。set/unset 排入同一条独占操作链:入口检查提前拒绝(已释放、空值、被环境遮蔽),队列在运行时会重新判定,随后在写锁下执行读-改-写、提交,并恰好触发一次 credentials/reference-updated。
modifyRecord 走同一条链与同一把锁:它重新读取文档、把当前记录交给变更函数、准入其结果——非空的 api key、能经受 JSON 往返的 grant payload——整体渲染该记录并提交,恰好触发一次 credentials/record-updated。并非由产品 CLI 启动的组合只有继承环境这一层。
重载生命周期
watcher 事件或 ready 对账把一次刷新排到同一链条之后。reconcileFromDisk 重新检查权限、重读文本,文本有差异时整体替换两个快照,并按变更引用或记录逐个发布事件;与文本缓存一致的内容——包括提供方自己的写入——是 no-op。释放时设置 closed 标志、停止接收事件、关闭 watcher,并等待排队操作结算完毕,确保 teardown 之后不再有任何发布。
文档版本化
文档携带 version: 1,每次写入都会盖上版本戳。启动时若识别出预发布扁平布局——没有 version 的裸引用名 mapping——会在写锁下就地升级,把原始行嵌套到 refs: 之下,值、注释与拼写逐字节保留;任何其他无版本形态都会按名拒绝,而不会被当作空存储。运行期热重载绝不迁移:中途恢复的扁平文档会保留最后可用快照,直到下一次启动。
诊断信息绝不引用值
YAML 解析器自己的消息会引用出错的那行源码,而在这份文档里那行就是机密本身。因此每条诊断只携带错误码与位置——键名可以安全打印,值不行。
进一步探索
当提供方级约定不够用时阅读以下页面。它们从 seam 约定逐步进入环境快照、原子写入原语与启动期环境层。
- 凭据引用 seam——
resolve、describe、set、unset、记录操作与 seam 的更新事件。 - 凭据子系统参考——
CredentialRef、按操作解析、对 UI 安全的CredentialInfo、提供方层。 - 启动环境快照——解析读取的冻结层快照,而非
process.env。 - 原子写入——每次写入所用的写锁与原子替换。
- 应用启动与 Harness home 各层——产品 CLI 如何把
.env载入快照与process.env。
模型体验
经由 ctx.credentials 的消费方间接生效:消费方拥有存储值所启用的全部模型可见行为。
KV Cache 影响
无直接失效;存储值绝不进入请求前缀。
已知限制与延期工作
这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
- 同一引用的并发写入是后写胜出——写锁加读-改-写让并发写入者不会丢掉彼此的条目,但两个写入者编辑同一个引用时仍以较后的写入为准;没有修订检查。
- 同 UID 进程可以读取该文档——文件效果沙箱模式不会拒绝读取,OS 钥匙串提供方仍是延后项。
- 环境变化不可见——快照在启动时冻结,因此启动之后 export 的变量既不会进入解析,也不会进入
describe;要更换来自环境的凭据需要重启。 - 原子但不具备崩溃持久性——继承自
dsh-atomic-write;存储在启动时重新读取。
开发备注
维护者的工作上下文——点击展开
本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为与限制以上文和包代码为准。
OS 钥匙串提供方——一种模型进程根本无法读取的存储——是同 UID 限制的延后答案,应当作为平级包与本提供方并列。seam 的接口还为辅助命令与 KMS 后端提供方预留了空间;目前没有任何一种随附。