Skip to content

Conversation

@Co1lin
Copy link
Contributor

@Co1lin Co1lin commented Jan 7, 2026

Summary

  • Previously, if we want to mount a host dir into the dockerworkspace, we use the parameter mount_dir, but this leads to the following issues:
    • We cannot mount multiple dirs into the docker.
    • The host dir is hardcoded to be mounted as /workspace in the container. However, OpenHands writes to this directory, such as saving conversations to /workspace/conversations.
      • If the host dir gives no write permission to "others", then OpenHands will encounter an exception and exit, because it cannot make the dir /workspace/conversations in the container.
      • If the host dir gives write permission to "others", then OpenHands will create the dir above in the container; but on the host, we cannot remove the dir anymore because host_dir / conversations are owned by the user in the docker.
  • This PR proposes to
    • We support custom dirs mounting. In the following example, we can mount the codebase to analyze into the container as a read-only one to prevent any editing.
    • With custom dirs mounting, we avoid reusing /workspace, bypassing the permission issues mentioned above.
      • By separating the codebase mounted into the container out of /workspace, when we instruct the agent to work on it, it will not read OpenHands files like the ones in /workspace/conversations, avoiding interference. For example, I observed that if we mount dirs in the old way, i.e., putting codebases in /workspace, then when the agent performs searching using grep, it will see the conversation history stored in /workspace, because /workspace is the working dir, which could be very messy and confusing for the models.

Welcome comments before I moving to make this PR more complete, such as implementing test cases!

Example Code:

import asyncio
from pathlib import Path
from openhands.sdk import (
    LLM,
    Conversation,
    RemoteConversation,
)
from openhands.tools.preset.default import get_default_agent
from openhands.workspace import DockerWorkspace


llm = LLM(
    model="openai/gpt-5.1-codex-mini",
)

agent = get_default_agent(llm=llm, cli_mode=True)


async def work(workspace: DockerWorkspace, code_path: str) -> RemoteConversation:
    loop = asyncio.get_event_loop()

    conversation = Conversation(agent=agent, workspace=workspace)

    def run():
        conversation.send_message(
            f"Read the code in {code_path} and summarize it. You should respond with a report in markdown format in your last message. Your report should only focus on the code in {code_path} without information about other files. Your report should be concise and clear. Do NOT change or write any files into the environment."
        )
        conversation.run()

    await loop.run_in_executor(None, run)  # run()
    return conversation


if __name__ == "__main__":
    host_dir = Path(__file__).parent / "z_code_test"
    src_dir = host_dir / "src"
    src_dir.mkdir(parents=True, exist_ok=True)
    (src_dir / "code_0.py").write_text("print('Hello, world!')\n")
    (src_dir / "code_1.py").write_text("print('Goodbye, world!')\n")
    
    with DockerWorkspace(
        server_image="ghcr.io/openhands/agent-server:latest-python",
        host_port=8010,
        # mount_dir=str(host_dir.resolve()), # BEFORE: mount host_dir to /workspace
        volumes=[f"{src_dir.resolve()}:/mounted_src:ro"], # NOW: coustomized volumes mounting: mount src_dir to /mounted_src as read-only as specified
        working_dir='/mounted_src',
    ) as workspace:

        asyncio.run(work(workspace, "code_0.py"))

Checklist

  • If the PR is changing/adding functionality, are there tests to reflect this?
  • If there is an example, have you run the example to make sure that it works?
  • If there are instructions on how to run the code, have you followed the instructions and made sure that it works?
  • If the feature is significant enough to require documentation, is there a PR open on the OpenHands/docs repository with the same branch name?
  • Is the github CI passing?

Copilot AI review requested due to automatic review settings January 7, 2026 02:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the single mount_dir field with a more flexible volumes list field in DockerWorkspace, enabling users to mount multiple volumes with custom configurations. Additionally, it sets a default value for server_image.

Key changes:

  • Replaced mount_dir: str | None with volumes: list[str] to support multiple volume mounts
  • Changed server_image default from None to "ghcr.io/openhands/agent-server:latest-python"
  • Updated volume mounting logic to iterate through the volumes list

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution! I believe we did have support in OpenHands V0 for multiple volumes, so it seems didn't port it to V1

I'll start CI, and maybe it's worth noting, we have a deprecation mechanism in the SDK that we could use.

