8 min de lectura

Control local de acciones para flujos de programación con IA: dónde encaja

El control local de acciones ayuda a los agentes de programación con IA a actuar sin exponer secretos. Descubre cuándo encajan las aprobaciones del desarrollador y cuándo deben decidir los controles del servidor.

Control local de acciones para flujos de programación con IA: dónde encaja

El control local de acciones encaja en un flujo de programación con IA cuando un desarrollador es el propietario del equipo, está presente para autorizar una ejecución y necesita que un agente actúe fuera del repositorio sin recibir los secretos subyacentes. Es una buena respuesta para un problema concreto y habitual: un agente puede editar código de forma suficientemente segura dentro de un espacio de trabajo, pero también necesita consultar una API, descargar un artefacto privado o ejecutar un comando mediante SSH.

Es una mala respuesta cuando se intenta convertirlo en autorización para toda una flota. Una aprobación en un Mac puede establecer que un proceso local concreto puede actuar con una credencial. No puede indicar a un servicio de producción qué inquilino, entorno, ventana de cambios o regla de negocio debe permitir la solicitud resultante. Esas decisiones deben tomarse donde vive el recurso.

El límite útil es sencillo: el control local protege las credenciales y recupera la intención humana en el equipo del desarrollador. El control del servidor protege los recursos compartidos y sigue funcionando cuando no hay nadie sentado frente a ese equipo. Los problemas aparecen cuando se pide a uno de los dos que haga el trabajo del otro.

El control local pertenece a un punto de ejecución propiedad de una persona

El control local de acciones tiene sentido cuando el equipo que inicia el agente tiene un propietario claro, capaz de reconocer el trabajo e interrumpirlo. Normalmente se trata de un Mac de desarrollador con una sesión de programación interactiva, no de un trabajador de compilación desatendido con un nombre de host amigable.

La cuestión de la propiedad parece obvia hasta que el equipo observa cómo se ejecuta realmente el trabajo. Un portátil puede estar asignado a una persona, pero usarse habitualmente mediante escritorio remoto. Un Mac de laboratorio compartido puede tener a varios ingenieros conectados a lo largo de una semana. Un host de compilación gestionado puede ejecutarse con la cuenta de un desarrollador, pero realizar trabajos activados por una solicitud de incorporación de cambios. Ninguno de esos hechos crea por sí solo un control humano significativo.

Hazte cuatro preguntas concretas antes de añadir aprobaciones locales:

  • ¿Quién puede desbloquear físicamente el equipo y aprobar una solicitud?
  • ¿Qué ejecutable inicia el agente y puede el propietario identificar su autoridad de firma de código?
  • ¿El trabajo del agente permanece dentro de una sesión interactiva o puede continuar después de que la persona se marche?
  • Si el equipo está comprometido, ¿qué limita el alcance de la credencial en el servicio remoto?

Las dos primeras preguntas establecen si la aprobación tiene un sujeto humano. Las dos últimas establecen si tiene un alcance de impacto razonable. Si un desarrollador pulsa «Aprobar» para un proceso que puede funcionar durante toda la noche y desplegar en todos los entornos, ese clic concede mucha más autoridad de la que probablemente pretendía.

Una pasarela local debe guardar el secreto y realizar por sí misma la acción saliente. Entregar un token al agente mediante una variable de entorno, un archivo de configuración o la respuesta de una herramienta convierte la barrera local en algo casi ceremonial. El agente podría repetirlo en una transcripción, escribirlo en un parche, dejarlo en el historial del shell o enviarlo a otra herramienta. No puedes recuperar de forma fiable una credencial después de que haya entrado en el contexto de un agente.

Por eso, una configuración parecida a un proxy no basta por sí sola. Un proxy HTTP puede enrutar el tráfico, pero enrutarlo no demuestra que los secretos nunca entren en el proceso cliente. El diseño debe garantizar que el agente solicite una acción, que el componente local inyecte la credencial y que el agente reciba solo el resultado remoto.

La propiedad del equipo es más que un nombre de usuario

El nombre de una cuenta de desarrollador no demuestra que el proceso aprobado sea el que realiza el trabajo. En macOS, un agente de programación puede iniciarse desde un terminal, una extensión del editor, un proceso auxiliar o un script escrito en el repositorio. Esos orígenes tienen niveles de confianza muy distintos.

