1. はじめに
この Codelab では、前の 2 つの Codelab を組み合わせます。Antigravity に機能リクエストを送信し、脅威モデルを使用して計画を立て、失敗するテストを作成し、機能を実装して push する様子を確認します。push 時に、前の Codelab のセキュリティ ゲートが、変更されたファイルに対して CodeMender を実行します。サンプル アプリケーションでは、エージェントが編集しているファイルにコマンド インジェクションがまだ開いているため、最初の push がブロックされます。エージェントが検出結果をテストファーストでクリアし、再度 push することがわかります。その間、CodeMender からの人間によるプロンプトはありません。
学習内容
- フックもエージェントも
[Y/n]プロンプトでブロックされないように、CodeMender をヘッドレスで実行する方法。 - エージェントが機能を構築する際に、計画 → 赤 → 緑 → リファクタリングのループによって生成されるもの:
threat_model.md、テスト、コード、commit。 - push されたファイルに未解決の検出結果がある場合のセキュリティ ゲートの対応と、エージェントが
security-test-writerスキルとdefensive-developerスキルを使用してそれらをクリアする方法。 - ゲートが残した監査証跡の読み方:
.security-gate/findings-log.ndjson、CONTEXT.mdで自動的に進化した規則、Git 履歴。
前提条件
- 前の 2 つの 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: push 先のリモートを作成する
フックは作業ツリーを origin/main と比較し、git push には移動先が必要です。同じマシン上のベア リポジトリで十分です。
git init --bare ~/codemender-remote.git
git remote add origin ~/codemender-remote.git
git push -u origin main
この push は独自のターミナルで行われるため、Antigravity フックはそれを認識しません。これ以降は、エージェントが push を行います。
3. CodeMender Headless の実行
ステップ 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 をフォーム フィールドとして受け入れます。変更が完了し、テストスイートに合格したら、変更を commit して origin main に push します。
ステップ 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: 失敗するテスト(赤)
エージェントは 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: リファクタリング、スキャン、commit
エージェントは local-refactor-scanner を読み込み、git diff を確認し、スイート全体を再実行して、以下を 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. git push のセキュリティ ゲート
最初の push が拒否される
エージェントは git push origin main を実行します。Antigravity は run_command ツールを hooks.json と照合し、security_gate_hook.sh を実行します。 は push を認識し、変更されたファイル(sample_app/data_service.py、tests/test_update_profile.py、threat_model.md)をスキャンします(CodeMender は .md ファイルをスキップします。そのパスの cm find はソースファイルを見つけられず、フックは次に進みます)。
これには数分かかります。フックは同期的であるため、cm find と cm report の実行中にエージェントのターンが push コマンドでハングしているように見えます。スキャンが完了すると、CodeMender CLI Basics が修正しなかった /ping のコマンド インジェクションがゲートで検出されます。これは CRITICAL であり、OPEN であり、push されるファイル内にあるため、フックは拒否します。
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", ...}
push は実行されませんでした。エージェントは、ツール呼び出しの結果として reason テキストを確認し、ゲートはこの検出結果の DENIED_TO_AGENT イベントをログに記録します。
エージェントがテストファーストで検出結果をクリアする
拒否では、使用するスキルが指定されており、ルール項目 6(複数の検出結果の修復プロトコル)では、検出結果を 1 つずつ処理するように指示されています。/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"
エージェントはスイート全体を実行し、検出結果と修正内容を説明するメッセージを付けて commit し、再度 push します。
2 回目の push は許可されます
ゲートが sample_app/data_service.py を再スキャンします。コマンド インジェクションはなくなりました。ブロックする検出結果が残っていないため、ゲートは独自のログを確認し、以前に拒否した検出結果がもう存在しないことを確認して、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 が実行され、リモートに 3 つの新しい commit(機能、修正、フック自体が行った docs(context) commit)が追加されます。
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 と 2 つの新しいテストファイルがリポジトリに保存され、次の機能のビルドに使用できるようになります。
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、2 番目の Codelab ではスキルとフック、そしてこの Codelab では、ブロック、修正、ログ記録、許可された push を、ループ内で CodeMender プロンプトを使用せずに処理するエージェントについて説明しました。