Skip to content

Contratos: cobertura y rechazo de informes parciales — noche del 19 octubre 2026

Investigación inicialmente de solo lectura; el coordinador autorizó después un fix acotado y, tras revisión, su despliegue selectivo. Aplicado sin commit/push. No se ejecutaron fleet, check/check-http, endpoints de productos, sync/watch/update, sesiones Sablier, notificaciones ni trabajos funcionales. No se auditaron sistemas de clientes ni Carmelo. Las pruebas nuevas usan payloads sintéticos y comandos/escrituras mock. La validación posterior sí ejecutó el inspector real de las7rutas por su clave y comando forzado de solo lectura, sin peticiones a apps.

Lo comprobado y sus límites

Fuente y runtime inicial coinciden en VM208 para service_check.py, check-services.sh, check_routes.py y los11manifiestos. CT218 tiene el mismo service_check.py y contrato ABS; wrapper de comandos forzados coincide con fuente. CT212 tiene el inspector y allowlist esperado de7hostnames. Lectura de CT218 mediante pmx2/pct con confianza existente: el acceso directo desde Mac no tenía host trust disponible, no se cambió ninguna clave para superarlo.

Informe guardado latest.json observado 2026-10-19T21:23:03.750750+00:00, unos 28min de antigüedad al iniciar la auditoría: 11contratos,251checks,0fallos, 29sin verificar. No se presenta esa lectura como ejecución nueva del checker. Informe routes.json 2026-10-19T21:36:21.213186Z:7rutas,exit0.

Inspección Docker seleccionada posterior: los10servicios locales incluidos están running/healthy con RestartCount0, salvo Karakeep created, correcto para su contrato on-demand. ABS en CT218 running/healthy,RestartCount0. Las copias históricas detenidas de ABS/Navidrome/Grafana/ntfy en VM208 no equivalen a caída del propietario migrado. Otros productos dormidos sin contrato no se han probado ni despertado; se separan de los fallos, no se declaran saludables.

Los29pendientes, sin convertirlos en29averías

Categoría Número Detalle
Flujo funcional 11 Recibos de septiembre fuera de su ventana30d; no se han renovado sin prueba
Acceso/login 11 5recibos vencidos y6pendientes explícitos; health/HTTP no sustituyen login
Política de imagen 5 ABS,Karakeep,Prowlarr,Radarr,Sonarr con referencia mutable según checker
Recuperación 2 Plundrio y plundrio-onepacerr sin ensayo completo acreditado

Distribución por producto: ABS3,Immich2,Karakeep3,hunter2,onepacerr3,scanner2, Plundrio3,Prowlarr3,Radarr3,Sonarr3,Japón2. El salto anterior26→29 ya documentado fue recuperar la lectura de ABS, no añadir tres averías.

Además hay9recibos recovery «recorded». No son nueve restauraciones completas: 7declaran explícitamente dump/SQLite/integridad/copias con restore completo sin probar (Immich,Karakeep,hunter,scanner y los tres ARR). ABS cubre restauración nativa sin medios NAS; Japón cubre datos/documentos/fotos, con firma/cloud fuera. El checker distingue histórico mediante scope, pero29no enumera todas las limitaciones de recuperación que esos scopes reconocen.

Los checks de backup/conversor existentes pasan en el informe guardado: ABS native_backup y dos assertions del conversor; copias nativas semanales Prowlarr/Radarr/Sonarr. Comprobar mtime/JSON no prueba restauración integral.

Cobertura ausente

El fleet no pretende cubrir todo el homelab:10productos en VM208 y uno en CT218. La ejecución multihost admite explícitamente solo ABS/personal-218; no hay contratos de servicio para CT214/215/216/217 ni los otros productos de CT218. En VM208 están sin contrato, por ejemplo, Paperless,AudioMuse,Soularr,TRMNL, Jellyseerr,Lidarr,Bazarr y Mylar3. Esto es ausencia de este monitor; pueden tener otras comprobaciones, que no se han descartado por no estar en el fleet.

