Spaces:
Sleeping
Sleeping
Commit History
-
Las reglas clínicas del suelo de seguridad pasan a data/ajustes_clinicos.json
Jose Salazar +1: Claude Opus 5El puerto Python del suelo (§1.1) dejaba dos implementaciones de las mismas reglas. Pero lo que se desincroniza no es la LÓGICA —multiplicar límites, comparar, clasificar; eso no cambia casi nunca— sino los VALORES: umbrales de gravedad, cortes explícitos, factores por edad y raza. Son además lo único que un veterinario querría tocar, y hasta ahora exigían un desarrollador y un build. Se extraen a `data/ajustes_clinicos.json`, que leen los dos motores: `analisis.ts` por fetch (como valores_referencia.json), `motor/gravedad.py` de disco y `evals/engine_runner.ts` de disco. Quedan 0 constantes clínicas duplicadas en ambos motores; lo que permanece es la lógica de aplicarlas. Se carga en runtime y NO se empaqueta en el bundle: un `import` de JSON lo inlinearía en tiempo de build y editar el fichero dejaría de tener efecto sin recompilar, que es exactamente lo que esto viene a evitar. También se mueven los límites de las categorías de edad: que «senior» empiece a los 7 años es una decis…5b69117 -
El suelo de seguridad se recalcula en el servidor
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §1.1. El motor determinista del que depende toda la historia de seguridad corre en el NAVEGADOR, y el backend aceptaba su veredicto como un hecho: los `hallazgos` llegaban como campos de la petición y el docstring del esquema lo decía por escrito («El backend NO recalcula»). Un cliente que mandara `hallazgos: []` desactivaba de una sola petición el suelo de derivación obligatoria y la detección de analitos fabricados. No hacía falta un ataque —era el contrato documentado de la API— y bastaba un bundle cacheado de ayer para que el servidor aplicara una seguridad distinta de la que creía aplicar. QUÉ SE PORTA. Sólo lo que sostiene el suelo: rangos ajustados por edad y raza, y clasificación de gravedad (~250 líneas de las 1062). La detección de patrones se queda en el cliente porque ninguna decisión de seguridad cuelga de ella; portarla habría creado una segunda implementación del motor validado por veterinario. Se descartó meter Node en la imagen de producción: depen…c205c6b -
Sesiones revocables: logout real y cierre en todos los dispositivos
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §2.3. Las sesiones son cookies firmadas sin estado y el logout se limitaba a borrar la cookie del navegador: el token seguía siendo VÁLIDO allá donde se hubiera copiado, hasta `session_max_age_s` (8h). No había forma de invalidar una sesión filtrada ni de echar a nadie tras un incidente, salvo rotar MORPHOS_SESSION_SECRET, que tira a todos los usuarios a la vez. Dos mecanismos, porque resuelven cosas distintas (migración 4): - `sesiones_revocadas`: una sesión concreta, por `jti`. Se recuerda sólo hasta que su firma caduca por sí sola; a partir de ahí la fila sobra y se poda. - `usuarios.sesiones_validas_desde`: TODAS las de una cuenta a la vez, con el nuevo POST /api/auth/logout-todas. Un sello temporal en vez de un contador de versión porque la pregunta es «¿se emitió antes del corte?». `jti` y `emitida_en` los añade `firmar_sesion`, no quien llama: si se generaran fuera, una ruta podría emitir por descuido una sesión irrevocable. TIMESTAMPS EN PYTHON, NO …2f5c422 -
Aislar por clínica los resultados de analizador
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §2.1 (+ la parte de identidad de §2.9). Era el hallazgo más grave de la revisión: `lab/almacen.py` era un dict global sin dueño y `routers/lab.py` no filtraba nada, así que CUALQUIER sesión autenticada podía leer la muestra de cualquier clínica, y `/api/lab/pendientes` las enumeraba todas —sin necesidad siquiera de adivinar IDs—. Cada `muestra_id` abre el panel completo de analitos más las pistas de paciente (nombre de la mascota, raza, sexo). IDENTIDAD DE DISPOSITIVO. `lab_api_keys` pasa de lista de secretos anónimos a `clinica:clave`. `verificar_dispositivo` devuelve el tenant dueño en vez de None, y el recorrido sigue siendo en tiempo constante sobre TODAS las claves (un lookup por diccionario sobre un secreto filtra por tiempo). Revocar = quitar la clave y reiniciar. DE DÓNDE SALE EL TENANT. Del servidor, siempre: de la API key en la ingesta y de la cookie FIRMADA en la lectura. Nunca del cuerpo de la petición —si el puente pudiera declarar su clínica, mentir …1339cdc -
Esquema versionado y BD de usuarios en volumen persistente si lo hay
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §3.4. MIGRACIONES. El esquema era un único `CREATE TABLE IF NOT EXISTS`: creaba las tablas en una BD vacía y no hacía NADA sobre una existente, así que añadir una columna era una operación manual sobre un fichero al que, en Spaces, nadie puede llegar. Ahora `PRAGMA user_version` con pasos numerados; sólo se aplican los pendientes. Los pasos son reejecutables sobre lo ya existente porque las BD creadas antes de esto están en la versión 0 con las tablas del paso 1 ya dentro —ese es el caso de la instancia desplegada, y hay prueba de que se pone al día sin perder cuentas—. Paso 2: índice para el throttle de login, que filtraba por email+ip+momento con scan completo en cada intento. PERSISTENCIA. `db_path` apuntaba siempre a `instance/`, que en Spaces no sobrevive al reinicio: cada rebuild se llevaba cuentas, hashes e historial. Ahora se elige /data si hay un volumen persistente montado ahí, y si no se cae a `instance/` AVISANDO al arrancar. Se elige solo porque el fa…b90d0d2 -
IP real detrás del proxy y caché de papers atómica y acotada
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §2.6 y §2.5. PROXY (§2.6). El limitador usaba `request.client.host`, que detrás de un proxy —HF Spaces tiene uno— es la dirección del PROXY. `limite_login` (5/minute) y `limite_papers` no eran por IP sino GLOBALES: a la vez un bypass (fuerza bruta desde muchas IPs contra un único contador) y una auto-denegación de servicio (un cliente ruidoso agota el login de todos). El throttle por email+IP de `auth.py` tenía el mismo problema. `X-Forwarded-For` NO se lee a ciegas: la manda el cliente, y creérsela permite falsificar la IP en cada petición. El operador declara cuántos proxies hay delante (`MORPHOS_PROXY_SALTOS_CONFIABLES`, 0 por defecto) y se toma el elemento -N, que es el último escrito por infraestructura de confianza; lo que hay a la izquierda lo controla el cliente y se descarta. Aviso al arrancar si en prod sigue a 0, porque el fallo es silencioso: los límites "funcionan", sólo que compartidos. CACHÉ DE PAPERS (§2.5). Tres fallos: - `write_text` no es atómi…5118fc3 -
Sacar del bucle de eventos la recuperación RAG, SQLite y scrypt
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §3.1. `interpretar` es `async`, pero sus dos operaciones más caras eran SÍNCRONAS y corrían en el bucle de eventos, así que mientras una interpretación recuperaba, el proceso entero no contestaba a NADA: ni health check, ni el login de otro veterinario, ni una ingesta del puente. La concurrencia efectiva era 1 en una app que se anuncia multiusuario. - `ai/service.py`: `recuperar` / `recuperar_multi` a un hilo. Es lo más caro del request —embedding bge-m3, búsqueda LanceDB y cross-encoder sobre hasta `rag_candidatos` filas—, del orden de segundos en cpu-basic. - `routers/auth.py`: SQLite y scrypt de login y alta. scrypt (n=2**14) es caro a propósito: es justo el trabajo que no puede vivir en el bucle. - `routers/lab.py`: la escritura de `lab_persistir`, que corre por cada muestra con el analizador enviando en ráfaga (120/minute). `security/authz.py` no toca la BD (la sesión es una cookie firmada), así que las peticiones autenticadas ya no pasaban por SQLite…e26316b -
Cerrar el alta de cuentas y apagar la cola de muestras por defecto
Jose Salazar +1: Claude Opus 5Contención inmediata de ARCHITECTURE_REVIEW §2.2 y §2.1. Ninguna de las dos es el arreglo estructural —ese es el scoping por tenant— pero ambas reducen hoy la superficie sin tocar el modelo de datos. ALTA CERRADA (§2.2). Cualquiera podía POSTear /api/auth/registro y alcanzar /api/interpret, que gasta cuota de ZeroGPU compartida y, por la ruta Claude, dinero real: `limite_interpret_usuario` era un techo sobre una identidad que costaba una petición HTTP acuñar. Ahora `registro_abierto=False` + `registro_allowlist`. Dos detalles que no son cosméticos: - La comprobación de allowlist va ANTES que la de existencia. Al revés, un email no aprobado distinguiría 409 (existe) de 403 (no existe) y el alta sería un oráculo de la base de usuarios. Fuera de la lista: 403 siempre. - Es una lista de emails y no un booleano porque `instance/` es efímero (§3.4): las cuentas desaparecen en cada reinicio, así que un alta cerrada sin lista dejaría la instancia sin acceso posible. Se avisa al arran…61cd0db -
Léxico clínico compartido y detección de analitos fabricados
Jose Salazar +1: Claude Opus 5`ai/lexico.py` centraliza el vocabulario clínico (variantes, abreviaturas, nombres) que hasta ahora vivía disperso, y `ai/coherencia.py` lo usa para la comprobación que faltaba: distinguir «no se midió» de «se midió y salió en rango». Sin ese dato el modelo rellenaba el hueco —medido el 2026-08-04: medGemma afirmó un leucograma con neutrofilia sobre un panel de calcio/fósforo/ BUN/creatinina, y llamó «trombocitopenia leve» a unas plaquetas en rango—. De ahí `analitos_medidos` en `PeticionInterpretacion` y en el cliente (`ia.ts`): el panel COMPLETO, no sólo lo alterado. Campo opcional, así que un cliente antiguo que no lo mande se comporta como antes. Co-Authored-By: Claude Opus 5 <[email protected]>7dba92c
-
Allow list para modelos locales configurables por el usuario
Jose Salazar6ab7946 -
Ajustes varios al engine y al prompt basado en resultados de los evals
Jose Salazarbf9f7d1
-
Implementacion de AI judge para evals y actualizacion de readme
Jose Salazar1e215c9
-
No reintentar el 429 y limitar /api/interpret también por usuario
Jose Salazar +1: Claude Opus 5Dos cambios que atacan lo mismo: la cuota de GPU compartida es el recurso escaso de una instancia pública gratuita, y hoy se malgasta. 1) El reintento no distinguía qué fallo estaba tratando. service.py reintentaba ante cualquier ErrorModelo. Para la salida malformada de medGemma (razonamiento filtrado, bucle de repetición) eso es correcto y es su razón de ser. Para un 429 es contraproducente: gasta otra reserva de GPU del mismo pozo agotado, justo cuando el Space pide parar. Para un rechazo por seguridad o una clave ausente, no puede cambiar nada. ErrorModelo pasa a llevar `reintentable` y `saturado`; cada punto de fallo se clasifica (429 y cuota → no reintentable; 5xx → sí; config y rechazos → no). Un 429 ahora cuesta UNA llamada en vez de dos. /api/interpret responde 503 + Retry-After ante saturación, no un 502 genérico: el 502 invita a recargar en bucle contra una cuota ya agotada. 2) El rate limit era sólo por IP. ZeroGPU es por cuenta, no por …a5d73ff -
No tragar errores de programación al leer la sesión
Jose Salazar +1: Claude Opus 5leer_sesion capturaba (BadSignature, Exception). El `Exception` hacía redundante el `BadSignature` y, sobre todo, convertía cualquier error interno en «no hay sesión»: un bug en la capa de sesión salía como un 401 silencioso en vez de propagarse, que es justo lo que no se quiere en el camino de autenticación. Se captura BadData, el padre común de: - BadSignature firma inválida o falsificada - SignatureExpired sesión caducada (subclase de BadSignature) - BadPayload token corrupto Las tres son «cookie no válida» → sin sesión. Un TypeError o un fallo de configuración ahora se propagan. Nota: (BadSignature, SignatureExpired) —la corrección que se propuso primero— habría repetido el mismo error en pequeño, porque SignatureExpired ya es subclase de BadSignature. Se añaden tests de sesión (no había ninguno), incluido el caso negativo que motiva el cambio: un error interno debe propagarse, no devolver None. Co-Authored-By: Claude Opus 5 <[email protected]>5af9605 -
Añadir el stack nuevo: Vite/TypeScript + FastAPI
Jose Salazar +1: Claude Opus 5frontend/ Motor de patrones portado a TS (analisis.ts) con 27 tests dorados que fijan el comportamiento validado por veterinarios, más el resto de módulos (ui, pdf-parser, auth, papers, lab-import, ia). backend/ Servicio FastAPI: salida estructurada con Pydantic, clientes medGemma/Claude, recuperación RAG y toda la seguridad (sesión, CSRF, rate limit, CORS cerrado, BD fuera del webroot). bridge/ Puente local ASTM/HL7 para analizadores de laboratorio. evals/ Arnés de evaluación clínica + puerta de CI. index.html pasa a cargar el bundle TS en lugar de js/main.js. Co-Authored-By: Claude Opus 5 <[email protected]>70e641d