Compare commits

...

16 Commits

Author SHA1 Message Date
openhands
7ce89b4348 Add entrypoint script to fix workspace ownership at container startup
- Create entrypoint.sh that runs as root to fix /workspace ownership
- Modify Dockerfile.j2 to use the entrypoint script
- Entrypoint fixes ownership then switches to openhands user
- This ensures mounted volumes get correct ownership before action execution server starts
- Handles the case where Docker volume mounts override Dockerfile ownership settings
2025-08-07 18:50:41 +00:00
openhands
d23ee42b91 Enhanced workspace ownership fix with detailed logging and fallbacks
- Add detailed logging to debug chown/chmod command execution
- Check current user and use sudo only when not running as root
- Add fallback approaches if chown fails (chmod g+rwx, usermod -aG root)
- Verify ownership before and after changes
- Handle mounted volumes that remain root-owned
2025-08-07 18:48:45 +00:00
openhands
9c2b5039d2 Fix workspace ownership with sudo and error logging
- Use sudo for chown and chmod commands to handle root-owned mounted volumes
- Add error logging to debug permission issues
- Ensure /workspace directory ownership is properly set to openhands:openhands even when mounted as volume
2025-08-07 18:24:08 +00:00
openhands
c877d076ba Fix workspace directory ownership to use openhands:openhands
- Change chown command in runtime_init.py from username:root to username:username
- This ensures /workspace directory and all contents are owned by openhands:openhands
- Complements the Dockerfile.j2 fixes for comprehensive permission management
2025-08-07 18:04:58 +00:00
chuckbutkus
75adcc48f5 Merge branch 'main' into update-group-id 2025-08-07 13:16:14 -04:00
openhands
347b45877d Fix Dockerfile.j2 template structure to properly create workspace directories
- Move user and directory creation into appropriate template sections
- Add user creation to setup_base_system() macro for build_from_scratch path
- Add conditional user creation for default build path (non-scratch builds)
- Ensure /workspace and /workspace/.openhands directories are created with proper ownership
- Use conditional checks to avoid conflicts with existing users/groups
- Template now works correctly for all build scenarios: default, from_scratch, and from_versioned
2025-08-07 17:08:12 +00: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
5 changed files with 233 additions and 86 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

