Раздел: AI & Agents
Идентификатор:remote· Экран:ui/codex/RemoteAgentsScreen.kt(683) +RemoteAgentsFormat.kt
Снимок: 2026-10-11
1. Зачем и что это
Смысл схемы: телефон владельца — «мозг», он держит модели и веб-инструменты; сервер — вычислитель и файлы. Связь двусторонняя:
- forward-туннель — телефон поднимает SSH-туннель до сервера и проксирует локальный
127.0.0.1:8789(codex app-server) наружу; - reverse-туннель — сервер держит
127.0.0.1:19000, через который ходит на телефон за моделями и MCP.
Экран управляет SSH-частью: добавить сервер, проверить, подключить, перезапустить демон, посмотреть логи, перевыпустить токены, поставить окружение.
2. Как попасть
Хаб инструментов → секция AI & Agents → карточка «Удалённые агенты» (hub_remote_title).
| Откуда ещё | Путь |
|---|---|
| Экран терминала | «Управление SSH-серверами» → Screen.RemoteAgents (MainRouter.kt:620) |
| Мост | app_navigate с screen: "remote_agents" |
Маршрут: Screen.kt:135 ("remote" -> Screen.RemoteAgents) → MainRouter.kt:380.
3. Экран: элементы и действия
| Элемент | Что делает | Где в коде |
|---|---|---|
Шапка y83@ssh :~$ | Терминальная шапка; цвет каретки зависит от состояния; meta: «N servers · M connected · updated HH:MM:SS» | RemoteAgentsScreen:321, SSH_PROMPT |
| Кнопка Refresh | Перезагрузка списка и статусов (700 мс анимации) | refreshNow() |
Кнопка Add | Диалог добавления сервера | onAddServer |
Секция // connected | Живые серверы, по имени | splitSshSections |
Секция // offline | Остальные | То же |
| Карточка сервера | Точка статуса, user@host:port, host key, метки | SshServerCard |
| Метрики карточки | user@host:port, host key <fp>, ssh key, setup ready/incomplete/in progress, seen HH:MM:SS | sshMetaItems |
| Кнопки карточки | Connect / Disconnect, Test, Setup, Restart, Logs, Rotate token | onConnect, runTest, onSetup, runService, rotateTokens |
| Свайп по карточке | Запрос удаления (с подтверждением) | SwipeableSshRow → ConfirmDeleteDialog |
| Шторка вывода | Результат сервисной операции (restart / logs / rotate) | ServiceDialogState |
| Диалог Setup | Прогресс установки node.js / codex / systemd | SshSetupDialog |
| Диалог host key | Предупреждение о смене host key с отпечатками | HostKeyTrustRequest |
| Snackbar | Типизированные уведомления (успех / предупреждение / ошибка) | TypedNoticeBar |
4. Возможности подробно
Статус сервера
sshStatusKind(server, tunnelConnected) — приоритет состояний:
| Состояние | Условие | Метка |
|---|---|---|
CONNECTED | туннель живой | connected |
ONLINE | status == reachable | online |
FAILED | status == auth_failed | auth failed |
OFFLINE | иначе | offline |
Живой туннель важнее результата последней проверки: если tunnelIsLive(status, localPort) вернул true, сервер показывается как connected, даже если проверка SSH проходила давно. Секции // connected и // offline строятся по этой же функции, поэтому всегда согласованы с точками в карточках.
Проверка (Test)
runTest(server) вызывает remoteTest(host, port, username, authRef):
| Условие | Плашка | Статус в БД |
|---|---|---|
ok | connected (+ версия codex, если есть) | reachable |
errorKind == host_key_changed | host key changed + диалог доверия | — |
status == auth_failed | auth failed | auth_failed |
| иначе | unreachable | unreachable |
Плюс обновляются lastSeenAt, hostKeyFingerprint (TOFU — «trust on first use») и authType переключается на key, если result.keyInstalled.
Host key (TOFU)
Отпечаток host key запоминается при первом подключении и сверяется при каждом. applyHostKey(server) сама подтягивает текущий отпечаток (remoteHostKey) и записывает его. Если отпечаток поменялся после того, как был зафиксирован, экран показывает предупреждение с двумя значениями (known / got) и ждёт явного решения — автоматического доверия новому ключу нет.
Connect / Disconnect
Подключение вызывает onConnect(server.id) и через 1.5 секунды обновляет статус туннеля и host key. При разрыве метка ставится до запроса на закрытие туннеля, чтобы watchdog не успел пересоздать туннель за пользователя.
Сервисные операции
runService(server, action) — действия над codex app-server на сервере: restart и logs. Результат показывается в шторке вывода: сначала output, при пустом — error, иначе ok / no output.
Перевыпуск токенов
rotateTokens(server) вызывает remoteRotateTokens(serverId, host, port, username, password) и получает пару (ok, message). На сервере выпускаются новые per-server bearer-токены для роутера моделей и MCP. Шторка показывает «token rotated» или «token rotation failed».
Установка окружения (Setup)
Диалог SshSetupDialog шлёт POST /api/v1/remote/setup с таймаутом чтения 120 секунд (установка долгая). Ответ разбирается пофлажно:
| Поле ответа | Что означает |
|---|---|
nodeInstalled | node.js установлен |
codexInstalled | codex cli установлен |
systemdReady | systemd-unit готов |
output содержит started / running | демон запущен |
setupState становится ready, только если все три флага ok; иначе failed. Состояние пишется в локальную БД (RemoteServerEntity).
Секции и шапка
sshSummaryItems(total, live) даёт «N servers» и, если есть живые, «M connected». Время снимка — sshClockLabel в формате HH:MM:SS. Разделение на секции — splitSshSections: живые сверху, офлайн снизу, внутри — по имени (стабильный порядок).
5. Данные: БД, файлы, настройки
| Что | Где лежит | Как меняется | Кто читает |
|---|---|---|---|
| Список серверов | Room, RemoteServerEntity | Диалог добавления/правки, remoteServerDao().upsert | Экран, терминал, туннели |
| Host key fingerprint | Поле hostKeyFingerprint | applyHostKey / runTest | Проверка SSH при подключении |
| Статус и setup | Поля status, setupState, lastSeenAt | runTest, Setup-диалог | Экран |
| Креды сервера | authRef (пароль SSH; в базе — шифротекст, поле SecretString) | Диалог сервера | SSH-транспорт, туннель |
6. Backend: REST/роуты
| Метод и путь | Назначение |
|---|---|
POST /api/v1/remote/tunnel/open | открыть SSH-туннель к серверу |
POST /api/v1/remote/tunnel/close | закрыть туннель |
GET /api/v1/remote/tunnel/status | статус туннеля (status, localPort, reverseHealthy, daemonRunning, lastError, reconnectCount) |
POST /api/v1/remote/tunnel/reconnect | переподнять туннель |
POST /api/v1/remote/tunnel/check | «Check now» для одного сервера |
POST /api/v1/remote/setup | установка node.js / codex / systemd на сервере |
POST /api/v1/remote/service | действие над codex app-server (restart / logs) |
GET /api/v1/remote/tokens | текущие токены роутера/MCP сервера |
POST /api/v1/remote/tokens/rotate | перевыпуск per-server токенов |
POST /api/v1/remote/tokens/revoke | отзыв токенов |
POST /api/v1/remote/test | проверка SSH + host key |
GET /api/v1/remote/hostkey | отпечаток host key |
Роуты: backend/internal/api/v1/routes.go:389–426 (tunHandler.Open/Close/ Status/Reconnect/Check/Setup/Service/Tokens/RotateTokens/RevokeTokens).
7. Мост и MCP
| Что | Как | Примечание |
|---|---|---|
| Открыть экран | app_navigate с screen: "remote_agents" | BridgeScreenCatalog.kt:164 |
| Диагностика туннеля | GET /api/v1/remote/tunnel/status или POST …/check | Скилл y83-remote |
| e2e-проверка | PROBE=1 PROBE_MCP=1 bash scripts/remote-e2e.sh <host> | Скилл y83-remote |
MCP-профиль /mcp/remote (на сервере) даёт агенту на Ubuntu инструменты через телефон, но не даёт управления телефоном (bridge-команд) и выгрузки токенов.
8. Настройки, конфиг, квоты и лимиты
| Параметр | Значение | Где задаётся |
|---|---|---|
| Порт SSH по умолчанию | 22 | RemoteServerEntity.port |
| Таймаут Setup | 120 с чтения | Диалог Setup |
| Connect → refresh | 1.5 с | onConnect |
| Анимация Refresh | 700 мс | refreshNow |
| Статусы | unknown / reachable / unreachable / auth_failed | RemoteServerEntity |
| Setup-состояния | none / in_progress / ready / failed | То же |
9. Ловушки и ограничения
- Здоровье туннеля = ready + reverseHealthy + daemonRunning. Поле
lastErrorпри этом может быть непустым — это история сбоев, а не текущая проблема. Ориентироваться нужно наerrorActiveиreverseHealthy/daemonRunning. - Живой туннель перекрывает статус проверки. Сервер может показываться connected, даже если последний Test был давно.
- Смена host key требует явного решения. Автодоверия нет — это защита от MITM.
- Setup долгий (до 120 с), а «daemon started» определяется по подстроке в выводе, а не по отдельному флагу — при нестандартном выводе будет «check manually».
- Все строки захардкожены по-английски («connected», «setup ready» и т. д.) — экран не локализован.
setupStateобновляется только после Setup — сервер сам туда не пишет.- Телефон не читает токены сервера. Для remote-профиля нужно, чтобы туннель был переподнят с per-server токеном; legacy-токен авторизует остальные инструменты, но не читает remote-файлы.
bridge_commandна remote-профиле недоступен — в отличие от первоклассных browser-тулов.
10. Источники правды
app/src/main/java/com/y83/ws/ui/codex/RemoteAgentsScreen.kt— экран.app/src/main/java/com/y83/ws/ui/codex/RemoteAgentsFormat.kt— статусы, секции, форматирование.app/src/main/java/com/y83/ws/data/db/RemoteServerEntity.kt— модель сервера.app/src/main/java/com/y83/ws/ui/agents/AgentsViewModel.kt—tunnelIsLive.app/src/main/java/com/y83/ws/data/api/— клиент:remoteTest,remoteService,remoteRotateTokens,tunnelStatus,tunnelClose.backend/internal/api/v1/routes.go:389–426— роуты remote/tunnels.skills/y83-remote/SKILL.md— архитектура remote, диагностика, туннель.scripts/remote-e2e.sh— e2e-проверка связки.