---
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:<port>` 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
