TOWER
универсальный мост сообщений между агентскими CLI Claude Code ↔ Codex ↔ Kimi ↔ всё, что умеет MCP — между машинами, через relay, который вы хостите сами.
лицензия Apache-2.0 язык Go, 4 статических бинарника зависимости websocket + toml
проиграть доставку
МАШИНА wsl МАШИНА om3 MCP UDS WS + Ed25519 WS + Ed25519 UDS MCP claude-code alice@wsl tower-mcp stdio-шим towerd демон outbox/ · на диске tower-relay stateless · ничего не хранит towerd политика: default hold held/ inbox/bob/ tower-mcp stdio-шим codex bob@om3
выберите сценарий — или посмотрите прогон по умолчанию
установка

Две команды. Дальше оно само

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

на relay-хостеодин раз
# файл флота создастся сам, пустой — это нормально
tower-relay --listen :7443 \
  --authorized-keys /etc/tower/authorized_keys

# чеканим приглашение для машины
tower-relay invite --name om3 \
  --url wss://relay.example.com/ws
выведет готовую строку: tower join wss://relay.example.com/ws --token gsd3gs…
на машинеодна команда
tower join wss://relay.example.com/ws \
  --token gsd3gs…
сгенерировал ключ машины
погасил инвайт — relay сам вписал ключ в свой authorized_keys
записал config.toml
прописал tower-mcp в claude-code, codex, kimi, gemini, opencode
поставил systemd-юнит и поднял демон
и сразу: tower agents — видно весь флот
ничего не правится руками

Ключи не копируются, authorized_keys не редактируется, конфиги CLI находятся сами. Одна машина без флота? Тогда и relay не нужен: tower setup — и агенты на ней уже общаются.

доверие флоту вместо подтверждений

Машина попала во флот только потому, что вы выдали ей инвайт — членство и есть согласие. Никаких «одобрить сообщение» на каждый чих. Нужно строже — tower lock, и всё чужое ждёт вашего approve.

диагностика ничего не чинит молча

tower doctor только смотрит и говорит, что не так, — менять состояние это дело tower setup. Команда, которая чинит втихую, однажды сломает то, чего вы не просили.

один владелец демона

Шим поднимает демона сам, если тот не запущен, но при наличии systemd-юнита уступает ему запуск. Занятый сокет — не ошибка, а «уже есть, отхожу»: никаких войн процессов и рестарт-циклов.

как агент ждёт другого

Ожидание без блокировки

В tower нет «повиснуть на сокете и ждать». Ожидание — это договорённость из трёх частей: reply_to связывает вопрос с ответом, durable inbox хранит ответ на диске сколько угодно, а nudge — необязательный будильник для совсем простаивающей сессии. Никто никого не держит.

alice@wsl claude-code · спрашивает и продолжает жить
bob@om3 codex · занят своим ходом
alice → send_message
«миграция доехала? можно ребейзиться на main?» → m_42
alice не ждёт на проводе: получает id, заканчивает ход. В её outbox: queued → delivered — доставлено в inbox боба, но ещё не прочитано.
bob · середина хода
гоняет тесты, 4-я минута tool-вызова… конверт лежит в inbox/bob/ на диске
доставка не врывается в ход: сообщение не прерывает запущенный инструмент и не теряется — оно просто ждёт в файле.
bob → send_message (по другому делу)
ответ инструмента: «отправлено … · пока ты работал, пришло 1 сообщение: [m_42] от alice@wsl: …»
piggyback: каждый send заодно отдаёт непрочитанное. Болтливый агент получает почти push-латентность без всякого push-механизма.
bob → send_message
«да: колонка tenant_id, ребейзись смело» reply_to: m_42
корреляция по id, а не по хрупкому «в ответ на твоё последнее сообщение».
towerd@wsl → tmux-пана alice
[tower] 1 unread message — call check_inbox
nudge (opt-in): alice простаивает — демон печатает строку ей в пану. Это at-most-once подсказка: потерянный nudge — это latency, не потерянное сообщение.
alice → check_inbox
«[m_51] от bob@om3 (ответ на m_42): да: колонка tenant_id, ребейзись смело»
ожидание сомкнулось: alice видит, что это ответ именно на её вопрос, и продолжает с того места.
спросить и ждатьОтправь с текстом «отчитайся reply_to», заканчивай ход. Ответ догонит: piggyback на send, check_inbox на паузе или nudge.
статус длинной задачиМиграция на om3 шлёт «этап 3/7 готов» — сессия-наблюдатель читает у себя, когда удобно. Никто не поллит по ssh.
если ответа нетЭто видно, а не подвисло: tower outbox показывает queued с причиной, TTL переведёт в expired, и отправитель узнает.
оркестрация

Оркестратор в herdr

herdr держит агентов по вкладкам: в каждой — свой CLI в своём git-worktree. tower делает вкладки адресуемыми: диспетчер раздаёт задачи сообщениями, исполнители отчитываются обратно, и всё это — durable-файлы, а не хрупкий скрейпинг чужих терминалов. Одна машина — общение по UDS без сети; разнеси вкладки по om-нодам — тот же код поедет через relay.

herdr 1:dispatcher2:codex3:grok4:kimi
claude-codedispatcher@wsl · worktree main
codexcodex-auth@wsl · worktree fix-auth
grokgrok-ui@wsl · worktree ui-panel
kimikimi-tests@wsl · worktree tests

Диспетчер не поллит исполнителей в цикле — он заканчивает ход, а отчёты будят его nudge-ем или догоняют piggyback-ом. Рестартни любую вкладку: её inbox и очереди — файлы на диске, задачи не испаряются. tower outbox в любой момент показывает, какая задача у кого «висит». А петля из двух слишком вежливых агентов гасится rate-limit-ом и дедупом повторов.

рой

Двадцать четыре агента — это те же конверты, только много

Рой не требует нового механизма. Fan-out — это N сообщений, каждое со своим durable-файлом в outbox и своим сквозным ack-ом. Воркеры — headless-сессии (claude -p, codex exec) с тем же tower-mcp: хоть во вкладках herdr, хоть под systemd на om-нодах. Упавшая нода — это queued, а не «потерялось»: очередь доезжает сама, когда нода возвращается.

queued 0 working 0 done 0/24 диспетчер раздаёт задачи волнами
claude-code dispatcher@wsl outbox/ · 24 файла
адресация масштабируетсяlist_agents отдаёт весь ростер с relay: worker-01@om3 … worker-24@om7. Раздача — обычный цикл send_message.
сводка без опросаДиспетчер не поллит 24 воркеров: tower outbox — это готовый роллап (кто delivered, кто queued и почему), а отчёты сами будят его nudge-ем.
рой не идёт вразносRate-limit на отправителя, дедуп повторов и потолок непрочитанного: петля из двух чересчур услужливых агентов гаснет сама.
дорожная карта · v0.2 «Swarm Dispatch»

Доставка как пробуждение

Сегодня рою нужен кто-то, кто заранее запустил воркеров. В v0.2 это исчезает: сообщение объявленному, но не запущенному агенту запускает его. Рой материализуется по требованию — а из этого следует набор жёстких механических решений, каждое из которых проверено тремя независимыми моделями.

локальная активация: включена объявлением юнита удалённая: remote = false по умолчанию
# agents.toml на om3 — команду задаёт машина, не сообщение
[[unit]]
name          = "worker-*"
command       = ["claude", "-p", "$(cat prompts/worker.md)"]
cwd           = "/srv/wt/{{name}}"
max_instances = 8
drain_deadline = "120s"
remote        = false   # ← удалённый спавн только явно
1seen-set — id сообщения не встречался: это не реплей, путь активации открыт
2policy accept + совпал юнит worker-*, у юнита remote = true
3registry lock по имени агента — 11 остальных конвертов волны спавна не порождают
4spawn setsid, детач, pid в реестр, запись в аудит-лог
5inbox-drain — воркер монтирует tower-mcp и первым делом зовёт check_inbox
6report — ответ с reply_to, воркер выходит; towerd жнёт exit-код
процессне запущен
inbox/worker-07пусто
outbox отправителя
нажмите «проиграть», чтобы посмотреть путь сообщения через активацию
launcher, не supervisor

towerd запускает ребёнка detached, пишет pid и жнёт exit-код для наблюдаемости — и никогда не рестартит. Повторный запуск — побочный эффект следующей доставки, а не политика супервизии.

не строим: рестарты, DAG, зависимости, worker leasing
один путь доставки

Текст сообщения никогда не подставляется в argv или env: задача уже лежит в durable inbox, воркер её вычерпывает. Нет второго пути доставки — нет ни инъекций, ни секретов в ps.

не строим: шаблоны {{text}} в команде
шестое состояние: stalled

Воркер запустился, но не вычерпал inbox за drain_deadline — отправителю уходит честный stalled (с exit-кодом, если ребёнок умер). Строго «не забрал сообщение», а не «провалил задачу».

не строим: вывод об успехе задачи
ровно один спавн

Замок по имени агента коалесцирует одновременную волну в один запуск; реплеи ловит 7-дневный seen-set, проверяемый строго до пути активации. thread при этом не получает никакой lifecycle-семантики.

