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.

Related pages