OpenHands/openhands/app_server/sandbox/sandbox_spec_service.py
Tim O'Farrell 09af93a02a
Agent server env override (#12068)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-12-25 03:55:06 +00:00

96 lines
3.4 KiB
Python

import asyncio
import os
from abc import ABC, abstractmethod
from openhands.agent_server import env_parser
from openhands.app_server.errors import SandboxError
from openhands.app_server.sandbox.sandbox_spec_models import (
SandboxSpecInfo,
SandboxSpecInfoPage,
)
from openhands.app_server.services.injector import Injector
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_IMAGE = 'ghcr.io/openhands/agent-server:97652be-python'
class SandboxSpecService(ABC):
"""Service for managing Sandbox specs.
At present this is read only. The plan is that later this class will allow building
and deleting sandbox specs and limiting access by user and group. It would also be
nice to be able to set the desired number of warm sandboxes for a spec and scale
this up and down.
"""
@abstractmethod
async def search_sandbox_specs(
self, page_id: str | None = None, limit: int = 100
) -> SandboxSpecInfoPage:
"""Search for sandbox specs."""
@abstractmethod
async def get_sandbox_spec(self, sandbox_spec_id: str) -> SandboxSpecInfo | None:
"""Get a single sandbox spec, returning None if not found."""
async def get_default_sandbox_spec(self) -> SandboxSpecInfo:
"""Get the default sandbox spec."""
page = await self.search_sandbox_specs()
if not page.items:
raise SandboxError('No sandbox specs available!')
return page.items[0]
async def batch_get_sandbox_specs(
self, sandbox_spec_ids: list[str]
) -> list[SandboxSpecInfo | None]:
"""Get a batch of sandbox specs, returning None for any not found."""
results = await asyncio.gather(
*[
self.get_sandbox_spec(sandbox_spec_id)
for sandbox_spec_id in sandbox_spec_ids
]
)
return results
class SandboxSpecServiceInjector(
DiscriminatedUnionMixin, Injector[SandboxSpecService], ABC
):
pass
def get_agent_server_image() -> str:
agent_server_image_repository = os.getenv('AGENT_SERVER_IMAGE_REPOSITORY')
agent_server_image_tag = os.getenv('AGENT_SERVER_IMAGE_TAG')
if agent_server_image_repository and agent_server_image_tag:
return f'{agent_server_image_repository}:{agent_server_image_tag}'
return AGENT_SERVER_IMAGE
def get_agent_server_env() -> dict[str, str]:
"""Get environment variables to be injected into agent server sandbox environments.
This function reads environment variable overrides from the OH_AGENT_SERVER_ENV
environment variable, which should contain a JSON string mapping variable names
to their values.
Usage:
Set OH_AGENT_SERVER_ENV to a JSON string:
OH_AGENT_SERVER_ENV='{"DEBUG": "true", "LOG_LEVEL": "info", "CUSTOM_VAR": "value"}'
This will inject the following environment variables into all sandbox environments:
- DEBUG=true
- LOG_LEVEL=info
- CUSTOM_VAR=value
Returns:
dict[str, str]: Dictionary of environment variable names to values.
Returns empty dict if OH_AGENT_SERVER_ENV is not set or invalid.
Raises:
JSONDecodeError: If OH_AGENT_SERVER_ENV contains invalid JSON.
"""
return env_parser.from_env(dict[str, str], 'OH_AGENT_SERVER_ENV')