1. Introdução
Neste codelab, você vai juntar os dois codelabs anteriores. Você vai enviar uma solicitação de recurso para o Antigravity e acompanhar o planejamento com um modelo de ameaças, a criação de testes com falha, a implementação do recurso e o push. No push, o portão de segurança do codelab anterior executa o CodeMender nos arquivos modificados. Como o aplicativo de exemplo ainda tem uma injeção de comando aberta no arquivo que o agente está editando, o primeiro push é bloqueado. Você vai ver o agente limpar a descoberta test-first e fazer o push novamente, sem comandos humanos do CodeMender ao longo do processo.
O que você vai aprender
- Como executar o CodeMender de forma headless para que nem o hook nem o agente bloqueiem os comandos
[Y/n]. - O que o ciclo Planejar → Vermelho → Verde → Refatorar produz quando o agente cria um recurso:
threat_model.md, testes, código e um commit. - Como o portão de segurança responde quando um arquivo enviado tem descobertas abertas e como o agente usa as habilidades
security-test-writeredefensive-developerpara resolvê-las. - Como ler a trilha de auditoria que o portão deixa para trás:
.security-gate/findings-log.ndjson, convenções autoevolutivas emCONTEXT.mde histórico do Git.
Pré-requisitos
- O espaço de trabalho
~/codemender-labdos dois codelabs anteriores, com uma árvore de trabalho limpa do Git na ramificaçãomain. - O Antigravity instalado, com o
~/codemender-labdisponível para ser aberto como uma pasta. git,jqe o ambiente virtual do Python com o Flask instalado.
2. Como verificar a configuração dos codelabs anteriores
Etapa 1: voltar ao diretório do laboratório
cd ~/codemender-lab && source .venv/bin/activate
Etapa 2: confirmar se os arquivos do framework estão no lugar
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
Você verá os seis diretórios de habilidades (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, um git status vazio e main.
Execute os testes de hook off-line mais uma vez para confirmar que nada mudou:
bash .agents/tests/run_tests.sh | tail -1
60 passed, 0 failed
Etapa 3: confirmar o ambiente do CodeMender
cm init --verify
Ele deve informar git como o tipo de VCS e python3 -m compileall -q . como o comando de build, conforme configurado em Noções básicas da CLI do CodeMender.
Etapa 4: limpar as descobertas desatualizadas
cm clean
Digite y quando solicitado. Isso esvazia ~/.codemender/state.db.
Etapa 5: criar um repositório remoto para enviar por push
O hook compara sua árvore de trabalho com origin/main, e git push precisa de um destino. Um repositório bare na mesma máquina é suficiente:
git init --bare ~/codemender-remote.git
git remote add origin ~/codemender-remote.git
git push -u origin main
Esse push acontece no seu próprio terminal, então o hook Antigravity não o vê. A partir de agora, o agente faz os pushes.
3. Como executar o CodeMender Headless
Etapa 1: desativar as solicitações de confirmação
Por padrão, o CodeMender pergunta [Y/n] antes de cada comando que executa e de cada arquivo que grava. Dentro de um gancho, não há ninguém para responder. Abra ~/.codemender/config.yaml e defina as duas flags tools como false ou execute:
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
A seção resultante deve ser:
tools:
confirm_commands: false
confirm_writes: false
Etapa 2: verificar
cm init --verify
4. A solicitação de recurso
Etapa 1: abrir o espaço de trabalho
Abra ~/codemender-lab como uma pasta no Antigravity e inicie uma nova conversa. A regra secure_tdd_workflow sempre ativa agora faz parte de cada turno, e as seis habilidades estão disponíveis.
Etapa 2: atribuir a tarefa ao agente
Cole o seguinte comando. Observe que não há nada sobre segurança. Direcionar o agente para a modelagem de ameaças, o desenvolvimento focado em testes e os padrões defensivos é o trabalho da regra e das habilidades, não do comando:
Adicione um endpoint POST /update_profile a sample_app/data_service.py que permita que um usuário atualize o endereço de e-mail. Ele aceita user_id e email como campos de formulário. Quando a mudança for concluída e o pacote de testes for aprovado, confirme e envie para origin main.
Etapa 3: aprovar os comandos do agente
O hook é executado fora do sistema de permissões de ferramentas, mas os comandos do agente não. Espere solicitações para python3 -m unittest ..., git add/commit/push e qualquer comando cm que ele decida executar. Aprove-as. Se o Antigravity oferecer a opção de lembrar a decisão, aceite para que o loop de correção mais adiante neste codelab não seja interrompido por solicitações repetidas.
5. O que o agente faz: planejar, vermelho, verde, refatorar
Veja a seguir uma execução representativa. A redação, os nomes de arquivos e o código exato do seu agente serão diferentes, e não há problema nisso. O que não deve mudar é a ordem: um modelo de ameaças antes dos testes, testes com falha antes do código, um conjunto verde antes do commit.
Fase A: planejar e modelar ameaças
O agente lê CONTEXT.md, carrega threat-model-assessor e grava threat_model.md na raiz do espaço de trabalho:
# 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: testes com falha (VERMELHO)
O agente carrega security-test-writer e cria tests/test_update_profile.py. Uma versão típica usa um users.db descartável no diretório de trabalho. Por isso, o codelab anterior adicionou 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)
O agente executa python3 -m unittest discover -s tests e confirma que os novos testes falham com 404 != 200 e 404 != 400, porque a rota ainda não existe. Esse é o motivo certo para falhar. Uma falha causada por um erro de importação ou um erro de digitação não é VERMELHA, é um teste quebrado.
Fase C: implementação mínima (VERDE)
O agente carrega defensive-developer e adiciona o endpoint. Ele usa uma consulta parametrizada porque o CONTEXT.md nomeia isso como o padrão necessário para 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
O pacote agora é aprovado: os quatro novos testes e o teste de caracterização do codelab anterior.
Fase D: refatorar, verificar, confirmar
O agente carrega local-refactor-scanner, analisa git diff, executa novamente o pacote completo e faz o 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. O Security Gate no git push
O primeiro push é negado
O agente executa git push origin main. O Antigravity compara a ferramenta run_command com hooks.json e executa security_gate_hook.sh, que reconhece o push e verifica os arquivos alterados: sample_app/data_service.py, tests/test_update_profile.py e threat_model.md (o CodeMender ignora os arquivos .md; cm find nesse caminho não encontra arquivos de origem, e o hook segue em frente).
Isso levará alguns minutos. O hook é síncrono, então a vez do agente parece travar no comando push enquanto cm find e cm report são executados. Quando a verificação termina, o portão encontra a injeção de comando em /ping que o CodeMender CLI Basics nunca corrigiu. É CRITICAL, é OPEN e está em um arquivo que está sendo enviado, então o 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", ...}
O push não foi executado. O agente vê o texto reason como o resultado da chamada de ferramenta, e o portão registrou um evento DENIED_TO_AGENT para essa descoberta.
O agente limpa a descoberta test-first
A rejeição nomeia as habilidades que devem ser usadas, e o item 6 da regra (Protocolo de correção de várias descobertas) diz para analisar as descobertas uma por uma. Para a descoberta /ping, uma execução representativa é assim.
Um teste de limite com falha em 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)
A correção mínima, substituindo os.system por um subprocess.run em forma de lista, conforme exigido por CONTEXT.md, além de um analisador de lista de permissões em vez de verificações de string:
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"
O agente executa o pacote completo, faz um commit com uma mensagem descrevendo a descoberta e a correção e envia novamente.
O segundo push é permitido
O portão verifica novamente sample_app/data_service.py. A injeção de comando desapareceu. Não há mais descobertas de bloqueio. Portanto, o portão verifica o próprio registro, vê que a descoberta negada anteriormente não está mais presente e registra o resultado antes de permitir o 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"}
O git push origin main é executado, e o repositório remoto agora tem três novos commits: o recurso, a correção e um commit docs(context) que o hook fez por conta própria.
7. Análise da trilha de auditoria
Tudo o que o portão decidiu está no disco em texto simples. Em um terminal no ~/codemender-lab:
O registro do portão
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}
Cada linha também contém actor (seu e-mail do Git) e commit (SHA curto no momento), o que permite responder à pergunta "quem permitiu isso e quando" sem um banco de dados.
Histórico do 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
...
O contexto evoluído
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.
A entrada do hook é concisa porque ele só conhece o ID, o arquivo e a gravidade da descoberta. A habilidade skill-evolution-updater existe para transformar isso em uma regra que um humano escreveria. Peça ao agente: "Use o skill-evolution-updater para reescrever a última convenção autoevolutiva em CONTEXT.md como uma regra de projeto e sugira qualquer mudança em SKILL.md que isso implique."
Visualização do CodeMender
cm report
A descoberta de injeção de comando agora deve mostrar um status diferente de OPEN, e threat_model.md, além dos dois novos arquivos de teste, estão no repositório para o próximo recurso ser criado.
8. Como fazer a limpeza
Restaurar os padrões interativos do 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
Limpe o estado local do CodeMender (digite y no prompt):
cm clean
Para remover também os diretórios do laboratório:
cd ~ && rm -rf ~/codemender-lab ~/codemender-remote.git
Parabéns! Você concluiu o codelab "Fluxo de trabalho de segurança agêntica de ponta a ponta com o CodeMender". Você viu a CLI no primeiro codelab, as habilidades e o hook no segundo e um agente trabalhando em um push que foi bloqueado, corrigido, registrado e permitido sem um comando do CodeMender no loop.