Kompleksowy proces bezpieczeństwa agentowego z CodeMender

1. Wprowadzenie

W tym ćwiczeniu połączysz 2 poprzednie ćwiczenia. Przekażesz Antigravity prośbę o dodanie funkcji, a następnie zobaczysz, jak planuje on działanie z modelem zagrożeń, pisze testy, które się nie powiodą, wdraża funkcję i przesyła ją. Po przekazaniu bramka zabezpieczeń z poprzedniego ćwiczenia z programowania uruchamia CodeMender na zmienionych plikach. Ponieważ przykładowa aplikacja nadal ma otwarte wstrzyknięcie polecenia w pliku, który edytuje agent, pierwsze przesłanie zostanie zablokowane. Zobaczysz, że agent najpierw usunie wynik testu, a następnie ponownie prześle plik, bez żadnych podpowiedzi od CodeMender.

Czego się nauczysz

  • Jak uruchomić CodeMender bez interfejsu, aby ani hook, ani agent nie blokowały się na promptach [Y/n].
  • Co powstaje w pętli Planowanie → Czerwony → Zielony → Refaktoryzacja, gdy agent tworzy funkcję: threat_model.md, testy, kod i zatwierdzenie.
  • Jak brama zabezpieczeń reaguje, gdy przesłany plik zawiera otwarte wyniki, i jak agent używa umiejętności security-test-writer i defensive-developer, aby je usunąć.
  • Jak odczytać rejestr kontrolny pozostawiony przez bramę: .security-gate/findings-log.ndjson, automatycznie rozwijane konwencje w CONTEXT.md i historię Gita.

Wymagania wstępne

  • Obszar roboczy ~/codemender-lab z poprzednich 2 ćwiczeń z czystym drzewem roboczym Git w gałęzi main.
  • Zainstalowano Antigravity, a ~/codemender-lab można otworzyć jako folder.
  • git, jq i środowisko wirtualne Pythona z zainstalowanym Flask.

2. Weryfikacja konfiguracji z poprzednich ćwiczeń programistycznych

Krok 1. Wróć do katalogu modułów

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

Krok 2. Sprawdź, czy pliki platformy są na miejscu

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

Powinno być widocznych 6 katalogów umiejętności (defensive-developer, history-context-seeder, local-refactor-scanner, security-test-writer, skill-evolution-updater, threat-model-assessor), tests/__init__.py i tests/test_data_service.py, pusty plik git status oraz plik main.

Uruchom testy punktu zaczepienia offline jeszcze raz, aby upewnić się, że nic się nie zmieniło:

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

Krok 3. Potwierdź środowisko CodeMender

cm init --verify

Powinien on zgłaszać git jako typ VCS i python3 -m compileall -q . jako polecenie kompilacji, zgodnie z konfiguracją w Podstawach interfejsu wiersza poleceń CodeMender.

Krok 4. Usuń nieaktualne wyniki

cm clean

Gdy pojawi się prośba, wpisz y. Spowoduje to opróżnienie ~/.codemender/state.db.

Krok 5. Utwórz zdalne repozytorium, do którego chcesz przesyłać dane

Hook porównuje drzewo robocze z origin/main, a git push musi mieć miejsce docelowe. Wystarczy puste repozytorium na tym samym komputerze:

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

To wypchnięcie odbywa się w Twoim terminalu, więc Antigravity go nie widzi. Od tego momentu agent będzie przesyłać dane.

3. Uruchamianie agenta CodeMender bez interfejsu graficznego

Krok 1. Wyłącz prośby o potwierdzenie

Domyślnie CodeMender pyta [Y/n] przed każdym uruchomieniem polecenia i zapisaniem pliku. W haku nikt nie odpowiada. Otwórz ~/.codemender/config.yaml i ustaw obie flagi tools na false lub uruchom:

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

Wynikowa sekcja powinna wyglądać tak:

tools:
  confirm_commands: false
  confirm_writes: false

Krok 2. Przeprowadź weryfikację

cm init --verify

4. Prośba o dodanie funkcji

Krok 1. Otwórz obszar roboczy

Otwórz ~/codemender-lab jako folder w Antigravity i rozpocznij nową rozmowę. Zasada secure_tdd_workflow jest teraz częścią każdej tury, a 6 umiejętności jest dostępnych.

Krok 2. Przypisz zadanie agentowi

Wklej ten prompt. Zwróć uwagę, że nie ma tam nic o bezpieczeństwie. Kierowanie agenta w stronę modelowania zagrożeń, programowania opartego na testach i wzorców obronnych jest zadaniem reguły i umiejętności, a nie prompta:

Dodaj do sample_app/data_service.py punkt końcowy POST /update_profile, który umożliwia użytkownikowi aktualizowanie adresu e-mail. Akceptuje pola formularza user_id i email. Po wprowadzeniu zmiany i pomyślnym przejściu zestawu testów zatwierdź ją i prześlij do origin main.

Krok 3. Zatwierdź polecenia agenta

Zaczep działa poza systemem uprawnień narzędzi, ale polecenia agenta już nie. Spodziewaj się próśb o potwierdzenie dotyczących python3 -m unittest ..., git add/commit/push i wszystkich poleceń cm, które zostaną uruchomione. Zatwierdź je. Jeśli Antigravity zaproponuje zapamiętanie decyzji, zaakceptuj ją, aby pętla poprawek w dalszej części tego ćwiczenia nie zatrzymywała się na powtarzających się prośbach.

5. Co robi agent: planowanie, czerwony, zielony, refaktoryzacja

Poniżej znajdziesz przykładowe uruchomienie. Sformułowania, nazwy plików i dokładny kod agenta będą się różnić, ale to nie problem. To, co nie powinno się różnić, to kolejność: model zagrożeń przed testami, nieudane testy przed kodem, zielony zestaw przed zatwierdzeniem.

Faza A. Planowanie i modelowanie zagrożeń

Agent odczytuje plik CONTEXT.md, wczytuje plik threat-model-assessor i zapisuje plik threat_model.md w katalogu głównym obszaru roboczego:

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

Faza B. Nieudane testy (CZERWONY)

Agent wczytuje security-test-writer i tworzy tests/test_update_profile.py. Typowa wersja używa jednorazowego pliku users.db w katalogu roboczym, dlatego w poprzednim ćwiczeniu z programowania dodaliśmy users.db do .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)

Agent uruchamia polecenie python3 -m unittest discover -s tests i potwierdza, że nowe testy nie powiodły się z powodu błędów 404 != 200 i 404 != 400, ponieważ trasa jeszcze nie istnieje. To jest właściwy powód niepowodzenia. Niepowodzenie spowodowane błędem importu lub literówką nie jest CZERWONE – to uszkodzony test.

Etap C: minimalna implementacja (ZIELONY)

Agent wczytuje defensive-developer i dodaje punkt końcowy. Używa zapytania z parametrami, ponieważ CONTEXT.md wskazuje je jako wymagany wzorzec dla 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

Zestaw testów teraz przechodzi: 4 nowe testy oraz test charakterystyki z poprzedniego ćwiczenia z programowania.

Etap D. Refaktoryzacja, skanowanie, zatwierdzanie

Agent wczytuje local-refactor-scanner, sprawdza git diff, ponownie uruchamia cały zestaw i zatwierdza:

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. Bramka zabezpieczeń w przypadku polecenia git push

Pierwsze powiadomienie push zostało odrzucone.

Agent działa w git push origin main. Antigravity dopasowuje narzędzie run_command do hooks.json i uruchamia security_gate_hook.sh, które rozpoznaje push i skanuje zmienione pliki: sample_app/data_service.py, tests/test_update_profile.py i threat_model.md (CodeMender pomija pliki .md; cm find na tej ścieżce nie znajduje plików źródłowych i hook przechodzi dalej).

Potrwa to kilka minut. Punkt zaczepienia jest synchroniczny, więc tura agenta wydaje się zawieszać na poleceniu push, gdy działają cm find i cm report. Po zakończeniu skanowania brama znajduje wstrzyknięcie polecenia w pliku /ping, którego interfejs wiersza poleceń CodeMender CLI Basics nigdy nie naprawił. Jest to CRITICAL, jest to OPEN i znajduje się w przesyłanym pliku, więc skrypt hook odrzuca:

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

Przesłanie nie zostało wykonane. Agent widzi tekst reason jako wynik wywołania narzędzia, a bramka zarejestrowała zdarzenie DENIED_TO_AGENT dla tego wyniku.

Agent usuwa wynik test-first

W odrzuceniu wymieniono umiejętności, których należy użyć, a punkt 6 reguły (protokół działań naprawczych w przypadku wielu wyników) mówi, że należy rozpatrywać wyniki pojedynczo. W przypadku wyniku /ping reprezentatywne uruchomienie wygląda tak.

Nieudany test graniczny w 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)

Minimalna poprawka, która zastępuje os.system listą subprocess.run zgodnie z wymaganiami CONTEXT.md, oraz parser listy dozwolonych zamiast sprawdzania ciągów znaków:

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"

Agent uruchamia cały pakiet, zatwierdza zmiany, dodając wiadomość opisującą problem i jego rozwiązanie, i ponownie przesyła kod.

Drugie powiadomienie push jest dozwolone

Bramka ponownie skanuje sample_app/data_service.py. Wstrzyknięcie polecenia zostało usunięte. Nie ma już żadnych wyników blokujących, więc brama sprawdza własny log, widzi, że wynik, który wcześniej odrzuciła, już nie istnieje, i rejestruje wynik przed zezwoleniem na push:

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

Uruchamiane jest polecenie git push origin main, a zdalne repozytorium zawiera teraz 3 nowe commity: funkcję, poprawkę i commit docs(context) utworzony przez hook.

7. Przeglądanie rejestru kontrolnego

Wszystkie decyzje bramki są zapisywane na dysku w formie zwykłego tekstu. W terminalu w ~/codemender-lab:

Dziennik bramy

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}

Każdy wiersz zawiera też actor (Twój adres e-mail w git) i commit (krótki SHA w danym momencie), co pozwala później odpowiedzieć na pytanie „kto i kiedy na to zezwolił” bez bazy danych.

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

Ewolucja kontekstu

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.

Wpis elementu webhook jest krótki, ponieważ zawiera tylko identyfikator wyniku, plik i poziom ważności. Umiejętność skill-evolution-updater pozwala przekształcić to w regułę, którą napisałby człowiek. Poproś agenta: „Użyj skill-evolution-updater, aby przepisać ostatnią automatycznie rozwiniętą konwencję w pliku CONTEXT.md jako regułę projektu i zasugerować wszelkie zmiany w pliku SKILL.md, które z tego wynikają”.

Widok CodeMender

cm report

Wynik dotyczący wstrzyknięcia polecenia powinien teraz mieć stan inny niż OPEN, a plik threat_model.md i 2 nowe pliki testowe powinny znajdować się w repozytorium, aby można było utworzyć kolejną funkcję.

8. Czyszczenie danych

Przywróć domyślne ustawienia interaktywne CodeMendera:

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

Wyczyść stan lokalny CodeMendera (wpisz y w prompt):

cm clean

Aby usunąć też katalogi modułu:

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

Gratulacje! Udało Ci się ukończyć ćwiczenie z programowania dotyczące kompleksowego agentowego przepływu pracy związanego z bezpieczeństwem z użyciem CodeMender. W pierwszym ćwiczeniu pokazaliśmy interfejs wiersza poleceń, w drugim – umiejętności i punkt zaczepienia, a w tym – agenta, który obsługuje push, który został zablokowany, naprawiony, zarejestrowany i zezwolony bez prompta CodeMender w pętli.