# Подписанный stop почтой — B-N-1536

Обычная почта и stop берутся одним MessageReceipts.receive. Stop распознаётся до обычного разбора и передаётся отдельному dispatcher; модель его не исполняет. Native claim остаётся одним wrapper ConversationPause.attach. Native и SDK читают общий durable fence и удержанный backlog. Д3 B-N-1530 сохраняется: B → pressure → mail. Stop является верхним барьером вне текстового composer.

## Кто может послать

Приёмник сверяет Ed25519-подпись, конкретный recipient/ledger, action, delivery_id и TTL ≤120000 мс. В root-owned coordinatorTrustFile отдельный controlKeys для ключей, которым разрешён только stop, и таблица controlPermissions: principal zabava получает pause/replace для перечисленных recipients, principal iskra — pause и, с 2.5.101 на приёмнике и 2.5.108 у отправителя, new поимённо (MAIL-NEW.md). Остальные не получают control. Имя отправителя, OT, имя файла и слова письма прав не дают. Полученные статусы используют другой заголовок и криптографический domain, они не получают control права.

Физические обёртки на aclaude: iskra-tell и его ссылки; coder-tell делегирует тому же коду. Кандидаты — courier/ этого пакета. Старые вызовы сохранены. Новые:

- coder-tell beta --stop-pause
- coder-tell beta --stop-replace -f /path/to/replacement.txt
- zarya-tell --stop-pause
- avrora-tell --stop-replace -f /path/to/replacement.txt

Неизвестные ключи, лишние аргументы, пустой replace и непустой pause отвергаются до передачи. Новый control подписывается последней операцией; обычный штамп и delivery-v1 в его байты не добавляются. На отправляющей стороне helper src/send-mail-stop.mjs и его исходники лежат в /usr/local/lib/dsh-mail-stop/ как целый принятый пакет. DSH_MAIL_STOP_SENDER может указать другой установленный путь helper. Прямая команда helper для Искры на sadik (B-N-1624): sudo node /opt/agent-plugins/dsh-telegram-multiagent/src/send-mail-stop.mjs --recipient beta --mode pause — боевая копия пакета, отдельной копии в /usr/local/lib на sadik нет.

## Конфигурация отправителя

DSH_MAIL_STOP_CONFIG выбирает root-owned JSON (умолчание /etc/dsh-mail-stop/sender.json): version=1, key_id, key_file, modes, recipients. transport=ssh — умолчание для aclaude; transport=local — для Искры на sadik. host (необязательное, с A-N-1948 №20) — имя хоста или псевдоним ssh для transport=ssh, умолчание sadik; допустимы буквы, цифры, «.», «_», «-», первый знак — буква или цифра, иначе отказ sender_config_untrusted. Поле machine в выводе placed — этот хост (при local — имя своей машины). recipients сопоставляет имя с directory полного входящего ящика и ledger_uuid именно этого получателя. Эти адреса/UUID снимает монтирующая рука с текущего боя. Не подставлять UUID другой копии или старого workspace.

Конфигурация не writable группой/прочими. Закрытый Ed25519-файл root-owned, без доступа прочим и без записи группой; допустимо чтение только выделенной группой доверенного отправителя. На aclaude ключ доступен только Забаве, на sadik отдельный ключ — только Искре. Остальным ключи не раздавать: receiver ACL основывается на этих ключах. Значения ключей не выводить и не передавать argv. Helper читает их внутри процесса. Для local отправителю нужны права записи в ящик получателя. На sadik ящики /opt/<агент>/a2a/in принадлежат агентам (755/775), учётка iskra в них писать не может — поэтому helper Искры идёт целиком под sudo: root подписывает и публикует, publish-mail-stop.py отдаёт файл владельцу ящика (B-N-1624). Граница: полный sudo без пароля на sadik есть у нескольких учёток (на 07.10: iskra, zarya, avrora, marc, petrovich, coders, claud, claude) — любой закрытый ключ на машине им читаем; права файла ключа отделяют от прочих учёток, не от них.

