znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第14章:排障、安全与最佳实践

这是本教程的最后一章。前面十三章你学会了使用 dsh、理解 Cordis、读源码、做扩展、写测试、走贡献流程。收尾这一章解决两个最实际的问题:出问题时怎么定位,以及怎么安全地用。

DeepSeek Harness 的定位是”把 shell 和文件系统交给一个模型”。安全模型因此不是可有可无的附加题,而是产品的核心设计。本章从高频报错出发,逐层拆解权限预设、沙箱后端、凭据安全、会话持久化、平台差异与遥测,最后给出学习路线图和版本风险提示。

14.1 高频报错定位表

先把最常见的几类报错集中在一张表里。大部分来自 docs/user/guide/providers.md 的 Troubleshooting 一节。

报错/现象 成因 解决
MISSING_CREDENTIAL 模型路由找不到凭据 通过 Models 页保存 provider key,或提供引用的环境变量
UNKNOWN_MODEL 请求了一个未配置的模型 在模型选择器里选一个已配置模型,或给自定义 provider 补上该模型
自定义 provider 拉取模型返回 401 模型发现调的是 OpenAI 兼容的 GET /models 端点 检查 key;对不提供该端点的服务,手动录入模型
图片在发送前被拒 手工录入的模型默认按纯文本处理,未声明图片模态 给模型加 input: [text, image];DeepSeek 官方 chat-completions 路由本身纯文本,无法配置成视觉
provider 拒绝了带图的请求 模型声明了图片,但端点实际不服务图片 input 或路由 defaultInput 里去掉 image,然后开新会话(已附加图片留在会话日志里,会话不挪走会重复发送)
Web UI 局域网无法访问 dsh web 尚不支持 --host 0.0.0.0 见 14.1.1

14.1.1 Web UI 无法访问:--host 0.0.0.0 的坑

dsh web 默认只服务 http://127.0.0.1:3080apps/cli/reference/README.md 写得很直白:

CLI 目前刻意不支持 --host 0.0.0.0,遇到会以 usage error 退出;--trusted-host 用于添加 /api 浏览器信任栅栏接受的有名 authority。

设计考量在于 /api 信任栅栏:任何 Host 既不是 loopback、也不在声明名单里的请求都会被拒绝。所以正确姿势不是 --host 0.0.0.0,而是用可重复的 --trusted-host 声明部署被访问的名字(dsh CLI 会自己推导机器的局域网 IP 字面量):

dsh web --trusted-host my-host:3080

要对外暴露,优先用反向代理 + 显式 --trusted-host,而不是裸开端口。一个不是”裸的、规范的 authority”的条目会在插件加载时就失败。

14.2 安全模型:权限预设

dsh 的安全模型是文件效果(file-effect)沙箱 + 审批(approval)两层,通过”权限预设”打包给用户选择。核心文档是 docs/subsystems/sandbox.mddocs/subsystems/approval.md

14.2.1 SandboxMode:只管文件效果

SandboxMode 三个取值,且只管文件系统效果——网络与进程可见性在这个词汇表之外:

type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
模式 文件效果
read-only 拒绝写;POSIX runner 额外放行 shell 需要的 /dev/null 下沉点
workspace-write 允许在工作区根 + 后端承诺的临时区写
danger-full-access 完全绕过限制

只有前两种会真的送进 provider。danger-full-access 的消费者直接 spawn 原始 argv,根本不调用 ctx.sandbox

两个关键点:

  • 强制是”报告的事实”full 表示后端管控了该模式承诺的全部文件效果;partial 表示旧内核 ABI 或 Windows ACL 边界只能管控子集。要求绝对边界的消费者必须区分这个差异。
  • 策略按调用解析ctx.sandboxPolicy.resolve() 接受会话与可选的审批通过的模式覆盖,解析出每次调用的完整策略(mode + workspaceRoot + sessionId)。一个会话的 cwd 就是它的 workspace-write 边界。

14.2.2 默认预设:workspace-write + ask

权限预设把沙箱模式和审批策略捆成一个可选项。配置目录 docs/config-catalog.md@deepseek-ai/dsh-permission-presets 的声明写着默认表:

预设名 sandbox approval
workspace-write workspace-write ask
danger-full-access danger-full-access never

