From 9881e5e366ca7fd48103cd38681210c4a6970704 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Wed, 17 Jun 2026 18:19:12 +0000 Subject: [PATCH] feat(browser-use): improve mobile monitoring ux --- .../browser-use/browser-use.service.ts | 94 +------------------ .../tests/browser-use.service.test.ts | 13 +-- .../browser-use/view/BrowserUsePanel.tsx | 94 ++++++++++++------- 3 files changed, 62 insertions(+), 139 deletions(-) diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 5d14f17c..e3837b6b 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -1,10 +1,8 @@ import { createRequire } from 'node:module'; import { randomBytes, randomUUID } from 'node:crypto'; import { spawn } from 'node:child_process'; -import dns from 'node:dns/promises'; import fs from 'node:fs'; import os from 'node:os'; -import net from 'node:net'; import path from 'node:path'; import { appConfigDb } from '@/modules/database/index.js'; @@ -16,7 +14,6 @@ const __dirname = getModuleDir(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 BROWSER_USE_SETTINGS_KEY = 'browser_use_settings'; const BROWSER_USE_MCP_TOKEN_KEY = 'browser_use_mcp_token'; @@ -299,71 +296,7 @@ async function installRuntime(): Promise<{ success: boolean; message: string }> } } -function isPrivateIpv4(address: string): boolean { - const parts = address.split('.').map((part) => Number.parseInt(part, 10)); - if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) { - return true; - } - - const [first, second] = parts; - return first === 0 - || first === 10 - || first === 127 - || (first === 169 && second === 254) - || (first === 172 && second >= 16 && second <= 31) - || (first === 192 && second === 168) - || first >= 224; -} - -function isPrivateIpv6(address: string): boolean { - const normalized = address.toLowerCase(); - return normalized === '::1' - || normalized === '::' - || normalized.startsWith('fc') - || normalized.startsWith('fd') - || normalized.startsWith('fe80:') - || normalized.startsWith('::ffff:127.') - || normalized.startsWith('::ffff:10.') - || normalized.startsWith('::ffff:192.168.') - || /^::ffff:172\.(1[6-9]|2\d|3[0-1])\./.test(normalized) - || /^::ffff:169\.254\./.test(normalized); -} - -export function isBlockedBrowserUseAddress(address: string): boolean { - const version = net.isIP(address); - if (version === 4) { - return isPrivateIpv4(address); - } - if (version === 6) { - return isPrivateIpv6(address); - } - return true; -} - -async function assertPublicHttpTarget(parsedUrl: URL): Promise { - if (ALLOW_PRIVATE_NETWORKS) { - return; - } - - const hostname = parsedUrl.hostname; - if (!hostname) { - throw new Error('URL hostname is required.'); - } - - if (net.isIP(hostname)) { - if (isBlockedBrowserUseAddress(hostname)) { - throw new Error('Browser Use cannot navigate to private or local network addresses.'); - } - return; - } - - const addresses = await dns.lookup(hostname, { all: true, verbatim: true }); - if (addresses.length === 0 || addresses.some((entry) => isBlockedBrowserUseAddress(entry.address))) { - throw new Error('Browser Use cannot navigate to private or local network addresses.'); - } -} - -async function normalizeUrl(rawUrl: string): Promise { +function normalizeUrl(rawUrl: string): string { const trimmed = rawUrl.trim(); if (!trimmed) { throw new Error('URL is required.'); @@ -377,31 +310,9 @@ async function normalizeUrl(rawUrl: string): Promise { throw new Error('Only http and https URLs are supported.'); } - await assertPublicHttpTarget(parsed); - return parsed.toString(); } -async function assertAllowedBrowserRequest(rawUrl: string): Promise { - const parsed = new URL(rawUrl); - if (!['http:', 'https:'].includes(parsed.protocol)) { - return; - } - - await assertPublicHttpTarget(parsed); -} - -async function attachRequestGuard(context: any): Promise { - await context.route('**/*', async (route: any) => { - try { - await assertAllowedBrowserRequest(route.request().url()); - await route.continue(); - } catch { - await route.abort('blockedbyclient'); - } - }); -} - function publicSession(session: BrowserUseSession): PublicBrowserUseSession { const { ownerId: _ownerId, ...publicFields } = session; return publicFields; @@ -630,7 +541,6 @@ export const browserUseService = { context = await browser.newContext(contextOptions); page = await context.newPage(); } - await attachRequestGuard(context); session.status = 'ready'; session.message = 'Browser session is ready.'; sessions.set(session.id, session); @@ -680,7 +590,7 @@ export const browserUseService = { throw new Error('Browser runtime handle is not available.'); } - const url = await normalizeUrl(rawUrl); + const url = normalizeUrl(rawUrl); await handle.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); session.lastAction = `navigate:${url}`; session.cursor = null; diff --git a/server/modules/browser-use/tests/browser-use.service.test.ts b/server/modules/browser-use/tests/browser-use.service.test.ts index 3aefcd2d..9494a3e1 100644 --- a/server/modules/browser-use/tests/browser-use.service.test.ts +++ b/server/modules/browser-use/tests/browser-use.service.test.ts @@ -1,18 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { browserUseService, isBlockedBrowserUseAddress } from '@/modules/browser-use/browser-use.service.js'; - -test('browser use blocks private and local network addresses by default', () => { - assert.equal(isBlockedBrowserUseAddress('127.0.0.1'), true); - assert.equal(isBlockedBrowserUseAddress('10.0.0.12'), true); - assert.equal(isBlockedBrowserUseAddress('172.16.4.8'), true); - assert.equal(isBlockedBrowserUseAddress('192.168.1.4'), true); - assert.equal(isBlockedBrowserUseAddress('169.254.169.254'), true); - assert.equal(isBlockedBrowserUseAddress('::1'), true); - assert.equal(isBlockedBrowserUseAddress('8.8.8.8'), false); - assert.equal(isBlockedBrowserUseAddress('2001:4860:4860::8888'), false); -}); +import { browserUseService } from '@/modules/browser-use/browser-use.service.js'; test('browser use monitor list starts empty without agent sessions', async () => { const sessions = await browserUseService.listSessions(); diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index 03331024..dc326112 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -1,12 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { - Activity, Bot, Clock3, Download, Expand, ExternalLink, - Globe2, Loader2, MonitorPlay, RefreshCw, @@ -99,19 +97,25 @@ function formatAction(action: string | null): string { function getStatusTone(status: BrowserUseSession['status']): string { if (status === 'ready') { - return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'; + return 'border-primary/30 bg-primary/5 text-foreground'; } if (status === 'stopped') { return 'border-border bg-muted text-muted-foreground'; } - return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300'; + return 'border-border bg-background text-muted-foreground'; } function getRuntimeTone(status: BrowserUseStatus | null, installing: boolean): string { if (!status?.enabled) return 'border-border bg-muted text-muted-foreground'; - if (status.available) return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'; - if (status.installInProgress || installing) return 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-300'; - return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300'; + if (status.available) return 'border-primary/30 bg-primary/5 text-foreground'; + if (status.installInProgress || installing) return 'border-primary/30 bg-primary/5 text-foreground'; + return 'border-border bg-background text-muted-foreground'; +} + +function getStatusDot(status: BrowserUseSession['status']): string { + if (status === 'ready') return 'bg-primary'; + if (status === 'stopped') return 'bg-muted-foreground/50'; + return 'bg-border'; } const PROMPTS = [ @@ -233,10 +237,13 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs >
-
{session.title || getDomain(session.url)}
-
{getDomain(session.url)}
+
+ +
{session.title || getDomain(session.url)}
+
+
{getDomain(session.url)}
- + {session.status}
@@ -269,7 +276,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs {needsBrowserBinaries && ( -
+
Runtime setup required

{status?.message}

+ ))} +
+
+ )} + +
-
-
-
- - Active -
-
{activeSessions.length}
+
+
+ {activeSessions.length} active + / + {sessions.length} total
-
-
- - Current -
-
{getDomain(selectedSession?.url || null)}
-
-
-
- - Updated -
-
{formatRelativeTime(selectedSession?.updatedAt || null)}
+
+ Updated {formatRelativeTime(selectedSession?.updatedAt || null)}
@@ -425,9 +444,14 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
{formatAction(selectedSession?.lastAction || null)}
- + +
{renderBrowserSurface()} @@ -436,7 +460,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs )}
-