ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

DeepSeek Harness 凭据存储与用户环境分层:`.credentials.yaml` 与 `$DSH_HOME/.env` 的职责拆分架构解析

DeepSeek Harness 凭据存储与用户环境分层:`.credentials.yaml` 与 `$DSH_HOME/.env` 的职责拆分架构解析 DeepSeek Harness 凭据存储与用户环境分层.credentials.yaml与$DSH_HOME/.env的职责拆分架构解析【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读本文围绕 DeepSeek Harness 的一项核心架构决策展开把原本由$DSH_HOME/.env一个文件承担的“凭据秘密存储”与“用户环境变量层”两项互不相容的职责拆分为.credentials.yamlprovider 管理的严格凭据文档与$DSH_HOME/.env用户普通环境层两个文件。读完本文你将理解这一拆分的完整问题背景、决策内容、凭据四层优先级语义、0600权限边界的行为与边界、被否定的备选方案以及它们在 credentials-local、app-boot、subprocess 等包中的源码级实现依据。本文以仓库内架构决策笔记 2026-08-04-credentials-yaml-and-user-environment-layer.mdStatus: implemented为主体骨架并结合其后续实现代码与测试进行纵深印证。问题背景一个.env文件承担了两项互不相容的职责拆分前的$DSH_HOME/.env同时背着两项彼此冲突的工作它是credentials-local的可写秘密存储。[credentials-local](https://link.gitcode.com/i/06665452728285aba6a3ad566c03edda)把 API Key 等秘密写入该文件因此任何表层都不能把它提升hoist进process.env——一旦提升存储的每个键都会被当作只读的启动覆盖项读取用户就无法再从 Models 页面轮换rotation凭据。它的文件名与 dotenv 格式承诺它是一个环境文件。用户自然会往里面放非秘密的配置项如DEEPSEEK_BASE_URL但这些值实际上哪里都到不了只有凭据 provider 会读取这个文档而它只处理凭据引用。于是出现了一个极具误导性的静默失效场景在同一个文件里DEEPSEEK_BASE_URL紧挨着一个能正常工作的DEEPSEEK_API_KEY前者却被悄悄忽略——用户以为配置生效了实际上毫无作用。“一个文件不可能既是 Harness 拥有并隔离的存储又是按普通环境规则传播的一层。”——原笔记的核心判断这一矛盾的伏笔来自上游的请求级 LLM 配置与凭据缝决策当时为了让 Harness 与同类产品的 home.env行为对齐选择了 dotenv 作为凭据文档格式。直到一个“非秘密”也需要使用同一个文件时混用问题才真正暴露出来。决策总览一个 Harness Home 下的两个文件拆分后Harness Home$DSH_HOME默认为~/.dsh下由两个文件各司其职文件职责归属是否被物化进process.env$DSH_HOME/.credentials.yaml凭据秘密存储provider 管理、可写credentials-localprovider从不$DSH_HOME/.env用户普通环境层发现式、只读回退dsh-app-boot的loadLayeredEnv是.credentials.yaml是 provider 管理的存储一个严格的CredentialRef→ 非空字符串的 YAML 映射。$DSH_HOME/.env是用户的普通环境层由dsh-app-boot的loadLayeredEnv解析最终物化进process.env因此能到达子进程同时要经过子进程凭据擦洗。.credentials.yaml由 provider 管理的严格凭据存储文档格式只装凭据的严格映射按决策笔记的定义新文档是严格的CredentialRef到非空字符串的 YAML 映射没有version字段、没有包裹层DEEPSEEK_API_KEY: sk-… OPENAI_API_KEY: sk-…因为“文档只装凭据、别无其他”所以任何偏离都是拒绝rejection而不是跳过skipped entry。以下所有情况都会失败loud fail非映射根non-mapping root如数组、标量键不是 POSIX 标识符不能作为凭据引用被寻址值不是字符串空字符串值重复键YAML 语法错误失败时机有三类时机行为启动时boot直接失败进程不启动写入时write写入操作被拒绝热重载live reload警告并保留最后一份良好的快照继续服务被静默忽略的键会读作“我存的秘密没有生效”——这正是本次拆分要消除的失效模式。相关严格解析逻辑见 credentials-local 源码parseCredentialsDocument使用parseDocument时开启prettyErrors与uniqueKeys重复键直接作为解析错误抛出非空文档必须声明version未知顶层键、不可寻址键、错误类型值、未知 record 标签与字段一律拒绝。补丁式写入dotenv 行编辑器被替换原来按物理行编辑的 dotenv 编辑器被替换为对已解析文档的补丁patch收益明确注释与未改动条目的格式原样保留任意字符串值包括多行值都能完整往返round-trip无需引号技巧不再存在“因为找不到合适的引号风格而无法写入”的条目。实现上写入路径通过mutableDocument/renderRef基于 YAML 解析树原地修改见 index.ts#L425-L448删除条目时还会连带移除其上方注释块deleteSectionEntry避免注释“漂移”到另一条凭据头上。写入协议与生命周期保持不变的部分以下机制在拆分前后保持不变直接沿用到新文档跨进程写锁writer lockread-modify-write先读后改再写外部并发编辑不会互相覆盖当前实现中锁等待上限为DOCUMENT_LOCK_WAIT_MS 30_000index.ts#L112。原子0600写入目录为0700。精确路径 watcher监听文件本体而非父目录。内容相等自写抑制watcher 事件内容与文本缓存相等则视为 no-op——这同时抑制了“自己写自己”的假重载。静默quiescent销毁dispose 置关闭标志、停止接受事件、关闭 watcher并等待排队操作完成确保拆除后不再发布事件。从决策到实现文档格式的后续演进需要说明的是决策笔记2026-08-04记录的初始形态是不带version的扁平映射而当前仓库中已实现的文档在此基础上演进为带version: 1、含refs与records两个键空间的版本化文档见 credentials-local README 的“The credential file”一节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: prodrefs按环境变量名保存键值records按owner/id保存每插件凭据标签为api-key或grant源码中仍保留对预发布扁平布局的就地迁移renderFlatLayoutMigration将无version的裸映射原样缩进嵌套进refs:使值、注释与拼写逐字节保留index.ts#L242-L266任何无法证明能理解的文档都拒绝改写而不是当成空存储读取。provider 的插件配置项同样可从 README 与源码确认字段默认值含义pathharness home/.credentials.yaml凭据文件位置resolveSpec中resolve(config.path ?? join(resolveDshHome(config.dshHome), CREDENTIALS_FILENAME))index.ts#L88-L94dshHome$DSH_HOME或~/.dsh省略path时使用的 Harness Homewatchtrue磁盘变化时自动重载文件debounceMs100变化后等待多少毫秒再重载一个值得注意的实现细节诊断信息绝不引用值。YAML 解析器自带的消息会引用出错源码行——在凭据文档里那行就是秘密本身因此describeYamlError只输出错误码与行列位置index.ts#L159-L164键名可以打印值永远不能。$DSH_HOME/.env用户的普通环境层拆分的另一半是让$DSH_HOME/.env回归它名字承诺的本职——用户的普通环境层。loadLayeredEnv的分层语义user project inheriteddsh-app-boot中的loadLayeredEnv按顺序解析两个文件调用目录invoking directory的.env项目层Harness Home 的.env用户层得到user project inherited的优先级只有在进程没有更高层值的情况下每个被接受的键才会被物化materialize。源码实现印证了这一点app-boot/src/index.ts#L180-L201先解析两个文件再遍历每一层仅当process.env[name] undefined时才写入最后生成一个冻结的环境快照来源依次为processproject-envuser-env。Harness Home 的解析顺序防止项目.env重定向用户文档一个关键的防御性设计Harness Home 在加载两个文件之前先从继承环境中解析出来resolveDshHome()因此项目.env无法通过设置DSH_HOME之类变量来重定向“到底读哪一个用户文档”。这与“发现式文件不得决定引导行为”的配置来源所有权架构笔记一脉相承。边界只有产品 CLI 分层只有产品 CLIdsh会对这两个文件分层SDK 与示例 bin 继续通过loadEnv只加载自己目录下的.envapp-boot/src/index.ts#L81-L92不得继承开发者机器的$DSH_HOME——否则就会把开发者本地的DSH_*事实和凭据泄漏进 SDK 进程。此外app-bootREADME 明确决定进程如何启动的变量PATH、代理、DSH_*、XDG_*等会被从文件中拒绝应通过导出export提供。凭据优先级继承环境与发现文件的区分credentials-local的四层解析顺序在 src/index.ts 顶部注释与 README 中均有明确记录层级来源可写胜过谁继承环境launched-in environmentDEEPSEEK_API_KEY… dsh、CI secret、容器-e否只读一切受管文档.credentials.yamlset/unset是两个.env项目.envinvocation cwd/.env此处不可写Home.envHome.env$DSH_HOME/.env此处不可写无关键语义区分继承环境与发现文件discovered files被明确区分——继承环境的值是本次运行的只读覆盖项per-run override启动时的显式意图产品内部无法编辑因此报告为只读向其写入会被拒绝受管文档次之这保证 Models 页面存下的键立即生效即使旧的键还躺在某个.env里项目与用户的.env值只是可写回退fallback。因此set是替换发现文件中的值而不是因为“扁平的process.env视图看起来被遮蔽shadowed”而拒绝一次写入。无迁移策略拆分不提供任何迁移已经存在于$DSH_HOME/.env中的键继续作为回退解析而一旦 Models 页面存储了对应引用受管文档立即胜出。用户旧文件不会失效只是地位降级——这是一个“真实”的结果而不是一个“静默”的结果。影响与代价权衡得失放弃的Given up留在$DSH_HOME/.env中的键会被物化进process.env因此在子进程凭据擦洗之下会到达子进程而不是停留在 provider 内部。它仍然是.credentials.yaml之下的可写回退而 Harness 应拥有并隔离的秘密应当放入受管文档——受管文档永远不会被物化。买到的Bought用户.env里的非秘密终于生效了——这正是最初缺陷文档格式能拒绝它无法服务的内容拒绝而非静默跳过0600只覆盖一个只装秘密的文件而不是一个用户被告知“可以放普通配置”的文件。0600的读端强制不止写端provider 写出的0600同样在读取时被强制源码assertOwnerOnlyindex.ts#L127-L146检查GROUP_OTHER_BITS 0o077POSIX文档只要带有任何 group 或 other 权限位启动即在读取内容之前失败——启动时和每次重载都检查诊断信息会指名修复方法chmod 600 fileWindows没有可检查的 mode其 ACL 无法在此表达因此跳过检查而非假装检查。测试用例印证了这一行为credentials-local/tests/local.spec.ts#L170-L179以0o644模式写入文档后挂载 provider期望抛出/readable beyond its owner \(mode 644\)/而新写入的文档在 POSIX 上模式必须为0o600local.spec.ts#L314-L322。0600边界的本分阻止其他 OS 用户不阻止模型0600边界能阻止其他操作系统用户读取但阻止不了模型agent——agent 的工具进程以你的 OS 用户身份运行读取该文件与读取你拥有的其他文件并无区别。这一限制由 provider README 明确负责连同 OS 钥匙串 provider 的延迟计划。这是“谨慎discretion”不是“边界boundary”若某个部署必须让 provider key 远离自己的 agent仅靠文件权限做不到。被否定的备选方案及其理由原笔记逐条记录了五个被否定的方案理解它们能更清楚拆分的边界保留一个$DSH_HOME/.env并教 CLI 把它 hoist 进环境否决hoisting 存储正是使存储键不可轮换的原因——这也就是 app-boot README 最初记录该排除项的原因。冲突在于“一个文件的两种职责”而不是 loader。$DSH_HOME/.credentials.env——第二个 dotenv 文件否决dotenv 适合环境层但无法表达“一个以凭据引用为索引的受管文档”。它无法拒绝非字符串值或不可寻址的键而且其行编辑器早已拒绝它无法引用的值导致条目可读不可写。给新文档加version字段否决按当时决策格式只是一个受模式约束的字符串映射没有历史变体需要区分产品尚未发布时改变结构并拒绝旧结构胜过承诺一个迁移协议。注当前仓库实现中已引入version: 1并保留对预发布扁平布局的就地迁移属于后续演进。首次运行时把凭据形状的键迁出$DSH_HOME/.env否决迁移代码会把一个短命格式变成长期维护面而且“判断未知文件里哪些键是秘密”恰恰就是本次拆分要消除的歧义。旧文件继续作为环境工作是真实而非静默的结果。完全去掉用户.env层只保留继承环境此处否决为超范围这是一个自洽的设计层更少、每个值只有一个位置但它移除了用户已有的工作流分层问题应归属于推迟的优先级决策而不是本次拆分。相关阅读与源码溯源本决策笔记2026-08-04-credentials-yaml-and-user-environment-layer.md含中文版 .zh.md上游决策请求级 LLM 配置与凭据缝request-level LLM config credential seam——解释了“秘密是引用、值在ctx.credentials之后”及按请求解析的设计相关决策配置来源所有权——发现式文件不得决定引导行为Provider 实现credentials-local/src/index.ts 与 credentials-local READMEProvider 测试credentials-local/tests/local.spec.ts环境分层实现boot/app-boot/src/index.tsloadEnv与loadLayeredEnv与 app-boot README子进程擦洗subprocess/subprocess READMEscrubbedParentEnv凭据形状的名称与环境中DSH_*事实会被擦洗调用方显式env在擦洗后合并支撑工具dsh-home-pathsresolveDshHome、dsh-launch-environment启动时冻结快照、dsh-atomic-write写锁与原子替换实操要点速查秘密放$DSH_HOME/.credentials.yaml或通过 Models 页面 /ctx.credentials.set(ref, value)写入它不会进入process.env、不会到达子进程普通配置放$DSH_HOME/.env或项目目录.env优先级为user project inherited会按普通环境规则生效并传播继承环境DEEPSEEK_API_KEY… dsh是只读的本次运行覆盖想改就清掉启动 shell 里的变量.credentials.yaml必须0600否则 POSIX 上启动失败并提示chmod 600空字符串值等于“没有键”删除用删除操作而非清空文档里任何偏离“凭据映射”的内容都会被响亮拒绝而不是被静默跳过。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表