# ¿Puede la aprobación de scripts para agentes de IA confiar en un intérprete firmado?

Un intérprete firmado no demuestra qué script aprobaste. Demuestra algo más concreto: que el sistema operativo inició un binario de intérprete determinado cuya firma pudo validar. Después, el intérprete puede leer cualquier cantidad de archivos modificables, aceptar código desde una cadena de comandos, cargar código mediante un resolvedor de paquetes y actuar con credenciales que el propio script nunca tuvo.

Esa diferencia se pierde cuando una tarjeta de aprobación dice «Python» o «Node» y muestra una tranquilizadora insignia de firma. He visto a revisores aprobar esa solicitud porque el nombre del binario les resultaba familiar y pasar después una tarde desagradable averiguando qué copia del repositorio, enlace simbólico, configuración del entorno o precarga de paquetes había proporcionado realmente el código. Los ejecutables conocidos merecen una confianza adecuada para los ejecutables. No convierten cualquier código fuente en código confiable.

## Un intérprete firmado solo identifica al intérprete

La firma de código responde a una pregunta de procedencia sobre un archivo ejecutable. En macOS, la herramienta `codesign` puede inspeccionar la firma de un ejecutable y su requisito designado. Gatekeeper y las protecciones de ejecución de la plataforma usan información relacionada al evaluar el software. Ninguno de esos mecanismos afirma que un archivo de Python pasado en la línea de comandos proceda del mismo desarrollador, que no haya cambiado desde la revisión ni que sus imports sean benignos.

Considera estas dos invocaciones:

```text
/usr/bin/python3 /Users/dev/work/release/publish.py
/usr/bin/python3 -c "import os; os.system('curl ...')"
```

La identidad del intérprete puede ser idéntica en ambos casos. La identidad del código fuente es completamente distinta. La primera llamada contiene una ruta que puede ayudar al revisor a encontrar el código. La segunda no tiene ningún archivo de script. Una solicitud que reduzca cualquiera de las dos a «Python firmado solicita acceso a la red» elimina justo la información que una persona necesita para juzgarla.

La documentación de Python describe formas distintas de línea de comandos para ejecutar un archivo, ejecutar un comando con `-c`, ejecutar un módulo con `-m` y leer código desde la entrada estándar. Es un comportamiento normal del intérprete, no un defecto. El error consiste en tratar esas formas como si todas tuvieran detrás un único programa estable y firmado.

El mismo error aparece con la identidad del proceso. Un sistema de aprobación puede decirte correctamente que un proceso del agente procede de una autoridad de firma de código que reconoces. Aun así, no puede deducir que todos los archivos locales que el proceso pida ejecutar a un intérprete merezcan la misma aprobación. La identidad del solicitante y la identidad del código fuente responden a preguntas distintas:

- La identidad del solicitante pregunta quién inició la solicitud.
- La identidad del intérprete pregunta qué binario analiza el código fuente.
- La identidad del código fuente pregunta qué bytes analizará el intérprete.
- La identidad de la acción pregunta qué host, ruta de API, cuenta o comando recibe la solicitud resultante.

Si una pantalla de revisión solo tiene espacio para las dos primeras, transmite una falsa sensación de precisión. En la práctica, el código fuente y la acción determinan si la solicitud es aceptable.

## El objetivo de la revisión es una tupla de ejecución

El revisor necesita una descripción estable de la ejecución exacta, no una etiqueta amigable. Llamo a esa descripción tupla de ejecución: el intérprete resuelto, sus argumentos, el artefacto de código fuente, el contexto de ejecución y la acción externa solicitada. Si cambia un miembro importante de la tupla, tienes una ejecución distinta que necesita una nueva decisión.

Para una llamada de Python basada en un archivo, la tupla mínima útil se parece a esto:

