mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 17:01:53 +03:00
feat(sandbox): separate bind mounts for browser containers (#16230)
* feat(sandbox): add separate browser.binds config for browser containers Allow configuring bind mounts independently for browser containers via sandbox.browser.binds. When set, browser containers use browser-specific binds instead of inheriting docker.binds. Falls back to docker.binds when browser.binds is not configured for backwards compatibility. Closes #14614 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(sandbox): honor empty browser binds override (#16230) (thanks @seheepeak) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Sandbox: add `sandbox.browser.binds` to configure browser-container bind mounts separately from exec containers. (#16230) Thanks @seheepeak.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins.
|
- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins.
|
||||||
|
|||||||
@@ -933,6 +933,7 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway
|
|||||||
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in main config.
|
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in main config.
|
||||||
|
|
||||||
- `allowHostControl: false` (default) blocks sandboxed sessions from targeting the host browser.
|
- `allowHostControl: false` (default) blocks sandboxed sessions from targeting the host browser.
|
||||||
|
- `sandbox.browser.binds` mounts additional host directories into the sandbox browser container only. When set (including `[]`), it replaces `docker.binds` for the browser container.
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ Format: `host:container:mode` (e.g., `"/home/user/source:/source:rw"`).
|
|||||||
|
|
||||||
Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"`, per-agent binds are ignored.
|
Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"`, per-agent binds are ignored.
|
||||||
|
|
||||||
|
`agents.defaults.sandbox.browser.binds` mounts additional host directories into the **sandbox browser** container only.
|
||||||
|
|
||||||
|
- When set (including `[]`), it replaces `agents.defaults.sandbox.docker.binds` for the browser container.
|
||||||
|
- When omitted, the browser container falls back to `agents.defaults.sandbox.docker.binds` (backwards compatible).
|
||||||
|
|
||||||
Example (read-only source + docker socket):
|
Example (read-only source + docker socket):
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
|
|||||||
@@ -106,9 +106,13 @@ export async function ensureSandboxBrowser(params: {
|
|||||||
const state = await dockerContainerState(containerName);
|
const state = await dockerContainerState(containerName);
|
||||||
if (!state.exists) {
|
if (!state.exists) {
|
||||||
await ensureSandboxBrowserImage(params.cfg.browser.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE);
|
await ensureSandboxBrowserImage(params.cfg.browser.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE);
|
||||||
|
const browserDockerCfg =
|
||||||
|
params.cfg.browser.binds !== undefined
|
||||||
|
? { ...params.cfg.docker, network: "bridge", binds: params.cfg.browser.binds }
|
||||||
|
: { ...params.cfg.docker, network: "bridge" };
|
||||||
const args = buildSandboxCreateArgs({
|
const args = buildSandboxCreateArgs({
|
||||||
name: containerName,
|
name: containerName,
|
||||||
cfg: { ...params.cfg.docker, network: "bridge" },
|
cfg: browserDockerCfg,
|
||||||
scopeKey: params.scopeKey,
|
scopeKey: params.scopeKey,
|
||||||
labels: { "openclaw.sandboxBrowser": "1" },
|
labels: { "openclaw.sandboxBrowser": "1" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ export function resolveSandboxBrowserConfig(params: {
|
|||||||
}): SandboxBrowserConfig {
|
}): SandboxBrowserConfig {
|
||||||
const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser;
|
const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser;
|
||||||
const globalBrowser = params.globalBrowser;
|
const globalBrowser = params.globalBrowser;
|
||||||
|
const binds = [...(globalBrowser?.binds ?? []), ...(agentBrowser?.binds ?? [])];
|
||||||
|
// Treat `binds: []` as an explicit override, so it can disable `docker.binds` for the browser container.
|
||||||
|
const bindsConfigured = globalBrowser?.binds !== undefined || agentBrowser?.binds !== undefined;
|
||||||
return {
|
return {
|
||||||
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
|
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
|
||||||
image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE,
|
image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE,
|
||||||
@@ -107,6 +110,7 @@ export function resolveSandboxBrowserConfig(params: {
|
|||||||
agentBrowser?.autoStartTimeoutMs ??
|
agentBrowser?.autoStartTimeoutMs ??
|
||||||
globalBrowser?.autoStartTimeoutMs ??
|
globalBrowser?.autoStartTimeoutMs ??
|
||||||
DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS,
|
DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS,
|
||||||
|
binds: bindsConfigured ? binds : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type SandboxBrowserConfig = {
|
|||||||
allowHostControl: boolean;
|
allowHostControl: boolean;
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
autoStartTimeoutMs: number;
|
autoStartTimeoutMs: number;
|
||||||
|
binds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SandboxPruneConfig = {
|
export type SandboxPruneConfig = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveSandboxBrowserConfig } from "../agents/sandbox/config.js";
|
||||||
import { validateConfigObject } from "./config.js";
|
import { validateConfigObject } from "./config.js";
|
||||||
|
|
||||||
describe("sandbox docker config", () => {
|
describe("sandbox docker config", () => {
|
||||||
@@ -52,3 +53,83 @@ describe("sandbox docker config", () => {
|
|||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("sandbox browser binds config", () => {
|
||||||
|
it("accepts binds array in sandbox.browser config", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: {
|
||||||
|
browser: {
|
||||||
|
binds: ["/home/user/.chrome-profile:/data/chrome:rw"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
if (res.ok) {
|
||||||
|
expect(res.config.agents?.defaults?.sandbox?.browser?.binds).toEqual([
|
||||||
|
"/home/user/.chrome-profile:/data/chrome:rw",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-string values in browser binds array", () => {
|
||||||
|
const res = validateConfigObject({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: {
|
||||||
|
browser: {
|
||||||
|
binds: [123],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges global and agent browser binds", () => {
|
||||||
|
const resolved = resolveSandboxBrowserConfig({
|
||||||
|
scope: "agent",
|
||||||
|
globalBrowser: { binds: ["/global:/global:ro"] },
|
||||||
|
agentBrowser: { binds: ["/agent:/agent:rw"] },
|
||||||
|
});
|
||||||
|
expect(resolved.binds).toEqual(["/global:/global:ro", "/agent:/agent:rw"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats empty binds as configured (override to none)", () => {
|
||||||
|
const resolved = resolveSandboxBrowserConfig({
|
||||||
|
scope: "agent",
|
||||||
|
globalBrowser: { binds: [] },
|
||||||
|
agentBrowser: {},
|
||||||
|
});
|
||||||
|
expect(resolved.binds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores agent browser binds under shared scope", () => {
|
||||||
|
const resolved = resolveSandboxBrowserConfig({
|
||||||
|
scope: "shared",
|
||||||
|
globalBrowser: { binds: ["/global:/global:ro"] },
|
||||||
|
agentBrowser: { binds: ["/agent:/agent:rw"] },
|
||||||
|
});
|
||||||
|
expect(resolved.binds).toEqual(["/global:/global:ro"]);
|
||||||
|
|
||||||
|
const resolvedNoGlobal = resolveSandboxBrowserConfig({
|
||||||
|
scope: "shared",
|
||||||
|
globalBrowser: {},
|
||||||
|
agentBrowser: { binds: ["/agent:/agent:rw"] },
|
||||||
|
});
|
||||||
|
expect(resolvedNoGlobal.binds).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined binds when none configured", () => {
|
||||||
|
const resolved = resolveSandboxBrowserConfig({
|
||||||
|
scope: "agent",
|
||||||
|
globalBrowser: {},
|
||||||
|
agentBrowser: {},
|
||||||
|
});
|
||||||
|
expect(resolved.binds).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export type SandboxBrowserSettings = {
|
|||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
/** Max time to wait for CDP to become reachable after auto-start (ms). */
|
/** Max time to wait for CDP to become reachable after auto-start (ms). */
|
||||||
autoStartTimeoutMs?: number;
|
autoStartTimeoutMs?: number;
|
||||||
|
/** Additional bind mounts for the browser container only. When set, replaces docker.binds for the browser container. */
|
||||||
|
binds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SandboxPruneSettings = {
|
export type SandboxPruneSettings = {
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export const SandboxBrowserSchema = z
|
|||||||
allowHostControl: z.boolean().optional(),
|
allowHostControl: z.boolean().optional(),
|
||||||
autoStart: z.boolean().optional(),
|
autoStart: z.boolean().optional(),
|
||||||
autoStartTimeoutMs: z.number().int().positive().optional(),
|
autoStartTimeoutMs: z.number().int().positive().optional(),
|
||||||
|
binds: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
Reference in New Issue
Block a user