# Когда выделение TTY в SSH меняет поведение команды?

Параметр `-t` в команде SSH делает намного больше, чем просто показывает запрос. Он заменяет три отдельных удаленных потока терминалом, включает правила обработки терминальной линии, назначает сеансу управляющий терминал и побуждает программы вести себя так, будто за ними наблюдает человек. Это может исправить интерактивный запуск `sudo`, но незаметно сломать парсер, конвейер или отмену операции.

Для работы без оператора по умолчанию PTY выделять не следует. Запрашивайте его только тогда, когда удаленной программе действительно нужны свойства терминала, и считайте этот выбор частью контракта команды. Я видел слишком много сценариев развертывания, в которые во время аварии добавили `-tt`, а затем оставили на годы, когда уже никто не помнил, какую неполадку он скрывал.

## PTY меняет окружение процесса

Выделение TTY в SSH меняет сами файловые дескрипторы, которые получает удаленный процесс. Внешним видом вывода дело не ограничивается. Без PTY OpenSSH подключает стандартный ввод, стандартный вывод и стандартный поток ошибок через каналы или пары сокетов. С PTY OpenSSH назначает его управляющим терминалом и дублирует один терминальный дескриптор на все три стандартных потока.

Эта деталь реализации дает заметные последствия. Программа может вызвать `isatty()` и выбрать интерактивную ветку. Драйвер терминала может обрабатывать специальные символы ввода, повторять ввод на экране, преобразовывать окончания строк и следить за группой процессов переднего плана. У стандартного потока ошибок больше нет отдельного удаленного канала, потому что оба выходных дескриптора указывают на один терминал.

RFC 4254 разделяет эти понятия на уровне протокола. Сеанс может отправить запрос `pty-req`, а затем запросить shell или команду `exec`. Запрос PTY содержит тип терминала, его размеры и закодированные режимы. Он не превращает запрос `exec` в login shell.

OpenSSH дает три удобных варианта:

- `ssh -T host command` явно отключает выделение PTY.
- `ssh -t host command` запрашивает PTY, если у локального клиента есть терминал.
- `ssh -tt host command` принудительно отправляет запрос, даже если локальный ввод идет из канала или другого нетерминального источника.

Два `-t` не дают более мощный удаленный терминал. Они обходят локальную защитную проверку. Это различие важно в сборочном процессе, процессе агента или конструкции `printf ... | ssh`, где один `-t` может вывести «Pseudo-terminal will not be allocated because stdin is not a terminal» и продолжить работу без свойств терминала, на которые рассчитывал автор.

Без PTY сохраняется прозрачный путь для байтов. Руководство OpenSSH `ssh(1)` называет этот режим подходящим для надежной передачи двоичных данных. PTY относится к символьным устройствам с правилами обработки, поэтому через него не стоит передавать архив, дамп базы данных или машиночитаемый вывод, в котором каждый байт должен сохраниться без изменений.

Тот же выбор может скрываться в конфигурации. Параметр клиента OpenSSH `RequestTTY` принимает `no`, `yes`, `force` или `auto`. Эти значения соответствуют поведению, которое обычно задают через `-T`, `-t` или оставляют по умолчанию. Если команда получает терминал без явного запроса, проверьте итоговую конфигурацию клиента командой `ssh -G host.example`. Блоки для узлов и подключенные файлы могут превратить две одинаковые с виду команды в разные сеансы.

Сервер тоже участвует в решении. `PermitTTY` может запретить выделение, а запись authorized key может содержать ограничение `no-pty`. Принудительная команда работает на PTY только тогда, когда клиент его запросил, а сервер разрешил. Отказ говорит о настоящем несовпадении интерфейсов, а не о том, что нужно повторять попытки с дополнительными `-t`.

## Сначала измерьте поведение обоих режимов

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

```sh
probe='
for fd in 0 1 2; do
    if test -t "$fd"; then kind=tty; else kind=not-tty; fi
    printf "fd%s=%s\n" "$fd" "$kind"
done
printf "term=%s\n" "${TERM-unset}"
printf "stdout-marker\n"
printf "stderr-marker\n" >&2
exit 23
'

ssh -T host.example "$probe" >no-pty.out 2>no-pty.err
printf 'no-pty ssh status=%s\n' "$?"

ssh -tt host.example "$probe" >pty.out 2>pty.err
printf 'pty ssh status=%s\n' "$?"
```

