Compare commits

...

60 Commits

Author SHA1 Message Date
Chuck Butkus
75a4033bef Update dockerfile 2025-08-27 01:42:18 -04:00
Chuck Butkus
2f9a4f96ae Fix dockerfile 2025-08-26 19:18:00 -04:00
chuckbutkus
ac152da39e Merge branch 'main' into non-root-user 2025-08-26 14:25:39 -04:00
Chuck Butkus
77210bc678 Revert "Fix failing Docker-dependent tests in test_runtime_build.py"
This reverts commit 39049b173a.
2025-08-26 13:08:38 -04:00
Chuck Butkus
425b4f77a9 Merge branch 'main' into non-root-user 2025-08-26 12:57:00 -04:00
chuckbutkus
a84670e6bf Merge branch 'main' into non-root-user 2025-08-18 17:39:39 -04:00
chuckbutkus
fc3b3f733f Merge branch 'main' into non-root-user 2025-08-15 17:28:04 -04:00
chuckbutkus
150f56f252 Merge branch 'main' into non-root-user 2025-08-13 12:39:17 -04:00
chuckbutkus
05171f08fb Merge branch 'main' into non-root-user 2025-08-13 00:28:53 -04:00
chuckbutkus
64a160cf03 Merge branch 'main' into non-root-user 2025-08-12 23:12:11 -04:00
chuckbutkus
8b1f38e52e Merge branch 'main' into non-root-user 2025-08-12 15:44:52 -04:00
Chuck Butkus
868f434f97 Fix logging 2025-08-12 12:49:30 -04:00
Chuck Butkus
210f7fc653 Lint fix 2025-08-12 12:40:03 -04:00
Chuck Butkus
f9512cd234 Move logging to debug 2025-08-12 12:31:57 -04:00
chuckbutkus
ff8e659905 Merge branch 'main' into non-root-user 2025-08-12 12:17:54 -04:00
chuckbutkus
c32610a440 Merge branch 'main' into non-root-user 2025-08-11 20:40:40 -04:00
Chuck Butkus
26abb81a86 Fix merge 2025-08-11 16:51:56 -04:00
Chuck Butkus
23121301eb Revert "Fix circular import by moving refine_prompt to platform_utils"
This reverts commit e075873962.
2025-08-11 16:50:18 -04:00
chuckbutkus
97cd7eb0a2 Merge branch 'main' into non-root-user 2025-08-11 16:42:35 -04:00
Chuck Butkus
12e8183336 Revert "test"
This reverts commit b69b01cc15.
2025-08-11 16:42:18 -04:00
openhands
e075873962 Fix circular import by moving refine_prompt to platform_utils
- Created new openhands/utils/platform_utils.py module for platform-specific utilities
- Moved refine_prompt function from openhands/agenthub/codeact_agent/tools/bash.py to platform_utils
- Updated imports in openhands/utils/prompt.py and bash.py to use the new location
- Resolves circular import: utils.prompt -> agenthub -> codeact_agent -> memory.conversation_memory -> utils.prompt

This fixes the ImportError when starting pods:
'cannot import name ConversationInstructions from partially initialized module openhands.utils.prompt'
2025-08-11 06:13:28 +00:00
Chuck Butkus
ae1288a3e8 revert 2025-08-11 02:04:31 -04:00
Chuck Butkus
3a2327a879 Fix circular reference 2025-08-11 01:05:03 -04:00
Chuck Butkus
d1f40ffab1 test 2025-08-11 00:46:18 -04:00
Chuck Butkus
31241f2490 Change shell to bash 2025-08-11 00:24:53 -04:00
Chuck Butkus
08ffd53636 revert 2025-08-11 00:11:02 -04:00
openhands
c88173c1df Fix Docker installation for Ubuntu 24.04 (noble) by using jammy repository 2025-08-11 04:05:02 +00:00
Chuck Butkus
5f8f4fd42c Another try 2025-08-10 23:57:45 -04:00
Chuck Butkus
310fa75535 Revert "Fix Dockerfile.j2"
This reverts commit a0dfbe39bf.
2025-08-10 23:56:46 -04:00
Chuck Butkus
a0dfbe39bf Fix Dockerfile.j2 2025-08-10 23:52:30 -04:00
Chuck Butkus
b69b01cc15 test 2025-08-10 23:33:07 -04:00
chuckbutkus
acf8539a3f Merge branch 'main' into non-root-user 2025-08-10 23:19:14 -04:00
Chuck Butkus
08c6686026 Update Dockerfile 2025-08-10 22:52:39 -04:00
openhands
691f42cf77 Fix test assertions for COPY command with --chown flag
The Dockerfile generation now includes --chown=openhands:openhands in COPY commands,
but the test assertions were still expecting the old format without the chown flag.

Updated test assertions in:
- test_generate_dockerfile_build_from_scratch
- test_generate_dockerfile_build_from_lock
- test_generate_dockerfile_build_from_versioned

