Просмотр исходного кода

Document various runtimes (#4536)

Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Robert Brennan 1 год назад
Родитель
Сommit
2e50a5bef5

+ 1 - 1
.github/workflows/ghcr-build.yml

@@ -1,5 +1,5 @@
 # Workflow that builds, tests and then pushes the OpenHands and runtime docker images to the ghcr.io repository
 # Workflow that builds, tests and then pushes the OpenHands and runtime docker images to the ghcr.io repository
-name: Build, Test and Publish RT Image
+name: Docker
 
 
 # Always run on "main"
 # Always run on "main"
 # Always run on tags
 # Always run on tags

+ 11 - 13
README.md

@@ -40,30 +40,28 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
 system requirements and more information.
 system requirements and more information.
 
 
 ```bash
 ```bash
-export WORKSPACE_BASE=$(pwd)/workspace
+docker pull docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik
 
 
-docker pull ghcr.io/all-hands-ai/runtime:0.11-nikolaik
-
-docker run -it --pull=always \
-    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.11-nikolaik \
-    -e SANDBOX_USER_ID=$(id -u) \
-    -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-    -v $WORKSPACE_BASE:/opt/workspace_base \
+docker run -it --rm --pull=always \
+    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -p 3000:3000 \
     -p 3000:3000 \
     --add-host host.docker.internal:host-gateway \
     --add-host host.docker.internal:host-gateway \
-    --name openhands-app-$(date +%Y%m%d%H%M%S) \
-    ghcr.io/all-hands-ai/openhands:0.11
+    --name openhands-app \
+    docker.all-hands.dev/all-hands-ai/openhands:0.11
 ```
 ```
 
 
 You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
 You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
 
 
-You'll need a model provider and API key. One option that works well: [Claude 3.5 Sonnet](https://www.anthropic.com/api), but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
+You'll need a model provider and API key.
+[Anthropic's Claude 3.5 Sonnet (`anthropic/claude-3-5-sonnet-20241022`)](https://www.anthropic.com/api)
+works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
 
 
 ---
 ---
 
 
-You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
-or as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode).
+You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes),
+run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
+or interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode).
 
 
 Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
 Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
 
 

+ 1 - 0
containers/app/Dockerfile

@@ -41,6 +41,7 @@ ENV SANDBOX_LOCAL_RUNTIME_URL=http://host.docker.internal
 ENV USE_HOST_NETWORK=false
 ENV USE_HOST_NETWORK=false
 ENV WORKSPACE_BASE=/opt/workspace_base
 ENV WORKSPACE_BASE=/opt/workspace_base
 ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
 ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
+ENV SANDBOX_USER_ID=0
 RUN mkdir -p $WORKSPACE_BASE
 RUN mkdir -p $WORKSPACE_BASE
 
 
 RUN apt-get update -y \
 RUN apt-get update -y \

+ 5 - 0
containers/app/entrypoint.sh

@@ -18,6 +18,11 @@ if [ -z "$SANDBOX_USER_ID" ]; then
   exit 1
   exit 1
 fi
 fi
 
 
+if [ -z "$WORKSPACE_MOUNT_PATH" ]; then
+  # This is set to /opt/workspace in the Dockerfile. But if the user isn't mounting, we want to unset it so that OpenHands doesn't mount at all
+  unset WORKSPACE_BASE
+fi
+
 if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
 if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
   echo "Running OpenHands as root"
   echo "Running OpenHands as root"
   export RUN_AS_OPENHANDS=false
   export RUN_AS_OPENHANDS=false

+ 1 - 1
docs/modules/usage/how-to/cli-mode.md

@@ -57,7 +57,7 @@ docker run -it \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -v /var/run/docker.sock:/var/run/docker.sock \
     --add-host host.docker.internal:host-gateway \
     --add-host host.docker.internal:host-gateway \
     --name openhands-app-$(date +%Y%m%d%H%M%S) \
     --name openhands-app-$(date +%Y%m%d%H%M%S) \
-    ghcr.io/all-hands-ai/openhands:0.11 \
+    docker.all-hands.dev/all-hands-ai/openhands:0.11 \
     python -m openhands.core.cli
     python -m openhands.core.cli
 ```
 ```
 
 

+ 1 - 1
docs/modules/usage/how-to/headless-mode.md

@@ -51,6 +51,6 @@ docker run -it \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -v /var/run/docker.sock:/var/run/docker.sock \
     --add-host host.docker.internal:host-gateway \
     --add-host host.docker.internal:host-gateway \
     --name openhands-app-$(date +%Y%m%d%H%M%S) \
     --name openhands-app-$(date +%Y%m%d%H%M%S) \
