Files
deepagents/libs/code/deepagents_code/plugins/models.py
T
Johannes du Plessis 0187d14b83 feat(code): add plugin marketplace support (#4554)
`deepagents-code` now supports experimental plugin marketplaces with
namespaced skills and MCP servers. Enable with
`DEEPAGENTS_CODE_EXPERIMENTAL=1`.

Docs: https://github.com/langchain-ai/docs/pull/4905

---

`deepagents-code` can now manage experimental plugin marketplaces and
load installed plugins that contribute namespaced skills and MCP
servers. The new plugin manager establishes the `tui/modals`,
`tui/screens`, and `tui/widgets` UI convention, with colocated styling
and focused state/content modules.

- Supports local, GitHub, Git, and marketplace JSON sources.
- Adds install, enable, disable, uninstall, and safe marketplace removal
through `/plugins` and `dcode plugin`.
- Reloads plugin skills and plugin MCP configuration with `/reload` when
experimental mode is enabled.
- Namespaces plugin skills as `{plugin_id}:{skill_name}` through a
code-local `PluginSkillsMiddleware` adapter, so no SDK changes are
required.

Made by [Open
SWE](https://openswe.vercel.app/agents/019f5dea-0f1f-73fe-9803-32d1b13fbd4c)

---------

Signed-off-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: Alexander Olsen <aolsenjazz@gmail.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-07-15 09:11:28 -04:00

228 lines
6.0 KiB
Python

"""Data models for plugins."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
from pathlib import Path
MarketplaceSourceType = Literal["directory", "file", "github", "git", "url"]
ExternalPluginRepositorySourceType = Literal["github", "git-subdir", "url"]
JsonValue = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
JsonObject = dict[str, JsonValue]
@dataclass(frozen=True, slots=True, kw_only=True)
class LocalMarketplaceSource:
"""Local directory or JSON file used as a marketplace source."""
source_type: Literal["directory", "file"]
value: str
@dataclass(frozen=True, slots=True, kw_only=True)
class RepositoryMarketplaceSource:
"""GitHub or Git repository used as a marketplace source.
`ref` selects an optional branch or tag. Commit SHA checkout is not part of
the shallow-clone flow.
"""
source_type: Literal["github", "git"]
value: str
ref: str | None
@dataclass(frozen=True, slots=True, kw_only=True)
class UrlMarketplaceSource:
"""Marketplace manifest downloaded from an HTTP URL."""
source_type: Literal["url"]
value: str
MarketplaceSource = (
LocalMarketplaceSource | RepositoryMarketplaceSource | UrlMarketplaceSource
)
@dataclass(frozen=True, slots=True, kw_only=True)
class PluginManifest:
"""Parsed plugin manifest.
Attributes:
name: Plugin name from the manifest, or `None` for manifest-less plugins.
version: Version string from the plugin manifest.
component_paths: Validated skill and MCP paths keyed by component name.
inline_mcp: Inline MCP servers declared in the manifest.
"""
name: str | None
version: str | None
component_paths: dict[str, tuple[Path, ...]]
inline_mcp: JsonObject
@dataclass(frozen=True, slots=True, kw_only=True)
class ComponentInventory:
"""Inventory of supported plugin components."""
skills: tuple[Path, ...] = ()
mcp_files: tuple[Path, ...] = ()
warnings: tuple[str, ...] = ()
@dataclass(frozen=True, slots=True, kw_only=True)
class PluginInstance:
"""A discovered plugin ready to feed dcode adapters.
Attributes:
plugin_id: Stable id in `{name}@{marketplace}` form.
name: Plugin namespace name.
marketplace: Parent marketplace used for identity and namespacing.
version: Version declared by the plugin manifest, if any.
root: Plugin root directory.
data_dir: Writable data directory for this plugin.
manifest: Parsed manifest, if any.
inventory: Component inventory.
"""
plugin_id: str
name: str
marketplace: str
version: str | None
root: Path
data_dir: Path
manifest: PluginManifest | None
inventory: ComponentInventory
def __post_init__(self) -> None:
"""Validate the canonical plugin identity.
Raises:
ValueError: If `plugin_id` disagrees with `name` and `marketplace`.
"""
expected = f"{self.name}@{self.marketplace}"
if self.plugin_id != expected:
msg = f"Plugin id {self.plugin_id!r} does not match {expected!r}"
raise ValueError(msg)
@dataclass(frozen=True, slots=True, kw_only=True)
class LocalPluginSource:
"""A plugin stored relative to its marketplace."""
source_type: Literal["local"]
path: str
@dataclass(frozen=True, slots=True, kw_only=True)
class GithubPluginSource:
"""A plugin sourced from a GitHub repository."""
source_type: Literal["github"]
repo: str
ref: str | None = None
path: str | None = None
@dataclass(frozen=True, slots=True, kw_only=True)
class GitSubdirectoryPluginSource:
"""A plugin sourced from a subdirectory in a Git repository."""
source_type: Literal["git-subdir"]
url: str
ref: str | None = None
path: str | None = None
@dataclass(frozen=True, slots=True, kw_only=True)
class UrlPluginSource:
"""A plugin sourced from a Git repository URL."""
source_type: Literal["url"]
url: str
ref: str | None = None
path: str | None = None
PluginSource = (
LocalPluginSource
| GithubPluginSource
| GitSubdirectoryPluginSource
| UrlPluginSource
)
@dataclass(frozen=True, slots=True, kw_only=True)
class MarketplacePluginEntry:
"""A catalog entry from a marketplace manifest."""
name: str
source: PluginSource
description: str | None = None
author: str | JsonObject | None = None
@dataclass(frozen=True, slots=True, kw_only=True)
class PluginMarketplace:
"""A parsed marketplace manifest."""
name: str
root: Path
manifest_path: Path
metadata: JsonObject
plugins: tuple[MarketplacePluginEntry, ...]
warnings: tuple[str, ...] = ()
@dataclass(frozen=True, slots=True, kw_only=True)
class MarketplaceRecord:
"""Persisted marketplace source record."""
name: str
source_type: MarketplaceSourceType
source: str
install_location: str
ref: str | None = None
@dataclass(frozen=True, slots=True, kw_only=True)
class InstalledPluginEntry:
"""Install record for a plugin.
`version` is the value declared by the plugin manifest, if any.
"""
install_path: str
version: str | None
@dataclass(frozen=True, slots=True, kw_only=True)
class PluginDiscoveryResult:
"""Result from plugin discovery."""
plugins: tuple[PluginInstance, ...]
warnings: tuple[str, ...] = ()
def split_plugin_id(plugin_id: str) -> tuple[str, str]:
"""Split a plugin id in `{plugin}@{marketplace}` form.
Returns:
Plugin and marketplace names.
Raises:
ValueError: If either part is missing.
"""
if "@" not in plugin_id:
msg = f"Invalid plugin id {plugin_id!r}; expected name@marketplace"
raise ValueError(msg)
plugin, marketplace = plugin_id.rsplit("@", 1)
if not plugin or not marketplace:
msg = f"Invalid plugin id {plugin_id!r}; expected name@marketplace"
raise ValueError(msg)
return plugin, marketplace