8 min de lectura

¿Funciona la limpieza de procesos remotos después de cancelar SSH?

La limpieza de procesos remotos tras cancelar SSH necesita grupos de procesos, estados duraderos, pruebas de procesos hijos y un plan honesto para las desconexiones.

¿Funciona la limpieza de procesos remotos después de cancelar SSH?

Una ejecución cancelada del agente no equivale a un comando remoto detenido. El proceso local puede salir correctamente mientras el transporte SSH sigue activo, el transporte puede desaparecer mientras el shell remoto continúa ejecutándose, y el shell puede morir mientras sus hijos siguen en otro grupo de procesos. Si reduces todos esos eventos a un único estado llamado «cancelado», tarde o temprano dejarás activa una migración de base de datos, una instalación de paquetes, un worker de pruebas o una ayuda de despliegue después de que el agente haya informado que se detuvo.

La solución no es una trampa ingeniosa para señales. Necesitas un contrato de cancelación que identifique la ejecución remota, cree un límite terminable alrededor de sus descendientes, conserve suficiente salida para diagnosticar una interrupción y tenga una respuesta en el lado remoto para cuando desaparezca el controlador. Construye y prueba ese contrato antes de permitir que un agente ejecute comandos con efectos secundarios.

La cancelación SSH tiene cuatro saltos distintos

Una solicitud de cancelación debe atravesar cuatro límites: el agente decide detenerse, el supervisor local detiene o señala su cliente SSH, el protocolo SSH transporta un evento o una señal del canal y el host remoto actúa sobre ese evento. Cada salto puede fallar de forma independiente.

RFC 4254 separa estos conceptos. Define un mensaje de cierre del canal y, por separado, una solicitud de canal signal para nombres como TERM, INT y HUP. Cerrar el canal es un evento de transporte. No significa «envía SIGTERM a todos los descendientes remotos». El RFC también indica que, si es posible, los datos enviados antes del cierre deben entregarse. «Si es posible» deja mucho margen cuando un portátil entra en suspensión, desaparece una ruta de red o se fuerza el cierre de un proceso local.

Esa diferencia revela un diseño defectuoso muy común:

  1. Un agente inicia ssh host long-command.
  2. El usuario pulsa cancelar.
  3. El ejecutor del agente termina su proceso hijo local.
  4. La interfaz marca el trabajo como cancelado.
  5. long-command, o uno de sus hijos, sigue ejecutándose en el host.

La quinta línea no es un caso raro. Es el resultado predeterminado cuando el servidor no tiene motivos para terminar el comando o cuando el comando se separó de la sesión antes de que desapareciera la conexión.

Un contrato de cancelación útil especifica qué intentará el lado local y qué responsabilidad tiene el lado remoto:

  • El lanzador crea una ejecución remota identificable con un ID aleatorio.
  • El wrapper remoto inicia el trabajo en su propio grupo de procesos o sesión.
  • Una cancelación normal envía TERM a ese grupo y registra el resultado.
  • El wrapper escala a KILL solo después de un periodo de gracia definido.
  • Un plazo o una concesión remota termina el trabajo si el controlador nunca regresa.
  • La salida y el estado final sobreviven al flujo SSH.

No llames cancelada a una tarea hasta obtener uno de estos resultados: un registro remoto final confirmado o un resultado explícito de «estado desconocido». Fingir certeza después de perder la conexión ralentiza la respuesta a incidentes porque todos parten de una premisa falsa.

Un PID remoto no basta para limpiar los procesos hijos

Terminar el PID del shell remoto solo es seguro cuando el shell nunca crea procesos hijos, inicia una tubería, lanza un proceso en segundo plano ni invoca una herramienta que cree helpers. Son pocos los comandos que cumplen esa condición.

Considera este comando remoto habitual:

build-assets | tee build.log &
wait

El shell tiene un PID, pero la tubería tiene más procesos. tee puede seguir escribiendo en su registro después de que el shell salga. Un compilador puede iniciar workers. Un gestor de paquetes puede delegar el trabajo en un servicio. Si envías kill -TERM "$shell_pid", has terminado un miembro de un grupo mayor y sabes muy poco sobre los demás.

