เวิร์กโฟลว์การรักษาความปลอดภัยแบบ Agentic ตั้งแต่ต้นจนจบด้วย CodeMender

1. บทนำ

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

สิ่งที่คุณจะได้เรียนรู้

  • วิธีเรียกใช้ CodeMender แบบ Headless เพื่อไม่ให้ทั้ง Hook และ Agent บล็อกพรอมต์ [Y/n]
  • สิ่งที่ลูปวางแผน → แดง → เขียว → ปรับโครงสร้างโค้ดสร้างขึ้นเมื่อ Agent สร้างฟีเจอร์: threat_model.md, การทดสอบ, โค้ด และคอมมิต
  • วิธีที่เกตความปลอดภัยตอบสนองเมื่อไฟล์ที่พุชมีผลการค้นหาที่เปิดอยู่ และวิธีที่ Agent ใช้ทักษะ security-test-writer และ defensive-developer เพื่อล้างผลการค้นหาเหล่านั้น
  • วิธีอ่านเส้นทางการตรวจสอบที่เกตทิ้งไว้: .security-gate/findings-log.ndjson, ข้อตกลงที่พัฒนาขึ้นโดยอัตโนมัติใน CONTEXT.md และประวัติ git

ข้อกำหนดเบื้องต้น

  • พื้นที่ทำงาน ~/codemender-lab จาก Codelab 2 รายการก่อนหน้า โดยมี Git Working Tree ที่สะอาดในสาขา main
  • ติดตั้ง Antigravity แล้ว โดยมี ~/codemender-lab ที่พร้อมเปิดเป็นโฟลเดอร์
  • git, jq และสภาพแวดล้อมเสมือนของ Python ที่ติดตั้ง Flask ไว้

2. การยืนยันการตั้งค่าจาก Codelab ก่อนหน้า

ขั้นตอนที่ 1: กลับไปที่ไดเรกทอรีห้องทดลอง

cd ~/codemender-lab && source .venv/bin/activate

ขั้นตอนที่ 2: ยืนยันว่าไฟล์เฟรมเวิร์กอยู่ในตำแหน่งที่ถูกต้อง

ls .agents/hooks.json .agents/security_gate_hook.sh .agents/rules/secure_tdd_workflow.md
ls .agents/skills
ls tests
git status --short && git branch --show-current

คุณควรเห็นไดเรกทอรีทักษะ 6 รายการ (defensive-developer, history-context-seeder, local-refactor-scanner, security-test-writer, skill-evolution-updater, threat-model-assessor), tests/__init__.py และ tests/test_data_service.py, git status ที่ว่างเปล่า และ main

เรียกใช้การทดสอบฮุกออฟไลน์อีกครั้งเพื่อยืนยันว่าไม่มีอะไรคลาดเคลื่อน

bash .agents/tests/run_tests.sh | tail -1
60 passed, 0 failed

ขั้นตอนที่ 3: ยืนยันสภาพแวดล้อม CodeMender

cm init --verify

โดยควรรายงาน git เป็นประเภท VCS และ python3 -m compileall -q . เป็นคำสั่งบิลด์ ตามที่กำหนดค่าไว้ในข้อมูลเบื้องต้นเกี่ยวกับ CLI ของ CodeMender

ขั้นตอนที่ 4: ล้างการค้นพบที่ไม่มีการอัปเดต

cm clean

พิมพ์ y เมื่อได้รับข้อความแจ้ง การดำเนินการนี้จะล้างข้อมูลใน ~/.codemender/state.db

ขั้นตอนที่ 5: สร้างรีโมตเพื่อพุช

ฮุกจะเปรียบเทียบแผนผังการทำงานของคุณกับ origin/main และ git push ต้องมีที่ไป ที่เก็บเปล่าในเครื่องเดียวกันก็เพียงพอแล้ว

git init --bare ~/codemender-remote.git
git remote add origin ~/codemender-remote.git
git push -u origin main

การพุชนี้เกิดขึ้นในเทอร์มินัลของคุณเอง ดังนั้น Antigravity Hook จึงไม่เห็นการพุชนี้ จากนี้ไป Agent จะเป็นผู้พุช

3. กำลังเรียกใช้ CodeMender Headless

ขั้นตอนที่ 1: ปิดข้อความแจ้งให้ยืนยัน

โดยค่าเริ่มต้น CodeMender จะถาม [Y/n] ก่อนที่จะเรียกใช้คำสั่งและเขียนไฟล์ทุกครั้ง ในฮุก ไม่มีใครตอบคำถาม เปิด ~/.codemender/config.yaml และตั้งค่าแฟล็ก tools ทั้ง 2 รายการเป็น false หรือเรียกใช้คำสั่งต่อไปนี้

