目录

引子

让 AI 写代码已经不新鲜了。

真正的问题是——
你敢让它在你的服务器上直接执行吗?

当一个模型生成:

pip install xxx
rm -rf /
curl something | bash

它并不知道你的环境变量里有什么,也不知道宿主机上跑着什么服务。

这也是为什么最近开始出现一批专门做“安全执行环境”的产品:

  • Modal 把 GPU 和执行环境打包成隔离单元,
  • Daytona 提供可以快速拉起的开发沙箱,
  • Runloop 则主打一次性、可销毁的 devbox。

它们的核心卖点不是算力,而是——边界。

当代码越来越自动化地产生,执行环境必须越来越严格地隔离。

这篇文章,我会从工程视角出发,分析这三种沙箱产品背后的设计思路,以及沙箱为什么正在成为 AI 基础设施的一部分。

Modal

Modal 是一个 AI 基础设施平台,允许你:

  • 使用开源权重或自定义模型,运行低延迟推理(冷启动时间低于 1 秒)
  • 将批处理任务大规模并行扩展运行
  • 在最新 GPU 上训练或微调开源权重或自定义模型
  • 启动成千上万个隔离且安全的沙箱,用于执行 AI 生成的代码
  • 在几秒内启动 GPU 支持的 Notebook,并与同事实时协作

Modal 提供完全的 Serverless 执行模式和按秒计费机制,因为所有基础设施都由平台托管,并按实际使用时间计费。

值得注意的是,Modal 几乎零配置 —— 包括容器环境和 GPU 规格在内的一切都是通过代码定义的。没有 YAML 文件的世界,确实清爽很多。

一个最小可运行示例:在 Modal 上进行 LLM 推理:

from pathlib import Path

import modal

app = modal.App("example-inference")
image = modal.Image.debian_slim().uv_pip_install("transformers[torch]")


@app.function(gpu="h100", image=image)
def chat(prompt: str | None = None) -> list[dict]:
    from transformers import pipeline

    if prompt is None:
        prompt = f"/no_think Read this code.\n\n{Path(__file__).read_text()}\nIn one paragraph, what does the code do?"

    print(prompt)
    context = [{"role": "user", "content": prompt}]

    chatbot = pipeline(
        model="Qwen/Qwen3-1.7B-FP8", device_map="cuda", max_new_tokens=1024
    )
    result = chatbot(context)
    print(result[0]["generated_text"][-1]["content"])

    return result

你可以把这段代码复制到本地 Python 文件中,然后运行:

modal run path/to/file.py

它是如何工作的?

Modal 会:

  1. 接收你的代码
  2. 将其打包进容器
  3. 在云端执行

如果流量增加,Modal 会自动扩展容器数量。

这意味着你不需要:

  • 配置 Kubernetes
  • 管理 Docker
  • 甚至不需要 AWS 账户

Modal 在多个主流云厂商之间进行算力池化,这使得它可以根据当前可用资源,动态选择最优执行位置,从而在 GPU 可用性和成本之间取得平衡。

Python 是构建 Modal 应用和实现 Modal Functions 的主要语言。

此外,你也可以使用:

  • JavaScript / TypeScript
  • Go

来调用 Modal Functions、运行沙箱或管理 Modal 资源。

使用 Modal 开发非常简单,无需搭建任何基础设施:

  1. 在 modal.com 创建账号

  2. 执行:

    pip install modal
    
  3. 运行:

    modal setup
    