Empieza examinando el árbol de procesos real durante una ejecución inofensiva. Este comando muestra los identificadores de proceso, los padres, los usuarios, el tiempo transcurrido y las líneas de comando:

ps -axo pid,ppid,user,etime,command | grep -i '[a]gent'

El resultado debe mostrar una cadena que reconozcas. Por ejemplo, un terminal interactivo puede aparecer como padre del proceso de agente aprobado. Un script de shell dentro de un repositorio no confiable, un elemento de inicio en segundo plano o un auxiliar desconocido deberían cambiar tu decisión. No apruebes un proceso solo porque su línea de comandos contenga el nombre de una herramienta que esperabas.

Después inspecciona el ejecutable en lugar de confiar en su nombre de archivo:

codesign -dv --verbose=4 /path/to/executable 2>&1 | \
  grep -E '^(Identifier|TeamIdentifier|Authority)='

Un binario firmado aún puede contener errores, pero la autoridad de firma ofrece al operador un elemento estable que reconocer. Una ruta sin más dentro de un directorio de proyecto editable no lo ofrece. Una aprobación de sesión que muestra primero la autoridad de firma es mucho mejor que un diálogo que solo dice «el agente quiere acceder». Ayuda a rechazar un binario copiado, un envoltorio local o una actualización inesperada antes de que realice una acción con credenciales.

Por eso también conviene prestar atención a los alias del shell y a los scripts envoltorio. Son útiles para trabajar, pero un alias puede iniciar algo distinto del ejecutable que la persona cree haber aprobado. Mantén los comandos de inicio de la pasarela de acciones fuera de los repositorios editables por el agente. Si un agente puede modificar el script que define su propia ruta de acceso, la barrera deja de expresar la decisión del operador.

La especificación de Model Context Protocol define cómo un cliente y un servidor MCP intercambian llamadas a herramientas. No establece que quien llama a una herramienta sea un proceso local aprobado. La compatibilidad con MCP responde a una cuestión de interfaz. La identidad del proceso, la custodia de las credenciales y la autorización siguen siendo trabajos independientes.

La aprobación por sesión funciona cuando una ejecución tiene un límite natural

La aprobación por sesión es un valor predeterminado práctico para una ejecución interactiva de programación, porque solicita confirmación una vez, cuando comienza un proceso de agente nuevo, y después deja trabajar. Evita un diálogo de confirmación para cada lectura inofensiva, pero conserva un punto en el que la persona puede comprobar quién está solicitando la acción.

El límite debe ser la vida real de un proceso, no una idea vaga como «el trabajo de esta tarde». Una sesión termina cuando el proceso sale. Esta regla es fácil de explicar, revocar y difícil de reinterpretar para un agente. Si el proceso se reinicia, vuelve a solicitar aprobación. Si una persona la revoca, las llamadas futuras fallan en lugar de heredar la confianza de un clic anterior.

La fatiga de aprobación indica que el límite es incorrecto. Si un desarrollador ve una solicitud cada vez que un agente consulta un gestor de incidencias, descarga un paquete y lee una API de puesta en escena, terminará aprobando de forma mecánica. Las advertencias repetidas no producen mejores decisiones. Enseñan a despejar ventanas para que el trabajo continúe.

Una aprobación de sesión tiene un propósito más limitado que un motor de políticas. Dice: «Reconozco este proceso local y le permito usar la ruta de acciones autorizada mientras esté activo». No debe intentar inferir si una consulta SQL es segura, si el título de una incidencia parece legítimo o si la rama actual merece acceso a producción. Esas son decisiones de autorización remota o reglas de flujo de trabajo, y los avisos en lenguaje natural son un lugar pésimo para almacenarlas.

Incluso una sesión interactiva puede no ser adecuada para una concesión general. Un agente que abre espacios de trabajo locales arbitrarios, acepta tareas desde un chat o ejecuta complementos de repositorios no revisados tiene una superficie de entrada mayor que una sesión limitada a un repositorio concreto. En ese caso, reduce el conjunto de acciones disponible para la ejecución o exige aprobación para la credencial específica que entraña el riesgo.