```json
{
  "caller": {
    "pid": 48172,
    "signing_authority": "Example Development Team"
  },
  "interpreter": {
    "resolved_path": "/usr/local/bin/python3.12",
    "signing_identity": "Python Software Foundation",
    "sha256": "c44e...9a10"
  },
  "argv": ["/usr/local/bin/python3.12", "/private/var/run/gateway-src/publish.py"],
  "source": {
    "display_path": "/Users/dev/work/release/publish.py",
    "sha256": "6ab1...ee42"
  },
  "context": {
    "working_directory": "/Users/dev/work/release",
    "environment": {"DEPLOY_ENV": "staging"}
  },
  "requested_action": "POST https://api.example.invalid/releases"
}
```

Los digest abreviados deben aparecer en la tarjeta de aprobación, pero el digest completo debe quedar en el registro. Normalmente, el revisor necesita la ruta legible y una diferencia o vista previa del código. El investigador necesita un valor inequívoco que pueda comparar más adelante.

No pongas todas las variables de entorno en la tarjeta. Eso convierte una decisión en una prueba de agudeza visual. Captura los valores que afectan a la selección del código, la resolución de comandos, las credenciales, el enrutamiento del proxy, la selección del destino y los interruptores de funciones. En Python pueden incluir `PYTHONPATH`, `PYTHONHOME` y una ruta de configuración proporcionada de forma explícita. En la ejecución de shell suelen incluir `PATH`, el directorio actual y las variables interpoladas en el comando. Registra el entorno completo en datos de auditoría protegidos si tu modelo de amenazas lo requiere y muestra a la persona el subconjunto relevante.

Aquí los equipos también confunden una segunda diferencia: la reproducibilidad no es autorización. Un lockfile, un commit de Git o una imagen de contenedor pueden ayudar a reproducir lo que se ejecutó. No indican si ese código debería llamar a producción, borrar una rama remota o abrir una sesión SSH. Combina la identidad del código fuente con una solicitud de acción clara.

## Python puede ocultar código detrás de lanzadores habituales

Python hace que una invocación a un archivo normal parezca más sencilla de lo que es. `python deploy.py` indica dónde comienza la ejecución, pero el intérprete puede importar módulos desde el directorio del script, los paquetes instalados, las rutas de búsqueda configuradas y el código seleccionado por la lógica de la aplicación. Un entorno virtual también puede cambiar a qué intérprete resuelve un `python` sin ruta completa.

Empieza por resolver el ejecutable antes de evaluar su firma. Las etiquetas `python`, `python3` o `venv/bin/python` no son identidades. Un lanzador puede ser un enlace simbólico, un shim o un binario diferente después de una actualización de la cadena de herramientas. La pasarela debe resolver el objeto que iniciará el kernel, inspeccionarlo y registrar su ruta y su digest.

Después, trata la ruta del código fuente como una ayuda visual, no como el límite de seguridad. Resuelve los enlaces simbólicos hasta una ubicación canónica para el registro de revisión, pero no ejecutes después de la revisión el original modificable. Una copia del repositorio puede reemplazar `deploy.py` sin cambiar la ruta. Un enlace simbólico puede apuntar a otro destino. Una comprobación de ruta no detecta por sí sola ninguno de esos eventos.

Una secuencia práctica es:

1. Lee los bytes del script solicitado y calcula su SHA-256.
2. Copia esos bytes en un directorio privado propiedad de la pasarela y con permisos restrictivos.
3. Muestra para la aprobación la ruta del solicitante, la ruta canónica, el digest y una vista previa del código.
4. Invoca el intérprete verificado con la copia privada y registra el resultado asociado a ese digest.

Esa copia no es trabajo innecesario. Cierra la brecha entre la comprobación y el uso. Si el revisor aprobó el digest `6ab1...ee42`, el intérprete debe leer los bytes cuyo digest es `6ab1...ee42`. Calcular el hash del archivo del repositorio y pedir después a Python que vuelva a leerlo deja una ventana pequeña, pero real, para que alguien lo reemplace.

