ModuleManager & Plugin Contract
This page is a short orientation to how ModuleManager discovers, verifies,
and loads a plugin. It intentionally stays brief: docs/internal/25_Core_and_SDK_Boundary.md
covers the full V1/V2/V3 loading history and the Core/SDK package split in
depth, and docs/internal/24_Plugin_Communication_Protocol.md covers the
isolated-process wire protocol — this page previously duplicated (and drifted
from) both, describing a get_plugin() factory contract that doesn't exist
in the code. It's gone; below is what's actually there.
flowchart TD
A[Plugin package on disk] --> B[Manifest parsing]
B --> C[Trust & signature check]
C --> D{process_model?}
D -->|"in_process (default)"| E[Import into the Hub's interpreter]
D -->|isolated| F[Spawn in the plugin's own .venv]
E --> G[PluginContext + entry_point]
F --> H[PluginUIDaemon + ui_daemon_runtime.run]
High-level responsibilities
karcytics/core/plugins/discovery.py— scans bundled and user-installed plugin directories, parses manifests (pyproject.toml[tool.karcytics.plugin], or a legacymanifest.json), and classifies each as V1 (dead — see below)/V2 legacy/V3.TrustManager(karcytics_sdk/host/trust_manager.py) — verifies Ed25519 signatures and per-file SHA-256 hashes against a signedsecurity.jsonbefore anything is imported or spawned. Seedocs/internal/20_Security_and_Signing.md.karcytics/core/module_manager.py— orchestrates load/unload, dispatches to the in-process or isolated path based onmanifest.get("process_model").PluginLoaderFactory(karcytics/core/plugins/loader.py) — the actual import/instantiate logic for in-process plugins (load_ui, V2/V3 paths) and the zero-arg factory builder for isolated ones (_load_ui_isolated).
Loading lifecycle (in-process, V3 — current)
PluginLoaderFactory.load_uisees anentry_pointin the manifest ("module:function", e.g."karcytics_plugins.flow_cytometry:initialize").- The named module is imported directly (via
sys.path, afterPluginEnvironmentInjector.inject_pathputs the plugin's own.venv/site-packagesandsrc/on it). - A
PluginContextis built from a services dict (task_scheduler,logger,event_bus— the last one is currently alwaysNone, see doc 25's Migration status) and the parsed manifest. - The named function is called as
entry_point(context)and its return value — aPluginBase(orQWidget) instance — is inserted into the Hub'sWorkspaceWindow.
An isolated plugin (process_model = "isolated") skips all of this —
see doc 24 for that path in full.
Discovery & security, in one line each
- Malformed or incompatible manifests are filtered out at discovery time, not at load time.
- A V1 (
author-field) manifest is discovered only to explain to the user why it's rejected (OutdatedModuleError); it never loads. - An unverified or hash-mismatched package is rejected unless the user
explicitly grants a local trust override (
TrustManager's override flow).
The real plugin contract
See docs/internal/16_PluginBase_and_SDK_Contract.md for PluginBase's
actual shape and a real entry_point example — this page only covers how
that contract gets discovered and invoked, not what it looks like.
Threading considerations
- UI objects are created on the Hub's Qt main thread only — a Qt
requirement, not a convention. Long-running computation belongs in an
AnalysisBasesubclass submitted to the sharedTaskScheduler(QThreadPool-backed); see doc 24's "Threading model" for in-process plugins. - Nothing here posts to an
EventBuson a plugin's behalf — see doc 25's Migration status item #2 for the current, real gap there.
Links
docs/internal/25_Core_and_SDK_Boundary.md— full V1/V2/V3 history, the Core/SDK package split, and known migration gaps.docs/internal/24_Plugin_Communication_Protocol.md— the isolated-process wire protocol in full.docs/internal/16_PluginBase_and_SDK_Contract.md— the actualPluginBase/entry_pointshape a plugin author implements.karcytics/core/module_manager.py,karcytics/core/plugins/loader.py,discovery.py— the real loading orchestration.