การที่ agent สองตัวอยู่เครื่องเดียวกันไม่ได้ทำให้เชื่อมกันได้เอง ต้องมีโปรโตคอลที่ตกลงกัน มีการยืนยันตัวตน และมีหลักฐานว่าข้อความไปถึงและได้คำตอบกลับ บทความนี้เล่าการเชื่อม Hermes Agent กับ Muse Code ด้วยโปรโตคอล A2A ตั้งแต่ข้อจำกัดที่ทำให้ต้องเขียน adapter จนถึงผล QA โดยแยกให้ชัดว่าอะไรทดสอบแล้วและอะไรยังไม่ได้ทดสอบ

สรุปสำหรับผู้บริหาร

  • Muse Code รุ่นที่ทดสอบไม่มี A2A server จึงใช้ adapter แปลงคำขอเป็นการเรียก muse exec แบบจำกัดสิทธิ์
  • คำตอบจาก headless exec ยืนยันการเรียกผ่าน adapter ไม่ใช่การรับทราบหรืออนุมัติของ Muse session ที่เปิดอยู่
  • Adapter ใช้ token แยกตามผู้เรียก จำกัดอัตราคำขอ และบันทึก audit เฉพาะข้อมูลกำกับ
  • QA แยกการทดสอบจำลองกับโมเดลจริง และให้ agent อีกฝั่งรีวิวโดยบันทึกข้อจำกัดและผลที่ผ่านเมื่อรันซ้ำ
  • ทดสอบเฉพาะเครื่องเดียวบน localhost ยังไม่ทดสอบข้ามเครื่องหรือรีบูตจริง และยังไม่ควรเปิดออกนอกเครื่องโดยไม่ตรวจเพิ่ม

เป้าหมายและโปรโตคอลที่เลือก

เราต้องการให้ agent สองตัวส่งงานและคำตอบหากันได้ และต้องรองรับการเชื่อมข้ามเครื่องในอนาคต จึงเลือก A2A (Agent2Agent protocol) ซึ่งเป็นโปรโตคอลเปิดสำหรับให้ agent คุยกัน ในงานนี้เราใช้รูปแบบ JSON-RPC และให้แต่ละ agent เผยแพร่ Agent Card อธิบายตัวเองไว้ที่เส้นทางมาตรฐาน

ฝั่ง Hermes มี A2A plugin ที่ทั้งเรียก agent อื่นและรับคำขอเข้ามาได้ ตามเอกสารของ plugin หากไม่ตั้ง token ตัวรับจะผูกกับ localhost เท่านั้น เราจึงเริ่มทดสอบบนเครื่องเดียวและไม่เปิดออกนอกเครื่อง

ข้อจำกัดที่เจอ: ทำไมต่อตรงไม่ได้

ฝั่ง Muse เรามีหลักฐานจากการลองคำสั่งจริง ไม่ได้เดาจากชื่อ

ช่องทางสิ่งที่พบใช้เป็นช่อง A2A ได้หรือไม่
muse session-messageส่งและอ่านข้อความไม่ได้ ระบบตอบ external_agent_ingress_closedไม่ได้
muse serveเป็น session host แบบ MSP ผ่าน stdio ไม่ใช่ HTTPไม่ได้
muse execรันหนึ่งคำสั่งแบบไม่มีหน้าจอ รับ prompt และส่งผลเป็น JSONLใช้เป็นฐานของตัวเชื่อมได้

เราไม่ได้ลองหาทางเลี่ยงช่องที่ระบบปิดไว้ เราสรุปว่า Muse เวอร์ชันที่ใช้ไม่มี A2A server และเลือกสร้างตัวเชื่อมแทน ตัวเชื่อมแปลคำขอ A2A เป็นการเรียก muse exec ทีละครั้ง

เรายังเห็นอีกทางคือระบบประสานงานของเครื่องมือจัดการ terminal ที่ใช้อยู่ คำสั่งช่วยเหลือของมันระบุว่าส่งข้อความและสั่งงานข้ามเครื่องได้ แต่เราไม่ได้ลองใช้ และมันไม่ใช่โปรโตคอล A2A จึงไม่ตอบเป้าหมายเรื่องคุยกับ agent ทั่วไป

