PluginBase & SDK Contract
The real plugin contract, as implemented in karcytics_sdk.plugin.base.PluginBase
and wired up via a V3 entry_point — not the get_plugin()/karcytics_sdk.core
shape this page used to describe, which never matched the code. See
docs/internal/15_ModuleManager_and_PluginContract.md for how a plugin gets
discovered and this entry point gets called in the first place.
The entry_point function
A plugin's pyproject.toml declares one under [tool.karcytics.plugin]:
module:function — the named module is imported, the named function is
called with a PluginContext, and its return value (a PluginBase
instance, in practice) is what the Hub inserts into WorkspaceWindow:
# karcytics_plugins/flow_cytometry/__init__.py (the real, current shape)
from karcytics_sdk.plugin.context import PluginContext
def initialize(context: PluginContext) -> Any:
...
return FlowCytometryPanel(plugin_id="flow_cytometry")
PluginBase (karcytics_sdk/plugin/base.py)
A QWidget subclass, not a bare interface — a plugin's panel is a
PluginBase, not something that owns one separately.
class PluginBase(QWidget):
def __init__(self, plugin_id: str, parent=None): ...
# Must be implemented by every subclass:
def get_state(self) -> PluginState: ...
def set_state(self, state: PluginState) -> None: ...
# Provided, ready to use:
def push_state(self, label: str = "") -> None: ... # record one named undo step
def undo(self) -> bool: ... # True if a step was undone
def redo(self) -> bool: ...
def can_undo(self) -> bool: ...
def can_redo(self) -> bool: ...
def undo_text(self) -> str: ... # "Undo Delete Gate" — Edit menu text
def redo_text(self) -> str: ...
undo_history: UndoHistory # property; created on first use
def bind_undo_history(self, history: UndoHistory, restore) -> None: ...
def cleanup(self) -> None: ... # RAII-style resource release via ResourceInspector
def publish_event(self, topic: str, data: Any = None) -> None: ... # CentralEventBus
def subscribe_event(self, topic: str, callback) -> None: ... # CentralEventBus
get_state()/set_state() work in terms of PluginState
(karcytics_sdk/plugin/state.py), not a raw dict. self.state_changed
(proxied through __getattr__ to self.signals, a PluginSignals instance
covering status/state_changed/analysis_started/analysis_finished/
analysis_error, etc.) fires automatically from push_state()/undo()/redo().
Undo/redo
The history lives in the plugin's own process, as an SDK UndoHistory
(karcytics_sdk/plugin/history.py). It never uses the Hub's
HistoryManager, which an isolated plugin can't import (see doc 24).
UndoHistory stores labelled snapshots, refuses to record a snapshot equal
to the current one, and tracks a saved ("clean") revision, so undoing back
to the last save reads as unsaved-changes-free again.
- Default:
push_state(label)recordsget_state().to_dict()as one step, andundo()/redo()restore it viaset_state(type(state).from_dict(...)). The firstpush_state()only sets the baseline. Call it once per finished user action, not on every tick of a drag. - Own state store: a plugin that restores state in place calls
bind_undo_history(history, restore)with its ownUndoHistoryand arestore(snapshot)callable. Flow Cytometry does this with itsFlowStore(itsdocs/developer/10_STATE_UNDO_AND_PERSISTENCE.md). - If
restoreraises, the history pointer moves back, the error is logged andundo()/redo()returnFalse, so the history never disagrees with the live state. - Any history change emits
undo_available,redo_availableandundo_state_changed.
The isolated window (ui_daemon_runtime.py) gives every plugin an
Edit → Undo/Redo menu: Cmd+Z / Cmd+Shift+Z on macOS, Ctrl+Z / Ctrl+Y
(and Ctrl+Shift+Z) elsewhere. Item text and enabled state follow
undo_text()/can_undo(). While a text field has focus, the shortcut
undoes that field's own typing instead.
Closing and autosave
- A user-initiated close of the isolated window first calls
panel.confirm_close()if the panel defines it; returningFalsekeeps the window open, for example to offer Save / Discard / Cancel. A close the Hub requests is never vetoed. setup_workflow_autosave(..., has_unsaved_changes=callable)skips an autosave tick, including its reminder toast, when the callable reports nothing to save.
Minimal example
from karcytics_sdk.plugin.base import PluginBase
from karcytics_sdk.plugin.state import PluginState
class MyState(PluginState):
threshold: float = 0.5
class MyPlugin(PluginBase):
def __init__(self, plugin_id: str):
super().__init__(plugin_id)
self._state = MyState()
# build UI here
def get_state(self) -> MyState:
return self._state
def set_state(self, state: MyState) -> None:
self._state = state
self.update_ui()
def cleanup(self) -> None:
super().cleanup() # releases heavy references via ResourceInspector
Analysis workers (off-UI thread)
Long-running computation belongs in an AnalysisBase subclass
(karcytics_sdk/plugin/analysis.py), submitted to a TaskScheduler
(context.get("task_scheduler") for an in-process plugin; a local,
per-process one for an isolated plugin — see doc 25's "Where UI comes from,
where analysis comes from"). task_started/task_finished/task_error/
task_progress signals, keyed by task_id, are how a result gets back to
the UI thread — never a direct return value across a thread boundary.
Signing & distribution
Covered in full by docs/internal/20_Security_and_Signing.md and
21_Supply_Chain_Security.md — summary: a plugin ships with a signed
security.json (per-file SHA-256 + an Ed25519 signature chaining to the
Karcytics Core Authority root key, a project CI key, or an explicit local
override), verified by TrustManager before module_manager.py will load
or spawn it. The SDK CLI's security.py commands (init-identity, sign,
project-sign) generate the identity and produce that signature.
Testing & contract verification
karcytics_sdk.testing.contract.ContractTestBase — a pytest base class a
plugin author subclasses to get test_manifest_is_valid and
test_headless_initialization for free (the latter mocks every capability
the manifest declares under requires and confirms the plugin's
entry_point resolves and initializes with no running Hub at all). See
tests/sdk/test_plugin_contract.py in this repo for worked examples.
Links
docs/internal/15_ModuleManager_and_PluginContract.md— discovery, trust verification, and howentry_pointgets invoked.docs/internal/25_Core_and_SDK_Boundary.md— which package ownsPluginBasevs.PluginContextvs. the concrete services behind them.karcytics_sdk/plugin/base.py,state.py,context.py,analysis.py— the real source.