From 2455eb4725d89493ae82959edc262f9f6710d832 Mon Sep 17 00:00:00 2001 From: Shivangi Date: Sat, 15 Aug 2026 00:42:12 +0530 Subject: [PATCH] fix: resolve plugin presets before conversation start Assisted-by: OpenAI Codex --- .../automation/presets/plugin/sdk_main.py | 33 +++++++++++++++---- tests/test_preset_router.py | 13 +++++++- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/openhands/automation/presets/plugin/sdk_main.py b/openhands/automation/presets/plugin/sdk_main.py index 86f1b8f9..417d296c 100644 --- a/openhands/automation/presets/plugin/sdk_main.py +++ b/openhands/automation/presets/plugin/sdk_main.py @@ -71,6 +71,7 @@ import sys import time from datetime import datetime, timezone +from pathlib import Path # Detect execution mode based on AGENT_SERVER_URL presence agent_server_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/") @@ -135,7 +136,7 @@ from openhands.sdk.mcp.config import coerce_mcp_config as _coerce_mcp_config except ImportError: _coerce_mcp_config = None -from openhands.sdk.plugin import PluginSource +from openhands.sdk.plugin import Plugin, PluginSource from openhands.sdk.workspace.remote.base import RemoteWorkspace from openhands.tools.preset.default import get_default_agent from openhands.workspace import OpenHandsCloudWorkspace @@ -344,15 +345,33 @@ def _build_conversation_title(event_context) -> str | None: {USER_PROMPT}""" - # Deserialize plugin sources using Pydantic validation - plugin_sources = [PluginSource.model_validate(p) for p in plugins_config] - print("\n=== PLUGINS CONFIG ===") - print(f" loading {len(plugin_sources)} plugin(s):") - for ps in plugin_sources: + # Resolve plugins before creating the remote conversation. Passing remote + # coordinates makes the agent server fetch them lazily when the conversation + # starts, which turns fetch failures into opaque errors while polling events. + # The preset runner and local agent server share this workspace, so passing + # the resolved path keeps loading server-side while making fetch failures + # visible to this script and its completion callback. + plugin_cache_dir = Path(workspace_base) / ".openhands" / "plugin-cache" + plugin_sources = [] + print(f" resolving {len(plugins_config)} plugin(s):") + for raw_plugin_source in plugins_config: + plugin_source = PluginSource.model_validate(raw_plugin_source) + plugin_path = Plugin.fetch( + source=plugin_source.source, + ref=plugin_source.ref, + repo_path=plugin_source.repo_path, + cache_dir=plugin_cache_dir, + ) + # Validate the manifest here so the run receives an actionable failure + # before the agent server starts the conversation. + Plugin.load(plugin_path) + plugin_sources.append(PluginSource(source=str(plugin_path))) + + ps = plugin_source ref_str = f"@{ps.ref}" if ps.ref else "" path_str = f" ({ps.repo_path})" if ps.repo_path else "" - print(f" - {ps.source}{ref_str}{path_str}") + print(f" - {ps.source}{ref_str}{path_str} -> {plugin_path}") # Get LLM config via workspace/profile APIs print("\n=== GET_LLM ===") diff --git a/tests/test_preset_router.py b/tests/test_preset_router.py index 64109b82..3188655c 100644 --- a/tests/test_preset_router.py +++ b/tests/test_preset_router.py @@ -133,6 +133,15 @@ def test_plugin_preset_sdk_main_syntax(self): # compile() raises SyntaxError if the code is invalid compile(source, str(sdk_main_path), "exec") + def test_plugin_preset_resolves_plugins_before_starting_conversation(self): + """Plugin fetch failures must surface before event polling begins.""" + sdk_main_path = PRESETS_DIR / "plugin" / "sdk_main.py" + source = sdk_main_path.read_text() + + assert "Plugin.fetch(" in source + assert "Plugin.load(plugin_path)" in source + assert "PluginSource(source=str(plugin_path))" in source + def test_plugin_preset_setup_sh_exists(self): """Verify plugin setup.sh exists and is not empty.""" setup_sh_path = PRESETS_DIR / "plugin" / "setup.sh" @@ -1242,7 +1251,9 @@ def test_generate_plugin_tarball_main_py_content(self): # Verify key SDK imports and patterns are present assert "from openhands.sdk import" in main_content - assert "from openhands.sdk.plugin import PluginSource" in main_content + assert ( + "from openhands.sdk.plugin import Plugin, PluginSource" in main_content + ) assert "Conversation" in main_content assert "OpenHandsCloudWorkspace" in main_content assert "keep_alive=True" in main_content