$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.
5.2 KiB
dsh-credentials-local
English | 中文
文件型凭据 provider:两层来源,一条诚实的优先级。
| 层 | 来源 id | 可写 | 优先 |
|---|---|---|---|
| 活跃进程环境 | env |
否 | 恒定优先 |
$DSH_HOME/.credentials.yaml 文档 |
file |
是(set/unset) |
其余情况 |
环境优先,因为启动时覆盖(DEEPSEEK_API_KEY=… dsh、CI 机密、加载了仓库 .env 的开发 shell)代表本次运行的操作者意图——而它无法从进程内部修改,就必须可见地只读:describe() 报告 source: 'env', writable: false,set/unset 直接拒绝,而不是写下一个读取方永远看不到的变更。解析实时读取 process.env,绝不写回。
配置
| 字段 | 默认值 | 含义 |
|---|---|---|
path |
<harness home>/.credentials.yaml |
凭据文档位置。 |
dshHome |
$DSH_HOME 或 ~/.dsh |
path 缺省时使用的 harness home。 |
watch |
true |
热发布外部编辑。 |
debounceMs |
100 |
watcher 写入稳定窗口。 |
文档本身
一个从凭据引用到值的 YAML mapping,除此之外别无他物:
DEEPSEEK_API_KEY: sk-…
OPENAI_API_KEY: sk-…
该文档只存放凭据,因此任何偏离都是拒绝,而不是跳过某个条目——被静默忽略的键读起来就是「我存进去的密钥没有生效」。非 mapping 的根、非 POSIX 标识符的键、非字符串值、空字符串、重复键以及格式错误的 YAML 全部失败:启动时响亮失败,运行期热重载则告警并保留最后可用快照。没有 version 字段,也没有包装层;格式就是这个 mapping。
写入是对已解析文档打补丁而不是重建,因此注释与所有未触及条目的排版都会保留。直接位于某条目上方的注释属于该条目的注解,会随它一起删除。每次写入都先在 dsh-atomic-write 的跨进程写锁下重读文档、把此前未观察到的一切发布出去,再在仅属主可访问(0700)的目录下以 0600 权限原子提交——因此并发写入者、或落在 watcher 防抖窗口内的外部编辑会被并入,而不是被覆盖。磁盘上已经无法解析的文档会让写入失败,而不是覆盖 provider 读不懂的内容。
任何字符串值都能往返,包括多行值,因此不会再有条目因为缺少可用引号样式而不可写。空的存储值等于不存在(seam 规则)——这也正是文档中的空字符串被直接拒绝的原因:unset 删除键,而不是把它置空。
热重载
外部编辑在快照整体替换后按变更引用逐个发布 credentials/updated——磁盘上删掉的条目绝不在内存滞留。provider 自己的写入按内容识别,只发布属于该次提交的一个事件。运行期文档不可读或无效时保留最后可用快照并告警;文件不存在即空存储;启动时不可读或无效则响亮失败。
安全边界
文档在 0700 目录下以 0600 权限存放,这挡得住其他 OS 用户,挡不住模型。工具进程(bash、文件系统工具)以同一用户身份运行,而已交付的 workspace-write 文件策略限制的是修改而非读取,因此它们读这个文件与读该用户拥有的任何其他文件毫无二致;也没有任何沙箱模式会把它单独挑出来。harness 真正守住的更窄:它绝不把该文档的解析后路径交给模型,也绝不把它载入进程环境——这与用户的普通环境层 $DSH_HOME/.env 不同(见 app-boot 的个人配置)——因此要拿到这个值,需要刻意去读一条并未交给 agent 的路径。
这是审慎,不是边界。必须让提供方密钥远离自身 agent 的部署无法靠文件权限做到;OS 钥匙串 provider——一个模型的进程根本读不到的存储——才是延后的答案,它应当作为平级包与本 provider 并列。
Model Experience
经由消费它的 LLM 适配器间接生效:存储的值为适配器的提供方请求授权,每个模型可见面都归适配器所有。
KV Cache effect
无直接失效;凭据绝不进入请求前缀。
Known Limitations and Deferred Work
- 多行条目拒绝
set/unset——行编辑器不改写会被它破坏的条目;describe把它们报为writable: false,编辑必须直接落到文件上。 - 同一引用的并发写入是后写胜出——写锁加读-改-写让并发写入者不会丢掉彼此的条目,但两个写入者编辑同一个引用时仍以较后的写入为准;没有修订检查。
- 同 UID 进程可以读取该文档——见安全边界:文件效果沙箱模式不会拒绝读取,OS 钥匙串 provider 仍是延后项。
- 无法表示的值响亮失败——控制字符,或同时混用两种引号又含反斜杠的值,无法在 dotenv 行格式中往返。
- 环境变化不可见——每次解析实时读取
process.env,但那里的变化不可能发出事件。 - 原子但不保证崩溃持久——继承自
dsh-atomic-write;存储在启动时重新读取。