ssh передаёт готовые подписанные байты, затем ограниченный publisher по SSH stdin. Publisher проверяет SHA/размер ≤98304, пишет через дескрипторы в защищённом временном каталоге и делает renameat2(RENAME_NOREPLACE). Итоговое имя stop-<control_id>.txt создаётся единожды, owner/mode назначены до видимости в ящике. Не открывается предсказуемый .part. Linux renameat2 требуется; при отсутствии или отказе — отказ/UNKNOWN без повтора. Передача имеет предел15с на scp и15с на ssh, без ретрая. Вывод placed — укладка файла, не завершение stop; содержит control_id и wire_sha для сверки ответа.

## Коды выхода и вывод (A-N-1948 №19)

| helper | код 0 | код 2 | код 3 |
|---|---|---|---|
| src/send-mail-stop.mjs | stdout — одна строка JSON stage=placed (machine, path, control_id, wire_sha, recipient, mode, execution=unconfirmed) | stderr «ОТКАЗ stop sender: <причина>»: доводы, конфиг, ключ, адресат, режим, тело; передачи не было | stderr «ОТКАЗ/UNKNOWN передачи control <id>»: передача начата, укладка не подтверждена; повтора нет, исход сверять по статусу |
| src/read-mail-stop-status.mjs | stdout — JSON проверенного статуса | stderr «ОТКАЗ status reader: <причина>» | — |
| src/sign-coordinator.mjs | stdout — JSON status=signed_not_delivered (подписано, не доставлено) | stderr «ОТКАЗ подписания: <причина>» | — |
| src/observe-mail-stop.mjs | служба: «mail-stop-arrival: ready», выход 0 по SIGINT/SIGTERM | stderr «ОТКАЗ observer: <причина>» при старте | — |

Код 2 означает, что действие не начиналось. Код 3 есть только у sender, и повторять его вслепую нельзя: control мог лечь в ящик.

## Конфигурация приёмника и caller

mailStopConfigFile в настройках Telegram указывает root-owned JSON с полями version=1, recipient, status_key_id, status_key_file, arrival_root. Key ID относится к отдельному ключу статусов машины; private key читается внутри receiver при mount, значения не журналируются. Этот ключ не добавлять ни в keys, ни в controlKeys, ни в controlPermissions. Без читаемого signing слоя действие не начинается.

В coordinatorTrustFile добавить controlKeys, controlPermissions и returnRoutes. Ключ Искры хранить только в controlKeys: общий keys остаётся прежним набором координаторских ключей для обычной почты/goal/decision. Добавление stop-ключа в общий keys расширило бы другие полномочия — так не монтировать. Для ранее принятого ключа Забавы keys сохраняется; crypto-only stop допускает его по соответствующему controlPermissions. returnRoutes выбирается только здесь: recipient адресата ответа, ledger_uuid его канала статусов, directory полной исходящей щели. Для Забавы на aclaude использовать утверждённую исходящую щель tell с recipient zabavy; для Искры — её местный inbox и recipient iskra. Входящий control не выбирает обратный путь. crypto-verified отказы получают подписанный статус с причиной; неподписанные получают audit без ответа. Зовущий порогового сторожа — receiver. При5 неподтверждённых controls за600с он один раз вызывает проверенный owner Telegram; дальнейшие в том же окне остаются в audit.

Первым запустить отдельный root observer src/observe-mail-stop.mjs по systemd/mail-stop-arrival.service. Root-owned конфигурация содержит root=arrival_root и mailboxes со всеми действующими recipient/directory, максимум32. Root metadata-каталог не writable группой/прочими. Unit ReadWritePaths должен совпасть с каталогом. Observer видит новые публикации до parser, сохраняет wall-clock delivered_at, event_id, exact bytes SHA и publication identity. Startup файлы остаются delivery-unknown: время не выдумывается из mtime, issued_at или первого parser. Предел4096 записей; при переполнении новый факт неизвестен и причина журналируется. Монтажная рука следит за заполнением и архивирует старые данные отдельно; автоматического удаления наблюдений нет.

Зовущий приёмника — настоящий pollReceiptDirectory для A2A/spool. Control jobs асинхронны, обычные файлы не ждут SDK terminal. Если watcher ещё не обработал публикацию, source удерживается до1000мс (опрос20мс). Без доказанного arrival — signed refusal delivery_unobserved, без управления. Просроченный подписанный control получает expired с delivered_at/processed_at/delay_ms. Finished ID и сохранённый отказ не становятся новой попыткой даже после смены target/ACL. Противоречивые байты одного ID отвергаются.