(如果失败,可以尝试 python -m modal setup

完成后即可开始运行任务。

Sandboxes(沙箱)

Sandboxes 是在 Modal 上用于执行不受信任的用户或 Agent 代码的安全容器。

关于 modal.Sandbox 接口的详细参考文档,请参阅官方 API 文档页面。

除了 Function 接口之外,Modal 还提供了一个运行时直接定义容器并在其中安全执行任意代码的接口

在以下场景中,这种能力尤其有用:

  • 执行由语言模型生成的代码
  • 创建隔离环境来运行不受信任的代码
  • 克隆一个 git 仓库并对其运行命令(例如测试套件或 npm lint)
  • 运行带有任意依赖和初始化脚本的容器

每一个独立运行的任务称为一个 Sandbox,可以通过 Sandbox.create 构造器创建。

python示例:

import modal

app = modal.App.lookup("my-app", create_if_missing=True)

sb = modal.Sandbox.create(app=app)

p = sb.exec("python", "-c", "print('hello')", timeout=3)
print(p.stdout.read())

p = sb.exec("bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done", timeout=5)
for line in p.stdout:
    # 使用 end="" 避免重复换行
    print(line, end="")

sb.terminate()
sb.detach()

注意:
你可以直接使用 python my_script.py 运行上述示例脚本。
由于这里没有定义入口函数(entrypoint),因此不需要使用 modal run

当在 Modal 容器外部创建 Sandbox 时,必须传入一个 App 实例。

你可以:

  • 直接传入一个已有的 App 对象
  • 或通过 App.lookup() 按名称查找一个 App

如果在 App.lookup() 中设置 create_if_missing=True,当指定名称的 App 不存在时,系统会自动创建一个对应名称的 App。

生命周期(Lifecycle)

Sandbox 默认的最长生命周期为 5 分钟

你可以在 Sandbox.create(...) 中传入 timeout 参数,将最长运行时间设置为最多 24 小时

sb = modal.Sandbox.create(app=my_app, timeout=10*60)  # 10 分钟
sb.detach()

如果你需要让 Sandbox 运行超过 24 小时,官方建议:

  • 使用 Filesystem Snapshots(文件系统快照) 保存当前状态
  • 然后通过新的 Sandbox 从该快照恢复运行

Sandbox 还支持在一段时间无活动后自动终止

你可以通过设置 idle_timeout 参数启用该机制。

当满足以下任一条件时,Sandbox 被视为“活跃”:

  • 有正在运行的命令(通过 sb.exec(...)
  • 正在向 stdin 写入数据(通过 sb.stdin.write()
  • 通过某个 Tunnel 存在打开的 TCP 连接

配置(Configuration)

Sandbox 支持几乎所有 modal.Function 的配置选项。
详细参数请参考 Sandbox.create 的官方文档。

例如,你可以像使用 Function 一样配置:

  • Image
  • Volume
  • workdir
  • 等等

示例:

sb = modal.Sandbox.create(
    image=modal.Image.debian_slim().pip_install("pandas"),
    volumes={"/data": modal.Volume.from_name("my-volume")},
    workdir="/repo",
    app=my_app,
)
sb.detach()

这意味着你可以为每个 Sandbox 自定义:

  • 运行镜像环境
  • 挂载数据卷
  • 工作目录
  • 以及其他运行时参数

从工程角度看,Sandbox 本质上是一个可编程、可配置、可自动回收的隔离执行单元

环境(Environments)

环境变量(Environment variables)

你可以通过 内联 Secret 的方式为 Sandbox 设置环境变量:

secret = modal.Secret.from_dict({"MY_SECRET": "hello"})

sb = modal.Sandbox.create(
    secrets=[secret],
    app=my_app,
)

p = sb.exec("bash", "-c", "echo $MY_SECRET")
print(p.stdout.read())

sb.detach()

这里通过 modal.Secret.from_dict() 注入环境变量,然后在 Sandbox 内部通过 $MY_SECRET 访问。

自定义镜像(Custom Images)

Sandbox 与 modal.Function 一样支持自定义镜像。

不同的是:

  • 调用 Function 通常使用 modal run
  • 启动 Sandbox 通常直接通过 Python 脚本执行

因此,如果你希望看到镜像构建日志,可能需要手动开启输出流:

image = modal.Image.debian_slim().pip_install("pandas", "numpy")

with modal.enable_output():
    sb = modal.Sandbox.create(image=image, app=my_app)

sb.detach()
动态定义环境(Dynamically defined environments)

任何合法的 ImageMount 都可以用于 Sandbox,即使它们此前未被定义。

这意味着:

  • 可以在运行时根据需求构建镜像
  • 可以动态创建 Mount
  • 甚至可以让语言模型生成代码和镜像定义,然后立即启动一个 Sandbox 执行它

这种“运行时构建执行环境”的能力,使 Sandbox 成为 AI 代码执行场景中的关键组件。

以入口命令运行 Sandbox(Entrypoint)

大多数情况下,Sandbox 被视为一个可运行任意命令的通用容器。

但在某些场景下,你可能希望:

  • 只运行一个长期驻留的服务
  • 或将某个命令作为容器入口

可以通过在构造函数中传入命令参数实现:

sb = modal.Sandbox.create(
    "python", "-m", "http.server", "8080",
    app=my_app,
    timeout=10
)

for line in sb.stdout:
    print(line, end="")

sb.detach()

这种方式特别适合运行长生命周期后台服务(例如 Notebook、HTTP 服务等)。

在其他代码中引用 Sandbox

如果你已经有一个正在运行的 Sandbox,可以通过 from_id() 方法重新获取它:

sb = modal.Sandbox.create(app=my_app)
sb_id = sb.object_id

# 稍后在程序的其他位置

sb2 = modal.Sandbox.from_id(sb_id)

p = sb2.exec("echo", "hello")
print(p.stdout.read())

sb2.terminate()
sb2.detach()
sb.detach()

一个常见用例是:

  • 维护一个 Sandbox 池
  • 保存“已打开”Sandbox 的 object_id
  • 在任务到达时复用这些 Sandbox
  • 在合适时机再统一关闭

这种模式适用于需要频繁执行任务、但希望减少冷启动成本的场景。

Logging(日志)

你可以通过开启 verbose 选项来查看 Sandbox 的执行日志。例如:

sb = modal.Sandbox.create(app=my_app, verbose=True)

p = sb.exec("python", "-c", "print('hello')")
print(p.stdout.read())

with sb.open("test.txt", "w") as f:
    f.write("Hello World\n")
sb.detach()

会显示如下 Sandbox 日志:

Sandbox exec started: python -c print('hello')
Opened file 'test.txt': fd-yErSQzGL9sig6WAjyNgTPR
Wrote to file: fd-yErSQzGL9sig6WAjyNgTPR
Closed file: fd-yErSQzGL9sig6WAjyNgTPR

Named Sandboxes(命名 Sandbox)

你可以在创建 Sandbox 时为其指定一个名称。在同一个 App 内,每个名称必须是唯一的——同一时间内,只能有一个运行中的 Sandbox 使用该名称。

注意:关联的 App 必须是**已部署(deployed)**的 App。

当某个 Sandbox 完全停止运行后,其名称会重新变为可用状态。某些应用场景中,Sandbox 名称可用于确保某个资源或项目在同一时间只运行一个 Sandbox。

如果使用的名称已经被某个运行中的 Sandbox 占用,create() 会抛出错误。

sb1 = modal.Sandbox.create(app=my_app, name="my-name")
# 这里会抛出 modal.exception.AlreadyExistsError
sb2 = modal.Sandbox.create(app=my_app, name="my-name")

可以使用 from_name() 从一个已部署的 App 中获取指定名称的 Sandbox,但前提是该 Sandbox 当前正在运行。如果没有找到正在运行的 Sandbox,from_name() 会抛出错误。

my_app = modal.App.lookup("my-app", create_if_missing=True)

sb1 = modal.Sandbox.create(app=my_app, name="my-name")

# 从名为 "my-app" 的已部署 App 中获取当前运行的 "my-name" Sandbox
sb2 = modal.Sandbox.from_name("my-app", "my-name")

assert sb1.object_id == sb2.object_id  # sb1 和 sb2 指向同一个 Sandbox

sb1.detach()
sb2.detach()

Sandbox 名称仅允许包含:

  • 字母数字字符(alphanumeric)
  • 连字符 -
  • 点号 .
  • 下划线 _

并且长度必须小于 64 个字符。

Tagging(标签)

Sandbox 也可以设置任意的键值对标签(key-value tags)。这些标签可用于在 Sandbox.list 中进行筛选。

sandbox_v1_1 = modal.Sandbox.create("sleep", "10", app=my_app)
sandbox_v1_2 = modal.Sandbox.create("sleep", "20", app=my_app)

sandbox_v1_1.set_tags({"major_version": "1", "minor_version": "1"})
sandbox_v1_2.set_tags({"major_version": "1", "minor_version": "2"})

# 获取所有 sandbox
for sandbox in modal.Sandbox.list(app_id=my_app.app_id):
    print(sandbox.object_id)

# 仍然是所有 sandbox(因为 major_version 都是 1)
for sandbox in modal.Sandbox.list(
    app_id=my_app.app_id,
    tags={"major_version": "1"},
):
    print(sandbox.object_id)

# 只获取 minor_version 为 2 的 sandbox
for sandbox in modal.Sandbox.list(
    app_id=app.app_id,
    tags={"major_version": "1", "minor_version": "2"},
):
    print(sandbox.object_id)

sandbox_v1_1.detach()
sandbox_v1_2.detach()

Cleaning up Client-side Connections(清理客户端连接)

与其他 Modal 对象不同,本地的 Sandbox 会直接持有到底层计算资源的连接。

虽然这个连接通常会在垃圾回收(garbage collection)时自动关闭,但强烈建议在与 Sandbox 交互完成后,显式调用 detach() 方法来释放资源

sb = modal.Sandbox.create(app=my_app)
sb.detach()

调用 detach() 之后,任何基于该 Sandbox 对象的操作都不再保证可用

如果你想继续与一个仍在运行的 Sandbox 交互,可以使用 Sandbox.from_id 获取一个新的 Sandbox 对象来引用原始 Sandbox。

  • Python SDK 中,terminate 不会自动 detach,因此建议在终止后手动调用 detach()
  • Go/JavaScript SDK 中,Terminate 会自动执行 detach 操作。

在 Sandbox 中运行命令

创建好 Sandbox 之后,可以通过 Sandbox.exec 方法在其中运行命令。

sb = modal.Sandbox.create(app=my_app)

process = sb.exec("echo", "hello", timeout=3)
print(process.stdout.read())

process = sb.exec("python", "-c", "print(1 + 1)", timeout=3)
print(process.stdout.read())

process = sb.exec(
    "bash",
    "-c",
    "for i in $(seq 1 10); do echo foo $i; sleep 0.1; done",
    timeout=5,
)
for line in process.stdout:
    print(line, end="")

sb.terminate()
sb.detach()

Sandbox.exec 会返回一个 ContainerProcess 对象,你可以通过它访问进程的 stdoutstderrstdin

timeout 参数用于限制该命令的最长执行时间(单位:秒)。

Input(输入)

Sandbox 和 ContainerProcessstdinStreamWriter 对象。
它支持同步和异步两种方式来刷新(flush)写入数据。

同步示例:

import asyncio

sb = modal.Sandbox.create(app=my_app)

p = sb.exec("bash", "-c", "while read line; do echo $line; done")
p.stdin.write(b"foo bar\n")
p.stdin.write_eof()
p.stdin.drain()
p.wait()

sb.terminate()
sb.detach()

异步示例:

async def run_async():
    sb = await modal.Sandbox.create.aio(app=my_app)
    p = await sb.exec.aio(
        "bash", "-c", "while read line; do echo $line; done"
    )
    p.stdin.write(b"foo bar\n")
    p.stdin.write_eof()
    await p.stdin.drain.aio()
    await p.wait.aio()
    await sb.terminate.aio()
    await sb.detach.aio()

asyncio.run(run_async())
Output(输出)

Sandbox 和 ContainerProcessstdoutstderrStreamReader 对象。

它们支持同步和异步方式读取流数据,并且会遵守 Sandbox.exec 中设置的 timeout

如果想在进程执行完之后一次性读取完整输出,可以使用 read() 方法。

该方法会阻塞直到进程结束,然后返回完整输出。

sb = modal.Sandbox.create(app=my_app)
p = sb.exec("echo", "hello")
print(p.stdout.read())

sb.terminate()
sb.detach()

stdoutstderr 本身是可迭代对象,因此可以直接进行流式读取。

同步流式读取:

import asyncio

sb = modal.Sandbox.create(app=my_app)

p = sb.exec(
    "bash",
    "-c",
    "for i in $(seq 1 10); do echo foo $i; sleep 0.1; done",
)

for line in p.stdout:
    # 行末已包含换行符,因此使用 end="" 避免重复换行
    print(line, end="")

p.wait()
sb.terminate()
sb.detach()

异步流式读取:

async def run_async():
    sb = await modal.Sandbox.create.aio(app=my_app)
    p = await sb.exec.aio(
        "bash",
        "-c",
        "for i in $(seq 1 10); do echo foo $i; sleep 0.1; done",
    )
    async for line in p.stdout:
        # 使用 end="" 避免双重换行
        print(line, end="")
    await p.wait.aio()
    await sb.terminate.aio()
    await sb.detach.aio()

asyncio.run(run_async())
Stream 类型

默认情况下,所有流都会被缓存在内存中,等待客户端读取。

你可以通过 stdoutstderr 参数来控制这一行为。

这些参数在概念上类似于 Python subprocess 模块中的 stdoutstderr 设置。

from modal.stream_type import StreamType

sb = modal.Sandbox.create(app=my_app)

# 默认行为:缓存在内存中
p = sb.exec(
    "bash",
    "-c",
    "echo foo; echo bar >&2",
    stdout=StreamType.PIPE,
    stderr=StreamType.PIPE,
)
print(p.stdout.read())
print(p.stderr.read())

# 实时打印到当前进程的 STDOUT
p = sb.exec(
    "bash",
    "-c",
    "echo foo; echo bar >&2",
    stdout=StreamType.STDOUT,
    stderr=StreamType.STDOUT,
)
p.wait()

# 丢弃所有输出
p = sb.exec(
    "bash",
    "-c",
    "echo foo; echo bar >&2",
    stdout=StreamType.DEVNULL,
    stderr=StreamType.DEVNULL,
)
p.wait()

sb.terminate()
sb.detach()

不同的 StreamType 行为总结:

  • StreamType.PIPE:默认,缓存在内存中
  • StreamType.STDOUT:实时输出到当前进程标准输出
  • StreamType.DEVNULL:丢弃所有输出

Networking and Security(网络与安全)

Sandbox 采用 secure-by-default(默认安全) 设计。这意味着:

默认情况下,Sandbox 无法接受入站网络连接,也无法访问你的 Modal 资源

由于 Sandbox 可能运行不受信任的代码,因此可以对其网络访问进行限制。

如果希望彻底禁止网络访问,可以在 Sandbox.create 中设置:

block_network=True

🎯 更细粒度的网络控制

可以通过 cidr_allowlist 参数限制 出站网络访问

该参数接受一个 CIDR 范围列表,Sandbox 只能访问这些范围内的 IP,其他所有出站流量都会被阻止。

通过 HTTP 和 WebSocket 连接 Sandbox

你可以通过生成 Sandbox Connect Token 来向 Sandbox 发起经过认证的 HTTP 或 WebSocket 请求。

示例流程如下:

# 启动一个在 8080 端口运行服务器的 Sandbox
sb = modal.Sandbox.create(
    "bash", "-c", "python3 -m http.server 8080",
    app=my_app,
)

# 创建一个 connect token(可附带任意用户元数据)
creds = sb.create_connect_token(user_metadata={"user_id": "foo"})

# 通过 Authorization header 发送 HTTP 请求
requests.get(creds.url, headers={"Authorization": f"Bearer {creds.token}"})

# 也可以通过 _modal_connect_token 查询参数传递 token
url = f"{creds.url}/?_modal_connect_token={creds.token}"
ws_url = url.replace("https://", "wss://")

with websockets.connect(ws_url) as socket:
    socket.send("Hello world!")

sb.detach()

容器中运行在 8080 端口的服务器会收到一个经过认证的请求,其中包含一个不可伪造的请求头:

X-Verified-User-Data

该 header 的值是你在 create_connect_token() 中传入的 user_metadata(JSON 序列化后的结果)。

这可以用于:

  • 访问控制
  • 用户识别
  • 审计日志
  • 多租户隔离

📌 使用 Sandbox Connect Token 时需要注意

  1. 容器内服务必须监听 8080 端口

  2. token 可以通过以下方式传递:

    • Authorization header
    • _modal_connect_token 查询参数
    • _modal_connect_token Cookie
  3. 如果通过查询参数传递,响应会包含 Set-Cookie

  4. user_metadata 必须:

    • 可 JSON 序列化
    • 序列化后小于 512 字符
端口转发(Forwarding Ports)

虽然推荐使用 Sandbox Connect Token 来访问 HTTP/WebSocket 服务,但你也可以直接暴露原始 TCP 端口到公网。

这适用于:

  • 运行自定义 TCP 服务
  • 服务自行处理认证
  • 非 HTTP 协议场景

使用 encrypted_portsunencrypted_ports 参数指定需要转发的端口。

然后通过 Sandbox.tunnels() 获取公网访问地址:

import requests
import time

sb = modal.Sandbox.create(
    "python",
    "-m",
    "http.server",
    "12345",
    encrypted_ports=[12345],
    app=my_app,
)

tunnel = sb.tunnels()[12345]

time.sleep(1)  # 等待服务器启动

print(f"Connecting to {tunnel.url}...")
print(requests.get(tunnel.url, timeout=5).text)

sb.detach()

你也可以使用 h2_ports 参数创建支持 HTTP/2 + TLS 的加密端口。

适用于:

  • 在 Sandbox 内运行 HTTP/2 服务器
  • 需要 H2 协议支持的场景

示例:

import time

port = 4359
sb = modal.Sandbox.create(
    app=my_app,
    image=my_image,
    h2_ports=[port],
)

p = sb.exec("python", "my_http2_server.py")

tunnel = sb.tunnels()[port]
time.sleep(1)

print(f"Tunnel URL: {tunnel.url}")

sb.detach()
Security Model

Sandbox 构建在 gVisor 之上。

gVisor 是由 Google 开发的容器运行时,提供更强的隔离能力。

🛡 gVisor 的优势

  • 拦截并限制恶意系统调用
  • 比标准 runc 容器具有更强隔离性
  • 减少容器逃逸风险

与 Modal Functions 默认可以访问 Workspace 资源不同:

Sandbox 默认 没有权限访问你的 Modal Workspace 中的其他资源

因此,即使运行了恶意代码,其影响范围(blast radius)也会被限制在当前 Sandbox 容器内部。

文件系统访问(Filesystem Access)

有多种方式可以将文件上传到 Sandbox,并从 Sandbox 外部访问这些文件。

高效文件同步(Efficient file syncing)

如果希望高效地将本地文件上传到 Sandbox,可以在 Image 类上使用 add_local_fileadd_local_dir 方法:

sb = modal.Sandbox.create(
    app=my_app,
    image=modal.Image.debian_slim().add_local_dir(
        local_path="/home/user/my_dir",
        remote_path="/app"
    )
)
p = sb.exec("ls", "/app")
print(p.stdout.read())
p.wait()
sb.detach()

你也可以使用 Modal VolumesCloudBucketMounts

它们的优势在于:

在 Sandbox 内创建的文件,可以方便地在 Sandbox 外部访问。

可以通过 Volumebatch_upload 方法高效上传文件,例如使用一个 临时 Volume(ephemeral) —— 当 App 结束时会被自动垃圾回收:

with modal.Volume.ephemeral() as vol:
    import io
    with vol.batch_upload() as batch:
        batch.put_file("local-path.txt", "/remote-path.txt")
        batch.put_directory("/local/directory/", "/remote/directory")
        batch.put_file(io.BytesIO(b"some data"), "/foobar")

    sb = modal.Sandbox.create(
        volumes={"/cache": vol},
        app=my_app,
    )
    p = sb.exec("cat", "/cache/remote-path.txt")
    print(p.stdout.read())
    p.wait()
    sb.terminate()
    sb.detach()

即使 Sandbox 已终止,调用方仍然可以访问 Volume 中的文件:

with modal.Volume.ephemeral() as vol:
    sb = modal.Sandbox.create(
        volumes={"/cache": vol},
        app=my_app,
    )
    p = sb.exec("bash", "-c", "echo foo > /cache/a.txt")
    p.wait()
    sb.terminate(wait=True)

    for data in vol.read_file("a.txt"):
        print(data)

    sb.detach()

如果你希望在多个 Sandbox 调用之间保留文件(例如构建一个有状态的代码解释器),可以创建一个带动态标签的持久 Volume:

session_id = "example-session-id-123abc"
vol = modal.Volume.from_name(f"vol-{session_id}", create_if_missing=True)

sb = modal.Sandbox.create(
    volumes={"/cache": vol},
    app=my_app,
)

p = sb.exec("bash", "-c", "echo foo > /cache/a.txt")
p.wait()
sb.terminate(wait=True)

for data in vol.read_file("a.txt"):
    print(data)

sb.detach()

Volume 与 CloudBucketMount 的同步差异:

文件同步行为有所不同:

  • Volume:文件只会在 Sandbox 终止时同步回存储
  • CloudBucketMount:文件会自动实时同步
使用 sync 提交 Volume 变更(仅 v2)

对于 Volume v2,可以在 Sandbox 运行期间显式提交变更。

通过在挂载点运行 sync 命令,可以立即持久化数据和元数据,而无需等待 Sandbox 终止:

sb = modal.Sandbox.create(
    volumes={"/data": modal.Volume.from_name("my-v2-volume")},
    app=my_app,
)

# 写入文件
sb.exec("bash", "-c", "echo 'hello' > /data/output.txt").wait()

# 立即提交变更
p = sb.exec("sync", "/data")
p.wait()
if p.returncode != 0:
    raise Exception(f"sync failed with exit code {p.returncode}")

# 变更已持久化并对其他容器可见
sb.terminate()
sb.detach()

这种方式特别适用于:

  • 长时间运行的 Sandbox
  • 需要持久化中间结果
  • 需要在 Sandbox 结束前让其他容器读取数据
文件系统 API(Alpha)

如果你更关注“使用方便”,而不是上传效率,可以使用文件系统 API,在 Sandbox 执行期间方便地读写文件。

支持:

  • 读取最大 100 MiB 的文件
  • 写入最大 1 GiB 的文件

⚠️ 当前为 Alpha 版本,不建议用于生产环境。

示例:

import modal

app = modal.App.lookup("sandbox-fs-demo", create_if_missing=True)

sb = modal.Sandbox.create(app=app)

with sb.open("test.txt", "w") as f:
    f.write("Hello World\n")

f = sb.open("test.txt", "rb")
print(f.read())
f.close()

sb.terminate()
sb.detach()

该文件系统 API 类似于 Python 内置的 io.FileIO,支持常见方法:

  • read
  • readline
  • readlines
  • write
  • flush
  • seek
  • close

此外还提供更易用的命令:

  • mkdir
  • rm
  • ls

方便与 Sandbox 文件系统进行交互。

Daytona

Daytona 是一个开源、安全且具备弹性扩展能力的基础设施,用于运行由 AI 生成的代码。

Daytona 提供隔离的沙箱环境,你可以通过 Daytona SDK 以编程方式进行管理,用于运行和控制代码执行。

Daytona SDK 支持以下语言接口:

  • Python
  • TypeScript
  • Ruby
  1. 创建账户

    打开 Daytona Dashboard ↗ 并创建账户。

    Daytona 支持以下注册方式:

    • 使用邮箱和密码注册
    • 通过 Google 账号登录
    • 通过 GitHub 账号登录
  2. 获取 API Key

    在 Daytona Dashboard ↗ 中生成一个 API Key,用于:

    • 认证 SDK 请求
    • 访问 Daytona 服务

    ⚠️ 请妥善保存该 API Key,因为生成后将不会再次显示。

    💡 提示

    Daytona 支持多种方式来配置运行环境:

    • 在代码中配置
    • 使用环境变量
    • 使用 .env 文件
    • 使用默认配置值
  3. 安装 SDK(Install the SDK)

    安装 Daytona SDK,以便在你的代码中通过 Python、TypeScript 或 Ruby 与沙箱进行交互。

    pip install daytona
    
  4. 创建 Sandbox(Create a Sandbox)

    创建一个 Daytona Sandbox,在隔离环境中安全运行你的代码。

    # 导入 Daytona SDK
    from daytona import Daytona, DaytonaConfig
    
    # 定义配置
    config = DaytonaConfig(api_key="YOUR_API_KEY")  # 替换为你的 API Key
    
    # 初始化 Daytona 客户端
    daytona = Daytona(config)
    
    # 创建 Sandbox 实例
    sandbox = daytona.create()
    
  5. 编写代码(Write code)

    创建一个程序,在 Daytona Sandbox 内运行代码。
    下面是一个在 Sandbox 中安全运行的 “Hello World” 示例。

    # 导入 Daytona SDK
    from daytona import Daytona, DaytonaConfig
    
    # 定义配置
    config = DaytonaConfig(api_key="YOUR_API_KEY")  # 替换为你的 API Key
    
    # 初始化 Daytona 客户端
    daytona = Daytona(config)
    
    # 创建 Sandbox 实例
    sandbox = daytona.create()
    
    # 在 Sandbox 中安全运行代码
    response = sandbox.process.code_run('print("Hello World")')
    
    # 检查执行结果
    if response.exit_code != 0:
        print(f"Error: {response.exit_code} {response.result}")
    else:
        print(response.result)
    
    # 清理资源
    sandbox.delete()
    
  6. 运行代码(Run code)

    运行程序,即可在一个安全、隔离的 Daytona Sandbox 环境中执行代码。

    python main.py
    

    输出:

    Hello World
    

按照上述步骤,你已经成功:

  • 创建 Daytona 账户
  • 获取 API Key
  • 安装 SDK
  • 创建 Sandbox
  • 编写代码
  • 在 Daytona Sandbox 中安全运行代码

💡 提示

为了配合 AI Agent 或助手更高效地开发,可以使用官方提供的 LLM 上下文文件:

  • llms-full.txt
  • llms.txt

将这些文件复制到你的项目中,或加入到聊天上下文中,可提升 AI 辅助效果。

Sandboxes

Sandbox 是由 Daytona 管理的隔离运行时环境。

默认情况下,Daytona Sandbox 资源配置为:

  • 1 vCPU
  • 1 GB 内存
  • 3 GiB 磁盘

每个组织的最大资源上限为:

  • 4 vCPU
  • 8 GB 内存
  • 10 GB 磁盘

Sandbox 生命周期(Sandbox lifecycle)

在整个生命周期中,一个 Daytona Sandbox 会经历多个不同状态。
官方提供的生命周期图展示了各状态以及它们之间可能的转换关系。

在这里插入图片描述

多运行时支持(Multiple runtime support)

Daytona Sandbox 支持以下语言运行时,可直接在沙箱内执行代码:

  • python
  • typescript
  • javascript

通过 language 参数控制使用的运行时。

如果未指定,Daytona SDK 默认使用 python

如需使用其他语言,请在创建 Sandbox 时显式设置 language

💡 注意

Sandbox 名称(Sandbox Names)

你可以通过 name 参数为 Sandbox 指定自定义名称。

  • 如果未提供,默认使用 Sandbox ID 作为名称
  • 名称可复用:当某个 Sandbox 被销毁后,其名称可以再次使用

创建 Sandbox(Create Sandboxes)

Daytona 支持以编程方式创建 Sandbox,使用默认或自定义配置。

你可以为每个 Sandbox 指定:

  • 运行语言
  • Snapshot
  • 资源配置
  • 环境变量
  • Volumes

运行中的 Sandbox 会占用 CPU、内存和磁盘资源,

所有资源均按秒计费。

from daytona import Daytona, CreateSandboxFromSnapshotParams

daytona = Daytona()

# 创建默认 Sandbox
sandbox = daytona.create()

# 创建 Python 运行时 Sandbox
params = CreateSandboxFromSnapshotParams(language="python")
sandbox = daytona.create(params)

# 创建带自定义名称的 Sandbox
params = CreateSandboxFromSnapshotParams(name="my_awesome_sandbox")
sandbox = daytona.create(params)

# 创建带自定义标签的 Sandbox
params = CreateSandboxFromSnapshotParams(labels={"LABEL": "label"})
sandbox = daytona.create(params)

默认资源配置:

  • 1 vCPU
  • 1 GB 内存
  • 3 GiB 磁盘

Daytona 维护一组使用默认 Snapshot 的“预热 Sandbox 池”。

如果可用,Sandbox 可在毫秒级启动,而无需冷启动。

可以使用 Resources 类设置 CPU、内存和磁盘:

from daytona import Daytona, Resources, CreateSandboxFromImageParams, Image

daytona = Daytona()

resources = Resources(
    cpu=2,      # 2 个 CPU 核心
    memory=4,   # 4 GB 内存
    disk=8,     # 8 GB 磁盘
)

params = CreateSandboxFromImageParams(
    image=Image.debian_slim("3.12"),
    resources=resources
)

sandbox = daytona.create(params)

所有资源参数都是可选的。

如果未指定,Daytona 会根据语言与使用场景选择合适的默认值。

Ephemeral Sandbox(临时 Sandbox)

Ephemeral Sandbox 在停止后会被自动删除。

适用于:

  • 短生命周期任务
  • 测试场景

创建方式:

from daytona import Daytona, CreateSandboxFromSnapshotParams

daytona = Daytona()

params = CreateSandboxFromSnapshotParams(
    ephemeral=True,
    auto_stop_interval=5  # 5 分钟无活动后自动删除
)

sandbox = daytona.create(params)

💡 注意

设置 autoDeleteInterval: 0ephemeral=True 效果相同。

网络设置(防火墙)

Daytona Sandbox 提供可配置的网络防火墙控制,用于增强安全性和管理网络连接。

默认情况下,网络访问遵循标准安全策略。

你也可以在创建 Sandbox 时自定义网络配置。

启动沙箱(Start Sandboxes)

Daytona 提供多种方式来启动沙箱:可以通过 Daytona Dashboard ↗,也可以通过以下方式以编程方式启动:

  • Python SDK
  • TypeScript SDK
  • Ruby SDK
  • Go SDK
  • CLI
  • API

操作步骤(控制台方式):

  1. 进入 Daytona Sandboxes ↗
  2. 点击目标沙箱旁的启动图标(▶)
  3. 启动指定 ID 的沙箱:<sandbox-id>
sandbox = daytona.create(CreateSandboxFromSnapshotParams(language="python"))

# 启动沙箱
sandbox.start()

更多信息请参阅对应语言的 SDK 与 API 文档:

  • start (Python SDK)
  • start (TypeScript SDK)
  • start (Ruby SDK)
  • Start (Go SDK)
  • start (CLI)
  • start (API)

列出沙箱(List Sandboxes)

Daytona 支持查看沙箱信息(包括 ID、根目录、状态)并管理其生命周期。

# 列出所有沙箱
result = daytona.list()

# 遍历结果
for sandbox in result.items:
    print(f"Sandbox: {sandbox.id} (state: {sandbox.state})")

# 按标签过滤沙箱
result = daytona.list(labels={"env": "dev"})

更多信息请参阅:

  • list (Python SDK)
  • list (TypeScript SDK)
  • list (Ruby SDK)
  • List (Go SDK)
  • list (CLI)
  • list (API)

停止沙箱(Stop Sandboxes)

可以通过 Daytona Dashboard ↗ 或使用 Python / TypeScript / Ruby SDK 以编程方式停止沙箱。

控制台方式:

  1. 进入 Daytona Sandboxes ↗
  2. 点击目标沙箱旁的停止图标(⏹)
  3. 停止指定 ID 的沙箱:<sandbox-id>

关于停止状态:

  • 停止后,文件系统会被保留
  • 内存状态会被清空
  • 只产生磁盘存储费用
  • 可随时重新启动

如果预计沙箱很快会再次使用,建议使用 stop 状态。
如果长期不用,建议先停止再归档(archive),以消除磁盘成本。

sandbox = daytona.create(CreateSandboxFromSnapshotParams(language="python"))

# 停止沙箱
sandbox.stop()

print(sandbox.id)  # 7cd11133-96c1-4cc8-9baa-c757b8f8c916

# 根据 ID 查找沙箱
sandbox = daytona.find_one("7cd11133-96c1-4cc8-9baa-c757b8f8c916")

# 重新启动
sandbox.start()

归档沙箱(Archive Sandboxes)

可以通过 Dashboard 或 SDK 将沙箱归档。

归档后:

  • 整个文件系统会被迁移到低成本对象存储
  • 适合长期保存
  • 启动归档沙箱所需时间会比停止状态更长(取决于大小)
  • 必须先 stop 才能 archive
  • 归档后可像停止状态一样重新启动
# 归档沙箱
sandbox.archive()

相关接口:

  • archive (Python SDK)
  • archive (TypeScript SDK)
  • archive (Ruby SDK)
  • Archive (Go SDK)
  • archive (CLI)
  • archive (API)

恢复沙箱(Recover Sandboxes)

可以通过 Dashboard 或 SDK 恢复沙箱。

# 恢复沙箱
sandbox.recover()

相关接口:

  • recover (Python SDK)
  • recover (TypeScript SDK)
  • recover (Ruby SDK)
  • recover (API)
从错误状态恢复(Recover from error state)

当沙箱进入 error 状态 时,根据具体错误原因,有时可以通过 recover() 方法恢复。

沙箱对象中的 recoverable 标志表示该错误是否支持自动恢复。

注意
恢复操作不会自动执行,因为某些错误需要用户进一步干预(例如释放存储空间)。

# 检查是否可恢复
if sandbox.recoverable:
    sandbox.recover()
    print("Sandbox recovered successfully")

调整沙箱资源(Resize Sandboxes)

Daytona 支持在沙箱创建后调整资源配置,包括 CPU、内存和磁盘

  • 已启动(started)沙箱

    • 可以增加 CPU 和内存
    • 不能减少 CPU 和内存
    • 不能修改磁盘容量
  • 已停止(stopped)沙箱

    • 可以修改 CPU、内存和磁盘
    • 磁盘容量只能增加,不能减少
from daytona import Daytona, Resources

daytona = Daytona()
sandbox = daytona.create()

# 调整已启动沙箱(仅可增加 CPU 和内存)
sandbox.resize(Resources(cpu=2, memory=4))

# 调整已停止沙箱(可修改 CPU、内存和磁盘)
sandbox.stop()
sandbox.resize(Resources(cpu=4, memory=8, disk=20))
sandbox.start()

相关接口:

  • resize (Python SDK)
  • resize (TypeScript SDK)
  • Resize (Go SDK)
  • resize (Ruby SDK)

删除沙箱(Delete Sandboxes)

可以通过 Daytona Dashboard ↗ 或 SDK 以编程方式删除沙箱。

控制台方式:

  1. 进入 Daytona Sandboxes ↗
  2. 点击目标沙箱旁的 Delete 按钮
  3. 删除指定 ID 的沙箱:<sandbox-id>
# 删除沙箱
sandbox.delete()

相关接口:

  • delete (Python SDK)
  • delete (TypeScript SDK)
  • delete (Ruby SDK)
  • Delete (Go SDK)
  • delete (CLI)
  • delete (API)

自动生命周期管理(Automated lifecycle management)

Daytona 沙箱支持根据用户定义的时间间隔自动执行:

  • 自动停止(Auto-stop)
  • 自动归档(Auto-archive)
  • 自动删除(Auto-delete)
自动停止间隔(Auto-stop interval)

auto_stop_interval 用于设置沙箱在运行状态下,多长时间无活动后自动停止。

⚠ 注意:

  • 即使沙箱内部有进程在运行,也可能触发自动停止
  • 系统会区分“内部进程”和“用户主动交互”
  • 单纯运行脚本或后台任务不足以维持沙箱存活

可设置值:

  • 指定分钟数
  • 0:禁用自动停止(无限期运行)
  • 未设置时默认 15 分钟
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
    snapshot="my-snapshot-name",
    # 禁用自动停止(默认 15 分钟)
    auto_stop_interval=0,
))

相关接口:

  • set_autostop_interval (Python SDK)
  • setAutostopInterval (TypeScript SDK)
  • auto_stop_interval (Ruby SDK)
  • set_auto_stop_interval (API)

只有以下 外部交互行为 会重置空闲计时器:

  • 访问沙箱预览(通过 preview URL 的网络请求)
  • 活跃的 SSH 连接
  • Daytona Toolbox SDK API 调用

以下行为 不会 重置计时器:

  • 非 Toolbox 类型的 SDK 调用
  • 后台脚本(例如 npm run dev 这种 fire-and-forget 命令)
  • 无外部交互的长时间运行任务
  • 未被主动监控的进程

例如:如果运行一个耗时超过 15 分钟的 LLM 推理任务,但没有任何外部交互,沙箱可能会在执行过程中被自动停止,因为该进程不被视为“活动”。

自动归档间隔(Auto-archive interval)

auto_archive_interval 用于设置沙箱在持续停止状态下,多久后自动归档。

可设置值:

  • 指定分钟数
  • 0:使用最大间隔(30 天)
  • 未设置时默认 7 天
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
    snapshot="my-snapshot-name",
    # 沙箱停止 1 小时后自动归档
    auto_archive_interval=60,
))
自动删除间隔(Auto-delete interval)

