Răsfoiți Sursa

[Arch] Removing docker exec box (#2802)

* depracting docker exec box

* remove doc exec from workflow and docs
Xingyao Wang 1 an în urmă
părinte
comite
0d3b3ffbf8

+ 1 - 1
.github/workflows/ghcr.yml

@@ -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
 

+ 1 - 1
.github/workflows/review-pr.yml

@@ -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"

+ 2 - 2
.github/workflows/solve-issue.yml

@@ -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"

+ 1 - 1
agenthub/micro/commit_writer/README.md

@@ -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 ./
 ```
 

+ 0 - 1
docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/troubleshooting/troubleshooting.md

@@ -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
 

+ 0 - 1
docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/troubleshooting/troubleshooting.md

@@ -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
 

+ 0 - 1
docs/modules/usage/troubleshooting/troubleshooting.md

@@ -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 - 2
opendevin/runtime/__init__.py

@@ -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']

+ 0 - 410
opendevin/runtime/docker/exec_box.py

@@ -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()

+ 2 - 5
opendevin/runtime/runtime.py

@@ -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)

+ 1 - 1
opendevin/server/session/agent.py

@@ -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)

+ 15 - 20
tests/unit/test_ipython.py

@@ -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()

+ 68 - 108
tests/unit/test_sandbox.py

@@ -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)