Skip to content

Conversation

@jpshackelford
Copy link
Contributor

@jpshackelford jpshackelford commented Jan 8, 2026

Summary

Extends the agent server to support loading plugins when starting conversations. When a plugin_source is provided in the StartConversationRequest, the agent server will fetch the plugin (using Plugin.fetch() from #1647) and load its skills, hooks, and MCP configuration into the conversation context.

Implements: #1650

Dependency: This PR is based on feat/plugin-fetch branch (PR #1647) and requires it to be merged first.

Context

This is part of the Plugin Directory feature (OpenHands/OpenHands#12088).

The key architectural insight is that plugin fetching must happen inside the sandbox/runtime (where the agent server runs), not on the app server. This is because:

  • Plugins may contain scripts or hooks that need to execute in the sandbox
  • Plugin MCP servers need to run inside the sandbox
  • Skills may reference files that need to exist in the sandbox filesystem

See OpenHands/OpenHands#12085 comment for how this fits with other components.

Changes

1. Extended StartConversationRequest model

Added two new optional fields:

  • plugin_source: Plugin source to fetch (e.g., github:owner/repo, git URL, or local path)
  • plugin_ref: Optional branch, tag, or commit for the plugin

2. Added plugin loading logic to ConversationService

  • _load_and_merge_plugin(): Fetches and loads a plugin when plugin_source is provided
  • _merge_plugin_into_request(): Merges plugin skills and MCP config into the agent

3. Plugin content merging

  • Skills: Plugin skills are merged with existing skills; plugin skills override existing skills with the same name
  • MCP Config: Plugin MCP config is merged with existing config, with plugin values taking precedence
  • Hooks: Currently logged as a warning (not yet implemented)

Testing

Added comprehensive tests for:

  • Merging plugin skills into requests (with and without existing skills)
  • Merging plugin MCP config
  • Handling empty plugins
  • Successful plugin loading flow
  • Error handling (fetch errors, load errors)
  • Skipping plugin loading when no source is provided

Files Changed

  • openhands-agent-server/openhands/agent_server/models.py - Added plugin_source and plugin_ref fields
  • openhands-agent-server/openhands/agent_server/conversation_service.py - Added plugin loading logic
  • tests/agent_server/test_conversation_service.py - Added tests for plugin loading

Related Issues

@jpshackelford can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:79e66ae-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-79e66ae-python \
  ghcr.io/openhands/agent-server:79e66ae-python

All tags pushed for this build

ghcr.io/openhands/agent-server:79e66ae-golang-amd64
ghcr.io/openhands/agent-server:79e66ae-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:79e66ae-golang-arm64
ghcr.io/openhands/agent-server:79e66ae-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:79e66ae-java-amd64
ghcr.io/openhands/agent-server:79e66ae-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:79e66ae-java-arm64
ghcr.io/openhands/agent-server:79e66ae-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:79e66ae-python-amd64
ghcr.io/openhands/agent-server:79e66ae-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:79e66ae-python-arm64
ghcr.io/openhands/agent-server:79e66ae-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:79e66ae-golang
ghcr.io/openhands/agent-server:79e66ae-java
ghcr.io/openhands/agent-server:79e66ae-python

About Multi-Architecture Support

  • Each variant tag (e.g., 79e66ae-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 79e66ae-python-amd64) are also available if needed

Implements #1645 - adds the ability to fetch plugins from remote sources
(GitHub repositories, git URLs) and cache them locally.

Changes:
- Add Plugin.fetch() classmethod to fetch from remote sources
- Add parse_plugin_source() to parse various source formats:
  - GitHub shorthand: 'github:owner/repo'
  - Git URLs: HTTPS, SSH, git:// protocol
  - Local paths (returned as-is)
- Add PluginFetchError exception for fetch failures
- Implement caching at ~/.openhands/cache/plugins/
- Support shallow clones for efficiency
- Support specific ref (branch/tag/commit) checkout
- Add comprehensive unit tests (34 new tests)

Co-authored-by: openhands <[email protected]>
- Add blank line after imports in fetch.py
- Remove unused import PluginFetchError in plugin.py
- Reformat long lines in test file

Co-authored-by: openhands <[email protected]>
- Create GitHelper class to encapsulate all git operations (clone, fetch, checkout, etc.)
- Refactor fetch.py to use GitHelper via dependency injection
- Update unit tests to mock GitHelper instead of subprocess.run
- Add integration tests for real git operations
- Add support for file:// URLs for local testing

This improves test coverage by:
1. Allowing unit tests to execute actual fetch.py logic while mocking git operations
2. Providing separate integration tests that use real git operations

Co-authored-by: openhands <[email protected]>
@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands/agent_server
   conversation_service.py38125034%65, 68, 79–80, 83–86, 88, 92, 94, 97–104, 107–108, 111–115, 118–120, 122–125, 127, 134–135, 137–139, 142, 146, 148, 150, 157, 163, 171–172, 181–184, 193, 216–217, 219, 221–222, 229–230, 232, 240, 242, 244–245, 247, 263–264, 267–269, 273–276, 279, 281, 283–284, 288, 290–291, 297–298, 300–302, 307–308, 314–316, 318, 327, 332–333, 336, 349–350, 366, 372, 375, 386–390, 392–395, 398–403, 406–409, 411–413, 416, 419–421, 426–429, 437, 442–444, 458–462, 465, 467, 470–472, 474, 478, 482, 489–493, 496–497, 501–505, 508–509, 513–517, 520–521, 527–532, 539–540, 544, 546–547, 552–553, 559–560, 566–568, 586, 610, 638, 640–641, 667, 669, 671–674, 679, 681–682, 686–687, 689–690, 693–695, 698, 704, 709–712, 719–720, 724–728, 730, 735, 739–741, 745–746, 748–750, 752, 754, 767–769, 772, 775, 778–781, 788–789, 793–795, 798–799, 801
   models.py1211488%56–57, 196–197, 199–201, 203, 207–208, 210, 216, 219, 221
openhands/sdk/plugin
   plugin.py1552285%274–276, 278–283, 299–302, 311–315, 331–332, 350–351
TOTAL15899492169% 

openhands-agent and others added 6 commits January 8, 2026 21:36
Add comprehensive tests to improve coverage for the plugin module:

- fetch.py: Add tests for set_git_helper(), relative path parsing,
  default cache_dir fallback, and PluginFetchError re-raise
- git_helper.py: Add unit tests for error handling paths including
  CalledProcessError and TimeoutExpired for all git operations
- plugin.py: Add tests for plugin loading edge cases including
  manifest parsing, skills/agents/commands loading, and error handling
- types.py: Add tests for AgentDefinition and CommandDefinition
  frontmatter parsing including complex field handling

Simplify integration test to avoid skill loading complexity.

Co-authored-by: openhands <[email protected]>
The --forked flag was preventing coverage from being collected properly
because pytest-forked uses os.fork() which doesn't combine coverage data.

Changes:
- Split SDK test run: forked tests run separately, then non-forked tests
  with --cov-append to combine coverage data
- Add 'forked' pytest marker to identify tests needing process isolation
- Add coverage configuration with parallel mode and branch coverage
- Add coverage report exclusions for common non-coverable patterns

This allows most tests to run without forking for accurate coverage,
while still supporting tests that need process isolation.

Co-authored-by: openhands <[email protected]>
The change to run tests without --forked exposed pre-existing test pollution
issues in test_state_serialization.py where tests share state incorrectly.

Reverting to the original --forked approach until those tests are fixed.
Coverage collection will remain affected by --forked, but tests will pass.

Co-authored-by: openhands <[email protected]>
Copy link
Contributor Author

Requested Changes: Add plugin_path field

Context

During testing, we identified a bug where plugin paths were being embedded in the plugin_source field (e.g., github:owner/repo/plugins/sub-plugin), which parse_plugin_source() in the SDK rejects because it validates that GitHub shorthand has only one /. To fix this and maintain consistency with the App Server API, we need to pass plugin_path as a separate field.

Changes Needed

openhands-agent-server/openhands/agent_server/models.py - Add plugin_path field:

class StartConversationRequest(BaseModel):
    # ... existing fields ...
    
    plugin_source: str | None = Field(
        default=None,
        description="Plugin source: 'github:owner/repo', git URL, or local path",
    )
    plugin_ref: str | None = Field(
        default=None,
        description="Optional branch, tag, or commit for the plugin.",
    )
    plugin_path: str | None = Field(  # NEW FIELD
        default=None,
        description="Optional subdirectory path within the plugin repository.",
    )

openhands-agent-server/openhands/agent_server/conversation_service.py - Pass plugin_path to Plugin.fetch():

def _load_and_merge_plugin(
    self, request: StartConversationRequest
) -> StartConversationRequest:
    if not request.plugin_source:
        return request

    try:
        logger.info(f"Fetching plugin from: {request.plugin_source}")
        plugin_path = Plugin.fetch(
            source=request.plugin_source,
            ref=request.plugin_ref,
            subpath=request.plugin_path,  # NEW: pass subpath
        )
        # ... rest unchanged ...

This change depends on PR #1647 adding the subpath parameter to Plugin.fetch().

See OpenHands/OpenHands#12087 (comment) for the full API design showing consistency across all layers.

jpshackelford and others added 2 commits January 10, 2026 15:29
Add optional subpath parameter to both fetch_plugin() and Plugin.fetch()
methods. This allows fetching plugins from subdirectories within a
repository, which is needed when plugin paths are specified separately
rather than embedded in the source string.

Changes:
- Add subpath parameter to fetch_plugin() in fetch.py
- Add subpath parameter to Plugin.fetch() in plugin.py
- Apply subpath to both local paths and remote repositories
- Strip leading/trailing slashes from subpath
- Raise PluginFetchError if subpath doesn't exist
- Add 8 new unit tests for subpath functionality

Addresses review comment on PR #1647.

Co-authored-by: openhands <[email protected]>
@openhands-ai
Copy link

openhands-ai bot commented Jan 12, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Pre-commit checks

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1651 at branch `feat/agent-server-plugin-loading`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

- Add plugin_source and plugin_ref fields to StartConversationRequest
- Add _load_and_merge_plugin() method to fetch and load plugins
- Add _merge_plugin_into_request() method to merge plugin skills and MCP config
- Plugin skills override existing skills with the same name
- Plugin MCP config is merged with existing config
- Add comprehensive tests for plugin loading scenarios

Closes #1650
Add plugin_path field to StartConversationRequest and pass it as
subpath parameter to Plugin.fetch(). This enables fetching plugins
from subdirectories within repositories (e.g., monorepos with
multiple plugins).

Changes:
- models.py: Add plugin_path optional field to StartConversationRequest
- conversation_service.py: Pass plugin_path as subpath to Plugin.fetch()
- test_conversation_service.py: Update tests to verify subpath handling

Note: This depends on PR #1647 adding the subpath parameter to Plugin.fetch()

Co-authored-by: openhands <[email protected]>
@jpshackelford jpshackelford force-pushed the feat/agent-server-plugin-loading branch from 4e78b1b to 96b6102 Compare January 12, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(agent-server): Support plugin loading when starting conversations

3 participants