Skip to content

Receiver NAS: exclusión por generación — 20 octubre 2026

Estado actual: 6dcc7cca desplegado a las 00:48:34, con exclusión por generación y privacidad específica de dumps. El despliegue anterior a28c832 se conserva como fase histórica. Publicación y despliegue coordinados por root; la preparación de fuente no ejecutó copias de producción.

Problema y alcance

Tras el reboot de VM208 sobrevivió un receiver NAS de la generación pics iniciada el 19 y llegó a coexistir con otro receiver de esa misma generación. El lock del emisor no evita esa concurrencia remota. El coordinador gestionó por separado la retirada del par antiguo. Cambiar el script no añade un lock a procesos que ya están ejecutándose: antes de instalar y reanudar otra generación hay que acreditar que no queda un writer antiguo sin lock.

El cambio añade flock(LOCK_EX|LOCK_NB) local en el NAS, por pareja grupo/generación, en BASE/.locks/<group>.<stamp>.lock. Directorio 0700 y ficheros 0600, propietario efectivo, tipo regular y un solo enlace. No se borran ni reemplazan los inodos de lock al terminar. El descriptor se conserva explícitamente a través de exec; los procesos que lo heredan comparten la misma descripción abierta.

El writer toma el lock después de validar los argumentos existentes y antes de ejecutar rsync. Otro writer o complete de la misma generación devuelve 75 mientras exista un titular; otro grupo o sello puede avanzar. complete mantiene el lock durante el rename. Después de bloquear, el writer vuelve a comprobar que staging existe y que el destino final no existe: un writer tardío no resucita una generación ya publicada. Sender, status y hash conservan su comportamiento sin lock. No se amplían grupos, flags rsync ni rutas permitidas; no hay cambios de key, sshd, daemon o cron.

Hallazgo adicional: opciones cortas y capacidades rsync

La revisión del coordinador identificó que la clase de caracteres anterior mezclaba flags cortos con el payload de compatibilidad de -e.. Control negativo con os.execv sustituido por captura JSON, BASE privado y ningún rsync ejecutado: -L, -rL, -s y -rs alcanzaban exec con exit0. Esto permitiría pedir copy-links o secluded-args sin pasar por el perfil previsto; no se intentó explotar contra datos ni leer rutas privadas.

Ahora sólo se aceptan flags cortos l/t/r/i/v antes de e.; después de e. se permite la clase de capacidades ya utilizada (iLsfxCIvu). L y s dentro de ese argumento de compatibilidad no son flags copy-links/secluded-args. Los perfiles reales del cliente 3.2.7, writer -ltre.iLsfxCIvu y sender -te.LsfxCIvu, se conservan. Tests negativos cubren los cuatro casos iniciales y mezclas en argumentos de compatibilidad; los positivos llegan al mock exec. La prueba aislada del agente Immich con receiver live anterior sólo validó quoting y tres ficheros sintéticos; no equivale a desplegar este hardening.

Gramática posicional, rutas y publicación inmutable

La revisión independiente maint_finish reprodujo otro bypass: el wrapper aceptaba --server --sender -r . . <target>, y rsync interpreta el punto adicional como otro origen desde su cwd. El nuevo test utiliza únicamente árboles temporales, dos marcadores sintéticos y el rsync real local. Una copia fixture que restaura sólo la gramática anterior descarga outside-marker.txt del cwd además de inside.txt; el candidato rechaza el mismo comando antes de exec y no descarga ninguno. El sender legítimo sí descarga únicamente inside.txt. No se ha probado con datos ni cwd de producción.

Se exige ahora un único argumento . exactamente penúltimo, con el target como único argumento posterior. Todas las opciones se validan antes de ese delimitador; se rechazan también opciones situadas después. El argv enviado a exec usa el realpath validado para target y --link-dest, preservando el slash final del target. Cuando la ruta explícita contiene componentes symlink (abspath != realpath), se rechaza para no cambiar silenciosamente una petición de enlace por una petición de su destino. Esto restringe los operandos explícitos; los descendientes symlink de un árbol continúan sujetos a las opciones rsync autorizadas. La BASE de producción debe seguir siendo canónica, condición que ya necesitaba la comprobación de prefijo existente.