sed -i.bak 's/^  confirm_commands: true/  confirm_commands: false/; s/^  confirm_writes: true/  confirm_writes: false/' ~/.codemender/config.yaml
rm ~/.codemender/config.yaml.bak

ส่วนที่ได้ควรมีข้อความดังนี้

tools:
  confirm_commands: false
  confirm_writes: false

ขั้นตอนที่ 2: ยืนยัน

cm init --verify

4. คำขอฟีเจอร์

ขั้นตอนที่ 1: เปิดพื้นที่ทำงาน

เปิด ~/codemender-lab เป็นโฟลเดอร์ใน Antigravity และเริ่มการสนทนาใหม่ ตอนนี้กฎ secure_tdd_workflow ที่เปิดตลอดเวลาเป็นส่วนหนึ่งของทุกเทิร์น และมีทักษะ 6 อย่างให้เลือกใช้

ขั้นตอนที่ 2: มอบหมายงานให้ Agent

วางพรอมต์ต่อไปนี้ โปรดทราบว่าข้อความนี้ไม่ได้กล่าวถึงความปลอดภัย การกำหนดทิศทางให้ Agent มุ่งเน้นไปที่การสร้างโมเดลภัยคุกคาม การพัฒนาแบบทดสอบก่อน และรูปแบบการป้องกันเป็นหน้าที่ของกฎและทักษะ ไม่ใช่พรอมต์

เพิ่มปลายทาง POST /update_profile ไปยัง sample_app/data_service.py ซึ่งช่วยให้ผู้ใช้อัปเดตอีเมลของตนได้ โดยจะยอมรับ user_id และ email เป็นช่องแบบฟอร์ม เมื่อทำการเปลี่ยนแปลงเสร็จสมบูรณ์และชุดโปรแกรมทดสอบผ่านแล้ว ให้คอมมิตและพุชไปยัง origin main

ขั้นตอนที่ 3: อนุมัติคำสั่งของ Agent

ฮุกจะทำงานนอกระบบสิทธิ์ของเครื่องมือ แต่คำสั่งของ Agent เองจะไม่ทำงาน คาดว่าจะได้รับพรอมต์สำหรับ python3 -m unittest ..., git add/commit/push และคำสั่ง cm ที่ตัดสินใจเรียกใช้ อนุมัติ หาก Antigravity เสนอให้จดจำการตัดสินใจ ให้ยอมรับเพื่อให้ลูปการแก้ไขในภายหลังใน Codelab นี้ไม่หยุดชะงักเมื่อมีข้อความแจ้งซ้ำๆ

5. สิ่งที่ Agent ทำ: วางแผน, แดง, เขียว, ปรับโครงสร้าง

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

ระยะที่ 1: วางแผนและสร้างโมเดลภัยคุกคาม

Agent จะอ่าน CONTEXT.md, โหลด threat-model-assessor และเขียน threat_model.md ที่รูทของพื้นที่ทำงาน ดังนี้

# Feature Plan & Threat Model: Update Profile Email

## 1. Functional Scope & Requirements
- **Goal**: Allow a user to change the email address stored for their account.
- **Deliverables**: `POST /update_profile` in `sample_app/data_service.py`; tests in `tests/test_update_profile.py`.
- **Functional Acceptance Criteria**: Valid `user_id` + `email` returns 200 and the row is updated; unknown `user_id` returns 404.

## 2. Entry Points & Data Inputs
- Endpoint / Input: `POST /update_profile`, form fields `user_id`, `email`
- Format & Constraints: `user_id` positive integer; `email` <= 254 chars, single `@`, non-empty local and domain parts.

## 3. Trust Boundaries & Access Controls
- Authentication: None exists in this sample. Recorded as a known gap; out of scope for this change.
- Authorization Role: N/A (see above).

## 4. STRIDE Threat Matrix & Security Acceptance Criteria
- Tampering: SQL injection through `user_id`/`email` -> parameterized `UPDATE`; test that `"1 OR 1=1"` returns 400.
- Tampering: Malformed email stored -> strict validation; test that `"not-an-email"` returns 400.
- Information Disclosure: Stack traces on bad input -> return JSON error bodies only.
- Denial of Service: Oversized email -> length cap at 254; test returns 400.

ระยะที่ 2: การทดสอบไม่ผ่าน (สีแดง)