Los grupos de procesos proporcionan la unidad adecuada de cancelación para una ejecución remota breve. En Linux, cada proceso pertenece a un grupo de procesos y cada grupo pertenece a una sesión. Las señales generadas por el terminal llegan al grupo de procesos en primer plano, por eso el comportamiento de un terminal puede parecer más misterioso de lo que es. La documentación de Linux para setpgid(2) también deja claro que un hijo hereda el grupo de procesos de su padre salvo que algo lo cambie.

Para un comando controlado por un agente, crea una sesión nueva para el trabajo. Normalmente, el líder de la sesión tiene un PID igual a su PGID y SID. Entonces, un PID negativo en kill se dirige al grupo de procesos:

kill -TERM -- -"$pgid"

El signo menos inicial distingue terminar un proceso de terminar su grupo. -- también importa: evita que un valor mal formado se interprete como una opción.

No supongas sin más que el PID del trabajo es su ID de grupo. Compruébalo al iniciar. Un wrapper de shell, un gestor de servicios o un programa que llame a setpgid puede cambiar el árbol. Este es el comando de inspección mínimo que merece la pena conservar en tu kit de pruebas:

ps -o pid=,ppid=,pgid=,sid=,stat=,etime=,command= -p "$pid"

La salida habitual tiene esta forma:

24182  24177  24182  24182 Ss       00:03 bash ./worker.sh /tmp/agent-runs/6c4...

Aquí coinciden PID, PGID y SID. Eso demuestra que kill -TERM -- -24182 apunta al límite previsto. Si el PGID no coincide con el registro de la ejecución, falla el lanzamiento en lugar de adivinar.

Un grupo de procesos también tiene límites. Un hijo puede llamar a setsid, un runtime de contenedores puede mover un proceso a otro lugar y un trabajo puede pedir a un gestor de servicios que ejecute algo fuera del grupo. Ese comportamiento puede ser legítimo, pero significa que tu garantía de cancelación termina en esa transferencia. Trata el trabajo separado como un tipo distinto de tarea, con identidad, operación de detención y registro de auditoría propios.

Un wrapper de cancelación necesita una ruta de limpieza real

Un wrapper de shell remoto debe poseer el PID del trabajo, capturar las señales de terminación esperadas, dirigirse al grupo del trabajo, esperar brevemente y escribir un registro final. No debe usar pkill command-name, analizar una lista de procesos imprecisa ni terminar todos los procesos de una cuenta. Esos atajos funcionan hasta que dos ejecuciones comparten usuario, cambia el nombre de un host o el nombre de un comando coincide con el trabajo de otra persona.

Esta prueba orientada a Linux es deliberadamente sencilla. Crea un directorio protegido para la ejecución, inicia un trabajo en una sesión nueva, escribe la salida en archivos y termina el grupo del trabajo cuando el wrapper recibe TERM, INT o HUP.

#!/usr/bin/env bash
set -Eeuo pipefail

run_id=${1:?run ID required}
shift
run_dir="${HOME}/.agent-runs/${run_id}"
umask 077
mkdir -p "$run_dir"

child_pid=""
child_pgid=""
finished=0

write_status() {
  local state=$1
  local code=${2:-}
  local tmp="$run_dir/status.tmp"
  printf '{"run_id":"%s","state":"%s","exit_code":"%s"}\n' \
    "$run_id" "$state" "$code" >"$tmp"
  mv "$tmp" "$run_dir/status.json"
}

stop_group() {
  if [[ -z ${child_pgid:-} ]]; then
    return
  fi

  kill -TERM -- "-$child_pgid" 2>/dev/null || true
  for _ in 1 2 3 4 5; do
    if ! kill -0 -- "-$child_pgid" 2>/dev/null; then
      return
    fi
    sleep 1
  done
  kill -KILL -- "-$child_pgid" 2>/dev/null || true
}

cancel() {
  local signal=$1
  trap - TERM INT HUP
  write_status "cancelling:$signal"
  stop_group
  wait "$child_pid" 2>/dev/null || true
  write_status "cancelled:$signal"
  finished=1
  exit 143
}

