mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Make plugins sandbox-agnostic (#2101)
* tmp * tmp * merge main * feat: auto build image cache * remove plugins * use config file * update mamba setup shell * support agnostic sandbox image autobuild * remove config * Update .gitignore Co-authored-by: Xingyao Wang <xingyao6@illinois.edu> * Update opendevin/runtime/docker/ssh_box.py Co-authored-by: Xingyao Wang <xingyao6@illinois.edu> * update setup.sh * readd sudo * add sudo in dockerfile * remove export * move od-runtime dependencies to sandbox dockerfile * factor out re-build logic into a separate util file * tweak existing plugin to use OD specific sandbox * update testcase * attempt to fix unit test using image built in ghcr * use cache tag * try to fix unit tests * add unittest * add unittest * add some unittests * revert gh workflow changes * feat: optimize sandbox image naming rule * add pull latest image hint * add opendevin python hint and use mamba to install gcc * update docker image naming rule and fix mamba issue * Update opendevin/runtime/docker/ssh_box.py Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk> * fix: opendevin user use correct pip * fix lint issue * fix custom sandbox base image * rename test name * add skipif --------- Co-authored-by: Graham Neubig <neubig@gmail.com> Co-authored-by: Xingyao Wang <xingyao6@illinois.edu> Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk> Co-authored-by: Yufan Song <33971064+yufansong@users.noreply.github.com> Co-authored-by: tobitege <tobitege@gmx.de>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -211,3 +211,5 @@ cache
|
||||
# configuration
|
||||
config.toml
|
||||
config.toml.bak
|
||||
|
||||
containers/agnostic_sandbox
|
||||
@@ -41,4 +41,4 @@ RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> /opendevin/bash.bashrc
|
||||
# - agentskills dependencies
|
||||
RUN /opendevin/miniforge3/bin/pip install --upgrade pip
|
||||
RUN /opendevin/miniforge3/bin/pip install jupyterlab notebook jupyter_kernel_gateway flake8
|
||||
RUN /opendevin/miniforge3/bin/pip install python-docx PyPDF2 python-pptx pylatexenc openai opencv-python
|
||||
RUN /opendevin/miniforge3/bin/pip install python-docx PyPDF2 python-pptx pylatexenc openai opencv-python
|
||||
|
||||
95
opendevin/runtime/docker/image_agnostic_util.py
Normal file
95
opendevin/runtime/docker/image_agnostic_util.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import tempfile
|
||||
|
||||
import docker
|
||||
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
def generate_dockerfile_content(base_image: str) -> str:
|
||||
"""
|
||||
Generate the Dockerfile content for the agnostic sandbox image based on user-provided base image.
|
||||
|
||||
NOTE: This is only tested on debian yet.
|
||||
"""
|
||||
# FIXME: Remove the requirement of ssh in future version
|
||||
dockerfile_content = (
|
||||
f'FROM {base_image}\n'
|
||||
'RUN apt update && apt install -y openssh-server wget sudo\n'
|
||||
'RUN mkdir -p -m0755 /var/run/sshd\n'
|
||||
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
|
||||
'RUN wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"\n'
|
||||
'RUN bash Miniforge3-$(uname)-$(uname -m).sh -b -p /opendevin/miniforge3\n'
|
||||
'RUN bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"\n'
|
||||
'RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> ~/.bashrc\n'
|
||||
'RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> /opendevin/bash.bashrc\n'
|
||||
).strip()
|
||||
return dockerfile_content
|
||||
|
||||
|
||||
def _build_sandbox_image(
|
||||
base_image: str, target_image_name: str, docker_client: docker.DockerClient
|
||||
):
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
dockerfile_content = generate_dockerfile_content(base_image)
|
||||
logger.info(f'Building agnostic sandbox image: {target_image_name}')
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
)
|
||||
with open(f'{temp_dir}/Dockerfile', 'w') as file:
|
||||
file.write(dockerfile_content)
|
||||
|
||||
image, logs = docker_client.images.build(
|
||||
path=temp_dir, tag=target_image_name
|
||||
)
|
||||
|
||||
for log in logs:
|
||||
if 'stream' in log:
|
||||
print(log['stream'].strip())
|
||||
|
||||
logger.info(f'Image {image} built successfully')
|
||||
except docker.errors.BuildError as e:
|
||||
logger.error(f'Sandbox image build failed: {e}')
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f'An error occurred during sandbox image build: {e}')
|
||||
raise e
|
||||
|
||||
|
||||
def _get_new_image_name(base_image: str) -> str:
|
||||
if ":" not in base_image:
|
||||
base_image = base_image + ":latest"
|
||||
|
||||
[repo, tag] = base_image.split(':')
|
||||
return f'od_sandbox:{repo}__{tag}'
|
||||
|
||||
|
||||
def get_od_sandbox_image(base_image: str, docker_client: docker.DockerClient) -> str:
|
||||
"""Return the sandbox image name based on user-provided base image.
|
||||
|
||||
The returned sandbox image is assumed to contains all the required dependencies for OpenDevin.
|
||||
If the sandbox image is not found, it will be built.
|
||||
"""
|
||||
# OpenDevin's offcial sandbox already contains the required dependencies for OpenDevin.
|
||||
if 'ghcr.io/opendevin/sandbox' in base_image:
|
||||
return base_image
|
||||
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
|
||||
# Detect if the sandbox image is built
|
||||
images = docker_client.images.list()
|
||||
for image in images:
|
||||
if new_image_name in image.tags:
|
||||
logger.info('Found existing od_sandbox image, reuse:' + new_image_name)
|
||||
return new_image_name
|
||||
|
||||
# If the sandbox image is not found, build it
|
||||
logger.info(
|
||||
f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
|
||||
)
|
||||
_build_sandbox_image(base_image, new_image_name, docker_client)
|
||||
return new_image_name
|
||||
@@ -17,6 +17,7 @@ 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.image_agnostic_util import get_od_sandbox_image
|
||||
from opendevin.runtime.docker.process import DockerProcess, Process
|
||||
from opendevin.runtime.plugins import AgentSkillsRequirement, JupyterRequirement
|
||||
from opendevin.runtime.sandbox import Sandbox
|
||||
@@ -222,6 +223,9 @@ class DockerSSHBox(Sandbox):
|
||||
|
||||
self.timeout = timeout
|
||||
self.container_image = container_image or config.sandbox_container_image
|
||||
self.container_image = get_od_sandbox_image(
|
||||
self.container_image, self.docker_client
|
||||
)
|
||||
self.container_name = self.container_name_prefix + self.instance_id
|
||||
|
||||
# set up random user password
|
||||
@@ -271,7 +275,7 @@ class DockerSSHBox(Sandbox):
|
||||
self.execute('mkdir -p /tmp')
|
||||
# set git config
|
||||
self.execute('git config --global user.name "OpenDevin"')
|
||||
self.execute('git config --global user.email "opendevin@opendevin.ai"')
|
||||
self.execute('git config --global user.email "opendevin@all-hands.dev"')
|
||||
atexit.register(self.close)
|
||||
super().__init__()
|
||||
|
||||
@@ -342,6 +346,31 @@ class DockerSSHBox(Sandbox):
|
||||
raise Exception(
|
||||
f'Failed to chown home directory for opendevin in sandbox: {logs}'
|
||||
)
|
||||
# check the miniforge3 directory exist
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c', '[ -d "/opendevin/miniforge3" ] && exit 0 || exit 1'],
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
if exit_code != 0:
|
||||
if exit_code == 1:
|
||||
raise Exception(
|
||||
f'OPENDEVIN_PYTHON_INTERPRETER is not usable. Please pull the latest Docker image: docker pull ghcr.io/opendevin/sandbox:main'
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
f'An error occurred while checking if miniforge3 directory exists: {logs}'
|
||||
)
|
||||
# chown the miniforge3
|
||||
exit_code, logs = self.container.exec_run(
|
||||
['/bin/bash', '-c', 'chown -R opendevin:root /opendevin/miniforge3'],
|
||||
workdir=self.sandbox_workspace_dir,
|
||||
environment=self._env,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f'Failed to chown miniforge3 directory for opendevin in sandbox: {logs}'
|
||||
)
|
||||
exit_code, logs = self.container.exec_run(
|
||||
[
|
||||
'/bin/bash',
|
||||
@@ -714,7 +743,7 @@ class DockerSSHBox(Sandbox):
|
||||
)
|
||||
logger.info('Container started')
|
||||
except Exception as ex:
|
||||
logger.exception('Failed to start container', exc_info=False)
|
||||
logger.exception('Failed to start container: ' + str(ex), exc_info=False)
|
||||
raise ex
|
||||
|
||||
# wait for container to be ready
|
||||
@@ -766,7 +795,8 @@ if __name__ == '__main__':
|
||||
)
|
||||
|
||||
# Initialize required plugins
|
||||
ssh_box.init_plugins([AgentSkillsRequirement(), JupyterRequirement()])
|
||||
plugins = [AgentSkillsRequirement(), JupyterRequirement()]
|
||||
ssh_box.init_plugins(plugins)
|
||||
logger.info(
|
||||
'--- AgentSkills COMMAND DOCUMENTATION ---\n'
|
||||
f'{AgentSkillsRequirement().documentation}\n'
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
set -e
|
||||
|
||||
OPENDEVIN_PYTHON_INTERPRETER=/opendevin/miniforge3/bin/python
|
||||
# check if OPENDEVIN_PYTHON_INTERPRETER exists and it is usable
|
||||
if [ -z "$OPENDEVIN_PYTHON_INTERPRETER" ] || [ ! -x "$OPENDEVIN_PYTHON_INTERPRETER" ]; then
|
||||
echo "OPENDEVIN_PYTHON_INTERPRETER is not usable. Please pull the latest Docker image!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# add agent_skills to PATH
|
||||
echo 'export PATH=/opendevin/plugins/agent_skills:$PATH' >> ~/.bashrc
|
||||
export PATH=/opendevin/plugins/agent_skills:$PATH
|
||||
|
||||
# add agent_skills to PYTHONPATH
|
||||
echo 'export PYTHONPATH=/opendevin/plugins/agent_skills:$PYTHONPATH' >> ~/.bashrc
|
||||
export PYTHONPATH=/opendevin/plugins/agent_skills:$PYTHONPATH
|
||||
|
||||
pip install flake8 python-docx PyPDF2 python-pptx pylatexenc openai opencv-python
|
||||
source ~/.bashrc
|
||||
|
||||
$OPENDEVIN_PYTHON_INTERPRETER -m pip install flake8 python-docx PyPDF2 python-pptx pylatexenc openai opencv-python
|
||||
|
||||
@@ -2,14 +2,25 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Hardcoded to use the Python interpreter from the OpenDevin runtime client
|
||||
OPENDEVIN_PYTHON_INTERPRETER=/opendevin/miniforge3/bin/python
|
||||
# check if OPENDEVIN_PYTHON_INTERPRETER exists and it is usable
|
||||
if [ -z "$OPENDEVIN_PYTHON_INTERPRETER" ] || [ ! -x "$OPENDEVIN_PYTHON_INTERPRETER" ]; then
|
||||
echo "OPENDEVIN_PYTHON_INTERPRETER is not usable. Please pull the latest Docker image!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# use mamba to install c library
|
||||
/opendevin/miniforge3/bin/mamba install -y gcc
|
||||
|
||||
# Install dependencies
|
||||
$OPENDEVIN_PYTHON_INTERPRETER -m pip install jupyterlab notebook jupyter_kernel_gateway
|
||||
|
||||
source ~/.bashrc
|
||||
# ADD /opendevin/plugins to PATH to make `jupyter_cli` available
|
||||
echo 'export PATH=$PATH:/opendevin/plugins/jupyter' >> ~/.bashrc
|
||||
export PATH=/opendevin/plugins/jupyter:$PATH
|
||||
|
||||
# get current PythonInterpreter
|
||||
OPENDEVIN_PYTHON_INTERPRETER=$(which python3)
|
||||
|
||||
# if user name is `opendevin`, add '/home/opendevin/.local/bin' to PATH
|
||||
if [ "$USER" = "opendevin" ]; then
|
||||
echo 'export PATH=$PATH:/home/opendevin/.local/bin' >> ~/.bashrc
|
||||
@@ -26,12 +37,6 @@ if [ "$USER" = "root" ]; then
|
||||
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
pip install jupyterlab notebook jupyter_kernel_gateway
|
||||
|
||||
# Create logs directory
|
||||
sudo mkdir -p /opendevin/logs && sudo chmod 777 /opendevin/logs
|
||||
|
||||
# Run background process to start jupyter kernel gateway
|
||||
# write a bash function that finds a free port
|
||||
find_free_port() {
|
||||
@@ -50,7 +55,9 @@ find_free_port() {
|
||||
}
|
||||
|
||||
export JUPYTER_GATEWAY_PORT=$(find_free_port 20000 30000)
|
||||
jupyter kernelgateway --KernelGatewayApp.ip=0.0.0.0 --KernelGatewayApp.port=$JUPYTER_GATEWAY_PORT > /opendevin/logs/jupyter_kernel_gateway.log 2>&1 &
|
||||
$OPENDEVIN_PYTHON_INTERPRETER -m \
|
||||
jupyter kernelgateway --KernelGatewayApp.ip=0.0.0.0 --KernelGatewayApp.port=$JUPYTER_GATEWAY_PORT > /opendevin/logs/jupyter_kernel_gateway.log 2>&1 &
|
||||
|
||||
export JUPYTER_GATEWAY_PID=$!
|
||||
echo "export JUPYTER_GATEWAY_PID=$JUPYTER_GATEWAY_PID" >> ~/.bashrc
|
||||
export JUPYTER_GATEWAY_KERNEL_ID="default"
|
||||
@@ -60,7 +67,7 @@ echo "JupyterKernelGateway started with PID: $JUPYTER_GATEWAY_PID"
|
||||
# Start the jupyter_server
|
||||
export JUPYTER_EXEC_SERVER_PORT=$(find_free_port 30000 40000)
|
||||
echo "export JUPYTER_EXEC_SERVER_PORT=$JUPYTER_EXEC_SERVER_PORT" >> ~/.bashrc
|
||||
/opendevin/plugins/jupyter/execute_server > /opendevin/logs/jupyter_execute_server.log 2>&1 &
|
||||
$OPENDEVIN_PYTHON_INTERPRETER /opendevin/plugins/jupyter/execute_server > /opendevin/logs/jupyter_execute_server.log 2>&1 &
|
||||
export JUPYTER_EXEC_SERVER_PID=$!
|
||||
echo "export JUPYTER_EXEC_SERVER_PID=$JUPYTER_EXEC_SERVER_PID" >> ~/.bashrc
|
||||
echo "Execution server started with PID: $JUPYTER_EXEC_SERVER_PID"
|
||||
|
||||
@@ -13,12 +13,21 @@ class SandboxProtocol(Protocol):
|
||||
def initialize_plugins(self) -> bool: ...
|
||||
|
||||
def execute(
|
||||
self, cmd: str, stream: bool = False
|
||||
self, cmd: str, stream: bool = False
|
||||
) -> tuple[int, str | CancellableStream]: ...
|
||||
|
||||
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False): ...
|
||||
|
||||
|
||||
def _source_bashrc(sandbox: SandboxProtocol):
|
||||
exit_code, output = sandbox.execute('source /opendevin/bash.bashrc && source ~/.bashrc')
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f'Failed to source /opendevin/bash.bashrc and ~/.bashrc with exit code {exit_code} and output: {output}'
|
||||
)
|
||||
logger.info('Sourced /opendevin/bash.bashrc and ~/.bashrc successfully')
|
||||
|
||||
|
||||
class PluginMixin:
|
||||
"""Mixin for Sandbox to support plugins."""
|
||||
|
||||
@@ -35,6 +44,9 @@ class PluginMixin:
|
||||
exit_code, output = self.execute('rm -f ~/.bashrc && touch ~/.bashrc')
|
||||
|
||||
for requirement in requirements:
|
||||
# source bashrc file when plugin loads
|
||||
_source_bashrc(self)
|
||||
|
||||
# copy over the files
|
||||
self.copy_to(
|
||||
requirement.host_src, requirement.sandbox_dest, recursive=True
|
||||
@@ -62,7 +74,7 @@ class PluginMixin:
|
||||
output.close()
|
||||
if _exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f'Failed to initialize plugin {requirement.name} with exit code {_exit_code} and output {total_output}'
|
||||
f'Failed to initialize plugin {requirement.name} with exit code {_exit_code} and output: {total_output}'
|
||||
)
|
||||
logger.info(f'Plugin {requirement.name} initialized successfully')
|
||||
else:
|
||||
@@ -75,11 +87,6 @@ class PluginMixin:
|
||||
logger.info('Skipping plugin initialization in the sandbox')
|
||||
|
||||
if len(requirements) > 0:
|
||||
exit_code, output = self.execute('source ~/.bashrc')
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f'Failed to source ~/.bashrc with exit code {exit_code} and output: {output}'
|
||||
)
|
||||
logger.info('Sourced ~/.bashrc successfully')
|
||||
_source_bashrc(self)
|
||||
|
||||
self.plugin_initialized = True
|
||||
|
||||
42
tests/unit/test_image_agnostic_util.py
Normal file
42
tests/unit/test_image_agnostic_util.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
from opendevin.runtime.docker.image_agnostic_util import (
|
||||
generate_dockerfile_content,
|
||||
_get_new_image_name,
|
||||
get_od_sandbox_image,
|
||||
)
|
||||
|
||||
|
||||
def test_generate_dockerfile_content():
|
||||
base_image = "debian:11"
|
||||
dockerfile_content = generate_dockerfile_content(base_image)
|
||||
assert base_image in dockerfile_content
|
||||
assert "RUN apt update && apt install -y openssh-server wget sudo" in dockerfile_content
|
||||
|
||||
|
||||
def test_get_new_image_name():
|
||||
base_image = "debian:11"
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == "od_sandbox:debian__11"
|
||||
|
||||
base_image = "ubuntu:22.04"
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == "od_sandbox:ubuntu__22.04"
|
||||
|
||||
base_image = "ubuntu"
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == "od_sandbox:ubuntu__latest"
|
||||
|
||||
|
||||
@patch("opendevin.runtime.docker.image_agnostic_util._build_sandbox_image")
|
||||
@patch("opendevin.runtime.docker.image_agnostic_util.docker.DockerClient")
|
||||
def test_get_od_sandbox_image(mock_docker_client, mock_build_sandbox_image):
|
||||
base_image = "debian:11"
|
||||
mock_docker_client.images.list.return_value = [MagicMock(tags=["od_sandbox:debian__11"])]
|
||||
|
||||
image_name = get_od_sandbox_image(base_image, mock_docker_client)
|
||||
assert image_name == "od_sandbox:debian__11"
|
||||
|
||||
mock_docker_client.images.list.return_value = []
|
||||
image_name = get_od_sandbox_image(base_image, mock_docker_client)
|
||||
assert image_name == "od_sandbox:debian__11"
|
||||
mock_build_sandbox_image.assert_called_once_with(base_image, "od_sandbox:debian__11", mock_docker_client)
|
||||
@@ -63,7 +63,7 @@ async def test_run_python_backticks():
|
||||
[
|
||||
call('mkdir -p /tmp'),
|
||||
call('git config --global user.name "OpenDevin"'),
|
||||
call('git config --global user.email "opendevin@opendevin.ai"'),
|
||||
call('git config --global user.email "opendevin@all-hands.dev"'),
|
||||
call(expected_write_command),
|
||||
call(expected_execute_command),
|
||||
]
|
||||
|
||||
@@ -294,6 +294,52 @@ def test_sandbox_jupyter_plugin(temp_dir):
|
||||
)
|
||||
box.close()
|
||||
|
||||
def _test_sandbox_jupyter_agentskills_fileop_pwd_impl(box):
|
||||
box.init_plugins([AgentSkillsRequirement, JupyterRequirement])
|
||||
exit_code, output = box.execute('mkdir test')
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
|
||||
exit_code, output = box.execute(
|
||||
'echo "create_file(\'a.txt\')" | execute_cli'
|
||||
)
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip().split('\r\n') == (
|
||||
'[File: /workspace/a.txt (1 lines total)]\r\n'
|
||||
'1|\r\n'
|
||||
'[File a.txt created.]'
|
||||
).strip().split('\r\n')
|
||||
|
||||
exit_code, output = box.execute('cd test')
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
|
||||
exit_code, output = box.execute(
|
||||
'echo "create_file(\'a.txt\')" | execute_cli'
|
||||
)
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip().split('\r\n') == (
|
||||
'[File: /workspace/test/a.txt (1 lines total)]\r\n'
|
||||
'1|\r\n'
|
||||
'[File a.txt created.]'
|
||||
).strip().split('\r\n')
|
||||
|
||||
exit_code, output = box.execute('rm -rf /workspace/*')
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
box.close()
|
||||
|
||||
|
||||
def test_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
|
||||
# get a temporary directory
|
||||
@@ -303,41 +349,21 @@ def test_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
|
||||
config, 'sandbox_type', new='ssh'
|
||||
):
|
||||
for box in [DockerSSHBox()]:
|
||||
box.init_plugins([AgentSkillsRequirement, JupyterRequirement])
|
||||
exit_code, output = box.execute('mkdir test')
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
_test_sandbox_jupyter_agentskills_fileop_pwd_impl(box)
|
||||
|
||||
exit_code, output = box.execute(
|
||||
'echo "create_file(\'a.txt\')" | execute_cli'
|
||||
)
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip().split('\r\n') == (
|
||||
'[File: /workspace/a.txt (1 lines total)]\r\n'
|
||||
'1|\r\n'
|
||||
'[File a.txt created.]'
|
||||
).strip().split('\r\n')
|
||||
|
||||
exit_code, output = box.execute('cd test')
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
|
||||
exit_code, output = box.execute(
|
||||
'echo "create_file(\'a.txt\')" | execute_cli'
|
||||
)
|
||||
print(output)
|
||||
assert exit_code == 0, (
|
||||
'The exit code should be 0 for ' + box.__class__.__name__
|
||||
)
|
||||
assert output.strip().split('\r\n') == (
|
||||
'[File: /workspace/test/a.txt (1 lines total)]\r\n'
|
||||
'1|\r\n'
|
||||
'[File a.txt created.]'
|
||||
).strip().split('\r\n')
|
||||
@pytest.mark.skipif(os.getenv('TEST_IN_CI') != 'true',
|
||||
reason='The unittest need to download image, so only run on CI',
|
||||
)
|
||||
def test_agnostic_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
|
||||
for base_sandbox_image in ['ubuntu:22.04', 'debian:11']:
|
||||
# get a temporary directory
|
||||
with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
|
||||
config, 'workspace_mount_path', new=temp_dir
|
||||
), patch.object(config, 'run_as_devin', new='true'), patch.object(
|
||||
config, 'sandbox_type', new='ssh'
|
||||
), patch.object(
|
||||
config, 'sandbox_container_image', new=base_sandbox_image
|
||||
):
|
||||
for box in [DockerSSHBox()]:
|
||||
_test_sandbox_jupyter_agentskills_fileop_pwd_impl(box)
|
||||
Reference in New Issue
Block a user