# Sanitización de secuencias de escape ANSI para terminales seguras para agentes

La salida de una terminal es una entrada. En cuanto un agente puede leer el resultado de un comando, un archivo del repositorio, un registro de compilación, un banner de SSH o una respuesta de API, cada byte de ese resultado puede intentar alterar lo que el agente ve o la forma en que se comporta una terminal cercana. Confiar en stdout porque el comando lo ejecutó tu propio sistema es un error. A menudo el comando se limita a retransmitir datos controlados por otra persona.

La sanitización de secuencias de escape ANSI debe situarse en el límite donde la salida de una herramienta se convierte en contexto del agente. Debe ocurrir antes de que el modelo reciba el texto, antes de que una persona abra una transcripción en directo y antes de que un registro de sesión pueda reproducirse. Quitar los códigos de color después de que el modelo ya los haya leído solo limpia las pruebas.

Esto no significa convertir cada resultado en un bloque estéril. Los agentes necesitan diagnósticos útiles. El objetivo es conservar el contenido semántico de la salida y rechazar las instrucciones de terminal, los movimientos del cursor, las solicitudes al portapapeles, los hipervínculos y demás tráfico de control al cruzar un límite de confianza.

## Una transcripción de terminal es una entrada no confiable

Un comando de shell no controla su propia salida. `git log` imprime mensajes de commits. Un compilador muestra rutas y fragmentos de código fuente. Un ejecutor de pruebas puede repetir un recurso de prueba. Un cliente SSH muestra el banner del servidor antes de ofrecer el indicador. Un gestor de paquetes lee nombres, versiones, metadatos y mensajes de error de registros remotos. En todos estos casos, una parte externa puede influir en el texto que después llega al agente.

El fallo habitual empieza con una comodidad útil: capturar stdout y stderr, concatenarlos y añadir el resultado a la conversación del agente. Así, la salida cumple dos funciones. Informa del resultado de un programa y actúa como instrucciones dentro del contexto de un modelo de lenguaje. Los controles de terminal hacen menos fiable la primera función, y el contenido con forma de instrucciones vuelve peligrosa la segunda.

Imagina un repositorio que contiene un nombre de archivo con un retorno de carro y una secuencia de escape. Un comando que enumera el archivo puede producir una vista que sobrescribe su propio prefijo. El revisor ve una ruta inofensiva, pero el flujo de bytes original contiene otra cosa. Si una herramienta convierte ese flujo en un mensaje para el agente sin mostrar los límites de los bytes ni aplicar una política de control, el agente recibe un artefacto ambiguo.

No limites el modelo de amenazas a un colaborador malicioso del repositorio. Los artefactos de compilación, las trazas de pila de dependencias, los dispositivos de red, los errores de bases de datos y la salida de comandos remotos cruzan el mismo límite. Un servidor comprometido no necesita acceso de shell al equipo del agente para devolver un banner diseñado para contaminar una transcripción.

Hay dos riesgos distintos:

- Un emulador de terminal puede ejecutar instrucciones de control cuando una persona ve la salida original.
- Un agente puede interpretar texto visible, oculto o reordenado como instrucciones en lugar de datos.

Una transcripción limpia del agente reduce el segundo riesgo. Un visor que nunca interpreta controles de terminal reduce el primero. Cuando una persona inspecciona la salida capturada, hacen falta ambos.

## Los controles de terminal hacen más que añadir color

Las secuencias de control de terminal pueden mover el cursor, borrar texto anterior, establecer el título de una ventana, crear un enlace en el que se puede hacer clic, colocar contenido en el portapapeles, consultar una terminal o transmitir datos a una función de la terminal. El color SGR es solo el subconjunto conocido. Un filtro que elimina `ESC[` seguido de dígitos y `m` gestiona los colores, pero deja intactas secuencias mucho más capaces.

