diff --git a/CHANGELOG.md b/CHANGELOG.md index 2817e14251c..f677a5f5b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/gateway/sandboxing.md b/docs/gateway/sandboxing.md index a2a3567d88d..e953635e68a 100644 --- a/docs/gateway/sandboxing.md +++ b/docs/gateway/sandboxing.md @@ -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. + **Docker-out-of-Docker (DooD) constraints** diff --git a/src/agents/sandbox-create-args.test.ts b/src/agents/sandbox-create-args.test.ts index 60b6241f58a..4f5fae4b9be 100644 --- a/src/agents/sandbox-create-args.test.ts +++ b/src/agents/sandbox-create-args.test.ts @@ -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", diff --git a/src/agents/sandbox-merge.test.ts b/src/agents/sandbox-merge.test.ts index 742701017d2..4b2ff1c77f0 100644 --- a/src/agents/sandbox-merge.test.ts +++ b/src/agents/sandbox-merge.test.ts @@ -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 [ { diff --git a/src/agents/sandbox/config.ts b/src/agents/sandbox/config.ts index 54a4c36aae3..d8688a801ff 100644 --- a/src/agents/sandbox/config.ts +++ b/src/agents/sandbox/config.ts @@ -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, diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index d6c3eb38693..ce7477531b5 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -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) { diff --git a/src/config/config.sandbox-docker.test.ts b/src/config/config.sandbox-docker.test.ts index bf86e71bf8c..db8a6eac6ba 100644 --- a/src/config/config.sandbox-docker.test.ts +++ b/src/config/config.sandbox-docker.test.ts @@ -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: { diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 9393915c08b..bd7765fada5 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -422,6 +422,10 @@ export const FIELD_HELP: Record = { "DANGEROUS break-glass override that allows sandbox Docker network mode container:. 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": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 3d2600a5904..ce63efef686 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -635,6 +635,7 @@ export const FIELD_LABELS: Record = { "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 = { "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", diff --git a/src/config/types.sandbox.ts b/src/config/types.sandbox.ts index 04128e2ffaa..1a4670d3c2a 100644 --- a/src/config/types.sandbox.ts +++ b/src/config/types.sandbox.ts @@ -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 }. diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 2d0c8aca7b1..57837910204 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -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(),