From 55760ec4ddc669daf4a0b8b36028d2e73c9ab17a Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Mon, 8 Apr 2024 12:59:18 +0800 Subject: [PATCH] feat(sandbox): Support sshd-based stateful docker session (#847) * support sshd-based stateful docker session * use .getLogger to avoid same logging message to get printed twice * update poetry lock for dependency * fix ruff * bump docker image version with sshd * set-up random user password and only allow localhost connection for sandbox * fix poetry * move apt install up --- opendevin/sandbox/Dockerfile | 4 ++ opendevin/sandbox/Makefile | 2 +- opendevin/sandbox/sandbox.py | 86 ++++++++++++++++++++++++------------ poetry.lock | 33 +++++++++++++- pyproject.toml | 1 + 5 files changed, 96 insertions(+), 30 deletions(-) diff --git a/opendevin/sandbox/Dockerfile b/opendevin/sandbox/Dockerfile index 6b4a8924cbdd..4aaeeba5c8d4 100644 --- a/opendevin/sandbox/Dockerfile +++ b/opendevin/sandbox/Dockerfile @@ -14,4 +14,8 @@ RUN apt-get update && apt-get install -y \ python3-venv \ python3-dev \ build-essential \ + openssh-server \ + sudo \ && rm -rf /var/lib/apt/lists/* + +RUN service ssh start diff --git a/opendevin/sandbox/Makefile b/opendevin/sandbox/Makefile index 0ead3c322865..dc13649dc9db 100644 --- a/opendevin/sandbox/Makefile +++ b/opendevin/sandbox/Makefile @@ -1,7 +1,7 @@ DOCKER_BUILD_REGISTRY=ghcr.io DOCKER_BUILD_ORG=opendevin DOCKER_BUILD_REPO=sandbox -DOCKER_BUILD_TAG=v0.1.0 +DOCKER_BUILD_TAG=v0.1.1 FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):$(DOCKER_BUILD_TAG) LATEST_FULL_IMAGE=$(DOCKER_BUILD_REGISTRY)/$(DOCKER_BUILD_ORG)/$(DOCKER_BUILD_REPO):latest diff --git a/opendevin/sandbox/sandbox.py b/opendevin/sandbox/sandbox.py index 072733638f32..494d55073b84 100644 --- a/opendevin/sandbox/sandbox.py +++ b/opendevin/sandbox/sandbox.py @@ -4,11 +4,11 @@ import sys import time import uuid +from pexpect import pxssh from collections import namedtuple from typing import Dict, List, Tuple import docker -import concurrent.futures from opendevin import config from opendevin.logging import opendevin_logger as logger @@ -132,23 +132,50 @@ def __init__( if not self.is_container_running(): self.restart_docker_container() + # set up random user password + self._ssh_password = str(uuid.uuid4()) if RUN_AS_DEVIN: self.setup_devin_user() + self.start_ssh_session() + else: + # TODO: implement ssh into root + raise NotImplementedError( + 'Running as root is not supported at the moment.') atexit.register(self.cleanup) def setup_devin_user(self): exit_code, logs = self.container.exec_run( - [ - '/bin/bash', - '-c', - f'useradd --shell /bin/bash -u {USER_ID} -o -c "" -m devin', - ], + ['/bin/bash', '-c', + f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {USER_ID} opendevin'], + workdir='/workspace', + ) + exit_code, logs = self.container.exec_run( + ['/bin/bash', '-c', + f"echo 'opendevin:{self._ssh_password}' | chpasswd"], + workdir='/workspace', + ) + exit_code, logs = self.container.exec_run( + ['/bin/bash', '-c', "echo 'opendevin-sandbox' > /etc/hostname"], workdir='/workspace', ) + def start_ssh_session(self): + # start ssh session at the background + self.ssh = pxssh.pxssh() + hostname = 'localhost' + username = 'opendevin' + self.ssh.login(hostname, username, self._ssh_password, port=2222) + + # Fix: https://github.com/pexpect/pexpect/issues/669 + self.ssh.sendline("bind 'set enable-bracketed-paste off'") + self.ssh.prompt() + # cd to workspace + self.ssh.sendline('cd /workspace') + self.ssh.prompt() + def get_exec_cmd(self, cmd: str) -> List[str]: if RUN_AS_DEVIN: - return ['su', 'devin', '-c', cmd] + return ['su', 'opendevin', '-c', cmd] else: return ['/bin/bash', '-c', cmd] @@ -159,26 +186,27 @@ def read_logs(self, id) -> str: return bg_cmd.read_logs() def execute(self, cmd: str) -> Tuple[int, str]: - # TODO: each execute is not stateful! We need to keep track of the current working directory - def run_command(container, command): - return container.exec_run(command, workdir='/workspace') - - # Use ThreadPoolExecutor to control command and set timeout - with concurrent.futures.ThreadPoolExecutor() as executor: - future = executor.submit( - run_command, self.container, self.get_exec_cmd(cmd) - ) - try: - exit_code, logs = future.result(timeout=self.timeout) - except concurrent.futures.TimeoutError: - logger.exception( - 'Command timed out, killing process...', exc_info=False) - pid = self.get_pid(cmd) - if pid is not None: - self.container.exec_run( - f'kill -9 {pid}', workdir='/workspace') - return -1, f'Command: "{cmd}" timed out' - return exit_code, logs.decode('utf-8') + # use self.ssh + self.ssh.sendline(cmd) + success = self.ssh.prompt(timeout=self.timeout) + if not success: + logger.exception( + 'Command timed out, killing process...', exc_info=False) + # send a SIGINT to the process + self.ssh.sendintr() + self.ssh.prompt() + command_output = self.ssh.before.decode( + 'utf-8').lstrip(cmd).strip() + return -1, f'Command: "{cmd}" timed out. Sending SIGINT to the process: {command_output}' + command_output = self.ssh.before.decode('utf-8').lstrip(cmd).strip() + + # get the exit code + self.ssh.sendline('echo $?') + self.ssh.prompt() + exit_code = self.ssh.before.decode('utf-8') + # remove the echo $? itself + exit_code = int(exit_code.lstrip('echo $?').strip()) + return exit_code, command_output def execute_in_background(self, cmd: str) -> BackgroundCommand: result = self.container.exec_run( @@ -267,10 +295,12 @@ def restart_docker_container(self): # start the container self.container = docker_client.containers.run( self.container_image, - command='tail -f /dev/null', + # only allow connections from localhost + command="/usr/sbin/sshd -D -p 2222 -o 'ListenAddress=127.0.0.1'", network_mode='host', working_dir='/workspace', name=self.container_name, + hostname='opendevin_sandbox', detach=True, volumes={self.workspace_dir: { 'bind': '/workspace', 'mode': 'rw'}}, diff --git a/poetry.lock b/poetry.lock index 7e5d80ed2a39..12331f39e91d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3363,6 +3363,20 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + [[package]] name = "pillow" version = "10.3.0" @@ -3596,6 +3610,17 @@ files = [ {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, ] +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + [[package]] name = "pulsar-client" version = "3.4.0" @@ -3909,26 +3934,31 @@ python-versions = ">=3.8" files = [ {file = "PyMuPDF-1.24.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:6427aee313e24447f57edbfc7a28aa6bbca007fe0ad77603f54a371c6c510eeb"}, {file = "PyMuPDF-1.24.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:12078c0bee337de969dbd6d89ef446312794d74db365cb9ac14902b863b35414"}, + {file = "PyMuPDF-1.24.1-cp310-none-manylinux2014_aarch64.whl", hash = "sha256:73f86eefd7f3878f112fa10791aa2e63934cf59a4c024dd54cd6fe94443c352c"}, {file = "PyMuPDF-1.24.1-cp310-none-manylinux2014_x86_64.whl", hash = "sha256:caf6ceb1dbebe9f70bf7dd683cc91b896604a7c62873e5b50089f9e85e85c517"}, {file = "PyMuPDF-1.24.1-cp310-none-win32.whl", hash = "sha256:468a8bb2b95828e0f6739fbfe509700cc0dac600f756d8cb6316316e1eba9689"}, {file = "PyMuPDF-1.24.1-cp310-none-win_amd64.whl", hash = "sha256:e47504391908e2d721c743aed36196310a5e15355a85459c1c4ddcf8f2002fbe"}, {file = "PyMuPDF-1.24.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:c54ff927257b432ffd39dc6a0a46bd1120e85d192100efca021f27d4b881cfd6"}, {file = "PyMuPDF-1.24.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:6d412da9f9a73f66973eea4284776f292135906700a06c39122e862a1e3ccf58"}, + {file = "PyMuPDF-1.24.1-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:95a54611abb7322f5b10b44cbf19b605ed172df2c4c7995ad78854bc8423dd9c"}, {file = "PyMuPDF-1.24.1-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:9a3b21c8fc274ff42855ca2da65961e2319b05b75ef9e2caf25c04f9083ec79c"}, {file = "PyMuPDF-1.24.1-cp311-none-win32.whl", hash = "sha256:8a81106a8bc229823736487d2492fd3af724a94521a1cd9b67849dd04b9c31ed"}, {file = "PyMuPDF-1.24.1-cp311-none-win_amd64.whl", hash = "sha256:de5b6c4db4a2a9f28937e79135f732827c424f7444c12767cc1081c8006f0430"}, {file = "PyMuPDF-1.24.1-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:02a6586979df2ad958b524ba42955beaa67fd21661616a0ed04ac07db009474c"}, {file = "PyMuPDF-1.24.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8eb292d16671166acdaa280e98cac4368298f32556f2de2ee690782a635df8ee"}, + {file = "PyMuPDF-1.24.1-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:f7b7f2011fa522a57fb3d6a7a58bcdcf01ee59bdad536ef9eb5c3fdf1e04e6c3"}, {file = "PyMuPDF-1.24.1-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:6832f1d9332810760b587ad375eb84d64ec8d8f29395995b463cb5f30533a413"}, {file = "PyMuPDF-1.24.1-cp312-none-win32.whl", hash = "sha256:f775bb56391629e81b5f870fc3dec0a0fb44cb34a92b4696b9207b31234711df"}, {file = "PyMuPDF-1.24.1-cp312-none-win_amd64.whl", hash = "sha256:8489df092473d590fb14903433bd99a07dc3d2924f5a5c8ead615795f2d65a65"}, {file = "PyMuPDF-1.24.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:ee9cfac470aeb6b5b7deb4f6472b7796c3132856849c635c8e56c7a371e40238"}, {file = "PyMuPDF-1.24.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:825c62367b01e61b4bce0cc96d45b0ec336475422cfa36de6f441b4d3389a26e"}, + {file = "PyMuPDF-1.24.1-cp38-none-manylinux2014_aarch64.whl", hash = "sha256:73d07e127936948a29a7dbd4c831e9eb45a60b495d72e604d454fd040fd08c5f"}, {file = "PyMuPDF-1.24.1-cp38-none-manylinux2014_x86_64.whl", hash = "sha256:d2b4f8956d0ca7564604491db8b29cd7872a2b4d65f1d7e16a1bccfecf84bb56"}, {file = "PyMuPDF-1.24.1-cp38-none-win32.whl", hash = "sha256:7df966954ff0edbcd5d743c5f6fb68b3203e67534747e8753691b8ffedeaa518"}, {file = "PyMuPDF-1.24.1-cp38-none-win_amd64.whl", hash = "sha256:6952d47f0f05cf9338470dda078e4533ddb876368b199ebfa2f9e6076311898b"}, {file = "PyMuPDF-1.24.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:e3f7a101a14d742c93b660b7586ab4c1491caea9062a5de9c308578a7a4f8b69"}, {file = "PyMuPDF-1.24.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:dbc5d67dfd07123293993eb93bee35d329fce0bc8134b9cd5514ef75c68ffee8"}, + {file = "PyMuPDF-1.24.1-cp39-none-manylinux2014_aarch64.whl", hash = "sha256:0edda1024ada67603e5888f31656048d3fd53167c8b0d56f435b986eb507df8f"}, {file = "PyMuPDF-1.24.1-cp39-none-manylinux2014_x86_64.whl", hash = "sha256:38728bb6aab9e3879aa8ac4d337be8fe838d33973f43e3b7805b86265c24f349"}, {file = "PyMuPDF-1.24.1-cp39-none-win32.whl", hash = "sha256:b8a5247d0cec87765481c38d2b8602f0264bf7ca6b5dc3013caf64ce46ad4d5e"}, {file = "PyMuPDF-1.24.1-cp39-none-win_amd64.whl", hash = "sha256:d1078ea265635e962693d7298bd39be64af7d1dd2c6dc663a8562e75f547f948"}, @@ -3947,6 +3977,7 @@ python-versions = ">=3.8" files = [ {file = "PyMuPDFb-1.24.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:37179e363bf69ce9be637937c5469957b96968341dabe3ce8f4b690a82e9ad92"}, {file = "PyMuPDFb-1.24.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:17444ea7d6897c27759880ad76af537d19779f901de82ae9548598a70f614558"}, + {file = "PyMuPDFb-1.24.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:490f7fff4dbe362bc895cefdfc5030d712311d024d357a1388d64816eb215d34"}, {file = "PyMuPDFb-1.24.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0fbcc0d2a9ce79fa38eb4e8bb5c959b582f7a49938874e9f61d1a6f5eeb1e4b8"}, {file = "PyMuPDFb-1.24.1-py3-none-win32.whl", hash = "sha256:ae67736058882cdd9459810a4aae9ac2b2e89ac2e916cb5fefb0f651c9739e9e"}, {file = "PyMuPDFb-1.24.1-py3-none-win_amd64.whl", hash = "sha256:01c8b7f0ce9166310eb28c7aebcb8d5fe12a4bc082f9b00d580095eebeaf0af5"}, @@ -5874,4 +5905,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "eb7d77f58c52f70702e9a8501084b09c307d62caf179428b70b781860508a0fb" +content-hash = "0168adb891fac11fcad6bcfe2e8d13453040f5d5e6ebd8c6713e36d8e4a318da" diff --git a/pyproject.toml b/pyproject.toml index 1ea27c7c406e..bb1c234c2316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ types-toml = "*" numpy = "*" json-repair = "*" playwright = "*" +pexpect = "*" [tool.poetry.group.llama-index.dependencies] llama-index = "*"