-    ghcr.io/all-hands-ai/openhands:0.11 \
+    docker.all-hands.dev/all-hands-ai/openhands:0.11 \
     python -m openhands.core.main -t "write a bash script that prints hi"
     python -m openhands.core.main -t "write a bash script that prints hi"
 ```
 ```

+ 6 - 6
docs/modules/usage/how-to/openshift-example.md

@@ -150,7 +150,7 @@ metadata:
 spec:
 spec:
   containers:
   containers:
   - name: openhands-app-2024
   - name: openhands-app-2024
-    image: ghcr.io/all-hands-ai/openhands:main
+    image: docker.all-hands.dev/all-hands-ai/openhands:main
     env:
     env:
     - name: SANDBOX_USER_ID
     - name: SANDBOX_USER_ID
       value: "1000"
       value: "1000"
@@ -164,7 +164,7 @@ spec:
     ports:
     ports:
     - containerPort: 3000
     - containerPort: 3000
   - name: openhands-sandbox-2024
   - name: openhands-sandbox-2024
-    image: ghcr.io/all-hands-ai/sandbox:main
+    image: docker.all-hands.dev/all-hands-ai/runtime:main
     ports:
     ports:
     - containerPort: 51963
     - containerPort: 51963
     command: ["/usr/sbin/sshd", "-D", "-p 51963", "-o", "PermitRootLogin=yes"]
     command: ["/usr/sbin/sshd", "-D", "-p 51963", "-o", "PermitRootLogin=yes"]
@@ -205,10 +205,10 @@ LAST SEEN   TYPE     REASON                   OBJECT
 9s          Normal   SuccessfulAttachVolume   pod/openhands-app-2024                AttachVolume.Attach succeeded for volume "pvc-2b1d223a-1c8f-4990-8e3d-68061a9ae252"
 9s          Normal   SuccessfulAttachVolume   pod/openhands-app-2024                AttachVolume.Attach succeeded for volume "pvc-2b1d223a-1c8f-4990-8e3d-68061a9ae252"
 9s          Normal   SuccessfulAttachVolume   pod/openhands-app-2024                AttachVolume.Attach succeeded for volume "pvc-31f15b25-faad-4665-a25f-201a530379af"
 9s          Normal   SuccessfulAttachVolume   pod/openhands-app-2024                AttachVolume.Attach succeeded for volume "pvc-31f15b25-faad-4665-a25f-201a530379af"
 6s          Normal   AddedInterface           pod/openhands-app-2024                Add eth0 [10.128.2.48/23] from openshift-sdn
 6s          Normal   AddedInterface           pod/openhands-app-2024                Add eth0 [10.128.2.48/23] from openshift-sdn
-6s          Normal   Pulled                   pod/openhands-app-2024                Container image "ghcr.io/all-hands-ai/openhands:main" already present on machine
+6s          Normal   Pulled                   pod/openhands-app-2024                Container image "docker.all-hands.dev/all-hands-ai/openhands:main" already present on machine
 6s          Normal   Created                  pod/openhands-app-2024                Created container openhands-app-2024
 6s          Normal   Created                  pod/openhands-app-2024                Created container openhands-app-2024
 6s          Normal   Started                  pod/openhands-app-2024                Started container openhands-app-2024
 6s          Normal   Started                  pod/openhands-app-2024                Started container openhands-app-2024
-6s          Normal   Pulled                   pod/openhands-app-2024                Container image "ghcr.io/all-hands-ai/sandbox:main" already present on machine
+6s          Normal   Pulled                   pod/openhands-app-2024                Container image "docker.all-hands.dev/all-hands-ai/sandbox:main" already present on machine
 5s          Normal   Created                  pod/openhands-app-2024                Created container openhands-sandbox-2024
 5s          Normal   Created                  pod/openhands-app-2024                Created container openhands-sandbox-2024
 5s          Normal   Started                  pod/openhands-app-2024                Started container openhands-sandbox-2024
 5s          Normal   Started                  pod/openhands-app-2024                Started container openhands-sandbox-2024
 83s         Normal   WaitForFirstConsumer     persistentvolumeclaim/workspace-pvc   waiting for first consumer to be created before binding
 83s         Normal   WaitForFirstConsumer     persistentvolumeclaim/workspace-pvc   waiting for first consumer to be created before binding
@@ -334,7 +334,7 @@ spec:
     spec:
     spec:
       containers:
       containers:
       - name: openhands-app-2024
       - name: openhands-app-2024