Los imports también necesitan una decisión. Si `publish.py` importa un archivo local como `release_helpers.py`, un helper modificado puede cambiar el comportamiento aunque el archivo de entrada permanezca fijo. La opción estricta consiste en usar un manifiesto de código fuente que incluya todos los módulos locales permitidos para esa ejecución. Una opción más práctica para el trabajo habitual es preparar juntos el script de entrada y su árbol de paquetes locales declarado, rechazar imports fuera de ese árbol preparado y exigir una nueva aprobación cuando cambie el digest del manifiesto.

No finjas que esto detecta imports dinámicos, extensiones nativas, `sitecustomize` o código obtenido en tiempo de ejecución. No lo hace. La pantalla de aprobación debe indicar esas excepciones cuando existan. Un script que dice `importlib.import_module(os.environ["PLUGIN"])` no merece la misma aprobación amplia que un script autosuficiente solo porque ambos comienzan con el mismo intérprete firmado.

## El archivo de entrada de Node es solo una parte del programa

Node añade otra capa de ambigüedad. `node task.js` tiene un archivo de entrada, pero la resolución de módulos puede seleccionar código mediante `package.json`, las exportaciones de paquetes, lockfiles, enlaces simbólicos y el directorio actual. La documentación de la CLI de Node también describe precargas como `--require` y `--import`, que pueden ejecutar código antes de que comience el archivo de entrada.

Eso significa que un sistema de revisión debe mostrar el vector de argumentos completo, no solo la ruta final `.js`. Estas llamadas merecen un análisis distinto:

```text
node tools/publish.mjs
node --import ./tools/setup.mjs tools/publish.mjs
node --require ./tools/patch.cjs tools/publish.mjs
node -e "require('child_process').execSync(process.argv[1])" "git push --force"
```

Un revisor que solo vea `tools/publish.mjs` no detectará el código que se ejecuta antes en la segunda y la tercera llamada. En la última llamada no hay ningún archivo de entrada revisado. La cadena de comandos es el artefacto de código fuente y debe mostrarse, guardarse y someterse a hash como tal.

La variable de entorno `NODE_OPTIONS` de Node merece el mismo tratamiento. Node la documenta como una forma de pasar opciones de línea de comandos permitidas a través del entorno. Si un proceso puede proporcionar precargas o activar comportamientos de depuración mediante ella, una pasarela que la ignore ha inspeccionado un comando incompleto. No necesitas abrumar a los revisores con cada ajuste del entorno de ejecución, pero sí debes mostrar los que hagan que se cargue código o cambien la selección del destino.

Los gestores de paquetes crean otra trampa. `npm run publish` suele parecer una tarea con nombre, pero el comportamiento procede de un `package.json` modificable, sus scripts, el lockfile, los hooks del gestor de paquetes y los binarios encontrados en el árbol de dependencias del proyecto. La tarea debe expandirse antes de aprobarse. Muestra el comando resuelto, la revisión del proyecto o el manifiesto preparado y cada hook del ciclo de vida que se ejecutará. Si no puedes obtener esa expansión, solicita una aprobación más limitada o rechaza la petición. «Ejecutar script del paquete» no describe una acción significativa cuando el archivo del paquete puede cambiar mientras tanto.

Para automatizaciones repetibles con Node, prepara una instantánea revisada del espacio de trabajo o usa un artefacto de compilación inmutable. El hash de `publish.mjs` por sí solo sirve únicamente cuando el script no tiene dependencias locales ni una ruta de precarga. La mayoría de los proyectos que no son triviales no cumplen esa condición.

## Las cadenas de shell deben tratarse como código fuente

Las solicitudes de shell fallan en la revisión cuando alguien las llama comandos en lugar de programas. `sh -c` analiza una cadena de código fuente con expansiones, sustituciones, redirecciones, tuberías, funciones y búsqueda de comandos. La cadena puede ser corta, pero puede invocar una secuencia ilimitada de otros programas.

