Fix permissions issue in docker Sandbox (#11549)

This commit is contained in:
Tim O'Farrell
2025-10-28 14:24:54 -06:00
committed by GitHub
parent 7447cfdb3d
commit fccc6f3196
10 changed files with 58 additions and 60 deletions

30
enterprise/poetry.lock generated
View File

@@ -5737,7 +5737,7 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]] [[package]]
name = "openhands-agent-server" name = "openhands-agent-server"
version = "1.0.0a3" version = "1.0.0a4"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent" description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
@@ -5758,9 +5758,9 @@ wsproto = ">=1.2.0"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/All-Hands-AI/agent-sdk.git" url = "https://github.com/OpenHands/agent-sdk.git"
reference = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e" reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
resolved_reference = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e" resolved_reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
subdirectory = "openhands-agent-server" subdirectory = "openhands-agent-server"
[[package]] [[package]]
@@ -5805,9 +5805,9 @@ memory-profiler = "^0.61.0"
numpy = "*" numpy = "*"
openai = "1.99.9" openai = "1.99.9"
openhands-aci = "0.3.2" openhands-aci = "0.3.2"
openhands-agent-server = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e", subdirectory = "openhands-agent-server"} openhands-agent-server = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "ce0a71af55dfce101f7419fbdb0116178f01e109", subdirectory = "openhands-agent-server"}
openhands-sdk = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e", subdirectory = "openhands-sdk"} openhands-sdk = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "ce0a71af55dfce101f7419fbdb0116178f01e109", subdirectory = "openhands-sdk"}
openhands-tools = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e", subdirectory = "openhands-tools"} openhands-tools = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "ce0a71af55dfce101f7419fbdb0116178f01e109", subdirectory = "openhands-tools"}
opentelemetry-api = "^1.33.1" opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1" opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1" pathspec = "^0.12.1"
@@ -5863,7 +5863,7 @@ url = ".."
[[package]] [[package]]
name = "openhands-sdk" name = "openhands-sdk"
version = "1.0.0a3" version = "1.0.0a4"
description = "OpenHands SDK - Core functionality for building AI agents" description = "OpenHands SDK - Core functionality for building AI agents"
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
@@ -5886,14 +5886,14 @@ boto3 = ["boto3 (>=1.35.0)"]
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/All-Hands-AI/agent-sdk.git" url = "https://github.com/OpenHands/agent-sdk.git"
reference = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e" reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
resolved_reference = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e" resolved_reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
subdirectory = "openhands-sdk" subdirectory = "openhands-sdk"
[[package]] [[package]]
name = "openhands-tools" name = "openhands-tools"
version = "1.0.0a3" version = "1.0.0a4"
description = "OpenHands Tools - Runtime tools for AI agents" description = "OpenHands Tools - Runtime tools for AI agents"
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
@@ -5913,9 +5913,9 @@ pydantic = ">=2.11.7"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/All-Hands-AI/agent-sdk.git" url = "https://github.com/OpenHands/agent-sdk.git"
reference = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e" reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
resolved_reference = "8d8134ca5a87cc3e90e3ff968327a7f4c961e22e" resolved_reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
subdirectory = "openhands-tools" subdirectory = "openhands-tools"
[[package]] [[package]]

View File

@@ -12,8 +12,8 @@ from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartTask, AppConversationStartTask,
) )
from openhands.app_server.services.injector import Injector from openhands.app_server.services.injector import Injector
from openhands.sdk import Workspace
from openhands.sdk.utils.models import DiscriminatedUnionMixin from openhands.sdk.utils.models import DiscriminatedUnionMixin
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
class AppConversationService(ABC): class AppConversationService(ABC):
@@ -90,8 +90,7 @@ class AppConversationService(ABC):
async def run_setup_scripts( async def run_setup_scripts(
self, self,
task: AppConversationStartTask, task: AppConversationStartTask,
workspace: Workspace, workspace: AsyncRemoteWorkspace,
working_dir: str,
) -> AsyncGenerator[AppConversationStartTask, None]: ) -> AsyncGenerator[AppConversationStartTask, None]:
"""Run the setup scripts for the project and yield status updates""" """Run the setup scripts for the project and yield status updates"""
yield task yield task

View File

