From fd76dae90b705d17449b208da75d4b0d63a2cafe Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 18 Jan 2026 17:29:42 -0700 Subject: [PATCH] feat: add network throttling tools Add three new tools for simulating slow network conditions using Chrome DevTools Protocol: - browser_set_network_conditions: Set throttling with presets or custom values Presets: offline, slow-3g, fast-3g, regular-4g, wifi, no-throttle Custom: downloadThroughput, uploadThroughput, latency (bytes/s, ms) - browser_get_network_conditions: Get current throttling settings - browser_clear_network_conditions: Remove throttling (restore full speed) Note: Requires Chromium-based browser (Chrome, Edge). Firefox and WebKit do not support CDP network emulation. --- src/tools.ts | 2 + src/tools/network-throttle.ts | 375 ++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 src/tools/network-throttle.ts diff --git a/src/tools.ts b/src/tools.ts index 1e8f431..4fdb95b 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -26,6 +26,7 @@ import install from './tools/install.js'; import keyboard from './tools/keyboard.js'; import navigate from './tools/navigate.js'; import network from './tools/network.js'; +import networkThrottle from './tools/network-throttle.js'; import notifications from './tools/notifications.js'; import pdf from './tools/pdf.js'; import sensors from './tools/sensors.js'; @@ -55,6 +56,7 @@ export const allTools: Tool[] = [ ...keyboard, ...navigate, ...network, + ...networkThrottle, ...notifications, ...mouse, ...pdf, diff --git a/src/tools/network-throttle.ts b/src/tools/network-throttle.ts new file mode 100644 index 0000000..45b9bb7 --- /dev/null +++ b/src/tools/network-throttle.ts @@ -0,0 +1,375 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import { defineTool } from './tool.js'; + +import type { Context } from '../context.js'; +import type { Response } from '../response.js'; + +// Network condition type for mutable use +interface NetworkConditions { + offline: boolean; + latency: number; + downloadThroughput: number; + uploadThroughput: number; + connectionType: string; + description: string; +} + +// Network condition presets based on Chrome DevTools throttling profiles +const networkPresets: Record = { + 'offline': { + offline: true, + latency: 0, + downloadThroughput: 0, + uploadThroughput: 0, + connectionType: 'none', + description: 'No network connectivity - all requests will fail' + }, + 'slow-3g': { + offline: false, + latency: 2000, + downloadThroughput: 50000, // 50 KB/s (400 kbps) + uploadThroughput: 50000, // 50 KB/s (400 kbps) + connectionType: 'cellular3g', + description: 'Slow 3G: ~400 kbps, 2000ms latency - typical for poor mobile coverage' + }, + 'fast-3g': { + offline: false, + latency: 563, + downloadThroughput: 180000, // ~180 KB/s (1.44 Mbps) + uploadThroughput: 84375, // ~84 KB/s (675 kbps) + connectionType: 'cellular3g', + description: 'Fast 3G: ~1.5 Mbps down, 563ms latency - typical 3G connection' + }, + 'regular-4g': { + offline: false, + latency: 170, + downloadThroughput: 1500000, // ~1.5 MB/s (12 Mbps) + uploadThroughput: 750000, // ~750 KB/s (6 Mbps) + connectionType: 'cellular4g', + description: 'Regular 4G/LTE: ~12 Mbps down, 170ms latency - typical 4G connection' + }, + 'wifi': { + offline: false, + latency: 28, + downloadThroughput: 3000000, // ~3 MB/s (24 Mbps) + uploadThroughput: 1500000, // ~1.5 MB/s (12 Mbps) + connectionType: 'wifi', + description: 'WiFi: ~24 Mbps down, 28ms latency - typical home WiFi' + }, + 'no-throttle': { + offline: false, + latency: 0, + downloadThroughput: -1, // -1 means no throttling + uploadThroughput: -1, + connectionType: 'none', + description: 'No throttling - use full network speed' + } +}; + +type NetworkPreset = 'offline' | 'slow-3g' | 'fast-3g' | 'regular-4g' | 'wifi' | 'no-throttle'; + +// Store current network conditions per context +const currentNetworkConditions = new WeakMap(); + +const setNetworkConditionsSchema = z.object({ + preset: z.enum(['offline', 'slow-3g', 'fast-3g', 'regular-4g', 'wifi', 'no-throttle']).optional() + .describe('Network condition preset. Use "offline" to block all requests, "slow-3g" for poor mobile, "fast-3g" for typical mobile, "regular-4g" for LTE, "wifi" for home WiFi, or "no-throttle" to remove throttling.'), + downloadThroughput: z.number().min(-1).optional() + .describe('Custom download speed in bytes/second. Use -1 for no throttling. Overrides preset if specified.'), + uploadThroughput: z.number().min(-1).optional() + .describe('Custom upload speed in bytes/second. Use -1 for no throttling. Overrides preset if specified.'), + latency: z.number().min(0).optional() + .describe('Custom latency in milliseconds to add to each request. Overrides preset if specified.'), + offline: z.boolean().optional() + .describe('Set to true to simulate offline mode. Overrides preset if specified.') +}); + +const getNetworkConditionsSchema = z.object({}); + +const clearNetworkConditionsSchema = z.object({}); + +const setNetworkConditions = defineTool({ + capability: 'core', + schema: { + name: 'browser_set_network_conditions', + title: 'Set network throttling conditions', + description: `Simulate slow network conditions using Chrome DevTools Protocol. Choose from presets or specify custom values. + +**Presets:** +- offline: Block all network requests +- slow-3g: ~400 kbps, 2s latency (poor mobile) +- fast-3g: ~1.5 Mbps, 563ms latency (typical 3G) +- regular-4g: ~12 Mbps, 170ms latency (LTE) +- wifi: ~24 Mbps, 28ms latency (home WiFi) +- no-throttle: Remove all throttling + +**Note:** This feature requires a Chromium-based browser (Chrome, Edge). Firefox and WebKit are not supported.`, + inputSchema: setNetworkConditionsSchema, + type: 'destructive', + }, + handle: async (context: Context, params: z.output, response: Response) => { + const tab = context.currentTab(); + if (!tab) + throw new Error('No active browser tab. Navigate to a page first.'); + + // Check if we're using Chromium + if (context.config.browser.browserName !== 'chromium') + throw new Error('Network throttling requires a Chromium-based browser (Chrome, Edge). Firefox and WebKit do not support CDP network emulation.'); + + // Validate that at least one parameter is provided + if (!params.preset && params.downloadThroughput === undefined && params.uploadThroughput === undefined && params.latency === undefined && params.offline === undefined) + throw new Error('Please specify a preset or at least one custom network condition (downloadThroughput, uploadThroughput, latency, or offline).'); + + try { + // Create CDP session + const cdpSession = await tab.page.context().newCDPSession(tab.page); + + // Start with preset values if specified, otherwise use no-throttle as base + let conditions = params.preset + ? { ...networkPresets[params.preset] } + : { ...networkPresets['no-throttle'] }; + + // Override with any custom values + if (params.offline !== undefined) + conditions.offline = params.offline; + if (params.latency !== undefined) + conditions.latency = params.latency; + if (params.downloadThroughput !== undefined) + conditions.downloadThroughput = params.downloadThroughput; + if (params.uploadThroughput !== undefined) + conditions.uploadThroughput = params.uploadThroughput; + + // Apply network conditions via CDP + // Cast connectionType to the expected CDP Protocol enum type + await cdpSession.send('Network.emulateNetworkConditions', { + offline: conditions.offline, + latency: conditions.latency, + downloadThroughput: conditions.downloadThroughput, + uploadThroughput: conditions.uploadThroughput, + connectionType: (conditions.connectionType || 'none') as 'none' | 'cellular2g' | 'cellular3g' | 'cellular4g' | 'bluetooth' | 'ethernet' | 'wifi' | 'wimax' | 'other' + }); + + // Store current conditions for later retrieval + const conditionsToStore: { + preset?: NetworkPreset; + custom?: { + offline: boolean; + latency: number; + downloadThroughput: number; + uploadThroughput: number; + }; + } = {}; + + if (params.preset && !params.downloadThroughput && !params.uploadThroughput && !params.latency && params.offline === undefined) { + conditionsToStore.preset = params.preset; + } else { + conditionsToStore.custom = { + offline: conditions.offline, + latency: conditions.latency, + downloadThroughput: conditions.downloadThroughput, + uploadThroughput: conditions.uploadThroughput + }; + if (params.preset) + conditionsToStore.preset = params.preset; + } + + currentNetworkConditions.set(context, conditionsToStore); + + // Build response message + const lines: string[] = []; + + if (conditions.offline) { + lines.push('**Network Status:** OFFLINE'); + lines.push('All network requests will be blocked.'); + } else if (conditions.downloadThroughput === -1 && conditions.uploadThroughput === -1 && conditions.latency === 0) { + lines.push('**Network Status:** No throttling'); + lines.push('Network requests will use full available bandwidth.'); + } else { + lines.push('**Network Throttling Active**'); + lines.push(''); + + if (params.preset) { + const presetInfo = networkPresets[params.preset]; + lines.push(`Preset: **${params.preset}**`); + lines.push(`${presetInfo.description}`); + lines.push(''); + } + + lines.push('**Current Settings:**'); + lines.push(`- Download: ${formatThroughput(conditions.downloadThroughput)}`); + lines.push(`- Upload: ${formatThroughput(conditions.uploadThroughput)}`); + lines.push(`- Latency: ${conditions.latency}ms added to each request`); + } + + response.addResult(lines.join('\n')); + + } catch (error) { + if (error instanceof Error && error.message.includes('Target closed')) + throw new Error('Browser connection lost. Please navigate to a page first.'); + throw new Error(`Failed to set network conditions: ${error}`); + } + }, +}); + +const getNetworkConditions = defineTool({ + capability: 'core', + schema: { + name: 'browser_get_network_conditions', + title: 'Get current network throttling settings', + description: 'Get the current network throttling configuration. Returns preset name if using a preset, or custom values if manually configured.', + inputSchema: getNetworkConditionsSchema, + type: 'readOnly', + }, + handle: async (context: Context, _params: z.output, response: Response) => { + const tab = context.currentTab(); + if (!tab) + throw new Error('No active browser tab. Navigate to a page first.'); + + // Check if we're using Chromium + if (context.config.browser.browserName !== 'chromium') { + response.addResult('Network throttling is only available for Chromium-based browsers (Chrome, Edge).\n\nCurrent browser: ' + context.config.browser.browserName); + return; + } + + const conditions = currentNetworkConditions.get(context); + + if (!conditions) { + response.addResult( + '**Network Throttling:** Not configured\n\n' + + 'Network requests are using full available bandwidth.\n\n' + + 'Use `browser_set_network_conditions` to simulate slow network conditions.' + ); + return; + } + + const lines: string[] = ['**Current Network Conditions:**', '']; + + if (conditions.preset) { + const presetInfo = networkPresets[conditions.preset]; + lines.push(`Preset: **${conditions.preset}**`); + lines.push(`${presetInfo.description}`); + lines.push(''); + } + + if (conditions.custom) { + if (conditions.custom.offline) { + lines.push('**Status:** OFFLINE'); + lines.push('All network requests are being blocked.'); + } else { + lines.push('**Settings:**'); + lines.push(`- Download: ${formatThroughput(conditions.custom.downloadThroughput)}`); + lines.push(`- Upload: ${formatThroughput(conditions.custom.uploadThroughput)}`); + lines.push(`- Latency: ${conditions.custom.latency}ms`); + } + } else if (conditions.preset) { + const preset = networkPresets[conditions.preset]; + if (preset.offline) { + lines.push('**Status:** OFFLINE'); + } else if (preset.downloadThroughput === -1) { + lines.push('**Status:** No throttling'); + } else { + lines.push('**Settings:**'); + lines.push(`- Download: ${formatThroughput(preset.downloadThroughput)}`); + lines.push(`- Upload: ${formatThroughput(preset.uploadThroughput)}`); + lines.push(`- Latency: ${preset.latency}ms`); + } + } + + response.addResult(lines.join('\n')); + }, +}); + +const clearNetworkConditions = defineTool({ + capability: 'core', + schema: { + name: 'browser_clear_network_conditions', + title: 'Remove network throttling', + description: 'Remove all network throttling and restore full network speed. Equivalent to setting preset "no-throttle".', + inputSchema: clearNetworkConditionsSchema, + type: 'destructive', + }, + handle: async (context: Context, _params: z.output, response: Response) => { + const tab = context.currentTab(); + if (!tab) + throw new Error('No active browser tab. Navigate to a page first.'); + + // Check if we're using Chromium + if (context.config.browser.browserName !== 'chromium') + throw new Error('Network throttling requires a Chromium-based browser (Chrome, Edge).'); + + try { + // Create CDP session and disable throttling + const cdpSession = await tab.page.context().newCDPSession(tab.page); + + await cdpSession.send('Network.emulateNetworkConditions', { + offline: false, + latency: 0, + downloadThroughput: -1, + uploadThroughput: -1, + connectionType: 'none' + }); + + // Clear stored conditions + currentNetworkConditions.delete(context); + + response.addResult( + '**Network Throttling Removed**\n\n' + + 'Network requests will now use full available bandwidth.\n\n' + + 'Use `browser_set_network_conditions` to re-enable throttling.' + ); + + } catch (error) { + if (error instanceof Error && error.message.includes('Target closed')) + throw new Error('Browser connection lost. Please navigate to a page first.'); + throw new Error(`Failed to clear network conditions: ${error}`); + } + }, +}); + +/** + * Format throughput value for display + */ +function formatThroughput(bytesPerSecond: number): string { + if (bytesPerSecond === -1) + return 'No limit'; + if (bytesPerSecond === 0) + return 'Blocked'; + + if (bytesPerSecond >= 1000000) + return `${(bytesPerSecond / 1000000).toFixed(1)} MB/s (${(bytesPerSecond * 8 / 1000000).toFixed(1)} Mbps)`; + + if (bytesPerSecond >= 1000) + return `${(bytesPerSecond / 1000).toFixed(1)} KB/s (${(bytesPerSecond * 8 / 1000).toFixed(1)} kbps)`; + + return `${bytesPerSecond} B/s`; +} + +export default [ + setNetworkConditions, + getNetworkConditions, + clearNetworkConditions, +];