ECMA-48 define funciones de control y la forma general de las secuencias CSI. CSI suele comenzar con ESC seguido de `[`, y continúa con bytes de parámetros, bytes intermedios y un byte final. También permite formas C1 de un solo byte en el intervalo 0x80 a 0x9f. Los emuladores de terminal añaden comportamientos privados sobre este estándar. La documentación de xterm sobre secuencias de control describe cadenas OSC, DCS, APC, PM y SOS, incluidos terminadores distintos de los de una secuencia CSI.

Esta gramática importa porque los controles no siempre llegan como una línea de texto ordenada. Un programa puede escribir ESC en un fragmento y `[` en el siguiente. Un pseudo-terminal puede dividir la salida en cualquier byte. Una herramienta puede emitir una cadena OSC terminada por BEL o por la forma ST de dos bytes, ESC seguido de una barra invertida. Un componente correcto en el límite debe conservar el estado entre lecturas.

Algunos ejemplos muestran por qué no basta con eliminar colores:

- `ESC[2J` pide a la terminal que borre la pantalla.
- `ESC[H` mueve el cursor a la posición inicial.
- `ESC]8;;URI ESC\\` inicia un hipervínculo OSC 8 en las terminales que lo admiten.
- `ESC]52;... BEL` es la operación de portapapeles que reconocen muchos emuladores de terminal.
- Un retorno de carro devuelve el cursor a la columna cero y puede sobrescribir una línea de estado anterior.

No hace falta que todas las terminales admitan una secuencia para que esta importe. Un recolector de salida no puede predecir qué terminal usará un ingeniero dentro de seis meses, qué visor reproducirá un registro o qué analizador convertirá un byte de control en un token visible. Elimina la ambigüedad al recopilar la salida.

Los caracteres de control fuera de las secuencias de escape también merecen atención. El retroceso, el retorno de carro, el salto de página, la campana y muchos bytes C1 pueden cambiar la presentación o confundir el análisis por líneas. Conserva solo los controles que necesite el contrato de salida, normalmente el salto de línea y quizá el tabulador. Conserva el retorno de carro únicamente cuando un analizador le dé un significado explícito y probado.

## Elimina los controles antes de que el texto llegue al modelo

La opción predeterminada más segura es sencilla: recopila bytes, limita su tamaño, analiza los controles de terminal como bytes, descarta las instrucciones de control, decodifica el contenido imprimible restante con una política de errores definida y envía ese texto limpio al agente. No decodifiques primero esperando que una rutina de limpieza Unicode encuentre el material peligroso. ESC es un byte ASCII, los controles C1 pueden aparecer directamente y las secuencias de bytes no válidas no deben hacer que el recolector omita el filtrado.

Una ruta práctica de salida tiene cuatro registros, aunque solo expongas uno al agente. Conserva el flujo de bytes original en un almacenamiento protegido para investigar. Produce texto plano normalizado para el contexto del modelo. Produce un informe de eliminaciones para operadores y registros. Guarda metadatos como el estado de salida, la duración, si hubo truncamiento, el número de bytes y un resumen criptográfico del contenido original.

El informe de eliminaciones importa. Borrar en silencio puede ocultar un problema a la persona que debe investigarlo. Un informe útil podría indicar que el recolector eliminó dos secuencias CSI, una cadena OSC, tres retornos de carro y una secuencia de bytes no válida. No necesita reproducir el payload. Repetir un payload OSC en el informe puede reintroducir el mismo peligro.

Aplica el límite a stdout y stderr por separado antes de unirlos. Los programas intercalan los flujos de formas que no conservan el orden original cuando un envoltorio los combina. Si el agente necesita una sola narración, etiqueta los dos flujos ya limpiados e incluye un número de secuencia asignado por el recolector. Eso conserva más información que una transcripción ficticia que afirma un orden preciso que nunca observó.

Los límites de tamaño pertenecen al mismo componente. Un atacante puede usar una cadena OSC sin terminador o una salida repetitiva para consumir memoria y contexto. Establece un límite estricto para el total de bytes capturados y otro menor para cualquier cadena de control en curso. Cuando se alcance el límite, cierra o drena la fuente según la política del proceso, marca el resultado como truncado y deja el artefacto original parcial fuera del mensaje del agente.

