Added anchorbrowser remote browser support

This commit is contained in:
guybensimhon1 2026-01-27 09:36:46 +02:00
parent 9daa846457
commit a2f0229640
No known key found for this signature in database
GPG Key ID: E8719764258ADCC8
9 changed files with 884 additions and 4 deletions

View File

@ -1,3 +1,7 @@
export {
cleanupAnchorBrowserSession,
cleanupAllAnchorBrowserSessions,
} from "./sandbox/anchorbrowser-session.js";
export {
resolveSandboxBrowserConfig,
resolveSandboxConfigForAgent,
@ -21,6 +25,7 @@ export {
type SandboxBrowserInfo,
type SandboxContainerInfo,
} from "./sandbox/manage.js";
export { cleanupSandboxBrowserForScope } from "./sandbox/prune.js";
export {
formatSandboxToolPolicyBlockedMessage,
resolveSandboxRuntimeStatus,
@ -29,8 +34,10 @@ export {
export { resolveSandboxToolPolicyForAgent } from "./sandbox/tool-policy.js";
export type {
AnchorBrowserSettings,
SandboxBrowserConfig,
SandboxBrowserContext,
SandboxBrowserProvider,
SandboxConfig,
SandboxContext,
SandboxDockerConfig,

View File

@ -0,0 +1,238 @@
/**
* Anchorbrowser session management for sandbox browser contexts.
*
* Manages the lifecycle of Anchorbrowser sessions:
* - Creates sessions on-demand when the browser tool is first used
* - Tracks active sessions for reuse within the same scope
* - Cleans up sessions when the agent session ends
*/
import type { BrowserBridge } from "../../browser/bridge-server.js";
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js";
import type { ResolvedBrowserConfig } from "../../browser/config.js";
import {
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_CLAWD_BROWSER_COLOR,
} from "../../browser/constants.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
createAnchorBrowserSession,
endAnchorBrowserSession,
type AnchorBrowserSession,
} from "./anchorbrowser.js";
import type { SandboxBrowserConfig, SandboxBrowserContext } from "./types.js";
const log = createSubsystemLogger("sandbox").child("anchorbrowser");
// ---------------------------------------------------------------------------
// Session tracking
// ---------------------------------------------------------------------------
type AnchorSessionEntry = {
session: AnchorBrowserSession;
bridge: BrowserBridge;
apiKey: string;
apiUrl?: string;
};
/** Map of scopeKey -> active Anchorbrowser session entry. */
const ANCHOR_SESSIONS = new Map<string, AnchorSessionEntry>();
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Resolve the Anchorbrowser API key from config or environment.
*/
function resolveAnchorApiKey(browserCfg: SandboxBrowserConfig): string {
const apiKey =
browserCfg.anchorbrowser?.apiKey?.trim() || process.env.ANCHORBROWSER_API_KEY?.trim();
if (!apiKey) {
throw new Error(
"Anchorbrowser API key not configured. " +
"Set agents.defaults.sandbox.browser.anchorbrowser.apiKey in config " +
"or ANCHORBROWSER_API_KEY environment variable.",
);
}
return apiKey;
}
/**
* Build a ResolvedBrowserConfig for a remote CDP endpoint.
*/
function buildRemoteBrowserConfig(params: {
cdpUrl: string;
evaluateEnabled: boolean;
}): ResolvedBrowserConfig {
const parsed = new URL(params.cdpUrl);
const cdpHost = parsed.hostname;
const cdpProtocol = parsed.protocol === "https:" ? "https" : "http";
const isLoopback =
cdpHost === "localhost" ||
cdpHost === "127.0.0.1" ||
cdpHost === "0.0.0.0" ||
cdpHost === "::1";
return {
enabled: true,
evaluateEnabled: params.evaluateEnabled,
controlPort: 0, // Will be assigned by the bridge server
cdpProtocol,
cdpHost,
cdpIsLoopback: isLoopback,
remoteCdpTimeoutMs: 5000, // Longer timeout for remote
remoteCdpHandshakeTimeoutMs: 10000,
color: DEFAULT_CLAWD_BROWSER_COLOR,
executablePath: undefined,
headless: false, // Anchorbrowser manages this
noSandbox: false,
attachOnly: true, // Never launch locally
defaultProfile: "remote",
profiles: {
remote: {
cdpUrl: params.cdpUrl,
color: DEFAULT_CLAWD_BROWSER_COLOR,
},
},
};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Ensure an Anchorbrowser session exists for the given scope.
*
* If a session already exists for this scope, reuses it.
* Otherwise, creates a new session via the Anchorbrowser API.
*/
export async function ensureAnchorBrowser(params: {
scopeKey: string;
cfg: SandboxBrowserConfig;
evaluateEnabled?: boolean;
}): Promise<SandboxBrowserContext | null> {
// Check for existing session
const existing = ANCHOR_SESSIONS.get(params.scopeKey);
if (existing) {
log.debug(`Reusing existing Anchorbrowser session for scope ${params.scopeKey}`);
return {
bridgeUrl: existing.bridge.baseUrl,
liveViewUrl: existing.session.liveViewUrl,
sessionId: existing.session.id,
};
}
// Resolve API key
const apiKey = resolveAnchorApiKey(params.cfg);
const apiUrl = params.cfg.anchorbrowser?.apiUrl;
log.info(`Creating new Anchorbrowser session for scope ${params.scopeKey}`);
// Create new session via API
const session = await createAnchorBrowserSession({
apiKey,
apiUrl,
headless: params.cfg.anchorbrowser?.headless ?? params.cfg.headless,
viewport: params.cfg.anchorbrowser?.viewport,
proxy: params.cfg.anchorbrowser?.proxy,
captchaSolver: params.cfg.anchorbrowser?.captchaSolver,
adblock: params.cfg.anchorbrowser?.adblock,
popupBlocker: params.cfg.anchorbrowser?.popupBlocker,
timeout: params.cfg.anchorbrowser?.timeout,
recording: params.cfg.anchorbrowser?.recording,
extraStealth: params.cfg.anchorbrowser?.extraStealth,
});
log.info(`Anchorbrowser session created: ${session.id}`);
if (session.liveViewUrl) {
log.info(`Live view URL: ${session.liveViewUrl}`);
}
// Start local bridge server pointing at the remote CDP
const evaluateEnabled = params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED;
const bridge = await startBrowserBridgeServer({
resolved: buildRemoteBrowserConfig({
cdpUrl: session.cdpUrl,
evaluateEnabled,
}),
});
// Track the session
ANCHOR_SESSIONS.set(params.scopeKey, {
session,
bridge,
apiKey,
apiUrl,
});
return {
bridgeUrl: bridge.baseUrl,
liveViewUrl: session.liveViewUrl,
sessionId: session.id,
};
}
/**
* Clean up an Anchorbrowser session for the given scope.
*
* Stops the local bridge server and ends the remote session via API.
*/
export async function cleanupAnchorBrowserSession(scopeKey: string): Promise<void> {
const entry = ANCHOR_SESSIONS.get(scopeKey);
if (!entry) return;
log.info(`Cleaning up Anchorbrowser session for scope ${scopeKey}`);
// Stop the local bridge server
try {
await stopBrowserBridgeServer(entry.bridge.server);
} catch (err) {
log.warn(`Failed to stop bridge server: ${err instanceof Error ? err.message : String(err)}`);
}
// End the remote session
try {
await endAnchorBrowserSession({
apiKey: entry.apiKey,
apiUrl: entry.apiUrl,
sessionId: entry.session.id,
});
log.info(`Anchorbrowser session ${entry.session.id} ended`);
} catch (err) {
log.warn(
`Failed to end Anchorbrowser session: ${err instanceof Error ? err.message : String(err)}`,
);
}
ANCHOR_SESSIONS.delete(scopeKey);
}
/**
* Clean up all active Anchorbrowser sessions.
*
* Called during gateway shutdown.
*/
export async function cleanupAllAnchorBrowserSessions(): Promise<void> {
const scopeKeys = Array.from(ANCHOR_SESSIONS.keys());
for (const scopeKey of scopeKeys) {
await cleanupAnchorBrowserSession(scopeKey);
}
}
/**
* Check if an Anchorbrowser session exists for the given scope.
*/
export function hasAnchorBrowserSession(scopeKey: string): boolean {
return ANCHOR_SESSIONS.has(scopeKey);
}
/**
* Get the Anchorbrowser session entry for a scope (if any).
*/
export function getAnchorBrowserSessionEntry(scopeKey: string): AnchorSessionEntry | undefined {
return ANCHOR_SESSIONS.get(scopeKey);
}

View File

@ -0,0 +1,264 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
createAnchorBrowserSession,
endAnchorBrowserSession,
getAnchorBrowserSession,
} from "./anchorbrowser.js";
describe("anchorbrowser client", () => {
const mockFetch = vi.fn();
const originalFetch = global.fetch;
beforeEach(() => {
global.fetch = mockFetch;
mockFetch.mockReset();
});
afterEach(() => {
global.fetch = originalFetch;
});
describe("createAnchorBrowserSession", () => {
it("creates a session with minimal params", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: {
id: "session-123",
cdp_url: "wss://browser.anchorbrowser.io/session-123",
live_view_url: "https://app.anchorbrowser.io/live/session-123",
},
}),
});
const session = await createAnchorBrowserSession({
apiKey: "test-api-key",
});
expect(session).toEqual({
id: "session-123",
cdpUrl: "wss://browser.anchorbrowser.io/session-123",
liveViewUrl: "https://app.anchorbrowser.io/live/session-123",
});
expect(mockFetch).toHaveBeenCalledWith(
"https://api.anchorbrowser.io/v1/sessions",
expect.objectContaining({
method: "POST",
headers: {
"anchor-api-key": "test-api-key",
"Content-Type": "application/json",
},
}),
);
});
it("creates a session with all params", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: {
id: "session-456",
cdp_url: "wss://browser.anchorbrowser.io/session-456",
},
}),
});
const session = await createAnchorBrowserSession({
apiKey: "test-api-key",
apiUrl: "https://custom.api.io/v1",
headless: true,
viewport: { width: 1920, height: 1080 },
proxy: { active: true, type: "anchor_residential", countryCode: "de" },
captchaSolver: true,
adblock: false,
timeout: { maxDuration: 30, idleTimeout: 10 },
});
expect(session.id).toBe("session-456");
expect(mockFetch).toHaveBeenCalledWith(
"https://custom.api.io/v1/sessions",
expect.objectContaining({
method: "POST",
body: expect.any(String),
}),
);
// Parse the body to verify structure
const call = mockFetch.mock.calls[0];
const body = JSON.parse(call[1].body as string);
expect(body.browser.headless).toEqual({ active: true });
expect(body.browser.viewport).toEqual({ width: 1920, height: 1080 });
expect(body.browser.captcha_solver).toEqual({ active: true });
expect(body.browser.adblock).toEqual({ active: false });
expect(body.session.proxy).toEqual({
active: true,
type: "anchor_residential",
country_code: "de",
});
expect(body.session.timeout).toEqual({
max_duration: 30,
idle_timeout: 10,
});
});
it("throws on API error", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
json: async () => ({
error: "Invalid API key",
}),
});
await expect(createAnchorBrowserSession({ apiKey: "invalid-key" })).rejects.toThrow(
"Anchorbrowser API error: Invalid API key",
);
});
it("throws on non-JSON error response", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
json: async () => {
throw new Error("Not JSON");
},
});
await expect(createAnchorBrowserSession({ apiKey: "test-key" })).rejects.toThrow(
"Anchorbrowser API error: 500 Internal Server Error",
);
});
});
describe("endAnchorBrowserSession", () => {
it("ends a session successfully", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
});
await endAnchorBrowserSession({
apiKey: "test-api-key",
sessionId: "session-123",
});
expect(mockFetch).toHaveBeenCalledWith(
"https://api.anchorbrowser.io/v1/sessions/session-123",
expect.objectContaining({
method: "DELETE",
headers: {
"anchor-api-key": "test-api-key",
"Content-Type": "application/json",
},
}),
);
});
it("accepts 404 (session already ended)", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
// Should not throw
await endAnchorBrowserSession({
apiKey: "test-api-key",
sessionId: "session-123",
});
});
it("throws on other errors", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
json: async () => ({ message: "Server error" }),
});
await expect(
endAnchorBrowserSession({
apiKey: "test-api-key",
sessionId: "session-123",
}),
).rejects.toThrow("Anchorbrowser API error: Server error");
});
it("uses custom API URL", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
});
await endAnchorBrowserSession({
apiKey: "test-api-key",
apiUrl: "https://custom.api.io/v1",
sessionId: "session-123",
});
expect(mockFetch).toHaveBeenCalledWith(
"https://custom.api.io/v1/sessions/session-123",
expect.anything(),
);
});
});
describe("getAnchorBrowserSession", () => {
it("returns session info", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: {
id: "session-123",
cdp_url: "wss://browser.anchorbrowser.io/session-123",
live_view_url: "https://app.anchorbrowser.io/live/session-123",
},
}),
});
const session = await getAnchorBrowserSession({
apiKey: "test-api-key",
sessionId: "session-123",
});
expect(session).toEqual({
id: "session-123",
cdpUrl: "wss://browser.anchorbrowser.io/session-123",
liveViewUrl: "https://app.anchorbrowser.io/live/session-123",
});
});
it("returns null for 404", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
const session = await getAnchorBrowserSession({
apiKey: "test-api-key",
sessionId: "nonexistent",
});
expect(session).toBeNull();
});
it("throws on other errors", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
json: async () => ({}),
});
await expect(
getAnchorBrowserSession({
apiKey: "test-api-key",
sessionId: "session-123",
}),
).rejects.toThrow("Anchorbrowser API error: 500 Internal Server Error");
});
});
});

