Repository: All-Hands-AI/OpenHands
Pull request #7612: feat: add argument --base-container-image to resolver
Author: login: Tetsu-is
Labels: name: lint-fix
description: Attempts to fix lint issues on the PR
Created: 2025-04-01T02:48:17Z | Merged: 2025-04-21T14:01:03Z

- [x] This change is worth documenting at https://docs.all-hands.dev/
- [x] Include this change in the Release Notes. If checked, you **must** provide an **end-user friendly** description for your change below

**End-user friendly description of the problem this fixes or functionality that this introduces.**

Enabled custom sandbox for resolver. You can apply base image with `--base-container image YOUR_BASE_CONTAINER_IMAGE`

example
```
python -m openhands.resolver.resolve_issue --selected-repo [OWNER]/[REPO] --issue-number [NUMBER] --base-container-image [YOUR_BASE_CONTAINER_IMAGE]
```

---
**Give a summary of what the PR does, explaining any non-trivial design decisions.**
I added `base_container_image` setting for resolver in order to enable github actions to use any programming language.
Instead, we have to specify `-runtime-container-image` even when we don't use custom sandbox(I will fix it so that user dont have to specify when using defaultt runtime image)

- add command line argument `--base-container-image` to resolver.resolve_issue.py
- add argument line`--base-container-image` to resolver.resolve_all_issues.py
---
**Link of any specific issues this addresses.**

I haven't created issue about this topic.


Diff:
diff --git a/openhands/core/config/sandbox_config.py b/openhands/core/config/sandbox_config.py
index 5cdecea8efc1..9fee66071399 100644
--- a/openhands/core/config/sandbox_config.py
+++ b/openhands/core/config/sandbox_config.py
@@ -46,7 +46,7 @@ class SandboxConfig(BaseModel):
     pause_closed_runtimes: bool = Field(default=True)
     rm_all_containers: bool = Field(default=False)
     api_key: str | None = Field(default=None)
