mcpField
L
lightning-sandboxes

lightning-sandboxes

github.com/lightning-ai云与 DevOps962 次安装2 周前更新
代码仓库

关于 lightning-sandboxes

lightning-sandboxes 是用于在 Lightning AI 上创建隔离虚拟机并执行代码的技能,适合需要临时运行环境的开发者和 Agent。它介绍 CLI 与 Python SDK 操作、持久化快照、网络策略及 Docker 运行时。

主要功能

  1. 01创建隔离且快速启动的虚拟机
  2. 02执行命令并读写文件
  3. 03配置网络策略与端口
  4. 04通过持久化快照支持停止和恢复
  5. 05在 Docker 专用运行时中构建和运行容器

适用场景

  1. 01在隔离环境中运行不可信代码
  2. 02在干净的虚拟机中测试代码
  3. 03创建可停止后恢复的开发环境
  4. 04在沙盒中构建并检查容器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lightning-ai/skills --skill lightning-sandboxes

安装后在 Claude Code 中输入/lightning-sandboxes确认已加载。

详细介绍

Lightning AI 沙盒

沙盒是用于执行代码、启动速度快且相互隔离的 VM。默认情况下,沙盒是临时的:停止会删除沙盒,只有设置 persistent=True 才能通过自动快照停止并恢复。请通过子包导入:from lightning_sdk.sandbox import Sandbox。顶层的 lightning_sdk.sandbox.py _Sandbox 类已弃用;请忽略。

设置与身份验证(仅适用于沙盒)

export DEBUG=0 LIGHTNING_DEBUG=0   # an inherited DEBUG=1 logs the Authorization header; set this in every new shell
# Use the Lightning AI CLI from the current env; install or upgrade it there if it's missing or older than 2026.9.18
v=$(lightning --version 2>/dev/null | sed -n 's/^Lightning CLI version //p')
[ -n "$v" ] && [ "$(printf '%s\n' 2026.9.18 "$v" | sort -V | head -1)" = 2026.9.18 ] \
  || uv pip install -U lightning-sdk || python3 -m pip install -U lightning-sdk
lightning --version   # must print "Lightning CLI version …"; if not, see the table below
# `sandbox <cmd>` is also installed standalone == `lightning sandbox <cmd>`

这会使用 Agent 当前的环境,并在需要时将其安装到该环境中(项目 venv、conda 环境 等);随后在所有地方直接调用 lightning …。如果设置过程不顺利:

症状解决方法
没有活动环境、出现 externally-managed-environment,或与项目自身的固定版本冲突单独安装:uv tool install lightning-sdk(或 pipx install lightning-sdk),如果它不在 PATH 中,则调用 "$(uv tool dir --bin)/lightning"
安装后仍没有 Lightning CLI version 行另一个 lightning(PyTorch Lightning 的版本)或未激活的环境排在前面:检查 command -v lightning,然后激活环境或调用其中的 bin/lightning
No such command、No such option 或 unexpected extra argumentCLI 版本太旧:在 command -v lightning 指向的位置升级它(uv pip install -U lightning-sdk,或 uv tool upgrade lightning-sdk)
安装受阻(环境或主目录为只读,或处于 Agent 沙盒中)将每条命令都作为 UV_CACHE_DIR="${TMPDIR:-/tmp}/uv" UV_TOOL_DIR="${TMPDIR:-/tmp}/uv-tools" uvx lightning-sdk … 运行
每次调用都出现 SSL/CA 错误,即使是 lightning --version 也如此,且仅发生在 Agent 沙盒中**/*.pem 读取拒绝规则会隐藏 certifi 的 cacert.pem:允许访问该路径;不要禁用沙盒
NameResolutionError,且仅发生在 Agent 沙盒中如果 lightning api 也失败,请让用户允许访问 lightning.ai 和 *.lightning.ai(/sandbox)。如果只有 studio/job 命令或 SDK 失败,说明它们绕过了沙盒代理:仅在沙盒外运行这些命令。后台轮询器也会以相同方式静默失败

上述安装也会让 lightning_sdk 能在该环境中导入,因此 Python 代码片段会使用其中的 python 运行。如果 CLI 来自 uv tool 或 uvx 回退方案,请使用 uv run --with lightning-sdk python script.py 运行一次性脚本。对于会留在用户项目中的代码,请先询问用户,然后使用项目自身的工具将 lightning-sdk 声明为依赖项(uv add、poetry add)。

组织范围由 API 密钥决定——没有组织标志或 LIGHTNING_ORG_ID 环境变量(使用它会被拒绝)。 沙盒需要在 LIGHTNING_SANDBOX_API_KEY 中设置组织级或团队空间级 API 密钥;个人 lightning login 凭据会失败,并显示 "Use a teamspace- or org-scoped API key (Members → API keys), not your personal login key."

你可以通过 CLI 创建组织级密钥(需要有效的个人身份验证):

export LIGHTNING_SANDBOX_API_KEY=$(lightning api-key create --org my-org --name sandbox-agent)
# or reuse the org default key: lightning api-key get --org my-org

如果用户属于多个组织,且尚未配置任何组织,请询问要使用哪个组织:

lightning api /v1/memberships | jq -r '.memberships[] | select(.ownerType=="organization") | [.ownerId, .name] | @tsv'

持久沙盒的快照/停止操作需要团队空间级密钥(组织级密钥不够)。

CLI 参考

# create — blocks until running (usually seconds)
lightning sandbox create --name devbox \
  [--instance-type cpu-1]            # default cpu-1 \
  [--runtime python313]              # DEFAULT IS node24 (Node.js, NO python!) — use python313 for Python \
                                     # node22 | node24 | python313, each with a -docker variant \
  [--image ghcr.io/org/img:latest]   # custom rootfs, CPU-only; mutually exclusive with --runtime \
  [--image-secret-ref <docker-registry-secret>] \
  [--docker]                         # dockerd at boot; == appending -docker to --runtime \
  [--teamspace owner/teamspace] [--persistent] [--spot] [--port 8000] \
  [--snapshot-id snap-...]           # warm start from a snapshot \
  [--timeout 3600000]                # auto-stop lifetime in MILLISECONDS \
  [--json]                           # prints the sandbox id — capture it for later commands

lightning sandbox list [--teamspace owner/teamspace] [--limit N] [--json]

# run a command (use -- before the command); CLI exits with the command's exit code
lightning sandbox run <SANDBOX_ID> [--cwd /workspace] [--env KEY=VALUE] -- python -c "print('hi')"
lightning sandbox run <SANDBOX_ID> --detached -- bash -lc "long-task"   # prints cmd_id
lightning sandbox command  <SANDBOX_ID> <COMMAND_ID>     # status + output
lightning sandbox logs     <SANDBOX_ID> <COMMAND_ID>
lightning sandbox commands <SANDBOX_ID>                  # history

# lifecycle
lightning sandbox stop   <SANDBOX_ID>    # persistent: auto-snapshot + pause; ephemeral: == delete
lightning sandbox start  <SANDBOX_ID>    # resume a stopped persistent sandbox
lightning sandbox delete <SANDBOX_ID> -y # destroys sandbox AND its auto-snapshot; -y required non-interactively

# snapshots (filesystem only — running processes are not preserved)
lightning sandbox snapshot create <SANDBOX_ID> [--expiration <MS>] [--exclude PATH]
lightning sandbox snapshot list [--name N] [--teamspace owner/teamspace]
lightning sandbox snapshot get <SNAPSHOT_ID>
lightning sandbox snapshot delete <SNAPSHOT_ID> -y

运行时 ID 为 node22、node24 和 python313,每种运行时都有一个 -docker 变体。其他任何 ID 都会失败,并显示 invalid runtime: <id>。自定义 --image 会在 gVisor 下使用 CPU 运行。在 SDK 中,对应选项为 image=;对于私有注册表,则使用命名 Docker 注册表密钥的 image_secret_ref=。

没有 cp/上传 CLI。请使用 run 和 shell 命令移动文件,或使用下方的 SDK 文件 API。

Python SDK

from lightning_sdk.sandbox import Sandbox, SandboxConfig, RunCommandOpts, NetworkPolicy, PtyCreateOpts

# optional explicit config; otherwise env (LIGHTNING_SANDBOX_API_KEY) / lightning login creds are used
Sandbox.configure(api_key="...")

sb = Sandbox.create(
    name="devbox",
    instance_type="cpu-1",
    runtime="python313",                   # default is node24 (Node.js only — no python!)
    teamspace="owner/teamspace",
    persistent=True,                       # enables stop()/resume()
    docker=True,                           # dockerd at boot; see "Docker inside a sandbox"
    ports=[8080],                          # exposes public HTTPS URLs for these ports
    network_policy=NetworkPolicy(allow_cidrs=["10.0.0.0/8"]),  # or "deny-all" / default open egress
    timeout=30 * 60 * 1000,                # auto-stop, MILLISECONDS
)                                          # blocks until running

# commands — non-detached blocks until exit
cmd = sb.run_command("python -c 'print(42)'")
print(cmd.exit_code, cmd.output)           # output = combined stdout+stderr
bg = sb.run_command(RunCommandOpts(cmd="python", args=["train.py"], cwd="/workspace",
                                   env={"MODE": "test"}, detached=True))
bg.wait(timeout=600); print(bg.output)     # or sb.kill_command(bg.cmd_id)

# files — content strings via REST; no host<->sandbox copy helper
sb.write_file("/workspace/app.py", "print('hi')")
text = sb.read_file("/workspace/out.txt")  # None if missing
sb.fs.mkdir("/workspace/data", recursive=True)
sb.fs.exists("/workspace/app.py"); sb.fs.readdir("/workspace"); sb.fs.rm("/tmp/x", recursive=True)

# exposed ports (only those passed as ports=[...] at create) — proxied public HTTPS
sb.port_urls                               # {"8080": "https://8080-<sandbox-id>-s.cloudspaces.litng.ai"}
sb.get_port_url(8080)                      # ValueError if 8080 wasn't exposed at create
# normally populated by create; if empty, re-get: Sandbox().get(sb.sandbox_id).port_urls

# lifecycle
sb.extend_timeout(10 * 60 * 1000)          # heartbeat; milliseconds, min 1000
snap = sb.snapshot()                       # filesystem snapshot; later: Sandbox.create(snapshot_id=snap.id)
auto_snap_id = sb.stop()                   # persistent only; sb.resume() brings it back with same id
sb.delete()                                # ALWAYS clean up — GC does not delete remote sandboxes

# find existing
client = Sandbox()
for s in client.list(teamspace="owner/teamspace").sandboxes: print(s.sandbox_id, s.status)
sb = client.get("sbx-...")

交互式 PTY(SDK 附带 websocket-client):sb.process.create_pty(PtyCreateOpts(session_name="main")) → pty.send_input("ls\n")、pty.wait()。连接正常关闭时,结果中的 exit_code 是 shell 的实际状态;后端未报告状态时为 0;连接中断时为 -1(error 会说明原因);运行期间为 None。如果需要保证获得退出代码,请优先使用 run_command。

沙盒中的 Docker

-docker 运行时自带 Docker(引擎、CLI、buildx、compose),并且**dockerd 在 create 返回时已经运行**,因此 docker version 立即可用。明确选择变体,或传入 --docker / docker=True,将后缀追加到基础运行时:

lightning sandbox create --runtime python313-docker --port 8080 --json   # cpu-1 default is fine for Docker
lightning sandbox create --runtime python313 --docker   # identical — resolves to python313-docker
lightning sandbox create --docker                       # no runtime -> node24-docker (Node, NOT Python)

如果同时传入 image(自定义镜像需带有 OCI 标签 ai.lightning.sandbox.docker=true 才能启用 Docker)或 snapshot_id(运行时来自快照),则不接受 docker=True。

镜像存储位置及可用空间

cpu-1 和 cpu-4 将镜像存储在磁盘上;其他所有规格都将镜像存储在 RAM 中。 选择实例类型时,应考虑 /var/lib/docker 所在位置,而不是 RAM 大小。在磁盘规格上,镜像不会占用内存,因此尽管默认的 cpu-1 RAM 较小,它仍是可用的 Docker 主机。

--instance-typeRAM/var/lib/docker镜像空间storage_gb 能扩容吗?
cpu-1(默认)1.25 GiB磁盘5 GB可以
cpu-28.75 GiBRAM(tmpfs)4.4 GB不可以
cpu-418.75 GiB磁盘40 GB可以
cpu-838.75 GiBRAM(tmpfs)20 GB不可以
cpu-1677.5 GiBRAM(tmpfs)39 GB不可以
  • df -h /var/lib/docker 会报告实际上限;请以它为准,而不要只看此表。 表中是当前测得的默认值,可用于规划;在确定拉取镜像所需空间前,请先检查 df。
  • 磁盘规格的所有内容共用一个配额。 /、/tmp 和 /var/lib/docker 使用同一空间池,因此 /root 中的 3 GB 文件会让镜像可用空间减少 3 GB。通过仅限 SDK 的 storage_gb 扩容(Sandbox.create(..., instance_type="cpu-1", storage_gb=20) 会给 Docker 提供 20 GB)。CLI 没有对应的标志。
  • RAM 规格的上限是沙盒内存的一半,并计入内存预算。 镜像加上占用大量资源的进程仍可能导致内存不足而被终止。storage_gb 只会增加 / 的磁盘空间,不会增加镜像空间。
  • 根据工作负载选择: 预算有限但需要处理大量镜像时选 cpu-1;构建时既需要空间又需要 RAM 时选 cpu-4;5 GB 不够时使用 storage_gb。只有在工作负载需要更多内存时才选 cpu-2 / cpu-8,并在这些规格上保持镜像较小。

数据根目录空间已满时,拉取操作会失败并提示 no space left on device,但沙盒会继续运行。 删除镜像后重试:

lightning sandbox run $SBX -- bash -lc 'docker system df; docker image prune -af; df -h /var/lib/docker'

较旧的集群则会终止沙盒。 在这些集群上,df 报告的空间大小毫无意义,显示为数百 GB;Docker 的限制只有沙盒内存,超出限制会导致沙盒终止:之后的每次调用都会返回 429 sandbox terminated: out of memory。请确保镜像远小于 RAM 列所示容量,或改用 cpu-1 / cpu-4。

网络:必须适配的部分

dockerd 使用 --bridge=none --iptables=false --ip6tables=false 运行,因此只存在 host 和 none 两种 docker 网络(docker network ls 可确认):

  • 使用 --network=host 运行容器,否则容器完全无法联网。 DNS 解析会失败,并显示 wget: bad address。无论采用哪种方式,拉取镜像都可以正常工作,因为 dockerd 本身运行在沙箱的 netstack 上。
  • 使用 --network=host 构建,否则每个 RUN 步骤都会失败,并显示 failed to solve: ... network bridge not found。在 compose 中:build: {context: ., network: host}。
  • 通过 127.0.0.1:<port> 访问其他容器,绝不能使用 compose 服务名。 Host 网络没有桥接 DNS,因此 db 或 api 会失败,并显示 gaierror: [Errno -2] Name or service not known。普通的 localhost 可以正常解析。
  • -p 8080:80 会被静默忽略:docker run -p 以 0 退出,但不会提供任何服务。 端口发布依赖桥接网络。进程必须监听你需要的端口(将 nginx 配置为使用 8080,或使用 uvicorn --port 8080)。如果创建沙箱时声明了该端口(--port 8080 / ports=[8080],然后调用 sb.get_port_url(8080)),沙箱的公开 URL 就会提供该端口上的服务。

以下 compose 模板可直接使用(FastAPI + Redis)。请以分离模式运行 docker compose up -d --build,并设置较宽裕的超时时间,因为构建过程会拉取镜像层:

services:
  web:
    build: {context: ., network: host}    # host net so pip install reaches PyPI
    network_mode: host
    environment: [REDIS_HOST=127.0.0.1]   # NOT "redis" — no bridge DNS
    depends_on: [redis]
  redis:
    image: redis:7-alpine
    network_mode: host
Docker 注意事项
  • 网络出口受限时,镜像拉取会失败;请在默认开放网络出口的环境中拉取,或将镜像预先打包到自定义镜像中。 在 network_policy="deny-all" 下,dockerd 仍会启动,但每次拉取都会卡住,最终失败,并显示 Get "https://registry-1.docker.io/v2/": context deadline exceeded。允许列表中包含解析器的 CIDR(见下文 DNS 注意事项)可以修复名称解析,但拉取随后会卡在 registry IP 上,因为 Docker Hub 使用的 CDN IP 会不断变化。
  • Docker Engine 固定为 v27;不要在沙箱内升级。 已知 v28+ 在此处无法正常工作,且不受支持。docker info 会显示确切的构建版本。Docker 仅支持 CPU 沙箱,绝不支持 GPU。
  • 创建后无法向普通运行时添加 Docker;请预先选择 -docker 运行时。 普通运行时中没有 docker 和 dockerd,且 mount 会返回 permission denied。
  • REST API 没有 docker 字段。 如果直接调用 /v1/core/sandboxes,请发送 "runtime": "node24-docker"。

示例工作流

此技能可处理的提示词:“在安全的地方运行这段不可信脚本”、“在干净的 VM 中测试我的代码”、“启动一个来自 ghcr.io/... 的容器并探索一下”、“给我一个重启后仍能保留状态的开发环境”。

安全地执行不可信/生成的代码(无网络出口,自动清理):

from lightning_sdk.sandbox import Sandbox

sb = Sandbox.create(name="quarantine", teamspace="my-org/my-teamspace", runtime="python313",
                    network_policy="deny-all", timeout=15 * 60 * 1000)  # hard kill after 15 min
try:
    sb.write_file("/workspace/suspect.py", open("suspect.py").read())
    cmd = sb.run_command("python /workspace/suspect.py")
    print(cmd.exit_code, cmd.output)
finally:
    sb.delete()

通过 CLI 在干净的环境中测试代码:

SBX=$(lightning sandbox create --name test-run --teamspace my-org/my-teamspace --runtime python313 --timeout 1800000 --json | jq -r .id)
lightning sandbox run $SBX -- python --version
lightning sandbox run $SBX --cwd /workspace -- bash -lc "pip install requests && python -c 'import requests; print(requests.__version__)'"
lightning sandbox run $SBX --detached -- bash -lc "pytest -q > /workspace/test.log 2>&1"   # prints cmd_id
lightning sandbox command $SBX <cmd_id>          # poll status + output
lightning sandbox delete $SBX -y

运行容器化应用,并通过公开 URL 预览:

SBX=$(lightning sandbox create --name preview --runtime python313-docker \
  --port 8080 --timeout 1800000 --json | jq -r .id)     # cpu-1 default: images go to disk

# --network=host is mandatory, and the container must LISTEN on 8080 (-p is ignored)
lightning sandbox run $SBX -- docker run -d --network=host --name web \
  -v /workspace:/srv -w /srv python:3.13-alpine python -m http.server 8080

lightning sandbox run $SBX -- curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080
lightning sandbox list --json | jq -r ".sandboxes[] | select(.id==\"$SBX\") | .port_urls[\"8080\"]"
lightning sandbox delete $SBX -y

试用自定义镜像:

lightning sandbox create --name img-test --image ghcr.io/myorg/myimage:latest \
  --teamspace my-org/my-teamspace --timeout 1800000
# private registry: --image-secret-ref <docker-registry-secret-name>

使用基于快照的分支功能创建持久化开发环境:

sb = Sandbox.create(name="devbox", teamspace="my-org/my-teamspace", runtime="python313", persistent=True)
sb.run_command("pip install torch numpy pandas")     # slow setup, do once
snap = sb.snapshot()                                 # golden image
sb.stop()                                            # pause billing; sb.resume() later, same id
fresh = Sandbox.create(name="experiment-1", snapshot_id=snap.id,
                       teamspace="my-org/my-teamspace")   # warm clone with deps preinstalled

原始 API 回退方案

lightning api /v1/core/sandboxes -X GET -f "organizationId=${ORG_ID}" -f "projectId=${PROJECT_ID}" -f limit=20
lightning api /v1/core/sandboxes -X GET ... | jq -r '.sandboxes[] | .name // .id'
lightning api "/v1/core/sandboxes/${SANDBOX_ID}" -X GET -f "organizationId=${ORG_ID}"   # -X GET: fields without it send a POST
lightning api "/v1/core/sandboxes/${SANDBOX_ID}/commands" -X POST -f command=ls -F detached=false

从成员资格中获取 ORG_ID:lightning api /v1/memberships | jq -r '[.memberships[] | select(.ownerType == "organization") | .ownerId][0]'。

注意事项

  • 超时单位不同。 create --timeout、extend_timeout() 和快照 --expiration 使用毫秒。sandbox run --timeout(分离等待)使用秒。
  • 沙盒会持续计费,直到停止或删除,且垃圾回收永远不会移除它们。 务必调用 stop()/delete(),并在创建时设置 timeout 作为安全保障。
  • 不加 -y 时,sandbox delete 和 sandbox snapshot delete 会退出而不删除。 在非交互模式下,它们会从已关闭的 stdin 读取提示,打印 Are you sure you want to delete? [y/N]: Aborted.,并让沙盒继续计费。这是最容易造成费用损失的方式。
  • 沙盒没有费用查询功能,因此请在创建时设置 timeout 来控制预算。 /v1/billing/usage、/v1/projects/<pid>/usage 和 /v1/core/sandboxes/<id>/usage 均会返回 404。没有任何 CLI 会报告支出,而且沙盒实例类型(cpu-1、cpu-2)未列入按价格计费的加速器目录,也未出现在 lightning machine list 中,因此即使手动计算也无法为运行定价。
  • 出口策略只能通过 SDK 设置,且创建后固定不变。 sandbox create 没有网络策略标志,因此 deny-all 和 CIDR 允许列表需要使用 lightning_sdk。这里的其他操作都可以通过 CLI 完成。无法更改运行中沙盒的出口规则。默认出口策略为开放(allow-all);对于不可信代码,请使用 "deny-all" 或 CIDR 允许列表。
  • CIDR 允许列表本身不会放行 DNS。 allow_cidrs 在 IP 层执行,而 /etc/resolv.conf 指向 1.1.1.1 和 8.8.8.8。因此,所有主机名查询都会失败并返回 Temporary failure in name resolution,而原始 IP 连接正常。请将 1.1.1.1/32 和 8.8.8.8/32 加入允许列表,或将沙盒指向其中的解析器。
  • 从快照恢复的文件,其 mtime 会变为 1970-01-01T00:00:00Z。 零纪元时间戳会导致 make、ccache 和 pip/setuptools 的陈旧检查出错。请触碰依赖的文件,或避免在恢复的沙盒中使用基于 mtime 的陈旧判断逻辑。
  • 沙盒内的命令以 root 身份运行。
  • CLI 和 SDK 会以提示的形式返回作用域密钥错误。 Use a teamspace- or org-scoped API key ...(原始错误:"organization_id is required")→ 你使用的是个人登录密钥。Your teamspace-scoped API key is not authorized for the project requested via teamspace= ...(原始错误:"API key is not authorized for this project")→ 该密钥绑定到了其他 teamspace。This operation requires a teamspace-scoped API key ... → 使用 org 作用域密钥创建快照或停止沙盒。只有原始 lightning api 调用会显示原始错误措辞。

评论

支持 Markdown