From c0bb84dfa20b4973effbe938d19ed7342e42d4be Mon Sep 17 00:00:00 2001 From: chuckbutkus Date: Wed, 27 Aug 2025 02:23:39 -0400 Subject: [PATCH] Non root user (#10155) Co-authored-by: openhands --- containers/app/Dockerfile | 26 +-- openhands/core/config/sandbox_config.py | 3 + openhands/runtime/action_execution_server.py | 1 - .../runtime/impl/remote/remote_runtime.py | 1 + openhands/runtime/utils/command.py | 9 +- openhands/runtime/utils/runtime_init.py | 108 +++++------ .../utils/runtime_templates/Dockerfile.j2 | 167 +++++++++++++++--- .../runtime/builder/test_runtime_build.py | 15 +- 8 files changed, 229 insertions(+), 101 deletions(-) diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index e6b748399b..87f9adfcd3 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -58,34 +58,34 @@ RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs # Default is 60000, but we've seen up to 200000 RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs -RUN groupadd --gid $OPENHANDS_USER_ID app +RUN groupadd --gid $OPENHANDS_USER_ID openhands RUN useradd -l -m -u $OPENHANDS_USER_ID --gid $OPENHANDS_USER_ID -s /bin/bash openhands && \ - usermod -aG app openhands && \ + usermod -aG openhands openhands && \ usermod -aG sudo openhands && \ echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers -RUN chown -R openhands:app /app && chmod -R 770 /app -RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE +RUN chown -R openhands:openhands /app && chmod -R 770 /app +RUN sudo chown -R openhands:openhands $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE USER openhands ENV VIRTUAL_ENV=/app/.venv \ PATH="/app/.venv/bin:$PATH" \ PYTHONPATH='/app' -COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} +COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --chown=openhands:app --chmod=770 ./microagents ./microagents -COPY --chown=openhands:app --chmod=770 ./openhands ./openhands -COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins -COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./ +COPY --chown=openhands:openhands --chmod=770 ./microagents ./microagents +COPY --chown=openhands:openhands --chmod=770 ./openhands ./openhands +COPY --chown=openhands:openhands --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins +COPY --chown=openhands:openhands pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./ # This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership RUN python openhands/core/download.py # No-op to download assets # Add this line to set group ownership of all files/directories not already in "app" group -# openhands:openhands -> openhands:app -RUN find /app \! -group app -exec chgrp app {} + +# openhands:openhands -> openhands:openhands +RUN find /app \! -group openhands -exec chgrp openhands {} + -COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build -COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh +COPY --chown=openhands:openhands --chmod=770 --from=frontend-builder /app/build ./frontend/build +COPY --chown=openhands:openhands --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh USER root diff --git a/openhands/core/config/sandbox_config.py b/openhands/core/config/sandbox_config.py index a76f132adf..b0f9446966 100644 --- a/openhands/core/config/sandbox_config.py +++ b/openhands/core/config/sandbox_config.py @@ -2,6 +2,8 @@ import os from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator +from openhands.core.logger import openhands_logger as logger + class SandboxConfig(BaseModel): """Configuration for the sandbox. @@ -55,6 +57,7 @@ class SandboxConfig(BaseModel): ) runtime_container_image: str | None = Field(default=None) user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000) + logger.debug(f'SandboxConfig user_id default: {user_id}') timeout: int = Field(default=120) remote_runtime_init_timeout: int = Field(default=180) remote_runtime_api_timeout: int = Field(default=10) diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index b478fdfb03..2275f17cdb 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -647,7 +647,6 @@ class ActionExecutor: if __name__ == '__main__': logger.warning('Starting Action Execution Server') - parser = argparse.ArgumentParser() parser.add_argument('port', type=int, help='Port to listen on') parser.add_argument('--working-dir', type=str, help='Working directory') diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index d122ca5514..b98d99d11f 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -76,6 +76,7 @@ class RemoteRuntime(ActionExecutionClient): user_id, git_provider_tokens, ) + logger.debug(f'RemoteRuntime.init user_id {user_id}') if self.config.sandbox.api_key is None: raise ValueError( 'API key is required to use the remote runtime. ' diff --git a/openhands/runtime/utils/command.py b/openhands/runtime/utils/command.py index 18a3d85117..01701b5f1d 100644 --- a/openhands/runtime/utils/command.py +++ b/openhands/runtime/utils/command.py @@ -1,4 +1,5 @@ from openhands.core.config import OpenHandsConfig +from openhands.core.logger import openhands_logger as logger from openhands.runtime.plugins import PluginRequirement DEFAULT_PYTHON_PREFIX = [ @@ -23,6 +24,9 @@ def get_action_execution_server_startup_command( python_executable: str = 'python', ) -> list[str]: sandbox_config = app_config.sandbox + logger.debug(f'app_config {vars(app_config)}') + logger.debug(f'sandbox_config {vars(sandbox_config)}') + logger.debug(f'override_user_id {override_user_id}') # Plugin args plugin_args = [] @@ -39,9 +43,7 @@ def get_action_execution_server_startup_command( username = override_username or ( 'openhands' if app_config.run_as_openhands else 'root' ) - user_id = override_user_id or ( - sandbox_config.user_id if app_config.run_as_openhands else 0 - ) + user_id = override_user_id or (1000 if app_config.run_as_openhands else 0) base_cmd = [ *python_prefix, @@ -62,5 +64,6 @@ def get_action_execution_server_startup_command( if not app_config.enable_browser: base_cmd.append('--no-enable-browser') + logger.debug(f'get_action_execution_server_startup_command: {base_cmd}') return base_cmd diff --git a/openhands/runtime/utils/runtime_init.py b/openhands/runtime/utils/runtime_init.py index 060e08b47d..2a55b2a9e8 100644 --- a/openhands/runtime/utils/runtime_init.py +++ b/openhands/runtime/utils/runtime_init.py @@ -49,6 +49,61 @@ def init_user_and_working_directory( if username == os.getenv('USER') and username not in ['root', 'openhands']: return None + # Skip root since it is already created + if username != 'root': + # Check if the username already exists + logger.debug(f'Attempting to create user `{username}` with UID {user_id}.') + existing_user_id = -1 + try: + result = subprocess.run( + f'id -u {username}', shell=True, check=True, capture_output=True + ) + existing_user_id = int(result.stdout.decode().strip()) + + # The user ID already exists, skip setup + if existing_user_id == user_id: + logger.debug( + f'User `{username}` already has the provided UID {user_id}. Skipping user setup.' + ) + else: + logger.warning( + f'User `{username}` already exists with UID {existing_user_id}. Skipping user setup.' + ) + return existing_user_id + return None + except subprocess.CalledProcessError as e: + # Returncode 1 indicates, that the user does not exist yet + if e.returncode == 1: + logger.debug( + f'User `{username}` does not exist. Proceeding with user creation.' + ) + else: + logger.error( + f'Error checking user `{username}`, skipping setup:\n{e}\n' + ) + raise + + # Add sudoer + sudoer_line = r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers" + output = subprocess.run(sudoer_line, shell=True, capture_output=True) + if output.returncode != 0: + raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}') + logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]') + + command = ( + f'useradd -rm -d /home/{username} -s /bin/bash ' + f'-g root -G sudo -u {user_id} {username}' + ) + output = subprocess.run(command, shell=True, capture_output=True) + if output.returncode == 0: + logger.debug( + f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]' + ) + else: + raise RuntimeError( + f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]' + ) + # First create the working directory, independent of the user logger.debug(f'Client working directory: {initial_cwd}') command = f'umask 002; mkdir -p {initial_cwd}' @@ -64,57 +119,4 @@ def init_user_and_working_directory( out_str += output.stdout.decode() logger.debug(f'Created working directory. Output: [{out_str}]') - # Skip root since it is already created - if username == 'root': - return None - - # Check if the username already exists - existing_user_id = -1 - try: - result = subprocess.run( - f'id -u {username}', shell=True, check=True, capture_output=True - ) - existing_user_id = int(result.stdout.decode().strip()) - - # The user ID already exists, skip setup - if existing_user_id == user_id: - logger.debug( - f'User `{username}` already has the provided UID {user_id}. Skipping user setup.' - ) - else: - logger.warning( - f'User `{username}` already exists with UID {existing_user_id}. Skipping user setup.' - ) - return existing_user_id - return None - except subprocess.CalledProcessError as e: - # Returncode 1 indicates, that the user does not exist yet - if e.returncode == 1: - logger.debug( - f'User `{username}` does not exist. Proceeding with user creation.' - ) - else: - logger.error(f'Error checking user `{username}`, skipping setup:\n{e}\n') - raise - - # Add sudoer - sudoer_line = r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers" - output = subprocess.run(sudoer_line, shell=True, capture_output=True) - if output.returncode != 0: - raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}') - logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]') - - command = ( - f'useradd -rm -d /home/{username} -s /bin/bash ' - f'-g root -G sudo -u {user_id} {username}' - ) - output = subprocess.run(command, shell=True, capture_output=True) - if output.returncode == 0: - logger.debug( - f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]' - ) - else: - raise RuntimeError( - f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]' - ) return None diff --git a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 index a9ed4658f5..4f3ec322ba 100644 --- a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 +++ b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 @@ -14,12 +14,16 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \ {% macro setup_base_system() %} +# Set PATH early to ensure system commands are available +ENV PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH" + # Install base system dependencies {% if (('ubuntu' in base_image) or ('mswebench' in base_image)) %} RUN apt-get update && \ apt-get install -y --no-install-recommends \ wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \ + coreutils util-linux procps findutils grep sed \ {%- if (base_image.endswith(':latest') or base_image.endswith(':24.04') or ('mswebench' in base_image)) -%} libgl1 \ {%- else %} @@ -41,6 +45,7 @@ RUN apt-get update && \ RUN apt-get update && \ apt-get install -y --no-install-recommends \ wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \ + coreutils util-linux procps findutils grep sed \ libgl1-mesa-glx \ libasound2-plugins libatomic1 \ # Install Docker dependencies @@ -58,15 +63,30 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/openhands/ # Add /openhands/bin to PATH ENV PATH="/openhands/bin:${PATH}" -# Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts +# Remove UID 1000 and GID 1000 users/groups that might conflict with openhands user RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \ - (if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi) + (if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi) && \ + (if getent group 1000 | grep -q pn; then groupdel pn; fi) && \ + (if getent group 1000 | grep -q ubuntu; then groupdel ubuntu; fi) +# Create openhands group and user +RUN groupadd -g 1000 openhands && \ + useradd -u 1000 -g 1000 -m -s /bin/bash openhands && \ + usermod -aG sudo openhands && \ + echo 'openhands ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ + # Set empty password for openhands user to allow passwordless su + passwd -d openhands && \ + # Set empty password for root user as well to ensure su works in both directions + passwd -d root && \ + # Ensure root can su to openhands without password by configuring PAM + sed -i '/pam_rootok.so/d' /etc/pam.d/su && \ + sed -i '1i auth sufficient pam_rootok.so' /etc/pam.d/su # Create necessary directories RUN mkdir -p /openhands && \ mkdir -p /openhands/logs && \ - mkdir -p /openhands/poetry + mkdir -p /openhands/poetry && \ + chown -R openhands:openhands /openhands # ================================================================ @@ -147,14 +167,16 @@ RUN if [ -z "${RELEASE_TAG}" ]; then \ if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi && \ mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT} && \ cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \ - rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz + rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz && \ + chown -R openhands:openhands ${OPENVSCODE_SERVER_ROOT} {% endmacro %} {% macro install_vscode_extensions() %} -# Install our custom extension +# Install our custom extensions as openhands user +USER openhands RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \ cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/hello-world/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world/ @@ -165,27 +187,72 @@ RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \ RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,ini,npm} {% endmacro %} -{% macro install_dependencies() %} -# Install all dependencies +{% macro install_dependencies_root() %} +# Install system-level dependencies that require root +USER root +RUN \ + {% if enable_browser %} + # Install system dependencies for Playwright (requires root) + apt-get update && \ + apt-get install -y --no-install-recommends \ + libnss3 libnspr4 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 \ + libxdamage1 libxrandr2 libgbm1 libxss1 && \ + # Install libasound2 - try new package name first (Ubuntu 24.04+), fallback to old name + (apt-get install -y --no-install-recommends libasound2t64 || apt-get install -y --no-install-recommends libasound2) && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && \ + # Install Playwright browsers in shared location accessible to all users + export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers && \ + mkdir -p /opt/playwright-browsers && \ + /openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \ + # Set proper permissions for shared access + chmod -R 755 /opt/playwright-browsers && \ + # Create cache directories and symlinks for both users + mkdir -p /home/openhands/.cache && \ + mkdir -p /root/.cache && \ + ln -sf /opt/playwright-browsers /home/openhands/.cache/ms-playwright && \ + ln -sf /opt/playwright-browsers /root/.cache/ms-playwright && \ + chown -h openhands:openhands /home/openhands/.cache/ms-playwright && \ + # Set environment variable for all users + echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /etc/environment && \ + {% endif %} + # Set environment variables (requires root) + /openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \ + # Set permissions for shared read-only access + chmod -R 755 /openhands/poetry && \ + chmod -R 755 /openhands/micromamba && \ + chown -R openhands:openhands /openhands/poetry && \ + mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \ + chown -R openhands:openhands /openhands/workspace && \ + chown -R openhands:openhands /openhands/micromamba && \ + # Ensure PATH includes system binaries early in startup + echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"' >> /etc/environment && \ + echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"' >> /etc/bash.bashrc && \ + # Set up conda environment activation for all users + echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /etc/bash.bashrc && \ + echo 'micromamba activate openhands 2>/dev/null || true' >> /etc/bash.bashrc && \ + # Set up environment for root user + echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/openhands/micromamba/bin:$PATH"' >> /root/.bashrc && \ + echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /root/.bashrc && \ + echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /root/.bashrc && \ + echo 'micromamba activate openhands 2>/dev/null || true' >> /root/.bashrc && \ + # Clean up system packages (requires root) + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +{% endmacro %} + +{% macro install_dependencies_user() %} +# Install user-level dependencies as openhands user WORKDIR /openhands/code +USER openhands RUN \ /openhands/micromamba/bin/micromamba config set changeps1 False && \ /openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \ /openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12 && \ # Install project dependencies /openhands/micromamba/bin/micromamba run -n openhands poetry install --only main,runtime --no-interaction --no-root && \ - # Update and install additional tools - # (There used to be an "apt-get update" here, hopefully we can skip it.) - {% if enable_browser %}/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \{% endif %} - # Set environment variables - /openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \ - # Set permissions - chmod -R g+rws /openhands/poetry && \ - mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \ - # Clean up + # Clean up user caches /openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ /openhands/micromamba/bin/micromamba clean --all {% endmacro %} @@ -203,7 +270,16 @@ RUN \ RUN mkdir -p /openhands/micromamba/bin && \ /bin/bash -c "PREFIX_LOCATION=/openhands/micromamba BIN_FOLDER=/openhands/micromamba/bin INIT_YES=no CONDA_FORGE_YES=yes $(curl -L https://micro.mamba.pm/install.sh)" && \ /openhands/micromamba/bin/micromamba config remove channels defaults && \ - /openhands/micromamba/bin/micromamba config list + /openhands/micromamba/bin/micromamba config list && \ + chown -R openhands:openhands /openhands/micromamba && \ + # Create read-only shared access to micromamba for all users + # This allows both root and openhands users to access the same packages + # while maintaining security by keeping openhands as the owner + chmod -R 755 /openhands/micromamba && \ + # Create a separate writable location for root's micromamba cache/config + mkdir -p /root/.local/share/micromamba && \ + # Set up environment variables for system-wide access + echo 'export PATH="/openhands/micromamba/bin:$PATH"' >> /etc/environment # Create the openhands virtual environment and install poetry and python RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \ @@ -214,40 +290,75 @@ RUN \ if [ -d /openhands/code ]; then rm -rf /openhands/code; fi && \ mkdir -p /openhands/code/openhands && \ touch /openhands/code/openhands/__init__.py && \ + chown -R openhands:openhands /openhands/code && \ # Set global git configuration to ensure proper author/committer information git config --global user.name "openhands" && \ git config --global user.email "openhands@all-hands.dev" -COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/ +COPY --chown=openhands:openhands ./code/pyproject.toml ./code/poetry.lock /openhands/code/ -{{ install_dependencies() }} +{{ install_dependencies_user() }} +{{ install_dependencies_root() }} # ================================================================ # END: Build Runtime Image from Scratch # ================================================================ {% endif %} +# Ensure openhands user/group and base dirs exist even when not building from scratch +USER root +RUN \ + # Ensure group exists (prefer GID 1000 if available) + if ! getent group openhands >/dev/null 2>&1; then \ + if getent group 1000 >/dev/null 2>&1; then groupadd openhands; else groupadd -g 1000 openhands; fi; \ + fi && \ + # Ensure user exists (prefer UID 1000 if available) + if ! id -u openhands >/dev/null 2>&1; then \ + if getent passwd 1000 >/dev/null 2>&1; then useradd -m -s /bin/bash -g openhands openhands; else useradd -u 1000 -g openhands -m -s /bin/bash openhands; fi; \ + fi && \ + # Ensure home and required directories exist before later steps + mkdir -p /home/openhands && \ + mkdir -p /openhands && \ + mkdir -p $(dirname ${OPENVSCODE_SERVER_ROOT}) && \ + # Ensure ownership is correct for all OpenHands paths + chown -R openhands:openhands /home/openhands || true && \ + chown -R openhands:openhands /openhands || true + {{ setup_vscode_server() }} # ================================================================ # Copy Project source files # ================================================================ RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi -COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/ +COPY --chown=openhands:openhands ./code/pyproject.toml ./code/poetry.lock /openhands/code/ RUN if [ -d /openhands/code/microagents ]; then rm -rf /openhands/code/microagents; fi -COPY ./code/microagents /openhands/code/microagents -COPY ./code/openhands /openhands/code/openhands -RUN chmod a+rwx /openhands/code/openhands/__init__.py - +COPY --chown=openhands:openhands ./code/microagents /openhands/code/microagents +COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands +RUN chmod a+rwx /openhands/code/openhands/__init__.py && \ + chown -R openhands:openhands /openhands/code # ================================================================ # END: Build from versioned image # ================================================================ {% if build_from_versioned %} -{{ install_dependencies() }} +{{ install_dependencies_user() }} +{{ install_dependencies_root() }} {{ install_vscode_extensions() }} {% endif %} -# Install extra dependencies if specified -{% if extra_deps %}RUN {{ extra_deps }} {% endif %} +# Install extra dependencies if specified (as openhands user) +{% if extra_deps %} +USER openhands +RUN {{ extra_deps }} +{% endif %} + +# Set up environment for openhands user +USER root +RUN \ + # Set up environment for openhands user + echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/openhands/micromamba/bin:$PATH"' >> /home/openhands/.bashrc && \ + echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /home/openhands/.bashrc && \ + echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /home/openhands/.bashrc && \ + echo 'micromamba activate openhands 2>/dev/null || true' >> /home/openhands/.bashrc && \ + chown openhands:openhands /home/openhands/.bashrc diff --git a/tests/unit/runtime/builder/test_runtime_build.py b/tests/unit/runtime/builder/test_runtime_build.py index 46d50f6ef7..306865c499 100644 --- a/tests/unit/runtime/builder/test_runtime_build.py +++ b/tests/unit/runtime/builder/test_runtime_build.py @@ -166,7 +166,10 @@ def test_generate_dockerfile_build_from_scratch(): assert 'python=3.12' in dockerfile_content # Check the update command - assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content + assert ( + 'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands' + in dockerfile_content + ) assert ( '/openhands/micromamba/bin/micromamba run -n openhands poetry install' in dockerfile_content @@ -188,7 +191,10 @@ def test_generate_dockerfile_build_from_lock(): assert 'poetry install' not in dockerfile_content # These update commands SHOULD still in the dockerfile - assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content + assert ( + 'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands' + in dockerfile_content + ) def test_generate_dockerfile_build_from_versioned(): @@ -206,7 +212,10 @@ def test_generate_dockerfile_build_from_versioned(): # this SHOULD exist when build from versioned assert 'poetry install' in dockerfile_content - assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content + assert ( + 'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands' + in dockerfile_content + ) def test_get_runtime_image_repo_and_tag_eventstream():