Запуск без терминала должен записать три строки `fdN=not-tty`, `term=unset` и `stdout-marker` в `no-pty.out`. В `no-pty.err` должен попасть только `stderr-marker`. Точное значение `TERM` может отличаться, потому что клиенты и серверы по-разному принимают настройки окружения. Запишите результат своего настоящего маршрута, а не полагайтесь на пример.

Запуск с PTY должен показать `tty` для всех трех дескрипторов. Оба маркера обычно попадают в `pty.out`, нередко с окончанием строк из возврата каретки и перевода строки. В `pty.err` остаются локальные диагностические сообщения SSH, если они есть, но восстановить исходный удаленный поток ошибок отдельно этот файл не может. Обе команды SSH должны вернуть 23, поскольку само выделение PTY не отбрасывает удаленный код выхода.

Перед изменением задания добавьте в проверку настоящий исполняемый файл. Включите `command -V tool`, `pwd`, отфильтрованный вывод окружения и безопасные диагностические параметры программы. Если вывод различается, найдите конкретную причину ветвления: проверку терминала, `TERM`, ширину терминала, объединенные потоки, стартовый файл или запрос. Фраза «с `-t` работает» описывает лишь симптом.

Запускайте проверку через тот же механизм. У команды, вставленной в shell, есть локальный TTY, а у агента или процесса непрерывной интеграции его часто нет. Поэтому опыт с `-t` за клавиатурой может отличаться от автоматического запуска, пока вы не проверите `-tt`. Принудительный PTY также может открыть заданию доступ к вводу, которого оно не ожидало.

Проверяйте ввод отдельно от вывода. `ssh -n` перенаправляет стандартный ввод клиента из `/dev/null`, а параметр `StdinNull` делает то же через конфигурацию. Это полезно, когда удаленная команда ни при каких условиях не должна потреблять ввод вызывающего процесса, но само по себе не отключает PTY. Принудительный PTY с пустым вводом по-прежнему меняет проверку терминала, объединение потоков и реакцию на разрыв.

Будьте осторожны с SSH внутри цикла. Без `-n` первая удаленная команда может прочитать строки, предназначенные циклу, даже если удаленный процесс не собирался их потреблять. С PTY терминальное эхо может также вернуть эти байты в запись сеанса. Сначала явно определите владельца стандартного ввода, а затем разбирайте остальные различия.

## Для запросов sudo нужен явный контракт

`sudo` нужен способ аутентификации, а PTY дает терминал, из которого можно прочитать пароль. В руководстве sudo сказано, что обычно пароль читается из терминала пользователя. Без терминала требование пароля приводит к ошибке, если только нет помощника askpass или вызывающая сторона не выбрала другой способ ввода.

Поэтому часто помогает такая команда:

```sh
ssh -t host.example 'sudo systemctl restart example.service'
```

Она подходит человеку, который собирается ввести пароль и наблюдать за командой. Для выполнения без оператора это плохое исправление. Задание может бесконечно ждать ввода, запрос может испортить ожидаемый вывод, а кто-нибудь со временем передаст пароль по тому же каналу, по которому идут данные команды.

Для автоматизации запускайте sudo без интерактивного режима:

```sh
ssh -T host.example \
  'sudo -n /usr/bin/systemctl restart example.service'
```

Параметр `-n` запрещает sudo запрашивать ввод. Если сохраненные учетные данные или политика sudoers не разрешают действие без участия человека, sudo завершается с ошибкой. Такая ошибка полезна: вызывающая сторона получает определенный код вместо скрытого запроса. Если привилегированная автоматизация действительно нужна, добавьте узкое правило sudoers для точной команды и ее аргументов. Разрешение любого shell без пароля заменяет проблему доступности намного более серьезной проблемой полномочий.

