Get started

การรีแฟกเตอร์วงจรชีวิต ACP

วงจรชีวิตของ ACP ทำงานได้ในปัจจุบัน แต่ส่วนใหญ่ถูกอนุมานย้อนหลังมากเกินไป การล้างโปรเซสสร้างความเป็นเจ้าของขึ้นใหม่จาก PID, สตริงคำสั่ง, เส้นทาง wrapper และตารางโปรเซสที่กำลังทำงานอยู่ การมองเห็นเซสชันสร้างความเป็นเจ้าของขึ้นใหม่ จากสตริง session-key บวกกับการค้นหา sessions.list({ spawnedBy }) ขั้นรอง วิธีนี้ทำให้การแก้ไขแบบแคบทำได้ แต่ก็ทำให้พลาดกรณีขอบได้ง่ายเช่นกัน: การนำ PID กลับมาใช้ซ้ำ, คำสั่งที่ถูก quote, โปรเซสรุ่นหลานของอะแดปเตอร์, รากสถานะของหลาย Gateway, cancel เทียบกับ close, และการมองเห็นแบบ tree เทียบกับ all ล้วนกลายเป็นคนละจุด ที่ต้องค้นพบกฎความเป็นเจ้าของชุดเดียวกันใหม่

การปรับโครงสร้างนี้ทำให้ความเป็นเจ้าของเป็นสิ่งชั้นหนึ่ง เป้าหมายไม่ใช่พื้นผิวผลิตภัณฑ์ ACP ใหม่ แต่เป็นสัญญาภายในที่ปลอดภัยขึ้นสำหรับพฤติกรรม ACP และ ACPX ที่มีอยู่

เป้าหมาย

  • การล้างจะไม่ส่งสัญญาณไปยังโปรเซส เว้นแต่หลักฐานสดปัจจุบันตรงกับ สิทธิ์การถือครองที่ OpenClaw เป็นเจ้าของ
  • cancel, close, และการเก็บกวาดตอนเริ่มต้นมีเจตนาวงจรชีวิตที่แยกจากกัน
  • sessions_list, sessions_history, sessions_send, และการตรวจสอบสถานะใช้ โมเดลเซสชันที่ผู้ร้องขอเป็นเจ้าของเดียวกัน
  • การติดตั้งหลาย Gateway ไม่สามารถเก็บกวาด wrapper ของ ACPX ของกันและกันได้
  • ระเบียนเซสชัน ACPX เก่ายังคงทำงานระหว่างการย้ายข้อมูล
  • รันไทม์ยังคงเป็นของ Plugin; core ไม่เรียนรู้รายละเอียดแพ็กเกจ ACPX

สิ่งที่ไม่ใช่เป้าหมาย

  • แทนที่ ACPX หรือเปลี่ยนพื้นผิวคำสั่ง /acp สาธารณะ
  • ย้ายพฤติกรรมอะแดปเตอร์ ACP เฉพาะผู้ขายเข้าไปใน core
  • บังคับให้ผู้ใช้ล้างสถานะด้วยตนเองก่อนอัปเกรด
  • ทำให้ cancel ปิดเซสชัน ACP ที่นำกลับมาใช้ซ้ำได้

โมเดลเป้าหมาย

อัตลักษณ์อินสแตนซ์ของ Gateway

แต่ละโปรเซส Gateway ควรมีรหัสอินสแตนซ์รันไทม์ที่เสถียร:

type GatewayInstanceId = string;

สามารถสร้างได้เมื่อ Gateway เริ่มต้น และบันทึกไว้ในสถานะตลอดอายุของ การติดตั้งนั้น นี่ไม่ใช่ความลับด้านความปลอดภัย แต่เป็นตัวแยกแยะความเป็นเจ้าของที่ใช้ เพื่อหลีกเลี่ยงการสับสนโปรเซส ACP ของ Gateway หนึ่งกับโปรเซสของ Gateway อีกตัว

ความเป็นเจ้าของเซสชัน ACP

ทุกเซสชัน ACP ที่ถูกสร้างควรมีเมทาดาทาความเป็นเจ้าของที่ทำให้เป็นรูปแบบมาตรฐาน:

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

Gateway ควรคืนค่าฟิลด์เหล่านี้บนแถวเซสชันเมื่อทราบค่าแล้ว การกรองการมองเห็นควรเป็นการตรวจสอบแบบบริสุทธิ์บนเมทาดาทาของแถว:

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

วิธีนี้นำการเรียก sessions.list({ spawnedBy }) ขั้นรองที่ซ่อนอยู่ ออกจาก การตรวจสอบการมองเห็น ลูก ACP ข้ามเอเจนต์ที่ถูกสร้างขึ้นเป็นของผู้ร้องขอ เพราะ แถวระบุไว้เช่นนั้น ไม่ใช่เพราะมี query ที่สองบังเอิญพบมัน

สิทธิ์การถือครองโปรเซส ACPX

ทุกการเปิด wrapper ที่สร้างขึ้นควรสร้างระเบียนสิทธิ์การถือครอง:

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";
};

โปรเซส wrapper ควรได้รับรหัสสิทธิ์การถือครองและรหัสอินสแตนซ์ Gateway ใน สภาพแวดล้อม:

OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...

เมื่อแพลตฟอร์มรองรับ การยืนยันควรเลือกใช้เมทาดาทาโปรเซสสด ที่ไม่สับสนได้จากการ quote คำสั่ง:

  • PID รากยังคงมีอยู่
  • เส้นทาง wrapper สดอยู่ใต้ wrapperRoot
  • กลุ่มโปรเซสตรงกับสิทธิ์การถือครองเมื่อมีข้อมูล
  • สภาพแวดล้อมมีรหัสสิทธิ์การถือครองที่คาดไว้เมื่ออ่านได้
  • แฮชคำสั่งหรือเส้นทาง executable ตรงกับสิทธิ์การถือครอง

หากไม่สามารถยืนยันโปรเซสสดได้ การล้างจะปิดทางโดยไม่ดำเนินการ

ตัวควบคุมวงจรชีวิต

เพิ่มตัวควบคุมวงจรชีวิต ACPX หนึ่งตัวที่เป็นเจ้าของสิทธิ์การถือครองโปรเซสและนโยบาย การล้าง:

interface AcpxLifecycleController {
  ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
  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 ร้องขอการยกเลิก turn เท่านั้น ต้องไม่เก็บกวาด wrapper หรือโปรเซสอะแดปเตอร์ที่นำกลับมาใช้ซ้ำได้

closeSession สามารถเก็บกวาดได้ แต่ต้องทำหลังจากโหลดระเบียนเซสชัน, โหลดสิทธิ์การถือครอง, และยืนยันว่า tree ของโปรเซสสดยังเป็นของ สิทธิ์การถือครองนั้นเท่านั้น

reapStartupOrphans เริ่มจากสิทธิ์การถือครองที่เปิดอยู่ในสถานะ สามารถใช้ตารางโปรเซส เพื่อค้นหาลูกหลานได้ แต่ไม่ควรสแกนคำสั่งใดๆ ที่ดูเหมือน ACP โดยพลการก่อน แล้วค่อยตัดสินว่าน่าจะเป็นของเรา

สัญญา Wrapper

wrapper ที่สร้างขึ้นควรมีขนาดเล็กต่อไป ควร:

  • เริ่มอะแดปเตอร์ในกลุ่มโปรเซสเมื่อรองรับ
  • ส่งต่อสัญญาณ termination ปกติไปยังกลุ่มโปรเซส
  • ตรวจจับการตายของ parent
  • เมื่อ parent ตาย ให้ส่ง SIGTERM แล้วให้ wrapper ยังทำงานอยู่จนกว่า fallback SIGKILL จะทำงาน
  • รายงาน PID รากและรหัสกลุ่มโปรเซสกลับไปยังตัวควบคุมวงจรชีวิตเมื่อ มีข้อมูลนั้น

wrapper ไม่ควรตัดสินนโยบายเซสชัน มันบังคับใช้เฉพาะการล้าง tree โปรเซสภายใน สำหรับกลุ่มอะแดปเตอร์ของตัวเองเท่านั้น

สัญญาการมองเห็นเซสชัน

การมองเห็นควรใช้ความเป็นเจ้าของของแถวที่ทำให้เป็นรูปแบบมาตรฐาน:

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 เป็นแบบ monotonic: all ต้องไม่ซ่อนลูกที่เป็นเจ้าของ ซึ่ง tree จะแสดง

แผนการย้ายข้อมูล

เฟส 1: เพิ่มอัตลักษณ์และสิทธิ์การถือครอง

  • เพิ่ม gatewayInstanceId ไปยังสถานะ Gateway
  • เพิ่ม store สิทธิ์การถือครอง ACPX ใต้ไดเรกทอรีสถานะ ACPX
  • เขียนสิทธิ์การถือครองก่อนสร้าง wrapper ที่สร้างขึ้น
  • เก็บ leaseId บนระเบียนเซสชัน ACPX ใหม่
  • เก็บฟิลด์ PID และคำสั่งเดิมไว้สำหรับระเบียนเก่า

เฟส 2: การล้างโดยยึดสิทธิ์การถือครองเป็นหลัก

  • เปลี่ยนการล้างตอนปิดให้โหลด leaseId ก่อน
  • ยืนยันความเป็นเจ้าของโปรเซสสดเทียบกับสิทธิ์การถือครองก่อนส่งสัญญาณ
  • เก็บ fallback ของ PID รากและ wrapper-root ปัจจุบันไว้เฉพาะระเบียน legacy
  • ทำเครื่องหมายสิทธิ์การถือครองเป็น closed หลังการล้างที่ยืนยันแล้ว
  • ทำเครื่องหมายสิทธิ์การถือครองเป็น lost เมื่อโปรเซสหายไปก่อนการล้าง

เฟส 3: การเก็บกวาดตอนเริ่มต้นโดยยึดสิทธิ์การถือครองเป็นหลัก

  • การเก็บกวาดตอนเริ่มต้นสแกนสิทธิ์การถือครองที่เปิดอยู่
  • สำหรับแต่ละสิทธิ์การถือครอง ให้ยืนยันโปรเซสรากและรวบรวมลูกหลาน
  • เก็บกวาด tree ที่ยืนยันแล้วโดยจัดการลูกก่อน
  • หมดอายุสิทธิ์การถือครอง closed และ lost เก่าด้วยกรอบการเก็บรักษาที่มีขอบเขต
  • เก็บการสแกน command-marker ไว้เป็น fallback legacy ชั่วคราวเท่านั้น โดยมี wrapper root และอินสแตนซ์ Gateway คุมเมื่อเป็นไปได้

เฟส 4: แถวความเป็นเจ้าของเซสชัน

  • เพิ่มเมทาดาทาความเป็นเจ้าของไปยังแถวเซสชัน Gateway
  • สอนตัวเขียน ACPX, subagent, งานเบื้องหลัง, และ session-store ให้เติม ownerSessionKey หรือ spawnedBy
  • แปลงการตรวจสอบการมองเห็นเซสชันให้ใช้เมทาดาทาของแถว
  • ลบการค้นหา sessions.list({ spawnedBy }) ขั้นรองในเวลาตรวจการมองเห็น

เฟส 5: ลบ heuristic legacy

หลังหนึ่งช่วง release:

  • หยุดพึ่งพาสตริงคำสั่งรากที่จัดเก็บไว้สำหรับการล้าง ACPX ที่ไม่ใช่ legacy
  • ลบการสแกน command-marker ตอนเริ่มต้น
  • ลบการค้นหารายการ fallback สำหรับการมองเห็น
  • เก็บพฤติกรรมปิดทางโดยไม่ดำเนินการเชิงป้องกันสำหรับสิทธิ์การถือครองที่หายไปหรือยืนยันไม่ได้

การทดสอบ

เพิ่มชุดทดสอบแบบ table-driven สองชุด

ตัวจำลองวงจรชีวิตโปรเซส:

  • PID ถูกนำกลับมาใช้ซ้ำโดยโปรเซสที่ไม่เกี่ยวข้อง
  • PID ถูกนำกลับมาใช้ซ้ำโดย wrapper root ของ Gateway อีกตัว
  • คำสั่ง wrapper ที่จัดเก็บไว้ถูก shell-quoted แต่คำสั่ง ps สดไม่ใช่
  • ลูกของอะแดปเตอร์ออกไปแล้ว แต่หลานยังอยู่ในกลุ่มโปรเซส
  • fallback SIGTERM เมื่อ parent ตายไปถึง SIGKILL
  • รายการโปรเซสไม่พร้อมใช้งาน
  • สิทธิ์การถือครองค้างที่ไม่มีโปรเซส
  • orphan ตอนเริ่มต้นที่มี wrapper, ลูกอะแดปเตอร์, และหลาน

เมทริกซ์การมองเห็นเซสชัน:

  • self, tree, agent, all
  • a2a เปิดใช้งานและปิดใช้งาน
  • แถวของเอเจนต์เดียวกัน
  • แถวข้ามเอเจนต์
  • แถว ACP ข้ามเอเจนต์ที่ถูกสร้างและผู้ร้องขอเป็นเจ้าของ
  • ผู้ร้องขอแบบ sandboxed ถูกจำกัดไว้ที่ tree
  • การกระทำ list, history, send, และ status

อินวาเรียนต์สำคัญ: ลูกที่ถูกสร้างและผู้ร้องขอเป็นเจ้าของจะมองเห็นได้ทุกที่ ที่การมองเห็นที่กำหนดค่ารวม tree เซสชันของผู้ร้องขอ และ all ไม่ด้อยความสามารถกว่า tree

หมายเหตุความเข้ากันได้

ระเบียนเซสชันเก่าอาจไม่มี leaseId ควรใช้เส้นทางการล้าง legacy ที่ปิดทางโดยไม่ดำเนินการ:

  • ต้องมีโปรเซสรากสด
  • ต้องมีความเป็นเจ้าของ wrapper-root เมื่อคาดว่าจะมี wrapper ที่สร้างขึ้น
  • ต้องมีความสอดคล้องของคำสั่งสำหรับรากที่ไม่ใช่ wrapper
  • ห้ามส่งสัญญาณโดยอิงเฉพาะเมทาดาทา PID ที่จัดเก็บไว้และค้างแล้ว

หากระเบียน legacy ยืนยันไม่ได้ ให้ปล่อยไว้ตามเดิม การล้างสิทธิ์การถือครองตอนเริ่มต้นและ ช่วง release ถัดไปควรเลิกใช้ fallback ได้ในที่สุด

เกณฑ์ความสำเร็จ

  • การปิดเซสชัน ACPX เก่าหรือค้างไม่สามารถฆ่าโปรเซสของ Gateway อีกตัวได้
  • การตายของ parent ไม่ปล่อยให้โปรเซสรุ่นหลานของอะแดปเตอร์ที่ดื้อยังทำงานอยู่
  • cancel ยกเลิก turn ที่กำลังทำงานโดยไม่ปิดเซสชันที่นำกลับมาใช้ซ้ำได้
  • sessions_list สามารถแสดงลูก ACP ข้ามเอเจนต์ที่ผู้ร้องขอเป็นเจ้าของภายใต้ทั้ง tree และ all
  • การล้างตอนเริ่มต้นขับเคลื่อนด้วยสิทธิ์การถือครอง ไม่ใช่การสแกนสตริงคำสั่งแบบกว้าง
  • ชุดทดสอบเมทริกซ์โปรเซสและการมองเห็นแบบเจาะจงครอบคลุมทุกกรณีขอบที่ ก่อนหน้านี้ต้องใช้การแก้จาก review แบบเฉพาะจุด