# Atajos de teclado para aprobar que resisten errores

Un control de aprobación mediante teclado forma parte del límite de autorización. No es una función cómoda con una etiqueta de seguridad. Si una pulsación rutinaria del terminal puede atravesar un cambio de foco, un modal recién abierto o una cola de entradas repetidas y autorizar una llamada del agente, el control no obtuvo una decisión. Obtuvo una coincidencia temporal.

Es fácil publicar este fallo porque el camino normal parece excelente. Un desarrollador inicia una acción, aparece una ventana de aprobación, Return la acepta y todo el mundo considera rápida la interacción. Después, ese mismo desarrollador está leyendo la salida del terminal, mantiene pulsada una tecla modificadora mientras cambia de ventana o pulsa Return para cerrar un aviso no relacionado. La aprobación llega en el momento equivocado y la acción obtiene un consentimiento que nadie dio conscientemente.

## Un atajo de aprobación es un límite de seguridad

Un atajo de aprobación debe representar una decisión nueva y atribuible sobre una acción activa concreta. Que la persona sea dueña del teclado o que el proceso solicitante tenga permiso para preguntar no convierte cada carácter en una aprobación.

Hay tres hechos distintos que los equipos suelen reducir a uno solo:

- Un proceso puede solicitar una acción.
- Puede haber una persona delante del equipo.
- Esa persona puede aprobar ahora esta acción exacta.

El primer hecho tiene que ver con la identidad del proceso. El segundo, con la presencia local. El tercero es la autorización. Un proceso firmado puede establecer bien el primer hecho. Una ventana visible puede sugerir el segundo. Ninguno demuestra el tercero.

Trata la interfaz de aprobación como una pequeña máquina de estados, con una entrada estrecha al estado aprobado. Debe saber qué solicitud muestra, si tiene actualmente el control de la entrada, cuándo puede activarse mediante el teclado y si la solicitud sigue siendo válida. Cuando cambie cualquiera de esos hechos, invalida la ruta anterior hacia la aprobación.

Un error de implementación habitual coloca el atajo en un gestor de comandos general y comprueba solo si existe una ventana de aprobación. En una demostración parece inofensivo. En uso, significa que un evento entregado tarde puede aprobar otra solicitud, una solicitud antigua después de ser reemplazada o una ventana que acaba de volver del segundo plano. El botón no tenía que estar visible justo en el instante en que la persona pulsó la tecla.

La solución no consiste en eliminar todos los atajos. La confirmación solo con el ratón resulta incómoda para quienes usan bien el teclado y los empuja a hacer clic deprisa con el puntero. La solución es exigir más condiciones para una activación mediante teclado que para mostrar una etiqueta en un botón. El sistema debe aceptarla solo cuando coincidan el diálogo actual, la solicitud actual, la generación de foco actual y la pulsación física actual.

Esta distinción cambia las revisiones de código. Pregunta «¿qué evento exacto puede atravesar este límite?» en lugar de «¿Return hace clic en el botón?». Una buena respuesta nombra el origen del evento, el estado de la ventana, el identificador de la solicitud y los casos de rechazo. «El diálogo está abierto» no es una respuesta.

## El foco debe demostrarse, no darse por supuesto

Un diálogo debe descartar la posibilidad de activación mediante teclado cada vez que pierde el foco, aunque vuelva a recibirlo un instante después. Las transiciones de foco son el punto en que una entrada destinada a una superficie de la aplicación se interpreta en otra.

Piensa en una secuencia habitual. Un agente ejecuta un comando en un terminal. La persona escribe un comando, cambia a la documentación y entonces aparece una ventana modal de autorización. El sistema operativo cambia la ventana activa mientras las manos siguen moviéndose. Según el orden de los eventos y el framework de la aplicación, una pulsación de Return puede llegar después de que el modal reciba el foco, aunque la intención se formara antes de que la persona lo viera.

La regla insegura es sencilla: si el botón de autorización es el botón predeterminado y la ventana está en primer plano, acepta Return. Esa regla trata el hecho de estar en primer plano como una prueba de que la persona vio y evaluó la solicitud. Solo demuestra que la ventana ocupaba la parte superior de la pila en el momento del envío.

Usa una generación de foco. Cuando el diálogo se active, incrementa un contador y desarma la activación mediante teclado. Empieza a aceptar una pulsación válida solo después de que la aplicación haya observado que el foco se ha estabilizado para la generación actual y después de detectar una pulsación nueva de la persona. Al desactivarse, borra de inmediato el estado armado. No lo restaures automáticamente cuando vuelva el foco.

