Saltar a contenido

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:

  1. Instalable sin el resto del monorepo (wheel + entry point cortex.plugins)
  2. Descriptor SPI declara requires, optional_requires, provides y stateless
  3. API bajo /api/v1/{namespace} con guard require_plugin_enabled
  4. Bootstrap UI solo si el plugin está enabled (no solo instalado)
  5. Tests de aislamiento: plugin + framework stub, sin depender de otros dominios
  6. Persistencia: Alembic propio con prefijo {plugin_id}_* o stateless: true explí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:

  1. discover_plugins() devuelve todos los plugins instalados.
  2. resolve_enabled_plugins() lee el state store (/control/plugins) y builtins (template, control embebido).
  3. _register_plugin_hooks y carga de instancias API solo para enabled (plugins de dominio).
  4. Entry point cortex.panels: load_panel_hosts() + register_resources en boot para paneles dedicados; activate_pending_plugins() repite register_resources / register_forms si el panel se activa en caliente.
  5. PluginCatalog expone 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):

  1. clients → clients_*
  2. resources → resources_*
  3. booking → conectar Alembic existente
  4. 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 .env de 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 en products/unosportclub/tests (ADR 025).
  • Smoke pip: venv limpio sin workspace plugins/ (PLUGINS_ROOT vací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