HIPAA y LLM: la fuga de datos de pacientes como error de tipo
En un pipeline de LLM, los datos de salud protegidos cruzan fronteras que ningún tipo ve. Una demo con salida real del compilador: quitas el control que los cubre y el programa deja de compilar. Y lo que eso demuestra, y lo que no.
- #hipaa
- #cumplimiento
- #axon
- #agentes
- #tipos
El fallo que no hace ruido
Una clínica usa un modelo de lenguaje para resumir notas clínicas para el médico de guardia. El endpoint recibe la nota, la pasa por una capa que detecta y bloquea datos identificables, llama al modelo y devuelve el resumen.
Un día alguien refactoriza el endpoint. Mueve la llamada al modelo a una función nueva, más limpia, y en el camino la capa de protección se queda fuera. No falla ningún test, porque los tests comprueban que el resumen sale bien, y sale bien. El linter no dice nada. La revisión de código ve un diff que mejora la legibilidad. El cambio llega a producción, y desde ese día las notas con nombres, fechas de nacimiento y números de historia clínica viajan sin filtro hacia el proveedor del modelo y hacia los logs.
Nadie se entera hasta la auditoría. O hasta la brecha.
Ese es el problema de fondo del cumplimiento normativo en sistemas con IA: vive en un documento de políticas, en una revisión y en un filtro que alguien tiene que acordarse de conectar. Nada de eso forma parte del programa. El programa sin el filtro sigue siendo un programa perfectamente válido.
Qué pide HIPAA, en lo que toca a un LLM
HIPAA es la ley estadounidense que regula la información de salud. Lo que protege es la PHI (Protected Health Information): información de salud unida a algo que identifica a una persona. La vía más usada para anonimizar, llamada Safe Harbor, enumera 18 identificadores que hay que quitar: nombres, fechas, teléfonos, correos, números de la seguridad social, números de historia clínica, entre otros.
Para un pipeline de LLM, eso se traduce en preguntas muy concretas:
- ¿Quién recibe la PHI? Si la nota llega al proveedor del modelo, ese proveedor es un business associate y necesitas un contrato con él (un BAA).
- ¿Se envía solo lo necesario? El principio de mínimo necesario pide no mandar más PHI de la que la tarea requiere.
- ¿Queda registro de cada acceso? Los controles de auditoría de la regla de seguridad piden poder reconstruir quién vio qué y cuándo.
Todas tienen la misma forma: cada lugar por donde la PHI cruza una frontera —el prompt, el proveedor, los logs, la respuesta hacia otro sistema— es un punto que tiene que estar controlado.
Por qué un lenguaje normal no puede verlo
En Python, TypeScript o Go, el texto de la nota clínica es un string. El mismo tipo que el
nombre de un botón. Nada en el tipo dice "esto es PHI", así que nada en el compilador puede
exigir que pase por un control antes de salir.
def resumir(nota: NotaClinica) -> str:
texto = redactar_phi(nota.texto) # la protección es una llamada
return llm(texto) # que se puede borrar sin romper nada
Borra la primera línea y cambia texto por nota.texto. El programa sigue compilando,
los tipos siguen cuadrando y los tests siguen en verde. Es la misma sombra de la que hablo
en JSON es la caverna de Platón: el formato transporta
la forma del dato, no lo que el dato es.
El mismo pipeline en AXON
Una advertencia antes de seguir, porque cambia cómo debes leer lo que viene: AXON lo
construyo yo. Es un lenguaje de programación compilado cuyo destino es un modelo en lugar
de un procesador —AXON programming language en la documentación, axon-lang en la
terminal—, y una de sus ideas centrales es justo esta: que la clase regulatoria de un dato
forme parte de su tipo. La documentación la llama
cumplimiento en compilación.
Este es el pipeline de la clínica. Todo lo que sigue lo ejecuté con el compilador de AXON, y las salidas están copiadas de la terminal; solo he partido las líneas largas para que se lean.
type ClinicalNote compliance [HIPAA] { patient_id: String, text: String }
type NoteSummary compliance [HIPAA] { patient_id: String, summary: String }
shield PHIShield {
scan: [pii_leak]
on_breach: halt
severity: critical
compliance: [HIPAA]
}
flow Summarize(patient_id: String, text: String) -> NoteSummary {
step Summary {
ask: "Summarize the clinical note for the attending physician."
output: NoteSummary
}
return Summary.output
}
axonendpoint SummarizeNote {
method: POST
path: "/notes/summary"
body: ClinicalNote
execute: Summarize
output: FlowEnvelope<NoteSummary>
shield: PHIShield
backend: anthropic
}
Cuatro piezas:
- Dos tipos que llevan la clase
HIPAA. No es un comentario: viaja con el valor por todo el programa. - Un shield, el control que actúa en tiempo de ejecución: busca fugas de datos identificables y, si encuentra una, detiene la ejecución. Declara qué clases cubre.
- Un flow, el trabajo cognitivo: el paso que le pide al modelo el resumen.
- Un axonendpoint, la frontera: recibe la nota por HTTP, ejecuta el flujo y devuelve el resultado. Declara qué shield la protege.
$ axon check clinica.axon
✓ clinica.axon 108 tokens · 5 declarations · 0 errors
Compila. El endpoint transporta PHI y el shield que declara cubre HIPAA.
Ahora quita el shield
Es el refactor de la historia del principio: borrar la línea shield: PHIShield del
endpoint.
$ axon check sin_shield.axon
X sin_shield.axon 2 error(s)
error [line 19]: axon-T890 axonendpoint 'SummarizeNote' declares no
authorization coverage (no `requires:`, no `shield:`, no `compliance:`) and is
not marked `public: true`. Every endpoint is a trust boundary — doctrine
`every_boundary_is_guarded`: [...]
error [line 19]: axon-T957 axonendpoint 'SummarizeNote' carries regulated data
(kappa = {HIPAA}) across a trust boundary but declares no `shield:`. Regulated
boundaries require a shield whose `compliance:` covers the type's kappa — ESK
Fase 6.1 coverage rule. Declare `shield: <Name>` on the endpoint, where that
shield lists at least [HIPAA] in its `compliance:`. Declaring the classes on
the endpoint's own `compliance:` does NOT cover them: that list is a label, the
shield is the control that acts on a breach.
axon check sale con código 1, igual que ante un error de sintaxis, y no se construye
nada. El refactor que en Python llegaba a producción sin que nadie lo notara aquí no pasa
de la máquina de quien lo escribió.
El compilador da dos errores. El primero es general: todo endpoint es una frontera de
confianza y tiene que declarar qué lo protege, o decir explícitamente que es público. El
segundo es el de HIPAA: esta frontera transporta datos de clase HIPAA (la kappa del
mensaje es el conjunto de clases regulatorias) y no hay ningún shield que los cubra.
Tres formas de intentar engañarlo
Un control que se puede esquivar con una línea no es un control. Así que probé las tres trampas más obvias.
1. Poner la etiqueta en el endpoint
¿Y si en lugar del shield declaro compliance: [HIPAA] en el propio endpoint?
$ axon check etiqueta.axon
X etiqueta.axon 1 error(s)
error [line 19]: axon-T957 axonendpoint 'SummarizeNote' carries regulated data
(kappa = {HIPAA}) across a trust boundary but declares no `shield:`. [...]
Declaring the classes on the endpoint's own `compliance:` does NOT cover them:
that list is a label, the shield is the control that acts on a breach.
Desaparece el primer error, porque el endpoint ya declara algo, pero no el de HIPAA. Es la decisión más importante del diseño: una etiqueta no es un control. Si el compilador aceptara la etiqueta como cobertura, un programa podría certificarse a sí mismo.
2. Un shield que cubre solo una parte
Ahora la nota también lleva datos de pacientes europeos, así que su tipo pasa a ser
compliance [HIPAA, GDPR]. Pero el shield solo declara [GDPR].
$ axon check parcial.axon
X parcial.axon 1 error(s)
error [line 19]: axon-T957 axonendpoint 'SummarizeNote' declares `shield:
PHIShield`, but that shield does not cover kappa = {HIPAA} carried across the
boundary — ESK Fase 6.1 coverage rule. Add [HIPAA] to shield 'PHIShield's
`compliance:` list, or name a shield that already covers them.
No basta con que haya un shield. La regla es una diferencia de conjuntos: las clases que cruzan la frontera, menos las que cubre el shield, tienen que dar el conjunto vacío. El diagnóstico nombra exactamente lo que falta.
3. Una errata
¿Y si escribo HIPPA en el shield? En un sistema de etiquetas libres, una errata simétrica
en el tipo y en el shield haría cuadrar la comparación sin proteger nada.
$ axon check errata.axon
X errata.axon 2 error(s)
error [line 19]: axon-T957 axonendpoint 'SummarizeNote' declares `shield:
PHIShield`, but that shield does not cover kappa = {HIPAA} [...]
error [line 4]: axon-T1214 shield 'PHIShield' declares `HIPPA`, which is not a
regulatory class. Did you mean `HIPAA`? The canonical registry is ["HIPAA",
"PCI_DSS", "GDPR", "SOX", "FINRA", "ISO27001", "SOC2", "FISMA", "GxP", "CCPA",
"NIST_800_53", "NOM151", "LFPDPPP", "LGPD", "LEY1581"] (case-sensitive). A
compliance label is an ASSERTION a regulated reader trusts — an unrecognised
one is not a weaker claim, it is a claim about nothing that still satisfies
every coverage law that compares these strings.
El vocabulario es cerrado: quince clases, y ninguna más. Eso es lo que hace que la regla de cobertura sea sólida y no un juego de cadenas.
Lo que el compilador no comprueba
Aquí conviene ser tan preciso como en lo anterior, porque es donde un artículo así suele mentir por omisión.
Cambié el shield para que en lugar de buscar datos identificables buscara solo intentos de manipulación del prompt, sin tocar nada más:
shield PHIShield {
scan: [prompt_injection]
on_breach: halt
severity: critical
compliance: [HIPAA]
}
$ axon check otro_scan.axon
✓ otro_scan.axon 108 tokens · 5 declarations · 0 errors
Compila. El shield declara que cubre HIPAA, así que la regla de cobertura se cumple,
aunque lo que escanea no tiene nada que ver con proteger datos de pacientes.
Eso marca el límite exacto de lo que se demuestra. El compilador garantiza que en cada frontera por la que cruza PHI hay un control declarado para cubrirla, que las clases son reales y que la cobertura es completa. No garantiza que ese control sea el adecuado. Elegir qué escanea el shield sigue siendo tu diseño.
Y hay obligaciones de HIPAA que ningún lenguaje puede cubrir, porque no están en el código. La propia página de HIPAA de la documentación las enumera: el contrato con cada proveedor que toca la PHI, empezando por el del modelo; la evaluación de riesgos periódica; el procedimiento de notificación de brechas; las salvaguardas físicas. AXON no hace que tu sistema cumpla HIPAA. Hace que un tipo concreto de error, el más fácil de cometer y el más difícil de ver, no pueda llegar a producción.
Por qué en compilación y no en ejecución
La alternativa habitual son los guardrails de ejecución: un filtro que inspecciona el tráfico mientras el sistema funciona. Son necesarios, y el shield es justamente eso. El problema es otro: si nadie conecta el guardrail, nadie se entera. Una ruta sin protección se descubre en producción, si se descubre.
La comprobación en compilación no sustituye al guardrail; garantiza que existe. Son dos capas que se necesitan:
| Guardrail de ejecución | Regla de cobertura en compilación | |
|---|---|---|
| Cuándo actúa | Mientras el sistema responde | Antes de que exista un artefacto desplegable |
| Qué detecta | Una fuga concreta, en un mensaje concreto | Una frontera sin control, en todo el programa |
| Si falta | Nadie lo nota | El programa no compila |
| Qué deja como evidencia | Registros de lo que bloqueó | Cada build que pasa es una prueba de cobertura |
La última fila es la que más le importa a un auditor. En un sistema normal, demostrar que todas las rutas que tocan PHI están protegidas exige revisar el código a mano. Aquí, que el programa compile ya es esa demostración, al menos para la parte que el compilador puede ver.
Llévalo a tu caso
La regla completa, con todo lo que demuestra y lo que no, está en la documentación, en cumplimiento en compilación. Y si tienes un pipeline con datos de pacientes, o con cualquier otra clase regulada, y quieres ver qué fronteras quedarían al descubierto, hablemos de tu arquitectura.
Si se te ocurre una trampa que compile y no debería, es exactamente el tipo de hallazgo que quiero leer.
Para llevarte
- En un pipeline de LLM, la PHI cruza fronteras —el prompt, el proveedor, los logs— que en un lenguaje normal ningún tipo ve.
- Si la clase regulatoria forma parte del tipo, quitar la protección deja de ser un refactor inocente y pasa a ser un error de compilación.
- La regla resiste las trampas obvias: una etiqueta no cuenta como control, la cobertura es una diferencia de conjuntos y el vocabulario es cerrado.
- Lo que no demuestra es que el control sea el adecuado, ni nada de lo que HIPAA pide fuera del código. Es una garantía estrecha y exacta, y por eso sirve.