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<OwnedProcessTree | null>;
}
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 แบบเฉพาะจุด