auto_delete_interval 用于设置沙箱在持续停止状态下,多久后自动删除。

默认情况下:沙箱不会被自动删除

可设置值:

  • 指定分钟数
  • -1:禁用自动删除
  • 0:沙箱停止后立即删除
  • 未设置:不会自动删除
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
    snapshot="my-snapshot-name",
    # 停止 1 小时后自动删除
    auto_delete_interval=60,
))

# 停止后立即删除
sandbox.set_auto_delete_interval(0)

# 禁用自动删除
sandbox.set_auto_delete_interval(-1)
无限运行(Running indefinitely)

默认情况下,Daytona 沙箱在 15 分钟无活动后自动停止

如果希望沙箱长期运行而不被中断,在创建时将 auto_stop_interval 设置为 0

sandbox = daytona.create(CreateSandboxFromSnapshotParams(
    snapshot="my_awesome_snapshot",
    # 禁用自动停止(默认 15 分钟)
    auto_stop_interval=0,
))

RunLoop

Runloop:面向 AI Agent 工作流的沙箱工具平台

Runloop 是一个“开箱即用”(batteries included)的平台,专为构建与优化 AI 驱动的软件工程 Agent 而设计。现在即可开始使用。

通过 Runloop 平台,你可以获得:

  • Devboxes:极速、安全、隔离的开发沙箱环境,用于运行 Agent 及其工具
  • Blueprints:创建并共享 Devbox 模板,支持自定义配置
  • Snapshots:保存、挂起并恢复 Devbox 的运行状态
  • Repo Connect:自动生成 Blueprint,与现有代码仓库协同工作
  • Benchmarks:使用业界先进的基准测试与评估体系,或自定义评测来衡量并提升 Agent 性能