trap 'cancel TERM' TERM
trap 'cancel INT' INT
trap 'cancel HUP' HUP

write_status "starting"
setsid "$@" >"$run_dir/stdout.log" 2>"$run_dir/stderr.log" &
child_pid=$!
child_pgid=$(ps -o pgid= -p "$child_pid" | tr -d ' ')

if [[ "$child_pgid" != "$child_pid" ]]; then
  printf 'unexpected PGID for %s: %s\n' "$child_pid" "$child_pgid" \
    >"$run_dir/stderr.log"
  kill -TERM "$child_pid" 2>/dev/null || true
  write_status "launch_failed"
  exit 70
fi

printf '%s\n' "$child_pid" >"$run_dir/pid"
printf '%s\n' "$child_pgid" >"$run_dir/pgid"
write_status "running"

set +e
wait "$child_pid"
code=$?
set -e

if [[ $finished -eq 0 ]]; then
  write_status "finished" "$code"
fi
exit "$code"

Este wrapper evita un fallo concreto: la señal de cancelación llega al wrapper, pero este solo se termina a sí mismo y deja el trabajo abandonado. No promete detener descendientes que se hayan separado deliberadamente. Tampoco debería prometerlo.

El manual de Linux para setsid(2) indica que setsid() crea una sesión nueva y convierte al proceso que llama en líder de un grupo de procesos nuevo, inicialmente sin terminal de control. El comando setsid de util-linux ejecuta un programa en esa sesión y crea un proceso hijo cuando es necesario. Por eso este mecanismo es un límite práctico para una ejecución, no un interruptor mágico de limpieza.

Mantén el wrapper reducido. Debe iniciar, registrar, detener e informar. No escondas en él la lógica de negocio. El trabajo debe tener sus propias reglas para transacciones, archivos temporales e idempotencia.

Una desconexión limpia del transporte no garantiza la limpieza

Los usuarios de SSH suelen deducir demasiado de una prueba en un terminal. Ejecutan un comando con PTY, cierran el terminal, ven que un proceso termina después de SIGHUP y concluyen que la limpieza tras una desconexión funciona. Después un agente usa un canal SSH no interactivo sin PTY y el comportamiento cambia.

Un PTY crea semántica de terminal. La desconexión de un terminal puede provocar la entrega de SIGHUP, pero solo bajo las condiciones que rigen los terminales de control y los grupos de procesos en primer plano. El manual de Linux describe SIGHUP como una desconexión de un terminal de control o la muerte de un proceso de control. Eso no significa que cada desconexión SSH envíe señales a todos los procesos iniciados mediante SSH.

SSH no interactivo suele ser el mejor valor predeterminado para los agentes porque ofrece una salida más limpia y menos sorpresas de los scripts de inicio del shell. También elimina cualquier dependencia accidental del comportamiento del terminal. Usa un PTY solo cuando el programa remoto lo necesite, por ejemplo, un instalador antiguo que se niegue a ejecutarse de otro modo. Documenta entonces que el PTY forma parte del comportamiento del comando y pruébalo por separado.

Conviene distinguir tres casos de desconexión:

El cliente envía una cancelación deliberada

El supervisor local aún tiene una conexión activa y puede enviar una señal del protocolo, o abrir una acción de control autenticada independiente que señale el PGID registrado. Este es el mejor caso. El wrapper remoto recibe TERM, limpia el grupo y escribe cancelled:TERM.

No dependas de que una SIGINT local recibida por un cliente SSH haga exactamente esto si no has probado la biblioteca y la invocación que distribuyes. Un cliente de terminal, una biblioteca SSH integrada y una herramienta MCP pueden traducir la cancelación local de forma distinta. Algunos cierran un socket, otros terminan el proceso local y otros pueden enviar una solicitud SSH signal. Son implementaciones distintas de una interfaz que los usuarios llaman «cancelar».

El cliente local falla o pierde la red

El comando remoto puede continuar. El servidor no puede distinguir un problema temporal de enrutamiento de un usuario que quiere que el trabajo continúe si tu protocolo no le indica cómo hacerlo. Una concesión remota es la respuesta honesta.