Bajo el lock, complete comprueba lexists antes de rename: POSIX permite reemplazar un directorio destino vacío, pero una generación completada no debe sobrescribirse. El test conserva tanto un directorio completado vacío (incluido inodo) como un symlink destino colgante, y preserva staging. El writer también usa lexists al comprobar el destino final. Un prepare concurrente podría dejar staging adicional, pero el writer y complete lo rechazan si el destino completo ya existe.

Límite explícito: validación de ruta seguida de exec no es un sandbox ni elimina TOCTOU. Otro proceso con permiso de escritura puede cambiar nombres/componentes o descendientes después de validar; normalizar argv reduce la discrepancia entre ruta validada y ejecutada, pero no fija inodos de todo el árbol. No se promete contención frente a un administrador o proceso NAS que modifique concurrentemente esos árboles.

Fuentes y validación

  • Fuente: scripts/original-backups/synology-receiver.py, SHA256 <REDACTED>.
  • Tests nuevos: scripts/original-backups/test_receiver_lock.py, SHA256 <REDACTED>.
  • python3 -m unittest discover -s scripts/original-backups -p 'test_receiver*.py': 25/25 PASS; nueve existentes y dieciséis nuevos.
  • python3 scripts/original-backups/immich-recovery-test-receiver.py: 12/12 PASS, incluye los casos existentes y los de immich-dumps.

Los nuevos casos cubren dos procesos escritores, bloqueo de complete, grupos/sellos independientes, permisos e inodo persistente, sender/hash/status sin bloqueo, symlink/hardlink rechazados y writer tardío. Un ejecutable fixture retiene el FD y hace fork: el hijo mantiene el lock incluso después de salir el padre. Además se usa el rsync real instalado en el Mac, esperando protocolo y haciendo una transferencia sintética de 512 KiB limitada para observar el lock mientras transfiere; contenido final exacto.

El binario local es Openrsync compatible 2.6.9/protocolo29; no es el NAS. Su cliente agrega --dirs junto a -r; sólo el transporte sintético del test elimina ese flag redundante. La whitelist de producción permanece intacta. Python2 no está instalado localmente. No se ha aceptado licencia Xcode, instalado compilador ni compilado otro rsync.

El agente de Immich acreditó por lectura autorizada que /usr/bin/rsync del NAS es 3.0.9, protocolo30. Inspección del main.c oficial v3.0.9: el fork del receiver cierra extremos concretos de pipe y f_out, sin barrido general de FD en ese camino. cleanup.c oficial v3.0.9 sitúa close_all bajo SHUTDOWN_ALL_SOCKETS en cleanup. Es evidencia del upstream; no certifica modificaciones del fabricante.

Fixture NAS aislada

El coordinador autorizó sólo una prueba con copia candidata y BASE temporal privado, vía acceso admin que ya maneja el agente Immich. Harness temporal local /tmp/receiver-lock-nas-fixture.py, diseñado para /usr/bin/python 2.7, sin credenciales embebidas. Reemplaza únicamente el literal BASE en una copia del candidato. Inicia el rsync real NAS sin datos de protocolo, comprueba FD heredado en /proc, segundo writer y complete 75; comprueba sender esperando handshake, hash y status libres; cierra stdin por EOF y comprueba complete y rechazo del writer tardío. Limpia sólo su propio tempfile cuando todos sus procesos han salido. No instala ni invoca el receiver real de producción.

Resultado NAS comunicado por el agente Immich: PASS, ejecutado con Python2.7.18 y rsync3.0.9 sobre el candidato SHA <REDACTED>. FD3 seguía abierto en el proceso rsync tras exec; segundo writer y complete devolvieron75; sender esperó sin adquirir lock; hash/status funcionaron; tras EOF ambos rsync salieron43 (handshake intencionadamente incompleto), después complete funcionó y el writer tardío fue rechazado. Directorio0700/fichero0600. Harness y SSH exit0. BASE /tmp/receiver-lock-fixture-pm73AK eliminado; cero procesos fixture remanentes; production_changed=false. Esto sí ejecuta el candidato en Python2 y acredita startup del binario NAS, sin certificación del fork durante una transferencia NAS completa: para ese punto quedan la prueba genérica de fork, el rsync local y la inspección upstream como evidencias complementarias. No se transfirió contenido de producción ni se modificó el receiver instalado. Esta ejecución NAS corresponde al hash 90bb5de… anterior al endurecimiento posicional/canónico y al guard complete; no certifica ejecución en NAS del candidato final a28c832…. Esos cambios adicionales sí tienen los tests locales anteriores.

Despliegue condicionado y rollback

No desplegar durante una copia o mientras exista un receiver legado. Una vez revisado el diff y autorizado por el coordinador: leer hash del receiver instalado, compararlo con el baseline revisado, guardar copia 0600 en directorio privado 0700, subir candidato como temporal con permisos del original, comprobar hash y sintaxis con el Python del NAS y publicar mediante rename atómico. No cambiar key ni forced command y no iniciar una copia como prueba. Releer hash final. Los locks deben residir en el almacenamiento local del NAS y conservarse: no borrar .locks en instalación, rollback o limpieza automática.

Rollback: restaurar atómicamente la copia exacta del script anterior tras comprobar que no hay writers activos. Retirar el script nuevo no detiene procesos existentes; volver al anterior elimina la protección en nuevos writers, de modo que no se debe reanudar trabajo concurrente hasta resolver la causa. No borrar ficheros lock para intentar desbloquear un writer: el kernel libera el lock cuando sale el último proceso que retiene el descriptor.

Validación NAS final e independiente

El agente Immich ejecutó después el candidato final <REDACTED> en Python2.7.18/rsync3.0.9protocol30: PASS. Cuatro casos de operandos extra y ocho flags peligrosos salieron1; los perfiles reales writer/sender alcanzaron espera de protocolo y EOF43. FD3 persistente, writer/complete75, sender libre, repeatcomplete/latewriter rechazados y hash de completado intacto. BASE temporal eliminado, cero procesos remanentes. Evidencia NAS privada0600/fsync: .ssh/receiver-fixture-a28c832-20261019T223431Z.json. No se instaló producción. Sigue sin acreditarse una transferencia completa con fork del binario NAS.

maint_finish completó revisión independiente:25tests PASS (16lock+9receiver), confirmó gramática, canonicalización y guardlexists, sin otro bypass concreto demostrado; conserva el límite TOCTOU de árboles mutables.

Despliegue autorizado comunicado por coordinación

El agente Immich comunicó instalación atómica autorizada por Root a00:35:13CEST del candidato final <REDACTED>. Preflight: hash anterior5bab exacto y cero writers. Backup NAS privado0600/fsync .ssh/originals-receiver.py.before-a28c832-20261019T223513Z; runtimeUID1026/modo0700 y hash final coincidente. Los cinco grupos existentes devuelven generación19 completada y sin staging. No se alteraron datos ni inodos de lock existentes. Esta tarea no realizó el despliegue; lo ejecutó el agente encargado del NAS con autorización del coordinador.

Preparación del prerrequisito de privacidad, anterior al ensayo NAS

Cambio autorizado por Root, interfaz acordada con el agente publisher. SHA256 candidato receiver <REDACTED>; nuevo test_receiver_privacy.py SHA256 <REDACTED>. No se ha ejecutado NAS, SSH, fixture remota, despliegue ni commit de esta ampliación. Runtime conocido continúa en a28c832.

prepare, writer rsync y complete de immich-dumps fijan umask077. No afecta a otros grupos, sender, hash ni status. No modifica permisos de dumps existentes; el lock conserva su comportamiento privado anterior. Desde una umask heredada022, el writer mock crea fichero0600 en dumps y644 en pics, respectivamente.

Sólo el status de immich-dumps incorpora capabilities:["immich-dump-private-v1"] y private_permissions:{"complete":bool/null,"staging":bool/null}. Ausente devuelve null. Directorio con lstat propio y modo exactamente0700, vacío o con hasta dos hijos inmediatos, devuelve true si todos son regulares, pertenecen al UID efectivo, no tienen bits de grupo/otros ni más de un enlace. Enlaces simbólicos, directorios hijos, hardlinks, propietario distinto, modos públicos o más de dos entradas devuelven false. Sólo se leen metadatos; no se abre contenido ni se devuelven nombres de ficheros. True no implica que existan ambos dump+manifest ni demuestra integridad o restaurabilidad: el publisher debe validar su propio inventario y hashes.