Compara estas solicitudes:

```text
/bin/sh -c 'curl -fsS "$RELEASE_URL" | sh'
/bin/sh /private/var/run/gateway-src/release.sh
```

La primera solicitud necesita la cadena de comandos exacta, cada valor relevante del entorno y una explicación de lo que recibe el programa posterior. La segunda necesita el mismo tratamiento de ruta y contenido del script que Python o Node. La firma de `/bin/sh` te dice quién proporcionó el analizador. No autoriza ninguna de las dos entradas de código fuente.

No apruebes un comando de shell por su primer verbo. `git status` puede ser inofensivo con un vector de argumentos exacto, mientras que `git -c credential.helper=...` cambia las entradas que cargará Git. `curl` puede obtener datos, escribir un archivo o enviar bytes a otro intérprete mediante una tubería. El revisor necesita suficiente sintaxis para ver las redirecciones y sustituciones, además de suficiente contexto de ejecución para saber cómo se resuelven los programas.

A menudo se pasa por alto `PATH`. Un script que invoca `deploy` sin una ruta absoluta delega la selección del ejecutable en el entorno. Si la solicitud procede del espacio de trabajo de un agente, un atacante que pueda modificarlo puede colocar un programa antes en `PATH`. Cuando sea posible, captura la ruta del ejecutable resuelto para cada subcomando sensible desde el punto de vista de la seguridad. Cuando la evaluación completa del shell sea demasiado insegura o incierta, usa una interfaz de comandos limitada en lugar de intentar construir un analizador de shell perfecto.

Este último punto va en contra de una recomendación popular: «Permite un shell firmado y muestra una solicitud en cada llamada». Es popular porque el shell existe en todas partes y la aprobación parece sencilla. Es incorrecta porque una sola aprobación no tiene un objeto de código fuente estable a menos que el sistema registre la cadena exacta o el script inmutable junto con el contexto relevante. La aprobación por llamada también puede aprobar algo equivocado con una coherencia admirable.

## Un hash de contenido necesita un archivo al que pueda vincularse realmente

Un hash aporta evidencia sobre unos bytes, no demuestra que el programa previsto vaya a usarlos. La implementación debe vincular los bytes revisados con la ejecución. Ahí es donde fallan muchos diseños que, por lo demás, son cuidadosos.

El patrón inseguro es fácil de reconocer:

```text
1. Read /workspace/scripts/publish.py
2. Calculate and display SHA-256
3. Wait for approval
4. Run python /workspace/scripts/publish.py
```

Entre los pasos 2 y 4, otro proceso puede editar el archivo, reemplazar un enlace simbólico o cambiar un directorio montado. El registro de aprobación sigue siendo exacto respecto a lo que vio el revisor, pero deja de decir qué se ejecutó.

Usa uno de estos modelos:

- Copia los bytes revisados en un directorio privado de ejecución, establece permisos que impidan que el proceso solicitante los modifique y ejecuta la copia.
- Ejecuta un artefacto inmutable creado previamente cuyo digest se haya revisado y registrado.
- Conserva un objeto de archivo ya abierto durante el proceso de comprobación y ejecución solo si el intérprete y el sistema operativo permiten ejecutar ese objeto exacto sin volver a resolver una ruta modificable.

El modelo de la copia privada suele ser más fácil de explicar y auditar. También te proporciona un artefacto estable para investigar incidentes. Conserva la ruta original que se mostró como contexto, porque las personas necesitan saber qué archivo del proyecto provocó la ejecución, pero no la confundas con los bytes que se ejecutaron.

El hash del contenido tiene límites que deben seguir visibles. No puede decidir si el código fuente es seguro. No puede estabilizar respuestas remotas, comportamientos dependientes del tiempo, valores aleatorios ni código cargado después del hash. Sí evita una clase concreta de error de aprobación: revisar una versión local del script y ejecutar otra. Es un límite valioso, siempre que lo describas con honestidad.

