feat: Add support for extra HTTP headers in Browser Agent

- Add browser_http_headers field to AgentConfig for custom HTTP headers - Implement UI settings field with textarea input for header configuration - Add validation function to sanitize and validate HTTP headers - Integrate headers into browser session initialization with error handling - Support KEY=VALUE format in settings UI for easy header configuration - Add safety checks to prevent dangerous headers that could break browser functionality Resolves #657

kaghatim committed Aug 21, 2025 at 15:13 UTC e5d924a8cb291ceb6af10290a3d218a1beffe6e4
4 files changed +74 -2
agent.py
+1
@@ -221,6 +221,7 @@ class AgentConfig:
221 profile: str = ""
222 memory_subdir: str = ""
223 knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
224 + browser_http_headers: dict[str, str] = field(default_factory=dict) # Custom HTTP headers for browser requests
225 code_exec_ssh_enabled: bool = True
226 code_exec_ssh_addr: str = "localhost"
227 code_exec_ssh_port: int = 55022
initialize.py
+1
@@ -79,6 +79,7 @@ def initialize_agent():
79 memory_subdir=current_settings["agent_memory_subdir"],
80 knowledge_subdirs=[current_settings["agent_knowledge_subdir"], "default"],
81 mcp_servers=current_settings["mcp_servers"],
82 + browser_http_headers=current_settings["browser_http_headers"],
83 # code_exec params get initialized in _set_runtime_config
84 # additional = {},
85 )
python/helpers/settings.py
+59 -1
@@ -52,6 +52,7 @@ class Settings(TypedDict):
52 browser_model_rl_input: int
53 browser_model_rl_output: int
54 browser_model_kwargs: dict[str, str]
55 + browser_http_headers: dict[str, str]
56
57 agent_profile: str
58 agent_memory_subdir: str
@@ -503,6 +504,16 @@ def convert_out(settings: Settings) -> SettingsOutput:
504 }
505 )
506
507 + browser_model_fields.append(
508 + {
509 + "id": "browser_http_headers",
510 + "title": "HTTP Headers",
511 + "description": "HTTP headers to include with all browser requests. Format is KEY=VALUE on individual lines, just like .env file. Example: Authorization=Bearer token123",
512 + "type": "textarea",
513 + "value": _dict_to_env(settings.get("browser_http_headers", {})),
514 + }
515 + )
516 +
517 browser_model_section: SettingsSection = {
518 "id": "browser_model",
519 "title": "Web Browser Model",
@@ -1213,7 +1224,14 @@ def convert_in(settings: dict) -> Settings:
1224 )
1225
1226 if not should_skip:
1216 - if field["id"].endswith("_kwargs"):
1227 + # Special handling for browser_http_headers
1228 + if field["id"] == "browser_http_headers":
1229 + headers_dict = _env_to_dict(field["value"])
1230 + # Validate headers before saving
1231 + validated_headers = _validate_http_headers(headers_dict)
1232 + current[field["id"]] = validated_headers
1233 + PrintStyle().info(f"Set browser_http_headers: {validated_headers}")
1234 + elif field["id"].endswith("_kwargs"):
1235 current[field["id"]] = _env_to_dict(field["value"])
1236 elif field["id"].startswith("api_key_"):
1237 current["api_keys"][field["id"]] = field["value"]
@@ -1365,6 +1383,7 @@ def get_default_settings() -> Settings:
1383 browser_model_rl_input=0,
1384 browser_model_rl_output=0,
1385 browser_model_kwargs={"temperature": "0"},
1386 + browser_http_headers={},
1387 memory_recall_enabled=True,
1388 memory_recall_delayed=False,
1389 memory_recall_interval=3,
@@ -1538,6 +1557,45 @@ def _dict_to_env(data_dict):
1557 return "\n".join(lines)
1558
1559
1560 +def _validate_http_headers(headers: dict[str, str]) -> dict[str, str]:
1561 + """Validate and sanitize HTTP headers for browser requests"""
1562 + valid_headers = {}
1563 +
1564 + # Headers that should not be set manually as they're controlled by the browser
1565 + dangerous_headers = {
1566 + 'host', 'content-length', 'connection', 'upgrade', 'expect',
1567 + 'transfer-encoding', 'te', 'trailer', 'proxy-connection'
1568 + }
1569 +
1570 + for key, value in headers.items():
1571 + # Remove any leading/trailing whitespace
1572 + key = key.strip()
1573 + value = value.strip()
1574 +
1575 + # Skip empty keys or values
1576 + if not key or not value:
1577 + continue
1578 +
1579 + # Check for dangerous headers
1580 + if key.lower() in dangerous_headers:
1581 + PrintStyle().warning(f"Skipping potentially dangerous header: {key}")
1582 + continue
1583 +
1584 + # Basic header name validation (RFC 7230)
1585 + if not re.match(r'^[!#$%&\'*+\-.0-9A-Z^_`a-z|~]+$', key):
1586 + PrintStyle().warning(f"Invalid header name format: {key}")
1587 + continue
1588 +
1589 + # Header value validation - remove control characters except tab
1590 + cleaned_value = re.sub(r'[\x00-\x08\x0A-\x1F\x7F]', '', value)
1591 + if cleaned_value != value:
1592 + PrintStyle().warning(f"Cleaned invalid characters from header value: {key}")
1593 +
1594 + valid_headers[key] = cleaned_value
1595 +
1596 + return valid_headers
1597 +
1598 +
1599 def set_root_password(password: str):
1600 if not runtime.is_dockerized():
1601 raise Exception("root password can only be set in dockerized environments")
python/tools/browser_agent.py
+13 -1
@@ -39,7 +39,18 @@ class State:
39
40 # for some reason we need to provide exact path to headless shell, otherwise it looks for headed browser
41 pw_binary = ensure_playwright_binary()
42 -
42 +
43 + # Prepare HTTP headers with error handling
44 + try:
45 + http_headers = self.agent.config.browser_http_headers or {}
46 + if http_headers:
47 + PrintStyle().info(f"Using HTTP headers: {list(http_headers.keys())}")
48 + else:
49 + PrintStyle().info("No custom HTTP headers configured")
50 + except Exception as e:
51 + PrintStyle().warning(f"Error processing HTTP headers, using defaults: {e}")
52 + http_headers = {}
53 +
54 self.browser_session = browser_use.BrowserSession(
55 browser_profile=browser_use.BrowserProfile(
56 headless=True,
@@ -64,6 +75,7 @@ class State:
75 / "profiles"
76 / f"agent_{self.agent.context.id}"
77 ),
78 + extra_http_headers=http_headers,
79 )
80 )
81