Get started

Рефакторинг жизненного цикла ACP

Жизненный цикл ACP сейчас работает, но слишком многое в нём определяется постфактум. При очистке процессов принадлежность восстанавливается по PID, строкам команд, путям обёрток и текущей таблице процессов. При определении видимости сеансов принадлежность восстанавливается по строкам ключей сеансов и дополнительным обращениям к sessions.list({ spawnedBy }). Это позволяет вносить точечные исправления, но также упрощает упущение пограничных случаев: повторное использование PID, команды в кавычках, процессы-внуки адаптера, корни состояния нескольких Gateway, cancel по сравнению с close, а также видимость tree по сравнению с all — всё это становится отдельными местами, где приходится заново определять одни и те же правила принадлежности.

Этот рефакторинг делает принадлежность явной частью модели. Цель — не новая пользовательская поверхность ACP, а более безопасный внутренний контракт для существующего поведения ACP и ACPX.

Цели

  • Очистка никогда не отправляет сигнал процессу, если текущие фактические данные не соответствуют аренде, принадлежащей OpenClaw.
  • cancel, close и очистка при запуске имеют разные цели жизненного цикла.
  • sessions_list, sessions_history, sessions_send и проверки состояния используют одну и ту же модель сеансов, принадлежащих запрашивающей стороне.
  • Установки с несколькими Gateway не могут завершать обёртки ACPX друг друга.
  • Старые записи сеансов ACPX продолжают работать во время миграции.
  • Среда выполнения остаётся во владении плагина; ядро не получает сведений о пакете ACPX.

Не входит в цели

  • Замена ACPX или изменение публичной поверхности команды /acp.
  • Перенос специфичного для поставщика поведения адаптера ACP в ядро.
  • Требование к пользователям вручную очищать состояние перед обновлением.
  • Закрытие повторно используемых сеансов ACP через cancel.

Целевая модель

Идентификатор экземпляра Gateway

Каждый процесс Gateway должен иметь стабильный идентификатор экземпляра среды выполнения:

ts
type GatewayInstanceId = string;

Он может создаваться при запуске Gateway и сохраняться в состоянии на весь срок существования этой установки. Это не секрет безопасности, а признак принадлежности, используемый для предотвращения путаницы между процессами ACP одного Gateway и процессами другого Gateway.

Принадлежность сеанса ACP

Каждый запущенный сеанс ACP должен иметь нормализованные метаданные принадлежности:

ts
type AcpSessionOwner = {  sessionKey: string;  spawnedBy?: string;  parentSessionKey?: string;  ownerSessionKey: string;  agentId: string;  backend: "acpx";  gatewayInstanceId: GatewayInstanceId;  createdAt: number;};

Gateway должен возвращать эти поля в строках сеансов, для которых они известны. Фильтрация видимости должна быть чистой проверкой метаданных строки:

ts
canSeeSessionRow({  row,  requesterSessionKey,  visibility,  a2aPolicy,});

Это устраняет скрытые дополнительные вызовы sessions.list({ spawnedBy }) из проверок видимости. Запущенный межагентный дочерний сеанс ACP принадлежит запрашивающей стороне, поскольку это указано в строке, а не потому, что второй запрос случайно его обнаружил.

Аренды процессов ACPX

Каждый запуск созданной обёртки должен создавать запись аренды:

ts
type AcpxProcessLease = {  leaseId: string;  gatewayInstanceId: GatewayInstanceId;  sessionKey: string;  wrapperRoot: string;  wrapperPath: string;  rootPid: number;  processGroupId?: number;  commandHash: string;  startedAt: number;  state: "open" | "closing" | "closed" | "lost";};

Процесс обёртки должен получать идентификатор аренды и идентификатор экземпляра Gateway через переменные окружения:

sh
OPENCLAW_ACPX_LEASE_ID=...OPENCLAW_GATEWAY_INSTANCE_ID=...

