Workflow di sicurezza agentica end-to-end con CodeMender

1. Introduzione

In questo codelab, combinerai i due codelab precedenti. Fornirai ad Antigravity una richiesta di funzionalità, poi osserverai come pianifica con un modello di minacce, scrive test non riusciti, implementa la funzionalità ed esegue il push. Al momento del push, il controllo di sicurezza del codelab precedente esegue CodeMender sui file modificati. Poiché l'applicazione di esempio ha ancora un'inserimento di comandi aperta nel file che l'agente sta modificando, il primo push viene bloccato e vedrai l'agente cancellare il risultato test-first ed eseguire nuovamente il push, senza prompt umani da CodeMender lungo il percorso.

Cosa imparerai a fare

  • Come eseguire CodeMender in modalità headless in modo che né l'hook né l'agente si blocchino sui prompt di [Y/n].
  • Cosa produce il ciclo Pianifica → Rosso → Verde → Rifattorizza quando l'agente crea una funzionalità: threat_model.md, test, codice e un commit.
  • Come risponde il controllo di sicurezza quando un file di cui è stato eseguito il push ha risultati aperti e come l'agente utilizza le competenze security-test-writer e defensive-developer per cancellarli.
  • Come leggere la traccia di audit lasciata dal gate: .security-gate/findings-log.ndjson, convenzioni auto-evolute in CONTEXT.md e cronologia git.

Prerequisiti

  • L'area di lavoro ~/codemender-lab dei due codelab precedenti, con un albero di lavoro Git pulito sul branch main.
  • Antigravity installato, con ~/codemender-lab disponibile per l'apertura come cartella.
  • git, jq e l'ambiente virtuale Python con Flask installato.

2. Verifica della configurazione dei codelab precedenti

Passaggio 1: torna alla directory del lab

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

Passaggio 2: verifica che i file del framework siano presenti

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

Dovresti vedere le sei directory delle competenze (defensive-developer, history-context-seeder, local-refactor-scanner, security-test-writer, skill-evolution-updater, threat-model-assessor), tests/__init__.py e tests/test_data_service.py, una directory git status vuota e main.

Esegui un'altra volta i test degli hook offline per verificare che non ci siano deviazioni:

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

Passaggio 3: conferma l'ambiente CodeMender

cm init --verify

Dovrebbe segnalare git come tipo di VCS e python3 -m compileall -q . come comando di build, come configurato in CodeMender CLI Basics.

Passaggio 4: cancella i risultati obsoleti

cm clean

Digita y quando richiesto. In questo modo svuoti ~/.codemender/state.db.

Passaggio 5: crea un repository remoto su cui eseguire il push

L'hook confronta l'albero di lavoro con origin/main e git push deve avere una destinazione. È sufficiente un repository bare sulla stessa macchina:

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

Questo push avviene nel tuo terminale, quindi l'hook Antigravity non lo vede. Da questo momento in poi, l'agente esegue i push.

3. Esecuzione di CodeMender in modalità headless

Passaggio 1: disattiva le richieste di conferma

Per impostazione predefinita, CodeMender chiede [Y/n] prima di ogni comando che esegue e di ogni file che scrive. All'interno di un hook, non c'è nessuno che risponde. Apri ~/.codemender/config.yaml e imposta entrambi i flag tools su false oppure esegui:

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

La sezione risultante dovrebbe essere la seguente:

tools:
  confirm_commands: false
  confirm_writes: false

Passaggio 2: verifica

cm init --verify

4. La richiesta di funzionalità

Passaggio 1: apri l'area di lavoro

Apri ~/codemender-lab come cartella in Antigravity e avvia una nuova conversazione. La regola secure_tdd_workflow sempre attiva ora fa parte di ogni turno e le sei competenze sono disponibili.

Passaggio 2: assegna l'attività all'agente

Incolla il seguente prompt. Come vedi, non si parla di sicurezza. Indirizzare l'agente verso la modellazione delle minacce, lo sviluppo test-first e i pattern difensivi è compito della regola e delle competenze, non del prompt:

Aggiungi un endpoint POST /update_profile a sample_app/data_service.py che consenta a un utente di aggiornare il proprio indirizzo email. Accetta user_id e email come campi del modulo. Una volta completata la modifica e superata la suite di test, esegui il commit e il push su origin main.

Passaggio 3: approva i comandi dell'agente

L'hook viene eseguito al di fuori del sistema di autorizzazioni degli strumenti, ma i comandi dell'agente no. Aspettati prompt per python3 -m unittest ..., git add/commit/push e qualsiasi comando cm che decida di eseguire. Approvale. Se Antigravity offre di ricordare la decisione, accettala in modo che il ciclo di correzione più avanti in questo codelab non si blocchi su prompt ripetuti.

5. Cosa fa l'agente: pianifica, rosso, verde, rifattorizza

Di seguito è riportata un'esecuzione rappresentativa. La formulazione, i nomi dei file e il codice esatto dell'agente saranno diversi, ma non è un problema. Ciò che non deve cambiare è l'ordine: un modello di minacce prima dei test, test non superati prima del codice, una suite verde prima del commit.