Los diálogos de aprobación también necesitan un comportamiento fiable ante errores. Si el almacén está bloqueado, toda acción debe fallar. Si la interfaz de aprobación no puede aparecer porque el Mac está bloqueado o suspendido, toda acción debe fallar. Un mecanismo alternativo oculto que permita continuar las solicitudes durante un error de la interfaz convierte la barrera humana en mera decoración.

La aprobación por llamada es para acciones con consecuencias costosas

La aprobación por llamada encaja con una credencial cuyo uso puede crear un cambio en producción, gastar dinero, borrar datos o cruzar un límite que el desarrollador debería considerar activamente cada vez. No encaja con toda credencial cuyo nombre suene intimidante.

Clasifica la acción por sus consecuencias, no por el protocolo. Un POST HTTP puede limitarse a crear un registro de vista previa desechable. Un comando SSH puede limitarse a leer un registro de despliegue. Una solicitud GET puede exportar un conjunto de datos completo de clientes. El método y el transporte no deciden el modo de aprobación.

Usa una barrera por llamada cuando la acción tenga una o más de estas características:

  • El servicio remoto no puede deshacer el resultado de forma fiable.
  • La credencial puede afectar a un recurso de producción compartido.
  • La solicitud puede transferir datos sensibles fuera del destino previsto.
  • La operación es lo bastante poco frecuente como para que una confirmación deliberada no se vuelva rutinaria.

El cuarto punto importa. Pedir confirmación por llamada para una acción frecuente produce los mismos clics mecánicos que un aviso de sesión excesivo. Divide las credenciales cuando sea posible. Asigna al trabajo rutinario de desarrollo una credencial limitada a recursos de desarrollo y reserva la credencial con capacidad de producción para el pequeño número de acciones que justifican una interrupción.

Un agente nunca debe decidir qué llamadas propias necesitan confirmación. Si puede etiquetar una solicitud como «solo lectura» o seleccionar por sí mismo la clase de credencial, una inyección de instrucciones o un simple error de implementación pueden dirigirlo hacia la ruta más sencilla. La persona propietaria de la cuenta debe establecer el requisito de aprobación fuera del espacio de trabajo del agente.

Haz que la confirmación sea legible y permita tomar una decisión real. La persona necesita el nombre o propósito de la credencial, el destino, el método de solicitud o la forma del comando y la identidad del proceso que inició la llamada. Mostrar cuerpos de solicitud sin filtrar puede exponer secretos o abrumar a la persona. Mostrar solo «¿Aprobar acción?» no aporta contexto útil. Un buen diseño de confirmación muestra menos, pero ofrece al operador suficiente información para rechazar una llamada inesperada.

Mantener los secretos locales resuelve un problema, no todos

Aprueba el proceso que reconoces
Aprueba una vez un proceso de agente nuevo y revisa su autoridad de firma antes de que pueda actuar.

Un almacén local evita que el agente tenga claves de API y claves privadas SSH en texto plano. Esto reduce mucho la divulgación accidental mediante avisos, transcripciones, registros de herramientas, archivos copiados y código escrito por el agente. También permite al operador revocar una ejecución activa sin tener que rotar inmediatamente la credencial.

No cambia lo que puede hacer la credencial remota. Si un token de API puede borrar todos los proyectos, el servicio aceptará una solicitud de borrado después de una aprobación local. Si una cuenta SSH tiene amplios permisos de sudo, proteger la clave privada no convierte esa cuenta en una identidad de despliegue limitada. El mínimo privilegio remoto sigue determinando el daño máximo.

Mantén claras dos distinciones:

  1. La custodia de credenciales pregunta si el agente puede obtener o reproducir un secreto. Un almacén local puede responder bien a esto.
  2. La autorización de recursos pregunta si el servicio debe aceptar una acción dadas las condiciones actuales. La API, el host, el proveedor de identidad o el sistema de despliegue deben responder a esto.

Los equipos suelen mezclarlas porque ambos temas aparecen cuando un agente llama a una API. El resultado es predecible. Instalan un almacén local de secretos y dejan detrás un token de administrador de larga duración. El token ya no se filtra con tanta facilidad, pero la ruta de acciones sigue siendo demasiado amplia.

SSH hace que la distinción sea especialmente clara. El equipo local puede proteger la clave privada, pero el host decide qué puede ejecutar la cuenta de la clave pública. Cuando sea posible, crea cuentas separadas o restricciones de comandos forzados para la automatización. Limita el acceso al host. Evita usar la clave personal de administrador de un ingeniero como clave general del agente. Las claves personales acumulan excepciones con el tiempo, y esas excepciones son precisamente las que un proceso autónomo no debería heredar.

Sallyport aplica este modelo local manteniendo las credenciales de API y SSH en un almacén cifrado de la aplicación y realizando la acción HTTP o SSH sin pasar el secreto al agente. Esta propiedad ayuda en un equipo de desarrollo, pero los equipos siguen necesitando credenciales limitadas y permisos en el servidor.

Verifica la ruta de acción antes de confiar en ella

Debes probar una pasarela de acciones con un endpoint inofensivo y una credencial temporal antes de conectarla a una capacidad de producción. La prueba debe demostrar tres cosas: que el agente no recibió el secreto, que la pasarela registró la llamada y que el servicio remoto vio la identidad esperada.

Crea una credencial HTTP temporal que pueda llamar a un endpoint no sensible, como un recurso de prueba que devuelva la identidad del solicitante. Pide al agente que realice esa única acción. Inspecciona su transcripción y el resultado de la herramienta para comprobar que no aparece el token literal ni cadenas con forma de token. El agente debe recibir el cuerpo de la respuesta o un error redactado, nunca el valor del encabezado utilizado para autenticar la solicitud.

Después prueba la denegación deliberadamente. Bloquea el almacén local y repite la acción. Termina el proceso de agente aprobado e inicia uno nuevo. Revoca la sesión activa si la pasarela permite revocarla y vuelve a intentarlo desde el proceso original. Cada intento debe fallar en el límite local. Si las llamadas siguen funcionando, averigua si otro proceso conservó la credencial, si una variable de entorno evita la pasarela o si el servicio remoto tiene otra ruta de autorización almacenada en caché.

Un registro de auditoría útil necesita suficiente información para reconstruir un evento sin conservar el secreto. Para una acción HTTP, registra la fecha y hora, la identidad del proceso o de la sesión, la etiqueta de la credencial, el destino, el método, el estado del resultado y un identificador de solicitud cuando el servicio remoto lo proporcione. Para SSH, registra el destino, la etiqueta de la cuenta, el resultado del comando y una representación del comando adecuada a tus reglas de sensibilidad. No registres valores bearer, claves privadas ni cargas completas sensibles solo para que el rastro de auditoría parezca completo.

El encadenamiento mediante hashes ayuda a detectar un historial local reescrito, pero no demuestra que la acción original fuera sensata. Conserva el registro de la pasarela junto al registro de auditoría del servicio receptor. Si no coinciden, trátalo como una investigación, no como una razón para declarar automáticamente correcto uno de los dos.

Sallyport proyecta diarios de sesión y actividad desde un registro de auditoría cifrado y encadenado mediante hashes, y sp audit verify puede comprobar esa cadena sin conexión y sin una clave del almacén. Ejecuta la verificación durante la revisión de un incidente o la entrega de una versión, pero no confundas la comprobación de integridad con la autorización.

Los controles del servidor toman el relevo cuando no hay personas o los recursos son compartidos

Mantén los secretos fuera del contexto
Mantén los secretos de API y SSH en el almacén cifrado de Sallyport, mientras el agente recibe solo los resultados de las acciones.

Los controles del servidor son necesarios cuando la acción debe continuar sin que un desarrollador concreto esté disponible para aprobarla. Esto incluye trabajos de CI, correcciones programadas, agentes alojados en servidores, ejecutores compartidos y trabajadores de despliegue. Un Mac local no puede ser la autoridad final para un trabajo que debe sobrevivir a la batería, un viaje, la suspensión o la salida de una persona de la empresa.

Sitúa la decisión de autorización cerca del recurso protegido cuando se cumpla cualquiera de estas condiciones:

  • Varias personas o sistemas pueden activar el mismo flujo de trabajo.
  • El objetivo es producción, datos de clientes, actividad financiera o un sistema regulado.
  • Un agente se ejecuta en infraestructura y no en el equipo interactivo de una persona.
  • El servicio debe aplicar límites de inquilino, ventanas de cambios, reglas de entorno o separación de funciones.
  • El flujo necesita alta disponibilidad sin que una persona apruebe un diálogo.

