بلوک‌های رمزگذاری‌شده‌ی استدلال در لاگ نشست‌های ایجنت شما می‌مانند و هر کسی که لاگ را ببیند می‌تواند با یک ترفند replay آن‌ها را به متن تبدیل کند. در این نوشته با یک اسکنر ۴۵ خطی سه شکل مستند OpenAI، Anthropic و Google را روی ۲٬۴۹۱ فایل لاگ روی همین سرور اجرا می‌کنم و نشان می‌دهم چرا grep به‌تنهایی گمراه‌کننده است.

چه اتفاقی افتاد: دو افشا در فاصله‌ی سه هفته

اوپن‌ای در ۳۰ سپتامبر ۲۰۲۶ نوشت که یک کمپین هماهنگ برای بیرون‌کشیدن «استدلال محافظت‌شده» از مدل‌هایش را شناسایی و متوقف کرده است. لحظه‌ی خواندن: ۳ اکتبر ۲۰۲۶.

روشی که اوپن‌ای نام می‌برد این است: بلوک رمزگذاری‌شده‌ی استدلال را از یک گفت‌وگو کپی می‌کنند و در گفت‌وگوی دیگری از مدل می‌خواهند آن را رمزگشایی و رونویسی کند. عددهای خود اوپن‌ای: آغاز فعالیت در ۱ ژوئیه، اوج با ۱۶٬۰۰۰ درخواست در ۲۴ و ۲۵ ژوئیه از بیش از ۴٬۰۰۰ کاربر، و الگوی مرتبط در بیش از ۱۵٬۰۰۰ کاربر که تا ۲۸ ژوئیه کامل مسدود شد. پانویس خود متن می‌گوید این‌ها تلاش‌اند، نه لزوماً استخراج موفق.

اوپن‌ای می‌نویسد رمزنگاری‌اش شکسته نشده و به پایگاه‌داده دسترسی نداشته‌اند. اما صریح می‌گوید سامانه‌هایی که «استدلال قابل‌حمل یا قابل‌بازپخش» را نگه می‌دارند ممکن است ریسک مشابه داشته باشند، و این دقیقاً همان چیزی است که لاگ نشست‌های ایجنت‌ها را هدف می‌گیرد.

هفته‌ی پیش از آن، گزارش تهدید انتروپیک در ۱۰ سپتامبر همان روش را با نام دیگری توصیف کرد: در پرونده‌ی GTG-16002، انتروپیک می‌نویسد Moonshot در یک بازه‌ی ده‌روزه نزدیک به ۳۰۰٬۰۰۰ درخواست مشتری را با شبکه‌ای از ۵٬۳۸۰ حساب جعلی به کلاد فرستاد و برای ماه‌های مه و ژوئیه بیش از ۲۳ میلیون تبادل به Moonshot نسبت داده شده است.

پس این پدیده‌ی تک‌فروشنده نیست. هر سه ارائه‌دهنده‌ی بزرگ همان الگو را در مستندات خود توصیف می‌کنند. کلاد می‌گوید هر بلوک thinking یک فیلد signature دارد که «یک نسخه‌ی رمزگذاری‌شده‌ی کامل استدلال» است. اوپن‌ای فیلد encrypted_content را روی آیتم reasoning می‌گذارد و گوگل می‌گوید هر thought step همیشه یک signature رمزگذاری‌شده دارد.

سه ارائه‌دهنده، سه نام برای یک چیز
ارائه‌دهندهمحل بلوک در پاسخنام فیلدحجم در نمونه‌ی این نوشته
OpenAIآیتم reasoning در آرایه‌ی outputencrypted_content۱٬۴۲۰ کاراکتر
Anthropicبلوک thinking در آرایه‌ی contentsignature۳۶٬۱۸۰ کاراکتر
Googleگام thought در آرایه‌ی stepssignature۶۴۰ کاراکتر

