# dsh-credentials-local

[English](README.md) | 中文

文件型[凭据](../credentials/README.md) provider：两层来源，一条诚实的优先级。

| 层 | 来源 id | 可写 | 优先 |
|---|---|---|---|
| 活跃进程环境 | `env` | 否 | 恒定优先 |
| `$DSH_HOME/.env` 文档 | `file` | 是（`set`/`unset`） | 其余情况 |

环境优先，因为启动时覆盖（`DEEPSEEK_API_KEY=… dsh`、CI 机密、加载了仓库 `.env` 的开发 shell）代表本次运行的操作者意图——而它无法从进程内部修改，就必须*可见地*只读：`describe()` 报告 `source: 'env', writable: false`，`set`/`unset` 直接拒绝，而不是写下一个读取方永远看不到的变更。解析实时读取 `process.env`，绝不写回。

## 配置

| 字段 | 默认值 | 含义 |
|---|---|---|
| `path` | `<harness home>/.env` | 凭据文档位置。 |
| `dshHome` | `$DSH_HOME` 或 `~/.dsh` | `path` 缺省时使用的 harness home。 |
| `watch` | `true` | 热发布外部编辑。 |
| `debounceMs` | `100` | watcher 写入稳定窗口。 |

## 文档本身

dotenv 格式，用 `dotenv` 解析；写回用物理行级编辑器，保留一切不属于本次编辑的字节：`set` 原位改写该键的第一条赋值行、沿用该行自身的行尾（丢弃后续重复行——dotenv 按最后一条生效，重复行会反过来覆盖这次编辑），`unset` 只删除所属行，注释、无关行、CRLF 行尾，以及另一个键的引号多行值的续行，都逐字保留。每次写入都先在 [`dsh-atomic-write`](../../util/atomic-write/README.md) 的跨进程写锁下重读文档、把此前未观察到的一切发布出去，再在仅属主可访问（`0700`）的目录下以 `0600` 权限原子提交——因此并发写入者、或落在 watcher 防抖窗口内的外部编辑会被并入，而不是被覆盖。

值按 dotenv 能逐字读回的最窄样式渲染——裸值，其次单引号（完全字面），再次双引号（仅限无反斜杠，双引号读取会展开转义）。任何样式都无法表示的值，以及已经跨越多个物理行的条目，都会响亮失败而不是被静默破坏。空的存储值等于不存在（seam 规则）。

## 热重载

外部编辑在快照**整体替换**后按变更引用逐个发布 `credentials/updated`——磁盘上删掉的条目绝不在内存滞留。provider 自己的写入按内容识别，只发布属于该次提交的一个事件。运行期文档不可读时保留最后可用快照并告警；文件不存在即空存储；启动时不可读则响亮失败。非 POSIX 标识符的键属于被保留的文件内容，seam 无法寻址。

## 安全边界

文档在 `0700` 目录下以 `0600` 权限存放，这挡得住其他 OS 用户，**挡不住**模型。工具进程（bash、文件系统工具）以同一用户身份运行，因此在出厂默认的 `danger-full-access` 下，它们读这个文件与读该用户拥有的任何其他文件毫无二致，也没有任何沙箱模式会把它单独挑出来。harness 真正守住的更窄：它绝不把该文档的解析后路径交给模型，也绝不把它载入进程环境（见 [app-boot 的个人配置](../../ui/app-boot/README.md#personal-config)），因此要拿到这个值，需要刻意去读一条并未交给 agent 的路径。

这是审慎，不是边界。必须让提供方密钥远离自身 agent 的部署无法靠文件权限做到；OS 钥匙串 provider——一个模型的进程根本读不到的存储——才是延后的答案，它应当作为平级包与本 provider 并列。

## Model Experience

经由消费它的 LLM 适配器间接生效：存储的值为适配器的提供方请求授权，每个模型可见面都归适配器所有。

#### KV Cache effect

无直接失效；凭据绝不进入请求前缀。

## Known Limitations and Deferred Work

- **多行条目拒绝 `set`/`unset`**——行编辑器不改写会被它破坏的条目；`describe` 把它们报为 `writable: false`，编辑必须直接落到文件上。
- **同一引用的并发写入是后写胜出**——写锁加读-改-写让并发写入者不会丢掉彼此的条目，但两个写入者编辑同一个引用时仍以较后的写入为准；没有修订检查。
- **同 UID 进程可以读取该文档**——见[安全边界](#security-boundary)：只有受限沙箱模式会拒绝它，OS 钥匙串 provider 仍是延后项。
- **无法表示的值响亮失败**——控制字符，或同时混用两种引号又含反斜杠的值，无法在 dotenv 行格式中往返。
- **环境变化不可见**——每次解析实时读取 `process.env`，但那里的变化不可能发出事件。
- **原子但不保证崩溃持久**——继承自 `dsh-atomic-write`；存储在启动时重新读取。