Rutas: solo podcasts,pics,bookmarks,prowlarr,radarr,sonarr,japon.monxas.casa. Las7 tienen desired/tunnel/ambosCaddy/DNS pass en el último informe. Access fue intencionadamente no solicitado y auth privada sigue sin probar. exit0 no acredita permisos de Cloudflare ni login. No se infiere disponibilidad del resto de rutas a partir de esas siete.

Otro hueco: fleet descubre los contratos presentes mediante glob. Si desaparece uno pero quedan otros, el informe pierde esa cobertura sin un inventario esperado independiente. Propuesta posterior: manifestar el conjunto aceptado de nombres y reportar removed/unregistered por separado, sin descubrir ni despertar productos. Prioridad de extensión: Paperless, dado el valor de sus datos y restauración parcial ya trabajada; resolver antes su fuente/backup/propietario actual.

Falso OK confirmado y reparación local

Antes del cambio, controles negativos en memoria:

  • route_check.exit_code({results:[]}) devolvía0.
  • Un payload con observed_at válido y los7hostnames, sin un solo check de rutas, atravesaba check_routes.main y producía exit0/report.exit_code0.
  • remote_audiobookshelf aceptaba un único runtime.state/pass: omitía los otros 26checks del propietario sin declarar cobertura perdida.

No se ha encontrado ese truncamiento en los informes reales: estaban completos. La debilidad era aceptar futuros errores de formato como éxito.

Corrección aplicada:

  1. route_check.validate_report valida timestamp con timezone, lista no vacía, hostnames únicos, campos/estados admitidos y cobertura completa cuando desired pasa. Exige ambosCaddy distintos, datos de upstream/lazy y coherencia cuando un nodo declara pass. Tunnel pass debe contener el ingress esperado completo; DNS pass exige sus campos/conteos; Access pass exige conteos y expectativa coherentes. Un desired missing/fail conserva el cortocircuito legítimo y exit1.
  2. check_routes valida la estructura y el conjunto exacto de7hostnames antes de guardar. Payload truncado/duplicado/inválido produce salida saneada exit2.
  3. exit_code rechaza informes vacíos/malformed con2. La excepción not_requested solo permite omitir Access; desired no admite notchecked y un DNS/tunnel/ nodo Caddy no comprobado nunca se convierte en0 por copiar ese reason.
  4. RemoteABS exige conjuntos de checks derivados del manifiesto del caller, nombres únicos, estados válidos para cada categoría y contadores coherentes. Health obligatorio cuando el ABS always declara runtime.state/pass. Mantiene collector.remote/fail y el estado sin health de un propietario realmente detenido como fallos, sin inventar error de reposo. Las dos assertions del conversor se siguen añadiendo en VM208; no se trasladan al propietario.

El esquema valida formato/timezone del timestamp, no su frescura. No se introduce un umbral nuevo para el informe diario de rutas. Los límites existentes se conservan: status del fleet marca informes >90min como obsoletos, y append_artifacts exige los max_age_hours declarados para backups y resultados JSON. Un informe de rutas antiguo pero estructuralmente válido requiere evaluar su fecha por separado; esta reparación no elimina ese límite de cobertura.

DNS mantiene la semántica existente de TXT: sus contadores exact/wildcard son registros brutos, no solo A/AAAA/CNAME. Un TXT exacto puede coexistir con un CNAME wildcard correcto: modewildcard exige wildcard>0, sin exigir exact==0. Modeexact sí exige exact>0. No se convierte este límite de esquema en una nueva verificación de registros IP; el selector DNS original ya decide qué usar.

Pruebas y compatibilidad

