feat: add browser use runtime setup settings

This commit is contained in:
Simos Mikelatos
2026-06-15 17:52:27 +00:00
parent 828d1a2302
commit 260070bae0
11 changed files with 450 additions and 54 deletions

View File

@@ -22,8 +22,54 @@ function readParam(value: string | string[] | undefined): string {
return Array.isArray(value) ? value[0] || '' : value || '';
}
router.get('/status', (_req, res) => {
res.json({ success: true, data: browserUseService.getStatus() });
router.get('/status', async (_req, res) => {
try {
res.json({ success: true, data: await browserUseService.getStatus() });
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to load Browser Use status.',
});
}
});
router.get('/settings', async (_req, res) => {
try {
res.json({ success: true, data: { settings: await browserUseService.getSettings() } });
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to load Browser Use settings.',
});
}
});
router.put('/settings', async (req, res) => {
try {
const settings = await browserUseService.updateSettings(req.body || {});
res.json({ success: true, data: { settings } });
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to save Browser Use settings.',
});
}
});
router.post('/runtime/install', async (_req, res) => {
try {
const result = await browserUseService.installRuntime();
res.status(result.success ? 200 : 500).json({
success: result.success,
data: result,
error: result.success ? undefined : result.message,
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to install Browser Use runtime.',
});
}
});
router.get('/sessions', async (req: AuthenticatedRequest, res) => {

View File

@@ -1,13 +1,19 @@
import { createRequire } from 'node:module';
import { randomUUID } from 'node:crypto';
import { spawn } from 'node:child_process';
import dns from 'node:dns/promises';
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
const require = createRequire(import.meta.url);
const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true';
const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10);
const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10);
const ALLOW_PRIVATE_NETWORKS = process.env.CLOUDCLI_BROWSER_USE_ALLOW_PRIVATE_NETWORKS === '1';
const SETTINGS_PATH = path.join(os.homedir(), '.cloudcli', 'browser-use-settings.json');
type BrowserUseRuntime = 'cloud' | 'local';
type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable';
@@ -37,23 +43,71 @@ type BrowserUseOwner = {
id: string | number;
};
type BrowserUseSettings = {
enabled: boolean;
};
type RuntimeReadiness = {
playwright: any | null;
playwrightInstalled: boolean;
chromiumInstalled: boolean;
chromiumExecutablePath: string | null;
installInProgress: boolean;
installMessage: string | null;
};
const sessions = new Map<string, BrowserUseSession>();
const handles = new Map<string, RuntimeHandle>();
let installPromise: Promise<{ success: boolean; message: string }> | null = null;
let lastInstallMessage: string | null = null;
const DEFAULT_SETTINGS: BrowserUseSettings = {
enabled: true,
};
function getRuntime(): BrowserUseRuntime {
return IS_PLATFORM ? 'cloud' : 'local';
}
function isBrowserUseEnabled(): boolean {
return process.env.CLOUDCLI_BROWSER_USE_ENABLED === '1';
async function readSettings(): Promise<BrowserUseSettings> {
try {
const raw = await fsPromises.readFile(SETTINGS_PATH, 'utf8');
const parsed = JSON.parse(raw) as Partial<BrowserUseSettings>;
return {
enabled: parsed.enabled !== false,
};
} catch (error: any) {
if (error?.code !== 'ENOENT') {
console.warn('[Browser Use] Failed to read settings:', error?.message || error);
}
return DEFAULT_SETTINGS;
}
}
function getSetupMessage(): string {
if (!isBrowserUseEnabled()) {
return 'Browser Use is disabled. Set CLOUDCLI_BROWSER_USE_ENABLED=1 after provisioning a Playwright/Chromium runtime.';
async function writeSettings(settings: BrowserUseSettings): Promise<BrowserUseSettings> {
const normalized = {
enabled: settings.enabled !== false,
};
await fsPromises.mkdir(path.dirname(SETTINGS_PATH), { recursive: true });
await fsPromises.writeFile(SETTINGS_PATH, JSON.stringify(normalized, null, 2), 'utf8');
return normalized;
}
function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string {
if (!settings.enabled) {
return 'Browser Use is disabled in settings.';
}
return 'Playwright is not available in this runtime. Install/provision Playwright or point CloudCLI at a managed browser worker.';
if (!readiness.playwrightInstalled) {
return 'Install Playwright and Chromium to use browser sessions.';
}
if (!readiness.chromiumInstalled) {
return 'Playwright is installed, but Chromium is missing. Install the Chromium runtime to continue.';
}
return readiness.installMessage || 'Browser Use runtime is not ready.';
}
function getPlaywright(): any | null {
@@ -64,6 +118,85 @@ function getPlaywright(): any | null {
}
}
function getRuntimeReadiness(): RuntimeReadiness {
const playwright = getPlaywright();
const readiness: RuntimeReadiness = {
playwright,
playwrightInstalled: Boolean(playwright),
chromiumInstalled: false,
chromiumExecutablePath: null,
installInProgress: Boolean(installPromise),
installMessage: lastInstallMessage,
};
if (!playwright) {
return readiness;
}
try {
const executablePath = playwright.chromium.executablePath();
readiness.chromiumExecutablePath = executablePath;
readiness.chromiumInstalled = Boolean(executablePath && fs.existsSync(executablePath));
} catch {
readiness.chromiumInstalled = false;
}
return readiness;
}
function runCommand(command: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: process.cwd(),
env: process.env,
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
});
const output: string[] = [];
child.stdout.on('data', (chunk) => output.push(String(chunk)));
child.stderr.on('data', (chunk) => output.push(String(chunk)));
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`));
});
});
}
async function installRuntime(): Promise<{ success: boolean; message: string }> {
if (installPromise) {
return installPromise;
}
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
installPromise = (async () => {
try {
lastInstallMessage = 'Installing Playwright package...';
await runCommand(npmCommand, ['install', '--no-save', '--no-package-lock', 'playwright']);
lastInstallMessage = 'Installing Chromium runtime...';
await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']);
lastInstallMessage = 'Browser Use runtime installed.';
return { success: true, message: lastInstallMessage };
} catch (error) {
lastInstallMessage = error instanceof Error ? error.message : 'Failed to install Browser Use runtime.';
return { success: false, message: lastInstallMessage };
}
})();
try {
return await installPromise;
} finally {
installPromise = null;
}
}
function getOwnerId(owner: BrowserUseOwner): string {
if (owner.id === undefined || owner.id === null || String(owner.id).trim() === '') {
throw new Error('Authenticated user is required.');
@@ -218,19 +351,43 @@ async function captureSession(session: BrowserUseSession, page: any): Promise<vo
}
export const browserUseService = {
getStatus() {
const playwright = getPlaywright();
const enabled = isBrowserUseEnabled() && Boolean(playwright);
async getSettings() {
return readSettings();
},
async updateSettings(settings: Partial<BrowserUseSettings>) {
const current = await readSettings();
return writeSettings({
...current,
enabled: settings.enabled ?? current.enabled,
});
},
async getStatus() {
const settings = await readSettings();
const readiness = getRuntimeReadiness();
const available = settings.enabled && readiness.playwrightInstalled && readiness.chromiumInstalled;
return {
enabled,
enabled: settings.enabled,
runtime: getRuntime(),
available: enabled,
available,
playwrightInstalled: readiness.playwrightInstalled,
chromiumInstalled: readiness.chromiumInstalled,
installInProgress: readiness.installInProgress,
sessionCount: sessions.size,
mcpRecommended: true,
message: enabled
message: available
? 'Browser Use runtime is available.'
: getSetupMessage(),
: getSetupMessage(settings, readiness),
};
},
async installRuntime() {
const result = await installRuntime();
return {
...result,
status: await this.getStatus(),
};
},
@@ -264,14 +421,15 @@ export const browserUseService = {
throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active sessions per user.`);
}
const playwright = getPlaywright();
if (!isBrowserUseEnabled() || !playwright) {
session.message = getSetupMessage();
const settings = await readSettings();
const readiness = getRuntimeReadiness();
if (!settings.enabled || !readiness.playwrightInstalled || !readiness.chromiumInstalled || !readiness.playwright) {
session.message = getSetupMessage(settings, readiness);
sessions.set(session.id, session);
return publicSession(session);
}
const browser = await playwright.chromium.launch({
const browser = await readiness.playwright.chromium.launch({
headless: true,
args: ['--disable-dev-shm-usage'],
});

View File

@@ -15,27 +15,16 @@ test('browser use blocks private and local network addresses by default', () =>
});
test('browser use sessions are listed only for their owner', async () => {
const originalEnabled = process.env.CLOUDCLI_BROWSER_USE_ENABLED;
process.env.CLOUDCLI_BROWSER_USE_ENABLED = '0';
const ownerA = { id: `owner-a-${Date.now()}-${Math.random()}` };
const ownerB = { id: `owner-b-${Date.now()}-${Math.random()}` };
try {
const ownerASession = await browserUseService.createSession(ownerA);
await browserUseService.createSession(ownerB);
const ownerASession = await browserUseService.createSession(ownerA);
await browserUseService.createSession(ownerB);
const ownerASessions = await browserUseService.listSessions(ownerA);
const ownerBSessions = await browserUseService.listSessions(ownerB);
const ownerASessions = await browserUseService.listSessions(ownerA);
const ownerBSessions = await browserUseService.listSessions(ownerB);
assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true);
assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false);
assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false);
} finally {
if (originalEnabled === undefined) {
delete process.env.CLOUDCLI_BROWSER_USE_ENABLED;
} else {
process.env.CLOUDCLI_BROWSER_USE_ENABLED = originalEnabled;
}
}
assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true);
assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false);
assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false);
});