No pidas al modelo que decida si una secuencia de escape es inofensiva. Los modelos analizan texto, no protocolos de bytes, y su decisión puede variar según el contexto. Un analizador determinista debe tomar esa decisión antes de que el modelo vea la salida.

## El aislamiento conserva las pruebas sin conservar el comportamiento

La eliminación y el aislamiento resuelven problemas distintos. Eliminar crea texto utilizable al quitar instrucciones de presentación. Aislar mantiene los bytes originales disponibles para quien tenga una razón legítima para inspeccionarlos. Llamar «sanitizada» a una captura original porque la codificaste en base64 confunde el transporte con la autorización.

Normalmente un agente necesita las primeras cientos de líneas de una compilación fallida, no una reproducción byte a byte de una sesión de terminal. Dale texto normalizado con marcadores explícitos de truncamiento y eliminaciones. Si necesita más detalles, permite que solicite un fragmento limitado y limpio por intervalo de líneas o término de búsqueda. No respondas pegando la captura original en el mismo contexto.

Cuando un investigador necesite el original, ábrelo en un visor orientado a bytes que muestre los bytes de control de forma visible y nunca los envíe a una terminal. Los volcados hexadecimales funcionan bien porque hacen evidentes los límites de los bytes. Sustituir ESC por `^[` puede ayudar, pero el visor también debe tratar correctamente los caracteres C1 y los payloads de cadenas. Un visor que ejecuta `cat` sobre las pruebas no es una herramienta forense.

Este comando de shell crea un archivo de ejemplo sin emitir sus controles en la terminal actual. El archivo contiene SGR de texto rojo, una secuencia CSI que sube el cursor, un hipervínculo OSC 8 y un retorno de carro:

```sh
printf 'build: \033[31mFAIL\033[0m\nnotice\033[1A\033]8;;https://example.invalid\033\\open\033]8;;\033\\\rPASS\n' > terminal-sample.bin
od -An -tx1c terminal-sample.bin
```

La salida de `od` debe contener `1b` para ESC y `0d` para el retorno de carro. Nunca debe hacer que la terminal siga el enlace o mueva el cursor, porque `od` muestra una representación de los bytes en lugar de reproducirlos.

El aislamiento también necesita controles de acceso. Un artefacto original puede incluir secretos que un comando imprimió por error. Sanitizar los controles de terminal no oculta tokens, contraseñas ni datos personales. Ejecuta la detección y redacción de secretos como una fase separada, con su propia política de falsos positivos. No mezcles ambas tareas: un redactor que no detecte una credencial tampoco debe decidir si sobrevive una solicitud OSC 52.

## Una expresión regular no es un analizador de terminal

Las expresiones regulares siguen siendo populares porque eliminan en una línea la salida colorida de una compilación. Fallan como límite de seguridad porque la gramática de terminal es secuencial y funciona en streaming. Muchos patrones también presentan problemas de rendimiento con entradas largas y malformadas, justo el tipo de entrada que puede proporcionar un adversario.

Usa una máquina de estados basada en bytes con un comportamiento explícito para ESC, CSI y los controles de cadenas. La siguiente función de Python es deliberadamente limitada. Conserva el tabulador y el salto de línea, convierte el retorno de carro según una política visible de saltos de línea, descarta los demás controles C0 y C1 y elimina secuencias ESC, incluidas las cadenas OSC, DCS, APC, PM y SOS. Acepta fragmentos solo después de que la persona que llama los haya combinado, por lo que una versión de producción debe conservar los campos de estado entre lecturas.

```python
def clean_terminal_bytes(data: bytes) -> tuple[str, dict[str, int]]:
    out = bytearray()
    counts = {"esc": 0, "csi": 0, "string": 0, "control": 0}
    i = 0

    while i < len(data):
        b = data[i]

        if b == 0x1b:  # ESC
            counts["esc"] += 1
            i += 1
            if i >= len(data):
                break
            nxt = data[i]

            if nxt == ord('['):  # CSI
                counts["csi"] += 1
                i += 1
                while i < len(data):
                    c = data[i]
                    i += 1
                    if 0x40 <= c <= 0x7e:
                        break
                continue

            if nxt in b']P_^X':  # OSC, DCS, APC, PM, SOS
                counts["string"] += 1
                i += 1
                while i < len(data):
                    c = data[i]
                    if c == 0x07:  # BEL
                        i += 1
                        break
                    if c == 0x1b and i + 1 < len(data) and data[i + 1] == ord('\\'):
                        i += 2
                        break
                    i += 1
                continue

            i += 1  # Two-byte ESC function or unknown ESC form
            continue

        if b == 0x9b:  # Single-byte C1 CSI
            counts["csi"] += 1
            i += 1
            while i < len(data):
                c = data[i]
                i += 1
                if 0x40 <= c <= 0x7e:
                    break
            continue

        if 0x80 <= b <= 0x9f or b < 0x20 and b not in (0x09, 0x0a):
            counts["control"] += 1
            i += 1
            continue

        out.append(b)
        i += 1

    return out.decode("utf-8", errors="replace"), counts
```

Este ejemplo tiene límites. No modela todas las funciones de control de ECMA-48 y trata las formas ESC desconocidas como eliminables. Esa actitud conservadora es adecuada cuando la salida entra en el contexto de un agente. Un emulador de terminal necesita una compatibilidad amplia. El límite del agente necesita una superficie aceptada pequeña.

No copies esta función y des por terminado el trabajo. Añade longitudes máximas para los parámetros CSI y las cadenas de control. Conserva el estado del analizador entre límites de lectura. Cuenta las secuencias malformadas y sin terminador. Sobre todo, escribe pruebas que confirmen que el payload original nunca aparece en el resultado limpio. Un filtro de seguridad necesita pruebas negativas, no solo atractivas capturas de antes y después.

## Las secuencias OSC requieren un tratamiento especial

Las cadenas OSC son el punto en el que muchos equipos descubren que la salida de una terminal transporta algo más que formato. Xterm documenta comandos OSC para funciones como títulos de ventana e hipervínculos. Los emuladores modernos implementan subconjuntos diferentes, por lo que las listas de permitidos basadas en la terminal local actual son una mala opción.

Los enlaces OSC 8 pueden convertir un texto aparentemente inocente en un destino en el que se puede hacer clic. La etiqueta visible puede decir `build report`, mientras que el destino lleva a otro lugar. Una persona que lee una transcripción limpia del agente no necesita un enlace activo. Conserva la etiqueta como texto normal si puedes analizarla de forma segura, o elimina todo el envoltorio OSC y conserva solo los bytes imprimibles posteriores. No conserves los destinos URI salvo que tu producto tenga una política separada para validar y mostrar URL.

OSC 52 puede pedir a una terminal que establezca el contenido del portapapeles. Algunas terminales lo desactivan o requieren una configuración, pero el recolector no puede depender de ello. Si un registro original se reproduce en una terminal permisiva, el comando puede colocar contenido elegido por un atacante en el portapapeles del operador. El siguiente pegado puede acabar en un shell, un ticket, un chat o un campo de credenciales.

Las secuencias que establecen títulos crean un problema más discreto. Pueden modificar el título mostrado en un multiplexor de terminal, en el selector de tareas del sistema operativo o en una grabación. Un título que parezca una solicitud de aprobación o una implementación correcta puede confundir a un operador que revisa muchas ventanas. Eliminar todo el tráfico OSC elimina la categoría completa sin mantener una lista que quedará obsoleta.

No intentes conservar las cadenas OSC para que el modelo las comprenda. Un agente no necesita ejecutar una actualización del título de terminal, hacer clic en un hipervínculo de terminal ni recibir una operación de portapapeles. Si el resultado de un comando contiene una URL útil, haz que el comando la imprima como texto normal o extráela de una respuesta estructurada antes de que entre en la ruta de terminal.