Tests: python3 -m unittest discover -s scripts/original-backups -p 'test_receiver*.py' 33/33 PASS (25invariantes anteriores+8privacidad); python3 scripts/original-backups/immich-recovery-test-receiver.py 12/12 PASS. Validación local Python3 con árboles temporales; fuente escrita con APIs/sintaxis Python2 compatibles, pero este nuevo hash aún no se ha ejecutado en Python2/NAS. La auditoría agregada es una observación de metadatos, no un snapshot atómico frente a modificaciones concurrentes. No se cambian locks, gramática rsync ni publicación inmutable ya desplegados.

Privacidad comprobada y desplegada a las 00:48:34

El fixture final usó un cliente real Mac hacia rsync 3.0.9 del NAS, con payload sintético de tres bytes y UID1026. Origen 0644/umask heredada022 produjo dump0600 y pics0644, con hashes iguales. Auditoría true/false/null, gramática, FD heredado, exclusión75 y generación inmutable: PASS. Los intentos previos de cliente local NAS estaban limitados por la política DSM y no se presentan como pruebas pasadas. No se cambió esa política ni el servicio rsync. Fixture final eliminado y sin PIDs.

Preflight: runtimea28 exacto y cero writers; instalación atómica de <REDACTED>, UID1026/modo0700, con rollback0600 y fsync .ssh/originals-receiver.py.before-6dcc7cca-20261019T224834Z. El status de dump19 mantiene complete=true/staging=false, capability nueva y private_permissions complete=true/staging=null; no se cambiaron permisos previos, datos, claves ni locks. Evidencias privadas: .ssh/receiver-fixture-6dcc7cca-mac-20261019T224813Z.json y .ssh/receiver-deploy-6dcc7cca-20261019T224834Z.json.

El publicador diario sigue desactivado; desplegar esta capability no ejecuta una copia.

Incremental del 20 de octubre: corrección de gramática, sólo fuente

El ciclo natural de las 11:10 falló durante documents, con unsupported backup command, rsync código 12 y cero bytes recibidos por el cliente según los logs retenidos por el coordinador. No se ha reintentado la copia ni ejecutado el publisher. El diagnóstico aislado en VM208 utilizó un archivo sintético de tres bytes y un transporte SSH falso que únicamente escribía argv en un temporal privado: GNU rsync 3.2.7, protocolo 31 genera --link-dest y su ruta como dos argumentos, mientras que el receptor admitía sólo --link-dest=RUTA. El perfil sin incremental no añade esa opción. El Openrsync local de macOS genera la forma con =, por lo que su captura anterior no acreditaba el comportamiento del cliente GNU instalado. Los códigos 12 de la captura sintética son esperados: el transporte falso termina sin hablar el protocolo rsync y no conecta con el NAS.

El candidato consume la opción y su operando mediante un recorrido por índice. Acepta ambas formas y normaliza siempre a un único argumento --link-dest=RUTA_CANÓNICA antes de exec. Conserva las comprobaciones del directorio anterior: mismo grupo, identificador de generación válido, existencia, confinamiento y ausencia de componentes symlink explícitos. Mantiene el único . penúltimo, la escritura sólo en raíz staging, la inmutabilidad de generaciones completadas y la herencia del lock. No añade opciones rsync autorizadas ni modifica datos, SSH, temporizadores o la política de reintentos.

test_receiver_incremental.py añade cinco pruebas con el argv GNU capturado, compatibilidad de =, canonicalización y rechazos de operandos ausentes/opciones como operando, argumentos extra, otro grupo, fuera de BASE, symlink y destino completado. Todos los árboles son temporales y exec está interceptado. La validación de sintaxis legacy local no sustituye una compilación con Python 2.7 del NAS; el receptor no incorpora sintaxis ni APIs modernas.

