---
read_when:
    - การเปลี่ยนแปลงวงจรการอนุมัติ พื้นที่จัดเก็บ โปรโตคอล หรือการให้สิทธิ์ของ exec หรือ Plugin
    - การเพิ่มลิงก์อนุมัติหรือตัวควบคุมการอนุมัติแบบเนทีฟลงในช่องทาง
    - การแสดงการอนุมัติของเซสชันลูกในมุมมองของเซสชันแม่หรือตัวประสานงาน
summary: การออกแบบการอนุมัติที่คงทนและลิงก์ตรงได้อย่างเจาะจงสำหรับ Control UI, แอปเนทีฟ, ช่องทาง และเซสชันหลัก
title: การอนุมัติของผู้ดำเนินการบนหลายอินเทอร์เฟซ
x-i18n:
    generated_at: "2026-07-16T19:41:13Z"
    model: gpt-5.6
    postprocess_version: locale-links-v1
    prompt_version: 32
    provider: openai
    source_hash: 9defdaada1911df1184f64429e1787c4881e735c433d6dbc30a5946e11cc7cce
    source_path: refactor/operator-approvals.md
    workflow: 16
---

# การอนุมัติของผู้ดำเนินการจากหลายพื้นผิว

การออกแบบนี้ติดตาม [#103505](https://github.com/openclaw/openclaw/issues/103505) โดยแทนที่อำนาจการอนุมัติภายในโปรเซสด้วยวงจรชีวิตเดียวที่ Gateway เป็นเจ้าของและมี SQLite รองรับ การอนุมัติ exec หรือ Plugin/เครื่องมือทุกครั้งที่ Gateway เป็นเจ้าของจะได้รับ ID ที่คงที่หนึ่งรายการ เส้นทาง Control UI ที่ผ่านการตรวจสอบสิทธิ์หนึ่งเส้นทาง การตัดสินผลแบบอะตอมมิกที่คำตอบแรกเป็นผู้ชนะ และการฉายข้อมูลสำหรับผู้ดำเนินการเท่านั้นไปยังสตรีมของเซสชันต้นทางและเซสชันบรรพบุรุษ

การดำเนินการแบบอินไลน์และลิงก์เชิงลึกใช้งานร่วมกันได้ ไม่มีสวิตช์สลับโหมดการอนุมัติ

## เป้าหมาย

- ออบเจ็กต์การอนุมัติถาวรหนึ่งรายการสำหรับด่าน exec และ Plugin/เครื่องมือ
- เส้นทาง `${controlUiBasePath}/approve/{approvalId}` ที่คงที่
- ตัดสินผลจาก Control UI, แอปแบบเนทีฟ หรือพื้นผิวช่องทางใดก็ตามที่ได้รับอนุญาต
- พฤติกรรมแบบอะตอมมิกที่คำตอบแรกเป็นผู้ชนะในพื้นผิวที่ทำงานพร้อมกัน
- การลองซ้ำด้วยคำตอบเดียวกันเป็นแบบ idempotent ส่วนคำตอบที่ขัดแย้งและมาภายหลังไม่สามารถเขียนทับคำตอบที่ชนะได้
- การหมดเวลา คำตัดสินที่เชื่อถือได้แต่มีรูปแบบไม่ถูกต้อง เส้นทางที่ขาดหาย การยกเลิก และการเริ่มใหม่ต้องปฏิเสธโดยค่าเริ่มต้น
- เหตุการณ์ร้องขอและเหตุการณ์ปลายทางต้องไปถึงเซสชันต้นทางและเจ้าของระดับพาเรนต์/ตัวประสานงานที่เกี่ยวข้องทั้งหมด
- ช่องทางได้รับการดำเนินการสำหรับการอนุมัติและการนำทางที่มีชนิดข้อมูล ส่วนข้อมูลคอลแบ็กของทรานสปอร์ตยังคงเป็นข้อมูลเฉพาะภายในช่องทาง
- เมธอด exec/Plugin ของ Gateway ที่มีอยู่ยังคงเข้ากันได้ ขณะที่การนำไปใช้งานของเมธอดเหล่านั้นรวมเข้าสู่บริการเดียว

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

- การคงอยู่หรือดำเนินการเครื่องมือที่ถูกบล็อกต่อหลังจาก Gateway เริ่มใหม่
- การทำให้ ID หรือ URL การอนุมัติเป็นข้อมูลรับรองแบบ bearer
- การเพิ่มพรอมต์การอนุมัติลงในทรานสคริปต์ที่โมเดลมองเห็น หรือปลุกเอเจนต์พาเรนต์
- การย้ายนโยบายการอนุมัติ คำสั่งผลิตภัณฑ์ หรือการอนุญาตผู้ตรวจสอบไปยัง Plugin ช่องทาง
- การโคลนสถานะการอนุมัติแยกตามช่องทาง อุปกรณ์ หรือบรรพบุรุษ
- การออกแบบรายการอนุญาต exec การประกอบนโยบาย Plugin หรือการคงอยู่ของ `allow-always` ใหม่ ยกเว้นในส่วนที่จำเป็นเพื่อทำให้ผลลัพธ์ปลายทางไม่กำกวม
- การทำให้ TUI แบบฝังที่ไม่มี Gateway เข้าถึงได้จากระยะไกลในระยะแรก โดยยังคงใช้งานได้เฉพาะภายในเครื่องและต้องปฏิเสธโดยค่าเริ่มต้นเมื่อไม่มีผู้ตรวจสอบ

## ค่าพื้นฐานก่อนเปิดตัวและแผนผังหลักฐาน

ตารางนี้บันทึกสถานะการนำไปใช้งานเมื่อเปิด #103505 ส่วนการเปิดตัวด้านล่างติดตามรีจิสทรีถาวร การดำเนินการที่มีชนิดข้อมูล หน้าเว็บลิงก์เชิงลึก และส่วนเพิ่มของไคลเอนต์แบบเนทีฟที่สร้างต่อยอดจากค่าพื้นฐานดังกล่าว

| พื้นผิว           | จุดเริ่มต้นและเจ้าของตามค่าพื้นฐาน                                                                                                                                  | พฤติกรรมและช่องว่างตามค่าพื้นฐาน                                                                                                                                                                    |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| exec ของเอเจนต์        | `src/agents/bash-tools.exec-approval-request.ts`, `src/agents/bash-tools.exec-host-shared.ts`                                                                   | การลงทะเบียน `exec.approval.*` แบบสองระยะช่วยป้องกันภาวะแข่งขันของ `/approve` ที่เกิดก่อนเวลา แต่การหมดเวลายังอาจกลายเป็นการอนุญาตผ่าน `askFallback` ได้                                                        |
| ด่านเครื่องมือของ Plugin  | `src/agents/agent-tools.before-tool-call.ts`                                                                                                                    | ร้องขอ `plugin.approval.*`; `timeoutBehavior: "allow"` สามารถอนุมัติด่านที่หมดเวลาแล้วได้ โหมดฝังมีอำนาจภายในโปรเซสแยกต่างหากใน `src/infra/embedded-plugin-approval-broker.ts` |
| ด่าน Node ของ Plugin  | `src/gateway/node-invoke-plugin-policy.ts`                                                                                                                      | สร้างและกระจายโดยตรงผ่านตัวจัดการ Plugin ทำให้เกิดการทำซ้ำบางส่วนของวงจรชีวิตเมธอดเซิร์ฟเวอร์                                                                                 |
| อำนาจของ Gateway | `src/gateway/server-aux-handlers.ts`, `src/gateway/exec-approval-manager.ts`, `src/gateway/server-methods/approval-shared.ts`                                   | ตัวจัดการ exec และ Plugin แยกกันใช้แมปภายในโปรเซส รายการปลายทางคงอยู่เป็นเวลา 15 วินาที หลักคำตอบแรกเป็นผู้ชนะมีผลเฉพาะภายในโปรเซสเดียว                                          |
| โปรโตคอล Gateway  | `packages/gateway-protocol/src/schema/exec-approvals.ts`, `packages/gateway-protocol/src/schema/plugin-approvals.ts`, `src/gateway/methods/core-descriptors.ts` | exec มี `get` สำหรับรายการที่รอดำเนินการเท่านั้น ส่วน Plugin ไม่มี `get` และไม่มีการค้นหาปลายทางที่ไม่ขึ้นกับชนิดสำหรับลิงก์เชิงลึก                                                                                   |
| การส่งมอบ          | `src/infra/exec-approval-channel-runtime.ts`, `src/infra/approval-native-runtime.ts`, `src/infra/approval-handler-runtime.ts`                                   | รองรับการกำหนดเส้นทางไปยังต้นทาง DM ของผู้อนุมัติ การเล่นซ้ำรายการที่รอดำเนินการ ตัวจัดการแบบเนทีฟ และการล้างข้อมูลปลายทางภายในโปรเซส ส่วนงานติดตามผลแยกต่างหากจะเพิ่มการกระทบยอดปลายทางแบบถาวร                          |
| การดำเนินการแบบพกพา  | `src/interactive/payload.ts`, `src/plugin-sdk/interactive-runtime.ts`, `src/plugin-sdk/approval-reply-runtime.ts`                                               | ปุ่มอนุมัติเป็นการดำเนินการคำสั่งที่มี `/approve ...` ส่วนเป้าหมาย URL และ Web App เป็นฟิลด์ปุ่มที่ไม่มีชนิดข้อมูล                                                                           |
| Telegram          | `extensions/telegram/src/approval-handler.runtime.ts`, `extensions/telegram/src/button-types.ts`                                                                | ตัวเรนเดอร์แยกวิเคราะห์ข้อความคำสั่งเพื่อระบุความหมายเชิงการอนุมัติก่อนสร้างข้อมูลคอลแบ็กส่วนตัว                                                                                     |
| Control UI        | `ui/src/app/exec-approval.ts`, `ui/src/app/overlays.ts`, `ui/src/components/exec-approval.ts`                                                                   | UI การอนุมัติเป็นโมดัลส่วนกลาง `ui/src/app-route-paths.ts` และ `ui/src/app-routes.ts` ใช้เส้นทางแบบตรงตัวและเขียนเส้นทางที่ไม่รู้จักใหม่ให้ไปยัง Chat                                                    |
| ความเป็นเจ้าของเซสชัน | `src/agents/subagent-registry.types.ts`, `src/agents/subagent-registry-read.ts`, `src/config/sessions/types.ts`                                                 | มีความเป็นเจ้าของโดยคอนโทรลเลอร์ ผู้ร้องขอ พาเรนต์ที่ระบุอย่างชัดเจน และการสร้างแบบเดิมอยู่แล้ว แต่เหตุการณ์การอนุมัติยังไม่ได้ฉายไปยังสตรีมของเซสชันเหล่านั้น                                                    |
| สถานะที่ใช้ร่วมกัน      | `src/state/openclaw-state-schema.sql`, `src/state/openclaw-state-db.ts`                                                                                         | ธุรกรรมแบบทันทีและการอัปเดตตามเงื่อนไขของ Kysely ที่มีอยู่รองรับการเปรียบเทียบและตั้งค่าแบบถาวรใน `state/openclaw.sqlite`                                                                   |

การทดสอบปัจจุบันที่เป็นตัวแทนประกอบด้วย `src/gateway/exec-approval-manager.test.ts`, `src/gateway/server-methods/approval-shared.test.ts`, `src/agents/bash-tools.exec-gateway-approval.e2e.test.ts`, `extensions/telegram/src/approval-handler.runtime.test.ts` และ `ui/src/e2e/approval-flow.e2e.test.ts`

SDK ของ Plugin ยังคงเป็นขอบเขตเดียวระหว่างช่องทาง/Plugin การเปลี่ยนแปลงรันไทม์และการนำเสนอการอนุมัติต้องส่งออกผ่านพาธย่อย `src/plugin-sdk/approval-*.ts` และ `src/plugin-sdk/interactive-runtime.ts` ที่มีอยู่ โค้ดใช้งานจริงของ Plugin ต้องไม่นำเข้าส่วนภายในของ Gateway

## งานที่มีมาก่อน

Omnigent มี UX และความหมายของความล้มเหลวที่เป็นประโยชน์:

- [`approval.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/runtime/policies/approval.py) พัก ASK ไว้ ใช้การหมดเวลาตามแต่ละนโยบาย และถือว่าเฉพาะการยอมรับที่ตรงกันเท่านั้นเป็นการอนุมัติ
- [`sessions.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/server/routes/sessions.py) มีด่านชุดทดสอบแบบเนทีฟฝั่งเซิร์ฟเวอร์และการฉายคำขอ/การตัดสินผลไปยังบรรพบุรุษ
- [`ApprovePage.tsx`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/web/src/pages/ApprovePage.tsx) มีหน้าการอนุมัติบนอุปกรณ์เคลื่อนที่แบบสแตนด์อโลน

อย่าคัดลอกคำกล่าวอ้างเรื่องพื้นที่จัดเก็บของระบบดังกล่าวโดยไม่พิจารณาอย่างรอบคอบ สถานะที่รอดำเนินการและใช้งานอยู่ในปัจจุบันเป็นข้อมูลภายในโปรเซสใน [`_elicitation_registry.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/server/_elicitation_registry.py) และตารางรายการที่รอดำเนินการซึ่งไม่ได้ใช้ถูกลบโดย [`e3b1f2a4c9d7_drop_pending_tool_calls_table.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/db/migrations/versions/e3b1f2a4c9d7_drop_pending_tool_calls_table.py) OpenClaw จงใจดำเนินการให้ไกลกว่านั้น โดยให้ SQLite เป็นแหล่งข้อมูลที่มีอำนาจ และทุกการเปลี่ยนผ่านไปยังสถานะปลายทางเป็นการเปรียบเทียบและตั้งค่าฐานข้อมูล

## สถาปัตยกรรมและความเป็นเจ้าของ

Gateway เป็นเจ้าของวงจรชีวิต:

1. เอเจนต์ ฮุกของ Plugin หรือนโยบาย Node จัดเตรียมคำขอเฉพาะชนิดและการผูกการดำเนินการภายในโปรเซส
2. Gateway ตรวจสอบคำขอและสร้างการฉายข้อมูลสำหรับผู้ตรวจสอบที่ผ่านการกรอง
3. บริการการอนุมัติคำนวณกลุ่มผู้รับที่เป็นต้นทาง/เจ้าของ แทรกแถวข้อมูลหลัก แล้วลงทะเบียนตัวรอภายในโปรเซส
4. หลังจากแทรกข้อมูลถาวรแล้ว Gateway จะเผยแพร่เหตุการณ์การอนุมัติที่มีอยู่ การฉายข้อมูลเซสชัน การแจ้งเตือนช่องทาง และการพุชแบบเนทีฟ
5. ทุกพื้นผิวตัดสินผลผ่านบริการเดียวกัน
6. บริการคอมมิตการเปลี่ยนผ่านปลายทางหนึ่งครั้ง ปลุกตัวรอของรันไทม์ และเผยแพร่การฉายข้อมูลปลายทาง
7. การส่งเหตุการณ์ที่ล้มเหลวจะไม่ย้อนกลับการตัดสินใจที่คอมมิตแล้ว ไคลเอนต์กู้คืนผ่าน `approval.get` หรือการเล่นซ้ำรายการ

ขอบเขตความเป็นเจ้าของ:

- `src/gateway/`: บริการการอนุมัติ การตรวจสอบสิทธิ์ อะแดปเตอร์ RPC การสร้าง URL วงจรชีวิตตัวรอ และการเผยแพร่เหตุการณ์
- `src/state/`: สคีมาที่ใช้ร่วมกันและชนิดข้อมูล Kysely ที่สร้างขึ้น
- `src/infra/`: โมเดลมุมมองการอนุมัติที่ผ่านการกรองและการสร้างการนำเสนอแบบพกพา
- `src/agents/`: ร้องขอ รอ และใช้คำตัดสินที่ส่งกลับมา โดยไม่คงข้อมูล
- `src/channels/` และ `extensions/*`: เรนเดอร์การดำเนินการที่มีชนิดข้อมูล ตรวจสอบสิทธิ์ผู้ใช้ช่องทาง เข้ารหัสคอลแบ็กส่วนตัว และอัปเดตตัวควบคุมที่ส่งมอบแล้ว
- `src/plugin-sdk/`: เฉพาะสัญญาการอนุมัติและการนำเสนอสาธารณะ
- `ui/`: หน้าแบบสแตนด์อโลนและไคลเอนต์คิว/โมดัลที่มีอยู่

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

## ระเบียนถาวร

เพิ่มตาราง `operator_approvals` หนึ่งตารางลงในฐานข้อมูลสถานะที่ใช้ร่วมกัน

| คอลัมน์                                             | วัตถุประสงค์                                                                                                                                       |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `approval_id`                                      | ID มาตรฐานที่ไม่ซ้ำกันทั่วทั้งระบบ คง exec ID และ ID ของ `plugin:` ที่มีอยู่ไว้เพื่อความเข้ากันได้ของโปรโตคอล แต่อย่าอนุมานชนิดจากคำนำหน้า      |
| `resolution_ref`                                   | ตัวระบุตำแหน่ง SHA-256 แบบเต็มในรูปแบบ base64url ที่ไม่ซ้ำกัน สำหรับ callback ของการขนส่งที่ไม่สามารถส่ง ID มาตรฐานได้ ไม่ใช่การให้สิทธิ์หรือ ID ของ URL สาธารณะ |
| `kind`                                             | ตัวจำแนก `exec \| plugin` แบบปิด                                                                                                        |
| `status`                                           | สถานะ `pending \| allowed \| denied \| expired \| cancelled` แบบปิด                                                                          |
| `presentation_json`                                | โปรเจกชันสำหรับผู้ตรวจสอบที่ผ่านการตรวจสอบและติดแท็กชนิด คำขอรันไทม์ดิบ การผูกคำสั่ง และเพย์โหลด callback ยังคงอยู่ภายในโปรเซส               |
| `source_agent_id`, `source_session_key`            | จุดยึดสำหรับโปรเจกชันข้อมูลประจำตัวต้นทางและเซสชัน คีย์เซสชันมีความคงทนถาวร แต่ UUID เซสชันที่หมุนเวียนไม่มีความคงทน                                           |
| `audience_session_keys_json`                       | อาร์เรย์ JSON ที่เรียงลำดับและตัดรายการซ้ำ ซึ่งสร้างจากการเดินตรวจสอบความเป็นเจ้าของแบบกว้างก่อนที่มีขอบเขต เหตุการณ์ที่ร้องขอและเหตุการณ์ปลายทางใช้สแนปช็อตเดียวกันนี้ |
| `requested_by_device_id`, `requested_by_client_id` | เมทาดาทาของผู้ร้องขอ/การตรวจสอบที่คงทนถาวร ID การเชื่อมต่อจะอยู่ในหน่วยความจำและไม่ใช่ตัวการหลักข้ามพื้นผิว                                         |
| `reviewer_device_ids_json`                         | อุปกรณ์ผู้ตรวจสอบเป้าหมายที่ระบุอย่างชัดเจนซึ่งเป็นทางเลือก และจัดหาโดยรันไทม์การอนุมัติที่เชื่อถือได้เท่านั้น                                                  |
| `runtime_epoch`                                    | ยุคของโปรเซสที่เป็นเจ้าของการดำเนินการซึ่งถูกพักไว้ ใช้ยกเลิกแถวที่ไม่มีเจ้าของหลังจากรีสตาร์ต                                                     |
| `created_at_ms`, `expires_at_ms`, `updated_at_ms`  | การกำหนดเวลาที่เป็นแหล่งข้อมูลหลัก                                                                                                                         |
| `decision`                                         | การตัดสินใจของผู้ใช้อย่างชัดเจนเมื่อมีอยู่                                                                                                       |
| `terminal_reason`                                  | เหตุผลแบบปิด เช่น `user`, `timeout`, `malformed-verdict`, `no-route`, `run-aborted` หรือ `gateway-restart`                                |
| `resolved_at_ms`, `resolver_kind`, `resolver_id`   | ข้อมูลประจำตัวของผู้ชนะและการตรวจสอบที่เก็บไว้ฝั่งเซิร์ฟเวอร์ โปรเจกชันสำหรับผู้ตรวจสอบจะละเว้นตัวระบุผู้แก้ไขปัญหาแบบดิบ                                           |
| `consumed_at_ms`, `consumed_by`                    | ตัวป้องกันการเล่นซ้ำแยกต่างหากสำหรับ `allow-once` การใช้งานต้องไม่ลบการตัดสินใจที่บันทึกไว้                                                       |

ดัชนีที่จำเป็น:

| ดัชนี                                      | วัตถุประสงค์                                                                     |
| ------------------------------------------ | --------------------------------------------------------------------------- |
| unique `(resolution_ref)`                  | ปฏิเสธความกำกวมข้ามคอลัมน์ของ `approval_id`/`resolution_ref` ระหว่างการแทรก |
| `(status, expires_at_ms)`                  | ค้นหาการอนุมัติที่รอดำเนินการและปรับกำหนดเวลาที่เป็นแหล่งข้อมูลหลักให้สอดคล้องกัน               |
| `(source_session_key, created_at_ms DESC)` | เล่นซ้ำการอนุมัติล่าสุดสำหรับเซสชันต้นทางหนึ่งเซสชัน                             |
| `(resolved_at_ms)`                         | ล้างการอนุมัติปลายทางที่เก็บไว้ตามนโยบายการเก็บรักษาแบบคงที่  |

อาร์เรย์กลุ่มเป้าหมายมีขนาดเล็กและมีขอบเขต การเล่นซ้ำที่กรองตามเซสชันจะเลือกแถวที่รอดำเนินการซึ่งมองเห็นได้ผ่าน Kysely ก่อน จากนั้นจึงถอดรหัสและกรองอาร์เรย์กลุ่มเป้าหมายที่มีขอบเขตในโค้ดแอปพลิเคชัน โดยไม่ใช้การจับคู่สตริงหรือคิวรี JSON ด้วย SQL ดิบ

เก็บแถวปลายทางไว้เป็นเวลา 30 วัน ให้สอดคล้องกับการเก็บรักษาเมทาดาทาการตรวจสอบใน `src/audit/audit-event-store.ts` การล้างข้อมูลเป็นนโยบายการบำรุงรักษาแบบคงที่ ไม่ใช่พื้นผิวการกำหนดค่าใหม่ ฐานข้อมูลเป็นสถานะระนาบควบคุมภายในเครื่องแบบส่วนตัว แต่ API สำหรับผู้ตรวจสอบต้องไม่เปิดเผยคำขอที่จัดเก็บไว้ทั้งหมดหรือการผูกรันไทม์

## เครื่องสถานะและการเปรียบเทียบแล้วกำหนดค่า

เฉพาะการเปลี่ยนสถานะต่อไปนี้เท่านั้นที่ถูกต้อง:

- `pending -> allowed`: `allow-once` หรือ `allow-always` ที่ระบุอย่างชัดเจน
- `pending -> denied`: การปฏิเสธที่ระบุอย่างชัดเจน คำตัดสินปลายทางที่ผิดรูปแบบจากแหล่งที่เชื่อถือได้ หรือไม่มีเส้นทางส่ง
- `pending -> expired`: ถึงกำหนดเวลาที่เป็นแหล่งข้อมูลหลัก
- `pending -> cancelled`: การยกเลิกการรัน การปิดระบบอย่างราบรื่น หรือการกู้คืนรายการไร้เจ้าของหลังรีสตาร์ต

สถานะปลายทางทุกสถานะที่ไม่ได้รับอนุญาตมีคำตัดสินที่มีผลเป็นปฏิเสธ

การแก้ไขใช้ธุรกรรม SQLite แบบทันทีหนึ่งรายการ และการอัปเดตตามเงื่อนไขของ Kysely ที่เทียบเท่ากับ:

```sql
UPDATE operator_approvals
SET status = ?, decision = ?, terminal_reason = ?, resolved_at_ms = ?
WHERE approval_id = ?
  AND status = 'pending'
  AND expires_at_ms > ?;
```

หากการอัปเดตไม่ส่งผลต่อแถวใด ธุรกรรมเดียวกันจะอ่านระเบียน:

- ไม่พบหรือไม่ได้รับอนุญาต: ส่งคืนว่าไม่พบ และอย่าเปิดเผยว่ามีอยู่
- ยังรอดำเนินการแต่ถึงกำหนดเวลาแล้ว: เปรียบเทียบแล้วกำหนดค่าเป็น `expired` จากนั้นส่งคืนแถวปลายทางนั้น
- เป็นการตัดสินใจเดียวกับที่บันทึกไว้: ส่งคืนความสำเร็จแบบทำซ้ำได้โดยให้ผลเดิมพร้อมผู้ชนะที่บันทึกไว้
- เป็นการตัดสินใจที่ต่างกัน: API แบบรวมส่งคืน `applied: false` พร้อมผู้ชนะที่บันทึกไว้ ส่วนอะแดปเตอร์แบบเดิมจะคง `APPROVAL_ALREADY_RESOLVED` ไว้ในกรณีที่สัญญาที่เผยแพร่แล้วกำหนด
- สถานะปลายทางใดๆ: ห้ามแก้ไขโดยเด็ดขาด

`now == expires_at_ms` หมดอายุแล้ว เวลาของ Gateway เป็นแหล่งข้อมูลหลัก

การดำเนินการ `allow-once` ใช้ CAS ครั้งที่สองกับ `consumed_at_ms IS NULL` โดยผูกกับบริบทคำสั่ง/การรันระบบเดิมที่ตรงกันทุกประการ แถวการอนุมัติยังคงเป็นระเบียนการตรวจสอบหลังการใช้งาน

อินพุต HTTP/RPC ที่ผิดรูปแบบซึ่งไม่สามารถยืนยันตัวตนหรือระบุการอนุมัติได้จะถูกปฏิเสธโดยไม่มีการแก้ไข และไม่สามารถอนุมัติได้โดยเด็ดขาด คำตัดสินปลายทางที่ผิดรูปแบบซึ่งได้รับจากฮาร์เนส/ตัวรอที่เชื่อถือได้สำหรับการอนุมัติที่ทราบ จะเปลี่ยนสถานะเป็น `denied`

## API ของ Gateway

เพิ่มเมธอดสำหรับผู้ตรวจสอบที่ไม่ขึ้นกับชนิด:

| เมธอด                                    | สัญญา                                                                                                                                                                                                            |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approval.get { id }`                     | ส่งคืนโปรเจกชันที่มองเห็นได้ซึ่งอยู่ระหว่างรอดำเนินการหรือเป็นปลายทางที่เก็บรักษาไว้                                                                                                                                                          |
| `approval.resolve { id, kind, decision }` | รับ ID มาตรฐานหรือตัวอ้างอิงการขนส่งขนาดคงที่ จากนั้นดำเนินการตรวจสอบสิทธิ์ ตรวจสอบชนิดและการตัดสินใจที่อนุญาต ปรับกำหนดเวลาให้สอดคล้อง และทำ CAS ปลายทาง การตอบกลับจะมี ID มาตรฐานเสมอ |

หลังจาก CAS สำเร็จ ให้ส่งคืนโปรเจกชันที่คอมมิตแล้วทันที เหตุการณ์แบบเดิม ตัวส่งต่อของช่องทาง และตัวทำให้ push เป็นปลายทาง เป็นการดำเนินการติดตามผลแบบพยายามให้ดีที่สุด พื้นผิวที่ช้าหรือล้มเหลวต้องไม่ทำให้การตอบกลับที่ชนะล่าช้าหรือย้อนกลับ

การตรวจสอบคำขอเฉพาะชนิดยังคงอยู่ใน `exec.approval.request` และ `plugin.approval.request` ส่วน `exec.approval.get/list/waitDecision/resolve` และ `plugin.approval.list/waitDecision/resolve` ที่มีอยู่จะกลายเป็นอะแดปเตอร์ขอบเขตโปรโตคอลไปยังบริการมาตรฐาน เนื่องจากเป็น API ของ Gateway ที่เผยแพร่แล้ว ผู้เรียกใช้ภายในจะย้ายไปยังบริการในการเปลี่ยนแปลงเดียวกัน

โปรเจกชันสำหรับผู้ตรวจสอบเป็น tagged union:

```ts
type OperatorApproval = {
  id: string;
  status: OperatorApprovalStatus;
  presentation:
    | { kind: "exec"; commandText: string /* ตัวอย่าง exec ที่ปลอดภัย */ }
    | { kind: "plugin"; title: string; description: string /* ตัวอย่าง Plugin ที่ปลอดภัย */ };
  // ฟิลด์วงจรชีวิตส่วนกลาง
};
```

พาธที่เสถียรถูกสร้างขึ้น ไม่ได้จัดเก็บถาวร `approval.get` ส่งคืน `urlPath` พื้นผิวที่ทราบต้นทางสาธารณะที่ได้รับอนุมัติอาจได้รับ `url` แบบสัมบูรณ์ด้วย สแนปช็อตสำหรับผู้ตรวจสอบจะละเว้นคีย์เซสชันของต้นทางและกลุ่มเป้าหมาย Gateway เก็บคีย์การกำหนดเส้นทางเหล่านั้นไว้ฝั่งเซิร์ฟเวอร์สำหรับโปรเจกชัน `session.approval` ที่แยกต่างหาก

## เหตุการณ์และการดำเนินการแบบพกพา

PR 1 คงชื่อเหตุการณ์ เพย์โหลด และตัวกรองผู้รับระดับระเบียนที่เผยแพร่แล้วไว้:

- `exec.approval.requested`
- `exec.approval.resolved`
- `plugin.approval.requested`
- `plugin.approval.resolved`

เหตุการณ์แบบเดิมเหล่านั้นอาจมีคำขอรันไทม์ทั้งหมด ดังนั้นจึงต้องไม่กระจายไปยังไคลเอนต์ทุกตัวที่อยู่ในขอบเขตการอนุมัติ PR 5 เพิ่มฟิลด์วงจรชีวิตที่มีแท็ก (`status`, `sourceSessionKey`, `urlPath`, เมทาดาทาปลายทาง และ `kind` ระดับการนำเสนอ) ผ่านโปรเจกชันวงจรชีวิตที่ผ่านการปรับให้ปลอดภัย แทนการขยายการส่งเหตุการณ์แบบเดิม

เพิ่มเหตุการณ์โปรเจกชัน `session.approval` ที่อยู่ในขอบเขตการอนุมัติ เผยแพร่เหตุการณ์มาตรฐานหนึ่งครั้งพร้อมคีย์กลุ่มเป้าหมายที่จัดเก็บถาวร โดยผู้สมัครรับข้อมูลของเซสชันที่ตรงกันทุกประการจะได้รับเหตุการณ์เดียวกันสำหรับแต่ละคีย์ที่ตรงกัน:

- `sessionKey`: สตรีมที่รับโปรเจกชัน
- `sourceSessionKey`: ลูก/ต้นทางที่ทำให้เกิดเกต
- `phase`: `pending \| terminal` ซึ่งจำแนกตามสถานะการอนุมัติ
- โปรเจกชัน `OperatorApproval` ที่ปลอดภัยหนึ่งรายการ

ไคลเอนต์เลือกเข้าร่วมด้วย `sessions.messages.subscribe { key, agentId?, includeApprovals: true }` การตอบกลับที่สำเร็จจะเพิ่ม `approvalReplay` ซึ่งมีการอนุมัติที่รอดำเนินการในปัจจุบันได้สูงสุด 1,000 รายการ สำหรับคีย์สตรีมที่ตรงกันทุกประการและไคลเอนต์ที่สมัครรับข้อมูลได้รับอนุญาตระดับระเบียนให้ตรวจสอบด้วย `truncated: false` ทำให้การเล่นซ้ำที่กรองแล้วเป็นแหล่งข้อมูลหลัก และไคลเอนต์ที่เชื่อมต่อใหม่จะแทนที่ชุดรายการที่รอดำเนินการในเครื่องด้วยข้อมูลนั้น ส่วน `truncated: true` เป็นสัญญาณโอเวอร์โหลด และไคลเอนต์ต้องเก็บรายการในเครื่องที่ยังไม่เห็นไว้จนกว่าการค้นหาแบบมาตรฐานหรือเหตุการณ์วงจรชีวิตในภายหลังจะยุติสถานะรายการเหล่านั้น การหมดเวลาแบบคงทนที่พบภายหลังระหว่างการเล่นซ้ำจะส่ง tombstone ปลายทางเฉพาะไปยังกลุ่มเป้าหมายที่สมัครรับข้อมูลและได้รับอนุญาตระดับระเบียน ก่อนส่งคืนสแนปช็อตใหม่ `operator.admin` อาจเลือกเข้าร่วมโดยตรง ส่วนไคลเอนต์ที่มีขอบเขตแคบกว่าต้องมีทั้งข้อมูลประจำตัวอุปกรณ์ที่จับคู่แล้วและ `operator.approvals` การสมัครรับข้อมูลเซสชันเพียงอย่างเดียวไม่เคยมอบสิทธิ์มองเห็นการอนุมัติ

ลงทะเบียนเหตุการณ์ภายใต้ `operator.approvals` ใน `src/gateway/server-broadcast.ts` โปรเจกชันนี้ใช้เพื่อการสังเกตการณ์ โดยจะไม่เพิ่มแถวทรานสคริปต์ ส่ง `sessions.changed` หรือปลุกเอเจนต์

ขยาย `MessagePresentationAction` ใน `src/interactive/payload.ts`:

```ts
type MessagePresentationAction =
  | { type: "command"; command: string }
  | { type: "callback"; value: string }
  | {
      type: "approval";
      approvalId: string;
      approvalKind: "exec" | "plugin";
      decision: ExecApprovalDecision;
    }
  | { type: "url"; url: string }
  | { type: "web-app"; url: string };
```

Core สร้างการดำเนินการตัดสินใจแบบมีชนิดและลิงก์ตรวจสอบแยกต่างหากเมื่อมีต้นทาง Control UI แบบสัมบูรณ์ที่ได้รับอนุมัติ ช่องทางต่าง ๆ เข้ารหัสการดำเนินการอนุมัติเป็นรูปแบบ callback ของตนเองและส่งผลการตัดสินไปยังบริการมาตรฐาน callback จะใช้ ID มาตรฐานที่ตรงกันทุกประการเมื่อมีขนาดพอดี มิฉะนั้นจะใช้ `resolution_ref` แบบไดเจสต์เต็มที่ไม่ซ้ำกันของแถว การอ้างอิงเป็นเพียงคีย์ค้นหาแบบย่อเท่านั้น โดยยังคงต้องใช้การยืนยันตัวตนของ Gateway ตามปกติ การอนุญาตระดับระเบียน ชนิดที่ระบุอย่างชัดเจน การตรวจสอบการตัดสินใจที่อนุญาต การกระทบยอดกำหนดเวลา และ CAS แบบคำตอบแรก ช่องทางต้องไม่ตัดทอน ID แก้ไขคำนำหน้าแฮช แยกวิเคราะห์ข้อความ `/approve` หรืออนุมานชนิดจากคำนำหน้า ID

เก็บ `button.url`, `button.webApp` และตัวควบคุมการอนุมัติที่มีคำสั่งรองรับไว้เป็นอินพุตความเข้ากันได้ของ plugin SDK ที่เลิกใช้แล้ว ปรับให้อยู่ในรูปแบบมาตรฐานที่ขอบเขต SDK และย้ายผู้เรียกใช้ภายในแบบรวมทุกตัวใน PR เดียวกัน `/approve {id} {decision}` ยังคงเป็นข้อความสำรองและคำสั่ง CLI/แชต ไม่ใช่สัญญาเชิงความหมายของปุ่ม

## Control UI

เส้นทางคือ `${basePath}/approve/{approvalId}` โดย ID เป็นพารามิเตอร์พาธเพียงรายการเดียว ส่วนข้อมูลระบุตัวตนของเซสชันต้นทางมาจากระเบียน

เนื่องจากเราเตอร์ปัจจุบันมีเส้นทางคงที่ที่ตรงกันทุกประการและเขียนเส้นทางที่ไม่รู้จักใหม่ไปยังแชต ให้ตรวจหาดีปลิงก์นี้ใน `ui/src/app/bootstrap.ts` ก่อนการปรับเส้นทางให้อยู่ในรูปแบบมาตรฐานตามปกติ ใช้การตั้งค่า Gateway/การยืนยันตัวตนตามปกติซ้ำ แต่แสดงหน้าการอนุมัติแบบสแตนด์อโลนนอกเชลล์แถบด้านข้างและโมดอลส่วนกลาง

เอกสารเป็นของ Gateway ที่ให้บริการ URL นั้น การเชื่อมต่อเริ่มต้นจะไม่สนใจการเลือก Gateway ระยะไกลที่แอปเต็มรูปแบบบันทึกไว้ โดยไม่เปลี่ยนแปลงหรือคัดลอกการตั้งค่าของการเลือกดังกล่าว มีเพียงการยืนยันตัวตนที่ยังคงจำกัดขอบเขตอยู่ในเซสชันของ Gateway ที่ให้บริการ การยืนยันตัวตนแบบเนทีฟที่เชื่อถือได้หรือการแทนที่ `gatewayUrl` ที่ยืนยันแยกต่างหากอาจเปลี่ยนเป้าหมายได้ Core สงวนเนมสเปซ `/approve` แบบหนึ่งเซกเมนต์ไว้ก่อนเส้นทาง HTTP ของ plugin และการตรวจหาส่วนขยายแบบคงที่ รวมถึง ID ที่ลงท้ายด้วย `.json` หรือ `.js` เมื่อปิดใช้งานการให้บริการ Control UI เส้นทางที่สงวนไว้จะปฏิเสธอย่างปลอดภัยด้วย `404` เก็บหน้าไว้ในบันเดิลหลักของ Control UI เพื่อไม่ให้ lazy chunk ที่ล้มเหลวทำให้การตัดสินใจด้านความปลอดภัยค้างอยู่ที่ตัวหมุนโหลด

สถานะของหน้า:

- กำลังโหลด
- ต้องยืนยันตัวตน
- รอดำเนินการ
- กำลังตัดสินผล
- อนุมัติหรือปฏิเสธที่นี่แล้ว
- ตัดสินผลจากที่อื่นแล้ว
- หมดอายุ
- ยกเลิกแล้ว
- ไม่ได้รับอนุญาต/ไม่พบ
- เกิดข้อผิดพลาดในการเชื่อมต่อพร้อมให้ลองใหม่

หน้าเรียกใช้ Gateway RPC ไม่ใช่ REST API ที่สองซึ่งไม่มีการยืนยันตัวตน การรีเฟรชเบราว์เซอร์จะอ่านสถานะถาวรอีกครั้ง และจะไม่ใส่ข้อมูลประจำตัวของ Gateway ไว้ใน URL คิวรี หรือส่วน fragment

## การอนุญาตและความเป็นส่วนตัว

URL เป็นตัวระบุตำแหน่ง ไม่ใช่อำนาจ การตัดสินผลต้องมี:

1. การเชื่อมต่อ Gateway ที่ยืนยันตัวตนแล้ว;
2. `operator.approvals` หรือ `operator.admin`;
3. การอนุญาตผู้ตรวจสอบระดับระเบียน

กฎระดับระเบียน:

- `operator.admin` สามารถตรวจสอบได้
- `reviewer_device_ids` มีอำนาจชี้ขาดเมื่อมีค่า เฉพาะอุปกรณ์
  `operator.approvals` ที่จับคู่และอยู่ในรายการเท่านั้นที่ตรวจสอบได้ อุปกรณ์ที่ส่งคำขอไม่มี
  สิทธิ์เข้าถึงโดยนัย เว้นแต่จะอยู่ในรายการด้วย
- หากไม่มีรายชื่อผู้ตรวจสอบที่ระบุอย่างชัดเจน อุปกรณ์
  `operator.approvals` ที่จับคู่และส่งคำขอสามารถตรวจสอบระเบียนของตนเองได้
- ระเบียนเก่าจริงที่ไม่มีการผูกกับผู้ส่งคำขอหรือผู้ตรวจสอบยังคงให้
  อุปกรณ์ที่จับคู่มองเห็นได้อย่างกว้างขวาง เพื่อไม่ให้การอัปเกรดทำให้งานที่รอดำเนินการอยู่แล้วค้างอยู่
- รันไทม์ภายในที่ไม่มีอุปกรณ์สามารถตัดสินผลได้ แต่ไม่สามารถอ่านได้ ผ่านการเชื่อมต่อ
  รันไทม์การอนุมัติแบบจำกัดขอบเขต อำนาจดังกล่าวมาจากโทเค็นรันไทม์
  ที่เซิร์ฟเวอร์ยืนยันตัวตนแล้วเท่านั้น ฟิลด์ `approval.resolve` แบบสาธารณะไม่สามารถ
  สร้างอำนาจนี้ได้
- ความเป็นเจ้าของการเชื่อมต่อสดของผู้ส่งคำขอยังคงใช้ได้กับอะแดปเตอร์รุ่นเก่า โดยจะ
  ไม่อนุมานจากชื่อไคลเอนต์ที่ตรงกัน
- การเป็นสมาชิกกลุ่มผู้รับเปลี่ยนเฉพาะการนำเสนอเท่านั้น และจะไม่ขยายการอนุญาต

`approval.get` เปิดเผยเฉพาะโปรเจกชันผู้ตรวจสอบที่ผ่านการทำให้ปลอดภัยแล้ว และละเว้นคีย์การกำหนดเส้นทางต้นทาง/กลุ่มผู้รับภายใน เหตุการณ์ `session.approval` ของ PR 5 มีปลายทางเดียวคือ `sessionKey` พร้อม `sourceSessionKey` หลังจาก Gateway ใช้สแนปช็อตกลุ่มผู้รับที่บันทึกไว้ในฝั่งเซิร์ฟเวอร์ เหตุการณ์ exec/plugin ที่มีอยู่จะคงเพย์โหลดเดิมและผู้รับที่จำกัดไว้จนกว่าผู้ใช้เหตุการณ์จะย้ายระบบ คำขอที่เรียกใช้ได้ การผูกคำสั่ง และการดำเนินการต่อยังคงอยู่ในตัวรอภายในโปรเซสเท่านั้น แถวถาวรมีการนำเสนอที่ปลอดภัย พร้อมข้อมูลเมตาวงจรชีวิต การกำหนดเส้นทาง และการตรวจสอบ โดยจะไม่เก็บค่าสภาพแวดล้อมดิบ ข้อมูลประจำตัว ส่วนหัวการยืนยันตัวตน หรือข้อมูล callback ของช่องทาง

## การฉายกลุ่มผู้รับ

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

ใช้การเดินแบบกว้างก่อนที่กำหนดผลได้แน่นอน:

1. เริ่มต้นคิวด้วยคีย์เซสชันต้นทาง
2. สำหรับแต่ละคีย์ที่นำออกจากคิว ให้อ่านแถวรีจิสทรี subagent ล่าสุดและเพิ่มขอบความเป็นเจ้าของที่แตกต่างกันทั้งสองลงในคิวตามลำดับคงที่: `controllerSessionKey` แล้วตามด้วย `requesterSessionKey`
3. เมื่อมีแถวรีจิสทรีที่ใช้ได้ อย่าติดตามสายสืบทอดของรายการเซสชันเพิ่มเติม ซึ่งอาจล้าสมัยหลังการบังคับทิศทาง มิฉะนั้นให้เพิ่มขอบสำรองปัจจุบันเพียงขอบเดียว `parentSessionKey ?? spawnedBy` ลงในคิว
4. ปรับให้อยู่ในรูปแบบมาตรฐานและขจัดรายการซ้ำขณะเพิ่มลงในคิว เพื่อให้พาธแรกที่สั้นที่สุดชนะ
5. หยุดเมื่อครบ 64 คีย์ที่ไม่ซ้ำกัน ขีดจำกัดขนาดกลุ่มผู้รับนี้ยังจำกัดความลึกของการเดินด้วย

แหล่งรีจิสทรีคือ `src/agents/subagent-registry-read.ts` ฟิลด์ความเป็นเจ้าของกำหนดไว้ใน `src/agents/subagent-registry.types.ts` ส่วนฟิลด์สำรองของเซสชันกำหนดไว้ใน `src/config/sessions/types.ts`

โปรเจกชันที่ร้องขอและโปรเจกชันปลายทางใช้กลุ่มผู้รับที่บันทึกไว้ชุดเดียวกัน แม้ว่าความเป็นเจ้าของโฟกัส/ตัวควบคุมจะเปลี่ยนไปขณะการอนุมัติรอดำเนินการ วิธีนี้รับประกันการล้างสถานะปลายทางสำหรับสตรีมเซสชันของกลุ่มผู้รับทุกสตรีมที่ได้รับโปรเจกชันคำขอ การตัดสินผลจะกำหนดเป้าหมายไปยัง ID การอนุมัติต้นทางเสมอ เซสชันของกลุ่มผู้รับจะไม่ได้รับสถานะการอนุมัติที่โคลนมา การล้างข้อความช่องทางที่ส่งต่อยังคงเป็นขั้นตอนติดตามตัวระบุตำแหน่งการส่งมอบแยกต่างหากด้านล่าง

อย่าเขียนข้อความทรานสคริปต์ แทรกพรอมต์ระบบ เริ่มเทิร์นของเจ้าของ หรือปล่อย `sessions.changed` เพียงเพราะมีการอนุมัติ

## การบรรจบกันของพื้นผิวที่ส่งมอบ

ตัวจัดการการอนุมัติแบบเนทีฟเก็บรายการข้อความที่ส่งมอบไว้นานพอที่จะเปลี่ยนหรือเลิกใช้ตัวควบคุมที่ใช้งานอยู่แล้ว ปัจจุบันข้อความการอนุมัติทั่วไปที่ส่งต่อจะทิ้ง `MessageReceipt` ดังนั้นการตัดสินใจบนพื้นผิวอื่นอาจทำให้ตัวควบคุมเก่ายังคงดูเหมือนรอดำเนินการ ขั้นตอนติดตามแยกต่างหากจะปิดช่องว่างนี้ด้วยตารางลูก `operator_approval_deliveries` ในฐานข้อมูลสถานะที่ใช้ร่วมกัน

แต่ละแถวเก็บ ID การอนุมัติ ID การส่งมอบที่ไม่ซ้ำกัน ช่องทาง/บัญชี/เส้นทางที่ตรงกันทุกประการ ตัวระบุตำแหน่งข้อความส่วนตัวของช่องทางที่มีขอบเขตจำกัดและผ่านการตรวจสอบ JSON แล้ว เวลาประทับการส่งมอบ และสถานะการทำให้เป็นปลายทาง โดยจะไม่เก็บข้อมูล callback โทเค็นการตัดสินใจ หรือคำขออนุมัติดิบ ช่องทางเป็นเจ้าของการเข้ารหัสตัวระบุตำแหน่งและการแก้ไขข้อความ ส่วน Core เป็นเจ้าของสถานะมาตรฐาน การเลือกเป้าหมาย นโยบายการลองใหม่ และข้อความปลายทางสำรอง

การลงทะเบียนการส่งมอบและการตัดสินผลปลายทางแข่งขันกันได้อย่างปลอดภัย:

1. หลังจากการส่งที่รอดำเนินการส่งคืนใบรับ ให้แทรกตัวระบุตำแหน่งการส่งมอบและอ่านสถานะการอนุมัติพาเรนต์ในธุรกรรมเดียว
2. หากพาเรนต์อยู่ในสถานะปลายทางแล้ว ให้กำหนดเวลาทำให้เป็นปลายทางทันทีแทนที่จะปล่อยให้การส่งมอบล่าช้ายังคงรอดำเนินการ
3. การเปลี่ยนผ่านสู่สถานะปลายทางที่คอมมิตแล้วทุกรายการจะกำหนดเวลาแถวการส่งมอบที่ยังไม่เสร็จสิ้นทั้งหมดแยกต่างหาก การบรอดแคสต์ที่ทิ้งได้ไม่ใช่ตัวกระตุ้น
4. ตัวทำให้เป็นปลายทางของช่องทางรายงาน `replaced`, `retired` หรือ `unsupported` โดย Replaced จะระงับข้อความปลายทางซ้ำ Retired จะส่งข้อความติดตามปลายทางที่มีอยู่ ส่วนการไม่รองรับหรือความล้มเหลวจะใช้ทางสำรองโดยไม่ย้อนกลับ CAS การอนุมัติ
5. เมื่อเริ่มต้นระบบ ให้ลองดำเนินการอนุมัติปลายทางที่มีการส่งมอบไม่เสร็จสิ้นอีกครั้ง ทำให้การล้างสถานะทนต่อการรีสตาร์ต Gateway

วงจรชีวิตของการขนส่งนี้เป็นฮุกอะแดปเตอร์การส่งมอบแบบเลือกใช้ ไม่ใช่ตัวเรนเดอร์หรือการดำเนินการข้อความที่โมเดลมองเห็น ปัจจุบันข้อความ QQ แบบ C2C/กลุ่มไม่มี API สำหรับแก้ไข ลบ หรือล้างแป้นพิมพ์ อะแดปเตอร์ดังกล่าวจึงยังไม่รองรับและแสดงความจริงมาตรฐานได้เฉพาะหลังการคลิกในภายหลัง จนกว่าการขนส่งจะมี API สำหรับแก้ไข

## ความหมายของการรีสตาร์ต การหมดเวลา และเส้นทาง

การคงอยู่ใน SQLite ไม่ได้หมายความว่าจะดำเนินการต่อได้ การผูกคำสั่ง/เครื่องมือยังคงอยู่ในหน่วยความจำ เพราะอาจมีข้อเท็จจริงของรันไทม์ที่ละเอียดอ่อนด้านความปลอดภัยและไม่ใช่สัญญางานที่ดำเนินการต่อได้

เมื่อ Gateway เริ่มต้น:

- สร้าง epoch รันไทม์ใหม่;
- เปลี่ยนแถวที่รอดำเนินการจาก epoch เก่าไปเป็น `cancelled` แบบอะตอมมิก โดยใช้เหตุผล `gateway-restart`;
- เก็บแถวไว้เพื่อให้ URL อธิบายสิ่งที่เกิดขึ้น;
- ห้ามดำเนินการอนุมัติในภายหลังกับการผูกรันไทม์ที่หายไป

ตัวจับเวลาเป็นการเพิ่มประสิทธิภาพสำหรับการปลุก อำนาจของกำหนดเวลาถูกเก็บไว้ใน `expires_at_ms` การอ่าน การรอ และการตัดสินผลทั้งหมดจะดำเนินการกระทบยอดการหมดอายุ

พฤติกรรมเข้มงวดขั้นสุดท้าย:

- หมดเวลา -> `expired`, ปฏิเสธ;
- ไม่มีเส้นทาง -> `denied`, ปฏิเสธ;
- ยกเลิกการรัน -> `cancelled`, ปฏิเสธ;
- คำตัดสินที่เชื่อถือได้แต่ผิดรูปแบบ -> `denied`, ปฏิเสธ;
- เฉพาะการตัดสินใจอนุญาตอย่างชัดเจนที่ได้รับอนุญาต -> `allowed`

พฤติกรรม exec ที่เผยแพร่อยู่ในปัจจุบันยังคงขัดแย้งกับสัญญานี้:

- `src/agents/bash-tools.exec-host-shared.ts` อาจใช้ `askFallback`
- `docs/tools/exec-approvals.md` และ `docs/cli/approvals.md` บันทึกพื้นผิวดังกล่าวไว้

ขณะนี้การอนุมัติของ plugin จะปฏิเสธอย่างปลอดภัยเมื่อหมดเวลาและเมื่อคำตัดสินผิดรูปแบบ ฟิลด์รุ่นเก่า
`timeoutBehavior` ยังคงยอมรับได้แต่จะถูกละเว้น ขั้นตอนติดตามความหมายแบบเข้มงวด
ของ exec ต้องอัปเดตโค้ด ชนิด เอกสาร การทดสอบ และบันทึกการเปลี่ยนแปลงพร้อมกัน โดยมี
การตรวจสอบจากเจ้าของ/ฝ่ายความปลอดภัยอย่างชัดเจน `askFallback` อาจยังคงอธิบาย
การเลือกนโยบายก่อนเกตระหว่างการย้ายระบบ แต่ต้องไม่เปลี่ยนการหมดเวลาของระเบียน
ที่สร้างและรอดำเนินการอยู่ให้กลายเป็นการอนุมัติ

## แผนความเข้ากันได้

- โปรโตคอล Gateway แบบเพิ่มส่วนขยาย โดยไม่เพิ่มเวอร์ชันโปรโตคอล
- คงเมธอดและเหตุการณ์ exec/plugin ที่มีอยู่ไว้ที่ขอบเขตภายนอก
- คง ID ที่มีอยู่ รวมถึงคำนำหน้า `plugin:` แต่หยุดใช้คำนำหน้าเป็นข้อมูลชนิด
- คงพฤติกรรมคำสั่งข้อความ `/approve`
- คงฟิลด์ URL/Web App ของปุ่มรุ่นเก่าและการดำเนินการคำสั่งไว้เป็นอินพุตความเข้ากันได้ของ plugin SDK โดยเอาต์พุต Core ใหม่เป็นแบบมีชนิด
- ย้ายช่องทางแบบรวมทั้งหมดและผู้เรียกใช้ภายในทั้งหมดในการเปลี่ยนแปลงการดำเนินการแบบมีชนิดเดียวกัน
- เพิ่มรายการบันทึกการเปลี่ยนแปลงสำหรับ URL/หน้าใหม่และสำหรับการเปลี่ยนแปลงพฤติกรรมการหมดเวลาในภายหลัง
- อย่าเพิ่มการตั้งค่าโหมดการขอข้อมูล

## การทยอยเปิดใช้

### PR 1: วงจรชีวิตแบบถาวร

- บันทึกการออกแบบนี้
- สคีมา SQLite ที่ใช้ร่วมกัน การสร้าง Kysely สโตร์ และการตัดทิ้งหลัง 30 วัน
- บริการอนุมัติของ Gateway บริดจ์ตัวรอรันไทม์ และการจัดการรายการกำพร้าหลังรีสตาร์ต
- `approval.get/resolve` แบบรวมเป็นหนึ่ง
- อะแดปเตอร์เมธอด exec/plugin
- การทดสอบคำตอบแรกชนะ ความทำซ้ำได้โดยไม่เกิดผลเพิ่ม การหมดอายุ การอนุญาต และการใช้งาน
- ยังไม่มีการเปลี่ยนแปลงพฤติกรรมของ UI หรือช่องทาง

### PR 2: การดำเนินการแบบมีชนิดและ callback ของช่องทาง

- การดำเนินการอนุมัติ, URL และ Web App แบบระบุชนิด
- ตัวสร้างการนำเสนอหลักและการส่งออก SDK ของ Plugin
- การเข้ารหัสคอลแบ็กที่เป็นข้อมูลส่วนตัวของการขนส่ง พร้อมระบุชนิดเจ้าของอย่างชัดเจน
- การอ้างอิงคอลแบ็กขนาดคงที่แบบคงทนสำหรับ ID มาตรฐานที่เกินขีดจำกัดของการขนส่ง
- การย้ายช่องทางที่รวมมาให้เลิกอนุมานจากข้อความคำสั่งและ ID การอนุมัติ
- สถานะจริงของคำตอบแรกตามมาตรฐานบนพื้นผิวที่คลิก และการอัปเดตสถานะสิ้นสุดแบบเนทีฟที่ยังทำงานอยู่โดยพยายามให้ดีที่สุด ส่วนการทำให้ข้อความช่องทางเข้าสู่สถานะสิ้นสุดอย่างคงทนยังเป็นงานติดตามผล
- การทดสอบ SDK และช่องทางที่รวมมา

### PR 3: ดีปลิงก์ของ UI ควบคุม

- หน้าการอนุมัติแบบสแตนด์อโลนที่ผ่านการตรวจสอบสิทธิ์ และการกำหนดเส้นทางเริ่มต้นที่คำนึงถึงพาธฐาน
- การผูกกับ Gateway ที่ให้บริการโดยไม่แก้ไขการเลือกรีโมตที่ผู้ปฏิบัติงานบันทึกไว้
- เนมสเปซ HTTP สำหรับการอนุมัติที่แกนหลักเป็นเจ้าของ รวมถึง ID ที่มีลักษณะคล้ายแอสเซ็ต
- เพย์โหลด URL ที่ Gateway สร้าง และการสำรวจสถานะรอดำเนินการจนกว่าจะส่งมอบเหตุการณ์วงจรชีวิต
- หลักฐานสำหรับความกว้างระดับมือถือ การเชื่อมต่อใหม่ คำตอบที่แข่งขันกัน การโหลดใหม่ และพาธที่เมานต์

### PR 4: ไคลเอนต์แบบเนทีฟ

- พื้นผิวการตรวจสอบบน iOS และ Android ใช้ `approval.get/resolve` ที่รับรู้ชนิด ส่วน watchOS ถ่ายทอดพรอมต์และการตัดสินใจที่ปลอดภัยสำหรับผู้ตรวจสอบผ่าน iPhone ที่จับคู่
- Watch เสนอการตัดสินใจสำหรับ exec ที่สัญญารีเลย์แบบกะทัดรัดรองรับ ได้แก่ อนุญาตครั้งเดียวและปฏิเสธ
- สถานะจริงของคำตอบแรกตามมาตรฐานในสถานะสิ้นสุดจะแทนที่สถานะการตัดสินใจที่พยายามดำเนินการภายในเครื่อง
- การตอบรับการแก้ไขที่สูญหายหรือกำกวมจะระงับตัวควบคุมไว้จนกว่าจะอ่านสถานะมาตรฐานกลับมา
- อินสแตนซ์ Gateway v4 ที่เผยแพร่ก่อนหน้านี้ยังคงรองรับการตรวจสอบ exec ผ่านวิธีสำรองแบบเดิมที่มีขอบเขตแคบ ส่วนสถานะสิ้นสุดที่คงไว้ข้ามพื้นผิวต้องใช้เมธอดแบบรวม
- คำเตือนสำหรับผู้ตรวจสอบและบริบทเจ้าของยังคงมองเห็นได้บน iPhone, Watch และ Android
- หลักฐานจากยูนิตเนทีฟ บิลด์ และแพลตฟอร์ม

### PR 5: การเผยแพร่วงจรชีวิตไปยังบรรพบุรุษ

- การส่งสถานะรอดำเนินการ/สิ้นสุดของ `session.approval` จากสแนปช็อตผู้รับที่บันทึกอย่างคงทนใน PR 1
- การสมัครรับข้อมูลเฉพาะเซสชัน การเล่นซ้ำเมื่อเชื่อมต่อใหม่ และทูมบ์สโตนสถานะสิ้นสุด โดยไม่แก้ไขทรานสคริปต์หรือปลุกเอเจนต์
- คอลแบ็กวงจรชีวิตทำงานหลังการแทรก/CAS แบบคงทน และจะไม่กลายเป็นผู้มีอำนาจอนุมัติ
- หลักฐานสำหรับซับเอเจนต์แบบซ้อนและการเชื่อมต่อใหม่

### PR 6: พฤติกรรมปิดเมื่อเกิดความล้มเหลว

- ย้าย `node-invoke-plugin-policy.ts` และโบรกเกอร์ Plugin แบบฝังออกจากการมีอำนาจซ้ำซ้อน
- ความหมายเชิงความหมายที่เข้มงวดสำหรับการหมดเวลา ข้อมูลผิดรูป ไม่มีเส้นทาง การผูก และการใช้สิทธิ์อนุญาตครั้งเดียว
- เลิกใช้การตั้งค่าการหมดเวลาแบบผ่อนปรนที่เผยแพร่แล้ว โดยไม่ปฏิบัติตามการตั้งค่าเหล่านั้นหลังจากคำขออยู่ในสถานะรอดำเนินการ
- หลักฐานสำหรับการแข่งขันหลายพื้นผิวและการฉีดความล้มเหลว

### งานติดตามผล: การล้างข้อความรีโมตอย่างคงทน

- บันทึกตัวระบุตำแหน่งการส่งต่ออย่างคงทน และทำให้ทุกข้อความช่องทางที่ส่งแล้วเข้าสู่สถานะสิ้นสุดหลังการรีสตาร์ต
- แยกวงจรชีวิตการขนส่งนี้ออกจากอำนาจอนุมัติมาตรฐานและการดำเนินการนำเสนอแบบระบุชนิด

## การทดสอบ

ความครอบคลุมเฉพาะจุดที่จำเป็น:

- การเปิด SQLite ใหม่ยังคงการฉายสถานะรอดำเนินการและสถานะสิ้นสุด
- ตัวแก้ไขพร้อมกันสองตัวให้ผู้ชนะ CAS เพียงหนึ่งรายเท่านั้น
- การลองใหม่ด้วยการตัดสินใจเดิมสำเร็จแบบไอดิมโพเทนต์ ส่วนการลองใหม่ที่ขัดแย้งจะคืนค่าผู้ชนะที่บันทึกไว้
- การแก้ไขเมื่อถึงหรือหลังเส้นตายไม่สามารถอนุมัติได้
- `allow-once` ใช้ได้เพียงครั้งเดียวโดยไม่ลบสถานะการตรวจสอบสถานะสิ้นสุด
- การเริ่มต้นยกเลิกยุครันไทม์ที่เก่ากว่า
- การค้นหาและการแก้ไขที่ไม่ได้รับอนุญาตจะไม่เปิดเผยว่ามีเรกคอร์ดอยู่หรือไม่
- รายการอนุญาตผู้ตรวจสอบที่ระบุชัดเจนและพฤติกรรม `operator.approvals` ที่จับคู่ทั่วไป
- เมธอดแบบเดิมของ exec และ Plugin ใช้สโตร์เดียวกัน
- สคีมาคำขอ/รายการ/รับ/แก้ไขของ Gateway และเพย์โหลดเหตุการณ์แบบเพิ่มเติม
- การทำให้การดำเนินการแบบระบุชนิดเป็นมาตรฐาน การเรนเดอร์สำรอง การส่งออก SDK และสวิตช์ช่องทางที่รวมมา
- การเข้ารหัสคอลแบ็กของ Telegram มีข้อมูลส่วนตัวของการขนส่งและไม่มีการอนุมานสตริงคำสั่ง
- เจ้าของที่เป็นลูกโดยตรง เจ้าของตัวควบคุม/ผู้ร้องขอแบบแตกแขนง เจ้าของแบบซ้อน การมอบหมายใหม่ ทางเลือกสำรองของฟิลด์เซสชัน วงจร และขีดจำกัดขนาดผู้รับ
- อาร์เรย์ผู้รับที่ร้องขอและที่เข้าสู่สถานะสิ้นสุดเหมือนกันทุกประการ
- การฉายข้อมูลเจ้าของไม่ทำให้ทรานสคริปต์เปลี่ยนแปลงหรือปลุกเอเจนต์
- เส้นทาง UI ควบคุมทำงานที่ `/` และพาธฐานที่กำหนดค่าไว้ การรีเฟรชแสดงสถานะจริงที่รอดำเนินการหรือสิ้นสุด
- คำตอบจาก UI ควบคุมและ Telegram พร้อมกันแสดงผู้ชนะหนึ่งราย และแสดง "แก้ไขที่อื่นแล้ว" สำหรับฝ่ายที่แพ้
- ตัวระบุการอนุมัติแบบเนทีฟและตัวระบุเจ้าของ Gateway รักษาไบต์ UTF-8 ไว้อย่างถูกต้องทุกประการตลอดการกำหนดเส้นทางและการกระทบยอด
- การเจรจาตระกูล RPC แบบเนทีฟจะปักหมุดตระกูลมาตรฐานหรือแบบเดิมหนึ่งตระกูลต่อเส้นทาง Gateway ที่รับเข้า และไม่ลดระดับอย่างเงียบ ๆ หลังเริ่มใช้งาน
- การตอบรับการแก้ไขแบบเนทีฟที่สูญหายจะระงับการดำเนินการจนกว่าจะอ่านสถานะมาตรฐานกลับมา การอ่านกลับที่ล้มเหลวต้องไม่สร้างผู้ชนะขึ้นเองหรือยืนยันการรีเฟรชของ Watch
- ยอมรับความสัมพันธ์ของคำขอสแนปช็อต Watch เฉพาะสำหรับเจ้าของ Gateway ที่จับคู่ตรงกันทุกประการและการอ่านสถานะมาตรฐานกลับบน iPhone ที่เสร็จสมบูรณ์แล้ว
- หลักฐานเส้นทางผู้ใช้ผ่าน Testbox/Crabbox รวมถึงหน้าการอนุมัติความกว้างระดับมือถือ การล้างการดำเนินการของ Telegram และการเดินทางไปกลับหนึ่งรอบสำหรับสถานะรอดำเนินการ/การแก้ไข/ผู้แพ้ที่ตอบช้า ครอบคลุม Android, iPhone และ Watch

## ความสามารถในการสังเกตการณ์

ส่งออกบันทึกการเปลี่ยนสถานะแบบมีโครงสร้างและไม่มีเนื้อหา โดยมี ID การอนุมัติ ชนิด คีย์เซสชันต้นทาง สถานะ เหตุผล และเวลาแฝง ห้ามบันทึกตัวอย่างแสดงผลหรือการผูกแบบดิบ

ติดตาม:

- จำนวนที่ร้องขอแยกตามชนิด
- จำนวนสถานะสิ้นสุดแยกตามชนิด/สถานะ/เหตุผล
- เกจสถานะรอดำเนินการ
- เวลาแฝงตั้งแต่คำขอจนถึงสถานะสิ้นสุด
- ผลลัพธ์การแข่งขันในการแก้ไข: ผู้ชนะ การลองใหม่แบบไอดิมโพเทนต์ ความขัดแย้ง หมดอายุ
- จำนวนเส้นทางการส่งและการปฏิเสธเนื่องจากไม่มีเส้นทาง
- การยกเลิกรายการกำพร้าเมื่อเริ่มต้น
- ขนาดผู้รับ

การเปลี่ยนสถานะที่คอมมิตแล้วถือว่าสำเร็จ แม้ว่าการส่งเหตุการณ์ภายหลังจะล้มเหลว ผู้สมัครรับวงจรชีวิตกู้คืนผ่านการเล่นซ้ำใน PR 5 และการค้นหาแบบมาตรฐาน การทำให้ข้อความช่องทางเข้าสู่สถานะสิ้นสุดอย่างคงทนยังคงเป็นงานติดตามผลแยกต่างหากข้างต้น

## การตัดสินใจที่ยังเปิดอยู่

1. **ต้นทาง UI ควบคุมที่เข้าถึงได้จากภายนอก** ทุกสแนปช็อตมี `urlPath` แบบสัมพัทธ์ที่เสถียร อาจประกาศ URL แบบสัมบูรณ์ได้เฉพาะจากตำแหน่ง Tailscale Serve/Funnel ที่แคชไว้หลังจากเปิดเผย Gateway สำเร็จแล้วเท่านั้น โดย `allowedOrigins`, ส่วนหัว Host ของคำขอ, `gateway.remote.url` และตัวเลือก loopback/LAN ที่ใช้แสดงผลเท่านั้นไม่ใช่ต้นทางมาตรฐาน Telegram สามารถใช้ตัวห่อ Mini App ที่ผ่านการตรวจสอบสิทธิ์เพื่อคงพาธการอนุมัติไว้ตลอดการบูตสแตรป พร็อกซีย้อนกลับตามอำเภอใจยังคงใช้ได้เฉพาะแบบสัมพัทธ์ จนกว่าจะมีสัญญา URL สาธารณะที่ระบุชัดเจนและผ่านการตรวจสอบแยกต่างหาก ห้ามให้ช่องทางคาดเดาต้นทาง
2. **จุดเปลี่ยนความเข้ากันได้ของการหมดเวลาแบบเข้มงวดสำหรับ exec** ขณะนี้การหมดเวลาการอนุมัติของ Plugin จะปิดเมื่อเกิดความล้มเหลว และเลิกใช้ `timeoutBehavior` แล้ว สัญญา `askFallback` ที่เผยแพร่แล้วและยังเหลืออยู่ต้องผ่านการตรวจสอบจากเจ้าของ/ฝ่ายความปลอดภัยอย่างชัดเจน มีบันทึกการเปลี่ยนแปลง เอกสาร และการตัดสินใจเรื่องการย้าย/เลิกใช้ ก่อนที่จะหยุดอนุญาตการดำเนินการหลังคำขอที่รอดำเนินการหมดเวลา
3. **โหมดฝังตัวที่ไม่มี Gateway** คำแนะนำ: ให้ใช้เฉพาะภายในเครื่องในช่วงแรก จากนั้นทำให้เป็นไคลเอนต์ของบริการมาตรฐานเมื่อมี Gateway อย่าโฆษณาดีปลิงก์ที่ไม่มีเซิร์ฟเวอร์ใดแก้ไขได้