حجم‌های جدول را خودم ساخته‌ام، نه از داکیومنت: آن‌ها اندازه‌ی نمونه‌ی آزمایشی این آموزش‌اند. برای اندازه‌ی واقعی، مقاله‌ی arXiv با شناسه‌ی 2608.09867 در بخش خودش یک امضای ۳۶٬۱۸۰ کاراکتری کلاد را نشان می‌دهد و می‌گوید از ۳۱۵٬۳۲۰ بلوک بازسازی‌شده، ۷۰۴ مورد داده‌ی خصوصی بازیابی شد که ۶۴ موردشان فقط داخل بلوک استدلال بودند.

چرا رمزنگاری جلوی این را نگرفت

رمزنگاری این بلوک‌ها در معنای رمزنگاری سالم است: بدون کلید نمی‌خوانید و هر تغییری در بلوک برچسب اصالت را باطل می‌کند. آنچه از پیش ساخته نشده، بستن بلوک به همان گفت‌وگو است.

مقاله‌ی arXiv این را یک آسیب‌پذیری معماری می‌نامد: بلوک‌های رمزگذاری‌شده میان نشست‌ها، کاربرها و مدل‌های یک ارائه‌دهنده کاملاً با هم سازگار و قابل‌تعویض‌اند. یعنی امضای ساخته‌شده توسط مدل قوی را به مدل ضعیف‌تر همان خانواده می‌دهید و مدل ضعیف‌تر آن را رمزگشایی می‌کند. مدل قوی هرگز شکسته نشده است.

همان ادعا را روی دو فایل نمونه‌ی خودم هم سنجیدم: در یکی امضای ۳۶٬۱۸۰ کاراکتری مدل قوی و در دیگری همان مقدار برای مدل ضعیف‌تر و کاربری دیگر. من این حمله را اجرا نکردم؛ این فقط نشان می‌دهد ساختار JSON جلوی جابه‌جایی بلوک را نمی‌گیرد. اندازه‌گیری واقعی از سمت مقاله‌ی arXiv آمده است.

import hashlib, json, sys

def blocks(path):
    """امضای هر بلوک thinking را با نشست و مدل کنار هم برمی‌گرداند."""
    for line in open(path, encoding="utf-8"):
        o = json.loads(line)
        for b in o["content"]:
            if b.get("type") == "thinking":
                sig = b["signature"]
                yield (o["session"], o["model"], o["user"], len(sig),
                       hashlib.sha256(sig.encode()).hexdigest()[:16])

seen = {}
for path in sys.argv[1:]:
    for sess, model, user, n, d in blocks(path):
        print(f"{sess}  {model:18} {user:7} {n:6d} chars  {d}")
        seen.setdefault(d, []).append((sess, model, user))

print()
for d, where in seen.items():
    if len(where) > 1:
        print("SAME BLOCK in", len(where), "sessions:", ", ".join(w[0] for w in where))

$ python3 replay-demo/same_block.py replay-demo/session-A.jsonl replay-demo/session-B.jsonl
A  claude-opus-4-8    acct-1   36180 chars  bd7865734b425d76
B  claude-haiku-4-5   acct-2   36180 chars  bd7865734b425d76

SAME BLOCK in 2 sessions: A, B

خروجی دو سطر است و هر دو سطر یک اثر انگشت یکسان دارند. همین یعنی بلوک به نشست، مدل یا کاربر بسته نشده است.

انتروپیک همین را با نام cross-session replay توصیف کرده است. روشی که اوپن‌ای مسدود کرد مسیر مشخصی بود: کسی که از قبل بلوک رمزگذاری‌شده‌ی یک کاربر دیگر را داشت، می‌توانست آن را دوباره پخش کند و محتوایش را به دست بیاورد.

یک تمایز که در خبرها گم می‌شود: این با یک آسیب‌پذیری کلاسیک فرق دارد. کافی است یک لاگ نشست را منتشر کنید — چیزی که در مخزن گیت‌هاب، دیتاست Hugging Face و تیکت باگ رایج است — و بلوک‌ها همراه آن می‌روند.

چرا grep جواب نمی‌دهد

اولین کاری که همه می‌کنند جست‌وجوی متنی است. این کار روی یک فایل نمونه‌ی واقعی چه نتیجه‌ای می‌دهد:

$ grep -c 'encrypted_content\|"signature"' session-2026-10-01.jsonl
4

