feat: add sandbox browser control allowlists
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
## 2026.1.11-1
|
||||
|
||||
### New Features and Changes
|
||||
- Agents/Browser: add `browser.target` (sandbox/host/custom) with sandbox host-control gating via `agents.defaults.sandbox.browser.allowHostControl`, and expand browser tool docs (remote control, profiles, internals).
|
||||
- Agents/Browser: add `browser.target` (sandbox/host/custom) with sandbox host-control gating via `agents.defaults.sandbox.browser.allowHostControl`, allowlists for custom control URLs/hosts/ports, and expand browser tool docs (remote control, profiles, internals).
|
||||
|
||||
## 2026.1.10-4
|
||||
|
||||
|
||||
@@ -1377,6 +1377,9 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
||||
headless: false,
|
||||
enableNoVnc: true,
|
||||
allowHostControl: false,
|
||||
allowedControlUrls: ["http://10.0.0.42:18791"],
|
||||
allowedControlHosts: ["browser.lab.local", "10.0.0.42"],
|
||||
allowedControlPorts: [18791],
|
||||
autoStart: true,
|
||||
autoStartTimeoutMs: 12000
|
||||
},
|
||||
@@ -1424,6 +1427,11 @@ sandboxed sessions to explicitly target the **host** browser control server
|
||||
via the browser tool (`target: "host"`). Leave this off if you want strict
|
||||
sandbox isolation.
|
||||
|
||||
Allowlists for remote control:
|
||||
- `allowedControlUrls`: exact control URLs permitted for `target: "custom"`.
|
||||
- `allowedControlHosts`: hostnames permitted (hostname only, no port).
|
||||
- `allowedControlPorts`: ports permitted (defaults: http=80, https=443).
|
||||
|
||||
### `models` (custom providers + base URLs)
|
||||
|
||||
Clawdbot uses the **pi-coding-agent** model catalog. You can add custom providers
|
||||
|
||||
@@ -22,6 +22,7 @@ and process access when the model does something dumb.
|
||||
- By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it.
|
||||
Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`.
|
||||
- `agents.defaults.sandbox.browser.allowHostControl` lets sandboxed sessions target the host browser explicitly.
|
||||
- Optional allowlists gate `target: "custom"`: `allowedControlUrls`, `allowedControlHosts`, `allowedControlPorts`.
|
||||
|
||||
Not sandboxed:
|
||||
- The Gateway process itself.
|
||||
|
||||
@@ -244,5 +244,6 @@ How it maps:
|
||||
- `controlUrl` sets `target: "custom"` implicitly (remote control server).
|
||||
- In sandboxed sessions, `target: "host"` requires `agents.defaults.sandbox.browser.allowHostControl=true`.
|
||||
- If `target` is omitted: sandboxed sessions default to `sandbox`, non-sandbox sessions default to `host`.
|
||||
- Sandbox allowlists can restrict `target: "custom"` to specific URLs/hosts/ports.
|
||||
|
||||
This keeps the agent deterministic and avoids brittle selectors.
|
||||
|
||||
@@ -18,6 +18,9 @@ import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||
export function createClawdbotTools(options?: {
|
||||
browserControlUrl?: string;
|
||||
allowHostBrowserControl?: boolean;
|
||||
allowedControlUrls?: string[];
|
||||
allowedControlHosts?: string[];
|
||||
allowedControlPorts?: number[];
|
||||
agentSessionKey?: string;
|
||||
agentProvider?: GatewayMessageProvider;
|
||||
agentAccountId?: string;
|
||||
@@ -41,6 +44,9 @@ export function createClawdbotTools(options?: {
|
||||
createBrowserTool({
|
||||
defaultControlUrl: options?.browserControlUrl,
|
||||
allowHostControl: options?.allowHostBrowserControl,
|
||||
allowedControlUrls: options?.allowedControlUrls,
|
||||
allowedControlHosts: options?.allowedControlHosts,
|
||||
allowedControlPorts: options?.allowedControlPorts,
|
||||
}),
|
||||
createCanvasTool(),
|
||||
createNodesTool(),
|
||||
|
||||
@@ -480,6 +480,9 @@ type EmbeddedSandboxInfo = {
|
||||
browserControlUrl?: string;
|
||||
browserNoVncUrl?: string;
|
||||
hostBrowserAllowed?: boolean;
|
||||
allowedControlUrls?: string[];
|
||||
allowedControlHosts?: string[];
|
||||
allowedControlPorts?: number[];
|
||||
elevated?: {
|
||||
allowed: boolean;
|
||||
defaultLevel: "on" | "off";
|
||||
@@ -572,6 +575,9 @@ export function buildEmbeddedSandboxInfo(
|
||||
browserControlUrl: sandbox.browser?.controlUrl,
|
||||
browserNoVncUrl: sandbox.browser?.noVncUrl,
|
||||
hostBrowserAllowed: sandbox.browserAllowHostControl,
|
||||
allowedControlUrls: sandbox.browserAllowedControlUrls,
|
||||
allowedControlHosts: sandbox.browserAllowedControlHosts,
|
||||
allowedControlPorts: sandbox.browserAllowedControlPorts,
|
||||
...(elevatedAllowed
|
||||
? {
|
||||
elevated: {
|
||||
|
||||
@@ -627,6 +627,9 @@ export function createClawdbotCodingTools(options?: {
|
||||
...createClawdbotTools({
|
||||
browserControlUrl: sandbox?.browser?.controlUrl,
|
||||
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
||||
allowedControlUrls: sandbox?.browserAllowedControlUrls,
|
||||
allowedControlHosts: sandbox?.browserAllowedControlHosts,
|
||||
allowedControlPorts: sandbox?.browserAllowedControlPorts,
|
||||
agentSessionKey: options?.sessionKey,
|
||||
agentProvider: resolveGatewayMessageProvider(options?.messageProvider),
|
||||
agentAccountId: options?.agentAccountId,
|
||||
|
||||
@@ -79,6 +79,9 @@ export type SandboxBrowserConfig = {
|
||||
headless: boolean;
|
||||
enableNoVnc: boolean;
|
||||
allowHostControl: boolean;
|
||||
allowedControlUrls?: string[];
|
||||
allowedControlHosts?: string[];
|
||||
allowedControlPorts?: number[];
|
||||
autoStart: boolean;
|
||||
autoStartTimeoutMs: number;
|
||||
};
|
||||
@@ -140,6 +143,9 @@ export type SandboxContext = {
|
||||
docker: SandboxDockerConfig;
|
||||
tools: SandboxToolPolicy;
|
||||
browserAllowHostControl: boolean;
|
||||
browserAllowedControlUrls?: string[];
|
||||
browserAllowedControlHosts?: string[];
|
||||
browserAllowedControlPorts?: number[];
|
||||
browser?: SandboxBrowserContext;
|
||||
};
|
||||
|
||||
@@ -310,6 +316,12 @@ export function resolveSandboxBrowserConfig(params: {
|
||||
const agentBrowser =
|
||||
params.scope === "shared" ? undefined : params.agentBrowser;
|
||||
const globalBrowser = params.globalBrowser;
|
||||
const allowedControlUrls =
|
||||
agentBrowser?.allowedControlUrls ?? globalBrowser?.allowedControlUrls;
|
||||
const allowedControlHosts =
|
||||
agentBrowser?.allowedControlHosts ?? globalBrowser?.allowedControlHosts;
|
||||
const allowedControlPorts =
|
||||
agentBrowser?.allowedControlPorts ?? globalBrowser?.allowedControlPorts;
|
||||
return {
|
||||
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
|
||||
image:
|
||||
@@ -339,6 +351,18 @@ export function resolveSandboxBrowserConfig(params: {
|
||||
agentBrowser?.allowHostControl ??
|
||||
globalBrowser?.allowHostControl ??
|
||||
false,
|
||||
allowedControlUrls:
|
||||
Array.isArray(allowedControlUrls) && allowedControlUrls.length > 0
|
||||
? allowedControlUrls
|
||||
: undefined,
|
||||
allowedControlHosts:
|
||||
Array.isArray(allowedControlHosts) && allowedControlHosts.length > 0
|
||||
? allowedControlHosts
|
||||
: undefined,
|
||||
allowedControlPorts:
|
||||
Array.isArray(allowedControlPorts) && allowedControlPorts.length > 0
|
||||
? allowedControlPorts
|
||||
: undefined,
|
||||
autoStart: agentBrowser?.autoStart ?? globalBrowser?.autoStart ?? true,
|
||||
autoStartTimeoutMs:
|
||||
agentBrowser?.autoStartTimeoutMs ??
|
||||
@@ -1331,6 +1355,9 @@ export async function resolveSandboxContext(params: {
|
||||
docker: cfg.docker,
|
||||
tools: cfg.tools,
|
||||
browserAllowHostControl: cfg.browser.allowHostControl,
|
||||
browserAllowedControlUrls: cfg.browser.allowedControlUrls,
|
||||
browserAllowedControlHosts: cfg.browser.allowedControlHosts,
|
||||
browserAllowedControlPorts: cfg.browser.allowedControlPorts,
|
||||
browser: browser ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -249,6 +249,21 @@ export function buildAgentSystemPrompt(params: {
|
||||
: params.sandboxInfo.hostBrowserAllowed === false
|
||||
? "Host browser control: blocked."
|
||||
: "",
|
||||
params.sandboxInfo.allowedControlUrls?.length
|
||||
? `Browser control URL allowlist: ${params.sandboxInfo.allowedControlUrls.join(
|
||||
", ",
|
||||
)}`
|
||||
: "",
|
||||
params.sandboxInfo.allowedControlHosts?.length
|
||||
? `Browser control host allowlist: ${params.sandboxInfo.allowedControlHosts.join(
|
||||
", ",
|
||||
)}`
|
||||
: "",
|
||||
params.sandboxInfo.allowedControlPorts?.length
|
||||
? `Browser control port allowlist: ${params.sandboxInfo.allowedControlPorts.join(
|
||||
", ",
|
||||
)}`
|
||||
: "",
|
||||
params.sandboxInfo.elevated?.allowed
|
||||
? "Elevated bash is available for this session."
|
||||
: "",
|
||||
|
||||
@@ -136,6 +136,9 @@ function resolveBrowserBaseUrl(params: {
|
||||
controlUrl?: string;
|
||||
defaultControlUrl?: string;
|
||||
allowHostControl?: boolean;
|
||||
allowedControlUrls?: string[];
|
||||
allowedControlHosts?: string[];
|
||||
allowedControlPorts?: number[];
|
||||
}) {
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser);
|
||||
@@ -145,6 +148,51 @@ function resolveBrowserBaseUrl(params: {
|
||||
params.target ??
|
||||
(normalizedControlUrl ? "custom" : normalizedDefault ? "sandbox" : "host");
|
||||
|
||||
const assertAllowedControlUrl = (url: string) => {
|
||||
const allowedUrls = params.allowedControlUrls?.map((entry) =>
|
||||
entry.trim().replace(/\/$/, ""),
|
||||
);
|
||||
const allowedHosts = params.allowedControlHosts?.map((entry) =>
|
||||
entry.trim().toLowerCase(),
|
||||
);
|
||||
const allowedPorts = params.allowedControlPorts;
|
||||
if (
|
||||
!allowedUrls?.length &&
|
||||
!allowedHosts?.length &&
|
||||
!allowedPorts?.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
throw new Error(`Invalid browser controlUrl: ${url}`);
|
||||
}
|
||||
const normalizedUrl = parsed.toString().replace(/\/$/, "");
|
||||
if (allowedUrls?.length && !allowedUrls.includes(normalizedUrl)) {
|
||||
throw new Error("Browser controlUrl is not in the allowed URL list.");
|
||||
}
|
||||
if (allowedHosts?.length && !allowedHosts.includes(parsed.hostname)) {
|
||||
throw new Error(
|
||||
"Browser controlUrl hostname is not in the allowed host list.",
|
||||
);
|
||||
}
|
||||
if (allowedPorts?.length) {
|
||||
const port =
|
||||
parsed.port?.trim() !== ""
|
||||
? Number(parsed.port)
|
||||
: parsed.protocol === "https:"
|
||||
? 443
|
||||
: 80;
|
||||
if (!Number.isFinite(port) || !allowedPorts.includes(port)) {
|
||||
throw new Error(
|
||||
"Browser controlUrl port is not in the allowed port list.",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (target !== "custom" && params.target && normalizedControlUrl) {
|
||||
throw new Error('controlUrl is only supported with target="custom".');
|
||||
}
|
||||
@@ -153,7 +201,9 @@ function resolveBrowserBaseUrl(params: {
|
||||
if (!normalizedControlUrl) {
|
||||
throw new Error("Custom browser target requires controlUrl.");
|
||||
}
|
||||
return normalizedControlUrl.replace(/\/$/, "");
|
||||
const normalized = normalizedControlUrl.replace(/\/$/, "");
|
||||
assertAllowedControlUrl(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (target === "sandbox") {
|
||||
@@ -173,18 +223,40 @@ function resolveBrowserBaseUrl(params: {
|
||||
"Browser control is disabled. Set browser.enabled=true in ~/.clawdbot/clawdbot.json.",
|
||||
);
|
||||
}
|
||||
return resolved.controlUrl.replace(/\/$/, "");
|
||||
const normalized = resolved.controlUrl.replace(/\/$/, "");
|
||||
assertAllowedControlUrl(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function createBrowserTool(opts?: {
|
||||
defaultControlUrl?: string;
|
||||
allowHostControl?: boolean;
|
||||
allowedControlUrls?: string[];
|
||||
allowedControlHosts?: string[];
|
||||
allowedControlPorts?: number[];
|
||||
}): AnyAgentTool {
|
||||
const targetDefault = opts?.defaultControlUrl ? "sandbox" : "host";
|
||||
const hostHint =
|
||||
opts?.allowHostControl === false
|
||||
? "Host target blocked by policy."
|
||||
: "Host target allowed.";
|
||||
const allowlistHint =
|
||||
opts?.allowedControlUrls?.length ||
|
||||
opts?.allowedControlHosts?.length ||
|
||||
opts?.allowedControlPorts?.length
|
||||
? "Custom targets are restricted by sandbox allowlists."
|
||||
: "Custom targets are unrestricted.";
|
||||
return {
|
||||
label: "Browser",
|
||||
name: "browser",
|
||||
description:
|
||||
"Control clawd's dedicated browser (status/start/stop/tabs/open/snapshot/screenshot/actions). Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
|
||||
description: [
|
||||
"Control clawd's dedicated browser (status/start/stop/tabs/open/snapshot/screenshot/actions).",
|
||||
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
|
||||
`target selects browser location (sandbox|host|custom). Default: ${targetDefault}.`,
|
||||
"controlUrl implies target=custom (remote control server).",
|
||||
hostHint,
|
||||
allowlistHint,
|
||||
].join(" "),
|
||||
parameters: BrowserToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
@@ -201,6 +273,9 @@ export function createBrowserTool(opts?: {
|
||||
controlUrl,
|
||||
defaultControlUrl: opts?.defaultControlUrl,
|
||||
allowHostControl: opts?.allowHostControl,
|
||||
allowedControlUrls: opts?.allowedControlUrls,
|
||||
allowedControlHosts: opts?.allowedControlHosts,
|
||||
allowedControlPorts: opts?.allowedControlPorts,
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
|
||||
@@ -865,6 +865,21 @@ export type SandboxBrowserSettings = {
|
||||
* Default: false.
|
||||
*/
|
||||
allowHostControl?: boolean;
|
||||
/**
|
||||
* Allowlist of exact control URLs for target="custom".
|
||||
* When set, any custom controlUrl must match this list.
|
||||
*/
|
||||
allowedControlUrls?: string[];
|
||||
/**
|
||||
* Allowlist of hostnames for control URLs (hostname only, no ports).
|
||||
* When set, controlUrl hostname must match.
|
||||
*/
|
||||
allowedControlHosts?: string[];
|
||||
/**
|
||||
* Allowlist of ports for control URLs.
|
||||
* When set, controlUrl port must match (defaults: http=80, https=443).
|
||||
*/
|
||||
allowedControlPorts?: number[];
|
||||
/**
|
||||
* When true (default), sandboxed browser control will try to start/reattach to
|
||||
* the sandbox browser container when a tool call needs it.
|
||||
|
||||
@@ -746,6 +746,9 @@ const SandboxBrowserSchema = z
|
||||
headless: z.boolean().optional(),
|
||||
enableNoVnc: z.boolean().optional(),
|
||||
allowHostControl: z.boolean().optional(),
|
||||
allowedControlUrls: z.array(z.string()).optional(),
|
||||
allowedControlHosts: z.array(z.string()).optional(),
|
||||
allowedControlPorts: z.array(z.number().int().positive()).optional(),
|
||||
autoStart: z.boolean().optional(),
|
||||
autoStartTimeoutMs: z.number().int().positive().optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user