feat(services): open-core community/professional tier split
Pro-tier honeypots load from an optional decnet/services/pro/ subpackage that the registry auto-discovers when present; the Community build omits it, so the directory's absence IS the entitlement gate (no runtime licence check). Recurse subclasses so a pro service may extend a community one. Exclude pro from the community wheel and git-ignore the path (it lives in the private decnet-professional repo). Add LICENSING.md documenting the dual-license: AGPL-3.0-or-later core plus a commercial EULA for the Professional tier.
This commit is contained in:
@@ -5,6 +5,12 @@ Service plugin registry.
|
||||
Auto-discovers all BaseService subclasses by importing every module in the
|
||||
services package. Adding a new service requires nothing beyond dropping a
|
||||
new .py file here that subclasses BaseService.
|
||||
|
||||
Professional-tier honeypots live in the optional ``decnet.services.pro``
|
||||
subpackage, which ships only in the Professional build (a private tree merged
|
||||
in at packaging time) and is absent from the open-core Community build. The
|
||||
registry scans it when present, so absence of the directory IS the entitlement
|
||||
gate — no licence check, no feature flag.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
@@ -17,6 +23,13 @@ _registry: dict[str, BaseService] = {}
|
||||
_loaded = False
|
||||
|
||||
|
||||
def _all_subclasses(cls: type) -> set[type]:
|
||||
# Recurse: a pro honeypot may extend a community service, not BaseService
|
||||
# directly, and __subclasses__() only returns direct children.
|
||||
subs = set(cls.__subclasses__())
|
||||
return subs.union(*(_all_subclasses(s) for s in subs))
|
||||
|
||||
|
||||
def _load_plugins() -> None:
|
||||
global _loaded
|
||||
if _loaded:
|
||||
@@ -26,7 +39,12 @@ def _load_plugins() -> None:
|
||||
if module_info.name in ("base", "registry"):
|
||||
continue
|
||||
importlib.import_module(f"decnet.services.{module_info.name}")
|
||||
for cls in BaseService.__subclasses__():
|
||||
# Professional build only: present == entitled. Community build has no pro/.
|
||||
pro_dir = package_dir / "pro"
|
||||
if pro_dir.is_dir():
|
||||
for mi in pkgutil.iter_modules([str(pro_dir)]):
|
||||
importlib.import_module(f"decnet.services.pro.{mi.name}")
|
||||
for cls in _all_subclasses(BaseService):
|
||||
if not cls.__module__.startswith("decnet.services."):
|
||||
continue
|
||||
instance = cls() # type: ignore[abstract]
|
||||
|
||||
Reference in New Issue
Block a user