AgentScope第23式·画地沙箱

AgentScope第23式·沙箱边界

危险操作不该发生在宿主进程里,但太多团队第一步就搞错了谁在隔离谁。

[读者] 已经把 Agent 跑到 Memory、Skill、Plan 阶段,准备让模型真正读写文件、执行脚本的 Java 工程师
[痛点] Agent 在宿主目录里拼路径、跑脚本,越权读写和资源失控没有任何兜底
[现在读] 第23式是 Agent 从“会说”走到“会做”的关卡,沙箱是这条路上唯一的安全阀
[读完] 能讲清 Sandbox 与 Filesystem 的职责边界,理解 SandboxManager 的生命周期与快照/恢复,并完成一次“危险操作被拒、正常脚本产物安全交付”的验收

[读者] Java 后端 / AI Agent 工程师
[现状] Agent 已能调工具,但文件与脚本仍落在宿主
[目标] 让不受信脚本在隔离环境里跑完,并把产物安全交出来
[旧方案] 宿主进程 + 相对路径拼字符串
    |
    v
[新需求] 执行不受信脚本,路径/网络/命令/资源都要限
    |
    v
[冲突] 隔离写在应用代码里,挡不住进程级副作用
    |
    v
[后果] 越权读写与资源耗尽直接落在宿主机上

我是老李,Java 后端做了十几年,最近两年带着一队兄弟用 AgentScope Java 2.x 做企业数字人。

我们这个 Demo 从第3式冻结到现在,已经长到二十多章:能聊天、能记账、能按计划调工具,前两天甚至能读上传的 CSV,自己写脚本画趋势图。

但只要 Agent 开始写文件、跑脚本,我这根弦就绷起来了——它执行时的权限,和宿主 Java 进程是同一份。第22式的 Plan 让它学会了“先规划再动手”,第23式必须解决“动手时别把家拆了”。

当 Agent 开始跑脚本,你还敢让它在宿主进程里跑吗?

Sandbox 和 Filesystem 这两个词天天一起出现,到底谁负责隔离,谁负责语义?

沙箱一旦引入,生命周期、快照/恢复、产物交付,这些又该谁来管?

01、故事

上周三晚上,我在自己机器上把 chapter/22-plan 拉起来,给数字人下了一条指令:把上传的销售 CSV 画成趋势图,再把结论写进项目开场白。

Plan 模块很顺畅地拆出三步,read_file 读数据,run_python 出图,最后回写项目配置。整个链路跑完不到十秒,我当时的反应是:可以了,Agent 终于不只是嘴强王者。

任务是让数字人自己算出结论并写回项目配置,而当时的实现只有三段:一个新的 ProcessBuilder,一个 workspace/{projectId} 目录,一个把相对路径拼成绝对路径的小工具类。

第二天演示,同事看完只问了一句:如果模型这次生成的脚本不是画图,而是 rm -rf ~,或者把 /etc/passwd 读出来塞进开场白,你这套代码拦得住吗?

我盯着那段路径拼接代码看了半分钟,答案是拦不住。路径校验写在工具实现里,工具是模型挑的;命令白名单只写在 systemPrompt 里,那是给模型看的,不是给系统看的。

冲突就出现在这里:能力越强,边界就越不能靠自觉。

宿主不是边界
路径不能靠自觉
先立规矩再动手
沙箱里见真章

02、问题

旧做法是“把 Agent 当成一个受信任的本地脚本调用者”:文件读写用相对路径拼绝对路径,命令执行用 ProcessBuilder,资源限制没有,超时时间随便写个 30 秒。

这套做法在 Demo 阶段没问题,一旦 Agent 开始处理外部上传的数据、执行模型自己写出来的脚本,三个技术表现会立刻暴露:

第一,路径逃逸。相对路径里的 ../ 一旦没被规范化就被拼接,工作区之外的文件照样能读写。

第二,命令注入。只要允许 sh -c,脚本里出现 ;、|、&& 就能挂上第二条命令,白名单写的是 python3,实际跑的是别的东西。

第三,资源失控。死循环脚本、大内存分配、外网回连,宿主进程只能眼看着 CPU 被打满。

业务影响也很直接:数字人项目里存着多个客户的配置和上传文件,一次越权读取就是一次数据泄露,一次资源耗尽就是整个非实时接口全挂。

所以本篇的完成标准定得很具体:危险路径和越权操作必须被拒绝,正常脚本必须在沙箱内执行并把产物安全交付。这两条都做到了,第23式才算过关。