@@ -36,35 +36,36 @@ class GitAppConversationService(AppConversationService, ABC):
self, self,
task: AppConversationStartTask, task: AppConversationStartTask,
workspace: AsyncRemoteWorkspace, workspace: AsyncRemoteWorkspace,
working_dir: str,
) -> AsyncGenerator[AppConversationStartTask, None]: ) -> AsyncGenerator[AppConversationStartTask, None]:
task.status = AppConversationStartTaskStatus.PREPARING_REPOSITORY task.status = AppConversationStartTaskStatus.PREPARING_REPOSITORY
yield task yield task
await self.clone_or_init_git_repo(task, workspace, working_dir) await self.clone_or_init_git_repo(task, workspace)
task.status = AppConversationStartTaskStatus.RUNNING_SETUP_SCRIPT task.status = AppConversationStartTaskStatus.RUNNING_SETUP_SCRIPT
yield task yield task
await self.maybe_run_setup_script(workspace, working_dir) await self.maybe_run_setup_script(workspace)
task.status = AppConversationStartTaskStatus.SETTING_UP_GIT_HOOKS task.status = AppConversationStartTaskStatus.SETTING_UP_GIT_HOOKS
yield task yield task
await self.maybe_setup_git_hooks(workspace, working_dir) await self.maybe_setup_git_hooks(workspace)
async def clone_or_init_git_repo( async def clone_or_init_git_repo(
self, self,
task: AppConversationStartTask, task: AppConversationStartTask,
workspace: AsyncRemoteWorkspace, workspace: AsyncRemoteWorkspace,
working_dir: str,
): ):
request = task.request request = task.request
if not request.selected_repository: if not request.selected_repository:
if self.init_git_in_empty_workspace: if self.init_git_in_empty_workspace:
_logger.debug('Initializing a new git repository in the workspace.') _logger.debug('Initializing a new git repository in the workspace.')
await workspace.execute_command( cmd = (
'git init && git config --global --add safe.directory ' 'git init && git config --global '
+ working_dir f'--add safe.directory {workspace.working_dir}'
) )
result = await workspace.execute_command(cmd, workspace.working_dir)
if result.exit_code:
_logger.warning(f'Git init failed: {result.stderr}')
else: else:
_logger.info('Not initializing a new git repository.') _logger.info('Not initializing a new git repository.')
return return
@@ -79,7 +80,8 @@ class GitAppConversationService(AppConversationService, ABC):
# Clone the repo - this is the slow part! # Clone the repo - this is the slow part!
clone_command = f'git clone {remote_repo_url} {dir_name}' clone_command = f'git clone {remote_repo_url} {dir_name}'
await workspace.execute_command(clone_command, working_dir) result = await workspace.execute_command(clone_command, workspace.working_dir)
print(result)
# Checkout the appropriate branch # Checkout the appropriate branch
if request.selected_branch: if request.selected_branch:
@@ -89,15 +91,14 @@ class GitAppConversationService(AppConversationService, ABC):
random_str = base62.encodebytes(os.urandom(16)) random_str = base62.encodebytes(os.urandom(16))
openhands_workspace_branch = f'openhands-workspace-{random_str}' openhands_workspace_branch = f'openhands-workspace-{random_str}'
checkout_command = f'git checkout -b {openhands_workspace_branch}' checkout_command = f'git checkout -b {openhands_workspace_branch}'
await workspace.execute_command(checkout_command, working_dir) await workspace.execute_command(checkout_command, workspace.working_dir)
async def maybe_run_setup_script( async def maybe_run_setup_script(
self, self,
workspace: AsyncRemoteWorkspace, workspace: AsyncRemoteWorkspace,
working_dir: str,
): ):
"""Run .openhands/setup.sh if it exists in the workspace or repository.""" """Run .openhands/setup.sh if it exists in the workspace or repository."""
setup_script = working_dir + '/.openhands/setup.sh' setup_script = workspace.working_dir + '/.openhands/setup.sh'
await workspace.execute_command( await workspace.execute_command(
f'chmod +x {setup_script} && source {setup_script}', timeout=600 f'chmod +x {setup_script} && source {setup_script}', timeout=600
@@ -111,11 +112,10 @@ class GitAppConversationService(AppConversationService, ABC):
async def maybe_setup_git_hooks( async def maybe_setup_git_hooks(
self, self,
workspace: AsyncRemoteWorkspace, workspace: AsyncRemoteWorkspace,
working_dir: str,
): ):
"""Set up git hooks if .openhands/pre-commit.sh exists in the workspace or repository.""" """Set up git hooks if .openhands/pre-commit.sh exists in the workspace or repository."""
command = 'mkdir -p .git/hooks && chmod +x .openhands/pre-commit.sh' command = 'mkdir -p .git/hooks && chmod +x .openhands/pre-commit.sh'
result = await workspace.execute_command(command, working_dir) result = await workspace.execute_command(command, workspace.working_dir)
if result.exit_code: if result.exit_code:
return return
@@ -131,7 +131,9 @@ class GitAppConversationService(AppConversationService, ABC):
f'mv {PRE_COMMIT_HOOK} {PRE_COMMIT_LOCAL} &&' f'mv {PRE_COMMIT_HOOK} {PRE_COMMIT_LOCAL} &&'
f'chmod +x {PRE_COMMIT_LOCAL}' f'chmod +x {PRE_COMMIT_LOCAL}'
) )
result = await workspace.execute_command(command, working_dir) result = await workspace.execute_command(
command, workspace.working_dir
)
if result.exit_code != 0: if result.exit_code != 0:
_logger.error( _logger.error(
f'Failed to preserve existing pre-commit hook: {result.stderr}', f'Failed to preserve existing pre-commit hook: {result.stderr}',

View File

@@ -181,11 +181,11 @@ class LiveStatusAppConversationService(GitAppConversationService):
# Run setup scripts # Run setup scripts
workspace = AsyncRemoteWorkspace( workspace = AsyncRemoteWorkspace(
host=agent_server_url, api_key=sandbox.session_api_key host=agent_server_url,
api_key=sandbox.session_api_key,
working_dir=sandbox_spec.working_dir,
) )
async for updated_task in self.run_setup_scripts( async for updated_task in self.run_setup_scripts(task, workspace):
task, workspace, sandbox_spec.working_dir
):
yield updated_task yield updated_task
# Build the start request # Build the start request

View File

@@ -40,10 +40,10 @@ def get_default_sandbox_specs():
'OPENVSCODE_SERVER_ROOT': '/openhands/.openvscode-server', 'OPENVSCODE_SERVER_ROOT': '/openhands/.openvscode-server',
'OH_ENABLE_VNC': '0', 'OH_ENABLE_VNC': '0',
'LOG_JSON': 'true', 'LOG_JSON': 'true',
'OH_CONVERSATIONS_PATH': '/home/openhands/conversations', 'OH_CONVERSATIONS_PATH': '/workspace/conversations',
'OH_BASH_EVENTS_DIR': '/home/openhands/bash_events', 'OH_BASH_EVENTS_DIR': '/workspace/bash_events',
}, },
working_dir='/home/openhands/workspace', working_dir='/workspace/project',
) )
] ]