## Окончание и продолжение

Диспетчер сохраняет pause и один effect intent до await. Remote stop адресует exact SDK request/thread/turn через stop-exact. Reader в adapter получает task-status ready/terminal. Producer сначала сохраняет finite SDK result/turn-completed точного lease; у Codex закрытие хода и доказательство разведены (B-N-1536 r2, У1): любое turn/completed своей нити закрывает ход, как в gateway 1.4.36, а terminal для stop/replace выходит только при точном id хода — иной id или ранний без пары (буфер ≤8) дают unknown → HOLD; наружу terminal выходит только после postprocessing старого HTTP request, непосредственно перед done. Все зарегистрированные SDK попытки должны иметь положительный terminal; неизвестная попытка не закрывается успехом следующей. Поздний terminal-seen lease при ещё идущем native ходе нельзя захватить для нового stop: HOLD. Ready может обновить SDK tuple при штатной повторной попытке до control intent; активный control запрещает такую смену. accepted ACK и HTTP close этого не подтверждают. Native cancellation не выгружает reader до положительного SDK terminal. Общий предел ожидания ≤30000мс и ≤срока подписи. Неизвестный исход — durable HOLD без автоматического повтора; после рестарта незавершённое также HOLD. Требуется отдельная ручная сверка, данные не сбрасываются вводом модели.

pause использует тот же stopKeepingContext, что человеческий /stop: keepInbox, сохранение claimed контекста и пауза. Owner input во время ожидания terminal сохраняется и продолжает после завершения собственного pause; новый человеческий /stop и prior pause не снимаются replacement. replace разрешается только после обоих terminal, при неизменном поколении и собственной власти паузы. В ledger один CAS permit. Новый native input получает durable queue/flush и допуск единственным claim wrapper; завершение control требует actual user/message, claim нового turn, byte/body identity и повторный flush. Старый backlog сохраняется и не передаётся через native или SDK поверх replacement; прежние sdk_inputs unknown/heldIds/CAS и лимиты B-N-1530 не сбрасываются. Explicit stop запрещает старое pressure continuation и запуск вспомогательного physical compact после terminal.

## Кто читает ответ

Caller Забава/Искра получает подписанный status в указанном обратном ящике. Для криптографической сверки он вызывает node <пакет>/src/read-mail-stop-status.mjs <status-file> <control_id> <wire_sha> — последние два поля из вывода sender. DSH_MAIL_STOP_STATUS_TRUST (умолчание /etc/dsh-mail-stop/status-trust.json) указывает root-owned отдельный public trust-store: version=1, keys, receivers, recipient, ledger_uuid. С 2.5.79 (B-N-1590) receivers обязателен: на каждый key_id ровно одно имя агента, за которого этот ключ вправе подписывать статус (receivers и keys — один и тот же набор key_id); статус, подписанный ключом другого агента, — отказ key_receiver_mismatch даже в общем trust-store; trust-store без receivers — отказ status_trust_unavailable. Переход: в каждый status-trust-<агент>.json дописать "receivers":{"status-<агент>-sadik-1":"<агент>"} ДО установки нового reader. Ключи alfa/beta/gamma читает одна учётка coders — подписать статус друг за друга они могут и после этого: receivers разделяет ключи, а не учётки. Reader требует подпись, правильный получатель, срок и корреляцию ID/SHA; чужой или неподтверждённый статус не принимает. Это состояние доставки/управления, не обещание понимания моделью и не отзыв уже совершённых внешних действий.