Не ставьте задание без оператора в зависимость от сохраненной временной метки sudo. Кэш относится к контексту аутентификации, а детали терминала и сеанса в нем зависят от политики. Если задание работает только потому, что администратор использовал sudo минуту назад, а после спокойных выходных падает, у него нет продуманной авторизации. Ему просто повезло со временем.

`sudo -S` читает пароль из стандартного ввода. Так можно запустить команду без PTY, но секрет смешивается с каналом данных, а обработка пароля становится обязанностью вызывающей стороны. Не используйте этот вариант для агентов и обычной автоматизации. Помощник askpass может подходить для графического сценария с участием человека, однако это все равно интерактивная аутентификация, которую нельзя внезапно вставлять в якобы автономное задание.

Здесь часто путают два PTY. SSH может выделить удаленный PTY перед запуском sudo. Отдельно современный sudo может выполнить разрешенную команду в собственном PTY ради журнала ввода и вывода и изоляции; в sudo 1.9.14 параметр `use_pty` включен по умолчанию. Этот внутренний PTY не создает внешний запрос пароля, когда у сеанса SSH нет терминала. Он появляется после того, как sudo уже получил достаточно сведений для запуска команды.

Некоторые конфигурации sudoers также требуют терминал на уровне политики. В старых корпоративных конфигурациях часто встречался `requiretty`, и он мог сохраниться в современной системе. Проверяйте действующую политику sudoers, а не считайте, что каждое сообщение «terminal is required» связано с запросом пароля.

## PTY не выбирает стартовые файлы shell

Команда SSH с PTY все равно запускается настроенным для учетной записи shell через `-c`. Руководство OpenSSH `sshd(8)` прямо описывает такой запуск, а код сервера передает команду shell пользователя. Выделение PTY меняет окружающие его дескрипторы, но не добавляет `-i`, `-l` или режим login.

Это важно, когда `PATH`, менеджер версий языка или псевдоним есть в интерактивной строке, но отсутствует в автоматизации. Может показаться, что `-t` исправляет путь, потому что стартовый файл проверяет наличие терминала или вызванная программа определяет терминал. Параметр может вообще ничего не изменить. Зависимость от такого поведения связывает задание с файлами, которые разработчики правят для собственных терминалов.

У Bash есть особый случай, который усиливает путаницу. В его руководстве сказано, что интерактивный login shell читает файлы профиля, интерактивный shell без login читает `.bashrc`, а неинтерактивный использует `BASH_ENV`. Bash также пытается распознать запуск удаленным shell daemon и может прочитать `.bashrc` даже без интерактивного режима, кроме вызова под именем `sh`. Это поведение Bash, а не обещание SSH, и оно не переносится на любой shell учетной записи.

Если нужен конкретный режим shell, выберите его прямо:

```sh
# A predictable noninteractive Bash command with explicit inputs
ssh -T host.example \
  'PATH=/usr/local/bin:/usr/bin:/bin /bin/bash -c '\''command -v deploy && deploy'\'''

# A login environment, requested because the command truly depends on it
ssh -T host.example \
  '/bin/bash -lc '\''command -v deploy && deploy'\'''
```

Второй вариант намеренно читает стартовые файлы login и получает связанные с ними риски: вывод из профиля может испортить протокол, а изменение профиля может повлиять на поведение без проверки развертывания. В рабочих заданиях обычно лучше указать абсолютный путь к программе и небольшой явный набор переменных окружения.

Псевдонимы тоже создают ловушки. Неинтерактивный Bash не раскрывает их, если не включен `expand_aliases`, а функция shell существует лишь тогда, когда стартовый механизм ее определил или импортировал. Если `ssh host deploy` работает только после добавления `-t`, убедитесь, что `deploy` относится к настоящему исполняемому файлу. Наличие терминала не должно определять, найдет ли удаленная учетная запись программу.

Помните, что SSH отправляет строку команды, а не массив аргументов для конечной программы. Удаленный shell разбирает эту строку после того, как локальный shell уже обработал свои кавычки. Выделение PTY не меняет ни один разбор. Если значения могут содержать пробелы или специальные символы shell, отправьте сценарий с фиксированными входными данными, используйте удаленный помощник с определенным форматом или внимательно экранируйте каждый слой. Терминал не исправит аргумент, который локальный shell раскрыл слишком рано.

