DeepSeek Harness 系列(07):能力 Seam——换一行配置,能力全换
英文标题:DeepSeek Harness 系列(07):能力 Seam——换一行配置,能力全换
为什么 dsh 不直接 import fs?为什么 Shell 执行可以一键切到远程沙箱?这篇文章讲 dsh 的能力 Seam 设计
6.3 KB
中文
先从一个家用比喻开始
你家的电灯开关,它不关心电从哪里来——市电、太阳能还是发电机都行。开关只定义一件事:"开/关"。只要背后接的东西遵守这个接口,你按下开关,灯就亮。
这就是 Seam (缝隙)的本质:一条接口边界,把"用这个能力"和"这个能力怎么实现"隔开来。
在 dsh 里:
- 文件系统操作 是一个 Seam(
ctx.fs) - Shell 执行 是一个 Seam(
ctx.shell) - LLM 调用 是一个 Seam(
ctx.llm) - 进程沙箱 是一个 Seam(
ctx.sandbox)
你可以把默认的本地文件系统换成 E2B 沙箱文件系统,工具代码一行不改。这就是 Capability Seam 要解决的问题。
Seam 的三个角色
每个 Seam 都由三类参与者构成:
┌────────────────────────────────────────────────────────┐
│ Consumer: tool-fs │
│ import ctx.fs → calls ctx.fs.readFile(path) │
│ ctx.fs.writeFile(path, content) │
└─────────────────────┬──────────────────────────────────┘
│ 依赖服务 "ctx.fs"
┌─────────────────────▼──────────────────────────────────┐
│ Service Definition: dsh-fs │
│ interface FileSystem { readFile, writeFile, ... } │
└─────────────────────┬──────────────────────────────────┘
│ 实现
┌───────────────┴───────────────┐
▼ ▼
fs-local fs-e2b
(本地文件系统) (E2B 沙箱)
三个角色的分工:
| 角色 | 职责 | 举例 |
|---|---|---|
| Service Definition | 定义接口 + 服务名 | dsh-fs:定义 ctx.fs 的方法签名 |
| Service Provider | 实现接口,注册到 ctx |
fs-local、fs-e2b、fs-sandbox |
| Service Consumer | 使用接口,不关心实现 | tool-fs:读写文件的工具插件 |
Consumer 只知道服务名(如 ctx.fs),不知道背后是哪个 Provider。切换 Provider 只影响 Bundle 的配置,Consumer 完全无感。
核心 Seam 一览
dsh 内置了以下核心 Seam:
| Seam | 服务名 | 默认实现 | 可替换为 |
|---|---|---|---|
| 文件系统 | ctx.fs |
fs-local(本机) |
fs-e2b(E2B 沙箱)、fs-sandbox(受限本机) |
| Shell 执行 | ctx.shell |
bash-local |
bash-sandbox(受限执行)、远程 Shell |
| 进程沙箱 | ctx.sandbox |
sandbox-local(bwrap/Seatbelt/ACL) |
容器、microVM |
| LLM 适配器 | ctx.llm |
llm-deepseek |
llm-pi-ai 等第三方 |
| 认证凭据 | ctx.credentials |
credentials-local |
远程 vault |
| 用户设置 | ctx.settings |
settings-file |
远程设置服务 |
| 持久化存储 | ctx.sessionPersistence |
session-persistence-jsonl |
SQLite、远端 |
重点:ctx.fs(文件系统 Seam)
为什么不直接 import fs from 'node:fs'?
直接 import Node.js 内置的 fs 模块,会带来三个具体问题:
- 测试时 :要测试"读文件"逻辑,必须在磁盘上真的创建文件。或者用 mock,但 mock Node.js 内置模块需要额外工具(jest.mock 等),很麻烦。
- 在沙箱里跑时 :E2B 有自己的文件系统接口,不是本地 Node.js
fs。如果工具代码直接 import 了node:fs,到 E2B 里就完全跑不起来。 - 审计 :你无法统一拦截所有文件读写操作,比如做日志、权限检查,因为调用点散落在每个工具里。
用 ctx.fs Seam,这三个问题都消失了:
- 测试时注入内存文件系统 Provider
- E2B 里注入
fs-e2bProvider - 审计时在 Provider 层统一记录所有调用
Consumer 代码示例(概念示例)
export const inject = ['fs']
export function apply(ctx: Context): void {
ctx.tools.register(defineTool({
name: 'read_file',
description: '读取本地文件内容',
execute: async (args, exec) => {
const content = await ctx.fs.readFile(args.path)
return content
},
}))
}
export default [
'@deepseek-ai/dsh-fs-sandbox',
'@deepseek-ai/dsh-tool-fs',
]
重点:ctx.sandbox(进程沙箱 Seam)
Shell 执行背后是进程沙箱。dsh 定义了三种沙箱模式:
type SandboxMode =
| 'read-only'
| 'workspace-write'
| 'danger-full-access'
平台实现是自动适配的:
| 平台 | 底层机制 |
|---|---|
| Linux | bwrap + Landlock |
| macOS | Seatbelt |
| Windows | ACL 受限令牌 |
一行配置换后端(概念示例):
同一份 tool-shell 代码,在本地跑用 sandbox-local,在 CI 里跑用 sandbox-e2b,切换只发生在 Bundle 配置层。
重点:ctx.llm(LLM 适配器 Seam)
LLM 也是 Seam。这个设计乍一看有点出人意料,但仔细想想很合理。
为什么 LLM 调用也要抽象?
- 切换供应商 :同一个 Agent,本地测试用轻量模型,生产用 DeepSeek,只换 Provider 插件。
- 回放测试 :
llm-replayProvider——不发真实 API 请求,直接按顺序回放历史 session 的 assistant 响应。测试 Agent 行为不需要 API key,也不会有随机性。 - 多模型路由 :可以实现一个 Provider,根据任务类型路由到不同模型。
实战:自己实现一个 Provider
实现一个只读文件系统 Provider——所有写操作直接报错,适合给 Agent 开只读访问权限。
export const name = 'my-readonly-fs'
export function apply(ctx: Context): void {
ctx.provide('fs', {
async readFile(path: string): Promise<string> {
return await localReadFile(path)
},
async writeFile(path: string, content: string): Promise<void> {
throw new Error(`Read-only filesystem: cannot write to ${path}`)
},
async listFiles(dir: string): Promise<string[]> {
return await localListFiles(dir)
},
})
}
export default [
'./my-readonly-fs',
'@deepseek-ai/dsh-tool-fs',
]
这个模式的强大之处:你不需要改 tool-fs 的任何代码,就限制了 Agent 的文件系统权限。
Seam 设计的核心价值
用一张表对比:
| 场景 | 不用 Seam | 用 Seam |
|---|---|---|
| 切换 LLM 供应商 | 改遍所有 API 调用代码 | 换一个 Provider 插件 |
| 单元测试文件操作 | 需要 mock node:fs,或在磁盘上建真实文件 |
直接注入内存文件系统 Provider |
| 部署到沙箱环境 | 需要改大量工具代码适配沙箱 API | 换 fs-sandbox 或 fs-e2b |
| 新增平台/后端支持 | 改核心代码,可能引入 bug | 实现新 Provider,Consumer 不受影响 |
| 权限控制 | 在每个工具里加 if 判断 | 在 Provider 层统一拦截 |
Seam 本质上是依赖注入(DI)的一种特化形式,只是在插件系统里,注入的粒度是"服务名",而不是构造函数参数。
设计总结
dsh Capability Seam 的核心思路:
Consumer 只知道接口名,Provider 只知道接口,切换发生在配置层。
| 设计决策 | 原因 |
|---|---|
| 所有外部能力走 Seam | 测试、部署、多后端支持全都受益 |
| LLM 也是 Seam | 支持 replay 测试、供应商切换、多模型路由 |
| 沙箱也是 Seam | 本地/CI/远程环境用不同沙箱后端,Agent 代码不变 |
| Consumer 只声明服务名 | 解耦彻底,不依赖任何具体实现 |
| Bundle 是配置层 | 组合不同 Provider 只改一处,不改业务代码 |
系列下一篇
下一篇讲 多 Agent 协作 :当一个 Agent 解决不了复杂任务时,dsh 是怎么支持多个 Agent 分工配合的——委托(delegation)、子 Agent、并行执行,以及这些机制在 Session 层面是怎么表示的。
在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页
English / 原文
先从一个家用比喻开始
你家的电灯开关,它不关心电从哪里来——市电、太阳能还是发电机都行。开关只定义一件事:"开/关"。只要背后接的东西遵守这个接口,你按下开关,灯就亮。
这就是 Seam (缝隙)的本质:一条接口边界,把"用这个能力"和"这个能力怎么实现"隔开来。
在 dsh 里:
- 文件系统操作 是一个 Seam(
ctx.fs) - Shell 执行 是一个 Seam(
ctx.shell) - LLM 调用 是一个 Seam(
ctx.llm) - 进程沙箱 是一个 Seam(
ctx.sandbox)
你可以把默认的本地文件系统换成 E2B 沙箱文件系统,工具代码一行不改。这就是 Capability Seam 要解决的问题。
Seam 的三个角色
每个 Seam 都由三类参与者构成:
┌────────────────────────────────────────────────────────┐
│ Consumer: tool-fs │
│ import ctx.fs → calls ctx.fs.readFile(path) │
│ ctx.fs.writeFile(path, content) │
└─────────────────────┬──────────────────────────────────┘
│ 依赖服务 "ctx.fs"
┌─────────────────────▼──────────────────────────────────┐
│ Service Definition: dsh-fs │
│ interface FileSystem { readFile, writeFile, ... } │
└─────────────────────┬──────────────────────────────────┘
│ 实现
┌───────────────┴───────────────┐
▼ ▼
fs-local fs-e2b
(本地文件系统) (E2B 沙箱)
三个角色的分工:
| 角色 | 职责 | 举例 |
|---|---|---|
| Service Definition | 定义接口 + 服务名 | dsh-fs:定义 ctx.fs 的方法签名 |
| Service Provider | 实现接口,注册到 ctx |
fs-local、fs-e2b、fs-sandbox |
| Service Consumer | 使用接口,不关心实现 | tool-fs:读写文件的工具插件 |
Consumer 只知道服务名(如 ctx.fs),不知道背后是哪个 Provider。切换 Provider 只影响 Bundle 的配置,Consumer 完全无感。
核心 Seam 一览
dsh 内置了以下核心 Seam:
| Seam | 服务名 | 默认实现 | 可替换为 |
|---|---|---|---|
| 文件系统 | ctx.fs |
fs-local(本机) |
fs-e2b(E2B 沙箱)、fs-sandbox(受限本机) |
| Shell 执行 | ctx.shell |
bash-local |
bash-sandbox(受限执行)、远程 Shell |
| 进程沙箱 | ctx.sandbox |
sandbox-local(bwrap/Seatbelt/ACL) |
容器、microVM |
| LLM 适配器 | ctx.llm |
llm-deepseek |
llm-pi-ai 等第三方 |
| 认证凭据 | ctx.credentials |
credentials-local |
远程 vault |
| 用户设置 | ctx.settings |
settings-file |
远程设置服务 |
| 持久化存储 | ctx.sessionPersistence |
session-persistence-jsonl |
SQLite、远端 |
重点:ctx.fs(文件系统 Seam)
为什么不直接 import fs from 'node:fs'?
直接 import Node.js 内置的 fs 模块,会带来三个具体问题:
- 测试时 :要测试"读文件"逻辑,必须在磁盘上真的创建文件。或者用 mock,但 mock Node.js 内置模块需要额外工具(jest.mock 等),很麻烦。
- 在沙箱里跑时 :E2B 有自己的文件系统接口,不是本地 Node.js
fs。如果工具代码直接 import 了node:fs,到 E2B 里就完全跑不起来。 - 审计 :你无法统一拦截所有文件读写操作,比如做日志、权限检查,因为调用点散落在每个工具里。
用 ctx.fs Seam,这三个问题都消失了:
- 测试时注入内存文件系统 Provider
- E2B 里注入
fs-e2bProvider - 审计时在 Provider 层统一记录所有调用
Consumer 代码示例(概念示例)
export const inject = ['fs']
export function apply(ctx: Context): void {
ctx.tools.register(defineTool({
name: 'read_file',
description: '读取本地文件内容',
execute: async (args, exec) => {
const content = await ctx.fs.readFile(args.path)
return content
},
}))
}
export default [
'@deepseek-ai/dsh-fs-sandbox',
'@deepseek-ai/dsh-tool-fs',
]
重点:ctx.sandbox(进程沙箱 Seam)
Shell 执行背后是进程沙箱。dsh 定义了三种沙箱模式:
type SandboxMode =
| 'read-only'
| 'workspace-write'
| 'danger-full-access'
平台实现是自动适配的:
| 平台 | 底层机制 |
|---|---|
| Linux | bwrap + Landlock |
| macOS | Seatbelt |
| Windows | ACL 受限令牌 |
一行配置换后端(概念示例):
同一份 tool-shell 代码,在本地跑用 sandbox-local,在 CI 里跑用 sandbox-e2b,切换只发生在 Bundle 配置层。
重点:ctx.llm(LLM 适配器 Seam)
LLM 也是 Seam。这个设计乍一看有点出人意料,但仔细想想很合理。
为什么 LLM 调用也要抽象?
- 切换供应商 :同一个 Agent,本地测试用轻量模型,生产用 DeepSeek,只换 Provider 插件。
- 回放测试 :
llm-replayProvider——不发真实 API 请求,直接按顺序回放历史 session 的 assistant 响应。测试 Agent 行为不需要 API key,也不会有随机性。 - 多模型路由 :可以实现一个 Provider,根据任务类型路由到不同模型。
实战:自己实现一个 Provider
实现一个只读文件系统 Provider——所有写操作直接报错,适合给 Agent 开只读访问权限。
export const name = 'my-readonly-fs'
export function apply(ctx: Context): void {
ctx.provide('fs', {
async readFile(path: string): Promise<string> {
return await localReadFile(path)
},
async writeFile(path: string, content: string): Promise<void> {
throw new Error(`Read-only filesystem: cannot write to ${path}`)
},
async listFiles(dir: string): Promise<string[]> {
return await localListFiles(dir)
},
})
}
export default [
'./my-readonly-fs',
'@deepseek-ai/dsh-tool-fs',
]
这个模式的强大之处:你不需要改 tool-fs 的任何代码,就限制了 Agent 的文件系统权限。
Seam 设计的核心价值
用一张表对比:
| 场景 | 不用 Seam | 用 Seam |
|---|---|---|
| 切换 LLM 供应商 | 改遍所有 API 调用代码 | 换一个 Provider 插件 |
| 单元测试文件操作 | 需要 mock node:fs,或在磁盘上建真实文件 |
直接注入内存文件系统 Provider |
| 部署到沙箱环境 | 需要改大量工具代码适配沙箱 API | 换 fs-sandbox 或 fs-e2b |
| 新增平台/后端支持 | 改核心代码,可能引入 bug | 实现新 Provider,Consumer 不受影响 |
| 权限控制 | 在每个工具里加 if 判断 | 在 Provider 层统一拦截 |
Seam 本质上是依赖注入(DI)的一种特化形式,只是在插件系统里,注入的粒度是"服务名",而不是构造函数参数。
设计总结
dsh Capability Seam 的核心思路:
Consumer 只知道接口名,Provider 只知道接口,切换发生在配置层。
| 设计决策 | 原因 |
|---|---|
| 所有外部能力走 Seam | 测试、部署、多后端支持全都受益 |
| LLM 也是 Seam | 支持 replay 测试、供应商切换、多模型路由 |
| 沙箱也是 Seam | 本地/CI/远程环境用不同沙箱后端,Agent 代码不变 |
| Consumer 只声明服务名 | 解耦彻底,不依赖任何具体实现 |
| Bundle 是配置层 | 组合不同 Provider 只改一处,不改业务代码 |
系列下一篇
下一篇讲 多 Agent 协作 :当一个 Agent 解决不了复杂任务时,dsh 是怎么支持多个 Agent 分工配合的——委托(delegation)、子 Agent、并行执行,以及这些机制在 Session 层面是怎么表示的。
在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页