Agent Sandbox

Agent 沙箱隔离技术:Docker、gVisor 与 WASM 方案对比

深入对比 AI Agent 沙箱隔离的三种主流方案,分析安全性、性能和易用性的权衡,帮助选择最适合的隔离技术。

当 AI Agent 获得执行代码、操作文件系统、调用外部 API 的能力时,一个关键问题随之而来:如何防止 Agent 的操作越界?沙箱(Sandbox)隔离技术为 Agent 提供了一个受控的执行环境,确保即使 Agent 做出错误决策,也不会损害宿主系统。

为什么需要沙箱

┌──────────────────────────────────────────┐
│           Agent 操作风险                   │
│                                          │
│  代码执行 → 可能访问文件系统              │
│  工具调用 → 可能执行危险命令              │
│  API 调用 → 可能泄露敏感数据              │
│  文件操作 → 可能覆盖重要文件              │
│                                          │
│  ┌──────────────────────────────────┐    │
│  │         沙箱隔离层               │    │
│  │  限制文件访问 | 网络隔离          │    │
│  │  资源限制    | 权限控制          │    │
│  └──────────────────────────────────┘    │
└──────────────────────────────────────────┘

方案对比

特性DockergVisorWASM
隔离级别进程级内核级字节码级
启动速度秒级秒级毫秒级
内存开销50-200MB30-100MB1-10MB
系统调用完整受限受限
安全性
生态系统完善成长中早期
适合场景通用不可信代码轻量级函数

Docker 沙箱

基础配置

class DockerSandbox {
  private docker: Docker;

  async create(config: SandboxConfig): Promise<Sandbox> {
    const container = await this.docker.createContainer({
      Image: config.image || 'node:20-slim',
      Cmd: config.command || ['/bin/bash'],
      HostConfig: {
        // 资源限制
        Memory: config.memoryLimit || 256 * 1024 * 1024,  // 256MB
        CpuQuota: config.cpuQuota || 50000,  // 50% CPU
        PidsLimit: config.pidLimit || 100,

        // 网络隔离
        NetworkMode: config.networkEnabled ? 'bridge' : 'none',

        // 只读根文件系统
        ReadonlyRootfs: !config.writableRootfs,

        // 安全选项
        SecurityOpt: ['no-new-privileges'],
        CapDrop: ['ALL'],
        CapAdd: config.capabilities || [],

        // 挂载卷
        Binds: config.volumes || [],
      },
    });

    await container.start();
    return new Sandbox(container);
  }

  async execute(sandbox: Sandbox, command: string): Promise<ExecResult> {
    const exec = await sandbox.container.exec({
      Cmd: ['sh', '-c', command],
      AttachStdout: true,
      AttachStderr: true,
    });

    const stream = await exec.start({ Detach: false });
    return this.readStream(stream);
  }
}

网络策略

class NetworkPolicy {
  private allowedDomains: Set<string> = new Set();
  private blockedPorts: Set<number> = new Set([22, 3389, 5900]);

  createIptablesRules(): string[] {
    const rules: string[] = [];

    // 默认拒绝所有出站
    rules.push('iptables -A OUTPUT -j DROP');

    // 允许 DNS
    rules.push('iptables -A OUTPUT -p udp --dport 53 -j ACCEPT');

    // 允许特定域名(通过 IP)
    for (const domain of this.allowedDomains) {
      const ips = this.resolveDomain(domain);
      for (const ip of ips) {
        rules.push(`iptables -A OUTPUT -d ${ip} -p tcp --dport 443 -j ACCEPT`);
      }
    }

    // 阻止危险端口
    for (const port of this.blockedPorts) {
      rules.push(`iptables -A OUTPUT -p tcp --dport ${port} -j DROP`);
    }

    return rules;
  }
}

gVisor 沙箱

gVisor 在用户空间实现了一个操作系统内核,提供了比 Docker 更强的隔离:

class GVisorSandbox {
  async create(config: SandboxConfig): Promise<Sandbox> {
    // gVisor 通过 runsc 运行时集成 Docker
    const container = await this.docker.createContainer({
      Image: config.image,
      HostConfig: {
        Runtime: 'runsc',  // 使用 gVisor 运行时

        // gVisor 特有的安全配置
        SecurityOpt: [
          'no-new-privileges',
          `seccomp=${this.createSeccompProfile()}`,
        ],

        // 限制系统调用
        ReadonlyRootfs: true,
        NetworkMode: 'none',
      },
    });

    await container.start();
    return new Sandbox(container);
  }

  private createSeccompProfile(): string {
    // 创建严格的 seccomp 配置文件
    return JSON.stringify({
      defaultAction: 'SCMP_ACT_ERRNO',
      architectures: ['SCMP_ARCH_X86_64'],
      syscalls: [
        {
          names: [
            'read', 'write', 'open', 'close',
            'stat', 'fstat', 'mmap', 'mprotect',
            'brk', 'exit_group',
          ],
          action: 'SCMP_ACT_ALLOW',
        },
      ],
    });
  }
}

WASM 沙箱

WebAssembly 提供了最轻量级的隔离方案:

class WasmSandbox {
  private runtime: WasmRuntime;

