From ccbbabac1cc14cc2fce7f36890b74fad0d536e00 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Wed, 1 May 2024 00:39:52 +0800 Subject: [PATCH] Get RUN_AS_DEVIN working with app sandbox (#1426) * get RUN_AS_DEVIN and network=host working with app sandbox * attempt to fix the workspace base permission * sandbox might failed in chown due to mounting, but it won't be fatal * update sshbox instruction * remove default user id since it will be passed in the instruction * revert permission fix since it should be resolved by correct SANDBOX_USER_ID * the permission issue can be fixed by simply provide correct env var * remove log * set sandbox user id to getuid by default * move logging to initializer * make the uid consistent across host, app container, and sandbox * remove hostname as it causes sudo issue * fix permission of entrypoint script * make the uvicron app run as host user uid for jupyter plugin * revert use host network * get docker socket gid and usermod instead of chmod 777 * try to fix app build disk space issue --- .github/workflows/ghcr.yml | 17 ++++++++++++++-- containers/app/Dockerfile | 30 ++++++++++++++++++++++++++--- containers/app/entrypoint.sh | 23 ++++++++++++++++++++++ opendevin/config.py | 1 + opendevin/sandbox/docker/ssh_box.py | 9 +++++---- 5 files changed, 71 insertions(+), 9 deletions(-) create mode 100755 containers/app/entrypoint.sh diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index d6cb826edc..7d8405ebaf 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -42,8 +42,21 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: true + + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true - name: Build and push ${{ matrix.image }} if: github.event.pull_request.head.repo.full_name == github.repository diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index 9d456a6271..be434419f0 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -32,7 +32,8 @@ FROM python:3.12-slim as runtime WORKDIR /app -ENV RUN_AS_DEVIN=false +ENV RUN_AS_DEVIN=true +ENV SANDBOX_USER_ID=1000 ENV USE_HOST_NETWORK=false ENV SSH_HOSTNAME=host.docker.internal ENV WORKSPACE_BASE=/opt/workspace_base @@ -40,13 +41,23 @@ ENV OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION RUN mkdir -p $WORKSPACE_BASE RUN apt-get update -y \ - && apt-get install -y curl ssh + && apt-get install -y curl ssh sudo + +RUN useradd -m -u $SANDBOX_USER_ID -s /bin/bash opendevin && \ + usermod -aG sudo opendevin && \ + echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN chown -R opendevin:opendevin /app +USER opendevin ENV VIRTUAL_ENV=/app/.venv \ PATH="/app/.venv/bin:$PATH" \ PYTHONPATH='/app' COPY --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} +# change ownership of the virtual environment to the sandbox user +USER root +RUN chown -R opendevin:opendevin ${VIRTUAL_ENV} +USER opendevin COPY ./opendevin ./opendevin COPY ./agenthub ./agenthub @@ -55,4 +66,17 @@ RUN playwright install --with-deps chromium COPY --from=frontend-builder /app/dist ./frontend/dist -CMD ["uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"] +USER root +RUN chown -R opendevin:opendevin /app +# make group permissions the same as user permissions +RUN chmod -R g=u /app +USER opendevin + +# change ownership of the app directory to the sandbox user +COPY ./containers/app/entrypoint.sh /app/entrypoint.sh + +# run the script as root +USER root +RUN chown opendevin:opendevin /app/entrypoint.sh +RUN chmod 777 /app/entrypoint.sh +CMD ["/app/entrypoint.sh"] diff --git a/containers/app/entrypoint.sh b/containers/app/entrypoint.sh new file mode 100755 index 0000000000..843790b352 --- /dev/null +++ b/containers/app/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# check user is root +if [ "$(id -u)" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if [ -z "$SANDBOX_USER_ID" ]; then + echo "SANDBOX_USER_ID is not set" + exit 1 +fi + +# change uid of opendevin user to match the host user +# but the group id is not changed, so the user can still access everything under /app +usermod -u $SANDBOX_USER_ID opendevin + +# get the user group of /var/run/docker.sock and set opendevin to that group +DOCKER_SOCKET_GID=$(stat -c '%g' /var/run/docker.sock) +echo "Docker socket group id: $DOCKER_SOCKET_GID" +usermod -aG $DOCKER_SOCKET_GID opendevin + +# switch to the user and start the server +su opendevin -c "cd /app && uvicorn opendevin.server.listen:app --host 0.0.0.0 --port 3000" diff --git a/opendevin/config.py b/opendevin/config.py index e38790a85a..91917b9e3e 100644 --- a/opendevin/config.py +++ b/opendevin/config.py @@ -52,6 +52,7 @@ DEFAULT_CONFIG: dict = { ConfigType.USE_HOST_NETWORK: 'false', ConfigType.SSH_HOSTNAME: 'localhost', ConfigType.DISABLE_COLOR: 'false', + ConfigType.SANDBOX_USER_ID: os.getuid() if hasattr(os, 'getuid') else None, ConfigType.SANDBOX_TIMEOUT: 120, ConfigType.GITHUB_TOKEN: None } diff --git a/opendevin/sandbox/docker/ssh_box.py b/opendevin/sandbox/docker/ssh_box.py index 142c3c211f..d0aef06fb1 100644 --- a/opendevin/sandbox/docker/ssh_box.py +++ b/opendevin/sandbox/docker/ssh_box.py @@ -41,7 +41,6 @@ if SANDBOX_USER_ID := config.get(ConfigType.SANDBOX_USER_ID): elif hasattr(os, 'getuid'): USER_ID = os.getuid() - class DockerSSHBox(Sandbox): instance_id: str container_image: str @@ -62,6 +61,7 @@ class DockerSSHBox(Sandbox): timeout: int = 120, sid: str | None = None, ): + logger.info(f'SSHBox is running as {"opendevin" if RUN_AS_DEVIN else "root"} user with USER_ID={USER_ID} in the sandbox') # Initialize docker client. Throws an exception if Docker is not reachable. try: self.docker_client = docker.from_env() @@ -150,8 +150,10 @@ class DockerSSHBox(Sandbox): workdir=SANDBOX_WORKSPACE_DIR, ) if exit_code != 0: - raise Exception( - f'Failed to chown workspace directory for opendevin in sandbox: {logs}') + # This is not a fatal error, just a warning + logger.warning( + f'Failed to chown workspace directory for opendevin in sandbox: {logs}. But this should be fine if the {SANDBOX_WORKSPACE_DIR=} is mounted by the app docker container.' + ) else: exit_code, logs = self.container.exec_run( # change password for root @@ -351,7 +353,6 @@ class DockerSSHBox(Sandbox): **network_kwargs, working_dir=SANDBOX_WORKSPACE_DIR, name=self.container_name, - hostname='opendevin_sandbox', detach=True, volumes={ mount_dir: {