FAQ & Troubleshooting
On this page
Answers here are drawn directly from behavior observed in the source, including a few surprising edge cases worth knowing about before you hit them in production.
"My plugin instantiation raises TypeError immediately"
BasePlugin.__init__ checks hasattr(self.__class__, "METADATA") as its first action and raises
TypeError(f"{cls.__name__} must define a class-level METADATA attribute.") if it's missing. Make sure
METADATA is set as a class attribute, not assigned in __init__ before calling super().__init__().
"PluginManager.register() silently returns False — why?"
register() has four sequential gates, any of which can cause a silent False return (with a log
message, not an exception): system-version incompatibility, missing declared dependencies, a failed
permission check (only in safe_mode), and an already-registered name. Check your logs at INFO/ERROR
level — the reason is always logged even though the method itself doesn't raise.
"Why did my plugin's status flip to ERROR on its own?"
BasePlugin._record_failure automatically forces status = PluginStatus.ERROR once a plugin has more
than 10 failed executions and its overall failure rate exceeds 50%. This is the only automatic
status transition not triggered by an explicit PluginManager call — see
"execute_pipeline isn't passing my retrieval results into the next plugin"
execute_pipeline only forwards the previous step's output as-is if it's a dict; a list result
(which is what RetrievalPlugin.execute() returns, per its contract) gets replaced with {} before
being passed to the next plugin. See the worked example and fix in the
Plugin Development tutorial.
"Does safe_mode=True actually block execution when permissions are missing?"
PluginManager.execute() calls security.check_all_required(plugin, ctx) in safe_mode, but based on
the source, this call's boolean result is not checked/enforced at that point — unlike register(),
which does act on the check result. Review plugin_manager.md for the
exact code path, and consider calling security.assert_permission() yourself inside your plugin's
execute() if you need a hard guarantee.
"Is ExecutionSandbox actually enforcing my hard_timeout?"
PluginManager constructs an ExecutionSandbox, but execute() calls plugin.safe_execute() /
safe_execute_with_retry() directly rather than routing through sandbox.run(). The timeout that
actually applies per call is plugin.config.timeout (default 30s). See
"watchdog isn't installed — does hot reload break?"
No. Both plugins/__init__'s import of hot_reload and hot_reload.py's own import of watchdog
are wrapped in try/except ImportError. Without watchdog, PluginHotReloader.start() automatically
falls back to a polling backend — no error, just a log message and a slower detection interval.
"Can I pass manager to PluginHotReloader and expect it to re-initialize reloaded plugins?"
PluginHotReloader._trigger_reload calls manager.initialize_plugin(name) (a public method name), but
PluginManager only defines the equivalent logic as the private _initialize_plugin. Passing a stock
PluginManager instance as manager and expecting this to work will raise AttributeError unless you
add a public initialize_plugin method yourself .
"Is the registry thread-safe?"
No — PluginRegistry's own docstring says it's "designed for cooperative async" and recommends the
caller serialize concurrent registrations with an asyncio.Lock. PluginManager itself does not add
this locking around registry mutations based on the source. Concurrent registration from multiple
coroutines/threads should be serialized at your application layer.
"How do I know what Python version I need?"
base_plugin imports datetime.UTC, which requires Python 3.11+. No other explicit version marker
exists in the source. See installation.