72pruebas pasan:48existentes+24nuevas. La fixture antigua del test de SSH fijo se completa para seguir probando el mismo objetivo bajo el esquema nuevo. Controles nuevos: JSON truncado, campos omitidos, duplicados de host/nodo/check, estados/tipos/counters inválidos, no confundir opt-out Access con pérdida de DNS/tunnel, checks ABS incompletos y preservación de errores reales de Caddy (inactivo, upstream ausente/equivocado, lazy incorrecto, adminAPI/SSH inaccesible). También conserva reposo esperado y opt-out HTTP como unverified.

Validación local contra informes reales guardados, sin llamar al endpoint: las7rutas pasan el esquema final y siguen exit0. Los27checks owner de ABS (extraídos de los29del fleet excluyendo solo las2assertions del conversor) pasan el esquema con0fallos/3pendientes. No se han alterado los29pendientes ni las caducidades. py_compile y git diff --check correctos.

No se cambian contratos JSON, wrappers/comandos forzados, claves, scheduler, calendarios, constantes de pin de controller, ni los propios controllers.

Despliegue validado y recuperación

Archivo SHA256 instalado, idéntico a fuente
service_check.py <REDACTED>
route_check.py <REDACTED>
check_routes.py <REDACTED>

Runtime previo service_check en208/218: <REDACTED>; route_check CT212: <REDACTED>; check_routes VM208: <REDACTED>.

Se sustituyeron únicamente los3scripts de VM208, route_check del CT212 y service_check del CT218. Se verificaron hashes previos exactos, AST de cada candidato, dueños/modos originales y paridad posterior. Reemplazos por rename atómico; VM208 bajo ambos locks existentes check.lock y routes.lock. No se reinició ni reinstaló ningún servicio.

Backups en directorio0700, cada archivo0600 y hash igual al baseline anterior:

  • VM208: /home/monxas/scripts/service-onboarding/rollback-schema-20261019T215820Z/ con los3scripts. Runtime conserva monxas:monxas; service_check/check_routes0644, route_check0775, exactamente los modos anteriores.
  • CT212: /opt/service-onboarding/rollback-schema-20261019T215820Z/route_check.py, runtime root:root0644.
  • CT218: /opt/service-onboarding/rollback-schema-20261019T215820Z/service_check.py, runtime root:root0644.

Hashes de wrappers permanecen intactos: CT212remote-routes <REDACTED> y CT218remote-contract <REDACTED>. El controller central conserva <REDACTED>; sus dos pins, las claves y los comandos forzados no se modificaron.

Validación instalada sin HTTP a productos: AST correcto en las cinco copias; lectura de informes reales guardados sigue7rutas/exit0 y27checks ABS owner con 0fallos/3pendientes. Controles negativos en VM208 instalado: informe vacío→exit2, owner parcial rechazado. latest.json permanece observado21:23Z,0fallos/29pendientes; no se ejecutó el colector funcional ni se renovaron recibos.

Inspector real por la identidad dedicada VM208→CT212, comando literal routes: 21:59:32Z, siete hostnames exactos, exit0. Todos desired/tunnel/DNS y ambos Caddy pass; Access notchecked intencional. Sin sync, cambios de ruta, escrituras de caché/controller ni solicitudes a productos/Sablier. Se validó la respuesta en memoria sin sobrescribir routes.json. El límite de auth sigue vigente.

Rollback: adquirir ambos locks existentes en VM208, comprobar que el runtime sigue en los hashes instalados y que los backups coinciden con los baselines registrados; copiar cada backup a un temporal del directorio destino, recuperar su dueño/modo runtime indicado arriba y renombrar atómicamente. En CT212/218, restaurar solo el script correspondiente, manteniendo root:root0644. No volver claves, wrappers, contratos, reportes, estado ni controllers. La vuelta atrás reabre la aceptación de reportes incompletos: conservar este diagnóstico. La siguiente ejecución natural del fleet confirmará colección completa con el nuevo esquema, sin necesidad de una ejecución HTTP manual.

Revisión de fuente: OOM no equivale a servicio dormido