白名单写在提示词
等于把门交给模型
边界必须落在代码
拒绝要比补救便宜

03、原理

先把名词边界说清楚,只说本篇用得上的部分。

Sandbox,在 AgentScope Java 里指的是给“不受信执行”准备的隔离执行环境抽象:它决定脚本在哪跑、能看到哪些文件、能用多少资源。

SandboxManager,是这套东西的组织者:负责沙箱实例的创建、获取、释放,负责选择后端,也负责和快照/恢复这些生命周期能力打交道。

Filesystem,是 Agent 视角下的文件读写抽象:它决定 Agent 用什么路径、什么语义去读写,解决的是可移植和一致性,不解决隔离。

反直觉判断:Filesystem 抽象不是安全边界。 把读写抽成接口,只是让你能换实现、能在测试里 Mock,真正决定“能不能读到 /etc/passwd”的,是它下面挂的哪个根目录、跑在哪个进程或哪个容器里。抽象层做得再漂亮,沙箱没配好,一样白搭。

反直觉判断:Guard 越靠外越可靠,越靠内越容易被绕过。 很多人习惯把路径校验写在 ReadFileTool 里,把命令白名单写在 RunScriptTool 里。问题是工具的调用入口不止一个:Agent 直接调、Subagent 调、Plan 执行器调、未来还可能被 HTTP 接口直接调。守卫写在最外层(沙箱编排层),所有入口都得先过这道门;写在最里层,等于门上装了锁,但旁边还开着窗。

还有一个容易被忽略的点:沙箱的快照/恢复解决的是“可重放、可回滚”,不是“更安全”。一次执行前打快照,是为了失败后能回到干净状态,让同一段 Plan 可以重跑;它并不会让越权操作变得无害。把这两件事混在一起,就会写出“反正能恢复,先跑跑看”的危险代码。

官方文档 agentscope-java/docs/v2/zh/docs/harness/sandbox.md(v2.0.3)对 Sandbox 的定位、后端扩展思路和生命周期都有描述,本篇的实现原则都以这份文档和 agentscope-harness 下的 SandboxManager.java 为准。

文件系统管语义
沙箱只负责隔离
两件事别混一起
混淆就是漏洞源

04、架构

[输入] Agent 请求:读文件 / 写文件 / 执行脚本
    |
    v
[模块] 沙箱编排层(SandboxManager + 策略 + Guard)
    |
    v
[数据/状态] Sandbox 实例 + 挂载的工作区 + 执行前快照
    |
    v
[处理] 路径校验 -> 命令校验 -> 隔离执行 -> 资源限额 -> 产物导出
    |
    v
[输出] 受限读写结果 / 执行输出 / 产物路径(回传 Agent)

这个链路里,官方 SandboxManager 是装配点,教程的 DigitalHumanSandboxService 是业务门面,Guard 和策略是教程自己写的、可测的那部分。

  • 边界:Sandbox 负责“在哪跑、能看到什么、能用多少资源”;Filesystem 负责“Agent 用什么路径语义读写”。Realtime Bridge、LiveKit 完全不参与这一层,它们只关心最终产出的话术和音频。
  • 收益:模型生成的脚本即使写错,破坏范围也被限制在工作区快照和容器内;路径、命令、网络、资源四条限制全部落在代码上,不依赖模型自觉。
  • 代价:多一层编排意味着多一次进程/容器启动,本地调试链路变长;Docker 不可用时需要降级策略。
  • 适用条件:需要执行模型生成的脚本、处理外部上传文件、或者 Agent 会碰多租户数据。
  • 不适用条件:纯计算、无外部输入、无文件写出的工具调用,硬套沙箱只会增加延迟和运维负担。

后端扩展思路上,官方 sandbox 文档给出的方向是“Sandbox 是可替换的后端”:本地进程、Docker、Kubernetes、E2B、AgentRun 这类远端沙箱,差别在于隔离强度和启动成本。教程在 chapter/23-sandbox 先用 Docker 后端把链路跑通,业务代码只依赖 SandboxRunner 接口,后续换后端不动业务。

生命周期归管理
快照恢复可重放
守卫放在最外层
产物走受控通道

05、实战一次

本章对应教程分支 chapter/23-sandbox,从第22式继续创建:

 git checkout chapter/22-plan
 git pull --ff-only
 git checkout -b chapter/23-sandbox

建议提交信息:feat(ch23): execute risky operations in sandbox。

