สถานะของข้อมูล: รายการในหน้านี้ยืนยันว่าเอกสารถูกบันทึกใน Registry เท่านั้น ไม่ได้ยืนยันว่า API ทุกเส้นทางเปิดใช้งานบน production แล้ว โปรดตรวจ environment และสถานะ deploy ก่อนเรียก API การเรียกใช้ความสามารถที่มีผลต่อข้อมูลต้องผ่านสิทธิ์และขอบเขตที่ระบุในสัญญา
เอกสารรุ่น 1.1 · เส้นทางการเชื่อมต่อรุ่น 2.3 · JSON machine-readable
สถาปัตยกรรมและเลเยอร์หลัก
โครงสร้างเลเยอร์ ทิศทางการพึ่งพา ลำดับการทำงานของคำขอ และ Registry ที่เป็นแหล่งความจริงเดียวของระบบ
เครื่องยนต์ธุรกิจและการคำนวณ
สัญญาของเครื่องยนต์ราคา ภาษี ต้นทุน และเส้นทาง dispatch ที่แปลง command เป็นผลลัพธ์ธุรกิจจริง
สเปกการเชื่อมต่อและโครงสร้างข้อมูล
Endpoint จริงทั้งหมด พร้อม header, body schema, ตัวอย่าง JSON, รหัสข้อผิดพลาด และขั้นตอนทดสอบ
การเชื่อมต่อ LINE, Bot และ Mini App
Webhook ของ LINE, MCP Server สำหรับ AI agents, และวงจรชีวิตของ Connector ที่ผูกกับ Provider Registry
การจัดการคีย์ ระบบสิทธิ์ และเกณฑ์ความปลอดภัย
ตัวแปรสภาพแวดล้อม ระดับสิทธิ์ของบัญชีและ token ขอบเขตข้อมูล และเกณฑ์ deploy-gate ที่บังคับใช้ก่อนทุก release
system
ภาพรวมสถาปัตยกรรมระบบ
Architecture Overview
DNA OS แบ่งเป็น 8 เลเยอร์ที่พึ่งพาได้ทางเดียว (UI → API → Engine → Core → Services) โดยมี Registry เป็นแหล่งความจริงกลางที่ทุกเลเยอร์อ่าน
เลเยอร์หลักและความรับผิดชอบ
ทุกโมดูลต้องประกาศเลเยอร์ของตนใน registry/module-registry.json และถูกตรวจซ้ำโดย architecture book ในทุกการคอมไพล์
| เลเยอร์ | พาธ | ความรับผิดชอบ | Registry ที่อ้างอิง |
|---|
| UI / Presentation | app/, components/, presenter/ | หน้าเว็บ, shell, การแสดงผล และการเรียก API ผ่าน route handler | portals.json, architecture.json |
| API / Gateway | app/api/, gateway/ | รับคำขอ, ตรวจสิทธิ์, แปลง payload, เรียก engine ผ่าน dispatcher | command-registry.json, permissions.json |
| Business Engines | engines/ | คำนวณราคา ภาษี ต้นทุน ระยะทาง เอกสาร และตรวจ business rule | command-registry.json, business-coverage.json |
| Cognitive | cognitive/ | Intent → Plan → Execute → Reflect สำหรับงานที่ต้องใช้เหตุผล | intent-dictionary.json, decisions.json |
| Core | core/ | Config loader, execution engine, workspace composer, authority context | CONSTITUTION.json, constraints.json |
| Services | services/ | ขอบเขต I/O: Supabase, Realtime, AI bridge, token store | services.json, infrastructure.json |
| Plugins & Drivers | plugins/ | ไดรเวอร์ LINE, Chrome, GitHub, macOS และ platform runtime | drivers.json, PROVIDER_DIRECTORY.json |
| Registry | registry/ | แหล่งความจริงเดียวของ command, module, capability, schema และสิทธิ์ | architecture-book.json, dependency-graph.json |
ทิศทางการพึ่งพา (Dependency Direction)
กราฟการพึ่งพาถูกบังคับด้วย registry/dependency-graph.json และตรวจใน pre-commit ทุกครั้ง
- UI เรียกได้เฉพาะ route handler ของตัวเอง ไม่ import engine โดยตรง
- Route handler เรียก engine ผ่าน engines/validation/dispatcher.ts เท่านั้น เพื่อให้ registry gate และ Rule 72 ทำงานทุกเส้นทาง
- Engine อ่านค่าตั้งต้นจาก core/config-loader (ตาราง app_configs) ไม่มีค่าคงที่ซ่อนอยู่ในโค้ด
- Presenter สรุปผลที่คำนวณเสร็จแล้วเท่านั้น และไม่ import engines
- Services เป็นชั้นเดียวที่แตะ Supabase โดยตรง
- Registry เป็นอ่านอย่างเดียว (read-only) สำหรับ runtime ทุกชั้น
app/ (UI)
└─ app/api/ (route handler)
└─ engines/validation/dispatcher.ts ── registry/command-registry.json
└─ engines/* ── core/config-loader ── app_configs
└─ services/supabase/* ── PostgreSQL + RLS
└─ presenter/* (สรุปผล)
registry/* ← อ่านโดยทุกชั้น ห้ามเขียนจาก runtime
สัญญาข้อมูลกลาง (Core Contracts)
โครงสร้างข้อมูลที่ทุกเลเยอร์ใช้ร่วมกัน ประกาศไว้ที่ registry/schemas.json
| Contract | ฟิลด์บังคับ | ฟิลด์เสริม | ใช้ที่ไหน |
|---|
| Command | version, command | args | POST /api/v1/query, dispatcher |
| Event | id, type, source, timestamp | data, traceId | event bus, business_events |
| DriverResponse | success, command, timestamp | data, error | plugins/drivers/* |
| AgentCall | driver, command | args | cognitive/executor |
| DocumentData | type, items | company, customer, events, payment, debt | engines/documents/* |
{
"version": 1,
"command": "debt.list",
"args": {}
}
system
ลำดับการทำงานของคำขอ
Request Lifecycle
ทุกคำขอเดินผ่าน 7 ขั้นตอนเดียวกัน ตั้งแต่ auth guard จนถึง presenter ทำให้ตรวจสอบย้อนกลับได้ด้วย request_id เดียว
เจ็ดขั้นตอนมาตรฐาน
- 1. Auth guard — requireAuth / requireAuthPage ตรวจ session และระดับสิทธิ์ (ADMIN_TIER)
- 2. Parse & validate — route handler ตรวจ JSON, header และ query parameter
- 3. Registry gate — dispatcher ตรวจว่าคำสั่งมีจริงใน registry/command-registry.json
- 4. Policy gate — Rule 72 บังคับให้ write command ต้องยืนยันจากมนุษย์ผ่าน /admin UI
- 5. Data scope — resolveTokenDataScope จำกัดขอบเขตข้อมูลตามบัญชีผู้ถือ token
- 6. Execute — engine คำนวณโดยอ่านค่าตั้งต้นจาก app_configs ผ่าน core/config-loader
- 7. Respond — presenter จัดรูปแบบผลลัพธ์พร้อม request_id และ capability context
ตัวอย่างการเดินทางของคำขอ
POST /api/v1/query
header X-DNA-Token: <token>
body { "command": "debt.list", "args": {} }
↓
services/supabase/api-tokens.ts validateApiToken() → token record
core/auth/session.ts profiles.role ∈ admin_tier → ผ่าน
engines/authority/session-scope resolveTokenDataScope() → company scope
engines/validation/dispatcher.ts registry gate + Rule 72 → allow (read)
cognitive/debts query invoice_debts → rows
app/api/v1/query/route.ts JSON + request_id → 200
รหัสสถานะที่ระบบใช้
| Status | ความหมาย | สิ่งที่ client ควรทำ |
|---|
| 200 | คำสั่งสำเร็จและคืนข้อมูล | อ่านฟิลด์ data |
| 400 | body ไม่ใช่ JSON หรือไม่ได้ระบุ command | แก้ payload ตาม schema |
| 401 | ไม่มี token หรือ token ไม่ถูกต้อง/ถูก revoke | สร้าง token ใหม่ที่ /admin/api-tokens |
| 403 | token ไม่มีสิทธิ์ หรือคำสั่งต้องยืนยันจากมนุษย์ | ใช้ /admin UI สำหรับ write command |
| 503 | เซิร์ฟเวอร์ยังไม่พร้อมใช้งาน token store | ตรวจการตั้งค่า SUPABASE_SERVICE_ROLE_KEY |
หลักประกันที่ไม่เปลี่ยน
- ทุกคำสั่งภายนอกต้องผ่าน registry gate — คำสั่งที่ไม่มีใน registry จะไม่ถูก execute
- write command ไม่ถูกเปิดให้ external AI เรียกตรง (Rule 72)
- สิทธิ์ถูกประเมินใหม่ทุกคำขอจาก profiles.role ไม่เชื่อสถานะฝั่ง client
- ทุก response ประกอบด้วย request_id เพื่อใช้ trace ข้ามชั้น
- การอ่านค่าตั้งต้นที่หายไปจะหยุดด้วย [CONFIG MISSING] ไม่คืนค่าเดา
system
แผนที่ Registry กลาง
Central Registry Map
Registry แต่ละไฟล์มีเจ้าของชัดเจนและมี endpoint หรือ runtime เดียวที่อ่านมัน AI agent ใช้ตารางนี้เพื่อเลือกแหล่งข้อมูลก่อนเขียนโค้ดเชื่อมต่อ
ไฟล์ registry และผู้ใช้งานจริง
| Registry | ความจริงที่เก็บ | อ่านโดย | เข้าถึงผ่าน |
|---|
| command-registry.json | คำสั่งทั้งหมด: owner, layer, input, output, requires_confirmation | dispatcher, /api/v1/spec | GET /api/v1/spec, GET /api/v1/mcp/registry/commands |
| module-registry.json | ทุกโมดูล: layer, purpose, exports, access, status | architecture book, IDE, /api/v1/modules | GET /api/v1/modules |
| capability-registry.json | CapabilityId และ OwnerModule ที่ผูกกับ command | composer, /api/v1/capabilities | GET /api/v1/capabilities |
| schemas.json | สัญญาข้อมูล Command / Event / DriverResponse / AgentCall | ทุกเลเยอร์ | import ตรงจาก registry |
| permissions.json | role_tiers (admin_tier ฯลฯ) และสิทธิ์รายบทบาท | core/auth/session.ts | requireAuth(ADMIN_TIER) |
| drivers.json | ไดรเวอร์ฮาร์ดแวร์และแพลตฟอร์ม | plugins, /api/v1/drivers | GET /api/v1/drivers |
| commands/workflows.json | Workflow Registry ที่ executor อ่านจริง | core/executor/engine.ts | GET /api/v1/workflows |
| CONSTITUTION.json | Iron Rules และหลักการออกแบบ | /api/v1/constitution, governance | GET /api/v1/constitution |
| mcp-planned-manifest.json | สัญญา HTTP ของเครื่องมือ MCP และสถานะ integration | dna-os-mcp-server | GET /api/v1/mcp/manifest |
| PROVIDER_DIRECTORY.json | Provider ที่รองรับและสถานะการเชื่อมต่อ | connectors, admin console | /admin/connectors |
| connector-lifecycle.json | กฎวงจรชีวิตของ connector และ failure flow | connector center | /admin/connectors |
| architecture.json | รายชื่อเลเยอร์และโมดูลของแต่ละเลเยอร์ | architecture book, portal | registry/architecture-book.json |
รูปแบบการอ่าน registry สำหรับ agent
โหลดผ่าน HTTP เพื่อไม่ผูกกับโครงสร้างไฟล์ภายใน หรือ import ตรงเมื่อเขียนโค้ดใน repository นี้
// ภายใน repository
import commandRegistry from "@/registry/command-registry.json";
const readCommands = Object.entries(commandRegistry.commands)
.filter(([, c]) => !c.requires_confirmation)
.map(([name, c]) => ({ name, owner: c.owner, output: c.output }));
// ภายนอกรายการคำสั่งทั้งหมด (tool schema พร้อมใช้กับ LLM)
const spec = await fetch("/api/v1/spec").then((r) => r.json());
กฎการใช้งาน Registry
- หนึ่งความจริง หนึ่งไฟล์ — ห้ามคัดลอกข้อมูล registry ไปเก็บซ้ำในโค้ดหรือเอกสาร
- เพิ่ม endpoint ใหม่ต้องเพิ่มใน spec นี้ไฟล์เดียว เพื่อให้ทั้งหน้าเว็บและ JSON index ตรงกัน
- ค่า config เชิงธุรกิจอยู่ในตาราง app_configs ไม่ใช่ค่าคงที่ในซอร์สโค้ด
- ทุกโมดูลใหม่ต้องลงทะเบียนใน module-registry.json ก่อน commit (pre-commit ตรวจอัตโนมัติ)
- Registry integrity gate ตรวจความสอดคล้อง Source ↔ Registry ↔ Git ในทุกคอมมิต
engines
เครื่องยนต์คำนวณราคา
Pricing Engine
engines/pricing.ts คำนวณต้นทุนสึกหรอ กำไร ค่าธรรมเนียมระบบ ค่าด่วน และ VAT โดยอ่านอัตราทั้งหมดจาก app_configs
คีย์ตั้งต้นใน app_configs
ทุกอัตราต้องมีอยู่ในฐานข้อมูลจริง หากคีย์ใดหายไป core/config-loader จะหยุดด้วย [CONFIG MISSING] แทนการคืนค่าเดา
| คีย์ | ชนิด | ความหมาย | ตัวอย่าง |
|---|
| pricing-markup-percent | number (%) | กำไรที่บวกเพิ่มจากต้นทุนรวมหลังสึกหรอ | 15 |
| system-fee-percent | number (%) | ค่าธรรมเนียมระบบ หักจากกำไร | 3 |
| urgent-fee-percent | number (%) | ค่าบริการงานด่วน ใช้เมื่อ is_urgent = true | 10 |
| wear-percent | number (%) | อัตราสึกหรอ คิดจากค่าน้ำมัน | 5 |
| vat-rate | number (fraction) | อัตรา VAT เก็บเป็นเศษส่วน | 0.07 |
สัญญาอินพุตและเอาต์พุต
// PricingInput
{
"total_cost": 18500,
"material_cost": 12000,
"fuel_cost": 6500,
"trips": 3,
"is_urgent": true
}
// PricingResult
{
"wear_cost": 325,
"selling_price_ex_vat": 22022.5,
"vat": 1541.58,
"selling_price": 23564.08,
"profit": 2536.13,
"system_fee": 564.75,
"urgent_fee": 1882.5,
"price_per_trip": 7854.69,
"audit": { "engine": "pricing", "input": {}, "rules": {}, "computed": {} }
}
ขั้นตอนตรวจสอบ
- ตรวจว่าคีย์ครบ: select key, value from app_configs where key in ('pricing-markup-percent','system-fee-percent','urgent-fee-percent','wear-percent','vat-rate');
- เรียก calcPricing ด้วยอินพุตตัวอย่าง แล้วเทียบผลกับสูตรด้านบนทีละบรรทัด
- ยืนยันว่า audit.rules.formula ในผลลัพธ์สะท้อนอัตราที่อ่านมาจริง
- รัน npx tsc --noEmit เพื่อยืนยันสัญญา PricingInput / PricingResult
engines
สัญญาของ Dispatcher
Dispatcher Contract
engines/validation/dispatcher.ts เป็นประตูเดียวที่แปลง command เป็นการเรียก engine โดยตรวจ registry และกฎความปลอดภัยก่อนทุกครั้ง
รายการ command ใน registry
ทุกคำสั่งประกาศ owner, layer, input, output และ requires_confirmation — ตัวอย่างจริงของ debt.list
| ฟิลด์ | ชนิด | ความหมาย |
|---|
| owner | string | โมดูลเจ้าของคำสั่ง ใช้ resolve capability context |
| layer | string | เลเยอร์ที่คำสั่งสังกัด |
| input | object | แผนที่ชื่อพารามิเตอร์ → ชนิด; เติม ? ท้ายชื่อเมื่อไม่บังคับ |
| output | string | ชนิดผลลัพธ์ที่ผู้เรียกได้รับ |
| requires_confirmation | boolean | true = write command ปิดสำหรับ external AI (Rule 72) |
{
"debt.list": {
"owner": "cognitive/debts",
"layer": "cognitive",
"input": {},
"output": "InvoiceDebt[]",
"description": "แสดงรายการหนี้ค้างชำระ (OPEN)",
"description_th": "แสดงรายการหนี้ที่ยังค้างชำระ (สถานะ OPEN)",
"requires_confirmation": false
}
}
โหมดการเรียกและผลลัพธ์
| โหมด | เส้นทาง | ผลลัพธ์ |
|---|
| Read ผ่าน HTTP | POST /api/v1/query (token) | dispatch(command, args, false) → data |
| Read ผ่าน UI | /admin UI (session) | dispatch(command, args, false) → presenter |
| Write ผ่าน UI | /admin UI + การยืนยันของมนุษย์ | dispatch(command, args, true) เมื่อได้รับอนุมัติ |
| Read ผ่าน MCP | POST /api/v1/mcp/business/dispatch | คำสั่ง read ที่อยู่ใน whitelist เท่านั้น |
// สำเร็จ
{ "ok": true, "command": "debt.list", "user": "owner@example.com", "data": [], "capability": { "owner_module": "cognitive/debts", "capability_ids": [] } }
// ถูกปฏิเสธเพราะต้องยืนยัน
{ "ok": false, "command": "debt.create", "error": "requires human confirmation", "requires_confirmation": true }
ขั้นตอนตรวจสอบ
- ยืนยันว่าคำสั่งที่ไม่มีใน registry ถูกปฏิเสธก่อนถึง engine
- ยืนยันว่า write command คืน requires_confirmation = true และ HTTP 403
- ตรวจว่า response มี capability.owner_module ตรงกับ owner ใน registry
- ทดสอบผ่าน /playground ด้วย token จริงเพื่อดูผลแบบครบวงจร
api
เรียกคำสั่งธุรกิจ
Runtime Query
POST /api/v1/query คือท่อหลักสำหรับ AI ภายนอกและ connector รับ command พร้อม args แล้วคืนข้อมูลที่ผ่านการตรวจสิทธิ์และขอบเขตข้อมูลแล้ว
สเปก endpoint
POST/api/v1/query
Execute read command ผ่าน dispatcher พร้อมตรวจ role ระดับ admin tier และจำกัดขอบเขตข้อมูลตามบัญชีผู้ถือ token
การยืนยันตัวตน: X-DNA-Token header หรือ ?token= (token ออกที่ /admin/api-tokens)
พารามิเตอร์
X-DNA-Token (header, string, ไม่บังคับ) — API token ของบัญชีที่อยู่ใน admin tier (ใช้แทน query param ได้) ตัวอย่าง: dna_xxxxxxxxtoken (query, string, ไม่บังคับ) — ทางเลือกสำหรับ client ที่ไม่สามารถตั้ง header ได้ ตัวอย่าง: dna_xxxxxxxxcommand (body, string, จำเป็น) — ชื่อคำสั่งใน registry รองรับรูปแบบ OpenAI double-underscore เช่น debt__list ตัวอย่าง: debt.listargs (body, object, ไม่บังคับ) — พารามิเตอร์ของคำสั่ง ตาม input ใน registry ตัวอย่าง: {}
ตัวอย่างคำขอ
curl -X POST https://<host>/api/v1/query \
-H "Content-Type: application/json" \
-H "X-DNA-Token: $DNA_OS_API_TOKEN" \
-d '{"command":"debt.list","args":{}}'
ตัวอย่างผลตอบกลับ
{
"ok": true,
"command": "debt.list",
"user": "owner@example.com",
"data": [],
"capability": { "owner_module": "cognitive/debts", "capability_ids": [] }
}
หลักฐานและเงื่อนไขตรวจสอบ
- เรียกโดยไม่ใส่ token → 401 และ error ระบุให้สร้าง token ที่ /admin/api-tokens
- เรียกด้วย token ที่ถูก revoke → 401
- เรียก write command เช่น debt.create → 403 พร้อม requires_confirmation = true
- เรียก read command → 200 พร้อมฟิลด์ data และ capability
- เรียกด้วย command ที่ไม่มีใน registry → 400 พร้อม violations
- ส่ง body ที่ไม่ใช่ JSON → 400
ไฟล์อ้างอิง: registry/command-registry.json · registry/permissions.json · registry/capability-registry.json
CORS และการเรียกจากเบราว์เซอร์
- OPTIONS /api/v1/query คืน 204 พร้อม Allow-Origin: * และอนุญาต header Content-Type, X-DNA-Token
- ฝั่ง client ต้องแนบ token ใน header ทุกคำขอ ไม่มี cookie-based flow สำหรับ endpoint นี้
- แนะนำให้เรียกจาก server-side หรือ proxy ของคุณเองเพื่อไม่ให้ token ปรากฏในเบราว์เซอร์
api
Endpoint ค้นหาโครงสร้างระบบ
Discovery Endpoints
ชุด endpoint แบบอ่านอย่างเดียวที่เปิดให้ทุก client ใช้ค้นหา command, module, capability, driver, workflow และกฎรัฐธรรมนูญของระบบ
ภาพรวม
ทุก endpoint ในหมวดนี้เป็น GET, ไม่ต้องใช้ token และมี Cache-Control 300 วินาที (constitution 3600 วินาที) เพื่อให้ agent โหลดโครงสร้างระบบซ้ำได้โดยไม่สร้างภาระกับฐานข้อมูล
ใช้ /api/v1/spec เป็นจุดเริ่มต้นเมื่อต้องต่อ LLM tool-use เพราะคืน tool schema ที่กรอง deprecated และ write command ออกแล้ว
| Endpoint | คืนอะไร | ใช้เมื่อ |
|---|
| GET /api/v1/spec | tool schema รูปแบบ OpenAI/Claude + _dna_command | สร้าง tool ให้ LLM เรียก DNA OS |
| GET /api/v1/modules | โมดูลทั้งหมดพร้อม layer, purpose, exports, access | หาเจ้าของฟีเจอร์หรือโมดูลที่ต้องเรียก |
| GET /api/v1/capabilities | existence.json + capability registry + composition plan | วางแผนประกอบ capability หลายตัว |
| GET /api/v1/drivers | ไดรเวอร์ฮาร์ดแวร์และแพลตฟอร์ม | ตรวจว่ามีไดรเวอร์สำหรับอุปกรณ์ที่ต้องการ |
| GET /api/v1/workflows | workflow registry ที่ executor อ่านจริง | สร้าง automation ตาม workflow ที่มีอยู่ |
| GET /api/v1/constitution | Iron Rules และหลักการออกแบบ | ตรวจข้อกำหนดก่อนออกแบบ connector |
| GET /api/v1/developer-portal | สเปกพอร์ทัลนี้ทั้งหมดในรูป JSON | ให้ agent อ่านสเปกแล้วเขียนโค้ดเชื่อมต่อ |
Tool specification
GET/api/v1/spec
คืนรายการ tool ทั้งหมดที่ external AI เรียกได้ พร้อม JSON Schema ของพารามิเตอร์และชื่อคำสั่งดั้งเดิมในฟิลด์ _dna_command
การยืนยันตัวตน: ไม่ต้องใช้ token (public)
ตัวอย่างผลตอบกลับ
{
"schema_version": "1.0",
"system": "DNA OS",
"query_endpoint": "/api/v1/query",
"auth": "Header: X-DNA-Token: <your-token>",
"tools": [
{
"type": "function",
"function": {
"name": "debt__list",
"description": "แสดงรายการหนี้ค้างชำระ (OPEN) | layer: cognitive | output: InvoiceDebt[]",
"parameters": { "type": "object", "properties": {}, "required": [] },
"_dna_command": "debt.list"
}
}
]
}
หลักฐานและเงื่อนไขตรวจสอบ
- curl -s /api/v1/spec | jq '.tools | length' → ต้องมากกว่า 0
- ยืนยันว่าไม่มี tool ใดมี _dna_command ที่เป็น write command
- นำ tool แรกไปเรียก POST /api/v1/query ด้วยชื่อใน _dna_command แล้วต้องได้ 200
ไฟล์อ้างอิง: registry/command-registry.json
GET/api/v1/capabilities
คืน existence.json และข้อมูล capability registry พร้อม composition plan เมื่อระบุ capability ที่ต้องการ
การยืนยันตัวตน: ไม่ต้องใช้ token (public)
พารามิเตอร์
resolve (query, string (comma-separated), ไม่บังคับ) — รายการ CapabilityId ที่ต้องการให้ composer ประกอบเป็นแผน ตัวอย่าง: cap.x,cap.yapplication_id (query, string, ไม่บังคับ) — ขอบเขตแอปพลิเคชันของ composition plan ตัวอย่าง: api.v1.capabilitiesworkspace_id (query, string, ไม่บังคับ) — ขอบเขต workspace ของ composition plan ตัวอย่าง: default
ตัวอย่างผลตอบกลับ
{
"schema_version": "1.0",
"capability_registry": {
"source": "registry/capability-registry.json",
"version": 1,
"checksum": "..."
},
"composition_plan": null
}
หลักฐานและเงื่อนไขตรวจสอบ
- เรียกโดยไม่ใส่ resolve → composition_plan เป็น null และได้ registry summary
- เรียกด้วย resolve=<id> → ได้ CompositionPlan ที่อ้างอิง capability จริง
- ยืนยันว่า checksum เปลี่ยนเมื่อ registry เปลี่ยน
ไฟล์อ้างอิง: registry/existence.json · registry/capability-registry.json
GET/api/v1/modules
คืนรายการโมดูลทั้งหมดพร้อม layer, owner, exports และสถานะ
การยืนยันตัวตน: ไม่ต้องใช้ token (public)
ตัวอย่างผลตอบกลับ
{
"schema_version": "1.0",
"source": "registry/module-registry.json",
"total": 1079,
"modules": [
{ "path": "engines/pricing.ts", "owner": "engines", "layer": "engines", "purpose_th": "คำนวณราคา", "exports": ["calcPricing"], "status": "active" }
]
}
หลักฐานและเงื่อนไขตรวจสอบ
- curl -s /api/v1/modules | jq '.total' → ตรงกับจำนวน entry ใน module-registry.json
- ตรวจว่า path ที่ต้องการค้นหาปรากฏในผลลัพธ์
ไฟล์อ้างอิง: registry/module-registry.json
api
การออกและจัดการ API Token
API Token Management
Token ถูกออกโดยผู้ดูแลระบบเท่านั้น ผ่าน endpoint ที่ต้องมี session ระดับ admin tier และผูกกับบัญชีผู้ใช้จริงในระบบ
สเปก endpoint
GET/api/admin/api-tokens
คืนรายการ token ของบัญชีที่ล็อกอินอยู่
การยืนยันตัวตน: Session ระดับ ADMIN_TIER
ตัวอย่างผลตอบกลับ
{
"tokens": [
{ "id": "...", "label": "contractor-runtime", "scopes": ["read"], "created_at": "2026-11-11T00:00:00Z", "revoked_at": null }
]
}
หลักฐานและเงื่อนไขตรวจสอบ
- เรียกด้วย session ผู้ใช้ทั่วไป → ถูกปฏิเสธด้วย auth guard
- เรียกด้วย session ผู้ดูแล → 200 และเห็นเฉพาะ token ของตน
ไฟล์อ้างอิง: registry/permissions.json · services/supabase/api-tokens.ts
POST/api/admin/api-tokens
ออก token ใหม่โดยเลือก scope จาก allow-list ของระบบ
การยืนยันตัวตน: Session ระดับ ADMIN_TIER
พารามิเตอร์
label (body, string, ไม่บังคับ) — ชื่อเรียกของ token เพื่อให้ผู้ดูแลแยกแยะ ตัวอย่าง: contractor-runtimescopes (body, string[], ไม่บังคับ) — ขอบเขตสิทธิ์: read, governance:read, governance:propose ตัวอย่าง: ["read"]
ตัวอย่างคำขอ
curl -X POST https://<host>/api/admin/api-tokens \
-H "Content-Type: application/json" \
--cookie "session=<admin-session>" \
-d '{"label":"contractor-runtime","scopes":["read"]}'
ตัวอย่างผลตอบกลับ
{
"token": {
"id": "...",
"label": "contractor-runtime",
"scopes": ["read"],
"token": "dna_xxxxxxxxxxxxxxxx"
}
}
หลักฐานและเงื่อนไขตรวจสอบ
- ส่ง scope ที่ไม่อยู่ใน allow-list → 400 พร้อมรายการ allowed
- ส่ง label ว่าง → ใช้ค่า default
- นำ token ที่ได้ไปเรียก POST /api/v1/query → 200
ไฟล์อ้างอิง: registry/permissions.json · services/supabase/api-tokens.ts
กฎของ scope
- client ไม่สามารถกำหนด scope เองได้ — มีเพียง route นี้ที่อ่านค่าดังกล่าวและต้องผ่าน session ผู้ดูแล
- token ที่ถูก revoke จะถูกปฏิเสธทันทีในคำขอถัดไป
- role ของบัญชีถูกตรวจใหม่ทุกคำขอ หากถูกถอดออกจาก admin tier token จะใช้ไม่ได้ทันที
| Scope | อนุญาต | หมายเหตุ |
|---|
| read | เรียก read command ผ่าน /api/v1/query | ขอบเขตข้อมูลยังถูกจำกัดตามบัญชีผู้ถือ token |
| governance:read | อ่านสถานะ governance และ proposal | ไม่สามารถอนุมัติ |
| governance:propose | ส่งข้อเสนอเข้าคิวให้มนุษย์อนุมัติ | การอนุมัติต้องทำผ่าน /admin UI |
integrations
LINE Webhook และ Mini App
LINE Messaging Integration
supabase/functions/line-webhook-v2 รับ event จาก LINE ตรวจลายเซ็น x-line-signature แล้วกระจายงานตามชนิด event ก่อนตอบ ACK ภายในเวลาที่ LINE กำหนด
สเปก webhook
POST$SUPABASE_URL/functions/v1/line-webhook-v2
รับ LINE webhook event ทุกชนิด (message, follow, unfollow, postback, location, image, file, audio, group) และส่งต่อไปยัง handler ที่รับผิดชอบ
การยืนยันตัวตน: ลายเซ็น x-line-signature (HMAC-SHA256 ของ channel secret) — ฟังก์ชันตั้ง verify_jwt = false
พารามิเตอร์
x-line-signature (header, string (base64), จำเป็น) — HMAC-SHA256 ของ raw body ด้วย channel secretdestination (body, string, จำเป็น) — LINE user/group id ของบัญชีที่รับ event ใช้เลือก credential ของร้านนั้นevents (body, array, จำเป็น) — รายการ event ตามสเปก LINE Messaging APIevents[].type (body, string, จำเป็น) — message | follow | unfollow | postback | join | leave ตัวอย่าง: messageevents[].message.type (body, string, ไม่บังคับ) — text | image | file | audio | location ตัวอย่าง: textevents[].replyToken (body, string, ไม่บังคับ) — token สำหรับตอบกลับภายในเวลาที่กำหนด
ตัวอย่างคำขอ
curl -X POST "$SUPABASE_URL/functions/v1/line-webhook-v2" \
-H "Content-Type: application/json" \
-H "x-line-signature: <base64-hmac>" \
-d '{
"destination": "Uxxxxxxxxxxxxxxxx",
"events": [{
"type": "message",
"replyToken": "xxxxxxxx",
"source": { "userId": "Uxxxxxxxxxxxxxxxx", "type": "user" },
"message": { "type": "text", "id": "1", "text": "ยอดค้างชำระ" }
}]
}'
ตัวอย่างผลตอบกลับ
{ "ok": true }
หลักฐานและเงื่อนไขตรวจสอบ
- ยืนยันว่า verify_jwt = false ใน supabase/config.toml สำหรับฟังก์ชันนี้ เพื่อให้ LINE เรียกได้โดยไม่มี Supabase JWT
- ตั้งค่า LINE_CHANNEL_SECRET และ LINE_CHANNEL_ACCESS_TOKEN ใน secrets ของโปรเจกต์
- ส่งคำขอที่ลายเซ็นไม่ถูกต้อง → 401 และไม่มีการประมวลผล event
- ส่งคำขอที่ลายเซ็นถูกต้อง → 200 {"ok":true} และ event ถูกบันทึกในตาราง line_events
- ทดสอบ event text ที่เป็นคำสั่งคนขับ เช่น eta 82-1234 → ต้องได้รับการตอบกลับผ่าน reply API
ไฟล์อ้างอิง: registry/communication/line.registry.json · supabase/functions/line-webhook-v2 · registry/PROVIDER_DIRECTORY.json
ลำดับการประมวลผล event
- ตรวจลายเซ็นจาก raw body ก่อน parse เพื่อป้องกัน payload ปลอม
- เลือก channel secret และ access token ตาม destination ผ่าน connect_external_account_credentials
- แปลง event เป็น handler เฉพาะทาง: follow, unfollow, text, location, driver, postback, image, file, audio, group
- ประมวลผล event พร้อมกันและส่งงานเบื้องหลังผ่าน EdgeRuntime.waitUntil() เพื่อตอบ 200 ทันที
- บันทึก event ลง line_events เพื่อให้ admin console และ analytics ตรวจย้อนหลังได้
การเชื่อมต่อ Mini App
- Mini App ที่ฝังใน LINE ใช้ LIFF token เพื่อระบุผู้ใช้ แล้วเรียกข้อมูลผ่าน endpoint ของ DNA OS ด้วย token ที่ระบบออกให้
- ห้ามฝัง channel access token หรือ service role key ฝั่ง client เด็ดขาด — ให้ proxy ผ่าน route handler ของคุณ
- ผลลัพธ์ที่แสดงใน Mini App ต้องมาจาก read command เดียวกับที่ /api/v1/query ใช้ เพื่อให้ตัวเลขตรงกับระบบหลัก
integrations
MCP Server สำหรับ AI Agent
MCP Server
dna-os-mcp-server เป็น protocol adapter ที่แปลง MCP tool call เป็น HTTP request ไปยัง endpoint ของ DNA OS โดยไม่ถือ business logic ของตัวเอง
สัญญาเครื่องมือ MCP
สัญญาทั้งหมดประกาศใน registry/mcp-planned-manifest.json ซึ่งเป็นไฟล์ที่ยึด HTTP contract ก่อนเครื่องมือถูกเลื่อนสถานะเป็น active
| MCP tool | HTTP endpoint | ลักษณะงาน | สถานะ |
|---|
| contractor.task.claim | POST /api/v1/mcp/contractor/task-claim | รับงานจากคิวและสร้าง JobContract | active |
| contractor.evidence.submit | POST /api/v1/mcp/contractor/evidence-submit | ส่งหลักฐานและตรวจ reference | active |
| contractor.status | GET /api/v1/mcp/contractor/status?ownerId= | อ่านสถานะงาน (read-only) | active |
| business.dispatch | POST /api/v1/mcp/business/dispatch | execute read command ที่อยู่ใน whitelist | active |
| architecture.system_summary | GET /api/v1/mcp/architecture/summary | สรุปสถาปัตยกรรมและกฎรัฐธรรมนูญ | active |
| architecture.get_dependency_graph | GET /api/v1/mcp/architecture/dependency-graph | กราฟการพึ่งพา | active |
| registry.list_modules | GET /api/v1/modules | รายการโมดูล | active |
| registry.list_commands | GET /api/v1/mcp/registry/commands | รายการคำสั่งดิบจาก registry | active |
| runtime.query | POST /api/v1/query | passthrough ไปยัง dispatcher | planned |
| governance.status | GET /api/v1/mcp/governance/status | สถานะ governance | active |
| governance.registry_proposals | GET /api/v1/mcp/governance/registry-proposals | ข้อเสนอที่รออนุมัติ | active |
| governance.deploy_approval_status | GET /api/v1/mcp/governance/deploy-approval-status | สถานะการอนุมัติ deploy | active |
| governance.session_proposals | GET /api/v1/mcp/governance/session-proposals | ข้อเสนอของ session | active |
| governance.impact | GET /api/v1/mcp/governance/impact/[evidenceEventId] | ผลกระทบของหลักฐานหนึ่งรายการ | active |
การตั้งค่าฝั่ง client
- Server นี้ไม่มี allow-list ของโมดูลภายใน — เข้าถึงสถานะ DNA OS ผ่าน HTTP ตาม manifest เท่านั้น
- เครื่องมือที่ต้องใช้ token ต้องได้รับ DNA_OS_API_TOKEN จากผู้ดูแลระบบ
- การยืนยัน write command ยังคงเกิดขึ้นที่ dispatcher ฝั่ง DNA OS ไม่ใช่ที่ MCP client
{
"mcpServers": {
"dna-os": {
"command": "node",
"args": ["dna-os-mcp-server/dist/index.js"],
"env": {
"DNA_OS_BASE_URL": "https://<host>",
"DNA_OS_API_TOKEN": "<token จาก /admin/api-tokens>"
}
}
}
}
integrations
Connector และ Provider Registry
Connectors & Providers
Connector ทุกตัวอ้างอิง Provider ที่ประกาศใน registry/PROVIDER_DIRECTORY.json และเดินตามวงจรชีวิตที่กำหนดใน registry/connector-lifecycle.json
วงจรชีวิตของ connector
- ประกาศ provider และ capability ที่รองรับใน PROVIDER_DIRECTORY.json ก่อนสร้าง connector
- ลงทะเบียน connector พร้อม credential ที่เก็บในที่ปลอดภัย ไม่เขียนค่าลงซอร์สโค้ด
- ทดสอบการเชื่อมต่อผ่านปุ่ม Test ของ /admin/connectors ซึ่งเรียก tester จริง
- เปิดใช้งานเมื่อทดสอบผ่าน และบันทึกเหตุการณ์วงจรชีวิตลงตาราง connectors
- เมื่อล้มเหลว ระบบทำตาม failure_flow ใน connector-lifecycle.json: error, auto_disable, log, notify_status
จุดเชื่อมต่อที่ผู้ใช้เห็น
| Surface | หน้าที่ | แหล่งข้อมูล |
|---|
| /admin/connectors | ศูนย์ควบคุม connector: Test / Enable / Remove และประวัติเหตุการณ์ | ตาราง connectors + WORKER_ADAPTER_REGISTRY + Integration Gateway |
| /admin/god-view | มุมมองรวมสถานะระบบและ override ที่ต้องมีสิทธิ์ | registry + runtime state |
| /admin/roles | จัดการบทบาทและระดับสิทธิ์ | registry/permissions.json + profiles |
| /admin/rules | ดูและรันกฎ governance | registry/GOVERNANCE_REGISTRY.json |
| /admin/line-customers | ผูกบัญชี LINE กับ partner และดูประวัติลูกค้า | line_events + partners |
การจัดเก็บ credential
- credential ของ provider เก็บในฐานข้อมูลหรือ secret store ของแพลตฟอร์ม ไม่เก็บใน registry ที่ commit ลง git
- เอกสารนี้แสดงเฉพาะชื่อตัวแปรและตำแหน่งที่ระบบอ่านค่า ไม่แสดงค่าใด ๆ
- ห้ามส่ง secret กลับใน response ของ API ทุกตัว
settings
ตัวแปรสภาพแวดล้อม
Environment Variables
รายการคีย์ที่ระบบอ่านจริง พร้อมขอบเขตการใช้งาน ค่าจริงเก็บใน .env.local ระหว่างพัฒนา และใน environment ของแพลตฟอร์มเมื่อขึ้น production
คีย์ที่ระบบใช้งาน
| ตัวแปร | ขอบเขต | ใช้ที่ไหน | หมายเหตุ |
|---|
| NEXT_PUBLIC_SUPABASE_URL | public | client และ server | URL ของโปรเจกต์ Supabase production |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | public | client | คีย์สาธารณะสำหรับอ่านตาม RLS |
| SUPABASE_SERVICE_ROLE_KEY | server only | token store, admin queries, deploy-gate | ห้ามส่งไป client เด็ดขาด |
| NEXT_PUBLIC_APP_URL | public | canonical URL, sitemap, structured data | ถ้าไม่ตั้ง ระบบจะไม่สร้าง hostname เอง |
| LINE_CHANNEL_SECRET | server only | line-webhook-v2 | ใช้ตรวจลายเซ็นของ event |
| LINE_CHANNEL_ACCESS_TOKEN | server only | line-webhook-v2 | ใช้ตอบกลับผ่าน Messaging API |
| EQUIPMENT_TELEMETRY_SECRET | server only | app/api/internal/equipment/telemetry | bearer secret สำหรับ ingestion อุปกรณ์ |
ข้อปฏิบัติ
- ค่าจริงอยู่ใน .env.local (ไม่ถูก commit) หรือ environment ของแพลตฟอร์มเท่านั้น
- ไฟล์เอกสารและ registry ระบุได้เฉพาะชื่อตัวแปร ไม่ระบุค่า
- เมื่อคีย์ service role หาย ระบบจะคืน 503 พร้อมข้อความบอกผู้ดูแล ไม่เดาค่าแทน
- ห้ามใช้ NEXT_PUBLIC_ นำหน้าคีย์ที่ให้สิทธิ์เขียนหรือข้าม RLS
- ตรวจความพร้อมทั้งชุดด้วย npm run verify-env ก่อน deploy
settings
ระบบสิทธิ์และขอบเขตข้อมูล
Access Control & Data Scope
สิทธิ์ถูกตัดสินจาก profiles.role ทุกคำขอ โดยเทียบกับ role_tiers ใน registry/permissions.json และขอบเขตข้อมูลถูกจำกัดตามบัญชีผู้ถือ token
ระดับสิทธิ์
| ระดับ | แหล่งประกาศ | ใช้กับ | ผลเมื่อไม่ผ่าน |
|---|
| ADMIN_TIER | registry/permissions.json → role_tiers.admin_tier | หน้า /admin, /api/admin/*, token ภายนอก | 401 หรือ 403 |
| Session ผู้ใช้ทั่วไป | core/auth/session.ts | พอร์ทัล user/partner | redirect ไปหน้า login |
| API token | services/supabase/api-tokens.ts | /api/v1/query | 401 เมื่อ revoke, 403 เมื่อ role หลุดจาก admin tier |
| Worker contract | app/api/admin/ide/_lib/workerContract.ts | /api/v1/mcp/contractor/* | ปฏิเสธงานที่ไม่อยู่ในสัญญา |
ขอบเขตข้อมูล
- resolveTokenDataScope แปลงโปรไฟล์ของผู้ถือ token เป็น DataScope ก่อน execute ทุกคำสั่ง
- runWithDataScope ครอบการเรียก dispatcher เพื่อให้ engine เห็นเฉพาะข้อมูลในขอบเขต
- RLS ในฐานข้อมูลเป็นชั้นป้องกันที่สอง ไม่ใช่ชั้นเดียว
- ตารางสำคัญเปิด RLS และกำหนด policy สำหรับ service role และ admin workspace แยกกัน
การจัดการคีย์
- คีย์ service role ใช้ได้เฉพาะฝั่ง server และไม่ถูก log
- ระบบ log เฉพาะขนาดและรูปแบบของ token ไม่บันทึกค่าหรือ prefix
- การหมุนคีย์ทำได้โดยอัปเดต environment แล้วรีสตาร์ท service โดยไม่ต้องแก้โค้ด
- Vault ของระบบรองรับการเขียน/อ่าน/ลบคีย์ผ่าน RPC ที่ต้องใช้ service client
settings
เกณฑ์การปล่อยระบบ (Deploy Gate)
Deploy Gate Criteria
ทุก release ต้องผ่าน deploy-gate ซึ่งตรวจสภาพแวดล้อม หลักฐาน Dev Loop สถานะสุขภาพ และความสอดคล้องของเลขเวอร์ชันก่อนอนุญาตให้ build
เกณฑ์ที่ต้องผ่าน
| เกณฑ์ | สิ่งที่ตรวจ | ผลเมื่อไม่ผ่าน |
|---|
| Environment | คีย์ที่จำเป็นครบและอ่านได้ | NOT READY พร้อมระบุคีย์ที่ขาด |
| Dev Loop evidence | อ่าน dev_loop_check_results ได้จริงและไม่มีผล fail | NOT READY พร้อมเหตุผลของ path ที่ fail |
| Registry proposals | ไม่มีข้อเสนอสถานะ pending | NOT READY พร้อมรายการที่รออนุมัติ |
| Release chain | package.json เป็น semver และ CURRENT_STATE อยู่ใน CHANGELOG | NOT READY เมื่อหลักฐานหายหรือเลขเวอร์ชันผิด |
| Health & governance | DNA_OS_HEALTH.json และ governance summary | NOT READY เมื่อต่ำกว่าเกณฑ์ |
วิธีรันและการอ่านผล
- เกณฑ์นี้ออกแบบแบบ fail-closed: หากอ่านหลักฐานไม่ได้ ระบบถือว่าไม่ผ่านและหยุดทันที
- คิวรีที่คืน error, โยน exception, คืน dataset ว่าง หรือคืน null จะถูกนับเป็นหลักฐานไม่ครบทั้งหมด
- ทุกครั้งที่ gate ผ่าน จะมีการบันทึกหลักฐานการตรวจไว้ใน registry/VALIDATION_RESULTS.json
- การอนุมัติ deploy ของมนุษย์ยังต้องทำผ่าน governance flow ก่อนปล่อยจริง
# ตรวจก่อน build (prebuild เรียกอัตโนมัติ)
npm run deploy-gate
# ผลที่ผ่าน
DNA OS Health Score: 100 / 100
PASSED: 8 / 8
FAILED: 0
READY
# ผลที่ไม่ผ่าน
Deploy Blocked — Dev Loop Pipeline
- Dev Loop evidence query failed: <reason>
ขั้นตอนยืนยันก่อนปล่อย
- รัน npm run verify-env และต้องได้ exit 0
- รัน npm run deploy-gate และต้องเห็น READY
- รัน npx tsc --noEmit และต้องไม่มีข้อผิดพลาด
- รัน npm run build และต้องคอมไพล์ทุกหน้าสำเร็จ
- ตรวจ smoke test เส้นทางหลัก: /, /products, /admin/docs และ /api/v1/developer-portal
เส้นทางเชื่อมต่อระบบ
LINE → GatewayEvent → canonical evidence log Inbound LINE Observe
Trigger — LINE ยิง event เข้ามา — LINE ส่ง POST /api/gateway/observe/line พร้อม destination + events[] โดยไม่มี session; route อ่าน body เท่านั้นเพื่อรู้ว่ามาจากบัญชีไหน ยังไม่แตะฐานข้อมูล
app/api/gateway/observe/line/route.ts · POST
Gate — เลือก secret ของบัญชีปลายทาง — destination ถูกใช้ resolve channel secret จาก public.connect_external_account_credentials; ถ้าไม่พบจึงถอยไปใช้ LINE_CHANNEL_SECRET ของบัญชีหลัก
engines/connect/accounts.ts · resolveExternalAccountCredential
ปฏิเสธเมื่อ: ไม่มี secret เลย → 503
Gate — ตรวจลายเซ็นก่อนอ่าน/เขียนใด ๆ — HMAC-SHA256 ของ raw body เข้ารหัส base64 แล้วเทียบ x-line-signature ด้วย timingSafeEqual — body ที่ยังไม่ผ่านไม่ถูกตีความต่อ
engines/integration-gateway/adapters/line.ts · verifyLineSignature
ปฏิเสธเมื่อ: ลายเซ็นผิด/หาย → 401 · JSON เสีย → 400
Normalize — LINE event → GatewayEvent — แต่ละ event แปลเป็น capability กลาง (message / document / location / identity signal / interaction) และ evidence ถูกจำกัดด้วย key whitelist 9 คีย์ — ข้อความลูกค้าและ reply token ไม่มีช่องเดินทางเข้า core
engines/integration-gateway/adapters/line.ts · normalizeLineWebhook / EVIDENCE_KEYS
ปฏิเสธเมื่อ: ชนิด event ที่ยังไม่รองรับ → นับ skipped ไม่แต่ง capability ขึ้นเอง
Gate + Bus — สัญญาและช่องทาง — publish() ตรวจ isGatewayEvent ก่อน แล้วตรวจ channelIsEnabled (observe/propose เท่านั้น) จากนั้น dispatchEvent('gateway.<capability>') และเขียน integration_gateway_events; ช่อง propose มีแถว proposal รอเจ้าของตัดสิน
engines/integration-gateway/event-bus/bus.ts · publish · policy/gate.ts
ปฏิเสธเมื่อ: สัญญาไม่ครบ/ช่องปิด → ok:false แล้ว route ตอบ 500
Ack — เฉพาะเมื่อเขียนสำเร็จ — ตอบ { ok, accepted, skipped } หลังทุก event ผ่าน bus เท่านั้น ไม่ ack ล่วงหน้า
app/api/gateway/observe/line/route.ts · NextResponse.json
KBank webhook → staging → Observe Bank Inbound Ingest
Trigger — ธนาคารยิง webhook — POST /api/connectors/webhook/kbank รับเฉพาะ body ที่ผ่าน gate; ระหว่างรอ sandbox ยังไม่มี secret จริง จึงยังไม่มีการรับ event จากธนาคารได้
app/api/connectors/webhook/kbank/route.ts
Gate — สถานะ connector — getConnectedConnector ต้องได้ connector ที่ status = 'connected' และ enabled = true เท่านั้น; connector ที่ปิดหรือ error หยุดรับทันทีโดยไม่ต้อง deploy
core/connector-loader.ts · getConnectedConnector
ปฏิเสธเมื่อ: 404 connector_not_found / connector_not_enabled
Gate — ขนาด body — ปฏิเสธก่อน buffer เมื่อ content-length หรือ byte จริงเกิน 64KB
app/api/connectors/webhook/[slug]/_lib/webhookGate.ts · MAX_WEBHOOK_BODY_BYTES
ปฏิเสธเมื่อ: 413 body_too_large
Gate — ลายเซ็น (fail-closed สองชั้น) — ถ้า connector ไม่ได้ตั้ง signature_header หรือ signing_secret ระบบปฏิเสธ (ไม่ใช่ข้ามการตรวจ); เมื่อตั้งแล้วต้องมี header และ HMAC ต้องตรง timingSafeEqual ก่อน parse body
app/api/connectors/webhook/[slug]/_lib/webhookGate.ts · evaluateWebhookRequest
ปฏิเสธเมื่อ: 503 signature_not_configured · 401 signature_missing / signature_invalid
Staging — เก็บ raw ก่อนแปล — เขียน kbank_webhook_events (raw_body + allow-listed headers, status 'received') ก่อน normalize เพื่อให้ reconcile/payout มีหลักฐานระดับไบต์ ไม่ใช่แค่ evidence ที่แปลงแล้ว
app/api/connectors/webhook/kbank/route.ts · PHASE 1 (kbank_webhook_events insert)
ปฏิเสธเมื่อ: เขียน staging ไม่ได้ → 500 ไม่ ack body ที่ผ่านลายเซ็นแล้ว
Normalize + Publish — normalizeKBankWebhook แปลงเป็น capability กลาง payment.* / payout.* / transfer.* แล้ว publish เข้า Observe Channel
engines/integration-gateway/adapters/kbank.ts · normalizeKBankWebhook
ปฏิเสธเมื่อ: ไม่มี event ที่แปลงได้ → 400 · publish ล้ม → 500
Ack — { received: true, published } — ตอบหลัง publish สำเร็จทุก event เท่านั้น; ไม่มี published ก็ไม่ตอบว่ารับแล้ว
app/api/connectors/webhook/kbank/route.ts · NextResponse.json
GatewayEvent → Connect → canonical business event Business Event Bridge
Trigger — transport ส่ง canonical command — POST /api/internal/connect/event รับเฉพาะคำสั่งที่ normalize แล้ว (ไม่มี payload ของ LINE ติดมา, ไม่มีการ branch ตาม provider)
app/api/internal/connect/event/route.ts
Gate — secret ของ bridge — header x-connect-internal-secret ต้องตรงกับ CONNECT_INTERNAL_SECRET
app/api/internal/connect/event/route.ts · process.env.CONNECT_INTERNAL_SECRET
ปฏิเสธเมื่อ: ไม่ตั้ง secret → 503 · ไม่ตรง → 401
Gate — คำสั่งปิด + actor — รับ 4 คำสั่งเท่านั้น: business_event.create / .transition / .enrich / .link_transaction และต้องมี actor ทุกครั้ง
app/api/internal/connect/event/route.ts · COMMANDS
ปฏิเสธเมื่อ: คำสั่งนอกชุด/ไม่มี actor → 400
Write — canonical event writer เท่านั้น — createBusinessEvent / transitionBusinessEvent / enrichBusinessEvent / linkBusinessEventTransaction เป็นเจ้าของ allow-list ของ field และชุดสถานะ; ผู้เรียกส่ง row shape เข้ามาไม่ได้
engines/canonical/event-write.ts
ปฏิเสธเมื่อ: 422 EVENT_UNKNOWN_STATE / EVENT_TRANSITION_REFUSED / EVENT_FIELD_NOT_ALLOWED
Audit — metadata ที่บอกที่มา — ทุกแถวที่สร้างพก written_by = canonical:event-write@<actor> และ source_provider ไว้ใน metadata เพื่อตรวจย้อนได้โดยไม่ต้องเดา
engines/canonical/event-write.ts · createBusinessEvent
สลิป → canonical payment (guards → RPC เดียว) Money In
Trigger — transport แจ้งว่าพบหลักฐานการจ่าย — POST /api/internal/connect/payment-confirm รับเฉพาะ receipt_document_id + actor; ไม่รับ amount/outstanding เพราะผู้เรียกใส่ตัวเลขเงินเองไม่ได้
app/api/internal/connect/payment-confirm/route.ts
Gate — secret ของ bridge — header x-payment-confirm-internal-secret เทียบ PAYMENT_CONFIRM_INTERNAL_SECRET
app/api/internal/connect/payment-confirm/route.ts · process.env.PAYMENT_CONFIRM_INTERNAL_SECRET
ปฏิเสธเมื่อ: ไม่ตั้ง → 503 · ไม่ตรง → 401 · body ผิดรูป → 400
Flow + guards — ตรวจก่อนแตะเงิน — confirmReceiptPayment → confirmPayment: receipt ต้องเป็น transfer_slip/payment_advice, ต้องยังไม่ถูกจัดสรร, Σ allocation ≤ ยอด receipt, invoice ต้องไม่ VOID บริษัทตรง และไม่เกินยอดคงค้าง
engines/canonical/receipt-confirm-flow.ts · engines/canonical/payment-write.ts
ปฏิเสธเมื่อ: throw เป็นรหัส PAYMENT_* (เช่น PAYMENT_OVERPAYMENT_NOT_ALLOWED, PAYMENT_ALREADY_ALLOCATED)
Commit — RPC เดียว ไม่มี fallback — confirm_payment_allocation เขียน allocations + ledger + invoice projection + receipt state + snapshot + audit ใน transaction เดียว; writer ไม่เขียนแถวเงินเองเลย และไม่มีเส้นสำรอง
engines/canonical/payment-write.ts · confirmPayment → db.rpc('confirm_payment_allocation')
ปฏิเสธเมื่อ: RPC หาย/ปฏิเสธ → fail closed (mapPaymentRpcError)
Ack — ผลจริง ไม่ใช่ผลที่หวัง — ตอบ 200 { already_reconciled, new_outstanding, debt_event_id, acknowledgement }; acknowledgement เป็นส่วนเสริมและล้มได้โดยไม่ทำให้การยืนยันเงินล้ม
app/api/internal/connect/payment-confirm/route.ts · acknowledgePayment
เงินออก → canonical expense / payable Money Out
Trigger — transport แจ้งค่าใช้จ่าย/คำขอจ่าย — POST /api/internal/connect/expense รับ 3 คำสั่ง: expense.record, payable.request, payable.transition
app/api/internal/connect/expense/route.ts · COMMANDS
Gate — secret ของ bridge — header x-expense-internal-secret เทียบ EXPENSE_INTERNAL_SECRET
app/api/internal/connect/expense/route.ts
ปฏิเสธเมื่อ: ไม่ตั้ง → 503 · ไม่ตรง → 401
Write — writer ตัดสิน ไม่ใช่ผู้เรียก — recordCompanyExpense ตรวจ source (line_chat / subscription_email_sync / admin_manual) และ amount > 0; requestPartnerPayable ตั้ง status 'pending' เอง; transitionPartnerPayable อัปเดตแบบกรองสถานะต้นทาง
engines/canonical/expense-write.ts
ปฏิเสธเมื่อ: 0 แถวถูกอัปเดต → PAYABLE_TRANSITION_INVALID · 422 EXPENSE_* / PAYABLE_*
Policy — เวลาเสร็จมาจากหลักฐาน — completed_at ถูก derive จาก bank_email_verified เท่านั้น — ผู้เรียกตั้งเวลาเสร็จเองไม่ได้
engines/canonical/expense-write.ts · transitionPartnerPayable
Audit — ทุกแถวเงินออกมีร่องรอย — recordCanonicalAudit เขียน expense.recorded / payable.requested / payable.transitioned ทุกครั้ง
engines/canonical/audit.ts · recordCanonicalAudit
VCC → engine hours → total_engine_hours Telemetry to Engine Hours
Trigger — กล่อง GPS/รีเลย์ยิง ping — POST /api/internal/equipment/telemetry รับ { imei, vccVoltage, recordedAt? } จากฮาร์ดแวร์โดยตรง — ไม่มี session เพราะกล่องไม่ได้ล็อกอิน และยังไม่มีผู้ให้บริการกล่อง/SMS gateway รายใดถูกเลือก จึงใช้ shape กลางนี้แทนการเดา wire format ของ vendor
app/api/internal/equipment/telemetry/route.ts
Gate — bearer secret ของฮาร์ดแวร์ — header x-equipment-telemetry-secret ต้องตรงกับ EQUIPMENT_TELEMETRY_SECRET; ถ้ายังไม่ตั้งค่า ระบบตอบ 500 (ไม่ใช่ 503 แบบ bridge ธุรกิจ) ตามโค้ดจริง
app/api/internal/equipment/telemetry/route.ts · process.env.EQUIPMENT_TELEMETRY_SECRET
ปฏิเสธเมื่อ: ไม่ตั้งค่า → 500 · ไม่ตรง → 401
Validate — IMEI ต้องเป็นเครื่องที่รู้จัก — imei และ vccVoltage ต้องมาและมีชนิดถูกต้อง; ค้น equipment_assets ด้วย imei ถ้าไม่พบคือปฏิเสธ ไม่สร้างเครื่องใหม่ให้เอง
app/api/internal/equipment/telemetry/route.ts · equipment_assets lookup by imei
ปฏิเสธเมื่อ: ข้อมูลไม่ครบ → 400 · ไม่พบ IMEI → 404
Decision — VCC ≥ 13.5V คือเครื่องติด — engine_running = vccVoltage >= 13.5 โดยค่าคงที่ ENGINE_RUNNING_THRESHOLD_VCC อยู่ที่เดียว ไม่กระจายไปตามผู้ให้บริการกล่อง
app/api/internal/equipment/telemetry/route.ts · ENGINE_RUNNING_THRESHOLD_VCC
Evidence — เก็บ ping ดิบก่อนสะสม — insert equipment_telemetry_pings (recorded_at, engine_running, vcc_voltage, raw_payload) — เก็บ payload ดิบไว้ตรวจย้อน
app/api/internal/equipment/telemetry/route.ts · equipment_telemetry_pings insert
ปฏิเสธเมื่อ: เขียน ping ไม่ได้ → 500
Accumulate — ชั่วโมงเกิดจาก ping คู่ — accumulateEngineHours อ่าน ping ก่อนหน้า (strictly before) และบวกเฉพาะเมื่อทั้ง ping ก่อนและ ping ใหม่เป็น engine_running; delta ถูก cap ที่ MAX_GAP_HOURS = 0.5 ชม. เพราะ ping ปกติมาทุก 10–30 วินาที — ช่องว่างที่ใหญ่กว่านั้นคือสัญญาณขาด ไม่ใช่ชั่วโมงเดินเครื่องที่มองไม่เห็น และค่า gap จริงถูกคืนเป็น capped_from_hours
engines/equipment/engine-hours.ts · computeEngineHoursDelta / accumulateEngineHours
ปฏิเสธเมื่อ: ping ย้อนหลัง/เวลาเสีย → delta 0 ไม่ลบชั่วโมง · accumulate ล้ม → 500 พร้อม telemetry_saved:true
Ack + สถานะเครื่อง — ถ้า engine_running เป็นจริง อัปเดต current_status = RUNNING แล้วตอบ { ok, engine_running, delta_hours, capped_from_hours, new_total_hours } — ตัวเลขที่ตอบคือค่าที่เขียนจริง ไม่ใช่ค่าที่คำนวณลอย
app/api/internal/equipment/telemetry/route.ts · NextResponse.json
ชั่วโมงเครื่อง → resource transaction (ราคาจาก config จริง) Machinery Hours
Trigger — telematics/อีเมลแจ้งชั่วโมงเดินเครื่อง — POST /api/internal/connect/resource-transaction พร้อม source_event_id และเวลาจริงจากต้นทาง
app/api/internal/connect/resource-transaction/route.ts
Gate — secret ของ bridge — header x-resource-transaction-internal-secret เทียบ RESOURCE_TRANSACTION_INTERNAL_SECRET
app/api/internal/connect/resource-transaction/route.ts
ปฏิเสธเมื่อ: ไม่ตั้ง → 503 · ไม่ตรง → 401
Validate — ต้องอ้างของจริง — job_id ต้องมีอยู่จริงใน jobs, duration_hours > 0, started_at ต้องไม่เกิน received_at และ evidence_ref ต้องมี
engines/canonical/resource-transaction-write.ts · recordResourceTransaction
ปฏิเสธเมื่อ: RESOURCE_TRANSACTION_JOB_NOT_FOUND / INVALID_TIME / INVALID_DURATION
Authority — ราคาอ่านจาก config — fuel-price และ machinery_rates (base_rate_per_hour, fuel_consumption_lph, markup_margin) อ่านจาก app_configs; ขาดหรือผิดรูปคือหยุด ไม่ถอยไปใช้เลขเดิมในโค้ด
engines/canonical/resource-transaction-write.ts · readConfigAuthorities
ปฏิเสธเมื่อ: CONFIG_AUTHORITY_MISSING (zero rows written)
Idempotency — event เดียวสร้างแถวเดียว — source_event_id เป็น unique; ซ้ำจะคืนแถวเดิมพร้อม duplicate: true และยังกัน race 23505 ได้
engines/canonical/resource-transaction-write.ts · idempotency check
ปฏิเสธเมื่อ: ไม่ throw ซ้ำ — คืน duplicate: true แทนการเขียนทับ
Compute + Write + Audit — unit_cost = base_rate_per_hour + fuel_consumption_lph × fuel_price; unit_price = unit_cost + markup; total = duration_hours × unit_cost แล้วเขียนพร้อม audit transaction.created
engines/canonical/resource-transaction-write.ts · recordResourceTransaction
ข้อเสนอผู้ขาย ≠ ราคากลาง Price Authority
Trigger — มีข้อความตอบราคาเข้ามา — ช่องทางใดก็ได้ส่งข้อความตอบกลับ; ข้อความนั้นเป็นหลักฐาน ไม่ใช่สิทธิ์แก้ราคา
engines/canonical/material-price-write.ts · recordSellerOffer
Evidence — บันทึกข้อเสนอพร้อม audit ใน transaction เดียว — record_seller_offer_atomic เขียนแถวคำขอ + audit พร้อมกัน และคืน price_changed: false เสมอ
engines/canonical/material-price-write.ts · recordSellerOfferAtomic
ปฏิเสธเมื่อ: PRICE_REQUEST_NOT_FOUND / PRICE_REQUEST_NOT_PENDING / PRICE_INVALID
Authority — ใครมีสิทธิ์เปลี่ยนราคากลาง — applyMaterialPrice ใช้ resolveAuthority(action 'price.write'): เจ้าของหรือ delegation ที่ระบุชัดเท่านั้น — ADMIN_TIER อย่างเดียวหรือชื่อในแชตไม่พอ
core/authority/resolve.ts · resolveAuthority
ปฏิเสธเมื่อ: PRICE_FORBIDDEN:<reason>
Atomic commit — ราคาและ audit ไปด้วยกัน — apply_material_price_atomic เขียนราคา + audit ใน transaction เดียว และไม่มีเส้น 'เขียนแล้วค่อยย้อน' เพราะ compensation รับประกันไม่ได้
engines/canonical/material-price-write.ts · applyMaterialPriceAtomic
ปฏิเสธเมื่อ: RPC ยังไม่ apply → PRICE_ATOMIC_RPC_MISSING (ไม่ลดชั้นเป็นเขียนครึ่งเดียว)
Agent เรียกของจริงได้แค่ประตูที่เปิด Agent Authority
Trigger — agent/MCP client เรียกคำสั่ง — คำขอเข้ามาที่ route ของ MCP/dispatch ตามชนิดงาน (อ่านข้อมูล / วิเคราะห์ / รับงาน / อนุมัติ)
app/api/v1/mcp/business/dispatch/route.ts
Gate — session แอดมิน — business.dispatch ใช้ requireAuth(ADMIN_TIER) เป็นประตูเดียวกับ route แอดมินอื่น
app/api/v1/mcp/business/dispatch/route.ts · requireAuth(ADMIN_TIER)
ปฏิเสธเมื่อ: ไม่มี session/ชั้นไม่ถึง → ปฏิเสธก่อนอ่านข้อมูล
Gate — whitelist คำสั่งอ่าน — รับเฉพาะคำสั่งอ่านที่อยู่ในรายการ; คำสั่งอื่นไม่ถูกส่งต่อให้ dispatcher
app/api/v1/mcp/business/dispatch/route.ts · COMMAND_NOT_PERMITTED
ปฏิเสธเมื่อ: 403 COMMAND_NOT_PERMITTED
Gate — Execution Channel — channelIsEnabled('execute') ยังเป็น false ระบบจึงรายงานความสามารถตรง ๆ ว่า NOT_IMPLEMENTED แทนการจำลองสำเร็จ
app/api/admin/workspace/ai-agents/route.ts · engines/integration-gateway/policy/gate.ts
ปฏิเสธเมื่อ: execute_capability.status = 'NOT_IMPLEMENTED'
Gate — เจ้าของต้องยืนยันเอง — การอนุมัติที่แตะ production ต้องแนบ owner_verification_artifact; session แอดมินอย่างเดียวไม่พอ
app/api/gateway/proposals/[id]/decide/route.ts
ปฏิเสธเมื่อ: 403 OWNER_VERIFICATION_REQUIRED
Handoff — ส่งสัญญางาน ไม่ใช่สิทธิ์ — contractor ได้ JobContract + WorkerRequest พร้อม permittedReadScope/permittedWriteScope และ approvalPolicy; runtime ยัง PAUSED ตาม TASK-082
app/api/v1/mcp/contractor/task-claim/route.ts · registry/TASK_QUEUE.json
ปฏิเสธเมื่อ: intake ปิด → ไม่จ่ายงานใหม่