Plan de aplicación pendiente de revisión: revalidar el hash actual de .ssh/originals-receiver.py (baseline documentado <REDACTED>), UID 1026/modo 0700 y ausencia de writers; confirmar la ruta absoluta mediante el forced command existente sin cambiarlo. Guardar backup privado 0600, preparar temporal en el mismo directorio, comprobar hash y sintaxis con el Python 2.7 existente, publicar con rename atómico y fsync y releer hash/metadatos. Conservar todos los inodos de .locks, datos y staging. El rollback sería exclusivamente el archivo anterior, bajo las mismas guardas y sin copiar, completar o borrar generaciones. Este cambio de fuente no acredita un incremental de producción correcto ni autoriza iniciarlo.

Validación del candidato: 38/38 pruebas del receptor PASS con Python 3.14 (8,389 s) y Python 3.12 (7,304 s), sin skips; 12/12 pruebas de recuperación PASS. Incluyen los locks y procesos locales reales ya existentes. La fixture que reconstruye deliberadamente la antigua vulnerabilidad del punto extra se adaptó al recorrido while: conserva el punto extra en argv y avanza el índice, manteniendo las mismas expectativas de fuga del marcador sintético en legacy y rechazo en el receptor corregido. Antes de esa adaptación, la fixture legacy quedaba en bucle y agotaba su timeout; no era un resultado de producción ni un fallo omitido. Sintaxis legacy local y git diff --check PASS. Python 2.7 nativo y cualquier aplicación al NAS siguen pendientes.

Corrección aplicada20oct11:28:32 CEST

Fuente main4baa63c, receiverSHA <REDACTED>. CI39399972171 correcta, incluyendo38casos receiver y12recuperación en ambos Python3.10/3.12. El NAS compiló la fuente con su Python nativo sin ejecutar el receiver ni acceder a BASE. Preflight/CAS conservaron baseline6dcc, UID1026, GID100,0700/nlink1/6614bytes; backup privado600 y directorio700: /var/services/homes/monxas/.ssh/receiver-rollbacks/gnu-link-dest-20261020T092832Z-d932a610/. Instalación por rename/fsync, lectura final hash exacto, UID/GID/modo intactos, nuevo inode37588/7057bytes y recibo duradero. No se tocaron claves, datos, generaciones, staging ni inodos de locks.

Un intento anterior abortó en baseline, sin backup ni reemplazo. La lectura posterior encontró todos los pins correctos y un rsync en modo daemon sin --server; no era receptor por el forced command. La clasificación del instalador se acotó para contar ese modo por separado, conservando bloqueo para writers, clientes, receivers y lecturas desconocidas; seis fixtures puras lo verifican. Antes del reemplazo final:0writers/0receivers/0unreadables y1daemon. No se detuvo ningún proceso ni se relajaron los pins del archivo. Los escaneos/CAS son instantáneas cooperativas, no un bloqueo global de nuevos procesos.

Instalador privado finalSHA <REDACTED>. No se relanzó el backup: el ciclo11:10 permanece fallido y la publicación del dump no se ejecutó. El resultado de un incremental real posterior sigue pendiente; no se convierte la compilación o los fixtures en una copia de producción correcta.

Protección de la interfaz del backend20oct

El journal registra dos invocaciones con --help a las11:22. La interfaz anterior interpretaba argumentos desconocidos como una copia real; la identidad de quien las lanzó no está acreditada. La nueva interfaz usa argparse sin abreviaciones: ayuda y argumentos inválidos terminan antes de crear ROOT, abrir locks, leer histórico o ejecutar copia/publicación. Los modos de lectura no admiten flags de copia y conservan su salida. Las cuatro combinaciones válidas de copia conservan el comportamiento anterior. No se cambia la planificación ni se relanza trabajo.

Validación local:39tests correctos, uno omitido por Openrsync de macOS; las tres pruebas nuevas cubren ayuda/errores sin IO, despacho de lectura y las cuatro combinaciones válidas de copia con operaciones interceptadas. Este apartado documenta la fuente; instalación y comprobación remota pendientes.

Conservación del histórico al cambiar de generación