Когда платформа это позволяет, при проверке следует отдавать предпочтение текущим метаданным процесса, которые невозможно перепутать из-за кавычек в команде:

  • корневой PID всё ещё существует
  • текущий путь обёртки находится внутри wrapperRoot
  • группа процессов соответствует аренде, если она доступна
  • окружение содержит ожидаемый идентификатор аренды, если его можно прочитать
  • хеш команды или путь к исполняемому файлу соответствует аренде

Если текущий процесс невозможно проверить, очистка должна завершаться безопасным отказом.

Контроллер жизненного цикла

Следует ввести единый контроллер жизненного цикла ACPX, которому принадлежат аренды процессов и политика очистки:

ts
interface AcpxLifecycleController {  ensureSession(input: AcpRuntimeEnsureInput): Promise&lt;AcpRuntimeHandle&gt;;  cancelTurn(handle: AcpRuntimeHandle): Promise<void>;  closeSession(input: {    handle: AcpRuntimeHandle;    discardPersistentState?: boolean;    reason?: string;  }): Promise<void>;  reapStartupOrphans(): Promise<void>;  verifyOwnedTree(lease: AcpxProcessLease): Promise&lt;OwnedProcessTree | null&gt;;}

cancelTurn запрашивает только отмену текущего хода. Он не должен завершать повторно используемые процессы обёртки или адаптера.

closeSession может выполнять завершение, но только после загрузки записи сеанса, загрузки аренды и проверки того, что текущее дерево процессов по-прежнему принадлежит этой аренде.

reapStartupOrphans начинает работу с открытых аренд в состоянии. Для поиска потомков можно использовать таблицу процессов, но не следует сначала сканировать произвольные команды, похожие на ACP, а затем решать, что они, вероятно, принадлежат нам.

Контракт обёртки

Создаваемые обёртки должны оставаться небольшими. Они должны:

  • запускать адаптер в группе процессов, если это поддерживается
  • перенаправлять обычные сигналы завершения группе процессов
  • обнаруживать завершение родительского процесса
  • при завершении родительского процесса отправлять SIGTERM, а затем сохранять процесс обёртки до выполнения резервного завершения через SIGKILL
  • передавать корневой PID и идентификатор группы процессов обратно контроллеру жизненного цикла, если они доступны

Обёртки не должны определять политику сеансов. Они лишь обеспечивают локальную очистку дерева процессов собственной группы адаптера.

Контракт видимости сеансов

Для определения видимости следует использовать нормализованные данные о принадлежности строки:

ts
type SessionVisibilityInput = {  requesterSessionKey: string;  row: {    key: string;    agentId: string;    ownerSessionKey?: string;    spawnedBy?: string;    parentSessionKey?: string;  };  visibility: "self" | "tree" | "agent" | "all";  a2aPolicy: AgentToAgentPolicy;};

Правила:

  • self: только сеанс запрашивающей стороны.
  • tree: сеанс запрашивающей стороны и строки, принадлежащие ей или порождённые из её сеанса.
  • all: все строки того же агента, разрешённые a2a межагентные строки и принадлежащие запрашивающей стороне порождённые межагентные строки, даже если общий доступ a2a отключён.
  • agent: только тот же агент, если явная связь принадлежности не указывает, что строка принадлежит запрашивающей стороне.

Это делает tree и all монотонными: all не должен скрывать принадлежащий запрашивающей стороне дочерний сеанс, который отображается при tree.

План миграции

Этап 1: добавление идентификатора и аренд

  • Добавить gatewayInstanceId в состояние Gateway.
  • Добавить хранилище аренд ACPX в каталог состояния ACPX.
  • Записывать аренду до запуска созданной обёртки.
  • Сохранять leaseId в новых записях сеансов ACPX.
  • Сохранить существующие поля PID и команды для старых записей.

Этап 2: очистка на основе аренд

  • Изменить очистку при закрытии, чтобы сначала загружался leaseId.
  • Проверять принадлежность текущего процесса по аренде перед отправкой сигнала.
  • Сохранить текущие резервные проверки корневого PID и корня обёртки только для устаревших записей.
  • Помечать аренды как closed после подтверждённой очистки.
  • Помечать аренды как lost, если процесс исчез до очистки.