не строим: таймерные эвристики cooldown
что ещё едет в v0.2: поле thread + tower outbox --thread glob-волны worker-*@om3 с экспансией на демоне-отправителе ростер объявляет declared-юниты, а не только запущенных claude-native nudge через own-child сокет threat model · подписанные бинарники · conformance-набор
что летит по проводу

Одна JSON-строка. Больше ничего.

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

{
  "v": 1,
  "id": "m_8f3ka92c11d04b7e5c20ff41",
  "from": "alice@wsl",
  "to": "bob@om3",
  "text": "миграция готова: колонка
           tenant_id, ребейз на main безопасен",
  "reply_to": "m_c41d…",
  "ts": "2026-08-08T12:00:00Z",
  "ttl_s": 86400
}
idСквозной ключ идемпотентности. Приёмник держит seen-set на 7 дней: пересланный дубль получит повторный ack, но не повторную доставку.
from / toagent@machine. Машинную часть relay сверяет с Ed25519-ключом отправителя — подделать нельзя. Имя агента — это имя, не идентичность.
textПросто текст, максимум 64 КиБ. На приёмной стороне никогда не исполняется и не парсится на команды.
ttl_sНе доставлено к ts + ttl — outbox отправителя переходит в expired вместо вечных ретраев.
vproto_version = 1. Новые поля — только опциональные, незнакомые — игнорируются. Есть зарезервированное поле enc под будущее E2E-шифрование.
состояния доставки

Каждое сообщение отвечает за себя

Durability живёт в outbox отправителя — по JSON-файлу на сообщение, на машине, которую контролируете вы. Relay не хранит ничего, поэтому его рестарт не теряет ни одного сообщения. tower outbox показывает ровно одно из шести состояний:

queued

На диске, пересылается с backoff-ом, пока не придёт сквозной ack.

delivered

Принимающий демон положил в inbox адресата и подтвердил.

held

Политика приёмника отложила для оператора. Станет delivered или refused.

refused

Политика сказала «нет» или оператор отклонил. Отправителю сообщат.

no_agent

На целевой машине нет агента с таким именем.

expired

TTL кончился раньше, чем случилась передача.

stalled v0.2

Воркер запустился по сообщению, но не вычерпал inbox за drain_deadline.

queued → delivered | held | refused | no_agent | expired   held → delivered | refused (решает оператор)   stalled — только про активацию, не про исход задачи

четыре бинарника

Один репозиторий, по одной работе на каждого

Статические Go-бинарники без runtime-зависимостей. Шим — единственное, что видит CLI; демон — единственный владелец состояния; relay — единственный, кто ходит по сети.

tower-mcp
шим — на сессию

stdio MCP-сервер, который монтирует любой агентский CLI. Три инструмента: send_message, check_inbox, list_agents.

Каждый send заодно возвращает непрочитанное — почти push-латентность без push-механики.

хранит: ничего. поднимет towerd, если тот не запущен.
towerd
демон — на машину

Владеет реестром агентов, durable-outbox-ом, inbox-ами, очередью held, политикой и единственным соединением с relay.

Доставка внутри машины идёт по Unix-сокету и не касается сети вообще.

хранит: всё — по JSON-файлу на сообщение.
tower-relay
хаб — self-hosted

Аутентифицирует машины Ed25519-челленджем по authorized_keys, маршрутизирует конверты и ack-и между демонами онлайн, раздаёт ростер.

Намеренно stateless: ни диска, ни очередей, нечего бэкапить.

хранит: ничего. рестарт безопасен по построению.
tower
cli — для вас

agents · send · inbox · outbox · held · approve · deny · doctor

Сторона одобрения default-hold живёт здесь: tower approve m_8f… --persist доставит сообщение и запишет accept-правило для этого отправителя.

хранит: контроль в ваших руках.
модель доверия

Правила SSH, а не церемонии OAuth

Ключ на машину, строка на машину в relay, строка на отзыв — и ни одну из этих строк вы не пишете руками. Внутри флота текст сообщений остаётся читаемым на каждом хопе: это и есть отладочная поверхность, так задумано.

Машины аутентифицируются

Ключ рождается при первом запуске, а в реестр флота попадает сам — через одноразовый инвайт. Файл выглядит так, но пишет его tower join, не вы:

# authorized_keys — реестр флота, заполняется сам
MdElefiQO62…h+w= wsl
lOlGyyP73aJ…JZo= om3

Приёмники решают

Нет правила — работает доверие флоту: машина зашла по вашему инвайту, значит членство и есть согласие. tower lock переключает на «держать и спрашивать», а правила дают промежуточное:

# policy.toml — нужен, только если хочется строже
[[rule]]
machine = "om*"       # glob по верифицированной машине
agent   = "spam-*"
action  = "refuse"   # accept|hold|refuse