Usa SHA-256 u otro digest criptográfico actual, con una codificación fija, e incluye siempre el nombre del algoritmo en los registros. Una cadena hexadecimal sin más invita a confusiones futuras. Un registro de digest debe decir `sha256:6ab1...ee42`, no solo `6ab1...ee42`.

## Las tarjetas de aprobación deben mostrar pruebas útiles para una persona

Una buena tarjeta de aprobación permite que el revisor decida con rapidez sin ocultar los hechos que cambian la decisión. No empieces con el hash del archivo. Las personas no pueden evaluar un hash a simple vista. Empieza con la acción solicitada y el solicitante, y muestra después el intérprete, la ubicación del código fuente, su estado y el contexto de ejecución relevante.

Para una solicitud de despliegue, una tarjeta compacta podría decir:

```text
Caller: signed process from Example Development Team, PID 48172
Action: POST release data to api.example.invalid
Interpreter: /usr/local/bin/python3.12, signed by Python Software Foundation
Source: /Users/dev/work/release/publish.py
Reviewed bytes: sha256:6ab1...ee42
Execution copy: /private/var/run/gateway-src/6ab1...ee42/publish.py
Context: DEPLOY_ENV=staging, working directory /Users/dev/work/release
```

La tarjeta debe ofrecer la vista previa del código o una diferencia frente al último digest aprobado. Para el trabajo repetido, una diferencia suele ser mejor porque dirige la atención a las líneas modificadas. Aun así, proporciona el código completo cuando se solicite. Una vista previa recortada de forma engañosa es peor que no mostrar ninguna.

Evita etiquetas de aprobación vagas como «Permitir herramientas de despliegue» o «Permitir acceso de Python». Enseñan a las personas a hacer clic por reconocimiento de marca. Una decisión también debe indicar su duración. Una ejecución, una sesión de proceso y una versión de artefacto revisada son alcances distintos. La aprobación de una sesión para un proceso de agente puede reducir la fatiga causada por las solicitudes, pero cualquier script cuyo digest cambie debe activar una nueva decisión sobre el código fuente antes de reutilizar la autoridad externa.

Esto es distinto de un motor de reglas. No necesitas un lenguaje para que las personas escriban condiciones como «permitir scripts seguros». Necesitas un objeto de revisión fijo que no pueda ampliarse silenciosamente después de la aprobación. El sistema debe construir ese objeto a partir de entradas resueltas, mostrarlo y vincular la ejecución a él.

## Los límites de las dependencias deben ser explícitos

El digest de un script de entrada solo basta cuando el límite de código del programa es realmente ese único archivo. Trátalo como una excepción, no como el caso predeterminado. Los imports de Python, los módulos de Node, los comandos `source` del shell, las plantillas, los archivos de configuración y los plugins ejecutables pueden cambiar el comportamiento después de que el punto de entrada supere la revisión.

Define el límite según las consecuencias de la acción. Para una lectura de bajo riesgo contra un servicio de desarrollo, quizá aceptes un script de entrada preparado y una declaración explícita de que puede importar paquetes instalados. Para una escritura en producción o un comando SSH, incluye las dependencias locales del código fuente en un manifiesto, fija las dependencias externas y rechaza las descargas de código ejecutable durante la ejecución. Así, el registro explica qué querías decir con «el script».

Un manifiesto sencillo puede contener rutas relativas y digests:

```text
sha256  publish.py  6ab1...ee42
sha256  release_helpers.py  9d07...1a3c
sha256  config/targets.json  743e...64b1
```

La pasarela debe calcular este manifiesto a partir de copias preparadas o de una entrada de compilación controlada, en lugar de aceptar un manifiesto proporcionado por el mismo espacio de trabajo modificable. Si un proyecto declara un lockfile como parte de su límite, calcula también el hash de ese lockfile. Un lockfile solo ayuda cuando el entorno de ejecución lo respeta y el proceso revisado no puede sustituir el árbol de dependencias por otro.

