38 KiB
KomiTerm — SSH/Telnet клиент на Rust
Минималистичная альтернатива MobaXterm под Linux: session manager + SSH/Telnet в терминале, без X11-сервера и RDP/VNC (сознательно вырезано ради MVP).
Архитектура
main.rs — CLI (clap): запуск сессии + subcommand `profile add/list/remove`
session.rs — конфиг сессий (~/.config/komiterm/sessions.toml), AuthMethod/PassphraseSource
profiles.rs — именованные credential-профили (~/.config/komiterm/credentials.toml)
hotkeys.rs — конфигурируемые хоткеи табов (~/.config/komiterm/hotkeys.toml)
sftp.rs — обёртка над russh_sftp::client::SftpSession (листинг/cd/download/upload)
localfs.rs — локальный файловый браузер для выбора файла при закачке (u в SFTP-режиме)
hoststats.rs — парсинг CPU/памяти/load average из /proc для строки состояния
sshconfig.rs — парсер ~/.ssh/config, включая Include (см. ниже)
resolve.rs — слияние приоритетов: session > credential-профиль > ssh_config > дефолты
transport/
mod.rs — трейт Transport (write/resize/close/open_sftp/run_command) — общий интерфейс
ssh.rs — реализация через russh, поддерживает Password/Key/Agent + open_sftp/run_command
telnet.rs — реализация telnet с ответом на DO/WILL (не просто дроп)
serial.rs — последовательный порт (COM/tty) через tokio-serial
term.rs — обёртка над alacritty_terminal (парсинг escape-последовательностей)
ui.rs — рендер сетки терминала + боковая панель (сессии / SFTP-браузер)
app.rs — главный цикл: incoming -> term_state -> render, ввод -> transport
Аутентификация и профили
Способ входа можно задать двумя способами:
- Прямо в сессии — поле
authвsessions.toml. - Через именованный профиль — заведи один раз в
credentials.toml(командойkomiterm profile add), затем ссылайся на него по имени полемcredentialиз любой сессии. Удобно, когда один логин/ключ используется для нескольких хостов.
Приоритет при разрешении (см. resolve.rs): explicit auth в сессии >
credential-профиль > подсказка из ~/.ssh/config (IdentityFile) > запрос
пароля как последний вариант.
komiterm profile add # интерактивно: имя, логин, пароль/ключ/agent*
komiterm profile list
komiterm profile remove myprofile
* ssh-agent как способ входа временно не работает (см. TODO ниже) — профиль с ним создать можно, но подключение с таким профилем откажет.
Пароли и passphrase никогда не сохраняются на диск — только логин,
путь к ключу и признак "спросить passphrase при подключении". Файл
credentials.toml на Unix создаётся с правами 600.
known_hosts / защита от MITM
check_server_key в transport/ssh.rs больше не доверяет серверу
безусловно — сверяется с ~/.ssh/known_hosts через russh::keys::check_known_hosts:
- Ключ совпадает с записанным — подключаемся молча, как обычно.
- Записи для хоста нет вообще (первое подключение) — печатается
отпечаток ключа, и спрашивается подтверждение (Trust On First Use, как
делает обычный
sshпри "authenticity of host can't be established"). При согласии ключ дописывается в known_hosts — своей маленькой функцией (transport/ssh.rs::learn_known_hosts), а неrussh::keys::learn_known_hosts: в используемой версии russh (0.62.2) это имя не реэкспортируется на верхнем уровнеrussh::keys(толькоcheck_known_hosts), так что строку формируем сами черезPublicKey::to_openssh()— формат known_hosts простой и стабилен десятилетиями, велосипед тут минимальный. - Запись есть, но ключ другой — это подозрение на MITM или
переустановленный сервер. Здесь нет интерактивного "продолжить всё
равно": подключение отклоняется безусловно, с инструкцией вручную
почистить строку в
~/.ssh/known_hosts, если пользователь уверен, что смена ключа легитимна. Слабое место так просто не обойти повторным нажатием Enter.
Из-за этого пришлось поднять версию russh с 0.44 до текущей (>=0.50,
реально резолвится в 0.62.x) — модуль работы с ключами в старой версии
жил отдельным крейтом russh-keys и не имел этих функций в удобном виде;
в текущей версии russh::keys включает и агента, и известные хосты, и
работу с ключами в одном месте. Заодно поменялся API аутентификации по
ключу (authenticate_publickey теперь принимает PrivateKeyWithHashAlg
вместо голого Arc<PrivateKey>) — учтено в текущем коде.
~/.ssh/config
Если у сессии use_ssh_config = true (по умолчанию), поле host сначала
ищется как алиас в ~/.ssh/config — так же, как если бы ты написал
ssh myalias. Оттуда подтягиваются HostName, User, Port,
IdentityFile, если сессия их не переопределяет явно.
Путь один и тот же на всех трёх платформах (<домашняя_директория>/.ssh/config),
поэтому специального кода под Windows не потребовалось — OpenSSH для Windows
использует ту же схему.
Поддержана директива Include (в т.ч. с *-wildcard в имени файла и путями
относительно директории текущего конфига — Include config.d/* работает).
Совпадения *-паттерна обходятся в отсортированном порядке, как это делает
сам OpenSSH, иначе порядок Include стал бы недетерминированным. Есть защита
от циклов (a.conf включает b.conf включает a.conf) через отслеживание
уже разобранных файлов и предел глубины. Из не поддержанного: Match-блоки
и полноценные glob-паттерны (?, [abc], отрицания через !) — см. TODO
в конце sshconfig.rs.
Табы (мультиплексирование сессий)
komiterm myserver staging-box old-switch # три таба сразу при запуске
komiterm # один таб, сессия через менеджер
Хоткеи читаются из ~/.config/komiterm/hotkeys.toml (создаётся при первом
запуске с показанными ниже значениями по умолчанию; Alt+1..Alt+9 в конфиг
не вынесены — см. комментарий в hotkeys.rs, почему):
| Комбинация | Действие |
|---|---|
Ctrl+PageUp/PageDown |
предыдущий / следующий таб |
Alt+1..Alt+9 |
прыжок на таб N |
F1 |
фокус на боковую панель / обратно на терминал |
F2 |
новый таб (выбор сессии из списка) |
Ctrl+F2 |
закрыть активный таб |
F3 |
свернуть/развернуть боковую панель |
F4 |
SFTP-браузер для активного таба (только SSH) |
F10 |
выйти из KomiTerm целиком |
Все табы держат живое соединение и вычитывают входящие данные каждый кадр
(фон не "замирает", даже если ты сейчас смотришь в другой таб) — рисуется
только активный. Открытие нового таба (F2) и начальный выбор сессии (если
она не передана аргументом) показывают список сессий прямо поверх текущего
экрана через ratatui::widgets::List — без выхода из raw-режима и без
"грязного" экрана, как было в первой версии.
Если сохранённых сессий вообще нет (первый запуск), список не ошибка, а
пустой экран с подсказкой — нажми n, чтобы создать сессию прямо тут же,
не выходя из TUI и не редактируя sessions.toml руками. n работает и
при непустом списке — добавить ещё одну сессию можно в любой момент, когда
показан этот список (при F2 или при начальном выборе). Мастер — короткий
пошаговый опрос (протокол → имя → поля по протоколу, включая способ входа
для SSH), каждое поле — Enter продолжить, Esc отменяет всё создание сразу.
Готовая сессия дописывается в sessions.toml и сразу открывается табом.
Как закрыть/выйти из одной сессии (не из всего komiterm): Ctrl+F2
закрывает активный таб явно. Выйти изнутри сессии обычным способом
(exit или Ctrl+D в шелле) тоже работает — таб закрывается сам, как
только замечает, что канал от удалённой стороны закрылся (проверяется
каждый кадр). Если все табы закрылись — komiterm завершается сам, выходить
из него отдельно уже не нужно.
Боковая панель сессий
Слева — как в MobaXterm — постоянная панель со списком всех сохранённых
сессий (ui::render_sidebar), фиксированной ширины (SIDEBAR_WIDTH = 24
колонки). Открытые сейчас как табы сессии помечены точкой, активная —
подсвечена инверсией. Сворачивается/разворачивается по F3, освобождая
эти 24 колонки под содержимое терминала.
Важный нюанс: при сворачивании/разворачивании панели меняется реальная
ширина видимой области терминала, поэтому F3 не просто перерисовывает
UI — он ресайзит PTY на удалённой стороне (transport.resize) для всех
табов, как при обычном ресайзе окна. Без этого full-screen приложения на
той стороне (vim, htop, less) не узнали бы о появившемся/пропавшем месте.
Панель интерактивна, но не одновременно с терминалом — есть явный
фокус, переключаемый F1 (если панель была скрыта, F1 заодно её
показывает). В фокусе рамка панели становится жёлтой — это единственный
визуальный признак того, куда сейчас уйдёт следующая клавиша. Пока панель
в фокусе, любая клавиша принадлежит ей, а не терминалу (даже
нераспознанная — просто игнорируется), чтобы не было пограничных случаев
вида "стрелка ушла и туда, и туда":
↑/↓(илиj/k) — выбор сессии в спискеEnter— переключиться на уже открытый таб этой сессии, либо открыть новый (без модального picker'а — сессия уже выбрана курсором)Esc— вернуть фокус терминалу (панель остаётся видна)
F4 (SFTP) при открытии сразу переводит фокус на панель — незачем
открывать браузер файлов и не иметь возможности сразу же по нему
перемещаться. Esc внутри SFTP-режима возвращает и режим панели к списку
сессий, и фокус терминалу — одним действием.
SFTP-браузер
F4 открывает в той же боковой панели файловый браузер для активного
таба (переиспользует уже открытое SSH-соединение — отдельный channel с
subsystem sftp, без нового пароля/ключа). Работает только для SSH,
для Telnet-сессий покажет заглушку "SFTP недоступен".
Пока панель в режиме SFTP, стрелки/j/k/Enter/Esc уходят в неё, а не в
шелл — так и должно быть, иначе непонятно, куда идёт навигация:
↑/↓(илиj/k) — выбор записи в спискеEnterна папке — зайти внутрь;..наверху списка — выйти на уровень вышеEnterна файле — скачать в~/Downloads/komiterm/<имя_сессии>/u— открыть локальный файловый браузер и закачать выбранный файл в текущую директорию (см. ниже)Esc— вернуть панель к списку сессий (сама SFTP-сессия при этом не закрывается — переключение обратно наF4покажет тот же листинг без повторного похода на сервер)- Повторный
F4— то же самое, чтоEsc
Закачка файлов (u)
u внутри SFTP-режима открывает localfs::LocalBrowser — тот же список
файлов, но по локальному диску, начиная с домашней директории (обычный
std::fs, никакого SSH). Навигация та же (↑/↓/j/k, .. наверху),
Enter на файле закачивает его в текущую директорию SFTP-браузера
активного таба под тем же именем и возвращает в SFTP-режим с уже
обновлённым листингом (закачанный файл сразу виден в списке). Esc —
отмена, назад в SFTP-режим без закачки.
sftp.rs — тонкая обёртка над russh_sftp::client::SftpSession: листинг
директории (папки впереди файлов, дальше по алфавиту), cd через
canonicalize (чтобы .. и симлинки резолвил сам сервер, а не наша
склейка строк), download/upload. Download/upload грузят файл целиком в
память, для реально больших файлов нужно будет стримить чанками — см.
TODO в конце sftp.rs.
API russh_sftp сверен с реальными примерами из репозитория
(AspectUnk/russh-sftp/examples/client.rs, тот же паттерн — в
russh/examples/sftp_client.rs), а не написан по памяти.
Строка состояния хоста
Внизу экрана (под содержимым терминала, отдельной строкой) — CPU/память/
load average активного таба, обновляется раз в STATS_INTERVAL (5 секунд
по умолчанию, константа в app.rs).
Данные берутся не парсингом вывода интерактивного шелла (это было бы
ненадёжно — зависит от промпта, алиасов, запущенной программы), а отдельным
exec-каналом той же SSH-сессии — Transport::run_command(), новый
метод трейта, третий по счёту наряду с open_sftp (тоже дефолт Ok(None)
для Telnet). Команда гоняет cat /proc/loadavg, первые строки
/proc/meminfo и первую строку /proc/stat, разделяя секции меткой @@@
для надёжного парсинга (hoststats::parse). Соответственно работает
только на Linux-хостах и только по SSH — для Telnet и не-Linux строка
состояния просто показывает имя сессии без метрик, без ошибок в интерфейсе.
CPU% — не то же самое, что load average: это доля "не-idle" времени между
двумя последовательными снятиями /proc/stat (как считает top/htop),
поэтому появляется только со второго опроса — на первом кадре после
подключения будет прочерк, пока не накопится дельта.
Честный компромисс: опрос выполняется прямо в основном цикле и блокирует отрисовку/ввод на время round-trip команды (обычно доли секунды, но на дальних/медленных соединениях может быть заметно) — раз в 5 секунд это редкость, но не бесплатно. Опрашивается только активный таб — у фоновых вкладок статистика не обновляется, пока на них не переключишься.
Serial / COM-подключения
Для консольных портов сетевого/embedded-оборудования — как в MobaXterm, но
без модема и прочего "телефонного" наследия PuTTY. Транспорт (serial.rs)
использует tokio-serial — тот же паттерн read/write-задач, что и у telnet,
просто без протокола поверх (сырые байты в обе стороны, никакого IAC).
[[session]]
name = "switch-console"
protocol = "serial"
port = "/dev/ttyUSB0" # Linux/macOS; на Windows — "COM3", "COM15" и т.п.
baud_rate = 115200 # выбор скорости — обязательное поле, дефолта нет
# Остальное — опционально, дефолт 8N1 без flow control (подходит для
# подавляющего большинства консольных портов):
# data_bits = "eight" # five | six | seven | eight
# parity = "none" # none | odd | even
# stop_bits = "one" # one | two
# flow_control = "none" # none | software | hardware
При подключении к serial-сессии в терминал печатается небольшая справка (один раз, при открытии таба) — те самые нюансы, которые не видны нигде в интерфейсе:
--- Serial-подключение: /dev/ttyUSB0 @ 115200 бод ---
- Порт занят другим приложением? Один порт — одно соединение одновременно.
- Linux: Permission denied при открытии? Добавь себя в группу dialout
(или uucp) и перелогинься: sudo usermod -aG dialout $USER
- data bits/parity/stop bits/flow control должны совпадать с устройством
на другом конце — по умолчанию тут 8N1 без flow control.
Ограничения, которые стоит держать в голове:
- Занятость порта. ОС не даёт двум процессам открыть один и тот же
serial-порт одновременно — если он уже занят (в другом терминале,
в MobaXterm, в
screen/minicom), подключение вернёт ошибку открытия, а не зависнет. - Права доступа только на Linux/macOS. На Windows обычно ничего
дополнительно настраивать не нужно; на Linux без группы
dialout(uucpна некоторых дистрибутивах) —Permission denied. - Настройки порта должны совпадать с устройством. Неверный baud rate/parity — не ошибка подключения, а нечитаемая ерунда вместо текста ("mojibake"), потому что порт открывается успешно независимо от того, правильно ли задан протокол данных поверх него.
- Нет ни SFTP, ни строки состояния хоста — оба используют SSH-специфику
(
open_sftp/run_command), а serial-транспорт не переопределяет эти методы трейта, так что дефолтныеOk(None)отключают их автоматически, без специального кода в UI. Строка состояния просто покажет имя сессии, как для Telnet. resize()— no-op. У физического провода нет понятия "размер терминала" — устройство на другом конце просто пишет вслепую в свой обычный vt100/ANSI, ресайз окна KomiTerm ни на что не влияет.
Рендер терминала
ui::render_terminal проходит по сетке alacritty_terminal напрямую через
grid[Line(row)][Column(col)], а не через display_iter() — так проще
гарантированно сохранить построчную раскладку. Поддержано: цвет текста/фона
(Named/Indexed/Spec — включая true color), bold/dim/italic/underline/
strikeout/inverse, курсор (инверсией текущей ячейки). Соседние ячейки с
одинаковым стилем схлопываются в один Span, чтобы не плодить по Span на
символ на длинных однотонных строках.
Что реально работает
Ниже — история того, что уже прогонялось через настоящий cargo build на
реальной машине (не только в песочнице), а не только через сверку с
документацией. Первый прогон нашёл 7 реальных ошибок компиляции, второй —
ещё одну (сигнатура Channel::data). После этого cargo build --release
прошёл полностью — остались только предупреждения о неиспользуемом коде
(почищены). Живое подключение к серверу/устройству пока не проверялось.
Что было сломано и как исправлено (см. git-историю/этот README для контекста, если он у тебя есть):
async-traitне был объявлен вCargo.toml, хотя использовался everywhere дляTransport— добавлено.russh::keys::learn_known_hostsне реэкспортируется на верхнем уровне в версии 0.62.2 (там есть толькоcheck_known_hosts) — записываем строку в~/.ssh/known_hostsсами, черезPublicKey::to_openssh()+ дозапись в файл, без обращения к приватным/непереэкспортированным функциям russh.authenticate_publickey/authenticate_passwordвозвращаютAuthResult(с методом.success() -> bool), а не голыйbool, как предполагалось изначально — поправлено.Handle::window_changeв этой версии не существует — толькоChannel::window_change. Раз уж пришлось трогать эту часть, канал (shell/PTY) целиком переехал в отдельную задачу, которая мультиплексирует входящие сообщения от сервера и команды (write/resize/close) отTransportчерезtokio::select!, вместо того чтобы держатьChannelIdи слать всё черезHandle.alacritty_terminal::event::WindowSizeне реализует нужный трейтDimensionsдляTerm::new/Term::resize(это разные вещи:WindowSize— размер в пикселях для PTY,Dimensions— размер в колонках/строках символьной сетки) — завели свою маленькую структуруGridSizeвterm.rs.Handle::data(...)возвращаетResult<(), Bytes>(не пропущенные данные возвращаются как есть, а не завёрнуты в нормальныйError) — обёрнуто черезmap_err, чтобы?не спотыкался о недостающийimpl StdError.- (Второй раунд, после того как первый прогон дошёл до
Channel::data) —Channel::data<R: AsyncRead + Unpin>(&self, data: R)принимает источник для чтения, а не готовый буфер/что-то конвертируемое черезInto(в отличие отHandle::data, у которого сигнатура другая) — вместоdata.into()теперьchannel.data(&data[..]),&[u8]сам реализуетAsyncRead. - (Третий раунд — уже не ошибка компиляции, а баг в поведении: экран при
запуске не очищался.)
enable_raw_mode()включает построчный ввод, но сам по себе не переключает терминал на отдельный "alternate screen buffer" — тот самый механизм, из-за которогоvim/htop/lessрисуют на чистом экране, а при выходе возвращают терминал ровно в то состояние, в котором он был (включая историю команд шелла). Без него komiterm рисовал свой UI поверх текущего скроллбэка — добавленыenter_tui()/leave_tui()вapp.rs, вызывающиеEnterAlternateScreen/LeaveAlternateScreenизcrosstermпарно сenable_raw_mode()/disable_raw_mode()на каждом пути выхода изrun()(их несколько — отмена выбора сессии, ошибка подключения, обычное завершение). - (Четвёртый раунд — первое подключение к реальному SSH-серверу прошло
успешно, но нашлось два визуальных бага.) Промпт passphrase от
rpassword(и TOFU-промпт known_hosts) пишет прямо в tty в обходratatui—Terminalпро эту запись не знает и её не перерисовывает, поэтому огрызок текста промпта оставался на экране поверх строки состояния. Добавленterminal.clear()?сразу после каждогоtransport::connect(), случающегося уже внутри TUI (стартовый picker,F2,Enterпо сессии в фокусированной панели) — форсирует полную перерисовку экрана с нуля вместо диффинга, который не видел "чужой" записи. - (Пятый раунд, тот же скриншот.) Название сессии в боковой панели не
помещалось / было не видно целиком. Вероятная причина — "тяжёлые"
юникод-символы (● ➤ 📁 и т.п.), чья реальная ширина в конкретном шрифте
терминала может не совпадать с тем, что предполагает расчёт колонок в
ratatui(типичная проблема для символов вне базовой латиницы/кириллицы — не все шрифты меряют их как честную одну ячейку). Заменил все декоративные символы на простой ASCII (*,>,[D],_,-) и увеличилSIDEBAR_WIDTHс 24 до 30 колонок с запасом. Если после этого текст всё ещё обрезается — это, вероятно, уже про шрифт терминала (кириллица не всегда идеально моноширинная в некоторых шрифтах), а не про код. - (Шестой раунд — не баг, а конфликт хоткея, плюс найденный заодно баг.)
Дефолт
quitсменён сctrl+alt+cнаf10(конфликтовал с DE/WM у пользователя; существующиеhotkeys.tomlэто не трогает — старые конфиги правятся вручную). Заодно нашлась дыра: если сессия завершалась сама (exit/Ctrl+Dв шелле), таб не закрывался, а следующая же клавиша в нём валила всё приложение —tab.transport.write(...).await?пробрасывал ошибку записи в мёртвый канал через весьmain_loop. Теперь канал каждого таба каждый кадр проверяется наTryRecvError::Disconnected(не просто "пусто сейчас", а "закрыт навсегда") — таб с завершившейся сессией закрывается автоматически, без валящего приложение падения.
Что осталось (честные пробелы):
- ssh.rs:
AuthMethod::Agentвременно отключён — метод, которым это было изначально реализовано (authenticate_futureнаHandle, взят из старого примера в репозитории russh), в используемой версии (0.62.2) не существует. Актуальную замену не удалось подтвердить без живой компиляции — вместо третьей попытки угадать API вслепую, честно возвращаю ошибку с указанием использоватьKeyилиPassword. Если agent важен — дай знать, разберёмся предметно по фактической ошибке компилятора, как в этот раз. - ui.rs: мастер создания сессии (
nв списке сессий) — текстовые поля только добавляют/стирают символы с конца строки, без перемещения курсора внутри неё и без валидации на лету (например, нечисловой порт просто тихо упадёт на дефолт при парсинге). Для тех же целейkomiterm profile add(создание credential-профиля) по-прежнему только черезkomiterm profile addв терминале, не из этого мастера — они пока не связаны. Из боковой панели в режиме списка сессий (F1, неF2)nпока не работает — только из самого пикера приF2/начальном запуске. - sftp.rs: закачка привязана к UI, но download/upload по-прежнему
грузят файл целиком в память, нет rename/mkdir/delete в браузере (у
SftpSession методы есть) — см. TODO в конце
sftp.rs. - hoststats.rs: только Linux (
/proc) и только SSH; опрос блокирует главный цикл на время round-trip. Не проверено на реальном хосте. - serial.rs: создать serial-сессию можно и через мастер (
n), и руками вsessions.toml— но нетSerialPort::available_ports()ни там, ни там (списка реально подключённых портов, а не текстового поля вслепую). Не проверено на реальном устройстве. - telnet.rs: отвечаем на DO/WILL только для ECHO и SUPPRESS-GO-AHEAD; NAWS (передача размера окна) не реализован.
- sshconfig.rs:
Match-блоки и полноценные glob-паттерны не поддержаны. - profiles.rs: добавление профиля — простые вопросы в stdin, не форма в ratatui (осознанно: это CLI-подкоманда вне raw-режима основного TUI).
- Версии крейтов в
Cargo.tomlпо-прежнему не проверялись сборкой в песочнице (тот же древнийrustc 1.75, упирается в edition2024 на первом же транзитивном пакете) — но теперь у нас есть подтверждение с реальной машины на актуальном тулчейне, и именно оттуда взяты все исправления выше.
Дальше по roadmap (после MVP)
- SSH-туннели (local/remote/dynamic port forwarding) — russh это умеет
- Логирование сессии в файл
- NAWS для telnet (передача размера окна серверу)
- rename/mkdir/delete в SFTP-браузере (методы у SftpSession уже есть)
Сборка
cargo build --release
Сессии редактируются в ~/.config/komiterm/sessions.toml, формат:
# Явный ключ прямо в сессии, host — реальный, ssh_config не трогаем
[[session]]
name = "myserver"
protocol = "ssh"
host = "example.com"
port = 22
user = "vladimir"
use_ssh_config = false
[session.auth]
method = "key"
path = "/home/vladimir/.ssh/id_ed25519"
passphrase = { source = "prompt" }
# host — алиас из ~/.ssh/config; HostName/User/IdentityFile подтянутся
# оттуда сами, если тут не переопределены
[[session]]
name = "prod-box"
protocol = "ssh"
host = "prod" # ищется в ~/.ssh/config как `Host prod`
# Логин/способ входа переиспользуются из именованного профиля
[[session]]
name = "staging-box"
protocol = "ssh"
host = "staging.internal"
credential = "work-key" # см. `komiterm profile add` / credentials.toml
[[session]]
name = "old-switch"
protocol = "telnet"
host = "192.168.1.1"
port = 23
Профили входа лежат отдельно в ~/.config/komiterm/credentials.toml и
управляются через komiterm profile add/list/remove (см. раздел выше) —
руками их редактировать не обязательно.