Skip to content
Draft
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
60 changes: 60 additions & 0 deletions openhands-sdk/openhands/sdk/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Extensions module for OpenHands SDK.

This module provides shared infrastructure for installable extensions
(plugins, skills, etc.), including:

- Source specification types for describing where to fetch extensions
- Catalog entry types for extension marketplaces
- Generic installation management infrastructure
- Fetching utilities for remote sources

The types and utilities here are used by the plugin, skills, and marketplace
modules to provide consistent behavior for extension management.

Example:
>>> from openhands.sdk.extensions import ExtensionSource, InstalledExtensionManager
>>> source = ExtensionSource(source="github:owner/repo", ref="v1.0.0")
"""

from openhands.sdk.extensions.catalog import (
ExtensionAuthor,
ExtensionCatalogEntry,
)
from openhands.sdk.extensions.fetch import (
DEFAULT_CACHE_DIR,
ExtensionFetchError,
fetch_extension,
fetch_extension_with_resolution,
get_cache_path,
parse_extension_source,
)
from openhands.sdk.extensions.installed import (
InstalledExtensionInfo,
InstalledExtensionManager,
InstalledExtensionMetadata,
)
from openhands.sdk.extensions.source import (
ExtensionSource,
ResolvedExtensionSource,
)


__all__ = [
# Source types
"ExtensionSource",
"ResolvedExtensionSource",
# Catalog types
"ExtensionAuthor",
"ExtensionCatalogEntry",
# Installed extension management
"InstalledExtensionInfo",
"InstalledExtensionMetadata",
"InstalledExtensionManager",
# Fetching utilities
"ExtensionFetchError",
"fetch_extension",
"fetch_extension_with_resolution",
"parse_extension_source",
"get_cache_path",
"DEFAULT_CACHE_DIR",
]
70 changes: 70 additions & 0 deletions openhands-sdk/openhands/sdk/extensions/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Extension catalog entry types.

These types define entries in extension catalogs (marketplaces) that list
available extensions with their metadata and source locations.
"""

from __future__ import annotations

from typing import Any, Self

from pydantic import BaseModel, Field, field_validator


class ExtensionAuthor(BaseModel):
"""Author information for an extension."""

name: str = Field(description="Author's name")
email: str | None = Field(default=None, description="Author's email address")
url: str | None = Field(
default=None, description="Author's URL (e.g., GitHub profile)"
)

@classmethod
def from_string(cls, author_str: str) -> Self:
"""Parse author from string format 'Name <email>'.

Examples:
>>> ExtensionAuthor.from_string("John Doe <john@example.com>")
ExtensionAuthor(name='John Doe', email='john@example.com', url=None)

>>> ExtensionAuthor.from_string("Jane Doe")
ExtensionAuthor(name='Jane Doe', email=None, url=None)
"""
if "<" in author_str and ">" in author_str:
name = author_str.split("<")[0].strip()
email = author_str.split("<")[1].split(">")[0].strip()
return cls(name=name, email=email)
return cls(name=author_str.strip())


class ExtensionCatalogEntry(BaseModel):
"""Entry in an extension catalog (marketplace).

This is the base type for catalog entries that point to extensions
(plugins, skills, etc.) with their metadata and source locations.

Source is a string path that can be:
- Local path: "./path/to/extension", "/absolute/path"
- GitHub URL: "https://github.com/owner/repo/tree/branch/path"
"""

name: str = Field(description="Identifier (kebab-case, no spaces)")
source: str = Field(description="Path to extension directory (local or GitHub URL)")
description: str | None = Field(default=None, description="Brief description")
version: str | None = Field(default=None, description="Version")
author: ExtensionAuthor | None = Field(default=None, description="Author info")
category: str | None = Field(default=None, description="Category for organization")
homepage: str | None = Field(
default=None, description="Homepage or documentation URL"
)

model_config = {"extra": "allow", "populate_by_name": True}

@field_validator("author", mode="before")
@classmethod
def _parse_author(cls, v: Any) -> Any:
"""Parse author from string if needed."""
if isinstance(v, str):
return ExtensionAuthor.from_string(v)
return v
Loading
Loading