Files
deepseek-harness/packages/ui/app-boot/README.zh.md
T
Yichen Jiang 03b534de16 feat(credentials): move the store to .credentials.yaml and layer $DSH_HOME/.env
$DSH_HOME/.env carried two incompatible jobs. As credentials-local's writable
secret store it could not be hoisted into process.env — hoisting makes every
stored key read as a read-only launch override and blocks rotation from the
TUI and the web page. But its name and dotenv format promise an environment
file, so a DEEPSEEK_BASE_URL sitting beside a working DEEPSEEK_API_KEY in the
same file was silently ignored: only the credential provider read the
document, and it addresses credential references alone.

Split the two jobs into two files.

.credentials.yaml is the provider-managed store: a strict YAML mapping of
CredentialRef to non-empty string, no version field, no wrapper level. Because
it holds credentials and nothing else, a non-mapping root, a non-identifier
key, a non-string value, an empty string, a duplicate key, and malformed YAML
are all rejections rather than skipped entries — loud at boot and at a write,
warn-and-keep-last-good on a live reload. The dotenv physical-line editor
gives way to a patch of the parsed document, so comments and untouched entries
keep their formatting and any string value round-trips, multi-line included.
Writer lock, read-modify-write, atomic 0600 write under a 0700 directory,
watcher, self-write suppression, and quiescent disposal are unchanged.

$DSH_HOME/.env becomes the user's ordinary environment layer. app-boot's new
loadLayeredEnv loads the invoking directory's .env then the Harness home's,
giving user < project < inherited; the home resolves from the inherited
environment first, so a project .env cannot redirect it.

Credential precedence is unchanged: the live environment still wins read-only
over the file, and shadowed writes still reject. Whether a provider-managed
store should instead win over the environment is a separate decision.

No migration: a key already in $DSH_HOME/.env keeps resolving through the new
environment layer, as a read-only env source that shadows the stored one.
2026-08-04 14:50:38 +08:00

12 KiB
Raw Blame History

@deepseek-ai/dsh-app-boot

English | 中文

供 app bin(dsh、dsh-cli-demo、dsh-acp-demo)共用的启动粘合层:每个 bin 都是在这些 helper 上构建的精简自执行组合,并以自身诊断前缀参数化。这样,Loader 故障处理知识只需维护一处并接受逐文件覆盖率门禁,不会在已发布产物之间逐渐分化。

