在这里插入图片描述
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

前言

做AI Agent开发的兄弟,谁没脑补过Agent删库跑路的名场面?

你让它改个配置文件,它反手给你执行rm -rf /;你让它清理日志,它把系统盘一起扬了。能跑命令、能写文件的Agent,说好听点是生产力工具,说难听点就是没装保险丝的电锯——用着爽,出事就是重伤。

今天就聊DeepSeek Harness的沙箱系统,看看人家是怎么给Agent套上安全围栏的。

1 先给沙箱定个性:只管文件系统效果

很多人以为沙箱是万能盾,网络、进程、啥都能管。错了,dsh的沙箱定义特别明确:就管文件系统那点事。

网络访问限不限制?进程能不能看见宿主?这些都不在沙箱的业务范围内。想管网络?去进程层面做,沙箱只管“文件能不能读、能不能写”这件事。

1.1 三种模式,对应三种信任等级

直接上代码,沙箱模式就三种:

type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'

给大家翻译成人话:

read-only:纯观光模式。只能看文件,啥也改不了,临时文件都只能往/dev/null里写。适合代码审查、只读分析这种场景,就像你去博物馆,只能看不能摸,摸了就报警。

workspace-write:正常办公模式。工作目录随便你写,系统目录想都别想,/tmp也给你用。这是最常用的模式,就像公司给你分配了工位,你在自己工位上贴海报吃外卖都行,去总裁办公室乱翻试试?

danger-full-access:全权托管模式。啥都能写,根目录都给你敞开。注意了,这个模式根本不走沙箱服务,直接原始执行,相当于你把家门钥匙、银行卡密码全交出去了,不是极度信任的环境千万别开。

补个冷知识:read-only模式下,POSIX的runner还会给shell留个/dev/null当“垃圾桶”,Windows的ACL runner就比较抠,连个可写的地方都不给。

2 强制执行完整性:别拿partial当full用

沙箱后端会报告自己的执行完整性,就俩值:full和partial。

type SandboxEnforcement = 'full' | 'partial'

full就是承诺的所有文件效果都管住了,实打实的安全边界。partial就是因为各种原因,只能管住一部分,有缺口。

2.1 哪些场景会出现partial?

给大家列一下坑:

平台partial原因影响范围
Linux旧Landlock ABI版本部分文件操作拦不住
WindowsACL runner的Everyone边界环境ACL有缺口
Windows硬链接边界能通过硬链接绕过去

这里必须夸一句dsh的原则:fail loud,no silent degradation。

啥意思?宁可直接拒绝执行,也不偷偷降级。我做不到就是做不到,告诉你这是partial,你自己掂量着用。不像有些产品,宣传的时候说安全拉满,实际防护半残,还不告诉你,等出事了才发现裤衩都漏风。

划重点:需要绝对安全边界的场景,别用partial,直接拒绝,别凑合。

3 逐调用策略:不是全局配置,是每次调用单独算

很多人以为沙箱策略是全局配一次就完事了,dsh偏不,每次能力调用都会生成一份独立的执行策略。

interface SandboxExecutionPolicy {
  mode: SandboxMode         // 文件效果模式
  workspaceRoot: string     // workspace-write 可写的绝对根目录
  sessionId?: SessionId     // 调用会话的 opaque 身份标识
}
3.1 策略解析的优先级链

策略怎么来的?有个优先级链:

1. 显式批准的 mode 覆盖(用户审批后重试时携带)
    ↓ 如果没有
2. 会话策略
    ↓ 如果没有
3. 部署配置回退(没有 agent 时)

最有意思的是workspaceRoot的来源:会话的当前工作目录,而且要做两步规范化,不能光看字面路径。

为啥要两步?因为有符号链接的坑啊。比如路径是/home/user/link/…,词法上看是/home/user,实际link可能指向完全别的地方,光改字符串就被骗了。

这种设计的好处是什么?两个并发会话,可以向同一个沙箱服务要不同的边界,互不干扰,不用改服务状态。就像你和你室友合租,各自房间各自锁,不用出门还要给大门换锁,方便得很。

4 三个平台后端实现,原生隔离各显神通

dsh-sandbox-local给三个平台都做了原生隔离后端,不是一套方案到处套。

4.1 Linux:bwrap + Landlock 双保险

Linux这边是组合拳:bwrap做命名空间隔离,给你整个独立的文件系统视图;Landlock做内核级的访问控制,在内核层面把写入限制焊死。

看bwrap的参数就懂了:

// bwrap 命令行参数
export function bwrapProfileArgs(policy: SandboxPolicy): string[] {
  const args = [
    '--ro-bind', '/', '/',    // 根文件系统只读挂载
    '--dev', '/dev',           // 挂载 /dev
    '--proc', '/proc',         // 挂载 /proc
    '--die-with-parent'        // 父进程退出时杀子进程
  ]
  if (policy.mode === 'workspace-write') {
    args.push('--tmpfs', '/tmp')                        // /tmp 用 tmpfs
    args.push('--bind', policy.workspaceRoot, policy.workspaceRoot)  // 工作区可写挂载
  }
  return args
}

根目录直接只读挂载,工作区单独挂成可写,父进程挂了子进程立刻陪葬,防止变成孤儿进程搞事情。

4.2 macOS:Seatbelt 内核沙箱

macOS用的是系统自带的Seatbelt,用SBPL语言写策略。逻辑很简单:默认允许所有操作,先拒绝所有写入,再把允许写的路径一个个加上。

export function seatbeltProfileArgs(policy: SandboxPolicy): string[] {
  const forms = [
    '(version 1)',
    '(allow default)',
    '(deny file-write*)',
    `(allow file-write* (literal "${sbplString('/dev/null')}"))`
  ]
  const roots = writableRoots(policy)
  if (roots.length > 0) {
    forms.push(`(allow file-write* ${roots.map(root => `(subpath "${root}")`).join(' ')})`)
  }
  return ['-p', forms.join(' ')]
}

相当于先把所有门都锁死,再给你配几把钥匙开对应的门,简单粗暴但有效。

4.3 Windows:ACL 受限令牌

Windows这边是给子进程一个受限的安全令牌,靠ACL控制权限。read-only模式下连个显式可写目录都不给,而且因为环境ACL有缺口,只能报告partial。

懂的都懂,Windows的权限系统向来都是“看起来很严,实际总能找到缝”,老传统了。

4.4 三个后端横向对比
维度Linux bwrap+LandlockmacOS SeatbeltWindows ACL
隔离机制命名空间 + 内核LSM内核沙箱策略受限令牌 + ACL
read-only可写区/dev/null/dev/null
workspace-write可写区/tmp + workspace/tmp + workspaceworkspace
完整性full(新内核)/ partial(旧Landlock)fullpartial
进程隔离是(命名空间)部分部分

多说一句:容器、microVM、远程执行这些,都是同级的执行环境方案,不是沙箱的后端。它们是直接替换整个执行环境的,和本地沙箱是平行选项。

5 沙箱在工具链里的位置:不是内嵌,是包装

很多人以为沙箱是在工具内部调用的,其实不是。它是在tools/execute的事件链里,作为wrapper注入进去的。

以bash-sandbox为例,执行流程是这样的:

模型请求执行 bash 命令
  |
  v tools/pre-execute (waterfall)
  |-- permission-presets: 静态规则匹配
  |-- user-approval: 需要时人工审批
  |
  v tools/execute (waterfall)
  |-- bash-sandbox 包装器:
  |     1. ctx.sandboxPolicy.resolve(session) 解析策略
  |     2. mode = danger-full-access -> 直接 spawn
  |     3. mode = read-only 或 workspace-write -> ctx.sandbox.run()
  |     4. 沙箱 Provider 包装 argv:
  |        Linux: bwrap --ro-bind / / ... -- bash -c "command"
  |        macOS: sandbox-exec -p "(deny file-write*)..." bash -c "command"
  |        Windows: 创建受限令牌进程
  |     5. 执行命令,收集输出
  |     6. 记录 mode + enforcement 到 processFacts
  |
  v tools/post-execute (waterfall)
  |-- 结果处理

说白了,就是命令执行前必经沙箱这一关,给你包装一层再跑。就像你寄快递,快递员不是直接把东西扔车上,是先给你套个快递盒,再贴上面单,出问题也知道是哪个包裹的。

而且bash-sandbox和bash-local是两个平行的Provider,实现同一个接口,消费方根本感知不到区别。想用哪个配哪个,代码不用改。

6 权限审批和沙箱:分工明确,不抢活

dsh的权限系统和沙箱是协作关系,不是一回事。

职责介入时机机制
权限预设dsh-permission-presets静态规则匹配tools/pre-executeallow/deny/ask
用户审批dsh-user-approval人工确认tools/pre-executeask后追问
沙箱dsh-sandbox-local文件效果隔离tools/executeargv包装
单调守卫dsh-tools(内置)不可推翻否决tools/executeguard()
环境清理dsh-bash-localenv脱敏执行前ENV_OVERRIDES

一句话总结:权限系统问“能不能做”,沙箱管“做了能影响啥”。一个管决策放行,一个管后果边界,分层不重叠。

举个例子:默认workspace-write被权限系统拒了,用户审批后同意用danger-full-access重试,这时候沙箱就直接放行,不做隔离了——用户都拍板了,沙箱就不拦着了。