La misma regla se aplica a las cadenas de control de dispositivos y a los comandos de programas de aplicación. Algunos emuladores los ignoran. Otros añaden comportamientos con el tiempo. Un límite de salida debe fallar de forma segura: si reconoce el inicio de una cadena de control, consume hasta un terminador válido o hasta la longitud máxima configurada, registra la anomalía y mantiene el payload fuera del contexto normal.

## Define un contrato de salida para cada herramienta

Un sanitizador no puede reparar una interfaz de salida que pida a una transcripción de terminal transportar a la vez una base de datos, un informe y una interfaz de usuario. Las herramientas deben indicar qué devuelven al agente: texto plano UTF-8, JSON estructurado producido en un canal que no sea una terminal o una referencia a un artefacto aislado. Haz que la salida similar a la de una terminal sea la excepción, no el formato de intercambio predeterminado.

En los comandos que controlas, desactiva la decoración cuando los llame un agente. Muchos programas ofrecen una opción sin color, un modo legible por máquinas o un ajuste de entorno. Prefiere la salida estructurada solo cuando controles su esquema y limites su tamaño. JSON también puede contener inyección de instrucciones en campos de texto, así que etiqueta cada campo como datos y mantén las descripciones no confiables separadas de las instrucciones.

Para los comandos que no controlas, ejecútalos con tuberías en lugar de un pseudo-terminal, salvo que necesiten comportamiento de terminal. Un pseudo-terminal fomenta barras de progreso, reescrituras del cursor, controles de título y rutas de código diferentes. Las tuberías no hacen segura la salida, pero reducen la cantidad de protocolo de terminal que debes gestionar.

Registra la procedencia junto al resultado. El agente y el revisor deben poder ver el ejecutable invocado, el directorio de trabajo, el código de salida, el flujo de origen, el límite de captura y si el sanitizador eliminó contenido. No permitas que la salida de la herramienta suplante esos campos. Coloca los metadatos generados por el recolector en un envoltorio distinto, fuera del texto no confiable.

Un envoltorio útil puede tener este aspecto:

```json
{
  "command": "test-runner --report plain",
  "exit_code": 1,
  "stdout": "142 tests passed\n",
  "stderr": "fixture failed at tests/login.txt:18\n",
  "sanitizer": {"removed_controls": 4, "truncated": false},
  "raw_artifact": "restricted:sha256:..."
}
```

El campo `raw_artifact` debe ser una referencia que el agente normal no pueda abrir. Si devuelves el contenido cuando lo solicite sin una nueva decisión de autorización, la referencia era solo decorativa.

## Prueba la salida hostil fuera de tu shell habitual

Un sanitizador que supera una prueba con códigos de color verde y rojo apenas ha comenzado. Prueba bytes que atraviesen estados del analizador, terminen inesperadamente e interactúen con controles de presentación. Ejecuta el corpus mediante la ruta de captura exacta que usan tus agentes, incluido el lanzamiento de procesos, el almacenamiento en búfer, el almacenamiento de registros y la representación de transcripciones.

Empieza con un corpus pequeño que contenga un byte ESC al final de un fragmento y `[` al principio del siguiente. Añade secuencias CSI con longitudes de parámetros inusuales, cadenas OSC terminadas tanto por BEL como por ST, una cadena OSC sin terminador, bytes CSI C1, retrocesos, retornos de carro repetidos y UTF-8 no válido alrededor de una secuencia de escape. Comprueba que el texto visible para el agente no contenga ESC, bytes del intervalo C1 ni payloads de las cadenas eliminadas.

Después prueba la semántica de presentación. Pasa una línea de estado como `working 10%\rworking 20%\rfailure` por la política de retorno de carro elegida. Si lo conservas como salto de línea, el agente ve el historial. Si simulas la sobrescritura, el agente solo ve `failure`. Cualquiera de las dos políticas puede funcionar, pero debes documentarla y probarla. Los cambios silenciosos durante una actualización del analizador complican la revisión de incidentes.

