使用 CodeMender 建立端對端代理式資安工作流程

1. 簡介

在本程式碼實驗室中,您將結合前兩個程式碼實驗室的內容。您將向 Antigravity 提出功能要求,然後觀察它如何使用威脅模型規劃、編寫失敗測試、實作功能,以及推送。推送時,先前程式碼實驗室的安全閘道會對變更的檔案執行 CodeMender。由於範例應用程式在代理編輯的檔案中仍有開放式指令插入,因此第一次推送會遭到封鎖,您會看到代理先清除發現項目測試,然後再次推送,過程中 CodeMender 不會顯示任何人工提示。

學習目標

  • 如何以無頭模式執行 CodeMender,讓掛鉤和代理都不會因 [Y/n] 提示而遭到封鎖。
  • 代理建構功能時,計畫 → 紅色 → 綠色 → 重構循環會產生:threat_model.md、測試、程式碼和修訂版本。
  • 推送的檔案有待解決的發現項目時,安全閘道會如何回應,以及代理如何使用 security-test-writer 和 defensive-developer 技能清除這些項目。
  • 如何解讀閘道留下的稽核追蹤記錄:.security-gate/findings-log.ndjson、CONTEXT.md 中自動演進的慣例,以及 git 記錄。

必要條件

  • 前兩個程式碼實驗室的 ~/codemender-lab 工作區,在分支版本 main 上有乾淨的 git 工作樹。
  • 已安裝 Antigravity,~/codemender-lab 可做為資料夾開啟。
  • git、jq,以及已安裝 Flask 的 Python 虛擬環境。

2. 驗證先前程式碼研究室的設定

步驟 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

您應該會看到六個技能目錄 (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 . 為建構指令,如 CodeMender CLI 基本概念中的設定。

步驟 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 鉤點不會看到。從這裡開始,代理會推送。

3. 執行 CodeMender Headless

步驟 1:關閉確認提示

根據預設,CodeMender 在執行指令和寫入檔案前,都會先詢問[Y/n]。在掛鉤中,沒有人可以回答。開啟 ~/.codemender/config.yaml,並將兩個 tools 旗標設為 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:開啟工作區

在 Antigravity 中以資料夾形式開啟 ~/codemender-lab,並發起新對話。secure_tdd_workflow隨時可用規則現在會套用到每個回合,且六項技能皆可使用。

步驟 2:指派任務給代理

貼上下列提示詞。請注意,其中並未提及安全性。引導代理執行威脅建模、測試優先開發和防禦模式,是規則和技能的工作,而非提示詞:

在 sample_app/data_service.py 中新增 POST /update_profile 端點,讓使用者更新電子郵件地址。接受 user_id 和 email 做為表單欄位。變更完成且測試套件通過後,請提交變更並推送至 origin main。

步驟 3:核准代理的指令

掛鉤會在工具權限系統外執行,但代理本身的指令不會。系統會提示您輸入 python3 -m unittest ...、git add/commit/push,以及決定執行的任何 cm 指令。請核准這些要求。如果 Antigravity 詢問是否要記住這項決定,請接受,這樣本程式碼實驗室後續的修正迴圈就不會因重複提示而停滯。

5. 代理的功用:規劃、紅燈、綠燈、重構

以下是代表性執行作業。代理的用詞、檔案名稱和確切程式碼會有所不同,這是正常現象。不應改變的是順序:先建立威脅模型再測試、先測試失敗再編寫程式碼、先確保測試套件通過再提交。

階段 A:規劃和威脅模型

代理會讀取 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.

階段 B:測試失敗 (紅色)

代理程式會載入 security-test-writer 並建立 tests/test_update_profile.py。一般版本會在目前使用的目錄中使用一次性 users.db,這也是上一個程式碼實驗室將 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。這才是失敗的正確原因。因匯入錯誤或錯字而導致的失敗並非紅燈,而是測試中斷。

階段 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

現在,測試套件已通過:四項新測試,以及先前程式碼實驗室的特徵測試。

階段 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 的安全閘道

第一次推送遭拒

代理會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 在該路徑上找不到來源檔案,掛鉤會繼續執行)。

這項作業會在幾分鐘內完成。掛鉤是同步的,因此在 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 事件。

代理會先清除發現項目 test-first

拒絕訊息中指明要使用的技能,而規則項目 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)

最簡單的修正方式是按照 CONTEXT.md 的要求,將 os.system 替換為清單形式的 subprocess.run,並使用許可清單剖析器,而非字串檢查:

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"

代理執行完整套件,提交訊息說明發現的問題和修正方式,然後再次推送。

允許第二次推送

閘道會重新掃描 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 執行,遠端現在有三個新修訂版本:功能、修正和掛鉤本身建立的 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 的項目很簡短,因為它只知道發現項目 ID、檔案和嚴重性。skill-evolution-updater 技能可將這項功能轉換為人類編寫的規則。向代理下達以下指令:「使用 skill-evolution-updater,將 CONTEXT.md 中最後一個自動演進的慣例改寫為專案規則,並建議任何隱含的 SKILL.md 變更。」

CodeMender 的檢視畫面

cm report

指令插入發現項目現在應顯示非 OPEN 狀態,且 threat_model.md 和兩個新的測試檔案已在存放區中,可供下一個功能使用。

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

恭喜!您已完成「使用 CodeMender 建立端對端代理安全工作流程」程式碼實驗室。您已在第一個程式碼實驗室中看過 CLI,在第二個實驗室中看過技能和掛鉤,並看過代理在沒有 CodeMender 提示的情況下,處理被封鎖、修正、記錄和允許的推送。