导出 职责
resolveConfigPath(path, snapshotMode, cwd?) 生成绝对配置路径;当 snapshotMode === 'replay' 时,把 basename 为 cordis.yml/.yaml 的文件替换为同级 cordis.snapshot.yml
loadEnv(binName, dir?, warn?) 加载已被 git 忽略的 .env(Node process.loadEnvFile);文件不存在不影响启动,文件无法加载时输出一行带标签的警告(默认写入 stderr)
loadLayeredEnv(binName, cwd?, warn?) dsh 产品 CLI(命令行界面)的用户环境:先对调用目录、再对 Harness home 调用 loadEnv,得到 用户 < 项目 < 继承 的层次。Harness home 先从继承的环境解析,因此项目 .env 无法改变它的指向
installFailLoud(binName, proc?, release?) 将启动期或后续未处理的 Loader rejection 转换为一行带标签的 stderr 消息并执行 exit(1);两者之间会等待可选的 release 拆卸回调(以 FAIL_LOUD_RELEASE_TIMEOUT_MS 为上限),使持有终端的界面能在退出前恢复终端;返回卸载函数(供测试使用)
FAIL_LOUD_RELEASE_TIMEOUT_MS installFailLoud 等待其 release 回调的时长;卡住的 disposer 只会延迟致命退出,而不会取消它
assertEntriesLoaded(ctx, binName) 树结算后,如果其中存在已启用但没有 fiber 的条目,则抛出异常,并以 Cordis 启动故障的形式报告每个未解析插件的名称
assertEntriesActivated(ctx, binName) 先执行 assertEntriesLoaded 检查,再在 Loader 结算后等待每个已启用配置项;抛出的错误包含每个失败插件的原始错误堆栈,或每个等待中插件尚未解析的服务
loadPersonalPatches(binName, dir?) 解析 Harness home 中可选的 config.yaml(默认使用 resolveDshHome():先取 $DSH_HOME,否则取 ~/.dsh):其顶层是一个 YAML 数组,内容为 include 的 PatchOptions(按 id 定位的配置覆盖、insert 列表,允许 !!js);文件不存在时返回 undefined,文件不可读、不可解析或内容不是数组时抛出异常
loadOverlayPatches(binName, file) 解析一份必需的 patch 列表文件,其形状与个人配置相同;读取或解析失败时抛出带标签的错误
mountRootInclude(ctx, absoluteConfigPath, patches?) 挂载静态导入的 Include builtin,并保留个人配置 HMR(热模块替换)使用的确切根配置项
watchPersonalPatches(ctx, options) 向现有 Cordis HMR 服务注册 $DSH_HOME/config.yaml;每次新增、变更或移除都会通过调用方的 compose 闭包(应用自有层围绕当前个人 overlay)以事务方式重新组合完整 patch 列表,并返回异步 disposer
boot(binName, absoluteConfigPath, patches?, prepare?) 创建根上下文,向 Loader !!js 配置表达式暴露 dshHomePath(...segments) 并安装 Loader,在配置树条目挂载前执行可选的宿主准备操作(prepare 可以使用 Loader,也可以提供由启动器拥有的上下文插槽,例如 MAIN_SESSION_ID_KEY),再挂载并等待 include 树结算,断言所有条目均已加载并激活,最后返回根上下文——失败时 dispose(资源释放)部分构造的上下文,并以带标签的错误 reject
renderConfigDump(binName, absoluteConfigPath, layers, warn?) 离线合成基础配置与带标签的覆盖层——使用 include 自己的解析器和补丁算法(entryListSchema/applyEntryPatches),因此结果与 boot() 挂载的内容一致——并渲染为 YAML,!!js 表达式原样保留;每段来源相同的连续行之前都有一条 # == 注释,标明贡献该段的文件以及修补过它的层,输出仍是一份可加载的文档;未匹配到行的补丁连同其层标签交给 warn(默认:一行 stderr),读取/解析/形状失败则抛出
addHarnessSourceSection(ctx, sourceRoot) 添加全局 harness:source 提示词段落(顺序紧随 harness 身份、位于 persona 之前),告知 agent(智能体)DSH 实现代码 checkout 的磁盘路径,同时提醒它不得据此推断当前工作目录,而应使用 pwd;如果已启动树没有此项服务,则不执行操作并返回 undefined。这里的服务是 systemPrompt;该段落注册到它的 fiber,因此开发环境 HMR(热模块替换)重新加载系统提示词后,它会消失直至下次启动
HARNESS_SOURCE_SECTION 'harness:source' 段落名称,供 addHarnessSourceSection 注册使用

Loader 结算会在导入或生命周期失败时 reject,并携带失败的配置项与阶段;boot() 会 dispose 部分构造的上下文,并用 bin 名称包装该失败。结算后遗留的配置项由独立审计处理:assertEntriesLoaded 将已启用却没有 fiber 的配置项转换为 rejection 并列出每个未解析插件;assertEntriesActivated 会显式等待每个失败的 fiber,把原始错误堆栈写入启动 rejection,并列出每个等待中配置项尚未解析的服务。抛出错误前,审计会通过一个进程级检查点标记这些 rejection 的确切原因,从而让 installFailLoud 将 Loader 的重复通知合并为一次,而所有无关的未处理 rejection 仍然致命。

Loader 并发挂载各个条目,因此当其他环节失败时,某个界面可能已经持有终端:此时不经过整棵树自身的拆卸就退出,会把 raw 模式、bracketed paste 和键盘协议残留在用户的 shell 上,而尚未返回的终端查询响应会在下一个提示符处显示为字面文本。配置树失败会经 boot() 结算:它先释放部分构建的上下文(从而执行该界面自身的 shutdown),再抛出带标签的 rejection。对于 boot() 看不到的 rejection(插件游离的异步工作在挂载期间或挂载完成后失败),持有终端的 bin 会传入 release,在提交退出前释放整棵树;dsh 在 boot() 的 prepare 回调中捕获根上下文,而不是取其返回值,使该回调覆盖整个挂载窗口。release 执行期间处理函数保持注册并加闩:被报告的始终是第一个 rejection,后续 rejection(包括拆卸自身的)会被吞掉,而不会变成未捕获错误、在拆卸中途杀死进程。

