CodeMender를 사용한 엔드 투 엔드 에이전트형 보안 워크플로

1. 소개

이 Codelab에서는 이전 두 Codelab을 결합합니다. Antigravity에 기능 요청을 제공한 다음 위협 모델을 사용하여 계획하고, 실패하는 테스트를 작성하고, 기능을 구현하고, 푸시하는 과정을 지켜봅니다. 푸시하면 이전 Codelab의 보안 게이트가 변경된 파일에 대해 CodeMender를 실행합니다. 샘플 애플리케이션에는 에이전트가 수정 중인 파일에 여전히 열려 있는 명령어 삽입이 있으므로 첫 번째 푸시가 차단되고, 에이전트가 발견 항목을 테스트 우선으로 지우고 다시 푸시하는 것을 볼 수 있습니다. 이 과정에서 CodeMender의 사람 프롬프트는 없습니다.

학습할 내용

  • CodeMender를 헤드리스로 실행하여 후크나 에이전트가 [Y/n] 프롬프트에서 차단되지 않도록 하는 방법
  • 에이전트가 기능을 빌드할 때 계획 → 빨간색 → 녹색 → 리팩터링 루프가 생성하는 것: threat_model.md, 테스트, 코드, 커밋
  • 푸시된 파일에 미해결 발견 사항이 있을 때 보안 게이트가 응답하는 방식과 에이전트가 security-test-writer 및 defensive-developer 기술을 사용하여 이를 해결하는 방법
  • 게이트가 남기는 감사 추적(.security-gate/findings-log.ndjson, CONTEXT.md의 자동 진화 규칙, git 기록)을 읽는 방법

기본 요건

  • 이전 두 Codelab의 ~/codemender-lab 작업공간(main 브랜치에 깨끗한 git 작업 트리가 있음)
  • Antigravity가 설치되었으며 ~/codemender-lab을(를) 폴더로 열 수 있습니다.
  • git, jq, Flask가 설치된 Python 가상 환경

2. 이전 Codelab의 설정 확인

1단계: 실습 디렉터리로 돌아가기

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

2단계: 프레임워크 파일이 제자리에 있는지 확인하기

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

6개의 기술 디렉터리(defensive-developer, history-context-seeder, local-refactor-scanner, security-test-writer, skill-evolution-updater, threat-model-assessor), tests/__init__.py, tests/test_data_service.py, 빈 git status, main가 표시됩니다.

오프라인 후크 테스트를 한 번 더 실행하여 드리프트가 없는지 확인합니다.

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

3단계: CodeMender 환경 확인

cm init --verify

CodeMender CLI 기본사항에서 구성한 대로 VCS 유형으로 git, 빌드 명령어로 python3 -m compileall -q .가 보고되어야 합니다.

4단계: 오래된 발견 항목 삭제

cm clean

메시지가 표시되면 y를 입력합니다. ~/.codemender/state.db이(가) 비워집니다.

5단계: 푸시할 원격 저장소 만들기

후크는 작업 트리를 origin/main와 비교하며 git push는 이동할 위치가 필요합니다. 동일한 머신의 베어 저장소로 충분합니다.

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

이 푸시는 자체 터미널에서 발생하므로 Antigravity 후크는 이를 인식하지 못합니다. 이제부터는 에이전트가 푸시를 수행합니다.

3. CodeMender 헤드리스 실행

1단계: 확인 메시지 사용 중지하기

기본적으로 CodeMender는 실행하는 모든 명령어와 작성하는 모든 파일에 대해 [Y/n]에 확인을 요청합니다. 후크 안에는 응답할 사람이 없습니다. ~/.codemender/config.yaml를 열고 두 tools 플래그를 모두 false로 설정하거나 다음을 실행합니다.

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

결과 섹션은 다음과 같이 표시됩니다.

tools:
  confirm_commands: false
  confirm_writes: false

2단계: 인증

cm init --verify

4. 기능 요청

1단계: 작업공간 열기

Antigravity에서 ~/codemender-lab를 폴더로 열고 새 대화를 시작합니다. 이제 상시 사용 secure_tdd_workflow 규칙이 모든 차례에 적용되며 6가지 스킬을 사용할 수 있습니다.

2단계: 에이전트에 작업 부여

다음 프롬프트를 붙여넣습니다. 보안에 관한 내용은 없습니다. 에이전트가 위협 모델링, 테스트 우선 개발, 방어 패턴을 따르도록 유도하는 것은 프롬프트가 아닌 규칙과 기술의 역할입니다.

사용자가 이메일 주소를 업데이트할 수 있도록 sample_app/data_service.py에 POST /update_profile 엔드포인트를 추가합니다. user_id 및 email를 양식 필드로 허용합니다. 변경이 완료되고 테스트 모음이 통과되면 커밋하고 origin main에 푸시합니다.

3단계: 에이전트의 명령어 승인하기

후크는 도구 권한 시스템 외부에서 실행되지만 에이전트 자체 명령어는 그렇지 않습니다. python3 -m unittest ..., git add/commit/push, 실행하기로 결정한 cm 명령어에 대한 프롬프트가 표시됩니다. 승인합니다. Antigravity에서 결정을 기억하겠다고 제안하면 이를 수락하여 이 Codelab의 후반부에 있는 수정 루프가 반복되는 프롬프트에서 중단되지 않도록 합니다.

5. 에이전트가 하는 일: 계획, 레드, 그린, 리팩터링

다음은 대표적인 실행입니다. 에이전트의 문구, 파일 이름, 정확한 코드는 다를 수 있으며 이는 정상입니다. 달라지지 않아야 하는 것은 순서입니다. 테스트 전 위협 모델, 코드 전 실패 테스트, 커밋 전 그린 스위트입니다.

