Get started
Подтверждения оператора в нескольких интерфейсах
Подтверждения оператора на нескольких поверхностях
Этот проект отслеживает #103505. Он заменяет локальные для процесса полномочия подтверждения единым жизненным циклом, принадлежащим Gateway и использующим SQLite. Каждое принадлежащее Gateway подтверждение выполнения или действия плагина/инструмента получает один стабильный идентификатор, один аутентифицированный маршрут Control UI, атомарное разрешение по принципу «первый ответ побеждает» и доступные только оператору проекции в потоки исходного и родительских сеансов.
Встроенные действия и прямые ссылки сосуществуют. Переключателя режима подтверждения нет.
Цели
- Один долговечный объект подтверждения для шлюзов выполнения и плагинов/инструментов.
- Стабильный маршрут
${controlUiBasePath}/approve/{approvalId}. - Разрешение с любой авторизованной поверхности Control UI, нативного приложения или канала.
- Атомарное поведение по принципу «первый ответ побеждает» для параллельно используемых поверхностей.
- Идемпотентные идентичные повторные запросы; конфликтующие поздние ответы не могут перезаписать победивший ответ.
- Тайм-аут, некорректные доверенные вердикты, отсутствующие маршруты, отмена и перезапуск приводят к безопасному отказу.
- События запроса и завершения поступают в исходный сеанс и всем соответствующим владельцам родительских сеансов/оркестраторов.
- Каналы получают типизированные действия подтверждения и навигации; данные обратного вызова транспорта остаются закрытыми для канала.
- Существующие методы Gateway для выполнения и плагинов сохраняют совместимость, а их реализация сводится к единому сервису.
Не входит в цели
- Сохранение или возобновление самого заблокированного выполнения инструмента после перезапуска Gateway.
- Превращение идентификатора или URL подтверждения в учётные данные на предъявителя.
- Добавление запросов подтверждения в видимые модели расшифровки или пробуждение родительских агентов.
- Перенос политики подтверждений, продуктовых команд или авторизации проверяющего в плагины каналов.
- Клонирование состояния подтверждения для каждого канала, устройства или родительского элемента.
- Переработка списков разрешённых команд выполнения, композиции политик плагинов или сохранения
allow-always, кроме случаев, необходимых для однозначности конечных результатов. - Предоставление удалённого доступа к встроенному TUI без Gateway на первом этапе. Он остаётся доступным только локально и должен безопасно отклонять запросы при отсутствии проверяющего.
Исходное состояние до развёртывания и карта доказательств
В этой таблице зафиксировано состояние реализации на момент открытия #103505. В разделах развёртывания ниже описываются создаваемые поверх этой основы долговечный реестр, типизированные действия, страница прямой ссылки и этапы реализации нативного клиента.
| Поверхность | Исходная точка входа и владелец | Исходное поведение и недостаток |
|---|---|---|
| Выполнение агентом | src/agents/bash-tools.exec-approval-request.ts, src/agents/bash-tools.exec-host-shared.ts |
Двухэтапная регистрация exec.approval.* предотвращает раннее состояние гонки /approve, но тайм-аут всё ещё может привести к разрешению через askFallback. |
| Шлюз инструмента плагина | src/agents/agent-tools.before-tool-call.ts |
Запрашивает plugin.approval.*; timeoutBehavior: "allow" может подтвердить шлюз после истечения его тайм-аута. Во встроенном режиме действуют отдельные локальные для процесса полномочия в src/infra/embedded-plugin-approval-broker.ts. |
| Шлюз узла плагина | src/gateway/node-invoke-plugin-policy.ts |
Создаёт и рассылает данные непосредственно через диспетчер плагинов, дублируя часть жизненного цикла серверного метода. |
| Полномочия Gateway | src/gateway/server-aux-handlers.ts, src/gateway/exec-approval-manager.ts, src/gateway/server-methods/approval-shared.ts |
Отдельные диспетчеры выполнения и плагинов используют локальные для процесса отображения. Конечные записи сохраняются 15 секунд. Принцип «первый ответ побеждает» действует только в рамках одного процесса. |
| Протокол Gateway | packages/gateway-protocol/src/schema/exec-approvals.ts, packages/gateway-protocol/src/schema/plugin-approvals.ts, src/gateway/methods/core-descriptors.ts |
Для выполнения предусмотрен только ожидающий get; для плагина нет get; отсутствует не зависящий от вида поиск конечного результата для прямой ссылки. |
| Доставка | src/infra/exec-approval-channel-runtime.ts, src/infra/approval-native-runtime.ts, src/infra/approval-handler-runtime.ts |
Поддерживает маршрутизацию к источнику, личные сообщения подтверждающим лицам, повторное воспроизведение ожидающих запросов, нативные обработчики и очистку конечных результатов внутри процесса. В отдельном последующем изменении добавляется долговечная сверка конечных результатов. |
| Переносимые действия | src/interactive/payload.ts, src/plugin-sdk/interactive-runtime.ts, src/plugin-sdk/approval-reply-runtime.ts |
Кнопки подтверждения являются командными действиями, содержащими /approve ...; цели URL и веб-приложений представлены нетипизированными полями кнопок. |
| Telegram | extensions/telegram/src/approval-handler.runtime.ts, extensions/telegram/src/button-types.ts |
Средство визуализации анализирует текст команды, чтобы распознать семантику подтверждения перед формированием закрытых данных обратного вызова. |
| Control UI | ui/src/app/exec-approval.ts, ui/src/app/overlays.ts, ui/src/components/exec-approval.ts |
Интерфейс подтверждения представляет собой глобальное модальное окно. ui/src/app-route-paths.ts и ui/src/app-routes.ts используют точные маршруты и перенаправляют неизвестные пути в чат. |
| Владение сеансом | src/agents/subagent-registry.types.ts, src/agents/subagent-registry-read.ts, src/config/sessions/types.ts |
Существуют контроллер, инициатор запроса, явно заданный родитель и владение устаревшим порождённым процессом, но события подтверждения не проецируются в потоки этих сеансов. |
| Общее состояние | src/state/openclaw-state-schema.sql, src/state/openclaw-state-db.ts |
Существующие немедленные транзакции и условные обновления Kysely поддерживают долговечную операцию сравнения с обменом в state/openclaw.sqlite. |
Репрезентативные текущие тесты включают src/gateway/exec-approval-manager.test.ts, src/gateway/server-methods/approval-shared.test.ts, src/agents/bash-tools.exec-gateway-approval.e2e.test.ts, extensions/telegram/src/approval-handler.runtime.test.ts и ui/src/e2e/approval-flow.e2e.test.ts.
SDK плагинов остаётся единственной границей каналов и плагинов. Изменения среды выполнения и представления подтверждений должны экспортироваться через существующие подпути src/plugin-sdk/approval-*.ts и src/plugin-sdk/interactive-runtime.ts; рабочий код плагинов не должен импортировать внутренние компоненты Gateway.
Предшествующие решения
Omnigent предлагает полезные подходы к пользовательскому интерфейсу и семантике сбоев:
approval.pyприостанавливает ASK, применяет тайм-ауты для каждой политики и считает подтверждением только точное принятие.sessions.pyсодержит серверный шлюз нативной оснастки и проекцию запросов и разрешений в родительские элементы.ApprovePage.tsxпредоставляет автономную мобильную страницу подтверждения.
Не следует некритично копировать заявленный подход к хранению. Текущее активное ожидающее состояние является локальным для процесса в _elicitation_registry.py, а неиспользуемая таблица ожидающих запросов удаляется посредством e3b1f2a4c9d7_drop_pending_tool_calls_table.py. OpenClaw намеренно идёт дальше: SQLite является авторитетным источником, а каждый конечный переход выполняется как операция сравнения с обменом в базе данных.
Архитектура и владение
Gateway владеет жизненным циклом:
- Агент, перехватчик плагина или политика узла предоставляет зависящий от вида запрос и локальную для процесса привязку выполнения.
- Gateway проверяет его и создаёт очищенную проекцию для проверяющего.
- Сервис подтверждений вычисляет аудиторию источника и владельцев, вставляет каноническую строку, а затем регистрирует внутрипроцессный объект ожидания.
- После долговечной вставки Gateway публикует существующие события подтверждения, проекции сеансов, уведомления каналов и нативные push-уведомления.
- Каждая поверхность выполняет разрешение через один и тот же сервис.
- Сервис фиксирует один конечный переход, пробуждает объект ожидания среды выполнения и публикует конечные проекции.
- Сбой доставки события никогда не откатывает зафиксированное решение; клиенты восстанавливают состояние через
approval.getили повторное воспроизведение списка.
Границы владения:
src/gateway/: сервис подтверждений, авторизация, адаптеры RPC, построение URL, жизненный цикл объектов ожидания и публикация событий.src/state/: общая схема и сгенерированные типы Kysely.src/infra/: очищенные модели представления подтверждений и построение переносимого представления.src/agents/: запрашивает, ожидает и применяет возвращённый вердикт; без сохранения состояния.src/channels/иextensions/*: визуализируют типизированные действия, авторизуют пользователей каналов, кодируют закрытые обратные вызовы и обновляют доставленные элементы управления.src/plugin-sdk/: только публичные контракты подтверждений и представления.ui/: автономная страница и существующие клиенты очереди/модального окна.
Внутрипроцессный объект ожидания является механизмом уведомления, а не источником полномочий. Регистрация вставляет строку и синхронно устанавливает объект ожидания до публикации запроса, поэтому обработчик разрешения не может вклиниться между этими этапами. Каждый последующий обработчик разрешения сначала фиксирует результат через SQLite и только затем завершает этот объект ожидания.
Постоянная запись
Добавьте одну таблицу operator_approvals в общую базу данных состояния.
| Столбец | Назначение |
|---|---|
approval_id |
Глобально уникальный канонический идентификатор. Сохраняйте существующие идентификаторы выполнения и идентификаторы plugin: для совместимости протокола, но никогда не определяйте тип по префиксу. |
resolution_ref |
Уникальный полный указатель SHA-256 в формате base64url для транспортных обратных вызовов, которые не могут передать канонический идентификатор. Он не является средством авторизации или идентификатором общедоступного URL. |
kind |
Закрытый дискриминатор exec | plugin. |
status |
Закрытое состояние pending | allowed | denied | expired | cancelled. |
presentation_json |
Проверенная проекция для рецензента с меткой типа. Необработанные запросы среды выполнения, привязки команд и полезные данные обратных вызовов остаются локальными для процесса. |
source_agent_id, source_session_key |
Идентификатор источника и опорная точка проекции сеанса. Ключ сеанса долговечен, а сменяемый UUID сеанса — нет. |
audience_session_keys_json |
Упорядоченный JSON-массив без дубликатов, формируемый ограниченным обходом владения в ширину. События запроса и терминальные события используют один и тот же снимок. |
requested_by_device_id, requested_by_client_id |
Долговечные метаданные инициатора запроса и аудита. Идентификатор соединения остаётся в памяти и не является субъектом, общим для разных поверхностей. |
reviewer_device_ids_json |
Необязательные явно указанные устройства рецензентов, предоставляемые только доверенной средой выполнения подтверждений. |
runtime_epoch |
Эпоха процесса, которой принадлежит приостановленное выполнение; используется для отмены осиротевших строк после перезапуска. |
created_at_ms, expires_at_ms, updated_at_ms |
Авторитетные временные данные. |
decision |
Явное решение пользователя, если оно существует. |
terminal_reason |
Закрытая причина, например user, timeout, malformed-verdict, no-route, run-aborted или gateway-restart. |
resolved_at_ms, resolver_kind, resolver_id |
Идентификаторы победителя и аудита, сохраняемые на стороне сервера. Проекции для рецензентов не содержат необработанные идентификаторы разрешившего субъекта. |
consumed_at_ms, consumed_by |
Отдельная защита от повторного воспроизведения для allow-once; потребление не должно удалять записанное решение. |
Обязательные индексы:
- уникальный
(resolution_ref); вставки также отклоняют неоднозначностьapproval_id/resolution_refмежду столбцами (status, expires_at_ms)(source_session_key, created_at_ms DESC)(resolved_at_ms)для очистки по сроку хранения
Массивы аудиторий малы и ограничены. При повторном воспроизведении с фильтрацией по сеансу сначала через Kysely выбираются видимые ожидающие строки, затем ограниченные массивы аудиторий декодируются и фильтруются в коде приложения; сопоставление строк или необработанные SQL-запросы к JSON не используются.
Храните терминальные строки в течение 30 дней в соответствии со сроком хранения метаданных аудита в src/audit/audit-event-store.ts. Очистка является фиксированной политикой обслуживания, а не новой поверхностью конфигурации. База данных представляет собой закрытое локальное состояние плоскости управления, однако API рецензентов никогда не должны раскрывать полный сохранённый запрос или привязку среды выполнения.
Конечный автомат и сравнение с заменой
Допустимы только следующие переходы:
pending -> allowed: явноеallow-onceилиallow-always.pending -> denied: явный отказ, доверенный некорректный терминальный вердикт или отсутствие маршрута доставки.pending -> expired: достигнут авторитетный крайний срок.pending -> cancelled: прерывание выполнения, штатное завершение работы или восстановление осиротевших записей после перезапуска.
Эффективным вердиктом любого неразрешённого терминального состояния является отказ.
Разрешение использует одну немедленную транзакцию SQLite и условное обновление Kysely, эквивалентное:
UPDATE operator_approvalsSET status = ?, decision = ?, terminal_reason = ?, resolved_at_ms = ?WHERE approval_id = ? AND status = 'pending' AND expires_at_ms > ?;Если обновление не затронуло ни одной строки, та же транзакция считывает запись:
- Отсутствует или нет авторизации: вернуть «не найдено»; не раскрывать существование.
- Всё ещё ожидает решения, но крайний срок достигнут: атомарно изменить состояние на
expired, затем вернуть эту терминальную строку. - То же записанное решение: вернуть идемпотентный успешный результат с записанным победителем.
- Другое решение: унифицированный API возвращает
applied: falseс записанным победителем; устаревшие адаптеры сохраняютAPPROVAL_ALREADY_RESOLVED, если это требуется их выпущенным контрактом. - Любое терминальное состояние: никогда не изменять его.
now == expires_at_ms истекло. Время Gateway является авторитетным.
Выполнение allow-once использует вторую операцию сравнения с заменой над consumed_at_ms IS NULL, привязанную к существующему точному контексту команды или системного запуска. После потребления строка подтверждения остаётся записью аудита.
Некорректные входные данные HTTP/RPC, которые невозможно аутентифицировать или по которым невозможно определить подтверждение, отклоняются без изменений и никогда не могут привести к подтверждению. Некорректный терминальный вердикт, полученный от доверенного стенда или ожидающего процесса для известного подтверждения, переводит его в denied.
API Gateway
Добавьте не зависящие от типа методы для рецензентов:
| Метод | Контракт |
|---|---|
approval.get { id } |
Возвращает видимую ожидающую или сохранённую терминальную проекцию. |
approval.resolve { id, kind, decision } |
Принимает канонический идентификатор или транспортную ссылку фиксированного размера, затем выполняет авторизацию, проверку типа и допустимого решения, согласование крайнего срока и терминальную операцию сравнения с заменой. Ответ всегда содержит канонический идентификатор. |
После успешной операции сравнения с заменой немедленно верните зафиксированную проекцию. Устаревшие события, перенаправители каналов и механизмы терминализации push-уведомлений выполняются по возможности; медленная или отказавшая поверхность не должна задерживать или откатывать победивший ответ.
Проверка запросов для конкретных типов остаётся в exec.approval.request и plugin.approval.request. Существующие exec.approval.get/list/waitDecision/resolve и plugin.approval.list/waitDecision/resolve становятся адаптерами на границе протокола для канонического сервиса, поскольку они входят в выпущенный API Gateway. Внутренние вызывающие стороны переводятся на сервис в рамках того же изменения.
Проекция для рецензента представляет собой размеченное объединение:
type OperatorApproval = { id: string; status: OperatorApprovalStatus; presentation: | { kind: "exec"; commandText: string /* безопасный предварительный просмотр выполнения */ } | { kind: "plugin"; title: string; description: string /* безопасный предварительный просмотр плагина */ }; // общие поля жизненного цикла};Стабильный путь вычисляется, а не сохраняется. approval.get возвращает urlPath; поверхности, которым известен утверждённый общедоступный источник, также могут получить абсолютный url. Снимки для рецензентов не содержат ключи сеансов источника и аудитории. Gateway хранит эти ключи маршрутизации на стороне сервера для отдельной проекции session.approval.
События и переносимые действия
PR 1 сохраняет выпущенные имена событий, полезные данные и существующие фильтры получателей на уровне записей:
exec.approval.requestedexec.approval.resolvedplugin.approval.requestedplugin.approval.resolved
Эти устаревшие события могут содержать полный запрос среды выполнения, поэтому их нельзя рассылать всем клиентам, относящимся к подтверждению. PR 5 добавляет размеченные поля жизненного цикла (status, sourceSessionKey, urlPath, терминальные метаданные и kind уровня представления) через очищенную проекцию жизненного цикла вместо расширения доставки устаревших событий.
Добавьте событие проекции session.approval, ограниченное областью подтверждения. Опубликуйте каноническое событие один раз с сохранёнными ключами аудитории; подписчики точного сеанса получают одно и то же событие для каждого совпадающего ключа:
sessionKey: поток, получающий проекцию.sourceSessionKey: дочерний элемент или источник, инициировавший блокировку.phase:pending \| terminal, дискриминированное по состоянию подтверждения.- одна безопасная проекция
OperatorApproval.
Клиенты явно подключают эту возможность с помощью sessions.messages.subscribe { key, agentId?, includeApprovals: true }. Успешный ответ добавляет approvalReplay, содержащий до 1 000 текущих ожидающих подтверждений для этого точного ключа потока, которые подписывающийся клиент также имеет право проверять на уровне записи. truncated: false делает отфильтрованное повторное воспроизведение авторитетным, и повторно подключающиеся клиенты заменяют им свой локальный набор ожидающих подтверждений; truncated: true является сигналом перегрузки, и клиенты должны сохранять невидимые локальные записи, пока канонический поиск или последующие события жизненного цикла не определят их состояние. Обнаруженный при повторном воспроизведении более поздний долговечный тайм-аут перед возвратом нового снимка отправляет терминальные надгробные записи только подписанным аудиториям, авторизованным на уровне записи. operator.admin может подключаться напрямую; более узким клиентам требуются как связанный идентификатор устройства, так и operator.approvals. Одна лишь подписка на сеанс никогда не предоставляет видимость подтверждений.
Зарегистрируйте событие в src/gateway/server-broadcast.ts под operator.approvals. Проекция носит наблюдательный характер: она никогда не добавляет строки расшифровки, не отправляет sessions.changed и не пробуждает агента.
Расширьте MessagePresentationAction в src/interactive/payload.ts:
type MessagePresentationAction = | { type: "command"; command: string } | { type: "callback"; value: string } | { type: "approval"; approvalId: string; approvalKind: "exec" | "plugin"; decision: ExecApprovalDecision; } | { type: "url"; url: string } | { type: "web-app"; url: string };Ядро создаёт типизированные действия принятия решения и отдельную ссылку «Проверить», когда доступен утверждённый абсолютный источник Control UI. Каналы кодируют действие подтверждения в собственный формат обратного вызова и отправляют решение каноническому сервису. Обратный вызов использует точный канонический идентификатор, если он помещается; в противном случае используется уникальный указатель полного дайджеста строки resolution_ref. Ссылка является только компактным ключом поиска: по-прежнему применяются обычная аутентификация Gateway, авторизация на уровне записи, явный тип, проверка допустимого решения, согласование крайнего срока и операция сравнения с заменой по принципу первого ответа. Каналы не должны усекать идентификаторы, разрешать префиксы хешей, разбирать текст /approve или определять тип по префиксу идентификатора.
Сохраните button.url, button.webApp и элементы управления подтверждениями на основе команд как устаревшие входные данные для совместимости SDK плагинов. Нормализуйте их на границе SDK; перенесите все встроенные внутренние вызывающие стороны в том же PR. /approve {id} {decision} остаётся текстовым резервным вариантом и командой CLI/чата, а не семантическим контрактом кнопки.
Control UI
Маршрут — ${basePath}/approve/{approvalId}. ID является единственным параметром пути; идентификатор исходного сеанса берётся из записи.
Поскольку текущий маршрутизатор использует точные статические маршруты и перенаправляет неизвестные пути в Chat, распознавайте эту глубокую ссылку в ui/src/app/bootstrap.ts до обычной нормализации маршрута. Повторно используйте стандартную настройку Gateway и аутентификации, но отображайте отдельную страницу подтверждения вне оболочки с боковой панелью и глобального модального окна.
Документ принадлежит Gateway, который обслужил его URL. При первоначальном подключении игнорируется сохранённый выбор удалённого Gateway для полного приложения без изменения или копирования настроек этого выбора; только аутентификация остаётся привязанной к сеансу обслуживающего Gateway. Доверенная нативная аутентификация или отдельно подтверждённое переопределение gatewayUrl может перенаправить его. Ядро резервирует одно-сегментное пространство имён /approve перед HTTP-маршрутами плагинов и обнаружением статических расширений, включая ID, оканчивающиеся на .json или .js; когда обслуживание Control UI отключено, зарезервированный маршрут безопасно завершается ошибкой 404. Оставьте страницу в основном пакете Control UI, чтобы сбой отложенно загружаемого фрагмента не оставил решение по безопасности зависшим на индикаторе загрузки.
Состояния страницы:
- загрузка
- требуется аутентификация
- ожидание
- выполняется решение
- подтверждено или отклонено здесь
- решено в другом месте
- срок действия истёк
- отменено
- доступ запрещён/не найдено
- ошибка подключения с возможностью повтора
Страница вызывает RPC Gateway, а не второй неаутентифицированный REST API. При обновлении страницы в браузере долговременное состояние считывается заново. Учётные данные Gateway никогда не помещаются в URL, строку запроса или фрагмент.
Авторизация и конфиденциальность
URL — это указатель, а не полномочие. Для принятия решения требуются:
- аутентифицированное подключение к Gateway;
operator.approvalsилиoperator.admin;- авторизация проверяющего на уровне записи.
Правила на уровне записи:
operator.adminможет выполнять проверку.reviewer_device_idsявляется определяющим при наличии. Проверку может выполнять только указанное сопряжённое устройствоoperator.approvals; запрашивающее устройство не получает неявного доступа, если оно также не указано.- При отсутствии явного списка проверяющих запрашивающее сопряжённое
устройство
operator.approvalsможет проверять собственную запись. - Действительно устаревшие записи без привязки запрашивающей стороны или проверяющего сохраняют широкую видимость для сопряжённых устройств, чтобы обновления не оставляли уже ожидающую работу без возможности завершения.
- Внутренние среды выполнения без устройства могут принимать решение, но не читать данные, через ограниченное
подключение среды выполнения подтверждений. Эти полномочия предоставляются только
аутентифицированным сервером токеном среды выполнения; общедоступные поля
approval.resolveне могут создать его. - Принадлежность активного подключения запрашивающей стороне остаётся допустимой для устаревших адаптеров; она никогда не выводится из совпадения имени клиента.
- Членство в аудитории изменяет только представление. Оно никогда не расширяет авторизацию.
approval.get предоставляет только очищенную проекцию для проверяющего и не включает внутренние ключи маршрутизации источника и аудитории. Событие session.approval из PR 5 содержит единственное назначение sessionKey и sourceSessionKey после того, как Gateway применит сохранённый снимок аудитории на стороне сервера. Существующие события выполнения и плагинов сохраняют прежнюю полезную нагрузку и ограниченный круг получателей до миграции потребителей. Исполняемый запрос, привязка команды и продолжение остаются только в локальном для процесса объекте ожидания. Долговременная строка содержит безопасное представление, а также метаданные жизненного цикла, маршрутизации и аудита; в ней никогда не хранятся необработанные значения среды, учётные данные, заголовки аутентификации или данные обратных вызовов канала.
Проекция аудитории
Вычислите аудиторию один раз перед вставкой и сохраните упорядоченный снимок. Владение представляет собой граф, а не всегда единственную родительскую цепочку: у дочернего элемента могут одновременно быть текущий контроллер и исходный запрашивающий субъект, и эти владельцы могут вести к разным корням.
Используйте детерминированный обход в ширину:
- Поместите ключ исходного сеанса в очередь.
- Для каждого извлечённого из очереди ключа прочитайте последнюю строку реестра субагентов и добавьте в очередь оба различающихся ребра владения в фиксированном порядке:
controllerSessionKey, затемrequesterSessionKey. - Если существует пригодная строка реестра, не переходите дополнительно по происхождению записи сеанса, которое может устареть после перенаправления. В противном случае добавьте в очередь единственное текущее резервное ребро
parentSessionKey ?? spawnedBy. - Нормализуйте и устраняйте дубликаты при добавлении в очередь, чтобы побеждал первый, кратчайший путь.
- Остановитесь после 64 уникальных ключей; это ограничение размера аудитории также ограничивает глубину обхода.
Источником реестра является src/agents/subagent-registry-read.ts; поля владения определены в src/agents/subagent-registry.types.ts. Резервные поля сеанса определены в src/config/sessions/types.ts.
Проекции запроса и терминального состояния используют одну и ту же сохранённую аудиторию, даже если владение фокусом или контроллером меняется во время ожидания подтверждения. Это гарантирует терминальную очистку для каждого потока сеанса аудитории, который получил проекцию запроса. Принятие решения всегда относится к ID исходного подтверждения; сеансы аудитории никогда не получают клонированное состояние подтверждения. Очистка пересланных сообщений канала остаётся отдельной последующей задачей по указателю доставки, описанной ниже.
Не записывайте сообщения в расшифровку, не внедряйте системные подсказки, не запускайте ходы владельца и не создавайте sessions.changed исключительно ради подтверждения.
Согласование доставленных поверхностей
Нативные обработчики подтверждений уже сохраняют записи доставленных сообщений достаточно долго, чтобы заменить или деактивировать активные элементы управления. Обычные пересланные сообщения подтверждения сейчас отбрасывают MessageReceipt, поэтому решение, принятое на другой поверхности, может оставить их старые элементы управления визуально в состоянии ожидания. Отдельная последующая доработка устраняет этот пробел с помощью дочерней таблицы operator_approval_deliveries в общей базе данных состояния.
Каждая строка хранит ID подтверждения, уникальный ID доставки, канал/учётную запись/точный маршрут, ограниченный прошедший проверку JSON приватный для канала указатель сообщения, временные метки доставки и состояние завершения. В ней никогда не хранятся данные обратного вызова, токены решения или необработанные запросы подтверждения. Канал отвечает за кодирование указателя и изменение сообщения; ядро отвечает за канонический статус, выбор цели, политику повторных попыток и резервный текст завершения.
Регистрация доставки и терминальное разрешение безопасно обрабатывают состояние гонки:
- После того как отправка ожидающего сообщения вернёт квитанцию, вставьте указатель доставки и прочитайте статус родительского подтверждения в одной транзакции.
- Если родитель уже находится в терминальном состоянии, запланируйте немедленное завершение вместо сохранения поздней доставки в состоянии ожидания.
- Каждый зафиксированный терминальный переход отдельно планирует обработку всех незавершённых строк доставки; отбрасываемые широковещательные события не являются триггером.
- Терминализатор канала сообщает
replaced,retiredилиunsupported. Замена подавляет дублирующее терминальное сообщение; деактивация отправляет существующее завершающее сообщение; отсутствие поддержки или ошибка приводит к резервному поведению без отката CAS подтверждения. - При запуске повторно обрабатываются терминальные подтверждения с незавершёнными доставками, что делает очистку устойчивой к перезапуску Gateway.
Этот жизненный цикл транспорта является необязательным перехватчиком адаптера доставки, а не средством визуализации или действием сообщения, доступным модели. Сообщения QQ C2C/групп сейчас не имеют API редактирования, удаления или очистки клавиатуры; такой адаптер остаётся неподдерживаемым и может показать каноническое состояние только после последующего нажатия, пока транспорт не получит API изменения.
Семантика перезапуска, тайм-аута и маршрута
Сохранение в SQLite не подразумевает возобновления выполнения. Привязки команд и инструментов остаются в памяти, поскольку могут содержать чувствительные с точки зрения безопасности сведения среды выполнения и не являются контрактом возобновляемой задачи.
При запуске Gateway:
- создайте новую эпоху среды выполнения;
- атомарно переведите ожидающие строки из более старых эпох в
cancelledс причинойgateway-restart; - сохраните строки, чтобы по их URL можно было понять, что произошло;
- никогда не выполняйте позднее подтверждение при отсутствии привязки среды выполнения.
Таймеры служат оптимизацией пробуждения. Определяющий срок хранится в expires_at_ms; операции чтения, ожидания и принятия решения всегда выполняют сверку истечения срока.
Итоговое строгое поведение:
- тайм-аут ->
expired, отклонить; - маршрут отсутствует ->
denied, отклонить; - прерывание выполнения ->
cancelled, отклонить; - некорректный доверенный вердикт ->
denied, отклонить; - только допустимое явное разрешающее решение ->
allowed.
Текущее выпущенное поведение выполнения всё ещё противоречит этому контракту:
src/agents/bash-tools.exec-host-shared.tsможет применитьaskFallback.docs/tools/exec-approvals.mdиdocs/cli/approvals.mdдокументируют эту поверхность.
Подтверждения плагинов теперь безопасно завершаются отказом при тайм-ауте и некорректных вердиктах; устаревшее
поле timeoutBehavior по-прежнему принимается, но игнорируется. Последующая доработка
строгой семантики выполнения должна одновременно обновить код, типы, документацию, тесты и журнал изменений,
с явной проверкой владельца и специалистов по безопасности. askFallback может продолжать описывать
выбор политики до шлюза во время миграции, но не должно превращать тайм-аут созданной
ожидающей записи в подтверждение.
План совместимости
- Аддитивный протокол Gateway; без повышения версии протокола.
- Сохранить существующие методы и события выполнения/плагинов на внешней границе.
- Сохранить существующие ID, включая префиксы
plugin:, но перестать использовать префиксы как информацию о типе. - Сохранить поведение текстовой команды
/approve. - Сохранить устаревшие поля URL/Web App кнопок и действия команд как входные данные совместимости SDK плагинов; новый вывод ядра является типизированным.
- Мигрировать все встроенные каналы и внутренние вызывающие стороны в рамках того же изменения типизированных действий.
- Добавить запись в журнал изменений для нового URL/страницы и последующего изменения поведения тайм-аута.
- Не добавлять настройку режима запроса данных.
Развёртывание
PR 1: долговременный жизненный цикл
- Эта проектная записка.
- Общая схема SQLite, генерация Kysely, хранилище и очистка через 30 дней.
- Сервис подтверждений Gateway, мост к объекту ожидания среды выполнения и обработка осиротевших записей после перезапуска.
- Унифицированный
approval.get/resolve. - Адаптеры методов выполнения/плагинов.
- Тесты принципа первого ответа, идемпотентности, истечения срока, авторизации и потребления.
- Пока без изменений поведения UI или каналов.
PR 2: типизированные действия и обратные вызовы каналов
- Типизированные действия подтверждения, URL и Web App.
- Построители представления ядра и экспорты SDK плагинов.
- Приватное для транспорта кодирование обратных вызовов с явным типом владельца.
- Долговременные ссылки фиксированного размера для канонических ID, превышающих ограничения транспорта.
- Миграция встроенных каналов с отказом от вывода по тексту команды и ID подтверждения.
- Каноническая истина первого ответа на поверхности, где выполнено нажатие, и терминальные обновления активных нативных элементов по мере возможности; долговременное завершение сообщений каналов остаётся последующей задачей.
- Тесты SDK и встроенных каналов.
PR 3: глубокая ссылка Control UI
- Отдельная аутентифицированная страница подтверждения и начальная маршрутизация с учётом базового пути.
- Привязка к обслуживающему Gateway без изменения сохранённого оператором выбора удалённого Gateway.
- Принадлежащее ядру HTTP-пространство имён подтверждений, включая ID, похожие на ресурсы.
- Полезная нагрузка URL, сформированная Gateway, и опрос состояния ожидания до появления событий жизненного цикла.
- Проверка мобильной ширины, переподключения, конкурирующих ответов, перезагрузки и смонтированного пути.
PR 4: нативные клиенты
- В интерфейсах проверки iOS и Android используется учитывающий вид
approval.get/resolve; watchOS передает безопасные для проверяющего запросы и решения через сопряженный iPhone. - Watch поддерживает решения по выполнению, предусмотренные его компактным контрактом ретрансляции: однократное разрешение и отклонение.
- Канонический терминальный результат первого ответа заменяет локальное состояние попытки принятия решения.
- Потерянные или неоднозначные подтверждения разрешения блокируют элементы управления до канонического обратного чтения.
- Ранее выпущенные экземпляры Gateway v4 сохраняют проверку выполнения через узкий резервный путь устаревшего метода; для сохранения терминального состояния между интерфейсами требуются унифицированные методы.
- Предупреждения для проверяющего и контекст владельца остаются видимыми на iPhone, Watch и Android.
- Нативные модульные тесты, сборка и подтверждение на платформах.
PR 5: распространение жизненного цикла на предков
- Доставка ожидающего/терминального состояния
session.approvalиз снимка аудитории, сохраненного в PR 1. - Подписка на точный сеанс, повторное воспроизведение при переподключении и терминальные надгробия без изменения расшифровки или пробуждения агента.
- Обратные вызовы жизненного цикла выполняются после надежной вставки/CAS и никогда не становятся источником полномочий для одобрения.
- Подтверждение для вложенных субагентов и переподключения.
PR 6: поведение с отказом по умолчанию
- Перевести
node-invoke-plugin-policy.tsи встроенный брокер плагинов с дублирующего источника полномочий. - Строгая семантика тайм-аута, некорректных данных, отсутствия маршрута, привязки и использования однократного разрешения.
- Объявить устаревшими выпущенные разрешительные настройки тайм-аута и не учитывать их после перехода запроса в состояние ожидания.
- Подтверждение конкуренции нескольких интерфейсов и внедрения сбоев.
Последующая задача: надежная очистка удаленных сообщений
- Сохранять локаторы перенаправленной доставки и переводить каждое доставленное сообщение канала в терминальное состояние после перезапуска.
- Отделить этот жизненный цикл транспорта от канонического источника полномочий для одобрения и типизированных действий представления.
Тесты
Обязательное целевое покрытие:
- Повторное открытие SQLite сохраняет ожидающие и терминальные проекции.
- Два параллельных обработчика разрешения дают ровно одного победителя CAS.
- Повтор с тем же решением идемпотентно завершается успешно; повтор с конфликтующим решением возвращает зарегистрированного победителя.
- Разрешение в момент крайнего срока или после него не может дать одобрение.
allow-onceможет быть использован ровно один раз без удаления терминального состояния аудита.- При запуске отменяются более старые эпохи среды выполнения.
- Неавторизованные поиск и разрешение не раскрывают существование записи.
- Поведение явного списка разрешенных проверяющих и общего сопряженного
operator.approvals. - Устаревшие методы выполнения и плагинов используют одно хранилище.
- Схемы запроса/списка/получения/разрешения Gateway и аддитивные полезные нагрузки событий.
- Нормализация типизированных действий, резервный рендеринг, экспорт SDK и переключатели встроенных каналов.
- Кодирование обратного вызова Telegram содержит закрытые данные транспорта и не использует вывод на основе строк команд.
- Прямой дочерний элемент, разветвленные владельцы контроллера/запрашивающей стороны, вложенные владельцы, переназначение, резервное использование поля сеанса, цикл и ограничение размера аудитории.
- Массивы запрошенной и терминальной аудитории идентичны.
- Проекции владельцев не изменяют расшифровку и не пробуждают агента.
- Маршрут Control UI работает по адресу
/и с настроенным базовым путем; после обновления отображается каноническое ожидающее или терминальное состояние. - При одновременных ответах через Control UI и Telegram отображается один победитель и «разрешено в другом месте» у проигравшего.
- Нативные идентификаторы одобрения и идентификаторы владельцев Gateway сохраняют точные байты UTF-8 при маршрутизации и согласовании.
- Согласование нативного семейства RPC закрепляет одно каноническое или устаревшее семейство за каждым допущенным маршрутом Gateway и никогда не выполняет неявный переход на более старую версию после использования.
- Потерянные подтверждения нативного разрешения блокируют действия до канонического обратного чтения; неудачное обратное чтение не может сфабриковать победителя или подтвердить обновление Watch.
- Корреляция запроса снимка Watch принимается только для точного владельца сопряженного Gateway и завершенного канонического обратного чтения iPhone.
- Подтверждение пользовательского пути через Testbox/Crabbox, включая страницу одобрения мобильной ширины, очистку действий Telegram и один цикл ожидание/разрешение/запоздалый проигравший на Android, iPhone и Watch.
Наблюдаемость
Выводите структурированные журналы переходов без содержимого с идентификатором одобрения, видом, ключом исходного сеанса, состоянием, причиной и задержкой. Никогда не журналируйте предварительный просмотр или необработанную привязку.
Отслеживайте:
- количество запросов по видам;
- количество терминальных состояний по виду/состоянию/причине;
- показатель ожидающих запросов;
- задержку от запроса до терминального состояния;
- результаты гонки разрешений: победитель, идемпотентный повтор, конфликт, истечение срока;
- количество маршрутов доставки и отклонений из-за отсутствия маршрута;
- отмены осиротевших запросов при запуске;
- размер аудитории.
Зафиксированный переход считается успешным, даже если последующая доставка события завершается сбоем. Подписчики жизненного цикла восстанавливаются с помощью повторного воспроизведения из PR 5 и канонического поиска. Надежный перевод сообщений каналов в терминальное состояние остается отдельной последующей задачей, описанной выше.
Открытые вопросы
- Внешне доступный источник Control UI. Каждый снимок содержит стабильный относительный
urlPath. Абсолютный URL можно объявлять только из кэшированного расположения Tailscale Serve/Funnel после успешного предоставления доступа к Gateway;allowedOrigins, заголовки Host запросов,gateway.remote.urlи предназначенные только для отображения кандидаты loopback/LAN не являются каноническими источниками. Telegram может использовать свою аутентифицированную оболочку Mini App, чтобы сохранить путь одобрения через начальную загрузку. Произвольные обратные прокси остаются только относительными до появления отдельно проверенного явного контракта публичного URL. Никогда не позволяйте каналу угадывать источник. - Переход на строгий тайм-аут выполнения для совместимости. Тайм-ауты одобрения плагинов теперь приводят к отказу по умолчанию, а
timeoutBehaviorобъявлен устаревшим. Оставшийся выпущенный контрактaskFallbackтребует явной проверки владельцем и службой безопасности, записи в журнале изменений, документации и решения о миграции/выводе из эксплуатации, прежде чем он перестанет разрешать выполнение после истечения тайм-аута ожидающего запроса. - Встроенный режим без Gateway. Рекомендация: сначала оставить его только локальным, а затем сделать клиентом канонической службы при наличии Gateway. Не объявляйте глубокую ссылку, которую не может разрешить ни один сервер.