<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>源码拆解 on AI博士 万戈</title>
        <link>https://www.yesmiracle.net/tags/%E6%BA%90%E7%A0%81%E6%8B%86%E8%A7%A3/</link>
        <description>AI博士万戈的技术博客，聚焦 Agentic AI、AI Infra 与 Agent Security，分享 AI 基础设施与工程落地实践。</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <managingEditor>admin@yesmiracle.net (万戈)</managingEditor>
        <webMaster>admin@yesmiracle.net (万戈)</webMaster>
        <lastBuildDate>Tue, 27 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.yesmiracle.net/tags/%E6%BA%90%E7%A0%81%E6%8B%86%E8%A7%A3/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Anthropic 开源 srt 沙箱：不装容器也能关住 Agent，密钥防线其实在出口代理</title>
        <link>https://www.yesmiracle.net/post/20261027-anthropic-srt-sandbox-teardown/</link>
        <pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261027-anthropic-srt-sandbox-teardown/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261027-anthropic-srt-sandbox-teardown/cover.svg" alt="Featured image of post Anthropic 开源 srt 沙箱：不装容器也能关住 Agent，密钥防线其实在出口代理" /&gt;&lt;p&gt;Anthropic 这两天把 Claude Code 的 Bash 沙箱单独拎出来开源了，仓库叫 &lt;code&gt;anthropics/sandbox-runtime&lt;/code&gt;，命令行工具名 &lt;code&gt;srt&lt;/code&gt;，版本 0.0.81，Apache-2.0，目前约 6,695 star。README 的开场是一句话：在操作系统层面给任意进程加上文件和网络限制，不需要容器。&lt;/p&gt;
&lt;p&gt;听起来是个很克制的工具。我把它 clone 下来通读了一遍，这句话本身没错，但它把一半的故事留在了后面。等你翻完 &lt;code&gt;src/sandbox/&lt;/code&gt;，会发现这个仓库最重的部分不在「怎么把进程关起来」，而在「怎么让它能干活，同时拿不到钥匙」。&lt;/p&gt;
&lt;h2 id=&#34;它默认什么都不给&#34;&gt;它默认什么都不给&lt;/h2&gt;
&lt;p&gt;先说清楚它的行为。srt 是 secure-by-default 的路子：进程起来时几乎没有权限，你一点点把需要的洞打开。&lt;/p&gt;
&lt;p&gt;写是 allow-only。默认全禁，你必须显式列出可写路径，空列表就是完全不能写。读反过来，是 deny-then-allow，默认全放，你可以先禁掉一大片（比如整个 &lt;code&gt;/Users&lt;/code&gt;），再把里面具体的路径放回来。这里有个反直觉的地方：读的放行优先（&lt;code&gt;allowRead&lt;/code&gt; 盖过 &lt;code&gt;denyRead&lt;/code&gt;），写的禁止优先（&lt;code&gt;denyWrite&lt;/code&gt; 盖过 &lt;code&gt;allowWrite&lt;/code&gt;），两条路的优先级方向是反的。网络同样是 allow-only，默认没有出网路由，一个个列域名。&lt;/p&gt;
&lt;p&gt;官方在 README 和文档里反复强调一件事：文件和网络两个隔离必须同时在。只有文件隔离，被 prompt injection 的进程照样能把 &lt;code&gt;~/.ssh&lt;/code&gt; 读出来发到外网；只有网络隔离，进程可以先改系统文件、再借那个文件拿到网络。这条「双隔离」是全文的地基，后面所有机制都在为它服务。&lt;/p&gt;
&lt;h2 id=&#34;同一道边界三个平台三套做法&#34;&gt;同一道边界，三个平台三套做法&lt;/h2&gt;
&lt;p&gt;这是我觉得最有信息量的一段。同一个目标，三个平台落地的形态完全不同，我整理成了一张对照表：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;维度&lt;/th&gt;
          &lt;th&gt;macOS&lt;/th&gt;
          &lt;th&gt;Linux&lt;/th&gt;
          &lt;th&gt;Windows（alpha）&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;文件系统&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;sandbox-exec&lt;/code&gt; 动态生成 Seatbelt profile&lt;/td&gt;
          &lt;td&gt;bubblewrap 的 &lt;code&gt;--ro-bind&lt;/code&gt; / &lt;code&gt;--bind&lt;/code&gt; / &lt;code&gt;--tmpfs&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;专用 &lt;code&gt;srt-sandbox&lt;/code&gt; 本地账户、写受限 token、工作树显式 ACE&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;网络出口&lt;/td&gt;
          &lt;td&gt;Seatbelt 只允许连一个 localhost 端口&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;--unshare-net&lt;/code&gt; 拔掉整个网络命名空间&lt;/td&gt;
          &lt;td&gt;按账户 SID 下机器级 WFP 过滤器，只放环路到代理端口段&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;本地 IPC&lt;/td&gt;
          &lt;td&gt;交给 Seatbelt&lt;/td&gt;
          &lt;td&gt;seccomp-bpf 拦 &lt;code&gt;socket(AF_UNIX)&lt;/code&gt; 与 &lt;code&gt;AF_VSOCK&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;job object 加账户隔离&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;违规监控&lt;/td&gt;
          &lt;td&gt;读系统 sandbox violation 日志库&lt;/td&gt;
          &lt;td&gt;seccomp USER_NOTIF 观察写意图&lt;/td&gt;
          &lt;td&gt;每个会话真跑连接做 WFP 自检&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;后端语言&lt;/td&gt;
          &lt;td&gt;TypeScript&lt;/td&gt;
          &lt;td&gt;TypeScript 加 C（seccomp）&lt;/td&gt;
          &lt;td&gt;独立的 Rust crate&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Linux 这条路最干净。bwrap 的参数是 &lt;code&gt;--new-session --die-with-parent&lt;/code&gt;，再按需要挂上 &lt;code&gt;--unshare-net&lt;/code&gt;、&lt;code&gt;--unshare-pid&lt;/code&gt;、&lt;code&gt;--unshare-user&lt;/code&gt;，配合 &lt;code&gt;--ro-bind&lt;/code&gt; 铺只读目录。源码里有一条注释写得很直白：如果没上 &lt;code&gt;--unshare-pid&lt;/code&gt;，进程有机会逃出沙箱。所以 PID 命名空间是默认就 unshare 的，&lt;code&gt;/proc&lt;/code&gt; 也重新挂一份。&lt;/p&gt;