7 防御性模式里的两个安全细节,很容易被忽略

dsh的防御性模式里有两条规则,看着不起眼,实际踩过坑的都知道有多重要。

7.1 绝不把敏感环境变量暴露给子进程

工具执行的时候,会先清理环境变量,所有带KEY、SECRET、TOKEN、PASSWORD的变量全给你扬了。

const ENV_OVERRIDES = {
  NO_COLOR: '1',
  TERM: 'dumb',
  PAGER: 'cat',
  GIT_PAGER: 'cat',
}

别觉得这是小事。之前就有公司的Agent执行脚本,把环境变量里的云服务密钥打日志里泄露了,直接损失六位数。把密钥放环境变量里,再传给不可信的子进程,纯属把银行卡密码写在便利贴上贴显示器上。

7.2 删除文件:链接和目录分开处理

删除文件的时候,先判断是不是符号链接。是链接就删链接本身,不是再递归删目录。

// 防止跟随符号链接删除意外目标
if (lstatSync(path).isSymbolicLink()) {
  unlinkSync(path)    // 链接用 unlink
} else {
  rmSync(path, { recursive: true })  // 真实目录才用递归删除
}

这个坑踩过的人估计后背都发凉。要是不判断,一个指向根目录的符号链接,你递归删除,直接把整个系统盘给清了,删库跑路都没这么快。

8 沙箱是Seam:想换后端?消费方代码一行不用改

沙箱本质上是个接口,不是具体实现。你想自己写个Docker后端?完全可以,实现ctx.sandbox.run这个接口就行。

给大家看个Docker后端的示例:

import { SandboxService, type SandboxPolicy, type SandboxResult } from '@deepseek-ai/dsh-sandbox'

class DockerSandbox extends SandboxService {
async run(argv: string[], options: ExecOptions, policy: SandboxPolicy): Promise {
if (policy.mode === 'danger-full-access') {
return spawnDirectly(argv, options)
}
const dockerArgs = [
'docker', 'run', '--rm',
'--read-only',  // 只读根文件系统
]
if (policy.mode === 'workspace-write') {
dockerArgs.push('-v', ${policy.workspaceRoot}:/workspace:rw)
dockerArgs.push('--workdir', '/workspace')
}
// Docker 额外可限制网络(超出沙箱词汇表但 Docker 可做)
dockerArgs.push('--network', 'none')
dockerArgs.push('dsh-sandbox:latest')
dockerArgs.push(...argv)
const result = await spawnDirectly(dockerArgs, options)
return {
...result,
enforcement: 'full' as const  // Docker 容器隔离是完整的
}
}
}

写完之后,配置里换一下Provider名就行,上层的bash-sandbox代码一行都不用动。

这就是Seam的意思:接口统一,实现可替换。Linux用Landlock,macOS用Seatbelt,你想用Docker用Docker,消费方完全感知不到变化。就像你家插座,插什么牌子的充电器都能充电,不用为了换个充电器改家里的电路。

9 凭据隔离:配置只带引用,不带真实值

安全层还有个重要的部分:凭据服务。

// 凭据引用是 POSIX 风格环境变量名
type CredentialRef = Branded<'CredentialRef'>

// 解析结果
interface ResolvedCredential {
value: string    // 非空机密值
source: string   // 提供方定义的来源层 id
}

啥意思?配置文件里只写变量名,比如DEEPSEEK_API_KEY,不写真实的密钥值。值归凭据Provider管,每次操作的时候实时解析。

好处太多了:

首先,配置文件泄露了也不怕,里面都是引用,没有真实值。其次,密钥轮换不用重启服务,下一次模型请求自动用新值。

对比一下老系统:改个密钥要改配置,改完要重启服务,重启要等半天,业务都停摆了。dsh这设计,轮换密钥悄无声息就完成了。

10 最后放张全景表,部署的时候照着查

职责机制
权限预设dsh-permission-presets静态规则匹配tools/pre-execute
用户审批dsh-user-approval人工确认tools/pre-execute
沙箱dsh-sandbox / dsh-sandbox-local文件效果隔离tools/execute
单调守卫dsh-tools(内置)不可推翻否决tools/execute
环境清理dsh-bash-localenv脱敏执行前
凭据隔离dsh-credentials-local机密不进配置凭据服务
spill保护dsh-spill-policy工具输出溢出处理tools/code-dispatch-log

其实dsh的安全层说复杂也不复杂,核心就是每一层只管一件事,组合起来形成完整边界。权限管放行,沙箱管边界,凭据管机密,各司其职,不瞎掺和。

毕竟做AI Agent安全,最怕的就是“大而全”的设计,啥都想管,最后啥都管不好。不如像dsh这样,分层明确,每个模块做好自己的事,反而更靠谱。

P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