使用 CodeMender 的端到端智能体安全工作流

1. 简介

在此 Codelab 中,您将结合前两个 Codelab 的内容。您将向 Antigravity 发出功能请求,然后观察它如何使用威胁模型进行规划、编写失败的测试、实现功能并推送。推送时,上一个 Codelab 中的安全关卡会针对更改的文件运行 CodeMender。由于示例应用在智能体正在编辑的文件中仍存在开放式命令注入漏洞,因此第一次推送会被阻止,您会看到智能体先清除发现结果,然后再次推送,在此过程中 CodeMender 不会发出任何人工提示。

学习内容

  • 如何以无头模式运行 CodeMender,使钩子和智能体都不会在 [Y/n] 提示上阻塞。
  • 智能体在构建功能时,通过“计划 → 红色 → 绿色 → 重构”循环生成的内容:threat_model.md、测试、代码和提交。
  • 当推送的文件存在未解决的发现结果时,安全关卡会如何响应,以及智能体如何使用 security-test-writer 和 defensive-developer 技能来清除这些发现结果。
  • 如何读取门控留下的审计跟踪记录:.security-gate/findings-log.ndjson、CONTEXT.md 中自动演进的惯例以及 git 历史记录。

前提条件

  • 前两个 Codelab 中的 ~/codemender-lab 工作区,在 main 分支上有一个干净的 Git 工作树。
  • Antigravity 已安装,~/codemender-lab 可作为文件夹打开。
  • git、jq 以及安装了 Flask 的 Python 虚拟环境。

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

您应该会看到六个技能目录(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 提示记住该决定,请接受,这样本 Codelab 后面的修复循环就不会因重复提示而停滞。

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,这就是上一个 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

现在,测试套件通过了:四个新测试以及上一个 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 上的安全关卡

首次推送被拒

智能体在 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 基础知识从未修复过的命令注入。它是 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)

最小修复,将 os.system 替换为 CONTEXT.md 所需的列表形式 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.

钩子的条目很简短,因为它只知道发现结果 ID、文件和严重级别。skill-evolution-updater 技能可将此内容转换为人类会编写的规则。向智能体发出以下指令:“Use skill-evolution-updater to rewrite the last auto-evolved convention in CONTEXT.md as a project rule, and suggest any SKILL.md change it implies.”

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 的端到端智能体安全工作流”Codelab。您在第一个 Codelab 中了解了 CLI,在第二个 Codelab 中了解了技能和钩子,并看到了一个智能体在没有 CodeMender 提示的情况下,如何处理被阻止、修复、记录并允许的推送。