Es una decisión deliberadamente conservadora. Una persona que cambia de ventana y vuelve debe pulsar el atajo una vez después de regresar. Esa pulsación adicional cuesta poco. Enviar accidentalmente una llamada HTTP con credenciales o un comando SSH cuesta mucho más.

La misma regla se aplica cuando el sistema muestra otra hoja modal, una solicitud de contraseña, una superposición de accesibilidad, una interacción con una notificación o un cambio de espacio de trabajo. Tu aplicación no puede saber por qué se movió el foco y no debe adivinarlo. Sí puede saber que sus suposiciones anteriores ya no son válidas.

No dependas solo de los indicadores visuales de foco. Le dicen a la persona adónde parece dirigirse la entrada, pero una decisión de aprobación necesita una transición de estado que compruebe la ventana activa real de la aplicación y la generación actual del diálogo. El estado visual y el estado de autorización deben cambiar juntos desde una única fuente de verdad.

Una prueba manual útil consiste en abrir un diálogo de aprobación, mantener pulsada una tecla modificadora, cambiar de ventana, volver y pulsar Return de inmediato. Repítela haciendo clic fuera del diálogo, mostrando una alerta del sistema y cambiando rápidamente de aplicación. En todos los casos, la solicitud debe seguir pendiente hasta que ocurra una acción explícita nueva. Si una sola variante aprueba la acción, la ruta de teclado es demasiado amplia.

## Las entradas mantenidas necesitan una máquina de estados propia

Una tecla que ya estaba pulsada antes de que el diálogo pudiera activarse no puede autorizarlo. Esta regla cubre la clase de errores que aparece cuando alguien mantiene pulsadas Return, Space, Escape o una tecla modificadora mientras la interfaz cambia debajo de sus manos.

Las API de eventos suelen proporcionar fases de pulsación, repetición y liberación, pero el código de la aplicación tiende a reducirlas a «se recibió Return». Esa reducción elimina la información importante. Una pulsación que empezó en un terminal tiene un significado distinto de otra que empieza después de que la solicitud de aprobación esté visible y enfocada.

Rastrea el ciclo físico de cada atajo aceptado. El diálogo solo puede aceptar un atajo cuando observa un evento de pulsación no repetido después de su propia época de activación, mientras tiene el foco, seguido de la regla de finalización que use tu plataforma. Si el evento ya estaba activo cuando se abre el diálogo, establece un bloqueo para esa tecla y espera a su evento de liberación. No deduzcas una pulsación nueva a partir de una repetición.

Space merece el mismo cuidado que Return. Muchos controles usan Space para activarse con el teclado, y las personas mantienen Space para ver previsualizaciones, desplazarse o realizar acciones de accesibilidad. Escape también requiere atención. Solo debe cancelar la solicitud activa que aparece en pantalla y no debe cancelar una solicitud de reemplazo que haya aparecido después de pulsarla.

Las combinaciones de teclas modificadoras necesitan una política más estricta. No trates el estado aislado de una tecla modificadora como permiso y evita atajos que se solapen con hábitos del terminal, como Control-C, Control-D o Control-R. Si eliges una combinación, exige que todos sus componentes comiencen después de que el diálogo se arme y recházala si el foco cambió mientras alguno de ellos estaba pulsado. Una combinación que empieza en otra ventana no es una decisión sobre tu diálogo.

Basta con un pequeño registro interno:

```
requestId: 84f2
focusGeneration: 17
armedAfterEvent: 912
returnIsBlockedUntilUp: true
approvalState: pending
```

Los nombres no importan. La regla sí: un evento debe ser posterior a la generación de foco actual del diálogo y pertenecer a un ciclo de entrada nuevo. Los equipos suelen añadir este estado después de recibir un informe de aprobación accidental. Inclúyelo antes de añadir el atajo.

## El momento del modal convierte la entrada normal en autorización

Un modal que aparece en el instante equivocado puede recibir una entrada que la persona destinaba a la tarea subyacente. El peligro proviene del momento en que llegan los eventos, no de que el diálogo tenga un mensaje de confirmación cuidado.

Hay varias brechas temporales que conviene nombrar. Una solicitud puede llegar entre la pulsación y la liberación de una tecla. La interfaz puede renderizarse antes de que el estado de la aplicación termine de asociar el identificador de la solicitud. Una solicitud anterior puede cerrarse mientras un callback en cola todavía conserva su controlador de finalización. Un diálogo nuevo puede reutilizar el mismo objeto de botón y dejar una acción de teclado antigua apuntando al contenido nuevo.

