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 продолжают работать во время миграции.
  • Среда выполнения остается принадлежащей Plugin; ядро не узнает деталей пакета ACPX.

Не цели

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

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

Идентификатор экземпляра 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 строки между агентами и принадлежащие запрашивающему порожденные ACP-строки между агентами, даже если общий 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
  • сохраненная команда обертки экранирована shell-кавычками, а живая команда 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