-        image: ghcr.io/all-hands-ai/openhands:main
+        image: docker.all-hands.dev/all-hands-ai/openhands:main
         env:
         env:
         - name: SANDBOX_USER_ID
         - name: SANDBOX_USER_ID
           value: "1000"
           value: "1000"
@@ -356,7 +356,7 @@ spec:
         ports:
         ports:
         - containerPort: 3000
         - containerPort: 3000
       - name: openhands-sandbox-2024
       - name: openhands-sandbox-2024
-        image: ghcr.io/opendevin/sandbox:main
+        image: docker.all-hands.dev/all-hands-ai/runtime:main
     #    securityContext:
     #    securityContext:
     #      privileged: true  # Add this to allow privileged access
     #      privileged: true  # Add this to allow privileged access
         ports:
         ports:

+ 8 - 17
docs/modules/usage/installation.mdx

@@ -8,24 +8,18 @@
 
 
 ## Start the app
 ## Start the app
 
 
-The easiest way to run OpenHands is in Docker. You can change `WORKSPACE_BASE` below to point OpenHands to
-existing code that you'd like to modify.
+The easiest way to run OpenHands is in Docker.
 
 
 ```bash
 ```bash
-export WORKSPACE_BASE=$(pwd)/workspace
+docker pull docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik
 
 
-docker pull ghcr.io/all-hands-ai/runtime:0.11-nikolaik
-
-docker run -it --pull=always \
-    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.11-nikolaik \
-    -e SANDBOX_USER_ID=$(id -u) \
-    -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-    -v $WORKSPACE_BASE:/opt/workspace_base \
+docker run -it --rm --pull=always \
+    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -p 3000:3000 \
     -p 3000:3000 \
     --add-host host.docker.internal:host-gateway \
     --add-host host.docker.internal:host-gateway \
-    --name openhands-app-$(date +%Y%m%d%H%M%S) \
-    ghcr.io/all-hands-ai/openhands:0.11
+    --name openhands-app \
+    docker.all-hands.dev/all-hands-ai/openhands:0.11
 ```
 ```
 
 
 You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
 You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