El 20 de octubre se reprodujo con fixtures un falso OK: un contenedor on-demand con Status=exited, OOMKilled=true y código 137 se clasificaba como sueño esperado. No es evidencia de un OOM real en Karakeep ni en otro servicio. El candidato hace fallar runtime.state o dependency.* cuando Docker registra OOM; el campo ausente, incompleto o no booleano tampoco acredita un estado correcto. No deduce OOM del código de salida: 137/143 pueden proceder de una parada intencional.

Los servicios dormidos siguen omitiendo HTTP y docker exec, incluso tras OOM; los estados OOM/desconocidos que no son sueño esperado tampoco reciben probes. No se añaden checks ni cambian el esquema de informe o sus contadores. Seis regresiones nuevas, incluidas las variantes de datos inválidos y dependencias, completan 90 tests locales correctos. Esta validación es sólo de fuente: no instala el collector, cambia contratos, prueba productos o reinterpreta las observaciones runtime anteriores.

Guarda OOM instalada el20oct13:36 CEST

Las90pruebas y la CI Linux de contratos pasan. Se instalaron ambos helpers con SHA256 <REDACTED>:

Destino Instalación CEST Propietario y modo Respaldo privado
VM208 /home/monxas/scripts/service-onboarding/service_check.py 13:36:54 1000:1000,0644 /root/service-contract-oom-20261020-8_f_uk5t
CT218 /opt/service-onboarding/service_check.py 13:36:55 root:root,0644 /root/service-contract-oom-20261020-2bmwqmbh

VM208 partía de 2367836b…; CT218 de d3589fe2…. Por ello CT218 recibe además la validación de informes guardados de fuente ya existente (validate_saved_report y rama CLI status); no cambia la API check/manifest que consume su wrapper. El propietario CT218 se revalidó en marcha en proxmox2. Los reemplazos se compilaron nativamente y preservaron permisos, con recibos de instalación.

La lectura independiente13:37:57–59 confirma hash e identidad exactos contra ambos recibos, con27217bytes y un enlace. Los18archivos protegidos de VM208, los dos de CT218 y ambos locks de VM208 conservan identidad y contenido de preflight. Backups0700, before.py0600 y recibos privados están presentes con hash previo correcto. No se ejecutaron helper, check, status, fleet, imports ni HTTP a productos; no se renovaron sus informes. Es verificación de instalación, no prueba funcional de aplicaciones ni evidencia de OOM ocurrido en producción.

Recuperar sólo el helper afectado, verificando primero su hash instalado y el backup, conservando propietario/modo y los locks existentes. No restaurar wrappers, contratos, informes o controllers. La versión previa puede volver a clasificar como sueño esperado un contenedor con OOM.

Contador de reinicios: corrección y despliegue posterior

Una fixture adicional reprodujo que RestartCount ausente se convertía en0 y el resumen del servicio podía declarar cero fallos; booleanos y negativos también podían pasar. La fuente exige ahora un entero no negativo observado, y valida la política max_restart_count antes de ejecutar comandos. Dato ausente o inválido produce fallo explícito, sin deducir un contador nuevo. Los límites legítimos, incluido0, mantienen su comportamiento.

Cuatro regresiones nuevas, incluida la comprobación del resumen completo y el rechazo de política antes de comandos, completan94pruebas de la suite. En la revisión inicial esta corrección aún no estaba instalada en los helpers 2903a01f; la instalación posterior se acredita a continuación. No se consultaron productos ni se reinterpretaron sus informes anteriores.

Instalación posterior del contador,20oct13:48 CEST

La corrección anterior pasó CI Linux Python3.10/3.12 en main77ee263, publicada junto con documentación ca5f127. Se comprobó antes de instalar que los11contratos de VM208 y el de CT218 omiten max_restart_count y por tanto usan el default3 válido. No se publicaron contenidos de contratos. Los hashes previos eran2903a01f en ambos hosts; el reemplazo preserva los locks existentes, dueños y permisos.