## Ctrl-C подчиняется правилам терминала только с PTY

При наличии PTY Ctrl-C обычно передается как входной байт, который обрабатывает драйвер удаленного терминала. POSIX называет его символом `INTR`. Когда установлен флаг `ISIG`, драйвер отбрасывает этот байт и посылает `SIGINT` каждому процессу в группе переднего плана терминала. Поэтому удаленный конвейер переднего плана может остановиться как одно задание.

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

Без PTY нет удаленного терминального драйвера для обработки Ctrl-C и нет терминальной группы переднего плана. RFC 4254 определяет отдельный запрос канала SSH `signal`, но разрешает системам без реализации сигналов игнорировать его. Поэтому сочетание клиента, сервера, обертки и вызванной программы может отменять работу иначе, чем интерактивный терминал.

Ctrl-Z и управление заданиями показывают различие еще быстрее. В терминале символ `SUSP` может послать `SIGTSTP` группе переднего плана, после чего интерактивный shell способен возобновить задание. Разовая удаленная команда не получает полезный интерактивный диалог управления заданиями только из-за PTY. После приостановки клиент SSH может ждать остановленную удаленную группу, но строки shell для запуска `fg` не будет.

Не считайте удаленную транзакцию безопасной только потому, что оператор якобы сможет вовремя нажать Ctrl-C. Опишите отмену в удаленной команде. Обертка shell может перехватить сигналы и завершить группу дочерних процессов, менеджер служб может владеть процессом, а удаленный тайм-аут может ограничить выполнение. Затем обязательно проверьте и разрыв соединения вместе с клавиатурным сценарием.

```sh
ssh -tt host.example '
  trap '\''printf "wrapper got INT\n" >&2; exit 130'\'' INT
  printf "remote shell pid=%s\n" "$$"
  sleep 300
'
```

Нажатие Ctrl-C должно задействовать путь PTY и вернуть управление. Повторите проверку с настоящей программой, потому что shell, терминальные приложения и диспетчеры процессов устанавливают разные обработчики. Также проверьте закрытие сетевого соединения. Разбор PTY может вызвать сигнал отключения для управляющего сеанса, а дочерний процесс без PTY, сохранивший выходной канал, способен удерживать канал SSH открытым. Перевод процесса в фон сам по себе не дает надежного отделения ни в одном режиме.

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

## Код выхода сохраняется, но обертка может его заменить

OpenSSH возвращает код выхода удаленной команды в обоих режимах. Руководство клиента говорит, что `ssh` завершается с этим кодом или возвращает 255 при ошибке. RFC 4254 описывает сообщение канала `exit-status` как 32-битное беззнаковое значение и выделяет отдельное сообщение `exit-signal` для завершения по сигналу.

Выделение PTY не делает успешный статус надежнее. Состав команды shell определяет, какой код станет кодом удаленной команды. Следующая команда сообщает статус `printf` и может скрыть неудачное развертывание:

```sh
ssh -tt host.example 'deploy; printf "finished\n"'
```

Сохраните результат явно:

```sh
ssh -T host.example '
  deploy
  rc=$?
  printf "deploy_status=%s\n" "$rc" >&2
  exit "$rc"
'
rc=$?
printf 'ssh_status=%s\n' "$rc"
exit "$rc"
```

Локальный конвейер может снова его скрыть. В shell без функции проверки статусов конвейера конструкция `ssh host command | tee log` обычно возвращает статус `tee`. Сначала запишите вывод SSH в файл, используйте shell с проверенной функцией статусов конвейера или прочитайте код каждой команды. Не рассчитывайте, что `set -e` исправляет любой конвейер, условие или subshell.

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

Ошибки установки соединения происходят до того, как удаленная обертка успевает что-либо вывести. Классифицируйте их локально с помощью тайм-аутов клиента и достаточной диагностики SSH, но не допускайте эту диагностику в файл данных программы. После запуска обертки сервером добавьте идентификатор запуска и финальную запись статуса, чтобы вызывающая сторона отличила «не запускалось» от «запустилось, затем соединение потерялось». Так вы не станете вслепую повторять команду, которая уже могла изменить состояние.

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