@@ -34,9 +28,6 @@ You can also run OpenHands in a scriptable [headless mode](https://docs.all-hand
 
 
 After running the command above, you'll find OpenHands running at [http://localhost:3000](http://localhost:3000).
 After running the command above, you'll find OpenHands running at [http://localhost:3000](http://localhost:3000).
 
 
-The agent will have access to the `./workspace` folder to do its work. You can copy existing code here, or change `WORKSPACE_BASE` in the
-command to point to an existing folder.
-
 Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
 Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
 These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
 These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
 
 
@@ -52,9 +43,9 @@ The `Advanced Options` also allow you to specify a `Base URL` if required.
 ## Versions
 ## Versions
 
 
 The command above pulls the most recent stable release of OpenHands. You have other options as well:
 The command above pulls the most recent stable release of OpenHands. You have other options as well:
-- For a specific release, use `ghcr.io/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number.
+- For a specific release, use `docker.all-hands.dev/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number.
 - We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
 - We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
-- For the most up-to-date development version, you can use `ghcr.io/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only.
+- For the most up-to-date development version, you can use `docker.all-hands.dev/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only.
 
 
 You can choose the tag that best suits your needs based on stability requirements and desired features.
 You can choose the tag that best suits your needs based on stability requirements and desired features.
 
 

+ 7 - 31
docs/modules/usage/llms/local-llms.md

@@ -35,32 +35,15 @@ Use the instructions [here](../getting-started) to start OpenHands using Docker.
 But when running `docker run`, you'll need to add a few more arguments:
 But when running `docker run`, you'll need to add a few more arguments:
 
 
 ```bash
 ```bash
---add-host host.docker.internal:host-gateway \
--e LLM_OLLAMA_BASE_URL="http://host.docker.internal:11434" \
-```
-
-LLM_OLLAMA_BASE_URL is optional. If you set it, it will be used to show the available installed models in the UI.
-
-Example:
-
-```bash
-# The directory you want OpenHands to modify. MUST be an absolute path!
-export WORKSPACE_BASE=$(pwd)/workspace
-
-docker run \
-    -it \
-    --pull=always \
+docker run # ...
     --add-host host.docker.internal:host-gateway \
     --add-host host.docker.internal:host-gateway \
-    -e SANDBOX_USER_ID=$(id -u) \
     -e LLM_OLLAMA_BASE_URL="http://host.docker.internal:11434" \
     -e LLM_OLLAMA_BASE_URL="http://host.docker.internal:11434" \
-    -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-    -v $WORKSPACE_BASE:/opt/workspace_base \
-    -v /var/run/docker.sock:/var/run/docker.sock \
-    -p 3000:3000 \
-    ghcr.io/all-hands-ai/openhands:main
+    # ...
 ```
 ```
 
 
-You should now be able to connect to `http://localhost:3000/`
+LLM_OLLAMA_BASE_URL is optional. If you set it, it will be used to show
+the available installed models in the UI.
+
 
 
 ### Configure the Web Application
 ### Configure the Web Application
 
 
@@ -176,18 +159,11 @@ CUSTOM_LLM_PROVIDER="openai"
 ### Docker
 ### Docker
 
 
 ```bash
 ```bash
-docker run \
-    -it \
-    --pull=always \
-    -e SANDBOX_USER_ID=$(id -u) \
+docker run # ...
     -e LLM_MODEL="openai/lmstudio" \
     -e LLM_MODEL="openai/lmstudio" \
     -e LLM_BASE_URL="http://host.docker.internal:1234/v1" \
     -e LLM_BASE_URL="http://host.docker.internal:1234/v1" \
     -e CUSTOM_LLM_PROVIDER="openai" \
     -e CUSTOM_LLM_PROVIDER="openai" \
-    -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-    -v $WORKSPACE_BASE:/opt/workspace_base \
-    -v /var/run/docker.sock:/var/run/docker.sock \
-    -p 3000:3000 \
-    ghcr.io/all-hands-ai/openhands:main
+    # ...
 ```
 ```
 
 
 You should now be able to connect to `http://localhost:3000/`
 You should now be able to connect to `http://localhost:3000/`

+ 79 - 0
docs/modules/usage/runtimes.md

@@ -0,0 +1,79 @@
+# Runtime Configuration
+
+A Runtime is an environment where the OpenHands agent can edit files and run
+commands.
+
+By default, OpenHands uses a Docker-based runtime, running on your local computer.
+This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
+
+We also support "remote" runtimes, which are typically managed by third-parties.
+They can make setup a bit simpler and more scalable, especially
+if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
+
+## Docker Runtime
+This is the default Runtime that's used when you start OpenHands. You might notice
+some flags being passed to `docker run` that make this possible:
+
+```
+docker run # ...
+    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
+    -v /var/run/docker.sock:/var/run/docker.sock \
+    # ...
+```
+
+The `SANDBOX_RUNTIME_CONTAINER_IMAGE` from nikolaik is a pre-built runtime image
+that contains our Runtime server, as well as some basic utilities for Python and NodeJS.
+You can also [build your own runtime image](how-to/custom-sandbox-guide).
+
+### Connecting to Your filesystem
+One useful feature here is the ability to connect to your local filesystem.
+
+To mount your filesystem into the runtime, add the following options to
+the `docker run` command:
+
+```bash
+export WORKSPACE_BASE=/path/to/your/code
+
+docker run # ...
+    -e SANDBOX_USER_ID=$(id -u) \
+    -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
+    -v $WORKSPACE_BASE:/opt/workspace_base \
+    # ...
+```
+
+Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
+any files that are mounted into its workspace.
+
+This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
+but seems to work well on most systems.
+
+## All Hands Runtime
+The All Hands Runtime is currently in beta. You can request access by joining
+the #remote-runtime-limited-beta channel on Slack (see the README for an invite).
+
+To use the All Hands Runtime, set the following environment variables when
+starting OpenHands:
+
+```bash
+docker run # ...
+    -e RUNTIME=remote \
+    -e SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.app.all-hands.dev" \
+    -e SANDBOX_API_KEY="your-all-hands-api-key" \
+    -e SANDBOX_KEEP_REMOTE_RUNTIME_ALIVE="true" \
+    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
+    # ...
+```
+
+## Modal Runtime
+Our partners at [Modal](https://modal.com/) have also provided a runtime for OpenHands.
+
+To use the Modal Runtime, create an account, and then [create an API key](https://modal.com/settings)
+
+You'll then need to set the following environment variables when starting OpenHands:
+```bash
+docker run # ...
+    -e RUNTIME=modal \
+    -e MODAL_API_TOKEN_ID="your-id" \
+    -e MODAL_API_TOKEN_SECRET="your-secret" \
+    -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
+```

+ 5 - 0
docs/sidebars.ts

@@ -90,6 +90,11 @@ const sidebars: SidebarsConfig = {
             },
             },
           ],
           ],
         },
         },
