--- name: discover_local_services kind: function lang: py domain: infra version: "1.1.0" purity: impure signature: "discover_local_services(manifest_path: str, include_registry: bool = True) -> list[dict]" description: "Descubre los servicios locales del sistema local_hub expuestos como subdominios *.localhost. Lee el manifiesto YAML, normaliza la metadata de cada servicio, opcionalmente añade los servicios del registry con puerto via fn doctor, y comprueba up/down por chequeo de puerto TCP en 127.0.0.1. Robusta: no lanza por servicio caido (up=False) ni por fallo de fn doctor." tags: [local-hub, infra, services, discovery, caddy, glance, python] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [json, os, socket, subprocess, sys, yaml] params: - name: manifest_path desc: "ruta al manifiesto YAML de servicios (apps/local_hub/local_services.yaml) con claves dashboard_subdomain, glance_port y services[]" - name: include_registry desc: "si True, añade los servicios del registry con port>0 que no esten ya en el manifiesto (dedup por port y por subdomain), obtenidos de fn doctor services-spec --json" output: "lista de dicts normalizados, cada uno con las claves name, subdomain, port, health_path, title, icon, category, rewrite_host (bool, passthrough del manifiesto; False para servicios del registry; lo consume render_caddyfile para reescribir el header Host), up (bool de estado vivo por puerto TCP)" tested: true tests: - "test_golden_service_up_with_all_keys" - "test_edge_closed_port_is_down" - "test_defaults_derived_for_missing_fields" - "test_empty_manifest_returns_empty_list" - "test_rewrite_host_passthrough_desde_manifiesto" test_file_path: "python/functions/infra/discover_local_services_test.py" file_path: "python/functions/infra/discover_local_services.py" --- ## Ejemplo ```python from discover_local_services import discover_local_services # Solo manifiesto (sin tocar el registry): servicios = discover_local_services( "apps/local_hub/local_services.yaml", include_registry=False, ) for s in servicios: estado = "UP" if s["up"] else "DOWN" print(f'{s["title"]:<16} {s["subdomain"]}.localhost -> :{s["port"]} [{estado}]') # Manifiesto + servicios del registry con puerto: todos = discover_local_services("apps/local_hub/local_services.yaml") print(len([s for s in todos if s["up"]]), "servicios vivos") ``` Como script (imprime JSON a stdout): ```bash python/.venv/bin/python3 python/functions/infra/discover_local_services.py apps/local_hub/local_services.yaml ``` ## Cuando usarla Úsala como fase de descubrimiento del sistema `local_hub` antes de renderizar el Caddyfile o la config de Glance: cuando necesites la lista normalizada de servicios locales (`*.localhost`) con su estado up/down resuelto. También cuando quieras un inventario unificado de servicios manuales (contenedores, daemons de terceros) más los servicios del registry con puerto, deduplicados. ## Gotchas - `up` se decide por **conexión TCP** a `127.0.0.1:` con timeout 0.5s, NO por GET HTTP. Un servicio puede aceptar la conexión y devolver 404/500 en `/` y aun así marcar `up=True`. Es intencional: solo valida que el puerto esté escuchando. - Solo comprueba `127.0.0.1` (loopback). Servicios que bindean únicamente a otra interfaz se reportan como `down`. - `include_registry=True` ejecuta `fn doctor services-spec --json` (fallback a `services --json`) como subproceso desde la raíz del repo. Si `fn` no está, falla, tarda más de 20s o devuelve JSON inválido, la función **no lanza**: sigue solo con el manifiesto. Por eso el resultado puede variar según el entorno. - La raíz del repo se resuelve por `FN_REGISTRY_ROOT` o subiendo directorios hasta encontrar `registry.db`. Si no la encuentra, usa el cwd. - El dedup del registry es por `port` Y por `subdomain`: un servicio del registry cuyo puerto o subdominio derivado ya esté en el manifiesto se omite. - El subdominio de un servicio del registry se deriva por una tabla de alias (`dag_engine`->`dag`, `registry_api`->`registry`, `sqlite_api`->`sqlite`, `osint_db`->`osint`, ...) y, para el resto, el primer token antes de `_`. - Lanza `RuntimeError` solo si el manifiesto no se puede leer o parsear (path inexistente, YAML inválido). Eso sí es un error duro. - La clave `rewrite_host` es passthrough del manifiesto (default `False`); para los servicios añadidos desde el registry siempre es `False`. La consume `render_caddyfile` para emitir `header_up Host` en el bloque del servicio. ## Capability growth log - v1.1.0 (2026-06-20) — añade clave rewrite_host (passthrough del manifiesto) para que render_caddyfile reescriba el Host