mirror of https://github.com/openclaw/openclaw.git
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:
parent
302dafbe1a
commit
cb9a5e1cb9
|
|
@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
|
||||
- Sandbox: add `sandbox.browser.binds` to configure browser-container bind mounts separately from exec containers. (#16230) Thanks @seheepeak.
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
|
||||
- `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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
`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):
|
||||
|
||||
```json5
|
||||
|
|
|
|||
|
|
@ -106,9 +106,13 @@ export async function ensureSandboxBrowser(params: {
|
|||
const state = await dockerContainerState(containerName);
|
||||
if (!state.exists) {
|
||||
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({
|
||||
name: containerName,
|
||||
cfg: { ...params.cfg.docker, network: "bridge" },
|
||||
cfg: browserDockerCfg,
|
||||
scopeKey: params.scopeKey,
|
||||
labels: { "openclaw.sandboxBrowser": "1" },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ export function resolveSandboxBrowserConfig(params: {
|
|||
}): SandboxBrowserConfig {
|
||||
const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser;
|
||||
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 {
|
||||
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
|
||||
image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE,
|
||||
|
|
@ -107,6 +110,7 @@ export function resolveSandboxBrowserConfig(params: {
|
|||
agentBrowser?.autoStartTimeoutMs ??
|
||||
globalBrowser?.autoStartTimeoutMs ??
|
||||
DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS,
|
||||
binds: bindsConfigured ? binds : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export type SandboxBrowserConfig = {
|
|||
allowHostControl: boolean;
|
||||
autoStart: boolean;
|
||||
autoStartTimeoutMs: number;
|
||||
binds?: string[];
|
||||
};
|
||||
|
||||
export type SandboxPruneConfig = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSandboxBrowserConfig } from "../agents/sandbox/config.js";
|
||||
import { validateConfigObject } from "./config.js";
|
||||
|
||||
describe("sandbox docker config", () => {
|
||||
|
|
@ -52,3 +53,83 @@ describe("sandbox docker config", () => {
|
|||
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;
|
||||
/** Max time to wait for CDP to become reachable after auto-start (ms). */
|
||||
autoStartTimeoutMs?: number;
|
||||
/** Additional bind mounts for the browser container only. When set, replaces docker.binds for the browser container. */
|
||||
binds?: string[];
|
||||
};
|
||||
|
||||
export type SandboxPruneSettings = {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export const SandboxBrowserSchema = z
|
|||
allowHostControl: z.boolean().optional(),
|
||||
autoStart: z.boolean().optional(),
|
||||
autoStartTimeoutMs: z.number().int().positive().optional(),
|
||||
binds: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
|
|
|||
Loading…
Reference in New Issue