La fuente conserva cada grupo completo del informe anterior en su manifiesto de generación antes de reemplazar last.json. Copia literalmente el diccionario: no inventa muestras, fechas, hashes ni identificación de originales. Un manifiesto existente igual conserva bytes/inodo; uno distinto, symlink, permisos públicos, links adicionales o generación inválida bloquea el cambio sin resetear el informe. La publicación usa temporal exclusivo0600, fsync y hardlink sin sobreescritura; la limpieza sólo posee el temporal si su creación exclusiva tuvo éxito.

47tests locales correctos, uno omitido por Openrsync; revisión independiente de los ocho tests de historial y tres CLI correcta. Esta protección es para cambios futuros, no reconstruye el manifiesto photos antiguo ya ausente. No toca estado actual, datos NAS ni planificación durante su integración. Instalación pendiente. Un SIGKILL entre hardlink y limpieza puede dejar dos enlaces: el siguiente intento falla cerrado y requiere reconciliación manual; no hay limpieza automática ajena.

Protección CLI instalada20oct11:51:22 CEST

Commitbefef6f con CI39403378306 correcta en Python3.10/3.12. Reemplazo acotado atómico de /home/monxas/scripts/backup_originals.py, baseline4aeba3 a SHA <REDACTED>. Root:root0755/nlink1 conservados, compilación nativa correcta y backup0600 en /root/rollback-originals-cli-20261020T095122Z-2l0kxq5i/ (directorio0700). Dos scans previos no observaron backend mutador ni procesos ilegibles. Estos scans son observaciones, no una barrera global de admisión de procesos.

Comprobación posterior sobre el hash instalado: --help devuelve0; --does-not-exist y --metrics --after-current devuelven2. Las tres ejecuciones terminaron y conservaron identidad/contenido de los13archivos existentes de estado y la configuración RunWisp. No se ejecutaron métricas, copias, publicador ni lecturas de originales NAS. El histórico de main82c7547 aún no está instalado; la comprobación sólo acredita la protección CLI8485147.

Histórico instalado20oct11:54:28 CEST

Fuente main82c7547 y CI39403668371 correctas:47tests de backend y38receiver por Python3.10/3.12, incluyendo transporte GNU rsync; docs39403668426 correcta. Backend actualizado8485147→ <REDACTED>, root:root0755/nlink1, compilación nativa y readback correctos. Backup privado /root/rollback-originals-history-20261020T095428Z-q20iy2en/. El instalador adquirió el lock existente sin crear/truncar y conservó su inodo, reteniéndolo hasta terminar el recibo; scans previos0mutadores/0ilegibles. La exclusión sólo cubre cooperantes y no acredita ausencia universal de procesos.

Digest de identidad/contenido de los13archivos de estado idéntico antes/después de instalar y de probar ayuda0/argumento inválido2/combinación lectura-mutación2; configuración RunWisp idéntica f7b3a3. No se ejecutó copia, métrica, publicación, rollover ni restauración de manifiestos antiguos. La preservación futura tiene cobertura aislada de tests; su primera ejecución natural aún no está acreditada.

Comprobación natural posterior11:56:11 CEST: Prometheus conserva originals_collection_success=1, originals_process_running=0 y observación del backend11:55:01 (70segundos de antigüedad). La consulta sólo leyó tres métricas existentes tras revalidar CT217 en proxmox2; no ejecutó recolector ni backend. Acredita compatibilidad de la lectura programada tras instalar125f, no éxito de una copia ni del rollover.

Antigüedad del primer intento y lectura de intención configurada

Reproducción aislada del backend125f: dos fallos de montaje separados24h conservan stamp20261020 pero reemplazan documents.started; photos sigue sin éxito y la edad del primer ciclo vuelve a1s tras cada reintento. Con ese intervalo no se sostiene el umbral24h más15m de OriginalsStale. Dos fixtures con la expresión literal en Promtool3.15 confirman ausencia de alerta con reset y disparo24h16m si se conserva el inicio. No equivale a ausencia universal de alertas para otros intervalos.