&lt;p&gt;macOS 那条路的麻烦在 Seatbelt profile 本身：&lt;code&gt;sandbox-exec&lt;/code&gt; 拒绝超过 1025 字节的字符串字面量，编译几千个路径过滤器又要花时间。作者的解法是做一层 carve filter，把「大区域禁止 + 小范围放行」的老套路压成尽量少的正则。这类工程细节不上手写一遍是不会知道的。&lt;/p&gt;
&lt;p&gt;Windows 走的是第三条路，也是工程量最大的一条：起一个专用的 &lt;code&gt;srt-sandbox&lt;/code&gt; 本地账户，进程跑在这个身份下，再加一道机器级的 Windows Filtering Platform 出口封锁，按这个账户的 SID 过滤，只放行环路到代理端口段。它连「这个账户到底连不连得出去」都要每个会话实测一遍：用沙箱账户的身份真发几次连接，有一次通了就拒绝启动。&lt;/p&gt;
&lt;h2 id=&#34;便宜的那一半和贵的那一半&#34;&gt;便宜的那一半，和贵的那一半&lt;/h2&gt;
&lt;p&gt;把三个平台摊开看，有个结论挺清楚：文件系统隔离是最商品化的一环。bwrap、Seatbelt、WFP 都是系统现成的能力，包一层壳谁都能做。这个仓库真正把工程量堆上去的，是沙箱外面的出口代理。&lt;/p&gt;
&lt;p&gt;三个平台都把进程的原始出口掐死，只留一条通往宿主代理的路。Linux 是整个删掉网络命名空间，代理监听 unix socket、再 bind 进沙箱；macOS 的 Seatbelt 只准连一个本地端口；Windows 用 WFP 按身份封，代理就监听在那个端口段里。HTTP/HTTPS 走 HTTP 代理，其它 TCP 走 SOCKS5，两条都由代理按域名白名单放行或拒绝。&lt;/p&gt;
&lt;p&gt;代理这一层在这个仓库里是实打实的重量级：&lt;code&gt;http-proxy.ts&lt;/code&gt; 1,439 行，&lt;code&gt;tls-terminate-proxy.ts&lt;/code&gt; 1,514 行，外加 &lt;code&gt;socks-proxy.ts&lt;/code&gt;、&lt;code&gt;mux-proxy.ts&lt;/code&gt;、&lt;code&gt;parent-proxy.ts&lt;/code&gt;。但这些还只是运输层。真正有意思的是它往上长出来的凭据子系统。&lt;/p&gt;
&lt;h2 id=&#34;改了-http_proxy-也没用墙在操作系统里&#34;&gt;改了 HTTP_PROXY 也没用，墙在操作系统里&lt;/h2&gt;
&lt;p&gt;有个问题得单独拎出来：代理是「指路牌」，本身不是「墙」。任何工具都能 unset &lt;code&gt;HTTP_PROXY&lt;/code&gt;，或者干脆自己开个 socket 直连 IP。那 srt 拿什么保证流量一定走代理？&lt;/p&gt;
&lt;p&gt;答案是它不靠环境变量。三个平台各用一种操作系统级别的硬手段，把「除了代理没有别的路可走」变成一条事实。&lt;/p&gt;
&lt;p&gt;Linux 用的是网络命名空间。bwrap 的 &lt;code&gt;--unshare-net&lt;/code&gt; 把沙箱塞进一个连网络接口都没有的命名空间：进程里没有任何路由，直连外网 IP 会在 syscall 层直接失败。它能碰到的唯一端点，是同一命名空间里由 socat 桥监听的两个本地端口（HTTP 3128、SOCKS 1080），桥再通过宿主侧的 unix socket 接到真代理。所以 agent 改 &lt;code&gt;HTTP_PROXY&lt;/code&gt; 只是让它连错地方，unset 则让它哪儿都去不了，因为那个命名空间里除了这两条桥什么都没有。&lt;/p&gt;
&lt;p&gt;macOS 用的是 Seatbelt。开启网络限制后，profile 里只下发一条出站规则：&lt;code&gt;(allow network-outbound (remote ip &amp;quot;localhost:&amp;lt;代理端口&amp;gt;&amp;quot;))&lt;/code&gt;。&lt;code&gt;connect()&lt;/code&gt; 到别的任何远端地址，内核直接拒。这里有个我读到时觉得讲究的细节：Seatbelt 的 network-outbound 是按源地址评估的，而未绑定的 socket 在 &lt;code&gt;connect()&lt;/code&gt; 那一刻源地址是 &lt;code&gt;INADDR_ANY&lt;/code&gt;，&lt;code&gt;localhost&lt;/code&gt; 又会匹配任意地址，于是任何写成 &lt;code&gt;(local ip ...)&lt;/code&gt; 的出站规则等于放行一切，他们因此坚持用 &lt;code&gt;(remote ip &amp;quot;localhost:*&amp;quot;)&lt;/code&gt;。这种坑不真跑一遍踩不出来。&lt;/p&gt;
&lt;p&gt;Windows 用的是 WFP。机器级的 &lt;code&gt;ALE_AUTH_CONNECT&lt;/code&gt; 过滤器按 &lt;code&gt;srt-sandbox&lt;/code&gt; 账户的 SID 封出口，只放行到代理端口段的环路连接。README 有一句把这个模型说得最清楚：环境变量只是把工具指向代理，边界是过滤器，一个无视或者 unset 它们的进程照样被挡住。&lt;/p&gt;
&lt;p&gt;那这条路拦不住什么，也得一起说。Linux 的命名空间是「全有或全无」，它保证的只是「你必须经过代理」，域名白名单在这一层并不存在，全凭代理自己实现，这句话就写在源码注释里。macOS 一旦打开 &lt;code&gt;allowLocalBinding&lt;/code&gt;（为本地开发服务器而设），profile 会放行 &lt;code&gt;(remote ip &amp;quot;localhost:*&amp;quot;)&lt;/code&gt;，沙箱进程就能绕开代理直连本机端口。Windows 的那个归因缝前面提过，进程换个 token 就绕过了。还有一条更容易被忽略：请求出不出得去是一回事，代理替它做了什么又是另一回事。代码里专门把 &lt;code&gt;TRACE&lt;/code&gt; 和 &lt;code&gt;TRACK&lt;/code&gt; 直接拒掉（405），原因就是这类请求会回显请求内容，等于把代理注入的凭据原样吐回给沙箱。&lt;/p&gt;
&lt;h2 id=&#34;凭据不进沙箱只在出口换回来&#34;&gt;凭据不进沙箱，只在出口换回来&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;srt&lt;/code&gt; 的配置里有一个 &lt;code&gt;credentials&lt;/code&gt; 段，管环境变量和文件两类东西。每一类有两种模式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mode: &amp;quot;deny&amp;quot;&lt;/code&gt; 最简单，值直接不给，沙箱里读不到。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mode: &amp;quot;mask&amp;quot;&lt;/code&gt; 是这个仓库的核心设计。它给沙箱里的进程一个假值，真值留在宿主的代理进程里。假值长这样：&lt;code&gt;fake_value_&amp;lt;uuid4&amp;gt;&lt;/code&gt;，并且会按真值的长度做随机填充对齐，这样 HTTP body 在替换时 &lt;code&gt;Content-Length&lt;/code&gt; 不用变，代理可以直接原样转发头。替换发生在出口，而且只发往这一条配置里 &lt;code&gt;injectHosts&lt;/code&gt; 列出的主机，默认就是你的 &lt;code&gt;allowedDomains&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;文件的 mask 只在 Linux 生效：把一个装着哨兵内容的假文件只读覆盖在真文件上，沙箱读到的是假内容。macOS 和 Windows 退化成直接 deny。值本身存在进程内存里的一张注册表，不落盘、不打日志。&lt;/p&gt;
&lt;p&gt;env 变量这层还有一个更细的开关叫 &lt;code&gt;extract&lt;/code&gt;。不填它时是整值替换，一个哨兵顶掉整串。填了它则是结构化替换，用正则只圈出凭据那一段。拿一条 &lt;code&gt;DATABASE_URL&lt;/code&gt; 连接串举例，整值替换会让解析它的工具拿到一段语法都不成立的字符串，圈段替换只把密码换掉，其余部分原样保留，工具照常 parse。配套的 &lt;code&gt;onExtractNoMatch&lt;/code&gt; 决定正则什么都没匹配到时的行为，可以选直接把这个变量扣掉、退化成 deny，避免把一个没脱敏成功的真值放进沙箱。&lt;/p&gt;
&lt;p&gt;这里有个必须一提的约束：masking 依赖 &lt;code&gt;network.tlsTerminate&lt;/code&gt;。也就是说，要开这个功能，srt 得在进程内做 TLS 终结（也就是大伙常说的 MITM）：它给沙箱塞一个包含自己 CA 的信任 bundle，于是代理能解开 HTTPS，看到明文，做替换，再用真凭据向真实上游握手。设计上的取舍是明确的，真值只经由一条它自己验证过的 TLS 连接出去，否则宁可不开。证书固定的客户端、走 mTLS 的内部服务，都可以用 &lt;code&gt;excludeDomains&lt;/code&gt; 排除，让它们走不透明隧道，只是这样凭据注入也就对它们失效了。&lt;/p&gt;
&lt;p&gt;再往上还有一层，是给 AWS 用的。你把 &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; 打成 mask 之后，客户端拿假 secret 算出来的 SigV4 签名是错的，光换 header 里的 access key id 修不好。所以代理做的事是重签：剥掉客户端的 &lt;code&gt;Authorization&lt;/code&gt;，保留它的 credential scope、签名头集合和 &lt;code&gt;x-amz-date&lt;/code&gt;，用真 secret 按实际上行的请求重新算一遍签名。这套协议代码是对着官方 &lt;code&gt;aws-sig-v4-test-suite&lt;/code&gt; 的向量验过的。签名形态分几类，header 签名的可以重签，分块上传和预签名 URL 的重签成本太高，走 deny 或放行策略。&lt;/p&gt;
&lt;p&gt;读到这我大概理解了作者的取舍：拦住一个 Agent，难点从来不在把它关起来，而在让它正常干活的同时，别让密钥跟着它一起掉进沙箱。文件隔离解决的是「它改了什么」，这个代理层解决的是「它拿走了什么」。&lt;/p&gt;
&lt;h2 id=&#34;我数了一遍这个仓库&#34;&gt;我数了一遍这个仓库&lt;/h2&gt;
&lt;p&gt;为了不凭感觉说话，我把源码量了下：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;指标&lt;/th&gt;
          &lt;th&gt;实测&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;仓库&lt;/td&gt;
          &lt;td&gt;anthropics/sandbox-runtime，TypeScript，Apache-2.0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/*.ts&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;31,381 行，49 个文件&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;测试&lt;/td&gt;
          &lt;td&gt;131 个 &lt;code&gt;.test.ts&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Windows 后端&lt;/td&gt;
          &lt;td&gt;独立 Rust crate，27 个 &lt;code&gt;.rs&lt;/code&gt;，29,280 行&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;凭据相关模块&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;credential-*.ts&lt;/code&gt; 六个，另有 &lt;code&gt;aws-sigv4.ts&lt;/code&gt;、&lt;code&gt;mitm-ca.ts&lt;/code&gt;、&lt;code&gt;mitm-leaf.ts&lt;/code&gt;、&lt;code&gt;tls-terminate-proxy.ts&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;仓库文件总数&lt;/td&gt;
          &lt;td&gt;378（不含 &lt;code&gt;.git&lt;/code&gt;）&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两个数字值得停一下。一个是 Windows 那 29,280 行 Rust，比三个平台的 TypeScript 加起来还多，说明「在 Windows 上做同样的隔离」这件事被严重低估了。另一个是凭据相关的模块数量：六个 &lt;code&gt;credential-*&lt;/code&gt; 加上 MITM 与 SigV4 一整条链路。从模块划分看，凭据在这里是一套有独立设计的子系统，而不是随手加的补丁。&lt;/p&gt;
&lt;h2 id=&#34;它到底怎么用&#34;&gt;它到底怎么用&lt;/h2&gt;
&lt;p&gt;读完架构，落地形态有三种，值得分开看。&lt;/p&gt;
&lt;p&gt;最直接的是命令行。&lt;code&gt;srt &amp;quot;curl anthropic.com&amp;quot;&lt;/code&gt; 把这条命令裹进沙箱，配一个 &lt;code&gt;~/.srt-settings.json&lt;/code&gt; 描述读写和域名规则。没有配置文件时用一套内置默认值；但配置文件存在却读不了、或者校验不过，它会直接报错退出，而不是退回默认值。这个细节我认同：默认值和配置文件是两套不同的策略，静默降级会把用户写在文件里的 deny 规则一并丢掉。&lt;/p&gt;
&lt;p&gt;第二种是把它套在 MCP server 外面。&lt;code&gt;.mcp.json&lt;/code&gt; 里把 &lt;code&gt;command&lt;/code&gt; 从 &lt;code&gt;npx&lt;/code&gt; 换成 &lt;code&gt;srt&lt;/code&gt;，参数不动，那个 server 就整个跑进了边界里。官方拿 filesystem server 举例：套上之后让它写一个被 deny 的目录，会直接拿到 &lt;code&gt;EPERM&lt;/code&gt;。MCP server 是现在 agent 生态里权限最大的一批组件，能一行配置包起来，是它比较容易落地的入口。&lt;/p&gt;
&lt;p&gt;第三种是 &lt;code&gt;srt proxy&lt;/code&gt;，把这个 HTTP 代理单独跑起来，不带子进程。宿主把负载放在别处，另一台虚拟机也好、另一个容器也好，只把它的流量指到这个代理上，再另起一个 decider 进程做放行决策。代理本身不持有策略，decider 说放行才出去。这个形态是奔着「代理和沙箱解耦」去的，也解释了为什么它对 fd 传参、lifeline、握手超时这些细节抠得那么细。&lt;/p&gt;
&lt;h2 id=&#34;它自己承认拦不住什么&#34;&gt;它自己承认拦不住什么&lt;/h2&gt;
&lt;p&gt;这篇不是软文，值得把它 README 里的 Known limitations 拎出来。&lt;/p&gt;
&lt;p&gt;Windows 的 WFP 封锁有个结构性的缝：过滤引擎按「创建 socket 那一刻线程上的 token」归因，所以一个进程在创建 socket 之前把另一个 token 放到线程上，就不会被认成 &lt;code&gt;srt-sandbox&lt;/code&gt;，也就绕过了封锁；系统服务代它转发的流量同样不匹配。这是 alpha 阶段的已知缺口。&lt;/p&gt;
&lt;p&gt;macOS 有两个开关会削弱模型。&lt;code&gt;allowLocalBinding&lt;/code&gt; 让沙箱进程可以不经过代理直连环路端口，官方给的理由是本地开发服务器要用。&lt;code&gt;enableWeakerNetworkIsolation&lt;/code&gt; 为了 Go 程序（&lt;code&gt;gh&lt;/code&gt;、&lt;code&gt;gcloud&lt;/code&gt;、&lt;code&gt;terraform&lt;/code&gt; 这些）能验证证书，会打开 &lt;code&gt;com.apple.trustd.agent&lt;/code&gt;，文档里明确写了这是个潜在的数据外泄面。&lt;/p&gt;
&lt;p&gt;Linux 的 seccomp 过滤器范围很窄，只拦 &lt;code&gt;socket(AF_UNIX, ...)&lt;/code&gt;、&lt;code&gt;socket(AF_VSOCK, ...)&lt;/code&gt;，再加上 &lt;code&gt;io_uring_setup&lt;/code&gt;/&lt;code&gt;io_uring_enter&lt;/code&gt;/&lt;code&gt;io_uring_register&lt;/code&gt;（后三个是因为 Linux 5.19 起 &lt;code&gt;IORING_OP_SOCKET&lt;/code&gt; 能绕过 socket 规则）。它不拦父进程继承来的、或者通过 &lt;code&gt;SCM_RIGHTS&lt;/code&gt; 传进来的 unix socket 描述符，32 位 x86 上的 &lt;code&gt;socketcall()&lt;/code&gt; 也没堵。&lt;/p&gt;
&lt;p&gt;把这些列出来反而更有说服力。一个沙箱工具如果只讲自己拦住了什么，通常意味着它没认真想绕过路径。&lt;/p&gt;
&lt;h2 id=&#34;被拦下之后它怎么把账算清&#34;&gt;被拦下之后，它怎么把账算清&lt;/h2&gt;
&lt;p&gt;一个沙箱光是拦住还不够，出了边界尝试得让人知道是谁干的。srt 把三类事件收在一起：macOS 的 seatbelt 日志行、Linux 的 seccomp 事件、代理层的 deny。每条都挂在一个归因键下，&lt;code&gt;annotateStderrWithSandboxFailures&lt;/code&gt; 和 &lt;code&gt;getViolationsForCommand&lt;/code&gt; 用同一个键去查。&lt;/p&gt;
&lt;p&gt;默认的键就是被包裹的那条命令字符串。这里有个坑官方专门提示了：键只比较前 100 个字符，所以长命令如果前缀相同会互相串味，同一条命令重跑还会继承上一轮的事件。正确做法是传一个 &lt;code&gt;commandId&lt;/code&gt;（比如某个 tool-use id），把同一段脚本的多次调用分开。&lt;/p&gt;
&lt;p&gt;还有一个「问主人」的机制。当某个目标既没被 deny 列表也没被 allow 列表决定时，可以注册一个 &lt;code&gt;SandboxAskCallback&lt;/code&gt; 回调，交给宿主自己判断，只有返回 &lt;code&gt;true&lt;/code&gt; 才放行；没有注册回调就直接拒绝。这个回调和 Claude Code 里那个「这条命令要不要批」的交互是同一件事的底层，只是把决策权交回给了调用方。&lt;/p&gt;
&lt;h2 id=&#34;它替代不了微虚拟机这不是新旧关系&#34;&gt;它替代不了微虚拟机，这不是新旧关系&lt;/h2&gt;
&lt;p&gt;读到这很容易得出一个结论：操作系统原语又快又不用容器，那微虚拟机是不是可以不要了。不是。这两个是不同威胁模型下的不同边界，不是轻量版和重量版的替代关系。&lt;/p&gt;
&lt;p&gt;先看 srt 用的是哪一类边界。Seatbelt 管的是系统调用，bwrap 管的是命名空间，WFP 管的是网络过滤，它们全都跑在同一个内核里。这条路成立的前提是 host 可信、内核和这些原语本身都没被拿下。前面那段 Known limitations 里列的绕过，本质上都是同一内核之内的绕过，一旦绕过去，攻击者和宿主共用同一个内核，中间没有第二道墙。&lt;/p&gt;
&lt;p&gt;微虚拟机换的是另一个层级：整个 guest 内核都在边界里面。攻击者即使把 guest 内核打穿，还要再越一次 hypervisor。它付出的代价是启动慢、要管镜像、要处理挂载，换回来的是「哪一层被攻破才算数」这个问题的答案彻底不同。&lt;/p&gt;
&lt;p&gt;所以这里没有谁更先进的问题，只取决于你防的是谁。&lt;/p&gt;
&lt;p&gt;你自己就是宿主、主机可信，只是不想让一个被 prompt injection 的 agent 乱来，那 OS 原语这一档又快又够用，不用镜像、不用等启动，还能把审批弹窗压下来（Anthropic 自己的说法是内部使用中减少了 84% 的权限提示）。&lt;/p&gt;
&lt;p&gt;主机本身就不可信，比如多租户跑别人的 agent，或者客户要的是一句可被审计的「内核级隔离」，那这一档不成立，必须上微虚拟机。&lt;/p&gt;
&lt;p&gt;还有一点能说明 srt 自己也没打算替代谁。它只管 shell 这一层：Claude Code 的文件工具、WebFetch、本地 MCP server、hook 全部跑在沙箱外面。它还专门留了 &lt;code&gt;srt proxy&lt;/code&gt; 这个能单独跑、不带子进程的形态，明说负载可以放在「a VM, a container, another sandbox」里，这一层控制面坐在它外面。它是往你已有的边界上再叠一层，不是把边界换掉。&lt;/p&gt;
&lt;h2 id=&#34;写在最后&#34;&gt;写在最后&lt;/h2&gt;
&lt;p&gt;官方叙事是「操作系统级沙箱，不要容器」。这句话对，但它的重心把注意力引到了隔离层，而隔离层恰好是这行里最不稀缺的东西。bwrap、Seatbelt、WFP 都是系统给的能力，任何一支有经验的团队两周就能包出一个能用的壳。这个仓库真正花代价的是出口那半边：把凭据从沙箱里彻底拿掉，在出口按主机和请求范围重放，给 AWS 重新签名。&lt;/p&gt;
&lt;p&gt;顺着这个结构往下想，沙箱这个品类正在分成两层：下面一层是隔离，上面一层是凭据和授权。要注意的是，下面这一层被逐级商品化，不等于微虚拟机没用了。OS 原语这一档已经免费，容器这一档也差不多，微虚拟机这一档正在被各家云厂托管成一个 API。档位仍然需要，选哪一档取决于前面说的威胁模型；被消解掉的是「拿这档隔离当差异化」这件事。&lt;/p&gt;
&lt;p&gt;对做 Agent 基础设施的人，这份源码给出的信号很直接。如果你的差异化只是「我有隔离」，那它已经被一个 6,695 star 的开源项目覆盖了，而且这一档还在继续往下掉价。能收费的地方在出口：凭据的注入范围怎么定、重放策略怎么审、证据怎么留。而这一层目前还没有事实标准，谁先把接口做出来，谁就占住位置。&lt;/p&gt;
&lt;p&gt;相关阅读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261023-nvidia-openshell-agent-security-runtime-teardown/&#34; &gt;《NVIDIA OpenShell 拆解》&lt;/a&gt; — 另一条把 Agent 安全做进内核拦截的路线&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261018-perplexity-space-sandbox-source-teardown/&#34; &gt;《Perplexity SPACE 拆解》&lt;/a&gt; — 沙箱「一次做全」的另一种风格&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261019-opencode-byok-console-credential-breach/&#34; &gt;《OpenCode 凭据泄露》&lt;/a&gt; — 凭据没管好会是什么后果&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
    </channel>
</rss>