无论你是想构建一个可以自动回复 Pull Request 的 AI Agent,还是一个能够自动生成 UI 组件的 Agent,Runloop 都可以让你仅用几行代码就从 零到生产环境

为什么选择 Runloop?

Runloop的使命是让你专注于真正能提升 Agent 能力的工作,把基础设施和运维问题交给我们。

随着 Agent 的不断演进,你的需求也会变化。Runloop 为不同阶段的构建者提供支持:

阶段 为什么选择 Runloop
原型阶段(Prototyping) 无需担心基础设施,使用托管的“即开即用” Devbox。
快速构建、部署、学习与迭代。
生产阶段(Production) 团队共享 Blueprint 与项目。
全年 7×24 小时托管平台与值班支持。
符合 SOC2 合规标准。
增长阶段(Growth) 提供完整的 Benchmark 与评估体系,用于持续监控与优化 Agent 性能。

使用场景(Use Cases):

  • 自动回复 Pull Request,优化代码审查流程
  • 让用户通过对话方式理解与浏览代码库
  • 为现有代码库自动生成新的测试用例
  • 充当“结对程序员”(Pair Programmer)
  • 为前端自动生成新的 UI 组件
  • 创建自定义 Benchmark,并通过 强化微调(Reinforcement Fine Tuning, RFT) 训练 Agent
  • ……以及更多场景

