mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
feat(sandbox): add Docker GPU passthrough
Add opt-in `sandbox.docker.gpus` config plumbing for Docker sandbox containers. - thread the optional GPU passthrough field through config types, schema, resolution, and Docker create args - reject empty config values and emit `--gpus` as a separate Docker argv pair - document the Docker-only behavior and credit the original contributor in the changelog Fixes #57976. Carries forward #58124 from @cyan-ember. Co-authored-by: cyan-ember <5855097+cyan-ember@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Sandbox/Docker: add opt-in `sandbox.docker.gpus` passthrough for Docker sandbox containers so local GPU workloads can run inside sandboxed agents when the host Docker runtime supports `--gpus`. Fixes #57976; carries forward #58124. Thanks @cyan-ember.
|
||||
- iOS/Gateway: add an authenticated `node.presence.alive` protocol event and `node.list` last-seen fields so background iOS wakes can mark paired nodes recently alive without treating them as connected. Carries forward #63123. Thanks @ngutman.
|
||||
- Android: publish authenticated `node.presence.alive` events after node connect and background transitions so paired Android nodes retain durable last-seen metadata after disconnects. Carries forward #63123. Thanks @ngutman.
|
||||
- Gateway/chat: accept non-image attachments through `chat.send` by staging them as agent-readable media paths, while keeping unsupported RPC attachment paths explicit instead of silently dropping files. Fixes #48123. (#67572) Thanks @samzong.
|
||||
|
||||
@@ -89,6 +89,8 @@ SSH-specific config lives under `agents.defaults.sandbox.ssh`. OpenShell-specifi
|
||||
|
||||
Sandboxing is off by default. If you enable sandboxing and do not choose a backend, OpenClaw uses the Docker backend. It executes tools and sandbox browsers locally via the Docker daemon socket (`/var/run/docker.sock`). Sandbox container isolation is determined by Docker namespaces.
|
||||
|
||||
To expose host GPUs to Docker sandboxes, set `agents.defaults.sandbox.docker.gpus` or the per-agent `agents.list[].sandbox.docker.gpus` override. The value is passed to Docker's `--gpus` flag as a separate argument, for example `"all"` or `"device=GPU-uuid"`, and requires a compatible host runtime such as NVIDIA Container Toolkit.
|
||||
|
||||
<Warning>
|
||||
**Docker-out-of-Docker (DooD) constraints**
|
||||
|
||||
|
||||
@@ -164,6 +164,21 @@ describe("buildSandboxCreateArgs", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("emits Docker GPU passthrough as a separate argument", () => {
|
||||
const cfg = createSandboxConfig({
|
||||
gpus: "device=GPU-123",
|
||||
});
|
||||
|
||||
const args = buildSandboxCreateArgs({
|
||||
name: "openclaw-sbx-gpu",
|
||||
cfg,
|
||||
scopeKey: "main",
|
||||
createdAtMs: 1700000000000,
|
||||
});
|
||||
|
||||
expect(args).toEqual(expect.arrayContaining(["--gpus", "device=GPU-123"]));
|
||||
});
|
||||
|
||||
it("emits -v flags for safe custom binds", () => {
|
||||
const cfg: SandboxDockerConfig = {
|
||||
image: "openclaw-sandbox:bookworm-slim",
|
||||
|
||||
@@ -36,6 +36,29 @@ describe("sandbox config merges", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves sandbox docker GPU passthrough with agent precedence", () => {
|
||||
const inherited = resolveSandboxDockerConfig({
|
||||
scope: "agent",
|
||||
globalDocker: { gpus: "all" },
|
||||
agentDocker: {},
|
||||
});
|
||||
expect(inherited.gpus).toBe("all");
|
||||
|
||||
const overridden = resolveSandboxDockerConfig({
|
||||
scope: "agent",
|
||||
globalDocker: { gpus: "all" },
|
||||
agentDocker: { gpus: "device=GPU-123" },
|
||||
});
|
||||
expect(overridden.gpus).toBe("device=GPU-123");
|
||||
|
||||
const sharedScope = resolveSandboxDockerConfig({
|
||||
scope: "shared",
|
||||
globalDocker: { gpus: "all" },
|
||||
agentDocker: { gpus: "device=GPU-123" },
|
||||
});
|
||||
expect(sharedScope.gpus).toBe("all");
|
||||
});
|
||||
|
||||
it("resolves docker binds and shared-scope override behavior", () => {
|
||||
for (const scenario of [
|
||||
{
|
||||
|
||||
@@ -116,6 +116,7 @@ export function resolveSandboxDockerConfig(params: {
|
||||
memory: agentDocker?.memory ?? globalDocker?.memory,
|
||||
memorySwap: agentDocker?.memorySwap ?? globalDocker?.memorySwap,
|
||||
cpus: agentDocker?.cpus ?? globalDocker?.cpus,
|
||||
gpus: normalizeOptionalString(agentDocker?.gpus ?? globalDocker?.gpus),
|
||||
ulimits,
|
||||
seccompProfile: agentDocker?.seccompProfile ?? globalDocker?.seccompProfile,
|
||||
apparmorProfile: agentDocker?.apparmorProfile ?? globalDocker?.apparmorProfile,
|
||||
|
||||
@@ -444,6 +444,10 @@ export function buildSandboxCreateArgs(params: {
|
||||
if (typeof params.cfg.cpus === "number" && params.cfg.cpus > 0) {
|
||||
args.push("--cpus", String(params.cfg.cpus));
|
||||
}
|
||||
const gpus = params.cfg.gpus?.trim();
|
||||
if (gpus) {
|
||||
args.push("--gpus", gpus);
|
||||
}
|
||||
for (const [name, value] of Object.entries(params.cfg.ulimits ?? {})) {
|
||||
const formatted = formatUlimitValue(name, value);
|
||||
if (formatted) {
|
||||
|
||||
@@ -62,6 +62,39 @@ describe("sandbox docker config", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts non-empty Docker GPU passthrough config", () => {
|
||||
const res = validateConfigObject({
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
docker: {
|
||||
gpus: "all",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.agents?.defaults?.sandbox?.docker?.gpus).toBe("all");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects empty Docker GPU passthrough config", () => {
|
||||
const res = validateConfigObject({
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
docker: {
|
||||
gpus: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects network host mode via Zod schema validation", () => {
|
||||
const res = validateConfigObject({
|
||||
agents: {
|
||||
|
||||
@@ -422,6 +422,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"DANGEROUS break-glass override that allows sandbox Docker network mode container:<id>. This joins another container namespace and weakens sandbox isolation.",
|
||||
"agents.list[].sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"Per-agent DANGEROUS override for container namespace joins in sandbox Docker network mode.",
|
||||
"agents.defaults.sandbox.docker.gpus":
|
||||
'Optional Docker GPU passthrough value passed to --gpus, for example "all" or "device=GPU-uuid". Requires a compatible host runtime such as NVIDIA Container Toolkit.',
|
||||
"agents.list[].sandbox.docker.gpus":
|
||||
"Per-agent Docker GPU passthrough override for sandbox containers.",
|
||||
"agents.defaults.sandbox.browser.cdpSourceRange":
|
||||
"Optional CIDR allowlist for container-edge CDP ingress (for example 172.21.0.1/32).",
|
||||
"agents.list[].sandbox.browser.cdpSourceRange":
|
||||
|
||||
@@ -635,6 +635,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.sandbox.browser.cdpSourceRange": "Sandbox Browser CDP Source Port Range",
|
||||
"agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"Sandbox Docker Allow Container Namespace Join",
|
||||
"agents.defaults.sandbox.docker.gpus": "Sandbox Docker GPUs",
|
||||
commands: "Commands",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.nativeSkills": "Native Skill Commands",
|
||||
@@ -879,6 +880,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.list[].sandbox.browser.cdpSourceRange": "Agent Sandbox Browser CDP Source Port Range",
|
||||
"agents.list[].sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"Agent Sandbox Docker Allow Container Namespace Join",
|
||||
"agents.list[].sandbox.docker.gpus": "Agent Sandbox Docker GPUs",
|
||||
"discovery.mdns.mode": "mDNS Discovery Mode",
|
||||
plugins: "Plugins",
|
||||
"plugins.enabled": "Enable Plugins",
|
||||
|
||||
@@ -29,6 +29,8 @@ export type SandboxDockerSettings = {
|
||||
memorySwap?: string | number;
|
||||
/** Limit container CPU shares (e.g. 0.5, 1, 2). */
|
||||
cpus?: number;
|
||||
/** GPU devices to expose via Docker --gpus (e.g. "all", "device=GPU-uuid"). */
|
||||
gpus?: string;
|
||||
/**
|
||||
* Set ulimit values by name (e.g. nofile, nproc).
|
||||
* Use "soft:hard" string, a number, or { soft, hard }.
|
||||
|
||||
@@ -119,6 +119,7 @@ export const SandboxDockerSchema = z
|
||||
memory: z.union([z.string(), z.number()]).optional(),
|
||||
memorySwap: z.union([z.string(), z.number()]).optional(),
|
||||
cpus: z.number().positive().optional(),
|
||||
gpus: z.string().min(1).optional(),
|
||||
ulimits: z
|
||||
.record(
|
||||
z.string(),
|
||||
|
||||
Reference in New Issue
Block a user