1. Introduction
Dans cet atelier de programmation, vous allez combiner les deux ateliers de programmation précédents. Vous allez envoyer une demande de fonctionnalité à Antigravity, puis observer comment il planifie l'implémentation avec un modèle des menaces, écrit des tests qui échouent, implémente la fonctionnalité et effectue un push. Lors du push, la barrière de sécurité de l'atelier de programmation précédent exécute CodeMender sur les fichiers modifiés. Comme l'exemple d'application comporte toujours une injection de commande ouverte dans le fichier que l'agent modifie, le premier push est bloqué. Vous verrez l'agent effacer le résultat en testant d'abord, puis effectuer un nouveau push, sans aucune intervention humaine de CodeMender.
Objectifs de l'atelier
- Comment exécuter CodeMender sans interface graphique pour que ni le hook, ni l'agent ne bloquent les requêtes
[Y/n]. - Ce que la boucle Planifier → Rouge → Vert → Refactoriser produit lorsque l'agent crée une fonctionnalité :
threat_model.md, tests, code et commit. - Comment la barrière de sécurité réagit lorsqu'un fichier transféré présente des résultats ouverts, et comment l'agent utilise les compétences
security-test-writeretdefensive-developerpour les effacer. - Comment lire la piste d'audit laissée par la porte :
.security-gate/findings-log.ndjson, conventions auto-évoluées dansCONTEXT.mdet historique Git.
Prérequis
- L'espace de travail
~/codemender-labdes deux ateliers de programmation précédents, avec une arborescence de travail Git propre sur la branchemain. - Antigravity est installé, et
~/codemender-labest disponible pour être ouvert en tant que dossier. git,jqet l'environnement virtuel Python avec Flask installé.
2. Vérifier la configuration des ateliers de programmation précédents
Étape 1 : Revenez au répertoire de l'atelier
cd ~/codemender-lab && source .venv/bin/activate
Étape 2 : Vérifiez que les fichiers du framework sont en place
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
Vous devriez voir les six répertoires de compétences (defensive-developer, history-context-seeder, local-refactor-scanner, security-test-writer, skill-evolution-updater, threat-model-assessor), tests/__init__.py et tests/test_data_service.py, un répertoire git status vide et main.
Exécutez à nouveau les tests de hook hors connexion pour vérifier que rien n'a changé :
bash .agents/tests/run_tests.sh | tail -1
60 passed, 0 failed
Étape 3 : Confirmer l'environnement CodeMender
cm init --verify
Il doit indiquer git comme type de VCS et python3 -m compileall -q . comme commande de compilation, comme configuré dans les principes de base de la CLI CodeMender.
Étape 4 : Effacer les résultats obsolètes
cm clean
Saisissez y lorsque vous y êtes invité. Cela vide ~/.codemender/state.db.
Étape 5 : Créez un dépôt distant vers lequel effectuer un push
Le hook compare votre arborescence de travail à origin/main, et git push doit être stocké quelque part. Un dépôt nu sur la même machine suffit :
git init --bare ~/codemender-remote.git
git remote add origin ~/codemender-remote.git
git push -u origin main
Ce push se produit dans votre propre terminal, de sorte que le hook Antigravity ne le voit pas. À partir de là, l'agent effectue les pushs.
3. Exécuter CodeMender en mode headless
Éthought:
Par défaut, CodeMender demande [Y/n] avant chaque commande qu'il exécute et chaque fichier qu'il écrit. Dans un hook, personne n'est là pour répondre. Ouvrez ~/.codemender/config.yaml et définissez les deux options tools sur false, ou exécutez :
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 section obtenue doit se présenter comme suit :
tools:
confirm_commands: false
confirm_writes: false
Étape 2 : Valider
cm init --verify
4. La demande de fonctionnalité
Étape 1 : Ouvrez l'espace de travail
Ouvrez ~/codemender-lab en tant que dossier dans Antigravity et démarrez une nouvelle conversation. La règle secure_tdd_workflow toujours active fait désormais partie de chaque tour, et les six compétences sont disponibles.
Étape 2 : Confier la tâche à l'agent
Collez le prompt suivant. Notez qu'il n'est pas question de sécurité. C'est à la règle et aux compétences, et non au prompt, d'orienter l'agent vers la modélisation des menaces, le développement axé sur les tests et les modèles défensifs :
Ajoutez un point de terminaison POST /update_profile à sample_app/data_service.py pour permettre à un utilisateur de modifier son adresse e-mail. Il accepte user_id et email comme champs de formulaire. Une fois la modification effectuée et la suite de tests validée, effectuez un commit et un push vers origin main.
Étape 3 : Approuvez les commandes de l'agent
Le hook s'exécute en dehors du système d'autorisation des outils, mais pas les commandes de l'agent. Attendez-vous à des invites pour python3 -m unittest ..., git add/commit/push et toute commande cm qu'il décide d'exécuter. Approuvez-les. Si Antigravity propose de mémoriser la décision, acceptez-la afin que la boucle de correction plus loin dans cet atelier de programmation ne se bloque pas sur des invites répétées.
5. Ce que fait l'agent : Plan, Red, Green, Refactor
Voici un exemple d'exécution. La formulation, les noms de fichiers et le code exact de votre agent seront différents, et c'est normal. Ce qui ne doit pas changer, c'est l'ordre : un modèle des menaces avant les tests, des tests qui échouent avant le code, une suite verte avant le commit.
Phase A : Planification et modélisation des menaces
L'agent lit CONTEXT.md, charge threat-model-assessor et écrit threat_model.md à la racine de l'espace de travail :
# 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 : Tests qui échouent (ROUGE)
L'agent charge security-test-writer et crée tests/test_update_profile.py. Une version classique utilise un users.db jetable dans le répertoire de travail. C'est pourquoi l'atelier de programmation précédent a ajouté 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)
L'agent exécute python3 -m unittest discover -s tests et confirme que les nouveaux tests échouent avec 404 != 200 et 404 != 400, car la route n'existe pas encore. C'est la bonne raison d'échouer. Un échec causé par une erreur d'importation ou une faute de frappe n'est pas un échec (RED), mais un test défectueux.
Phase C : Implémentation minimale (VERT)
L'agent charge defensive-developer et ajoute le point de terminaison. Elle utilise une requête paramétrée, car CONTEXT.md l'indique comme modèle requis pour 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 est désormais validée : les quatre nouveaux tests ainsi que le test de caractérisation de l'atelier de programmation précédent.
Phase D : Refactoriser, analyser, valider
L'agent charge local-refactor-scanner, examine git diff, réexécute la suite complète et effectue un 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. La barrière de sécurité sur git push
La première notification push est refusée.
L'agent exécute git push origin main. Antigravity met en correspondance l'outil run_command avec hooks.json et exécute security_gate_hook.sh, qui reconnaît le push et analyse les fichiers modifiés : sample_app/data_service.py, tests/test_update_profile.py et threat_model.md (CodeMender ignore les fichiers .md ; cm find sur ce chemin ne trouve aucun fichier source et le hook passe à la suite).
L'opération prend quelques minutes. Le hook est synchrone. Le tour de l'agent semble donc se bloquer sur la commande push pendant l'exécution de cm find et cm report. Une fois l'analyse terminée, la porte d'accès détecte l'injection de commande dans /ping que CodeMender CLI Basics n'a jamais corrigée. Il s'agit de CRITICAL, il s'agit de OPEN et il se trouve dans un fichier en cours de transfert. Le hook refuse donc :
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", ...}
Le push n'a pas été exécuté. L'agent voit le texte reason comme résultat de son appel d'outil, et la porte a enregistré un événement DENIED_TO_AGENT pour ce résultat.
L'agent efface le résultat test-first
Le refus indique les compétences à utiliser, et l'élément de règle 6 (Protocole de correction de plusieurs résultats) indique de traiter les résultats un par un. Pour le résultat /ping, une exécution représentative se présente comme suit.
Un test de limite qui échoue dans 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 correction minimale, qui consiste à remplacer os.system par un subprocess.run sous forme de liste, comme l'exige CONTEXT.md, ainsi qu'un analyseur de liste d'autorisation au lieu de vérifications de chaînes :
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'agent exécute la suite complète, effectue un commit avec un message décrivant le problème et le correctif, puis effectue un nouveau push.
Le deuxième push est autorisé.
La porte d'accès réanalyse sample_app/data_service.py. L'injection de commande a disparu. Comme il ne reste plus de résultats bloquants, la porte d'accès vérifie son propre journal, constate que le résultat qu'elle a refusé précédemment n'est plus présent et enregistre le résultat avant d'autoriser le 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 s'exécute, et le dépôt distant comporte désormais trois nouveaux commits : la fonctionnalité, le correctif et un commit docs(context) effectué par le hook lui-même.
7. Examiner la piste d'audit
Tout ce que la porte a décidé est sur le disque en texte brut. Depuis un terminal dans ~/codemender-lab :
Le journal de la porte
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}
Chaque ligne contient également actor (votre adresse e-mail Git) et commit (SHA court à ce moment-là), ce qui permet de répondre ultérieurement à la question "Qui a autorisé cela et quand" sans base de données.
Historique 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
...
Le contexte évolué
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.
L'entrée du hook est concise, car elle ne connaît que l'ID du résultat, le fichier et la gravité. La compétence skill-evolution-updater permet de transformer cela en une règle qu'un humain pourrait écrire. Demandez à l'agent : "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."
Vue de CodeMender
cm report
Le résultat d'injection de commande devrait maintenant afficher un état autre que OPEN, et threat_model.md ainsi que les deux nouveaux fichiers de test se trouvent dans le dépôt pour la prochaine fonctionnalité à créer.
8. Nettoyage
Restaurez les paramètres par défaut interactifs de 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
Effacez l'état local de CodeMender (saisissez y dans l'invite) :
cm clean
Pour supprimer également les répertoires de l'atelier :
cd ~ && rm -rf ~/codemender-lab ~/codemender-remote.git
Félicitations ! Vous avez terminé l'atelier de programmation "Workflow de sécurité agentique de bout en bout avec CodeMender". Vous avez vu la CLI dans le premier atelier de programmation, les compétences et le hook dans le deuxième, et un agent traitant un push qui a été bloqué, corrigé, consigné et autorisé sans qu'un prompt CodeMender n'intervienne dans la boucle.