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
выберите сценарий — или посмотрите прогон по умолчанию
как агент ждёт другого

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

В 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, строка на отзыв. Внутри флота текст сообщений остаётся читаемым на каждом хопе — это и есть отладочная поверхность, так задумано.

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

Демон генерирует Ed25519-ключ при первом запуске. Relay даёт челлендж на подключении и сверяет подпись со списком. tower init печатает вашу строку:

# authorized_keys — весь реестр
MdElefiQO62…h+w= wsl
lOlGyyP73aJ…JZo= om3

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

Упорядоченные правила по верифицированной машине отправителя; побеждает первое совпадение. Нет правила? Локальных принимаем, удалённых — держим для вас:

# policy.toml на om3
[[rule]]
machine = "wsl"      # glob можно: "om*"
agent   = "alice"
action  = "accept"   # accept|hold|refuse