配置中的裸插件 specifier(@deepseek-ai/dsh-*、npm 包(package))通过 Cordis Loader 的内部模块 loader 解析。仓库 bin 会安装 Loader 的可选 peer node-addon-require-builtin;外部调用方必须提供该组件,或者把插件安装到普通 Node import 解析可以找到的位置。相对 specifier 无需原生 helper,并以配置目录为基准解析。构建后的 dsh-app-boot 产物内嵌静态挂载的 Include 实现,但仍将 Loader 保持为外部依赖,因此 include 树与 host 会绑定到同一个 Loader peer。dsh 源码启动器还会将 manifest(元数据清单)声明的 workspace 包映射到其 TypeScript 源码;其配置门禁要求每个 TUI/Web 裸插件都出现在解析所用 manifest 的 dependencies 中。bin 的子进程冒烟测试覆盖内部 loader 路径,而本包的单元测试套件会在进程内使用相对 specifier 配置驱动 boot()。

此包不包含 loader 钩子,也不提供开发模式接口。dsh 应用持有自己的 Node 源码启动钩子,并在启动序列中使用这些 helper;构建后的消费方仍使用普通 Node 包解析。

个人配置

开发者的机器本地偏好位于所有仓库之外的 Harness home 中(默认 ~/.dsh,可由 $DSH_HOME 覆盖;统一由根级 resolveDshHome 解析),并由 dsh CLI(命令行界面)的 TUI、Web 和无头界面(apps/cli)使用;demo bin 会原样启动仓库中提交的树。这里有两个可选文件:

  • .env:用户的普通环境层,由 dsh bin 经 loadLayeredEnv 加载,位于调用目录的 .env 与继承环境之下。它是具有普通环境作用域的普通环境值,而不是密钥边界:由 Harness 拥有并隔离的东西放在 .credentials.yaml 里,后者不会被任何表层提升。因此放进本文件的密钥仍然可以解析——但会作为只读的 env 层遮蔽已存储的那一份,并阻断从 TUI 与 Web 页面轮换密钥。
  • config.yaml:在发布的默认配置上应用 Loader overlay patch,语义与交付的 surface overlay 相同:按 id 定位的 patch 会替换对应条目的整个 config(未改字段也要重述),insert 会添加条目,!!js 表达式则在挂载时插值。如果 patch 指定的条目 id 不在已启动树中,则静默不执行任何操作。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用 overlay,请使用 [] 或删除该文件。

TUI 和 Web 会持续应用 config.yaml 的变更,具体由 watchPersonalPatches 负责;一次性无头运行只读取启动时的值。即使该文件或其直接父目录不存在,watcher 仍会监视确切的个人配置路径;它会串行处理突发变更,并按调用方的层次顺序重新组合个人 patch(surface overlay 在下、应用生成的 patch 在上)。读取失败、解析失败或 Loader 候选被拒时,最后一个可用树会继续运行;HMR 服务记录错误后广播 hmr/config-update-failed(filename, Error),并隔离 observer 失败。上下文 dispose 时会关闭 watcher,并等待进行中的刷新结束。

子进程测试 launcher 会把 DSH_HOME 指向逐测试隔离的目录,确保开发者的个人 overlay 不会泄漏到 fixture(测试前置数据)中。

模型体验

模型通过此包加载的插件树间接受到影响;该树决定最终应用中的提示词、schema、消息和模型适配器。唯一贡献模型可见文本的导出 addHarnessSourceSection,也只有在消费方启动后调用它时才会产生影响。

KV Cache 影响

boot() 不会直接使缓存失效;消费方调用 addHarnessSourceSection 时,会在系统提示词靠前位置、逐请求内容之前添加一行短文本,因此不会使跨轮次缓存失效。请求前缀的其他任何变化均由相应的具名消费方持有。

已知限制与延期工作

  • 裸包 specifier 依赖 Loader 内部机制:生产 bin 需要 Loader 的可选原生 helper;没有该 helper 的进程内调用方必须使用可解析的相对/file specifier,或提供自己的模块解析钩子。
  • 快照回放替换仅识别特定 basename:只有以 cordis.yml 或 cordis.yaml 结尾的配置会映射到同级 cordis.snapshot.yml;自定义配置名称需要调用方自行选择。
  • 环境加载按目录划分且为可选操作:每一层都是一个指定目录下的 .env,失败时发出警告;两个 helper 都不会搜索父目录,也不验证必需变量。loadLayeredEnv 的两层固定为调用目录与 Harness home,需要其他层次的调用方请自行组合 loadEnv。
  • 个人配置采用 patch 形式:按 id 定位的 patch 会替换条目的整个 config,而不是深度合并,因此个人覆盖必须重述需要保留的基础字段。