گردش کار امنیتی عامل‌گرا سرتاسری با CodeMender

۱. مقدمه

در این codelab، دو codelab قبلی را باهم ترکیب خواهید کرد. شما درخواست ویژگی را به Antigravity می‌دهید، سپس تماشا می‌کنید که چگونه با مدل تهدید برنامه‌ریزی می‌کند، آزمایش‌های ناموفق می‌نویسد، ویژگی را پیاده‌سازی می‌کند، و آن را ارسال می‌کند. درصورت ارسال، گیت امنیتی از آزمایشگاه کد قبلی CodeMender را در فایل‌های تغییریافته اجرا می‌کند. چون برنامه نمونه هنوز تزریق فرمان باز در فایلی دارد که عامل درحال ویرایش آن است، اولین ارسال مسدود می‌شود و خواهید دید که عامل ابتدا یافته را پاک می‌کند و سپس دوباره ارسال می‌کند، بدون اینکه درطول مسیر هیچ پیام‌واره انسانی از CodeMender دریافت کند.

آنچه خواهید آموخت

  • نحوه اجرای CodeMender بدون رابط کاربری به‌گونه‌ای که نه قلاب و نه عامل در پیام‌واره‌های [Y/n] مسدود نشوند.
  • آنچه حلقه «برنامه‌ریزی ← قرمز ← سبز ← بازسازی کد» هنگام ساختن ویژگی توسط عامل تولید می‌کند: threat_model.md، آزمایش‌ها، کد، و یک ثبت.
  • نحوه پاسخ‌دهی دروازه امنیتی وقتی فایل ارسال‌شده یافته‌های باز دارد، و نحوه استفاده عامل از مهارت‌های security-test-writer و defensive-developer برای پاک کردن آن‌ها.
  • نحوه خواندن دنباله ممیزی که دروازه به‌جا می‌گذارد: .security-gate/findings-log.ndjson، قراردادهای خودکار تکامل‌یافته در CONTEXT.md، و سابقه git.

پیش‌نیازها

  • فضای کاری ~/codemender-lab از دو آزمایشگاه کد قبلی، با درخت کاری git تمیز در شاخه main.
  • Antigravity نصب شده است و ~/codemender-lab برای باز شدن به‌عنوان پوشه دردسترس است.
  • ‫git،‏ jq، و محیط مجازی Python با Flask نصب‌شده.

۲. درستی‌سنجی راه‌اندازی از Codelabs قبلی

مرحله ۱: بازگشت به فهرست راهنمای آزمایشگاه

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

مرحله ۲: مطمئن شوید فایل‌های چارچوب در جای خود قرار دارند

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