custom 是保留名,用于”从预设派生出来的非预设状态”。

apps/cli/reference/README.md 一句话说清默认行为:

新会话默认 workspace-write 权限预设。Bash 与文件系统变更被限制在会话工作区和平台临时根;读取、网络访问、进程可见性不受限制。DSH_PERMISSION_MODE 改变进程级回退。

注意这句话的精妙之处:默认限制的是”写”,不是”读”。agent 可以读你整个家目录、可以访问网络、可以看到进程——被约束的只是它改东西的位置。这与凭据文档里”文件权限挡得住别的 OS 用户,挡不住模型”的论断一脉相承(见 14.4)。

14.2.3 审批:ask / never

审批层回答”这个具体动作可以吗”。ApprovalPolicy 只有两个取值:

type ApprovalPolicy = 'ask' | 'never'
  • ask(默认)——委托给组合的 answerer 链;没有组合任何 answerer 时落入 fail-closed 的 unavailable
  • never——绝不询问任何人,每次 ask 确定性地解析为 rejected(CI、无人值守运行的严格无头姿态)

ApprovalOutcome 是封闭且 fail-closed 的:allowed-once(一次性放行)是唯一的授予;rejectedcancelledunavailable 都拒绝。一个缺失的、不拥有的、抛异常的 answerer 一律变成 unavailable,而不是打开闸门。

Web UI 里,审批表现为”操作前询问”;ACP 自动化桥为它自己的 agent 提供一次性机器决策。审批的 approval/asked / approval/decided 事件是 log-only 的审计记录,不进模型转录。

14.2.4 danger-full-access 的风险与适用

danger-full-access 预设 = danger-full-access 沙箱 + never 审批:完全权限、完全自动,没有任何确认。它的风险是字面意义的——agent 的 Bash 和编辑器可以改动运行进程能访问的任何路径。

官方给它的适用场景收得很窄。Python SDK 教程 docs/user/guide/python-sdk.md 里直说:

该组合使用 danger-full-access。只在一次性的 checkout 或容器里运行它。

一句话原则:danger-full-access 是给”炸了也无所谓”的环境用的——一次性 checkout、容器、CI 里跑真实 API 测试。日常工作区请留在默认的 workspace-write

14.3 沙箱后端

ctx.sandbox 是抽象的进程沙箱 seam(SandboxProvider),ctx.sandboxPolicy 是策略服务。真正干活的是 provider。

14.3.1 本地 provider:三个平台三套 runner

dsh-sandbox-local 选择并缓存一个平台 runner:

平台 runner
Linux 优先可用的 bwrap,其次 Landlock
macOS Seatbelt
Windows ACL restricted-token(受限令牌)

不支持的平台、不可用的 runner 一律 fail-closed 抛 SANDBOX_UNAVAILABLE执行绝不静默无限制回退。每个 wrap 都带结构化 runner-failure 规则,消费者能区分”沙箱坏了”和”命令本身失败”。

强制完备度同样是报告的事实:Windows ACL runner 因为受限令牌必须保留 Everyone 来完成进程初始化,所以外部授予 Everyone 写权限的对象仍可写,NTFS 硬链接也会把文件对象跨工作区与外部路径别名——它如实报告 enforcement: 'partial',而不是把边界吹成 full。老内核的 Landlock 同理。

14.3.2 失败分类:沙箱坏了 vs 命令失败

sandbox-local 的 README 里有一条容易被忽略的工程细节:wrap 结果带两类正交的 stderr 分类器。

  • denialSignatures——识别”受限命令被拦下,而沙箱工作正常”(EROFS/ EACCES / EPERM 等后端方言)
  • runnerFailureRules——识别”沙箱 runner 在执行命令前就拒绝或失败”

消费者检查 runner-failure 规则(结构化的致命行匹配 + 可选的退出码门槛),再检查 denial 签名。因为”runner 失败”意味着命令根本没跑,”denial”意味着限制生效并拦下了它——把前者当成普通任务失败上报,会把基础设施故障伪装成任务错误。

14.3.3 Landlock 原生 addon

Landlock 的底层是 native/landlock-run——一个”自我限制然后 exec”的启动器,以 @deepseek-ai/node-addon-landlock-run 发布。sandbox-local 只负责”模式到授予的映射 + runner 选择”,路径解析与 probe 解析留给带版本的二进制,防止契约漂移。

14.3.4 E2B 远程运行时 POC

packages/e2b/ 是一个实验性的 provider 组合 POC,把整个文件系统/进程执行世界放进一个 E2B Linux 沙箱:

ctx key 角色
@deepseek-ai/dsh-e2b ctx.e2b 创建沙箱、准备工作/运行时目录、暴露 SDK 句柄、超时或销毁时删除
@deepseek-ai/dsh-fs-e2b ctx.fs 用 E2B Filesystem API 实现文件系统 seam
@deepseek-ai/dsh-subprocess-e2b ctx.subprocess 用 E2B Commands/PTY API 实现可执行查找、进程组、stdio、终端会话

E2B 只提供沙箱生命周期和两个基本 OS 适配器,高层能力由 provider 中立的消费者构建。关键点在于边界没动:harness 进程、Cordis 对象、模型调用、agent/会话状态、持久化、skills、高层协议状态都留在本地——只有”执行世界”(文件与进程)被搬到远端。

配置里 API key 读 E2B_API_KEY,且从不转发进沙箱;沙箱有 lifetime,到期必删。

14.3.5 远程沙箱为什么能迁移 Bash/PTY/LSP

dsh-bash-local、dsh-terminal-bash、dsh-lsp-stdio 不需要 E2B 专用 fork:它们把每个”执行世界”操作委托给 ctx.fsctx.subprocess。所以挂上两个 E2B 适配器,Bash、PTY 终端、LSP 的可变工作就自动落进同一个远程沙箱。这正是第 11 章”capability seam = Service Definition / Provider / Consumer”设计带来的红利:换 provider,不换消费者。

14.4 凭据安全

14.4.1 key 是 write-only 的

Web UI 的 Models 页保存 key 后,页面收到的是一个脱敏描述符,永远不是原始 secret。key 存进 $DSH_HOME/.credentials.yaml,settings 里只保留凭据引用。

14.4.2 凭据解析顺序

dsh-credentials-local 定义了一句话能讲清的四层优先级:

来源 id 可写 胜负
继承的进程环境 env 永远赢
$DSH_HOME/.credentials.yaml file 压过两层 .env
<调用目录>/.env project-env 压过用户 .env
$DSH_HOME/.env user-env 兜底

为什么环境变量永远赢?因为 DEEPSEEK_API_KEY=… dsh、CI secret、容器 -e 表达的是操作者对本轮运行的意图——而且它无法从内部改写,所以必须是可见地只读describe() 报告 source: 'env', writable: falseset/unset 直接拒绝。

为什么托管存储压过 .env?因为 Models 页写入的 key 要立即生效,即使旧 key 还躺在 .env 里。两个 .env 层只在没有托管值时才解析。

14.4.3 文件权限:0600 / 0700

provider 用 0700 建目录、0600 原子写文档。POSIX 上,文档带任何 group/other 权限位都会在解析内容之前失败,错误信息直接点名 chmod 600 修复。Windows 没有可检的模式位,跳过该检查而不是假装有。

14.4.4 安全边界:挡得住别人,挡不住模型

credentials-local 的 README 有一段重要的诚实声明:

文档是 06000700 目录下,挡的是其他 OS 用户——不是模型。工具进程(bash、文件系统工具)以同一用户运行,而 workspace-write 限制的是”变更”而非”读”,所以它们能像读用户拥有的任何其他文件一样读这份文档。

harness 真正守住的是更窄的承诺:从不把文档的解析路径交给模型,从不把它载入进程环境(不像 $DSH_HOME/.env 那样是普通环境层)。所以拿值需要”刻意读一个 agent 没被告知的路径”。

这是”自由裁量(discretion),不是边界(boundary)”。如果部署要求模型进程完全读不到 provider key,文件权限做不到——答案是 OS 密钥链 provider(模型进程根本读不到的存储),这是被推迟的、应当作为 sibling 包的方案。

14.4.5 绝不提交 .env

这条同时来自 development.md 和 AGENTS.md:绝不提交真实凭据。真实 API e2e 套件在 DEEPSEEK_API_KEY 未设置时自我跳过——所以”没 key 的 CI 照样绿”是设计目标,不是故障。