## PTY объединяет потоки и меняет байты

Запрос PTY означает отказ от четкого разделения удаленных стандартного вывода и потока ошибок. Сервер OpenSSH дублирует ведомую сторону PTY на дескрипторы 0, 1 и 2. В протоколе есть расширенный канал данных для стандартного потока ошибок, но подключенный к одному терминалу процесс уже записал оба выходных потока в единую последовательность байтов.

Из-за этого ломается распространенная схема автоматизации:

```sh
ssh -T host.example command >result.json 2>diagnostic.log
```

Без PTY стандартный поток ошибок удаленной программы может попасть в `diagnostic.log`, а JSON останется в `result.json`. С PTY удаленные предупреждения, запросы пароля, приветственные сообщения, индикаторы выполнения и JSON могут вместе попасть в `result.json`; локальная диагностика SSH по-прежнему может появиться в `diagnostic.log`. Перенаправление на стороне клиента не разделит байты после того, как сервер их объединил.

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

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

Если команда выдает машинные данные, оставьте `-T` и включите неинтерактивный формат программы. Если терминал действительно нужен, считайте весь вывод записью сеанса. Не разбирайте запись PTY как стабильный API.

## SSH агента должен завершаться без диалога

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

В запусках агента сочетайте отсутствие PTY с отключением запросов на каждом уровне. Для аутентификации и проверки узла SSH нужна заранее подготовленная политика. Повышение привилегий должно использовать `sudo -n`. Удаленному инструменту передайте неинтерактивный параметр, явный тайм-аут и ввод через файлы или именованные аргументы вместо диалога. Стандартный вывод, поток ошибок и статус собирайте отдельно.

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

Sallyport выносит границу учетных данных за пределы процесса агента: агент с поддержкой MCP запрашивает действие SSH через встроенный помощник `sp-ssh` без состояния, зашифрованное хранилище предоставляет ключ SSH, а журнал Activity записывает вызов. Команде все равно нужен явный контракт PTY, потому что изоляция секретов не определяет, ожидает ли удаленная программа свойства терминала.

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

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

## Выбирайте режим как часть интерфейса

Подходящий режим определяется интерфейсом удаленной программы, а не общим мнением о пользе или вреде PTY. Используйте этот список при проверке:

- Для JSON, архивов и точных байтов выбирайте `-T`, поскольку он сохраняет прозрачные данные и отдельный поток ошибок.
- Для привилегированной работы без оператора выбирайте `-T` с `sudo -n`, чтобы аутентификация завершилась ошибкой, а не ожиданием.
- Выбирайте `-t`, когда человек будет вводить пароль sudo или работать с терминальным интерфейсом.
- Используйте `-tt` из канала или запуска агента только после проверки последствий обхода локального TTY.
- Долгие задания отправляйте удаленному менеджеру, потому что PTY не обеспечивает долговременное владение процессом.

Запишите выбор рядом с командой. Тесты должны проверять типы дескрипторов, разделение потоков, отмену и статус. Нескольких ожидаемых слов в выводе недостаточно. Если после обновления зависимости программа начинает требовать терминал, дайте тесту упасть и найдите новый запрос или ветку проверки до добавления `-t`.

Добавляйте выбранный режим в журналы как структурированные метаданные, а не угадывайте его по цветному выводу или запросу. Если сбой затронул несколько узлов, одно это поле поможет оператору отделить поведение терминала от ошибок аутентификации, сети и приложения до повторного запуска.

Политика сервера может запретить PTY через конфигурацию OpenSSH или ограничение authorized key. Это еще одна причина не создавать случайную зависимость от терминала. Команда для автоматизации должна работать с `-T`, а интерактивная команда должна ясно завершаться ошибкой, если получить терминал нельзя.

Считайте любое предложение добавить `-tt` изменением интерфейса. Снова запустите проверку, перенаправьте оба локальных потока, отправьте Ctrl-C, заставьте удаленную команду вернуть ненулевой статус и разорвите соединение во время работы. Если результат нельзя точно описать, команда не готова к запуску без оператора.
