mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,11 +4,11 @@ import select
|
||||
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 @@ class DockerInteractive(CommandExecutor):
|
||||
|
||||
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 @@ class DockerInteractive(CommandExecutor):
|
||||
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 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()
|
||||
|
||||
# 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')
|
||||
# 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 @@ class DockerInteractive(CommandExecutor):
|
||||
# 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'}},
|
||||
|
||||
33
poetry.lock
generated
33
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -23,6 +23,7 @@ types-toml = "*"
|
||||
numpy = "*"
|
||||
json-repair = "*"
|
||||
playwright = "*"
|
||||
pexpect = "*"
|
||||
|
||||
[tool.poetry.group.llama-index.dependencies]
|
||||
llama-index = "*"
|
||||
|
||||
Reference in New Issue
Block a user