+        {
+          type: 'doc',
+          label: 'Runtime Configuration',
+          id: 'usage/runtimes',
+        },
         {
         {
           type: 'doc',
           type: 'doc',
           label: 'Custom Sandbox',
           label: 'Custom Sandbox',

Разница между файлами не показана из-за своего большого размера
+ 788 - 655
docs/yarn.lock


+ 0 - 2
openhands/core/config/__init__.py

@@ -3,7 +3,6 @@ from openhands.core.config.app_config import AppConfig
 from openhands.core.config.config_utils import (
 from openhands.core.config.config_utils import (
     OH_DEFAULT_AGENT,
     OH_DEFAULT_AGENT,
     OH_MAX_ITERATIONS,
     OH_MAX_ITERATIONS,
-    UndefinedString,
     get_field_info,
     get_field_info,
 )
 )
 from openhands.core.config.llm_config import LLMConfig
 from openhands.core.config.llm_config import LLMConfig
@@ -22,7 +21,6 @@ from openhands.core.config.utils import (
 __all__ = [
 __all__ = [
     'OH_DEFAULT_AGENT',
     'OH_DEFAULT_AGENT',
     'OH_MAX_ITERATIONS',
     'OH_MAX_ITERATIONS',
-    'UndefinedString',
     'AgentConfig',
     'AgentConfig',
     'AppConfig',
     'AppConfig',
     'LLMConfig',
     'LLMConfig',

+ 2 - 7
openhands/core/config/app_config.py

@@ -1,4 +1,3 @@
-import os
 import uuid
 import uuid
 from dataclasses import dataclass, field, fields, is_dataclass
 from dataclasses import dataclass, field, fields, is_dataclass
 from typing import ClassVar
 from typing import ClassVar
@@ -8,7 +7,6 @@ from openhands.core.config.agent_config import AgentConfig
 from openhands.core.config.config_utils import (
 from openhands.core.config.config_utils import (
     OH_DEFAULT_AGENT,
     OH_DEFAULT_AGENT,
     OH_MAX_ITERATIONS,
     OH_MAX_ITERATIONS,
-    UndefinedString,
     get_field_info,
     get_field_info,
 )
 )
 from openhands.core.config.llm_config import LLMConfig
 from openhands.core.config.llm_config import LLMConfig
@@ -55,11 +53,8 @@ class AppConfig:
     file_store: str = 'memory'
     file_store: str = 'memory'
     file_store_path: str = '/tmp/file_store'
     file_store_path: str = '/tmp/file_store'
     trajectories_path: str | None = None
     trajectories_path: str | None = None
-    # TODO: clean up workspace path after the removal of ServerRuntime
-    workspace_base: str = os.path.join(os.getcwd(), 'workspace')
-    workspace_mount_path: str | None = (
-        UndefinedString.UNDEFINED  # this path should always be set when config is fully loaded
-    )  # when set to None, do not mount the workspace
+    workspace_base: str | None = None
+    workspace_mount_path: str | None = None
     workspace_mount_path_in_sandbox: str = '/workspace'
     workspace_mount_path_in_sandbox: str = '/workspace'
     workspace_mount_rewrite: str | None = None
     workspace_mount_rewrite: str | None = None
     cache_dir: str = '/tmp/cache'
     cache_dir: str = '/tmp/cache'

+ 0 - 5
openhands/core/config/config_utils.py

@@ -1,4 +1,3 @@
-from enum import Enum
 from types import UnionType
 from types import UnionType
 from typing import get_args, get_origin
 from typing import get_args, get_origin
 
 
@@ -6,10 +5,6 @@ OH_DEFAULT_AGENT = 'CodeActAgent'
 OH_MAX_ITERATIONS = 100
 OH_MAX_ITERATIONS = 100
 
 
 
 
-class UndefinedString(str, Enum):
-    UNDEFINED = 'UNDEFINED'
-
-
 def get_field_info(f):
 def get_field_info(f):
     """Extract information about a dataclass field: type, optional, and default.
     """Extract information about a dataclass field: type, optional, and default.
 
 

+ 9 - 11
openhands/core/config/utils.py

@@ -15,7 +15,6 @@ from openhands.core.config.app_config import AppConfig
 from openhands.core.config.config_utils import (
 from openhands.core.config.config_utils import (
     OH_DEFAULT_AGENT,
     OH_DEFAULT_AGENT,
     OH_MAX_ITERATIONS,
     OH_MAX_ITERATIONS,
-    UndefinedString,
 )
 )
 from openhands.core.config.llm_config import LLMConfig
 from openhands.core.config.llm_config import LLMConfig
 from openhands.core.config.sandbox_config import SandboxConfig
 from openhands.core.config.sandbox_config import SandboxConfig
@@ -191,16 +190,15 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
 
 
 def finalize_config(cfg: AppConfig):
 def finalize_config(cfg: AppConfig):
     """More tweaks to the config after it's been loaded."""
     """More tweaks to the config after it's been loaded."""
-    cfg.workspace_base = os.path.abspath(cfg.workspace_base)
-    # Set workspace_mount_path if not set by the user
-    if cfg.workspace_mount_path is UndefinedString.UNDEFINED:
-        cfg.workspace_mount_path = cfg.workspace_base
-
-    if cfg.workspace_mount_rewrite:  # and not config.workspace_mount_path:
-        # TODO why do we need to check if workspace_mount_path is None?
-        base = cfg.workspace_base or os.getcwd()
-        parts = cfg.workspace_mount_rewrite.split(':')
-        cfg.workspace_mount_path = base.replace(parts[0], parts[1])
+    if cfg.workspace_base is not None:
+        cfg.workspace_base = os.path.abspath(cfg.workspace_base)
+        if cfg.workspace_mount_path is None:
+            cfg.workspace_mount_path = cfg.workspace_base
+
+        if cfg.workspace_mount_rewrite:
+            base = cfg.workspace_base or os.getcwd()
+            parts = cfg.workspace_mount_rewrite.split(':')
+            cfg.workspace_mount_path = base.replace(parts[0], parts[1])
 
 
     for llm in cfg.llms.values():
     for llm in cfg.llms.values():
         if llm.embedding_base_url is None:
         if llm.embedding_base_url is None:

+ 22 - 21
openhands/runtime/impl/eventstream/eventstream_runtime.py

@@ -205,11 +205,7 @@ class EventStreamRuntime(Runtime):
             self.log(
             self.log(
                 'info', f'Starting runtime with image: {self.runtime_container_image}'
                 'info', f'Starting runtime with image: {self.runtime_container_image}'
             )
             )
-            self._init_container(
-                sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,  # e.g. /workspace
-                mount_dir=self.config.workspace_mount_path,  # e.g. /opt/openhands/_test_workspace
-                plugins=self.plugins,
-            )
+            self._init_container()
             self.log('info', f'Container started: {self.container_name}')
             self.log('info', f'Container started: {self.container_name}')
 
 
         else:
         else:
@@ -246,19 +242,14 @@ class EventStreamRuntime(Runtime):
         stop=tenacity.stop_after_attempt(5) | stop_if_should_exit(),
         stop=tenacity.stop_after_attempt(5) | stop_if_should_exit(),
         wait=tenacity.wait_exponential(multiplier=1, min=4, max=60),
         wait=tenacity.wait_exponential(multiplier=1, min=4, max=60),
     )
     )
-    def _init_container(
-        self,
-        sandbox_workspace_dir: str,
-        mount_dir: str | None = None,
-        plugins: list[PluginRequirement] | None = None,
-    ):
+    def _init_container(self):
         try:
         try:
             self.log('debug', 'Preparing to start container...')
             self.log('debug', 'Preparing to start container...')
             self.send_status_message('STATUS$PREPARING_CONTAINER')
             self.send_status_message('STATUS$PREPARING_CONTAINER')
             plugin_arg = ''
             plugin_arg = ''
-            if plugins is not None and len(plugins) > 0:
+            if self.plugins is not None and len(self.plugins) > 0:
                 plugin_arg = (
                 plugin_arg = (
-                    f'--plugins {" ".join([plugin.name for plugin in plugins])} '
+                    f'--plugins {" ".join([plugin.name for plugin in self.plugins])} '
                 )
                 )
 
 
             self._host_port = self._find_available_port()
             self._host_port = self._find_available_port()
@@ -294,17 +285,27 @@ class EventStreamRuntime(Runtime):
                 environment['DEBUG'] = 'true'
                 environment['DEBUG'] = 'true'
 
 
             self.log('debug', f'Workspace Base: {self.config.workspace_base}')
             self.log('debug', f'Workspace Base: {self.config.workspace_base}')
-            if mount_dir is not None and sandbox_workspace_dir is not None:
+            if (
+                self.config.workspace_mount_path is not None
+                and self.config.workspace_mount_path_in_sandbox is not None
+            ):
                 # e.g. result would be: {"/home/user/openhands/workspace": {'bind': "/workspace", 'mode': 'rw'}}
                 # e.g. result would be: {"/home/user/openhands/workspace": {'bind': "/workspace", 'mode': 'rw'}}
-                volumes = {mount_dir: {'bind': sandbox_workspace_dir, 'mode': 'rw'}}
-                self.log('debug', f'Mount dir: {mount_dir}')
+                volumes = {
+                    self.config.workspace_mount_path: {
+                        'bind': self.config.workspace_mount_path_in_sandbox,
+                        'mode': 'rw',
+                    }
+                }
+                logger.debug(f'Mount dir: {self.config.workspace_mount_path}')
             else:
             else:
-                self.log(
-                    'warn',
-                    'Warning: Mount dir is not set, will not mount the workspace directory to the container!\n',
+                logger.debug(
+                    'Mount dir is not set, will not mount the workspace directory to the container'
                 )
                 )
                 volumes = None
                 volumes = None
-            self.log('debug', f'Sandbox workspace: {sandbox_workspace_dir}')
+            self.log(
+                'debug',
+                f'Sandbox workspace: {self.config.workspace_mount_path_in_sandbox}'
+            )
 
 
             if self.config.sandbox.browsergym_eval_env is not None:
             if self.config.sandbox.browsergym_eval_env is not None:
                 browsergym_arg = (
                 browsergym_arg = (
@@ -319,7 +320,7 @@ class EventStreamRuntime(Runtime):
                     f'/openhands/micromamba/bin/micromamba run -n openhands '
                     f'/openhands/micromamba/bin/micromamba run -n openhands '
                     f'poetry run '
                     f'poetry run '
                     f'python -u -m openhands.runtime.action_execution_server {self._container_port} '
                     f'python -u -m openhands.runtime.action_execution_server {self._container_port} '
-                    f'--working-dir "{sandbox_workspace_dir}" '
+                    f'--working-dir "{self.config.workspace_mount_path_in_sandbox}" '
                     f'{plugin_arg}'
                     f'{plugin_arg}'
                     f'--username {"openhands" if self.config.run_as_openhands else "root"} '
                     f'--username {"openhands" if self.config.run_as_openhands else "root"} '
                     f'--user-id {self.config.sandbox.user_id} '
                     f'--user-id {self.config.sandbox.user_id} '

+ 15 - 33
tests/unit/test_config.py

@@ -6,7 +6,6 @@ from openhands.core.config import (
     AgentConfig,
     AgentConfig,
     AppConfig,
     AppConfig,
     LLMConfig,
     LLMConfig,
-    UndefinedString,
     finalize_config,
     finalize_config,
     get_llm_config_arg,
     get_llm_config_arg,
     load_from_env,
     load_from_env,
@@ -82,12 +81,8 @@ def test_load_from_old_style_env(monkeypatch, default_config):
     assert default_config.get_agent_config().memory_enabled is True
     assert default_config.get_agent_config().memory_enabled is True
     assert default_config.default_agent == 'PlannerAgent'
     assert default_config.default_agent == 'PlannerAgent'
     assert default_config.workspace_base == '/opt/files/workspace'
     assert default_config.workspace_base == '/opt/files/workspace'
-    assert (
-        default_config.workspace_mount_path is UndefinedString.UNDEFINED
-    )  # before finalize_config
-    assert (
-        default_config.workspace_mount_path_in_sandbox is not UndefinedString.UNDEFINED
-    )
+    assert default_config.workspace_mount_path is None  # before finalize_config
+    assert default_config.workspace_mount_path_in_sandbox is not None
     assert default_config.sandbox.base_container_image == 'custom_image'
     assert default_config.sandbox.base_container_image == 'custom_image'
 
 
 
 
@@ -148,11 +143,8 @@ default_agent = "TestAgent"
     assert default_config.workspace_base == '/opt/files2/workspace'
     assert default_config.workspace_base == '/opt/files2/workspace'
     assert default_config.sandbox.timeout == 1
     assert default_config.sandbox.timeout == 1
 
 
-    # before finalize_config, workspace_mount_path is UndefinedString.UNDEFINED if it was not set
-    assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
-    assert (
-        default_config.workspace_mount_path_in_sandbox is not UndefinedString.UNDEFINED
-    )
+    assert default_config.workspace_mount_path is None
+    assert default_config.workspace_mount_path_in_sandbox is not None
     assert default_config.workspace_mount_path_in_sandbox == '/workspace'
     assert default_config.workspace_mount_path_in_sandbox == '/workspace'
 
 
     finalize_config(default_config)
     finalize_config(default_config)
@@ -231,8 +223,7 @@ sandbox_user_id = 1001
 
 
     load_from_toml(default_config, temp_toml_file)
     load_from_toml(default_config, temp_toml_file)
 
 
-    # before finalize_config, workspace_mount_path is UndefinedString.UNDEFINED if it was not set
-    assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
+    assert default_config.workspace_mount_path is None
 
 
     load_from_env(default_config, os.environ)
     load_from_env(default_config, os.environ)
 
 
@@ -244,11 +235,9 @@ sandbox_user_id = 1001
 
 
     # after we set workspace_base to 'UNDEFINED' in the environment,
     # after we set workspace_base to 'UNDEFINED' in the environment,
     # workspace_base should be set to that
     # workspace_base should be set to that
-    # workspace_mount path is still UndefinedString.UNDEFINED
-    assert default_config.workspace_base is not UndefinedString.UNDEFINED
+    assert default_config.workspace_base is not None
     assert default_config.workspace_base == 'UNDEFINED'
     assert default_config.workspace_base == 'UNDEFINED'
-    assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
-    assert default_config.workspace_mount_path == 'UNDEFINED'
+    assert default_config.workspace_mount_path is None
 
 
     assert default_config.disable_color is True
     assert default_config.disable_color is True
     assert default_config.sandbox.timeout == 1000
     assert default_config.sandbox.timeout == 1000
@@ -284,8 +273,7 @@ user_id = 1001
 
 
     load_from_toml(default_config, temp_toml_file)
     load_from_toml(default_config, temp_toml_file)
 
 
-    # before finalize_config, workspace_mount_path is UndefinedString.UNDEFINED if it was not set
-    assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
+    assert default_config.workspace_mount_path is None
 
 
     # before load_from_env, values are set to the values from the toml file
     # before load_from_env, values are set to the values from the toml file
     assert default_config.get_llm_config().api_key == 'toml-api-key'
     assert default_config.get_llm_config().api_key == 'toml-api-key'
@@ -338,9 +326,7 @@ user_id = 1001
 def test_defaults_dict_after_updates(default_config):
 def test_defaults_dict_after_updates(default_config):
     # Test that `defaults_dict` retains initial values after updates.
     # Test that `defaults_dict` retains initial values after updates.
     initial_defaults = default_config.defaults_dict
     initial_defaults = default_config.defaults_dict
-    assert (
-        initial_defaults['workspace_mount_path']['default'] is UndefinedString.UNDEFINED
-    )
+    assert initial_defaults['workspace_mount_path']['default'] is None
     assert initial_defaults['default_agent']['default'] == 'CodeActAgent'
     assert initial_defaults['default_agent']['default'] == 'CodeActAgent'
 
 
     updated_config = AppConfig()
     updated_config = AppConfig()
@@ -352,10 +338,7 @@ def test_defaults_dict_after_updates(default_config):
 
 
     defaults_after_updates = updated_config.defaults_dict
     defaults_after_updates = updated_config.defaults_dict
     assert defaults_after_updates['default_agent']['default'] == 'CodeActAgent'
     assert defaults_after_updates['default_agent']['default'] == 'CodeActAgent'
-    assert (
-        defaults_after_updates['workspace_mount_path']['default']
-        is UndefinedString.UNDEFINED
-    )
+    assert defaults_after_updates['workspace_mount_path']['default'] is None
     assert defaults_after_updates['sandbox']['timeout']['default'] == 120
     assert defaults_after_updates['sandbox']['timeout']['default'] == 120
     assert (
     assert (
         defaults_after_updates['sandbox']['base_container_image']['default']
         defaults_after_updates['sandbox']['base_container_image']['default']
@@ -384,17 +367,16 @@ def test_invalid_toml_format(monkeypatch, temp_toml_file, default_config):
 
 
 def test_finalize_config(default_config):
 def test_finalize_config(default_config):
     # Test finalize config
     # Test finalize config
-    assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
+    assert default_config.workspace_mount_path is None
+    default_config.workspace_base = None
     finalize_config(default_config)
     finalize_config(default_config)
 
 
-    assert default_config.workspace_mount_path == os.path.abspath(
-        default_config.workspace_base
-    )
+    assert default_config.workspace_mount_path is None
 
 
 
 
-# tests for workspace, mount path, path in sandbox, cache dir
 def test_workspace_mount_path_default(default_config):
 def test_workspace_mount_path_default(default_config):
-    assert default_config.workspace_mount_path is UndefinedString.UNDEFINED
+    assert default_config.workspace_mount_path is None
+    default_config.workspace_base = '/home/user/project'
     finalize_config(default_config)
     finalize_config(default_config)
     assert default_config.workspace_mount_path == os.path.abspath(
     assert default_config.workspace_mount_path == os.path.abspath(
         default_config.workspace_base
         default_config.workspace_base

Некоторые файлы не были показаны из-за большого количества измененных файлов