Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [Unreleased]

### Added
- feat: add SessionConfiguration with proxy, extensions, and profile support for browser sessions (#274)

## [1.3.2] - 2026-02-23

### Added
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"boto3>=1.40.52",
"botocore>=1.40.52",
"boto3>=1.42.54",
"botocore>=1.42.54",
"pydantic>=2.0.0,<2.41.3",
"urllib3>=1.26.0",
"starlette>=0.46.2",
Expand Down
16 changes: 16 additions & 0 deletions src/bedrock_agentcore/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,42 @@
from .browser_client import BrowserClient, browser_session
from .code_interpreter_client import CodeInterpreter, code_session
from .config import (
BasicAuth,
BrowserConfiguration,
BrowserExtension,
BrowserSigningConfiguration,
CodeInterpreterConfiguration,
ExtensionS3Location,
ExternalProxy,
NetworkConfiguration,
ProfileConfiguration,
ProxyConfiguration,
ProxyCredentials,
RecordingConfiguration,
SessionConfiguration,
ViewportConfiguration,
VpcConfig,
create_browser_config,
)

__all__ = [
"BasicAuth",
"BrowserClient",
"browser_session",
"CodeInterpreter",
"code_session",
"BrowserConfiguration",
"BrowserExtension",
"BrowserSigningConfiguration",
"CodeInterpreterConfiguration",
"ExtensionS3Location",
"ExternalProxy",
"NetworkConfiguration",
"ProfileConfiguration",
"ProxyConfiguration",
"ProxyCredentials",
"RecordingConfiguration",
"SessionConfiguration",
"ViewportConfiguration",
"VpcConfig",
"create_browser_config",
Expand Down
82 changes: 76 additions & 6 deletions src/bedrock_agentcore/tools/browser_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import secrets
import uuid
from contextlib import contextmanager
from typing import Dict, Generator, Optional, Tuple
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
from urllib.parse import urlparse

import boto3
Expand All @@ -22,6 +22,13 @@
from bedrock_agentcore._utils.user_agent import build_user_agent_suffix

from .._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
from .config import BrowserExtension, ProfileConfiguration, ProxyConfiguration, ViewportConfiguration


def _to_dict(value):
"""Convert a dataclass or dict to a dict. Passes dicts through unchanged."""
return value.to_dict() if hasattr(value, "to_dict") else value


DEFAULT_IDENTIFIER = "aws.browser.v1"
DEFAULT_SESSION_TIMEOUT = 3600
Expand Down Expand Up @@ -288,7 +295,10 @@ def start(
identifier: Optional[str] = DEFAULT_IDENTIFIER,
name: Optional[str] = None,
session_timeout_seconds: Optional[int] = DEFAULT_SESSION_TIMEOUT,
viewport: Optional[Dict[str, int]] = None,
viewport: Optional[Union[ViewportConfiguration, Dict[str, int]]] = None,
proxy_configuration: Optional[Union[ProxyConfiguration, Dict[str, Any]]] = None,
extensions: Optional[List[Union[BrowserExtension, Dict[str, Any]]]] = None,
profile_configuration: Optional[Union[ProfileConfiguration, Dict[str, Any]]] = None,
) -> str:
"""Start a browser sandbox session.
Expand All @@ -300,8 +310,20 @@ def start(
name (Optional[str]): A name for this session.
session_timeout_seconds (Optional[int]): The timeout for the session in seconds.
Range: 1-28800 (8 hours). Default: 3600 (1 hour).
viewport (Optional[Dict[str, int]]): The viewport dimensions:
viewport (Optional[Union[ViewportConfiguration, Dict[str, int]]]): The viewport
dimensions. Can be a ViewportConfiguration dataclass or a plain dict:
{'width': 1920, 'height': 1080}
proxy_configuration (Optional[Union[ProxyConfiguration, Dict[str, Any]]]): Proxy
configuration for routing browser traffic through external proxy servers.
Can be a ProxyConfiguration dataclass or a plain dict matching the API shape.
extensions (Optional[List[Union[BrowserExtension, Dict[str, Any]]]]): List of
browser extensions to load into the session. Each element can be a
BrowserExtension dataclass or a plain dict:
[{"location": {"s3": {"bucket": "...", "prefix": "..."}}}]
profile_configuration (Optional[Union[ProfileConfiguration, Dict[str, Any]]]): Profile
configuration for persisting browser state across sessions. Can be a
ProfileConfiguration dataclass or a plain dict:
{"profileIdentifier": "my-profile-id"}
Returns:
str: The session ID of the newly created session.
Expand All @@ -316,6 +338,20 @@ def start(
... viewport={'width': 1920, 'height': 1080},
... session_timeout_seconds=7200 # 2 hours
... )
>>>
>>> # Use proxy configuration
>>> session_id = client.start(
... proxy_configuration={
... "proxies": [{
... "externalProxy": {
... "server": "proxy.example.com",
... "port": 8080,
... "domainPatterns": [".example.com"],
... }
... }],
... "bypass": {"domainPatterns": [".amazonaws.com"]}
... }
... )
"""
self.logger.info("Starting browser session...")

Expand All @@ -326,7 +362,16 @@ def start(
}

if viewport is not None:
request_params["viewPort"] = viewport
request_params["viewPort"] = _to_dict(viewport)

if proxy_configuration is not None:
request_params["proxyConfiguration"] = _to_dict(proxy_configuration)

if extensions is not None:
request_params["extensions"] = [_to_dict(e) for e in extensions]

if profile_configuration is not None:
request_params["profileConfiguration"] = _to_dict(profile_configuration)

response = self.data_plane_client.start_browser_session(**request_params)

Expand Down Expand Up @@ -581,14 +626,26 @@ def release_control(self):

@contextmanager
def browser_session(
region: str, viewport: Optional[Dict[str, int]] = None, identifier: Optional[str] = None
region: str,
viewport: Optional[Union[ViewportConfiguration, Dict[str, int]]] = None,
identifier: Optional[str] = None,
proxy_configuration: Optional[Union[ProxyConfiguration, Dict[str, Any]]] = None,
extensions: Optional[List[Union[BrowserExtension, Dict[str, Any]]]] = None,
profile_configuration: Optional[Union[ProfileConfiguration, Dict[str, Any]]] = None,
) -> Generator[BrowserClient, None, None]:
"""Context manager for creating and managing a browser sandbox session.
Args:
region (str): AWS region.
viewport (Optional[Dict[str, int]]): Viewport dimensions.
viewport (Optional[Union[ViewportConfiguration, Dict[str, int]]]): Viewport dimensions.
Can be a ViewportConfiguration dataclass or a plain dict.
identifier (Optional[str]): Browser identifier (system or custom).
proxy_configuration (Optional[Union[ProxyConfiguration, Dict[str, Any]]]): Proxy
configuration. Can be a ProxyConfiguration dataclass or a plain dict.
extensions (Optional[List[Union[BrowserExtension, Dict[str, Any]]]]): Browser
extensions. Each element can be a BrowserExtension dataclass or a plain dict.
profile_configuration (Optional[Union[ProfileConfiguration, Dict[str, Any]]]): Profile
configuration. Can be a ProfileConfiguration dataclass or a plain dict.
Yields:
BrowserClient: An initialized and started browser client.
Expand All @@ -602,13 +659,26 @@ def browser_session(
>>> with browser_session('us-west-2', identifier='my-signed-browser') as client:
... # Automation with reduced CAPTCHA friction
... pass
...
>>> # Use proxy configuration
>>> with browser_session('us-west-2', proxy_configuration={
... "proxies": [{"externalProxy": {"server": "proxy.corp.com", "port": 8080}}],
... "bypass": {"domainPatterns": [".amazonaws.com"]}
... }) as client:
... ws_url, headers = client.generate_ws_headers()
"""
client = BrowserClient(region)
start_kwargs = {}
if viewport is not None:
start_kwargs["viewport"] = viewport
if identifier is not None:
start_kwargs["identifier"] = identifier
if proxy_configuration is not None:
start_kwargs["proxy_configuration"] = proxy_configuration
if extensions is not None:
start_kwargs["extensions"] = extensions
if profile_configuration is not None:
start_kwargs["profile_configuration"] = profile_configuration

client.start(**start_kwargs)

Expand Down
168 changes: 168 additions & 0 deletions src/bedrock_agentcore/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,174 @@ def mobile(cls) -> "ViewportConfiguration":
return cls(width=375, height=667)


@dataclass
class BasicAuth:
"""HTTP Basic Auth credentials stored in Secrets Manager.
Attributes:
secret_arn: ARN of the Secrets Manager secret containing
{"username": "...", "password": "..."} JSON
"""

secret_arn: str

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
return {"secretArn": self.secret_arn}


@dataclass
class ProxyCredentials:
"""Credentials for authenticating with a proxy server.
Currently supports HTTP Basic Auth. Modeled as a union to allow
future credential types (bearer token, mTLS, etc.) without breaking changes.
Attributes:
basic_auth: HTTP Basic Auth credentials via Secrets Manager
"""

basic_auth: Optional[BasicAuth] = None

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
creds = {}
if self.basic_auth:
creds["basicAuth"] = self.basic_auth.to_dict()
return creds


@dataclass
class ExternalProxy:
"""Configuration for an external proxy server.
Attributes:
server: Proxy server hostname
port: Proxy server port
domain_patterns: Domain patterns to route through this proxy
credentials: Optional credentials for proxy authentication
"""

server: str
port: int
domain_patterns: Optional[List[str]] = None
credentials: Optional[ProxyCredentials] = None

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
proxy = {"server": self.server, "port": self.port}
if self.domain_patterns:
proxy["domainPatterns"] = self.domain_patterns
if self.credentials:
proxy["credentials"] = self.credentials.to_dict()
return {"externalProxy": proxy}


@dataclass
class ProxyConfiguration:
"""Proxy configuration for routing browser traffic through external proxy servers.
Attributes:
proxies: List of external proxy configurations
bypass_patterns: Domain patterns that bypass all proxies
"""

proxies: List[ExternalProxy]
bypass_patterns: Optional[List[str]] = None

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
config = {"proxies": [p.to_dict() for p in self.proxies]}
if self.bypass_patterns:
config["bypass"] = {"domainPatterns": self.bypass_patterns}
return config


@dataclass
class ExtensionS3Location:
"""S3 location for a browser extension.
Attributes:
bucket: S3 bucket name
prefix: S3 key prefix for the extension
version_id: Optional S3 object version ID
"""

bucket: str
prefix: str
version_id: Optional[str] = None

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
location = {"bucket": self.bucket, "prefix": self.prefix}
if self.version_id:
location["versionId"] = self.version_id
return location


@dataclass
class BrowserExtension:
"""A browser extension to load into a session.
Attributes:
s3_location: S3 location of the extension package
"""

s3_location: ExtensionS3Location

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
return {"location": {"s3": self.s3_location.to_dict()}}


@dataclass
class ProfileConfiguration:
"""Profile configuration for persisting browser state across sessions.
Attributes:
profile_identifier: Identifier for the browser profile
"""

profile_identifier: str

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
return {"profileIdentifier": self.profile_identifier}


@dataclass
class SessionConfiguration:
"""Complete session configuration for start().
Bundles all session-level parameters into one composable type.
Usage: client.start(**session_config.to_dict())
Attributes:
viewport: Viewport dimensions for the browser session
proxy: Proxy configuration for routing browser traffic
extensions: Browser extensions to load into the session
profile: Profile configuration for persisting browser state
"""

viewport: Optional[ViewportConfiguration] = None
proxy: Optional[ProxyConfiguration] = None
extensions: Optional[List[BrowserExtension]] = None
profile: Optional[ProfileConfiguration] = None

def to_dict(self) -> Dict:
"""Convert to API-compatible dictionary."""
config = {}
if self.viewport:
config["viewport"] = self.viewport.to_dict()
if self.proxy:
config["proxy_configuration"] = self.proxy.to_dict()
if self.extensions:
config["extensions"] = [e.to_dict() for e in self.extensions]
if self.profile:
config["profile_configuration"] = self.profile.to_dict()
return config


@dataclass
class BrowserConfiguration:
"""Complete browser configuration for create_browser.
Expand Down
Loading
Loading