环境与依赖:沿用第2式跑通的 AgentScope Java v2.0.3 环境基线,本章不新增第三方坐标,沙箱启动只用 JDK 自带的 ProcessBuilder 调 Docker CLI(具体 sandbox 相关依赖坐标与版本,请以 v2.0.3 官方 README 和 pom 为准)。前提是本机 Docker 可用。

第一步,定义策略。 策略是唯一的事实来源,Guard 和 Runner 都读它:

package com.lifuchun.digitalhuman.sandbox;

import java.nio.file.Path;
import java.util.Set;

public record SandboxPolicy(
        Path workspaceRoot,
        boolean allowNetwork,
        Set<String> allowedCommands,
        long cpuMillis,
        long memoryMb,
        long wallClockSeconds) {

    public static SandboxPolicy forProject(Path workspaceRoot) {
        return new SandboxPolicy(
                workspaceRoot.toAbsolutePath().normalize(),
                false,
                Set.of("python3", "sh"),
                5_000L,
                512L,
                20L);
    }
}

第二步,路径 Guard。 注意这里用的是 Path.startsWith,不是字符串前缀比较:

package com.lifuchun.digitalhuman.sandbox;

import java.nio.file.Path;

public final class PathGuard {

    private final Path root;

    public PathGuard(Path root) {
        this.root = root.toAbsolutePath().normalize();
    }

    public Path resolve(String relative) {
        Path candidate = root.resolve(relative).normalize();
        if (!candidate.startsWith(root)) {
            throw new SandboxViolationException("path escaped workspace: " + relative);
        }
        return candidate;
    }
}

第三步,命令 Guard。 只认首词,并且先拦 shell 元字符:

package com.lifuchun.digitalhuman.sandbox;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public final class CommandGuard {

    private static final List<String> FORBIDDEN = List.of(";", "|", "&", "`", "$", ">", "<", "\n");

    private final Set<String> allowed;

    public CommandGuard(Set<String> allowed) {
        this.allowed = Set.copyOf(allowed);
    }

    public List<String> parse(String commandLine) {
        for (String token : FORBIDDEN) {
            if (commandLine.contains(token)) {
                throw new SandboxViolationException("forbidden shell token: " + token);
            }
        }
        String[] parts = commandLine.trim().split("\\s+");
        if (parts.length == 0 || !allowed.contains(parts[0])) {
            throw new SandboxViolationException("command not allowed: " + commandLine);
        }
        List<String> result = new ArrayList<>();
        for (String part : parts) {
            result.add(part);
        }
        return result;
    }
}

第四步,Runner 接口与 Docker 实现。 业务代码只依赖接口,换 Kubernetes 或远端沙箱时不动上层:

package com.lifuchun.digitalhuman.sandbox;

import java.util.List;