Las pruebas de fuzzing resultan útiles porque las gramáticas de escape tienen pocos estados y un espacio enorme de entradas malformadas. Genera flujos de bytes aleatorios dando prioridad a ESC, BEL, barra invertida, bytes C1 y cadenas largas sin terminador. Las propiedades son claras: el filtro termina dentro de los límites de tiempo y memoria, no lanza excepciones y su salida no contiene bytes de control prohibidos.

Prueba también la ruta humana. Una página de registros, una notificación del escritorio, un panel de terminal y una transcripción copiada pueden interpretar el contenido de forma distinta. Muestra el corpus original solo en un visor seguro de bytes. Muestra el texto limpio en todas las interfaces normales. Si un ingeniero puede copiar una entrada de registro original en una terminal interactiva durante una investigación rutinaria, hay un agujero en el límite de aislamiento.

## La aprobación humana no sanitiza una transcripción

Una solicitud de aprobación decide si un agente puede realizar una acción. No decide si los bytes devueltos por esa acción son seguros para mostrarlos o incluirlos en una instrucción para el modelo. Mantén estos controles separados, o una persona supondrá que aprobar `ssh host command` también equivale a aprobar todos los banners, nombres de archivo y errores remotos que devuelva el host.

Esta distinción importa en las pasarelas de acciones. Sallyport puede mantener las credenciales fuera del agente y exigir autorización humana para una acción, pero cualquier integración que envíe los resultados de la acción a un agente sigue necesitando un límite de normalización de salida. La custodia de credenciales y la seguridad de la transcripción responden a preguntas distintas.

No uses aprobaciones repetidas para compensar un tratamiento débil de la salida. Una persona a la que se pide aprobar cada lectura no inspeccionará todos los caracteres de una respuesta larga, y la respuesta puede llegar después de la aprobación. La revisión por llamada sirve para acciones sensibles, pero no convierte una salida no confiable en contexto confiable.

Conserva el registro de la decisión junto al resultado sanitizado. Registra que el operador aprobó un proceso o una llamada y, después, registra el resultado real del comando como datos no confiables de la herramienta, junto con el informe del sanitizador. Esta separación ayuda a que una revisión de incidentes responda dos preguntas sin mezclarlas: quién autorizó la operación y qué datos devolvió.

## La captura original debe quedar fuera del alcance habitual del agente

Una salida común destruye un diseño que por lo demás sería sólido: cuando el resultado limpio parece incompleto, el agente recibe una herramienta llamada `read_raw_output`. Esa herramienta convierte un límite de seguridad en un simple obstáculo. Un atacante solo necesita hacer que el resumen limpio parezca lo bastante confuso para que el agente solicite el original.

Usa una recuperación limitada. Permite que una persona abra el artefacto en un visor seguro. Permite que un extractor específico devuelva un intervalo hexadecimal limitado, una comparación de resúmenes o texto imprimible después de aplicar el mismo analizador. Si un flujo de trabajo necesita realmente inspeccionar el protocolo original, exige una decisión humana explícita y usa un visor que no ejecute controles de terminal.

Mantén separadas las políticas de conservación de los datos originales y los datos limpios. La salida original puede necesitar una conservación más breve porque puede contener secretos y payloads hostiles. Las transcripciones limpias pueden seguir siendo útiles para revisar sesiones del agente, pero también contienen datos empresariales. Los hashes conectan ambos registros sin obligar a los usuarios habituales a recuperar los bytes originales.

La primera tarea de implementación no es una regla de instrucciones que diga a los agentes que ignoren las instrucciones de terminal. Coloca un analizador de bytes entre la salida del proceso y cada transcripción visible para el agente, haz que la sintaxis de control desconocida desaparezca y conserva el original solo donde los agentes normales no puedan recuperarlo. Así eliminas una categoría completa de ambigüedad antes de que un modelo, una terminal o un operador cansado tenga que razonar sobre ella.