快速开始(Quickstart)

Runloop Devbox 是一个安全且隔离的环境,用于运行 AI 生成的代码。本教程将帮助你在大约 1 分钟内启动并运行你的第一个 Devbox。

为方便使用,我们提供:

  • Python SDK(支持同步与异步版本)
  • TypeScript SDK

👉 推荐使用 异步(async)Python SDK 以获得更好的性能。

1️⃣ 创建 API Key

访问 Runloop 注册页面创建账户。账户激活后,进入 Runloop Dashboard 的 Settings 页面创建 API Key。

2️⃣ 设置开发环境

将 Runloop API Key 设置为环境变量,以便 SDK 进行身份认证并创建 Devbox:

export RUNLOOP_API_KEY=<your_runloop_api_key_here>

3️⃣ 安装客户端 SDK

安装 Runloop 的 Python 或 TypeScript 客户端 SDK。

mkdir runloop-examples
cd runloop-examples
uv venv .runloop-venv
source .runloop-venv/bin/activate
uv pip install runloop_api_client

4️⃣ 创建 Devbox 并运行命令

现在我们创建一个 Devbox 作为沙箱环境。

将下面的脚本复制到文件中:

  • Python:testprog.py
  • TypeScript:testprog.ts
import asyncio
from runloop_api_client import AsyncRunloopSDK