El fallo habitual tiene este aspecto:

1. La persona pulsa Return en un terminal para enviar un comando.
2. El proceso del agente solicita permiso mientras la pulsación sigue en curso.
3. La aplicación muestra un diálogo de aprobación y activa su botón predeterminado.
4. La liberación de la tecla o un evento repetido llega al nuevo respondedor.
5. La aplicación trata ese evento como una aprobación y envía la acción.

Es posible que la persona nunca lea el diálogo. Aun así, la aplicación escribe en el registro de auditoría que la persona aprobó la acción, lo que agrava el fallo. El registro describe correctamente la ruta del código y falsamente el acto humano.

Evita esto con una barrera de activación. Al mostrar el diálogo, registra una secuencia de entrada monótona o una marca de tiempo del subsistema de eventos y rechaza los eventos que empezaron antes de esa barrera. Si la plataforma no puede proporcionar una secuencia fiable, exige una pulsación completamente nueva después de que el diálogo esté visiblemente activo. Cuando la plataforma deja lugar a dudas, elige el diseño conservador.

Usa generaciones de solicitudes además de generaciones de entradas. Vincula cada acción del botón y cada controlador de teclado a un token de solicitud inmutable. Cuando se cierre el diálogo, revoca ese token. Cuando otra solicitud lo sustituya, crea un token nuevo en lugar de modificar el objeto anterior. Un callback que lleve un token antiguo debe devolver una denegación, aunque casualmente haya un diálogo nuevo visible.

No intentes resolver el tiempo únicamente con un retraso como «ignorar Return durante 300 milisegundos». Los retrasos son populares porque son fáciles de explicar y de programar. Fallan en equipos lentos, cambios rápidos de foco, entradas de accesibilidad y personas que legítimamente tardan más. Un retraso mide tiempo. Tú necesitas medir la relación entre el evento de entrada y el estado actual del diálogo.

## La memoria muscular del terminal es una entrada hostil

El trabajo con terminales produce exactamente las teclas de las que un diálogo de aprobación debe desconfiar. Los desarrolladores pulsan Return para enviar comandos, Control-C para detener el trabajo, Space para avanzar por la salida y Escape para cancelar una edición. Los agentes hacen que la actividad del terminal sea más frecuente, así que la ventana de autorización aparece junto a un flujo de entradas habituales.

No supongas que un terminal está detrás de un límite entre aplicaciones separado. Las personas usan terminales integrados, paneles divididos, shells remotos, ventanas a pantalla completa y paneles de herramientas que parecen terminales. Un proceso también puede activar una solicitud de autorización justo después de que la salida indique al desarrollador que pulse una tecla. La historia visual puede cambiar más rápido que el plan motor de la persona.

La opción predeterminada segura es que el diálogo de aprobación no consuma ninguna entrada heredada. Puede recibir una pulsación nueva de Return después de recibir el foco y armarse, pero debe rechazar el Return que envió un comando del shell, el Space mantenido para paginar y cualquier repetición generada por cualquiera de las dos teclas. Si la interfaz no puede establecer esa distinción, elimina el atajo y exige una confirmación con el puntero o mediante biometría.

No vincules una aprobación peligrosa a comandos con apariencia de terminal en un mapa de comandos global. Un listener global ve eventos que van más allá de la cadena local de respondedores del modal y dificulta demostrar dónde comenzó una activación. Mantén el controlador asociado al diálogo activo concreto y exige que el diálogo verifique su propio token de solicitud antes de pedirle a la capa de autorización que actúe.

Aquí también merece atención copiar y pegar. Un salto de línea pegado puede ser texto normal en un terminal y una activación en un control de formulario. Los controles de aprobación deben ignorar la inserción de texto y el envío de comandos que no incluyan un evento de atajo físico válido. Un carácter pegado es un dato, no un consentimiento.

Prueba con un flujo de trabajo real en un terminal, no solo con una ventana sintética. Inicia un comando que solicite datos repetidamente, activa una solicitud mientras pulsas Return y prueba después de usar Control-C y Space. Hazlo cuando el terminal tenga el foco, cuando lo tenga una ventana de documentación y cuando la ventana de autorización aparezca durante un cambio de ventana. El objetivo es reproducir una intención que comenzó en otro lugar.

## Prueba el historial de eventos, no el botón dibujado