  async create(config: SandboxConfig): Promise<WasmSandboxInstance> {
    const module = await this.runtime.compile(config.wasmBinary);

    const instance = await this.runtime.instantiate(module, {
      // 导入的宿主函数
      env: {
        log: (msg: string) => console.log(msg),
        fetch: config.networkEnabled
          ? (url: string) => this.restrictedFetch(url, config.allowedDomains)
          : undefined,
      },
      // 内存限制
      memory: new WebAssembly.Memory({
        initial: config.memoryPages || 256,  // 16MB
        maximum: config.maxMemoryPages || 1024,  // 64MB
      }),
    });

    return new WasmSandboxInstance(instance);
  }

  async execute(instance: WasmSandboxInstance, funcName: string, args: any[]): Promise<any> {
    const func = instance.exports[funcName];
    if (!func) throw new Error(`Function ${funcName} not found`);

    // WASM 执行天然受限:无法访问文件系统、网络等
    return func(...args);
  }
}

class WasmCodeExecutor {
  async executeUserCode(code: string, input: any): Promise<any> {
    // 将用户代码编译为 WASM
    const wasmBinary = await this.compileToWasm(code);

    const sandbox = new WasmSandbox();
    const instance = await sandbox.create({
      wasmBinary,
      memoryPages: 128,  // 8MB
      networkEnabled: false,
    });

    return await instance.execute('main', [JSON.stringify(input)]);
  }

  private async compileToWasm(code: string): Promise<Buffer> {
    // 使用 AssemblyScript 或 Rust 编译为 WASM
    // 这里简化处理
    return await this.compiler.compile(code, {
      target: 'wasm32',
      optimization: 'size',
    });
  }
}

资源限制

class ResourceLimiter {
  private limits: Map<string, ResourceLimit> = new Map();

  setLimit(sandboxId: string, limit: ResourceLimit): void {
    this.limits.set(sandboxId, limit);
  }

  async enforce(sandboxId: string): Promise<void> {
    const limit = this.limits.get(sandboxId);
    if (!limit) return;

    const usage = await this.getUsage(sandboxId);

    if (usage.memory > limit.memory) {
      throw new ResourceExceededError('memory', usage.memory, limit.memory);
    }

    if (usage.cpuTime > limit.cpuTime) {
      throw new ResourceExceededError('cpu', usage.cpuTime, limit.cpuTime);
    }

    if (usage.executionTime > limit.executionTime) {
      throw new ResourceExceededError('time', usage.executionTime, limit.executionTime);
    }
  }

  private async getUsage(sandboxId: string): Promise<ResourceUsage> {
    // 从 cgroup 或容器统计中获取资源使用情况
    return {
      memory: await this.getMemoryUsage(sandboxId),
      cpuTime: await this.getCpuTime(sandboxId),
      executionTime: await this.getExecutionTime(sandboxId),
    };
  }
}

interface ResourceLimit {
  memory: number;        // bytes
  cpuTime: number;       // milliseconds
  executionTime: number; // milliseconds
  diskSpace: number;     // bytes
  networkBandwidth: number; // bytes/sec
}

沙箱管理器

class SandboxManager {
  private sandboxes: Map<string, SandboxInstance> = new Map();
  private docker: DockerSandbox;
  private gvisor: GVisorSandbox;
  private wasm: WasmSandbox;

  async createSandbox(config: SandboxConfig): Promise<string> {
    const id = generateId();

    let sandbox: Sandbox;

    switch (config.type) {
      case 'docker':
        sandbox = await this.docker.create(config);
        break;
      case 'gvisor':
        sandbox = await this.gvisor.create(config);
        break;
      case 'wasm':
        sandbox = await this.wasm.create(config);
        break;
      default:
        throw new Error(`Unknown sandbox type: ${config.type}`);
    }

    this.sandboxes.set(id, {
      id,
      sandbox,
      config,
      createdAt: Date.now(),
      status: 'running',
    });

    return id;
  }

  async execute(sandboxId: string, command: string): Promise<ExecResult> {
    const instance = this.sandboxes.get(sandboxId);
    if (!instance) throw new Error(`Sandbox ${sandboxId} not found`);

    // 检查超时
    if (Date.now() - instance.createdAt > instance.config.timeout) {
      await this.destroy(sandboxId);
      throw new Error('Sandbox execution timeout');
    }

    return await instance.sandbox.execute(command);
  }

  async destroy(sandboxId: string): Promise<void> {
    const instance = this.sandboxes.get(sandboxId);
    if (instance) {
      await instance.sandbox.destroy();
      this.sandboxes.delete(sandboxId);
    }
  }

  async destroyAll(): Promise<void> {
    for (const [id] of this.sandboxes) {
      await this.destroy(id);
    }
  }
}

常见问题(FAQ)

Agent 真的需要沙箱吗?

如果 Agent 要执行代码、写文件或访问网络,沙箱是必须的。即使 Agent 只读取数据,沙箱也能防止意外的系统调用。

沙箱会影响性能吗?

Docker 约增加 5-10% 开销,gVisor 约 10-20%,WASM 几乎无开销。选择取决于安全需求和性能要求。

如何选择沙箱方案?

不可信代码用 gVisor,轻量级函数用 WASM,通用场景用 Docker。生产环境通常组合使用。

总结

沙箱隔离是 Agent 安全的基石。Docker 提供了成熟的通用方案,gVisor 提供了更强的内核级隔离,WASM 提供了最轻量级的字节码隔离。根据安全需求和性能要求选择合适的方案,或组合使用以获得最佳平衡。