View File

@ -0,0 +1,234 @@
/**
* Anchorbrowser API client for creating and managing remote browser sessions.
*
* @see https://docs.anchorbrowser.io/api-reference/browser-sessions/start-browser-session
*/
import type { AnchorBrowserSettings } from "./types.js";
const DEFAULT_API_URL = "https://api.anchorbrowser.io/v1";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type AnchorBrowserSession = {
id: string;
cdpUrl: string;
liveViewUrl?: string;
};
export type AnchorBrowserCreateParams = AnchorBrowserSettings & {
/** API key (required). */
apiKey: string;
};
type AnchorApiSessionResponse = {
data: {
id: string;
cdp_url: string;
live_view_url?: string;
};
};
type AnchorApiErrorResponse = {
error?: string;
message?: string;
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function resolveApiUrl(settings?: AnchorBrowserSettings): string {
return settings?.apiUrl?.trim() || DEFAULT_API_URL;
}
function buildHeaders(apiKey: string): Record<string, string> {
return {
"anchor-api-key": apiKey,
"Content-Type": "application/json",
};
}
/**
* Build the request body for creating an Anchorbrowser session.
*/
function buildCreateSessionBody(params: AnchorBrowserCreateParams): Record<string, unknown> {
const body: Record<string, unknown> = {};
// Session config
const session: Record<string, unknown> = {};
if (params.recording !== undefined) {
session.recording = { active: params.recording };
}
if (params.proxy) {
session.proxy = {
active: params.proxy.active ?? false,
...(params.proxy.type && { type: params.proxy.type }),
...(params.proxy.countryCode && { country_code: params.proxy.countryCode }),
...(params.proxy.region && { region: params.proxy.region }),
...(params.proxy.city && { city: params.proxy.city }),
};
}
if (params.timeout) {
session.timeout = {
...(params.timeout.maxDuration !== undefined && { max_duration: params.timeout.maxDuration }),
...(params.timeout.idleTimeout !== undefined && { idle_timeout: params.timeout.idleTimeout }),
};
}
if (Object.keys(session).length > 0) {
body.session = session;
}
// Browser config
const browser: Record<string, unknown> = {};
if (params.adblock !== undefined) {
browser.adblock = { active: params.adblock };
}
if (params.popupBlocker !== undefined) {
browser.popup_blocker = { active: params.popupBlocker };
}
if (params.captchaSolver !== undefined) {
browser.captcha_solver = { active: params.captchaSolver };
}
if (params.headless !== undefined) {
browser.headless = { active: params.headless };
}
if (params.viewport) {
browser.viewport = {
width: params.viewport.width,
height: params.viewport.height,
};
}
if (params.extraStealth !== undefined) {
browser.extra_stealth = { active: params.extraStealth };
}
if (Object.keys(browser).length > 0) {
body.browser = browser;
}
return body;
}
// ---------------------------------------------------------------------------
// API Functions
// ---------------------------------------------------------------------------
/**
* Create a new Anchorbrowser session.
*
* @throws Error if the API call fails or returns an error.
*/
export async function createAnchorBrowserSession(
params: AnchorBrowserCreateParams,
): Promise<AnchorBrowserSession> {
const apiUrl = resolveApiUrl(params);
const url = `${apiUrl}/sessions`;
const body = buildCreateSessionBody(params);
const response = await fetch(url, {
method: "POST",
headers: buildHeaders(params.apiKey),
body: JSON.stringify(body),
});
if (!response.ok) {
let errorMessage = `Anchorbrowser API error: ${response.status} ${response.statusText}`;
try {
const errorData = (await response.json()) as AnchorApiErrorResponse;
if (errorData.error || errorData.message) {
errorMessage = `Anchorbrowser API error: ${errorData.error || errorData.message}`;
}
} catch {
// Ignore JSON parse errors
}
throw new Error(errorMessage);
}
const data = (await response.json()) as AnchorApiSessionResponse;
return {
id: data.data.id,
cdpUrl: data.data.cdp_url,
liveViewUrl: data.data.live_view_url,
};
}
/**
* End an Anchorbrowser session.
*
* @throws Error if the API call fails.
*/
export async function endAnchorBrowserSession(params: {
apiKey: string;
apiUrl?: string;
sessionId: string;
}): Promise<void> {
const apiUrl = params.apiUrl?.trim() || DEFAULT_API_URL;
const url = `${apiUrl}/sessions/${encodeURIComponent(params.sessionId)}`;
const response = await fetch(url, {
method: "DELETE",
headers: buildHeaders(params.apiKey),
});
// 404 is acceptable - session may have already ended
if (!response.ok && response.status !== 404) {
let errorMessage = `Anchorbrowser API error: ${response.status} ${response.statusText}`;
try {
const errorData = (await response.json()) as AnchorApiErrorResponse;
if (errorData.error || errorData.message) {
errorMessage = `Anchorbrowser API error: ${errorData.error || errorData.message}`;
}
} catch {
// Ignore JSON parse errors
}
throw new Error(errorMessage);
}
}
/**
* Get the status of an Anchorbrowser session.
*
* @returns The session info, or null if the session doesn't exist.
*/
export async function getAnchorBrowserSession(params: {
apiKey: string;
apiUrl?: string;
sessionId: string;
}): Promise<AnchorBrowserSession | null> {
const apiUrl = params.apiUrl?.trim() || DEFAULT_API_URL;
const url = `${apiUrl}/sessions/${encodeURIComponent(params.sessionId)}`;
const response = await fetch(url, {
method: "GET",
headers: buildHeaders(params.apiKey),
});
if (response.status === 404) {
return null;
}
if (!response.ok) {
let errorMessage = `Anchorbrowser API error: ${response.status} ${response.statusText}`;
try {
const errorData = (await response.json()) as AnchorApiErrorResponse;
if (errorData.error || errorData.message) {
errorMessage = `Anchorbrowser API error: ${errorData.error || errorData.message}`;
}
} catch {
// Ignore JSON parse errors
}
throw new Error(errorMessage);
}
const data = (await response.json()) as AnchorApiSessionResponse;
return {
id: data.data.id,
cdpUrl: data.data.cdp_url,
liveViewUrl: data.data.live_view_url,
};
}

View File

@ -4,6 +4,7 @@ import {
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_CLAWD_BROWSER_COLOR,
} from "../../browser/constants.js";
import { ensureAnchorBrowser } from "./anchorbrowser-session.js";
import { BROWSER_BRIDGES } from "./browser-bridges.js";
import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
import {
@ -86,6 +87,17 @@ export async function ensureSandboxBrowser(params: {
if (!params.cfg.browser.enabled) return null;
if (!isToolAllowed(params.cfg.tools, "browser")) return null;
// Branch based on provider
const provider = params.cfg.browser.provider ?? "docker";
if (provider === "anchorbrowser") {
return ensureAnchorBrowser({
scopeKey: params.scopeKey,
cfg: params.cfg.browser,
evaluateEnabled: params.evaluateEnabled,
});
}
// Docker provider (default)
const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey);
const name = `${params.cfg.browser.containerPrefix}${slug}`;
const containerName = name.slice(0, 63);

View File

@ -86,8 +86,32 @@ export function resolveSandboxBrowserConfig(params: {
}): SandboxBrowserConfig {
const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser;
const globalBrowser = params.globalBrowser;
// Merge Anchorbrowser settings
const globalAnchor = globalBrowser?.anchorbrowser;
const agentAnchor = agentBrowser?.anchorbrowser;
const anchorbrowser =
globalAnchor || agentAnchor
? {
apiKey: agentAnchor?.apiKey ?? globalAnchor?.apiKey ?? process.env.ANCHORBROWSER_API_KEY,
apiUrl: agentAnchor?.apiUrl ?? globalAnchor?.apiUrl,
proxy: agentAnchor?.proxy ?? globalAnchor?.proxy,
timeout: agentAnchor?.timeout ?? globalAnchor?.timeout,
captchaSolver: agentAnchor?.captchaSolver ?? globalAnchor?.captchaSolver,
adblock: agentAnchor?.adblock ?? globalAnchor?.adblock,
popupBlocker: agentAnchor?.popupBlocker ?? globalAnchor?.popupBlocker,
headless: agentAnchor?.headless ?? globalAnchor?.headless,
viewport: agentAnchor?.viewport ?? globalAnchor?.viewport,
recording: agentAnchor?.recording ?? globalAnchor?.recording,
extraStealth: agentAnchor?.extraStealth ?? globalAnchor?.extraStealth,
}
: undefined;
return {
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
provider: agentBrowser?.provider ?? globalBrowser?.provider ?? "docker",
// Docker-specific settings
image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE,
containerPrefix:
agentBrowser?.containerPrefix ??
@ -97,8 +121,13 @@ export function resolveSandboxBrowserConfig(params: {
vncPort: agentBrowser?.vncPort ?? globalBrowser?.vncPort ?? DEFAULT_SANDBOX_BROWSER_VNC_PORT,
noVncPort:
agentBrowser?.noVncPort ?? globalBrowser?.noVncPort ?? DEFAULT_SANDBOX_BROWSER_NOVNC_PORT,
headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false,
enableNoVnc: agentBrowser?.enableNoVnc ?? globalBrowser?.enableNoVnc ?? true,
// Anchorbrowser-specific settings
anchorbrowser,
// Common settings
headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false,
allowHostControl: agentBrowser?.allowHostControl ?? globalBrowser?.allowHostControl ?? false,
autoStart: agentBrowser?.autoStart ?? globalBrowser?.autoStart ?? true,
autoStartTimeoutMs:

View File

@ -1,5 +1,6 @@
import { stopBrowserBridgeServer } from "../../browser/bridge-server.js";
import { defaultRuntime } from "../../runtime.js";
import { cleanupAnchorBrowserSession } from "./anchorbrowser-session.js";
import { BROWSER_BRIDGES } from "./browser-bridges.js";
import { dockerContainerState, execDocker } from "./docker.js";
import {
@ -93,3 +94,21 @@ export async function ensureDockerContainerIsRunning(containerName: string) {
await execDocker(["start", containerName]);
}
}
/**
* Clean up sandbox browser resources for a specific session scope.
*
* For Docker-based browsers: stops the bridge server.
* For Anchorbrowser sessions: ends the remote session via API.
*/
export async function cleanupSandboxBrowserForScope(scopeKey: string): Promise<void> {
// Clean up Docker-based browser bridge
const bridge = BROWSER_BRIDGES.get(scopeKey);
if (bridge) {
await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined);
BROWSER_BRIDGES.delete(scopeKey);
}
// Clean up Anchorbrowser session
await cleanupAnchorBrowserSession(scopeKey);
}

View File

@ -1,6 +1,8 @@
import type { SandboxDockerConfig } from "./types.docker.js";
import type { AnchorBrowserSettings, SandboxBrowserProvider } from "../../config/types.sandbox.js";
export type { SandboxDockerConfig } from "./types.docker.js";
export type { AnchorBrowserSettings, SandboxBrowserProvider } from "../../config/types.sandbox.js";
export type SandboxToolPolicy = {
allow?: string[];
@ -29,13 +31,22 @@ export type SandboxWorkspaceAccess = "none" | "ro" | "rw";
export type SandboxBrowserConfig = {
enabled: boolean;
/** Browser provider: "docker" (default) or "anchorbrowser". */
provider: SandboxBrowserProvider;
// Docker-specific settings
image: string;
containerPrefix: string;
cdpPort: number;
vncPort: number;
noVncPort: number;
headless: boolean;
enableNoVnc: boolean;
// Anchorbrowser-specific settings
anchorbrowser?: AnchorBrowserSettings;
// Common settings
headless: boolean;
allowHostControl: boolean;
autoStart: boolean;
autoStartTimeoutMs: number;
@ -61,8 +72,14 @@ export type SandboxConfig = {
export type SandboxBrowserContext = {
bridgeUrl: string;
/** NoVNC URL for Docker-based browsers. */
noVncUrl?: string;
containerName: string;
/** Live view URL for Anchorbrowser sessions. */
liveViewUrl?: string;
/** Docker container name (Docker provider only). */
containerName?: string;
/** Anchorbrowser session ID (Anchorbrowser provider only). */
sessionId?: string;
};
export type SandboxContext = {

View File

@ -44,15 +44,75 @@ export type SandboxDockerSettings = {
binds?: string[];
};
/** Browser provider for sandbox sessions. */
export type SandboxBrowserProvider = "docker" | "anchorbrowser";
/** Anchorbrowser proxy configuration. */
export type AnchorBrowserProxySettings = {
/** Enable proxy. */
active?: boolean;
/** Proxy type (anchor_proxy, anchor_residential, anchor_mobile, anchor_gov). */
type?: "anchor_proxy" | "anchor_residential" | "anchor_mobile" | "anchor_gov";
/** Country code (ISO 2 lowercase, e.g. "us"). */
countryCode?: string;
/** Region code for geographic targeting. */
region?: string;
/** City name for precise targeting (requires region). */
city?: string;
};
/** Anchorbrowser timeout configuration. */
export type AnchorBrowserTimeoutSettings = {
/** Max session duration in minutes (default: 20). */
maxDuration?: number;
/** Idle timeout in minutes before session stops (default: 5). */
idleTimeout?: number;
};
/** Anchorbrowser-specific settings. */
export type AnchorBrowserSettings = {
/** API key (prefer env var ANCHORBROWSER_API_KEY). */
apiKey?: string;
/** Override API URL (default: https://api.anchorbrowser.io/v1). */
apiUrl?: string;
/** Proxy configuration. */
proxy?: AnchorBrowserProxySettings;
/** Timeout configuration. */
timeout?: AnchorBrowserTimeoutSettings;
/** Enable captcha solving (requires proxy). */
captchaSolver?: boolean;
/** Enable ad blocking (default: true). */
adblock?: boolean;
/** Enable popup blocking (default: true). */
popupBlocker?: boolean;
/** Run browser headless (default: false). */
headless?: boolean;
/** Viewport dimensions. */
viewport?: { width: number; height: number };
/** Enable session recording (default: true). */
recording?: boolean;
/** Enable extra stealth mode (requires proxy). */
extraStealth?: boolean;
};
export type SandboxBrowserSettings = {
enabled?: boolean;
/** Browser provider: "docker" (default) or "anchorbrowser". */
provider?: SandboxBrowserProvider;
// Docker-specific settings
image?: string;
containerPrefix?: string;
cdpPort?: number;
vncPort?: number;
noVncPort?: number;
headless?: boolean;
enableNoVnc?: boolean;
// Anchorbrowser-specific settings
anchorbrowser?: AnchorBrowserSettings;
// Common settings
headless?: boolean;
/**
* Allow sandboxed sessions to target the host browser control server.
* Default: false.