Las pruebas más útiles alimentan un reductor con un historial de eventos hostil y comprueban que no puede emitir una aprobación hasta que ocurre exactamente la secuencia correcta. Las pruebas de instantáneas pueden confirmar que un diálogo parece enfocado. No pueden decirte si un evento obsoleto atravesó el límite.

Modela el diálogo con eventos explícitos como `present`, `focusGained`, `focusLost`, `keyDown`, `keyRepeat`, `keyUp`, `requestRevoked` y `approveClick`. Da a cada evento un token de solicitud y una secuencia de entrada que aumente de forma monótona. El reductor debe producir uno de tres resultados: seguir pendiente, cancelar o emitir una aprobación para un token coincidente.

Este accesorio es lo bastante pequeño como para mantenerlo junto al código de interacción:

```json
[
  {"seq": 41, "type": "keyDown", "key": "Return"},
  {"seq": 42, "type": "present", "request": "r-19"},
  {"seq": 43, "type": "focusGained", "request": "r-19"},
  {"seq": 44, "type": "keyUp", "key": "Return"},
  {"seq": 45, "type": "keyDown", "key": "Return"},
  {"seq": 46, "type": "keyUp", "key": "Return"}
]
```

La forma esperada de la salida es igual de importante:

```json
[
  {"seq": 44, "decision": "pending", "reason": "inherited-input"},
  {"seq": 46, "decision": "approved", "request": "r-19"}
]
```

Si tu implementación aprueba en la secuencia 44, ha aceptado una pulsación que comenzó antes de que existiera el diálogo. Ese es el error, expresado sin una captura de pantalla ni una condición de carrera que aparezca solo una vez por semana.

Construye una matriz de pruebas compacta centrada en los cambios de estado, no en las etiquetas. Cubre al menos estos casos:

- Un atajo está pulsado antes de mostrar el diálogo y se libera después de que llegue el foco.
- El foco se va después de que comience una pulsación nueva y vuelve antes de la liberación.
- Llega un evento de repetición mientras una solicitud sigue pendiente.
- La solicitud A se cierra, se abre la solicitud B y se ejecuta un controlador antiguo.
- La bóveda u otro requisito previo cambia de estado después de que aparezca el diálogo.

En cada caso, comprueba que no se envía ninguna acción y que el registro de auditoría anota una denegación o ninguna decisión, según el diseño. Ignorar silenciosamente un evento puede estar bien. Un registro que afirma que hubo aprobación cuando el reductor la denegó no está bien.

Después prueba la integración de la interfaz, donde muchos reductores que por lo demás funcionan bien quedan fuera del circuito. Verifica que todas las rutas de activación pasan por la misma puerta: clic del puntero, Return, Space, acción de accesibilidad, acción programática del botón predeterminado y cualquier comando de menú. Una ruta de atajo separada que llame directamente a la acción se desviará con el tiempo de la ruta del botón.

Usa inyección de dependencias para el ejecutor de acciones en estas pruebas. La prueba solo debe recibir un objeto de llamada después de que el reductor haya emitido la aprobación para el token actual. Cuenta las llamadas, captura el token de solicitud y falla si un token antiguo llega al ejecutor. No pruebes esto enviando una solicitud HTTP o un comando SSH reales. El límite de autorización debe poder probarse sin credenciales ni red.

Las pruebas manuales siguen siendo importantes porque los sistemas operativos envían los eventos de foco y accesibilidad por rutas que los mocks pueden pasar por alto. Escribe un pequeño guion de regresión en lenguaje sencillo, ejecútalo en el sistema compatible más antiguo y en uno actual, y hazlo con un teclado físico. Incluye cambios rápidos de ventana, teclas mantenidas, teclas repetidas, alertas del sistema, bloqueo de pantalla, reactivación tras suspensión y una solicitud revocada mientras permanece visible. El guion debe indicar la decisión esperada después de cada acción, no limitarse a decir que el diálogo se comportó correctamente.

## Un evento denegado debe seguir denegado

Una vez que un evento no cumple el predicado de aprobación, los cambios posteriores de la interfaz no deben rehabilitarlo. Parece obvio hasta que una implementación guarda una marca genérica de «aprobación pendiente» y vuelve a comprobarla cuando regresa el foco o termina una animación.

Haz que el rechazo sea definitivo para el evento. Si Return llega mientras el diálogo está desarmado, regístralo como entrada heredada o descártalo y espera a una pulsación nueva posterior. Si el foco cambia durante una combinación, invalida esa combinación. Si cambia la solicitud, invalida todos los eventos y callbacks asociados a la solicitud anterior. El código no debe conservar una posible aprobación por si las condiciones mejoran.