С 2.5.73 (B-N-1596, B-N-1595). Незагруженный первичный a2a-разговор — простой, а не отсутствие цели: если разговор сохранён (есть поколение в channel_sessions), приёмник продолжает его через agentFor и ставит паузу/замену как на загруженном; новый разговор стоп не создаёт никогда. target_none несёт поле target_cause: holds_session (разговор удержан /new), resume_failed (продолжить не удалось), no_live_agent (разговора нет), session_created (другое поколение), subagent, cwd. Replace заканчивается подписанным статусом complete/replacement_admitted (поле admitted_turn); если процесс сменился раньше — статуса complete нет, control уходит в unknown, как и прежде. Копии владельцу (deliveryMode owner-all) исходов стопа и ответа на замену управляются одной настройкой mailStopOwnerCopies в JSON настроек Telegram: false — не копировать; решение берётся при создании квитанции стопа, уже поставленные копии не отзываются. Подписанный статус идёт отдельным маршрутом и настройкой не затрагивается.

С 2.5.74 (B-N-1596 r2). У простаивающего удалённого агента (claude-oauth) отсутствие удалённого запроса доказывается строкой mail_sdk_requests terminal для последнего хода, а если строки нет (ход шёл в прежнем процессе) — штатным turn/end этого хода с reason completed: адаптер завершает шаг только после done шлюза, шлюз шлёт done после конца SDK-цикла. Прерванный (aborted) или упавший последний ход не доказывает ничего: шлюз продолжает SDK-прогон после ухода клиента. Недоказанное отсутствие — отказ denied/remote_absence_unproved до fence и паузы; агент остаётся нетронутым.

С 2.5.91 (B-N-1608). Строка mail_sdk_requests, чей lease не просил SDK terminal (адаптер до телеметрии стоп-почты; с 2.5.91 reason=no_telemetry, у прежних — generation без единого следа телеметрии), доказывает не больше, чем её отсутствие: она не мешает, если тот же ход в журнале сессии закончен completed, и мешает, если нет. Строка с телеметрией без terminal — по-прежнему отказ.

С 2.5.75. Статус unknown несёт own_pause: true, если паузу разговора поставил этот же control. Пауза при unknown не снимается (исход удалённого хода неизвестен); после ручного снятия fence (mail-stop-hold snyat) её снимает mail-stop-hold snyat-pauzu <агент> <control_id> либо обычное сообщение владельца — только если чат владельца слит с этим разговором (mergeChatIntoA2A). У разговора a2a без слияния входа владельца нет.

Какие исходы оставляют паузу (2.5.75, только описание; поведение не менялось). Паузу control ставит до попытки остановки; после неё четыре исхода — unknown с own_pause: true — оставляют паузу вместе с fence: no_exact_sdk_lease, terminal_unconfirmed, replacement_permit_unavailable, replacement_admission_unknown. Порядок снятия: mail-stop-hold snyat (fence, после проверки хода и SDK), затем пауза — обычным сообщением владельца у агентов, чей чат владельца слит с a2a (mergeChatIntoA2A: iskra, zarya, avrora, spartak, marc, petrovich на 05.10), у кодеров alfa/beta/gamma — только mail-stop-hold snyat-pauzu. До 2.5.77 не ловился стоп, брошенный на beginDispatch→false: без подписанного статуса, строка admitted с прошедшим deadline держала fence до смены процесса. С 2.5.77 срок перепроверяется перед admit (истёкший — denied/expired до fence и паузы), а отказ beginDispatch переводит строку в unknown (dispatch_not_started) с подписанным статусом и own_pause — это пятый исход unknown после собственной паузы.

## Порядок монтажа после ворот

1. Сверить принятые пакеты с новой текущей головой; страховки и способы отката — в П7. Автор бой не монтирует.
2. Подготовить разрешённые signer/status public stores и isolated private key paths без вывода значений; снять настоящие recipient/ledger/cwd/ящики. Согласовать права ключей с назначенными руками двух машин.
3. Запустить отдельный arrival observer до поступления новых controls, сверить unit/config/root и положительный факт на безопасном локальном fixture. Старые лежащие письма не считать новым arrival.
4. Gateway → adapter → Telegram: смонтировать целые принятые пакеты, перезапустить назначенной рукой, сверить версии процесса/суммы/новые inode, затем объявить capability. Человеческий /stop не зависит от включения нового sender.
5. На aclaude Забава монтирует helper-пакет и обе обёртки, сохраняет существующие ссылки на iskra-tell. На sadik Искра получает local конфигурацию/helper своего разрешённого ключа. Сверить подписанные статусы через отдельный status reader. Живые stop-пробы — только отдельным поручением координатора.