Этап 3: очистка при запуске на основе аренд

  • Очистка при запуске сканирует открытые аренды.
  • Для каждой аренды проверяется корневой процесс и собираются его потомки.
  • Подтверждённые деревья завершаются начиная с дочерних процессов.
  • Старые аренды closed и lost удаляются по истечении ограниченного срока хранения.
  • Сканирование по маркерам команд временно сохраняется только как резервный механизм для устаревших записей, по возможности ограниченный корнем обёртки и экземпляром Gateway.

Этап 4: строки принадлежности сеансов

  • Добавить метаданные принадлежности в строки сеансов Gateway.
  • Научить средства записи ACPX, субагентов, фоновых задач и хранилища сеансов заполнять ownerSessionKey или spawnedBy.
  • Перевести проверки видимости сеансов на использование метаданных строк.
  • Удалить дополнительные обращения к sessions.list({ spawnedBy }) во время проверки видимости.

Этап 5: удаление устаревших эвристик

После одного цикла выпуска:

  • перестать использовать сохранённые строки корневых команд для очистки неустаревших записей ACPX
  • удалить сканирование при запуске по маркерам команд
  • удалить резервные обращения к спискам при проверке видимости
  • сохранить защитное поведение с безопасным отказом для отсутствующих или непроверяемых аренд

Тесты

Добавить два набора табличных тестов.

Симулятор жизненного цикла процессов:

  • PID повторно использован посторонним процессом
  • PID повторно использован корнем обёртки другого Gateway
  • сохранённая команда обёртки заключена в кавычки оболочки, а текущая команда ps — нет
  • дочерний процесс адаптера завершился, но процесс-внук остался в группе процессов
  • резервный механизм SIGTERM при завершении родительского процесса доходит до SIGKILL
  • список процессов недоступен
  • устаревшая аренда с отсутствующим процессом
  • осиротевший при запуске процесс с обёрткой, дочерним процессом адаптера и процессом-внуком

Матрица видимости сеансов:

  • self, tree, agent, all
  • a2a включён и отключён
  • строка того же агента
  • межагентная строка
  • принадлежащая запрашивающей стороне строка порождённого межагентного сеанса ACP
  • запрашивающая сторона в песочнице с ограничением до tree
  • действия получения списка, истории, отправки и состояния

Важный инвариант: принадлежащий запрашивающей стороне порождённый дочерний сеанс видим везде, где настроенная видимость включает дерево сеанса запрашивающей стороны, а all не обладает меньшими возможностями, чем tree.

Примечания о совместимости

В старых записях сеансов может отсутствовать leaseId. Для них следует использовать устаревший путь очистки с безопасным отказом:

  • требовать наличия текущего корневого процесса
  • требовать принадлежности корня обёртки, когда ожидается созданная обёртка
  • требовать совпадения команды для корней без обёртки
  • никогда не отправлять сигнал только на основании устаревших сохранённых метаданных PID

Если устаревшую запись невозможно проверить, её следует оставить без изменений. Очистка аренд при запуске и следующий цикл выпуска должны со временем позволить удалить резервный механизм.

Критерии успеха

  • Закрытие старого или устаревшего сеанса ACPX не может завершить процесс другого Gateway.
  • Завершение родительского процесса не оставляет работающие процессы-внуки адаптера, не поддающиеся обычному завершению.
  • cancel прерывает активный ход, не закрывая повторно используемые сеансы.
  • sessions_list может отображать принадлежащие запрашивающей стороне межагентные дочерние сеансы ACP как при tree, так и при all.
  • Очистка при запуске выполняется на основе аренд, а не широкого сканирования строк команд.
  • Целевые матричные тесты процессов и видимости охватывают каждый пограничный случай, который ранее требовал отдельных исправлений при проверке.
Was this useful?
On this page

On this page