文件系统与凭据管理
本教程共 32 篇 · 第 17 篇 · 更新于 2026-08-15 · 约 6 分钟阅读
本节目标:弄懂 Agent 读写文件时有哪些保护(先读后写、版本守卫、错误码),密钥存在哪里、怎么引用,以及
settings.yaml用户设置与环境变量的关系。
fs 工具与文件作用域
文件能力由四个部分组成:抽象服务 ctx.fs、本地磁盘后端、观察策略插件(dsh-fs-observation-policy)和面向模型的工具(read/write/edit/read_image)。替换后端不会改变策略或工具 schema——这是能力缝的典型用法。
ctx.fs 有几个值得知道的约定:
- 路径先解析成不透明的
targetKey,消费方禁止解析它,也不得假设它是本地绝对路径;需要给子进程用的路径走processPath(),判断包含关系走contains(); - 写入和编辑是原子的:匹配、行尾处理、陈旧检查、替换在同一个变更临界区内完成;
write/edit都有可选的版本守卫:createIfAbsent(目标已存在就拒绝,报FS_NOT_OBSERVED)和replaceIfVersion(版本不匹配就拒绝,报FS_STALE_VERSION)。省略守卫才是无条件覆盖。
默认组合加载了观察策略,行为是「先读后写/编辑」:
- 只有 read 观察过的文件,edit 才会被授权;
- write 在「从未见过」或「确认不存在」时用
createIfAbsent,在「确认存在」时用replaceIfVersion; - 策略通过
fs/*事件裁决:fs/write-intent、fs/edit-intent是单槽决策 waterfall,fs/observed是即发即弃的记录事件。
Warning这个策略防止的是「Agent 读完文件后文件被外部改动」导致的并发覆盖,以及「没读过就改」的盲目操作。它不防恶意代码——进程级路径围栏不是内核隔离。
文件系统故障有稳定的错误码体系,挂在 isError 结果的 { name, code } 上,按 code 分支即可,不用解析文本。高频的几个:
| 错误码 | 含义 |
|---|---|
FS_NOT_FOUND | 目标不存在(也用于策略因确认缺失而拒绝 edit) |
FS_NOT_OBSERVED | 没有此所有者的先前观测记录,或 createIfAbsent 遇到已有文件 |
FS_STALE_VERSION | 文件版本与观测到的版本不一致 |
FS_SANDBOX_DENIED | 沙箱策略边界拒绝了写入(区别于宿主内核拒绝的 FS_PERMISSION_DENIED) |
FS_AMBIGUOUS_EDIT / FS_EDIT_NOT_FOUND | 字面量匹配不唯一 / 没匹配到 |
read/write/edit 不接受 timeoutMs——本地系统调用无法被真正终止(fsync/rename 进行中停不下来),给个超时反而是假承诺。取消仍通过 signal 传播,在系统调用边界尽力中止。
凭据缝:密钥不进配置
凭据(credentials)缝把机密挡在配置之外。核心规则:settings 和 cordis.yml 里只放引用(环境变量名),不放值。值归凭据提供方所有,比如本地提供方 dsh-credentials-local 管理 $DSH_HOME/.credentials.yaml。
# 在 shell 里先导出密钥,settings 里用引用指向它
export GATEWAY_API_KEY="sk-xxxx"
# 文件路径:$DSH_HOME/settings.yaml
llm-pi-ai:
providers:
my-gateway:
apiKeyEnv: GATEWAY_API_KEY # 引用,不是密钥本身
api: openai-completions
baseURL: https://gateway.example/v1
凭据服务 ctx.credentials 有四个操作:
resolve(ref):解析引用得到值 + 来源层(本地提供方分env、file、project-env、user-env四层);describe(ref):回答「是否已配置、来自哪层、能否写入」,绝不暴露值;set(ref, value)/unset(ref):持久化存储 / 移除。
关键机制是按操作解析:消费方每次操作都重新 resolve,绝不跨操作缓存。LLM 适配器每次模型请求解析一次,所以轮换后的密钥无需重启就能作用于下一次请求。Web 模型页存密钥时,页面只收到脱敏描述符,明文只进 .credentials.yaml。
Note一条 seam 级规则:空的存储值在任何地方都视为不存在。空字符串不会伪装成已配置的密钥。另外,如果某个引用正被进程环境变量遮蔽(
writable: false),set会直接拒绝——写了也白写,解析永远返回遮蔽值。
settings 用户设置
用户设置缝 ctx.settings 持有一份按 namespace 分节的用户文档,落地文件就是 $DSH_HOME/settings.yaml。每个插件注册自己的 namespace 和 schema,解析顺序是:schema 默认值 → 注册方的组合 base 层 → 用户分节。组合配置(插件开关、profile 那些)仍留在 cordis.yml,namespace 只承载用户可编辑子集。
一个 namespace 的写入有三种:
update(patch):把稀疏 patch 合并进用户分节;replace(section):整体替换,缺席的键重新继承 base 与默认值(replace({})即重置);mutate(ops):按路径编辑,给持有脱敏视图的调用方用。
配置界面读 describe() 时必须传 redactSecrets: true,把 role('secret') 字段从三层剥离并枚举成 {path, set} 槽位,页面因此能渲染只写输入框而永远收不到机密值。
Tip两个事件帮你感知变化:
settings/updated(解析值变化,深相等时不发)和settings/document-updated(原始分节变化,即使解析值没变——字段从继承变成覆盖也值得知道)。
环境变量
环境变量在 dsh 里有两个角色:
- 凭据引用的名字空间:
apiKeyEnv: GATEWAY_API_KEY就是一个 CredentialRef,语法必须是 POSIX 风格环境变量名; - 运行上下文:启动器把继承的进程环境、项目
.env、用户.env复制成不可变快照,保留每个值的来源,解析顺序是 process → project-env → user-env。之后的chdir、切换工作区、恢复会话都不会换掉这次启动的环境。
在 bash 工具里,harness 环境信息通过托管的 $DSH_* 变量公开,模型需要时可以检查它们;Windows 的 pwsh 工具则是 $env:DSH_*。
Warning「留日志之外」是正常的所有者边界,不是神奇防泄漏。如果某个错误插件主动把密钥写进消息、工具结果或遥测,凭据服务无法替它擦除。所以别把密钥写进对话内容,也留意自己贴出去的日志。
小结
- fs:原子读写 + 版本守卫 + 先读后写策略,错误按稳定 code 路由。
- 凭据:配置里只有引用,值在
.credentials.yaml,按操作解析实现热更新。 - settings:namespace 分节 + schema 校验,脱敏描述符服务配置界面。
- 环境变量:既是凭据引用名,也是不可变的启动环境快照。