From 9af3ee82982da287a6c6e8fe834c3d33a03e6ada Mon Sep 17 00:00:00 2001 From: Graham Neubig Date: Thu, 15 Jan 2026 08:53:04 -0500 Subject: [PATCH] fix: Add WORKER_1 and WORKER_2 env vars to remote sandbox service (#12424) Co-authored-by: openhands --- .../app_server/app_conversation/skill_loader.py | 15 +++++++++++++++ .../app_server/sandbox/remote_sandbox_service.py | 6 ++++++ .../app_server/test_remote_sandbox_service.py | 6 ++++++ 3 files changed, 27 insertions(+) diff --git a/openhands/app_server/app_conversation/skill_loader.py b/openhands/app_server/app_conversation/skill_loader.py index 3bcbdbe4b2..88c58fb036 100644 --- a/openhands/app_server/app_conversation/skill_loader.py +++ b/openhands/app_server/app_conversation/skill_loader.py @@ -30,6 +30,20 @@ GLOBAL_SKILLS_DIR = os.path.join( WORK_HOSTS_SKILL = """The user has access to the following hosts for accessing a web application, each of which has a corresponding port:""" +WORK_HOSTS_SKILL_FOOTER = """ +When starting a web server, use the corresponding ports via environment variables: +- $WORKER_1 for the first port +- $WORKER_2 for the second port + +**CRITICAL: You MUST enable CORS and bind to 0.0.0.0.** Without CORS headers, the App tab cannot detect your server and will show an empty state. + +Example (Flask): +```python +from flask_cors import CORS +CORS(app) +app.run(host='0.0.0.0', port=int(os.environ.get('WORKER_1', 12000))) +```""" + def _find_and_load_global_skill_files(skill_dir: Path) -> list[Skill]: """Find and load all .md files from the global skills directory. @@ -73,6 +87,7 @@ def load_sandbox_skills(sandbox: SandboxInfo) -> list[Skill]: content_list = [WORK_HOSTS_SKILL] for url in urls: content_list.append(f'* {url.url} (port {url.port})') + content_list.append(WORK_HOSTS_SKILL_FOOTER) content = '\n'.join(content_list) return [Skill(name='work_hosts', content=content, trigger=None)] diff --git a/openhands/app_server/sandbox/remote_sandbox_service.py b/openhands/app_server/sandbox/remote_sandbox_service.py index 9e5b7740fb..d1d083f3fb 100644 --- a/openhands/app_server/sandbox/remote_sandbox_service.py +++ b/openhands/app_server/sandbox/remote_sandbox_service.py @@ -272,6 +272,12 @@ class RemoteSandboxService(SandboxService): # we are probably in local development and the only url in use is localhost environment[ALLOW_CORS_ORIGINS_VARIABLE] = self.web_url + # Add worker port environment variables so the agent knows which ports to use + # for web applications. These match the ports exposed via the WORKER_1 and + # WORKER_2 URLs. + environment[WORKER_1] = str(WORKER_1_PORT) + environment[WORKER_2] = str(WORKER_2_PORT) + return environment async def search_sandboxes( diff --git a/tests/unit/app_server/test_remote_sandbox_service.py b/tests/unit/app_server/test_remote_sandbox_service.py index 1bdcf87d59..dcec8e390f 100644 --- a/tests/unit/app_server/test_remote_sandbox_service.py +++ b/tests/unit/app_server/test_remote_sandbox_service.py @@ -295,6 +295,9 @@ class TestEnvironmentInitialization: assert environment['EXISTING_VAR'] == 'existing_value' assert environment[WEBHOOK_CALLBACK_VARIABLE] == expected_webhook_url assert environment[ALLOW_CORS_ORIGINS_VARIABLE] == 'https://web.example.com' + # Verify worker port environment variables are set + assert environment[WORKER_1] == '12000' + assert environment[WORKER_2] == '12001' @pytest.mark.asyncio async def test_init_environment_without_web_url(self, remote_sandbox_service): @@ -318,6 +321,9 @@ class TestEnvironmentInitialization: assert environment['EXISTING_VAR'] == 'existing_value' assert WEBHOOK_CALLBACK_VARIABLE not in environment assert ALLOW_CORS_ORIGINS_VARIABLE not in environment + # Worker port environment variables should still be set regardless of web_url + assert environment[WORKER_1] == '12000' + assert environment[WORKER_2] == '12001' class TestSandboxInfoConversion: