Читать 7 мин

Фоновые задания SSH переживают успешный вызов

Проверьте фоновые задания SSH, nohup, управление заданиями и отделённые процессы, чтобы не завершить аудит раньше времени.

Фоновые задания SSH переживают успешный вызов

Нулевой код выхода вызова SSH доказывает, что удалённая команда сообщила об успехе. Он не доказывает, что все порождённые ею процессы остановились, закончили запись или успешно выполнили задачу. Если команда запустила работу в фоне, главное действие может только начинаться в момент, когда инструмент уже отметил вызов как завершённый.

Я считаю этот разрыв границей аудита, а не занятной особенностью shell. Агент может запустить сценарий развёртывания, получить код 0 и пойти дальше, пока отделённая миграция продолжает менять данные. Журнал инструмента честно описывает канал SSH, но подталкивает к неверному выводу об удалённой работе. Нужно назвать жизненный цикл, который вы хотите наблюдать, проверить его в том же shell и с теми же условиями терминала, что использует агент, а подтверждение завершения получить от системы, отвечающей за долгоживущий процесс.

Ноль относится к удалённой команде, а не к её потомкам

OpenSSH возвращает код выхода удалённой команды или 255, если ошибка возникла в самом клиенте SSH. RFC 4254 формулирует точнее: когда команда на другой стороне завершается, сервер может отправить запрос канала exit-status, а затем закрыть канал. Ни один документ не обещает, что сервер рекурсивно дождётся каждого потомка команды.

Различие важно, когда shell выполняет асинхронный список. POSIX определяет команду с & в конце как асинхронную: shell запускает её и продолжает работу без ожидания. Если делать больше нечего, shell может успешно завершиться, пока асинхронный дочерний процесс ещё жив. Код SSH относится именно к этому shell.

Словом «успех» часто называют как минимум четыре разных результата:

  • Соединение SSH и аутентификация прошли успешно.
  • Удалённый shell принял и запустил команду.
  • Запущенная нагрузка завершилась с кодом 0.
  • Ожидаемый эффект сохранился и доступен для проверки.

Одно целое число не подтверждает все четыре результата. Понятная запись аудита должна указывать событие, которое его породило. Для результата канала я использую ssh_command_exit_status, а workload_result оставляю для доказательства от удалённого владельца работы.

То же предупреждение действует, когда удалённая программа сама превращается в демон. Пусковая программа может вернуть 0 после успешного fork, хотя её дочерний процесс через несколько секунд не сможет открыть базу данных, привязаться к порту или прочитать конфигурацию. Пусковая программа выполнила свой договор, аудитор выбрал не тот договор.

Разрыв показывает двенадцатисекундная проба

Такой обманчивый успех легко воспроизвести без демона, прав root и необычных настроек shell. Выполните команды под одноразовой учётной записью Unix. Явные перенаправления важны: они позволяют фоновому процессу отпустить канал SSH и продолжить работу.

ssh testhost 'rm -f /tmp/ssh-bg.done /tmp/ssh-bg.log; (sleep 12; date -u +%FT%TZ > /tmp/ssh-bg.done) > /tmp/ssh-bg.log 2>&1 < /dev/null & printf "launcher_pid=%s\n" "$!"'
printf 'ssh_status=%s\n' "$?"
ssh testhost 'test -f /tmp/ssh-bg.done; printf "done_status=%s\n" "$?"'
sleep 13
ssh testhost 'cat /tmp/ssh-bg.done'

Обычный немедленный результат выглядит так:

launcher_pid=41872
ssh_status=0
done_status=1
2026-07-24T10:14:05Z

PID и время будут другими. Противоречие здесь намеренное: ssh_status=0 и done_status=1 существуют одновременно, потому что отвечают на разные вопросы. Shell успешно запустил асинхронный список, но файл-маркер ещё не появился.

Не превращайте этот пример в производственную схему запуска. Файлы-маркеры в /tmp могут совпасть по имени, исчезнуть или быть подделаны другим процессом с достаточным доступом. Проба полезна тем, что делает время видимым. Для производственной записи о завершении нужны уникальный ID выполнения, защищённое хранилище, аутентифицированный автор и чётко определённое состояние ошибки.

Повторите пробу по тому же пути, которым пользуется агент. Прямая команда в терминале, неинтерактивный запрос SSH exec, вызов SSH с псевдотерминалом и шлюз инструментов могут выбрать разные стартовые файлы, shell и схемы файловых дескрипторов. Тест без этих деталей проверяет соседнюю систему.

Открытые дескрипторы создают видимость синхронной работы

Запуск в фоне и закрытие канала работают независимо. Дочерний процесс, унаследовавший стандартный вывод или поток ошибок канала SSH, может сохранять канал доступным для чтения после выхода удалённого shell. Локальный процесс ssh кажется ожидающим дочерний процесс, потому что канал ещё не получил конец файла, а не потому, что SSH следит за результатом потомка.

Сравните два вызова и измерьте время:

time ssh testhost 'sleep 12 &'
time ssh testhost 'sleep 12 > /tmp/sleep.log 2>&1 < /dev/null &'

В распространённых сочетаниях OpenSSH и shell первый вызов может оставаться открытым до завершения sleep, а второй быстро возвращает управление. Это наблюдение нужно проверять, оно не даёт переносимой гарантии. Результат могут изменить реализация shell, поведение сервера, выделение псевдотерминала и работа дочерней программы с дескрипторами.

Случайное ожидание служит слабым доказательством. Фоновый процесс может рано закрыть дескрипторы и продолжить работу. Он может породить внука, который их закроет. Вывод может идти через сокет или прямо в хранилище. И наоборот, вспомогательный процесс, который лишь держит stdout открытым, создаст видимость работы после сбоя главной задачи.

Дескрипторы всё равно стоит исследовать: они объясняют множество противоречивых тестов. В Linux сохраните удалённый PID и проверьте дескрипторы, пока вызов SSH активен:

pid=$(cat /run/user/$(id -u)/agent-job.pid)
ps -o pid=,ppid=,pgid=,sid=,stat=,etime=,args= -p "$pid"
ls -l "/proc/$pid/fd/0" "/proc/$pid/fd/1" "/proc/$pid/fd/2"

Запишите PID родителя, группу процессов, ID сеанса, состояние, время работы, команду и цели дескрипторов 0, 1 и 2. Если /proc недоступен, используйте штатные средства ОС. Не сводите проверку к pgrep name: имена совпадают, обёртки меняют имена, а PID после выхода может получить другой процесс.

nohup защищает от разрыва терминала, но не назначает владельца

nohup меняет обработку сигналов, чтобы запущенная команда игнорировала SIGHUP. Он не отправляет команду в фон. Руководство GNU Coreutils прямо говорит об этом и требует добавить & для асинхронного выполнения. В скопированных фрагментах развёртывания это условие часто теряется.

Правила перенаправления при работе через SSH тоже удивляют. GNU nohup перенаправляет стандартный ввод, только если это терминал, отправляет вывод в nohup.out, только если stdout подключён к терминалу, и обычно так же поступает с stderr. Неинтерактивная команда SSH часто использует каналы, поэтому nohup может оставить эти дескрипторы связанными с SSH.

Поэтому две команды дают разные обещания:

ssh testhost 'nohup /opt/jobs/rebuild-index &'
ssh testhost 'nohup /opt/jobs/rebuild-index > /var/log/rebuild-index.log 2>&1 < /dev/null &'

Вторая форма явно отсоединяет стандартные дескрипторы. Она всё равно не сообщает, завершился ли rebuild-index. nohup возвращает ошибки запуска, например отсутствие команды, а в остальных случаях следует коду вызванной команды. Когда shell отправляет вызов в фон, он обычно сообщает об успешном запуске, а не о будущем результате.

Невосприимчивость к SIGHUP покрывает лишь одну угрозу. Процесс может погибнуть, если менеджер входа удалит сеанс, менеджер служб остановит его контрольную группу, ядро применит политику нехватки памяти, администратор отзовёт учётную запись или хост перезагрузится. Процесс также может выжить и дать неверный результат. nohup не создаёт устойчивую личность задания, повторные попытки, границы ресурсов, постоянное состояние или надёжную запись о завершении.

Я применяю nohup для небольшой одноразовой работы, когда потеря результата приемлема и я наблюдаю за хостом. Я не превращаю с его помощью вызов SSH агента в управляемое производственное задание. Совет популярен, потому что команда короткая и часто переживает потерю терминала. Он ошибочен, если позже нужно доказать, что именно завершилось.

Терминал меняет управление заданиями

Не отдавайте ключи SSH агентам
Sallyport выполняет действия SSH, а ключ остаётся в зашифрованном хранилище.

Shell группирует процессы, чтобы интерактивный пользователь мог приостанавливать, возобновлять и переводить конвейеры между передним и задним планом. Неинтерактивный shell обычно работает без режима мониторинга, а запрос SSH exec не получает псевдотерминал, если клиент его не запросил. Сценарии, зависящие от jobs, %1, disown или терминальных сигналов, у агента могут вести себя иначе.

POSIX связывает ID заданий и известные фоновые PID с текущей средой выполнения shell. wait умеет ждать эти известные процессы, но запущенный в другом shell wait не наследует таблицу заданий. Такой приём аудита не работает:

ssh testhost 'long_task & printf "%s\n" "$!"'
ssh testhost 'wait 41872; printf "wait_status=%s\n" "$?"'

Второй вызов запускает новый shell. Даже если 41872 ещё жив, новый shell не считает его своим дочерним процессом. POSIX задаёт код 127 для неизвестного PID, переданного wait. Права доступа и повторное использование PID делают попытку восстановить связь ещё менее надёжной.

Если договор требует синхронного завершения, запускайте и ожидайте в одном shell:

ssh testhost 'long_task > /tmp/long-task.log 2>&1 < /dev/null & pid=$!; printf "pid=%s\n" "$pid"; wait "$pid"; rc=$?; printf "workload_status=%s\n" "$rc"; exit "$rc"'

Этот шаблон возвращает код дочернего процесса и сохраняет действие SSH открытым. Он подходит для процесса, который остаётся дочерним для этого shell. Если long_task делает fork и исходный процесс выходит, wait может завершиться раньше настоящего работника. До принятия такого договора проверяйте реальную программу, а не заменяющий её sleep.

Псевдотерминалы добавляют сигналы и меняют буферизацию. При завершении сеанса терминал может отправить SIGHUP, а фоновые группы, читающие управляющий терминал, могут получить SIGTTIN и остановиться. Некоторые программы включают построчную буферизацию или другой вывод, когда видят терминал. Если команде не нужны терминальные свойства, автоматизация не должна выделять псевдотерминал и должна явно настроить три стандартных дескриптора.

setsid отделяет процесс, но не создаёт доказательств

setsid создаёт новый сеанс и группу процессов, изначально без управляющего терминала. Это более сильное отделение, чем простое игнорирование SIGHUP. Поэтому потомок способен пережить shell, а терминальные сигналы перестают следовать за ним.

Надзора над процессом это не создаёт. После выхода исходного родителя потомка может принять другой процесс. На обычном хосте им может стать PID 1, а в контейнере или дереве служб это может быть subreaper. Новая связь с родителем ничего не говорит об успехе и может стереть самый удобный мост к действию запуска.

Полезный тест сохраняет личность до исчезновения shell:

ssh testhost 'run_id=agent-probe-20260724-1014; setsid sh -c '\''printf "%s\n" "$$" > /tmp/'"$run_id"'.pid; sleep 12; printf "complete\n" > /tmp/'"$run_id"'.state'\'' > /tmp/'"$run_id"'.log 2>&1 < /dev/null & printf "run_id=%s\n" "$run_id"'

Затем делайте запрос по возвращённому ID, используйте записанный PID только как подсказку и перед действием сверяйте время запуска и команду. Один PID не даёт устойчивой личности. Если процесс завершится, а ядро повторно выдаст номер, поздняя команда очистки может затронуть чужую работу.

Двойной fork, setsid, disown и закрытие дескрипторов относятся к технике реализации. Команды часто принимают их за протокол заданий, потому что терминал возвращает управление. Протокол отвечает на другие вопросы: кто теперь владелец работы, как запросить состояние, какие терминальные состояния возможны, где причина выхода, как отменить всё дерево и какой ID связывает запрос, журналы, эффект и аудит?

Если ответ запуска не содержит этих ответов, запишите отделённый запуск, а не завершённое действие.

Закрепите три события жизненного цикла в договоре аудита

Одобряйте каждый чувствительный ключ
Для отмеченного ключа SSH каждый вызов потребует нажатия или Touch ID.

Для проверяемого удалённого действия нужны отдельные события принятия, завершения канала и завершения нагрузки. Если сжать их в success, получится ложная уверенность, а расследование начнёт зависеть от истории shell.

Я использую запись примерно такой формы:

{
  "action_id": "act_01J3M8Q4",
  "remote_host": "worker-07",
  "launch": {"state": "accepted", "at": "2026-07-24T10:14:00Z"},
  "ssh_command": {"state": "exited", "status": 0, "at": "2026-07-24T10:14:01Z"},
  "workload": {"id": "job_8931", "state": "running", "result": null},
  "completion_source": "remote-job-manager"
}

Названия менее важны, чем разделение. accepted означает, что удалённый владелец проверил и принял запрос. exited означает завершение команды SSH. running говорит, что длительная работа ещё не достигла терминального состояния. Только владелец нагрузки вправе записать для неё succeeded, failed или cancelled.

Опишите правила переходов. Запуск может дать сбой до создания ID. Канал может оборваться после принятия задания удалённой системой, оставив вызывающую сторону в неопределённости, а не в состоянии ошибки. Нагрузка может сломаться после чистого выхода канала. Отмена может быть запрошена, но ещё не закончена. Модель без unknown рано или поздно запишет догадку как факт.

Идемпотентность входит в договор. Если клиент потерял канал после отправки, он должен повторить запрос с тем же ID действия и спросить, было ли оно уже принято. Запустить вторую миграцию из-за потерянного ответа хуже, чем получить неаккуратный журнал.

Доказательство завершения должно содержать ID нагрузки, терминальное состояние, причину выхода, время начала и конца и личность менеджера, наблюдавшего состояние. Если риск того требует, добавьте подтверждение эффекта: развёрнутую ревизию, полный манифест резервной копии или версию схемы. Не считайте строку с done единственным источником истины, если автор и хранилище журнала не входят в доверенный протокол.

Проверяйте окна отказа, а не только удачный путь

Полезная матрица меняет способ отделения процесса, судьбу дескрипторов и момент отказа соединения или процесса. Запустите её на каждом классе хостов, потому что менеджеры входа, shell и служб меняют поведение.

Покройте как минимум такие случаи:

  • Команда на переднем плане, фоновое задание shell, nohup с фоновым запуском, новый сеанс через setsid и программа с самостоятельной демонизацией.
  • Работа без терминала и с выделенным псевдотерминалом.
  • Унаследованные дескрипторы, перенаправление в файлы и закрытие дочерним процессом.
  • Разрыв до принятия, после принятия до ответа и после выхода команды SSH.
  • Ненулевой выход потомка, сигнал, зависание, создание внука и жизнь до явной отмены.

Для каждого случая сохраните четыре момента: начало на клиенте, подтверждение запуска, закрытие канала SSH и удалённое терминальное состояние. Код SSH и результат нагрузки записывайте отдельно. Во время работы проверьте группу процессов и сеанс, а затем докажите, что отмена достигает всех потомков.

Компактная проверка может сообщить об ошибке, если код 0 пришёл без терминального доказательства:

result=$(ssh testhost '/usr/local/bin/job-submit agent-probe-42')
ssh_rc=$?
printf 'ssh_rc=%s response=%s\n' "$ssh_rc" "$result"
job_id=$(printf '%s\n' "$result" | sed -n 's/^job_id=//p')
test "$ssh_rc" -eq 0 && test -n "$job_id" || exit 1
/usr/local/bin/poll-job "$job_id" || exit 1

Пример предполагает, что job-submit возвращает ровно одну строку job_id=, а poll-job аутентифицирует запрос, ждёт терминального состояния и выходит с результатом нагрузки. Это требования договора, а не свойства SSH. В настоящей проверке отклоняйте лишний вывод, ставьте срок, сохраняйте неопределённость при тайм-ауте и записывайте сырой ответ.

Проверьте и наблюдателя. Остановите агент после запуска. Перезапустите клиентскую машину. Смените данные доступа SSH. Перезагрузите удалённый хост, если задание должно это переживать. Система не поддаётся аудиту, если единственная копия ID существует в контекстном окне агента.

Длительной работой обычно должен владеть менеджер служб

Проверяйте цепочку без сети
sp audit verify проверяет зашифрованные записи без открытия хранилища.

Если работа должна пережить команду SSH, передайте её удалённой службе или менеджеру заданий и верните устойчивый ID. Менеджер должен владеть группой процессов, собирать вывод, применять ограничения ресурсов и отмену, хранить состояние и отвечать на запросы, различая работу и терминальный результат.

На хосте systemd временная или шаблонная служба даёт контрольную группу и личность в journal. Руководство systemd-run различает асинхронный запуск службы и ожидание её завершения, а также предупреждает: простая служба может считать запуск успешным сразу после fork, ещё до выполнения программы. Я предпочитаю Type=exec, если ошибка выполнения должна быть видна, но это по-прежнему доказывает запуск, а не конечный успех.

Шаблонный модуль может задать границу владения так:

[Unit]
Description=Agent job %i

[Service]
Type=exec
ExecStart=/usr/local/libexec/agent-job %i
StandardOutput=journal
StandardError=journal
KillMode=control-group
TimeoutStopSec=30s

Отправьте уникальный проверенный ID экземпляра и запрашивайте модуль до терминального состояния. Запишите ActiveState, SubState, Result, ExecMainStatus, время и ID. Проверьте поведение при fork, потому что тип службы должен соответствовать программе. Не превращайте произвольный ввод в имя модуля или аргумент без строгой проверки.

Очередь, пакетный планировщик, оркестратор контейнеров или таблица заданий приложения дают ту же границу. Выберите владельца, который уже отвечает за ресурсы и восстановление. SSH должен отправлять и запрашивать, а не изображать планировщик длинной цепочкой операторов shell.

Для короткой работы проще оставить команду на переднем плане и вернуть её настоящий код. Отделение требует ещё одного хранилища, личности, правил отмены, срока хранения и сверки. Платите эту цену, только если работа действительно должна пережить вызов.

История аудита заканчивается удалённым терминальным состоянием

Шлюз действий способен точно записать вызов SSH, не зная об удалённом потомке. Sallyport записывает действие SSH в Activity journal, а запуск агента в Sessions journal, поэтому запись вызова доказывает результат канала, но не перечисляет процессы хоста. ID удалённого задания и терминальное событие должны вернуться отдельным проверяемым действием.

Такое разделение сохраняет честность записей. Шлюз доказывает, какой запуск агента вызвал SSH, какой защищённый ключ разрешил действие, какой вызов произошёл и что вернулось. Удалённый менеджер доказывает последующие события. Свяжите записи ID действия, который агент не может незаметно заменить между запуском и запросом.

Не помечайте отделённый запуск как completed. Используйте submitted или detached, показывайте ID нагрузки и держите родительское действие открытым или явно ожидающим, пока доверенный наблюдатель не запишет терминальное состояние. При истечении наблюдения покажите unknown и потребуйте сверку. Красный статус может мешать, но зелёный статус не того процесса опасен.

Момент одобрения требует такой же точности. Одобрение использования ключа SSH разрешает одну попытку в рамках показанной тогда информации. Нажатие не разрешает все будущие действия неограниченного потомка и не подтверждает итоговый эффект. Если задание способно работать часами или создавать процессы, покажите это до запуска и свяжите одобрение с ID, хостом, намерением команды и типом задания. Записи одобрения и завершения входят в одну цепочку, но описывают разные решения.

Сохраняйте сырой вывод на каждой границе. Запишите ответ отправки до разбора, отдельно соберите stderr, если протокол позволяет, и отметьте, объединил ли псевдотерминал потоки. Парсер должен отклонять повторные ID, управляющие символы, обрезанные ответы и лишние строки, способные спутать ответы. Разобранные поля нужны автоматизации, сырые байты помогают проверить ошибку парсера, кавычек shell или удалённой программы. Ни одна форма не должна содержать секреты.

Маркеру завершения нужны правила атомарной публикации. Работник должен записать результат во временный файл в защищённом хранилище, сбросить данные, если важна сохранность, и переименовать файл только после готовности полной записи. Сторона запроса должна проверить ID, ожидаемого владельца, тип файла и состояние. Лучше, если менеджер или база отдаёт статус через аутентифицированный интерфейс. Доступный всем маркер в /tmp показывает время, но не разрешает расследование.

Учтите неопределённую доставку при отправке и отмене. Если соединение пропало после принятия задания, но до получения ID, правильное локальное состояние равно unknown. Подключитесь снова с исходным ключом идемпотентности и найдите отправку. Не повторяйте её молча. Если потерялся ответ на отмену, спрашивайте, пока менеджер не сообщит терминальное состояние и пустую группу процессов. Отправленный сигнал означает попытку, а не доказанную остановку.

Сверка должна пережить процесс агента. Храните нерешённые ID вне временного разговора, назначайте ответственного и периодически закрывайте или поднимайте старые записи. Разделяйте срок ожидания и ошибку: задание может превысить срок клиента, оставаясь исправным и управляемым. Журнал должен показать, когда клиент перестал ждать, кто продолжает наблюдение и пришло ли завершение позже. Иначе тайм-аут становится ещё одним ложным терминальным состоянием.

Проверяющим нужен тот же словарь, что и системе. Ищите действия с нулевым ssh_command.status и отсутствующим, просроченным или неизвестным workload.state. Ищите терминальные задания без одобрения запуска и повторные отправки с одним ключом идемпотентности. Такие запросы превращают различие жизненных циклов в контроль, который находит пробелы.

Дайте оператору одно действие для повторного открытия удалённой записи. Экран должен показывать время последнего наблюдения, сообщивший его компонент и источник статуса, живой запрос или кэш. Не меняйте unknown на failed ради очистки очереди. Сохраняйте неопределённость до ответа владельца или решения уполномоченного проверяющего с документированным доказательством. После сбоя останутся неудобные открытые записи, и это точное состояние.

Проверяйте не только создание, но и хранение. Записи нагрузки должны оставаться доступными дольше самого долгого ожидаемого задания и весь период аудита или расследования. Если менеджер сразу удаляет сведения о временном модуле, скопируйте терминальный результат в постоянную запись до удаления. ID, который на следующей неделе не может разрешить ни одна система, даёт связь без ответственности.

Запустите двенадцатисекундную пробу по настоящему пути агента, затем повторите её с важной нагрузкой. Если карточка SSH позеленела до появления удалённого маркера, вы нашли пробел аудита. Сохраните ноль: это действительное доказательство о команде. Не заставляйте его свидетельствовать о работе, которой он никогда не наблюдал.

Вопросы и ответы

Ждёт ли SSH завершения фоновых процессов?

SSH ждёт удалённую команду и закрытие данных канала, а не абстрактное дерево потомков. Фоновый процесс с открытым дескриптором может задержать клиент, но это случайное поведение, а не надзор.

Почему ssh возвращает 0, пока удалённое задание ещё работает?

Удалённый shell может успешно запустить асинхронную команду и выйти с кодом 0. SSH сообщает этот код, а у фонового задания позже появится собственный результат.

Запускает ли nohup команду SSH в фоне?

Нет. nohup меняет обработку SIGHUP, а оператор & запускает в фоне. Также нужно выбрать направления стандартного ввода, вывода и ошибок.

Почему nohup иногда создаёт впечатление, что SSH завис?

В неинтерактивном сеансе stdout и stderr могут быть каналами, поэтому nohup оставляет их без изменений. Потомок с открытыми дескрипторами задерживает закрытие канала.

Можно ли дождаться удалённого PID во втором вызове SSH?

Новый shell не может применить wait к процессу, который не входит в его дочерние процессы и таблицу заданий. Запрашивайте менеджер по устойчивому ID.

Достаточно ли setsid для надёжного отделённого задания?

setsid создаёт новый сеанс и убирает связь с управляющим терминалом. Он не добавляет устойчивую личность, хранение состояния, повторные попытки, полную отмену или доказательство успеха.

Нужно ли автоматической команде SSH выделять псевдотерминал?

Обычно нет, если ей не нужны терминальные свойства. Псевдотерминал меняет сигналы, буферизацию, управление заданиями и ввод, скрывая отличия от запуска агентом.

Что записывать при аудите фонового задания SSH?

Отдельно фиксируйте принятие, выход канала и завершение нагрузки. Храните удалённый ID, терминальное состояние, причину, время, источник завершения и общий ID действия.

Как безопасно отменить отделённый удалённый процесс?

Попросите владеющий заданием менеджер отменить его и проверьте состояние cancelled или failed. Убивать сохранённый PID опасно: потомки могут уйти, а PID получить другой процесс.

Когда команда SSH на переднем плане лучше менеджера заданий?

Используйте её для ограниченной работы, когда соединение может оставаться открытым, а команда возвращает настоящий результат. Менеджер нужен для переживающей разрыв работы, поздних запросов и групповой отмены.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov