Get started

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

Edit source

วงจรชีวิตของ 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 ควรมีรหัสอินสแตนซ์รันไทม์ที่เสถียร:

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 ข้ามเอเจนต์ที่ถูกสร้างขึ้นเป็นของผู้ร้องขอ เพราะ แถวระบุไว้เช่นนั้น ไม่ใช่เพราะมี query ที่สองบังเอิญพบมัน

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

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

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

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

sh
OPENCLAW_ACPX_LEASE_ID=...OPENCLAW_GATEWAY_INSTANCE_ID=...

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

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

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

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

เพิ่มตัวควบคุมวงจรชีวิต 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 ร้องขอการยกเลิก turn เท่านั้น ต้องไม่เก็บกวาด wrapper หรือโปรเซสอะแดปเตอร์ที่นำกลับมาใช้ซ้ำได้

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

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

สัญญา Wrapper

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

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

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

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

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

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 เป็นแบบ 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 แบบเฉพาะจุด
Was this useful?