Fase A: pianificazione e modello di minacce

L'agente legge CONTEXT.md, carica threat-model-assessor e scrive threat_model.md nella directory principale del workspace:

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

Fase B: test non superati (ROSSO)

L'agente carica security-test-writer e crea tests/test_update_profile.py. Una versione tipica utilizza un file users.db usa e getta nella directory di lavoro, motivo per cui il codelab precedente ha aggiunto users.db a .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)

L'agente esegue python3 -m unittest discover -s tests e conferma che i nuovi test non vanno a buon fine con 404 != 200 e 404 != 400, perché la route non esiste ancora. Questo è il motivo giusto per fallire. Un errore causato da un errore di importazione o da un errore di battitura non è un errore RED, ma un test non funzionante.

Fase C: implementazione minima (VERDE)

L'agente carica defensive-developer e aggiunge l'endpoint. Utilizza una query con parametri perché CONTEXT.md lo indica come pattern richiesto per 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

La suite ora viene superata: i quattro nuovi test più il test di caratterizzazione del codelab precedente.

Fase D: rifattorizza, scansiona, esegui il commit

L'agente carica local-refactor-scanner, esamina git diff, esegue nuovamente la suite completa ed esegue il commit:

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. Il controllo di sicurezza su git push

La prima notifica push viene rifiutata

L'agente viene eseguito git push origin main. Antigravity abbina lo strumento run_command a hooks.json ed esegue security_gate_hook.sh, che riconosce il push ed esegue la scansione dei file modificati: sample_app/data_service.py, tests/test_update_profile.py e threat_model.md (CodeMender ignora i file .md; cm find in quel percorso non trova file di origine e l'hook va avanti).

L'operazione richiede alcuni minuti. L'hook è sincrono, quindi il turno dell'agente sembra bloccarsi sul comando push mentre vengono eseguiti cm find e cm report. Al termine della scansione, il gate trova l'inserimento di comandi in /ping che CodeMender CLI Basics non ha mai corretto. È CRITICAL, è OPEN ed è in un file di cui viene eseguito il push, quindi l'hook nega:

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

Il push non è stato eseguito. L'agente vede il testo reason come risultato della chiamata allo strumento e il gate ha registrato un evento DENIED_TO_AGENT per questo risultato.

L'agente cancella il risultato test-first

Il rifiuto indica le skill da utilizzare e l'elemento della regola 6 (Protocollo di correzione di più risultati) indica di prendere i risultati uno alla volta. Per il risultato /ping, un'esecuzione rappresentativa è simile alla seguente.

Un test di confine non riuscito 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)

La correzione minima, che sostituisce os.system con un subprocess.run in formato elenco come richiesto da CONTEXT.md, oltre a un parser della lista consentita anziché controlli delle stringhe:

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"

L'agente esegue la suite completa, esegue il commit con un messaggio che descrive il risultato e la correzione ed esegue nuovamente il push.

Il secondo push è consentito

Il gate esegue nuovamente la scansione di sample_app/data_service.py. L'inserimento di comandi è stato rimosso. Non rimangono risultati bloccanti, quindi il gate controlla il proprio log, vede che il risultato negato in precedenza non è più presente e registra il risultato prima di consentire il 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"}

git push origin main viene eseguito e il repository remoto ora ha tre nuovi commit: la funzionalità, la correzione e un commit docs(context) eseguito dall'hook stesso.

7. Esamina la traccia di audit

Tutto ciò che il gate ha deciso è su disco in testo normale. Da un terminale in ~/codemender-lab:

Log del gate

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}

Ogni riga contiene anche actor (la tua email git) e commit (SHA breve al momento), il che consente di rispondere a una domanda successiva del tipo "chi ha consentito questo e quando" senza un database.

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

Il contesto evoluto

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.

La voce dell'hook è concisa perché conosce solo l'ID, il file e la gravità del risultato. La skill skill-evolution-updater serve a trasformare questo in una regola che una persona scriverebbe. Chiedi all'agente: "Utilizza skill-evolution-updater per riscrivere l'ultima convenzione evoluta automaticamente in CONTEXT.md come regola di progetto e suggerisci qualsiasi modifica di SKILL.md che implica."

Visualizzazione di CodeMender

cm report

Il risultato dell'inserimento di comandi dovrebbe ora mostrare uno stato diverso da OPEN e threat_model.md, oltre ai due nuovi file di test, si trovano nel repository per la prossima funzionalità da creare.

8. Pulizia

Ripristina le impostazioni predefinite interattive di 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

Cancella lo stato locale di CodeMender (digita y al prompt):

cm clean

Per rimuovere anche le directory del lab:

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

Complimenti! Hai completato il codelab Workflow di sicurezza agentico end-to-end con CodeMender. Hai visto l'interfaccia a riga di comando del primo codelab, le competenze e l'hook del secondo e un agente che lavora su un push che è stato bloccato, corretto, registrato e consentito senza un prompt di CodeMender nel loop.