Al iniciar, escribe un deadline_epoch en el directorio remoto de la ejecución. Un supervisor local lo renueva mientras la ejecución siga autorizada. Un proceso de vigilancia remoto lo comprueba y llama a la misma ruta de limpieza del grupo cuando caduca. Elige una duración que encaje con la operación. Una concesión de cinco minutos puede servir para un comando de shell, pero ser imprudente para una compilación con fases silenciosas largas y normales.

El host remoto falla o se reinicia

Puedes perder tanto el proceso como el estado final. No informes «cancelado» ni «completado» solo porque terminó la conexión SSH. Marca la ejecución como desconocida hasta que una reconciliación posterior lea el diario del host, el estado del despliegue, el registro de bloqueos o el resultado específico de la aplicación.

La dificultad no está en emitir una palabra de estado. Está en negarte a emitir una palabra que tu sistema no puede respaldar.

La salida parcial demuestra observación, no finalización

Deja pruebas de la cancelación
El registro de actividad guarda cada llamada SSH, incluida la acción de control usada para cancelar una ejecución remota.

Un flujo responde a «¿qué bytes ha recibido el cliente hasta ahora?». No responde a «¿en qué estado dejó el comando remoto el sistema?». El error aparece cuando un programa remoto imprime done antes de vaciar su último archivo o cuando la red cae después de que el comando termine, pero antes de que el cliente reciba el estado de salida SSH.

Conserva dos registros:

  • stdout.log y stderr.log contienen la salida de diagnóstico mientras el trabajo la escribe.
  • status.json es un registro final pequeño que se escribe atómicamente después de que el wrapper observe la salida o gestione la cancelación.

El mv del wrapper importa. Escribe un archivo de estado temporal en el mismo directorio y luego cámbialo de nombre para colocarlo en su sitio. Los lectores verán el archivo anterior completo o el nuevo completo. Nunca deberían analizar la mitad de un documento JSON e inventar un resultado.

La salida también necesita reglas. Un comando puede almacenar mucha salida en búfer cuando stdout es un archivo en vez de un terminal. Si el progreso importa, haz que el trabajo emita estados explícitos por líneas en stderr o usa un archivo de progreso a nivel de aplicación. No resuelvas el almacenamiento en búfer asignando un PTY a todos los comandos. Eso cambia el comportamiento y puede mezclar stdout con stderr, lo que empeora la auditoría y el análisis de fallos.

Trata estos casos de forma distinta en la interfaz o el diario del agente:

Registro remotoEstado del flujoSignificado
finished, código de salida presentecompletoEl wrapper observó una finalización normal.
cancelled:TERMpuede terminar abruptamenteEl wrapper inició la cancelación y detuvo el grupo.
Solo cancelling:TERMdesconectadoLa limpieza comenzó, pero no se observó el registro final. Hay que reconciliarlo.
Solo running, concesión válidadesconectadoEl trabajo puede seguir activo. No lo reintentes a ciegas.
Ningún registro utilizabledesconectadoEl estado es desconocido. Comprueba los efectos antes de volver a lanzarlo.

Evita colocar tokens de acceso, volcados de configuración sin redactar o credenciales en estos registros. La inyección de credenciales SSH puede mantener la clave privada fuera del agente, pero un comando remoto aún puede imprimir secretos que haya leído de su propio entorno o configuración. La retención de la salida forma parte del diseño del comando, no es un detalle posterior.

Prueba árboles de procesos, no un único shell inactivo

trap 'exit' TERM; sleep 600 es una prueba de cancelación débil. Demuestra que un shell en primer plano puede recibir una señal, pero no prueba los hijos, los grupos de procesos, la limpieza retrasada, la persistencia de la salida ni la desaparición del shell remoto en el momento equivocado.

Usa un trabajo que cree un árbol de procesos visible y registre cada señal. Guarda esto como worker.sh en un host Linux desechable:

#!/usr/bin/env bash
set -Eeuo pipefail
run_dir=${1:?run directory required}

note() {
  printf '%s pid=%s pgid=%s %s\n' \
    "$(date +%s)" "$$" "$(ps -o pgid= -p $$ | tr -d ' ')" "$1" \
    >>"$run_dir/worker.log"
}

trap 'note TERM; exit 143' TERM
trap 'note INT; exit 130' INT
trap 'note HUP; exit 129' HUP

(
  trap 'note grandchild_TERM; exit 143' TERM
  trap 'note grandchild_HUP; exit 129' HUP
  while :; do
    note grandchild_tick
    sleep 1
  done
) &
grandchild=$!

note "started grandchild=$grandchild"
while :; do
  note parent_tick
  sleep 1
done

Ejecútalo mediante el wrapper con un ID de ejecución aleatorio. En otra sesión SSH, inspecciona el árbol de procesos y el directorio de la ejecución:

run_id=cancel-test-$(date +%s)
ssh host.example './remote-wrapper.sh '"$run_id"' ./worker.sh "'$HOME/.agent-runs/'"$run_id"'"'

Las comillas exactas cambiarán en un lanzador real. No pasa nada. Lo que no debe cambiar son las pruebas: necesitas el ID remoto, el PID del wrapper, el PGID del trabajo, las ubicaciones de los registros y el estado final.

Prueba después estas rutas de fallo, una por una:

  1. Envía TERM al PID del wrapper. Confirma que tanto el padre como el nieto registraron la terminación y que ps no encuentra ningún proceso en el PGID registrado.
  2. Envía TERM directamente al PID del trabajo. Confirma que no das por segura la reacción de sus hijos. Esta prueba explica por qué el wrapper se dirige a un grupo.
  3. Termina el cliente SSH local sin enviar una señal remota. Confirma que el trabajo permanece activo hasta que caduca su concesión remota. Si se detiene de inmediato, registra el motivo, como el comportamiento de desconexión de un PTY, en lugar de tratar el resultado como universal.
  4. Desconecta después de que el wrapper escriba cancelling:TERM, pero antes de escribir el registro final. Confirma que la reconciliación distingue una observación incompleta de un trabajo nuevo que sigue ejecutándose.
  5. Inicia dos ejecuciones con la misma cuenta, cancela una y demuestra que la otra sigue activa. Esto detecta las llamadas peligrosas a pkill y la limpieza de toda la cuenta.

Usa ps y pgrep -a -g "$pgid" durante las pruebas y repite la comprobación después del periodo de gracia. Revisa el archivo de estado remoto y los registros solo después de comprobar la tabla de procesos. Un registro que dice «cancelado» mientras los workers siguen activos es un error del supervisor, no una discrepancia de información inofensiva.

Las señales de muerte del padre solo ayudan dentro de un worker controlado

Dirige SSH a través de una única puerta de enlace
Usa el puente MCP incluido para dirigir las acciones SSH del agente a través de la aplicación firmada de Sallyport para macOS.

Linux proporciona PR_SET_PDEATHSIG, que permite a un proceso pedir al kernel que le envíe una señal cuando termina el hilo que lo creó. Puede resultar útil cuando controlas un helper nativo pequeño que crea un hijo inmediato y quieres que ese hijo se detenga si muere el helper. La configuración sobrevive a execve en los casos habituales, pero el manual documenta excepciones importantes, incluidos los cambios de credenciales.

Por sí sola, no resuelve la limpieza de un agente remoto.

Primero, el proceso del servidor SSH no es necesariamente el padre que te interesa. Segundo, la señal se aplica al proceso que la configuró, no automáticamente a todos sus descendientes. Tercero, un comando remoto que crea procesos, se duplica dos veces o entrega el trabajo a otro servicio abandona esa relación. Cuarto, es una función específica de Linux, algo importante si tu flota tiene hosts Unix variados.

Úsala solo como refuerzo cuando el árbol de procesos sea tuyo. Por ejemplo, un worker Linux pequeño puede configurar PR_SET_PDEATHSIG antes de ejecutar un único hijo controlado, mientras el wrapper exterior sigue siendo dueño del grupo de procesos y de la concesión. Eso proporciona dos detectores de fallo con alcances distintos. No permite omitir el límite del grupo ni el registro remoto final.

