การ์ดที่ฉันสร้างให้ Claude Code ระเบิดใส่ข้อความคอมมิตของตัวมันเอง — ลากเส้นแบ่งระหว่างผู้ตรวจกับสิ่งที่ถูกตรวจใหม่ ด้วยตัวเนื้อหาจริง
บทนำ
ฉันเคยเขียนถึงการสร้าง “กลไกที่หยุดผู้ช่วย AI เขียนโค้ดก่อนที่มันจะลงมือ” ไว้แล้ว
ส่วนขยายของเรื่องนั้นนำไปสู่การสร้างการ์ดอีกตัว เพราะ การพิมพ์หมายเลขพอร์ตการเชื่อมต่อจากความจำล้วน ๆ และการทำเครื่องหมายคำพูดของเชลล์พัง เกิดขึ้นซ้ำ ๆ ภายในช่วงงานเดียวกัน การ์ดเข้าไปตัดการทำงานแล้วตรวจเนื้อหาของคำสั่งที่กำลังจะรันด้วยเครื่องจักร ระหว่างสร้างมัน การ์ดเจอข้อบกพร่องที่ไม่หวือหวาแต่กวนใจถึงสองครั้ง: มันลากตัวเองเข้าไปอยู่ในขอบเขตการตรวจของตัวมันเอง บทความนี้บันทึกข้อบกพร่องเหล่านั้นและเทสต์เชิงปฏิปักษ์ที่เขียนขึ้นเพื่อแก้มัน
การ์ดตรวจอะไร
สิ่งที่ฉันสร้างเข้าไปตัดทันทีก่อนคำสั่งทำงาน แล้วตรวจค่าสองชนิด
- A. หมายเลขพอร์ตที่ไม่มีที่มา ถ้ารูปแบบ
host:portปรากฏในคำสั่ง ให้ใช้git grepตรวจว่าหมายเลขพอร์ตนั้นเขียนอยู่ที่ไหนสักแห่งในซอร์สของเวิร์กสเปซจริงหรือไม่ ถ้าไม่เจอแม้แต่ครั้งเดียว ให้หยุดการทำงานด้วยความสงสัยว่ามันถูกพิมพ์จากความจำ แต่ถ้าตัวการตรวจเองล้มเหลว (การค้นหาใช้ไม่ได้) อย่าหยุด ให้ปล่อยผ่าน (fail-open) การถือว่า “ตรวจไม่ได้” เป็นการละเมิด จะทำให้ตัวงานเองหยุดในสภาพแวดล้อมที่เครื่องมือใช้ไม่ได้ - B. โค้ดยาวที่ฝังอยู่ในเชลล์โดยตรง หยุดเมื่อมีโค้ดเกิน 200 อักขระฝังอยู่ในเครื่องหมายคำพูดของเชลล์โดยตรง ในรูปแบบอย่าง
python -c "..."อุบัติเหตุจากเครื่องหมายคำพูดซ้อนและการขยายตัวแปรถูกป้องกันได้เกือบทั้งหมด เพียงแค่หยุดเรื่องนี้แล้วเขียนลงไฟล์แทน
อุบัติเหตุจริงที่จุดชนวนเรื่องนี้เป็นแบบนี้ ฉันเขียนหมายเลขพอร์ตการเชื่อมต่อจากความจำ และการตรวจการเชื่อมต่อทุกครั้งถูกปฏิเสธ ค่าที่ถูกต้องคือ หมายเลขที่ต่างกันอยู่อักขระเดียว และมันเขียนอยู่เป็นค่าคงที่ในอีกไฟล์หนึ่ง มันอยู่ในที่ที่การมองแวบเดียวหนึ่งวินาทีก็จบเรื่อง — และ ฉันเขียนมันจากความจำแทนที่จะไปดูซอร์ส
การ์ดตอบสนองต่อตัวเอง (1) — ข้อความอธิบายภายใน heredoc
ตอนคอมมิตการเปลี่ยนแปลงของการ์ดตัวนี้ ฉันเขียนคำอธิบายของการ์ดเองลงในเนื้อหาข้อความคอมมิตว่า “หยุดเมื่อมีโค้ดเกิน 200 อักขระฝังอยู่ในรูปแบบอย่าง python -c” ข้อความคอมมิตยาวหลายบรรทัด ฉันจึงเขียนมันในเชลล์ด้วยไวยากรณ์ที่ “ส่งบรรทัดต่อไปนี้ผ่านไปในฐานะข้อมูล” (heredoc)
ตรงนั้นการ์ดติดธงใส่ตัวเอง เนื้อหาของ heredoc ไม่ใช่โค้ดที่เชลล์จะรัน แต่คือข้อมูลธรรมดาที่ส่งให้คำสั่ง (ในที่นี้คือสตริงข้อความคอมมิต) — แต่การ์ดกลับรวมเนื้อหานั้นเข้าไปในการตรวจโค้ดของมัน ผลคือข้อความที่แค่อธิบายการเปลี่ยนแปลงของตัวเองถูกเข้าใจผิดว่าเป็น “โค้ดยาวที่ฝังอยู่ในเชลล์” และการทำงานก็หยุด
เส้นแบ่งของสิ่งที่ถูกตรวจ (แนวตั้ง = คำสั่งที่ผ่านการตรวจ,
แนวนอน = การแตกกิ่งตามธรรมชาติของสตริง, การซ้อน = ก่อนและหลังการแก้)
สตริงคำสั่ง
│
├─▶ นอก heredoc ── โค้ดที่เชลล์ตีความจริง
│ ├─ ก่อน ── ถูกตรวจ ── ✅ ถูกต้อง
│ └─ หลัง ── ถูกตรวจ ── ✅ ไม่เปลี่ยน ───────────────────┐
│ │
└─▶ ใน heredoc ── ข้อมูลธรรมดาที่ส่งให้คำสั่ง │
├─ ก่อน ── ถูกตรวจ ◀── ผลบวกลวง ① │
│ └─ คำว่า "python -c" แค่บังเอิญ │
│ ปรากฏในข้อความอธิบายของข้อความคอมมิต │
│ └─ ข้อความที่อธิบายการเปลี่ยนแปลงของตัวเอง │
│ หยุดการทำงานของตัวเอง │
│ │ │
│ └──(เส้นย้อนกลับ)──▶ ลากเส้นแบ่งใหม่
│ จาก "พาธ" ไปเป็น "ธรรมชาติของเนื้อหา"
│ │ │
└─ หลัง ── ตัดตั้งแต่เครื่องหมายเปิดถึงเครื่องหมายปิดออกนอกขอบเขต
│ │
└───────────┬───────────────────────────┘
▼
แยกสตริงคำสั่งเดียวกันเป็นสองส่วนตามธรรมชาติ แล้วค่อยตรวจ
การแก้คือตัดช่วงตั้งแต่เครื่องหมายเปิดของ heredoc ถึงเครื่องหมายปิดที่คู่กัน ออกจากขอบเขตการตรวจก่อน นี่เป็นรูปแบบเดียวกับบทเรียนที่ฉันเคยสถาปนาไว้ก่อนหน้าในบริบทอื่น — “คำที่คุณกำลังสกัดออกมา อาจปรากฏในตำแหน่งที่ไม่ได้ตั้งใจภายในสตริงในเครื่องหมายคำพูดหรือในข้อความ แล้วจับคู่ผิด” บทเรียนที่ควรจะเรียนไปแล้วครั้งหนึ่ง กลับมาโดนอีกในการอิมพลีเมนต์คนละตัว
การ์ดตอบสนองต่อตัวเอง (2) — ค่าสำหรับตรวจสอบภายในโค้ดเทสต์
ผลบวกลวงอีกอันเกิดขึ้นระหว่างเขียนโค้ดเทสต์เชิงปฏิปักษ์เพื่อยืนยันความถูกต้องของการ์ด
เทสต์ที่ยืนยันว่า “หมายเลขพอร์ตที่ไม่มีที่มาถูกหยุด” จำเป็นต้องมี “หมายเลขพอร์ตที่ไม่ควรมีอยู่ที่ไหนเลยในเวิร์กสเปซ” อย่างจงใจ การเขียนหมายเลขนั้นเป็นค่าคงตัวในโค้ดเทสต์ ทำให้ — เพราะขอบเขตการค้นหาของการ์ดคือทั้งเวิร์กสเปซ (รวมไฟล์ที่โค้ดเทสต์เองอยู่) — การค้นหาของการ์ดหยิบเอาข้อความนั้นในโค้ดเทสต์ขึ้นมาเป็นผลลัพธ์ที่ตรงกัน เทสต์ที่อยากยืนยันว่า “พอร์ตนี้ไม่มีอยู่ที่ไหนเลย” ถูกการค้นหาของการ์ดเองตัดสินว่า “มีอยู่ (ในโค้ดเทสต์)” — และคุณสมบัติที่กำลังถูกทดสอบก็ถูกทำลาย
ขอบเขตที่ค้นหาที่มา (แนวตั้ง = การค้นหาที่กำลังรัน, แนวนอน = ไฟล์ไหนนับเป็นที่มา,
การซ้อน = ค่าที่วางไว้ตรงนั้นแท้จริงคืออะไร)
หยิบหมายเลขพอร์ตมาหนึ่งตัว
│
└─▶ git grep ทั่วทั้งเวิร์กสเปซ
│
├─▶ โค้ดโปรดักชัน ── ค่าที่ถูกใช้เป็นปลายทางการเชื่อมต่อจริง
│ └─ ตรงกัน ─▶ ✅ ผ่านได้ในฐานะ "ค่าที่ยืนยันแล้ว" ────────┐
│ │
├─▶ ไฟล์ตั้งค่า ── ค่าที่ถูกใช้จริงเช่นกัน │
│ └─ ตรงกัน ─▶ ✅ ผ่านได้ ─────────────────────────────────┤
│ │
└─▶ โค้ดเทสต์ ── ค่าปลอมที่ดูเหมือนจริง เขียนไว้ │
│ อย่างจงใจเพื่อยืนยันว่า "ไม่มีที่มา" │
├─ ก่อน ── ตรงกัน ─▶ ⛔ ผลบวกลวง ② ─────────────────────┤
│ └─ เทสต์ที่อยากยืนยันว่า "ไม่มีอยู่ที่ไหนเลย"
│ ยืนยันตัวเอง โดยเอาข้อความของตัวเองเป็นที่มา
│ └─ เทสต์นั้นผ่านตลอดไปนับจากนั้น │
│ (= คุณสมบัติที่ถูกทดสอบหายไป)
│ │ │
│ └──(เส้นย้อนกลับ)──▶ ลากนิยามของ
│ ขอบเขตการค้นหาเองใหม่
│ │
└─ หลัง ── กันไฟล์ที่ตรงกับธรรมเนียมการตั้งชื่อเทสต์ออก ───┤
▼
เฉพาะสิ่งที่ถูกใช้จริงเท่านั้นที่เรียกได้ว่า "ที่มา"
การแก้คือกันไฟล์เทสต์ (ไฟล์ที่วางอยู่ใต้ธรรมเนียมการตั้งชื่ออย่าง test_*.py) ออกจากขอบเขตการค้นหาอย่างชัดเจน ค่าที่เขียนอยู่ในโค้ดเทสต์ไม่ใช่ที่มาของค่าที่ถูกใช้เป็นปลายทางการเชื่อมต่อจริง
สิ่งที่ผลบวกลวงทั้งสองมีร่วมกัน
ทั้งคู่มีรากเดียวกัน ฉันลากเส้นแบ่งระหว่าง “ผู้ตรวจ” กับ “สิ่งที่ถูกตรวจ” ด้วยพาธไฟล์และไดเรกทอรีล้วน ๆ โดยไม่ดูธรรมชาติของเนื้อหา (โค้ดที่จะถูกรัน หรือข้อมูลธรรมดา; ค่าโปรดักชัน หรือค่าปลอมที่เขียนไว้เพื่อตรวจสอบ) เนื้อหาของ heredoc “ดูเหมือนโค้ดแต่เป็นข้อมูล” ค่าคงตัวในโค้ดเทสต์ “ดูเหมือนจริงแต่เป็นของปลอม” ทั้งคู่ผิวเผินดูเหมือนเป็นส่วนหนึ่งของสตริงคำสั่งหรือไฟล์ซอร์ส แต่บทบาทจริงของมันต่างกัน
ยืนยันทั้งสองทิศทางด้วยเทสต์เชิงปฏิปักษ์
หลังการแก้ ฉันเขียนการตรวจ 24 รายการที่ครอบคลุมทั้งสองทิศทาง: สิ่งที่ควรผ่านถูกหยุดผิดหรือไม่ (ตรวจจับเกิน) และสิ่งที่ควรถูกหยุดถูกปล่อยหลุดหรือไม่ (ตรวจจับขาด) รันในสภาพแวดล้อมที่ติดตั้งใช้งานจริง ผลลัพธ์คือ:
[OK ] 許可: ソースに存在するポート(<実在する番号>) rc=0
[OK ] 拒否: ソースに存在しないポート(<非在の番号>) rc=2 contains=True
[OK ] 許可: well-known ポート(443) rc=0
[OK ] 誤爆しない: 時刻表記 12:34 rc=0
[OK ] 誤爆しない: バージョン番号 1.5.8 rc=0
[OK ] 誤爆しない: Windows パス C:\Users rc=0
[OK ] 拒否: python -c に長いコードを埋めている rc=2 contains=True
[OK ] 誤爆しない: メッセージ内に 'python -c' の語が出るだけ rc=0
[OK ] 誤爆しない: heredoc 本体の説明文に 'python -c' と長文が入っている rc=0
[OK ] 誤爆しない: heredoc 本体にソース非在のポート番号が語として出てくる rc=0
[OK ] 拒否: heredoc の外側にある実行対象のインラインコードは検査される rc=2 contains=True
[OK ] 空 stdin で落ちない rc=0
[OK ] 壊れた JSON で落ちない rc=0
[OK ] command 欠落で落ちない rc=0
failures=0
(ตัดมาจากเอาต์พุตจริง มีเพียงหมายเลขพอร์ตที่มีอยู่จริงที่ถูกปิดบังด้วย <…> ทั้ง 24 รายการแบ่งเป็น: ยืนยันสิ่งที่ควรอนุญาต, ยืนยันสิ่งที่ควรปฏิเสธ, ยืนยันว่าไม่มีสัญญาณเตือนลวง, ยืนยันเส้นแบ่งของ heredoc และยืนยันความทนทานต่ออินพุตที่ผิดรูป)
สิ่งที่ฉันคำนึงตรงนี้ไม่ใช่แค่การเรียง “กรณีที่ใช้ได้” แต่คือ การทำให้รูปร่างของผลบวกลวงแต่ละอันที่ฉันเคยโดน กลายเป็นชื่อของรายการเทสต์ เทสต์ที่ว่า “หมายเลขพอร์ตที่ไม่มีในซอร์สบังเอิญปรากฏเป็นคำในเนื้อ heredoc” ถูกเขียนขึ้นเพื่อสร้างข้อบกพร่อง ② ขึ้นมาใหม่พอดี มันทำหน้าที่เป็นเทสต์ป้องกันการถดถอย ไม่ให้ฝังข้อบกพร่องเดิมลงไปอีก
ขีดจำกัดของการออกแบบนี้
การ์ดเลือกการออกแบบที่ “ถ้าการค้นหาล้มเหลว อย่าหยุด” (fail-open) นั่นเป็นการตัดสินที่ทำไปเพื่อไม่ให้ตัวงานเองหยุด แต่ด้านกลับคือ เมื่อการค้นหาล้มเหลว มันยกสัญญาณเตือนขึ้นมาจริง แต่ไม่ออกแรงหยุดด้วยเครื่องจักรเลย เส้นทางนี้ถูกเดินบ่อยแค่ไหนยังไม่ได้รวมยอด ความจริงจึงยังไม่รู้
อีกอย่าง สิ่งที่ฉันพบและซ่อมตรงนี้มีเพียงการอ้างถึงตัวเองสองจุดที่ฉันโดนและสังเกตเห็นระหว่างทำงานจริงเท่านั้น ว่าข้อบกพร่องรูปแบบเดียวกันยังเหลืออยู่ที่อื่นหรือไม่ ยืนยันได้เพียงภายในขอบเขตของเทสต์เหล่านี้ เส้นแบ่งระหว่างผู้ตรวจกับสิ่งที่ถูกตรวจกลายเป็นสิ่งที่ลากใหม่ได้ทุกครั้งที่คุณเจอมันเท่านั้น