Files
deepseek-harness/packages/credentials/credentials-local/README.zh.md
T

14 KiB
Raw Blame History

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 的记录操作(readRecorddescribeRecordlistRecordsmodifyRecorddeleteRecord)管理它。

密钥从哪里来

密钥按一个固定顺序解析——先有值的位置胜出:

位置 可写? 优先于
你启动时的环境(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-keygrant 标签,其中 grant 的 payload 由存储逐字保留,因为只有它的拥有者能解释。产品写入时会保留注释与未触及条目的排版;直接位于某条目上方的注释属于该条目的注解,会随它一起删除。文件只存放凭据,因此任何其他内容都会被明确拒绝,而不是被静默忽略:非 mapping 的根、未知的顶层键、在其键空间内不可寻址的键、类型错误或空的值、未知的记录标签或字段、重复键以及格式错误的 YAML 都会在启动时失败;运行期热重载时则保留最后可用内容并告警。

密钥的值可以是任意文本,包括多行值——不需要任何引号技巧。空值等于「没有密钥」,这正是文件中的空字符串被拒绝的原因:移除密钥是删除它,而不是把它置空。grant 的 payload 必须经受 JSON 往返,进出两个方向都会强制这一点,因此存储会拒绝无法逐字读回的值。如果磁盘上的文件已无法解析,保存会失败,而不是覆盖产品读不懂的内容。

谁能读取该文件

只有你的 OS 用户能读取该文件:产品以仅属主可访问的权限创建它,在 POSIX 上还会拒绝加载任何其他用户可读的文件——错误会提示你运行 chmod 600。Windows 没有可检查的 mode,因此在那里跳过该检查而不是伪造它。agent 不是另一个用户:它的工具进程以你的身份运行,因此它们读这个文件与读你拥有的任何其他文件毫无二致。产品绝不把文件路径交给 agent,也绝不把文件载入环境,因此要拿到某个值,需要刻意去读一条并未交给 agent 的路径。这是审慎,不是边界:必须让提供方密钥远离自身 agent 的部署无法靠文件权限做到。

可能出错的地方

  • 启动环境提供的密钥是只读的——DEEPSEEK_API_KEY=… dsh 在本轮运行中优先,保存或移除它都会被拒绝。请先在启动 shell 中清除该变量。
  • 空值无法保存——存储空字符串会被拒绝;请改为移除密钥。
  • 存储拒绝加载它无法信任的文件——任何其他用户可读的文件、格式错误的 YAML 或无法到达的路径都会在启动时失败;运行期热重载则保留最后可用内容并告警。
  • 同一时刻的修改都会被保留——如果你在产品写入的同时编辑文件,你的变更会被并入,而不是被覆盖。

理解实现

实现细节——点击展开

本节解释提供方背后的设计决策,并指出实现它们的代码位置;可观察行为已在使用本包中完整说明。

设计理念

  • 一套明确的优先级。 继承环境优先,因为它是本次运行的明确意图且无法从进程内部修改;它之下的所有来源都输给受管存储,因此已存密钥永远不会被陈旧的 .env 挤掉。
  • 文档只存放凭据。 它是带 refsrecords 分节的版本化文档,而不是 dotenv 文件:一个 harness 拥有、且绝不物化进环境的存储,不能同时充当用户的环境层,否则会以自己的优先级遮蔽非机密条目。
  • 写入打补丁,重载整体替换。 行编辑在跨进程写锁下保留注释与未触及条目;重载整体交换解析后的快照,已删除条目绝不在内存滞留。
  • 信任攸关处明确报错。 启动与重载都会拒绝不可读、无效或可被属主之外读取的文档;失败的活动重载保留最后可用快照并告警,而不是拖垮进程。

源码地图

文件 职责
src/index.ts 提供方:层解析、严格文档解析、写锁下的引用与记录写路径、watcher 生命周期、权限检查
src/invariant.ts 不变式伴生插件(无运行时不变式;事件生命周期约定归 seam 伴生插件)

解析与写入路径

resolvedescribe 按优先级顺序读取继承环境快照、已解析文档快照与 .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 约定逐步进入环境快照、原子写入原语与启动期环境层。


模型体验

经由 ctx.credentials 的消费方间接生效:消费方拥有存储值所启用的全部模型可见行为。

KV Cache 影响

无直接失效;存储值绝不进入请求前缀。

已知限制与延期工作

这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。

  • 同一引用的并发写入是后写胜出——写锁加读-改-写让并发写入者不会丢掉彼此的条目,但两个写入者编辑同一个引用时仍以较后的写入为准;没有修订检查。
  • 同 UID 进程可以读取该文档——文件效果沙箱模式不会拒绝读取,OS 钥匙串提供方仍是延后项。
  • 环境变化不可见——快照在启动时冻结,因此启动之后 export 的变量既不会进入解析,也不会进入 describe;要更换来自环境的凭据需要重启。
  • 原子但不具备崩溃持久性——继承自 dsh-atomic-write;存储在启动时重新读取。

开发备注

维护者的工作上下文——点击展开

本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为与限制以上文和包代码为准。

OS 钥匙串提供方——一种模型进程根本无法读取的存储——是同 UID 限制的延后答案,应当作为平级包与本提供方并列。seam 的接口还为辅助命令与 KMS 后端提供方预留了空间;目前没有任何一种随附。