feat: add browser target selection for sandboxed agents
This commit is contained in:
@@ -17,6 +17,7 @@ import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||
|
||||
export function createClawdbotTools(options?: {
|
||||
browserControlUrl?: string;
|
||||
allowHostBrowserControl?: boolean;
|
||||
agentSessionKey?: string;
|
||||
agentProvider?: GatewayMessageProvider;
|
||||
agentAccountId?: string;
|
||||
@@ -37,7 +38,10 @@ export function createClawdbotTools(options?: {
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
return [
|
||||
createBrowserTool({ defaultControlUrl: options?.browserControlUrl }),
|
||||
createBrowserTool({
|
||||
defaultControlUrl: options?.browserControlUrl,
|
||||
allowHostControl: options?.allowHostBrowserControl,
|
||||
}),
|
||||
createCanvasTool(),
|
||||
createNodesTool(),
|
||||
createCronTool(),
|
||||
|
||||
@@ -45,6 +45,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||
allow: ["bash"],
|
||||
deny: ["browser"],
|
||||
},
|
||||
browserAllowHostControl: true,
|
||||
browser: {
|
||||
controlUrl: "http://localhost:9222",
|
||||
noVncUrl: "http://localhost:6080",
|
||||
@@ -59,6 +60,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||
agentWorkspaceMount: undefined,
|
||||
browserControlUrl: "http://localhost:9222",
|
||||
browserNoVncUrl: "http://localhost:6080",
|
||||
hostBrowserAllowed: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,6 +88,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||
allow: ["bash"],
|
||||
deny: ["browser"],
|
||||
},
|
||||
browserAllowHostControl: false,
|
||||
} satisfies SandboxContext;
|
||||
|
||||
expect(
|
||||
@@ -99,6 +102,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||
workspaceDir: "/tmp/clawdbot-sandbox",
|
||||
workspaceAccess: "none",
|
||||
agentWorkspaceMount: undefined,
|
||||
hostBrowserAllowed: false,
|
||||
elevated: { allowed: true, defaultLevel: "on" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -479,6 +479,7 @@ type EmbeddedSandboxInfo = {
|
||||
agentWorkspaceMount?: string;
|
||||
browserControlUrl?: string;
|
||||
browserNoVncUrl?: string;
|
||||
hostBrowserAllowed?: boolean;
|
||||
elevated?: {
|
||||
allowed: boolean;
|
||||
defaultLevel: "on" | "off";
|
||||
@@ -570,6 +571,7 @@ export function buildEmbeddedSandboxInfo(
|
||||
sandbox.workspaceAccess === "ro" ? "/agent" : undefined,
|
||||
browserControlUrl: sandbox.browser?.controlUrl,
|
||||
browserNoVncUrl: sandbox.browser?.noVncUrl,
|
||||
hostBrowserAllowed: sandbox.browserAllowHostControl,
|
||||
...(elevatedAllowed
|
||||
? {
|
||||
elevated: {
|
||||
|
||||
@@ -240,6 +240,7 @@ describe("Agent-specific tool filtering", () => {
|
||||
allow: ["read", "write", "bash"],
|
||||
deny: [],
|
||||
},
|
||||
browserAllowHostControl: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
required?: string[];
|
||||
};
|
||||
expect(parameters.properties?.action).toBeDefined();
|
||||
expect(parameters.properties?.target).toBeDefined();
|
||||
expect(parameters.properties?.controlUrl).toBeDefined();
|
||||
expect(parameters.properties?.targetUrl).toBeDefined();
|
||||
expect(parameters.properties?.request).toBeDefined();
|
||||
@@ -326,6 +327,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
allow: ["bash"],
|
||||
deny: ["browser"],
|
||||
},
|
||||
browserAllowHostControl: false,
|
||||
};
|
||||
const tools = createClawdbotCodingTools({ sandbox });
|
||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
||||
@@ -357,6 +359,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
allow: ["read", "write", "edit"],
|
||||
deny: [],
|
||||
},
|
||||
browserAllowHostControl: false,
|
||||
};
|
||||
const tools = createClawdbotCodingTools({ sandbox });
|
||||
expect(tools.some((tool) => tool.name === "read")).toBe(true);
|
||||
|
||||
@@ -626,6 +626,9 @@ export function createClawdbotCodingTools(options?: {
|
||||
createWhatsAppLoginTool(),
|
||||
...createClawdbotTools({
|
||||
browserControlUrl: sandbox?.browser?.controlUrl,
|
||||
allowHostBrowserControl: sandbox
|
||||
? sandbox.browserAllowHostControl
|
||||
: true,
|
||||
agentSessionKey: options?.sessionKey,
|
||||
agentProvider: resolveGatewayMessageProvider(options?.messageProvider),
|
||||
agentAccountId: options?.agentAccountId,
|
||||
|
||||
@@ -186,6 +186,7 @@ describe("sandboxed workspace paths", () => {
|
||||
env: { LANG: "C.UTF-8" },
|
||||
},
|
||||
tools: { allow: [], deny: [] },
|
||||
browserAllowHostControl: false,
|
||||
};
|
||||
|
||||
const testFile = "sandbox.txt";
|
||||
|
||||
@@ -78,6 +78,7 @@ export type SandboxBrowserConfig = {
|
||||
noVncPort: number;
|
||||
headless: boolean;
|
||||
enableNoVnc: boolean;
|
||||
allowHostControl: boolean;
|
||||
autoStart: boolean;
|
||||
autoStartTimeoutMs: number;
|
||||
};
|
||||
@@ -138,6 +139,7 @@ export type SandboxContext = {
|
||||
containerWorkdir: string;
|
||||
docker: SandboxDockerConfig;
|
||||
tools: SandboxToolPolicy;
|
||||
browserAllowHostControl: boolean;
|
||||
browser?: SandboxBrowserContext;
|
||||
};
|
||||
|
||||
@@ -333,6 +335,10 @@ export function resolveSandboxBrowserConfig(params: {
|
||||
headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false,
|
||||
enableNoVnc:
|
||||
agentBrowser?.enableNoVnc ?? globalBrowser?.enableNoVnc ?? true,
|
||||
allowHostControl:
|
||||
agentBrowser?.allowHostControl ??
|
||||
globalBrowser?.allowHostControl ??
|
||||
false,
|
||||
autoStart: agentBrowser?.autoStart ?? globalBrowser?.autoStart ?? true,
|
||||
autoStartTimeoutMs:
|
||||
agentBrowser?.autoStartTimeoutMs ??
|
||||
@@ -1324,6 +1330,7 @@ export async function resolveSandboxContext(params: {
|
||||
containerWorkdir: cfg.docker.workdir,
|
||||
docker: cfg.docker,
|
||||
tools: cfg.tools,
|
||||
browserAllowHostControl: cfg.browser.allowHostControl,
|
||||
browser: browser ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
agentWorkspaceMount?: string;
|
||||
browserControlUrl?: string;
|
||||
browserNoVncUrl?: string;
|
||||
hostBrowserAllowed?: boolean;
|
||||
elevated?: {
|
||||
allowed: boolean;
|
||||
defaultLevel: "on" | "off";
|
||||
@@ -243,6 +244,11 @@ export function buildAgentSystemPrompt(params: {
|
||||
params.sandboxInfo.browserNoVncUrl
|
||||
? `Sandbox browser observer (noVNC): ${params.sandboxInfo.browserNoVncUrl}`
|
||||
: "",
|
||||
params.sandboxInfo.hostBrowserAllowed === true
|
||||
? "Host browser control: allowed."
|
||||
: params.sandboxInfo.hostBrowserAllowed === false
|
||||
? "Host browser control: blocked."
|
||||
: "",
|
||||
params.sandboxInfo.elevated?.allowed
|
||||
? "Elevated bash is available for this session."
|
||||
: "",
|
||||
|
||||
@@ -105,6 +105,13 @@ const BrowserToolSchema = Type.Object({
|
||||
Type.Literal("dialog"),
|
||||
Type.Literal("act"),
|
||||
]),
|
||||
target: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("sandbox"),
|
||||
Type.Literal("host"),
|
||||
Type.Literal("custom"),
|
||||
]),
|
||||
),
|
||||
profile: Type.Optional(Type.String()),
|
||||
controlUrl: Type.Optional(Type.String()),
|
||||
targetUrl: Type.Optional(Type.String()),
|
||||
@@ -124,20 +131,60 @@ const BrowserToolSchema = Type.Object({
|
||||
request: Type.Optional(BrowserActSchema),
|
||||
});
|
||||
|
||||
function resolveBrowserBaseUrl(controlUrl?: string) {
|
||||
function resolveBrowserBaseUrl(params: {
|
||||
target?: "sandbox" | "host" | "custom";
|
||||
controlUrl?: string;
|
||||
defaultControlUrl?: string;
|
||||
allowHostControl?: boolean;
|
||||
}) {
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser);
|
||||
if (!resolved.enabled && !controlUrl?.trim()) {
|
||||
const normalizedControlUrl = params.controlUrl?.trim() ?? "";
|
||||
const normalizedDefault = params.defaultControlUrl?.trim() ?? "";
|
||||
const target =
|
||||
params.target ??
|
||||
(normalizedControlUrl
|
||||
? "custom"
|
||||
: normalizedDefault
|
||||
? "sandbox"
|
||||
: "host");
|
||||
|
||||
if (target !== "custom" && params.target && normalizedControlUrl) {
|
||||
throw new Error(
|
||||
'controlUrl is only supported with target="custom".',
|
||||
);
|
||||
}
|
||||
|
||||
if (target === "custom") {
|
||||
if (!normalizedControlUrl) {
|
||||
throw new Error('Custom browser target requires controlUrl.');
|
||||
}
|
||||
return normalizedControlUrl.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
if (target === "sandbox") {
|
||||
if (!normalizedDefault) {
|
||||
throw new Error(
|
||||
'Sandbox browser is unavailable. Enable agents.defaults.sandbox.browser.enabled or use target="host" if allowed.',
|
||||
);
|
||||
}
|
||||
return normalizedDefault.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
if (params.allowHostControl === false) {
|
||||
throw new Error("Host browser control is disabled by sandbox policy.");
|
||||
}
|
||||
if (!resolved.enabled) {
|
||||
throw new Error(
|
||||
"Browser control is disabled. Set browser.enabled=true in ~/.clawdbot/clawdbot.json.",
|
||||
);
|
||||
}
|
||||
const url = controlUrl?.trim() ? controlUrl.trim() : resolved.controlUrl;
|
||||
return url.replace(/\/$/, "");
|
||||
return resolved.controlUrl.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export function createBrowserTool(opts?: {
|
||||
defaultControlUrl?: string;
|
||||
allowHostControl?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Browser",
|
||||
@@ -149,10 +196,18 @@ export function createBrowserTool(opts?: {
|
||||
const params = args as Record<string, unknown>;
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const controlUrl = readStringParam(params, "controlUrl");
|
||||
const target = readStringParam(params, "target") as
|
||||
| "sandbox"
|
||||
| "host"
|
||||
| "custom"
|
||||
| undefined;
|
||||
const profile = readStringParam(params, "profile");
|
||||
const baseUrl = resolveBrowserBaseUrl(
|
||||
controlUrl ?? opts?.defaultControlUrl,
|
||||
);
|
||||
const baseUrl = resolveBrowserBaseUrl({
|
||||
target,
|
||||
controlUrl,
|
||||
defaultControlUrl: opts?.defaultControlUrl,
|
||||
allowHostControl: opts?.allowHostControl,
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
case "status":
|
||||
|
||||
@@ -860,6 +860,11 @@ export type SandboxBrowserSettings = {
|
||||
noVncPort?: number;
|
||||
headless?: boolean;
|
||||
enableNoVnc?: boolean;
|
||||
/**
|
||||
* Allow sandboxed sessions to target the host browser control server.
|
||||
* Default: false.
|
||||
*/
|
||||
allowHostControl?: boolean;
|
||||
/**
|
||||
* When true (default), sandboxed browser control will try to start/reattach to
|
||||
* the sandbox browser container when a tool call needs it.
|
||||
|
||||
@@ -724,6 +724,7 @@ const SandboxBrowserSchema = z
|
||||
noVncPort: z.number().int().positive().optional(),
|
||||
headless: z.boolean().optional(),
|
||||
enableNoVnc: z.boolean().optional(),
|
||||
allowHostControl: z.boolean().optional(),
|
||||
autoStart: z.boolean().optional(),
|
||||
autoStartTimeoutMs: z.number().int().positive().optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user