Files
deepagents/libs/code/deepagents_code/plugins/discovery.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

393 lines
12 KiB
Python

"""Plugin discovery, install, and enablement helpers."""
from __future__ import annotations
import logging
import shutil
from functools import partial
from pathlib import Path
from deepagents_code.plugins.manifest import (
PluginManifestError,
build_inventory,
load_manifest,
)
from deepagents_code.plugins.marketplace import (
MarketplaceError,
load_marketplace,
load_marketplace_location,
materialize_marketplace_source,
materialize_plugin_source,
parse_marketplace_source,
redact_urls_in_text,
)
from deepagents_code.plugins.models import (
MarketplacePluginEntry,
MarketplaceRecord,
PluginDiscoveryResult,
PluginInstance,
PluginMarketplace,
RepositoryMarketplaceSource,
split_plugin_id,
)
from deepagents_code.plugins.store import (
cache_and_register_plugin,
ensure_marketplace_cache_dir,
ensure_plugin_data_dir,
get_primary_install_entry,
load_enabled_plugin_ids,
load_installed_plugins,
load_marketplace_records,
plugin_data_dir,
remove_marketplace_record,
save_marketplace_record,
set_plugin_enabled,
uninstall_plugin as uninstall_plugin_record,
)
logger = logging.getLogger(__name__)
def add_local_marketplace(path: str | Path) -> PluginMarketplace:
"""Add a local marketplace to dcode state.
Args:
path: Marketplace root directory.
Returns:
Parsed marketplace.
"""
marketplace = load_marketplace(Path(path))
save_marketplace_record(
MarketplaceRecord(
name=marketplace.name,
source_type="directory",
source=str(marketplace.root),
install_location=str(marketplace.root),
)
)
return marketplace
def add_marketplace_source(raw: str) -> PluginMarketplace:
"""Add a marketplace from a pasted source string.
Args:
raw: GitHub shorthand, Git URL, marketplace JSON URL, file, or directory.
Returns:
Parsed marketplace.
"""
source = parse_marketplace_source(raw)
marketplace, location = materialize_marketplace_source(source)
save_marketplace_record(
MarketplaceRecord(
name=marketplace.name,
source_type=source.source_type,
source=source.value,
install_location=str(location),
ref=source.ref if isinstance(source, RepositoryMarketplaceSource) else None,
)
)
return marketplace
def remove_marketplace(name: str) -> bool:
"""Remove a marketplace and every plugin installed from it.
Local marketplace source directories are never deleted. Managed marketplace
clones and installed plugin caches are removed.
Args:
name: Marketplace name.
Returns:
`True` when a configured marketplace was removed.
"""
record = load_marketplace_records().get(name)
if record is None:
return False
installed = load_installed_plugins(strict=True)
enabled = load_enabled_plugin_ids(strict=True)
plugin_ids = set(installed) | set(enabled)
for plugin_id in plugin_ids:
try:
_plugin_name, marketplace_name = split_plugin_id(plugin_id)
except ValueError:
continue
if marketplace_name == name:
uninstall_plugin(plugin_id)
removed = remove_marketplace_record(name)
location = Path(record.install_location)
try:
resolved = location.resolve()
cache_root = ensure_marketplace_cache_dir().resolve()
except OSError:
return removed
if record.source_type in {"github", "git", "url"} and resolved.is_relative_to(
cache_root
):
if resolved.is_dir():
shutil.rmtree(resolved, ignore_errors=True)
elif resolved.is_file():
resolved.unlink(missing_ok=True)
return removed
def _require_installed_plugin(plugin_id: str) -> None:
"""Raise when `plugin_id` does not identify an installed plugin.
Raises:
MarketplaceError: If the plugin is not installed.
"""
if plugin_id not in load_installed_plugins(strict=True):
msg = f"Plugin {plugin_id!r} is not installed"
raise MarketplaceError(msg)
def set_installed_plugin_enabled(plugin_id: str, *, enabled: bool) -> None:
"""Set the enabled state of an installed plugin.
Args:
plugin_id: Plugin id in `{name}@{marketplace}` form.
enabled: Whether to enable the plugin.
"""
_require_installed_plugin(plugin_id)
set_plugin_enabled(plugin_id, enabled)
if enabled:
ensure_plugin_data_dir(plugin_id)
def uninstall_plugin(plugin_id: str) -> None:
"""Uninstall a plugin (disable, clear records, delete orphaned cache).
Args:
plugin_id: Plugin id in `{name}@{marketplace}` form.
"""
uninstall_plugin_record(plugin_id)
def _resolve_marketplace_and_entry(
plugin_id: str,
) -> tuple[PluginMarketplace, MarketplacePluginEntry]:
try:
plugin_name, marketplace_name = split_plugin_id(plugin_id)
except ValueError as exc:
raise MarketplaceError(str(exc)) from exc
records = load_marketplace_records()
record = records.get(marketplace_name)
if record is None:
msg = f"Marketplace {marketplace_name!r} is not configured"
raise MarketplaceError(msg)
marketplace = load_marketplace_location(Path(record.install_location))
entry = next(
(plugin for plugin in marketplace.plugins if plugin.name == plugin_name),
None,
)
if entry is None:
msg = f"Plugin {plugin_id!r} not found in marketplace {marketplace_name}"
raise MarketplaceError(msg)
return marketplace, entry
def install_plugin(plugin_id: str) -> PluginInstance:
"""Install a marketplace plugin into the versioned cache and enable it.
Copies the plugin source into `plugins/cache/{marketplace}/{plugin}/{version}/`,
writes `installed_plugins.json`, and enables the plugin.
Args:
plugin_id: Plugin id in `{name}@{marketplace}` form.
Returns:
Discovered plugin instance loaded from the cache path.
Raises:
MarketplaceError: If the marketplace/plugin cannot be resolved, the
source is unsupported, or the cached plugin fails to load.
"""
load_installed_plugins(strict=True)
load_enabled_plugin_ids(strict=True)
marketplace, entry = _resolve_marketplace_and_entry(plugin_id)
source_root = materialize_plugin_source(marketplace, entry)
if source_root is None:
msg = (
f"Plugin {plugin_id} has unsupported source "
f"{redact_urls_in_text(repr(entry.source))}; "
"use a local path, GitHub repository, or Git repository source"
)
raise MarketplaceError(msg)
try:
manifest, _manifest_path, manifest_warnings = load_manifest(
source_root, fallback_name=entry.name
)
except PluginManifestError as exc:
msg = f"Cannot install {plugin_id}: {exc}"
raise MarketplaceError(msg) from exc
for warning in manifest_warnings:
logger.debug("Plugin install warning for %s: %s", plugin_id, warning)
version = manifest.version if manifest is not None else None
cache_path = cache_and_register_plugin(
plugin_id,
source_root,
version=version,
validate=partial(
_validate_plugin_copy,
plugin_id=plugin_id,
fallback_name=entry.name,
),
)
set_plugin_enabled(plugin_id, True)
ensure_plugin_data_dir(plugin_id)
instance, warnings = _plugin_from_install_path(
plugin_id=plugin_id,
root=cache_path,
marketplace_name=marketplace.name,
fallback_name=entry.name,
)
if instance is None:
detail = "; ".join(warnings)
uninstall_plugin_record(plugin_id)
msg = f"Installed {plugin_id} but failed to load from cache: {detail}"
raise MarketplaceError(msg)
return instance
def _validate_plugin_copy(
root: Path,
*,
plugin_id: str,
fallback_name: str,
) -> None:
try:
manifest, _manifest_path, warnings = load_manifest(
root, fallback_name=fallback_name
)
except PluginManifestError as exc:
msg = f"Cannot install {plugin_id}: {exc}"
raise MarketplaceError(msg) from exc
build_inventory(root, manifest, warnings)
def _plugin_from_install_path(
*,
plugin_id: str,
root: Path,
marketplace_name: str,
fallback_name: str,
) -> tuple[PluginInstance | None, tuple[str, ...]]:
warnings: list[str] = []
try:
manifest, _manifest_path, manifest_warnings = load_manifest(
root, fallback_name=fallback_name
)
except PluginManifestError as exc:
return None, (f"Skipping plugin {plugin_id}: {exc}",)
warnings.extend(manifest_warnings)
name = manifest.name if manifest and manifest.name else fallback_name
inventory = build_inventory(root, manifest, tuple(warnings))
try:
instance = PluginInstance(
plugin_id=plugin_id,
name=name,
marketplace=marketplace_name,
version=manifest.version if manifest is not None else None,
root=root,
data_dir=plugin_data_dir(plugin_id),
manifest=manifest,
inventory=inventory,
)
except ValueError as exc:
return None, (f"Skipping plugin {plugin_id}: {exc}",)
return instance, inventory.warnings
def discover_plugins() -> PluginDiscoveryResult:
"""Discover enabled marketplace plugins from their install cache paths.
Returns:
Discovery result. Broken marketplaces/plugins are returned as warnings and
never abort sibling plugin loading.
"""
enabled = load_enabled_plugin_ids()
plugins: list[PluginInstance] = []
warnings: list[str] = []
for plugin_id in sorted(enabled):
try:
plugin_name, marketplace_name = split_plugin_id(plugin_id)
except ValueError:
warnings.append(f"Ignoring invalid plugin id {plugin_id!r}")
continue
entry = get_primary_install_entry(plugin_id)
if entry is None:
warnings.append(
f"Plugin {plugin_id} is enabled but not installed "
"(missing installed_plugins.json entry); run install to fix this"
)
continue
root = Path(entry.install_path)
try:
root_exists = root.is_dir()
except (OSError, RuntimeError) as exc:
warnings.append(f"Plugin {plugin_id} cache could not be inspected: {exc}")
continue
if not root_exists:
warnings.append(
f"Plugin {plugin_id} cache miss at {entry.install_path}; "
"re-run install to refresh"
)
continue
try:
plugin, plugin_warnings = _plugin_from_install_path(
plugin_id=plugin_id,
root=root,
marketplace_name=marketplace_name,
fallback_name=plugin_name,
)
except (OSError, RuntimeError) as exc:
warnings.append(f"Skipping plugin {plugin_id}: {exc}")
continue
warnings.extend(plugin_warnings)
if plugin is not None:
plugins.append(plugin)
return PluginDiscoveryResult(plugins=tuple(plugins), warnings=tuple(warnings))
def list_available_plugins() -> tuple[tuple[str, str, bool], ...]:
"""List plugins from configured marketplaces.
Returns:
Tuples of `(plugin_id, description, enabled)`.
"""
records = load_marketplace_records()
enabled = load_enabled_plugin_ids()
rows: list[tuple[str, str, bool]] = []
for name, record in sorted(records.items()):
try:
marketplace = load_marketplace_location(Path(record.install_location))
except MarketplaceError as exc:
rows.append((f"<marketplace:{name}>", str(exc), False))
continue
for plugin in marketplace.plugins:
plugin_id = f"{plugin.name}@{marketplace.name}"
rows.append((plugin_id, plugin.description or "", plugin_id in enabled))
return tuple(rows)
def list_installed_plugin_ids() -> frozenset[str]:
"""Return plugin ids that have install records.
Returns:
Set of installed plugin ids.
"""
return frozenset(load_installed_plugins())