feat(pro): generalize pro tier to multi-surface extension points

Move the pro mount decnet/services/pro/ -> decnet/pro/ so the Professional tier
can contribute to more than honeypots. The core wires each surface only when
decnet/pro/ is present (absence stays the entitlement gate):

* services  — registry scans decnet/pro/services/ (was decnet/services/pro/)
* API routes — decnet/pro/routes.py exposes ROUTERS, mounted under /api/v1
* web pages  — Vite aliases @pro to the pro frontend (community -> empty stub),
               App.tsx maps proRoutes into <Route>s, Layout renders a
               PROFESSIONAL nav group; both tree-shake out of the community build

Frontend gate mirrors the existing VITE_DECNET_DEVELOPER tree-shake pattern.
Tests: registry + router seams (backend), empty-stub contract (frontend).
This commit is contained in:
2026-06-17 15:02:28 -04:00
parent 80c92a6f80
commit a47f99c449
13 changed files with 151 additions and 57 deletions

View File

@@ -6,11 +6,11 @@ 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.
Professional-tier honeypots live in the optional ``decnet.pro.services``
subpackage (a clone of the private decnet-professional repo, mounted at
``decnet/pro/`` and 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
@@ -40,12 +40,13 @@ def _load_plugins() -> None:
continue
importlib.import_module(f"decnet.services.{module_info.name}")
# Professional build only: present == entitled. Community build has no pro/.
pro_dir = package_dir / "pro"
pro_dir = package_dir.parent / "pro" / "services"
if pro_dir.is_dir():
for mi in pkgutil.iter_modules([str(pro_dir)]):
importlib.import_module(f"decnet.services.pro.{mi.name}")
importlib.import_module(f"decnet.pro.services.{mi.name}")
for cls in _all_subclasses(BaseService):
if not cls.__module__.startswith("decnet.services."):
mod = cls.__module__
if not (mod.startswith("decnet.services.") or mod.startswith("decnet.pro.")):
continue
instance = cls() # type: ignore[abstract]
_registry[instance.name] = instance