@Co1lin
Copy link
Contributor Author

Co1lin commented Jan 7, 2026

Thanks @enyst ! Actually I have a question on the testing part of OpenHands.
I tried to run pytest to verify if my modification breaks anything. However, even on the main branch, running uv run pytest test/sdk leads to failed test cases.

`uv run pytest tests/sdk`
====================================================== FAILURES =======================================================
_______________________________________________ test_fifo_lock_fairness _______________________________________________
tests/sdk/conversation/test_fifo_lock.py:142: in test_fifo_lock_fairness
    assert actual_order == expected_order, (
E   AssertionError: Expected FIFO order [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], got [0, 2, 1, 4, 3, 5, 6, 7, 8, 9]
E   assert [0, 2, 1, 4, 3, 5, ...] == [0, 1, 2, 3, 4, 5, ...]
E     
E     At index 1 diff: 2 != 1
E     
E     Full diff:
E       [
E           0,
E     +     2,...
E     
E     ...Full output truncated (13 lines hidden), use '-vv' to show
_______________________________________ test_tool_serialization_deserialization _______________________________________
tests/sdk/tool/test_tool_serialization.py:22: in test_tool_serialization_deserialization
    deserialized_tool = ToolDefinition.model_validate_json(tool_json)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   pydantic_core._pydantic_core.ValidationError: 1 validation error for ToolDefinition
E   observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='FinishObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
_______________________________ test_tool_supports_polymorphic_field_json_serialization _______________________________
tests/sdk/tool/test_tool_serialization.py:44: in test_tool_supports_polymorphic_field_json_serialization
    deserialized_container = Container.model_validate_json(container_json)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   pydantic_core._pydantic_core.ValidationError: 1 validation error for Container
E   tool.observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='FinishObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
______________________________ test_tool_supports_nested_polymorphic_json_serialization _______________________________
tests/sdk/tool/test_tool_serialization.py:68: in test_tool_supports_nested_polymorphic_json_serialization
    deserialized_container = NestedContainer.model_validate_json(container_json)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   pydantic_core._pydantic_core.ValidationError: 2 validation errors for NestedContainer
E   tools.0.observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='FinishObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
E   tools.1.observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='ThinkObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
_________________________________________ test_tool_model_validate_json_dict __________________________________________
tests/sdk/tool/test_tool_serialization.py:89: in test_tool_model_validate_json_dict
    deserialized_tool = ToolDefinition.model_validate(tool_dict)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   pydantic_core._pydantic_core.ValidationError: 1 validation error for ToolDefinition
E   observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='FinishObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
________________________________________ test_tool_type_annotation_works_json _________________________________________
tests/sdk/tool/test_tool_serialization.py:130: in test_tool_type_annotation_works_json
    deserialized_model = TestModel.model_validate_json(model_json)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   pydantic_core._pydantic_core.ValidationError: 1 validation error for TestModel
E   tool.observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='FinishObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
______________________________________________ test_tool_kind_field_json ______________________________________________
tests/sdk/tool/test_tool_serialization.py:152: in test_tool_kind_field_json
    deserialized_tool = ToolDefinition.model_validate_json(tool_json)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   pydantic_core._pydantic_core.ValidationError: 1 validation error for ToolDefinition
E   observation_type
E     Value error, Local classes not supported! tests.sdk.tool.test_tool_definition.StrictObservation / openhands.sdk.tool.schema.Observation (Since they may not exist at deserialization time) [type=value_error, input_value='FinishObservation', input_type=str]
E       For further information visit https://errors.pydantic.dev/2.12/v/value_error
================================================== warnings summary ===================================================
tests/sdk/agent/test_non_executable_action_emission.py::test_emits_action_event_with_none_action_then_error_on_missing_tool
tests/sdk/agent/test_nonexistent_tool_handling.py::test_nonexistent_tool_returns_error_and_continues_conversation
tests/sdk/agent/test_nonexistent_tool_handling.py::test_nonexistent_tool_error_includes_available_tools
tests/sdk/agent/test_nonexistent_tool_handling.py::test_conversation_continues_after_tool_error
tests/sdk/agent/test_nonexistent_tool_handling.py::test_conversation_continues_after_tool_error
tests/sdk/agent/test_security_policy_integration.py::test_security_risk_param_ignored_when_no_analyzer
tests/sdk/agent/test_tool_execution_error_handling.py::test_tool_execution_valueerror_returns_error_event
tests/sdk/agent/test_tool_execution_error_handling.py::test_conversation_continues_after_tool_execution_error
tests/sdk/agent/test_tool_execution_error_handling.py::test_conversation_continues_after_tool_execution_error
  /home/colin/code/oh-software-agent-sdk/openhands-sdk/openhands/sdk/llm/utils/telemetry.py:244: UserWarning: Cost calculation failed: litellm.BadRequestError: LLM Provider NOT provided. Pass in the LLM provider you are trying to call. You passed model=test-model
   Pass model as E.g. For 'Huggingface' inference endpoints pass in `completion(model='huggingface/starcoder',..)` Learn more: https://docs.litellm.ai/docs/providers
    warnings.warn(f"Cost calculation failed: {e}")

tests/sdk/llm/test_llm.py::test_unmapped_model_with_logging_enabled
  /home/colin/code/oh-software-agent-sdk/openhands-sdk/openhands/sdk/llm/utils/telemetry.py:244: UserWarning: Cost calculation failed: litellm.BadRequestError: LLM Provider NOT provided. Pass in the LLM provider you are trying to call. You passed model=UnmappedTestModel
   Pass model as E.g. For 'Huggingface' inference endpoints pass in `completion(model='huggingface/starcoder',..)` Learn more: https://docs.litellm.ai/docs/providers
    warnings.warn(f"Cost calculation failed: {e}")

tests/sdk/llm/test_llm_litellm_extra_body.py::test_responses_forwards_extra_body_for_all_models
  /home/colin/code/oh-software-agent-sdk/openhands-sdk/openhands/sdk/llm/utils/telemetry.py:244: UserWarning: Cost calculation failed: litellm.BadRequestError: LLM Provider NOT provided. Pass in the LLM provider you are trying to call. You passed model=llama-3
   Pass model as E.g. For 'Huggingface' inference endpoints pass in `completion(model='huggingface/starcoder',..)` Learn more: https://docs.litellm.ai/docs/providers
    warnings.warn(f"Cost calculation failed: {e}")

tests/sdk/llm/test_telemetry_policy.py::test_responses_forwards_extra_body_for_all_models
  /home/colin/code/oh-software-agent-sdk/openhands-sdk/openhands/sdk/llm/utils/telemetry.py:244: UserWarning: Cost calculation failed: litellm.BadRequestError: LLM Provider NOT provided. Pass in the LLM provider you are trying to call. You passed model=llama-3.3-70b
   Pass model as E.g. For 'Huggingface' inference endpoints pass in `completion(model='huggingface/starcoder',..)` Learn more: https://docs.litellm.ai/docs/providers
    warnings.warn(f"Cost calculation failed: {e}")

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=============================================== short test summary info ===============================================
FAILED tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_fairness - AssertionError: Expected FIFO order [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], got [0, 2, 1, 4, 3, 5, 6, 7, 8, 9]
FAILED tests/sdk/tool/test_tool_serialization.py::test_tool_serialization_deserialization - pydantic_core._pydantic_core.ValidationError: 1 validation error for ToolDefinition
FAILED tests/sdk/tool/test_tool_serialization.py::test_tool_supports_polymorphic_field_json_serialization - pydantic_core._pydantic_core.ValidationError: 1 validation error for Container
FAILED tests/sdk/tool/test_tool_serialization.py::test_tool_supports_nested_polymorphic_json_serialization - pydantic_core._pydantic_core.ValidationError: 2 validation errors for NestedContainer
FAILED tests/sdk/tool/test_tool_serialization.py::test_tool_model_validate_json_dict - pydantic_core._pydantic_core.ValidationError: 1 validation error for ToolDefinition
FAILED tests/sdk/tool/test_tool_serialization.py::test_tool_type_annotation_works_json - pydantic_core._pydantic_core.ValidationError: 1 validation error for TestModel
FAILED tests/sdk/tool/test_tool_serialization.py::test_tool_kind_field_json - pydantic_core._pydantic_core.ValidationError: 1 validation error for ToolDefinition
==================================== 7 failed, 1616 passed, 12 warnings in 53.08s =====================================

But if I run the single first failed test case above, it can pass:

uv run pytest tests/sdk/conversation/test_fifo_lock.py
==================================== test session starts ====================================
platform linux -- Python 3.12.3, pytest-8.4.2, pluggy-1.6.0 -- /home/colin/code/oh-software-agent-sdk/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/colin/code/oh-software-agent-sdk
configfile: pyproject.toml
plugins: asyncio-1.2.0, forked-1.6.0, timeout-2.4.0, libtmux-0.53.0, anyio-4.11.0, xdist-3.8.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 7 items                                                                           

tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_basic_functionality PASSED   [ 14%]
tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_context_manager PASSED       [ 28%]
tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_non_blocking PASSED          [ 42%]
tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_timeout PASSED               [ 57%]
tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_fairness PASSED              [ 71%]
tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_error_handling PASSED        [ 85%]
tests/sdk/conversation/test_fifo_lock.py::test_fifo_lock_stress_test PASSED           [100%]

===================================== 7 passed in 0.17s =====================================

I wonder if there is anything wrong with my usage? Because I see the CI tests can pass. Got confused here.

@enyst
Copy link
Collaborator

enyst commented Jan 7, 2026

FWIW I see in your log

Value error, Local classes not supported!

... This is surprising, but FWIW we just fixed something like that, maybe it makes sense to update main branch. (but I don't fully understand, it wasn't broken on main 🤔)

The FIFO test failure smells like maybe it could be a bug in the tests, that make it depend on order, ugh.

I wouldn't worry about your setup TBH, just maybe you could review the readme instructions to make sure you're only using uv and its venv (but I seem to see that was the case)

@Co1lin
Copy link
Contributor Author

Co1lin commented Jan 7, 2026

Still facing the similar issue on the main branch. Created a new issue here #1629 .

if self.__class__ is DockerWorkspace and self.mount_dir:
warn_deprecated(
"DockerWorkspace.mount_dir",
deprecated_in="1.9.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingyaoww Hi, thanks for your feedbacks!

  • Is this what you expect? I am actually a bit confused by how to use the deprecation for an class attribute as I did not see much other usages in the repo.
  • Which version number should we use for deprecated_in?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's deprecate it in 1.10.0 since it is already 1.8.1 now.

Shall we just do if self.mount_dir:?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we just do if self.mount_dir:?

I am just copying line 129 above in the previous model validator, without knowing why self.__class__ is DockerWorkspace is needed. Do you want to change both?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OpenHands do you think this is an ideal thing to do?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on it! xingyaoww can track my progress at all-hands.dev

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

I addressed @xingyaoww's question about whether using self.__class__ is DockerWorkspace in _validate_mount_dir is ideal.

Answer: No, it's not ideal.

The key difference between the two validators:

  1. _validate_server_image: The self.__class__ is DockerWorkspace check is appropriate because subclasses like DockerDevWorkspace may have different image resolution logic (e.g., using base_image instead of server_image).

  2. _validate_mount_dir: The class check is problematic because:

    • The deprecation warning should apply to all users of mount_dir, including subclasses
    • The conversion from mount_dir to volumes must happen for all classes, otherwise the mount would silently fail for subclasses like DockerDevWorkspace

Changes Made

I updated openhands-workspace/openhands/workspace/docker/workspace.py:

  • Simplified the condition from if self.__class__ is DockerWorkspace and self.mount_dir: to if self.mount_dir:
  • Updated deprecated_in version from "1.9.0" to "1.10.0" per @xingyaoww's earlier feedback

The changes have been pushed to the PR branch at #1618

View full conversation

@Co1lin Co1lin requested a review from xingyaoww January 9, 2026 19:25
openhands-agent and others added 2 commits January 10, 2026 01:03
- Remove unnecessary `self.__class__ is DockerWorkspace` check from
  _validate_mount_dir validator. Unlike _validate_server_image, the
  mount_dir deprecation and conversion to volumes should apply to all
  subclasses (e.g., DockerDevWorkspace) to ensure mounts work correctly.
- Update deprecated_in version from 1.9.0 to 1.10.0 per review feedback.

Co-authored-by: openhands <[email protected]>
Copy link
Collaborator

@xingyaoww xingyaoww left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@xingyaoww xingyaoww enabled auto-merge (squash) January 12, 2026 17:09
@xingyaoww xingyaoww merged commit efb5105 into OpenHands:main Jan 12, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants