End-to-End-Workflow für agentische Sicherheit mit CodeMender

1. Einführung

In diesem Codelab kombinieren Sie die beiden vorherigen Codelabs. Sie senden Antigravity einen Feature Request und sehen dann, wie das Tool mit einem Bedrohungsmodell plant, fehlerhafte Tests schreibt, das Feature implementiert und pusht. Beim Push führt das Security Gate aus dem vorherigen Codelab CodeMender für die geänderten Dateien aus. Da die Beispielanwendung in der Datei, die der KI-Agent bearbeitet, immer noch eine offene Command Injection hat, wird der erste Push blockiert. Sie sehen, dass der KI-Agent das Ergebnis zuerst testet und dann noch einmal pusht, ohne dass CodeMender menschliche Prompts benötigt.

Lerninhalte

  • So führen Sie CodeMender monitorlos aus, damit weder der Hook noch der Agent bei [Y/n]-Prompts blockiert werden.
  • Was der Plan → Rot → Grün → Refaktorieren-Zyklus erzeugt, wenn der KI-Agent ein Feature erstellt: threat_model.md, Tests, Code und ein Commit.
  • Wie das Security Gate reagiert, wenn eine hochgeladene Datei offene Ergebnisse hat, und wie der Agent die Skills security-test-writer und defensive-developer verwendet, um sie zu beheben.
  • So lesen Sie den Audit-Trail, den das Gate hinterlässt: .security-gate/findings-log.ndjson, automatisch entwickelte Konventionen in CONTEXT.md und Git-Verlauf.

Vorbereitung

  • Den Arbeitsbereich ~/codemender-lab aus den vorherigen beiden Codelabs mit einem sauberen Git-Arbeitsbaum im Branch main.
  • Antigravity ist installiert und ~/codemender-lab kann als Ordner geöffnet werden.
  • git, jq und die virtuelle Python-Umgebung mit installiertem Flask.

2. Einrichtung aus den vorherigen Codelabs überprüfen

Schritt 1: Zum Lab-Verzeichnis zurückkehren

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

Schritt 2: Vorhandensein der Framework-Dateien prüfen

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

Sie sollten die sechs Skill-Verzeichnisse (defensive-developer, history-context-seeder, local-refactor-scanner, security-test-writer, skill-evolution-updater, threat-model-assessor), tests/__init__.py und tests/test_data_service.py, ein leeres git status und main sehen.

Führen Sie die Offline-Hook-Tests noch einmal aus, um zu prüfen, ob sich nichts geändert hat:

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

Schritt 3: CodeMender-Umgebung bestätigen

cm init --verify

Als VCS-Typ sollte git und als Build-Befehl python3 -m compileall -q . angegeben werden, wie in „CodeMender CLI Basics“ konfiguriert.

Schritt 4: Veraltete Ergebnisse löschen

cm clean

Geben Sie y ein, wenn Sie dazu aufgefordert werden. Dadurch wird ~/.codemender/state.db geleert.

Schritt 5: Remote-Repository erstellen, in das übertragen werden soll

Der Hook vergleicht Ihren Arbeitsbaum mit origin/main und git push benötigt ein Ziel. Ein Bare-Repository auf demselben Computer reicht aus:

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

Dieser Push erfolgt in Ihrem eigenen Terminal, sodass der Antigravity-Hook ihn nicht sieht. Ab hier übernimmt der Agent die Push-Vorgänge.

3. CodeMender Headless ausführen

Schritt 1: Bestätigungsaufforderungen deaktivieren

Standardmäßig fragt CodeMender vor jedem Befehl, den es ausführt, und jeder Datei, die es schreibt, [Y/n] um Erlaubnis. In einer Hook ist niemand da, der antworten könnte. Öffnen Sie ~/.codemender/config.yaml und setzen Sie beide tools-Flags auf false oder führen Sie Folgendes aus:

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

Der resultierende Abschnitt sollte so aussehen:

tools:
  confirm_commands: false
  confirm_writes: false

Schritt 2: Bestätigen

cm init --verify

4. Der Feature Request

Schritt 1: Arbeitsbereich öffnen

Öffnen Sie ~/codemender-lab als Ordner in Antigravity und starten Sie eine neue Unterhaltung. Die Always-On-Regel „secure_tdd_workflow“ ist jetzt Teil jeder Runde und die sechs Fähigkeiten sind verfügbar.

Schritt 2: Dem Agenten die Aufgabe zuweisen

Fügen Sie den folgenden Prompt ein: Beachten Sie, dass hier nichts über Sicherheit steht. Die Regel und die Skills, nicht der Prompt, sind dafür verantwortlich, den Agenten in Richtung Bedrohungsmodellierung, Test-First-Entwicklung und defensive Muster zu lenken:

Fügen Sie sample_app/data_service.py einen POST /update_profile-Endpunkt hinzu, über den ein Nutzer seine E-Mail-Adresse aktualisieren kann. user_id und email werden als Formularfelder akzeptiert. Wenn die Änderung abgeschlossen ist und die Test-Suite erfolgreich ausgeführt wurde, führen Sie einen Commit durch und übertragen Sie die Änderung per Push an origin main.

Schritt 3: Befehle des KI-Agenten genehmigen

Der Hook wird außerhalb des Tool-Berechtigungssystems ausgeführt, die eigenen Befehle des Agenten jedoch nicht. Es werden Aufforderungen für python3 -m unittest ..., git add/commit/push und alle cm-Befehle angezeigt, die ausgeführt werden sollen. Genehmigen Sie sie. Wenn Antigravity anbietet, sich die Entscheidung zu merken, akzeptieren Sie dies, damit die Fehlerbehebungsschleife später in diesem Codelab nicht durch wiederholte Aufforderungen ins Stocken gerät.

5. Was der KI-Agent tut: Planen, Rot, Grün, Refaktorieren

Im Folgenden finden Sie einen repräsentativen Durchlauf. Die Formulierungen, Dateinamen und der genaue Code Ihres KI-Agenten können abweichen. Das ist in Ordnung. Was sich nicht unterscheiden sollte, ist die Reihenfolge: ein Bedrohungsmodell vor den Tests, fehlgeschlagene Tests vor dem Code, eine grüne Suite vor dem Commit.

Phase A: Planen und Bedrohungsmodell erstellen

Der KI-Agent liest CONTEXT.md, lädt threat-model-assessor und schreibt threat_model.md im Stammverzeichnis des Arbeitsbereichs:

# 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.

Phase B: Fehlgeschlagene Tests (ROT)

Der Agent lädt security-test-writer und erstellt tests/test_update_profile.py. Eine typische Version verwendet eine temporäre users.db im Arbeitsverzeichnis. Deshalb wurde im vorherigen Codelab users.db zu .gitignore hinzugefügt:

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)

Der KI-Agent führt python3 -m unittest discover -s tests aus und bestätigt, dass die neuen Tests mit 404 != 200 und 404 != 400 fehlschlagen, da die Route noch nicht existiert. Das ist der richtige Grund, um zu scheitern. Ein Fehler, der durch einen Importfehler oder einen Tippfehler verursacht wird, ist nicht ROT, sondern ein fehlerhafter Test.

Phase C: Minimale Implementierung (GRÜN)

Der KI-Agent lädt defensive-developer und fügt den Endpunkt hinzu. Es wird eine parametrisierte Abfrage verwendet, da CONTEXT.md dies als erforderliches Muster für SQL angibt:

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

Die Suite besteht jetzt die vier neuen Tests sowie den Charakterisierungstest aus dem vorherigen Codelab.

Phase D: Refaktorieren, scannen, commiten

Der KI-Agent lädt local-refactor-scanner, prüft git diff, führt die gesamte Suite noch einmal aus und führt einen Commit durch:

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. Das Security Gate bei git push

Der erste Push wird abgelehnt.

Der KI-Agent führt git push origin main aus. Antigravity gleicht das Tool run_command mit hooks.json ab und führt security_gate_hook.sh aus. Dadurch wird der Push erkannt und die geänderten Dateien werden gescannt: sample_app/data_service.py, tests/test_update_profile.py und threat_model.md (CodeMender überspringt .md-Dateien; cm find findet in diesem Pfad keine Quelldateien und der Hook fährt fort).

Dies dauert ein paar Minuten. Der Hook ist synchron, sodass der Zug des KI-Agenten beim Push-Befehl hängen bleibt, während cm find und cm report ausgeführt werden. Wenn der Scan abgeschlossen ist, findet das Gate die Befehlsinjektion in /ping, die von CodeMender CLI Basics nie behoben wurde. Es ist CRITICAL, es ist OPEN und es befindet sich in einer Datei, die per Push übertragen wird. Der Hook lehnt also Folgendes ab:

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", ...}

Der Push wurde nicht ausgeführt. Der Agent sieht den Text reason als Ergebnis seines Toolaufrufs und das Gate hat ein DENIED_TO_AGENT-Ereignis für dieses Ergebnis protokolliert.

Der Agent löscht das Ergebnis test-first

In der Ablehnung werden die zu verwendenden Skills genannt. Gemäß Regelpunkt 6 (Protokoll zur Behebung mehrerer Ergebnisse) müssen die Ergebnisse einzeln bearbeitet werden. Für das Ergebnis /ping sieht eine repräsentative Ausführung so aus.

Ein fehlgeschlagener Grenztest in 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)

Die minimale Korrektur, bei der os.system durch eine subprocess.run in Listenform ersetzt wird, wie für CONTEXT.md erforderlich, sowie ein Parser für die Zulassungsliste anstelle von Stringprüfungen:

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"

Der Agent führt die gesamte Suite aus, führt einen Commit mit einer Nachricht aus, in der das Problem und die Lösung beschrieben werden, und führt einen weiteren Push aus.

Der zweite Push ist zulässig

Das Gate scannt sample_app/data_service.py noch einmal. Die Befehlsinjektion ist verschwunden. Es sind keine blockierenden Ergebnisse mehr vorhanden. Das Gate prüft also sein eigenes Log, sieht, dass das zuvor abgelehnte Ergebnis nicht mehr vorhanden ist, und zeichnet das Ergebnis auf, bevor der Push zugelassen wird:

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 wird ausgeführt und das Remote-Repository hat jetzt drei neue Commits: das Feature, den Fix und einen docs(context)-Commit, den der Hook selbst erstellt hat.

7. Audit-Trail prüfen

Alles, was das Gate entschieden hat, befindet sich im Nur-Text-Format auf dem Laufwerk. Führen Sie in einem Terminal in ~/codemender-lab Folgendes aus:

Das Gate-Log

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}

Jede Zeile enthält auch actor (Ihre Git-E-Mail-Adresse) und commit (kurzer SHA-Wert zu diesem Zeitpunkt). Dadurch kann eine spätere Frage wie „Wer hat das wann erlaubt?“ ohne Datenbank beantwortet werden.

Git-Verlauf

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
...

Der weiterentwickelte Kontext

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.

Der Eintrag des Hooks ist kurz, da er nur die Ergebnis-ID, die Datei und den Schweregrad kennt. Der Skill skill-evolution-updater dient dazu, dies in eine Regel umzuwandeln, die ein Mensch schreiben würde. Fragen Sie den KI-Agenten: „Verwende skill-evolution-updater, um die letzte automatisch weiterentwickelte Konvention in CONTEXT.md als Projektregel umzuschreiben, und schlage alle damit verbundenen Änderungen an SKILL.md vor.“

CodeMender-Ansicht

cm report

Das Ergebnis der Befehlsinjektion sollte jetzt einen Status ungleich OPEN haben. threat_model.md und die beiden neuen Testdateien befinden sich im Repository, damit das nächste Feature darauf aufbauen kann.

8. Bereinigen

Interaktive Standardeinstellungen von CodeMender wiederherstellen:

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

Löschen Sie den lokalen Status von CodeMender (geben Sie y in die Eingabeaufforderung ein):

cm clean

So entfernen Sie auch die Lab-Verzeichnisse:

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

Glückwunsch! Sie haben das Codelab „End-to-End-Sicherheitsworkflow mit CodeMender“ abgeschlossen. Sie haben die Befehlszeile aus dem ersten Code-Lab, die Skills und den Hook aus dem zweiten und einen KI-Agenten gesehen, der einen Push verarbeitet, der blockiert, korrigiert, protokolliert und zugelassen wurde, ohne dass ein CodeMender-Prompt erforderlich war.