En esos casos, utiliza una identidad de carga de trabajo con permisos limitados, una validez corta cuando el sistema de identidad lo permita y registros de auditoría en el servidor. Aplica los límites de entorno en el sistema de despliegue o en la API. Exige allí un registro de cambios o una aprobación humana si la organización lo necesita. El equipo local aún puede ayudar al desarrollador a preparar y revisar un cambio, pero no puede ser el punto de aplicación de la acción de producción.

NIST Special Publication 800-207 describe zero trust como un modelo en el que las decisiones de acceso se centran en proteger los recursos en lugar de confiar en la ubicación de red. La lección útil para los flujos con agentes no es que cada herramienta local necesite un lenguaje de políticas complejo. Es que la API o el host de producción deben decidir por sí mismos sobre el solicitante y el recurso pedido. Una aprobación desde un Mac no puede sustituir esa decisión.

No intentes resolverlo reenviando a CI la credencial local de un desarrollador. Eso convierte una credencial controlada por una persona en una credencial de servicio desatendida, sin conservar la claridad de ninguno de los dos modelos. Crea una identidad de carga de trabajo distinta y asígnale solo los permisos que el trabajo necesita.

No reconstruyas un motor de políticas dentro de los avisos de aprobación

Inyecta las credenciales de API localmente
Envía llamadas HTTP mediante Sallyport, con credenciales bearer, básicas o de encabezado personalizado inyectadas localmente.

Los equipos suelen pedir reglas como «permitir solicitudes GET excepto fuera del horario laboral» o «permitir SSH solo cuando el nombre de la rama contenga release». La petición es popular porque parece reducir los clics sin perder el control local. Normalmente crea un sistema de políticas frágil que nadie puede explicar bajo presión.

El control local de acciones debe tener una pequeña escalera de decisiones visible: almacén bloqueado o desbloqueado, sesión aprobada o no, credencial que requiere aprobación por llamada o no. Cada estado tiene una acción directa del operador. Una persona puede predecir qué ocurrirá, probarlo y revocarlo.

Cuando una herramienta local empieza a analizar nombres de ramas, texto de avisos, patrones de URL, etiquetas de incidencias y la intención proporcionada por el agente, comienza a tomar decisiones de autorización a partir de entradas que el agente puede influir. Las reglas acumulan excepciones. Las excepciones se convierten en permisos. En poco tiempo, un desarrollador ha construido en un portátil un sistema parcial de autorización del servidor, sin el contexto del servicio necesario para hacerlo bien.

Vincula las decisiones locales a hechos que el equipo local pueda establecer: si el almacén está abierto, qué proceso firmado solicitó una acción, si su sesión está aprobada y si la credencial necesita una confirmación nueva. Vincula las decisiones remotas a hechos que el servicio pueda establecer: recurso objetivo, identidad del solicitante, inquilino, entorno actual, contenido de la solicitud y controles de la organización.

Esta separación también facilita el diagnóstico de errores. Una llamada local denegada significa que el almacén, la sesión o la barrera por llamada la detuvo. Una llamada denegada por el servidor significa que la política remota la rechazó. Cuando una denegación puede proceder de un laberinto de reglas locales superpuestas, los desarrolladores desactivan los controles en lugar de repararlos.

Un flujo mixto asigna a cada control un trabajo definido

La mayoría de los equipos necesitan controles locales y del servidor. El diseño práctico no es una elección entre todo o nada.

Un desarrollador puede ejecutar localmente un agente interactivo para inspeccionar una API de puesta en escena, leer metadatos de paquetes privados o ejecutar diagnósticos SSH limitados. El desarrollador aprueba la ejecución después de comprobar la identidad del proceso. El almacén local proporciona una credencial que el agente nunca ve. Los registros de actividad permiten revisar lo ocurrido más tarde.

El mismo agente puede preparar un cambio de despliegue sin recibir autoridad para desplegarlo. Después, una canalización del servidor se ejecuta con su propia identidad de carga de trabajo, aplica las restricciones de producción y registra el despliegue resultante. Si la canalización necesita aprobación humana, sitúala en el sistema propietario del cambio de producción, donde seguirá siendo visible para las personas responsables de ese entorno.

Trata la transferencia como un límite que merece conservarse. El agente local puede producir un parche, un resultado de pruebas o una solicitud firmada para revisión. No debe introducir de contrabando la autoridad de sesión de un desarrollador en un trabajo desatendido. El trabajo del servidor no debe depender de un portátil desbloqueado para terminar.