This fixes the CI test failures that were occurring due to the mismatch between
expected and actual Dockerfile content.
2025-08-10 05:47:04 +00:00
openhands
39049b173a Fix failing Docker-dependent tests in test_runtime_build.py 2025-08-10 05:31:00 +00:00
chuckbutkus
1b911ea5ba Merge branch 'main' into non-root-user 2025-08-10 01:04:53 -04:00
Chuck Butkus
971fcd7296 Lint fixes 2025-08-10 00:58:01 -04:00
Chuck Butkus
a43778bf24 test 2025-08-10 00:10:35 -04:00
Chuck Butkus
67278772ad More logging 2025-08-09 22:47:49 -04:00
Chuck Butkus
33ec22ff91 More logging 2025-08-09 21:54:30 -04:00
Chuck Butkus
c8dd141997 Logging 2025-08-09 15:18:45 -04:00
Chuck Butkus
e2a56c9302 Fix su 2025-08-08 00:23:25 -04:00
Chuck Butkus
1d53587cbd Fix path 2025-08-07 23:51:58 -04:00
Chuck Butkus
880a798152 Update 2025-08-07 23:11:22 -04:00
chuckbutkus
a356b09f30 Merge branch 'main' into non-root-user 2025-08-07 22:34:55 -04:00
Chuck Butkus
01e91cebe4 Update 2025-08-07 22:30:09 -04:00
Chuck Butkus
93cbba4e06 Another try 2025-08-07 22:05:15 -04:00
Chuck Butkus
df45db5fa1 Try again 2025-08-07 21:16:58 -04:00
Chuck Butkus
36c628014f Update 2025-08-07 17:11:20 -04:00
chuckbutkus
ff38146cb6 Merge branch 'main' into non-root-user 2025-08-07 15:58:29 -04:00
openhands
7512ffc16a Update Dockerfile.j2 to create openhands user and group with proper ownership 2025-08-07 16:27:22 +00:00
Chuck Butkus
aa545c29f1 Revert "Try build for non-root"
This reverts commit 54cdc5c744.
2025-08-07 12:05:53 -04:00
Chuck Butkus
54cdc5c744 Try build for non-root 2025-08-07 00:46:01 -04:00
Chuck Butkus
a5afc1ff7a Another fix 2025-08-06 23:50:35 -04:00
Chuck Butkus
ecf23d3e74 Missed one 2025-08-06 23:48:27 -04:00
Chuck Butkus
c2e080a340 Change linux group name 2025-08-06 23:42:13 -04:00
chuckbutkus
4b2ca6ca71 Merge branch 'main' into update-group-id 2025-08-06 16:58:35 -04:00
Chuck Butkus
42d1a54670 Add debug logging 2025-08-05 12:05:26 -04:00
Chuck Butkus
25261673a1 Fix runtime image creation to create user before using it. 2025-08-05 10:25:57 -04:00
Chuck Butkus
d025d225ad Update user and group creation in Dockerfile 2025-08-05 09:16:08 -04:00
8 changed files with 244 additions and 102 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -646,8 +646,10 @@ class ActionExecutor:
if __name__ == '__main__':
logger.warning('Starting Action Execution Server')
logger.debug('Starting Action Execution Server')
logger.debug('Arguments passed to script:')
for i, arg in enumerate(sys.argv):
logger.debug(f'Argument {i}: {arg}')
parser = argparse.ArgumentParser()
parser.add_argument('port', type=int, help='Port to listen on')
parser.add_argument('--working-dir', type=str, help='Working directory')

View File

@@ -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. '

View File

@@ -1,4 +1,7 @@
import traceback
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 +26,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 +45,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 +66,10 @@ 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}')
logger.debug(
'get_action_execution_server_startup_command stack:\n%s',
''.join(traceback.format_stack()),
)
return base_cmd

View File

@@ -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

View File

@@ -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 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,80 @@ 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 \
# Remove existing user/group by name or UID/GID 1000
if getent passwd openhands >/dev/null 2>&1; then userdel -r -f openhands || true; fi && \
if getent passwd 1000 >/dev/null 2>&1; then userdel -r -f "$(getent passwd 1000 | cut -d: -f1)" || true; fi && \
if getent group openhands >/dev/null 2>&1; then groupdel openhands || true; fi && \
if getent group 1000 >/dev/null 2>&1; then groupdel "$(getent group 1000 | cut -d: -f1)" || true; fi && \
\
# Recreate group with GID 1000
groupadd -g 1000 openhands && \
\
# Recreate user with UID 1000
useradd -u 1000 -g openhands -m -s /bin/bash openhands && \
\
# Ensure home and required directories exist
mkdir -p /home/openhands && \
mkdir -p /openhands && \
mkdir -p $(dirname ${OPENVSCODE_SERVER_ROOT}) && \
\
# Ensure ownership is correct
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

View File

@@ -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():