ตัวแทนจะโหลด security-test-writer และสร้าง tests/test_update_profile.py เวอร์ชันทั่วไปจะใช้ users.db แบบใช้แล้วทิ้งในไดเรกทอรีที่ใช้งานอยู่ ซึ่งเป็นสาเหตุที่ Codelab ก่อนหน้านี้เพิ่ม users.db ลงใน .gitignore:

import os
import sqlite3
import unittest

from sample_app.data_service import app


class UpdateProfileTests(unittest.TestCase):
    def setUp(self):
        conn = sqlite3.connect('users.db')
        conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, email TEXT)")
        conn.execute("INSERT OR REPLACE INTO users (id, username, email) VALUES (1, 'alice', 'old@example.com')")
        conn.commit()
        conn.close()
        self.client = app.test_client()

    def tearDown(self):
        os.remove('users.db')

    def test_updates_email_for_valid_request(self):
        r = self.client.post('/update_profile', data={'user_id': '1', 'email': 'new@example.com'})
        self.assertEqual(r.status_code, 200)

    def test_rejects_non_numeric_user_id(self):
        r = self.client.post('/update_profile', data={'user_id': '1 OR 1=1', 'email': 'new@example.com'})
        self.assertEqual(r.status_code, 400)

    def test_rejects_malformed_email(self):
        r = self.client.post('/update_profile', data={'user_id': '1', 'email': 'not-an-email'})
        self.assertEqual(r.status_code, 400)

    def test_rejects_oversized_email(self):
        r = self.client.post('/update_profile', data={'user_id': '1', 'email': 'a' * 250 + '@x.io'})
        self.assertEqual(r.status_code, 400)

ตัวแทนจะเรียกใช้ python3 -m unittest discover -s tests และยืนยันว่าการทดสอบใหม่ไม่สำเร็จด้วย 404 != 200 และ 404 != 400 เนื่องจากยังไม่มีเส้นทาง นั่นเป็นเหตุผลที่ถูกต้องในการล้มเหลว ความล้มเหลวที่เกิดจากข้อผิดพลาดในการนำเข้าหรือการพิมพ์ผิดไม่ใช่ RED แต่เป็นการทดสอบที่ใช้งานไม่ได้

ระยะ C: การติดตั้งใช้งานขั้นต่ำ (สีเขียว)

ตัวแทนจะโหลด defensive-developer และเพิ่มปลายทาง ซึ่งใช้การค้นหาที่มีพารามิเตอร์เนื่องจาก CONTEXT.md ระบุว่าเป็นรูปแบบที่จำเป็นสำหรับ SQL ดังนี้

import re

EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")


@app.route('/update_profile', methods=['POST'])
def update_profile():
    user_id = request.form.get('user_id', '')
    email = request.form.get('email', '')
    if not user_id.isdigit():
        return jsonify({"error": "user_id must be a positive integer"}), 400
    if len(email) > 254 or not EMAIL_PATTERN.match(email):
        return jsonify({"error": "invalid email address"}), 400
    conn = sqlite3.connect('users.db')
    try:
        cursor = conn.execute("UPDATE users SET email = ? WHERE id = ?", (email, int(user_id)))
        conn.commit()
    finally:
        conn.close()
    if cursor.rowcount == 0:
        return jsonify({"error": "user not found"}), 404
    return jsonify({"status": "updated", "user_id": int(user_id)}), 200

ตอนนี้ชุดทดสอบผ่านแล้ว ซึ่งประกอบด้วยการทดสอบใหม่ 4 รายการและการทดสอบการกำหนดลักษณะจาก Codelab ก่อนหน้า

ระยะ D: ปรับโครงสร้างโค้ด สแกน และคอมมิต

ตัวแทนจะโหลด local-refactor-scanner, ตรวจสอบ git diff, เรียกใช้ชุดทดสอบทั้งหมดอีกครั้ง และคอมมิต:

git add sample_app/data_service.py tests/test_update_profile.py threat_model.md
git commit -m "feat: add POST /update_profile with validated input and parameterized update"

6. เกตเวย์ความปลอดภัยใน git push

การพุชครั้งแรกถูกปฏิเสธ

โดย Agent จะทํางานgit push origin main Antigravity จะจับคู่เครื่องมือ run_command กับ hooks.json และเรียกใช้ security_gate_hook.sh ซึ่งจะจดจำการพุชและสแกนไฟล์ที่มีการเปลี่ยนแปลง ได้แก่ sample_app/data_service.py, tests/test_update_profile.py และ threat_model.md (CodeMender จะข้ามไฟล์ .md ส่วน cm find ในเส้นทางนั้นจะไม่พบไฟล์ต้นฉบับและฮุกจะดำเนินการต่อ)

