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 до ядра.
  • Вимога до користувачів вручну очищати стан перед оновленням.
  • Закриття повторно використовуваних сеансів 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