La nueva fuente conserva first_cycle_started por generación antes de reemplazar started de los grupos. Usa la fecha ISO con zona ya acreditada más temprana, o la fecha actual real del primer intento; no deriva fechas del nombre de generación. Los reintentos conservan ese literal y sólo una generación nueva lo reinicia. Métricas leen el campo o su fallback histórico sin escribir.52tests backend y17 configuración correctos localmente (un caso GNU omitido por Openrsync de macOS). La corrección no reconstruye inicios antiguos ya perdidos.

La validación nativa de la lectura de intención encontró un drift: directorio /home/monxas/runwisp0775 frente a0755 declarado por el rol homelab_tools. Sin ACL/xattrs ni otros miembros de grupo detectados. Root restauró0755 sobre el mismo FD/inodo, UID/GID1000 conservados, CAS y recibo privado durable en /root/rollback-runwisp-dir-mode-hfk2k7tt/; no archivos modificados ni reload. Rollback exclusivamente de modo0775 requiere el mismo dev/inodo/propietario y modo0755 actual; no ejecutar una aplicación global de Ansible para revertirlo.

Antes de reparar el modo, el lector candidato devolvió conocido=false por config_parent; después, el mismo hash0962 devolvió conocido=true, enabled=true. Ambas comprobaciones ejecutaron sólo el lector de configuración en memoria con Python3.10 -B; sin main, métricas, job ni alteración de los13archivos de estado o configf7b3. No se relajó el lector. El backend instalado aún era125f en esas lecturas; la instalación de ambas nuevas protecciones queda pendiente de CI.

Nuevas métricas e inicio persistente instalados20oct12:07:49 CEST

Fuentes main5ac4195+38e6b9c; CI39405059521 y39405576853 correctas. Backend <REDACTED> instalado sobre125f con lock existente, CAS, copia privada y rename/fsync; root:root0755/nlink1 y compilación nativa conservados. Respaldo: /root/rollback-originals-monitoring-20261020T100749Z-ba747t4s/. Scans previos0backendmutadores/0ilegibles. La revisión independiente del merge comparó AST de helpers y combinación de métricas sin diferencias adicionales.

Verificación posterior: digest de los13archivos de estado idéntico al anterior a instalar el histórico11:54; configf7b3 idéntica. Ayuda0, inválido2 y mezcla lectura/mutación2 siguen sin cambiar estado. No se ejecutó main, métrica, copia, publicación ni cambio de generación durante instalación/comprobación. La lectura programada de las nuevas métricas queda pendiente del siguiente ciclo natural.

Se mantienen pendientes separados: preservar un intento real de publicación ante una copia manual válida sin flag, adaptar consumidores de las métricas antiguas, verificar incremental posterior y primer rollover natural. Las nuevas métricas acreditan intención del archivo, no carga efectiva del scheduler ni restauración.

Comprobación natural12:11:34 CEST: observación del backend12:10:01 (93s), collection_success1/process_running0 y ambas nuevas métricas configuration_known1/configured_enabled1. CT217/proxmox2 revalidado; sólo consulta Prometheus, sin ejecutar recolector. Acredita exposición natural de la intención configurada tras desplegar7b97, no éxito de publicación.

La siguiente fuente conserva exactamente el último diccionario dump_publication cuando una copia no solicita publicar, incluso al fallar o cambiar de generación. El wrapper de errores sólo modifica el intento de publicación si esa ejecución lo solicitó. Una publicación explícita sigue creando pending y conservando el último éxito como antes. No deriva intención por OR ni añade fechas ficticias. 56tests backend y17configuración locales correctos, un caso GNU omitido en Mac; revisión independiente correcta. Instalación de esta última mejora pendiente de CI.

Historial de publicación instalado20oct12:13:38 CEST

Fuente main30cd7c5/CI39406236074 correcta en ambas versiones. Backend <REDACTED> instalado sobre7b97 con las mismas guardas de lock/CAS/rename/fsync, compilación nativa y root:root0755/nlink1. Backup privado /root/rollback-originals-publication-history-20261020T101338Z-np100wzd/. Cero mutadores/ilegibles observados antes de reemplazar. Ayuda0/inválido2/conflicto2 conservan digest de los13archivos de estado, idéntico desde antes de instalar el histórico11:54, y configf7b3. No se ejecutó copia, publicación, métrica ni rollover. El estado antiguo perdido no fue reconstruido; el siguiente intento explícito será quien aporte nueva evidencia real. Adaptación de alertas todavía pendiente.

