From 35a40ddee87427c1fa3f156be42965ef5b61a840 Mon Sep 17 00:00:00 2001 From: Chris Bagwell Date: Wed, 18 Mar 2026 10:55:48 -0500 Subject: [PATCH] fix: handle containers with tagless images in DockerSandboxService (#13238) --- .../sandbox/docker_sandbox_service.py | 6 ++ .../app_server/test_docker_sandbox_service.py | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/openhands/app_server/sandbox/docker_sandbox_service.py b/openhands/app_server/sandbox/docker_sandbox_service.py index cb1466a8fa..6c692a680a 100644 --- a/openhands/app_server/sandbox/docker_sandbox_service.py +++ b/openhands/app_server/sandbox/docker_sandbox_service.py @@ -197,6 +197,12 @@ class DockerSandboxService(SandboxService): ) ) + if not container.image.tags: + _logger.debug( + f'Skipping container {container.name!r}: image has no tags (image id: {container.image.id})' + ) + return None + return SandboxInfo( id=container.name, created_by_user_id=None, diff --git a/tests/unit/app_server/test_docker_sandbox_service.py b/tests/unit/app_server/test_docker_sandbox_service.py index b951167d02..23a6d51b04 100644 --- a/tests/unit/app_server/test_docker_sandbox_service.py +++ b/tests/unit/app_server/test_docker_sandbox_service.py @@ -245,6 +245,61 @@ class TestDockerSandboxService: assert len(result.items) == 0 assert result.next_page_id is None + async def test_search_sandboxes_skips_containers_with_no_image_tags( + self, service, mock_running_container + ): + """Test that containers with tagless images are skipped without crashing. + + Regression test: when a container's image has been rebuilt with the same tag, + the old container's image loses its tags, causing container.image.tags to be + an empty list. Previously this caused an IndexError. + """ + # Setup a container with no image tags (e.g. image was retagged/rebuilt) + tagless_container = MagicMock() + tagless_container.name = 'oh-test-tagless' + tagless_container.status = 'paused' + tagless_container.image.tags = [] + tagless_container.image.id = 'sha256:abc123def456' + tagless_container.attrs = { + 'Created': '2024-01-15T10:30:00.000000000Z', + 'Config': {'Env': []}, + 'NetworkSettings': {'Ports': {}}, + } + + service.docker_client.containers.list.return_value = [ + mock_running_container, + tagless_container, + ] + service.httpx_client.get.return_value.raise_for_status.return_value = None + + # Execute - should not raise IndexError + result = await service.search_sandboxes() + + # Verify - only the properly tagged container is returned + assert isinstance(result, SandboxPage) + assert len(result.items) == 1 + assert result.items[0].id == 'oh-test-abc123' + + async def test_get_sandbox_returns_none_for_tagless_image(self, service): + """Test that get_sandbox returns None for containers with tagless images.""" + tagless_container = MagicMock() + tagless_container.name = 'oh-test-tagless' + tagless_container.status = 'paused' + tagless_container.image.tags = [] + tagless_container.image.id = 'sha256:abc123def456' + tagless_container.attrs = { + 'Created': '2024-01-15T10:30:00.000000000Z', + 'Config': {'Env': []}, + 'NetworkSettings': {'Ports': {}}, + } + service.docker_client.containers.get.return_value = tagless_container + + # Execute - should not raise IndexError + result = await service.get_sandbox('oh-test-tagless') + + # Verify - returns None for tagless container + assert result is None + async def test_search_sandboxes_filters_by_prefix(self, service): """Test that search filters containers by name prefix.""" # Setup