# API Key 会自动从 RUNLOOP_API_KEY 环境变量加载
runloop = AsyncRunloopSDK()

async def run_example():
    # 创建 devbox 并等待其就绪
    devbox = await runloop.devbox.create()
    print(f'Created Runloop Devbox: {devbox.id}')

    # 执行命令并等待完成
    result = await devbox.cmd.exec(command="echo 'Runloop!!'")
    print(await result.stdout())  # Runloop!!
    print(f'Exit code: {result.exit_code}')  # 0

    # 清理 Devbox
    await devbox.shutdown()

asyncio.run(run_example())

5️⃣ 运行示例

创建并启动 Devbox 只需几秒钟,即可安全运行 LLM 生成的代码、执行测试等。

上述示例代码完成了:

  • 创建并启动 Devbox
  • 运行一个简单命令
  • 执行完成后关闭 Devbox

整个过程仅需几秒钟。

运行方式如下:

uv run ./testprog.py

Devbox:Runloop 的沙箱环境

Runloop 提供了安全的沙箱执行环境,称为 Devboxes。Runloop Devboxes 为你的 AI Agent 提供了完整的执行环境。我们使用虚拟机技术,为你的 API Key、代码、密钥、敏感数据和内部系统提供隔离和安全保障。

最强大、最实用的 AI Agent 不只是简单聊天。成熟的开发团队需要能够执行以下任务的 Agent:

  • 查询外部 API
  • 从 Git 仓库拉取、构建并执行代码
  • 运行无头浏览器(headless browser)抓取或交互网站
  • 读写文件系统中的文件
  • 运行专有代码或二进制程序