Destino Instalación CEST Respaldo privado del helper2903a01f
VM208 13:48:03 /root/service-contract-restart-20261020-lti7a02v
CT218 13:48:05 /root/service-contract-restart-20261020-6beyz0rq

Hash instalado en ambos: <REDACTED>. Lecturas independientes13:48:30–32 confirman identidad exacta contra recibos, 27565bytes y un enlace: VM2081000:1000/0644, CT218root:root0644. Los20archivos protegidos y ambos locks conservan identidad y contenido; backups0700 con originales0600 de hash2903a01f y recibos presentes. Compilación nativa correcta en la fase de instalación. No se ejecutaron collectors ni HTTP a productos.

Para recuperación, aplicar las mismas guardas anteriores, usando estos backups si se quiere volver a la versión OOM sin la validación de contador. No confundir ese estado con los baselines anteriores al despliegue OOM.

Primer informe natural posterior:20oct14:23 CEST

Lectura del informe existente validado a las14:46: informe14:23:03,970 CEST, posterior a las instalaciones13:36/13:48 y no obsoleto al leerlo. Conserva 11contratos,2fallos y29comprobaciones no verificadas. Los fallos siguen siendo compose.environment de Hunter y Scanner, limitados a la clave N8N_API_KEY; no se muestran valores ni se relaja la detección de deriva.

Estado runtime:11PASS (10running y Karakeep sleeping_expected); contador de reinicios11PASS, todos0 observados como enteros no negativos; salud10PASS, sin despertar Karakeep. Las29pendientes se reparten en5imagepinpolicy, 11funcionales,11acceso y2recuperación. No se convierte esa cobertura parcial en salud funcional de todo el homelab.

El informe no expone OOMKilled explícito ni una atestación del hash de su productor; no prueba por sí mismo la guarda OOM ni su rama negativa. Las pruebas sintéticas y comprobación de instalación conservan su evidencia separada. No se ejecutó el colector, probes, Docker de productos ni escrituras remotas. SHA del informe validado8b2798a4; proyección privada SHA256 <REDACTED>; comparación con13:23 <REDACTED>.

Diagnóstico acotado de Hunter y Scanner, 20 de octubre a las 18:27 CEST

La lectura del informe existente, observado a las 18:23:03 CEST, conserva un fallo compose.environment por producto, limitado a N8N_API_KEY. Esto no es una prueba de autenticación fallida ni de credencial ausente: el checker compara el entorno declarado por Compose con el del contenedor, sin consultar N8N.

La lectura selectiva de las 18:27:25 CEST confirma que ambos valores están presentes y no vacíos, pero difieren, en Hunter y en Scanner. La comparación se hizo exclusivamente en RAM; no se guardaron valores, entornos completos ni configuraciones resueltas. Los contratos y los labels seleccionados coinciden en archivo y servicio: /home/monxas/stacks/media/compose.yaml. VM208 estaba running en proxmox, propietario revalidado a las 18:26:44 CEST. No se consultaron productos, N8N ni el colector.

Se trata de deriva real, no de un fallo de mapeo del checker. La fuente Stacks b1ae78c ya declara N8N_API_KEY= explícitamente para ambos servicios, pero esa definición todavía no coincide con el Compose desplegado, de hash <REDACTED>. Los archivos fuente inspeccionados de Hunter y Scanner no contienen referencias a N8N; sus credenciales de integración son las de Sonarr, Radarr y Put.io. Esto no acredita la versión del código dentro de las imágenes ni la validez de ninguna credencial.

No procede ocultar la diferencia ni generar credenciales. Quitar la declaración vacía permitiría heredar de nuevo la variable; se conserva la máscara publicada y queda pendiente la convergencia runtime bajo la revisión de ciclo de vida y el hold existentes. Esta investigación no autoriza ni ejecuta recreaciones.

Evidencia privada minimizada, con directorio 0700 y archivos 0600: ~/.local/state/hunter-scanner-contract-state-20261020-1rk8bpjl/. Proyección SHA256 <REDACTED>; resumen <REDACTED>.