La misma advertencia se aplica a nohup, disown y setsid dentro del trabajo. Son útiles cuando alguien quiere deliberadamente que el trabajo sobreviva a un terminal. Son incompatibles con la promesa de que cancelar una ejecución del agente detiene el trabajo. Haz explícita la elección al iniciar.

Un segundo canal de control suele ser mejor que terminar el primero

Confirma cada detención destructiva
Exige Touch ID o un clic para cada uso de una clave SSH marcada para aprobación por llamada.

Cuando un agente cancela una llamada SSH en curso, su propio contexto local puede estar cerrándose. Depender de ese proceso moribundo para enviar una señal final del protocolo introduce carreras. Un proceso supervisor independiente debe encargarse de la cancelación y la reconciliación.

Un diseño viable sería el siguiente:

  1. El supervisor genera un ID de ejecución criptográficamente aleatorio e invoca el wrapper remoto.
  2. El wrapper registra su PID, PGID, hora de inicio y estado en un directorio cuyo nombre es el ID de ejecución.
  3. El supervisor registra el ID y el host remoto antes de empezar a consumir la salida.
  4. Al cancelar, el supervisor abre una acción de control nueva que lee el registro, comprueba la propiedad y la antigüedad esperadas y señala el PGID registrado.
  5. El supervisor consulta el registro de estado final hasta ver un estado terminal o alcanzar un plazo de información.

La acción de control debe validar el registro antes de señalar nada. Como mínimo, comprueba que el directorio pertenece al usuario esperado, que el PID sigue existiendo, que el PGID registrado coincide con ps y que la hora de inicio corresponde al proceso lanzado. Linux puede reutilizar los PID. Un archivo PID antiguo junto con un kill incondicional es la forma de que un script de limpieza fallido termine trabajo ajeno semanas después.

No escondas el comando de limpieza detrás de una instrucción genérica del agente como «termina el proceso de mi tarea anterior». El agente debe recibir un identificador opaco. El supervisor traduce ese identificador a una acción remota estrictamente delimitada. Esto también hace legible la auditoría: el revisor ve que la ejecución 6c4... solicitó cancelar el PGID 24182 en un host concreto, no que un agente construyó un comando kill arbitrario.

Para agentes de programación autónomos, Sallyport puede ejecutar acciones SSH sin exponer la clave SSH al agente. Así se separa la custodia de credenciales del contrato de cancelación, que sigue necesitando el identificador de ejecución, la comprobación del grupo, la concesión remota y el registro final.

La decisión de reintentar depende del efecto secundario

Un comando que solo lee un archivo normalmente puede reintentarse después de una desconexión desconocida. Un comando que crea un usuario, aplica una migración, rota un certificado o inicia un despliegue no. La capa SSH no puede decirte si la operación se volvió segura de repetir.

Asigna a los comandos remotos con efectos secundarios un token de idempotencia derivado del ID de ejecución. El programa remoto debe guardar el token junto con el resultado de la operación y devolver el resultado existente si vuelve a ver el mismo token. Si no es posible, añade una consulta previa que identifique si el cambio solicitado ya ocurrió.

No uses la limpieza como sustituto de la idempotencia. Incluso un TERM perfecto puede llegar después de que una API remota acepte una solicitud, pero antes de que el comando imprima su respuesta. Incluso un KILL puede llegar después de que se confirme una transacción de base de datos. La limpieza de procesos responde a si el worker sigue ejecutándose. No revierte los efectos externos.

Conserva tres resultados en el plano de control: completado, cancelado con limpieza confirmada y desconocido. El estado desconocido resulta incómodo, pero ofrece al siguiente operador la información que necesita: inspeccionar el estado remoto antes de emitir otra acción. Es mucho mejor que un botón verde de reintento basado en ilusiones.

Una función de cancelación está lista cuando puedes interrumpirla en cada límite y explicar el árbol de procesos superviviente, la salida en disco, el registro final y la decisión de reintento. Si no puedes hacerlo en un host desechable, no confíes en ella contra un host de producción a las dos de la mañana.