View File

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

View File

@@ -21,7 +21,7 @@ class EncryptionKey(BaseModel):
@field_serializer('key') @field_serializer('key')
def serialize_key(self, key: SecretStr, info: Any): def serialize_key(self, key: SecretStr, info: Any):
"""Conditionally serialize the key based on context.""" """Conditionally serialize the key based on context."""
if info.context and info.context.get('reveal_secrets'): if info.context and info.context.get('expose_secrets'):
return key.get_secret_value() return key.get_secret_value()
return str(key) # Returns '**********' by default return str(key) # Returns '**********' by default

25
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]] [[package]]
name = "aiofiles" name = "aiofiles"
@@ -5711,11 +5711,8 @@ files = [
{file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"},
{file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"},
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"},
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"},
{file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"},
{file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"},
@@ -7275,7 +7272,7 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]] [[package]]
name = "openhands-agent-server" name = "openhands-agent-server"
version = "1.0.0a3" version = "1.0.0a4"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent" description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
@@ -7297,13 +7294,13 @@ wsproto = ">=1.2.0"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/OpenHands/agent-sdk.git" url = "https://github.com/OpenHands/agent-sdk.git"
reference = "93b481c50fab2bb45e6065606219155119d35656" reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
resolved_reference = "93b481c50fab2bb45e6065606219155119d35656" resolved_reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
subdirectory = "openhands-agent-server" subdirectory = "openhands-agent-server"
[[package]] [[package]]
name = "openhands-sdk" name = "openhands-sdk"
version = "1.0.0a3" version = "1.0.0a4"
description = "OpenHands SDK - Core functionality for building AI agents" description = "OpenHands SDK - Core functionality for building AI agents"
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
@@ -7327,13 +7324,13 @@ boto3 = ["boto3 (>=1.35.0)"]
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/OpenHands/agent-sdk.git" url = "https://github.com/OpenHands/agent-sdk.git"
reference = "93b481c50fab2bb45e6065606219155119d35656" reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
resolved_reference = "93b481c50fab2bb45e6065606219155119d35656" resolved_reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
subdirectory = "openhands-sdk" subdirectory = "openhands-sdk"
[[package]] [[package]]
name = "openhands-tools" name = "openhands-tools"
version = "1.0.0a3" version = "1.0.0a4"
description = "OpenHands Tools - Runtime tools for AI agents" description = "OpenHands Tools - Runtime tools for AI agents"
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
@@ -7354,8 +7351,8 @@ pydantic = ">=2.11.7"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/OpenHands/agent-sdk.git" url = "https://github.com/OpenHands/agent-sdk.git"
reference = "93b481c50fab2bb45e6065606219155119d35656" reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
resolved_reference = "93b481c50fab2bb45e6065606219155119d35656" resolved_reference = "ce0a71af55dfce101f7419fbdb0116178f01e109"
subdirectory = "openhands-tools" subdirectory = "openhands-tools"
[[package]] [[package]]
@@ -16524,4 +16521,4 @@ third-party-runtimes = ["daytona", "e2b-code-interpreter", "modal", "runloop-api
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12,<3.14" python-versions = "^3.12,<3.14"
content-hash = "b8620f03973119b97edf2ce1d44e4d8706cb2ecf155710bc8e2094daa766d139" content-hash = "aed9fa5020f1fdda19cf8191ac75021f2617e10e49757bcec23586b2392fd596"

View File

@@ -113,9 +113,9 @@ e2b-code-interpreter = { version = "^2.0.0", optional = true }
pybase62 = "^1.0.0" pybase62 = "^1.0.0"
# V1 dependencies # V1 dependencies
openhands-agent-server = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-agent-server", rev = "93b481c50fab2bb45e6065606219155119d35656" } openhands-agent-server = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-agent-server", rev = "ce0a71af55dfce101f7419fbdb0116178f01e109" }
openhands-sdk = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-sdk", rev = "93b481c50fab2bb45e6065606219155119d35656" } openhands-sdk = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-sdk", rev = "ce0a71af55dfce101f7419fbdb0116178f01e109" }
openhands-tools = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-tools", rev = "93b481c50fab2bb45e6065606219155119d35656" } openhands-tools = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-tools", rev = "ce0a71af55dfce101f7419fbdb0116178f01e109" }
python-jose = { version = ">=3.3", extras = [ "cryptography" ] } python-jose = { version = ">=3.3", extras = [ "cryptography" ] }
sqlalchemy = { extras = [ "asyncio" ], version = "^2.0.40" } sqlalchemy = { extras = [ "asyncio" ], version = "^2.0.40" }
pg8000 = "^1.31.5" pg8000 = "^1.31.5"

View File

@@ -365,7 +365,7 @@ class TestDockerSandboxSpecServiceInjector:
assert 'OPENVSCODE_SERVER_ROOT' in specs[0].initial_env assert 'OPENVSCODE_SERVER_ROOT' in specs[0].initial_env
assert 'OH_ENABLE_VNC' in specs[0].initial_env assert 'OH_ENABLE_VNC' in specs[0].initial_env
assert 'LOG_JSON' in specs[0].initial_env assert 'LOG_JSON' in specs[0].initial_env
assert specs[0].working_dir == '/home/openhands/workspace' assert specs[0].working_dir == '/workspace/project'
@patch( @patch(
'openhands.app_server.sandbox.docker_sandbox_spec_service._global_docker_client', 'openhands.app_server.sandbox.docker_sandbox_spec_service._global_docker_client',