A단계: 계획 및 위협 모델링

에이전트는 작업공간 루트에서 CONTEXT.md를 읽고, threat-model-assessor를 로드하고, threat_model.md를 작성합니다.

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

B단계: 실패 테스트(RED)

에이전트가 security-test-writer를 로드하고 tests/test_update_profile.py를 만듭니다. 일반적인 버전은 작업 디렉터리에서 일회용 users.db를 사용하므로 이전 Codelab에서 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)

에이전트는 python3 -m unittest discover -s tests를 실행하고 경로가 아직 존재하지 않기 때문에 새 테스트가 404 != 200 및 404 != 400로 실패하는 것을 확인합니다. 실패하는 것이 당연합니다. 가져오기 오류나 오타로 인한 실패는 RED가 아니라 손상된 테스트입니다.

C단계: 최소 구현(녹색)

에이전트가 defensive-developer를 로드하고 엔드포인트를 추가합니다. CONTEXT.md에서 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

이제 도구 모음이 통과됩니다. 4개의 새 테스트와 이전 Codelab의 특성화 테스트가 통과됩니다.

D단계: 리팩터링, 스캔, 커밋

에이전트가 local-refactor-scanner를 로드하고, git diff를 검토하고, 전체 도구 모음을 다시 실행하고, 커밋합니다.

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. git push의 보안 게이트

첫 번째 푸시가 거부됨

에이전트는 git push origin main에 실행됩니다. Antigravity는 run_command 도구를 hooks.json와 일치시키고 security_gate_hook.sh를 실행합니다. 이 도구는 푸시를 인식하고 변경된 파일(sample_app/data_service.py, tests/test_update_profile.py, threat_model.md)을 스캔합니다(CodeMender는 .md 파일을 건너뜁니다. 해당 경로의 cm find는 소스 파일을 찾지 못하고 후크가 계속 진행됩니다).

몇 분 정도 걸릴 수 있습니다. 후크는 동기식이므로 cm find 및 cm report가 실행되는 동안 에이전트의 차례가 푸시 명령어에서 중단된 것처럼 보입니다. 스캔이 완료되면 게이트는 CodeMender CLI Basics에서 수정하지 않은 /ping의 명령어 삽입을 찾습니다. CRITICAL이고 OPEN이며 푸시되는 파일에 있으므로 후크가 거부합니다.

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

푸시가 실행되지 않았습니다. 에이전트는 도구 호출의 결과로 reason 텍스트를 확인하고, 게이트는 이 발견 사항에 대해 DENIED_TO_AGENT 이벤트를 로깅합니다.

에이전트가 테스트 우선 발견 항목을 삭제합니다.

거부 사유에는 사용할 스킬이 명시되어 있으며, 규칙 항목 6(다중 발견 항목 해결 프로토콜)에는 발견 항목을 한 번에 하나씩 처리하라고 명시되어 있습니다. /ping 발견 항목의 경우 대표 실행은 다음과 같습니다.

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)

CONTEXT.md에 필요한 대로 os.system를 목록 형식 subprocess.run로 바꾸고 문자열 검사 대신 허용 목록 파서를 사용하는 최소한의 수정:

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"

에이전트는 전체 도구 모음을 실행하고 발견 사항과 수정사항을 설명하는 메시지와 함께 커밋하고 다시 푸시합니다.

두 번째 푸시가 허용됨

게이트에서 sample_app/data_service.py를 다시 스캔합니다. 명령어 삽입이 사라졌습니다. 차단 발견 항목이 남아 있지 않으므로 게이트는 자체 로그를 확인하고 이전에 거부한 발견 항목이 더 이상 존재하지 않는지 확인하고 결과를 기록한 후 푸시를 허용합니다.

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가 실행되고 원격에는 이제 세 개의 새 커밋(기능, 수정, 후크가 직접 만든 docs(context) 커밋)이 있습니다.

7. 감사 추적 검토

게이트에서 결정한 모든 항목은 디스크에 일반 텍스트로 저장됩니다. ~/codemender-lab의 터미널에서 다음을 실행합니다.

게이트 로그

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}

각 줄에는 actor(Git 이메일) 및 commit(당시의 짧은 SHA)도 포함되어 있으므로 나중에 데이터베이스 없이도 '누가 언제 이를 허용했는지'에 관한 질문에 답할 수 있습니다.

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

진화된 컨텍스트

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.

후크의 항목은 발견 항목 ID, 파일, 심각도만 알고 있으므로 간결합니다. skill-evolution-updater 기술은 이를 사람이 작성하는 규칙으로 변환하기 위해 존재합니다. 에이전트에게 다음과 같이 요청합니다. "skill-evolution-updater를 사용하여 CONTEXT.md의 마지막 자동 진화 규칙을 프로젝트 규칙으로 다시 작성하고, 이로 인해 발생하는 SKILL.md 변경사항을 제안해 줘."

CodeMender의 뷰

cm report

이제 명령어 삽입 발견 항목에 OPEN가 아닌 상태가 표시되고 threat_model.md와 두 개의 새 테스트 파일이 다음 기능을 빌드할 저장소에 있습니다.

8. 삭제

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

CodeMender의 로컬 상태를 지웁니다(프롬프트에 y 입력).

cm clean

실습 디렉터리도 삭제하려면 다음을 실행합니다.

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

수고하셨습니다 CodeMender를 사용한 엔드 투 엔드 에이전트형 보안 워크플로 Codelab을 완료했습니다. 첫 번째 Codelab에서는 CLI를, 두 번째 Codelab에서는 기술과 후크를, 세 번째 Codelab에서는 루프에 CodeMender 프롬프트 없이 차단, 수정, 로깅, 허용된 푸시를 통해 작동하는 에이전트를 살펴보았습니다.