FAQ

¿Cómo detengo un comando SSH remoto cuando un agente de IA lo cancela?

Trata la cancelación como una acción de control independiente, no como la salida del cliente SSH local. Envía una señal a un grupo de procesos remoto registrado, espera un periodo de limpieza limitado y verifica después el estado final de la ejecución desde un registro duradero. Si la ruta de control ya desapareció, una concesión remota o un proceso de vigilancia debe decidir cuándo detener el trabajo.

¿Cerrar una conexión SSH termina el comando remoto?

No. Cerrar un canal SSH termina el canal, pero no garantiza que el servidor envíe SIGTERM al comando ni a sus descendientes. Un PTY puede cambiar el comportamiento mediante señales de desconexión del terminal, pero eso tampoco es un contrato de limpieza en el que debas confiar.

¿Cómo termino los procesos secundarios iniciados por un script de shell remoto?

Usa un grupo de procesos o una sesión exclusiva para cada ejecución remota y envía la señal al PGID negativo, por ejemplo, kill -TERM -- -12345. Comprueba antes el PGID. Terminar solo el PID del shell deja procesos en segundo plano, tuberías y subprocesos activos.

¿Debo usar setsid para los comandos remotos de un agente?

Una invocación normal de setsid crea una sesión y un grupo de procesos nuevos para el comando iniciado, lo que da al supervisor un único grupo que terminar. No lo uses para trabajos que deban sobrevivir a una cancelación. Verifica el SID y el PGID resultantes con ps, porque los wrappers y los gestores de servicios pueden modificar el árbol.

¿Cómo limpio los procesos después de una desconexión SSH o un fallo de red?

Un proceso remoto debe tener un tiempo máximo de ejecución o una concesión renovable si dejarlo activo sería inseguro o costoso. La concesión debe vivir en el host remoto, porque no se puede confiar en que un cliente desconectado informe de que ha desaparecido. Una señal de cancelación puede detener el trabajo rápidamente, mientras que la concesión cubre el caso en que esa señal nunca llegue.

¿Puedo confiar en una salida parcial después de que se interrumpa una conexión SSH?

Usa la salida del flujo para observar lo ocurrido, pero escríbela también en un archivo o diario remoto. Guarda un registro de finalización separado solo después de que el comando termine y la limpieza concluya. Un flujo interrumpido indica que falló la entrega, no si el comando terminó.

¿Basta PR_SET_PDEATHSIG para limpiar procesos remotos?

Es específica de Linux y solo ayuda a que un proceso detecte que murió su padre directo. PR_SET_PDEATHSIG no cubre automáticamente a los nietos, y los cambios de privilegios pueden borrar la configuración. Úsala como refuerzo local dentro de un worker diseñado para ello, no como único mecanismo para detener comandos arbitrarios.

¿Debe un agente asignar un PTY para los comandos SSH remotos?

Normalmente no. Un terminal cambia la entrega de señales, el almacenamiento en búfer y el comportamiento del programa, mientras que un comando no interactivo ofrece una salida más limpia para las máquinas. Asigna un PTY solo cuando el programa remoto necesite realmente semántica de terminal y prueba aparte el comportamiento ante la desconexión.

¿Qué debe contener un registro de auditoría de una cancelación SSH?

Guarda un ID de ejecución aleatorio, la identidad del host remoto, el PID inicial, el PGID, el SID, la hora de inicio, el comando solicitado y el resultado final. Protege el registro remoto frente a otros usuarios y actualiza el estado final de forma atómica. Un PID por sí solo es una prueba débil porque los PID pueden reutilizarse.

¿Puede un agente ejecutar trabajo destructivo de forma segura por SSH?

No envíes un comando SSH sin límite y llames cancelado al trabajo cuando salga el proceso local. Exige un identificador de ejecución, una comprobación del grupo de procesos, un plazo de limpieza y un estado remoto final. Sallyport puede mantener la credencial SSH fuera del agente, pero el diseño del comando remoto sigue determinando si la cancelación es real.

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