-    base_container_image: str = Field(
+    base_container_image: str | None = Field(
         default='nikolaik/python-nodejs:python3.12-nodejs22'
     )
     runtime_container_image: str | None = Field(default=None)
diff --git a/openhands/resolver/resolve_all_issues.py b/openhands/resolver/resolve_all_issues.py
index 286f5cc2d21e..9d99010f4eb4 100644
--- a/openhands/resolver/resolve_all_issues.py
+++ b/openhands/resolver/resolve_all_issues.py
@@ -11,6 +11,7 @@
 from pydantic import SecretStr
 from tqdm import tqdm
 
+import openhands
 from openhands.core.config import LLMConfig
 from openhands.core.logger import openhands_logger as logger
 from openhands.integrations.service_types import ProviderType
@@ -59,6 +60,7 @@ async def resolve_issues(
     num_workers: int,
     output_dir: str,
     llm_config: LLMConfig,
+    base_container_image: str | None,
     runtime_container_image: str,
     prompt_template: str,
     issue_type: str,
@@ -199,6 +201,7 @@ async def resolve_issues(
                     max_iterations,
                     llm_config,
                     output_dir,
+                    base_container_image,
                     runtime_container_image,
                     prompt_template,
                     issue_handler,
@@ -249,6 +252,12 @@ def main() -> None:
         default=None,
         help='Github or Gitlab username to access the repository.',
     )
+    parser.add_argument(
+        '--base-container-image',
+        type=str,
+        default=None,
+        help='Base container image to use.',
+    )
     parser.add_argument(
         '--runtime-container-image',
         type=str,
@@ -331,9 +340,21 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image is not None:
+        raise ValueError('Cannot provide both runtime and base container images.')
+
+    if (
+        runtime_container_image is None
+        and base_container_image is None
+        and not my_args.is_experimental
+    ):
+        runtime_container_image = (
+            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
+        )
 
     owner, repo = my_args.selected_repo.split('/')
     token = my_args.token or os.getenv('GITHUB_TOKEN') or os.getenv('GITLAB_TOKEN')
@@ -386,6 +407,7 @@ def main() -> None:
             token=token,
             username=username,
             platform=platform,
+            base_container_image=base_container_image,
             runtime_container_image=runtime_container_image,
             max_iterations=my_args.max_iterations,
             limit_issues=my_args.limit_issues,
diff --git a/openhands/resolver/resolve_issue.py b/openhands/resolver/resolve_issue.py
index 0b9b25e3f92a..7758b46fdd15 100644
--- a/openhands/resolver/resolve_issue.py
+++ b/openhands/resolver/resolve_issue.py
@@ -167,6 +167,7 @@ async def process_issue(
     max_iterations: int,
     llm_config: LLMConfig,
     output_dir: str,
+    base_container_image: str | None,
     runtime_container_image: str | None,
     prompt_template: str,
     issue_handler: ServiceContextIssue | ServiceContextPR,
@@ -195,6 +196,7 @@ async def process_issue(
     # they're set by default if nothing else overrides them
     # FIXME we should remove them here
     sandbox_config = SandboxConfig(
+        base_container_image=base_container_image,
         runtime_container_image=runtime_container_image,
         enable_auto_lint=False,
         use_host_network=False,
@@ -360,6 +362,7 @@ async def resolve_issue(
     max_iterations: int,
     output_dir: str,
     llm_config: LLMConfig,
+    base_container_image: str | None,
     runtime_container_image: str | None,
     prompt_template: str,
     issue_type: str,
@@ -525,6 +528,7 @@ async def resolve_issue(
             max_iterations,
             llm_config,
             output_dir,
+            base_container_image,
             runtime_container_image,
             prompt_template,
             issue_handler,
@@ -567,6 +571,12 @@ def int_or_none(value: str) -> int | None:
         default=None,
         help='username to access the repository.',
     )
+    parser.add_argument(
+        '--base-container-image',
+        type=str,
+        default=None,
+        help='base container image to use.',
+    )
     parser.add_argument(
         '--runtime-container-image',
         type=str,
@@ -649,8 +659,18 @@ def int_or_none(value: str) -> int | None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None and not my_args.is_experimental:
+
+    if runtime_container_image is not None and base_container_image is not None:
+        raise ValueError('Cannot provide both runtime and base container images.')
+
+    if (
+        runtime_container_image is None
+        and base_container_image is None
+        and not my_args.is_experimental
+    ):
         runtime_container_image = (
             f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
         )
@@ -714,6 +734,7 @@ def int_or_none(value: str) -> int | None:
             token=token,
             username=username,
             platform=platform,
+            base_container_image=base_container_image,
             runtime_container_image=runtime_container_image,
             max_iterations=my_args.max_iterations,
             output_dir=my_args.output_dir,
diff --git a/openhands/runtime/builder/docker.py b/openhands/runtime/builder/docker.py
index 050471a67eb2..618fe913c067 100644
--- a/openhands/runtime/builder/docker.py
+++ b/openhands/runtime/builder/docker.py
@@ -159,6 +159,14 @@ def build(
             f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
         )
 
+        builder_cmd = ['docker', 'buildx', 'use', 'default']
+        subprocess.Popen(
+            builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+
         try:
             process = subprocess.Popen(
                 buildx_cmd,
diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py
index eb2e9b6f67af..161b5719143f 100644
--- a/openhands/runtime/impl/remote/remote_runtime.py
+++ b/openhands/runtime/impl/remote/remote_runtime.py
@@ -194,7 +194,10 @@ def _build_runtime(self):
             'debug',
             f'Runtime image repo: {os.environ["OH_RUNTIME_RUNTIME_IMAGE_REPO"]}',
         )
-
+        if self.config.sandbox.base_container_image is None:
+            raise ValueError(
+                'base_container_image is required to build the runtime image. '
+            )
         if self.config.sandbox.runtime_extra_deps:
             self.log(
                 'debug',
diff --git a/poetry.lock b/poetry.lock
index 46bb02dccec7..d180442a8e90 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2663,7 +2663,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi
 grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
 proto-plus = [
     {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
-    {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
+    {version = ">=1.22.3,<2.0.0dev"},
 ]
 protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
 requests = ">=2.18.0,<3.0.0.dev0"
@@ -2877,7 +2877,7 @@ google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
 grpc-google-iam-v1 = ">=0.14.0,<1.0.0dev"
 proto-plus = [
     {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
-    {version = ">=1.22.3,<2.0.0dev"},
+    {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
 ]
 protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
 
diff --git a/tests/unit/resolver/github/test_resolve_issues.py b/tests/unit/resolver/github/test_resolve_issues.py
index d90e91ff0131..01706ee1bd84 100644
--- a/tests/unit/resolver/github/test_resolve_issues.py
+++ b/tests/unit/resolver/github/test_resolve_issues.py
@@ -112,6 +112,7 @@ async def test_resolve_issue_no_issues_found():
                 max_iterations=5,
                 output_dir='/tmp',
                 llm_config=LLMConfig(model='test', api_key='test'),
+                base_container_image='test-image',
                 runtime_container_image='test-image',
                 prompt_template='test-template',
                 issue_type='pr',
@@ -344,6 +345,7 @@ async def test_process_issue(mock_output_dir, mock_prompt_template):
     repo_instruction = 'Resolve this repo'
     max_iterations = 5
     llm_config = LLMConfig(model='test_model', api_key='test_api_key')
+    base_container_image = 'test_image:latest'
     runtime_container_image = 'test_image:latest'
 
     # Test cases for different scenarios
@@ -449,6 +451,7 @@ async def test_process_issue(mock_output_dir, mock_prompt_template):
                 max_iterations,
                 llm_config,
                 mock_output_dir,
+                base_container_image,
                 runtime_container_image,
                 mock_prompt_template,
                 handler_instance,
diff --git a/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py b/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py
index 19d8478d2345..ff9b06107430 100644
--- a/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py
+++ b/tests/unit/resolver/gitlab/test_gitlab_resolve_issues.py
@@ -132,6 +132,7 @@ async def test_resolve_issue_no_issues_found():
                 max_iterations=5,
                 output_dir='/tmp',
                 llm_config=LLMConfig(model='test', api_key='test'),
+                base_container_image='test-image',
                 runtime_container_image='test-image',
                 prompt_template='test-template',
                 issue_type='pr',
@@ -384,6 +385,7 @@ async def test_process_issue(mock_output_dir, mock_prompt_template):
     repo_instruction = 'Resolve this repo'
     max_iterations = 5
     llm_config = LLMConfig(model='test_model', api_key='test_api_key')
+    base_container_image = 'test_image:latest'
     runtime_container_image = 'test_image:latest'
 
     # Test cases for different scenarios
@@ -489,6 +491,7 @@ async def test_process_issue(mock_output_dir, mock_prompt_template):
                 max_iterations,
                 llm_config,
                 mock_output_dir,
+                base_container_image,
                 runtime_container_image,
                 mock_prompt_template,
                 handler_instance,


Reviews:
user: login: neubig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T19:26:15Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T06:57:49Z
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T11:04:52Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T03:34:06Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T07:27:30Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T07:37:26Z
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T09:43:44Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T14:15:37Z
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T10:42:36Z
user: login: raymyers
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:16:22Z
user: login: raymyers
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:19:13Z
user: login: raymyers
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:09:19Z
user: login: xingyaoww
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T18:33:51Z
user: login: raymyers
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:37:01Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T16:03:05Z
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T16:54:29Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T18:35:59Z
user: login: enyst
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-18T20:55:03Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-19T01:13:57Z
user: login: enyst
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-19T01:33:09Z
user: login: Tetsu-is
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T03:49:02Z

Review comments:
diff_hunk: @@ -632,11 +641,9 @@ def int_or_none(value: str) -> int | None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None and not my_args.is_experimental:
-        runtime_container_image = (
-            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
-        )
path: openhands/resolver/resolve_issue.py
user: login: neubig
body: Thanks @Tetsu-is ! Could you explain why these lines were removed?
created_at: 2025-04-06T19:26:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -632,11 +641,9 @@ def int_or_none(value: str) -> int | None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None and not my_args.is_experimental:
-        runtime_container_image = (
-            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
-        )
path: openhands/resolver/resolve_issue.py
user: login: Tetsu-is
body: @neubig 
Of course. 

In the existing implementation, custom sandbox will be build only when `runtime_container_image` is None & `base_container_image is Not None (openhands/runtime/impl/docker/docker_runtime.py). 
```py
if self.runtime_container_image is None:
                if self.base_container_image is None:
                    raise ValueError(
                        'Neither runtime container image nor base container image is set'
                    )
                self.send_status_message('STATUS$STARTING_CONTAINER')
                self.runtime_container_image = build_runtime_image(
                   self.base_container_image,
                    ...
                )
```

When executed like`python -m openhands.resolver.resolve_issue --base-container-image BASE_IMAGE`, the runtime_container_image is not provided, and only the base_container_image is given. At that time, if 'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik' is assigned to runtime_container_image, the build of the custom sandbox will not be executed.

My ideal scenario is below.
| runtime-container-image | base-container-image | result |
----|----|----
| given | given | error |
| given | not given | build sandbox with runtime-container-image |
| not given | given | build sandbox with base-container-image |
| not given | not given | error  or build sandbox with default nikolaik |

When `runtime-container-image` is not given, do you think `ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik` should be set as default? 
created_at: 2025-04-08T06:57:49Z
author_association: NONE
diff_hunk: @@ -632,11 +641,9 @@ def int_or_none(value: str) -> int | None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None and not my_args.is_experimental:
-        runtime_container_image = (
-            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
-        )
path: openhands/resolver/resolve_issue.py
user: login: enyst
body: Please correct me if I'm misunderstanding, `not given` + `not given` is the default case, when the user doesn't have a special requirement for a particular image, so they run the resolver without setting any of those. In that case, the result should be "build sandbox with default nikolaik".

Unless they set `is_experimental`, in which case it shouldn't set it, so that it builds from source, which will mean build from `main`. (`experimental` installs `openhands main` instead of the latest released version IIRC)

Do you think this behavior should change, or actually, does it change in this PR?
created_at: 2025-04-08T11:04:52Z
author_association: COLLABORATOR
diff_hunk: @@ -632,11 +641,9 @@ def int_or_none(value: str) -> int | None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None and not my_args.is_experimental:
-        runtime_container_image = (
-            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
-        )
path: openhands/resolver/resolve_issue.py
user: login: Tetsu-is
body: @enyst 
Thank you for your advice. I didn't understand the meaning of `is_experimental` flag.
I agree with you . Default behavior should be building sandbox with default nikolaik image.

I will change it in this PR.
created_at: 2025-04-11T03:34:06Z
author_association: NONE
diff_hunk: @@ -632,11 +641,9 @@ def int_or_none(value: str) -> int | None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None and not my_args.is_experimental:
-        runtime_container_image = (
-            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
-        )
path: openhands/resolver/resolve_issue.py
user: login: Tetsu-is
body: fb9035cb9e14a336a2f2e780c1cb33d873b9cc49

when not specifying `base-conatiner-image` and `runtime-container-image`, use default nikolaik.
created_at: 2025-04-11T07:27:30Z
author_association: NONE
diff_hunk: @@ -159,6 +159,14 @@ def build(
             f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
         )
 
+        builder_cmd = ['docker', 'buildx', 'use', 'default']
+        subprocess.Popen(
+            builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+
path: openhands/runtime/builder/docker.py
user: login: Tetsu-is
body: If the default builder instance is not specified, the sandbox build will fail because it cannot reference the base container image.(when I tried in my [repository](https://github.com/Tetsu-is/openhands-actions-test/blob/main/.github/workflows/openhands-resolver.yml))
created_at: 2025-04-11T07:37:25Z
author_association: NONE
diff_hunk: @@ -156,6 +160,21 @@ async def connect(self):
                     extra_build_args=self.config.sandbox.runtime_extra_build_args,
                 )
 
+            if self.runtime_container_image != DEFAULT_RUNTIME_CONTAINER_IMAGE:
+                if self.base_container_image:
+                    raise ValueError(
+                        'Please specify either runtime_container_image or base_container_image.'
+                    )
+
+            if self.base_container_image:
+                self.send_status_message('STATUS$STARTING_CONTAINER')
+                self.runtime_container_image = build_runtime_image(
path: openhands/runtime/impl/docker/docker_runtime.py
user: login: enyst
body: Sorry if I'm missing something, I'm looking at the diff via github:
- if `runtime_container_image` is None, and `base_container_image` is set, then it builds at line 154
- and raises `ValueError` at line 165 ?

created_at: 2025-04-14T09:43:44Z
author_association: COLLABORATOR
diff_hunk: @@ -156,6 +160,21 @@ async def connect(self):
                     extra_build_args=self.config.sandbox.runtime_extra_build_args,
                 )
 
+            if self.runtime_container_image != DEFAULT_RUNTIME_CONTAINER_IMAGE:
+                if self.base_container_image:
+                    raise ValueError(
+                        'Please specify either runtime_container_image or base_container_image.'
+                    )
+
+            if self.base_container_image:
+                self.send_status_message('STATUS$STARTING_CONTAINER')
+                self.runtime_container_image = build_runtime_image(
path: openhands/runtime/impl/docker/docker_runtime.py
user: login: Tetsu-is
body: Yes. That should be happen. sorry it is my mistake.
if `runtime_container_image` is None & `base_container_image` is set, the runtime should build `runtime_container_image` from `base_container_image` and should not raise error.
created_at: 2025-04-14T14:15:37Z
author_association: NONE
diff_hunk: @@ -159,6 +159,14 @@ def build(
             f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
         )
 
+        builder_cmd = ['docker', 'buildx', 'use', 'default']
+        subprocess.Popen(
+            builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+
path: openhands/runtime/builder/docker.py
user: login: enyst
body: This addition looks fine to me, @raymyers what do you think ?
created_at: 2025-04-17T10:42:36Z
author_association: COLLABORATOR
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
+        raise ValueError('Cannot provide both runtime and base container images.')
+
+    if runtime_container_image is None and base_container_image == '' and not my_args.is_experimental:
+        runtime_container_image = (
+            f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
path: openhands/resolver/resolve_all_issues.py
user: login: raymyers
body: I appreciate the move to use the version var
created_at: 2025-04-17T17:16:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: raymyers
body: Is there a reason that one image is being compared to None and the other to `''`?
created_at: 2025-04-17T17:19:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -159,6 +159,14 @@ def build(
             f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
         )
 
+        builder_cmd = ['docker', 'buildx', 'use', 'default']
+        subprocess.Popen(
+            builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+
path: openhands/runtime/builder/docker.py
user: login: raymyers
body: Hmmm, I'm a bit concerned that requiring buildkit could break a GCP Cloud Build workflow, similar to what we saw recently with @xingyaoww. I wonder if there's a way we can test
created_at: 2025-04-17T18:09:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -159,6 +159,14 @@ def build(
             f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
         )
 
+        builder_cmd = ['docker', 'buildx', 'use', 'default']
+        subprocess.Popen(
+            builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+
path: openhands/runtime/builder/docker.py
user: login: xingyaoww
body: This one actually seems fine for me. This won't break GCP since it is not changing Dockefile.j2

And we have a separate builder for remote - which is separate from this: https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/builder/remote.py
created_at: 2025-04-17T18:33:51Z
author_association: COLLABORATOR
diff_hunk: @@ -159,6 +159,14 @@ def build(
             f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
         )
 
+        builder_cmd = ['docker', 'buildx', 'use', 'default']
+        subprocess.Popen(
+            builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+
path: openhands/runtime/builder/docker.py
user: login: raymyers
body: Thanks @xingyaoww, in that case this is good by me!
created_at: 2025-04-17T18:37:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: Tetsu-is
body: It is because the default value of `base_container_image` is `''`. 

When I added a new argument `base_container_image`, I set the type of that to `str` because `base_container_image` in `SandboxConfig` class is `str`. Then I set the default value (when argument `--base-container-image` not given) to `''` at `parser.add_argument()`.

```py 
    parser.add_argument(
        '--base-container-image',
        type=str,
        default='',
        help='base container image to use.',
    )
    parser.add_argument(
        '--runtime-container-image',
        type=str,
        default=None,
        help='Container image to use.',
    )
```

As you might have wondered, this might look unnatural for others. 
created_at: 2025-04-18T16:03:05Z
author_association: NONE
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: enyst
body: Does the config need any change? It works as `str` because it has a default value, or what am I missing?
created_at: 2025-04-18T16:54:29Z
author_association: COLLABORATOR
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: Tetsu-is
body: Yes. We must change type of `base_contaienr_image` in config from `str` to `str | None`
Config needs the change if we use `None` as the default for argument `--base-container-image` at `parser.add_argument()`. 
When it's not given, the value become None. Then Pydantic gives a validation error when creating config instance because field value is `None` but the defined type is `str`
```py
sandbox_config = SandboxConfig(
        base_container_image=base_container_image, #  it becomes None if arg not given
        runtime_container_image=runtime_container_image,
        enable_auto_lint=False,
        use_host_network=False,
        # large enough timeout, since some testcases take very long to run
        timeout=300,
)
```
`ERROR:root:<class 'pydantic_core._pydantic_core.ValidationError'>: 1 validation error for SandboxConfig
base_container_image
  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
    For further information visit https://errors.pydantic.dev/2.11/v/string_type`

On the other side. There's no need to change config if we use `''` as the default value at `parser.add_argument()`
created_at: 2025-04-18T18:35:59Z
author_association: NONE
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: Tetsu-is
body: I will apply above changes I said.
- set default value to `None`
- fix config
- some changes
  - validation if `base_container_image` is None in building process
  - test code
created_at: 2025-04-19T01:13:57Z
author_association: NONE
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: enyst
body: As you wish, just please consider that the base image needs to fallback to nikolaik
created_at: 2025-04-19T01:33:09Z
author_association: COLLABORATOR
diff_hunk: @@ -333,9 +342,17 @@ def main() -> None:
 
     my_args = parser.parse_args()
 
+    base_container_image = my_args.base_container_image
+
     runtime_container_image = my_args.runtime_container_image
-    if runtime_container_image is None:
-        runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.33.0-nikolaik'
+
+    if runtime_container_image is not None and base_container_image != '':
path: openhands/resolver/resolve_all_issues.py
user: login: Tetsu-is
body: I set nikolaik as the fallback
created_at: 2025-04-21T03:49:02Z
author_association: NONE

PR comments:
user: login: Tetsu-is
created_at: 2025-04-14T16:39:03Z
body: [60fd3d7](https://github.com/All-Hands-AI/OpenHands/pull/7612/commits/60fd3d7f56b7b7309da120c33651e15efb2d59a4)
[2105fa6](https://github.com/All-Hands-AI/OpenHands/pull/7612/commits/2105fa67a471b2dfda5b2bb25ffbb65684316044)
[01e2516](https://github.com/All-Hands-AI/OpenHands/pull/7612/commits/01e2516407781b1e38026dc60dc4c4e845c84e74)
[a531ac9](https://github.com/All-Hands-AI/OpenHands/pull/7612/commits/a531ac9cd7ae40782953d2c81119b4cc4ef5634e)

I've made the resolver handle the conditional branching logic based on the arguments. Runtimes should be focused on building  image with given arguments. It shouldn't care about the default behavior as an application.
author_association: NONE
user: login: enyst
created_at: 2025-04-19T17:03:14Z
body: @Tetsu-is you may want to make sure you run `make build` on the branch, that should install pre-commit hooks that fix lint automatically for minor issues. Saves time 😅
author_association: COLLABORATOR
user: login: Tetsu-is
created_at: 2025-04-21T01:03:00Z
body: Thank you. I will try that.
author_association: NONE
user: login: Tetsu-is
created_at: 2025-04-21T04:08:38Z
body: I've changed the type of base_container_image in config from `str` to `str | None`.

When not specifying both `base_container_image` and `runtime_container_image`, resolver fails with error about `is_static`. I guess it is related to this [issue](https://github.com/All-Hands-AI/OpenHands/issues/7947). The same error occurred.

In other cases, resolver works.
author_association: NONE