Llega un momento en que la ejecución local basada en un intérprete es demasiado amplia para una decisión de un solo clic. Si el código puede descubrir plugins en directorios arbitrarios, obtener y ejecutar contenido remoto, o escribir y ejecutar scripts generados, divide el trabajo. Aprueba una compilación que produzca un artefacto inmutable, inspecciona la acción declarada por el artefacto y aprueba después esa acción. Ese límite adicional cuesta menos que reconstruir un cambio accidental en producción.

## Los registros deben responder qué se ejecutó, no cómo lo llamó la interfaz

Cuando algo sale mal, la primera pregunta útil suele ser «¿qué se ejecutó exactamente con esa autoridad?». Una entrada de registro que diga «Python aprobado» no puede responderla. Conserva la tupla de ejecución, el alcance de la decisión, la interacción del revisor, las marcas de tiempo y el resultado observado. Oculta los secretos del registro, pero no ocultes la identidad del artefacto de código fuente ni la acción de destino.

Un registro sólido conecta los eventos relacionados. La entrada de sesión identifica el proceso del agente y su autoridad. La entrada de acción identifica el intérprete, el digest del código fuente preparado, los argumentos, el destino y el resultado. Si un revisor revoca una sesión, ese evento debe conectarse con la misma identidad de sesión. De lo contrario, los operadores no pueden saber si la revocación detuvo al solicitante que realizó la llamada.

La evidencia contra manipulaciones mejora la calidad del registro. Un registro encadenado mediante hashes puede hacer detectables las alteraciones posteriores, pero no corrige los campos ausentes. Verifica la cadena y pregunta también si registra la ruta resuelta, el digest del código fuente, el contexto y la acción real. La integridad conserva las pruebas; no crea pruebas que el sistema nunca capturó.

Los registros divididos de Sallyport resultan útiles aquí porque separan la ejecución del agente de las llamadas HTTP o SSH individuales, al tiempo que proyectan ambas desde un único registro de auditoría cifrado y encadenado mediante hashes. Su comando `sp audit verify` puede verificar la cadena sin conexión sobre el texto cifrado, que es la propiedad adecuada para comprobar si las pruebas conservadas cambiaron después.

## Trata los cambios del código fuente como una nueva autoridad

La opción predeterminada más segura es sencilla: cuando cambie el digest del código fuente, exige una nueva decisión para una acción externa. No arrastres silenciosamente una aprobación anterior del script solo porque la ruta, el intérprete, el nombre del proyecto o la firma del proceso parezcan conocidos.

Esta regla generará algunas solicitudes más durante el desarrollo activo. Es lo adecuado. La revisión del código cambia la autoridad cuando el código puede gastar una credencial, modificar un servicio remoto o ejecutar un comando SSH. La respuesta no consiste en ocultar todas las solicitudes. Mejora el artefacto de revisión, prepara código determinista y concede la sesión de proceso más amplia solo cuando el código fuente siga identificado de forma independiente.

Para los equipos que usan una pasarela de acciones, mantén el control centrado en el punto donde las credenciales salen del equipo local. El agente debe solicitar una llamada HTTP o un comando SSH con un registro de revisión vinculado al código fuente, mientras la pasarela conserva la credencial y devuelve el resultado. Así se evita entregar secretos a un script modificable, pero sigue siendo necesario indicar con claridad qué código solicitó la acción.

Empieza por tu llamada de intérprete más sensible. Resuelve el binario, muestra los argumentos completos, calcula el hash y prepara los bytes reales del código fuente, registra el contexto relevante y haz que un digest distinto signifique una aprobación distinta. Cuando exista ese registro, un intérprete firmado se convierte en una prueba útil dentro de una decisión completa, en lugar de ser una etiqueta tranquilizadora sobre un script desconocido.