باید شش دایرکتوری مهارت (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

مرحله ۳: تأیید کردن محیط CodeMender

cm init --verify

باید git را به‌عنوان نوع VCS و python3 -m compileall -q . را به‌عنوان فرمان ساخت گزارش کند، همان‌طور که در «مبانی CodeMender CLI» پیکربندی شده است.

مرحله ۴: پاک کردن یافته‌های قدیمی

cm clean

وقتی خواسته شد، y را تایپ کنید. با این کار، ~/.codemender/state.db خالی می‌شود.

مرحله ۵: ایجاد مخزن ازراه‌دور برای ارسال کد

قلاب درخت کاری شما را با origin/main مقایسه می‌کند و git push باید جایی برای رفتن داشته باشد. یک مخزن خالی در همان دستگاه کافی است:

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

این ارسال در پایانه خودتان اتفاق می‌افتد، بنابراین قلاب Antigravity آن را نمی‌بیند. از اینجا به بعد، عامل ارسال‌ها را انجام می‌دهد.

۳. درحال اجرای CodeMender Headless

مرحله ۱: خاموش کردن پیام‌واره‌های تأیید

به‌طور پیش‌فرض، 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

مرحله ۲: درستی‌سنجی کردن

cm init --verify

۴. درخواست ویژگی

مرحله ۱: باز کردن فضای کاری

~/codemender-lab را به‌عنوان پوشه در Antigravity باز کنید و مکالمه جدیدی شروع کنید. قانون همیشه فعال secure_tdd_workflow اکنون بخشی از هر نوبت است و شش مهارت دردسترس است.

مرحله ۲: تکلیف را به عامل بدهید

پیام‌واره زیر را جای‌گذاری کنید. توجه کنید که چیزی درباره امنیت نمی‌گوید. هدایت کردن عامل به‌سمت مدل‌سازی تهدید، توسعه آزمون‌محور، و الگوهای دفاعی وظیفه قانون و مهارت‌ها است، نه پیام‌واره:

نقطه پایانی POST /update_profile را به sample_app/data_service.py اضافه کنید تا کاربر بتواند نشانی ایمیلش را به‌روز کند. user_id و email را به‌عنوان فیلدهای فرم می‌پذیرد. وقتی تغییر کامل شد و مجموعه آزمون با موفقیت انجام شد، آن را ثبت کنید و به origin main ارسال کنید.

مرحله ۳: تأیید فرمان‌های عامل

قلاب خارج از سیستم اجازه ابزار اجرا می‌شود، اما فرمان‌های خود عامل این‌گونه نیستند. پیام‌واره‌هایی برای python3 -m unittest ...، git add/commit/push، و هر فرمان cm که تصمیم به اجرای آن می‌گیرد. آن‌ها را تأیید کنید؛ اگر Antigravity پیشنهاد داد که تصمیم را به‌خاطر بسپارد، آن را بپذیرید تا حلقه اصلاح در ادامه این codelab به‌دلیل درخواست‌های مکرر متوقف نشود.

۵. کاری که «عامل» انجام می‌دهد: برنامه‌ریزی، قرمز، سبز، بازسازی کد

آنچه در ادامه می‌آید یک اجرای نمونه است. عبارت‌بندی، نام فایل‌ها، و کد دقیق عامل شما متفاوت خواهد بود و این مشکلی ندارد. آنچه نباید متفاوت باشد ترتیب است: مدل تهدید قبل‌از آزمایش‌ها، آزمایش‌های ناموفق قبل‌از کد، مجموعه سبز قبل‌از ارسال.

فاز الف: طرح و مدل تهدید

عامل 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.

فاز ب: آزمایش‌های ناموفق (قرمز)

عامل 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 ناموفق هستند، زیرا مسیر هنوز وجود ندارد. این دلیل درستی برای شکست است. شکستی که به‌دلیل خطای وارد کردن یا غلط املایی ایجاد شده باشد، قرمز نیست؛ بلکه آزمایشی خراب است.

فاز ج: پیاده‌سازی حداقلی (سبز)

عامل 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

اکنون مجموعه با موفقیت اجرا می‌شود: چهار آزمایش جدید به‌علاوه آزمایش مشخصه‌سازی از 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"

‫۶. دروازه امنیتی در 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 در آن مسیر هیچ فایل منبعی پیدا نمی‌کند و قلاب ادامه می‌دهد).

این کار چند دقیقه طول می‌کشد. قلاب همگام است، بنابراین نوبت نماینده به‌نظر می‌رسد که روی فرمان push آویزان است درحالی‌که cm find و cm report اجرا می‌شوند. وقتی اسکن تمام می‌شود، دروازه تزریق فرمان را در /ping پیدا می‌کند که CodeMender CLI Basics هرگز آن را برطرف نکرده است. این 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 را برای این یافته ثبت کرده است.

عامل ابتدا یافته را پاک می‌کند

در رد کردن، مهارت‌هایی که باید استفاده شود نام برده شده است و مورد ۶ قانون (پروتکل اصلاح چندیافته‌ای) می‌گوید یافته‌ها را یکی‌یکی بررسی کنید. برای یافته /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)

The minimal fix, replacing os.system with a list-form subprocess.run as CONTEXT.md requires, plus an allow-list parser instead of string checks:

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) که قلاب خودش ایجاد کرده است.

‫۷. درحال بازبینی «رد ممیزی»

هرچیزی که دروازه تصمیم گرفته است به‌صورت نوشتار ساده در دیسک قرار دارد. از پایانه در ~/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.

ورودی قلاب مختصر است زیرا فقط شناسه، فایل، و شدت یافته را می‌داند. مهارت skill-evolution-updater برای تبدیل این به قانونی که یک انسان می‌نویسد وجود دارد. از نماینده بخواهید: «از skill-evolution-updater برای بازنویسی آخرین قرارداد تکامل‌یافته خودکار در CONTEXT.md به‌عنوان قانون پروژه استفاده کن و هرگونه تغییر SKILL.md را که دلالت می‌کند پیشنهاد بده.»

نمای CodeMender

cm report

یافته تزریق فرمان اکنون باید وضعیت غیرOPEN را نشان دهد و threat_model.md به‌علاوه دو فایل آزمایشی جدید در مخزن برای ویژگی بعدی که باید ساخته شود قرار دارند.

‫۸. درحال پاک‌سازی

بازیابی پیش‌فرض‌های تعاملی 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

تبریک! ‫Codelab «گردش کار امنیتی عامل‌گرا سرتاسر با CodeMender» را تکمیل کرده‌اید. شما CLI را از codelab اول، مهارت‌ها و قلاب را از codelab دوم، و عاملی را که ازطریق یک push کار می‌کند و مسدود، اصلاح، ثبت، و بدون درخواست CodeMender در حلقه مجاز شده است دیده‌اید.