feat(plugins): backend-declared plugin names and frontend metadata - #566
feat(plugins): backend-declared plugin names and frontend metadata#566hamza-56 wants to merge 1 commit into
Conversation
|
@claude-review |
|
Claude finished @hamza-56's task in 2m 40s —— View job Code Review —
|
| existing_class = type(self._loaded_plugins[plugin_name]).__name__ | ||
| logger.error( | ||
| f"Duplicate plugin name '{plugin_name}': " | ||
| f"'{plugin_class.__name__}' clashes with already-loaded '{existing_class}'; skipping it" |
There was a problem hiding this comment.
Low / informational: by the time the duplicate-name check runs, the plugin's __init__ has already executed and registered its side effects into the global hooks (DISPLAY_INFO, CONFIG_SCHEMAS, MCP_TOOLS, and notably PLUGIN_ROUTERS via register_router). Skipping here only prevents storing the instance in _loaded_plugins; it does not roll back those registrations.
In practice this self-heals because every one of those hooks is a PluginHook backed by a WeakKeyDictionary — once the loop drops its only strong reference to the skipped instance, its entries disappear. But that leaves the cleanup dependent on GC timing relative to when assemble_app reads PLUGIN_ROUTERS. Under CPython refcounting it's collected immediately, so it's fine today; worth a one-line comment noting the reliance, or (belt-and-suspenders) del plugin_instance on the skip path to make the intent explicit.
Plugin identity and display metadata were split across two disconnected registries, linked only by a name derived from the Python class name (#565, item 4 of #372). - Every plugin now declares its own kebab-case name explicitly by passing it positionally to super().__init__(); the loader constructs plugin classes with no arguments and never derives a name, so renaming a class no longer changes plugin identity. - New per-concern hooks in sparkth/lib/frontend (DISPLAY_INFO, SIDEBAR_ENTRIES, FRONTEND_APPS) let a plugin declare its display info, sidebar entry, and frontend page; icons cross the wire as lucide icon names, never as components. - UserPluginResponse exposes the declarations read-only (display, sidebar, has_frontend), so the frontend can render what the backend declares instead of keeping its own copy; frontend API types regenerated.
5e6f0a5 to
a61c772
Compare
Part of: Plugin identity and display metadata are split across disconnected backend and frontend registries
First PR of the sequence proposed in #372 (hooks + API exposure). Frontend consumption and the drift gate follow separately, so this does not close #565 yet.
What
The backend becomes the single source of truth for a plugin's identity and display metadata: plugins declare their own name explicitly (nothing is derived from the class name anymore) and contribute display info, sidebar entry, and a frontend-app marker through per-concern hooks, which the user-plugins API exposes read-only.
Changes
super().__init__(); the loader constructs classes with no arguments and stops deriving names from class namesDISPLAY_INFO,SIDEBAR_ENTRIES, andFRONTEND_APPShooks (sparkth/lib/frontend) with name-based lookup helpersdisplay,sidebar, andhas_frontendread-only onUserPluginResponse; regeneratefrontend/lib/api/generated.tsHow to Test
uv run pytest(new suites:tests/lib/test_frontend.py,tests/core/plugins/test_loader.py,sparkth/plugins/*/tests/test_plugin.py)make backend.up.dev, authenticate, thenGET /api/v1/user-plugins/: every plugin entry carriesdisplay,sidebar, andhas_frontend; canvas and open-edx now have a display name and descriptionmake docs: the Plugin authoring reference renders the newsparkth.lib.frontendsectionNotes
__init__no longer receives the name from the loader. Declare it explicitly:def __init__(self) -> None: super().__init__("my-app"). Old-style plugins are skipped at load time with an error in the logs.canvas,chat,google-drive,open-edx,slack), so existing DB rows keep matching.super().__init__()raisesValueErrorotherwise.This PR description was written with the assistance of an LLM (Claude).