@@ -676,7 +676,9 @@ class ActionExecutor:
if __name__ == '__main__':
logger.warning('Starting Action Execution Server')
logger.warning('Arguments passed to script:')
for i, arg in enumerate(sys.argv):
logger.warning(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

@@ -49,72 +49,124 @@ 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.info(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.info(
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}'
output = subprocess.run(command, shell=True, capture_output=True)
out_str = output.stdout.decode()
logger.debug(f'mkdir command result: returncode={output.returncode}, stdout=[{out_str}], stderr=[{output.stderr.decode()}]')
command = f'chown -R {username}:root {initial_cwd}'
# Check current ownership before changing it
check_cmd = f'ls -la {initial_cwd}'
check_output = subprocess.run(check_cmd, shell=True, capture_output=True)
logger.debug(f'Current ownership: {check_output.stdout.decode()}')
# Check if we're running as root
whoami_output = subprocess.run('whoami', shell=True, capture_output=True)
current_user = whoami_output.stdout.decode().strip()
logger.debug(f'Current user: {current_user}')
# Use sudo only if not running as root
sudo_prefix = '' if current_user == 'root' else 'sudo '
command = f'{sudo_prefix}chown -R {username}:{username} {initial_cwd}'
logger.debug(f'Executing chown command: {command}')
output = subprocess.run(command, shell=True, capture_output=True)
out_str += output.stdout.decode()
logger.debug(f'chown command result: returncode={output.returncode}, stdout=[{output.stdout.decode()}], stderr=[{output.stderr.decode()}]')
if output.returncode != 0 or output.stderr:
err_str = output.stderr.decode()
logger.error(f'chown command failed: returncode={output.returncode}, stderr: {err_str}')
out_str += f' [stderr: {err_str}]'
command = f'chmod g+rw {initial_cwd}'
command = f'{sudo_prefix}chmod g+rw {initial_cwd}'
logger.debug(f'Executing chmod command: {command}')
output = subprocess.run(command, shell=True, capture_output=True)
out_str += output.stdout.decode()
logger.debug(f'chmod command result: returncode={output.returncode}, stdout=[{output.stdout.decode()}], stderr=[{output.stderr.decode()}]')
if output.returncode != 0 or output.stderr:
err_str = output.stderr.decode()
logger.error(f'chmod command failed: returncode={output.returncode}, stderr: {err_str}')
out_str += f' [stderr: {err_str}]'
# Verify final ownership
check_cmd = f'ls -la {initial_cwd}'
check_output = subprocess.run(check_cmd, shell=True, capture_output=True)
final_ownership = check_output.stdout.decode()
logger.debug(f'Final ownership: {final_ownership}')
# If chown failed and directory is still owned by root, try alternative approaches
if 'root root' in final_ownership and username != 'root':
logger.warning(f'Directory {initial_cwd} is still owned by root, trying alternative approaches')
# Try to make it writable for the user's group
alt_command = f'{sudo_prefix}chmod -R g+rwx {initial_cwd}'
logger.debug(f'Executing alternative chmod command: {alt_command}')
alt_output = subprocess.run(alt_command, shell=True, capture_output=True)
logger.debug(f'Alternative chmod result: returncode={alt_output.returncode}, stderr=[{alt_output.stderr.decode()}]')
# Try to add the user to the root group (as a last resort)
if alt_output.returncode != 0:
group_command = f'{sudo_prefix}usermod -aG root {username}'
logger.debug(f'Executing usermod command: {group_command}')
group_output = subprocess.run(group_command, shell=True, capture_output=True)
logger.debug(f'Usermod result: returncode={group_output.returncode}, stderr=[{group_output.stderr.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

@@ -56,16 +56,8 @@ 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
RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi)
# Create necessary directories
RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry
# ================================================================
# Define Docker installation macro
@@ -111,6 +103,29 @@ RUN \
# Configure Docker daemon with MTU 1450 to prevent packet fragmentation issues
RUN mkdir -p /etc/docker && \
echo '{"mtu": 1450}' > /etc/docker/daemon.json
# 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 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
# Create necessary directories
RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry && \
mkdir -p /workspace && \
mkdir -p /workspace/.openhands && \
mkdir -p /home/openhands/.openhands && \
chown -R openhands:openhands /openhands && \
chown -R openhands:openhands /workspace && \
chown -R openhands:openhands /home/openhands
{% endmacro %}
# Install Docker only if not a swebench or mswebench image
@@ -150,7 +165,8 @@ 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}
@@ -159,10 +175,12 @@ RUN if [ -z "${RELEASE_TAG}" ]; then \
{% macro install_vscode_extensions() %}
# Install our custom extension
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/
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/hello-world/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world/ && \
chown -R openhands:openhands ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world
RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/memory-monitor/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor/
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/memory-monitor/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor/ && \
chown -R openhands:openhands ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor
# Some extension dirs are removed because they trigger false positives in vulnerability scans.
RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,ini,npm}
@@ -185,9 +203,12 @@ RUN \
{% 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
# Set permissions and ownership
chmod -R g+rws /openhands/poetry && \
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 && \
# Clean up
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
@@ -208,7 +229,8 @@ 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 the openhands virtual environment and install poetry and python
RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \
@@ -219,11 +241,12 @@ 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() }}
@@ -234,14 +257,43 @@ COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
{{ setup_vscode_server() }}
# ================================================================
# Ensure openhands user and directories exist (for non-scratch builds)
# ================================================================
{% if not build_from_scratch %}
# 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 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 if they don't exist
RUN (getent group openhands || groupadd -g 1000 openhands) && \
(getent passwd openhands || useradd -u 1000 -g 1000 -m -s /bin/bash openhands) && \
usermod -aG sudo openhands && \
echo 'openhands ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# Create necessary directories and set ownership
RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry && \
mkdir -p /workspace && \
mkdir -p /workspace/.openhands && \
mkdir -p /home/openhands/.openhands && \
chown -R openhands:openhands /openhands && \
chown -R openhands:openhands /workspace && \
chown -R openhands:openhands /home/openhands
{% endif %}
# ================================================================
# 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/
COPY ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py
COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py && \
chown -R openhands:openhands /openhands/code
@@ -255,3 +307,12 @@ RUN chmod a+rwx /openhands/code/openhands/__init__.py
# Install extra dependencies if specified
{% if extra_deps %}RUN {{ extra_deps }} {% endif %}
# Copy entrypoint script and make it executable
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Set the entrypoint to run as root first, then switch to openhands
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# Note: We don't set USER openhands here because the entrypoint handles the user switch

View File

@@ -0,0 +1,32 @@
#!/bin/bash
set -e
# This entrypoint script runs as root to fix workspace ownership before switching to openhands user
echo "🔧 OpenHands Runtime Entrypoint - Fixing workspace ownership..."
# Check if /workspace exists and fix ownership
if [ -d "/workspace" ]; then
echo "📁 Found /workspace directory, checking ownership..."
ls -la /workspace
# Fix ownership to openhands:openhands
echo "🔧 Changing ownership to openhands:openhands..."
chown -R openhands:openhands /workspace
chmod -R g+rw /workspace
echo "✅ Ownership fixed:"
ls -la /workspace
else
echo "⚠️ /workspace directory not found, will be created later"
fi
# If arguments are provided, execute them as the openhands user
if [ $# -gt 0 ]; then
echo "🚀 Switching to openhands user and executing: $@"
# Use exec to replace the current process and preserve all arguments
exec su openhands -c "exec \"\$@\"" -- "$@"
else
echo "🚀 Switching to openhands user with bash shell"
exec su - openhands
fi