mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 23:38:08 -05:00
[Arch] Removing docker exec box (#2802)
* depracting docker exec box * remove doc exec from workflow and docs
This commit is contained in:
2
.github/workflows/ghcr.yml
vendored
2
.github/workflows/ghcr.yml
vendored
@@ -132,7 +132,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
sandbox: ["ssh", "exec", "local"]
|
||||
sandbox: ["ssh", "local"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
2
.github/workflows/review-pr.yml
vendored
2
.github/workflows/review-pr.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
env:
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SANDBOX_TYPE: exec
|
||||
SANDBOX_TYPE: ssh
|
||||
run: |
|
||||
# Append path to launch poetry
|
||||
export PATH="/github/home/.local/bin:$PATH"
|
||||
|
||||
4
.github/workflows/solve-issue.yml
vendored
4
.github/workflows/solve-issue.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
echo "" >> task.txt
|
||||
echo "BODY:" >> task.txt
|
||||
echo "${ISSUE_BODY}" >> task.txt
|
||||
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SANDBOX_TYPE: exec
|
||||
SANDBOX_TYPE: ssh
|
||||
run: |
|
||||
# Append path to launch poetry
|
||||
export PATH="/github/home/.local/bin:$PATH"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
CommitWriterAgent can help write git commit message. Example:
|
||||
|
||||
```bash
|
||||
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_TYPE="exec" \
|
||||
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_TYPE="ssh" \
|
||||
poetry run python opendevin/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
|
||||
```
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ en particulier Windows, cela semble échouer.
|
||||
* Assurez-vous d'avoir les dernières versions de WSL et Docker
|
||||
* Vérifiez que votre distribution dans WSL est également à jour
|
||||
* Essayez [ce guide de réinstallation](https://github.com/OpenDevin/OpenDevin/issues/1156#issuecomment-2064549427)
|
||||
* Définissez `-e SANDBOX_TYPE=exec` pour passer au conteneur ExecBox de Docker
|
||||
|
||||
## Impossible de se connecter à LLM
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ pexpect.pxssh.ExceptionPxssh: Could not establish connection to host
|
||||
* 确保拥有最新版本的 WSL 和 Docker
|
||||
* 检查您的 WSL 分发版也已更新
|
||||
* 尝试[此重新安装指南](https://github.com/OpenDevin/OpenDevin/issues/1156#issuecomment-2064549427)
|
||||
* 设置 `-e SANDBOX_TYPE=exec` 切换到 ExecBox Docker 容器
|
||||
|
||||
## 无法连接到 LLM
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ especially Windows, this seems to fail.
|
||||
* Be sure to have the latest versions of WSL and Docker
|
||||
* Check that your distribution in WSL is up to date as well
|
||||
* Try [this reinstallation guide](https://github.com/OpenDevin/OpenDevin/issues/1156#issuecomment-2064549427)
|
||||
* Set `-e SANDBOX_TYPE=exec` to switch to the ExecBox docker container
|
||||
|
||||
---
|
||||
### Unable to connect to LLM
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from .docker.exec_box import DockerExecBox
|
||||
from .docker.local_box import LocalBox
|
||||
from .docker.ssh_box import DockerSSHBox
|
||||
from .e2b.sandbox import E2BBox
|
||||
from .sandbox import Sandbox
|
||||
|
||||
__all__ = ['Sandbox', 'DockerSSHBox', 'DockerExecBox', 'E2BBox', 'LocalBox']
|
||||
__all__ = ['Sandbox', 'DockerSSHBox', 'E2BBox', 'LocalBox']
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
import atexit
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
import uuid
|
||||
from collections import namedtuple
|
||||
from glob import glob
|
||||
|
||||
import docker
|
||||
|
||||
from opendevin.core.config import config
|
||||
from opendevin.core.const.guide_url import TROUBLESHOOTING_URL
|
||||
from opendevin.core.exceptions import SandboxInvalidBackgroundCommandError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import CancellableStream
|
||||
from opendevin.runtime.docker.process import DockerProcess, Process
|
||||
from opendevin.runtime.sandbox import Sandbox
|
||||
|
||||
ExecResult = namedtuple('ExecResult', 'exit_code,output')
|
||||
""" A result of Container.exec_run with the properties ``exit_code`` and
|
||||
``output``. """
|
||||
|
||||
|
||||
class DockerExecCancellableStream(CancellableStream):
|
||||
# Reference: https://github.com/docker/docker-py/issues/1989
|
||||
def __init__(self, _client, _id, _output):
|
||||
super().__init__(self.read_output())
|
||||
self._id = _id
|
||||
self._client = _client
|
||||
self._output = _output
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def exit_code(self):
|
||||
return self.inspect()['ExitCode']
|
||||
|
||||
def inspect(self):
|
||||
return self._client.api.exec_inspect(self._id)
|
||||
|
||||
def read_output(self):
|
||||
for chunk in self._output:
|
||||
yield chunk.decode('utf-8')
|
||||
|
||||
|
||||
def container_exec_run(
|
||||
container,
|
||||
cmd,
|
||||
stdout=True,
|
||||
stderr=True,
|
||||
stdin=False,
|
||||
tty=False,
|
||||
privileged=False,
|
||||
user='',
|
||||
detach=False,
|
||||
stream=False,
|
||||
socket=False,
|
||||
environment=None,
|
||||
workdir=None,
|
||||
) -> ExecResult:
|
||||
exec_id = container.client.api.exec_create(
|
||||
container.id,
|
||||
cmd,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
stdin=stdin,
|
||||
tty=tty,
|
||||
privileged=privileged,
|
||||
user=user,
|
||||
environment=environment,
|
||||
workdir=workdir,
|
||||
)['Id']
|
||||
|
||||
output = container.client.api.exec_start(
|
||||
exec_id, detach=detach, tty=tty, stream=stream, socket=socket
|
||||
)
|
||||
|
||||
if stream:
|
||||
return ExecResult(
|
||||
None, DockerExecCancellableStream(container.client, exec_id, output)
|
||||
)
|
||||
|
||||
if socket:
|
||||
return ExecResult(None, output)
|
||||
|
||||
return ExecResult(container.client.api.exec_inspect(exec_id)['ExitCode'], output)
|
||||
|
||||
|
||||
class DockerExecBox(Sandbox):
|
||||
instance_id: str
|
||||
container_image: str
|
||||
container_name_prefix = 'opendevin-sandbox-'
|
||||
container_name: str
|
||||
container: docker.models.containers.Container
|
||||
docker_client: docker.DockerClient
|
||||
|
||||
cur_background_id = 0
|
||||
background_commands: dict[int, Process] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
container_image: str | None = None,
|
||||
timeout: int = config.sandbox_timeout,
|
||||
sid: str | None = None,
|
||||
):
|
||||
# Initialize docker client. Throws an exception if Docker is not reachable.
|
||||
try:
|
||||
self.docker_client = docker.from_env()
|
||||
except Exception as ex:
|
||||
logger.exception(
|
||||
f'Error creating controller. Please check Docker is running and visit `{TROUBLESHOOTING_URL}` for more debugging information.',
|
||||
exc_info=False,
|
||||
)
|
||||
raise ex
|
||||
|
||||
self.instance_id = (
|
||||
sid + str(uuid.uuid4()) if sid is not None else str(uuid.uuid4())
|
||||
)
|
||||
|
||||
# TODO: this timeout is actually essential - need a better way to set it
|
||||
# if it is too short, the container may still waiting for previous
|
||||
# command to finish (e.g. apt-get update)
|
||||
# if it is too long, the user may have to wait for a unnecessary long time
|
||||
self.timeout = timeout
|
||||
self.container_image = (
|
||||
config.sandbox_container_image
|
||||
if container_image is None
|
||||
else container_image
|
||||
)
|
||||
self.container_name = self.container_name_prefix + self.instance_id
|
||||
|
||||
logger.info(
|
||||
'Starting Docker container with image %s, sandbox workspace dir=%s',
|
||||
self.container_image,
|
||||
self.sandbox_workspace_dir,
|
||||
)
|
||||
|
||||
# always restart the container, cuz the initial be regarded as a new session
|
||||
self.restart_docker_container()
|
||||
|
||||
if self.run_as_devin:
|
||||
self.setup_devin_user()
|
||||
atexit.register(self.close)
|
||||
super().__init__()
|
||||
|
||||
def setup_devin_user(self):
|
||||
cmds = [
|
||||
f'useradd --shell /bin/bash -u {self.user_id} -o -c "" -m devin',
|
||||
r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers",
|
||||
'sudo adduser devin sudo',
|
||||
]
|
||||
for cmd in cmds:
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c', cmd],
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(f'Failed to setup devin user: {logs}')
|
||||
|
||||
def get_exec_cmd(self, cmd: str) -> list[str]:
|
||||
if self.run_as_devin:
|
||||
return ['su', 'devin', '-c', cmd]
|
||||
else:
|
||||
return ['/bin/bash', '-c', cmd]
|
||||
|
||||
def read_logs(self, id) -> str:
|
||||
if id not in self.background_commands:
|
||||
raise SandboxInvalidBackgroundCommandError()
|
||||
bg_cmd = self.background_commands[id]
|
||||
return bg_cmd.read_logs()
|
||||
|
||||
def execute(
|
||||
self, cmd: str, stream: bool = False, timeout: int | None = None
|
||||
) -> tuple[int, str | CancellableStream]:
|
||||
timeout = timeout if timeout is not None else self.timeout
|
||||
wrapper = f'timeout {self.timeout}s bash -c {shlex.quote(cmd)}'
|
||||
_exit_code, _output = container_exec_run(
|
||||
self.container,
|
||||
wrapper,
|
||||
stream=stream,
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
|
||||
if stream:
|
||||
return _exit_code, _output
|
||||
|
||||
print(_output)
|
||||
_output = _output.decode('utf-8')
|
||||
if _output.endswith('\n'):
|
||||
_output = _output[:-1]
|
||||
return _exit_code, _output
|
||||
|
||||
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
|
||||
# mkdir -p sandbox_dest if it doesn't exist
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c', f'mkdir -p {sandbox_dest}'],
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to create directory {sandbox_dest} in sandbox: {logs}'
|
||||
)
|
||||
|
||||
if recursive:
|
||||
assert os.path.isdir(
|
||||
host_src
|
||||
), 'Source must be a directory when recursive is True'
|
||||
files = glob(host_src + '/**/*', recursive=True)
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
for file in files:
|
||||
tar.add(
|
||||
file, arcname=os.path.relpath(file, os.path.dirname(host_src))
|
||||
)
|
||||
else:
|
||||
assert os.path.isfile(
|
||||
host_src
|
||||
), 'Source must be a file when recursive is False'
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
|
||||
with tarfile.open(tar_filename, mode='w') as tar:
|
||||
tar.add(host_src, arcname=srcname)
|
||||
|
||||
with open(tar_filename, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
self.container.put_archive(os.path.dirname(sandbox_dest), data)
|
||||
os.remove(tar_filename)
|
||||
|
||||
def execute_in_background(self, cmd: str) -> Process:
|
||||
result = self.container.exec_run(
|
||||
self.get_exec_cmd(cmd),
|
||||
socket=True,
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
result.output._sock.setblocking(0)
|
||||
pid = self.get_pid(cmd)
|
||||
bg_cmd = DockerProcess(self.cur_background_id, cmd, result, pid)
|
||||
self.background_commands[bg_cmd.pid] = bg_cmd
|
||||
self.cur_background_id += 1
|
||||
return bg_cmd
|
||||
|
||||
def get_pid(self, cmd):
|
||||
exec_result = self.container.exec_run('ps aux', environment=self._env)
|
||||
processes = exec_result.output.decode('utf-8').splitlines()
|
||||
cmd = ' '.join(self.get_exec_cmd(cmd))
|
||||
|
||||
for process in processes:
|
||||
if cmd in process:
|
||||
pid = process.split()[1] # second column is the pid
|
||||
return pid
|
||||
return None
|
||||
|
||||
def kill_background(self, id: int) -> Process:
|
||||
if id not in self.background_commands:
|
||||
raise SandboxInvalidBackgroundCommandError()
|
||||
bg_cmd = self.background_commands[id]
|
||||
if bg_cmd.pid is not None:
|
||||
self.container.exec_run(
|
||||
f'kill -9 {bg_cmd.pid}',
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
assert isinstance(bg_cmd, DockerProcess)
|
||||
bg_cmd.result.output.close()
|
||||
self.background_commands.pop(id)
|
||||
return bg_cmd
|
||||
|
||||
def stop_docker_container(self):
|
||||
try:
|
||||
container = self.docker_client.containers.get(self.container_name)
|
||||
container.stop()
|
||||
container.remove()
|
||||
elapsed = 0
|
||||
while container.status != 'exited':
|
||||
time.sleep(1)
|
||||
elapsed += 1
|
||||
if elapsed > self.timeout:
|
||||
break
|
||||
container = self.docker_client.containers.get(self.container_name)
|
||||
except docker.errors.NotFound:
|
||||
pass
|
||||
|
||||
def is_container_running(self):
|
||||
try:
|
||||
container = self.docker_client.containers.get(self.container_name)
|
||||
if container.status == 'running':
|
||||
self.container = container
|
||||
return True
|
||||
return False
|
||||
except docker.errors.NotFound:
|
||||
return False
|
||||
|
||||
def restart_docker_container(self):
|
||||
try:
|
||||
self.stop_docker_container()
|
||||
logger.info('Container stopped')
|
||||
except docker.errors.DockerException as e:
|
||||
logger.exception('Failed to stop container', exc_info=False)
|
||||
raise e
|
||||
|
||||
try:
|
||||
# start the container
|
||||
mount_dir = config.workspace_mount_path
|
||||
self.container = self.docker_client.containers.run(
|
||||
self.container_image,
|
||||
command='tail -f /dev/null',
|
||||
network_mode='host',
|
||||
working_dir=self.sandbox_workspace_dir,
|
||||
name=self.container_name,
|
||||
detach=True,
|
||||
volumes={mount_dir: {'bind': self.sandbox_workspace_dir, 'mode': 'rw'}},
|
||||
)
|
||||
logger.info('Container started')
|
||||
except Exception as ex:
|
||||
logger.exception('Failed to start container', exc_info=False)
|
||||
raise ex
|
||||
|
||||
# wait for container to be ready
|
||||
elapsed = 0
|
||||
while self.container.status != 'running':
|
||||
if self.container.status == 'exited':
|
||||
logger.info('container exited')
|
||||
logger.info('container logs:')
|
||||
logger.info(self.container.logs())
|
||||
break
|
||||
time.sleep(1)
|
||||
elapsed += 1
|
||||
self.container = self.docker_client.containers.get(self.container_name)
|
||||
if elapsed > self.timeout:
|
||||
break
|
||||
if self.container.status != 'running':
|
||||
raise Exception('Failed to start container')
|
||||
|
||||
# clean up the container, cannot do it in __del__ because the python interpreter is already shutting down
|
||||
def close(self):
|
||||
containers = self.docker_client.containers.list(all=True)
|
||||
for container in containers:
|
||||
try:
|
||||
if container.name.startswith(self.container_name_prefix):
|
||||
container.remove(force=True)
|
||||
except docker.errors.NotFound:
|
||||
pass
|
||||
self.docker_client.close()
|
||||
|
||||
def get_working_directory(self):
|
||||
return self.sandbox_workspace_dir
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
return config.sandbox_user_id
|
||||
|
||||
@property
|
||||
def run_as_devin(self):
|
||||
# FIXME: On some containers, the devin user doesn't have enough permission, e.g. to install packages
|
||||
# How do we make this more flexible?
|
||||
return config.run_as_devin
|
||||
|
||||
@property
|
||||
def sandbox_workspace_dir(self):
|
||||
return config.workspace_mount_path_in_sandbox
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
exec_box = DockerExecBox()
|
||||
except Exception as e:
|
||||
logger.exception('Failed to start Docker container: %s', e)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(
|
||||
"Interactive Docker container started. Type 'exit' or use Ctrl+C to exit."
|
||||
)
|
||||
|
||||
bg_cmd = exec_box.execute_in_background(
|
||||
"while true; do echo -n '.' && sleep 1; done"
|
||||
)
|
||||
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
user_input = input('>>> ')
|
||||
except EOFError:
|
||||
logger.info('Exiting...')
|
||||
break
|
||||
if user_input.lower() == 'exit':
|
||||
logger.info('Exiting...')
|
||||
break
|
||||
if user_input.lower() == 'kill':
|
||||
exec_box.kill_background(bg_cmd.pid)
|
||||
logger.info('Background process killed')
|
||||
continue
|
||||
exit_code, output = exec_box.execute(user_input)
|
||||
logger.info('exit code: %d', exit_code)
|
||||
logger.info(output)
|
||||
if bg_cmd.pid in exec_box.background_commands:
|
||||
logs = exec_box.read_logs(bg_cmd.pid)
|
||||
logger.info('background logs: %s', logs)
|
||||
sys.stdout.flush()
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Exiting...')
|
||||
exec_box.close()
|
||||
@@ -26,7 +26,6 @@ from opendevin.events.observation import (
|
||||
)
|
||||
from opendevin.events.serialization.action import ACTION_TYPE_TO_CLASS
|
||||
from opendevin.runtime import (
|
||||
DockerExecBox,
|
||||
DockerSSHBox,
|
||||
E2BBox,
|
||||
LocalBox,
|
||||
@@ -38,10 +37,8 @@ from opendevin.runtime.tools import RuntimeTool
|
||||
from opendevin.storage import FileStore, InMemoryFileStore
|
||||
|
||||
|
||||
def create_sandbox(sid: str = 'default', sandbox_type: str = 'exec') -> Sandbox:
|
||||
if sandbox_type == 'exec':
|
||||
return DockerExecBox(sid=sid)
|
||||
elif sandbox_type == 'local':
|
||||
def create_sandbox(sid: str = 'default', sandbox_type: str = 'ssh') -> Sandbox:
|
||||
if sandbox_type == 'local':
|
||||
return LocalBox()
|
||||
elif sandbox_type == 'ssh':
|
||||
return DockerSSHBox(sid=sid)
|
||||
|
||||
@@ -99,7 +99,7 @@ class AgentSession:
|
||||
if not self.runtime or not isinstance(self.runtime.sandbox, DockerSSHBox):
|
||||
logger.warning(
|
||||
'CodeActAgent requires DockerSSHBox as sandbox! Using other sandbox that are not stateful'
|
||||
' (LocalBox, DockerExecBox) will not work properly.'
|
||||
' LocalBox will not work properly.'
|
||||
)
|
||||
self.runtime.init_sandbox_plugins(agent.sandbox_plugins)
|
||||
self.runtime.init_runtime_tools(agent.runtime_tools)
|
||||
|
||||
@@ -81,23 +81,18 @@ def test_sandbox_jupyter_plugin_backticks(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox()]:
|
||||
box.init_plugins([JupyterRequirement])
|
||||
test_code = "print('Hello, `World`!')"
|
||||
expected_write_command = (
|
||||
"cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n" f'{test_code}\n' 'EOL'
|
||||
)
|
||||
expected_execute_command = (
|
||||
'cat /tmp/opendevin_jupyter_temp.py | execute_cli'
|
||||
)
|
||||
exit_code, output = box.execute(expected_write_command)
|
||||
exit_code, output = box.execute(expected_execute_command)
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip() == 'Hello, `World`!', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
box.init_plugins([JupyterRequirement])
|
||||
test_code = "print('Hello, `World`!')"
|
||||
expected_write_command = (
|
||||
"cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n" f'{test_code}\n' 'EOL'
|
||||
)
|
||||
expected_execute_command = 'cat /tmp/opendevin_jupyter_temp.py | execute_cli'
|
||||
exit_code, output = box.execute(expected_write_command)
|
||||
exit_code, output = box.execute(expected_execute_command)
|
||||
print(output)
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
assert output.strip() == 'Hello, `World`!', (
|
||||
'The output should be the same as the input for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
@@ -6,7 +6,6 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from opendevin.core.config import config
|
||||
from opendevin.runtime.docker.exec_box import DockerExecBox
|
||||
from opendevin.runtime.docker.local_box import LocalBox
|
||||
from opendevin.runtime.docker.ssh_box import DockerSSHBox, split_bash_commands
|
||||
from opendevin.runtime.plugins import AgentSkillsRequirement, JupyterRequirement
|
||||
@@ -22,7 +21,7 @@ def temp_dir(monkeypatch):
|
||||
|
||||
def test_env_vars(temp_dir):
|
||||
os.environ['SANDBOX_ENV_FOOBAR'] = 'BAZ'
|
||||
for box_class in [DockerSSHBox, DockerExecBox, LocalBox]:
|
||||
for box_class in [DockerSSHBox, LocalBox]:
|
||||
box = box_class()
|
||||
box.add_to_env('QUUX', 'abc"def')
|
||||
assert box._env['FOOBAR'] == 'BAZ'
|
||||
@@ -137,18 +136,15 @@ def test_ssh_box_multi_line_cmd_run_as_devin(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox(), DockerExecBox()]:
|
||||
exit_code, output = box.execute('pwd && ls -l')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
expected_lines = ['/workspace', 'total 0']
|
||||
line_sep = '\r\n' if isinstance(box, DockerSSHBox) else '\n'
|
||||
assert output == line_sep.join(expected_lines), (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
exit_code, output = box.execute('pwd && ls -l')
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
expected_lines = ['/workspace', 'total 0']
|
||||
line_sep = '\r\n' if isinstance(box, DockerSSHBox) else '\n'
|
||||
assert output == line_sep.join(expected_lines), (
|
||||
'The output should be the same as the input for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_ssh_box_stateful_cmd_run_as_devin(temp_dir):
|
||||
@@ -158,29 +154,23 @@ def test_ssh_box_stateful_cmd_run_as_devin(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [
|
||||
DockerSSHBox()
|
||||
]: # FIXME: DockerExecBox() does not work with stateful commands
|
||||
exit_code, output = box.execute('mkdir test')
|
||||
assert exit_code == 0, 'The exit code should be 0.'
|
||||
assert output.strip() == ''
|
||||
box = DockerSSHBox()
|
||||
exit_code, output = box.execute('mkdir test')
|
||||
assert exit_code == 0, 'The exit code should be 0.'
|
||||
assert output.strip() == ''
|
||||
|
||||
exit_code, output = box.execute('cd test')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip() == '', (
|
||||
'The output should be empty for ' + box.__class__.__name__
|
||||
)
|
||||
exit_code, output = box.execute('cd test')
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
assert output.strip() == '', (
|
||||
'The output should be empty for ' + box.__class__.__name__
|
||||
)
|
||||
|
||||
exit_code, output = box.execute('pwd')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip() == '/workspace/test', (
|
||||
'The output should be /workspace for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
exit_code, output = box.execute('pwd')
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
assert output.strip() == '/workspace/test', (
|
||||
'The output should be /workspace for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_ssh_box_failed_cmd_run_as_devin(temp_dir):
|
||||
@@ -190,13 +180,13 @@ def test_ssh_box_failed_cmd_run_as_devin(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox(), DockerExecBox()]:
|
||||
exit_code, output = box.execute('non_existing_command')
|
||||
assert exit_code != 0, (
|
||||
'The exit code should not be 0 for a failed command for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
exit_code, output = box.execute('non_existing_command')
|
||||
assert exit_code != 0, (
|
||||
'The exit code should not be 0 for a failed command for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_single_multiline_command(temp_dir):
|
||||
@@ -205,23 +195,14 @@ def test_single_multiline_command(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox(), DockerExecBox()]:
|
||||
exit_code, output = box.execute('echo \\\n -e "foo"')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
if isinstance(box, DockerExecBox):
|
||||
assert output == 'foo', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
else:
|
||||
# FIXME: why is there a `>` in the output? Probably PS2?
|
||||
assert output == '> foo', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
exit_code, output = box.execute('echo \\\n -e "foo"')
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
# FIXME: why is there a `>` in the output? Probably PS2?
|
||||
assert output == '> foo', (
|
||||
'The output should be the same as the input for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_multiline_echo(temp_dir):
|
||||
@@ -230,23 +211,14 @@ def test_multiline_echo(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox(), DockerExecBox()]:
|
||||
exit_code, output = box.execute('echo -e "hello\nworld"')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
if isinstance(box, DockerExecBox):
|
||||
assert output == 'hello\nworld', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
else:
|
||||
# FIXME: why is there a `>` in the output?
|
||||
assert output == '> hello\r\nworld', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
exit_code, output = box.execute('echo -e "hello\nworld"')
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
# FIXME: why is there a `>` in the output?
|
||||
assert output == '> hello\r\nworld', (
|
||||
'The output should be the same as the input for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_sandbox_whitespace(temp_dir):
|
||||
@@ -256,22 +228,13 @@ def test_sandbox_whitespace(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox(), DockerExecBox()]:
|
||||
exit_code, output = box.execute('echo -e "\\n\\n\\n"')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
if isinstance(box, DockerExecBox):
|
||||
assert output == '\n\n\n', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
else:
|
||||
assert output == '\r\n\r\n\r\n', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
exit_code, output = box.execute('echo -e "\\n\\n\\n"')
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
assert output == '\r\n\r\n\r\n', (
|
||||
'The output should be the same as the input for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_sandbox_jupyter_plugin(temp_dir):
|
||||
@@ -281,18 +244,15 @@ def test_sandbox_jupyter_plugin(temp_dir):
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox()]:
|
||||
box.init_plugins([JupyterRequirement])
|
||||
exit_code, output = box.execute('echo "print(1)" | execute_cli')
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output == '1\r\n', (
|
||||
'The output should be the same as the input for '
|
||||
+ box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
box = DockerSSHBox()
|
||||
box.init_plugins([JupyterRequirement])
|
||||
exit_code, output = box.execute('echo "print(1)" | execute_cli')
|
||||
print(output)
|
||||
assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
|
||||
assert output == '1\r\n', (
|
||||
'The output should be the same as the input for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def _test_sandbox_jupyter_agentskills_fileop_pwd_impl(box):
|
||||
@@ -379,8 +339,8 @@ def test_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
|
||||
config, 'sandbox_type', new='ssh'
|
||||
), patch.object(config, 'enable_auto_lint', new=True):
|
||||
assert config.enable_auto_lint
|
||||
for box in [DockerSSHBox()]:
|
||||
_test_sandbox_jupyter_agentskills_fileop_pwd_impl(box)
|
||||
box = DockerSSHBox()
|
||||
_test_sandbox_jupyter_agentskills_fileop_pwd_impl(box)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
@@ -398,5 +358,5 @@ def test_agnostic_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
|
||||
config, 'sandbox_container_image', new=base_sandbox_image
|
||||
), patch.object(config, 'enable_auto_lint', new=False):
|
||||
assert not config.enable_auto_lint
|
||||
for box in [DockerSSHBox()]:
|
||||
_test_sandbox_jupyter_agentskills_fileop_pwd_impl(box)
|
||||
box = DockerSSHBox()
|
||||
_test_sandbox_jupyter_agentskills_fileop_pwd_impl(box)
|
||||
|
||||
Reference in New Issue
Block a user