mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
f49337595a
Centralize recursive JSON aliases in a neutral module so config and plugin code share one definition while preserving plugin import compatibility. The aliases remain static types only; existing runtime ingress validation is unchanged. <details> <summary>Test plan</summary> - `make -C libs/code format` - `make -C libs/code lint` - Focused JSON, hooks, model config, and MCP login tests (546 passed; one unrelated root-permission assertion failed) </details> Made by [Open SWE](https://openswe.vercel.app/agents/019f8107-bf8c-7545-9a37-299374653ddc) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
238 lines
6.4 KiB
Python
238 lines
6.4 KiB
Python
"""Data models for plugins."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Literal
|
|
|
|
from deepagents_code.json_types import JsonObject, JsonValue # noqa: TC001, F401
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
MarketplaceSourceType = Literal["directory", "file", "github", "git", "url"]
|
|
ExternalPluginRepositorySourceType = Literal["github", "git-subdir", "url"]
|
|
UnsupportedComponent = Literal["agents", "commands", "hooks"]
|
|
"""Plugin component directory that `deepagents-code` does not load."""
|
|
|
|
|
|
@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.
|
|
display_name: Optional human-readable label from `displayName`.
|
|
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
|
|
display_name: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class ComponentInventory:
|
|
"""Inventory of supported plugin components.
|
|
|
|
`unsupported` lists plugin component directories that `deepagents-code` does
|
|
not load (e.g. `agents/`, `commands/`, `hooks/`).
|
|
"""
|
|
|
|
skills: tuple[Path, ...] = ()
|
|
mcp_files: tuple[Path, ...] = ()
|
|
unsupported: tuple[UnsupportedComponent, ...] = ()
|
|
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
|
|
display_name: str | 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
|