Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions openhands/automation/presets/plugin/sdk_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ===")
Expand Down
13 changes: 12 additions & 1 deletion tests/test_preset_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading