Add VSCode URL support and worker ports to sandbox services (#11426)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell 2025-10-20 12:43:08 -06:00 committed by GitHub
parent 2889f736d9
commit 6d137e883f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 8027 additions and 161 deletions

3943
enterprise/poetry.lock generated

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,8 @@ from openhands.app_server.sandbox.docker_sandbox_spec_service import get_docker_
from openhands.app_server.sandbox.sandbox_models import (
AGENT_SERVER,
VSCODE,
WORKER_1,
WORKER_2,
ExposedUrl,
SandboxInfo,
SandboxPage,
@ -124,6 +126,10 @@ class DockerSandboxService(SandboxService):
session_api_key = None
if status == SandboxStatus.RUNNING:
# Get session API key first
env = self._get_container_env_vars(container)
session_api_key = env.get(SESSION_API_KEY_VARIABLE)
# Get the first exposed port mapping
exposed_urls = []
port_bindings = container.attrs.get('NetworkSettings', {}).get('Ports', {})
@ -141,19 +147,19 @@ class DockerSandboxService(SandboxService):
None,
)
if exposed_port:
url = self.container_url_pattern.format(port=host_port)
# VSCode URLs require the api_key and working dir
if exposed_port.name == VSCODE:
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
exposed_urls.append(
ExposedUrl(
name=exposed_port.name,
url=self.container_url_pattern.format(
port=host_port
),
url=url,
)
)
# Get session API key
env = self._get_container_env_vars(container)
session_api_key = env[SESSION_API_KEY_VARIABLE]
return SandboxInfo(
id=container.name,
created_by_user_id=None,
@ -394,6 +400,20 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
),
container_port=8001,
),
ExposedPort(
name=WORKER_1,
description=(
'The first port on which the agent should start application servers.'
),
container_port=8011,
),
ExposedPort(
name=WORKER_2,
description=(
'The first port on which the agent should start application servers.'
),
container_port=8012,
),
]
)
health_check_path: str | None = Field(

View File

@ -14,7 +14,7 @@ from openhands.app_server.sandbox.sandbox_spec_models import (
SandboxSpecInfo,
)
from openhands.app_server.sandbox.sandbox_spec_service import (
AGENT_SERVER_VERSION,
AGENT_SERVER_IMAGE,
SandboxSpecService,
SandboxSpecServiceInjector,
)
@ -34,7 +34,7 @@ def get_docker_client() -> docker.DockerClient:
def get_default_sandbox_specs():
return [
SandboxSpecInfo(
id=f'ghcr.io/all-hands-ai/agent-server:{AGENT_SERVER_VERSION[:7]}-python',
id=AGENT_SERVER_IMAGE,
command=['--port', '8000'],
initial_env={
'OPENVSCODE_SERVER_ROOT': '/openhands/.openvscode-server',

View File

@ -10,7 +10,7 @@ from openhands.app_server.sandbox.sandbox_spec_models import (
SandboxSpecInfo,
)
from openhands.app_server.sandbox.sandbox_spec_service import (
AGENT_SERVER_VERSION,
AGENT_SERVER_IMAGE,
SandboxSpecService,
SandboxSpecServiceInjector,
)
@ -20,7 +20,7 @@ from openhands.app_server.services.injector import InjectorState
def get_default_sandbox_specs():
return [
SandboxSpecInfo(
id=AGENT_SERVER_VERSION,
id=AGENT_SERVER_IMAGE,
command=['python', '-m', 'openhands.agent_server'],
initial_env={
# VSCode disabled for now

View File

@ -26,6 +26,9 @@ from openhands.app_server.event_callback.event_callback_service import (
)
from openhands.app_server.sandbox.sandbox_models import (
AGENT_SERVER,
VSCODE,
WORKER_1,
WORKER_2,
ExposedUrl,
SandboxInfo,
SandboxPage,
@ -144,6 +147,17 @@ class RemoteSandboxService(SandboxService):
url = runtime.get('url', None)
if url:
exposed_urls.append(ExposedUrl(name=AGENT_SERVER, url=url))
vscode_url = (
_build_service_url(url, 'vscode')
+ f'/?tkn={session_api_key}&folder={runtime["working_dir"]}'
)
exposed_urls.append(ExposedUrl(name=VSCODE, url=vscode_url))
exposed_urls.append(
ExposedUrl(name=WORKER_1, url=_build_service_url(url, 'work-1'))
)
exposed_urls.append(
ExposedUrl(name=WORKER_2, url=_build_service_url(url, 'work-2'))
)
else:
exposed_urls = None
else:
@ -383,6 +397,11 @@ class RemoteSandboxService(SandboxService):
return False
def _build_service_url(url: str, service_name: str):
scheme, host_and_path = url.split('://')
return scheme + '://' + service_name + '-' + host_and_path
async def poll_agent_servers(api_url: str, api_key: str, sleep_interval: int):
"""When the app server does not have a public facing url, we poll the agent
servers for the most recent data.

View File

@ -10,7 +10,7 @@ from openhands.app_server.sandbox.sandbox_spec_models import (
SandboxSpecInfo,
)
from openhands.app_server.sandbox.sandbox_spec_service import (
AGENT_SERVER_VERSION,
AGENT_SERVER_IMAGE,
SandboxSpecService,
SandboxSpecServiceInjector,
)
@ -20,7 +20,7 @@ from openhands.app_server.services.injector import InjectorState
def get_default_sandbox_specs():
return [
SandboxSpecInfo(
id=f'ghcr.io/all-hands-ai/agent-server:{AGENT_SERVER_VERSION[:7]}-python',
id=AGENT_SERVER_IMAGE,
command=['/usr/local/bin/openhands-agent-server', '--port', '60000'],
initial_env={
'OPENVSCODE_SERVER_ROOT': '/openhands/.openvscode-server',
@ -28,6 +28,7 @@ def get_default_sandbox_specs():
'OH_ENABLE_VNC': '0',
'OH_CONVERSATIONS_PATH': '/workspace/conversations',
'OH_BASH_EVENTS_DIR': '/workspace/bash_events',
'OH_VSCODE_PORT': '60001',
},
working_dir='/workspace/projects',
)

View File

@ -25,6 +25,8 @@ class ExposedUrl(BaseModel):
# Standard names
AGENT_SERVER = 'AGENT_SERVER'
VSCODE = 'VSCODE'
WORKER_1 = 'WORKER_1'
WORKER_2 = 'WORKER_2'
class SandboxInfo(BaseModel):

View File

@ -11,7 +11,7 @@ from openhands.sdk.utils.models import DiscriminatedUnionMixin
# The version of the agent server to use for deployments.
# Typically this will be the same as the values from the pyproject.toml
AGENT_SERVER_VERSION = '08cf609a996523c0199c61c768d74417b7e96109'
AGENT_SERVER_IMAGE = 'ghcr.io/all-hands-ai/agent-server:ab36fd6-python'
class SandboxSpecService(ABC):

4161
poetry.lock generated

File diff suppressed because one or more lines are too long

View File

@ -113,10 +113,10 @@ e2b-code-interpreter = { version = "^2.0.0", optional = true }
pybase62 = "^1.0.0"
# V1 dependencies
openhands-agent-server = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/agent_server", rev = "08cf609a996523c0199c61c768d74417b7e96109" }
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "08cf609a996523c0199c61c768d74417b7e96109" }
openhands-agent-server = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-agent-server", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-sdk", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
# This refuses to install
# openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "08cf609a996523c0199c61c768d74417b7e96109" }
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-tools", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
python-jose = { version = ">=3.3", extras = [ "cryptography" ] }
sqlalchemy = { extras = [ "asyncio" ], version = "^2.0.40" }
pg8000 = "^1.31.5"

View File

@ -94,7 +94,8 @@ def mock_running_container():
container.attrs = {
'Created': '2024-01-15T10:30:00.000000000Z',
'Config': {
'Env': ['OH_SESSION_API_KEYS_0=session_key_123', 'OTHER_VAR=other_value']
'Env': ['OH_SESSION_API_KEYS_0=session_key_123', 'OTHER_VAR=other_value'],
'WorkingDir': '/workspace',
},
'NetworkSettings': {
'Ports': {
@ -629,7 +630,10 @@ class TestDockerSandboxService:
assert agent_url.url == 'http://localhost:12345'
vscode_url = next(url for url in result.exposed_urls if url.name == VSCODE)
assert vscode_url.url == 'http://localhost:12346'
assert (
vscode_url.url
== 'http://localhost:12346/?tkn=session_key_123&folder=/workspace'
)
async def test_container_to_sandbox_info_invalid_created_time(self, service):
"""Test conversion with invalid creation timestamp."""