14.5 会话与持久化

14.5.1 JSONL 会话日志

会话的持久事件日志是 JSONL。配置目录里 persistenceCompression 字段写着默认值:

JSONL 产物编码;默认是带校验和的 Zstandard 帧(checksummed Zstandard frames)。

也就是说会话日志默认用 zstd 压缩(带校验和)。Python SDK 的最小组合则用未压缩 JSONL(见其教程的 “Session persistence: Uncompressed JSONL”)。

14.5.2 版本单调演进 + developer preview 拒绝旧格式

两个版本号管控兼容性(见根 AGENTS.md):

  • SQLite 用单调的 SCHEMA_VERSION
  • dsh-session 保持 SESSION_FORMAT_VERSION = 0无兼容性承诺

pre-release 的立场写得很直白:

后端拒绝旧的磁盘格式。……没有外部消费者时,宁可要正确的根基也不要兼容垫片。

升级前备份会话数据是这一阶段的自保动作——旧格式可能被直接拒绝,而不是平滑迁移。会话事件目录 docs/persistence-catalog.md 是每个可持久化事件的权威清单。

14.6 Windows 注意

14.6.1 基础 bundle 默认禁用 bash

bundle/base/README.md 写明:base bundle 的 patch 按平台门控两套 shell 栈——bash-sandbox/tool-bash 带着 disabled: !!js process.platform === 'win32'(bash 没有 Windows runner),孪生的 pwsh-sandbox/tool-pwsh 用相反表达式只在 win32 挂载。一份共享 patch 文件,每台主机正好一套 shell 栈。

Windows 上的权限面与 POSIX 完全一致:sandbox/sandbox-policy 通过 Windows ACL restricted-token runner(win32 链 dsh-sandbox-local@deepseek-ai/dsh-sandbox-windows-acl)强制执行文件效果策略,权限切换器和审批服务照常运行。

workspace-write 在 Windows 上把写限制在工作区 + 会话自己的临时子目录(<temp>\dsh-<hash>,TMP/TEMP 对受限子进程重写);read-only 什么都不授予。

如果你确实想用未受限的本地 pwsh 执行器或完全权限,在 profile 或 home 的 cordis.patch.yml 覆盖这些行——但 bash 恢复配方必须完整:禁用 pwsh-sandbox/tool-pwsh 且重新启用 bash-sandbox/tool-bash,因为两套执行器家族注册的是同一个 bash 服务,不完整的配方会在加载时大声失败。

14.6.2 Python SDK 的 PTY 组合不支持 Windows agent

Python SDK 教程明确列出前置条件:Linux x64 / Linux arm64 / macOS 14+ arm64——没有 Windows。原因:

持久 PTY 后端需要 POSIX 终端基底,因此该组合不支持 Windows agent。

如果你的目标是 Windows 上跑 Python SDK,当前版本做不到。

14.7 遥测

14.7.1 默认关闭

session telemetry 是默认关闭的。apps/cli/reference/README.md

会话遥测默认留在本地。DSH_TELEMETRY_MODE=FULL 把每个投影后的会话事件作为 OTLP/HTTP 日志流式上传;DSH_TELEMETRY_MODE=FEEDBACK_ONLY 仅在记录反馈时上传会话日志后缀;DSH_TELEMETRY_OTLP_URL 选择另一个 collector;任何非空的 DSH_TELEMETRY_DISABLED 都是权威的硬退出。

三个模式对应 dsh-session-telemetry-otel 的 config mode

mode 行为
FULL 每个投影记录(含生命周期操作记录)立即交给 OTel SDK
FEEDBACK_ONLY 每次 feedback/record 回放、投影、脱敏那段日志后缀;没反馈就不传
DISABLED 默认。不构建任何 coordinator/provider/processor/exporter,任何记录都不离开进程

上传是显式 opt-in、正向授权、fail-closed。

14.7.2 没有内置脱敏规则

session-telemetry 的 seam 不内置任何脱敏规则sessionTelemetry/record 瀑布的 innermost next() 原样放行。这意味着显式开启的导出可能包含消息文本、工具参数与结果、工作区路径。provider 凭据永不出现——因为 adapter API key 是构造参数,不是会话事件,结构性地缺席于日志与遥测。

