第 5 课:安全边界:能碰什么、不能碰什么
一句话版:DSH 的安全哲学是「默认不信任」——插件要先声明自己要用什么、命令要在沙箱里被包装、文件改动要过策略事件、密钥永远只以引用形式存在——「能碰什么」不是一句口号,而是四道可以逐层验证的防线。
1. 一个用户故事:陌生插件凭什么不敢乱来?
你在社区里看到一个小插件:「一键美化 Markdown 排版」。它跑在你自己电脑上,能读写文件、能执行命令。装下它之前,你心里有个朴素的问题:凭什么相信它不会悄悄删掉我的文件、不会把我终端里的密钥偷偷发给某个服务器?
传统的答案是「靠信任」:看作者口碑、读一遍源码、希望它别作恶。DSH 的答案不一样——不靠信任,靠结构。插件、模型、命令在默认情况下「什么都不能碰」,每想碰一下,都要穿过一道闸门。四道防线层层叠起来:
| 防线 | 管什么 | 谁在执行 |
|---|---|---|
| ① 依赖声明 | 插件能拿到哪些服务与依赖 | 加载器 + 上下文代理(Proxy) |
| ② 进程沙箱 | 命令能读写宿主文件系统的哪些部分 | ctx.sandbox |
| ③ 文件策略事件 | 写入与编辑必须先读、版本必须匹配 | fs 策略事件门禁 |
| ④ 守卫与审批 | 越界操作要不要放行、给多宽 | 审批守卫 / guard 插件 |
下面四节逐一拆开看。先看最靠近「信任问题」的第一道防线。
2. 第一道防线:声明即能力,未声明即拒绝
还记得第二章第 13 课讲的访问控制吗?Cordis 论文里有个关键机制:组件只能访问自己「声明过」的依赖,访问未声明的会报错——JavaScript 的上下文代理(Proxy)在每次访问时检查键是否在声明清单里,不在就拒绝。这就是「基于能力的安全」(capability-based security):权限来自持有引用,而不是来自「你是这个环境里的人」。
DSH 把这条原则原样带进插件体系:
- 插件在加载时静态声明它要注入哪些依赖——inject 声明就是能力请求;
- 未声明的访问在运行时被代理拒绝:就算代码里写了,也拿不到;
- 因为声明是静态的,加载器在加载时就能审查批准,不必等访问发生了才逐项发现。
所以「陌生插件」拿到手的,只有它声明过的那几样东西。它想越界?第一道闸就把它拦住了。
3. 第二道防线:进程沙箱与文件策略事件
语言层的检查管不住恶意代码——只要代码能摸到宿主运行时,就能直接操作底层对象。所以 DSH 对「命令」下手:让进程在沙箱里跑。
3.1 沙箱在做什么:包装 argv,限制进程
进程沙箱的核心是一个叫「包装 argv」的动作,仓库里的描述非常精确:
ctx.sandbox.confine(argv, policy)返回用于 spawn、应当取代调用方原始 argv 的 argv。返回值经过包装,使进程及其派生的所有进程都在限制下运行……没有可用后端时,它会抛出异常,绝不会原样传递 argv 使其不受限制地运行。—— 来源:packages/sandbox/sandbox/README.zh.md
翻译成人话:
- 你想执行的命令,先被包进一层「笼子」再启动;
- 不只是这条命令,它派生的所有子进程都在同一个笼子里——命令再拉起子命令也逃不掉;
- 如果这台机器上没有任何可用的沙箱后端,DSH 拒绝执行,绝不裸奔。
后端是平台原生的:Linux 上用 bubblewrap(bwrap)或 Landlock,macOS 上用 Seatbelt(sandbox-exec)。它们和宿主共享文件系统与内核,但文件效果被策略严格约束。
3.2 逐调用策略:同一个沙箱,不同的笼子
沙箱怎么知道「这条命令能碰哪里」?答案在逐调用策略——策略不是沙箱提供方身上的固定配置,而是每次调用时随调用一起传入的。三档模式:
| 模式 | 能做什么 | 典型场景 |
|---|---|---|
| read-only | 只读;写入一律拒绝(仅保留 /dev/null 等必需出口) | 默认模式,故障安全 |
| workspace-write | 可写会话工作区根目录 + 平台临时区域(如 /tmp) | 正常干活 |
| danger-full-access | 不加限制 | 明确信任的调用 |
两个要点:
- 默认是 read-only,故障安全:想写文件,必须先证明这次调用有 workspace-write 的资格;
- 策略随调用走,不挂在提供方身上:bash 可以在 read-only 下执行,而一个受限制的子 agent 同时让它的状态目录保持可写;获批的升权重试,只是用更宽策略发起的新调用。
注意这三档模式只约束文件操作——网络、进程可见性不在这个词汇表里(第 5 节会讲它们归谁管)。
3.3 文件系统策略事件:写入前先读、版本防覆盖
依赖声明挡住了「插件乱拿东西」,进程沙箱挡住了「命令乱写文件」。那模型通过文件工具读写的文件呢?还有一层:文件系统策略。
先看「沙箱化文件系统」后端(fs-sandbox)。它只给写入加按调用的模式围栏,并且有一条朴素原则:读取始终直接通过——所有模式都允许读取。具体地:
- read-only 下,所有变更被结构化拒绝(错误码
FS_SANDBOX_DENIED,携带当前模式); - workspace-write 下,只有目标规范化后位于可写根目录(工作区根目录 + 平台临时区域)之内才允许变更;
- danger-full-access 下不加围栏直接委托。
(来源:packages/fs/fs-sandbox/README.zh.md)
再往上一层,是文件系统栈的政策层(fs-observation-policy 插件)。它不提供任何服务方法,只通过 fs/* 事件门禁参与,专门管「编辑卫生」:
- 编辑前必须先读过:没读过某文件就想 edit,直接拒绝——「edit requires reading the file first」;
- 版本防护:写入与编辑基于观测到的版本做 CAS(比较并交换),文件已被别人改过就报版本过期(
FS_STALE_VERSION),提示重读后重试; - 好处:模型不会基于过期视图盲改文件,并发写者也不会被悄悄覆盖。
(来源:packages/fs/fs-observation-policy/README.zh.md)
3.4 守卫与审批:一次被拦下的越界写入
最后一道闸在「升权」路径上。模型遇到拒绝后,可以发起唯一一次更宽权限的重试——但这次重试要过守卫与审批。守卫(guard)插件负责监视 agent loop 的循环卫生(重复工具调用的建议性提醒、单次调用的时间预算),而更宽的模式切换本身是显式事件:会话切换模式,就是追加一条 sandbox/mode 事件;获批的升权重试,只是用更宽策略发起的新调用。
把整条链路走一遍,就是一个完整的真实例子(策略设计来自仓库的 fs-sandbox 与 sandbox-policy):
模型想写 app/config.json
↓ 当前策略 read-only
↓ 写入被拒:FS_SANDBOX_DENIED,渲染为 [sandbox: file access denied under read-only mode]
↓ 工具层给出唯一一次升权重试引导
↓ 守卫与审批:这次调用够格吗?
↓ 获批 → 以 workspace-write 发起新调用,目标在工作区内 → 写入成功
每一步都有据可查:策略解析结果、拒绝原因、审批决定,全部落在会话日志里。
沙箱 + 策略 + 审批:智能体只碰它被允许碰的东西
4. 第三道防线:设置与凭据
前两道防线管「代码能碰什么」。第三道管「配置里有什么」——尤其是密钥。
4.1 设置:按插件分命名空间
DSH 的用户设置通过注册的命名空间解析:每个插件在 settings 里注册自己的命名空间,互不串扰。settings 家族由「定义命名空间注册、分层解析与提交」的服务和「在本地文件中存储设置并观察外部编辑」的提供方组成——一个插件的设置项,永远不会冒名顶替另一个插件的设置项。
(来源:packages/settings/README.zh.md)
4.2 凭据:具名密钥引用,绝不内联
设置里最危险的东西是密钥。DSH 的规矩只有一句,写在凭据服务定义的 README 开头:
配置只携带对机密的引用,绝不携带机密本身。settings 分节或 cordis.yml 条目写
apiKeyEnv: DEEPSEEK_API_KEY,引用背后的值归凭据提供方所有。—— 来源:packages/credentials/credentials/README.zh.md
也就是说:配置里永远不会出现 sk-xxx 这样的明文。你写的是具名引用(如 apiKeyEnv: DEEPSEEK_API_KEY);真正的值存放在凭据提供方那里(本地是 $DSH_HOME/.credentials.yaml,以 0600 权限存放在 0700 目录下)。引用在每个操作开始时解析,绝不跨操作缓存——改过的凭据无需重启任何插件,就作用于下一次请求。
这带来三个朴素的好处:
- 设置文档可以放心同步、放心渲染进配置界面——里面没有任何机密;
- 轮换密钥不碰任何配置文件——改的是凭据提供方,不是配置;
- 空的存储值等于不存在——空白永远不会伪装成已配置的密钥。
5. 沙箱之外:不受信代码需要隔离运行时
前面三道防线都假设一件事:代码和宿主共享文件系统与内核,所以还能用「策略」管住它。但如果代码根本不可信呢?第二章第 13 课讲过论文 5.2 节的结论:不受信任的代码,必须扔到隔离边界外面——语言层的检查管不住恶意代码,真正的隔离需要语言层之外的执行边界:软件故障隔离、隔离的语言运行时、沙箱进程、虚拟化容器。
DSH 的对应设计同样干脆。沙箱服务的 README 明确写着:
只支持与宿主共享文件系统和内核的限制。容器、microVM 与远程执行器都不是该 seam 的后端:它们会以环境一致的分组替换整个能力 seam 的 Service provider(ctx.shell、ctx.fs)。
—— 来源:packages/sandbox/sandbox/README.zh.md
翻译一下:bwrap、Landlock、Seatbelt 这类机制,是和宿主「共用」文件系统与内核的前提下的策略限制;如果部署方要求的是彻底隔离(容器、microVM、远程执行),那不是给沙箱加一个后端的事,而是把整个执行能力(bash、fs)换成另一套实现。文件系统后端的 README 也持同一立场——「策略围栏,而非内核边界」:提供约束,但不是安全边界;不可信代码的内核级隔离仍由执行能力负责。
所以看一张安全拓扑图时,先问一句:代码是「被限制的邻居」还是「被关在另一个世界的囚犯」? 前者靠沙箱策略,后者靠换执行世界——两条路 DSH 都留好了。
6. 关键点回顾
- 默认不信任:能力来自声明与引用,不来自「你在环境里」。
- 声明即能力请求:插件只能访问声明过的依赖,未声明访问被 Proxy 拒绝,加载时就能审查批准。
- 沙箱包装 argv:进程及其后代都在限制下运行;没有可用后端就拒绝执行,绝不裸奔。
- 逐调用策略:read-only(默认、故障安全)/workspace-write/danger-full-access,策略随调用走。
- 文件策略事件:写入前必须先读、版本 CAS 防覆盖;拒绝是结构化错误,不是悄悄失败。
- 密钥绝不内联:settings 按插件分命名空间;credentials 具名引用、按操作解析、值归提供方。
- 沙箱不是万能的:不可信代码需要换执行世界(容器/microVM/远程执行),而不只是加策略。
🚀 下一课我们进入「感知与上下文」:模型怎么「看」到代码库、网页和运行中的环境——以及这些感知通道同样受策略与审批的约束。
自测题 · 安全边界
完成作答后点击「提交答案」,可以查看对错与解析。