การดำเนินการนี้จะใช้เวลาสักครู่ ฮุกเป็นแบบซิงโครนัส ดังนั้นเทิร์นของ Agent จะดูเหมือนค้างอยู่ที่คำสั่งพุชขณะที่ cm find และ cm report ทำงาน เมื่อสแกนเสร็จแล้ว เกตจะพบการแทรกคำสั่งใน /ping ที่ CodeMender CLI Basics ไม่เคยแก้ไข โดยเป็น CRITICAL, OPEN และอยู่ในไฟล์ที่พุช ดังนั้นฮุกจึงปฏิเสธ:

Stage 2: Running Semantic Analysis & Verification (CodeMender)...
Detected 1 blocking vulnerabilit(y/ies). Evaluating attempt budget per finding...
Finding 80bdddd6-... (CRITICAL) in /home/you/codemender-lab/sample_app/data_service.py: Attempt 1/3
{"allow_tool": false, "decision": "deny", "reason": "Security Gate blocked push (1 unresolved finding(s)).

Action Required: Execute the Secure TDD Loop sequentially for each finding:
For each finding below (one at a time):
  1. Invoke 'security-test-writer' to author one failing boundary test in tests/ (assert RED).
  2. Invoke 'defensive-developer' to apply the minimal defensive fix (assert GREEN).
  3. Run the local test to confirm it passes before moving to the next finding.
Once all findings are resolved, run the full regression test suite (python3 -m unittest discover -s tests) and retry 'git push'.

Open Findings:
- [80bdddd6-...] (CRITICAL) in /home/you/codemender-lab/sample_app/data_service.py (Attempt 1/3): OS Command Injection in ping_host endpoint", ...}

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

Agent จะล้างการค้นหาโดยทดสอบก่อน

การปฏิเสธจะระบุทักษะที่ควรใช้ และกฎข้อที่ 6 (โปรโตคอลการแก้ไขการค้นพบหลายรายการ) ระบุว่าให้แก้ไขการค้นพบทีละรายการ สำหรับการค้นพบ /ping การเรียกใช้ที่เป็นตัวแทนจะมีลักษณะดังนี้

การทดสอบขอบเขตที่ไม่ผ่านใน tests/test_ping.py:

import unittest

from sample_app.data_service import app


class PingBoundaryTests(unittest.TestCase):
    def setUp(self):
        self.client = app.test_client()

    def test_rejects_shell_metacharacters(self):
        r = self.client.get('/ping', query_string={'ip': '127.0.0.1; id'})
        self.assertEqual(r.status_code, 400)

    def test_rejects_hostname_that_is_not_an_ip(self):
        r = self.client.get('/ping', query_string={'ip': 'example.com'})
        self.assertEqual(r.status_code, 400)

การแก้ไขขั้นต่ำ โดยแทนที่ os.system ด้วย subprocess.run ในรูปแบบรายการตามที่ CONTEXT.md กำหนด รวมถึงโปรแกรมแยกวิเคราะห์รายการที่อนุญาตแทนการตรวจสอบสตริง

import ipaddress
import subprocess


@app.route('/ping')
def ping_host():
    ip = request.args.get('ip', '')
    try:
        target = str(ipaddress.ip_address(ip))
    except ValueError:
        return jsonify({"error": "ip must be a valid IP address"}), 400
    subprocess.run(["ping", "-c", "1", target], shell=False, check=False, timeout=5)
    return "Ping initiated"

ตัวแทนจะเรียกใช้ชุดทดสอบทั้งหมด คอมมิตพร้อมข้อความที่อธิบายสิ่งที่พบและการแก้ไข แล้วพุชอีกครั้ง

อนุญาตให้พุชครั้งที่ 2

ประตูจะสแกน sample_app/data_service.py อีกครั้ง การแทรกคำสั่งหายไปแล้ว เมื่อไม่มีผลการตรวจสอบที่บล็อกเหลืออยู่ เกตจะตรวจสอบบันทึกของตัวเอง และเห็นว่าผลการตรวจสอบที่ปฏิเสธก่อนหน้านี้ไม่มีอยู่อีกต่อไป และบันทึกผลลัพธ์ก่อนที่จะอนุญาตให้พุช

Stage 2: Running Semantic Analysis & Verification (CodeMender)...
No blocking vulnerabilities detected. Code verified clean.
Updated CONTEXT.md with auto-evolved convention for 80bdddd6-....
Continuous Evolution Suggestion: Consider updating relevant SKILL.md files (e.g. security-test-writer / defensive-developer) to encode CRITICAL.
{"allow_tool": true, "decision": "allow"}

git push origin main ทำงาน และตอนนี้รีโมตมีคอมมิตใหม่ 3 รายการ ได้แก่ ฟีเจอร์ การแก้ไข และคอมมิต docs(context) ที่ฮุกสร้างขึ้นเอง

7. การตรวจสอบบันทึกการตรวจสอบ

ทุกอย่างที่เกตเวย์ตัดสินใจจะอยู่ในดิสก์ในรูปแบบข้อความธรรมดา จากเทอร์มินัลใน ~/codemender-lab ให้ทำดังนี้

บันทึกเกตเวย์

jq -c '{ts, event, tool, finding_id, file, attempt}' .security-gate/findings-log.ndjson
{"ts":"2026-11-13T22:41:07Z","event":"DENIED_TO_AGENT","tool":"codemender","finding_id":"80bdddd6-...","file":"/home/you/codemender-lab/sample_app/data_service.py","attempt":1}
{"ts":"2026-11-13T22:58:32Z","event":"FIXED","tool":"codemender","finding_id":"80bdddd6-...","file":"/home/you/codemender-lab/sample_app/data_service.py","attempt":null}

แต่ละบรรทัดยังมี actor (อีเมล git ของคุณ) และ commit (SHA แบบสั้นในขณะนั้น) ซึ่งทำให้สามารถตอบคำถาม "ใครอนุญาตสิ่งนี้และเมื่อใด" ในภายหลังได้โดยไม่ต้องใช้ฐานข้อมูล

ประวัติ Git

git log --oneline -5
a1b2c3d docs(context): record auto-evolved convention for 80bdddd6-...
e4f5a6b fix: validate ip and use subprocess list form in /ping (CWE-78)
c7d8e9f feat: add POST /update_profile with validated input and parameterized update
...

บริบทที่พัฒนาขึ้น

tail -n 4 CONTEXT.md
## 4. Continuous Evolution: Auto-Evolved Conventions
- **Auto-Evolved Convention (80bdddd6-...)**: In `/home/you/codemender-lab/sample_app/data_service.py`, maintain defensive validation for: CRITICAL.

รายการของ Hook จะสั้นกระชับเนื่องจากทราบเฉพาะรหัสการค้นพบ ไฟล์ และความรุนแรงเท่านั้น ทักษะ skill-evolution-updater มีไว้เพื่อเปลี่ยนสิ่งนี้ให้เป็นกฎที่มนุษย์จะเขียนขึ้น ถาม Agent ว่า "ใช้ skill-evolution-updater เพื่อเขียนข้อตกลงที่พัฒนาโดยอัตโนมัติล่าสุดใน CONTEXT.md ใหม่เป็นกฎของโปรเจ็กต์ และแนะนำการเปลี่ยนแปลง SKILL.md ที่เกี่ยวข้อง"

มุมมองของ CodeMender

cm report

ตอนนี้การค้นพบการแทรกคำสั่งควรแสดงสถานะที่ไม่ใช่ OPEN และ threat_model.md รวมถึงไฟล์ทดสอบใหม่ 2 ไฟล์จะอยู่ในที่เก็บสำหรับฟีเจอร์ถัดไปที่จะสร้างขึ้น

8. กำลังล้างข้อมูล

คืนค่าเริ่มต้นแบบอินเทอร์แอกทีฟของ CodeMender:

sed -i.bak 's/^  confirm_commands: false/  confirm_commands: true/; s/^  confirm_writes: false/  confirm_writes: true/' ~/.codemender/config.yaml
rm ~/.codemender/config.yaml.bak

ล้างสถานะในเครื่องของ CodeMender (พิมพ์ y ที่พรอมต์)

cm clean

วิธีนำไดเรกทอรีของห้องปฏิบัติการออกด้วย

cd ~ && rm -rf ~/codemender-lab ~/codemender-remote.git

ยินดีด้วย คุณได้ทำ Codelab เรื่องเวิร์กโฟลว์การรักษาความปลอดภัยแบบ Agentic ตั้งแต่ต้นจนจบด้วย CodeMender เสร็จสมบูรณ์แล้ว คุณได้เห็น CLI จาก Codelab แรก ทักษะและฮุกจาก Codelab ที่ 2 และ Agent ที่ทำงานผ่านการพุชที่ถูกบล็อก แก้ไข บันทึก และอนุญาตโดยไม่มีพรอมต์ CodeMender ในลูป