部署在受信边界之外导出时,必须自己挂脱敏规则。

14.8 社区求助路径

路径 用途
GitHub Discussions 反馈、bug 报告、提问;给希望团队关注的帖子投票
Discord DeepSeek Harness 社区实时交流
dsh-plugin topic 发布插件、发现插件

求助前先做三件事:确认问题能复现、检查是否属于 14.1 的已知报错、在 Discussions 里搜过有没有同款。提问时附上 dsh --version、复现步骤和会话日志片段(注意脱敏)。

14.9 学习路线图:回顾 14 章

整部教程的结构是一根从”使用”到”深入”的线:

阶段 章节 主题
使用 第 1-7 章 安装、配置、模型接入、日常任务、会话、工具与权限基础
范式 第 8 章 Cordis 的”一切皆插件”、composable context
源码与运行时 第 9-10 章 仓库结构、agent-loop、会话持久化、运行时机制
seam 与扩展 第 11-12 章 capability seam(Service Definition/Provider/Consumer)、插件开发
测试与贡献 第 13 章 分层测试、snapshot 铁律、门禁、Agent Note、贡献流程
排障与安全 第 14 章(本章) 报错定位、权限/沙箱/凭据/遥测、平台差异、最佳实践

这条路线刻意把”怎么写测试”和”怎么安全用”放在最后:只有当你真正理解了一个 agent harness 把什么权力交给了模型,测试和安全这两件事才有意义。

14.10 版本风险提示

最后,一条贯穿全书的提醒:DeepSeek Harness 是 developer preview,迭代极快,会有破坏性变更。README.md 用大写写明了:

DeepSeek Harness 目前处于 developer preview,正在快速迭代。会有破坏兼容性的变更。

这带来几个务实的结论:

  • 学习与写作以 master 实际代码为准,不要依赖记忆或过时教程——本文所有细节都核对自 master 原文,但 master 会继续变
  • 关注官方 tag 与 README 变更,第一个 tagged release 会移除 AGENTS.md 里那段”pre-release 立场”
  • 会话/持久化格式会变SESSION_FORMAT_VERSION 无兼容承诺),升级前备份
  • 配置字段会重命名/重打包,pre-release 立场是”宁可要正确的根基也不要兼容垫片”

一句话收束:把它当成一个快速演进的、值得跟踪的开源项目来用,而不是一个稳定的、可以抄配置就忘的平台。

14.11 本章小结

  • 高频报错各有明确解法:MISSING_CREDENTIAL 补 key、UNKNOWN_MODEL 选/加模型、自定义 provider 401 是 GET /models 发现端点缺失、图片被拒是模态未声明、Web UI 外网访问走 --trusted-host 而非不支持的 --host 0.0.0.0
  • 安全模型 = 文件效果沙箱(read-only / workspace-write / danger-full-access)+ 审批(ask / never);默认 workspace-write + askDSH_PERMISSION_MODE 改进程回退。
  • danger-full-access 是完全权限 + 从不询问,只在一次性 checkout 或容器里用。
  • 沙箱后端:本地三平台 runner(bwrap/Landlock、Seatbelt、Windows ACL),E2B 远程 POC,native landlock addon;换 provider 不换消费者,Bash/PTY/LSP 自动迁入远程沙箱。
  • 凭据:key write-only,存 $DSH_HOME/.credentials.yaml(0600/0700),解析顺序环境 → .credentials.yaml → 调用目录 .env → $DSH_HOME/.env;文件权限挡别人不挡模型;绝不提交 .env,CI 无 key 自动跳过。
  • 会话日志 JSONL 默认 zstd 压缩;SCHEMA_VERSION 单调、SESSION_FORMAT_VERSION=0 无兼容承诺,升级前备份。
  • Windows:基础 bundle 默认禁用 bash 改挂 pwsh,Python SDK PTY 组合不支持 Windows agent。
  • 遥测默认关闭,DSH_TELEMETRY_MODE / DSH_TELEMETRY_DISABLED 控制,无内置脱敏规则。
  • 求助走 Discussions / Discord / dsh-plugin;developer preview 迭代快,以 master 为准、关注 tag 与 README 变更。