DSH / Atlas
2026-10-09implementedarchitecture

Stored credentials take precedence over the launch environment

存储的凭据优先于启动环境

[Configuration source ownership](2026-08-04-configuration-source-ownership.md) ranked the inherited process environment above `$DSH_HOME/.credentials.yaml`, and `credentials-local` refused `set` and `unset` for a name the launching shell exported. Two kinds of stored credential then had opposite precedence. pi-ai's resolvers read the credential record stored under `llm-pi-ai/<provider>` before any environment variabl

English

Problem

Configuration source ownership ranked the inherited process environment above $DSH_HOME/.credentials.yaml, and credentials-local refused set and unset for a name the launching shell exported. Two kinds of stored credential then had opposite precedence. pi-ai's resolvers read the credential record stored under llm-pi-ai/<provider> before any environment variable, so a stored OAuth grant or api-key record won over an exported key. A key that the Models page stored under a reference lost to the same exported name. Which credential a request used therefore depended on the provider family, and the Models page disabled its key field whenever the name was exported (#5527).

Decision

A stored credential wins for every provider. LocalCredentialProvider resolves a reference in this order:

$DSH_HOME/.credentials.yaml        (provider-managed, written by set/unset)
> inherited process environment    (fallback; the provider never writes it)
> <invocation cwd>/.env            (fallback; the provider never writes it)
> $DSH_HOME/.env                   (fallback; the provider never writes it)

A save always writes the winning layer, so describe reports writable: true for every reference, and set and unset never refuse a name because it is exported. resolve and describe still report the supplying layer as source (file, env, project-env, or user-env). authContextFrom in llm-pi-ai answers pi-ai's env() from the credential seam before the launch environment, so a pi-ai resolver reads its stored record first, then a stored reference, then the fallbacks in the order above.

The non-secret ordering and the environment restrictions in configuration source ownership are unchanged. This note supersedes only its credential ordering.

Per-run overrides

The previous order existed so that CI, containers, and a one-shot DEEPSEEK_API_KEY=… dsh could override a credential without editing machine state. A fresh CI or container harness home has no .credentials.yaml, so its exported secret is still the effective value. A harness home holds a stored key only because someone saved it there, through the Models page or by editing the file. To use an exported value for that name, the user deletes the refs entry, or launches with a separate DSH_HOME.

Alternatives considered

Keep the launch environment on top. It keeps per-run overrides without touching the store, but it keeps the opposite precedence of stored records and stored references, and the Models page cannot save or replace a key while the launching shell exports the name.

Refuse a write while only the launch environment supplies the name. Under the new order such a write takes effect, because the stored value outranks the environment. Refusing it would block the first save of a key for every exported name, and it would keep the read-only field state that the new order removes.

Warn when a stored value hides an exported variable. The provider would compare layers on every resolution and report the result for users who keep both values on purpose, such as a shell profile that exports a key for other tools. The Models page help text states the order, and describe reports the supplying layer.

Consequences

  • A key left in .credentials.yaml overrides an exported variable with the same name, including a CI secret on a machine whose harness home stores a value for that name. The upgrade guide tells affected users how to keep the exported account.
  • The Models page can always save, replace, or remove a key, and it does not disable the key field for an exported name.
  • There is no per-run flag above the store. A one-run override takes a separate DSH_HOME.
  • packages/credentials/credentials-local/tests/local.spec.ts covers a store over a launch environment value and its return after unset; packages/llm/llm-pi-ai/tests/auth.spec.ts covers a stored credential that wins over every launch layer.

中文

问题

配置来源所有权 把继承的进程环境排在 $DSH_HOME/.credentials.yaml 之前,并且 credentials-local 会拒绝对启动 shell 已导出的名字执行 set 和 unset。于是两类存储的凭据具有相反的优先级。pi-ai 的解析器先读取存储在 llm-pi-ai/<provider> 下的凭据记录,再读取任何环境变量,因此存储的 OAuth 授权或 api-key 记录会压过导出的密钥;而 Models 页面以引用形式存储的密钥会输给同名的导出变量。一次请求使用哪个凭据因此取决于提供方类别,而且只要该名字被导出,Models 页面就会禁用其密钥输入框(#5527)。

决策

存储的凭据对每个提供方都优先。 LocalCredentialProvider 按以下顺序解析引用:

$DSH_HOME/.credentials.yaml        (provider-managed, written by set/unset)
> inherited process environment    (fallback; the provider never writes it)
> <invocation cwd>/.env            (fallback; the provider never writes it)
> $DSH_HOME/.env                   (fallback; the provider never writes it)

保存总是写入胜出的那一层,因此 describe 对每个引用都报告 writable: true,set 与 unset 也不会因为名字已被导出而拒绝。resolve 与 describe 仍以 source(file、env、project-env 或 user-env)报告提供值的层。llm-pi-ai 中的 authContextFrom 先从凭据 seam、再从启动环境回答 pi-ai 的 env(),因此 pi-ai 解析器依次读取其存储的记录、存储的引用,再按上面的顺序读取后备来源。

配置来源所有权中的非秘密顺序与环境限制保持不变。本 Note 只替代其中的凭据优先级。

按次覆盖

之前的顺序让 CI、容器和一次性的 DEEPSEEK_API_KEY=… dsh 无需修改机器状态就能覆盖凭据。全新的 CI 或容器 harness home 中没有 .credentials.yaml,因此其导出的机密仍是生效值。harness home 中存有密钥,只可能是有人通过 Models 页面或直接编辑该文件存入的。要对该名字使用导出的值,用户应删除对应的 refs 条目,或使用单独的 DSH_HOME 启动。

考虑过的替代方案

保持启动环境在最上层。 它无需改动存储就能保留按次覆盖,但会保留存储记录与存储引用之间相反的优先级,而且只要启动 shell 导出了该名字,Models 页面就无法保存或替换密钥。

只有启动环境提供该名字时拒绝写入。 在新顺序下这样的写入会生效,因为存储值优先于环境。拒绝它会阻止为每个已导出的名字首次保存密钥,也会保留新顺序已经去掉的只读输入框状态。

存储值遮蔽导出变量时发出警告。 提供方需要在每次解析时比较各层,并向有意同时保留两个值的用户报告结果,例如在 shell 配置文件中为其他工具导出密钥的用户。Models 页面的帮助文本说明了该顺序,describe 也会报告提供值的层。

后果

  • 留在 .credentials.yaml 中的密钥会覆盖同名的导出变量,包括在 harness home 存有该名字的机器上运行的 CI 机密。升级指南说明受影响的用户如何继续使用导出的账户。
  • Models 页面总能保存、替换或删除密钥,也不会因名字被导出而禁用密钥输入框。
  • 存储之上没有按次生效的标志。一次运行的覆盖需要使用单独的 DSH_HOME。
  • packages/credentials/credentials-local/tests/local.spec.ts 覆盖在启动环境值之上存储、以及 unset 后恢复该值;packages/llm/llm-pi-ai/tests/auth.spec.ts 覆盖存储的凭据压过每个启动层。