Advanced Topics
August 10, 2026 · View on GitHub
Internals of find_plugins
def find_plugins(plug_type: HiveMindPluginTypes = None) -> dict:
Source: hivemind_plugin_manager/__init__.py:108
Entry-point iteration
_iter_entrypoints(plug_type) (__init__.py:92) tries importlib_metadata.entry_points
first. If importlib_metadata is not installed it falls back to
pkg_resources.iter_entry_points. This keeps HPM compatible with older Python environments
where importlib.metadata is absent or incomplete.
Error swallowing
When an entry point's .load() raises any exception:
- The entry point is appended to
find_plugins._errored(a module-level list initialised to[]at__init__.py:140). LOG.erroris called once for that entry point.- On subsequent calls the entry point is silently skipped (it stays in
_errored).
This prevents log spam when find_plugins is called repeatedly in a tight loop (e.g. from
a skills manager). The trade-off is that a broken plugin is invisible after the first
error. If you are debugging a missing plugin, reset the list:
from hivemind_plugin_manager import find_plugins
find_plugins._errored = []
result = find_plugins() # errors will surface again
No caching
find_plugins does not cache results. Every call re-iterates entry points. For
performance-sensitive startup code, call it once and hold the returned dict.
How DatabaseFactory Routes Local vs Remote
if issubclass(plugin, AbstractRemoteDB):
return plugin(name=name, subfolder=subfolder, password=password, host=host, port=port)
return plugin(name=name, subfolder=subfolder, password=password)
Source: hivemind_plugin_manager/__init__.py:33
The factory uses issubclass against AbstractRemoteDB at instantiation time. This means:
- Callers can always pass
hostandport- they are silently dropped for local plugins. - A plugin that subclasses
AbstractRemoteDBbut ignoreshost/portin its own__init__is fine; the values are passed as kwargs and Python discards them if the signature includes**kwargsor if the dataclass field is declared with a default. - A plugin that subclasses
AbstractDBdirectly but needs a host must override__post_init__and read fromself.configor environment variables instead.
Identity and NodeIdentity
All protocol base classes expose an .identity property via _SubProtocol:
@property
def identity(self) -> NodeIdentity:
if not self.hm_protocol:
return NodeIdentity()
return self.hm_protocol.identity
Source: hivemind_plugin_manager/protocols.py:41
NodeIdentity comes from hivemind-bus-client. It holds the cryptographic identity (key
pair, name, UUID) of the current HiveMind node. A fresh NodeIdentity() is returned when
the protocol is not yet attached to a HiveMindListenerProtocol - this is the expected
state during unit tests and during early construction.
The hm_protocol Circular Reference
When hivemind-core constructs a HiveMindListenerProtocol it accepts an
agent_protocol and a binary_protocol as constructor arguments. At that point
hm_protocol on both sub-protocols is None. After HiveMindListenerProtocol.__init__
runs, it assigns self back:
# pseudocode from hivemind-core (not in this package)
self.agent_protocol.hm_protocol = self
self.binary_protocol.hm_protocol = self
This is why the protocols' _SubProtocol property helpers always guard with
if not self.hm_protocol. Plugin code that calls self.database or self.clients during
__post_init__ will get None / {} - this is expected. Access them in handler methods
that are called after construction.
The comment in the source is explicit:
# usually AgentProtocol is passed as kwarg to hm_protocol
# and only then assigned in hm_protocol.__post_init__
Source: hivemind_plugin_manager/protocols.py:65
delete_item Tombstone and client_id Reuse
AbstractDB.delete_item replaces a client's api_key with "revoked" rather than
removing the row:
client = Client(client_id=client.client_id, api_key="revoked")
return self.update_item(client)
Source: hivemind_plugin_manager/database.py:192
The comment in the source explains the intent:
# leave the deleted entry in db, do not allow reuse of client_id !
When writing a database plugin you must honour this: your add_item must handle an
upsert (update-on-same-client_id) rather than always inserting. The test suite
verifies this behaviour at tests/test_database.py:185.
Version Compatibility
HPM declares no version constraints on its plugin implementations. The entry-point contract
is purely structural: a class that subclasses the right abstract base and implements the
required methods is a valid plugin. HPM does not check class hierarchies at discovery
time - it only calls entry_point.load(). Type-safety is the plugin author's
responsibility.
Discovery goes through the stdlib importlib.metadata.entry_points(group=...)
directly. There is no pkg_resources fallback and no _iter_entrypoints helper.
allowed_types is deny-by-default
allowed_types is the canonical admission whitelist, enforced by
MessageTypeACLPolicy in hivemind-core. An empty list denies every message.
Client.__post_init__ does type validation only. It does not substitute a
default set, and it does not append recognizer_loop:utterance to a custom
list. Grant access explicitly:
hivemind-core allow-msg recognizer_loop:utterance <node_id>
or pass allowed_types=[...] when constructing the Client.
⚠️ Earlier versions of this page documented an automatic default set of eight message types and an unconditional
recognizer_loop:utteranceappend. Neither ever survived HiveMind-core#85, and relying on that behaviour leaves a client that is denied on every message. The whitelist is deny-by-default with no implicit grants.