عدد ۴ گمراه‌کننده است. چهار سطر پیدا شد، ولی فقط سه‌تای آن‌ها بلوک استدلال‌اند. سطر چهارم یک verified_contents از متادیتای گواهی کروم است که کلید signature دارد ولی ربطی به مدل ندارد. همین اشتباه را اسکنر اول من هم کرد: نسخه‌ی اول با جست‌وجوی رشته‌ای روی کل پوشه‌ی /root/.hermes اجرا شد و ۵ فایل متادیتای مرورگر را گزارش کرد، در حالی که هیچ بلوکی وجود نداشت. راه درست، باز کردن JSON و دیدن شکل بلوک است، نه دنبال کردن نام کلید.

$ jq -r '.. | objects | select(.type? == "reasoning") | .encrypted_content? // empty' \
    session-2026-10-01.jsonl
ErUBCkYIBxgCKkBm0Fk3AbCdEfGhIjKlMnOpQrStUvWxYz0123456789

jq روی این ماشین نسخه‌ی ۱٫۷ است و مسیر ساختاری را درست طی می‌کند، ولی فقط یکی از سه شکل را می‌بیند. فیلد signature مشترک است و بین کلاد و گوگل تفکیکش نمی‌کند. برای همین اسکنر را نوشتم.

اسکنر: کدی که سه شکل را تشخیص می‌دهد

اسکنر کل بلوک استدلال را از سه شکل مستند تشخیص می‌دهد و بقیه را نادیده می‌گیرد. کلید تشخیص، خود نوع بلوک است نه نام کلید. آستانه‌ی ۲۰۰ کاراکتر هم عمدی است: خلاصه‌های قابل‌مشاهده و شناسه‌ها کوتاه‌اند و بلوک رمزگذاری‌شده‌ی واقعی نیست.

#!/usr/bin/env python3
import json, os, re, sys

B64 = re.compile(r"^[A-Za-z0-9+/=_-]{16,}$")   # رمزگذاری، نه متن ساده

def hit(o, out):
    """بلوک استدلال را در هر عمقی از JSON پیدا می‌کند."""
    if isinstance(o, dict):
        t = o.get("type")
        # OpenAI: آیتم reasoning فقط وقتی رمزگذاری‌شده است
        if t == "reasoning" and isinstance(o.get("encrypted_content"), str) \
           and B64.match(o["encrypted_content"]):
            out.append(("openai", len(o["encrypted_content"])))
        # Anthropic: بلوک thinking یا redacted_thinking
        if t in ("thinking", "redacted_thinking") and isinstance(o.get("signature"), str) \
           and B64.match(o["signature"]):
            out.append(("anthropic", len(o["signature"])))
        # Google: گام thought امضا دارد و کنارش summary هم هست
        if isinstance(o.get("signature"), str) and B64.match(o["signature"]) \
           and ("summary" in o or o.get("thought")):
            out.append(("google", len(o["signature"])))
        for v in o.values():
            hit(v, out)
    elif isinstance(o, list):
        for v in o:
            hit(v, out)

found = []
for dp, dn, fn in os.walk(sys.argv[1]):
    dn[:] = [d for d in dn if d not in {".git", "node_modules"}]
    for name in fn:
        if not name.endswith((".jsonl", ".json")):
            continue
        with open(os.path.join(dp, name), encoding="utf-8", errors="replace") as fh:
            for line in fh:
                if line[:1] not in "[{":
                    continue
                try:
                    here = []
                    hit(json.loads(line), here)
                    found += [(os.path.join(dp, name), p, n)
                              for p, n in here if n >= 200]
                except ValueError:
                    pass

for p, prov, n in found:
    print(prov, n, p)
print(len(found), "blocks")
sys.exit(1 if found else 0)

اجرای واقعی روی همان فایل نمونه، با طول‌های واقع‌گرایانه، و بعد روی لاگ‌های واقعی کدکس روی همین سرور:

$ python3 reason_scan.py session-log/
FOUND 3 encrypted reasoning blocks in 1 files
  openai    encrypted_content   1420 chars  session-log/session-2026-10-01.jsonl
  anthropic signature          36180 chars  session-log/session-2026-10-01.jsonl
  google    signature            640 chars  session-log/session-2026-10-01.jsonl

