ADR 023: Contrato de runtime para plugins instalables¶
Estado¶
Aceptado — 2026-07
Relacionado con ADR 002, ADR 017 y ADR 004.
Contexto¶
El monorepo HIVE desarrolló plugins first-party como módulos siempre presentes: discovery carga el catálogo completo, el bootstrap registra hooks de UI para plugins instalados aunque estén desactivados, y el bundle por defecto activa casi todos los dominios. La intención original era plugins opcionales e instalables por pip, integrados solo por contratos (REST, eventos, SPI).
Esta ADR fija vocabulario, criterios de “plugin real” y comportamiento del runtime para Camino B: distribución pip (índice privado primero), activación condicional y degradación graceful entre dominios.
Vocabulario¶
| Término | Definición |
|---|---|
| Plugin | Paquete pip instalable con entry point cortex.plugins, activable vía /control/plugins (state store PG) |
| Namespace de panel | panel_id de composición UI (operations, admin, …); generativo o dedicado vía cortex.panels opcional |
Module (moduleId) | Entrada de navegación UI dentro de un panel (ej. booking, clients) |
| Installed | Descubierto por entry points, workspace o sideload — presente en el entorno Python |
| Enabled | Subconjunto activo en runtime — expone API, módulos de panel y widgets |
| Bundle profile | Extra pip de un paquete de producto (ej. cortex-product-unosportclub[operations-core]) — no implica activación. Ver ADR 025. |
| Capability | Servicio de dominio declarado en PluginDescriptor.provides (ej. clients, payments) |
No renombrar el directorio plugins/ a modules/ — colisiona con moduleId de la UI declarativa.
Criterios de plugin real (checklist obligatoria)¶
Todo plugin nuevo o elevado de scaffold debe cumplir:
- Instalable sin el resto del monorepo (wheel + entry point
cortex.plugins) - Descriptor SPI declara
requires,optional_requires,providesystateless - API bajo
/api/v1/{namespace}con guardrequire_plugin_enabled - Bootstrap UI solo si el plugin está enabled (no solo instalado)
- Tests de aislamiento: plugin + framework stub, sin depender de otros dominios
- Persistencia: Alembic propio con prefijo
{plugin_id}_*ostateless: trueexplícito
Contrato SPI (PluginDescriptor)¶
@dataclass(frozen=True)
class PluginDescriptor:
plugin_id: str
display_name: str
version: str
requires: tuple[str, ...] = ()
optional_requires: tuple[str, ...] = ()
provides: tuple[str, ...] = ()
stateless: bool = False
| Campo | Semántica |
|---|---|
requires | IDs de plugins hard — boot falla en modo strict si faltan o están disabled |
optional_requires | Capabilities opcionales — composición UI/API degrada si faltan |
provides | Capabilities exportadas — indexadas en GET /api/v1/control/capabilities |
stateless | Sin tablas PG propias — válido solo para plugins sin persistencia de dominio |
Runtime: installed vs enabled¶
flowchart LR
Pip[pip install] --> Installed[Installed catalog]
Store[PluginStateStore PG] --> Enabled[Enabled subset]
Installed --> Discovery[discover_plugins]
Discovery --> Catalog[Plugin catalog API]
Enabled --> Hooks[Register hooks UI/API]
Enabled --> Guard[require_plugin_enabled]
Enabled --> Capabilities[Capabilities map] Reglas:
discover_plugins()devuelve todos los plugins instalados.resolve_enabled_plugins()lee el state store (/control/plugins) y builtins (template,controlembebido)._register_plugin_hooksy carga de instancias API solo para enabled (plugins de dominio).- Entry point
cortex.panels:load_panel_hosts()+register_resourcesen boot para paneles dedicados;activate_pending_plugins()repiteregister_resources/register_formssi el panel se activa en caliente. PluginCatalogexpone installed/enabled; desactivar un plugin oculta módulos sin reinicio (requiresRestart: false).
En tests: create_test_app(..., enabled_plugins="...") simula el state store en memoria.
Validación de dependencias¶
Variable PLUGIN_DEPS:
| Modo | Comportamiento |
|---|---|
strict (default prod) | Boot aborta con error claro si requires no satisfecho |
warn | Log de advertencia; continúa (dev/sandbox) |
Capabilities y degradación¶
En boot se construye provides → plugin_id desde descriptors de plugins enabled.
Composición cross-plugin (ej. reservation-flow):
- Pasos condicionados por capabilities disponibles
- Si falta
payments: omitir paso de pago o modo “confirmar sin cobro” - URLs de API desde manifest/config del widget, no hardcoded en shell
Bundle profiles (casos de uso)¶
Los extras pip no viven en el meta-paquete cortex. Fuente canónica: paquete cortex-product-unosportclub (ADR 025).
| Extra (producto) | Plugins incluidos (wheels) | Uso |
|---|---|---|
operations-core | booking, clients, resources, payments, pricing | Operación mínima |
operations-full | operations-core + discounts, events | Operación extendida |
admin-finance | clients, resources, payments, pricing, sales, billing | Parametrización finanzas |
sports-club-full | Perfil completo UnoSportClub | Referencia |
pip install "cortex-product-unosportclub[operations-core]" no activa plugins — solo instala wheels. El meta cortex[operations-core] re-exporta alias deprecados. Activación: superadmin en /control/plugins (ver activacion.md).
Widget ownership¶
| Tipo | Dueño |
|---|---|
Core (form, data-table, calendar, …) | framework / panel-shadcn |
Dominio (reservation-flow, client-picker, …) | plugin que declara register_widget_types |
API: GET /api/v1/ui/widget-types — tipos disponibles según plugins enabled.
Persistencia (gate Camino B)¶
Plugin sin PostgreSQL propio solo es válido con stateless: true. Orden de migración (ADR 017):
- clients →
clients_* - resources →
resources_* - booking → conectar Alembic existente
- payments →
payments_*
Backend de almacenamiento: PLUGIN_STORAGE=memory|postgres (default memory en tests; postgres en despliegue).
Autenticación mínima¶
JWT en middleware (AUTH_MODE=optional|required), scopes por panel, route guard en shell React. Sin auth, “plugin desactivado” no tiene sentido de seguridad multi-tenant.
Consecuencias¶
- Scaffolds GET-only sin checklist quedan deprecados — usar
plugins/_template/ - Default
.envde producción debe usar bundle mínimo (operations-core), no 9+ plugins - ADR 002 pasa a Aceptado parcial tras Fase 1 pip
- CI plataforma:
pytest -m "not product_composition"; grafo de producto enproducts/unosportclub/tests(ADR 025). - Smoke pip: venv limpio sin workspace
plugins/(PLUGINS_ROOTvacío).
Referencias¶
- Repositorios Git por plugin:
docs/guias/plugins/repositorios.md - Guía pip privado:
docs/guias/plugins/instalacion-pip.md - Roadmap:
reports/PLUGIN-ROADMAP-2026-07-17.md - Template autor:
plugins/_template/README.md