การออกแบบ adapter

ตัวเชื่อมเขียนด้วย Python มาตรฐานเท่านั้น ไม่มีไลบรารีเพิ่ม มีหน้าที่สามอย่างคือ เผยแพร่ Agent Card ตรวจ token ของผู้เรียก และเรียก Muse แบบจำกัดสิทธิ์

Hermes ส่งคำขอ A2A ไปยัง adapter ซึ่งตรวจ token เรียก Muse exec แบบจำกัดสิทธิ์ และบันทึก audit เฉพาะ metadata
Architecture: adapter เป็นจุดควบคุมระหว่างผู้เรียกกับ subprocess ของ Muse; เส้นสีเขียวคือทางหลัก เส้นประคือคำตอบกลับ ภาพแสดงขอบเขตของ adapter ไม่ใช่การเชื่อมข้ามเครื่อง

ในภาพ Peer tokens แทนข้อมูล token ที่โหลดไว้ตอนเริ่มบริการ ไม่ใช่การอ่านไฟล์ใหม่ทุกคำขอ ส่วน Muse exec คือการรันแบบ headless ครั้งใหม่ ไม่ใช่ session ที่ผู้ใช้กำลังสนทนาอยู่ จึงไม่ควรนับคำตอบจาก subprocess ว่า session เดิมได้รับทราบหรืออนุมัติงานแล้ว

ลำดับคำขอ A2A: Hermes ส่งคำขอผ่าน adapter ไป Muse exec แบบ headless แล้วรับ Task result กลับ ขณะที่ Muse session ที่กำลังสนทนาอยู่ไม่ได้รับข้อความ
Sequence: Task result ยืนยันเพียงว่าการเรียกผ่าน adapter จบลงและส่งผลกลับผู้เรียก ไม่ใช่หลักฐานว่า Muse session ที่ผู้ใช้กำลังสนทนาอยู่ได้รับข้อความ อ่านข้อความ หรือให้ความเห็นชอบ

ภาพนี้แสดงเฉพาะทางสำเร็จ: adapter ตรวจและรับคำขอ เริ่ม headless exec รับข้อความตอบกลับ แล้วจึงส่ง Task result กลับ Hermes กล่องเส้นประด้านขวาเป็น session ที่กำลังสนทนาอยู่ ไม่มี message หรือ activation bar เชื่อมเข้าหา จึงไม่ควรใช้คำตอบจาก headless exec เป็นหลักฐานแทน verdict หรือข้อตกลงของ session นั้น บันทึกที่ session เจ้าของงานเขียนใน CHANGES.log จึงเป็นหลักฐานที่ตรวจทานได้สำหรับเรื่องสำคัญ

แนวคิดความปลอดภัยที่ใช้มีดังนี้

  • ผูกกับ localhost และปฏิเสธการ bind ออกนอกเครื่อง เว้นแต่สั่งเปิดโดยตั้งใจ
  • ทุกคำขอต้องมี bearer token ที่แยกตามผู้เรียก ไฟล์ token ต้องมีสิทธิ์อ่านเฉพาะเจ้าของ
  • Muse ทำงานในโฟลเดอร์ชั่วคราวที่ปิด shell การเขียนไฟล์ และการใช้เว็บ แล้วลบโฟลเดอร์ทิ้งหลังตอบ
  • บันทึก audit เฉพาะข้อมูลกำกับ เช่น ผู้เรียก ขนาด และค่าแฮช ไม่เก็บเนื้อข้อความหรือ token
  • รับงานจาก Muse ได้ครั้งละหนึ่งงาน ไม่มีคิวรอ

ข้อสุดท้ายเป็นการแลกกัน การไม่มีคิวทำให้ระบบเรียบง่ายและไม่สะสมงานค้าง แต่ผู้เรียกต้องรับมือกับคำตอบ "ไม่ว่าง" เอง

สถานะของคำขอและ Task

คำขอผ่านการตรวจเบื้องต้นและได้ช่องรันจึงเข้าสู่ WORKING แล้วจบ COMPLETED หรือ FAILED; คำขอที่ไม่ผ่านหรือระบบไม่ว่างถูกปฏิเสธก่อนเริ่มรัน
State machine แบบย่อ: เส้นปกติแสดงทางผ่าน เส้นประแสดงการปฏิเสธหรือความล้มเหลว กรอบสองชั้นคือจุดจบของคำขอแต่ละเส้นทาง