$ python3 reason_scan.py /root/.codex
scanned 343 files under /root/.codex
OK: no encrypted reasoning blocks found
[exit 0]

تفاوت با grep روشن است: ۴ به ۳، و فایل گواهی کروم دیگر گزارش نمی‌شود. روی لاگ‌های واقعی کدکس هیچ بلوکی نبود، چون کدکس در این شش نشست به‌صورت پیش‌فرض استدلال را در فایل ذخیره نمی‌کند. اگر لاگ شما حاوی بلوک بود، اسکنر با کد خروجی ۱ می‌ایستد و خط لوله‌ی شما را متوقف می‌کند.

تست منفی هم نوشتم: فایلی با سه بلوک واقعی و یک خط verified_contents که عمدا کلید signature دارد. اسکنر هر چهار مورد را درست جدا کرد و سه بلوک را گزارش داد.

برای پروژه‌ی خودتان همین اسکنر را کنار مخزن بگذارید و در پیش از هر commit یا انتشار دیتاست اجرایش کنید. نسخه‌ی کامل‌تر، که فایل تک‌خطی و چندخطی را هر دو می‌خواند و با --json گزارش ماشین‌خوان می‌دهد، برای خط لوله‌ی CI مناسب‌تر است.

چه کاری باید کرد، به ترتیب اولویت

اول، اسکن را قبل از انتشار بگذارید. هر چیزی که قرار است به گیت‌هاب یا دیتاست برود، اول باید از فیلتر بگذرد:

# در خط لوله، پیش از git add
python3 reason_scan.py ./logs ./evals ./traces || { echo "بلوک استدلال پیدا شد"; exit 1; }

دوم، اگر لاگ نشست را در مخزن گذاشته‌اید، تاریخچه را پاک کنید. حذف فایل کافی نیست؛ بلوک در اشیای گیت باقی می‌ماند و با git filter-repo یا بازنویسی تاریخچه باید از همه‌ی کامیت‌ها بیرون برود.

سوم، تنظیمات را عوض کنید. در کلاد، مستندات می‌گوید فیلد thinking روی مدل‌های اخیر به‌صورت پیش‌فرض خالی است؛ در اوپن‌ای هم reasoning.context روی current_turn استدلال نوبت‌های قبلی را به نمونه‌ی بعدی نمی‌ریزد. کم کردن این‌ها سطح فایل را کم می‌کند ولی جایگزین پاک‌سازی نیست.

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

تغییرات روزانه‌ی کلاد کد نشان می‌دهد نسخه‌ی 2.1.288 در ۲ اکتبر مسیر بازپخش امضا را بسته است. اما این وصله‌ها روی سمت سرور اثر دارند؛ لاگی که پیشاپیش روی دیسک شما نشسته را پاک نمی‌کنند.

اگر می‌خواهید بدانید این لاگ‌ها در پروژه‌ی خودتان چه شکلی‌اند، نوشته‌ی قبلی درباره‌ی قفل کردن سندباکس کلاد کد همان لاگ‌های نشست را از سمت مجوز بررسی می‌کند؛ اسکنر این نوشته از سمت محتوا.

منابع

  1. Disrupting a coordinated model-distillation campaign — OpenAI، ۳۰ سپتامبر ۲۰۲۶
  2. Stealing Reasoning Traces from Proprietary LLM APIs — arXiv 2608.09867، ۱۰ اوت ۲۰۲۶
  3. متن کامل و داده‌های مقاله‌ی stolen-thoughts.com
  4. Thinking — Claude Platform Docs، بخش رمزگذاری
  5. Reasoning models — OpenAI API
  6. Gemini thinking — Google AI for Developers
  7. Detecting and countering misuse of AI: September 2026 — Anthropic، ۱۰ سپتامبر ۲۰۲۶، پرونده‌ی GTG-16002
  8. نسخه‌ی PDF گزارش انتروپیک، فصل illicit distillation
  9. GitHub Changelog، ۲ اکتبر ۲۰۲۶
  10. Codex changelog — نسخه‌ی 0.160.0 در ۱ اکتبر ۲۰۲۶