虽然开发者一开始可能会在本地工作站上完成这些操作,但这种方式无法扩展到多个并行工作流,同时也会带来安全和风险问题。这时 Devboxes 就派上用场了。

Runloop Devboxes 是虚拟的、沙箱化的工作站,AI Agent 可以在其中执行任务。通过在 Devboxes 中运行 Agent,开发者可以保持多个并行工作流,同时保障内部数据和系统的安全。

Devbox 关键特性:

  • 隔离、短暂的虚拟机:Devboxes 是基于云的虚拟工作站,按需创建,不再需要时自动删除。

  • 超快执行:从启动到运行第一个命令仅需几秒钟。

  • 有状态或无状态

    • 对于短时任务,可快速启动 Devbox,执行任务后直接丢弃。
    • 对于长时任务,可使用快照、挂起和恢复操作,通过简单 API 调用控制 Devbox 生命周期。
  • 可自定义大小和镜像:可以选择机器规格和资源,还能通过蓝图创建和自定义团队共享镜像。

  • 网络安全:通过网络策略控制出口网络访问,限制 Devboxes 能访问的外部服务。

使用 Devboxes:

你的 Agent 代码将通过 Runloop API 与 Devboxes 交互。我们提供 Python 和 TypeScript 客户端 SDK。

你也可以使用 Runloop CLIRunloop Dashboard 来查看、管理和监控你的 Devboxes。

Devbox 生命周期

Devbox 表示一个持久化的开发环境,可以根据需要启动和关闭。在 Devbox 的整个生命周期中,它会根据使用场景经历一系列状态:

  • Provisioning(资源分配):Runloop 正在分配和启动所需的基础设施资源。
  • Initializing(初始化):运行 Runloop 定义的启动脚本,为环境交互做准备。
  • Running(运行中):Devbox 已准备好进行交互操作。
  • Failure(失败):Devbox 在启动或执行用户请求操作时失败。
  • Shutdown(关闭):Devbox 已成功关闭,不再使用任何计算资源。
  • Suspending(挂起中):正在对 Devbox 磁盘进行快照,以便挂起。
  • Suspended(已挂起):Devbox 磁盘已保存,不再使用计算资源。
  • Resuming(恢复中):正在加载 Devbox 磁盘,以启动已挂起的 Devbox。

挂起与恢复 Devbox 以保存磁盘状态:

除了使用空闲管理配置外,你还可以手动挂起和恢复 Devbox。

注意:挂起/恢复操作只保存磁盘状态,不保留内存状态。

1. 挂起 Devbox

Python / TypeScript

devbox = runloop.devbox.from_id(devbox_id)
await devbox.suspend()

2. 等待 Devbox 挂起完成

Python / TypeScript

await devbox.await_suspended()

3. 需要时恢复 Devbox

Python / TypeScript

devbox = runloop.devbox.from_id(devbox_id)
await devbox.resume()

4. 等待 Devbox 运行中

Python / TypeScript

await devbox.await_running()

重要说明

  • 已挂起的 Devbox 在显式关闭前仍会产生存储费用。
  • 挂起/恢复过程通常只需几秒,具体取决于修改数据的量。
  • 挂起时运行的守护进程或其他进程在恢复后必须手动重启。
  • 原始 Devbox ID 和 SSH 密钥在挂起/恢复周期中会被保留。

其余内容请参考官方文档,这里不再赘述。

Docker

从前面的介绍可以看出,Modal、Daytona 和 Runloop 都提供了类似的沙箱云服务:它们为 AI Agent 或开发者提供隔离、可控、可编程的执行环境,支持多语言运行时、资源管理、快照/挂起/恢复、日志和网络安全控制。使用文档上也有很大相似性:都提供 SDK(Python、TypeScript 等)、CLI 和 Dashboard 管理方式,用户可以通过 API 创建、启动、停止、归档或删除沙箱实例。同时,LangChain 也针对这些厂商提供了对应的 langchain-x 包进行封装,使得在 Agent 工作流中调用沙箱更加便捷。

在功能对比上:

  • Modal:生态偏向漏洞/安全分析、Agent 工具集成,支持细粒度工具调用,适合需要快速处理多任务和高并发场景。
  • Daytona:强调快速启动、沙箱弹性和多语言支持,适合 AI 生成代码、开发与测试场景。
  • Runloop:专注于 Devbox 和 AI Agent 的开发迭代,提供蓝图、快照和团队共享功能,更适合团队协作和持续开发环境管理。

总体来说,这些云沙箱服务都提供了安全、隔离的环境,并简化了 AI Agent 的开发和执行流程,但它们毕竟是“托管型”沙箱——受限于提供商的接口、资源和费用控制。若需要更底层、更灵活的沙箱环境,或者想深入理解沙箱的隔离机制,就需要转向 runc/container/docker 等容器技术,从操作系统级别实现进程隔离和资源控制,这也是后续我们要讨论的重点。

从云沙箱到自建容器沙箱:Docker 是不是“底层真相”?

在前面我们看了 Modal、Daytona、Runloop 这三家云沙箱厂商的使用方式。

你会发现一个有趣的现象:

image = modal.Image.debian_slim().pip_install("pandas")

或者:

Image.debian_slim("3.12")

它们都在强调:

  • Image
  • Snapshot
  • Volume
  • Runtime
  • 资源限制(CPU / Memory)

这让人自然产生一个疑问:

这些沙箱的底层,是不是其实就是 Docker?

答案是:

大概率是容器技术,但不一定是“直接 Docker CLI”。

更准确地说:

底层通常是 containerd / runc / gVisor 这一类 OCI 容器运行时。

很多平台不会直接暴露 Docker,但会基于 OCI 规范构建自己的调度层。

如果我们把 Modal / Daytona / Runloop 的 SDK 抽象一下,会发现它们做的事情高度一致:

云沙箱能力 本质对应
设置 Image 容器镜像
创建 Sandbox 创建容器
执行命令 docker exec
Volume bind mount
Snapshot 文件系统快照
资源限制 cgroup
网络策略 namespace + iptables

换句话说:

云厂商帮你封装了一个“容器调度平台”。

所以如果我们不依赖云厂商,完全可以自己构建一个:

基于 Docker 的沙箱池。

一、使用 Docker 构建沙箱池

一个最简单的沙箱池架构如下:

主控服务 (Python / Go)
        │
        ▼
Docker Daemon
        │
        ├── sandbox-1
        ├── sandbox-2
        ├── sandbox-3

主控程序负责:

  • 创建容器
  • 执行命令
  • 获取 stdout/stderr
  • 控制资源
  • 删除容器
  • 维护容器池

二、Python 构建 Docker 沙箱池

1️⃣ 安装 SDK

pip install docker

2️⃣ 创建沙箱

import docker

client = docker.from_env()

def create_sandbox():
    container = client.containers.run(
        image="python:3.11-slim",
        command="sleep infinity",
        detach=True,
        mem_limit="512m",
        cpu_period=100000,
        cpu_quota=50000,  # 0.5 CPU
        network_mode="none",  # 禁用网络
        security_opt=["no-new-privileges"]
    )
    return container

3️⃣ 执行命令

def exec_code(container, code):
    cmd = ["python", "-c", code]
    result = container.exec_run(cmd)
    return result.output.decode()

4️⃣ 删除沙箱

def destroy(container):
    container.remove(force=True)

5️⃣ 简单沙箱池

sandbox_pool = []

for _ in range(3):
    sandbox_pool.append(create_sandbox())

你现在就拥有了:

  • 固定数量容器
  • 资源隔离
  • 禁止网络
  • 可执行任意代码

这就是一个最小可用沙箱系统。

三、Go 构建 Docker 沙箱池

Go 更适合做高并发沙箱调度。

安装

go get github.com/docker/docker/client

创建容器

cli, _ := client.NewClientWithOpts(client.FromEnv)

resp, _ := cli.ContainerCreate(
    ctx,
    &container.Config{
        Image: "python:3.11-slim",
        Cmd:   []string{"sleep", "infinity"},
    },
    &container.HostConfig{
        Memory: 512 * 1024 * 1024,
        NanoCPUs: 500000000,
        NetworkMode: "none",
    },
    nil,
    nil,
    "",
)

cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})

执行命令

execResp, _ := cli.ContainerExecCreate(ctx, resp.ID, types.ExecConfig{
    Cmd: []string{"python", "-c", "print(1+1)"},
    AttachStdout: true,
    AttachStderr: true,
})

Go 更适合:

  • 构建高并发沙箱平台
  • 控制生命周期
  • 做多租户调度

四、Docker vs containerd vs runc

当我们深入下去,会发现:

Docker
   ↓
containerd
   ↓
runc
   ↓
Linux kernel

1️⃣ runc

  • 最底层运行时
  • 直接操作 namespace + cgroup
  • 只负责“启动容器”

适合:

  • 构建极致轻量沙箱
  • 自定义 runtime
  • 自研容器平台

缺点:

  • API 非常底层
  • 几乎无调度能力

2️⃣ containerd

  • Kubernetes 默认运行时
  • 提供完整容器生命周期管理
  • 比 Docker 更轻

适合:

  • 构建云级沙箱平台
  • 大规模调度

3️⃣ Docker

  • 开发最简单
  • 生态成熟
  • API 丰富

适合:

  • 中小规模沙箱系统
  • 快速原型
  • Agent 执行平台

五、Python / Go 调用差异

方式 优点 缺点
Python + Docker SDK 开发快 性能一般
Go + Docker SDK 高并发强 开发复杂
直接 runc 极致性能 难度高
containerd API 生产级 学习成本高

六、云沙箱 vs 自建沙箱

维度 云厂商 自建 Docker
易用性 ⭐⭐⭐⭐⭐ ⭐⭐
控制力 ⭐⭐ ⭐⭐⭐⭐⭐
成本 按秒计费 固定服务器
安全隔离 VM + gVisor 需自行加强
运维复杂度

七、关键现实:云厂商真的就是 Docker 吗?

严格说:

不一定是“Docker”,但几乎一定是“容器 + OCI runtime”。

有些会使用:

  • gVisor
  • Firecracker VM
  • Kata Containers

来加强隔离。

但从抽象能力看:

它们对外暴露的 API,本质上就是一个“受控容器执行平台”。

OpenClaw 里的 Docker 沙箱设计

当我们回到开源项目 OpenClaw,会发现一个非常有意思的现象:

它没有依赖第三方云沙箱,而是直接用 Docker 构建了完整的本地沙箱体系。

在 OpenClaw 根目录下,可以看到三个核心 Dockerfile:

  • Dockerfile.sandbox
  • Dockerfile.sandbox-common
  • Dockerfile.sandbox-browser

这三个文件实际上构成了一套分层的沙箱架构设计

一、整体架构:三层沙箱模型

可以把 OpenClaw 的沙箱体系理解为三层:

基础运行层  →  开发能力层  →  浏览器能力层

分别对应:

Dockerfile 角色 目标
sandbox 极简计算沙箱 提供基础 CLI 执行能力
sandbox-common 通用开发沙箱 提供 Node / Python / Go / Rust 等开发运行能力
sandbox-browser 浏览器沙箱 在容器内运行 Chromium + VNC

二、Dockerfile.sandbox —— 极简执行沙箱

基础镜像:

FROM debian:bookworm-slim

这里出现 debian:bookworm-slim,也印证了前文的判断 ——
云沙箱服务里常见的 debian_slimubuntu_slim 等镜像设置,本质就是标准容器镜像。

这个镜像只安装最基础的工具:

  • bash
  • curl
  • git
  • jq
  • python3
  • ripgrep

然后创建一个普通用户:

useradd sandbox

默认命令:

CMD ["sleep", "infinity"]

这是一种典型的容器常驻模型

  1. docker run 启动容器
  2. 容器保持存活
  3. 通过 docker exec 执行实际命令

这个层的设计哲学:

  • 最小攻击面
  • 最少依赖
  • 仅提供 shell 执行能力
  • 作为上层镜像的“基础层”

三、Dockerfile.sandbox-common —— 通用开发能力层

这是最关键的一层。

它通过:

ARG BASE_IMAGE=openclaw-sandbox:bookworm-slim
FROM ${BASE_IMAGE}

继承基础沙箱,然后扩展完整开发工具链:

  • Node.js + npm
  • Python3
  • Go
  • Rust
  • build-essential
  • pkg-config
  • bun
  • pnpm
  • Homebrew

这其实已经不是“轻量执行容器”了,而是:

一个完整的“云端开发工作站镜像”。

为什么要单独拆成这一层?

原因很简单:

  • 有些 exec 任务只需要简单 bash
  • 有些 skill 需要安装 npm 包
  • 有些任务需要 cargo build
  • 有些任务需要 brew install

如果全部堆进基础镜像:

  • 镜像体积会暴涨
  • 启动时间会变慢
  • 安全风险增大

因此 OpenClaw 采用了:

基础镜像 + 可选扩展镜像

这种分层思想,和大型云沙箱厂商的“基础镜像 + 团队镜像”模式几乎完全一致。

四、Dockerfile.sandbox-browser —— 浏览器沙箱

这一层和前两层完全不同。

它安装了:

  • chromium
  • xvfb
  • x11vnc
  • novnc
  • websockify
  • 字体包

并暴露端口:

9222  (CDP)
5900  (VNC)
6080  (noVNC)

这是典型的“容器内 GUI 浏览器”方案:

Chromium
   ↓
Xvfb 虚拟显示
   ↓
x11vnc
   ↓
noVNC
   ↓
浏览器 Web UI

这说明:

OpenClaw 并不是通过 Playwright 远程调用宿主浏览器,而是直接在容器里构建了完整的“浏览器桌面环境”。

这是一种完全隔离的浏览器执行沙箱

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