Alertas consumidoras instaladas20oct12:22:08 CEST

Fuente mainff9a090; CI39406961586 y lint39406961491 correctas. Failed/Stale usan configured_enabled; el requisito Required conserva exactamente expresión, ventana, etiquetas y anotaciones. La nueva advertencia ConfigurationUnknown usa known!=1 o absent del selector acreditado jobproxmox-nodes/instancepmx-51.lan:9100, for10m. El selector identifica al exportador propietario, no a VM208. 15tests Python y17escenarios Promtool correctos; paridad independiente de las88 reglas restantes, incluidos todos los campos de Required.

Archivo CT217/proxmox2 actualizado db85→ <REDACTED>, 90→91reglas,40067bytes. Escritura sobre el FD existente preservó dev54, inodo1029537,1000:1000,0664,nlink1 y el bind readonly del mismo contenedor. Nativepromtool3.15 validó91antes ydespués; recarga única POSTlocalHTTP200, sin reinicio, proyección seleccionada de ID/estado/montaje idéntica. Backup privado /root/rollback-originals-config-alerts-20261020-bxythzi_/.

La escritura in place conserva el bind pero no es atómica ante un crash/SIGKILL. CAS, flock cooperante y bloqueo de señales protegen la ventana acotada; un writer que ignore flock queda fuera de esa exclusión. Ante incertidumbre, leer hash/identidad/recibos y decidir recuperación manual; nunca reintentar ni restaurar automáticamente. Rollback sólo in place desde el backup, con CAS del candidato instalado, comprobación nativa90 y recarga explícita tras revisión.

Postverificación RO12:23:37/12:23:56 CEST:91reglas healthok, grupo nas-originals interval15s con evaluaciones12:23:24.064→12:23:54.064. Las cuatro reglas de publicación están inactive y sin lastError en ambas lecturas; OriginalsCopyFailed sigue firing por el fallo real anterior. No se acredita una copia correcta por haber reparado sus controles. Hash y todos los campos de identidad del archivo coinciden con el recibo; ID/estado/bind del contenedor idénticos y propietario CT217/proxmox2 revalidado antes de cada lectura.

El ciclo natural12:20:01 del backend81481eb devuelve colección correcta, proceso0/configuración conocida1/habilitada1; edad observada216–235s en esas lecturas, dentro del calendario5min existente. No se ejecutaron backend, recolector, copias, métricas sintéticas, notificaciones ni recargas adicionales. Evidencia privada0600 post-verification.json SHA <REDACTED>.

Incremental GNU real en CI: 20 octubre 17:46–17:48 CEST

Se amplió el contrato con una transferencia incremental real en directorios locales temporales, sin NAS ni SSH de red. GNU rsync genera el argv que pasa por el transporte local y el receiver de fixture; se comprueba explícitamente --link-dest PATH separado. La generación nueva reutiliza el inodo del archivo sin cambios mediante hardlink y copia el modificado a otro inodo. Tras completar la generación temporal se conservan contenido, inodos y mtime de la anterior; el nlink del archivo compartido puede aumentar por el hardlink esperado.

CI 39451463152, commit <REDACTED>, terminó correctamente: Linux Python3.10 y3.12 con GNU rsync3.2.7-1ubuntu1.5 ejecutaron 39 tests del receptor y12 de recuperación, sin omisiones. Esta cifra sustituye38 sólo en esta fase; las validaciones históricas de38 permanecen como tales. La proyección privada de CI tiene SHA256 <REDACTED>. PR74 quedó integrado a las17:48:40 en main a6184354….

La fixture local Mac pasó39 tests con una omisión explícita del nuevo caso GNU: el binario disponible era Openrsync2.6.9 compatible. No se instaló otro rsync; la ejecución GNU efectiva corresponde a Linux CI. No cambió código de producción, workflow, receptor NAS, datos ni scheduler; tampoco se lanzó un incremental real al NAS. Esta prueba de protocolo no acredita el resultado posterior de las copias.