Empieza dibujando cada acción con credenciales de un flujo actual. Anota el proceso que la inicia, el equipo que guarda el secreto, el recurso que lo recibe, la persona que puede detenerla y si el trabajo debe ejecutarse sin esa persona. Enseguida encontrarás las acciones que encajan con el control local, las que requieren aplicación en el servidor y las incómodas que actualmente no tienen ninguno de los dos.

FAQ

¿Cuándo debo usar el control local de acciones para un agente de programación con IA?

Usa el control local cuando el agente se ejecuta en el Mac de un desarrollador y necesita llamar a un pequeño conjunto de API o destinos SSH con credenciales que deben mantenerse fuera de su contexto. Funciona mejor cuando una persona puede aprobar un proceso de agente nuevo y responder si el equipo solicita confirmación.

¿Un almacén local de credenciales hace seguros a los agentes de IA?

Un almacén local mantiene las credenciales fuera del proceso del agente, lo que elimina una vía importante de exposición. No convierte en fiable un equipo de desarrollo comprometido ni sustituye las reglas de autorización que aplica el servicio que recibe la solicitud.

¿Qué debo comprobar antes de aprobar una sesión de agente de IA?

Aprueba una sesión cuando reconoces el proceso que la inicia, su autoridad de firma y el trabajo que está a punto de realizar. No apruebes una sesión solo porque el aviso mencione el nombre de un proyecto conocido; los avisos no establecen la identidad del proceso.

¿Qué acciones del agente necesitan aprobación en cada ocasión?

Usa la aprobación por llamada para credenciales capaces de producir efectos irreversibles, como un token de despliegue en producción, una operación de pago o una API administrativa destructiva. Las llamadas rutinarias de solo lectura dejan de ser prácticas si requieren la misma interrupción cada vez.

¿Es seguro usar SSH mediante una pasarela de acciones para agentes?

No. La ejecución de comandos SSH solo constituye un control local cuando el destino, la cuenta, el alcance de los comandos y la confianza en el host están limitados en algún punto. Una clave privada protegida aún puede autorizar un comando peligroso en un host donde la cuenta tenga permisos amplios.

¿Puede el control local de acciones sustituir la autorización del servidor?

No. Las cuentas de servicio, los ejecutores de CI, la automatización de producción y las máquinas de compilación compartidas necesitan controles disponibles aunque ningún desarrollador esté frente a un Mac. En esos casos, sitúa la autorización y la auditoría cerca del servicio o de la carga de trabajo.

¿Cómo puedo comprobar que el proceso de agente es el que aprobé?

Empieza con ps para identificar la jerarquía de procesos y con codesign para revisar los detalles de firma del ejecutable. Después realiza una llamada inofensiva y comprueba qué recibe el agente, qué registra la pasarela de acciones y qué guarda en sus registros el servicio receptor.

¿MCP proporciona controles de aprobación para las herramientas de los agentes?

No. Un servidor MCP describe una interfaz de herramientas y un protocolo de solicitudes, pero no decide dónde se guardan las credenciales ni si una persona aprueba una acción. Trata la compatibilidad del transporte y la autorización de acciones como cuestiones de diseño independientes.

¿Basta un registro de auditoría resistente a manipulaciones para responsabilizar a un agente?

Un registro resistente a manipulaciones ayuda a detectar cambios en el historial y a reconstruir lo que se ejecutó, pero no puede deshacer una acción ni decidir si estaba autorizada. Conserva el diario de la pasarela, el registro de auditoría del servicio y los registros de despliegue, porque cada uno responde a una pregunta distinta.

¿Qué controles locales del agente nunca deben quedar en manos del propio agente?

No permitas que el agente elija el modo de aprobación, el alcance del destino o la clase de credencial desde su propio aviso o configuración. El operador debe establecer esos límites fuera del espacio de trabajo editable por el agente y probar la denegación y la revocación antes de confiar en ellos.

Sallyport

Sallyport ejecuta llamadas de API y comandos SSH por tu agente de IA. Las claves se quedan en una bóveda local de tu Mac; tú apruebas cada ejecución y cada acción queda en un registro sellado.

© 2026 Sallyport · Código abierto bajo Apache-2.0 · Oleg Sotnikov