2026 年 10 月 7 日,AWS 开源了 Strands Box,开发者预览版,Apache 2.0 许可。要解决的问题很具体:当一个 Agent 能读文件、跑 shell、执行生成的代码、调用 API 时,最危险的不是模型「想做坏事」,而是它读到的不可信内容把它引向越权操作。官方博客把前提写得很直接,这类工作负载可能被它读到的不可信内容操纵。
常见的做法是加一层沙箱,把 Agent 能碰到的东西框住。但 AWS 指出,光有隔离并不够。隔离决定了能碰到什么,没有决定碰到之后能做什么。一个排查线上故障的 Agent 可能需要读日志、看基础设施,但不应该能改它们。Strands Box 的答案是:操作系统级隔离负责划出硬边界,策略引擎负责边界内的每一个动作。
为什么 Agent 需要「默认拒绝」
安全文档里有一句话可以当整个设计的纲:「Anything you grant the agent, it can read, change, or send.」也就是你授予 Agent 的任何东西,它都能读取、修改或发送。
反过来说,只列禁止项的黑名单思路注定会漏。合理做法是反过来,只有被明确允许的操作才能执行。Box 的规则语义正是这样:引擎检查到的操作默认被拒绝,必须有匹配的 permit 才放行,而任何匹配的 forbid 会覆盖 permit。一旦策略加载失败,整个过程会停止;规则求值出错也一律按拒绝处理。
项目目前是开发者预览,版本号在 0.1.x,行为可能随版本变化。官方建议先用它对照自己的威胁模型,再决定要不要拿它跑不可信 Agent。
三层执行架构
官方把 Box 拆成三层,两层是执行,一层是边界。
- 操作系统沙箱,用操作系统级隔离(在 macOS 上是 Seatbelt)限制每个进程能触达的路径、主机和网络。这是硬边界。
- 策略引擎,内嵌的 Dogwood Local Engine 在多个执行点做默认拒绝的判定,判定点包括网络出口、Python 解释器、Shell 解释器和 MCP 服务器代理。
- 凭据边界,沙箱内每个进程持有的只是占位符,真实凭据由沙箱外的受信进程在放行时注入。
这些执行点用统一的方式上报动作,谁能触发都一样。一次文件读取,不管是 shell 命令还是 Python 脚本做的,都是一个 fs:read 事件;一次 HTTP 请求,不管来自 curl 还是 Python,都是 http:request。正因为这样,你才能写出「Agent 读过客户数据目录后,就阻断后续所有对外 HTTP 请求」这种不区分工具的规则。
Box 还特意解释了为什么嵌入两个代码解释器(Strands Shell 和 Python 的 Monty)。Agent 越来越倾向于写代码来干活,而这些代码能读文件、能发网络请求,正好是执行策略的好位置。举个例子,Agent 跑 rm -rf build/,Shell 会把命令解析开,对它将要删除的每个文件各抛出一个 fs:delete 决策,于是针对 fs:delete 的 forbid 能在删除发生前拦住它,而工作区内的正常编辑照常通过。策略看到的是文件和操作,而不是一个不透明的系统调用。
代价也很清楚:解释器运行在沙箱之外,它们本身就是执行策略的代码,所以 Box 的受信计算基变宽了。
信任边界方面,受信的是操作系统内核、Box 二进制及其受信进程、以及运维者编写的 box.toml、策略、凭据引用和声明的工具与 MCP 服务器;不受信的是工作负载本身(含 Agent 框架与模型输出)、工具和 MCP 服务器的返回值、以及工作负载抓取的远程内容。
官方还说明了为什么不用容器或 microVM。microVM 的隔离边界很强,是跑不可信代码的合理选择,但 Box 想让 Agent 待在开发者已有的环境里,而不是再起一个访客操作系统,省掉一层要维护的环境和「怎么把本地文件和工具暴露进去」的决策。它的取舍是:用 OS 级隔离划硬边界,把策略执行放在 Agent 越过边界的那些点上。
Dogwood 策略:permit、forbid 与时间条件
一个 Box 由两个文件配置,box.toml 描述运行环境(命令、工作目录、直接文件系统授权、可用工具、MCP 服务器、凭据绑定),policy.dw 存放 Dogwood 规则。Dogwood 沿用 Cedar 的语法,每条规则是对 principal、action、resource 的一个 permit 或 forbid,条件写在 when 里。在 Box 里 principal 永远是 Agent、resource 固定,所以规则主要匹配 action,请求细节放在 context.input。
官方给的第一个例子是一次性判定,允许外呼 Anthropic 模型 API:
@id("allow_model_api")
permit (principal, action == Box::Action::"http:request", resource)
when {
context.input.host == "api.anthropic.com" &&
context.input.port == 443
};
Dogwood 真正有意思的地方是时间操作符,它让授权可以取决于 Agent 之前做过什么。官方给的场景是,一个排查故障的 Agent 应该能往 Slack 事故频道发进度,但别刷屏把人类的更新淹没。目标是允许它发,但每 10 分钟不超过三条。
@id("allow_slack_post")
permit (principal, action == Box::Action::"http:request", resource)
when {
context.input.host == "slack.com" &&
context.input.port == 443 &&
context.input.method == "POST" &&
context.input.path == "/api/chat.postMessage"
};
@id("rate_limit_slack_posts")
@description("Slack posts are capped at three every 10 minutes. Wait before you post again.")
forbid (principal, action == Box::Action::"http:request", resource)
when {
context.input.host == "slack.com" &&
context.input.port == 443 &&
context.input.method == "POST" &&
context.input.path == "/api/chat.postMessage"
}
when temporal {
(count for (t: Timepoint). where (
formerly within 10m (
Box::Action::"http:request"::response{
input.host: "slack.com",
input.port: 443,
input.method: "POST",
input.path: "/api/chat.postMessage",
output.status: 200
} && tp(t)
)
)) >= 3
};
第一条放行发帖,第二条在最近十分钟已经记录到三次 200 响应后再禁止。时间子句从里往外读,::response{…} 匹配一次返回 200 的 Slack 发帖,formerly within 10m 只保留最近十分钟的,count for (t: Timepoint) 数符合条件的时刻。被拒绝时不是静默失败,网关返回一个 HTTP 403,正文写着 policy denied this operation,并带上规则的 @id 和 @description,这样 Agent 可以据此调整,比如选择等待而不是重试。
官方还点出了一个容易踩的细节:这条规则绑定的是 ::response 事件,所以只统计真正返回 200 的请求,被拒绝的尝试不算;如果绑成 ::request,拒绝本身也会被计入。
凭据边界:Agent 只拿到占位符
出口网关是网络侧的拦截点,它能看到每个请求的主机、端口、方法、路径,抛出 http:request 决策,只转发策略允许的部分。也因为它在请求路径上,它能做一件 Agent 自己安全做不到的事:附加凭据。对配置了 API key 的路由,Agent 拿到的是占位符令牌,网关在转发被允许的请求前把它换成真实值。真实密钥从未进入 Agent 环境。
目前支持的注入方式有五种,Bearer 令牌替换、自定义 HTTP 头、HTTP Basic 认证、查询参数,以及用 AWS 凭据做 SigV4 签名。此外,系统目录、机器级密钥和凭据存储属于固定拒绝项,不受策略配置影响。Shell 和 Python 也碰不到 Box 自己的状态,以及本次运行加载的 box.toml 和策略文件。
官方列出的遗留风险
安全文档没有把沙箱包装成万能方案,而是逐条写明即使正确配置也依然存在的风险,让运维者自己去管。
- 被放行的目的地本身就是外泄通道。Agent 能读的东西,就能发到策略允许的任何目的地。
- 云元数据端点默认可达。网关没有内置拒绝名单,需要在每条策略里对
net:connect显式禁止元数据主机和地址。 - Box 内的每个进程都能使用每一份凭据。网关无法分辨请求来自哪个进程,所以一个工具或第三方 stdio MCP 服务器能用上 Box 声明的任何凭据。
- 直接授权不产生决策记录。写在
box.toml里的路径可以绕过策略判定直接访问,只有走 Box 的 shell 和 Python 的操作才被判定。 - 解释器自身不会拒绝凭据存储。没有路径条件的
fs:read许可会让 shell 读到~/.aws和~/.ssh,所以这类许可一定要带路径条件。 - 工具和 MCP 服务器的权限比 Agent 更宽,一个
shell:spawn许可覆盖工具整棵进程树。 - 策略判定不保密。拒绝消息会点名规则和
@description,工作负载能据此摸清策略轮廓。 - 基于
::request的时间规则会统计被拒的尝试,应该改用::response。 - 资源未被限制。Box 不对进程数、CPU、内存、磁盘设限。
- 写入授权内无法恢复文件时间戳,依赖时间戳的构建或同步可能出问题。
- 决策记录是尽力而为、且不可防篡改的,不能当作审计轨迹。
- 内核漏洞和侧信道不在防护范围内,需要这类防护时要把 Box 放进 VM。
责任划分也写得清楚:Box 负责执行的正确性;运维者负责授权范围、策略宽度、每一处 contain_egress = false 例外,以及哪些程序共享凭据;平台方负责宿主环境隔离,多租户部署时每个会话应跑在各自的 VM 或 microVM 里。
上手前的检查清单
官方当前只从 macOS 起步,扩展到其他操作系统是下一步的重点之一,所以想在 Linux 服务器生产环境直接用,目前条件还不具备。评估时建议逐项确认:
- 环境是否在支持范围内,是否接受 macOS 15 上的一个已知差异(Box 的 Seatbelt 配置会按名拒绝
file-graft与file-ungraft,这两个操作在 macOS 26 之前没有定义,所以在 macOS 15 上不做这两项拒绝,且不告警)。 - 每一个被放行的 HTTP 目的地,是否接受它成为外泄通道。
- 是否显式禁止了云元数据端点。
- 是否存在设置
contain_egress = false的进程,原因是什么。 - 时间规则是否用了
::response而不是::request。 - 拒绝日志是否接入了现有审计系统,并清楚它的不可防篡改边界。
本文没有在真实环境中测试 Box 的性能开销或拦截效果,以上内容均基于官方博客与官方安全文档。