public interface SandboxRunner {
    SandboxResult run(List<String> command);
}
package com.lifuchun.digitalhuman.sandbox;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public final class DockerSandboxRunner implements SandboxRunner {

    private final SandboxPolicy policy;
    private final String image;

    public DockerSandboxRunner(SandboxPolicy policy, String image) {
        this.policy = policy;
        this.image = image;
    }

    @Override
    public SandboxResult run(List<String> command) {
        Path hostWorkspace = policy.workspaceRoot();
        String containerWorkspace = "/workspace";

        List<String> args = new ArrayList<>();
        args.add("docker");
        args.add("run");
        args.add("--rm");
        args.add("--network=" + (policy.allowNetwork() ? "bridge" : "none"));
        args.add("--memory=" + policy.memoryMb() + "m");
        args.add("--cpus=1");
        args.add("-v");
        args.add(hostWorkspace + ":" + containerWorkspace + ":rw");
        args.add("-w");
        args.add(containerWorkspace);
        args.add(image);
        args.addAll(command);

        ProcessBuilder builder = new ProcessBuilder(args);
        builder.redirectErrorStream(true);
        try {
            Process process = builder.start();
            String output;
            try (InputStream in = process.getInputStream()) {
                output = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            }
            boolean finished = process.waitFor(policy.wallClockSeconds(), TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                return new SandboxResult(false, output, "timeout after " + policy.wallClockSeconds() + "s");
            }
            return new SandboxResult(process.exitValue() == 0, output, null);
        } catch (IOException e) {
            return new SandboxResult(false, "", e.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new SandboxResult(false, "", "interrupted");
        }
    }
}

第五步,产物导出。 产物不走“直接把路径回给 Agent 让它自己读”,而是经一个受控通道复制到 artifacts 目录,再回传相对路径:

package com.lifuchun.digitalhuman.sandbox;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public final class ArtifactExporter {

    private final PathGuard guard;

    public ArtifactExporter(Path artifactsRoot) {
        this.guard = new PathGuard(artifactsRoot);
    }

    public Path export(Path workspace, String relative) throws IOException {
        Path source = new PathGuard(workspace).resolve(relative);
        if (!Files.isRegularFile(source)) {
            throw new SandboxViolationException("artifact not found: " + relative);
        }
        Path target = guard.resolve(relative);
        Files.createDirectories(target.getParent());
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
        return target;
    }
}

第六步,装配。 教程里通过官方 SandboxManager 统一暴露沙箱实例的生命周期(获取、释放、快照),业务侧只依赖 DigitalHumanSandboxService。SandboxManager 的当前方法签名与快照相关 API,请对照 v2.0.3 源码 agentscope-harness/src/main/java/io/agentscope/harness/agent/sandbox/SandboxManager.java 与同版本 sandbox.md 使用,教程不逐字抄写官方签名,避免版本漂移。

验收两条命令。 第一条是越权路径:

 curl -X POST http://localhost:8080/api/sandbox/exec \
   -H "Content-Type: application/json" \
   -d '{"projectId":1,"command":"python3 plot.py --input ../../etc/passwd"}'

未在当前环境实测,以下为预期结果(示例输出):

{
  "success": false,
  "error": "path escaped workspace: ../../etc/passwd"
}

第二条是正常脚本:

 curl -X POST http://localhost:8080/api/sandbox/exec \
   -H "Content-Type: application/json" \
   -d '{"projectId":1,"command":"python3 plot.py --input data/sales.csv --output artifacts/report.json"}'

预期结果(示例输出):

{
  "success": true,
  "artifact": "artifacts/report.json",
  "bytes": 2048
}

先建分支再动手
策略先行后装配
越权路径当场拒
正常脚本交产物

06、排查

证据链一:路径校验为什么会漏。

现象:本地把 workspace 设成 /data/ws/1,访问 ../2/secret.txt 被拒了,但访问工作区外的符号链接目录却没被拒。

怀疑:normalize() 之后还漏了什么。

检查:翻自己的实现,发现早期版本用的是 candidate.toString().startsWith(root.toString()),后来虽然换成了 Path.startsWith,但符号链接仍然没处理。

错误尝试: 最早我写的是字符串前缀比较 —— path.toString().startsWith(root.toString())。这个判断在 /data/ws/1 和 /data/ws/10 之间会直接误判,因为字符串前缀不等于路径层级前缀。Path.startsWith 是按路径元素比较的,这才是对的做法。字符串比较是最容易犯的错,错在“看起来对”。

根因:路径校验必须建立在规范化之后的 Path 上;涉及符号链接和挂载点时,还要进一步确认真实路径(toRealPath,注意目标必须已存在)。

修复:所有路径统一走 PathGuard.resolve,容器挂载只挂工作区子目录,宿主其他目录一律不挂。

证据链二:白名单写在提示词里为什么没用。

现象:systemPrompt 里明确写了“不要访问工作区之外的路径”,但用一段故意诱导的输入测试时,模型仍然生成了带 ../ 的脚本。

怀疑:是不是 prompt 写得不够强硬。

检查:连续改了五版提示词,拦截率不稳定,而且换一个模型或换一次温度,结果就变。

根因:提示词是“建议”,不是“约束”。模型输出的是文本,约束必须由执行它的代码承担。官方 sandbox 文档把 Sandbox 定位为隔离执行环境,本身也说明这层职责不该交给模型。

修复:提示词只保留“告诉模型沙箱存在、路径要写相对路径”,真正的拒绝交给 PathGuard 和 CommandGuard。

证据链三:容器挂载整个宿主目录,等于没做隔离。

现象:早期为了图省事,直接 -v /home/me:/workspace,脚本在容器里能读到所有项目文件。

根因:隔离强度由挂载范围和网络策略决定,不是由“跑了容器”这个事实决定的。

修复:只挂 workspace/{projectId},网络默认 none,需要联网时白名单出站。

上游贡献判断:本章涉及的“路径规范化后校验”“产物受控导出”这些点,是否已在 v2.0.3 的 sandbox.md 中有明确说明,需要对照固定 tag 文档核验;在写这篇时没有检索到已确认的对应 Issue/PR,只记录候选,后续按第15式的流程处理。

前缀匹配骗自己
提示词拦不住模型
挂载整个宿主盘
等于没有做隔离

07、优化

基于第06章的三条证据,V2 收敛成四条硬规则:

规则一:一切路径先规范化再比较。 所有入口统一走 PathGuard,禁止业务代码自己拼 Path。新增一个静态检查或单测,任何绕过 PathGuard 的文件访问都算失败。

规则二:命令白名单只认首词,且先拦元字符。 不提供 sh -c 通道;需要复杂脚本时,把脚本文件写进工作区再以文件名调用,脚本内容本身也受产物导出审计。

规则三:挂载范围最小化。 容器只挂工作区子目录;宿主其他目录不挂载;网络默认关闭。资源限额用容器参数而不是 Java 侧计时器,因为 Java 侧只能在事后发现。

规则四:产物必须走受控导出。 Agent 拿到的是 artifacts 下的相对路径,不是宿主绝对路径;导出时二次校验源文件在工作区内且是普通文件。

验证方式:把第05章的两条 curl 固化成集成测试——一条断言拒绝,一条断言产物存在且内容可解析。同时补一条注入用例,输入带 ; 的命令,断言抛 SandboxViolationException。

规范化后再比较
白名单只认首词
只挂工作区子目录
断网限额再开跑

08、演进

[同一输入] run_python:画图并读取工作区外的文件
    |
    +--[V1] 宿主 ProcessBuilder 直跑 / 零隔离,启动最快
    |
    +--[V2] 沙箱内执行 + 路径/命令/网络/资源四重 Guard / 多一次容器编排成本
    |
[Trade-off]
得到:可执行、可审计的信任边界
失去:启动延迟、本地调试便利、部分符号链接写法
边界:低风险纯计算工具可继续走宿主;有外部输入或写文件必须进沙箱
维度 V1 宿主直跑 V2 沙箱执行
正确性 依赖模型与提示词 由代码强制
稳定性 一个死循环拖垮宿主 限额内自生自灭
复杂度 低 中,多一层编排与镜像管理
成本 低 冷启动延迟 + 镜像维护
适用范围 内部可信脚本 模型生成脚本、外部上传数据
遗留问题 无法审计 产物导出通道仍需审计与限额

快换来的是风险
隔离换来的是延迟
不是所有场景都值
边界之内再谈快

09、洞见

9.1 抽象解决可移植,不解决安全

反直觉判断: 很多人以为给文件读写加一层 Filesystem 抽象就安全了,其实恰恰相反——抽象层让人产生“已经封装好了”的错觉,反而更容易忘记设置根目录和挂载范围。抽象的作用是换实现、做测试替身,安全边界必须由进程、容器、挂载点和权限决定,这两件事永远不要用同一套代码去表达。

9.2 守卫的位置比守卫的强度更重要

反直觉判断: 白名单写得再细,如果放在工具实现里,价值也有限;放在编排层即使规则简单,收益也更大。因为绕过路径的数量和工具入口数量成正比,而编排层是唯一被所有入口共享的位置。第06章的三条证据链,本质上是同一个结论的三种表现形式。

9.3 沙箱不是默认选项,是有条件的选项

什么时候不应该用本章能力?三种情况:一是纯计算、无文件无网络无外部输入的工具,进沙箱只是给每次调用加几百毫秒;二是开发调试期,需要频繁看宿主文件时,可以临时放开挂载,但必须用配置开关而不是改代码;三是已经在强隔离环境里(比如 Agent 本身就跑在专用容器集群里),再套一层沙箱收益递减,此时更应该加固外层。

9.4 快照/恢复解决可重放,不解决越权

快照让同一次 Plan 能安全重跑、让失败步骤能回滚,这是工程效率;但它对“脚本读了不该读的文件”毫无帮助。把两者混为一谈,会写出“有快照所以可以放开跑”的代码,这是第23式最容易踩的坑。

抽象不等于隔离
守卫越外越可靠
沙箱也非万能药
重放不等于安全

10、系统落地

原来有什么:从第3式的数字人 Demo 一路演进到第22式,项目有项目 CRUD、配置、运行页、LiveKit 实时会话、记忆、技能、Plan,Agent 已经能规划并调用工具。

本篇新增什么:chapter/23-sandbox 增加了沙箱编排层——策略、路径 Guard、命令 Guard、Runner 接口与 Docker 实现、产物导出,并把官方 SandboxManager 作为生命周期装配点接进来。

现在能做什么:数字人接到“读 CSV、画图、写结论”的需求时,脚本在沙箱里跑,越权路径和越权命令被当场拒绝,产物通过受控通道回到业务侧,再进入开场白或项目配置。

还缺什么:缺生产级的镜像治理(版本、漏洞扫描)、缺沙箱指标可观测(启动耗时、拒绝次数)、缺多后端切换的配置化,也缺把沙箱执行纳入统一评测的口径。

下一步如何演进:把沙箱能力与可观测、评测串起来,让每一次拒绝都是一条可统计的指标,而不是一行日志。

开源参与点:本章未发现明确的上游缺陷;沙箱路径规范化与产物导出的边界说明是否需要在官方文档中补充示例,可作为 Issue/PR 候选问题,需先按第15式流程在教程仓库做最小复现,再对比 v2.0.3 与官方 main,确认未被修复后再进入上游讨论。

从提示词到策略
从宿主到容器里
危险操作有兜底
产物交付有通道

11、小结

Q1 → 宿主进程不是安全边界,模型生成的脚本必须换一个执行体,靠提示词约束等于没约束
Q2 → Sandbox 管隔离、生命周期与资源,Filesystem 管路径语义与可移植,两者不能互相顶替
Q3 → 由 SandboxManager 统一装配:实例获取与释放、执行前快照、资源限额、产物受控导出,业务代码只依赖门面和策略
状态 → chapter/23-sandbox 可交付:越权路径与命令被拒绝,正常脚本在沙箱内执行,产物经受控通道安全交付

Q1 -> 宿主进程不是边界
Q2 -> Sandbox 管隔离,Filesystem 管语义
Q3 -> 生命周期与快照由编排层兜底
状态 -> 危险操作被拒 + 产物安全交付

三问都有了答案
边界已经说清楚
代码落在分支上
下一步是生产化

12、作业

12.1 理解题:为什么说 Filesystem 抽象不是安全边界?

参考答案:抽象层决定的是“Agent 用什么路径读写、怎么换实现”,是语义与可移植问题;能不能读到工作区外的文件,由根目录、挂载范围、进程身份决定。把两者混在一起,会出现“接口封装得很漂亮但挂载了整个宿主盘”的情况。

12.2 实战题:在本章分支上补一条注入用例测试。

参考答案:构造 python3 plot.py; cat /etc/passwd 作为输入,断言 CommandGuard.parse 抛 SandboxViolationException;再构造 ../../etc/passwd,断言 PathGuard.resolve 抛同样的异常。两条用例都应放进集成测试而不是只做手工验证。

12.3 排障题:容器已经跑起来了,脚本仍然读到了宿主其他目录,最可能的原因是什么?

参考答案:先看挂载参数。常见原因是挂载了过大的目录(比如整个 home 或项目根),或者用了 --privileged、或者容器内用户有额外能力。其次检查是否有符号链接指向工作区外的真实路径,此时 Path.startsWith 通过但实际读到了外部文件,需要结合真实路径核验。

12.4 架构判断题:如果团队要求“所有沙箱执行都要能重放”,你会怎么设计?

参考答案:把执行拆成“执行前快照 → 沙箱执行 → 产物导出 → 失败回滚”四段,快照在编排层统一打,产物导出必须幂等。同时要明确:可重放不等于可放开权限,快照只解决状态回滚,权限约束仍然由 Guard 和容器参数承担。这道题建议打开官方 v2.0.3 的 sandbox.md 与 SandboxManager.java 源码,确认快照/恢复在框架里的实际边界,再给出设计。

光看文章记不住
打开源码走一遍
自己动手改一行
才算真的学会了

13、思考

第23式真正要解决的冲突是:Agent 的能力必须继续长,但执行能力一旦落到宿主,增长速度就等于风险增长速度。

解法不是把 Agent 关起来,而是把“执行”这件事从宿主进程里挪出去,用一层可替换的沙箱后端承接,让路径、命令、网络、资源四条约束落在代码上,让产物从受控通道回来。

Frameworks 会用的人很多,能参与框架演进的人少;差别就在于,前者知道 API 怎么调,后者知道边界该划在哪、为什么划在那里。

边界先于能力
隔离胜于承诺
拒得早好过补
行得稳才算通

posted @ 2026-10-11 11:12  李福春  阅读(6)  评论(0)    收藏  举报