"""Plugin manifest schema (Pydantic v2).""" from __future__ import annotations from pydantic import BaseModel, Field, field_validator class PluginRouteDef(BaseModel): """Route definition within a plugin manifest.""" path: str = Field(..., description="URL path prefix, e.g. /api/v1/plugin-mail") module: str = Field(..., description="Dotted path to the module containing the APIRouter") router_attr: str = Field( default="router", description="Attribute name of the APIRouter in the module" ) class FieldDefinition(BaseModel): """Field definition for field-level permissions.""" module: str = Field(..., description="Module name (e.g. 'companies', 'contacts')") field: str = Field(..., description="Field name (e.g. 'annual_revenue')") label: str = Field(..., description="Human-readable label") sensitivity: str = Field(default="normal", description="normal|sensitive|critical") # ── Frontend UI definition models (Phase 3) ────────────────────────────────── class FrontendMenuItem(BaseModel): """A sidebar navigation item contributed by a plugin.""" label_key: str = Field(..., description="i18n key for the menu label") label: str = Field(default="", description="Fallback label if i18n key is missing") path: str = Field(..., description="Frontend route path, e.g. /mail") icon: str = Field(default="FileText", description="lucide-react icon name") group: str = Field(default="", description="Optional group label_key for tree-style nesting") order: int = Field(default=100, description="Sort order within the sidebar") badge_key: str = Field(default="", description="Optional store key for badge count") class FrontendPageRoute(BaseModel): """A frontend page route contributed by a plugin.""" path: str = Field(..., description="Frontend route path, e.g. /mail or /mail/settings") component: str = Field( ..., description="Dotted path to the React component, e.g. @/pages/Mail" ) parent: str = Field( default="", description="Parent route path for nested routes (e.g. /settings for a settings sub-page)", ) protected: bool = Field(default=True, description="Whether the route requires authentication") order: int = Field(default=100, description="Sort order") class FrontendDetailTab(BaseModel): """A detail tab contributed by a plugin for entity detail views.""" entity_type: str = Field(..., description="Entity type this tab applies to, e.g. 'contact'") label_key: str = Field(..., description="i18n key for the tab label") label: str = Field(default="", description="Fallback label") component: str = Field(..., description="Dotted path to the React component") icon: str = Field(default="FileText", description="lucide-react icon name") order: int = Field(default=100, description="Sort order within the detail view") permission: str = Field(default="", description="Optional permission required to see this tab") class FrontendSettingsPage(BaseModel): """A settings sub-page contributed by a plugin.""" path: str = Field(..., description="Settings sub-route path, e.g. mail or notifications") label_key: str = Field(..., description="i18n key for the settings nav label") label: str = Field(default="", description="Fallback label") component: str = Field(..., description="Dotted path to the React component") icon: str = Field(default="Settings", description="lucide-react icon name") order: int = Field(default=100, description="Sort order within settings nav") permission: str = Field(default="", description="Optional permission required") class FrontendDashboardWidget(BaseModel): """A dashboard widget contributed by a plugin.""" id: str = Field(..., description="Unique widget identifier") label_key: str = Field(..., description="i18n key for the widget title") label: str = Field(default="", description="Fallback label") component: str = Field(..., description="Dotted path to the React component") icon: str = Field(default="LayoutDashboard", description="lucide-react icon name") order: int = Field(default=100, description="Sort order on the dashboard") col_span: int = Field(default=1, description="Grid column span (1-4)") row_span: int = Field(default=1, description="Grid row span") permission: str = Field(default="", description="Optional permission required") class PluginManifest(BaseModel): """Manifest describing a plugin's metadata, dependencies, and capabilities.""" name: str = Field( ..., min_length=1, max_length=80, description="Unique plugin identifier (snake_case)" ) version: str = Field(..., min_length=1, max_length=40, description="Semantic version string") display_name: str = Field(..., min_length=1, max_length=120) description: str = Field(default="", max_length=500) dependencies: list[str] = Field( default_factory=list, description="Other plugin names this plugin depends on" ) routes: list[PluginRouteDef] = Field( default_factory=list, description="Route definitions to register on activation" ) events: list[str] = Field( default_factory=list, description="Event names this plugin listens to" ) migrations: list[str] = Field( default_factory=list, description="Migration file names (ordered, e.g. 0001_initial.sql)" ) permissions: list[str] = Field( default_factory=list, description="Required permissions for this plugin" ) is_core: bool = Field( default=False, description="Whether this is a core plugin that cannot be deactivated" ) field_definitions: list[FieldDefinition] = Field( default_factory=list, description="Field definitions for field-level permissions" ) agent_capabilities: list[str] = Field( default_factory=list, description="AI agent capabilities this plugin provides (e.g. 'contact_search', 'email_draft')", ) # ── Frontend UI contributions (Phase 3) ── menu_items: list[FrontendMenuItem] = Field( default_factory=list, description="Sidebar navigation items contributed by this plugin" ) page_routes: list[FrontendPageRoute] = Field( default_factory=list, description="Frontend page routes contributed by this plugin" ) detail_tabs: list[FrontendDetailTab] = Field( default_factory=list, description="Entity detail tabs contributed by this plugin" ) settings_pages: list[FrontendSettingsPage] = Field( default_factory=list, description="Settings sub-pages contributed by this plugin" ) dashboard_widgets: list[FrontendDashboardWidget] = Field( default_factory=list, description="Dashboard widgets contributed by this plugin" ) @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: if not v.replace("_", "").isalnum(): raise ValueError("Plugin name must be alphanumeric with underscores only") return v.lower() model_config = {"extra": "forbid"} class ManifestSchemaResponse(BaseModel): """Response model describing the manifest schema for API consumers.""" fields: dict[str, dict[str, str]] example: PluginManifest # Pre-built schema documentation for GET /api/v1/plugins/manifest endpoint MANIFEST_SCHEMA_DOC = ManifestSchemaResponse( fields={ "name": { "type": "str", "required": "true", "description": "Unique plugin identifier (snake_case, max 80 chars)", }, "version": {"type": "str", "required": "true", "description": "Semantic version string"}, "display_name": { "type": "str", "required": "true", "description": "Human-readable plugin name", }, "description": { "type": "str", "required": "false", "description": "Plugin description (max 500 chars)", }, "dependencies": { "type": "list[str]", "required": "false", "description": "Other plugin names required", }, "routes": { "type": "list[PluginRouteDef]", "required": "false", "description": "Route definitions to register", }, "events": { "type": "list[str]", "required": "false", "description": "Event names to listen to", }, "migrations": { "type": "list[str]", "required": "false", "description": "Migration file names (ordered)", }, "permissions": { "type": "list[str]", "required": "false", "description": "Required permissions", }, "menu_items": { "type": "list[FrontendMenuItem]", "required": "false", "description": "Sidebar navigation items (label_key, path, icon, group, order)", }, "page_routes": { "type": "list[FrontendPageRoute]", "required": "false", "description": "Frontend page routes (path, component, parent, protected)", }, "detail_tabs": { "type": "list[FrontendDetailTab]", "required": "false", "description": "Entity detail tabs (entity_type, label_key, component, icon, permission)", }, "settings_pages": { "type": "list[FrontendSettingsPage]", "required": "false", "description": "Settings sub-pages (path, label_key, component, icon, permission)", }, "dashboard_widgets": { "type": "list[FrontendDashboardWidget]", "required": "false", "description": "Dashboard widgets (id, label_key, component, col_span, permission)", }, }, example=PluginManifest( name="example_plugin", version="1.0.0", display_name="Example Plugin", description="An example plugin demonstrating the manifest schema.", dependencies=[], routes=[], events=["contact.created"], migrations=["0001_initial.sql"], permissions=["contacts.read"], menu_items=[ FrontendMenuItem( label_key="nav.examplePlugin", label="Example", path="/example", icon="Sparkles", order=50, ) ], page_routes=[ FrontendPageRoute( path="/example", component="@/pages/Example", protected=True, ) ], ), )