Este principio también protege contra acciones duplicadas. Un doble clic, una acción de teclado seguida de una acción del puntero o la repetición de un evento no deben provocar dos llamadas externas. Cuando la capa de autorización acepta un token de aprobación, marca la solicitud como consumida antes de enviar la acción. Las activaciones posteriores para ese token deben producir un rechazo inofensivo por duplicado.

Separa el resultado de la interfaz del resultado de la acción externa. La interfaz puede decir «aprobada para envío» solo después de cerrar el estado de decisión. El ejecutor puede tener éxito o fallar de forma independiente. Si falla una solicitud de red, no reactives el token de aprobación anterior ni reintentes silenciosamente con una pulsación posterior. Vuelve a preguntar si la persona debe autorizar otro intento, especialmente cuando hayan podido cambiar el contenido de la acción o el destino.

Aquí es donde el diseño de auditoría demuestra su valor. Registra el contexto suficiente para reconstruir que una entrada fue rechazada por ser obsoleta, repetida, desenfocada, revocada o ya consumida. Evita convertir el registro en una captura de cada pulsación. Necesitas pruebas de decisiones, no un registro de vigilancia.

## La identidad, la autorización y el alcance necesitan respuestas distintas

Un flujo de aprobación fiable responde a quién solicitó la acción, qué quiere hacer y si la persona aprueba esa acción concreta. No debe usar una respuesta como sustituta de otra.

La identidad del proceso es útil porque permite que la persona decida si el solicitante era esperable. En macOS, la autoridad de firma de código puede ofrecer una señal más significativa que un nombre de proceso modificable. Sin embargo, un proceso esperado puede realizar una solicitud inesperada y una persona puede aprobar la solicitud equivocada por culpa de un atajo accidental.

El alcance es otra cuestión. Un permiso de sesión puede ser razonable para tareas de bajo riesgo que una persona haya revisado, pero no convierte cada llamada futura en la misma decisión. La confirmación por acción tiene otro objetivo: obliga a tomar una decisión nueva en el momento en que una acción usa una credencial o llega a un destino sensible. Diseña la interacción para que la persona pueda ver qué capa está cambiando.

Evita etiquetas vagas como «Permitir» cuando la solicitud implica un efecto externo. Indica el método, el destino y la operación relevante en un lenguaje que el operador pueda comprobar. Tampoco conviertas el diálogo en un volcado denso de datos. Muestra primero los hechos relevantes para decidir y deja disponibles los detalles exactos sin hacer que la aprobación dependa de recorrer una pantalla interminable.

Sallyport mantiene estas capas deliberadamente acotadas: una bóveda bloqueada deniega todas las acciones, un proceso nuevo de agente solicita autorización de sesión de forma predeterminada y determinadas credenciales pueden exigir una aprobación nueva en cada uso. Ese diseño no hace segura por sí solo la interacción mediante teclado; el atajo todavía debe demostrar que la persona actual aprobó la llamada actual.

## Publica solo cuando el rastro del fallo resulte aburrido

Publica el atajo únicamente cuando las entradas hostiles produzcan denegaciones normales y explicables. Una persona debe poder mantener Return durante la presentación, cambiar de ventana a mitad de una combinación, activar una Space repetida o competir con una solicitud de reemplazo sin provocar una llamada externa.

Incluye el accesorio de pruebas en la misma revisión que el cambio de interfaz. Pide a una persona revisora que lea la secuencia de eventos que demuestra que una tecla obsoleta no puede atravesar el límite. Exige que la implementación exponga una única función de autorización que compruebe la identidad de la solicitud, la generación de foco, la vigencia de la entrada y el estado de consumo. Los gestores de conveniencia deben llamar a esa función y nunca saltársela.

Observa los registros de producción para encontrar patrones que indiquen fricción sin debilitar el predicado. Muchas denegaciones de entradas heredadas después de que aparezca un modal pueden indicar que la solicitud llega en un momento incómodo. Mejora cuándo y cómo se presenta la solicitud u ofrece una ruta mediante puntero y biometría. No «soluciones» la métrica aceptando los eventos que el sistema rechazó correctamente.

Un buen atajo de aprobación parece algo normal después de que la persona haya revisado la solicitud. Esa es la única velocidad que merece optimizarse. Cualquier otra ruta rápida es solo una forma de convertir la memoria muscular del terminal en autorización.