Received, Accepted และ Rejected ในภาพเป็นชื่อขั้นตอนเพื่ออธิบายวงจรคำขอ ไม่ใช่ค่า status ของ A2A Task ที่ adapter ส่งจริง ค่า Task ที่ใช้มีเพียง TASK_STATE_WORKING, TASK_STATE_COMPLETED และ TASK_STATE_FAILED โดยสร้าง Task หลังตรวจข้อความแล้ว คำขอที่ถูกปฏิเสธก่อนหน้านั้นไม่มี Task ให้ติดตาม

ตัวอย่างการปฏิเสธคือ token ไม่ถูกต้อง (401), ส่งถี่เกินกำหนด (429), body หรือ JSON ไม่ถูกต้อง (400), body ใหญ่เกินกำหนด (413) และมีงานรันอยู่แล้ว (503) ข้อผิดพลาดระดับ JSON-RPC บางชนิดตอบ HTTP 200 พร้อม error แทน ดังนั้นผู้เรียกต้องตรวจทั้ง HTTP status, JSON-RPC result และ Task state ไม่ใช่ดู HTTP 200 อย่างเดียว

เมื่อ Muse รันสำเร็จ Task จะมีข้อความและ artifact; เมื่อเริ่มไม่ได้ หมดเวลา หรือไม่ส่ง terminal event ที่สำเร็จ Task จะเป็น Failed การหมดเวลาไม่ได้หมายถึงแค่หยุดรอคำตอบ แต่ runner จะยุติ process group ด้วย ทั้ง Completed และ Failed ที่จบผ่านเส้นทางนี้มี audit metadata ส่วนคำขอที่ถูกปฏิเสธไม่จำเป็นต้องมี audit หนึ่งแถวต่อครั้ง เพราะการยืนยันตัวตนที่ล้มเหลวใช้การบันทึกแบบจำกัดต่อช่วงเวลา

Process: ใครทำอะไรในแต่ละขั้น

เจ็ดขั้นในสาม lane: ผู้เรียกส่งคำขอ adapter ตรวจและรับงาน จัด prompt ให้ Muse รัน แล้วจัดเก็บและส่งผลกลับผู้เรียก
Process ของทางสำเร็จ: Peer ส่งและรับผล, Adapter ควบคุมคำขอและจัดข้อความ, Muse exec สร้างคำตอบ; บนจอแคบเลื่อนแผนภาพแนวนอนได้

ผู้เรียกส่ง JSON-RPC พร้อม bearer token จากนั้น adapter ตรวจการยืนยันตัวตน อัตราคำขอ ขนาด body และรูปแบบ JSON-RPC ก่อนรับงานเข้าช่องรัน สำหรับ SendMessage ยังต้องตรวจว่ามีข้อความที่ใช้ได้และไม่เกินขนาดที่กำหนด การตรวจข้อความนี้เกิดหลังได้ช่องรันแต่ก่อนสร้าง Task และเรียกโมเดล จึงไม่ปรากฏเป็นกล่องแยกในภาพ

เมื่อรับงานได้ adapter ประกอบประวัติเป็น JSON ที่กำหนด role เองและสร้าง prompt file สิทธิ์จำกัดในโฟลเดอร์ชั่วคราว แล้วเรียก muse exec โดยปิด shell การเขียนไฟล์และการใช้เว็บ เมื่อสำเร็จจึงกรองข้อมูลลับที่รู้จัก เก็บบริบทในหน่วยความจำ บันทึก Task และ audit แล้วส่งผลกลับ การกรองนี้ไม่ใช่หลักประกันว่าจะตรวจพบข้อมูลลับทุกชนิด

ภาพ Process ย่อรายละเอียดเพื่อให้เห็นผู้รับผิดชอบและการส่งต่อ ขณะที่ State machine แสดงทางปฏิเสธและทางล้มเหลวเพิ่มเติม ทั้งสองภาพไม่มีขั้นรอคิว เพราะ adapter ปฏิเสธงานซ้อนทันที

ปัญหาที่เจอระหว่างทดสอบจริง

การทดสอบกับโมเดลจริงเผยปัญหาที่ชุดทดสอบจำลองไม่เห็น

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

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

คำตอบของ Hermes มีส่วนอธิบายการคิดนำหน้า การตรวจว่าคำตอบต้องตรงกับข้อความที่ขอแบบตัวต่อตัวจึงไม่เสถียรเสมอไป ควรใช้การเทียบที่ยืดหยุ่นกว่าสำหรับคำตอบของ Hermes

QA แบบสองฝั่ง

เราไม่ให้ผู้เขียนโค้ดรับรองงานตัวเอง การทดสอบแบ่งเป็นสองส่วนที่ทำโดย agent ต่างกัน

ฝั่งผู้สร้างทดสอบเป็นชั้นๆ ตั้งแต่ตรวจโค้ด ชุดทดสอบอัตโนมัติ ทดสอบโปรโตคอลและการยืนยันตัวตนกับ adapter ที่รันอยู่ ทดสอบกับ Muse จริง ทดสอบความทนทาน และทดสอบทิศทางกลับ จากนั้นให้ Muse ทำ QA ของตัวเองโดยไม่เชื่อผลของฝั่งผู้สร้าง

ชั้นทดสอบตัวอย่างสิ่งที่ตรวจผล
ชุดทดสอบอัตโนมัติใช้ Muse จำลอง ไม่เรียกโมเดลผ่านทุกรอบที่รัน ไม่พบผลที่ไม่นิ่ง
โปรโตคอลและ authtoken ผิด JSON เสีย ข้อความใหญ่เกิน ส่วนประกอบที่ไม่ใช่ข้อความผ่านทั้งหมด
Muse จริงข้อความตรงตัว ภาษาไทย บริบทต่อเนื่อง การสั่งให้เปิดเผย token การสั่ง shellผ่านทั้งหมด
ความทนทานส่งถี่เกินกำหนด เชื่อมต่อค้าง หมดเวลา เริ่มระบบด้วยค่าที่ผิดผ่านทั้งหมด
ทิศทางกลับMuse เรียก Hermes ด้วยข้อความภาษาไทยและข้อความขนาดใหญ่ผ่าน

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

สิ่งที่รีวิวพบและวิธีแก้

Muse รีวิวโค้ดอย่างอิสระและพบสี่ประเด็นแรก (ระดับปานกลางสองข้อ ระดับต่ำสองข้อ)

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

ข้อสุดท้ายควรอ่านอย่างระวัง การแยกบทบาทช่วยลดการปลอมผู้พูด แต่ ไม่ใช่การป้องกัน prompt injection ได้สมบูรณ์

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

รันเป็นบริการ

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

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

ข้อจำกัดที่ต้องบอกผู้อ่าน

  • ทดสอบบนเครื่องเดียวและเฉพาะ localhost ยังไม่ได้ทดสอบข้ามเครื่อง การเข้ารหัสการเชื่อมต่อ หรือการใช้งานระยะยาว
  • ฝั่ง Hermes ที่รับคำขอบน localhost ไม่ได้ตั้งการยืนยันตัวตน ซึ่งยอมรับได้เฉพาะเมื่อรับในเครื่องเท่านั้น และต้องตั้งก่อนเปิดออกนอกเครื่อง
  • ผู้ใช้บัญชีเดียวกันบนเครื่องยังอ่านไฟล์ชั่วคราวได้ และการปิด shell ไม่ได้เป็นการแยกสภาพแวดล้อมแบบเต็มรูปแบบ
  • ถ้าหยุดตัวเชื่อมกะทันหัน โฟลเดอร์ชั่วคราวอาจค้าง
  • ไม่รองรับ streaming การยกเลิกงาน และการแจ้งเตือนแบบ push
  • ทุกคำขอใช้โควตาโมเดลของ Muse
  • ทั้งหมดนี้เป็นการทดสอบการทำงานและความปลอดภัยเบื้องต้นของงานทดลอง ไม่ใช่การตรวจสอบความปลอดภัยโดยผู้เชี่ยวชาญภายนอก

สรุปที่นำไปใช้ต่อได้

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