mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Add CORS origins support to Docker sandbox service for remote browser access (#12489)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -27,6 +27,9 @@ from openhands.app_server.sandbox.sandbox_models import (
|
||||
SandboxStatus,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_service import (
|
||||
ALLOW_CORS_ORIGINS_VARIABLE,
|
||||
SESSION_API_KEY_VARIABLE,
|
||||
WEBHOOK_CALLBACK_VARIABLE,
|
||||
SandboxService,
|
||||
SandboxServiceInjector,
|
||||
)
|
||||
@@ -37,8 +40,6 @@ from openhands.app_server.utils.docker_utils import (
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
SESSION_API_KEY_VARIABLE = 'OH_SESSION_API_KEYS_0'
|
||||
WEBHOOK_CALLBACK_VARIABLE = 'OH_WEBHOOKS_0_BASE_URL'
|
||||
STARTUP_GRACE_SECONDS = 15
|
||||
|
||||
|
||||
@@ -79,6 +80,7 @@ class DockerSandboxService(SandboxService):
|
||||
health_check_path: str | None
|
||||
httpx_client: httpx.AsyncClient
|
||||
max_num_sandboxes: int
|
||||
web_url: str | None = None
|
||||
extra_hosts: dict[str, str] = field(default_factory=dict)
|
||||
docker_client: docker.DockerClient = field(default_factory=get_docker_client)
|
||||
startup_grace_seconds: int = STARTUP_GRACE_SECONDS
|
||||
@@ -326,6 +328,12 @@ class DockerSandboxService(SandboxService):
|
||||
f'http://host.docker.internal:{self.host_port}/api/v1/webhooks'
|
||||
)
|
||||
|
||||
# Set CORS origins for remote browser access when web_url is configured.
|
||||
# This allows the agent-server container to accept requests from the
|
||||
# frontend when running OpenHands on a remote machine.
|
||||
if self.web_url:
|
||||
env_vars[ALLOW_CORS_ORIGINS_VARIABLE] = self.web_url
|
||||
|
||||
# Prepare port mappings and add port environment variables
|
||||
port_mappings = {}
|
||||
for exposed_port in self.exposed_ports:
|
||||
@@ -524,10 +532,15 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
|
||||
) -> AsyncGenerator[SandboxService, None]:
|
||||
# Define inline to prevent circular lookup
|
||||
from openhands.app_server.config import (
|
||||
get_global_config,
|
||||
get_httpx_client,
|
||||
get_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Get web_url from global config for CORS support
|
||||
config = get_global_config()
|
||||
web_url = config.web_url
|
||||
|
||||
async with (
|
||||
get_httpx_client(state) as httpx_client,
|
||||
get_sandbox_spec_service(state) as sandbox_spec_service,
|
||||
@@ -542,6 +555,7 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
|
||||
health_check_path=self.health_check_path,
|
||||
httpx_client=httpx_client,
|
||||
max_num_sandboxes=self.max_num_sandboxes,
|
||||
web_url=web_url,
|
||||
extra_hosts=self.extra_hosts,
|
||||
startup_grace_seconds=self.startup_grace_seconds,
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ from openhands.app_server.sandbox.sandbox_models import (
|
||||
SandboxStatus,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_service import (
|
||||
ALLOW_CORS_ORIGINS_VARIABLE,
|
||||
WEBHOOK_CALLBACK_VARIABLE,
|
||||
SandboxService,
|
||||
SandboxServiceInjector,
|
||||
)
|
||||
@@ -48,8 +50,6 @@ from openhands.app_server.utils.sql_utils import Base, UtcDateTime
|
||||
from openhands.sdk.utils.paging import page_iterator
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
WEBHOOK_CALLBACK_VARIABLE = 'OH_WEBHOOKS_0_BASE_URL'
|
||||
ALLOW_CORS_ORIGINS_VARIABLE = 'OH_ALLOW_CORS_ORIGINS_0'
|
||||
polling_task: asyncio.Task | None = None
|
||||
POD_STATUS_MAPPING = {
|
||||
'ready': SandboxStatus.RUNNING,
|
||||
|
||||
@@ -18,6 +18,10 @@ from openhands.app_server.utils.docker_utils import (
|
||||
from openhands.sdk.utils.models import DiscriminatedUnionMixin
|
||||
from openhands.sdk.utils.paging import page_iterator
|
||||
|
||||
SESSION_API_KEY_VARIABLE = 'OH_SESSION_API_KEYS_0'
|
||||
WEBHOOK_CALLBACK_VARIABLE = 'OH_WEBHOOKS_0_BASE_URL'
|
||||
ALLOW_CORS_ORIGINS_VARIABLE = 'OH_ALLOW_CORS_ORIGINS_0'
|
||||
|
||||
|
||||
class SandboxService(ABC):
|
||||
"""Service for accessing sandboxes in which conversations may be run."""
|
||||
|
||||
@@ -615,6 +615,127 @@ class TestDockerSandboxService:
|
||||
call_args = mock_docker_client.containers.run.call_args
|
||||
assert call_args[1]['extra_hosts'] is None
|
||||
|
||||
@patch('openhands.app_server.sandbox.docker_sandbox_service.base62.encodebytes')
|
||||
@patch('os.urandom')
|
||||
async def test_start_sandbox_with_cors_origins(
|
||||
self,
|
||||
mock_urandom,
|
||||
mock_encodebytes,
|
||||
mock_sandbox_spec_service,
|
||||
mock_httpx_client,
|
||||
mock_docker_client,
|
||||
):
|
||||
"""Test that CORS origins are set when web_url is configured."""
|
||||
# Setup
|
||||
mock_urandom.side_effect = [b'container_id', b'session_key']
|
||||
mock_encodebytes.side_effect = ['test_container_id', 'test_session_key']
|
||||
|
||||
mock_container = MagicMock()
|
||||
mock_container.name = 'oh-test-test_container_id'
|
||||
mock_container.status = 'running'
|
||||
mock_container.image.tags = ['test-image:latest']
|
||||
mock_container.attrs = {
|
||||
'Created': '2024-01-15T10:30:00.000000000Z',
|
||||
'Config': {
|
||||
'Env': ['OH_SESSION_API_KEYS_0=test_session_key', 'TEST_VAR=test_value']
|
||||
},
|
||||
'NetworkSettings': {'Ports': {}},
|
||||
}
|
||||
mock_docker_client.containers.run.return_value = mock_container
|
||||
|
||||
# Create service with web_url configured for CORS
|
||||
service_with_cors = DockerSandboxService(
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
container_name_prefix='oh-test-',
|
||||
host_port=3000,
|
||||
container_url_pattern='http://192.168.1.100:{port}',
|
||||
mounts=[],
|
||||
exposed_ports=[
|
||||
ExposedPort(
|
||||
name=AGENT_SERVER, description='Agent server', container_port=8000
|
||||
),
|
||||
],
|
||||
health_check_path='/health',
|
||||
httpx_client=mock_httpx_client,
|
||||
max_num_sandboxes=3,
|
||||
web_url='http://192.168.1.100:3000',
|
||||
docker_client=mock_docker_client,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(service_with_cors, '_find_unused_port', return_value=12345),
|
||||
patch.object(service_with_cors, 'pause_old_sandboxes', return_value=[]),
|
||||
):
|
||||
# Execute
|
||||
await service_with_cors.start_sandbox()
|
||||
|
||||
# Verify CORS origins environment variable was set
|
||||
mock_docker_client.containers.run.assert_called_once()
|
||||
call_args = mock_docker_client.containers.run.call_args
|
||||
env_vars = call_args[1]['environment']
|
||||
assert 'OH_ALLOW_CORS_ORIGINS_0' in env_vars
|
||||
assert env_vars['OH_ALLOW_CORS_ORIGINS_0'] == 'http://192.168.1.100:3000'
|
||||
|
||||
@patch('openhands.app_server.sandbox.docker_sandbox_service.base62.encodebytes')
|
||||
@patch('os.urandom')
|
||||
async def test_start_sandbox_without_cors_origins(
|
||||
self,
|
||||
mock_urandom,
|
||||
mock_encodebytes,
|
||||
mock_sandbox_spec_service,
|
||||
mock_httpx_client,
|
||||
mock_docker_client,
|
||||
):
|
||||
"""Test that CORS origins are not set when web_url is None."""
|
||||
# Setup
|
||||
mock_urandom.side_effect = [b'container_id', b'session_key']
|
||||
mock_encodebytes.side_effect = ['test_container_id', 'test_session_key']
|
||||
|
||||
mock_container = MagicMock()
|
||||
mock_container.name = 'oh-test-test_container_id'
|
||||
mock_container.status = 'running'
|
||||
mock_container.image.tags = ['test-image:latest']
|
||||
mock_container.attrs = {
|
||||
'Created': '2024-01-15T10:30:00.000000000Z',
|
||||
'Config': {
|
||||
'Env': ['OH_SESSION_API_KEYS_0=test_session_key', 'TEST_VAR=test_value']
|
||||
},
|
||||
'NetworkSettings': {'Ports': {}},
|
||||
}
|
||||
mock_docker_client.containers.run.return_value = mock_container
|
||||
|
||||
# Create service without web_url (local development mode)
|
||||
service_without_cors = DockerSandboxService(
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
container_name_prefix='oh-test-',
|
||||
host_port=3000,
|
||||
container_url_pattern='http://localhost:{port}',
|
||||
mounts=[],
|
||||
exposed_ports=[
|
||||
ExposedPort(
|
||||
name=AGENT_SERVER, description='Agent server', container_port=8000
|
||||
),
|
||||
],
|
||||
health_check_path='/health',
|
||||
httpx_client=mock_httpx_client,
|
||||
max_num_sandboxes=3,
|
||||
web_url=None, # No web_url configured
|
||||
docker_client=mock_docker_client,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(service_without_cors, '_find_unused_port', return_value=12345),
|
||||
patch.object(service_without_cors, 'pause_old_sandboxes', return_value=[]),
|
||||
):
|
||||
# Execute
|
||||
await service_without_cors.start_sandbox()
|
||||
|
||||
# Verify CORS origins environment variable was NOT set
|
||||
mock_docker_client.containers.run.assert_called_once()
|
||||
call_args = mock_docker_client.containers.run.call_args
|
||||
env_vars = call_args[1]['environment']
|
||||
assert 'OH_ALLOW_CORS_ORIGINS_0' not in env_vars
|
||||
|
||||
async def test_resume_sandbox_from_paused(self, service):
|
||||
"""Test resuming a paused sandbox."""
|
||||
# Setup
|
||||
|
||||
Reference in New Issue
Block a user